From b2a8db14b69764e259452277da822f59a85887f7 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 2 Jan 2025 11:58:06 +0000 Subject: [PATCH] Update files atomically in Obsidian --- plugin/src/file-operations/file-operations.ts | 6 +- .../obsidian-file-operations.ts | 62 ++++++++++--------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/plugin/src/file-operations/file-operations.ts b/plugin/src/file-operations/file-operations.ts index 1470d79d..910b6d71 100644 --- a/plugin/src/file-operations/file-operations.ts +++ b/plugin/src/file-operations/file-operations.ts @@ -7,9 +7,13 @@ export interface FileOperations { getModificationTime: (path: RelativePath) => Promise; + // Create and write the file if it doesn't exist. Otherwise, it has the same behavior as write. + // All parent directories are created if they don't exist. create: (path: RelativePath, newContent: Uint8Array) => Promise; - // Writes new content to the file at the given path. If the file's content has changed since the expectedContent was read, the write will merge the changes. + // Update the file at the given path. + // If the file's content is different from `expectedContent`, the a 3-way merge is performed before writing. + // If the file no longer exists, the file is not recreated and an empty array is returned. write: ( path: RelativePath, expectedContent: Uint8Array, diff --git a/plugin/src/file-operations/obsidian-file-operations.ts b/plugin/src/file-operations/obsidian-file-operations.ts index 7f9a47aa..7c30ecda 100644 --- a/plugin/src/file-operations/obsidian-file-operations.ts +++ b/plugin/src/file-operations/obsidian-file-operations.ts @@ -2,7 +2,6 @@ import type { Vault } from "obsidian"; import { normalizePath } from "obsidian"; import type { FileOperations } from "./file-operations"; import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js"; -import { isEqualBytes } from "src/utils/is-equal-bytes"; import type { RelativePath } from "src/database/document-metadata"; export class ObsidianFileOperations implements FileOperations { @@ -27,33 +26,6 @@ export class ObsidianFileOperations implements FileOperations { return new Date(file.mtime); } - public async write( - path: RelativePath, - expectedContent: Uint8Array, - newContent: Uint8Array - ): Promise { - if (!(await this.vault.adapter.exists(normalizePath(path)))) { - // The caller assumed the file exists, but it doesn't, let's not recreate it - return new Uint8Array(0); - } - - const currentContent = await this.read(path); - if (!isEqualBytes(currentContent, expectedContent)) { - const result = lib.merge( - expectedContent, - currentContent, - newContent - ); - - await this.vault.adapter.writeBinary(normalizePath(path), result); - - return result; - } - await this.vault.adapter.writeBinary(normalizePath(path), newContent); - - return newContent; - } - public async create( path: RelativePath, newContent: Uint8Array @@ -67,6 +39,40 @@ export class ObsidianFileOperations implements FileOperations { await this.vault.adapter.writeBinary(normalizePath(path), newContent); } + public async write( + path: RelativePath, + expectedContent: Uint8Array, + newContent: Uint8Array + ): Promise { + if (!(await this.vault.adapter.exists(normalizePath(path)))) { + // The caller assumed the file exists, but it doesn't, let's not recreate it + return new Uint8Array(0); + } + + if (lib.isBinary(expectedContent)) { + await this.vault.adapter.writeBinary( + normalizePath(path), + newContent + ); + return newContent; + } + + const expetedText = new TextDecoder().decode(expectedContent); + const newText = new TextDecoder().decode(newContent); + + const resultText = await this.vault.adapter.process( + normalizePath(path), + (currentText) => { + if (currentText !== expetedText) { + return lib.mergeText(expetedText, currentText, newText); + } + + return newText; + } + ); + return new TextEncoder().encode(resultText); + } + public async remove(path: RelativePath): Promise { if (await this.vault.adapter.exists(normalizePath(path))) { return this.vault.adapter.remove(normalizePath(path));