- Fixed (Security)
   - Assigning IDs to chunks has been corrected for more safety.
  - Fixed
    - Conflict resolution dialogue has been fixed
    - Resolving conflicts by timestamp has been fixed
  - Improved
    -  Notifications can be suppressed for the hidden files update now.
    -  No longer uses the old-xxhash and sha1 for generating the chunk ID.
This commit is contained in:
vorotamoroz committed 2025-01-07 11:28:56 +00:00
1 parent 4b1fff852a
commit 7f853b0222
10 files changed
+99 -59

No files matched your search

+9 -24
View File
@@ -18,9 +18,8 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"idb": "^8.0.0", "idb": "^8.0.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.20", "octagonal-wheels": "^0.1.21",
"svelte-check": "^4.0.4", "svelte-check": "^4.0.4",
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}, },
"devDependencies": { "devDependencies": {
@@ -5105,13 +5104,11 @@
} }
}, },
"node_modules/octagonal-wheels": { "node_modules/octagonal-wheels": {
"version": "0.1.20", "version": "0.1.21",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.20.tgz", "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.21.tgz",
"integrity": "sha512-cHM0UroCsZUZ/u0CA+DTOeasH2lPd8kkrfUiIFzg0d51ANY5KDJNk70rHoJbdph7+npNYgKSsdl1D3fEkzggFQ==", "integrity": "sha512-yJYzli50IwXW1ARgVnHR8LpudhRay0pfkSYJyyxqDuk0WM8hbeWDWLtlB/77xga8WsqW1HnZZP/8LfyZYVg1ew==",
"dependencies": { "dependencies": {
"idb": "^8.0.0", "idb": "^8.0.0"
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
} }
}, },
"node_modules/once": { "node_modules/once": {
@@ -6631,11 +6628,6 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
}, },
"node_modules/xxhash-wasm": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz",
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
},
"node_modules/xxhash-wasm-102": { "node_modules/xxhash-wasm-102": {
"name": "xxhash-wasm", "name": "xxhash-wasm",
"version": "1.0.2", "version": "1.0.2",
@@ -10316,13 +10308,11 @@
} }
}, },
"octagonal-wheels": { "octagonal-wheels": {
"version": "0.1.20", "version": "0.1.21",
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.20.tgz", "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.21.tgz",
"integrity": "sha512-cHM0UroCsZUZ/u0CA+DTOeasH2lPd8kkrfUiIFzg0d51ANY5KDJNk70rHoJbdph7+npNYgKSsdl1D3fEkzggFQ==", "integrity": "sha512-yJYzli50IwXW1ARgVnHR8LpudhRay0pfkSYJyyxqDuk0WM8hbeWDWLtlB/77xga8WsqW1HnZZP/8LfyZYVg1ew==",
"requires": { "requires": {
"idb": "^8.0.0", "idb": "^8.0.0"
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
} }
}, },
"once": { "once": {
@@ -11427,11 +11417,6 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
}, },
"xxhash-wasm": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz",
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
},
"xxhash-wasm-102": { "xxhash-wasm-102": {
"version": "npm:xxhash-wasm@1.0.2", "version": "npm:xxhash-wasm@1.0.2",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz",
+1 -2
View File
@@ -72,9 +72,8 @@
"fflate": "^0.8.2", "fflate": "^0.8.2",
"idb": "^8.0.0", "idb": "^8.0.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.20", "octagonal-wheels": "^0.1.21",
"svelte-check": "^4.0.4", "svelte-check": "^4.0.4",
"xxhash-wasm": "0.4.2",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
} }
} }
+1
View File
@@ -85,6 +85,7 @@ export function getStoragePathFromUXFileInfo(file: UXFileInfoStub | string | Fil
return stripAllPrefixes(file.path); return stripAllPrefixes(file.path);
} }
export function getDatabasePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix) { export function getDatabasePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix) {
if (typeof file == "string" && file.startsWith(ICXHeader)) return file as FilePathWithPrefix;
const prefix = isInternalFile(file) ? ICHeader : ""; const prefix = isInternalFile(file) ? ICHeader : "";
if (typeof file == "string") return (prefix + stripAllPrefixes(file as FilePathWithPrefix)) as FilePathWithPrefix; if (typeof file == "string") return (prefix + stripAllPrefixes(file as FilePathWithPrefix)) as FilePathWithPrefix;
return (prefix + stripAllPrefixes(file.path)) as FilePathWithPrefix; return (prefix + stripAllPrefixes(file.path)) as FilePathWithPrefix;
@@ -154,7 +154,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
this.settings.syncInternalFilesBeforeReplication && this.settings.syncInternalFilesBeforeReplication &&
!this.settings.watchInternalFileChanges !this.settings.watchInternalFileChanges
) { ) {
await this.scanAllStorageChanges(); await this.scanAllStorageChanges(showNotice);
} }
return true; return true;
} }
@@ -1108,6 +1108,9 @@ Offline Changed files: ${files.length}`;
} }
queueNotification(key: FilePath) { queueNotification(key: FilePath) {
if (this.settings.suppressNotifyHiddenFilesChange) {
return;
}
const configDir = this.plugin.app.vault.configDir; const configDir = this.plugin.app.vault.configDir;
if (!key.startsWith(configDir)) return; if (!key.startsWith(configDir)) return;
const dirName = key.split("/").slice(0, -1).join("/"); const dirName = key.split("/").slice(0, -1).join("/");
+1 -1
Submodule src/lib updated: b299a4255c...89e825ef3b.
+3
View File
@@ -22,6 +22,7 @@ import { getDocDataAsArray, isDocContentSame, readContent } from "../../lib/src/
import { shouldBeIgnored } from "../../lib/src/string_and_binary/path"; import { shouldBeIgnored } from "../../lib/src/string_and_binary/path";
import type { ICoreModule } from "../ModuleTypes"; import type { ICoreModule } from "../ModuleTypes";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import { eventHub } from "../../common/events.ts";
export class ModuleFileHandler extends AbstractModule implements ICoreModule { export class ModuleFileHandler extends AbstractModule implements ICoreModule {
get db() { get db() {
@@ -356,6 +357,8 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`, `Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`,
LOG_LEVEL_VERBOSE LOG_LEVEL_VERBOSE
); );
// Before writing (or skipped ), merging dialogue should be cancelled.
eventHub.emitEvent("conflict-cancelled", path);
const ret = await this.dbToStorage(entry, targetFile); const ret = await this.dbToStorage(entry, targetFile);
this._log(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`); this._log(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`);
return ret; return ret;
@@ -11,10 +11,23 @@ import {
type diff_check_result, type diff_check_result,
type FilePathWithPrefix, type FilePathWithPrefix,
} from "../../lib/src/common/types"; } from "../../lib/src/common/types";
import { compareMTime, displayRev, TARGET_IS_NEW } from "../../common/utils"; import {
compareMTime,
displayRev,
isCustomisationSyncMetadata,
isPluginMetadata,
TARGET_IS_NEW,
} from "../../common/utils";
import diff_match_patch from "diff-match-patch"; import diff_match_patch from "diff-match-patch";
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path"; import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
import type { ICoreModule } from "../ModuleTypes.ts"; import type { ICoreModule } from "../ModuleTypes.ts";
import { eventHub } from "../../common/events.ts";
declare global {
interface LSEvents {
"conflict-cancelled": FilePathWithPrefix;
}
}
export class ModuleConflictResolver extends AbstractModule implements ICoreModule { export class ModuleConflictResolver extends AbstractModule implements ICoreModule {
async $$resolveConflictByDeletingRev( async $$resolveConflictByDeletingRev(
@@ -30,11 +43,16 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
); );
return MISSING_OR_ERROR; return MISSING_OR_ERROR;
} }
eventHub.emitEvent("conflict-cancelled", path);
this._log(`${title} Conflicted revision deleted ${displayRev(deleteRevision)} ${path}`, LOG_LEVEL_INFO); this._log(`${title} Conflicted revision deleted ${displayRev(deleteRevision)} ${path}`, LOG_LEVEL_INFO);
if ((await this.core.databaseFileAccess.getConflictedRevs(path)).length != 0) { if ((await this.core.databaseFileAccess.getConflictedRevs(path)).length != 0) {
this._log(`${title} some conflicts are left in ${path}`, LOG_LEVEL_INFO); this._log(`${title} some conflicts are left in ${path}`, LOG_LEVEL_INFO);
return AUTO_MERGED; return AUTO_MERGED;
} }
this._log(`${title} ${path} is a plugin metadata file, no need to write to storage`, LOG_LEVEL_INFO);
if (isPluginMetadata(path) || isCustomisationSyncMetadata(path)) {
return AUTO_MERGED;
}
// If no conflicts were found, write the resolved content to the storage. // If no conflicts were found, write the resolved content to the storage.
if (!(await this.core.fileHandler.dbToStorage(path, stripAllPrefixes(path), true))) { if (!(await this.core.fileHandler.dbToStorage(path, stripAllPrefixes(path), true))) {
this._log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE); this._log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE);
@@ -139,26 +157,42 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
} }
} }
this._log("[conflict] Manual merge required!"); this._log("[conflict] Manual merge required!");
eventHub.emitEvent("conflict-cancelled", filename);
await this.core.$anyResolveConflictByUI(filename, conflictCheckResult); await this.core.$anyResolveConflictByUI(filename, conflictCheckResult);
}); });
} }
async $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> { async $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
const currentRev = await this.core.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
if (currentRev == false) {
this._log(`Could not get current revision of ${filename}`);
return Promise.resolve(false);
}
const revs = await this.core.databaseFileAccess.getConflictedRevs(filename); const revs = await this.core.databaseFileAccess.getConflictedRevs(filename);
if (revs.length == 0) { if (revs.length == 0) {
return Promise.resolve(true); return Promise.resolve(true);
} }
const mTimeAndRev = ( const mTimeAndRev = (
await Promise.all( [
revs.map(async (rev) => { [currentRev.mtime, currentRev._rev],
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev); ...(await Promise.all(
if (leaf == false) { revs.map(async (rev) => {
return [0, rev] as [number, string]; const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
} if (leaf == false) {
return [leaf.mtime, rev] as [number, string]; return [0, rev] as [number, string];
}) }
) return [leaf.mtime, rev] as [number, string];
).sort((a, b) => b[0] - a[0]); })
)),
] as [number, string][]
).sort((a, b) => {
const diff = b[0] - a[0];
if (diff == 0) {
return a[1].localeCompare(b[1], "en", { numeric: true });
}
return diff;
});
console.warn(mTimeAndRev);
this._log( this._log(
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)` `Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
); );
@@ -1,10 +1,19 @@
import { App, Modal } from "../../../deps.ts"; import { App, Modal } from "../../../deps.ts";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
import { CANCELLED, LEAVE_TO_SUBSEQUENT, RESULT_TIMED_OUT, type diff_result } from "../../../lib/src/common/types.ts"; import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts";
import { escapeStringToHTML } from "../../../lib/src/string_and_binary/convert.ts"; import { escapeStringToHTML } from "../../../lib/src/string_and_binary/convert.ts";
import { delay, fireAndForget, sendValue, waitForValue } from "../../../lib/src/common/utils.ts"; import { delay } from "../../../lib/src/common/utils.ts";
import { eventHub } from "../../../common/events.ts";
import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts";
export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string;
declare global {
interface Slips extends LSSlips {
"conflict-resolved": typeof CANCELLED | MergeDialogResult;
}
}
export type MergeDialogResult = typeof LEAVE_TO_SUBSEQUENT | typeof CANCELLED | string;
export class ConflictResolveModal extends Modal { export class ConflictResolveModal extends Modal {
result: diff_result; result: diff_result;
filename: string; filename: string;
@@ -18,6 +27,7 @@ export class ConflictResolveModal extends Modal {
pluginPickMode: boolean = false; pluginPickMode: boolean = false;
localName: string = "Keep A"; localName: string = "Keep A";
remoteName: string = "Keep B"; remoteName: string = "Keep B";
offEvent?: ReturnType<typeof eventHub.onEvent>;
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) { constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
super(app); super(app);
@@ -32,23 +42,21 @@ export class ConflictResolveModal extends Modal {
// Send cancel signal for the previous merge dialogue // Send cancel signal for the previous merge dialogue
// if not there, simply be ignored. // if not there, simply be ignored.
// sendValue("close-resolve-conflict:" + this.filename, false); // sendValue("close-resolve-conflict:" + this.filename, false);
sendValue("cancel-resolve-conflict:" + this.filename, true);
} }
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
// Send cancel signal for the previous merge dialogue // Send cancel signal for the previous merge dialogue
// if not there, simply be ignored. // if not there, simply be ignored.
sendValue("cancel-resolve-conflict:" + this.filename, true); globalSlipBoard.submit("conflict-resolved", this.filename, CANCELLED);
setTimeout(() => { if (this.offEvent) {
fireAndForget(async () => { this.offEvent();
const forceClose = await waitForValue("cancel-resolve-conflict:" + this.filename); }
// debugger; this.offEvent = eventHub.onEvent("conflict-cancelled", (path) => {
if (forceClose) { if (path === this.filename) {
this.sendResponse(CANCELLED); this.sendResponse(CANCELLED);
} }
}); });
}, 10);
// sendValue("close-resolve-conflict:" + this.filename, false); // sendValue("close-resolve-conflict:" + this.filename, false);
this.titleEl.setText(this.title); this.titleEl.setText(this.title);
contentEl.empty(); contentEl.empty();
@@ -111,18 +119,19 @@ export class ConflictResolveModal extends Modal {
onClose() { onClose() {
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();
if (this.offEvent) {
this.offEvent();
}
if (this.consumed) { if (this.consumed) {
return; return;
} }
this.consumed = true; this.consumed = true;
sendValue("close-resolve-conflict:" + this.filename, this.response); globalSlipBoard.submit("conflict-resolved", this.filename, this.response);
sendValue("cancel-resolve-conflict:" + this.filename, false);
} }
async waitForResult(): Promise<MergeDialogResult> { async waitForResult(): Promise<MergeDialogResult> {
await delay(100); await delay(100);
const r = await waitForValue<MergeDialogResult>("close-resolve-conflict:" + this.filename); const r = await globalSlipBoard.awaitNext("conflict-resolved", this.filename);
if (r === RESULT_TIMED_OUT) return CANCELLED;
return r; return r;
} }
} }
@@ -1909,6 +1909,7 @@ I appreciate you for your great dedication.
}); });
} }
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("suppressNotifyHiddenFilesChange", {});
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", { new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", {
onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", true)), onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", true)),
}); });
@@ -2748,9 +2749,10 @@ ${stringifyYaml(pluginConfig)}`;
new Setting(paneEl).autoWireDropDown("hashAlg", { new Setting(paneEl).autoWireDropDown("hashAlg", {
options: { options: {
"": "Old Algorithm", "": "Old Algorithm",
xxhash32: "xxhash32 (Fast)", xxhash32: "xxhash32 (Fast but less collision resistance)",
xxhash64: "xxhash64 (Fastest)", xxhash64: "xxhash64 (Fastest)",
sha1: "Fallback (Without WebAssembly)", "mixed-purejs": "PureJS fallback (Fast, W/O WebAssembly)",
sha1: "Older fallback (Slow, W/O WebAssembly)",
} as Record<HashAlgorithm, string>, } as Record<HashAlgorithm, string>,
}); });
this.addOnSaved("hashAlg", async () => { this.addOnSaved("hashAlg", async () => {
@@ -369,6 +369,10 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
name: "Enable Developers' Debug Tools.", name: "Enable Developers' Debug Tools.",
desc: "Requires restart of Obsidian", desc: "Requires restart of Obsidian",
}, },
suppressNotifyHiddenFilesChange: {
name: "Suppress notification of hidden files change",
desc: "If enabled, the notification of hidden files change will be suppressed.",
},
}; };
function translateInfo(infoSrc: ConfigurationItem | undefined | false) { function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
if (!infoSrc) return false; if (!infoSrc) return false;