import type { Database } from "src/database/database"; import { unlockDocument, waitForDocumentLock } from "./locks"; import type { SyncService } from "src/services/sync-service"; import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js"; import { hash } from "src/utils/hash"; import type { components } from "src/services/types"; import type { FileOperations } from "src/file-operations/file-operations"; import { Logger } from "src/tracing/logger"; import type { SyncHistory } from "src/tracing/sync-history"; import { SyncSource, SyncStatus, SyncType } from "src/tracing/sync-history"; export async function syncRemotelyUpdatedFile({ database, syncServer, operations, history, remoteVersion, }: { database: Database; syncServer: SyncService; operations: FileOperations; history: SyncHistory; remoteVersion: components["schemas"]["DocumentVersionWithoutContent"]; }): Promise { Logger.getInstance().debug( `Syncing remotely updated file ${remoteVersion.relativePath}` ); await waitForDocumentLock(remoteVersion.relativePath); try { const currentVersion = database.getDocumentByDocumentId( remoteVersion.documentId ); if (!currentVersion) { if (remoteVersion.isDeleted) { history.addHistoryEntry({ status: SyncStatus.NO_OP, source: SyncSource.PULL, relativePath: remoteVersion.relativePath, message: `Remotely deleted file hasn't been synced yet, so there's no need to delete it locally`, type: SyncType.DELETE, }); return; } const content = ( await syncServer.get({ documentId: remoteVersion.documentId, }) ).contentBase64; const contentBytes = lib.base64_to_bytes(content); const contentHash = hash(contentBytes); await operations.create(remoteVersion.relativePath, contentBytes); await database.setDocument({ documentId: remoteVersion.documentId, relativePath: remoteVersion.relativePath, parentVersionId: remoteVersion.vaultUpdateId, hash: contentHash, }); history.addHistoryEntry({ status: SyncStatus.SUCCESS, source: SyncSource.PULL, relativePath: remoteVersion.relativePath, message: `Successfully downloaded remote file which hasn't existed locally`, type: SyncType.CREATE, }); return; } const [relativePath, metadata] = currentVersion; if (metadata.parentVersionId === remoteVersion.vaultUpdateId) { Logger.getInstance().debug( `Document ${relativePath} is already up to date` ); return; } if (relativePath !== remoteVersion.relativePath) { await waitForDocumentLock(relativePath); } try { if (remoteVersion.isDeleted) { await operations.remove(relativePath); await database.removeDocument(relativePath); history.addHistoryEntry({ status: SyncStatus.SUCCESS, source: SyncSource.PULL, relativePath: remoteVersion.relativePath, message: `Successfully deleted remotely deleted file locally`, type: SyncType.DELETE, }); } 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 until the event is processed` ); return; } const content = ( await syncServer.get({ documentId: remoteVersion.documentId, }) ).contentBase64; const contentBytes = lib.base64_to_bytes(content); const contentHash = hash(contentBytes); if (relativePath !== remoteVersion.relativePath) { await operations.move( relativePath, remoteVersion.relativePath ); } await operations.write( remoteVersion.relativePath, currentContent, contentBytes ); await database.moveDocument({ documentId: remoteVersion.documentId, oldRelativePath: relativePath, relativePath: remoteVersion.relativePath, parentVersionId: remoteVersion.vaultUpdateId, hash: contentHash, }); history.addHistoryEntry({ status: SyncStatus.SUCCESS, source: SyncSource.PULL, relativePath: remoteVersion.relativePath, message: `Successfully updated remotely updated file locally`, type: SyncType.UPDATE, }); } } finally { if (relativePath !== remoteVersion.relativePath) { unlockDocument(relativePath); } } } catch (e) { history.addHistoryEntry({ status: SyncStatus.ERROR, source: SyncSource.PULL, relativePath: remoteVersion.relativePath, message: `Failed to reconcile remotely updated file: ${e}`, }); throw e; } finally { unlockDocument(remoteVersion.relativePath); } }