Update files atomically in Obsidian

This commit is contained in:
Andras Schmelczer 2025-01-02 11:58:06 +00:00
parent a2b1a83663
commit b2a8db14b6
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
2 changed files with 39 additions and 29 deletions

View file

@ -7,9 +7,13 @@ export interface FileOperations {
getModificationTime: (path: RelativePath) => Promise<Date>;
// 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<void>;
// 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,

View file

@ -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<Uint8Array> {
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<Uint8Array> {
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<void> {
if (await this.vault.adapter.exists(normalizePath(path))) {
return this.vault.adapter.remove(normalizePath(path));