diff --git a/docs/sync_ignoring_large_files.md b/docs/sync_ignoring_large_files.md new file mode 100644 index 0000000..c78d7d8 --- /dev/null +++ b/docs/sync_ignoring_large_files.md @@ -0,0 +1,17 @@ +# Sync Ignoring Large Files + +Initially, the plugin does not ignore large files. + +From the new version in May 2022, it can ignore all files with some sizes. But we need some rules to make the function compatible with existing conditions. + +1. If users are using E2E password mode, then the file sizes are compared on the **encrypted sizes**, rather than the original unencripted file sizes. The reasons are: the encrypted ones are in transferations, and the encrypted sizes can be computed from unencrypted sizes but not the reverse. + +2. Assuming the file A, is already synced between local device and remote service before. + + - If the local size and remote size are both below the threshold, then the file can be synced normally. + - If the local size and remote size are both above the threshold, then the file will be ignored normally. + - If the local size is below the threshold, and the remote size is above the threshold, then the plugin **rejects** the sync, and throws the error to the user. + - If the local size is above the threshold, and the remote size is below the threshold, then the plugin **rejects** the sync, and throws the error to the user. + - When it somes to deletions, the same rules apply. + + The main point is that, if the file sizes "cross the line", the plugin does not introduce any further trouble and just reject to work for this file. diff --git a/src/baseTypes.ts b/src/baseTypes.ts index dc074c9..7d540d1 100644 --- a/src/baseTypes.ts +++ b/src/baseTypes.ts @@ -85,6 +85,7 @@ export interface RemotelySavePluginSettings { syncUnderscoreItems?: boolean; lang?: LangTypeAndAuto; logToDB?: boolean; + skipSizeLargerThan?: number; /** * @deprecated @@ -122,13 +123,24 @@ type DecisionTypeForFile = | "uploadLocalToRemote" // "skipLocal && uploadLocalToRemote && cleanLocalDelHist && cleanRemoteDelHist" | "downloadRemoteToLocal"; // "downloadRemoteToLocal && skipRemote && cleanLocalDelHist && cleanRemoteDelHist" +type DecisionTypeForFileSize = + | "skipUploadingTooLarge" + | "skipDownloadingTooLarge" + | "skipUsingLocalDelTooLarge" + | "skipUsingRemoteDelTooLarge" + | "errorLocalTooLargeConflictRemote" + | "errorRemoteTooLargeConflictLocal"; + type DecisionTypeForFolder = | "createFolder" | "uploadLocalDelHistToRemoteFolder" | "keepRemoteDelHistFolder" | "skipFolder"; -export type DecisionType = DecisionTypeForFile | DecisionTypeForFolder; +export type DecisionType = + | DecisionTypeForFile + | DecisionTypeForFileSize + | DecisionTypeForFolder; export interface FileOrFolderMixedState { key: string; @@ -139,7 +151,9 @@ export interface FileOrFolderMixedState { deltimeLocal?: number; deltimeRemote?: number; sizeLocal?: number; + sizeLocalEnc?: number; sizeRemote?: number; + sizeRemoteEnc?: number; changeRemoteMtimeUsingMapping?: boolean; changeLocalMtimeUsingMapping?: boolean; decision?: DecisionType; diff --git a/src/debugMode.ts b/src/debugMode.ts index a1917b0..598d75b 100644 --- a/src/debugMode.ts +++ b/src/debugMode.ts @@ -26,11 +26,13 @@ const turnSyncPlanToTable = (record: string) => { "remoteEncryptedKey", "existLocal", "sizeLocal", + "sizeLocalEnc", "mtimeLocal", "deltimeLocal", "changeLocalMtimeUsingMapping", "existRemote", "sizeRemote", + "sizeRemoteEnc", "mtimeRemote", "deltimeRemote", "changeRemoteMtimeUsingMapping", diff --git a/src/langs b/src/langs index b227202..ad48de9 160000 --- a/src/langs +++ b/src/langs @@ -1 +1 @@ -Subproject commit b227202f0e7d012efd93e904f19e145fcc726610 +Subproject commit ad48de922720d668477583a6b313a5eaaf4a7516 diff --git a/src/main.ts b/src/main.ts index 952f0c8..1656afe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import { import cloneDeep from "lodash/cloneDeep"; import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide"; import type { + FileOrFolderMixedState, RemotelySavePluginSettings, SyncTriggerSourceType, } from "./baseTypes"; @@ -64,6 +65,7 @@ import { exportVaultLoggerOutputToFiles, exportVaultSyncPlansToFiles, } from "./debugMode"; +import { SizesConflictModal } from "./syncSizesConflictNotice"; const DEFAULT_SETTINGS: RemotelySavePluginSettings = { s3: DEFAULT_S3_CONFIG, @@ -82,6 +84,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = { syncUnderscoreItems: false, lang: "auto", logToDB: false, + skipSizeLargerThan: -1, }; interface OAuth2Info { @@ -280,7 +283,7 @@ export default class RemotelySavePlugin extends Plugin { }) ); this.syncStatus = "generating_plan"; - const { plan, sortedKeys, deletions } = await getSyncPlan( + const { plan, sortedKeys, deletions, sizesGoWrong } = await getSyncPlan( remoteStates, local, localConfigDirContents, @@ -292,6 +295,7 @@ export default class RemotelySavePlugin extends Plugin { this.settings.syncConfigDir, this.app.vault.configDir, this.settings.syncUnderscoreItems, + this.settings.skipSizeLargerThan, this.settings.password ); log.info(plan.mixedStates); // for debugging @@ -317,10 +321,20 @@ export default class RemotelySavePlugin extends Plugin { sortedKeys, metadataFile, origMetadataOnRemote, + sizesGoWrong, deletions, (key: string) => self.trash(key), this.settings.password, this.settings.concurrency, + (ss: FileOrFolderMixedState[]) => { + new SizesConflictModal( + self.app, + self, + this.settings.skipSizeLargerThan, + ss, + this.settings.password !== "" + ).open(); + }, (i: number, totalCount: number, pathName: string, decision: string) => self.setCurrSyncMsg(i, totalCount, pathName, decision) ); diff --git a/src/settings.ts b/src/settings.ts index 3424899..397ca81 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -767,6 +767,25 @@ export class RemotelySaveSettingTab extends PluginSettingTab { }); }); + const skipLargeFilesDiv = generalDiv.createEl("div"); + new Setting(skipLargeFilesDiv) + .setName(t("settings_skiplargefiles")) + .setDesc(t("settings_skiplargefiles_desc")) + .addDropdown((dropdown) => { + dropdown.addOption("-1", t("settings_skiplargefiles_notset")); + + const mbs = [1, 5, 10, 50, 100, 500, 1000]; + for (const mb of mbs) { + dropdown.addOption(`${mb * 1000 * 1000}`, `${mb} MB`); + } + dropdown + .setValue(`${this.plugin.settings.skipSizeLargerThan}`) + .onChange(async (val) => { + this.plugin.settings.skipSizeLargerThan = parseInt(val); + await this.plugin.saveSettings(); + }); + }); + ////////////////////////////////////////////////// // below for general chooser (part 1/2) ////////////////////////////////////////////////// diff --git a/src/sync.ts b/src/sync.ts index edf100b..d79be82 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -19,6 +19,7 @@ import { decryptBase32ToString, decryptBase64urlToString, encryptStringToBase64url, + getSizeFromOrigToEnc, MAGIC_ENCRYPTED_PREFIX_BASE32, MAGIC_ENCRYPTED_PREFIX_BASE64URL, } from "./encrypt"; @@ -217,22 +218,29 @@ export const parseRemoteItems = async ( if (backwardMapping !== undefined) { key = backwardMapping.localKey; const mtimeRemote = backwardMapping.localMtime || entry.lastModified; + + // the backwardMapping.localSize is the file BEFORE encryption + // we want to split two sizes for comparation later + r = { key: key, existRemote: true, mtimeRemote: mtimeRemote, mtimeRemoteFmt: unixTimeToStr(mtimeRemote), - sizeRemote: backwardMapping.localSize || entry.size, + sizeRemote: backwardMapping.localSize, + sizeRemoteEnc: password === "" ? undefined : entry.size, remoteEncryptedKey: remoteEncryptedKey, changeRemoteMtimeUsingMapping: true, }; } else { + // do not have backwardMapping r = { key: key, existRemote: true, mtimeRemote: entry.lastModified, mtimeRemoteFmt: unixTimeToStr(entry.lastModified), - sizeRemote: entry.size, + sizeRemote: password === "" ? entry.size : undefined, + sizeRemoteEnc: password === "" ? undefined : entry.size, remoteEncryptedKey: remoteEncryptedKey, changeRemoteMtimeUsingMapping: false, }; @@ -305,7 +313,8 @@ const ensembleMixedStates = async ( localFileHistory: FileFolderHistoryRecord[], syncConfigDir: boolean, configDir: string, - syncUnderscoreItems: boolean + syncUnderscoreItems: boolean, + password: string ) => { const results = {} as Record; @@ -334,6 +343,8 @@ const ensembleMixedStates = async ( mtimeLocal: mtimeLocal, mtimeLocalFmt: unixTimeToStr(mtimeLocal), sizeLocal: entry.stat.size, + sizeLocalEnc: + password === "" ? undefined : getSizeFromOrigToEnc(entry.stat.size), }; } else if (entry instanceof TFolder) { key = `${entry.path}/`; @@ -343,6 +354,7 @@ const ensembleMixedStates = async ( mtimeLocal: undefined, mtimeLocalFmt: undefined, sizeLocal: 0, + sizeLocalEnc: password === "" ? undefined : getSizeFromOrigToEnc(0), }; } else { throw Error(`unexpected ${entry}`); @@ -358,6 +370,7 @@ const ensembleMixedStates = async ( results[key].mtimeLocal = r.mtimeLocal; results[key].mtimeLocalFmt = r.mtimeLocalFmt; results[key].sizeLocal = r.sizeLocal; + results[key].sizeLocalEnc = r.sizeLocalEnc; } else { results[key] = r; results[key].existRemote = false; @@ -374,6 +387,8 @@ const ensembleMixedStates = async ( mtimeLocal: mtimeLocal, mtimeLocalFmt: unixTimeToStr(mtimeLocal), sizeLocal: entry.size, + sizeLocalEnc: + password === "" ? undefined : getSizeFromOrigToEnc(entry.size), }; if (results.hasOwnProperty(key)) { @@ -382,6 +397,7 @@ const ensembleMixedStates = async ( results[key].mtimeLocal = r.mtimeLocal; results[key].mtimeLocalFmt = r.mtimeLocalFmt; results[key].sizeLocal = r.sizeLocal; + results[key].sizeLocalEnc = r.sizeLocalEnc; } else { results[key] = r; results[key].existRemote = false; @@ -484,6 +500,7 @@ const ensembleMixedStates = async ( const assignOperationToFileInplace = ( origRecord: FileOrFolderMixedState, keptFolder: Set, + skipSizeLargerThan: number, password: string = "" ) => { let r = origRecord; @@ -526,6 +543,18 @@ const assignOperationToFileInplace = ( ); } + if ( + (r.existLocal && password !== "" && r.sizeLocalEnc === undefined) || + (r.existRemote && password !== "" && r.sizeRemoteEnc === undefined) + ) { + throw new Error( + `Error: No encryption sizes: ${JSON.stringify(r, null, 2)}` + ); + } + + const sizeLocalComp = password === "" ? r.sizeLocal : r.sizeLocalEnc; + const sizeRemoteComp = password === "" ? r.sizeRemote : r.sizeRemoteEnc; + // 1. mtimeLocal if (r.existLocal) { const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; @@ -536,26 +565,79 @@ const assignOperationToFileInplace = ( r.mtimeLocal >= deltimeLocal && r.mtimeLocal >= deltimeRemote ) { + if (sizeLocalComp === undefined) { + throw new Error( + `Error: no local size but has local mtime: ${JSON.stringify( + r, + null, + 2 + )}` + ); + } if (r.mtimeLocal === r.mtimeRemote) { - // mtime the same - if (password === "") { - // no password, we can also compare the sizes! - if (r.sizeLocal === r.sizeRemote) { - r.decision = "skipUploading"; - r.decisionBranch = 1; - } else { + // local and remote both exist and mtimes are the same + if (sizeLocalComp === sizeRemoteComp) { + // do not need to consider skipSizeLargerThan in this case + r.decision = "skipUploading"; + r.decisionBranch = 1; + } else { + if (skipSizeLargerThan <= 0) { r.decision = "uploadLocalToRemote"; r.decisionBranch = 2; + } else { + // limit the sizes + if (sizeLocalComp <= skipSizeLargerThan) { + if (sizeRemoteComp <= skipSizeLargerThan) { + r.decision = "uploadLocalToRemote"; + r.decisionBranch = 18; + } else { + r.decision = "errorRemoteTooLargeConflictLocal"; + r.decisionBranch = 19; + } + } else { + if (sizeRemoteComp <= skipSizeLargerThan) { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 20; + } else { + r.decision = "skipUploadingTooLarge"; + r.decisionBranch = 21; + } + } } - } else { - // we have password, then the sizes are always unequal - // we can only rely on mtime - r.decision = "skipUploading"; - r.decisionBranch = 3; } } else { - r.decision = "uploadLocalToRemote"; - r.decisionBranch = 4; + // we have local laregest mtime, + // and the remote not existing or smaller mtime + if (skipSizeLargerThan <= 0) { + // no need to consider sizes + r.decision = "uploadLocalToRemote"; + r.decisionBranch = 4; + } else { + // need to consider sizes + if (sizeLocalComp <= skipSizeLargerThan) { + if (sizeRemoteComp === undefined) { + r.decision = "uploadLocalToRemote"; + r.decisionBranch = 22; + } else if (sizeRemoteComp <= skipSizeLargerThan) { + r.decision = "uploadLocalToRemote"; + r.decisionBranch = 23; + } else { + r.decision = "errorRemoteTooLargeConflictLocal"; + r.decisionBranch = 24; + } + } else { + if (sizeRemoteComp === undefined) { + r.decision = "skipUploadingTooLarge"; + r.decisionBranch = 25; + } else if (sizeRemoteComp <= skipSizeLargerThan) { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 26; + } else { + r.decision = "skipUploadingTooLarge"; + r.decisionBranch = 27; + } + } + } } keptFolder.add(getParentFolder(r.key)); return r; @@ -572,8 +654,49 @@ const assignOperationToFileInplace = ( r.mtimeRemote >= deltimeLocal && r.mtimeRemote >= deltimeRemote ) { - r.decision = "downloadRemoteToLocal"; - r.decisionBranch = 5; + // we have remote laregest mtime, + // and the local not existing or smaller mtime + if (sizeRemoteComp === undefined) { + throw new Error( + `Error: no remote size but has remote mtime: ${JSON.stringify( + r, + null, + 2 + )}` + ); + } + + if (skipSizeLargerThan <= 0) { + // no need to consider sizes + r.decision = "downloadRemoteToLocal"; + r.decisionBranch = 5; + } else { + // need to consider sizes + if (sizeRemoteComp <= skipSizeLargerThan) { + if (sizeLocalComp === undefined) { + r.decision = "downloadRemoteToLocal"; + r.decisionBranch = 28; + } else if (sizeLocalComp <= skipSizeLargerThan) { + r.decision = "downloadRemoteToLocal"; + r.decisionBranch = 29; + } else { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 30; + } + } else { + if (sizeLocalComp === undefined) { + r.decision = "skipDownloadingTooLarge"; + r.decisionBranch = 31; + } else if (sizeLocalComp <= skipSizeLargerThan) { + r.decision = "errorRemoteTooLargeConflictLocal"; + r.decisionBranch = 32; + } else { + r.decision = "skipDownloadingTooLarge"; + r.decisionBranch = 33; + } + } + } + keptFolder.add(getParentFolder(r.key)); return r; } @@ -589,10 +712,44 @@ const assignOperationToFileInplace = ( r.deltimeLocal >= mtimeRemote && r.deltimeLocal >= deltimeRemote ) { - r.decision = "uploadLocalDelHistToRemote"; - r.decisionBranch = 6; - if (r.existLocal || r.existRemote) { - // actual deletion would happen + if (skipSizeLargerThan <= 0) { + r.decision = "uploadLocalDelHistToRemote"; + r.decisionBranch = 6; + if (r.existLocal || r.existRemote) { + // actual deletion would happen + } + } else { + const localTooLargeToDelete = + r.existLocal && sizeLocalComp > skipSizeLargerThan; + const remoteTooLargeToDelete = + r.existRemote && sizeRemoteComp > skipSizeLargerThan; + if (localTooLargeToDelete) { + if (remoteTooLargeToDelete) { + r.decision = "skipUsingLocalDelTooLarge"; + r.decisionBranch = 34; + } else { + if (r.existRemote) { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 35; + } else { + r.decision = "skipUsingLocalDelTooLarge"; + r.decisionBranch = 36; + } + } + } else { + if (remoteTooLargeToDelete) { + if (r.existLocal) { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 37; + } else { + r.decision = "skipUsingLocalDelTooLarge"; + r.decisionBranch = 38; + } + } else { + r.decision = "uploadLocalDelHistToRemote"; + r.decisionBranch = 39; + } + } } return r; } @@ -608,10 +765,44 @@ const assignOperationToFileInplace = ( r.deltimeRemote >= mtimeRemote && r.deltimeRemote >= deltimeLocal ) { - r.decision = "keepRemoteDelHist"; - r.decisionBranch = 7; - if (r.existLocal || r.existRemote) { - // actual deletion would happen + if (skipSizeLargerThan <= 0) { + r.decision = "keepRemoteDelHist"; + r.decisionBranch = 7; + if (r.existLocal || r.existRemote) { + // actual deletion would happen + } + } else { + const localTooLargeToDelete = + r.existLocal && sizeLocalComp > skipSizeLargerThan; + const remoteTooLargeToDelete = + r.existRemote && sizeRemoteComp > skipSizeLargerThan; + if (localTooLargeToDelete) { + if (remoteTooLargeToDelete) { + r.decision = "skipUsingRemoteDelTooLarge"; + r.decisionBranch = 40; + } else { + if (r.existRemote) { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 41; + } else { + r.decision = "skipUsingRemoteDelTooLarge"; + r.decisionBranch = 42; + } + } + } else { + if (remoteTooLargeToDelete) { + if (r.existLocal) { + r.decision = "errorLocalTooLargeConflictRemote"; + r.decisionBranch = 43; + } else { + r.decision = "skipUsingRemoteDelTooLarge"; + r.decisionBranch = 44; + } + } else { + r.decision = "keepRemoteDelHist"; + r.decisionBranch = 45; + } + } } return r; } @@ -746,6 +937,10 @@ const DELETION_DECISIONS: Set = new Set([ "uploadLocalDelHistToRemoteFolder", "keepRemoteDelHistFolder", ]); +const SIZES_GO_WRONG_DECISIONS: Set = new Set([ + "errorLocalTooLargeConflictRemote", + "errorRemoteTooLargeConflictLocal", +]); export const getSyncPlan = async ( remoteStates: FileOrFolderMixedState[], @@ -759,6 +954,7 @@ export const getSyncPlan = async ( syncConfigDir: boolean, configDir: string, syncUnderscoreItems: boolean, + skipSizeLargerThan: number, password: string = "" ) => { const mixedStates = await ensembleMixedStates( @@ -769,13 +965,15 @@ export const getSyncPlan = async ( localFileHistory, syncConfigDir, configDir, - syncUnderscoreItems + syncUnderscoreItems, + password ); const sortedKeys = Object.keys(mixedStates).sort( (k1, k2) => k2.length - k1.length ); + const sizesGoWrong: FileOrFolderMixedState[] = []; const deletions: DeletionOnRemote[] = []; const keptFolder = new Set(); @@ -791,7 +989,16 @@ export const getSyncPlan = async ( } else { // get all operations of files // and at the same time get some helper info for folders - assignOperationToFileInplace(val, keptFolder, password); + assignOperationToFileInplace( + val, + keptFolder, + skipSizeLargerThan, + password + ); + } + + if (SIZES_GO_WRONG_DECISIONS.has(val.decision)) { + sizesGoWrong.push(val); } if (DELETION_DECISIONS.has(val.decision)) { @@ -834,6 +1041,7 @@ export const getSyncPlan = async ( plan: plan, sortedKeys: sortedKeys, deletions: deletions, + sizesGoWrong: sizesGoWrong, }; }; @@ -1015,6 +1223,14 @@ const dispatchOperationToActual = async ( await clearDeleteRenameHistoryOfKeyAndVault(db, r.key, vaultRandomID); } else if (r.decision === "skipFolder") { // do nothing! + } else if (r.decision === "skipUploadingTooLarge") { + // do nothing! + } else if (r.decision === "skipDownloadingTooLarge") { + // do nothing! + } else if (r.decision === "skipUsingLocalDelTooLarge") { + // do nothing! + } else if (r.decision === "skipUsingRemoteDelTooLarge") { + // do nothing! } else { throw Error(`unknown decision in ${JSON.stringify(r)}`); } @@ -1033,7 +1249,14 @@ const splitThreeSteps = (syncPlan: SyncPlanType, sortedKeys: string[]) => { const key = sortedKeys[i]; const val: FileOrFolderMixedState = Object.assign({}, mixedStates[key]); // copy to avoid issue - if (val.decision === "skipFolder" || val.decision === "skipUploading") { + if ( + val.decision === "skipFolder" || + val.decision === "skipUploading" || + val.decision === "skipDownloadingTooLarge" || + val.decision === "skipUploadingTooLarge" || + val.decision === "skipUsingLocalDelTooLarge" || + val.decision === "skipUsingRemoteDelTooLarge" + ) { // pass } else if (val.decision === "createFolder") { const level = atWhichLevel(key); @@ -1093,15 +1316,23 @@ export const doActualSync = async ( sortedKeys: string[], metadataFile: FileOrFolderMixedState, origMetadata: MetadataOnRemote, + sizesGoWrong: FileOrFolderMixedState[], deletions: DeletionOnRemote[], localDeleteFunc: any, password: string = "", concurrency: number = 1, + callbackSizesGoWrong?: any, callbackSyncProcess?: any ) => { const mixedStates = syncPlan.mixedStates; const totalCount = sortedKeys.length || 0; + if (sizesGoWrong.length > 0) { + log.debug(`some sizes are larger than the threshold, abort and show hints`); + callbackSizesGoWrong(sizesGoWrong); + return; + } + log.debug(`start syncing extra data firstly`); await uploadExtraMeta( client, diff --git a/src/syncSizesConflictNotice.ts b/src/syncSizesConflictNotice.ts new file mode 100644 index 0000000..ce88edd --- /dev/null +++ b/src/syncSizesConflictNotice.ts @@ -0,0 +1,90 @@ +import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian"; +import type RemotelySavePlugin from "./main"; // unavoidable +import type { TransItemType } from "./i18n"; +import type { FileOrFolderMixedState } from "./baseTypes"; + +import { log } from "./moreOnLog"; + +export class SizesConflictModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly skipSizeLargerThan: number; + readonly sizesGoWrong: FileOrFolderMixedState[]; + readonly hasPassword: boolean; + constructor( + app: App, + plugin: RemotelySavePlugin, + skipSizeLargerThan: number, + sizesGoWrong: FileOrFolderMixedState[], + hasPassword: boolean + ) { + super(app); + this.plugin = plugin; + this.skipSizeLargerThan = skipSizeLargerThan; + this.sizesGoWrong = sizesGoWrong; + this.hasPassword = hasPassword; + } + onOpen() { + let { contentEl } = this; + const t = (x: TransItemType, vars?: any) => { + return this.plugin.i18n.t(x, vars); + }; + + contentEl.createEl("h2", { + text: t("modal_sizesconflict_title"), + }); + + t("modal_sizesconflict_desc", { + thresholdMB: `${this.skipSizeLargerThan / 1000 / 1000}`, + thresholdBytes: `${this.skipSizeLargerThan}`, + }) + .split("\n") + .forEach((val) => { + contentEl.createEl("p", { text: val }); + }); + + const info = this.serialize(); + + contentEl.createDiv().createEl( + "button", + { + text: t("modal_sizesconflict_copybutton"), + }, + (el) => { + el.onclick = async () => { + await navigator.clipboard.writeText(info); + new Notice(t("modal_sizesconflict_copynotice")); + }; + } + ); + + contentEl.createEl("pre", { + text: info, + }); + } + + serialize() { + return this.sizesGoWrong + .map((x) => { + return [ + x.key, + this.hasPassword + ? `encrypted name: ${x.remoteEncryptedKey}` + : undefined, + `local ${this.hasPassword ? "encrypted " : ""}bytes: ${ + this.hasPassword ? x.sizeLocalEnc : x.sizeLocal + }`, + `remote ${this.hasPassword ? "encrypted " : ""}bytes: ${ + this.hasPassword ? x.sizeRemoteEnc : x.sizeRemote + }`, + ] + .filter((tmp) => tmp !== undefined) + .join("\n"); + }) + .join("\n\n"); + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +}