Basic syncing in the plugin

This commit is contained in:
Andras Schmelczer 2024-12-15 15:47:08 +00:00
parent dfdf1d016b
commit d088d42a65
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
17 changed files with 560 additions and 178 deletions

View file

@ -0,0 +1,40 @@
import { RelativePath } from "src/database/document-metadata";
const locked = new Set<RelativePath>();
const waiters = new Map<RelativePath, Array<() => void>>();
export function tryLockDocument(relativePath: RelativePath): boolean {
if (locked.has(relativePath)) {
return false;
}
locked.add(relativePath);
return true;
}
export function waitForDocumentLock(relativePath: RelativePath): Promise<void> {
if (tryLockDocument(relativePath)) {
return Promise.resolve();
}
return new Promise((resolve) => {
if (!waiters.has(relativePath)) {
waiters.set(relativePath, []);
}
waiters.get(relativePath)!.push(resolve);
});
}
export function unlockDocument(relativePath: RelativePath): void {
if (!locked.has(relativePath)) {
throw new Error(`Document ${relativePath} is not locked`);
}
const nextWaiting = waiters.get(relativePath)?.shift();
if (nextWaiting) {
nextWaiting();
} else {
locked.delete(relativePath);
}
}

View file

@ -0,0 +1,59 @@
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import { TFile } from "obsidian";
import { Database } from "src/database/database";
import { Logger } from "src/logger";
import { SyncServer } from "src/services/sync_service";
import { hash } from "src/utils/hash";
import { isEqualBytes } from "src/utils/is-equal-bytes";
import { unlockDocument, waitForDocumentLock } from "./locks.js";
import { FileOperations } from "src/file-operations/file-operations.js";
import { RelativePath } from "src/database/document-metadata.js";
/// This can be used when updating a files content and/or path.
export async function syncLocallyCreatedFile({
database,
syncServer,
operations,
updateTime,
filePath,
}: {
database: Database;
syncServer: SyncServer;
operations: FileOperations;
updateTime: Date;
filePath: RelativePath;
}): Promise<void> {
await waitForDocumentLock(filePath);
try {
const metadata = database.getDocument(filePath);
if (metadata) {
throw new Error(
`Document metadata found for ${filePath}, this is unexpected`
);
}
const contentBytes = await operations.read(filePath);
const response = await syncServer.create({
relativePath: filePath,
contentBytes,
createdDate: updateTime,
});
const responseBytes = lib.base64_to_bytes(response.contentBase64);
await operations.write(filePath, contentBytes, responseBytes);
await database.setDocument({
documentId: response.documentId,
relativePath: response.relativePath,
parentVersionId: response.vaultUpdateId,
hash: hash(responseBytes),
});
} catch (e) {
Logger.getInstance().error(
`Failed to sync locally updated file ${filePath}: ${e}`
);
} finally {
unlockDocument(filePath);
}
}

View file

@ -0,0 +1,38 @@
import { Database } from "src/database/database";
import { RelativePath } from "src/database/document-metadata";
import { Logger } from "src/logger";
import { SyncServer } from "src/services/sync_service";
import { tryLockDocument, unlockDocument, waitForDocumentLock } from "./locks";
export async function syncLocallyDeletedFile(
database: Database,
syncServer: SyncServer,
relativePath: RelativePath
): Promise<void> {
await waitForDocumentLock(relativePath);
try {
const metadata = database.getDocument(relativePath);
if (!metadata) {
Logger.getInstance().warn(
`Document metadata not found for ${relativePath}`
);
return;
}
await syncServer.delete({
documentId: metadata.documentId,
relativePath,
// We got the event now, so it must have been deleted just now
createdDate: new Date(),
});
await database.removeDocument(relativePath);
} catch (e) {
Logger.getInstance().error(
`Failed to sync locally deleted file ${relativePath}: ${e}`
);
} finally {
unlockDocument(relativePath);
}
}

View file

@ -0,0 +1,99 @@
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import { TFile } from "obsidian";
import { Database } from "src/database/database";
import { Logger } from "src/logger";
import { SyncServer } from "src/services/sync_service";
import { hash } from "src/utils/hash";
import { isEqualBytes } from "src/utils/is-equal-bytes";
import { unlockDocument, waitForDocumentLock } from "./locks.js";
import { FileOperations } from "src/file-operations/file-operations.js";
import { RelativePath } from "src/database/document-metadata.js";
/// This can be used when updating a files content and/or path.
export async function syncLocallyUpdatedFile({
database,
syncServer,
operations,
updateTime,
filePath,
oldPath,
}: {
database: Database;
syncServer: SyncServer;
operations: FileOperations;
updateTime: Date;
filePath: RelativePath;
oldPath?: RelativePath;
}): Promise<void> {
await waitForDocumentLock(filePath);
try {
const metadata = database.getDocument(oldPath || filePath);
if (!metadata) {
throw new Error(`Document metadata not found for ${filePath}`);
}
const contentBytes = await operations.read(filePath);
const contentHash = hash(contentBytes);
if (metadata.hash === contentHash && !oldPath) {
Logger.getInstance().info(
`Document hash matches, no need to sync ${filePath}`
);
return;
}
const response = await syncServer.put({
documentId: metadata.documentId,
parentVersionId: metadata.parentVersionId,
relativePath: filePath,
contentBytes,
createdDate: updateTime,
});
if (response.isDeleted) {
await operations.remove(oldPath || filePath);
if (metadata) {
await database.removeDocument(oldPath || filePath);
}
return;
}
const responseBytes = lib.base64_to_bytes(response.contentBase64);
if (response.relativePath != filePath) {
await waitForDocumentLock(response.relativePath);
try {
await operations.move(
oldPath || filePath,
response.relativePath
);
await operations.write(
response.relativePath,
contentBytes,
responseBytes
);
} finally {
unlockDocument(response.relativePath);
}
} else {
await operations.write(filePath, contentBytes, responseBytes);
}
await database.moveDocument({
documentId: metadata.documentId,
oldRelativePath: oldPath || filePath,
relativePath: response.relativePath,
parentVersionId: response.vaultUpdateId,
hash: contentHash,
});
} catch (e) {
Logger.getInstance().error(
`Failed to sync locally updated file ${filePath}: ${e}`
);
} finally {
unlockDocument(filePath);
}
}

View file

@ -0,0 +1,111 @@
import { Vault } from "obsidian";
import { Database } from "src/database/database";
import { unlockDocument, waitForDocumentLock } from "./locks";
import { SyncServer } from "src/services/sync_service";
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import { hash } from "src/utils/hash";
import { Logger } from "src/logger";
import { components } from "src/services/types";
import { FileOperations } from "src/file-operations/file-operations";
export async function syncRemotelyUpdatedFile({
database,
syncServer,
operations,
remoteVersion,
}: {
database: Database;
syncServer: SyncServer;
operations: FileOperations;
remoteVersion: components["schemas"]["DocumentVersionWithoutContent"];
}): Promise<void> {
Logger.getInstance().info(
`Syncing remotely updated file ${remoteVersion.relativePath}`
);
const content = (
await syncServer.get({
documentId: remoteVersion.documentId,
})
).contentBase64;
const currentVersion = database.getDocumentByDocumentId(
remoteVersion.documentId
);
if (!currentVersion) {
if (remoteVersion.isDeleted) {
return;
}
Logger.getInstance().info(
`Document metadata not found for ${remoteVersion.relativePath}, it must be new`
);
await waitForDocumentLock(remoteVersion.relativePath);
try {
const contentBytes = lib.base64_to_bytes(content);
operations.create(remoteVersion.relativePath, contentBytes);
await database.setDocument({
documentId: remoteVersion.documentId,
relativePath: remoteVersion.relativePath,
parentVersionId: remoteVersion.vaultUpdateId,
hash: hash(contentBytes),
});
} finally {
unlockDocument(remoteVersion.relativePath);
}
return;
}
const [relativePath, metadata] = currentVersion;
await waitForDocumentLock(relativePath);
try {
if (remoteVersion.isDeleted) {
Logger.getInstance().info(
`Document ${relativePath} has been deleted remotely`
);
await operations.remove(relativePath);
if (metadata) {
await database.removeDocument(relativePath);
}
} else {
const currentContent = await operations.read(relativePath);
const currentHash = hash(currentContent);
if (currentHash !== metadata.hash) {
Logger.getInstance().info(
`Document ${relativePath} has been updated both remotely and locally, skipping`
);
return;
} else {
if (relativePath !== remoteVersion.relativePath) {
await operations.move(
relativePath,
remoteVersion.relativePath
);
}
const contentBytes = lib.base64_to_bytes(content);
await operations.write(
remoteVersion.relativePath,
currentContent,
contentBytes
);
await database.moveDocument({
documentId: remoteVersion.documentId,
oldRelativePath: relativePath,
relativePath: remoteVersion.relativePath,
parentVersionId: remoteVersion.vaultUpdateId,
hash: metadata.hash,
});
}
}
} catch (e) {
Logger.getInstance().error(
`Failed to sync remotely updated file ${remoteVersion.relativePath}: ${e}`
);
} finally {
unlockDocument(relativePath);
}
}