diff --git a/pro/src/conflictLogic.ts b/pro/src/conflictLogic.ts index c19a7a7..36eca36 100644 --- a/pro/src/conflictLogic.ts +++ b/pro/src/conflictLogic.ts @@ -200,22 +200,141 @@ export function getFileRename(key: string) { return res; } +function arraysAreEqual(arr1: ArrayBuffer, arr2: ArrayBuffer) { + if (arr1.byteLength !== arr2.byteLength) { + return false; + } + const u1 = new Uint8Array(arr1); + const u2 = new Uint8Array(arr2); + + for (let i = 0; i < u1.byteLength; ++i) { + if (u1[i] !== u2[i]) { + return false; + } + } + + return true; +} + +/** + * 1. download remote + * 2. compare + * 3. if the same, update local but not upload + * 4. if not the same, rename local and save remote + */ +async function tryDuplicateFileForSameSizes( + key: string, + key2: string, + fsLocal: FakeFs, + fsRemote: FakeFs, + uploadCallback: (entity: Entity | undefined) => Promise, + downloadCallback: (entity: Entity | undefined) => Promise +) { + console.debug(`tryDuplicateFileForSameSizes: ${key}`); + + // 1. download + const remoteContent = await fsRemote.readFile(key); + + // 2. compare + const localContent = await fsLocal.readFile(key); + const eq = arraysAreEqual(localContent, remoteContent); + + if (eq) { + // 3. if the same, update local but not upload + // read meta of remote, as if we have downloaded the file + console.debug(`tryDuplicateFileForSameSizes: ${key} content equal`); + const entityRemote = await fsRemote.stat(key); + + // write + const downloadResultEntity = await fsLocal.writeFile( + key, + remoteContent, + entityRemote.mtimeCli ?? Date.now(), + entityRemote.mtimeCli ?? Date.now() + ); + await downloadCallback(downloadResultEntity); + + // no uploadCallback here + } else { + // 4. if not the same, rename local and save remote + console.debug(`tryDuplicateFileForSameSizes: ${key} content not equal`); + + await fsLocal.rename(key, key2); + + const entityRemote = await fsRemote.stat(key); + const downloadResultEntity = await fsLocal.writeFile( + key, + remoteContent, + entityRemote.mtimeCli ?? Date.now(), + entityRemote.mtimeCli ?? Date.now() + ); + await downloadCallback(downloadResultEntity); + + const entityLocal = await fsLocal.stat(key2); // key2 here! + const uploadResultEntity = await fsRemote.writeFile( + key2, // key2 here! + localContent, + entityLocal.mtimeCli ?? Date.now(), + entityLocal.mtimeCli ?? Date.now() + ); + await uploadCallback(uploadResultEntity); + } +} + /** * local: x.md -> x.dup.md -> upload to remote * remote: x.md -> download to local -> using original name x.md */ -export async function duplicateFile( +async function tryDuplicateFileForDiffSizes( key: string, - left: FakeFs, - right: FakeFs, - uploadCallback: (entity: Entity) => Promise, - downloadCallback: (entity: Entity) => Promise + key2: string, + fsLocal: FakeFs, + fsRemote: FakeFs, + uploadCallback: (entity: Entity | undefined) => Promise, + downloadCallback: (entity: Entity | undefined) => Promise +) { + console.debug(`tryDuplicateFileForDiffSizes: ${key}`); + + await fsLocal.rename(key, key2); + + /** + * x.dup.md -> upload to remote + */ + async function f1() { + const k = await copyFile(key2, fsLocal, fsRemote); + await uploadCallback(k.entity); + return k.entity; + } + + /** + * x.md -> download to local + */ + async function f2() { + const k = await copyFile(key, fsRemote, fsLocal); + await downloadCallback(k.entity); + return k.entity; + } + + const [resUpload, resDownload] = await Promise.all([f1(), f2()]); + + return { + upload: resUpload, + download: resDownload, + }; +} + +export async function tryDuplicateFile( + key: string, + fsLocal: FakeFs, + fsRemote: FakeFs, + uploadCallback: (entity: Entity | undefined) => Promise, + downloadCallback: (entity: Entity | undefined) => Promise ) { let key2 = getFileRename(key); let usable = false; do { try { - const s = await left.stat(key2); + const s = await fsLocal.stat(key2); if (s === null || s === undefined) { throw Error(`not exist $${key2}`); } @@ -228,30 +347,31 @@ export async function duplicateFile( usable = true; } } while (!usable); - await left.rename(key, key2); - /** - * x.dup.md -> upload to remote - */ - async function f1() { - const k = await copyFile(key2, left, right); - await uploadCallback(k.entity); - return k.entity; + const localSize = await fsLocal.stat(key); + const remoteSize = await fsRemote.stat(key); + + if ( + localSize !== undefined && + remoteSize !== undefined && + localSize.sizeRaw === remoteSize.sizeRaw + ) { + return await tryDuplicateFileForSameSizes( + key, + key2, + fsLocal, + fsRemote, + uploadCallback, + downloadCallback + ); + } else { + return await tryDuplicateFileForDiffSizes( + key, + key2, + fsLocal, + fsRemote, + uploadCallback, + downloadCallback + ); } - - /** - * x.md -> download to local - */ - async function f2() { - const k = await copyFile(key, right, left); - await downloadCallback(k.entity); - return k.entity; - } - - const [resUpload, resDownload] = await Promise.all([f1(), f2()]); - - return { - upload: resUpload, - download: resDownload, - }; } diff --git a/pro/src/sync.ts b/pro/src/sync.ts index d08d90a..2c01886 100644 --- a/pro/src/sync.ts +++ b/pro/src/sync.ts @@ -36,7 +36,7 @@ import { } from "../../src/misc"; import type { Profiler } from "../../src/profiler"; import { checkProRunnableAndFixInplace } from "./account"; -import { duplicateFile, isMergable, mergeFile } from "./conflictLogic"; +import { isMergable, mergeFile, tryDuplicateFile } from "./conflictLogic"; import { clearFileContentHistoryByVaultAndProfile, getFileContentHistoryByVaultAndProfile, @@ -1351,27 +1351,31 @@ const dispatchOperationToActualV3 = async ( r.key ); const mtimeCli = (await fsLocal.stat(r.key)).mtimeCli!; - const { upload, download } = await duplicateFile( + await tryDuplicateFile( r.key, fsLocal, fsEncrypt, async (upload) => { - // TODO: abstract away the dirty hack - fullfillMTimeOfRemoteEntityInplace(upload, mtimeCli); - await upsertPrevSyncRecordByVaultAndProfile( - db, - vaultRandomID, - profileID, - upload - ); + if (upload !== undefined) { + // TODO: abstract away the dirty hack + fullfillMTimeOfRemoteEntityInplace(upload, mtimeCli); + await upsertPrevSyncRecordByVaultAndProfile( + db, + vaultRandomID, + profileID, + upload + ); + } }, async (download) => { - await upsertPrevSyncRecordByVaultAndProfile( - db, - vaultRandomID, - profileID, - download - ); + if (download !== undefined) { + await upsertPrevSyncRecordByVaultAndProfile( + db, + vaultRandomID, + profileID, + download + ); + } } ); }