/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/sync/conflictManager.ts
144 строки
5 KB
Robert Kuzhin
feat: add Obsidian session and desktop UX
09 авг 2026, 13:07
09 авг 2026, 13:07
14fe125
Код
Авторство
О чём код?
import path from "node:path"; import { createFileId } from "../mirror/fileId.js"; import { assertVaultRelativePath } from "../mirror/pathSafety.js"; import type { ManifestEntry } from "../mirror/manifestTypes.js"; import type { ConflictResolution, ConflictStrategy, PlannedAction, SyncConflict, } from "./syncTypes.js"; type ResolvedAction = Exclude<PlannedAction, { readonly type: "conflict" }>; export class ConflictManager { public constructor(private readonly fileIdFactory: () => string = createFileId) {} public resolve( conflict: SyncConflict, strategy: ConflictStrategy, occupiedPaths: ReadonlySet<string> = new Set(), date = new Date(), ): ConflictResolution { switch (strategy) { case "keep-local": return { actions: actionsForLocal(conflict), vaultChanges: [] }; case "keep-remote": return { actions: actionsForRemote(conflict), vaultChanges: [] }; case "keep-both": return keepBoth(conflict, occupiedPaths, date, this.fileIdFactory); } } } function keepBoth( conflict: SyncConflict, occupiedPaths: ReadonlySet<string>, date: Date, fileIdFactory: () => string, ): ConflictResolution { const local = active(conflict.local); const remote = active(conflict.remote); if (local === undefined) { return { actions: actionsForRemote(conflict), vaultChanges: [] }; } if (remote === undefined) { return { actions: actionsForLocal(conflict), vaultChanges: [] }; } const remotePath = createConflictPath(remote.path, occupiedPaths, date); const needsDistinctIdentity = local.fileId === remote.fileId; const remoteFileId = needsDistinctIdentity ? fileIdFactory() : remote.fileId; return { actions: [ { type: "encrypt-from-vault", fileId: local.fileId, path: local.path }, { type: "decrypt-to-vault", fileId: remote.fileId, path: remotePath }, ...(needsDistinctIdentity ? [{ type: "encrypt-from-vault" as const, fileId: remoteFileId, path: remotePath }] : []), ], vaultChanges: [{ type: "create", path: remotePath }], }; } function actionsForLocal(conflict: SyncConflict): ResolvedAction[] { const local = active(conflict.local); if (local !== undefined) { const actions: ResolvedAction[] = [ { type: "encrypt-from-vault", fileId: local.fileId, path: local.path }, ]; const remote = active(conflict.remote); if ( conflict.kind === "path-collision" && remote !== undefined && remote.fileId !== local.fileId ) { actions.push({ type: "delete-from-mirror", fileId: remote.fileId }); } return actions; } const result: ResolvedAction[] = []; const remote = active(conflict.remote); if (remote !== undefined) result.push({ type: "delete-from-vault", path: remote.path }); if (conflict.remote !== undefined) { result.push({ type: "delete-from-mirror", fileId: conflict.remote.fileId }); } return result; } function actionsForRemote(conflict: SyncConflict): ResolvedAction[] { const remote = active(conflict.remote); if (remote === undefined) { const local = active(conflict.local); return local === undefined ? [] : [{ type: "delete-from-vault", path: local.path }]; } const result: ResolvedAction[] = []; const local = active(conflict.local); const isDistinctPathCollision = conflict.kind === "path-collision" && local !== undefined && local.fileId !== remote.fileId; if (isDistinctPathCollision) { if (local.path !== remote.path) result.push({ type: "delete-from-vault", path: local.path }); result.push({ type: "decrypt-to-vault", fileId: remote.fileId, path: remote.path }); result.push({ type: "delete-from-mirror", fileId: local.fileId }); return result; } result.push({ type: "decrypt-to-vault", fileId: remote.fileId, path: remote.path }); if (local !== undefined && local.path !== remote.path) { result.push({ type: "delete-from-vault", path: local.path }); } return result; } function active(entry: ManifestEntry | undefined): ManifestEntry | undefined { return entry === undefined || entry.deleted === true ? undefined : entry; } function createConflictPath( remotePath: string, occupiedPaths: ReadonlySet<string>, date: Date, ): string { const directory = path.posix.dirname(remotePath); const extension = path.posix.extname(remotePath); const basename = path.posix.basename(remotePath, extension); const dateStamp = formatDate(date); const occupied = new Set([...occupiedPaths].map(portablePathKey)); for (let suffix = 1; suffix <= 10_000; suffix += 1) { const numbered = suffix === 1 ? "" : `-${suffix}`; const filename = `${basename}.conflict-remote-${dateStamp}${numbered}${extension}`; const candidate = directory === "." ? filename : `${directory}/${filename}`; assertVaultRelativePath(candidate); if (!occupied.has(portablePathKey(candidate))) return candidate; } throw new Error("Unable to allocate a unique conflict path"); } function formatDate(date: Date): string { if (!Number.isFinite(date.getTime())) throw new TypeError("Conflict date is invalid"); return date.toISOString().slice(0, 10); } function portablePathKey(value: string): string { return value.normalize("NFC").toLocaleLowerCase("en-US"); }