Files
obsidian-livesync/src/serviceFeatures/documentHistoryRestoration.ts
T
vorotamoroz d557298ceb Adopt Commonlib storage path contracts
Use the shared Node adapter and retain one comparison policy throughout
each CLI scan. Propagate permission and I/O failures instead of reporting
absence or a successful partial scan.

Use shared namespace and comparison helpers in Obsidian while preserving
its Vault APIs and alias policy. Cover native spelling and host filename
compatibility, and document the dependency and integration boundaries.
2026-11-03 08:44:31 +00:00

99 lines
3.7 KiB
TypeScript

import type { FilePath, FilePathWithPrefix, UXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler";
import { parseDocumentPath, splitDocumentPath } from "@vrtmrz/livesync-commonlib/storage";
export type DocumentHistoryRestorationCore = {
databaseFileAccess: Pick<
DatabaseFileAccess,
"fetchEntry" | "fetchEntryMeta" | "getConflictedRevs" | "storeWithLiveBaseRevision"
>;
fileHandler: Pick<IFileHandler, "dbToStorageWithSpecificRev">;
};
export type DocumentHistoryRestorationResult =
| {
status: "restored";
path: FilePath;
revision: string;
conflictsRemain: boolean | undefined;
conflictCheckError?: unknown;
}
| { status: "source-unavailable" }
| { status: "current-unavailable" }
| { status: "unsupported-path" }
| { status: "database-write-refused" }
| { status: "stored-not-reflected"; path: FilePath; revision: string; cause?: unknown };
export type DocumentHistoryRestorationOptions = {
now?: () => number;
isPathValid?: (path: FilePath) => boolean;
};
/**
* Restore historical content as a new child of the current database winner, then reflect
* that exact new revision to the Vault. The historical revision supplies content, not ancestry.
*/
export async function restoreDocumentHistoryRevision(
core: DocumentHistoryRestorationCore,
path: FilePathWithPrefix,
sourceRevision: string,
options: DocumentHistoryRestorationOptions = {}
): Promise<DocumentHistoryRestorationResult> {
const now = options.now ?? Date.now;
const isPathValid = options.isPathValid ?? (() => true);
const source = await core.databaseFileAccess.fetchEntry(path, sourceRevision, true, true);
if (source === false) {
return { status: "source-unavailable" };
}
const current = await core.databaseFileAccess.fetchEntryMeta(path, undefined, true);
if (current === false || !current._rev) {
return { status: "current-unavailable" };
}
const body = readAsBlob(source);
const { prefix, path: storagePath } = splitDocumentPath(parseDocumentPath(current.path));
if (prefix !== "" || !isPathValid(storagePath)) {
return { status: "unsupported-path" };
}
const file: UXFileInfo = {
name: storagePath.split("/").pop() ?? storagePath,
path: storagePath,
stat: {
ctime: current.ctime,
mtime: now(),
size: body.size,
type: "file",
},
body,
};
const revision = await core.databaseFileAccess.storeWithLiveBaseRevision(file, current._rev, true);
if (revision === false) {
return { status: "database-write-refused" };
}
try {
const reflected = await core.fileHandler.dbToStorageWithSpecificRev(storagePath, revision, true);
if (!reflected) {
return { status: "stored-not-reflected", path: storagePath, revision };
}
} catch (cause) {
return { status: "stored-not-reflected", path: storagePath, revision, cause };
}
try {
const conflictsRemain = (await core.databaseFileAccess.getConflictedRevs(storagePath)).length > 0;
return { status: "restored", path: storagePath, revision, conflictsRemain };
} catch (conflictCheckError) {
return {
status: "restored",
path: storagePath,
revision,
conflictsRemain: undefined,
conflictCheckError,
};
}
}