mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-11 13:17:06 +00:00
133 lines
5.0 KiB
TypeScript
133 lines
5.0 KiB
TypeScript
import type { LoadedEntry } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
|
import { createBlob, isDocContentSame, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
|
import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess";
|
|
import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler";
|
|
import {
|
|
inspectFileDatabaseInfo,
|
|
readFileDatabaseRevisionLocally,
|
|
type FileDatabaseInfo,
|
|
type FileDatabaseInfoCore,
|
|
type RevisionDatabaseInfo,
|
|
} from "./fileDatabaseInfo";
|
|
|
|
export type FileRepairCore = FileDatabaseInfoCore & {
|
|
fileHandler: Pick<IFileHandler, "deleteRevisionFromDB">;
|
|
storageAccess: FileDatabaseInfoCore["storageAccess"] & Pick<StorageAccess, "readHiddenFileBinary">;
|
|
};
|
|
|
|
export type FileRepairRevision = {
|
|
role: "winner" | "conflict";
|
|
metadata: RevisionDatabaseInfo;
|
|
contentReadable: boolean;
|
|
contentMatchesStorage: boolean | null;
|
|
loadedEntry: LoadedEntry | false;
|
|
};
|
|
|
|
export type FileRepairInspection = {
|
|
information: FileDatabaseInfo;
|
|
revisions: FileRepairRevision[];
|
|
requiresAttention: boolean;
|
|
};
|
|
|
|
export type DiscardUnreadableRevisionResult =
|
|
| "discarded"
|
|
| "failed"
|
|
| "no-longer-live"
|
|
| "revision-is-readable";
|
|
|
|
export type DiscardLiveBranchResult = "discarded" | "failed" | "no-longer-live" | "only-live-revision";
|
|
|
|
export async function inspectFileRepair(core: FileRepairCore, path: string): Promise<FileRepairInspection> {
|
|
const information = await inspectFileDatabaseInfo(core, path);
|
|
const storageContent = information.storage.exists
|
|
? createBlob(await core.storageAccess.readHiddenFileBinary(path))
|
|
: undefined;
|
|
const revisions: FileRepairRevision[] = [];
|
|
|
|
for (const metadata of information.database.revisions) {
|
|
const loadedEntry =
|
|
metadata.deleted || !metadata.contentAvailableLocally
|
|
? false
|
|
: await readFileDatabaseRevisionLocally(core, path, metadata.revision ?? "");
|
|
const contentReadable = metadata.deleted || loadedEntry !== false;
|
|
const contentMatchesStorage =
|
|
storageContent && loadedEntry !== false
|
|
? await isDocContentSame(storageContent, readAsBlob(loadedEntry))
|
|
: null;
|
|
revisions.push({
|
|
role: metadata.current ? "winner" : "conflict",
|
|
metadata,
|
|
contentReadable,
|
|
contentMatchesStorage,
|
|
loadedEntry,
|
|
});
|
|
}
|
|
|
|
const winner = revisions.find(({ role }) => role === "winner");
|
|
const winnerRepresentsStoredFile = winner !== undefined && !winner.metadata.deleted;
|
|
const databaseAndStorageDiffer =
|
|
information.storage.exists !== winnerRepresentsStoredFile ||
|
|
(information.storage.exists &&
|
|
winnerRepresentsStoredFile &&
|
|
winner.contentMatchesStorage === false);
|
|
const unreadableLiveRevision =
|
|
information.database.unavailableConflictRevisions.length > 0 ||
|
|
revisions.some(({ contentReadable }) => !contentReadable);
|
|
const requiresAttention =
|
|
databaseAndStorageDiffer ||
|
|
information.database.conflictCount > 0 ||
|
|
unreadableLiveRevision ||
|
|
(information.database.exists && winner === undefined);
|
|
|
|
return {
|
|
information,
|
|
revisions,
|
|
requiresAttention,
|
|
};
|
|
}
|
|
|
|
export async function discardUnreadableLiveRevision(
|
|
core: FileRepairCore,
|
|
path: string,
|
|
revision: string
|
|
): Promise<DiscardUnreadableRevisionResult> {
|
|
const latest = await inspectFileDatabaseInfo(core, path);
|
|
const liveRevisions = [
|
|
latest.database.currentRevision,
|
|
...latest.database.conflictRevisions,
|
|
].filter((candidate): candidate is string => candidate !== null);
|
|
if (!liveRevisions.includes(revision)) {
|
|
return "no-longer-live";
|
|
}
|
|
|
|
const metadata = latest.database.revisions.find((candidate) => candidate.revision === revision);
|
|
const metadataUnavailable = latest.database.unavailableConflictRevisions.includes(revision);
|
|
if (!metadataUnavailable && (metadata?.deleted || metadata?.contentAvailableLocally)) {
|
|
return "revision-is-readable";
|
|
}
|
|
|
|
const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision);
|
|
return deleted ? "discarded" : "failed";
|
|
}
|
|
|
|
export async function discardLiveBranch(
|
|
core: FileRepairCore,
|
|
path: string,
|
|
revision: string
|
|
): Promise<DiscardLiveBranchResult> {
|
|
const latest = await inspectFileDatabaseInfo(core, path);
|
|
const liveRevisions = [
|
|
latest.database.currentRevision,
|
|
...latest.database.conflictRevisions,
|
|
].filter((candidate): candidate is string => candidate !== null);
|
|
if (!liveRevisions.includes(revision)) {
|
|
return "no-longer-live";
|
|
}
|
|
if (liveRevisions.length < 2) {
|
|
return "only-live-revision";
|
|
}
|
|
|
|
const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision);
|
|
return deleted ? "discarded" : "failed";
|
|
}
|