diff --git a/src/common/events.ts b/src/common/events.ts index 93be1e7..19b5aaa 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -17,13 +17,10 @@ export const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard"; export const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; export const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; - - export const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; export const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; - // export const EVENT_FILE_CHANGED = "file-changed"; declare global { @@ -37,17 +34,16 @@ declare global { [EVENT_SETTING_SAVED]: ObsidianLiveSyncSettings; [EVENT_PLUGIN_LOADED]: ObsidianLiveSyncPlugin; [EVENT_LAYOUT_READY]: undefined; - "event-file-changed": { file: FilePathWithPrefix, automated: boolean }; - "document-stub-created": - { - toc: Set, stub: { [key: string]: { [key: string]: Map> } } - }, + "event-file-changed": { file: FilePathWithPrefix; automated: boolean }; + "document-stub-created": { + toc: Set; + stub: { [key: string]: { [key: string]: Map> } }; + }; [EVENT_REQUEST_OPEN_SETTINGS]: undefined; [EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined; - [EVENT_FILE_RENAMED]: { newPath: FilePathWithPrefix, old: FilePathWithPrefix }; + [EVENT_FILE_RENAMED]: { newPath: FilePathWithPrefix; old: FilePathWithPrefix }; [EVENT_LEAF_ACTIVE_CHANGED]: undefined; } } export { eventHub }; - diff --git a/src/common/obsidianEvents.ts b/src/common/obsidianEvents.ts index 209cb0e..7b3b821 100644 --- a/src/common/obsidianEvents.ts +++ b/src/common/obsidianEvents.ts @@ -5,6 +5,8 @@ export const EVENT_REQUEST_SHOW_HISTORY = "show-history"; declare global { interface LSEvents { - [EVENT_REQUEST_SHOW_HISTORY]: { file: TFile, fileOnDB: LoadedEntry } | { file: FilePathWithPrefix, fileOnDB: LoadedEntry }; + [EVENT_REQUEST_SHOW_HISTORY]: + | { file: TFile; fileOnDB: LoadedEntry } + | { file: FilePathWithPrefix; fileOnDB: LoadedEntry }; } } diff --git a/src/common/types.ts b/src/common/types.ts index c2aacf2..1f28fa8 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,5 +1,11 @@ import { type PluginManifest, TFile } from "../deps.ts"; -import { type DatabaseEntry, type EntryBody, type FilePath, type UXFileInfoStub, type UXInternalFileInfoStub } from "../lib/src/common/types.ts"; +import { + type DatabaseEntry, + type EntryBody, + type FilePath, + type UXFileInfoStub, + type UXInternalFileInfoStub, +} from "../lib/src/common/types.ts"; export interface PluginDataEntry extends DatabaseEntry { deviceVaultName: string; @@ -55,15 +61,15 @@ export type FileEventArgs = { cache?: CacheData; oldPath?: string; ctx?: any; -} +}; export type FileEventItem = { - type: FileEventType, - args: FileEventArgs, - key: string, - skipBatchWait?: boolean, - cancelled?: boolean, - batched?: boolean -} + type: FileEventType; + args: FileEventArgs; + key: string; + skipBatchWait?: boolean; + cancelled?: boolean; + batched?: boolean; +}; // Hidden items (Now means `chunk`) export const CHeader = "h:"; @@ -82,4 +88,3 @@ export const ICXHeader = "ix:"; export const FileWatchEventQueueMax = 10; export const configURIBase = "obsidian://setuplivesync?settings="; - diff --git a/src/common/utils.ts b/src/common/utils.ts index 0427ce4..9282279 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,20 +1,48 @@ import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "../deps.ts"; -import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "../lib/src/string_and_binary/path.ts"; +import { + path2id_base, + id2path_base, + isValidFilenameInLinux, + isValidFilenameInDarwin, + isValidFilenameInWidows, + isValidFilenameInAndroid, + stripAllPrefixes, +} from "../lib/src/string_and_binary/path.ts"; import { Logger } from "../lib/src/common/logger.ts"; -import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type UXFileInfo, type UXFileInfoStub } from "../lib/src/common/types.ts"; +import { + LOG_LEVEL_VERBOSE, + type AnyEntry, + type DocumentID, + type EntryHasPath, + type FilePath, + type FilePathWithPrefix, + type UXFileInfo, + type UXFileInfoStub, +} from "../lib/src/common/types.ts"; import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types.ts"; import type ObsidianLiveSyncPlugin from "../main.ts"; import { writeString } from "../lib/src/string_and_binary/convert.ts"; import { fireAndForget } from "../lib/src/common/utils.ts"; import { sameChangePairs } from "./stores.ts"; -export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriodicTask, cancelAllPeriodicTask, } from "../lib/src/concurrency/task.ts"; +export { + scheduleTask, + setPeriodicTask, + cancelTask, + cancelAllTasks, + cancelPeriodicTask, + cancelAllPeriodicTask, +} from "../lib/src/concurrency/task.ts"; // For backward compatibility, using the path for determining id. // Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/". // The first slash will be deleted when the path is normalized. -export async function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise { +export async function path2id( + filename: FilePathWithPrefix | FilePath, + obfuscatePassphrase: string | false, + caseInsensitive: boolean +): Promise { const temp = filename.split(":"); const path = temp.pop(); const normalizedPath = normalizePath(path as FilePath); @@ -35,7 +63,6 @@ export function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefi } export function getPath(entry: AnyEntry) { return id2path(entry._id, entry); - } export function getPathWithoutPrefix(entry: AnyEntry) { const f = getPath(entry); @@ -50,7 +77,6 @@ export function getPathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWi return file.path; } - const memos: { [key: string]: any } = {}; export function memoObject(key: string, obj: T): T { memos[key] = obj; @@ -59,7 +85,7 @@ export function memoObject(key: string, obj: T): T { export async function memoIfNotExist(key: string, func: () => T | Promise): Promise { if (!(key in memos)) { const w = func(); - const v = w instanceof Promise ? (await w) : w; + const v = w instanceof Promise ? await w : w; memos[key] = v; } return memos[key] as T; @@ -75,7 +101,6 @@ export function disposeMemoObject(key: string) { delete memos[key]; } - export function isValidPath(filename: string) { if (Platform.isDesktop) { // if(Platform.isMacOS) return isValidFilenameInDarwin(filename); @@ -94,11 +119,10 @@ export function trimPrefix(target: string, prefix: string) { return target.startsWith(prefix) ? target.substring(prefix.length) : target; } - /** * returns is internal chunk of file * @param id ID - * @returns + * @returns */ export function isInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean { return id.startsWith(ICHeader); @@ -107,7 +131,7 @@ export function stripInternalMetadataPrefix Promise; _timer?: number; @@ -141,12 +164,16 @@ export class PeriodicProcessor { enable(interval: number) { this.disable(); if (interval == 0) return; - this._timer = window.setInterval(() => fireAndForget(async () => { - await this.process(); - if (this._plugin.$$isUnloaded()) { - this.disable(); - } - }), interval); + this._timer = window.setInterval( + () => + fireAndForget(async () => { + await this.process(); + if (this._plugin.$$isUnloaded()) { + this.disable(); + } + }), + interval + ); this._plugin.registerInterval(this._timer); } disable() { @@ -157,11 +184,21 @@ export class PeriodicProcessor { } } -export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => { +export const _requestToCouchDBFetch = async ( + baseUri: string, + username: string, + password: string, + path?: string, + body?: string | any, + method?: string +) => { const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]); const encoded = window.btoa(utf8str); const authHeader = "Basic " + encoded; - const transformedHeaders: Record = { authorization: authHeader, "content-type": "application/json" }; + const transformedHeaders: Record = { + authorization: authHeader, + "content-type": "application/json", + }; const uri = `${baseUri}/${path}`; const requestParam = { url: uri, @@ -171,9 +208,17 @@ export const _requestToCouchDBFetch = async (baseUri: string, username: string, body: JSON.stringify(body), }; return await fetch(uri, requestParam); -} +}; -export const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => { +export const _requestToCouchDB = async ( + baseUri: string, + username: string, + password: string, + origin: string, + path?: string, + body?: any, + method?: string +) => { const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]); const encoded = window.btoa(utf8str); const authHeader = "Basic " + encoded; @@ -187,23 +232,32 @@ export const _requestToCouchDB = async (baseUri: string, username: string, passw body: body ? JSON.stringify(body) : undefined, }; return await requestUrl(requestParam); -} -export const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string = "", key?: string, body?: string, method?: string) => { +}; +export const requestToCouchDB = async ( + baseUri: string, + username: string, + password: string, + origin: string = "", + key?: string, + body?: string, + method?: string +) => { const uri = `_node/_local/_config${key ? "/" + key : ""}`; return await _requestToCouchDB(baseUri, username, password, origin, uri, body, method); }; - export const BASE_IS_NEW = Symbol("base"); export const TARGET_IS_NEW = Symbol("target"); export const EVEN = Symbol("even"); - // Why 2000? : ZIP FILE Does not have enough resolution. const resolution = 2000; -export function compareMTime(baseMTime: number, targetMTime: number): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { - const truncatedBaseMTime = (~~(baseMTime / resolution)) * resolution; - const truncatedTargetMTime = (~~(targetMTime / resolution)) * resolution; +export function compareMTime( + baseMTime: number, + targetMTime: number +): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { + const truncatedBaseMTime = ~~(baseMTime / resolution) * resolution; + const truncatedTargetMTime = ~~(targetMTime / resolution) * resolution; // Logger(`Resolution MTime ${truncatedBaseMTime} and ${truncatedTargetMTime} `, LOG_LEVEL_VERBOSE); if (truncatedBaseMTime == truncatedTargetMTime) return EVEN; if (truncatedBaseMTime > truncatedTargetMTime) return BASE_IS_NEW; @@ -215,7 +269,7 @@ export function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mti if (mtime1 === mtime2) return true; const key = typeof file == "string" ? file : "_id" in file ? file._id : file.path; const pairs = sameChangePairs.get(key, []) || []; - if (pairs.some(e => e == mtime1 || e == mtime2)) { + if (pairs.some((e) => e == mtime1 || e == mtime2)) { sameChangePairs.set(key, [...new Set([...pairs, mtime1, mtime2])]); } else { sameChangePairs.set(key, [mtime1, mtime2]); @@ -224,17 +278,20 @@ export function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mti export function isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | string, mtimes: number[]) { const key = typeof file == "string" ? file : "_id" in file ? file._id : file.path; const pairs = sameChangePairs.get(key, []) || []; - if (mtimes.every(e => pairs.indexOf(e) !== -1)) { + if (mtimes.every((e) => pairs.indexOf(e) !== -1)) { return EVEN; } } -export function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { +export function compareFileFreshness( + baseFile: UXFileInfoStub | AnyEntry | undefined, + checkTarget: UXFileInfo | AnyEntry | undefined +): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { if (baseFile === undefined && checkTarget == undefined) return EVEN; if (baseFile == undefined) return TARGET_IS_NEW; if (checkTarget == undefined) return BASE_IS_NEW; - const modifiedBase = "stat" in baseFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0; - const modifiedTarget = "stat" in checkTarget ? checkTarget?.stat?.mtime ?? 0 : checkTarget?.mtime ?? 0; + const modifiedBase = "stat" in baseFile ? (baseFile?.stat?.mtime ?? 0) : (baseFile?.mtime ?? 0); + const modifiedTarget = "stat" in checkTarget ? (checkTarget?.stat?.mtime ?? 0) : (checkTarget?.mtime ?? 0); if (modifiedBase && modifiedTarget && isMarkedAsSameChanges(baseFile, [modifiedBase, modifiedTarget])) { return EVEN; @@ -242,21 +299,27 @@ export function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undef return compareMTime(modifiedBase, modifiedTarget); } -const _cached = new Map; -}>(); +const _cached = new Map< + string, + { + value: any; + context: Map; + } +>(); export type MemoOption = { key: string; forceUpdate?: boolean; validator?: (context: Map) => boolean; -} +}; -export function useMemo({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map, prev: T) => T): T { +export function useMemo( + { key, forceUpdate, validator }: MemoOption, + updateFunc: (context: Map, prev: T) => T +): T { const cached = _cached.get(key); const context = cached?.context || new Map(); - if (cached && !forceUpdate && (!validator || validator && !validator(context))) { + if (cached && !forceUpdate && (!validator || (validator && !validator(context)))) { return cached.value; } const value = updateFunc(context, cached?.value); @@ -267,11 +330,14 @@ export function useMemo({ key, forceUpdate, validator }: MemoOption, updateFu } // const _static = new Map(); -const _staticObj = new Map(); +const _staticObj = new Map< + string, + { + value: any; + } +>(); -export function useStatic(key: string): { value: (T | undefined) }; +export function useStatic(key: string): { value: T | undefined }; export function useStatic(key: string, initial: T): { value: T }; export function useStatic(key: string, initial?: T) { // if (!_static.has(key) && initial) { @@ -288,9 +354,9 @@ export function useStatic(key: string, initial?: T) { return this._buf as T; }, set value(value: T) { - this._buf = value - } - } + this._buf = value; + }, + }; _staticObj.set(key, obj); return obj; } @@ -310,4 +376,4 @@ export function displayRev(rev: string) { // export function getPathFromUXFileInfo(file: UXFileInfoStub | UXFileInfo | string) { // return (typeof file == "string" ? file : file.path) as FilePathWithPrefix; -// } \ No newline at end of file +// } diff --git a/src/deps.ts b/src/deps.ts index 159e505..c7c3778 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,13 +1,39 @@ import { type FilePath } from "./lib/src/common/types.ts"; export { - addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder, - parseYaml, ItemView, WorkspaceLeaf + addIcon, + App, + debounce, + Editor, + FuzzySuggestModal, + MarkdownRenderer, + MarkdownView, + Modal, + Notice, + Platform, + Plugin, + PluginSettingTab, + requestUrl, + sanitizeHTMLToDom, + Setting, + stringifyYaml, + TAbstractFile, + TextAreaComponent, + TFile, + TFolder, + parseYaml, + ItemView, + WorkspaceLeaf, } from "obsidian"; -export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo, ListedFiles } from "obsidian"; -import { - normalizePath as normalizePath_ +export type { + DataWriteOptions, + PluginManifest, + RequestUrlParam, + RequestUrlResponse, + MarkdownFileInfo, + ListedFiles, } from "obsidian"; +import { normalizePath as normalizePath_ } from "obsidian"; const normalizePath = normalizePath_ as (from: T) => T; -export { normalizePath } -export { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; \ No newline at end of file +export { normalizePath }; +export { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 42d7003..7f41ed3 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1,25 +1,75 @@ -import { writable } from 'svelte/store'; -import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles, diff_match_patch, Platform, addIcon } from "../../deps.ts"; +import { writable } from "svelte/store"; +import { + Notice, + type PluginManifest, + parseYaml, + normalizePath, + type ListedFiles, + diff_match_patch, + Platform, + addIcon, +} from "../../deps.ts"; -import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, AnyEntry, SavingEntry, diff_result } from "../../lib/src/common/types.ts"; -import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_SHINY } from "../../lib/src/common/types.ts"; -import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../../common/types.ts"; -import { createBlob, createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, getDocDataAsArray, isDocContentSame, isLoadedEntry, isObjectDifferent } from "../../lib/src/common/utils.ts"; +import type { + EntryDoc, + LoadedEntry, + InternalFileEntry, + FilePathWithPrefix, + FilePath, + AnyEntry, + SavingEntry, + diff_result, +} from "../../lib/src/common/types.ts"; +import { + CANCELLED, + LEAVE_TO_SUBSEQUENT, + LOG_LEVEL_DEBUG, + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + MODE_SELECTIVE, + MODE_SHINY, +} from "../../lib/src/common/types.ts"; +import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "../../common/types.ts"; +import { + createBlob, + createSavingEntryFromLoadedEntry, + createTextBlob, + delay, + fireAndForget, + getDocData, + getDocDataAsArray, + isDocContentSame, + isLoadedEntry, + isObjectDifferent, +} from "../../lib/src/common/utils.ts"; import { digestHash } from "../../lib/src/string_and_binary/hash.ts"; -import { arrayBufferToBase64, decodeBinary, readString } from '../../lib/src/string_and_binary/convert.ts'; +import { arrayBufferToBase64, decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts"; import { serialized, shareRunningResult } from "../../lib/src/concurrency/lock.ts"; import { LiveSyncCommands } from "../LiveSyncCommands.ts"; import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts"; -import { EVEN, PeriodicProcessor, disposeMemoObject, isCustomisationSyncMetadata, isMarkedAsSameChanges, isPluginMetadata, markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "../../common/utils.ts"; +import { + EVEN, + PeriodicProcessor, + disposeMemoObject, + isCustomisationSyncMetadata, + isMarkedAsSameChanges, + isPluginMetadata, + markChangesAreSame, + memoIfNotExist, + memoObject, + retrieveMemoObject, + scheduleTask, +} from "../../common/utils.ts"; import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts"; -import { QueueProcessor } from '../../lib/src/concurrency/processor.ts'; -import { pluginScanningCount } from '../../lib/src/mock_and_interop/stores.ts'; -import type ObsidianLiveSyncPlugin from '../../main.ts'; -import { base64ToArrayBuffer, base64ToString } from 'octagonal-wheels/binary/base64'; -import { ConflictResolveModal } from '../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts'; -import { Semaphore } from 'octagonal-wheels/concurrency/semaphore'; -import type { IObsidianModule } from '../../modules/AbstractObsidianModule.ts'; -import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from '../../common/events.ts'; +import { QueueProcessor } from "../../lib/src/concurrency/processor.ts"; +import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts"; +import type ObsidianLiveSyncPlugin from "../../main.ts"; +import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64"; +import { ConflictResolveModal } from "../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts"; +import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; +import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts"; +import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts"; import { PluginDialogModal } from "./PluginDialogModal.ts"; const d = "\u200b"; @@ -35,10 +85,10 @@ function serialize(data: PluginDataEx): string { ret += data.mtime + d2; for (const file of data.files) { ret += file.filename + d + (file.displayName ?? "") + d + (file.version ?? "") + d2; - const hash = digestHash((file.data ?? [])); + const hash = digestHash(file.data ?? []); ret += file.mtime + d + file.size + d + hash + d2; for (const data of file.data ?? []) { - ret += data + d + ret += data + d; } ret += d2; } @@ -50,7 +100,7 @@ const DUMMY_HEAD = serialize({ files: [], mtime: 0, term: "-", - displayName: `MIRAGED` + displayName: `MIRAGED`, }); const DUMMY_END = d + d2 + "\u200c"; function splitWithDelimiters(sources: string[]): string[] { @@ -126,8 +176,8 @@ function getTokenizer(source: string[]) { pos++; } lineRunOut = false; - } - } + }, + }; return t; } @@ -142,8 +192,14 @@ function deserialize2(str: string[]): PluginDataEx { tokens.nextLine(); const mtime = Number(tokens.next()); tokens.nextLine(); - const result: PluginDataEx = Object.assign(ret, - { category, name, term, version, mtime, files: [] as PluginDataExFile[] }) + const result: PluginDataEx = Object.assign(ret, { + category, + name, + term, + version, + mtime, + files: [] as PluginDataExFile[], + }); let filename = ""; do { filename = tokens.next(); @@ -162,17 +218,15 @@ function deserialize2(str: string[]): PluginDataEx { if (piece == "") break; data.push(piece); } while (piece != ""); - result.files.push( - { - filename, - displayName, - version, - mtime, - size, - data, - hash - } - ) + result.files.push({ + filename, + displayName, + version, + mtime, + size, + data, + hash, + }); tokens.nextLine(); } while (filename); return result; @@ -194,20 +248,19 @@ function deserialize(str: string[], def: T) { } } - export const pluginList = writable([] as PluginDataExDisplay[]); export const pluginIsEnumerating = writable(false); export const pluginV2Progress = writable(0); export type PluginDataExFile = { - filename: string, - data: string[], - mtime: number, - size: number, - version?: string, - hash?: string, - displayName?: string, -} + filename: string; + data: string[]; + mtime: number; + size: number; + version?: string; + hash?: string; + displayName?: string; +}; export interface IPluginDataExDisplay { documentPath: FilePathWithPrefix; category: string; @@ -219,26 +272,33 @@ export interface IPluginDataExDisplay { mtime: number; } export type PluginDataExDisplay = { - documentPath: FilePathWithPrefix, - category: string, - name: string, - term: string, - displayName?: string, - files: PluginDataExFile[], - version?: string, - mtime: number, -} + documentPath: FilePathWithPrefix; + category: string; + name: string; + term: string; + displayName?: string; + files: PluginDataExFile[]; + version?: string; + mtime: number; +}; type LoadedEntryPluginDataExFile = LoadedEntry & PluginDataExFile; function categoryToFolder(category: string, configDir: string = ""): string { switch (category) { - case "CONFIG": return `${configDir}/`; - case "THEME": return `${configDir}/themes/`; - case "SNIPPET": return `${configDir}/snippets/`; - case "PLUGIN_MAIN": return `${configDir}/plugins/`; - case "PLUGIN_DATA": return `${configDir}/plugins/`; - case "PLUGIN_ETC": return `${configDir}/plugins/`; - default: return ""; + case "CONFIG": + return `${configDir}/`; + case "THEME": + return `${configDir}/themes/`; + case "SNIPPET": + return `${configDir}/snippets/`; + case "PLUGIN_MAIN": + return `${configDir}/plugins/`; + case "PLUGIN_DATA": + return `${configDir}/plugins/`; + case "PLUGIN_ETC": + return `${configDir}/plugins/`; + default: + return ""; } } @@ -269,15 +329,15 @@ export class PluginDataExDisplayV2 { this.category = `${data.category}`; this.name = `${data.name}`; this.term = `${data.term}`; - this.files = [...data.files as LoadedEntryPluginDataExFile[]]; + this.files = [...(data.files as LoadedEntryPluginDataExFile[])]; this.confKey = `${categoryToFolder(this.category, this.term)}${this.name}`; this.applyLoadedManifest(); } async setFile(file: LoadedEntryPluginDataExFile) { - const old = this.files.find(e => e.filename == file.filename); + const old = this.files.find((e) => e.filename == file.filename); if (old) { - if (old.mtime == file.mtime && await isDocContentSame(old.data, file.data)) return; - this.files = this.files.filter(e => e.filename != file.filename); + if (old.mtime == file.mtime && (await isDocContentSame(old.data, file.data))) return; + this.files = this.files.filter((e) => e.filename != file.filename); } this.files.push(file); if (file.filename == "manifest.json") { @@ -285,7 +345,7 @@ export class PluginDataExDisplayV2 { } } deleteFile(filename: string) { - this.files = this.files.filter(e => e.filename != filename); + this.files = this.files.filter((e) => e.filename != filename); } _displayName: string | undefined; @@ -313,14 +373,14 @@ export class PluginDataExDisplayV2 { } } export type PluginDataEx = { - documentPath?: FilePathWithPrefix, - category: string, - name: string, - displayName?: string, - term: string, - files: PluginDataExFile[], - version?: string, - mtime: number, + documentPath?: FilePathWithPrefix; + category: string; + name: string; + displayName?: string; + term: string; + files: PluginDataExFile[]; + version?: string; + mtime: number; }; export class ConfigSync extends LiveSyncCommands implements IObsidianModule { @@ -329,8 +389,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { pluginScanningCount.onChanged((e) => { const total = e.value; pluginIsEnumerating.set(total != 0); - }) - + }); } get kvDB() { return this.plugin.kvDB; @@ -393,18 +452,25 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => this.showPluginSyncModal()); } - getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" { + getFileCategory( + filePath: string + ): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" { if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG"; - if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) return "THEME"; + if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`)) + return "THEME"; if (filePath.startsWith(`${this.app.vault.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET"; if (filePath.startsWith(`${this.app.vault.configDir}/plugins/`)) { - if (filePath.endsWith("/styles.css") || filePath.endsWith("/manifest.json") || filePath.endsWith("/main.js")) { + if ( + filePath.endsWith("/styles.css") || + filePath.endsWith("/manifest.json") || + filePath.endsWith("/main.js") + ) { return "PLUGIN_MAIN"; } else if (filePath.endsWith("/data.json")) { return "PLUGIN_DATA"; } else { // Planned at v0.19.0, realised v0.23.18! - return (this.useV2 && this.useSyncPluginEtc) ? "PLUGIN_ETC" : ""; + return this.useV2 && this.useSyncPluginEtc ? "PLUGIN_ETC" : ""; } // return "PLUGIN"; } @@ -443,7 +509,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } - this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); + this.periodicPluginSweepProcessor.enable( + this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges + ? PERIODIC_PLUGIN_SWEEP * 1000 + : 0 + ); return true; } $everyAfterResumeProcess(): Promise { @@ -454,7 +524,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { async reloadPluginList(showMessage: boolean) { this.pluginList = []; this.loadedManifest_mTime.clear(); - pluginList.set(this.pluginList) + pluginList.set(this.pluginList); await this.updatePluginList(showMessage); } async loadPluginData(path: FilePathWithPrefix): Promise { @@ -480,84 +550,104 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { wx.data = serialize(data); fireAndForget(() => this.localDatabase.putDBEntry(createSavingEntryFromLoadedEntry(wx))); } - return ({ + return { ...data, documentPath: this.getPath(wx), - files: xFiles - }) as PluginDataExDisplay; + files: xFiles, + } as PluginDataExDisplay; } return false; } - pluginScanProcessor = new QueueProcessor(async (v: AnyEntry[]) => { - const plugin = v[0]; - if (this.useV2) { - await this.migrateV1ToV2(false, plugin); - return []; - } - const path = plugin.path || this.getPath(plugin); - const oldEntry = (this.pluginList.find(e => e.documentPath == path)); - if (oldEntry && oldEntry.mtime == plugin.mtime) return []; - try { - const pluginData = await this.loadPluginData(path); - if (pluginData) { - let newList = [...this.pluginList]; - newList = newList.filter(x => x.documentPath != pluginData.documentPath); - newList.push(pluginData); - this.pluginList = newList; - pluginList.set(newList); + pluginScanProcessor = new QueueProcessor( + async (v: AnyEntry[]) => { + const plugin = v[0]; + if (this.useV2) { + await this.migrateV1ToV2(false, plugin); + return []; } - // Failed to load - return []; - - } catch (ex) { - this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); - } - return []; - }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline(); - - pluginScanProcessorV2 = new QueueProcessor(async (v: AnyEntry[]) => { - const plugin = v[0]; - const path = plugin.path || this.getPath(plugin); - const oldEntry = (this.pluginList.find(e => e.documentPath == path)); - if (oldEntry && oldEntry.mtime == plugin.mtime) return []; - try { - const pluginData = await this.loadPluginData(path); - if (pluginData) { - let newList = [...this.pluginList]; - newList = newList.filter(x => x.documentPath != pluginData.documentPath); - newList.push(pluginData); - this.pluginList = newList; - pluginList.set(newList); + const path = plugin.path || this.getPath(plugin); + const oldEntry = this.pluginList.find((e) => e.documentPath == path); + if (oldEntry && oldEntry.mtime == plugin.mtime) return []; + try { + const pluginData = await this.loadPluginData(path); + if (pluginData) { + let newList = [...this.pluginList]; + newList = newList.filter((x) => x.documentPath != pluginData.documentPath); + newList.push(pluginData); + this.pluginList = newList; + pluginList.set(newList); + } + // Failed to load + return []; + } catch (ex) { + this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); } - // Failed to load return []; - - } catch (ex) { - this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); + }, + { + suspended: false, + batchSize: 1, + concurrentLimit: 10, + delay: 100, + yieldThreshold: 10, + maintainDelay: false, + totalRemainingReactiveSource: pluginScanningCount, } - return []; - }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline(); + ).startPipeline(); + pluginScanProcessorV2 = new QueueProcessor( + async (v: AnyEntry[]) => { + const plugin = v[0]; + const path = plugin.path || this.getPath(plugin); + const oldEntry = this.pluginList.find((e) => e.documentPath == path); + if (oldEntry && oldEntry.mtime == plugin.mtime) return []; + try { + const pluginData = await this.loadPluginData(path); + if (pluginData) { + let newList = [...this.pluginList]; + newList = newList.filter((x) => x.documentPath != pluginData.documentPath); + newList.push(pluginData); + this.pluginList = newList; + pluginList.set(newList); + } + // Failed to load + return []; + } catch (ex) { + this._log(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); + } + return []; + }, + { + suspended: false, + batchSize: 1, + concurrentLimit: 10, + delay: 100, + yieldThreshold: 10, + maintainDelay: false, + totalRemainingReactiveSource: pluginScanningCount, + } + ).startPipeline(); filenameToUnifiedKey(path: string, termOverRide?: string) { const term = termOverRide || this.plugin.$$getDeviceAndVaultName(); const category = this.getFileCategory(path); - const name = (category == "CONFIG" || category == "SNIPPET") ? - (path.split("/").slice(-1)[0]) : - (category == "PLUGIN_ETC" ? - path.split("/").slice(-2).join("/") : - path.split("/").slice(-2)[0]); - return `${ICXHeader}${term}/${category}/${name}.md` as FilePathWithPrefix + const name = + category == "CONFIG" || category == "SNIPPET" + ? path.split("/").slice(-1)[0] + : category == "PLUGIN_ETC" + ? path.split("/").slice(-2).join("/") + : path.split("/").slice(-2)[0]; + return `${ICXHeader}${term}/${category}/${name}.md` as FilePathWithPrefix; } filenameWithUnifiedKey(path: string, termOverRide?: string) { const term = termOverRide || this.plugin.$$getDeviceAndVaultName(); const category = this.getFileCategory(path); - const name = (category == "CONFIG" || category == "SNIPPET") ? - (path.split("/").slice(-1)[0]) : path.split("/").slice(-2)[0]; + const name = + category == "CONFIG" || category == "SNIPPET" ? path.split("/").slice(-1)[0] : path.split("/").slice(-2)[0]; const baseName = category == "CONFIG" || category == "SNIPPET" ? name : path.split("/").slice(3).join("/"); return `${ICXHeader}${term}/${category}/${name}%${baseName}` as FilePathWithPrefix; } @@ -567,7 +657,13 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return `${ICXHeader}${term}/` as FilePathWithPrefix; } - parseUnifiedPath(unifiedPath: FilePathWithPrefix): { category: string, device: string, key: string, filename: string, pathV1: FilePathWithPrefix } { + parseUnifiedPath(unifiedPath: FilePathWithPrefix): { + category: string; + device: string; + key: string; + filename: string; + pathV1: FilePathWithPrefix; + } { const [device, category, ...rest] = stripAllPrefixes(unifiedPath).split("/"); const relativePath = rest.join("/"); const [key, filename] = relativePath.split("%"); @@ -577,7 +673,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { loadedManifest_mTime = new Map(); - async createPluginDataExFileV2(unifiedPathV2: FilePathWithPrefix, loaded?: LoadedEntry): Promise { + async createPluginDataExFileV2( + unifiedPathV2: FilePathWithPrefix, + loaded?: LoadedEntry + ): Promise { const { category, key, filename, device } = this.parseUnifiedPath(unifiedPathV2); if (!loaded) { const d = await this.localDatabase.getDBEntry(unifiedPathV2); @@ -592,7 +691,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { loaded = d; } const confKey = `${categoryToFolder(category, device)}${key}`; - const relativeFilename = `${categoryToFolder(category, "")}${(category == "CONFIG" || category == "SNIPPET") ? "" : (key + "/")}${filename}`.substring(1); + const relativeFilename = + `${categoryToFolder(category, "")}${category == "CONFIG" || category == "SNIPPET" ? "" : key + "/"}${filename}`.substring( + 1 + ); const dataSrc = getDocData(loaded.data); const dataStart = dataSrc.indexOf(DUMMY_END); const data = dataSrc.substring(dataStart + DUMMY_END.length); @@ -609,21 +711,27 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { try { const parsedManifest = JSON.parse(base64ToString(data)) as PluginManifest; setManifest(confKey, parsedManifest); - this.pluginList.filter(e => e instanceof PluginDataExDisplayV2 && e.confKey == confKey).forEach(e => (e as PluginDataExDisplayV2).applyLoadedManifest()); + this.pluginList + .filter((e) => e instanceof PluginDataExDisplayV2 && e.confKey == confKey) + .forEach((e) => (e as PluginDataExDisplayV2).applyLoadedManifest()); pluginList.set(this.pluginList); } catch (ex) { - this._log(`The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, LOG_LEVEL_VERBOSE); + this._log( + `The file ${loaded.path} seems to manifest, but could not be decoded as JSON`, + LOG_LEVEL_VERBOSE + ); this._log(ex, LOG_LEVEL_VERBOSE); } this.loadedManifest_mTime.set(confKey, file.mtime); } else { - this.pluginList.filter(e => e instanceof PluginDataExDisplayV2 && e.confKey == confKey).forEach(e => (e as PluginDataExDisplayV2).applyLoadedManifest()); + this.pluginList + .filter((e) => e instanceof PluginDataExDisplayV2 && e.confKey == confKey) + .forEach((e) => (e as PluginDataExDisplayV2).applyLoadedManifest()); pluginList.set(this.pluginList); } // } } return file; - } createPluginDataFromV2(unifiedPathV2: FilePathWithPrefix) { const { category, device, key, pathV1 } = this.parseUnifiedPath(unifiedPathV2); @@ -640,7 +748,6 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return ret; } - updatingV2Count = 0; async updatePluginListV2(showMessage: boolean, unifiedFilenameWithKey: FilePathWithPrefix): Promise { @@ -650,7 +757,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { // const unifiedFilenameWithKey = this.filenameWithUnifiedKey(updatedDocumentPath); const { pathV1 } = this.parseUnifiedPath(unifiedFilenameWithKey); - const oldEntry = this.pluginList.find(e => e.documentPath == pathV1); + const oldEntry = this.pluginList.find((e) => e.documentPath == pathV1); let entry: PluginDataExDisplayV2 | undefined = undefined; if (!oldEntry || !(oldEntry instanceof PluginDataExDisplayV2)) { @@ -668,10 +775,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } else { entry.deleteFile(unifiedFilenameWithKey); if (entry.files.length == 0) { - this.pluginList = this.pluginList.filter(e => e.documentPath != pathV1); + this.pluginList = this.pluginList.filter((e) => e.documentPath != pathV1); } } - const newList = this.pluginList.filter(e => e.documentPath != entry.documentPath); + const newList = this.pluginList.filter((e) => e.documentPath != entry.documentPath); newList.push(entry); this.pluginList = newList; @@ -693,7 +800,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } if (!v1Path.endsWith(".md") && !v1Path.startsWith(ICXHeader)) { this._log(`The entry ${v1Path} is not a customisation sync binder`, LOG_LEVEL_VERBOSE); - return + return; } if (v1Path.indexOf("%") !== -1) { this._log(`The entry ${v1Path} is already migrated`, LOG_LEVEL_VERBOSE); @@ -706,25 +813,25 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const pluginData = deserialize(getDocDataAsArray(loadedEntry.data), {}) as PluginDataEx; - const prefixPath = v1Path.slice(0, -(".md".length)) + "%"; + const prefixPath = v1Path.slice(0, -".md".length) + "%"; const category = pluginData.category; for (const f of pluginData.files) { const stripTable: Record = { - "CONFIG": 0, - "THEME": 2, - "SNIPPET": 1, - "PLUGIN_MAIN": 2, - "PLUGIN_DATA": 2, - "PLUGIN_ETC": 2, - } + CONFIG: 0, + THEME: 2, + SNIPPET: 1, + PLUGIN_MAIN: 2, + PLUGIN_DATA: 2, + PLUGIN_ETC: 2, + }; const deletePrefixCount = stripTable?.[category] ?? 1; const relativeFilename = f.filename.split("/").slice(deletePrefixCount).join("/"); const v2Path = (prefixPath + relativeFilename) as FilePathWithPrefix; // console.warn(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`); this._log(`Migrating ${v1Path} / ${relativeFilename} to ${v2Path}`, LOG_LEVEL_VERBOSE); const newId = await this.plugin.$$path2id(v2Path); - // const buf = + // const buf = const data = createBlob([DUMMY_HEAD, DUMMY_END, ...getDocDataAsArray(f.data)]); @@ -737,8 +844,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { datatype: "plain", type: "plain", children: [], - eden: {} - } + eden: {}, + }; const r = await this.plugin.localDatabase.putDBEntry(saving); if (r && r.ok) { this._log(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO); @@ -756,16 +863,20 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (!this._isThisModuleEnabled()) { this.pluginScanProcessor.clearQueue(); this.pluginList = []; - pluginList.set(this.pluginList) + pluginList.set(this.pluginList); return; } try { this.updatingV2Count++; pluginV2Progress.set(this.updatingV2Count); const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : ""; - const plugins = updatedDocumentPath ? - this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { include_docs: true, key: updatedDocumentId, limit: 1 }) : - this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true }); + const plugins = updatedDocumentPath + ? this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { + include_docs: true, + key: updatedDocumentId, + limit: 1, + }) + : this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true }); for await (const v of plugins) { if (v.deleted || v._deleted) continue; if (v.path.indexOf("%") !== -1) { @@ -776,7 +887,6 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const path = v.path || this.getPath(v); if (updatedDocumentPath && updatedDocumentPath != path) continue; this.pluginScanProcessor.enqueue(v); - } } finally { pluginIsEnumerating.set(false); @@ -798,12 +908,15 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const file = pluginData.files[0]; const doc = { ...loadDoc, ...file, datatype: "newnote" } as LoadedEntryPluginDataExFile; return doc; - } + }; const fileA = await loadFile(dataA); const fileB = await loadFile(dataB); this._log(`Comparing: ${dataA.documentPath} <-> ${dataB.documentPath}`, LOG_LEVEL_VERBOSE); if (!fileA || !fileB) { - this._log(`Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, LOG_LEVEL_NOTICE); + this._log( + `Could not load ${dataA.name} for comparison: ${!fileA ? dataA.term : ""}${!fileB ? dataB.term : ""}`, + LOG_LEVEL_NOTICE + ); return false; } let path = stripAllPrefixes(fileA.path.split("/").slice(-1).join("/") as FilePath); // TODO:adjust @@ -811,21 +924,36 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { path = path.split("%")[1] as FilePath; } if (fileA.path.endsWith(".json")) { - return serialized("config:merge-data", () => new Promise((res) => { - this._log("Opening data-merging dialog", LOG_LEVEL_VERBOSE); - // const docs = [docA, docB]; - const modal = new JsonResolveModal(this.app, path, [fileA, fileB], async (keep, result) => { - if (result == null) return res(false); - try { - res(await this.applyData(dataA, result)); - } catch (ex) { - this._log("Could not apply merged file"); - this._log(ex, LOG_LEVEL_VERBOSE); - res(false); - } - }, "Local", `${dataB.term}`, "B", true, true, "Difference between local and remote"); - modal.open(); - })); + return serialized( + "config:merge-data", + () => + new Promise((res) => { + this._log("Opening data-merging dialog", LOG_LEVEL_VERBOSE); + // const docs = [docA, docB]; + const modal = new JsonResolveModal( + this.app, + path, + [fileA, fileB], + async (keep, result) => { + if (result == null) return res(false); + try { + res(await this.applyData(dataA, result)); + } catch (ex) { + this._log("Could not apply merged file"); + this._log(ex, LOG_LEVEL_VERBOSE); + res(false); + } + }, + "Local", + `${dataB.term}`, + "B", + true, + true, + "Difference between local and remote" + ); + modal.open(); + }) + ); } else { const dmp = new diff_match_patch(); let docAData = getDocData(fileA.data); @@ -844,8 +972,8 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const diffResult: diff_result = { left: { rev: "A", ...fileA, data: docAData }, right: { rev: "B", ...fileB, data: docBData }, - diff: diff - } + diff: diff, + }; // console.dir(diffResult); const d = new ConflictResolveModal(this.app, path, diffResult, true, dataB.term); d.open(); @@ -871,7 +999,6 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { // If the content has applied, modified time will be updated to the current time. await this.plugin.storageAccess.writeHiddenFileAuto(path, content); await this.storeCustomisationFileV2(path, this.plugin.$$getDeviceAndVaultName()); - } else { const files = data.files; for (const f of files) { @@ -925,8 +1052,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { return true; } async applyData(data: IPluginDataExDisplay, content?: string): Promise { - this._log(`Applying ${data.displayName || data.name - }..`); + this._log(`Applying ${data.displayName || data.name}..`); if (data instanceof PluginDataExDisplayV2) { return this.applyDataV2(data, content); @@ -936,7 +1062,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (!data.documentPath) throw "InternalError: Document path not exist"; const dx = await this.localDatabase.getDBEntry(data.documentPath); if (dx == false) { - throw "Not found on database" + throw "Not found on database"; } const loadedData = deserialize(getDocDataAsArray(dx.data), {}) as PluginDataEx; for (const f of loadedData.files) { @@ -952,12 +1078,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { await this.plugin.storageAccess.writeHiddenFileAuto(path, content); } this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`); - } catch (ex) { this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`); this._log(ex, LOG_LEVEL_VERBOSE); } - } const uPath = `${baseDir}/${loadedData.files[0].filename}` as FilePath; await this.storeCustomizationFiles(uPath); @@ -969,14 +1093,24 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[]; //@ts-ignore const enabledPlugins = this.app.plugins.enabledPlugins as Set; - const pluginManifest = manifests.find((manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`); + const pluginManifest = manifests.find( + (manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}` + ); if (pluginManifest) { - this._log(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); + this._log( + `Unloading plugin: ${pluginManifest.name}`, + LOG_LEVEL_NOTICE, + "plugin-reload-" + pluginManifest.id + ); // @ts-ignore await this.app.plugins.unloadPlugin(pluginManifest.id); // @ts-ignore await this.app.plugins.loadPlugin(pluginManifest.id); - this._log(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id); + this._log( + `Plugin reloaded: ${pluginManifest.name}`, + LOG_LEVEL_NOTICE, + "plugin-reload-" + pluginManifest.id + ); } } else if (data.category == "CONFIG") { this.plugin.$$askReload(); @@ -993,45 +1127,56 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (data.documentPath) { const delList = []; if (this.useV2) { - const deleteList = this.pluginList.filter(e => e.documentPath == data.documentPath).filter(e => e instanceof PluginDataExDisplayV2).map(e => e.files).flat(); + const deleteList = this.pluginList + .filter((e) => e.documentPath == data.documentPath) + .filter((e) => e instanceof PluginDataExDisplayV2) + .map((e) => e.files) + .flat(); for (const e of deleteList) { delList.push(e.path); } } delList.push(data.documentPath); - const p = delList.map(async e => { + const p = delList.map(async (e) => { await this.deleteConfigOnDatabase(e); - await this.updatePluginList(false, e) + await this.updatePluginList(false, e); }); await Promise.allSettled(p); // await this.deleteConfigOnDatabase(data.documentPath); // await this.updatePluginList(false, data.documentPath); - this._log(`Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, LOG_LEVEL_NOTICE); + this._log( + `Deleted: ${data.category}/${data.name} of ${data.category} (${delList.length} items)`, + LOG_LEVEL_NOTICE + ); } return true; } catch (ex) { this._log(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE); this._log(ex, LOG_LEVEL_VERBOSE); return false; - } } async $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument) { if (!docs._id.startsWith(ICXHeader)) return undefined; if (this._isThisModuleEnabled()) { - await this.updatePluginList(false, (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath((docs as AnyEntry))); + await this.updatePluginList( + false, + (docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath(docs as AnyEntry) + ); } if (this._isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) { if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) { const fragment = createFragment((doc) => { doc.createEl("span", undefined, (a) => { a.appendText(`Some configuration has been arrived, Press `); - a.appendChild(a.createEl("a", undefined, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - this.showPluginSyncModal(); - }); - })); + a.appendChild( + a.createEl("a", undefined, (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + this.showPluginSyncModal(); + }); + }) + ); a.appendText(` to open the config sync dialog , or press elsewhere to dismiss this message.`); }); @@ -1047,8 +1192,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } scheduleTask(updatedPluginKey + "-close", 20000, () => { const popup = retrieveMemoObject(updatedPluginKey); - if (!popup) - return; + if (!popup) return; //@ts-ignore if (popup?.noticeEl?.isShown()) { popup.hide(); @@ -1068,7 +1212,11 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { if (this.settings.autoSweepPlugins) { await this.scanAllConfigFiles(false); } - this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0); + this.periodicPluginSweepProcessor.enable( + this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges + ? PERIODIC_PLUGIN_SWEEP * 1000 + : 0 + ); return true; } @@ -1095,7 +1243,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { displayName = `${json.name}`; } } catch (ex) { - this._log(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO); + this._log( + `Configuration sync data: ${path} looks like manifest, but could not read the version`, + LOG_LEVEL_INFO + ); this._log(ex, LOG_LEVEL_VERBOSE); } } @@ -1112,10 +1263,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { size: stat.size, version, displayName: displayName, - } + }; } - async storeCustomisationFileV2(path: FilePath, term: string, force = false) { const vf = this.filenameWithUnifiedKey(path, term); return await serialized(`plugin-${vf}`, async () => { @@ -1128,7 +1278,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const mtime = stat.mtime; const content = await this.plugin.storageAccess.readHiddenFileBinary(path); - const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...await arrayBufferToBase64(content)]); + const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...(await arrayBufferToBase64(content))]); // const contentBlob = createBlob(content); try { const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false); @@ -1145,11 +1295,14 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { children: [], deleted: false, type: "plain", - eden: {} + eden: {}, }; } else { if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) { - this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG); + this._log( + `STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, + LOG_LEVEL_DEBUG + ); return; } const docXDoc = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); @@ -1161,12 +1314,14 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const oldContent = dataSrc.substring(dataStart + DUMMY_END.length); const oldContentArray = base64ToArrayBuffer(oldContent); if (await isDocContentSame(oldContentArray, content)) { - this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE); + this._log( + `STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, + LOG_LEVEL_VERBOSE + ); markChangesAreSame(prefixedFileName, old.mtime, mtime + 1); return true; } - saveData = - { + saveData = { ...old, data: contentBlob, mtime, @@ -1186,7 +1341,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { this._log(ex, LOG_LEVEL_VERBOSE); return false; } - }) + }); } async storeCustomizationFiles(path: FilePath, termOverRide?: string) { const term = termOverRide || this.plugin.$$getDeviceAndVaultName(); @@ -1200,15 +1355,15 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const vf = this.filenameToUnifiedKey(path, term); // console.warn(`Storing ${path} to ${bareVF} :--> ${keyedVF}`); - return await serialized(`plugin-${vf}`, async () => { const category = this.getFileCategory(path); let mtime = 0; let fileTargets = [] as FilePath[]; // let savePath = ""; - const name = (category == "CONFIG" || category == "SNIPPET") ? - (path.split("/").reverse()[0]) : - (path.split("/").reverse()[1]); + const name = + category == "CONFIG" || category == "SNIPPET" + ? path.split("/").reverse()[0] + : path.split("/").reverse()[1]; const parentPath = path.split("/").slice(0, -1).join("/"); const prefixedFileName = this.filenameToUnifiedKey(path, term); const id = await this.path2id(prefixedFileName); @@ -1217,18 +1372,23 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { files: [], name: name, mtime: 0, - term: term - } + term: term, + }; // let scheduleKey = ""; - if (category == "CONFIG" || category == "SNIPPET" || category == "PLUGIN_ETC" || category == "PLUGIN_DATA") { + if ( + category == "CONFIG" || + category == "SNIPPET" || + category == "PLUGIN_ETC" || + category == "PLUGIN_DATA" + ) { fileTargets = [path]; if (category == "PLUGIN_ETC") { dt.displayName = path.split("/").slice(-1).join("/"); } } else if (category == "PLUGIN_MAIN") { - fileTargets = ["manifest.json", "main.js", "styles.css"].map(e => `${parentPath}/${e}` as FilePath); + fileTargets = ["manifest.json", "main.js", "styles.css"].map((e) => `${parentPath}/${e}` as FilePath); } else if (category == "THEME") { - fileTargets = ["manifest.json", "theme.css"].map(e => `${parentPath}/${e}` as FilePath); + fileTargets = ["manifest.json", "theme.css"].map((e) => `${parentPath}/${e}` as FilePath); } for (const target of fileTargets) { const data = await this.makeEntryFromFile(target); @@ -1243,7 +1403,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { dt.displayName = data.displayName; } // Use average for total modified time. - mtime = mtime == 0 ? data.mtime : ((data.mtime + mtime) / 2); + mtime = mtime == 0 ? data.mtime : (data.mtime + mtime) / 2; dt.files.push(data); } dt.mtime = mtime; @@ -1253,7 +1413,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { this._log(`Nothing left: deleting.. ${path}`); await this.deleteConfigOnDatabase(prefixedFileName); await this.updatePluginList(false, prefixedFileName); - return + return; } const content = createTextBlob(serialize(dt)); @@ -1272,7 +1432,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { children: [], deleted: false, type: "newnote", - eden: {} + eden: {}, }; } else { if (old.mtime == mtime) { @@ -1281,20 +1441,31 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const oldC = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); if (oldC) { - const d = await deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx; + const d = (await deserialize(getDocDataAsArray(oldC.data), {})) as PluginDataEx; if (d.files.length == dt.files.length) { - const diffs = (d.files.map(previous => ({ prev: previous, curr: dt.files.find(e => e.filename == previous.filename) })).map(async e => { - try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch { return false } - })) - const isSame = (await Promise.all(diffs)).every(e => e == true); + const diffs = d.files + .map((previous) => ({ + prev: previous, + curr: dt.files.find((e) => e.filename == previous.filename), + })) + .map(async (e) => { + try { + return await isDocContentSame(e.curr?.data ?? [], e.prev.data); + } catch { + return false; + } + }); + const isSame = (await Promise.all(diffs)).every((e) => e == true); if (isSame) { - this._log(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); + this._log( + `STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, + LOG_LEVEL_VERBOSE + ); return true; } } } - saveData = - { + saveData = { ...old, data: content, mtime, @@ -1314,7 +1485,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { this._log(ex, LOG_LEVEL_VERBOSE); return false; } - }) + }); } async $anyProcessOptionalFileEvent(path: FilePath): Promise { return await this.watchVaultRawEventsAsync(path); @@ -1327,20 +1498,21 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { // if (!this.isTargetPath(path)) return false; const stat = await this.plugin.storageAccess.statHidden(path); // Make sure that target is a file. - if (stat && stat.type != "file") - return false; + if (stat && stat.type != "file") return false; const configDir = normalizePath(this.app.vault.configDir); - const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting).filter(e => - e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY - ).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); - if (synchronisedInConfigSync.some(e => e.startsWith(path.toLowerCase()))) { + const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting) + .filter((e) => e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY) + .map((e) => e.files) + .flat() + .map((e) => `${configDir}/${e}`.toLowerCase()); + if (synchronisedInConfigSync.some((e) => e.startsWith(path.toLowerCase()))) { this._log(`Customization file skipped: ${path}`, LOG_LEVEL_VERBOSE); // This file could be handled by the other module. return false; } // this._log(`Customization file detected: ${path}`, LOG_LEVEL_VERBOSE); - const storageMTime = ~~((stat && stat.mtime || 0) / 1000); + const storageMTime = ~~(((stat && stat.mtime) || 0) / 1000); const key = `${path}-${storageMTime}`; if (this.recentProcessedInternalFiles.contains(key)) { // If recently processed, it may caused by self. @@ -1352,7 +1524,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { const keySchedule = this.filenameToUnifiedKey(path); scheduleTask(keySchedule, 100, async () => { await this.storeCustomizationFiles(path); - }) + }); // Okay, it may handled after 100ms. // This was my own job. return true; @@ -1369,10 +1541,14 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } const filesAll = await this.scanInternalFiles(); if (this.useV2) { - const filesAllUnified = filesAll.filter(e => this.isTargetPath(e)).map(e => [this.filenameWithUnifiedKey(e, term), e] as [FilePathWithPrefix, FilePath]); - const localFileMap = new Map(filesAllUnified.map(e => [e[0], e[1]])); + const filesAllUnified = filesAll + .filter((e) => this.isTargetPath(e)) + .map((e) => [this.filenameWithUnifiedKey(e, term), e] as [FilePathWithPrefix, FilePath]); + const localFileMap = new Map(filesAllUnified.map((e) => [e[0], e[1]])); const prefix = this.unifiedKeyPrefixOfTerminal(term); - const entries = this.localDatabase.findEntries(prefix + "", `${prefix}\u{10ffff}`, { include_docs: true }); + const entries = this.localDatabase.findEntries(prefix + "", `${prefix}\u{10ffff}`, { + include_docs: true, + }); const tasks = [] as (() => Promise)[]; const concurrency = 10; const semaphore = Semaphore(concurrency); @@ -1397,9 +1573,9 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } finally { releaser(); } - }) + }); } - await Promise.all(tasks.map(e => e())); + await Promise.all(tasks.map((e) => e())); // Extra files const taskExtra = [] as (() => Promise)[]; for (const [, filePath] of localFileMap) { @@ -1410,43 +1586,55 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } catch (ex) { this._log(`scanAllConfigFiles - Error: ${filePath}`, LOG_LEVEL_VERBOSE); this._log(ex, LOG_LEVEL_VERBOSE); - } - finally { + } finally { releaser(); } - }) + }); } - await Promise.all(taskExtra.map(e => e())); + await Promise.all(taskExtra.map((e) => e())); fireAndForget(() => this.updatePluginList(false)); } else { - const files = filesAll.filter(e => this.isTargetPath(e)).map(e => ({ key: this.filenameToUnifiedKey(e), file: e })); - const virtualPathsOfLocalFiles = [...new Set(files.map(e => e.key))]; - const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICXHeader + "", endkey: `${ICXHeader}\u{10ffff}`, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); - let deleteCandidate = filesOnDB.map(e => this.getPath(e)).filter(e => e.startsWith(`${ICXHeader}${term}/`)); + const files = filesAll + .filter((e) => this.isTargetPath(e)) + .map((e) => ({ key: this.filenameToUnifiedKey(e), file: e })); + const virtualPathsOfLocalFiles = [...new Set(files.map((e) => e.key))]; + const filesOnDB = ( + ( + await this.localDatabase.allDocsRaw({ + startkey: ICXHeader + "", + endkey: `${ICXHeader}\u{10ffff}`, + include_docs: true, + }) + ).rows.map((e) => e.doc) as InternalFileEntry[] + ).filter((e) => !e.deleted); + let deleteCandidate = filesOnDB + .map((e) => this.getPath(e)) + .filter((e) => e.startsWith(`${ICXHeader}${term}/`)); for (const vp of virtualPathsOfLocalFiles) { - const p = files.find(e => e.key == vp)?.file; + const p = files.find((e) => e.key == vp)?.file; if (!p) { this._log(`scanAllConfigFiles - File not found: ${vp}`, LOG_LEVEL_VERBOSE); continue; } await this.storeCustomizationFiles(p); - deleteCandidate = deleteCandidate.filter(e => e != vp); + deleteCandidate = deleteCandidate.filter((e) => e != vp); } for (const vp of deleteCandidate) { await this.deleteConfigOnDatabase(vp); } - fireAndForget(() => this.updatePluginList(false)) + fireAndForget(() => this.updatePluginList(false)); } }); } async deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite = false) { - // const id = await this.path2id(prefixedFileName); const mtime = new Date().getTime(); return await serialized("file-x-" + prefixedFileName, async () => { try { - const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false) as InternalFileEntry | false; + const old = (await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, false)) as + | InternalFileEntry + | false; let saveData: InternalFileEntry; if (old === false) { this._log(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted (Not found on database)`); @@ -1456,8 +1644,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { this._log(`STORAGE -x> DB:${prefixedFileName}: (config) already deleted`); return true; } - saveData = - { + saveData = { ...old, mtime, size: 0, @@ -1479,15 +1666,17 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } async scanInternalFiles(): Promise { - const filenames = (await this.getFiles(this.app.vault.configDir, 2)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); + const filenames = (await this.getFiles(this.app.vault.configDir, 2)) + .filter((e) => e.startsWith(".")) + .filter((e) => !e.startsWith(".trash")); return filenames as FilePath[]; } - async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean, enableOverwrite?: boolean }): Promise { + async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise { await this._askHiddenFileConfiguration(opt); return true; } - async _askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { + async _askHiddenFileConfiguration(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) { const message = `Would you like to enable **Customization sync**? > [!DETAILS]- @@ -1495,7 +1684,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { > > You may use this feature alongside hidden file synchronisation. When both features are enabled, items configured as \`Automatic\` in this feature will be managed by **hidden file synchronisation**. > Do not worry, you will be prompted to enable or keep disabled **hidden file synchronisation** after this dialogue. -` +`; const CHOICE_CUSTOMIZE = "Yes, Enable it"; const CHOICE_DISABLE = "No, Disable it"; const CHOICE_DISMISS = "Later"; @@ -1506,20 +1695,20 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { choices.push(CHOICE_DISMISS); const ret = await this.plugin.confirm.askSelectStringDialogue(message, choices, { - defaultAction: CHOICE_DISMISS, timeout: 40, - title: "Customisation sync" + defaultAction: CHOICE_DISMISS, + timeout: 40, + title: "Customisation sync", }); if (ret == CHOICE_CUSTOMIZE) { await this.configureHiddenFileSync("CUSTOMIZE"); } else if (ret == CHOICE_DISABLE) { await this.configureHiddenFileSync("DISABLE_CUSTOM"); } - } $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise { if (isPluginMetadata(path)) { - return Promise.resolve("newer") + return Promise.resolve("newer"); } if (isCustomisationSyncMetadata(path)) { return Promise.resolve("newer"); @@ -1529,7 +1718,10 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { $allSuspendExtraSync(): Promise { if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) { - this._log("Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + this._log( + "Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.", + LOG_LEVEL_NOTICE + ); this.plugin.settings.usePluginSync = false; this.plugin.settings.autoSweepPlugins = false; } @@ -1551,23 +1743,23 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`); if (!name) { if (Platform.isAndroidApp) { - name = "android-app" + name = "android-app"; } else if (Platform.isIosApp) { - name = "ios" + name = "ios"; } else if (Platform.isMacOS) { - name = "macos" + name = "macos"; } else if (Platform.isMobileApp) { - name = "mobile-app" + name = "mobile-app"; } else if (Platform.isMobile) { - name = "mobile" + name = "mobile"; } else if (Platform.isSafari) { - name = "safari" + name = "safari"; } else if (Platform.isDesktop) { - name = "desktop" + name = "desktop"; } else if (Platform.isDesktopApp) { - name = "desktop-app" + name = "desktop-app"; } else { - name = "unknown" + name = "unknown"; } name = name + Math.random().toString(36).slice(-4); } @@ -1580,10 +1772,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { } } - async getFiles( - path: string, - lastDepth: number - ) { + async getFiles(path: string, lastDepth: number) { if (lastDepth == -1) return []; let w: ListedFiles; try { @@ -1593,9 +1782,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule { this._log(ex, LOG_LEVEL_VERBOSE); return []; } - let files = [ - ...w.files - ]; + let files = [...w.files]; for (const v of w.folders) { files = files.concat(await this.getFiles(v, lastDepth - 1)); } diff --git a/src/features/ConfigSync/PluginDialogModal.ts b/src/features/ConfigSync/PluginDialogModal.ts index 2b334a2..7ce2fd2 100644 --- a/src/features/ConfigSync/PluginDialogModal.ts +++ b/src/features/ConfigSync/PluginDialogModal.ts @@ -18,10 +18,11 @@ export class PluginDialogModal extends Modal { this.contentEl.style.overflow = "auto"; this.contentEl.style.display = "flex"; this.contentEl.style.flexDirection = "column"; - this.titleEl.setText("Customization Sync (Beta3)") + this.titleEl.setText("Customization Sync (Beta3)"); if (!this.component) { this.component = new PluginPane({ - target: contentEl, props: { plugin: this.plugin }, + target: contentEl, + props: { plugin: this.plugin }, }); } } @@ -32,4 +33,4 @@ export class PluginDialogModal extends Modal { this.component = undefined; } } -} \ No newline at end of file +} diff --git a/src/features/HiddenFileCommon/JsonResolveModal.ts b/src/features/HiddenFileCommon/JsonResolveModal.ts index 90369a6..f711292 100644 --- a/src/features/HiddenFileCommon/JsonResolveModal.ts +++ b/src/features/HiddenFileCommon/JsonResolveModal.ts @@ -16,10 +16,18 @@ export class JsonResolveModal extends Modal { hideLocal: boolean; title: string = "Conflicted Setting"; - constructor(app: App, filename: FilePath, - docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise, - nameA?: string, nameB?: string, defaultSelect?: string, - keepOrder?: boolean, hideLocal?: boolean, title: string = "Conflicted Setting") { + constructor( + app: App, + filename: FilePath, + docs: LoadedEntry[], + callback: (keepRev?: string, mergedStr?: string) => Promise, + nameA?: string, + nameB?: string, + defaultSelect?: string, + keepOrder?: boolean, + hideLocal?: boolean, + title: string = "Conflicted Setting" + ) { super(app); this.callback = callback; this.filename = filename; @@ -57,14 +65,14 @@ export class JsonResolveModal extends Modal { defaultSelect: this.defaultSelect, keepOrder: this.keepOrder, hideLocal: this.hideLocal, - callback: (keepRev: string | undefined, mergedStr: string | undefined) => this.UICallback(keepRev, mergedStr), + callback: (keepRev: string | undefined, mergedStr: string | undefined) => + this.UICallback(keepRev, mergedStr), }, }); } return; } - onClose() { const { contentEl } = this; contentEl.empty(); diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index a6fdff1..539e5f5 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -1,8 +1,41 @@ import { normalizePath, type PluginManifest, type ListedFiles } from "../../deps.ts"; -import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry, type DocumentID, type UXStat, MODE_AUTOMATIC, type FilePathWithPrefixLC } from "../../lib/src/common/types.ts"; +import { + type EntryDoc, + type LoadedEntry, + type InternalFileEntry, + type FilePathWithPrefix, + type FilePath, + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + MODE_SELECTIVE, + MODE_PAUSED, + type SavingEntry, + type DocumentID, + type UXStat, + MODE_AUTOMATIC, + type FilePathWithPrefixLC, +} from "../../lib/src/common/types.ts"; import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "../../common/types.ts"; -import { readAsBlob, isDocContentSame, sendSignal, readContent, createBlob, fireAndForget } from "../../lib/src/common/utils.ts"; -import { BASE_IS_NEW, compareMTime, EVEN, getPath, isInternalMetadata, isMarkedAsSameChanges, markChangesAreSame, PeriodicProcessor, TARGET_IS_NEW } from "../../common/utils.ts"; +import { + readAsBlob, + isDocContentSame, + sendSignal, + readContent, + createBlob, + fireAndForget, +} from "../../lib/src/common/utils.ts"; +import { + BASE_IS_NEW, + compareMTime, + EVEN, + getPath, + isInternalMetadata, + isMarkedAsSameChanges, + markChangesAreSame, + PeriodicProcessor, + TARGET_IS_NEW, +} from "../../common/utils.ts"; import { serialized } from "../../lib/src/concurrency/lock.ts"; import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts"; import { LiveSyncCommands } from "../LiveSyncCommands.ts"; @@ -13,12 +46,17 @@ import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts"; import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts"; export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule { - _isThisModuleEnabled() { return this.plugin.settings.syncInternalFiles; } - periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this._isThisModuleEnabled() && this._isDatabaseReady() && await this.syncInternalFilesAndDatabase("push", false)); + periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor( + this.plugin, + async () => + this._isThisModuleEnabled() && + this._isDatabaseReady() && + (await this.syncInternalFilesAndDatabase("push", false)) + ); get kvDB() { return this.plugin.kvDB; @@ -40,10 +78,9 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule eventHub.onEvent(EVENT_SETTING_SAVED, () => { this.updateSettingCache(); }); - } async $everyOnDatabaseInitialized(showNotice: boolean) { - this.knownChanges = await this.plugin.kvDB.get("knownChanges") ?? {}; + this.knownChanges = (await this.plugin.kvDB.get("knownChanges")) ?? {}; if (this._isThisModuleEnabled()) { try { this._log("Synchronizing hidden files..."); @@ -57,7 +94,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule return true; } async $everyBeforeReplicate(showNotice: boolean) { - if (this._isThisModuleEnabled() && this._isDatabaseReady() && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) { + if ( + this._isThisModuleEnabled() && + this._isDatabaseReady() && + this.settings.syncInternalFilesBeforeReplication && + !this.settings.watchInternalFileChanges + ) { await this.syncInternalFilesAndDatabase("push", showNotice); } return true; @@ -70,42 +112,53 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule updateSettingCache() { const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); + .split(",") + .filter((e) => e) + .map((e) => new RegExp(e, "i")); this.ignorePatterns = ignorePatterns; this.shouldSkipFile = [] as FilePathWithPrefixLC[]; // Exclude files handled by customization sync const configDir = normalizePath(this.app.vault.configDir); - const shouldSKip = !this.settings.usePluginSync ? [] : - Object.values(this.settings.pluginSyncExtendedSetting). - filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED). - map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); + const shouldSKip = !this.settings.usePluginSync + ? [] + : Object.values(this.settings.pluginSyncExtendedSetting) + .filter((e) => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED) + .map((e) => e.files) + .flat() + .map((e) => `${configDir}/${e}`.toLowerCase()); this.shouldSkipFile = shouldSKip as FilePathWithPrefixLC[]; this._log(`Hidden file will skip ${this.shouldSkipFile.length} files`, LOG_LEVEL_INFO); - } shouldSkipFile = [] as FilePathWithPrefixLC[]; async $everyOnResumeProcess(): Promise { this.periodicInternalFileScanProcessor?.disable(); - if (this._isMainSuspended()) - return true; + if (this._isMainSuspended()) return true; if (this._isThisModuleEnabled()) { await this.syncInternalFilesAndDatabase("safe", false); } - this.periodicInternalFileScanProcessor.enable(this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); - return true + this.periodicInternalFileScanProcessor.enable( + this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval + ? this.settings.syncInternalFilesInterval * 1000 + : 0 + ); + return true; } $everyRealizeSettingSyncMode(): Promise { this.periodicInternalFileScanProcessor?.disable(); - if (this._isMainSuspended()) - return Promise.resolve(true); - if (!this.plugin.$$isReady()) - return Promise.resolve(true); - this.periodicInternalFileScanProcessor.enable(this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval ? (this.settings.syncInternalFilesInterval * 1000) : 0); + if (this._isMainSuspended()) return Promise.resolve(true); + if (!this.plugin.$$isReady()) return Promise.resolve(true); + this.periodicInternalFileScanProcessor.enable( + this._isThisModuleEnabled() && this.settings.syncInternalFilesInterval + ? this.settings.syncInternalFilesInterval * 1000 + : 0 + ); const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); + .split(",") + .filter((e) => e) + .map((e) => new RegExp(e, "i")); this.ignorePatterns = ignorePatterns; return Promise.resolve(true); } @@ -119,7 +172,15 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule await this.syncInternalFilesAndDatabase("pull", false, false, filenames); this._log(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE); return; - }, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 100, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount } + }, + { + batchSize: 100, + concurrentLimit: 1, + delay: 10, + yieldThreshold: 100, + suspended: false, + totalRemainingReactiveSource: hiddenFilesEventCount, + } ); async $anyProcessOptionalFileEvent(path: FilePath): Promise { @@ -130,7 +191,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (this._isMainSuspended()) return false; if (!this._isThisModuleEnabled()) return false; - if (this.shouldSkipFile.some(e => e.startsWith(path.toLowerCase()))) { + if (this.shouldSkipFile.some((e) => e.startsWith(path.toLowerCase()))) { this._log(`Hidden file skipped: ${path} is synchronized in customization sync.`, LOG_LEVEL_VERBOSE); return false; } @@ -145,12 +206,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule // This could be caused by self. so return true to prevent further processing. return true; } - const mtime = stat == null ? 0 : stat?.mtime ?? 0; - const storageMTime = ~~((mtime) / 1000); + const mtime = stat == null ? 0 : (stat?.mtime ?? 0); + const storageMTime = ~~(mtime / 1000); const prefixedFileName = addPrefix(path, ICHeader); const filesOnDB = await this.localDatabase.getDBEntryMeta(prefixedFileName); - const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000); + const dbMTime = ~~(((filesOnDB && filesOnDB.mtime) || 0) / 1000); // Skip unchanged file. if (dbMTime == storageMTime) { @@ -163,7 +224,12 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (storageMTime == 0) { await this.deleteInternalFileOnDatabase(path); } else { - await this.storeInternalFileToDatabase({ path: path, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }); + await this.storeInternalFileToDatabase({ + path: path, + mtime, + ctime: stat?.ctime ?? mtime, + size: stat?.size ?? 0, + }); } // Surely processed. return true; @@ -181,8 +247,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this.conflictResolutionProcessor.suspend(); try { for await (const doc of conflicted) { - if (!("_conflicts" in doc)) - continue; + if (!("_conflicts" in doc)) continue; if (isInternalMetadata(doc._id)) { this.conflictResolutionProcessor.enqueue(doc.path); } @@ -194,7 +259,13 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule await this.conflictResolutionProcessor.startPipeline().waitForAllProcessed(); } - async resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: EntryDoc, currentRev: string, conflictedRev: string) { + async resolveByNewerEntry( + id: DocumentID, + path: FilePathWithPrefix, + currentDoc: EntryDoc, + currentRev: string, + conflictedRev: string + ) { const conflictedDoc = await this.localDatabase.getRaw(id, { rev: conflictedRev }); // determine which revision should been deleted. // simply check modified time @@ -208,86 +279,110 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this._log(`Older one has been deleted:${path}`); const cc = await this.localDatabase.getRaw(id, { conflicts: true }); if (cc._conflicts?.length === 0) { - await this.extractInternalFileFromDatabase(stripAllPrefixes(path)) + await this.extractInternalFileFromDatabase(stripAllPrefixes(path)); } else { this.conflictResolutionProcessor.enqueue(path); } // check the file again - } - conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => { - const path = paths[0]; - sendSignal(`cancel-internal-conflict:${path}`); - try { - // Retrieve data - const id = await this.path2id(path, ICHeader); - const doc = await this.localDatabase.getRaw(id, { conflicts: true }); - // if (!("_conflicts" in doc)){ - // return []; - // } - if (doc._conflicts === undefined) return []; - if (doc._conflicts.length == 0) - return []; - this._log(`Hidden file conflicted:${path}`); - const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); - const revA = doc._rev; - const revB = conflicts[0]; + conflictResolutionProcessor = new QueueProcessor( + async (paths: FilePathWithPrefix[]) => { + const path = paths[0]; + sendSignal(`cancel-internal-conflict:${path}`); + try { + // Retrieve data + const id = await this.path2id(path, ICHeader); + const doc = await this.localDatabase.getRaw(id, { conflicts: true }); + // if (!("_conflicts" in doc)){ + // return []; + // } + if (doc._conflicts === undefined) return []; + if (doc._conflicts.length == 0) return []; + this._log(`Hidden file conflicted:${path}`); + const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); + const revA = doc._rev; + const revB = conflicts[0]; - if (path.endsWith(".json")) { - const conflictedRev = conflicts[0]; - const conflictedRevNo = Number(conflictedRev.split("-")[0]); - //Search - const revFrom = (await this.localDatabase.getRaw(id, { revs_info: true })); - const commonBase = revFrom._revs_info?.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? ""; - const result = await this.plugin.localDatabase.mergeObject(path, commonBase, doc._rev, conflictedRev); - if (result) { - this._log(`Object merge:${path}`, LOG_LEVEL_INFO); - const filename = stripAllPrefixes(path); - const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(filename); - if (!isExists) { - await this.plugin.storageAccess.ensureDir(filename); + if (path.endsWith(".json")) { + const conflictedRev = conflicts[0]; + const conflictedRevNo = Number(conflictedRev.split("-")[0]); + //Search + const revFrom = await this.localDatabase.getRaw(id, { revs_info: true }); + const commonBase = + revFrom._revs_info + ?.filter((e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo) + .first()?.rev ?? ""; + const result = await this.plugin.localDatabase.mergeObject( + path, + commonBase, + doc._rev, + conflictedRev + ); + if (result) { + this._log(`Object merge:${path}`, LOG_LEVEL_INFO); + const filename = stripAllPrefixes(path); + const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(filename); + if (!isExists) { + await this.plugin.storageAccess.ensureDir(filename); + } + await this.plugin.storageAccess.writeHiddenFileAuto(filename, result); + const stat = await this.plugin.storageAccess.statHidden(filename); + if (!stat) { + throw new Error(`conflictResolutionProcessor: Failed to stat file ${filename}`); + } + await this.storeInternalFileToDatabase({ path: filename, ...stat }); + await this.extractInternalFileFromDatabase(filename); + await this.localDatabase.removeRevision(id, revB); + this.conflictResolutionProcessor.enqueue(path); + return []; + } else { + this._log(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); } - await this.plugin.storageAccess.writeHiddenFileAuto(filename, result); - const stat = await this.plugin.storageAccess.statHidden(filename); - if (!stat) { - throw new Error(`conflictResolutionProcessor: Failed to stat file ${filename}`); - } - await this.storeInternalFileToDatabase({ path: filename, ...stat }); - await this.extractInternalFileFromDatabase(filename); - await this.localDatabase.removeRevision(id, revB); - this.conflictResolutionProcessor.enqueue(path); - return []; - } else { - this._log(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); + return [{ path, revA, revB, id, doc }]; } - return [{ path, revA, revB, id, doc }]; - } - // When not JSON file, resolve conflicts by choosing a newer one. - await this.resolveByNewerEntry(id, path, doc, revA, revB); - return []; - } catch (ex) { - this._log(`Failed to resolve conflict (Hidden): ${path}`); - this._log(ex, LOG_LEVEL_VERBOSE); - return []; - } - }, { - suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10, - pipeTo: new QueueProcessor(async (results) => { - const { id, doc, path, revA, revB } = results[0]; - const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA }); - const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB }); - if (docAMerge != false && docBMerge != false) { - if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) { - // Again for other conflicted revisions. - this.conflictResolutionProcessor.enqueue(path); - } - return; - } else { - // If either revision could not read, force resolving by the newer one. + // When not JSON file, resolve conflicts by choosing a newer one. await this.resolveByNewerEntry(id, path, doc, revA, revB); + return []; + } catch (ex) { + this._log(`Failed to resolve conflict (Hidden): ${path}`); + this._log(ex, LOG_LEVEL_VERBOSE); + return []; } - }, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 }) - }) + }, + { + suspended: false, + batchSize: 1, + concurrentLimit: 5, + delay: 10, + keepResultUntilDownstreamConnected: true, + yieldThreshold: 10, + pipeTo: new QueueProcessor( + async (results) => { + const { id, doc, path, revA, revB } = results[0]; + const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA }); + const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB }); + if (docAMerge != false && docBMerge != false) { + if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) { + // Again for other conflicted revisions. + this.conflictResolutionProcessor.enqueue(path); + } + return; + } else { + // If either revision could not read, force resolving by the newer one. + await this.resolveByNewerEntry(id, path, doc, revA, revB); + } + }, + { + suspended: false, + batchSize: 1, + concurrentLimit: 1, + delay: 10, + keepResultUntilDownstreamConnected: false, + yieldThreshold: 10, + } + ), + } + ); $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise { if (isInternalMetadata(path)) { @@ -315,7 +410,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule this.conflictResolutionProcessor.enqueue(path); } - knownChanges: { [key: string]: number; } = {}; + knownChanges: { [key: string]: number } = {}; markAsKnownChange(path: string, mtime: number) { this.knownChanges[path] = mtime; } @@ -324,37 +419,68 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } ignorePatterns: RegExp[] = []; //TODO: Tidy up. Even though it is experimental feature, So dirty... - async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, filesAll: InternalFileInfo[] | false = false, targetFilesSrc: string[] | false = false) { - const targetFiles = targetFilesSrc ? targetFilesSrc.map(e => stripAllPrefixes(e as FilePathWithPrefix)) : false; + async syncInternalFilesAndDatabase( + direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", + showMessage: boolean, + filesAll: InternalFileInfo[] | false = false, + targetFilesSrc: string[] | false = false + ) { + const targetFiles = targetFilesSrc + ? targetFilesSrc.map((e) => stripAllPrefixes(e as FilePathWithPrefix)) + : false; // debugger; await this.resolveConflictOnInternalFiles(); const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; this._log("Scanning hidden files.", logLevel, "sync_internal"); const configDir = normalizePath(this.app.vault.configDir); - let files: InternalFileInfo[] = - filesAll ? filesAll : (await this.scanInternalFiles()) - const allowedInHiddenFileSync = this.settings.usePluginSync ? Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_AUTOMATIC).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()) : undefined; + let files: InternalFileInfo[] = filesAll ? filesAll : await this.scanInternalFiles(); + const allowedInHiddenFileSync = this.settings.usePluginSync + ? Object.values(this.settings.pluginSyncExtendedSetting) + .filter((e) => e.mode == MODE_AUTOMATIC) + .map((e) => e.files) + .flat() + .map((e) => `${configDir}/${e}`.toLowerCase()) + : undefined; if (allowedInHiddenFileSync) { - const systemOrNot = files.reduce((acc, cur) => { - if (cur.path.startsWith(configDir)) { - acc.system.push(cur); - } else { - acc.user.push(cur); - } - return acc; - }, { system: [] as InternalFileInfo[], user: [] as InternalFileInfo[] }); + const systemOrNot = files.reduce( + (acc, cur) => { + if (cur.path.startsWith(configDir)) { + acc.system.push(cur); + } else { + acc.user.push(cur); + } + return acc; + }, + { system: [] as InternalFileInfo[], user: [] as InternalFileInfo[] } + ); - files = - [...systemOrNot.user, - ...systemOrNot.system.filter(file => allowedInHiddenFileSync.some(filterFile => file.path.toLowerCase().startsWith(filterFile)))]; + files = [ + ...systemOrNot.user, + ...systemOrNot.system.filter((file) => + allowedInHiddenFileSync.some((filterFile) => file.path.toLowerCase().startsWith(filterFile)) + ), + ]; } - const filesOnDB = ((await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[]).filter(e => !e.deleted); - const allFileNamesSrc = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => stripAllPrefixes(this.getPath(e)))])]; - let allFileNames = allFileNamesSrc.filter(filename => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1)); + const filesOnDB = ( + ( + await this.localDatabase.allDocsRaw({ startkey: ICHeader, endkey: ICHeaderEnd, include_docs: true }) + ).rows.map((e) => e.doc) as InternalFileEntry[] + ).filter((e) => !e.deleted); + const allFileNamesSrc = [ + ...new Set([ + ...files.map((e) => normalizePath(e.path)), + ...filesOnDB.map((e) => stripAllPrefixes(this.getPath(e))), + ]), + ]; + let allFileNames = allFileNamesSrc.filter( + (filename) => !targetFiles || (targetFiles && targetFiles.indexOf(filename) !== -1) + ); if (allowedInHiddenFileSync) { - allFileNames = allFileNames.filter(file => allowedInHiddenFileSync.some(filterFile => file.toLowerCase().startsWith(filterFile))); + allFileNames = allFileNames.filter((file) => + allowedInHiddenFileSync.some((filterFile) => file.toLowerCase().startsWith(filterFile)) + ); } const fileCount = allFileNames.length; @@ -366,7 +492,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule // .obsidian/plugins: 1 // .obsidian/plugins/recent-files-obsidian: 1 // .obsidian/plugins/recent-files-obsidian/data.json: 1 - const updatedFolders: { [key: string]: number; } = {}; + const updatedFolders: { [key: string]: number } = {}; const countUpdatedFolder = (path: string) => { const pieces = path.split("/"); let c = pieces.shift(); @@ -383,98 +509,115 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } }; - const filesMap = files.reduce((acc, cur) => { - acc[cur.path] = cur; - return acc; - }, {} as { [key: string]: InternalFileInfo; }); - const filesOnDBMap = filesOnDB.reduce((acc, cur) => { - acc[stripAllPrefixes(this.getPath(cur))] = cur; - return acc; - }, {} as { [key: string]: InternalFileEntry; }); - await new QueueProcessor(async (filenames: FilePath[]) => { - const filename = filenames[0]; - processed++; - if (processed % 100 == 0) { - this._log(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); - } - if (!filename) return []; - if (this.ignorePatterns.some(e => filename.match(e))) - return []; - if (await this.plugin.$$isIgnoredByIgnoreFiles(filename)) { - return []; - } + const filesMap = files.reduce( + (acc, cur) => { + acc[cur.path] = cur; + return acc; + }, + {} as { [key: string]: InternalFileInfo } + ); + const filesOnDBMap = filesOnDB.reduce( + (acc, cur) => { + acc[stripAllPrefixes(this.getPath(cur))] = cur; + return acc; + }, + {} as { [key: string]: InternalFileEntry } + ); + await new QueueProcessor( + async (filenames: FilePath[]) => { + const filename = filenames[0]; + processed++; + if (processed % 100 == 0) { + this._log(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal"); + } + if (!filename) return []; + if (this.ignorePatterns.some((e) => filename.match(e))) return []; + if (await this.plugin.$$isIgnoredByIgnoreFiles(filename)) { + return []; + } - const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined; - const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined; + const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined; + const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined; - return [{ - filename, - fileOnStorage, - fileOnDatabase, - }] - - }, { suspended: true, batchSize: 1, concurrentLimit: 10, delay: 0, totalRemainingReactiveSource: hiddenFilesProcessingCount }) - .pipeTo(new QueueProcessor(async (params) => { - const + return [ { filename, - fileOnStorage: xFileOnStorage, - fileOnDatabase: xFileOnDatabase - } = params[0]; - const xFileOnDatabaseExists = xFileOnDatabase !== undefined && !(xFileOnDatabase.deleted || xFileOnDatabase._deleted); - if (xFileOnStorage && xFileOnDatabaseExists) { - // Both => Synchronize - if ((direction != "pullForce" && direction != "pushForce") && isMarkedAsSameChanges(filename, [xFileOnDatabase.mtime, xFileOnStorage.mtime]) == EVEN) { - this._log(`Hidden file skipped: ${filename} is marked as same`, LOG_LEVEL_VERBOSE); - return; - } + fileOnStorage, + fileOnDatabase, + }, + ]; + }, + { + suspended: true, + batchSize: 1, + concurrentLimit: 10, + delay: 0, + totalRemainingReactiveSource: hiddenFilesProcessingCount, + } + ) + .pipeTo( + new QueueProcessor( + async (params) => { + const { filename, fileOnStorage: xFileOnStorage, fileOnDatabase: xFileOnDatabase } = params[0]; + const xFileOnDatabaseExists = + xFileOnDatabase !== undefined && !(xFileOnDatabase.deleted || xFileOnDatabase._deleted); + if (xFileOnStorage && xFileOnDatabaseExists) { + // Both => Synchronize + if ( + direction != "pullForce" && + direction != "pushForce" && + isMarkedAsSameChanges(filename, [xFileOnDatabase.mtime, xFileOnStorage.mtime]) == EVEN + ) { + this._log(`Hidden file skipped: ${filename} is marked as same`, LOG_LEVEL_VERBOSE); + return; + } - const nw = compareMTime(xFileOnStorage.mtime, xFileOnDatabase.mtime); - if (nw == BASE_IS_NEW || direction == "pushForce") { - if (await this.storeInternalFileToDatabase(xFileOnStorage) !== false) { - // countUpdatedFolder(filename); + const nw = compareMTime(xFileOnStorage.mtime, xFileOnDatabase.mtime); + if (nw == BASE_IS_NEW || direction == "pushForce") { + if ((await this.storeInternalFileToDatabase(xFileOnStorage)) !== false) { + // countUpdatedFolder(filename); + } + } else if (nw == TARGET_IS_NEW || direction == "pullForce") { + // skip if not extraction performed. + if (await this.extractInternalFileFromDatabase(filename)) countUpdatedFolder(filename); + } else { + // Even, or not forced. skip. + } + } else if (!xFileOnStorage && xFileOnDatabaseExists) { + if (direction == "push" || direction == "pushForce") { + if (xFileOnDatabase.deleted) return; + await this.deleteInternalFileOnDatabase(filename, false); + } else if (direction == "pull" || direction == "pullForce") { + if (await this.extractInternalFileFromDatabase(filename)) { + countUpdatedFolder(filename); + } + } else if (direction == "safe") { + if (xFileOnDatabase.deleted) return; + if (await this.extractInternalFileFromDatabase(filename)) { + countUpdatedFolder(filename); + } + } + } else if (xFileOnStorage && !xFileOnDatabaseExists) { + if (direction == "push" || direction == "pushForce" || direction == "safe") { + await this.storeInternalFileToDatabase(xFileOnStorage); + } else { + // Apply the deletion + if (await this.extractInternalFileFromDatabase(xFileOnStorage.path)) { + countUpdatedFolder(xFileOnStorage.path); + } + } + } else { + throw new Error("Invalid state on hidden file sync"); + // Something corrupted? } - } else if (nw == TARGET_IS_NEW || direction == "pullForce") { - // skip if not extraction performed. - if (await this.extractInternalFileFromDatabase(filename)) - countUpdatedFolder(filename); - } else { - // Even, or not forced. skip. - } - } else if (!xFileOnStorage && xFileOnDatabaseExists) { - if (direction == "push" || direction == "pushForce") { - if (xFileOnDatabase.deleted) - return; - await this.deleteInternalFileOnDatabase(filename, false); - } else if (direction == "pull" || direction == "pullForce") { - if (await this.extractInternalFileFromDatabase(filename)) { - countUpdatedFolder(filename); - } - } else if (direction == "safe") { - if (xFileOnDatabase.deleted) - return; - if (await this.extractInternalFileFromDatabase(filename)) { - countUpdatedFolder(filename); - } - } - } else if (xFileOnStorage && !xFileOnDatabaseExists) { - if (direction == "push" || direction == "pushForce" || direction == "safe") { - await this.storeInternalFileToDatabase(xFileOnStorage); - } else { - // Apply the deletion - if (await this.extractInternalFileFromDatabase(xFileOnStorage.path)) { - countUpdatedFolder(xFileOnStorage.path); - } - } - } else { - throw new Error("Invalid state on hidden file sync"); - // Something corrupted? - } - return; - }, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 0 })) - .root - .enqueueAll(allFileNames) - .startPipeline().waitForAllDoneAndTerminate(); + return; + }, + { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 0 } + ) + ) + .root.enqueueAll(allFileNames) + .startPipeline() + .waitForAllDoneAndTerminate(); // When files has been retrieved from the database. they must be reloaded. if ((direction == "pull" || direction == "pullForce") && filesChanged != 0) { @@ -487,44 +630,58 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[]; //@ts-ignore const enabledPlugins = this.app.plugins.enabledPlugins as Set; - const enabledPluginManifests = manifests.filter(e => enabledPlugins.has(e.id)); + const enabledPluginManifests = manifests.filter((e) => enabledPlugins.has(e.id)); for (const manifest of enabledPluginManifests) { if (manifest.dir && manifest.dir in updatedFolders) { // If notified about plug-ins, reloading Obsidian may not be necessary. updatedCount -= updatedFolders[manifest.dir]; const updatePluginId = manifest.id; const updatePluginName = manifest.name; - this.plugin.confirm.askInPopup(`updated-${updatePluginId}`, `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - fireAndForget(async () => { - this._log(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); - // @ts-ignore - await this.app.plugins.unloadPlugin(updatePluginId); - // @ts-ignore - await this.app.plugins.loadPlugin(updatePluginId); - this._log(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId); + this.plugin.confirm.askInPopup( + `updated-${updatePluginId}`, + `Files in ${updatePluginName} has been updated, Press {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`, + (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + fireAndForget(async () => { + this._log( + `Unloading plugin: ${updatePluginName}`, + LOG_LEVEL_NOTICE, + "plugin-reload-" + updatePluginId + ); + // @ts-ignore + await this.app.plugins.unloadPlugin(updatePluginId); + // @ts-ignore + await this.app.plugins.loadPlugin(updatePluginId); + this._log( + `Plugin reloaded: ${updatePluginName}`, + LOG_LEVEL_NOTICE, + "plugin-reload-" + updatePluginId + ); + }); }); - }); - } + } ); } } } catch (ex) { this._log("Error on checking plugin status."); this._log(ex, LOG_LEVEL_VERBOSE); - } // If something changes left, notify for reloading Obsidian. if (updatedCount != 0) { if (!this.plugin.$$isReloadingScheduled()) { - this.plugin.confirm.askInPopup(`updated-any-hidden`, `Hidden files have been synchronised, Press {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - this.plugin.$$scheduleAppReload(); - }); - }); + this.plugin.confirm.askInPopup( + `updated-any-hidden`, + `Hidden files have been synchronised, Press {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`, + (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + this.plugin.$$scheduleAppReload(); + }); + } + ); } } } @@ -563,7 +720,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule eden: {}, }; } else { - if (await isDocContentSame(readAsBlob(old), content) && !forceWrite) { + if ((await isDocContentSame(readAsBlob(old), content)) && !forceWrite) { // this._log(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE); const stat = await this.plugin.storageAccess.statHidden(storageFilePath); if (stat) { @@ -571,8 +728,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } return undefined; } - saveData = - { + saveData = { ...old, data: content, mtime, @@ -607,11 +763,13 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule const prefixedFileName = addPrefix(storeFilePath, ICHeader); const mtime = new Date().getTime(); if (await this.plugin.$$isIgnoredByIgnoreFiles(storageFilePath)) { - return undefined + return undefined; } return await serialized("file-" + prefixedFileName, async () => { try { - const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, true) as InternalFileEntry | false; + const old = (await this.localDatabase.getDBEntryMeta(prefixedFileName, undefined, true)) as + | InternalFileEntry + | false; let saveData: InternalFileEntry; if (old === false) { saveData = { @@ -623,7 +781,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule children: [], deleted: true, type: "newnote", - eden: {} + eden: {}, }; } else { // Remove all conflicted before deleting. @@ -631,15 +789,17 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule if (conflicts._conflicts !== undefined) { for (const conflictRev of conflicts._conflicts) { await this.localDatabase.removeRevision(old._id, conflictRev); - this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, LOG_LEVEL_VERBOSE); + this._log( + `STORAGE -x> DB: ${displayFileName}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, + LOG_LEVEL_VERBOSE + ); } } if (old.deleted) { this._log(`STORAGE -x> DB: ${displayFileName}: (hidden) already deleted`); return undefined; } - saveData = - { + saveData = { ...old, mtime, size: 0, @@ -675,19 +835,29 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule } return await serialized("file-" + prefixedFileName, async () => { try { - // Check conflicted status - const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true, true); - if (fileOnDB === false) - throw new Error(`File not found on database.:${displayFileName}`); + // Check conflicted status + const fileOnDB = await this.localDatabase.getDBEntry( + prefixedFileName, + { conflicts: true }, + false, + true, + true + ); + if (fileOnDB === false) throw new Error(`File not found on database.:${displayFileName}`); // Prevent overwrite for Prevent overwriting while some conflicted revision exists. if (fileOnDB?._conflicts?.length) { - this._log(`Hidden file ${displayFileName} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO); + this._log( + `Hidden file ${displayFileName} has conflicted revisions, to keep in safe, writing to storage has been prevented`, + LOG_LEVEL_INFO + ); return false; } const deleted = fileOnDB.deleted || fileOnDB._deleted || false; if (deleted) { if (!isExists) { - this._log(`STORAGE { return new Promise((res) => { this._log("Opening data-merging dialog", LOG_LEVEL_VERBOSE); @@ -795,7 +977,10 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule throw new Error("Stat failed"); } const mtime = stat?.mtime ?? 0; - await this.storeInternalFileToDatabase({ path: storageFilePath, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }, true); + await this.storeInternalFileToDatabase( + { path: storageFilePath, mtime, ctime: stat?.ctime ?? mtime, size: stat?.size ?? 0 }, + true + ); try { //@ts-ignore internalAPI await this.app.vault.adapter.reconcileInternalFile(storageFilePath); @@ -823,11 +1008,11 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule }); } - async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { + async $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) { await this._askHiddenFileConfiguration(opt); return true; } - async _askHiddenFileConfiguration(opt: { enableFetch?: boolean, enableOverwrite?: boolean }) { + async _askHiddenFileConfiguration(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) { const messageFetch = `${opt.enableFetch ? `> - Fetch: Use the files stored from other devices. Choose this option if you have already configured hidden file synchronization on those devices and wish to accept their files.\n` : ""}`; const messageOverwrite = `${opt.enableOverwrite ? `> - Overwrite: Use the files from this device. Select this option if you want to overwrite the files stored on other devices.\n` : ""}`; const messageMerge = `> - Merge: Merge the files from this device with those on other devices. Choose this option if you wish to combine files from multiple sources. @@ -840,7 +1025,7 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule ${messageFetch}${messageOverwrite}${messageMerge} > [!IMPORTANT] -> Please keep in mind that enabling this feature alongside customisation sync may override certain behaviors.` +> Please keep in mind that enabling this feature alongside customisation sync may override certain behaviors.`; const CHOICE_FETCH = "Fetch"; const CHOICE_OVERWRITE = "Overwrite"; const CHOICE_MERGE = "Merge"; @@ -855,7 +1040,13 @@ ${messageFetch}${messageOverwrite}${messageMerge} choices.push(CHOICE_MERGE); choices.push(CHOICE_DISABLE); - const ret = await this.plugin.confirm.confirmWithMessage("Hidden file sync", message, choices, CHOICE_DISABLE, 40); + const ret = await this.plugin.confirm.confirmWithMessage( + "Hidden file sync", + message, + choices, + CHOICE_DISABLE, + 40 + ); if (ret == CHOICE_FETCH) { await this.configureHiddenFileSync("FETCH"); } else if (ret == CHOICE_OVERWRITE) { @@ -869,7 +1060,10 @@ ${messageFetch}${messageOverwrite}${messageMerge} $allSuspendExtraSync(): Promise { if (this.plugin.settings.syncInternalFiles) { - this._log("Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + this._log( + "Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", + LOG_LEVEL_NOTICE + ); this.plugin.settings.syncInternalFiles = false; } return Promise.resolve(true); @@ -880,7 +1074,13 @@ ${messageFetch}${messageOverwrite}${messageMerge} } async configureHiddenFileSync(mode: "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN") { - if (mode != "FETCH" && mode != "OVERWRITE" && mode != "MERGE" && mode != "DISABLE" && mode != "DISABLE_HIDDEN") { + if ( + mode != "FETCH" && + mode != "OVERWRITE" && + mode != "MERGE" && + mode != "DISABLE" && + mode != "DISABLE_HIDDEN" + ) { return; } @@ -902,49 +1102,57 @@ ${messageFetch}${messageOverwrite}${messageMerge} this.plugin.settings.syncInternalFiles = true; await this.plugin.saveSettings(); this._log(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE); - } async scanInternalFiles(): Promise { const configDir = normalizePath(this.app.vault.configDir); const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); - const synchronisedInConfigSync = !this.settings.usePluginSync ? [] : Object.values(this.settings.pluginSyncExtendedSetting).filter(e => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED).map(e => e.files).flat().map(e => `${configDir}/${e}`.toLowerCase()); + .split(",") + .filter((e) => e) + .map((e) => new RegExp(e, "i")); + const synchronisedInConfigSync = !this.settings.usePluginSync + ? [] + : Object.values(this.settings.pluginSyncExtendedSetting) + .filter((e) => e.mode == MODE_SELECTIVE || e.mode == MODE_PAUSED) + .map((e) => e.files) + .flat() + .map((e) => `${configDir}/${e}`.toLowerCase()); const root = this.app.vault.getRoot(); const findRoot = root.path; - const filenames = (await this.getFiles(findRoot, [], undefined, ignoreFilter)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash")); - const files = filenames.filter(path => synchronisedInConfigSync.every(filterFile => !path.toLowerCase().startsWith(filterFile))).map(async (e) => { - return { - path: e as FilePath, - stat: await this.plugin.storageAccess.statHidden(e) // this.plugin.vaultAccess.adapterStat(e) - }; - }); + const filenames = (await this.getFiles(findRoot, [], undefined, ignoreFilter)) + .filter((e) => e.startsWith(".")) + .filter((e) => !e.startsWith(".trash")); + const files = filenames + .filter((path) => + synchronisedInConfigSync.every((filterFile) => !path.toLowerCase().startsWith(filterFile)) + ) + .map(async (e) => { + return { + path: e as FilePath, + stat: await this.plugin.storageAccess.statHidden(e), // this.plugin.vaultAccess.adapterStat(e) + }; + }); const result: InternalFileInfo[] = []; for (const f of files) { const w = await f; if (await this.plugin.$$isIgnoredByIgnoreFiles(w.path)) { - continue + continue; } - const mtime = w.stat?.mtime ?? 0 + const mtime = w.stat?.mtime ?? 0; const ctime = w.stat?.ctime ?? mtime; const size = w.stat?.size ?? 0; result.push({ ...w, - mtime, ctime, size + mtime, + ctime, + size, }); } return result; } - - - async getFiles( - path: string, - ignoreList: string[], - filter?: RegExp[], - ignoreFilter?: RegExp[] - ) { + async getFiles(path: string, ignoreList: string[], filter?: RegExp[], ignoreFilter?: RegExp[]) { let w: ListedFiles; try { w = await this.app.vault.adapter.list(path); @@ -957,11 +1165,11 @@ ${messageFetch}${messageOverwrite}${messageMerge} ...w.files .filter((e) => !ignoreList.some((ee) => e.endsWith(ee))) .filter((e) => !filter || filter.some((ee) => e.match(ee))) - .filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))) + .filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))), ]; let files = [] as string[]; for (const file of filesSrc) { - if (!await this.plugin.$$isIgnoredByIgnoreFiles(file)) { + if (!(await this.plugin.$$isIgnoredByIgnoreFiles(file))) { files.push(file); } } @@ -972,7 +1180,7 @@ ${messageFetch}${messageOverwrite}${messageMerge} continue L1; } } - if (ignoreFilter && ignoreFilter.some(e => v.match(e))) { + if (ignoreFilter && ignoreFilter.some((e) => v.match(e))) { continue L1; } if (await this.plugin.$$isIgnoredByIgnoreFiles(v)) { diff --git a/src/features/LiveSyncCommands.ts b/src/features/LiveSyncCommands.ts index ea5bb94..50ae617 100644 --- a/src/features/LiveSyncCommands.ts +++ b/src/features/LiveSyncCommands.ts @@ -1,9 +1,17 @@ import { Logger } from "octagonal-wheels/common/logger"; import { getPath } from "../common/utils.ts"; -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type LOG_LEVEL } from "../lib/src/common/types.ts"; +import { + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + type AnyEntry, + type DocumentID, + type EntryHasPath, + type FilePath, + type FilePathWithPrefix, + type LOG_LEVEL, +} from "../lib/src/common/types.ts"; import type ObsidianLiveSyncPlugin from "../main.ts"; - export abstract class LiveSyncCommands { plugin: ObsidianLiveSyncPlugin; get app() { @@ -49,5 +57,4 @@ export abstract class LiveSyncCommands { // console.log(msg); Logger(msg, level, key); }; - } diff --git a/src/main.ts b/src/main.ts index b4817dc..e3b934c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,31 @@ import { Plugin } from "./deps"; -import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type LOG_LEVEL, type diff_result, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, type HasSettings, type MetaEntry, type UXFileInfoStub, type MISSING_OR_ERROR, type AUTO_MERGED, type RemoteDBSettings, type TweakValues, } from "./lib/src/common/types.ts"; +import { + type EntryDoc, + type LoadedEntry, + type ObsidianLiveSyncSettings, + type LOG_LEVEL, + type diff_result, + type DatabaseConnectingStatus, + type EntryHasPath, + type DocumentID, + type FilePathWithPrefix, + type FilePath, + LOG_LEVEL_INFO, + type HasSettings, + type MetaEntry, + type UXFileInfoStub, + type MISSING_OR_ERROR, + type AUTO_MERGED, + type RemoteDBSettings, + type TweakValues, +} from "./lib/src/common/types.ts"; import { type FileEventItem } from "./common/types.ts"; import { type SimpleStore } from "./lib/src/common/utils.ts"; import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts"; -import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator.js"; +import { + LiveSyncAbstractReplicator, + type LiveSyncReplicatorEnv, +} from "./lib/src/replication/LiveSyncAbstractReplicator.js"; import { type KeyValueDatabase } from "./common/KeyValueDB.ts"; import { LiveSyncCommands } from "./features/LiveSyncCommands.ts"; import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts"; @@ -60,7 +82,6 @@ import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts"; import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts"; import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts"; - function throwShouldBeOverridden(): never { throw new Error("This function should be overridden by the module."); } @@ -78,11 +99,15 @@ const InterceptiveAny = Promise.resolve(undefined); * All of above performed on injectModules function. */ -export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv, LiveSyncJournalReplicatorEnv, LiveSyncCouchDBReplicatorEnv, HasSettings { - - - - +export default class ObsidianLiveSyncPlugin + extends Plugin + implements + LiveSyncLocalDBEnv, + LiveSyncReplicatorEnv, + LiveSyncJournalReplicatorEnv, + LiveSyncCouchDBReplicatorEnv, + HasSettings +{ // --> Module System getAddOn(cls: string) { for (const addon of this.addOns) { @@ -134,7 +159,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo // Common modules // Note: Platform-dependent functions are not entirely dependent on the core only, as they are from platform-dependent modules. Stubbing is sometimes required. new ModuleCheckRemoteSize(this), - // Test and Dev Modules + // Test and Dev Modules new ModuleDev(this, this), new ModuleReplicateTest(this, this), new ModuleIntegratedTest(this, this), @@ -142,25 +167,43 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo injected = injectModules(this, [...this.modules, ...this.addOns] as ICoreModule[]); // <-- Module System - $$isSuspended(): boolean { throwShouldBeOverridden(); } + $$isSuspended(): boolean { + throwShouldBeOverridden(); + } - $$setSuspended(value: boolean): void { throwShouldBeOverridden(); } + $$setSuspended(value: boolean): void { + throwShouldBeOverridden(); + } - $$isDatabaseReady(): boolean { throwShouldBeOverridden(); } + $$isDatabaseReady(): boolean { + throwShouldBeOverridden(); + } - $$getDeviceAndVaultName(): string { throwShouldBeOverridden(); } - $$setDeviceAndVaultName(name: string): void { throwShouldBeOverridden(); } + $$getDeviceAndVaultName(): string { + throwShouldBeOverridden(); + } + $$setDeviceAndVaultName(name: string): void { + throwShouldBeOverridden(); + } - $$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void { throwShouldBeOverridden() } - $$isReady(): boolean { throwShouldBeOverridden(); } - $$markIsReady(): void { throwShouldBeOverridden(); } - $$resetIsReady(): void { throwShouldBeOverridden(); } + $$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void { + throwShouldBeOverridden(); + } + $$isReady(): boolean { + throwShouldBeOverridden(); + } + $$markIsReady(): void { + throwShouldBeOverridden(); + } + $$resetIsReady(): void { + throwShouldBeOverridden(); + } // Following are plugged by the modules. settings!: ObsidianLiveSyncSettings; localDatabase!: LiveSyncLocalDB; - simpleStore!: SimpleStore + simpleStore!: SimpleStore; replicator!: LiveSyncAbstractReplicator; confirm!: Confirm; storageAccess!: StorageAccess; @@ -169,24 +212,36 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo rebuilder!: Rebuilder; kvDB!: KeyValueDatabase; - getDatabase(): PouchDB.Database { return this.localDatabase.localDatabase; } - getSettings(): ObsidianLiveSyncSettings { return this.settings; } + getDatabase(): PouchDB.Database { + return this.localDatabase.localDatabase; + } + getSettings(): ObsidianLiveSyncSettings { + return this.settings; + } + $$markFileListPossiblyChanged(): void { + throwShouldBeOverridden(); + } + $$customFetchHandler(): ObsHttpHandler { + throwShouldBeOverridden(); + } - $$markFileListPossiblyChanged(): void { throwShouldBeOverridden(); } + $$getLastPostFailedBySize(): boolean { + throwShouldBeOverridden(); + } - $$customFetchHandler(): ObsHttpHandler { throwShouldBeOverridden(); } + $$isStorageInsensitive(): boolean { + throwShouldBeOverridden(); + } - $$getLastPostFailedBySize(): boolean { throwShouldBeOverridden(); } + $$shouldCheckCaseInsensitive(): boolean { + throwShouldBeOverridden(); + } - - - $$isStorageInsensitive(): boolean { throwShouldBeOverridden() } - - $$shouldCheckCaseInsensitive(): boolean { throwShouldBeOverridden(); } - - $$isUnloaded(): boolean { throwShouldBeOverridden(); } + $$isUnloaded(): boolean { + throwShouldBeOverridden(); + } requestCount = reactiveSource(0); responseCount = reactiveSource(0); @@ -202,7 +257,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo _totalProcessingCount?: ReactiveValue; - replicationStat = reactiveSource({ sent: 0, arrived: 0, @@ -210,61 +264,102 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo maxPushSeq: 0, lastSyncPullSeq: 0, lastSyncPushSeq: 0, - syncStatus: "CLOSED" as DatabaseConnectingStatus + syncStatus: "CLOSED" as DatabaseConnectingStatus, }); - $$isReloadingScheduled(): boolean { throwShouldBeOverridden(); } - $$getReplicator(): LiveSyncAbstractReplicator { throwShouldBeOverridden(); } - - $$connectRemoteCouchDB(uri: string, auth: { - username: string; - password: string - }, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean): Promise; - info: PouchDB.Core.DatabaseInfo - }> { + $$isReloadingScheduled(): boolean { + throwShouldBeOverridden(); + } + $$getReplicator(): LiveSyncAbstractReplicator { throwShouldBeOverridden(); } + $$connectRemoteCouchDB( + uri: string, + auth: { + username: string; + password: string; + }, + disableRequestURI: boolean, + passphrase: string | false, + useDynamicIterationCount: boolean, + performSetup: boolean, + skipInfo: boolean, + compression: boolean + ): Promise< + | string + | { + db: PouchDB.Database; + info: PouchDB.Core.DatabaseInfo; + } + > { + throwShouldBeOverridden(); + } - $$isMobile(): boolean { throwShouldBeOverridden(); } - $$vaultName(): string { throwShouldBeOverridden(); } + $$isMobile(): boolean { + throwShouldBeOverridden(); + } + $$vaultName(): string { + throwShouldBeOverridden(); + } // --> Path - $$getActiveFilePath(): FilePathWithPrefix | undefined { throwShouldBeOverridden(); } + $$getActiveFilePath(): FilePathWithPrefix | undefined { + throwShouldBeOverridden(); + } // <-- Path // --> Path conversion - $$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix { throwShouldBeOverridden(); } + $$id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix { + throwShouldBeOverridden(); + } - $$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise { throwShouldBeOverridden(); } + $$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise { + throwShouldBeOverridden(); + } // Database - $$createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database { throwShouldBeOverridden(); } + $$createPouchDBInstance( + name?: string, + options?: PouchDB.Configuration.DatabaseConfiguration + ): PouchDB.Database { + throwShouldBeOverridden(); + } - $allOnDBUnload(db: LiveSyncLocalDB): void { return; } - $allOnDBClose(db: LiveSyncLocalDB): void { return; } + $allOnDBUnload(db: LiveSyncLocalDB): void { + return; + } + $allOnDBClose(db: LiveSyncLocalDB): void { + return; + } // Events @@ -306,64 +401,114 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo */ - - $everyOnLayoutReady(): Promise { return InterceptiveEvery } - $everyOnFirstInitialize(): Promise { return InterceptiveEvery } + $everyOnLayoutReady(): Promise { + return InterceptiveEvery; + } + $everyOnFirstInitialize(): Promise { + return InterceptiveEvery; + } // Some Module should call this function to start the plugin. - $$onLiveSyncReady(): Promise { throwShouldBeOverridden(); } - $$wireUpEvents(): void { throwShouldBeOverridden(); } - $$onLiveSyncLoad(): Promise { throwShouldBeOverridden(); } + $$onLiveSyncReady(): Promise { + throwShouldBeOverridden(); + } + $$wireUpEvents(): void { + throwShouldBeOverridden(); + } + $$onLiveSyncLoad(): Promise { + throwShouldBeOverridden(); + } - $$onLiveSyncUnload(): Promise { throwShouldBeOverridden(); } + $$onLiveSyncUnload(): Promise { + throwShouldBeOverridden(); + } $allScanStat(): Promise { return InterceptiveAll; } - $everyOnloadStart(): Promise { return InterceptiveEvery; } + $everyOnloadStart(): Promise { + return InterceptiveEvery; + } - $everyOnloadAfterLoadSettings(): Promise { return InterceptiveEvery; } + $everyOnloadAfterLoadSettings(): Promise { + return InterceptiveEvery; + } - $everyOnload(): Promise { return InterceptiveEvery; } + $everyOnload(): Promise { + return InterceptiveEvery; + } - $anyHandlerProcessesFileEvent(item: FileEventItem): Promise { return InterceptiveAny; } + $anyHandlerProcessesFileEvent(item: FileEventItem): Promise { + return InterceptiveAny; + } + $allStartOnUnload(): Promise { + return InterceptiveAll; + } + $allOnUnload(): Promise { + return InterceptiveAll; + } - $allStartOnUnload(): Promise { return InterceptiveAll } - $allOnUnload(): Promise { return InterceptiveAll; } + $$openDatabase(): Promise { + throwShouldBeOverridden(); + } + $$realizeSettingSyncMode(): Promise { + throwShouldBeOverridden(); + } + $$performRestart() { + throwShouldBeOverridden(); + } - $$openDatabase(): Promise { throwShouldBeOverridden() } + $$clearUsedPassphrase(): void { + throwShouldBeOverridden(); + } + $$loadSettings(): Promise { + throwShouldBeOverridden(); + } - $$realizeSettingSyncMode(): Promise { throwShouldBeOverridden(); } - $$performRestart() { throwShouldBeOverridden(); } + $$saveDeviceAndVaultName(): void { + throwShouldBeOverridden(); + } - $$clearUsedPassphrase(): void { throwShouldBeOverridden() } - $$loadSettings(): Promise { throwShouldBeOverridden() } + $$saveSettingData(): Promise { + throwShouldBeOverridden(); + } - $$saveDeviceAndVaultName(): void { throwShouldBeOverridden(); } + $anyProcessOptionalFileEvent(path: FilePath): Promise { + return InterceptiveAny; + } - $$saveSettingData(): Promise { throwShouldBeOverridden() } + $everyCommitPendingFileEvent(): Promise { + return InterceptiveEvery; + } - $anyProcessOptionalFileEvent(path: FilePath): Promise { return InterceptiveAny; } + // -> + $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise { + return InterceptiveAny; + } - $everyCommitPendingFileEvent(): Promise { return InterceptiveEvery } + $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise { + throwShouldBeOverridden(); + } - // -> - $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise { return InterceptiveAny; } + $$queueConflictCheck(file: FilePathWithPrefix): Promise { + throwShouldBeOverridden(); + } - $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise { throwShouldBeOverridden() } - - $$queueConflictCheck(file: FilePathWithPrefix): Promise { throwShouldBeOverridden() } - - $$waitForAllConflictProcessed(): Promise { throwShouldBeOverridden() } + $$waitForAllConflictProcessed(): Promise { + throwShouldBeOverridden(); + } //<-- Conflict Check - $anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise { return InterceptiveAny; } - - $anyProcessReplicatedDoc(doc: MetaEntry): Promise { return InterceptiveAny; } + $anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise { + return InterceptiveAny; + } + $anyProcessReplicatedDoc(doc: MetaEntry): Promise { + return InterceptiveAny; + } //---> Sync $$parseReplicationResult(docs: Array>): void { @@ -393,93 +538,169 @@ export default class ObsidianLiveSyncPlugin extends Plugin implements LiveSyncLo return InterceptiveEvery; } + $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { + throwShouldBeOverridden(); + } + $$checkAndAskUseRemoteConfiguration( + settings: RemoteDBSettings + ): Promise<{ result: false | TweakValues; requireFetch: boolean }> { + throwShouldBeOverridden(); + } - $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { throwShouldBeOverridden(); } + $everyBeforeReplicate(showMessage: boolean): Promise { + return InterceptiveEvery; + } + $$replicate(showMessage: boolean = false): Promise { + throwShouldBeOverridden(); + } - $$checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{ result: false | TweakValues, requireFetch: boolean }> { throwShouldBeOverridden(); } + $everyOnDatabaseInitialized(showingNotice: boolean): Promise { + throwShouldBeOverridden(); + } - $everyBeforeReplicate(showMessage: boolean): Promise { return InterceptiveEvery; } - $$replicate(showMessage: boolean = false): Promise { throwShouldBeOverridden() } + $$initializeDatabase(showingNotice: boolean = false, reopenDatabase = true): Promise { + throwShouldBeOverridden(); + } - $everyOnDatabaseInitialized(showingNotice: boolean): Promise { throwShouldBeOverridden() } + $anyAfterConnectCheckFailed(): Promise { + return InterceptiveAny; + } - $$initializeDatabase(showingNotice: boolean = false, reopenDatabase = true): Promise { throwShouldBeOverridden() } - - $anyAfterConnectCheckFailed(): Promise { return InterceptiveAny; } - - $$replicateAllToServer(showingNotice: boolean = false, sendChunksInBulkDisabled: boolean = false): Promise { throwShouldBeOverridden() } - $$replicateAllFromServer(showingNotice: boolean = false): Promise { throwShouldBeOverridden() } + $$replicateAllToServer( + showingNotice: boolean = false, + sendChunksInBulkDisabled: boolean = false + ): Promise { + throwShouldBeOverridden(); + } + $$replicateAllFromServer(showingNotice: boolean = false): Promise { + throwShouldBeOverridden(); + } // Remote Governing - $$markRemoteLocked(lockByClean: boolean = false): Promise { throwShouldBeOverridden() } + $$markRemoteLocked(lockByClean: boolean = false): Promise { + throwShouldBeOverridden(); + } - $$markRemoteUnlocked(): Promise { throwShouldBeOverridden() } + $$markRemoteUnlocked(): Promise { + throwShouldBeOverridden(); + } - $$markRemoteResolved(): Promise { throwShouldBeOverridden() } + $$markRemoteResolved(): Promise { + throwShouldBeOverridden(); + } // <-- Remote Governing - - $$isFileSizeExceeded(size: number): boolean { throwShouldBeOverridden() } - - $$performFullScan(showingNotice?: boolean): Promise { throwShouldBeOverridden(); } - - $anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise { return InterceptiveAny; } - $$resolveConflictByDeletingRev(path: FilePathWithPrefix, deleteRevision: string, subTitle = ""): Promise { throwShouldBeOverridden(); } - $$resolveConflict(filename: FilePathWithPrefix): Promise { throwShouldBeOverridden(); } - $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise { throwShouldBeOverridden(); } - - $$waitForReplicationOnce(): Promise { throwShouldBeOverridden(); } - - $$resetLocalDatabase(): Promise { throwShouldBeOverridden(); } - - $$tryResetRemoteDatabase(): Promise { throwShouldBeOverridden(); } - - $$tryCreateRemoteDatabase(): Promise { throwShouldBeOverridden(); } - - $$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise { throwShouldBeOverridden(); } - - $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise { throwShouldBeOverridden(); } - - - - $$askReload(message?: string) { throwShouldBeOverridden(); } - $$scheduleAppReload() { throwShouldBeOverridden(); } - - //--- Setup - $allSuspendAllSync(): Promise { return InterceptiveAll; } - $allSuspendExtraSync(): Promise { return InterceptiveAll; } - - $allAskUsingOptionalSyncFeature(opt: { - enableFetch?: boolean, - enableOverwrite?: boolean - }): Promise { + $$isFileSizeExceeded(size: number): boolean { throwShouldBeOverridden(); } - $anyConfigureOptionalSyncFeature(mode: string): Promise { throwShouldBeOverridden(); } - $$showView(viewType: string): Promise { throwShouldBeOverridden(); } + $$performFullScan(showingNotice?: boolean): Promise { + throwShouldBeOverridden(); + } + $anyResolveConflictByUI( + filename: FilePathWithPrefix, + conflictCheckResult: diff_result + ): Promise { + return InterceptiveAny; + } + $$resolveConflictByDeletingRev( + path: FilePathWithPrefix, + deleteRevision: string, + subTitle = "" + ): Promise { + throwShouldBeOverridden(); + } + $$resolveConflict(filename: FilePathWithPrefix): Promise { + throwShouldBeOverridden(); + } + $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise { + throwShouldBeOverridden(); + } + + $$waitForReplicationOnce(): Promise { + throwShouldBeOverridden(); + } + + $$resetLocalDatabase(): Promise { + throwShouldBeOverridden(); + } + + $$tryResetRemoteDatabase(): Promise { + throwShouldBeOverridden(); + } + + $$tryCreateRemoteDatabase(): Promise { + throwShouldBeOverridden(); + } + + $$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise { + throwShouldBeOverridden(); + } + + $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise { + throwShouldBeOverridden(); + } + + $$askReload(message?: string) { + throwShouldBeOverridden(); + } + $$scheduleAppReload() { + throwShouldBeOverridden(); + } + + //--- Setup + $allSuspendAllSync(): Promise { + return InterceptiveAll; + } + $allSuspendExtraSync(): Promise { + return InterceptiveAll; + } + + $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise { + throwShouldBeOverridden(); + } + $anyConfigureOptionalSyncFeature(mode: string): Promise { + throwShouldBeOverridden(); + } + + $$showView(viewType: string): Promise { + throwShouldBeOverridden(); + } // For Development: Ensure reliability MORE AND MORE. May the this plug-in helps all of us. - $everyModuleTest(): Promise { return InterceptiveEvery; } - $everyModuleTestMultiDevice(): Promise { return InterceptiveEvery; } - $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void { throwShouldBeOverridden(); } + $everyModuleTest(): Promise { + return InterceptiveEvery; + } + $everyModuleTestMultiDevice(): Promise { + return InterceptiveEvery; + } + $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void { + throwShouldBeOverridden(); + } - _isThisModuleEnabled(): boolean { return true; } - - - $anyGetAppId(): Promise { return InterceptiveAny; } + _isThisModuleEnabled(): boolean { + return true; + } + $anyGetAppId(): Promise { + return InterceptiveAny; + } // Plug-in's overrideable functions - onload() { void this.$$onLiveSyncLoad(); } - async saveSettings() { await this.$$saveSettingData(); } - onunload() { return void this.$$onLiveSyncUnload(); } + onload() { + void this.$$onLiveSyncLoad(); + } + async saveSettings() { + await this.$$saveSettingData(); + } + onunload() { + return void this.$$onLiveSyncUnload(); + } // <-- Plug-in's overrideable functions } - // For now, -export type LiveSyncCore = ObsidianLiveSyncPlugin; \ No newline at end of file +export type LiveSyncCore = ObsidianLiveSyncPlugin; diff --git a/src/modules/AbstractModule.ts b/src/modules/AbstractModule.ts index c757de1..0860ad5 100644 --- a/src/modules/AbstractModule.ts +++ b/src/modules/AbstractModule.ts @@ -3,8 +3,14 @@ import type { LOG_LEVEL } from "../lib/src/common/types"; import type { LiveSyncCore } from "../main"; import { unique } from "octagonal-wheels/collection"; import type { IObsidianModule } from "./AbstractObsidianModule.ts"; -import type { ICoreModuleBase, AllInjectableProps, AllExecuteProps, EveryExecuteProps, AnyExecuteProps, ICoreModule } from "./ModuleTypes"; - +import type { + ICoreModuleBase, + AllInjectableProps, + AllExecuteProps, + EveryExecuteProps, + AnyExecuteProps, + ICoreModule, +} from "./ModuleTypes"; function isOverridableKey(key: string): key is keyof ICoreModuleBase { return key.startsWith("$"); @@ -14,7 +20,6 @@ function isInjectableKey(key: string): key is keyof AllInjectableProps { return key.startsWith("$$"); } - function isAllExecuteKey(key: string): key is keyof AllExecuteProps { return key.startsWith("$all"); } @@ -35,15 +40,17 @@ function isAnyExecuteKey(key: string): key is keyof AnyExecuteProps { * All of above performed on injectModules function. */ export function injectModules(target: T, modules: ICoreModule[]) { - const allKeys = unique([...Object.keys(Object.getOwnPropertyDescriptors(target)), - ...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target)))]).filter(e => e.startsWith("$")) as (keyof ICoreModule)[]; + const allKeys = unique([ + ...Object.keys(Object.getOwnPropertyDescriptors(target)), + ...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target))), + ]).filter((e) => e.startsWith("$")) as (keyof ICoreModule)[]; const moduleMap = new Map(); for (const module of modules) { for (const key of allKeys) { if (isOverridableKey(key)) { if (key in module) { const list = moduleMap.get(key) || []; - if (typeof module[key] === 'function') { + if (typeof module[key] === "function") { module[key] = module[key].bind(module) as any; } list.push(module); @@ -74,7 +81,7 @@ export function injectModules(target: T, modules: ICoreMo } } return true; - } + }; for (const module of modules) { Logger(`[${module.constructor.name}]: Injected (All) ${key} `, LOG_LEVEL_VERBOSE); } @@ -94,7 +101,7 @@ export function injectModules(target: T, modules: ICoreMo } } return true; - } + }; for (const module of modules) { Logger(`[${module.constructor.name}]: Injected (Every) ${key} `, LOG_LEVEL_VERBOSE); } @@ -115,7 +122,7 @@ export function injectModules(target: T, modules: ICoreMo } } return false; - } + }; for (const module of modules) { Logger(`[${module.constructor.name}]: Injected (Any) ${key} `, LOG_LEVEL_VERBOSE); } @@ -127,7 +134,6 @@ export function injectModules(target: T, modules: ICoreMo return true; } - export abstract class AbstractModule { _log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => { if (typeof msg === "string" && level !== LOG_LEVEL_NOTICE) { @@ -173,11 +179,10 @@ export abstract class AbstractModule { return this.testFail(`${key} failed: ${ret}`); } this.addTestResult(key, true, ""); - } - catch (ex: any) { + } catch (ex: any) { this.addTestResult(key, false, "Failed by Exception", ex.toString()); return this.testFail(`${key} failed: ${ex}`); } return this.testDone(); } -} \ No newline at end of file +} diff --git a/src/modules/AbstractObsidianModule.ts b/src/modules/AbstractObsidianModule.ts index 76b5b77..99a4d1f 100644 --- a/src/modules/AbstractObsidianModule.ts +++ b/src/modules/AbstractObsidianModule.ts @@ -4,15 +4,12 @@ import type ObsidianLiveSyncPlugin from "../main"; import { AbstractModule } from "./AbstractModule.ts"; import type { ChainableExecuteFunction, OverridableFunctionsKeys } from "./ModuleTypes"; - export type IObsidianModuleBase = OverridableFunctionsKeys; -export type IObsidianModule = Prettify> +export type IObsidianModule = Prettify>; export type ModuleKeys = keyof IObsidianModule; export type ChainableModuleProps = ChainableExecuteFunction; - export abstract class AbstractObsidianModule extends AbstractModule { - addCommand = this.plugin.addCommand.bind(this.plugin); registerView = this.plugin.registerView.bind(this.plugin); addRibbonIcon = this.plugin.addRibbonIcon.bind(this.plugin); @@ -31,13 +28,15 @@ export abstract class AbstractObsidianModule extends AbstractModule { return this.plugin.app; } - constructor(public plugin: ObsidianLiveSyncPlugin, public core: LiveSyncCore) { + constructor( + public plugin: ObsidianLiveSyncPlugin, + public core: LiveSyncCore + ) { super(core); } saveSettings = this.plugin.saveSettings.bind(this.plugin); - _isMainReady() { return this.core.$$isReady(); } @@ -50,6 +49,6 @@ export abstract class AbstractObsidianModule extends AbstractModule { //should be overridden _isThisModuleEnabled() { - return true + return true; } -} \ No newline at end of file +} diff --git a/src/modules/ModuleTypes.ts b/src/modules/ModuleTypes.ts index 396b0ed..3151b51 100644 --- a/src/modules/ModuleTypes.ts +++ b/src/modules/ModuleTypes.ts @@ -1,38 +1,53 @@ import type { Prettify } from "../lib/src/common/types"; import type { LiveSyncCore } from "../main"; - export type OverridableFunctionsKeys = { [K in keyof T as K extends `$${string}` ? K : never]: T[K]; -} +}; export type ChainableExecuteFunction = { - [K in keyof T as K extends `$${string}` ? (T[K] extends (...args: any) => ChainableFunctionResult ? K : never) : never]: T[K]; -} + [K in keyof T as K extends `$${string}` + ? T[K] extends (...args: any) => ChainableFunctionResult + ? K + : never + : never]: T[K]; +}; export type ICoreModuleBase = OverridableFunctionsKeys; -export type ICoreModule = Prettify> +export type ICoreModule = Prettify>; export type CoreModuleKeys = keyof ICoreModule; export type ChainableFunctionResult = - Promise + | Promise | Promise | Promise | Promise; export type ChainableFunctionResultOrAll = Promise; type AllExecuteFunction = { - [K in keyof T as K extends `$all${string}` ? (T[K] extends (...args: any[]) => ChainableFunctionResultOrAll ? K : never) : never]: T[K]; -} + [K in keyof T as K extends `$all${string}` + ? T[K] extends (...args: any[]) => ChainableFunctionResultOrAll + ? K + : never + : never]: T[K]; +}; type EveryExecuteFunction = { - [K in keyof T as K extends `$every${string}` ? (T[K] extends (...args: any[]) => ChainableFunctionResult ? K : never) : never]: T[K]; -} + [K in keyof T as K extends `$every${string}` + ? T[K] extends (...args: any[]) => ChainableFunctionResult + ? K + : never + : never]: T[K]; +}; type AnyExecuteFunction = { - [K in keyof T as K extends `$any${string}` ? (T[K] extends (...args: any[]) => ChainableFunctionResult ? K : never) : never]: T[K]; -} + [K in keyof T as K extends `$any${string}` + ? T[K] extends (...args: any[]) => ChainableFunctionResult + ? K + : never + : never]: T[K]; +}; type InjectableFunction = { [K in keyof T as K extends `$$${string}` ? (T[K] extends (...args: any[]) => any ? K : never) : never]: T[K]; -} +}; export type AllExecuteProps = AllExecuteFunction; export type EveryExecuteProps = EveryExecuteFunction; export type AnyExecuteProps = AnyExecuteFunction; diff --git a/src/modules/core/ModuleDatabaseFileAccess.ts b/src/modules/core/ModuleDatabaseFileAccess.ts index 3f245fc..a6d4ba6 100644 --- a/src/modules/core/ModuleDatabaseFileAccess.ts +++ b/src/modules/core/ModuleDatabaseFileAccess.ts @@ -1,15 +1,29 @@ import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { EVENT_FILE_SAVED, eventHub } from "../../common/events"; import { getPathFromUXFileInfo, isInternalMetadata, markChangesAreSame } from "../../common/utils"; -import type { UXFileInfoStub, FilePathWithPrefix, UXFileInfo, MetaEntry, LoadedEntry, FilePath, SavingEntry } from "../../lib/src/common/types"; +import type { + UXFileInfoStub, + FilePathWithPrefix, + UXFileInfo, + MetaEntry, + LoadedEntry, + FilePath, + SavingEntry, +} from "../../lib/src/common/types"; import type { DatabaseFileAccess } from "../interfaces/DatabaseFileAccess"; import { type IObsidianModule } from "../AbstractObsidianModule.ts"; import { isPlainText, shouldBeIgnored } from "../../lib/src/string_and_binary/path"; -import { createBlob, createTextBlob, delay, determineTypeFromBlob, isDocContentSame, readContent } from "../../lib/src/common/utils"; +import { + createBlob, + createTextBlob, + delay, + determineTypeFromBlob, + isDocContentSame, + readContent, +} from "../../lib/src/common/utils"; import { serialized } from "octagonal-wheels/concurrency/lock"; import { AbstractModule } from "../AbstractModule.ts"; - export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidianModule, DatabaseFileAccess { $everyOnload(): Promise { this.core.databaseFileAccess = this; @@ -18,7 +32,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia async $everyModuleTest(): Promise { if (!this.settings.enableDebugTools) return Promise.resolve(true); - const testString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc" + const testString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec nunc"; // Before test, we need to delete completely. const conflicts = await this.getConflictedRevs("autoTest.md" as FilePathWithPrefix); for (const rev of conflicts) { @@ -27,16 +41,21 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia await this.delete("autoTest.md" as FilePathWithPrefix); // OK, begin! - await this._test("storeContent", async () => (await this.storeContent("autoTest.md" as FilePathWithPrefix, testString))); + await this._test( + "storeContent", + async () => await this.storeContent("autoTest.md" as FilePathWithPrefix, testString) + ); // For test, we need to clear the caches. await this.localDatabase.hashCaches.clear(); await this._test("readContent", async () => { const content = await this.fetch("autoTest.md" as FilePathWithPrefix); if (!content) return "File not found"; if (content.deleted) return "File is deleted"; - return (await content.body.text() == testString) ? true : `Content is not same ${await content.body.text()}`; + return (await content.body.text()) == testString + ? true + : `Content is not same ${await content.body.text()}`; }); - await this._test("delete", async () => (await this.delete("autoTest.md" as FilePathWithPrefix))); + await this._test("delete", async () => await this.delete("autoTest.md" as FilePathWithPrefix)); await this._test("read deleted content", async () => { const content = await this.fetch("autoTest.md" as FilePathWithPrefix); if (!content) return true; @@ -49,7 +68,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia async checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise { const path = getPathFromUXFileInfo(file); - if (!await this.core.$$isTargetFile(path)) { + if (!(await this.core.$$isTargetFile(path))) { this._log(`File is not target`, LOG_LEVEL_VERBOSE); return false; } @@ -61,7 +80,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia } async delete(file: UXFileInfoStub | FilePathWithPrefix, rev?: string): Promise { - if (!await this.checkIsTargetFile(file)) { + if (!(await this.checkIsTargetFile(file))) { return true; } const fullPath = getPathFromUXFileInfo(file); @@ -95,13 +114,18 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia type: "file", }, body: blob, - } + }; return await this._store(dummyUXFileInfo, true, false, false); } - async _store(file: UXFileInfo, force: boolean = false, skipCheck?: boolean, onlyChunks?: boolean): Promise { + async _store( + file: UXFileInfo, + force: boolean = false, + skipCheck?: boolean, + onlyChunks?: boolean + ): Promise { if (!skipCheck) { - if (!await this.checkIsTargetFile(file)) { + if (!(await this.checkIsTargetFile(file))) { return true; } } @@ -147,17 +171,33 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia const oldData = { data: old.data, deleted: old._deleted || old.deleted }; const newData = { data: d.data, deleted: d._deleted || d.deleted }; if (oldData.deleted != newData.deleted) return false; - if (!await isDocContentSame(old.data, newData.data)) return false; - this._log(msg + "Skipped (not changed) " + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE); + if (!(await isDocContentSame(old.data, newData.data))) return false; + this._log( + msg + "Skipped (not changed) " + fullPath + (d._deleted || d.deleted ? " (deleted)" : ""), + LOG_LEVEL_VERBOSE + ); markChangesAreSame(old, d.mtime, old.mtime); return true; // d._rev = old._rev; } } catch (ex) { if (force) { - this._log(msg + "Error, Could not check the diff for the old one." + (force ? "force writing." : "") + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE); + this._log( + msg + + "Error, Could not check the diff for the old one." + + (force ? "force writing." : "") + + fullPath + + (d._deleted || d.deleted ? " (deleted)" : ""), + LOG_LEVEL_VERBOSE + ); } else { - this._log(msg + "Error, Could not check the diff for the old one." + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE); + this._log( + msg + + "Error, Could not check the diff for the old one." + + fullPath + + (d._deleted || d.deleted ? " (deleted)" : ""), + LOG_LEVEL_VERBOSE + ); } this._log(ex, LOG_LEVEL_VERBOSE); return !force; @@ -177,7 +217,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia } async getConflictedRevs(file: UXFileInfoStub | FilePathWithPrefix): Promise { - if (!await this.checkIsTargetFile(file)) { + if (!(await this.checkIsTargetFile(file))) { return []; } const filename = getPathFromUXFileInfo(file); @@ -188,9 +228,13 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia return doc._conflicts || []; } - async fetch(file: UXFileInfoStub | FilePathWithPrefix, - rev?: string, waitForReady?: boolean, skipCheck = false): Promise { - if (skipCheck && !await this.checkIsTargetFile(file)) { + async fetch( + file: UXFileInfoStub | FilePathWithPrefix, + rev?: string, + waitForReady?: boolean, + skipCheck = false + ): Promise { + if (skipCheck && !(await this.checkIsTargetFile(file))) { return false; } @@ -210,39 +254,55 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia }, body: data, deleted: entry.deleted || entry._deleted, - } + }; if (isInternalMetadata(entry.path)) { fileInfo.isInternal = true; } return fileInfo; } - async fetchEntryMeta(file: UXFileInfoStub | FilePathWithPrefix, - rev?: string, skipCheck = false): Promise { + async fetchEntryMeta( + file: UXFileInfoStub | FilePathWithPrefix, + rev?: string, + skipCheck = false + ): Promise { const filename = getPathFromUXFileInfo(file); - if (skipCheck && !await this.checkIsTargetFile(file)) { + if (skipCheck && !(await this.checkIsTargetFile(file))) { return false; } - const doc = await this.localDatabase.getDBEntryMeta( - filename, rev ? { rev: rev } : undefined, true); + const doc = await this.localDatabase.getDBEntryMeta(filename, rev ? { rev: rev } : undefined, true); if (doc === false) { return false; } return doc as MetaEntry; } - async fetchEntryFromMeta(meta: MetaEntry, waitForReady: boolean = true, skipCheck = false): Promise { - if (skipCheck && !await this.checkIsTargetFile(meta.path)) { + async fetchEntryFromMeta( + meta: MetaEntry, + waitForReady: boolean = true, + skipCheck = false + ): Promise { + if (skipCheck && !(await this.checkIsTargetFile(meta.path))) { return false; } - const doc = await this.localDatabase.getDBEntryFromMeta(meta as LoadedEntry, undefined, false, waitForReady, true); + const doc = await this.localDatabase.getDBEntryFromMeta( + meta as LoadedEntry, + undefined, + false, + waitForReady, + true + ); if (doc === false) { return false; } return doc; } - async fetchEntry(file: UXFileInfoStub | FilePathWithPrefix, - rev?: string, waitForReady: boolean = true, skipCheck = false): Promise { - if (skipCheck && !await this.checkIsTargetFile(file)) { + async fetchEntry( + file: UXFileInfoStub | FilePathWithPrefix, + rev?: string, + waitForReady: boolean = true, + skipCheck = false + ): Promise { + if (skipCheck && !(await this.checkIsTargetFile(file))) { return false; } const entry = await this.fetchEntryMeta(file, rev, true); @@ -253,7 +313,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia return doc; } async deleteFromDBbyPath(fullPath: FilePath | FilePathWithPrefix, rev?: string): Promise { - if (!await this.checkIsTargetFile(fullPath)) { + if (!(await this.checkIsTargetFile(fullPath))) { this._log(`storeFromStorage: File is not target: ${fullPath}`); return true; } @@ -262,5 +322,4 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements IObsidia eventHub.emitEvent(EVENT_FILE_SAVED); return ret; } - -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleFileHandler.ts b/src/modules/core/ModuleFileHandler.ts index d8a924a..25fba94 100644 --- a/src/modules/core/ModuleFileHandler.ts +++ b/src/modules/core/ModuleFileHandler.ts @@ -1,16 +1,29 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { serialized } from "octagonal-wheels/concurrency/lock"; import type { FileEventItem } from "../../common/types"; -import type { FilePath, FilePathWithPrefix, MetaEntry, UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "../../lib/src/common/types"; +import type { + FilePath, + FilePathWithPrefix, + MetaEntry, + UXFileInfo, + UXFileInfoStub, + UXInternalFileInfoStub, +} from "../../lib/src/common/types"; import { AbstractModule } from "../AbstractModule.ts"; -import { compareFileFreshness, EVEN, getPath, getPathFromUXFileInfo, getPathWithoutPrefix, markChangesAreSame } from "../../common/utils"; +import { + compareFileFreshness, + EVEN, + getPath, + getPathFromUXFileInfo, + getPathWithoutPrefix, + markChangesAreSame, +} from "../../common/utils"; import { getDocDataAsArray, isDocContentSame, readContent } from "../../lib/src/common/utils"; import { shouldBeIgnored } from "../../lib/src/string_and_binary/path"; import type { ICoreModule } from "../ModuleTypes"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; export class ModuleFileHandler extends AbstractModule implements ICoreModule { - get db() { return this.core.databaseFileAccess; } @@ -31,10 +44,14 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { if (!readFile) { throw new Error(`File ${file.path} is not exist on the storage`); } - return readFile + return readFile; } - async storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force: boolean = false, onlyChunks: boolean = false): Promise { + async storeFileToDB( + info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, + force: boolean = false, + onlyChunks: boolean = false + ): Promise { const file = typeof info === "string" ? this.storage.getFileStub(info) : info; if (file == null) { this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE); @@ -42,10 +59,13 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { } // const file = item.args.file; if (file.isInternal) { - this._log(`Internal file ${file.path} is not allowed to be processed on processFileEvent`, LOG_LEVEL_VERBOSE); + this._log( + `Internal file ${file.path} is not allowed to be processed on processFileEvent`, + LOG_LEVEL_VERBOSE + ); return false; } - // First, check the file on the database + // First, check the file on the database const entry = await this.db.fetchEntry(file, undefined, true, true); if (!entry || entry.deleted || entry._deleted) { @@ -114,10 +134,13 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { } // const file = item.args.file; if (file.isInternal) { - this._log(`Internal file ${file.path} is not allowed to be processed on processFileEvent`, LOG_LEVEL_VERBOSE); + this._log( + `Internal file ${file.path} is not allowed to be processed on processFileEvent`, + LOG_LEVEL_VERBOSE + ); return false; } - // First, check the file on the database + // First, check the file on the database const entry = await this.db.fetchEntry(file, undefined, true, true); if (!entry || entry.deleted || entry._deleted) { this._log(`File ${file.path} is not exist or already deleted on the database`, LOG_LEVEL_VERBOSE); @@ -135,24 +158,39 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { return await this.db.delete(file); } - async deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise { + async deleteRevisionFromDB( + info: UXFileInfoStub | FilePath | FilePathWithPrefix, + rev: string + ): Promise { //TODO: Possibly check the conflicting. return await this.db.delete(info, rev); } - - async resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise { - if (!await this.deleteRevisionFromDB(info, rev)) { - this._log(`Failed to delete the conflicted revision ${rev} of ${getPathFromUXFileInfo(info)}`, LOG_LEVEL_VERBOSE); + async resolveConflictedByDeletingRevision( + info: UXFileInfoStub | FilePath, + rev: string + ): Promise { + if (!(await this.deleteRevisionFromDB(info, rev))) { + this._log( + `Failed to delete the conflicted revision ${rev} of ${getPathFromUXFileInfo(info)}`, + LOG_LEVEL_VERBOSE + ); return false; } - if (!await this.dbToStorageWithSpecificRev(info, rev, true)) { - this._log(`Failed to apply the resolved revision ${rev} of ${getPathFromUXFileInfo(info)} to the storage`, LOG_LEVEL_VERBOSE); + if (!(await this.dbToStorageWithSpecificRev(info, rev, true))) { + this._log( + `Failed to apply the resolved revision ${rev} of ${getPathFromUXFileInfo(info)} to the storage`, + LOG_LEVEL_VERBOSE + ); return false; } } - async dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise { + async dbToStorageWithSpecificRev( + info: UXFileInfoStub | UXFileInfo | FilePath | null, + rev: string, + force?: boolean + ): Promise { const file = typeof info === "string" ? this.storage.getFileStub(info) : info; if (file == null) { this._log(`File ${info} is not exist on the storage`, LOG_LEVEL_VERBOSE); @@ -166,12 +204,18 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { return await this.dbToStorage(docEntry, file, force); } - async dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise { + async dbToStorage( + entryInfo: MetaEntry | FilePathWithPrefix, + info: UXFileInfoStub | UXFileInfo | FilePath | null, + force?: boolean + ): Promise { const file = typeof info === "string" ? this.storage.getFileStub(info) : info; const mode = file == null ? "create" : "modify"; - const docEntry = typeof entryInfo === "string" ? - await this.db.fetchEntryMeta(entryInfo, undefined, true) : await this.db.fetchEntryMeta(entryInfo.path, undefined, true); + const docEntry = + typeof entryInfo === "string" + ? await this.db.fetchEntryMeta(entryInfo, undefined, true) + : await this.db.fetchEntryMeta(entryInfo.path, undefined, true); if (!docEntry) { this._log(`File ${entryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE); return false; @@ -255,7 +299,10 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { } // Let's apply the changes. } else { - this._log(`File ${docRead.path} ${existOnStorage ? "(new) " : ""} ${force ? " (forced)" : ""}`, LOG_LEVEL_VERBOSE); + this._log( + `File ${docRead.path} ${existOnStorage ? "(new) " : ""} ${force ? " (forced)" : ""}`, + LOG_LEVEL_VERBOSE + ); } await this.storage.ensureDir(path); const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime }); @@ -268,7 +315,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { const eventItem = item.args; const type = item.type; const path = eventItem.file.path; - if (!await this.core.$$isTargetFile(path)) { + if (!(await this.core.$$isTargetFile(path))) { this._log(`File ${path} is not the target file`, LOG_LEVEL_VERBOSE); return false; } @@ -296,7 +343,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { async $anyProcessReplicatedDoc(entry: MetaEntry): Promise { return await serialized(entry.path, async () => { - if (!await this.core.$$isTargetFile(entry.path)) { + if (!(await this.core.$$isTargetFile(entry.path))) { this._log(`File ${entry.path} is not the target file`, LOG_LEVEL_VERBOSE); return false; } @@ -312,13 +359,15 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { // Nothing to do and other modules should also nothing to do. return true; } else { - this._log(`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`, LOG_LEVEL_VERBOSE); + this._log( + `Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`, + LOG_LEVEL_VERBOSE + ); const ret = await this.dbToStorage(entry, targetFile); this._log(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`); return ret; } }); - } async createAllChunks(showingNotice?: boolean): Promise { @@ -329,11 +378,16 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { const filesStorageSrc = this.storage.getFiles(); const incProcessed = () => { processed++; - if (processed % 25 == 0) this._log(`Creating missing chunks: ${processed} of ${total} files`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "chunkCreation"); - } + if (processed % 25 == 0) + this._log( + `Creating missing chunks: ${processed} of ${total} files`, + showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, + "chunkCreation" + ); + }; const total = filesStorageSrc.length; const procAllChunks = filesStorageSrc.map(async (file) => { - if (!await this.core.$$isTargetFile(file)) { + if (!(await this.core.$$isTargetFile(file))) { incProcessed(); return true; } @@ -352,6 +406,10 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule { } }); await Promise.all(procAllChunks); - this._log(`Creating chunks Done: ${processed} of ${total} files`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "chunkCreation"); + this._log( + `Creating chunks Done: ${processed} of ${total} files`, + showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, + "chunkCreation" + ); } -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleLocalDatabaseObsidian.ts b/src/modules/core/ModuleLocalDatabaseObsidian.ts index 6cfacef..f44effa 100644 --- a/src/modules/core/ModuleLocalDatabaseObsidian.ts +++ b/src/modules/core/ModuleLocalDatabaseObsidian.ts @@ -5,7 +5,6 @@ import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICoreModule { - $everyOnloadStart(): Promise { return Promise.resolve(true); } @@ -23,5 +22,4 @@ export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICore $$isDatabaseReady(): boolean { return this.localDatabase != null && this.localDatabase.isReady; } - -} \ No newline at end of file +} diff --git a/src/modules/core/ModulePeriodicProcess.ts b/src/modules/core/ModulePeriodicProcess.ts index 6d82e8f..f8f8dd3 100644 --- a/src/modules/core/ModulePeriodicProcess.ts +++ b/src/modules/core/ModulePeriodicProcess.ts @@ -3,7 +3,6 @@ import { AbstractModule } from "../AbstractModule"; import type { ICoreModule } from "../ModuleTypes"; export class ModulePeriodicProcess extends AbstractModule implements ICoreModule { - periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.core.$$replicate()); _disablePeriodic() { @@ -11,20 +10,19 @@ export class ModulePeriodicProcess extends AbstractModule implements ICoreModule return Promise.resolve(true); } _resumePeriodic() { - this.periodicSyncProcessor.enable(this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0); + this.periodicSyncProcessor.enable( + this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0 + ); return Promise.resolve(true); } $allOnUnload() { return this._disablePeriodic(); - } $everyBeforeRealizeSetting(): Promise { return this._disablePeriodic(); - } $everyBeforeSuspendProcess(): Promise { return this._disablePeriodic(); - } $everyAfterResumeProcess(): Promise { return this._resumePeriodic(); @@ -32,4 +30,4 @@ export class ModulePeriodicProcess extends AbstractModule implements ICoreModule $everyAfterRealizeSetting(): Promise { return this._resumePeriodic(); } -} \ No newline at end of file +} diff --git a/src/modules/core/ModulePouchDB.ts b/src/modules/core/ModulePouchDB.ts index 442507d..61a8d38 100644 --- a/src/modules/core/ModulePouchDB.ts +++ b/src/modules/core/ModulePouchDB.ts @@ -3,7 +3,10 @@ import type { ICoreModule } from "../ModuleTypes"; import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser"; export class ModulePouchDB extends AbstractModule implements ICoreModule { - $$createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database { + $$createPouchDBInstance( + name?: string, + options?: PouchDB.Configuration.DatabaseConfiguration + ): PouchDB.Database { const optionPass = options ?? {}; if (this.settings.useIndexedDBAdapter) { optionPass.adapter = "indexeddb"; @@ -13,4 +16,4 @@ export class ModulePouchDB extends AbstractModule implements ICoreModule { } return new PouchDB(name, optionPass); } -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleRebuilder.ts b/src/modules/core/ModuleRebuilder.ts index c27ce3f..4d39a6c 100644 --- a/src/modules/core/ModuleRebuilder.ts +++ b/src/modules/core/ModuleRebuilder.ts @@ -1,5 +1,12 @@ import { delay } from "octagonal-wheels/promises"; -import { FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, REMOTE_COUCHDB, REMOTE_MINIO } from "../../lib/src/common/types.ts"; +import { + FLAGMD_REDFLAG2_HR, + FLAGMD_REDFLAG3_HR, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + REMOTE_COUCHDB, + REMOTE_MINIO, +} from "../../lib/src/common/types.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; @@ -7,12 +14,13 @@ import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchd import { fetchAllUsedChunks } from "../../lib/src/pouchdb/utils_couchdb.ts"; export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder { - $everyOnload(): Promise { this.core.rebuilder = this; return Promise.resolve(true); } - async $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise { + async $performRebuildDB( + method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks" + ): Promise { if (method == "localOnly") { await this.$fetchLocal(); } @@ -27,11 +35,13 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu } } - async askUsingOptionalFeature(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }) { - if (await this.core.confirm.askYesNoDialog("Do you want to enable extra features? If you are new to Self-hosted LiveSync, try the core feature first!", { title: "Enable extra features", defaultOption: "No", timeout: 15 }) == "yes") { + async askUsingOptionalFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }) { + if ( + (await this.core.confirm.askYesNoDialog( + "Do you want to enable extra features? If you are new to Self-hosted LiveSync, try the core feature first!", + { title: "Enable extra features", defaultOption: "No", timeout: 15 } + )) == "yes" + ) { await this.core.$allAskUsingOptionalSyncFeature(opt); } } @@ -55,7 +65,6 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu return this.rebuildRemote(); } - async rebuildEverything() { await this.core.$allSuspendExtraSync(); await this.askUseNewAdapter(); @@ -73,7 +82,6 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu await this.core.$$replicateAllToServer(true); await delay(1000); await this.core.$$replicateAllToServer(true, true); - } $rebuildEverything(): Promise { @@ -116,7 +124,6 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu await this.localDatabase.resetDatabase(); } - async suspendAllSync() { this.core.settings.liveSync = false; this.core.settings.periodicReplication = false; @@ -130,7 +137,10 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu async suspendReflectingDatabase() { if (this.core.settings.doNotSuspendOnFetching) return; if (this.core.settings.remoteType == REMOTE_MINIO) return; - this._log(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL_NOTICE); + this._log( + `Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, + LOG_LEVEL_NOTICE + ); this.core.settings.suspendParseReplicationResult = true; this.core.settings.suspendFileWatching = true; await this.core.saveSettings(); @@ -144,7 +154,6 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu await this.core.$$performFullScan(true); await this.core.$everyBeforeReplicate(false); //TODO: Check actual need of this. await this.core.saveSettings(); - } async askUseNewAdapter() { if (!this.core.settings.useIndexedDBAdapter) { @@ -153,7 +162,13 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu const CHOICE_NO = "No, keep compatibility"; const choices = [CHOICE_YES, CHOICE_NO]; - const ret = await this.core.confirm.confirmWithMessage("Database adapter", message, choices, CHOICE_YES, 10); + const ret = await this.core.confirm.confirmWithMessage( + "Database adapter", + message, + choices, + CHOICE_YES, + 10 + ); if (ret == CHOICE_YES) { this.core.settings.useIndexedDBAdapter = true; } @@ -200,10 +215,18 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu await this.core.$$resetLocalDatabase(); } async fetchRemoteChunks() { - if (!this.core.settings.doNotSuspendOnFetching && this.core.settings.readChunksOnline && this.core.settings.remoteType == REMOTE_COUCHDB) { + if ( + !this.core.settings.doNotSuspendOnFetching && + this.core.settings.readChunksOnline && + this.core.settings.remoteType == REMOTE_COUCHDB + ) { this._log(`Fetching chunks`, LOG_LEVEL_NOTICE); const replicator = this.core.$$getReplicator() as LiveSyncCouchDBReplicator; - const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.core.$$isMobile(), true); + const remoteDB = await replicator.connectRemoteCouchDBWithSetting( + this.settings, + this.core.$$isMobile(), + true + ); if (typeof remoteDB == "string") { this._log(remoteDB, LOG_LEVEL_NOTICE); } else { @@ -218,9 +241,14 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu let i = 0; for (const file of files) { - if (i++ % 10) this._log(`Check and Processing ${i} / ${files.length}`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes"); + if (i++ % 10) + this._log( + `Check and Processing ${i} / ${files.length}`, + LOG_LEVEL_NOTICE, + "resolveAllConflictedFilesByNewerOnes" + ); await this.core.$anyResolveConflictByNewest(file); } this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes"); } -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index c6ed9f0..87e7ebf 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -8,7 +8,14 @@ import { purgeUnreferencedChunks, balanceChunkPurgedDBs } from "../../lib/src/po import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator"; import { throttle } from "octagonal-wheels/function"; import { arrayToChunkedArray } from "octagonal-wheels/collection"; -import { SYNCINFO_ID, VER, type EntryBody, type EntryDoc, type LoadedEntry, type MetaEntry } from "../../lib/src/common/types"; +import { + SYNCINFO_ID, + VER, + type EntryBody, + type EntryDoc, + type LoadedEntry, + type MetaEntry, +} from "../../lib/src/common/types"; import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; import { getPath, isChunk, isValidPath, scheduleTask } from "../../common/utils"; import { sendValue } from "octagonal-wheels/messagepassing/signal"; @@ -17,13 +24,12 @@ import { EVENT_FILE_SAVED, eventHub } from "../../common/events"; import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator"; export class ModuleReplicator extends AbstractModule implements ICoreModule { - $everyOnloadAfterLoadSettings(): Promise { eventHub.onEvent(EVENT_FILE_SAVED, () => { if (this.settings.syncOnSave && !this.core.$$isSuspended()) { scheduleTask("perform-replicate-after-save", 250, () => this.core.$$waitForReplicationOnce()); } - }) + }); return Promise.resolve(true); } @@ -31,7 +37,7 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { const replicator = await this.core.$anyNewReplicator(); if (!replicator) { this._log("No replicator is available, this is the fatal error.", LOG_LEVEL_NOTICE); - return false + return false; } this.core.replicator = replicator; await yieldMicrotask(); @@ -48,7 +54,6 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { $everyOnResetDatabase(db: LiveSyncLocalDB): Promise { return this.setReplicator(); - } async $everyBeforeReplicate(showMessage: boolean): Promise { @@ -56,7 +61,7 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { return true; } async $$replicate(showMessage: boolean = false): Promise { - //--? + //--? if (!this.core.$$isReady()) return; if (isLockAcquired("cleanup")) { Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL_NOTICE); @@ -66,11 +71,11 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL_NOTICE); return; } - if (!await this.core.$everyCommitPendingFileEvent()) { + if (!(await this.core.$everyCommitPendingFileEvent())) { Logger("Some file events are pending. Replication has been cancelled.", LOG_LEVEL_NOTICE); return false; } - if (!await this.core.$everyBeforeReplicate(showMessage)) { + if (!(await this.core.$everyBeforeReplicate(showMessage))) { Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE); return false; } @@ -80,32 +85,41 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule { if (!ret) { if (this.core.replicator.tweakSettingsMismatched) { await this.core.$$askResolvingMismatchedTweaks(); - } else { if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) { if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) { - Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); + Logger( + `The remote database has been cleaned.`, + showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO + ); await skipIfDuplicated("cleanup", async () => { const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true); const message = `The remote database has been cleaned up. To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device. However, If there are many chunks to be deleted, maybe fetching again is faster. We will lose the history of this device if we fetch the remote database again. -Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.` +Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`; const CHOICE_FETCH = "Fetch again"; const CHOICE_CLEAN = "Cleanup"; const CHOICE_DISMISS = "Dismiss"; - const ret = await this.core.confirm.confirmWithMessage("Cleaned", message, [ - CHOICE_FETCH, - CHOICE_CLEAN, - CHOICE_DISMISS], CHOICE_DISMISS, 30); + const ret = await this.core.confirm.confirmWithMessage( + "Cleaned", + message, + [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], + CHOICE_DISMISS, + 30 + ); if (ret == CHOICE_FETCH) { await this.core.rebuilder.$performRebuildDB("localOnly"); } if (ret == CHOICE_CLEAN) { const replicator = this.core.$$getReplicator(); if (!(replicator instanceof LiveSyncCouchDBReplicator)) return; - const remoteDB = await replicator.connectRemoteCouchDBWithSetting(this.settings, this.core.$$isMobile(), true); + const remoteDB = await replicator.connectRemoteCouchDBWithSetting( + this.settings, + this.core.$$isMobile(), + true + ); if (typeof remoteDB == "string") { Logger(remoteDB, LOG_LEVEL_NOTICE); return false; @@ -114,16 +128,23 @@ Even if you choose to clean up, you will see this option again if you exit Obsid await purgeUnreferencedChunks(this.localDatabase.localDatabase, false); this.localDatabase.hashCaches.clear(); // Perform the synchronisation once. - if (await this.core.replicator.openReplication(this.settings, false, showMessage, true)) { + if ( + await this.core.replicator.openReplication(this.settings, false, showMessage, true) + ) { await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db); await purgeUnreferencedChunks(this.localDatabase.localDatabase, false); this.localDatabase.hashCaches.clear(); await this.core.$$getReplicator().markRemoteResolved(this.settings); - Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO) + Logger( + "The local database has been cleaned up.", + showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO + ); } else { - Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO) + Logger( + "Replication has been cancelled. Please try it again.", + showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO + ); } - } }); } else { @@ -131,23 +152,30 @@ Even if you choose to clean up, you will see this option again if you exit Obsid The remote database has been rebuilt. To synchronize, this device must fetch everything again once. Or if you are sure know what had been happened, we can unlock the database from the setting dialog. - ` + `; const CHOICE_FETCH = "Fetch again"; const CHOICE_DISMISS = "Dismiss"; - const ret = await this.core.confirm.confirmWithMessage("Locked", message, [ - CHOICE_FETCH, - CHOICE_DISMISS], CHOICE_DISMISS, 10); + const ret = await this.core.confirm.confirmWithMessage( + "Locked", + message, + [CHOICE_FETCH, CHOICE_DISMISS], + CHOICE_DISMISS, + 10 + ); if (ret == CHOICE_FETCH) { const CHOICE_RESTART = "Restart"; const CHOICE_WITHOUT_RESTART = "Without restart"; - if (await this.core.confirm.askSelectStringDialogue( - "Self-hosted LiveSync restarts Obsidian to fetch everything safely. However, you can do it without restarting. Please choose one.", - [CHOICE_RESTART, CHOICE_WITHOUT_RESTART], { - title: "Fetch again", - defaultAction: CHOICE_RESTART, - timeout: 30, - } - ) == CHOICE_RESTART) { + if ( + (await this.core.confirm.askSelectStringDialogue( + "Self-hosted LiveSync restarts Obsidian to fetch everything safely. However, you can do it without restarting. Please choose one.", + [CHOICE_RESTART, CHOICE_WITHOUT_RESTART], + { + title: "Fetch again", + defaultAction: CHOICE_RESTART, + timeout: 30, + } + )) == CHOICE_RESTART + ) { await this.core.rebuilder.scheduleFetch(); // await this.core.$$scheduleAppReload(); return; @@ -165,16 +193,18 @@ Or if you are sure know what had been happened, we can unlock the database from $$parseReplicationResult(docs: Array>): void { if (this.settings.suspendParseReplicationResult && !this.replicationResultProcessor.isSuspended) { - this.replicationResultProcessor.suspend() + this.replicationResultProcessor.suspend(); } this.replicationResultProcessor.enqueueAll(docs); if (!this.settings.suspendParseReplicationResult && this.replicationResultProcessor.isSuspended) { - this.replicationResultProcessor.resume() + this.replicationResultProcessor.resume(); } } _saveQueuedFiles = throttle(() => { - const saveData = this.replicationResultProcessor._queue.filter(e => e !== undefined && e !== null).map((e) => e?._id ?? "" as string) as string[]; - const kvDBKey = "queued-files" + const saveData = this.replicationResultProcessor._queue + .filter((e) => e !== undefined && e !== null) + .map((e) => e?._id ?? ("" as string)) as string[]; + const kvDBKey = "queued-files"; // localStorage.setItem(lsKey, saveData); fireAndForget(() => this.core.kvDB.set(kvDBKey, saveData)); }, 100); @@ -184,19 +214,19 @@ Or if you are sure know what had been happened, we can unlock the database from async loadQueuedFiles() { if (this.settings.suspendParseReplicationResult) return; if (!this.settings.isConfigured) return; - const kvDBKey = "queued-files" + const kvDBKey = "queued-files"; // const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[]; - const ids = [...new Set(await this.core.kvDB.get(kvDBKey) ?? [])]; + const ids = [...new Set((await this.core.kvDB.get(kvDBKey)) ?? [])]; const batchSize = 100; const chunkedIds = arrayToChunkedArray(ids, batchSize); for await (const idsBatch of chunkedIds) { const ret = await this.localDatabase.allDocsRaw({ keys: idsBatch, include_docs: true, - limit: 100 + limit: 100, }); - const docs = ret.rows.filter(e => e.doc).map(e => e.doc) as PouchDB.Core.ExistingDocument[]; - const errors = ret.rows.filter(e => !e.doc && !e.value.deleted); + const docs = ret.rows.filter((e) => e.doc).map((e) => e.doc) as PouchDB.Core.ExistingDocument[]; + const errors = ret.rows.filter((e) => !e.doc && !e.value.deleted); if (errors.length > 0) { Logger("Some queued processes were not resurrected"); Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE); @@ -204,131 +234,163 @@ Or if you are sure know what had been happened, we can unlock the database from this.replicationResultProcessor.enqueueAll(docs); await this.replicationResultProcessor.waitForAllProcessed(); } - } - replicationResultProcessor = new QueueProcessor(async (docs: PouchDB.Core.ExistingDocument[]) => { - if (this.settings.suspendParseReplicationResult) return; - const change = docs[0]; - if (!change) return; - if (isChunk(change._id)) { - // SendSignal? - // this.parseIncomingChunk(change); - sendValue(`leaf-${change._id}`, change); - return; - } - if (await this.core.$anyModuleParsedReplicationResultItem(change)) return; - // any addon needs this item? - // for (const proc of this.core.addOns) { - // if (await proc.parseReplicationResultItem(change)) { - // return; - // } - // } - if (change.type == "versioninfo") { - if (change.version > VER) { - this.core.replicator.closeReplication(); - Logger(`Remote database updated to incompatible version. update your Self-hosted LiveSync plugin.`, LOG_LEVEL_NOTICE); - } - return; - } - if (change._id == SYNCINFO_ID || // Synchronisation information data - change._id.startsWith("_design") //design document - ) { - return; - } - if (isAnyNote(change)) { - const docPath = getPath(change); - if (!await this.core.$$isTargetFile(docPath)) { - Logger(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE); + replicationResultProcessor = new QueueProcessor( + async (docs: PouchDB.Core.ExistingDocument[]) => { + if (this.settings.suspendParseReplicationResult) return; + const change = docs[0]; + if (!change) return; + if (isChunk(change._id)) { + // SendSignal? + // this.parseIncomingChunk(change); + sendValue(`leaf-${change._id}`, change); return; } - if (this.databaseQueuedProcessor._isSuspended) { - Logger(`Processing scheduled: ${docPath}`, LOG_LEVEL_INFO); - } - const size = change.size; - if (this.core.$$isFileSizeExceeded(size)) { - Logger(`Processing ${docPath} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE); + if (await this.core.$anyModuleParsedReplicationResultItem(change)) return; + // any addon needs this item? + // for (const proc of this.core.addOns) { + // if (await proc.parseReplicationResultItem(change)) { + // return; + // } + // } + if (change.type == "versioninfo") { + if (change.version > VER) { + this.core.replicator.closeReplication(); + Logger( + `Remote database updated to incompatible version. update your Self-hosted LiveSync plugin.`, + LOG_LEVEL_NOTICE + ); + } return; } - this.databaseQueuedProcessor.enqueue(change); - } - return; - }, { - batchSize: 1, - suspended: true, - concurrentLimit: 100, - delay: 0, - totalRemainingReactiveSource: this.core.replicationResultCount - }).replaceEnqueueProcessor((queue, newItem) => { - const q = queue.filter(e => e._id != newItem._id); - return [...q, newItem]; - }).startPipeline().onUpdateProgress(() => { - this.saveQueuedFiles(); - }); - - databaseQueuedProcessor = new QueueProcessor(async (docs: EntryBody[]) => { - const dbDoc = docs[0] as LoadedEntry; // It has no `data` - const path = getPath(dbDoc); - - // If `Read chunks online` is disabled, chunks should be transferred before here. - // However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them. - const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, {}, false, true, true); - if (!doc) { - Logger(`Something went wrong while gathering content of ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `, LOG_LEVEL_NOTICE) + if ( + change._id == SYNCINFO_ID || // Synchronisation information data + change._id.startsWith("_design") //design document + ) { + return; + } + if (isAnyNote(change)) { + const docPath = getPath(change); + if (!(await this.core.$$isTargetFile(docPath))) { + Logger(`Skipped: ${docPath}`, LOG_LEVEL_VERBOSE); + return; + } + if (this.databaseQueuedProcessor._isSuspended) { + Logger(`Processing scheduled: ${docPath}`, LOG_LEVEL_INFO); + } + const size = change.size; + if (this.core.$$isFileSizeExceeded(size)) { + Logger( + `Processing ${docPath} has been skipped due to file size exceeding the limit`, + LOG_LEVEL_NOTICE + ); + return; + } + this.databaseQueuedProcessor.enqueue(change); + } return; + }, + { + batchSize: 1, + suspended: true, + concurrentLimit: 100, + delay: 0, + totalRemainingReactiveSource: this.core.replicationResultCount, } + ) + .replaceEnqueueProcessor((queue, newItem) => { + const q = queue.filter((e) => e._id != newItem._id); + return [...q, newItem]; + }) + .startPipeline() + .onUpdateProgress(() => { + this.saveQueuedFiles(); + }); - if (await this.core.$anyProcessOptionalSyncFiles(dbDoc)) { - // Already processed - } else if (isValidPath(getPath(doc))) { - this.storageApplyingProcessor.enqueue(doc as MetaEntry); - } else { - Logger(`Skipped: ${doc._id.substring(0, 8)}`, LOG_LEVEL_VERBOSE); + databaseQueuedProcessor = new QueueProcessor( + async (docs: EntryBody[]) => { + const dbDoc = docs[0] as LoadedEntry; // It has no `data` + const path = getPath(dbDoc); + + // If `Read chunks online` is disabled, chunks should be transferred before here. + // However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them. + const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, {}, false, true, true); + if (!doc) { + Logger( + `Something went wrong while gathering content of ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `, + LOG_LEVEL_NOTICE + ); + return; + } + + if (await this.core.$anyProcessOptionalSyncFiles(dbDoc)) { + // Already processed + } else if (isValidPath(getPath(doc))) { + this.storageApplyingProcessor.enqueue(doc as MetaEntry); + } else { + Logger(`Skipped: ${doc._id.substring(0, 8)}`, LOG_LEVEL_VERBOSE); + } + return; + }, + { + suspended: true, + batchSize: 1, + concurrentLimit: 10, + yieldThreshold: 1, + delay: 0, + totalRemainingReactiveSource: this.core.databaseQueueCount, } - return; - }, { - suspended: true, - batchSize: 1, - concurrentLimit: 10, - yieldThreshold: 1, - delay: 0, - totalRemainingReactiveSource: this.core.databaseQueueCount - }).replaceEnqueueProcessor((queue, newItem) => { - const q = queue.filter(e => e._id != newItem._id); - return [...q, newItem]; - }).startPipeline(); + ) + .replaceEnqueueProcessor((queue, newItem) => { + const q = queue.filter((e) => e._id != newItem._id); + return [...q, newItem]; + }) + .startPipeline(); - - storageApplyingProcessor = new QueueProcessor(async (docs: MetaEntry[]) => { - const entry = docs[0]; - await this.core.$anyProcessReplicatedDoc(entry); - return; - }, { - suspended: true, - batchSize: 1, - concurrentLimit: 6, - yieldThreshold: 1, - delay: 0, - totalRemainingReactiveSource: this.core.storageApplyingCount - }).replaceEnqueueProcessor((queue, newItem) => { - const q = queue.filter(e => e._id != newItem._id); - return [...q, newItem]; - }).startPipeline() + storageApplyingProcessor = new QueueProcessor( + async (docs: MetaEntry[]) => { + const entry = docs[0]; + await this.core.$anyProcessReplicatedDoc(entry); + return; + }, + { + suspended: true, + batchSize: 1, + concurrentLimit: 6, + yieldThreshold: 1, + delay: 0, + totalRemainingReactiveSource: this.core.storageApplyingCount, + } + ) + .replaceEnqueueProcessor((queue, newItem) => { + const q = queue.filter((e) => e._id != newItem._id); + return [...q, newItem]; + }) + .startPipeline(); $everyBeforeSuspendProcess(): Promise { this.core.replicator.closeReplication(); return Promise.resolve(true); } - async $$replicateAllToServer(showingNotice: boolean = false, sendChunksInBulkDisabled: boolean = false): Promise { + async $$replicateAllToServer( + showingNotice: boolean = false, + sendChunksInBulkDisabled: boolean = false + ): Promise { if (!this.core.$$isReady()) return false; - if (!await this.core.$everyBeforeReplicate(showingNotice)) { + if (!(await this.core.$everyBeforeReplicate(showingNotice))) { Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE); return false; } if (!sendChunksInBulkDisabled) { if (this.core.replicator instanceof LiveSyncCouchDBReplicator) { - if (await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", { defaultOption: "No", timeout: 20 }) == "yes") { + if ( + (await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", { + defaultOption: "No", + timeout: 20, + })) == "yes" + ) { await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0); } } @@ -351,7 +413,4 @@ Or if you are sure know what had been happened, we can unlock the database from async $$waitForReplicationOnce(): Promise { return await shareRunningResult(`replication`, () => this.core.$$replicate()); } - - - -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleReplicatorCouchDB.ts b/src/modules/core/ModuleReplicatorCouchDB.ts index da617df..edc5412 100644 --- a/src/modules/core/ModuleReplicatorCouchDB.ts +++ b/src/modules/core/ModuleReplicatorCouchDB.ts @@ -10,10 +10,9 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu const settings = { ...this.settings, ...settingOverride }; // If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve. if (settings.remoteType == REMOTE_MINIO) { - return undefined! + return undefined!; } return Promise.resolve(new LiveSyncCouchDBReplicator(this.core)); - } $everyAfterResumeProcess(): Promise { if (this.settings.remoteType != REMOTE_MINIO) { @@ -30,4 +29,4 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu return Promise.resolve(true); } -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleReplicatorMinIO.ts b/src/modules/core/ModuleReplicatorMinIO.ts index 50f2ac8..1ad65c6 100644 --- a/src/modules/core/ModuleReplicatorMinIO.ts +++ b/src/modules/core/ModuleReplicatorMinIO.ts @@ -12,6 +12,4 @@ export class ModuleReplicatorMinIO extends AbstractModule implements ICoreModule } return undefined!; } - - -} \ No newline at end of file +} diff --git a/src/modules/core/ModuleTargetFilter.ts b/src/modules/core/ModuleTargetFilter.ts index 0b29c1f..853d933 100644 --- a/src/modules/core/ModuleTargetFilter.ts +++ b/src/modules/core/ModuleTargetFilter.ts @@ -1,16 +1,29 @@ import { LRUCache } from "octagonal-wheels/memory/LRUCache"; -import { getPathFromUXFileInfo, id2path, isInternalMetadata, path2id, stripInternalMetadataPrefix, useMemo } from "../../common/utils"; -import { LOG_LEVEL_VERBOSE, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type ObsidianLiveSyncSettings, type UXFileInfoStub } from "../../lib/src/common/types"; +import { + getPathFromUXFileInfo, + id2path, + isInternalMetadata, + path2id, + stripInternalMetadataPrefix, + useMemo, +} from "../../common/utils"; +import { + LOG_LEVEL_VERBOSE, + type DocumentID, + type EntryHasPath, + type FilePath, + type FilePathWithPrefix, + type ObsidianLiveSyncSettings, + type UXFileInfoStub, +} from "../../lib/src/common/types"; import { addPrefix, isAcceptedAll, stripAllPrefixes } from "../../lib/src/string_and_binary/path"; import { AbstractModule } from "../AbstractModule"; import type { ICoreModule } from "../ModuleTypes"; import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events"; import { isDirty } from "../../lib/src/common/utils"; export class ModuleTargetFilter extends AbstractModule implements ICoreModule { - reloadIgnoreFiles() { - this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim()); - + this.ignoreFiles = this.settings.ignoreFiles.split(",").map((e) => e.trim()); } $everyOnload(): Promise { eventHub.onEvent(EVENT_SETTING_SAVED, (evt: ObsidianLiveSyncSettings) => { @@ -32,10 +45,13 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule { } async $$path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise { const destPath = addPrefix(filename, prefix ?? ""); - return await path2id(destPath, this.settings.usePathObfuscation ? this.settings.passphrase : "", !this.settings.handleFilenameCaseSensitive); + return await path2id( + destPath, + this.settings.usePathObfuscation ? this.settings.passphrase : "", + !this.settings.handleFilenameCaseSensitive + ); } - $$isFileSizeExceeded(size: number) { if (this.settings.syncMaxSizeInMB > 0 && size > 0) { if (this.settings.syncMaxSizeInMB * 1024 * 1024 < size) { @@ -45,7 +61,6 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule { return false; } - $$markFileListPossiblyChanged(): void { this.totalFileEventCount++; } @@ -58,38 +73,39 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule { } async $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false) { - - const fileCount = useMemo>({ - key: "fileCount", // forceUpdate: !keepFileCheckList, - }, (ctx, prev) => { - if (keepFileCheckList && prev) return prev; - if (!keepFileCheckList && prev && !this.fileListPossiblyChanged) { - return prev; - } - const fileList = (ctx.get("fileList") ?? []) as FilePathWithPrefix[]; - // const fileNameList = (ctx.get("fileNameList") ?? []) as FilePath[]; - // const fileNames = - const vaultFiles = this.core.storageAccess.getFileNames().sort(); - if (prev && vaultFiles.length == fileList.length) { - const fl3 = new Set([...fileList, ...vaultFiles]); - if (fileList.length == fl3.size && vaultFiles.length == fl3.size) { + const fileCount = useMemo>( + { + key: "fileCount", // forceUpdate: !keepFileCheckList, + }, + (ctx, prev) => { + if (keepFileCheckList && prev) return prev; + if (!keepFileCheckList && prev && !this.fileListPossiblyChanged) { return prev; } - } - ctx.set("fileList", vaultFiles); - - - const fileCount: Record = {}; - for (const file of vaultFiles) { - const lc = file.toLowerCase(); - if (!fileCount[lc]) { - fileCount[lc] = 1; - } else { - fileCount[lc]++; + const fileList = (ctx.get("fileList") ?? []) as FilePathWithPrefix[]; + // const fileNameList = (ctx.get("fileNameList") ?? []) as FilePath[]; + // const fileNames = + const vaultFiles = this.core.storageAccess.getFileNames().sort(); + if (prev && vaultFiles.length == fileList.length) { + const fl3 = new Set([...fileList, ...vaultFiles]); + if (fileList.length == fl3.size && vaultFiles.length == fl3.size) { + return prev; + } } + ctx.set("fileList", vaultFiles); + + const fileCount: Record = {}; + for (const file of vaultFiles) { + const lc = file.toLowerCase(); + if (!fileCount[lc]) { + fileCount[lc] = 1; + } else { + fileCount[lc]++; + } + } + return fileCount; } - return fileCount; - }) + ); const filepath = getPathFromUXFileInfo(file); const lc = filepath.toLowerCase(); @@ -100,7 +116,7 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule { } const fileNameLC = getPathFromUXFileInfo(file).split("/").pop()?.toLowerCase(); if (this.settings.useIgnoreFiles) { - if (this.ignoreFiles.some(e => e.toLowerCase() == fileNameLC)) { + if (this.ignoreFiles.some((e) => e.toLowerCase() == fileNameLC)) { // We must reload ignore files due to the its change. await this.readIgnoreFile(filepath); } @@ -109,11 +125,11 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule { } } if (!this.localDatabase?.isTargetFile(filepath)) return false; - return true + return true; } ignoreFileCache = new LRUCache(300, 250000, true); - ignoreFiles = [] as string[] + ignoreFiles = [] as string[]; async readIgnoreFile(path: string) { try { const file = await this.core.storageAccess.readFileText(path); @@ -138,14 +154,18 @@ export class ModuleTargetFilter extends AbstractModule implements ICoreModule { if (!this.settings.useIgnoreFiles) { return false; } - const filepath = getPathFromUXFileInfo(file) + const filepath = getPathFromUXFileInfo(file); if (this.ignoreFileCache.has(filepath)) { // Renew await this.readIgnoreFile(filepath); } - if (!await isAcceptedAll(stripAllPrefixes(filepath), this.ignoreFiles, (filename) => this.getIgnoreFile(filename))) { + if ( + !(await isAcceptedAll(stripAllPrefixes(filepath), this.ignoreFiles, (filename) => + this.getIgnoreFile(filename) + )) + ) { return true; } return false; } -} \ No newline at end of file +} diff --git a/src/modules/coreFeatures/ModuleCheckRemoteSize.ts b/src/modules/coreFeatures/ModuleCheckRemoteSize.ts index 9435544..94ae66e 100644 --- a/src/modules/coreFeatures/ModuleCheckRemoteSize.ts +++ b/src/modules/coreFeatures/ModuleCheckRemoteSize.ts @@ -18,16 +18,21 @@ Do you want to enable this? > - 2000: Warn if the remote storage size exceeds 2GB. If we have reached the limit, we will be asked to enlarge the limit step by step. -` +`; const ANSWER_0 = "No, never warn please"; const ANSWER_800 = "800MB (Cloudant, fly.io)"; const ANSWER_2000 = "2GB (Standard)"; const ASK_ME_NEXT_TIME = "Ask me later"; - const ret = await this.core.confirm.askSelectStringDialogue(message, [ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME], { - defaultAction: ASK_ME_NEXT_TIME, - title: "Setting up database size notification", timeout: 40 - }); + const ret = await this.core.confirm.askSelectStringDialogue( + message, + [ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME], + { + defaultAction: ASK_ME_NEXT_TIME, + title: "Setting up database size notification", + timeout: 40, + } + ); if (ret == ANSWER_0) { this.settings.notifyThresholdOfRemoteStorageSize = 0; await this.core.saveSettings(); @@ -68,13 +73,20 @@ If we have reached the limit, we will be asked to enlarge the limit step by step const ANSWER_ENLARGE_LIMIT = `increase to ${newMax}MB`; const ANSWER_REBUILD = "Rebuild Everything Now"; const ANSWER_IGNORE = "Dismiss"; - const ret = await this.core.confirm.askSelectStringDialogue(message, [ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE,], { - defaultAction: ANSWER_IGNORE, - title: "Remote storage size exceeded the limit", timeout: 60 - - }); + const ret = await this.core.confirm.askSelectStringDialogue( + message, + [ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE], + { + defaultAction: ANSWER_IGNORE, + title: "Remote storage size exceeded the limit", + timeout: 60, + } + ); if (ret == ANSWER_REBUILD) { - const ret = await this.core.confirm.askYesNoDialog("This may take a bit of a long time. Do you really want to rebuild everything now?", { defaultOption: "No" }); + const ret = await this.core.confirm.askYesNoDialog( + "This may take a bit of a long time. Do you really want to rebuild everything now?", + { defaultOption: "No" } + ); if (ret == "yes") { this.core.settings.notifyThresholdOfRemoteStorageSize = -1; await this.saveSettings(); @@ -82,13 +94,19 @@ If we have reached the limit, we will be asked to enlarge the limit step by step } } else if (ret == ANSWER_ENLARGE_LIMIT) { this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100; - this._log(`Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`, LOG_LEVEL_NOTICE); + this._log( + `Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`, + LOG_LEVEL_NOTICE + ); await this.core.saveSettings(); } else { // Dismiss or Close the dialog } - this._log(`Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024)} `, LOG_LEVEL_INFO); + this._log( + `Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024)} `, + LOG_LEVEL_INFO + ); } else { this._log(`Remote storage size: ${sizeToHumanReadable(estimatedSize)}`, LOG_LEVEL_INFO); } @@ -97,5 +115,4 @@ If we have reached the limit, we will be asked to enlarge the limit step by step } return true; } - -} \ No newline at end of file +} diff --git a/src/modules/coreFeatures/ModuleConflictChecker.ts b/src/modules/coreFeatures/ModuleConflictChecker.ts index fecb6b3..fde5640 100644 --- a/src/modules/coreFeatures/ModuleConflictChecker.ts +++ b/src/modules/coreFeatures/ModuleConflictChecker.ts @@ -5,7 +5,6 @@ import { sendValue } from "octagonal-wheels/messagepassing/signal"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleConflictChecker extends AbstractModule implements ICoreModule { - async $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise { const path = file; if (this.settings.checkConflictOnlyOnOpen) { @@ -36,40 +35,44 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule } // TODO-> Move to ModuleConflictResolver? - conflictResolveQueue = new QueueProcessor(async (filenames: FilePathWithPrefix[]) => { - await this.core.$$resolveConflict(filenames[0]); - }, { - suspended: false, - batchSize: 1, - concurrentLimit: 1, - delay: 10, - keepResultUntilDownstreamConnected: false - }).replaceEnqueueProcessor((queue, newEntity) => { + conflictResolveQueue = new QueueProcessor( + async (filenames: FilePathWithPrefix[]) => { + await this.core.$$resolveConflict(filenames[0]); + }, + { + suspended: false, + batchSize: 1, + concurrentLimit: 1, + delay: 10, + keepResultUntilDownstreamConnected: false, + } + ).replaceEnqueueProcessor((queue, newEntity) => { const filename = newEntity; sendValue("cancel-resolve-conflict:" + filename, true); - const newQueue = [...queue].filter(e => e != newEntity); + const newQueue = [...queue].filter((e) => e != newEntity); return [...newQueue, newEntity]; }); - conflictCheckQueue = // First process - Check is the file actually need resolve - - new QueueProcessor((files: FilePathWithPrefix[]) => { - const filename = files[0]; - // const file = await this.core.storageAccess.isExists(filename); - // if (!file) return []; - // if (!(file instanceof TFile)) return; - // if ((file instanceof TFolder)) return []; - // Check again? - return Promise.resolve([filename]); - // this.conflictResolveQueue.enqueueWithKey(filename, { filename, file }); - }, { - suspended: false, - batchSize: 1, - concurrentLimit: 5, - delay: 10, - keepResultUntilDownstreamConnected: true, - pipeTo: this.conflictResolveQueue, - totalRemainingReactiveSource: this.core.conflictProcessQueueCount - }); - -} \ No newline at end of file + new QueueProcessor( + (files: FilePathWithPrefix[]) => { + const filename = files[0]; + // const file = await this.core.storageAccess.isExists(filename); + // if (!file) return []; + // if (!(file instanceof TFile)) return; + // if ((file instanceof TFolder)) return []; + // Check again? + return Promise.resolve([filename]); + // this.conflictResolveQueue.enqueueWithKey(filename, { filename, file }); + }, + { + suspended: false, + batchSize: 1, + concurrentLimit: 5, + delay: 10, + keepResultUntilDownstreamConnected: true, + pipeTo: this.conflictResolveQueue, + totalRemainingReactiveSource: this.core.conflictProcessQueueCount, + } + ); +} diff --git a/src/modules/coreFeatures/ModuleConflictResolver.ts b/src/modules/coreFeatures/ModuleConflictResolver.ts index 72ab468..dff2bad 100644 --- a/src/modules/coreFeatures/ModuleConflictResolver.ts +++ b/src/modules/coreFeatures/ModuleConflictResolver.ts @@ -1,17 +1,32 @@ import { serialized } from "octagonal-wheels/concurrency/lock"; import { AbstractModule } from "../AbstractModule.ts"; -import { AUTO_MERGED, CANCELLED, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, MISSING_OR_ERROR, NOT_CONFLICTED, type diff_check_result, type FilePathWithPrefix } from "../../lib/src/common/types"; +import { + AUTO_MERGED, + CANCELLED, + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + MISSING_OR_ERROR, + NOT_CONFLICTED, + type diff_check_result, + type FilePathWithPrefix, +} from "../../lib/src/common/types"; import { compareMTime, displayRev, TARGET_IS_NEW } from "../../common/utils"; import diff_match_patch from "diff-match-patch"; import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleConflictResolver extends AbstractModule implements ICoreModule { - - async $$resolveConflictByDeletingRev(path: FilePathWithPrefix, deleteRevision: string, subTitle = ""): Promise { + async $$resolveConflictByDeletingRev( + path: FilePathWithPrefix, + deleteRevision: string, + subTitle = "" + ): Promise { const title = `Resolving ${subTitle ? `[${subTitle}]` : ""}:`; - if (!await this.core.fileHandler.deleteRevisionFromDB(path, deleteRevision)) { - this._log(`${title} Could not delete conflicted revision ${displayRev(deleteRevision)} of ${path}`, LOG_LEVEL_NOTICE); + if (!(await this.core.fileHandler.deleteRevisionFromDB(path, deleteRevision))) { + this._log( + `${title} Could not delete conflicted revision ${displayRev(deleteRevision)} of ${path}`, + LOG_LEVEL_NOTICE + ); return MISSING_OR_ERROR; } this._log(`${title} Conflicted revision deleted ${displayRev(deleteRevision)} ${path}`, LOG_LEVEL_INFO); @@ -20,7 +35,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul return AUTO_MERGED; } // 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); return MISSING_OR_ERROR; } @@ -28,7 +43,6 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul return AUTO_MERGED; } - async checkConflictAndPerformAutoMerge(path: FilePathWithPrefix): Promise { // const ret = await this.localDatabase.tryAutoMerge(path, !this.settings.disableMarkdownAutoMerge); @@ -40,7 +54,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul const p = ret.result; // Merged content is coming. // 1. Store the merged content to the storage - if (!await this.core.databaseFileAccess.storeContent(path, p)) { + if (!(await this.core.databaseFileAccess.storeContent(path, p))) { this._log(`Merged content cannot be stored:${path}`, LOG_LEVEL_NOTICE); return MISSING_OR_ERROR; } @@ -65,13 +79,17 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul const isBinary = !isPlainText(path); const alwaysNewer = this.settings.resolveConflictsByNewerFile; if (isSame || isBinary || alwaysNewer) { - const result = compareMTime(leftLeaf.mtime, rightLeaf.mtime) + const result = compareMTime(leftLeaf.mtime, rightLeaf.mtime); let loser = leftLeaf; // if (lMtime > rMtime) { if (result != TARGET_IS_NEW) { loser = rightLeaf; } - const subTitle = [`${isSame ? "same" : ""}`, `${isBinary ? "binary" : ""}`, `${alwaysNewer ? "alwaysNewer" : ""}`].join(","); + const subTitle = [ + `${isSame ? "same" : ""}`, + `${isBinary ? "binary" : ""}`, + `${alwaysNewer ? "alwaysNewer" : ""}`, + ].join(","); return await this.core.$$resolveConflictByDeletingRev(path, loser.rev, subTitle); } // make diff. @@ -86,13 +104,15 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul }; } - - async $$resolveConflict(filename: FilePathWithPrefix): Promise { // const filename = filenames[0]; return await serialized(`conflict-resolve:${filename}`, async () => { const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename); - if (conflictCheckResult === MISSING_OR_ERROR || conflictCheckResult === NOT_CONFLICTED || conflictCheckResult === CANCELLED) { + if ( + conflictCheckResult === MISSING_OR_ERROR || + conflictCheckResult === NOT_CONFLICTED || + conflictCheckResult === CANCELLED + ) { // nothing to do. this._log(`conflict:Nothing to do:${filename}`); return; @@ -110,7 +130,10 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul if (this.settings.showMergeDialogOnlyOnActive) { const af = this.core.$$getActiveFilePath(); if (af && af != filename) { - this._log(`${filename} is conflicted. Merging process has been postponed to the file have got opened.`, LOG_LEVEL_NOTICE); + this._log( + `${filename} is conflicted. Merging process has been postponed to the file have got opened.`, + LOG_LEVEL_NOTICE + ); return; } } @@ -124,18 +147,26 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul if (revs.length == 0) { return Promise.resolve(true); } - const mTimeAndRev = (await Promise.all(revs.map(async (rev) => { - const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev); - if (leaf == false) { - return [0, rev] as [number, string]; - } - return [leaf.mtime, rev] as [number, string]; - }))).sort((a, b) => b[0] - a[0]); - this._log(`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`); + const mTimeAndRev = ( + await Promise.all( + revs.map(async (rev) => { + const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev); + if (leaf == false) { + return [0, rev] as [number, string]; + } + return [leaf.mtime, rev] as [number, string]; + }) + ) + ).sort((a, b) => b[0] - a[0]); + this._log( + `Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)` + ); for (let i = 1; i < mTimeAndRev.length; i++) { - this._log(`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`); + this._log( + `conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}` + ); await this.core.$$resolveConflictByDeletingRev(filename, mTimeAndRev[i][1], "NEWEST"); } return true; } -} \ No newline at end of file +} diff --git a/src/modules/coreFeatures/ModuleRedFlag.ts b/src/modules/coreFeatures/ModuleRedFlag.ts index 2d6d26b..320d00b 100644 --- a/src/modules/coreFeatures/ModuleRedFlag.ts +++ b/src/modules/coreFeatures/ModuleRedFlag.ts @@ -1,11 +1,16 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { normalizePath } from "../../deps.ts"; -import { FLAGMD_REDFLAG, FLAGMD_REDFLAG2, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3, FLAGMD_REDFLAG3_HR } from "../../lib/src/common/types.ts"; +import { + FLAGMD_REDFLAG, + FLAGMD_REDFLAG2, + FLAGMD_REDFLAG2_HR, + FLAGMD_REDFLAG3, + FLAGMD_REDFLAG3_HR, +} from "../../lib/src/common/types.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleRedFlag extends AbstractModule implements ICoreModule { - async isFlagFileExist(path: string) { const redflag = await this.core.storageAccess.isExists(normalizePath(path)); if (redflag) { @@ -26,9 +31,11 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule { } } - isRedFlagRaised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG) - isRedFlag2Raised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG2) || await this.isFlagFileExist(FLAGMD_REDFLAG2_HR) - isRedFlag3Raised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG3) || await this.isFlagFileExist(FLAGMD_REDFLAG3_HR) + isRedFlagRaised = async () => await this.isFlagFileExist(FLAGMD_REDFLAG); + isRedFlag2Raised = async () => + (await this.isFlagFileExist(FLAGMD_REDFLAG2)) || (await this.isFlagFileExist(FLAGMD_REDFLAG2_HR)); + isRedFlag3Raised = async () => + (await this.isFlagFileExist(FLAGMD_REDFLAG3)) || (await this.isFlagFileExist(FLAGMD_REDFLAG3_HR)); async deleteRedFlag2() { await this.deleteFlagFile(FLAGMD_REDFLAG2); @@ -47,14 +54,24 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule { if (isRedFlagRaised || isRedFlag2Raised || isRedFlag3Raised) { if (isRedFlag2Raised) { - if (await this.core.confirm.askYesNoDialog("Rebuild everything has been scheduled! Are you sure to rebuild everything?", { defaultOption: "Yes", timeout: 0 }) !== "yes") { + if ( + (await this.core.confirm.askYesNoDialog( + "Rebuild everything has been scheduled! Are you sure to rebuild everything?", + { defaultOption: "Yes", timeout: 0 } + )) !== "yes" + ) { await this.deleteRedFlag2(); await this.core.$$performRestart(); return false; } } if (isRedFlag3Raised) { - if (await this.core.confirm.askYesNoDialog("Fetch again has been scheduled! Are you sure?", { defaultOption: "Yes", timeout: 0 }) !== "yes") { + if ( + (await this.core.confirm.askYesNoDialog("Fetch again has been scheduled! Are you sure?", { + defaultOption: "Yes", + timeout: 0, + })) !== "yes" + ) { await this.deleteRedFlag3(); await this.core.$$performRestart(); return false; @@ -66,39 +83,63 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule { this.settings.suspendFileWatching = true; await this.saveSettings(); if (isRedFlag2Raised) { - this._log(`${FLAGMD_REDFLAG2} or ${FLAGMD_REDFLAG2_HR} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL_NOTICE); + this._log( + `${FLAGMD_REDFLAG2} or ${FLAGMD_REDFLAG2_HR} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, + LOG_LEVEL_NOTICE + ); await this.core.rebuilder.$rebuildEverything(); await this.deleteRedFlag2(); - if (await this.core.confirm.askYesNoDialog("Do you want to resume file and database processing, and restart obsidian now?", { defaultOption: "Yes", timeout: 15 }) == "yes") { + if ( + (await this.core.confirm.askYesNoDialog( + "Do you want to resume file and database processing, and restart obsidian now?", + { defaultOption: "Yes", timeout: 15 } + )) == "yes" + ) { this.settings.suspendFileWatching = false; await this.saveSettings(); this.core.$$performRestart(); return false; } } else if (isRedFlag3Raised) { - this._log(`${FLAGMD_REDFLAG3} or ${FLAGMD_REDFLAG3_HR} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL_NOTICE); - const makeLocalChunkBeforeSync = ((await this.core.confirm.askYesNoDialog(`Do you want to create local chunks before fetching? + this._log( + `${FLAGMD_REDFLAG3} or ${FLAGMD_REDFLAG3_HR} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, + LOG_LEVEL_NOTICE + ); + const makeLocalChunkBeforeSync = + (await this.core.confirm.askYesNoDialog( + `Do you want to create local chunks before fetching? > [!MORE]- > If creating local chunks before fetching, only the difference between the local and remote will be fetched. -`, { defaultOption: "Yes", title: "Trick to transfer efficiently" })) == "yes"); +`, + { defaultOption: "Yes", title: "Trick to transfer efficiently" } + )) == "yes"; await this.core.rebuilder.$fetchLocal(makeLocalChunkBeforeSync); await this.deleteRedFlag3(); if (this.settings.suspendFileWatching) { - if (await this.core.confirm.askYesNoDialog("Do you want to resume file and database processing, and restart obsidian now?", { defaultOption: "Yes", timeout: 15 }) == "yes") { + if ( + (await this.core.confirm.askYesNoDialog( + "Do you want to resume file and database processing, and restart obsidian now?", + { defaultOption: "Yes", timeout: 15 } + )) == "yes" + ) { this.settings.suspendFileWatching = false; await this.saveSettings(); this.core.$$performRestart(); return false; } } else { - this._log("Your content of files will be synchronised gradually. Please wait for the completion.", LOG_LEVEL_NOTICE); + this._log( + "Your content of files will be synchronised gradually. Please wait for the completion.", + LOG_LEVEL_NOTICE + ); } } else { // Case of FLAGMD_REDFLAG. this.settings.writeLogToTheFile = true; // await this.plugin.openDatabase(); - const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured."; + const warningMessage = + "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured."; this._log(warningMessage, LOG_LEVEL_NOTICE); } } @@ -108,5 +149,4 @@ export class ModuleRedFlag extends AbstractModule implements ICoreModule { } return true; } - -} \ No newline at end of file +} diff --git a/src/modules/coreFeatures/ModuleRemoteGovernor.ts b/src/modules/coreFeatures/ModuleRemoteGovernor.ts index 5bbb1f4..6f3cbbf 100644 --- a/src/modules/coreFeatures/ModuleRemoteGovernor.ts +++ b/src/modules/coreFeatures/ModuleRemoteGovernor.ts @@ -13,4 +13,4 @@ export class ModuleRemoteGovernor extends AbstractModule implements ICoreModule async $$markRemoteResolved(): Promise { return await this.core.replicator.markRemoteResolved(this.settings); } -} \ No newline at end of file +} diff --git a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts index 81ea3ef..b80857b 100644 --- a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts +++ b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts @@ -1,6 +1,12 @@ import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; import { extractObject } from "octagonal-wheels/object"; -import { TweakValuesShouldMatchedTemplate, CompatibilityBreakingTweakValues, confName, type TweakValues, type RemoteDBSettings } from "../../lib/src/common/types.ts"; +import { + TweakValuesShouldMatchedTemplate, + CompatibilityBreakingTweakValues, + confName, + type TweakValues, + type RemoteDBSettings, +} from "../../lib/src/common/types.ts"; import { escapeMarkdownValue } from "../../lib/src/common/utils.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; @@ -38,11 +44,13 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`; } - const additionalMessage = rebuildRequired ? ` + const additionalMessage = rebuildRequired + ? ` **Note**: We have detected that some of the values are different to make incompatible the local database with the remote database. If you choose to use the configured values, the local database will be rebuilt, and if you choose to use the values of this device, the remote database will be rebuilt. -Both of them takes a few minutes. Please choose after considering the situation.` : ""; +Both of them takes a few minutes. Please choose after considering the situation.` + : ""; const message = ` Your configuration has not been matched with the one on the remote server. @@ -67,9 +75,16 @@ Please select which one you want to use. const CHOICE_AND_VALUES = [ [CHOICE_USE_REMOTE, preferred], [CHOICE_USR_MINE, true], - [CHOICE_DISMISS, false]] + [CHOICE_DISMISS, false], + ]; const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record; - const retKey = await this.core.confirm.confirmWithMessage("Tweaks Mismatched or Changed", message, Object.keys(CHOICES), CHOICE_DISMISS, 60); + const retKey = await this.core.confirm.confirmWithMessage( + "Tweaks Mismatched or Changed", + message, + Object.keys(CHOICES), + CHOICE_DISMISS, + 60 + ); if (!retKey) return "IGNORE"; const conf = CHOICES[retKey]; @@ -78,7 +93,10 @@ Please select which one you want to use. if (rebuildRequired) { await this.core.rebuilder.$rebuildRemote(); } - Logger(`Tweak values on the remote server have been updated. Your other device will see this message.`, LOG_LEVEL_NOTICE); + Logger( + `Tweak values on the remote server have been updated. Your other device will see this message.`, + LOG_LEVEL_NOTICE + ); return "CHECKAGAIN"; } if (conf) { @@ -92,10 +110,11 @@ Please select which one you want to use. return "CHECKAGAIN"; } return "IGNORE"; - } - async $$checkAndAskUseRemoteConfiguration(trialSetting: RemoteDBSettings): Promise<{ result: false | TweakValues, requireFetch: boolean }> { + async $$checkAndAskUseRemoteConfiguration( + trialSetting: RemoteDBSettings + ): Promise<{ result: false | TweakValues; requireFetch: boolean }> { const replicator = await this.core.$anyNewReplicator(trialSetting); if (await replicator.tryConnectRemote(trialSetting)) { const preferred = await replicator.getRemotePreferredTweakValues(trialSetting); @@ -122,14 +141,20 @@ Please select which one you want to use. } if (differenceCount === 0) { - this._log("The settings in the remote database are the same as the local database.", LOG_LEVEL_NOTICE); + this._log( + "The settings in the remote database are the same as the local database.", + LOG_LEVEL_NOTICE + ); return { result: false, requireFetch: false }; } - const additionalMessage = (rebuildRequired && this.core.settings.isConfigured) ? ` + const additionalMessage = + rebuildRequired && this.core.settings.isConfigured + ? ` >[!WARNING] > Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required. -***Please ensure that you have time and are connected to a stable network to apply!***` : ""; +***Please ensure that you have time and are connected to a stable network to apply!***` + : ""; const message = ` The settings in the remote database are as follows. @@ -152,7 +177,7 @@ ${additionalMessage}`; const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, { title: "Use Remote Configuration", timeout: 0, - defaultAction: CHOICE_DISMISS + defaultAction: CHOICE_DISMISS, }); if (!retKey) return { result: false, requireFetch: false }; if (retKey === CHOICE_DISMISS) return { result: false, requireFetch: false }; @@ -168,4 +193,4 @@ ${additionalMessage}`; return { result: false, requireFetch: false }; } } -} \ No newline at end of file +} diff --git a/src/modules/coreObsidian/ModuleFileAccessObsidian.ts b/src/modules/coreObsidian/ModuleFileAccessObsidian.ts index cbea1cc..ab9070e 100644 --- a/src/modules/coreObsidian/ModuleFileAccessObsidian.ts +++ b/src/modules/coreObsidian/ModuleFileAccessObsidian.ts @@ -2,13 +2,20 @@ import { normalizePath, TFile, TFolder, type ListedFiles } from "obsidian"; import { SerializedFileAccess } from "./storageLib/SerializedFileAccess"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "../../lib/src/common/types"; +import type { + FilePath, + FilePathWithPrefix, + UXDataWriteOptions, + UXFileInfo, + UXFileInfoStub, + UXFolderInfo, + UXStat, +} from "../../lib/src/common/types"; import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "./storageLib/utilObsidian.ts"; import { StorageEventManagerObsidian, type StorageEventManager } from "./storageLib/StorageEventManager"; import type { StorageAccess } from "../interfaces/StorageAccess"; import { createBlob } from "../../lib/src/common/utils"; - export class ModuleFileAccessObsidian extends AbstractObsidianModule implements IObsidianModule, StorageAccess { vaultAccess!: SerializedFileAccess; vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core); @@ -53,7 +60,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements if (file instanceof TFile) { return this.vaultAccess.vaultModify(file, data, opt); } else if (file === null) { - return await this.vaultAccess.vaultCreate(path, data, opt) instanceof TFile; + return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile; } else { this._log(`Could not write file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE); return false; @@ -90,7 +97,6 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements } async appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise { try { - await this.vaultAccess.adapterAppend(path, data, opt); return true; } catch (e) { @@ -107,12 +113,11 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements ctime: file.stat.ctime, mtime: file.stat.mtime, size: file.stat.size, - type: "file" + type: "file", }); } else { throw new Error(`Could not stat file (Possibly does not exist): ${path}`); } - } statHidden(path: string): Promise { return this.vaultAccess.adapterStat(path); @@ -140,7 +145,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements return await this.vaultAccess.adapterReadBinary(path); } async isExistsIncludeHidden(path: string): Promise { - return await this.vaultAccess.adapterStat(path) !== null; + return (await this.vaultAccess.adapterStat(path)) !== null; } async ensureDir(path: string): Promise { try { @@ -180,8 +185,8 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements const data = await this.vaultAccess.vaultReadAuto(file); return { ...stub, - body: createBlob(data) - } + body: createBlob(data), + }; } getStub(path: string): UXFileInfoStub | UXFolderInfo | null { const file = this.vaultAccess.getAbstractFileByPath(path); @@ -193,10 +198,10 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements return null; } getFiles(): UXFileInfoStub[] { - return this.vaultAccess.getFiles().map(f => TFileToUXFileInfoStub(f)); + return this.vaultAccess.getFiles().map((f) => TFileToUXFileInfoStub(f)); } getFileNames(): FilePath[] { - return this.vaultAccess.getFiles().map(f => f.path as FilePath); + return this.vaultAccess.getFiles().map((f) => f.path as FilePath); } async getFilesIncludeHidden( @@ -213,20 +218,20 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements this._log(ex, LOG_LEVEL_VERBOSE); return []; } - skipFolder = skipFolder.map(e => e.toLowerCase()); + skipFolder = skipFolder.map((e) => e.toLowerCase()); let files = [] as string[]; for (const file of w.files) { - if (excludeFilter && excludeFilter.some(ee => file.match(ee))) { + if (excludeFilter && excludeFilter.some((ee) => file.match(ee))) { // If excludeFilter and includeFilter are both set, the file will be included in the list. if (includeFilter) { - if (!includeFilter.some(e => file.match(e))) continue; + if (!includeFilter.some((e) => file.match(e))) continue; } else { continue; } } if (includeFilter) { - if (!includeFilter.some(e => file.match(e))) continue; + if (!includeFilter.some((e) => file.match(e))) continue; } if (await this.plugin.$$isIgnoredByIgnoreFiles(file)) continue; files.push(file); @@ -234,19 +239,19 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements for (const v of w.folders) { const folderName = (v.split("/").pop() ?? "").toLowerCase(); - if (skipFolder.some(e => folderName === e)) { - continue + if (skipFolder.some((e) => folderName === e)) { + continue; } - if (excludeFilter && excludeFilter.some(e => v.match(e))) { + if (excludeFilter && excludeFilter.some((e) => v.match(e))) { if (includeFilter) { - if (!includeFilter.some(e => v.match(e))) { + if (!includeFilter.some((e) => v.match(e))) { continue; } } } if (includeFilter) { - if (!includeFilter.some(e => v.match(e))) continue; + if (!includeFilter.some((e) => v.match(e))) continue; } // OK, deep dive! files = files.concat(await this.getFilesIncludeHidden(v, includeFilter, excludeFilter, skipFolder)); @@ -258,7 +263,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements this.vaultAccess.touch(path as FilePath); } recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean { - const xFile = typeof file === "string" ? this.vaultAccess.getAbstractFileByPath(file) as TFile : file; + const xFile = typeof file === "string" ? (this.vaultAccess.getAbstractFileByPath(file) as TFile) : file; if (xFile === null) return false; if (xFile instanceof TFolder) return false; return this.vaultAccess.recentlyTouched(xFile); @@ -303,7 +308,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements async _deleteVaultItem(file: TFile | TFolder) { if (file instanceof TFile) { - if (!await this.core.$$isTargetFile(file.path)) return; + if (!(await this.core.$$isTargetFile(file.path))) return; } const dir = file.parent; if (this.settings.trashInsteadDelete) { @@ -316,7 +321,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements this._log(`files: ${dir.children.length}`); if (dir.children.length == 0) { if (!this.settings.doNotDeleteFolder) { - this._log(`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`); + this._log( + `All files under the parent directory (${dir.path}) have been deleted, so delete this one.` + ); await this._deleteVaultItem(dir); } } @@ -331,4 +338,4 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements return await this._deleteVaultItem(file); } } -} \ No newline at end of file +} diff --git a/src/modules/coreObsidian/ModuleInputUIObsidian.ts b/src/modules/coreObsidian/ModuleInputUIObsidian.ts index de898ab..1448fa3 100644 --- a/src/modules/coreObsidian/ModuleInputUIObsidian.ts +++ b/src/modules/coreObsidian/ModuleInputUIObsidian.ts @@ -1,15 +1,20 @@ -import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts'; -import { scheduleTask } from 'octagonal-wheels/concurrency/task'; -import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from '../../common/utils.ts'; -import { askSelectString, askString, askYesNo, confirmWithMessage, confirmWithMessageWithWideButton } from './UILib/dialogs.ts'; -import { Notice } from '../../deps.ts'; -import type { Confirm } from '../interfaces/Confirm.ts'; +import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; +import { scheduleTask } from "octagonal-wheels/concurrency/task"; +import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts"; +import { + askSelectString, + askString, + askYesNo, + confirmWithMessage, + confirmWithMessageWithWideButton, +} from "./UILib/dialogs.ts"; +import { Notice } from "../../deps.ts"; +import type { Confirm } from "../interfaces/Confirm.ts"; // This module cannot be a common module because it depends on Obsidian's API. // However, we have to make compatible one for other platform. export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm { - $everyOnload(): Promise { this.core.confirm = this; return Promise.resolve(true); @@ -22,8 +27,18 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb return askString(this.app, title, key, placeholder, isPassword); } - async askYesNoDialog(message: string, opt: { title?: string, defaultOption?: "Yes" | "No", timeout?: number } = { title: "Confirmation" }): Promise<"yes" | "no"> { - const ret = await confirmWithMessageWithWideButton(this.plugin, opt.title || "Confirmation", message, ["Yes", "No"], opt.defaultOption ?? "No", opt.timeout); + async askYesNoDialog( + message: string, + opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" } + ): Promise<"yes" | "no"> { + const ret = await confirmWithMessageWithWideButton( + this.plugin, + opt.title || "Confirmation", + message, + ["Yes", "No"], + opt.defaultOption ?? "No", + opt.timeout + ); return ret == "Yes" ? "yes" : "no"; } @@ -31,8 +46,19 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb return askSelectString(this.app, message, items); } - askSelectStringDialogue(message: string, buttons: string[], opt: { title?: string, defaultAction: (typeof buttons)[number], timeout?: number }): Promise<(typeof buttons)[number] | false> { - return confirmWithMessageWithWideButton(this.plugin, opt.title || "Select", message, buttons, opt.defaultAction, opt.timeout); + askSelectStringDialogue( + message: string, + buttons: string[], + opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number } + ): Promise<(typeof buttons)[number] | false> { + return confirmWithMessageWithWideButton( + this.plugin, + opt.title || "Select", + message, + buttons, + opt.defaultAction, + opt.timeout + ); } askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) { @@ -40,9 +66,11 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb const [beforeText, afterText] = dialogText.split("{HERE}", 2); doc.createEl("span", undefined, (a) => { a.appendText(beforeText); - a.appendChild(a.createEl("a", undefined, (anchor) => { - anchorCallback(anchor); - })); + a.appendChild( + a.createEl("a", undefined, (anchor) => { + anchorCallback(anchor); + }) + ); a.appendText(afterText); }); }); @@ -55,8 +83,7 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb } scheduleTask(popupKey + "-close", 20000, () => { const popup = retrieveMemoObject(popupKey); - if (!popup) - return; + if (!popup) return; if (popup?.noticeEl?.isShown()) { popup.hide(); } @@ -64,8 +91,13 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb }); }); } - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false> { + confirmWithMessage( + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number + ): Promise<(typeof buttons)[number] | false> { return confirmWithMessage(this.plugin, title, contentMd, buttons, defaultAction, timeout); } - -} \ No newline at end of file +} diff --git a/src/modules/coreObsidian/UILib/dialogs.ts b/src/modules/coreObsidian/UILib/dialogs.ts index 0e697e8..0b1471b 100644 --- a/src/modules/coreObsidian/UILib/dialogs.ts +++ b/src/modules/coreObsidian/UILib/dialogs.ts @@ -18,7 +18,7 @@ class AutoClosableModal extends Modal { onClose() { if (this.removeEvent) { this.removeEvent(); - this.removeEvent = undefined + this.removeEvent = undefined; } } } @@ -32,7 +32,14 @@ export class InputStringDialog extends AutoClosableModal { isManuallyClosed = false; isPassword = false; - constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void) { + constructor( + app: App, + title: string, + key: string, + placeholder: string, + isPassword: boolean, + onSubmit: (result: string | false) => void + ) { super(app); this.onSubmit = onSubmit; this.title = title; @@ -45,27 +52,32 @@ export class InputStringDialog extends AutoClosableModal { const { contentEl } = this; this.titleEl.setText(this.title); const formEl = contentEl.createDiv(); - new Setting(formEl).setName(this.key).setClass(this.isPassword ? "password-input" : "normal-input").addText((text) => - text.onChange((value) => { - this.result = value; - }) - ); - new Setting(formEl).addButton((btn) => - btn - .setButtonText("Ok") - .setCta() - .onClick(() => { - this.isManuallyClosed = true; - this.close(); + new Setting(formEl) + .setName(this.key) + .setClass(this.isPassword ? "password-input" : "normal-input") + .addText((text) => + text.onChange((value) => { + this.result = value; }) - ).addButton((btn) => - btn - .setButtonText("Cancel") - .setCta() - .onClick(() => { - this.close(); - }) - ); + ); + new Setting(formEl) + .addButton((btn) => + btn + .setButtonText("Ok") + .setCta() + .onClick(() => { + this.isManuallyClosed = true; + this.close(); + }) + ) + .addButton((btn) => + btn + .setButtonText("Cancel") + .setCta() + .onClick(() => { + this.close(); + }) + ); } onClose() { @@ -81,13 +93,18 @@ export class InputStringDialog extends AutoClosableModal { } export class PopoverSelectString extends FuzzySuggestModal { app: App; - callback: ((e: string) => void) | undefined = () => { }; + callback: ((e: string) => void) | undefined = () => {}; getItemsFun: () => string[] = () => { return ["yes", "no"]; + }; - } - - constructor(app: App, note: string, placeholder: string | undefined, getItemsFun: (() => string[]) | undefined, callback: (e: string) => void) { + constructor( + app: App, + note: string, + placeholder: string | undefined, + getItemsFun: (() => string[]) | undefined, + callback: (e: string) => void + ) { super(app); this.app = app; this.setPlaceholder((placeholder ?? "y/n) ") + note); @@ -119,7 +136,6 @@ export class PopoverSelectString extends FuzzySuggestModal { } export class MessageBox extends AutoClosableModal { - plugin: Plugin; title: string; contentMd: string; @@ -134,7 +150,16 @@ export class MessageBox extends AutoClosableModal { onSubmit: (result: string | false) => void; - constructor(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout: number | undefined, wideButton: boolean, onSubmit: (result: (typeof buttons)[number] | false) => void) { + constructor( + plugin: Plugin, + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout: number | undefined, + wideButton: boolean, + onSubmit: (result: (typeof buttons)[number] | false) => void + ) { super(plugin.app); this.plugin = plugin; this.title = title; @@ -196,20 +221,18 @@ export class MessageBox extends AutoClosableModal { this.timer = undefined; this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`); } - }) + }); for (const button of this.buttons) { buttonSetting.addButton((btn) => { - btn - .setButtonText(button) - .onClick(() => { - this.isManuallyClosed = true; - this.result = button; - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } - this.close(); - }) + btn.setButtonText(button).onClick(() => { + this.isManuallyClosed = true; + this.result = button; + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + this.close(); + }); if (button == this.defaultAction) { this.defaultButtonComponent = btn; btn.setCta(); @@ -219,8 +242,7 @@ export class MessageBox extends AutoClosableModal { btn.buttonEl.style.width = "100%"; } return btn; - } - ) + }); } } @@ -240,25 +262,42 @@ export class MessageBox extends AutoClosableModal { } } - - - -export function confirmWithMessage(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false> { +export function confirmWithMessage( + plugin: Plugin, + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number +): Promise<(typeof buttons)[number] | false> { return new Promise((res) => { - const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) => res(result)); + const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) => + res(result) + ); dialog.open(); }); } -export function confirmWithMessageWithWideButton(plugin: Plugin, title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false> { +export function confirmWithMessageWithWideButton( + plugin: Plugin, + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number +): Promise<(typeof buttons)[number] | false> { return new Promise((res) => { - const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) => res(result)); + const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) => + res(result) + ); dialog.open(); }); } export const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => { return new Promise((res) => { - const popover = new PopoverSelectString(app, message, undefined, undefined, (result) => res(result as "yes" | "no")); + const popover = new PopoverSelectString(app, message, undefined, undefined, (result) => + res(result as "yes" | "no") + ); popover.open(); }); }; @@ -271,11 +310,15 @@ export const askSelectString = (app: App, message: string, items: string[]): Pro }); }; - -export const askString = (app: App, title: string, key: string, placeholder: string, isPassword: boolean = false): Promise => { +export const askString = ( + app: App, + title: string, + key: string, + placeholder: string, + isPassword: boolean = false +): Promise => { return new Promise((res) => { const dialog = new InputStringDialog(app, title, key, placeholder, isPassword, (result) => res(result)); dialog.open(); }); }; - diff --git a/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts b/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts index a824c9d..3967c13 100644 --- a/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts +++ b/src/modules/coreObsidian/storageLib/SerializedFileAccess.ts @@ -9,7 +9,7 @@ import { markChangesAreSame } from "../../../common/utils.ts"; import { type UXFileInfo } from "../../../lib/src/common/types.ts"; function getFileLockKey(file: TFile | TFolder | string | UXFileInfo) { - return `fl:${typeof (file) == "string" ? file : file.path}`; + return `fl:${typeof file == "string" ? file : file.path}`; } function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBufferLike { if (arr instanceof Uint8Array) { @@ -35,9 +35,9 @@ async function processWriteFile(file: TFile | TFolder | string | UXFileInfo, } export class SerializedFileAccess { - app: App - plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }> - constructor(app: App, plugin: typeof this["plugin"]) { + app: App; + plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>; + constructor(app: App, plugin: (typeof this)["plugin"]) { this.app = app; this.plugin = plugin; } @@ -72,10 +72,12 @@ export class SerializedFileAccess { async adapterWrite(file: TFile | string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) { const path = file instanceof TFile ? file.path : file; - if (typeof (data) === "string") { + if (typeof data === "string") { return await processWriteFile(file, () => this.app.vault.adapter.write(path, data, options)); } else { - return await processWriteFile(file, () => this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options)); + return await processWriteFile(file, () => + this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options) + ); } } @@ -97,19 +99,17 @@ export class SerializedFileAccess { return await processReadFile(file, () => this.app.vault.readBinary(file)); } - async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) { - if (typeof (data) === "string") { + if (typeof data === "string") { return await processWriteFile(file, async () => { const oldData = await this.app.vault.read(file); if (data === oldData) { if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime); return true; } - await this.app.vault.modify(file, data, options) + await this.app.vault.modify(file, data, options); return true; - } - ); + }); } else { return await processWriteFile(file, async () => { const oldData = await this.app.vault.readBinary(file); @@ -117,13 +117,17 @@ export class SerializedFileAccess { if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime); return true; } - await this.app.vault.modifyBinary(file, toArrayBuffer(data), options) + await this.app.vault.modifyBinary(file, toArrayBuffer(data), options); return true; }); } } - async vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions): Promise { - if (typeof (data) === "string") { + async vaultCreate( + path: string, + data: string | ArrayBuffer | Uint8Array, + options?: DataWriteOptions + ): Promise { + if (typeof data === "string") { return await processWriteFile(path, () => this.app.vault.create(path, data, options)); } else { return await processWriteFile(path, () => this.app.vault.createBinary(path, toArrayBuffer(data), options)); @@ -135,7 +139,7 @@ export class SerializedFileAccess { } async adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) { - return await this.app.vault.adapter.append(normalizedPath, data, options) + return await this.app.vault.adapter.append(normalizedPath, data, options); } async delete(file: TFile | TFolder, force = false) { @@ -145,8 +149,6 @@ export class SerializedFileAccess { return await processWriteFile(file, () => this.app.vault.trash(file, force)); } - - isStorageInsensitive(): boolean { //@ts-ignore return this.app.vault.adapter.insensitive ?? true; @@ -188,22 +190,23 @@ export class SerializedFileAccess { } } - touchedFiles: string[] = []; - touch(file: TFile | FilePath) { - const f = file instanceof TFile ? file : this.getAbstractFileByPath(file) as TFile; + const f = file instanceof TFile ? file : (this.getAbstractFileByPath(file) as TFile); const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`; this.touchedFiles.unshift(key); this.touchedFiles = this.touchedFiles.slice(0, 100); } recentlyTouched(file: TFile | InternalFileInfo | UXFileInfoStub) { - const key = "stat" in file ? `${file.path}-${file.stat.mtime}-${file.stat.size}` : `${file.path}-${file.mtime}-${file.size}`; + const key = + "stat" in file + ? `${file.path}-${file.stat.mtime}-${file.stat.size}` + : `${file.path}-${file.mtime}-${file.size}`; if (this.touchedFiles.indexOf(key) == -1) return false; return true; } clearTouched() { this.touchedFiles = []; } -} \ No newline at end of file +} diff --git a/src/modules/coreObsidian/storageLib/StorageEventManager.ts b/src/modules/coreObsidian/storageLib/StorageEventManager.ts index 0990a4e..aafd65c 100644 --- a/src/modules/coreObsidian/storageLib/StorageEventManager.ts +++ b/src/modules/coreObsidian/storageLib/StorageEventManager.ts @@ -1,18 +1,32 @@ import { TAbstractFile, TFile, TFolder } from "../../../deps.ts"; import { Logger } from "../../../lib/src/common/logger.ts"; import { shouldBeIgnored } from "../../../lib/src/string_and_binary/path.ts"; -import { DEFAULT_SETTINGS, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, type FilePath, type FilePathWithPrefix, type UXFileInfoStub, type UXInternalFileInfoStub } from "../../../lib/src/common/types.ts"; +import { + DEFAULT_SETTINGS, + LOG_LEVEL_DEBUG, + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + type FilePath, + type FilePathWithPrefix, + type UXFileInfoStub, + type UXInternalFileInfoStub, +} from "../../../lib/src/common/types.ts"; import { delay, fireAndForget } from "../../../lib/src/common/utils.ts"; import { type FileEventItem, type FileEventType } from "../../../common/types.ts"; import { serialized, skipIfDuplicated } from "../../../lib/src/concurrency/lock.ts"; -import { finishAllWaitingForTimeout, finishWaitingForTimeout, isWaitingForTimeout, waitForTimeout } from "../../../lib/src/concurrency/task.ts"; +import { + finishAllWaitingForTimeout, + finishWaitingForTimeout, + isWaitingForTimeout, + waitForTimeout, +} from "../../../lib/src/concurrency/task.ts"; import { Semaphore } from "../../../lib/src/concurrency/semaphore.ts"; import type { LiveSyncCore } from "../../../main.ts"; import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts"; import ObsidianLiveSyncPlugin from "../../../main.ts"; // import { InternalFileToUXFileInfo } from "../platforms/obsidian.ts"; - export type FileEvent = { type: FileEventType; file: UXFileInfoStub | UXInternalFileInfoStub; @@ -21,19 +35,15 @@ export type FileEvent = { skipBatchWait?: boolean; }; - export abstract class StorageEventManager { abstract beginWatch(): void; abstract flushQueue(): void; abstract appendQueue(items: FileEvent[], ctx?: any): Promise; abstract cancelQueue(key: string): void; abstract isWaiting(filename: FilePath): boolean; - } - export class StorageEventManagerObsidian extends StorageEventManager { - plugin: ObsidianLiveSyncPlugin; core: LiveSyncCore; @@ -41,10 +51,10 @@ export class StorageEventManagerObsidian extends StorageEventManager { return this.core.settings?.batchSave && this.core.settings?.liveSync != true; } get batchSaveMinimumDelay(): number { - return this.core.settings?.batchSaveMinimumDelay ?? DEFAULT_SETTINGS.batchSaveMinimumDelay + return this.core.settings?.batchSaveMinimumDelay ?? DEFAULT_SETTINGS.batchSaveMinimumDelay; } get batchSaveMaximumDelay(): number { - return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay + return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay; } constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) { super(); @@ -83,10 +93,11 @@ export class StorageEventManagerObsidian extends StorageEventManager { } const data = info?.data as string; const fi: FileEvent = { - type: "CHANGED", file: TFileToUXFileInfoStub(file), cachedData: data, - } - void this.appendQueue([ - fi]) + type: "CHANGED", + file: TFileToUXFileInfoStub(file), + cachedData: data, + }; + void this.appendQueue([fi]); } watchVaultCreate(file: TAbstractFile, ctx?: any) { @@ -109,17 +120,27 @@ export class StorageEventManagerObsidian extends StorageEventManager { watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) { if (file instanceof TFile) { const fileInfo = TFileToUXFileInfoStub(file); - void this.appendQueue([ - { - type: "DELETE", file: { - path: oldFile as FilePath, name: file.name, stat: { - mtime: file.stat.mtime, - ctime: file.stat.ctime, - size: file.stat.size, - type: "file" - }, deleted: true - }, skipBatchWait: true - }, { type: "CREATE", file: fileInfo, skipBatchWait: true },], ctx); + void this.appendQueue( + [ + { + type: "DELETE", + file: { + path: oldFile as FilePath, + name: file.name, + stat: { + mtime: file.stat.mtime, + ctime: file.stat.ctime, + size: file.stat.size, + type: "file", + }, + deleted: true, + }, + skipBatchWait: true, + }, + { type: "CREATE", file: fileInfo, skipBatchWait: true }, + ], + ctx + ); } } // Watch raw events (Internal API) @@ -142,16 +163,23 @@ export class StorageEventManagerObsidian extends StorageEventManager { if (!path.startsWith(this.plugin.app.vault.configDir)) return; const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); - if (ignorePatterns.some(e => path.match(e))) return; + .split(",") + .filter((e) => e) + .map((e) => new RegExp(e, "i")); + if (ignorePatterns.some((e) => path.match(e))) return; if (path.endsWith("/")) { - // Folder + // Folder return; } - void this.appendQueue([ - { - type: "INTERNAL", file: InternalFileToUXFileInfoStub(path), - }], null); + void this.appendQueue( + [ + { + type: "INTERNAL", + file: InternalFileToUXFileInfoStub(path), + }, + ], + null + ); } // Cache file and waiting to can be proceed. async appendQueue(params: FileEvent[], ctx?: any) { @@ -164,25 +192,22 @@ export class StorageEventManagerObsidian extends StorageEventManager { if (shouldBeIgnored(param.file.path)) { continue; } - const atomicKey = [ - 0, - 0, - 0, - 0, - 0, - 0].map(e => `${Math.floor(Math.random() * 100000)}`).join("-"); + const atomicKey = [0, 0, 0, 0, 0, 0].map((e) => `${Math.floor(Math.random() * 100000)}`).join("-"); const type = param.type; const file = param.file; const oldPath = param.oldPath; if (type !== "INTERNAL") { const size = (file as UXFileInfoStub).stat.size; if (this.core.$$isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) { - Logger(`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`, LOG_LEVEL_NOTICE); + Logger( + `The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`, + LOG_LEVEL_NOTICE + ); continue; } } if (file instanceof TFolder) continue; - if (!await this.core.$$isTargetFile(file.path)) continue; + if (!(await this.core.$$isTargetFile(file.path))) continue; // Stop cache using to prevent the corruption; // let cache: null | string | ArrayBuffer; @@ -197,13 +222,19 @@ export class StorageEventManagerObsidian extends StorageEventManager { let cache: string | undefined = undefined; if (param.cachedData) { - cache = param.cachedData + cache = param.cachedData; } this.enqueue({ - type, args: { - file: file, oldPath, cache, ctx, - }, skipBatchWait: param.skipBatchWait, key: atomicKey - }) + type, + args: { + file: file, + oldPath, + cache, + ctx, + }, + skipBatchWait: param.skipBatchWait, + key: atomicKey, + }); processFiles.add(file.path as FilePath); if (oldPath) { processFiles.add(oldPath as FilePath); @@ -238,7 +269,7 @@ export class StorageEventManagerObsidian extends StorageEventManager { Logger(`Processing ${filename}: Started`, LOG_LEVEL_DEBUG); let noMoreFiles = false; do { - const target = this.bufferedQueuedItems.find(e => e.args.file.path == filename); + const target = this.bufferedQueuedItems.find((e) => e.args.file.path == filename); if (target === undefined) { noMoreFiles = true; break; @@ -251,7 +282,7 @@ export class StorageEventManagerObsidian extends StorageEventManager { // } const type = target.type; if (target.cancelled) { - Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG) + Logger(`Processing ${filename}: Cancelled (scheduled): ${operationType}`, LOG_LEVEL_DEBUG); this.cancelStandingBy(target); continue; } @@ -261,19 +292,31 @@ export class StorageEventManagerObsidian extends StorageEventManager { let canWait = true; const now = Date.now(); if (waitedSince !== undefined) { - if (waitedSince + (this.batchSaveMaximumDelay * 1000) < now) { - Logger(`Processing ${filename}: Could not wait no more: ${operationType}`, LOG_LEVEL_INFO) + if (waitedSince + this.batchSaveMaximumDelay * 1000 < now) { + Logger( + `Processing ${filename}: Could not wait no more: ${operationType}`, + LOG_LEVEL_INFO + ); canWait = false; } } if (canWait) { - if (waitedSince === undefined) this.waitedSince.set(filename, now) - target.batched = true - Logger(`Processing ${filename}: Waiting for batch save delay: ${operationType}`, LOG_LEVEL_DEBUG) + if (waitedSince === undefined) this.waitedSince.set(filename, now); + target.batched = true; + Logger( + `Processing ${filename}: Waiting for batch save delay: ${operationType}`, + LOG_LEVEL_DEBUG + ); this.updateStatus(); - const result = await waitForTimeout(`storage-event-manager-batchsave-${filename}`, this.batchSaveMinimumDelay * 1000); + const result = await waitForTimeout( + `storage-event-manager-batchsave-${filename}`, + this.batchSaveMinimumDelay * 1000 + ); if (!result) { - Logger(`Processing ${filename}: Cancelled by new queue: ${operationType}`, LOG_LEVEL_DEBUG) + Logger( + `Processing ${filename}: Cancelled by new queue: ${operationType}`, + LOG_LEVEL_DEBUG + ); // If could not wait for the timeout, possibly we got a new queue. therefore, currently processing one should be cancelled this.cancelStandingBy(target); continue; @@ -281,16 +324,19 @@ export class StorageEventManagerObsidian extends StorageEventManager { } } } else { - Logger(`Processing ${filename}:Requested to perform immediately ${filename}: ${operationType}`, LOG_LEVEL_DEBUG) + Logger( + `Processing ${filename}:Requested to perform immediately ${filename}: ${operationType}`, + LOG_LEVEL_DEBUG + ); } - Logger(`Processing ${filename}: Request main to process: ${operationType}`, LOG_LEVEL_DEBUG) + Logger(`Processing ${filename}: Request main to process: ${operationType}`, LOG_LEVEL_DEBUG); await this.requestProcessQueue(target); - } while (!noMoreFiles) + } while (!noMoreFiles); } finally { - release() + release(); } Logger(`Processing ${filename}: Finished`, LOG_LEVEL_DEBUG); - }) + }); } cancelStandingBy(fei: FileEventItem) { @@ -302,30 +348,30 @@ export class StorageEventManagerObsidian extends StorageEventManager { try { this.processingCount++; this.bufferedQueuedItems.remove(fei); - this.updateStatus() + this.updateStatus(); this.waitedSince.delete(fei.args.file.path); await this.handleFileEvent(fei); } finally { this.processingCount--; - this.updateStatus() + this.updateStatus(); } } isWaiting(filename: FilePath) { return isWaitingForTimeout(`storage-event-manager-batchsave-${filename}`); } flushQueue() { - this.bufferedQueuedItems.forEach(e => e.skipBatchWait = true) + this.bufferedQueuedItems.forEach((e) => (e.skipBatchWait = true)); finishAllWaitingForTimeout("storage-event-manager-batchsave-", true); } cancelQueue(key: string) { - this.bufferedQueuedItems.forEach(e => { - if (e.key === key) e.skipBatchWait = true - }) + this.bufferedQueuedItems.forEach((e) => { + if (e.key === key) e.skipBatchWait = true; + }); } updateStatus() { - const allItems = this.bufferedQueuedItems.filter(e => !e.cancelled) - const batchedCount = allItems.filter(e => e.batched && !e.skipBatchWait).length; - this.core.batched.value = batchedCount + const allItems = this.bufferedQueuedItems.filter((e) => !e.cancelled); + const batchedCount = allItems.filter((e) => e.batched && !e.skipBatchWait).length; + this.core.batched.value = batchedCount; this.core.processing.value = this.processingCount; this.core.totalQueued.value = allItems.length - batchedCount; } @@ -336,7 +382,7 @@ export class StorageEventManagerObsidian extends StorageEventManager { return await serialized(lockKey, async () => { // TODO CHECK const key = `file-last-proc-${queue.type}-${file.path}`; - const last = Number(await this.core.kvDB.get(key) || 0); + const last = Number((await this.core.kvDB.get(key)) || 0); if (queue.type == "INTERNAL" || file.isInternal) { await this.core.$anyProcessOptionalFileEvent(file.path as unknown as FilePath); } else { @@ -346,13 +392,16 @@ export class StorageEventManagerObsidian extends StorageEventManager { } else { if (file.stat.mtime == last) { Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE); - // Should Cancel the relative operations? (e.g. rename) + // Should Cancel the relative operations? (e.g. rename) // this.cancelRelativeEvent(queue); return; } - if (!await this.core.$anyHandlerProcessesFileEvent(queue)) { - Logger(`STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`, LOG_LEVEL_INFO); - // cancel running queues and remove one of atomic operation (e.g. rename) + if (!(await this.core.$anyHandlerProcessesFileEvent(queue))) { + Logger( + `STORAGE -> DB: Handler failed, cancel the relative operations: ${file.path}`, + LOG_LEVEL_INFO + ); + // cancel running queues and remove one of atomic operation (e.g. rename) this.cancelRelativeEvent(queue); return; } @@ -364,4 +413,4 @@ export class StorageEventManagerObsidian extends StorageEventManager { cancelRelativeEvent(item: FileEventItem): void { this.cancelQueue(item.key); } -} \ No newline at end of file +} diff --git a/src/modules/coreObsidian/storageLib/utilObsidian.ts b/src/modules/coreObsidian/storageLib/utilObsidian.ts index ed7be14..3e8ed71 100644 --- a/src/modules/coreObsidian/storageLib/utilObsidian.ts +++ b/src/modules/coreObsidian/storageLib/utilObsidian.ts @@ -6,10 +6,22 @@ import type { SerializedFileAccess } from "./SerializedFileAccess.ts"; import { addPrefix, isPlainText } from "../../../lib/src/string_and_binary/path.ts"; import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger"; import { createBlob } from "../../../lib/src/common/utils.ts"; -import type { FilePath, FilePathWithPrefix, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "../../../lib/src/common/types.ts"; +import type { + FilePath, + FilePathWithPrefix, + UXFileInfo, + UXFileInfoStub, + UXFolderInfo, + UXInternalFileInfoStub, +} from "../../../lib/src/common/types.ts"; import type { LiveSyncCore } from "../../../main.ts"; -export async function TFileToUXFileInfo(core: LiveSyncCore, file: TFile, prefix?: string, deleted?: boolean): Promise { +export async function TFileToUXFileInfo( + core: LiveSyncCore, + file: TFile, + prefix?: string, + deleted?: boolean +): Promise { const isPlain = isPlainText(file.name); const possiblyLarge = !isPlain; let content: Blob; @@ -34,11 +46,14 @@ export async function TFileToUXFileInfo(core: LiveSyncCore, file: TFile, prefix? type: "file", }, body: content, - } + }; } -export async function InternalFileToUXFileInfo(fullPath: string, vaultAccess: SerializedFileAccess, prefix: string = ICHeader): Promise { - +export async function InternalFileToUXFileInfo( + fullPath: string, + vaultAccess: SerializedFileAccess, + prefix: string = ICHeader +): Promise { const name = fullPath.split("/").pop() as string; const stat = await vaultAccess.adapterStat(fullPath); if (stat == null) throw new Error(`File not found: ${fullPath}`); @@ -64,7 +79,7 @@ export async function InternalFileToUXFileInfo(fullPath: string, vaultAccess: Se type: "file", }, body: content, - } + }; } export function TFileToUXFileInfoStub(file: TFile | TAbstractFile, deleted?: boolean): UXFileInfoStub { @@ -81,8 +96,8 @@ export function TFileToUXFileInfoStub(file: TFile | TAbstractFile, deleted?: boo ctime: file.stat.ctime, type: "file", }, - deleted: deleted - } + deleted: deleted, + }; return ret; } export function InternalFileToUXFileInfoStub(filename: FilePathWithPrefix, deleted?: boolean): UXInternalFileInfoStub { @@ -93,8 +108,8 @@ export function InternalFileToUXFileInfoStub(filename: FilePathWithPrefix, delet isFolder: false, stat: undefined, isInternal: true, - deleted - } + deleted, + }; return ret; } export function TFolderToUXFileInfoStub(file: TFolder): UXFolderInfo { @@ -103,7 +118,7 @@ export function TFolderToUXFileInfoStub(file: TFolder): UXFolderInfo { path: file.path as FilePathWithPrefix, parent: file.parent?.path as FilePath | undefined, isFolder: true, - children: file.children.map(e => TFileToUXFileInfoStub(e)), - } + children: file.children.map((e) => TFileToUXFileInfoStub(e)), + }; return ret; -} \ No newline at end of file +} diff --git a/src/modules/essential/ModuleInitializerFile.ts b/src/modules/essential/ModuleInitializerFile.ts index 01160cb..4d71ed4 100644 --- a/src/modules/essential/ModuleInitializerFile.ts +++ b/src/modules/essential/ModuleInitializerFile.ts @@ -3,22 +3,35 @@ import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; import { throttle } from "octagonal-wheels/function"; import { eventHub } from "../../common/events.ts"; import { BASE_IS_NEW, compareFileFreshness, EVEN, getPath, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts"; -import { type FilePathWithPrefixLC, type FilePathWithPrefix, type MetaEntry, isMetaEntry, type EntryDoc, LOG_LEVEL_VERBOSE, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, type UXFileInfoStub } from "../../lib/src/common/types.ts"; +import { + type FilePathWithPrefixLC, + type FilePathWithPrefix, + type MetaEntry, + isMetaEntry, + type EntryDoc, + LOG_LEVEL_VERBOSE, + LOG_LEVEL_NOTICE, + LOG_LEVEL_INFO, + LOG_LEVEL_DEBUG, + type UXFileInfoStub, +} from "../../lib/src/common/types.ts"; import { isAnyNote } from "../../lib/src/common/utils.ts"; import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleInitializerFile extends AbstractModule implements ICoreModule { - async $$performFullScan(showingNotice?: boolean): Promise { - this._log("Opening the key-value database", LOG_LEVEL_VERBOSE); - const isInitialized = await (this.core.kvDB.get("initialized")) || false; + const isInitialized = (await this.core.kvDB.get("initialized")) || false; // synchronize all files between database and storage. if (!this.settings.isConfigured) { if (showingNotice) { - this._log("LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.", LOG_LEVEL_NOTICE, "syncAll"); + this._log( + "LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.", + LOG_LEVEL_NOTICE, + "syncAll" + ); } return; } @@ -47,22 +60,25 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule return path as FilePathWithPrefixLC; } return (path as string).toLowerCase() as FilePathWithPrefixLC; - } + }; // If handleFilenameCaseSensitive is enabled, `FilePathWithPrefixLC` is the same as `FilePathWithPrefix`. - const storageFileNameMap = Object.fromEntries(_filesStorage.map((e) => [ - e.path, e] as [FilePathWithPrefix, UXFileInfoStub])); + const storageFileNameMap = Object.fromEntries( + _filesStorage.map((e) => [e.path, e] as [FilePathWithPrefix, UXFileInfoStub]) + ); const storageFileNames = Object.keys(storageFileNameMap) as FilePathWithPrefix[]; - const storageFileNameCapsPair = storageFileNames.map((e) => [ - e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]); + const storageFileNameCapsPair = storageFileNames.map( + (e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC] + ); // const storageFileNameCS2CI = Object.fromEntries(storageFileNameCapsPair) as Record; - const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map(e => [ - e[1], e[0]])) as Record; - + const storageFileNameCI2CS = Object.fromEntries(storageFileNameCapsPair.map((e) => [e[1], e[0]])) as Record< + FilePathWithPrefixLC, + FilePathWithPrefix + >; this._log("Collecting local files on the DB", LOG_LEVEL_VERBOSE); const _DBEntries = [] as MetaEntry[]; @@ -70,10 +86,15 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule let count = 0; for await (const doc of this.localDatabase.findAllNormalDocs()) { count++; - if (count % 25 == 0) this._log(`Collecting local files on the DB: ${count}`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll"); + if (count % 25 == 0) + this._log( + `Collecting local files on the DB: ${count}`, + showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, + "syncAll" + ); const path = getPath(doc); - if (isValidPath(path) && await this.core.$$isTargetFile(path, true)) { + if (isValidPath(path) && (await this.core.$$isTargetFile(path, true))) { if (!isMetaEntry(doc)) { this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO); continue; @@ -82,24 +103,28 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule } } - const databaseFileNameMap = Object.fromEntries(_DBEntries.map((e) => [ - getPath(e), e] as [FilePathWithPrefix, MetaEntry])); + const databaseFileNameMap = Object.fromEntries( + _DBEntries.map((e) => [getPath(e), e] as [FilePathWithPrefix, MetaEntry]) + ); const databaseFileNames = Object.keys(databaseFileNameMap) as FilePathWithPrefix[]; - const databaseFileNameCapsPair = databaseFileNames.map((e) => [ - e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC]); + const databaseFileNameCapsPair = databaseFileNames.map( + (e) => [e, convertCase(e)] as [FilePathWithPrefix, FilePathWithPrefixLC] + ); // const databaseFileNameCS2CI = Object.fromEntries(databaseFileNameCapsPair) as Record; - const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map(e => [ - e[1], e[0]])) as Record; + const databaseFileNameCI2CS = Object.fromEntries(databaseFileNameCapsPair.map((e) => [e[1], e[0]])) as Record< + FilePathWithPrefix, + FilePathWithPrefixLC + >; const allFiles = unique([ ...Object.keys(databaseFileNameCI2CS), - ...Object.keys(storageFileNameCI2CS)]) as FilePathWithPrefixLC[]; + ...Object.keys(storageFileNameCI2CS), + ]) as FilePathWithPrefixLC[]; this._log(`Total files in the database: ${databaseFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll"); this._log(`Total files in the storage: ${storageFileNames.length}`, LOG_LEVEL_VERBOSE, "syncAll"); this._log(`Total files: ${allFiles.length}`, LOG_LEVEL_VERBOSE, "syncAll"); - const filesExistOnlyInStorage = allFiles.filter((e) => !databaseFileNameCI2CS[e]); const filesExistOnlyInDatabase = allFiles.filter((e) => !storageFileNameCI2CS[e]); const filesExistBoth = allFiles.filter((e) => databaseFileNameCI2CS[e] && storageFileNameCI2CS[e]); @@ -128,92 +153,110 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule let success = 0; let failed = 0; const step = 10; - const processor = new QueueProcessor(async (e) => { - try { - await callback(e[0]); - success++; - // return - } catch (ex) { - this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE); - this._log(ex, LOG_LEVEL_VERBOSE); - failed++; - } - if ((success + failed) % step == 0) { - const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`; - updateLog(procedureName, msg); - } - return; - }, { - batchSize: 1, - concurrentLimit: 10, - delay: 0, - suspended: true, - maintainDelay: false, - interval: 0 - }, objects) + const processor = new QueueProcessor( + async (e) => { + try { + await callback(e[0]); + success++; + // return + } catch (ex) { + this._log(`Error while ${procedureName}`, LOG_LEVEL_NOTICE); + this._log(ex, LOG_LEVEL_VERBOSE); + failed++; + } + if ((success + failed) % step == 0) { + const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`; + updateLog(procedureName, msg); + } + return; + }, + { + batchSize: 1, + concurrentLimit: 10, + delay: 0, + suspended: true, + maintainDelay: false, + interval: 0, + }, + objects + ); await processor.waitForAllDoneAndTerminate(); const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`; - updateLog(procedureName, msg) - } - initProcess.push(runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => { - // console.warn("UPDATE DATABASE", e); - const file = storageFileNameMap[storageFileNameCI2CS[e]]; - if (!this.core.$$isFileSizeExceeded(file.stat.size)) { - const path = file.path; - await this.core.fileHandler.storeFileToDB(file); - // fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true)); - eventHub.emitEvent("event-file-changed", { file: path, automated: true }); - } else { - this._log(`UPDATE DATABASE: ${e} has been skipped due to file size exceeding the limit`, logLevel); - } - })); - initProcess.push(runAll("UPDATE STORAGE", filesExistOnlyInDatabase, async (e) => { - const w = databaseFileNameMap[databaseFileNameCI2CS[e]]; - const path = getPath(w) ?? e; - if (w && !(w.deleted || w._deleted)) { - if (!this.core.$$isFileSizeExceeded(w.size)) { - // await this.pullFile(path, undefined, false, undefined, false); - // Memo: No need to force - await this.core.fileHandler.dbToStorage(path, null, true); - // fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true)); - eventHub.emitEvent("event-file-changed", { - file: e, automated: true - }); - this._log(`Check or pull from db:${path} OK`); + updateLog(procedureName, msg); + }; + initProcess.push( + runAll("UPDATE DATABASE", filesExistOnlyInStorage, async (e) => { + // console.warn("UPDATE DATABASE", e); + const file = storageFileNameMap[storageFileNameCI2CS[e]]; + if (!this.core.$$isFileSizeExceeded(file.stat.size)) { + const path = file.path; + await this.core.fileHandler.storeFileToDB(file); + // fireAndForget(() => this.checkAndApplySettingFromMarkdown(path, true)); + eventHub.emitEvent("event-file-changed", { file: path, automated: true }); } else { - this._log(`UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`, logLevel); + this._log(`UPDATE DATABASE: ${e} has been skipped due to file size exceeding the limit`, logLevel); } - } else if (w) { - this._log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE); - } else { - this._log(`entry not found: ${path}`); - } - })); + }) + ); + initProcess.push( + runAll("UPDATE STORAGE", filesExistOnlyInDatabase, async (e) => { + const w = databaseFileNameMap[databaseFileNameCI2CS[e]]; + const path = getPath(w) ?? e; + if (w && !(w.deleted || w._deleted)) { + if (!this.core.$$isFileSizeExceeded(w.size)) { + // await this.pullFile(path, undefined, false, undefined, false); + // Memo: No need to force + await this.core.fileHandler.dbToStorage(path, null, true); + // fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true)); + eventHub.emitEvent("event-file-changed", { + file: e, + automated: true, + }); + this._log(`Check or pull from db:${path} OK`); + } else { + this._log( + `UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`, + logLevel + ); + } + } else if (w) { + this._log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE); + } else { + this._log(`entry not found: ${path}`); + } + }) + ); - const fileMap = filesExistBoth.map(path => { + const fileMap = filesExistBoth.map((path) => { const file = storageFileNameMap[storageFileNameCI2CS[path]]; const doc = databaseFileNameMap[databaseFileNameCI2CS[path]]; - return { file, doc } - }) - initProcess.push(runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => { - const { file, doc } = e; - if (!this.core.$$isFileSizeExceeded(file.stat.size) && !this.core.$$isFileSizeExceeded(doc.size)) { - await this.syncFileBetweenDBandStorage(file, doc); - // fireAndForget(() => this.checkAndApplySettingFromMarkdown(getPath(doc), true)); - eventHub.emitEvent("event-file-changed", { - file: getPath(doc), automated: true - }); - } else { - this._log(`SYNC DATABASE AND STORAGE: ${getPath(doc)} has been skipped due to file size exceeding the limit`, logLevel); - } - })) + return { file, doc }; + }); + initProcess.push( + runAll("SYNC DATABASE AND STORAGE", fileMap, async (e) => { + const { file, doc } = e; + if (!this.core.$$isFileSizeExceeded(file.stat.size) && !this.core.$$isFileSizeExceeded(doc.size)) { + await this.syncFileBetweenDBandStorage(file, doc); + // fireAndForget(() => this.checkAndApplySettingFromMarkdown(getPath(doc), true)); + eventHub.emitEvent("event-file-changed", { + file: getPath(doc), + automated: true, + }); + } else { + this._log( + `SYNC DATABASE AND STORAGE: ${getPath(doc)} has been skipped due to file size exceeding the limit`, + logLevel + ); + } + }) + ); await Promise.all(initProcess); // this.setStatusBarText(`NOW TRACKING!`); this._log("Initialized, NOW TRACKING!"); if (!isInitialized) { - await (this.core.kvDB.set("initialized", true)) + await this.core.kvDB.set("initialized", true); } if (showingNotice) { this._log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll"); @@ -222,14 +265,14 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule async syncFileBetweenDBandStorage(file: UXFileInfoStub, doc: MetaEntry) { if (!doc) { - throw new Error(`Missing doc:${(file as any).path}`) + throw new Error(`Missing doc:${(file as any).path}`); } if ("path" in file) { const w = this.core.storageAccess.getFileStub((file as any).path); if (w) { file = w; } else { - throw new Error(`Missing file:${(file as any).path}`) + throw new Error(`Missing file:${(file as any).path}`); } } @@ -240,21 +283,28 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule this._log("STORAGE -> DB :" + file.path); await this.core.fileHandler.storeFileToDB(file); eventHub.emitEvent("event-file-changed", { - file: file.path, automated: true + file: file.path, + automated: true, }); } else { - this._log(`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE); + this._log( + `STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`, + LOG_LEVEL_NOTICE + ); } break; case TARGET_IS_NEW: if (!this.core.$$isFileSizeExceeded(doc.size)) { this._log("STORAGE <- DB :" + file.path); - if (!await this.core.fileHandler.dbToStorage(doc, stripAllPrefixes(file.path), true)) { + if (!(await this.core.fileHandler.dbToStorage(doc, stripAllPrefixes(file.path), true))) { this._log(`STORAGE <- DB : Cloud not read ${file.path}, possibly deleted`, LOG_LEVEL_NOTICE); } return caches; } else { - this._log(`STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE); + this._log( + `STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`, + LOG_LEVEL_NOTICE + ); } break; case EVEN: @@ -263,31 +313,29 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule default: this._log("STORAGE ?? DB :" + file.path + " Something got weird"); } - } - // This method uses an old version of database accessor, which is not recommended. // TODO: Fix async collectDeletedFiles() { const limitDays = this.settings.automaticallyDeleteMetadataOfDeletedFiles; if (limitDays <= 0) return; this._log(`Checking expired file history`); - const limit = Date.now() - (86400 * 1000 * limitDays); + const limit = Date.now() - 86400 * 1000 * limitDays; const notes: { - path: string, - mtime: number, - ttl: number, - doc: PouchDB.Core.ExistingDocument + path: string; + mtime: number; + ttl: number; + doc: PouchDB.Core.ExistingDocument; }[] = []; for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) { if (isAnyNote(doc)) { - if (doc.deleted && (doc.mtime - limit) < 0) { + if (doc.deleted && doc.mtime - limit < 0) { notes.push({ path: getPath(doc), mtime: doc.mtime, ttl: (doc.mtime - limit) / 1000 / 86400, - doc: doc + doc: doc, }); } } @@ -308,11 +356,11 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule async $$initializeDatabase(showingNotice: boolean = false, reopenDatabase = true): Promise { this.core.$$resetIsReady(); - if ((!reopenDatabase) || await this.core.$$openDatabase()) { + if (!reopenDatabase || (await this.core.$$openDatabase())) { if (this.localDatabase.isReady) { await this.core.$$performFullScan(showingNotice); } - if (!await this.core.$everyOnDatabaseInitialized(showingNotice)) { + if (!(await this.core.$everyOnDatabaseInitialized(showingNotice))) { this._log(`Initializing database has been failed on some module`, LOG_LEVEL_NOTICE); return false; } @@ -325,4 +373,4 @@ export class ModuleInitializerFile extends AbstractModule implements ICoreModule return false; } } -} \ No newline at end of file +} diff --git a/src/modules/essential/ModuleKeyValueDB.ts b/src/modules/essential/ModuleKeyValueDB.ts index ead5949..b5d3209 100644 --- a/src/modules/essential/ModuleKeyValueDB.ts +++ b/src/modules/essential/ModuleKeyValueDB.ts @@ -6,7 +6,6 @@ import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleKeyValueDB extends AbstractModule implements ICoreModule { - tryCloseKvDB() { try { this.core.kvDB?.close(); @@ -42,7 +41,7 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule { } async $everyOnloadAfterLoadSettings(): Promise { - if (!await this.openKeyValueDB()) { + if (!(await this.openKeyValueDB())) { return false; } this.core.simpleStore = this.core.$$getSimpleStore("os"); @@ -60,11 +59,21 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule { delete: async (key: string): Promise => { await this.core.kvDB.del(`${prefix}${key}`); }, - keys: async (from: string | undefined, to: string | undefined, count?: number | undefined): Promise => { - const ret = this.core.kvDB.keys(IDBKeyRange.bound(`${prefix}${from || ""}`, `${prefix}${to || ""}`), count); - return (await ret).map(e => e.toString()).filter(e => e.startsWith(prefix)).map(e => e.substring(prefix.length)); - } - } + keys: async ( + from: string | undefined, + to: string | undefined, + count?: number | undefined + ): Promise => { + const ret = this.core.kvDB.keys( + IDBKeyRange.bound(`${prefix}${from || ""}`, `${prefix}${to || ""}`), + count + ); + return (await ret) + .map((e) => e.toString()) + .filter((e) => e.startsWith(prefix)) + .map((e) => e.substring(prefix.length)); + }, + }; } $everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise { return this.openKeyValueDB(); @@ -72,7 +81,7 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule { async $everyOnResetDatabase(db: LiveSyncLocalDB): Promise { try { - const kvDBKey = "queued-files" + const kvDBKey = "queued-files"; await this.core.kvDB.del(kvDBKey); // localStorage.removeItem(lsKey); await this.core.kvDB.destroy(); @@ -87,4 +96,4 @@ export class ModuleKeyValueDB extends AbstractModule implements ICoreModule { } return true; } -} \ No newline at end of file +} diff --git a/src/modules/essential/ModuleMigration.ts b/src/modules/essential/ModuleMigration.ts index 701e1c7..630e48d 100644 --- a/src/modules/essential/ModuleMigration.ts +++ b/src/modules/essential/ModuleMigration.ts @@ -1,16 +1,23 @@ -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from 'octagonal-wheels/common/logger.js'; -import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from '../../lib/src/common/types.js'; -import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from '../../common/events.ts'; +import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js"; +import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from "../../lib/src/common/types.js"; +import { + EVENT_REQUEST_OPEN_SETTING_WIZARD, + EVENT_REQUEST_OPEN_SETTINGS, + EVENT_REQUEST_OPEN_SETUP_URI, + eventHub, +} from "../../common/events.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; const URI_DOC = "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; export class ModuleMigration extends AbstractModule implements ICoreModule { - async migrateDisableBulkSend() { if (this.settings.sendChunksBulk) { - this._log("Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled.", LOG_LEVEL_NOTICE); + this._log( + "Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled.", + LOG_LEVEL_NOTICE + ); this.settings.sendChunksBulk = false; this.settings.sendChunksBulkMaxSize = 1; await this.saveSettings(); @@ -20,20 +27,27 @@ export class ModuleMigration extends AbstractModule implements ICoreModule { const old = this.settings.settingVersion; const current = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; // Check each migrations(old -> current) - if (!await this.migrateToCaseInsensitive(old, current)) { + if (!(await this.migrateToCaseInsensitive(old, current))) { this._log(`Migration failed or cancelled from ${old} to ${current}`, LOG_LEVEL_NOTICE); return; } } async migrateToCaseInsensitive(old: number, current: number) { - if (this.settings.handleFilenameCaseSensitive !== undefined && this.settings.doNotUseFixedRevisionForChunks !== undefined) { + if ( + this.settings.handleFilenameCaseSensitive !== undefined && + this.settings.doNotUseFixedRevisionForChunks !== undefined + ) { if (current < SETTING_VERSION_SUPPORT_CASE_INSENSITIVE) { this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; await this.saveSettings(); } return true; } - if (old >= SETTING_VERSION_SUPPORT_CASE_INSENSITIVE && this.settings.handleFilenameCaseSensitive !== undefined && this.settings.doNotUseFixedRevisionForChunks !== undefined) { + if ( + old >= SETTING_VERSION_SUPPORT_CASE_INSENSITIVE && + this.settings.handleFilenameCaseSensitive !== undefined && + this.settings.doNotUseFixedRevisionForChunks !== undefined + ) { return true; } @@ -43,9 +57,14 @@ export class ModuleMigration extends AbstractModule implements ICoreModule { try { const remoteInfo = await this.core.replicator.getRemotePreferredTweakValues(this.settings); if (remoteInfo) { - remoteHandleFilenameCaseSensitive = "handleFilenameCaseSensitive" in remoteInfo ? remoteInfo.handleFilenameCaseSensitive : false; - remoteDoNotUseFixedRevisionForChunks = "doNotUseFixedRevisionForChunks" in remoteInfo ? remoteInfo.doNotUseFixedRevisionForChunks : false; - if (remoteHandleFilenameCaseSensitive !== undefined || remoteDoNotUseFixedRevisionForChunks !== undefined) { + remoteHandleFilenameCaseSensitive = + "handleFilenameCaseSensitive" in remoteInfo ? remoteInfo.handleFilenameCaseSensitive : false; + remoteDoNotUseFixedRevisionForChunks = + "doNotUseFixedRevisionForChunks" in remoteInfo ? remoteInfo.doNotUseFixedRevisionForChunks : false; + if ( + remoteHandleFilenameCaseSensitive !== undefined || + remoteDoNotUseFixedRevisionForChunks !== undefined + ) { remoteChecked = true; } } else { @@ -79,7 +98,13 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an const OPTION_FETCH = "Yes, fetch again"; const DISMISS = "No, please ask again"; const options = [OPTION_FETCH, DISMISS]; - const ret = await this.core.confirm.confirmWithMessage("Case Sensitivity", message, options, "No, please ask again", 40); + const ret = await this.core.confirm.confirmWithMessage( + "Case Sensitivity", + message, + options, + "No, please ask again", + 40 + ); if (ret == OPTION_FETCH) { this.settings.handleFilenameCaseSensitive = remoteHandleFilenameCaseSensitive || false; this.settings.doNotUseFixedRevisionForChunks = remoteDoNotUseFixedRevisionForChunks || false; @@ -96,7 +121,6 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an } else { return false; } - } const ENABLE_BOTH = "Enable both"; @@ -120,10 +144,7 @@ ___However, to enable either of these changes, both remote and local databases n - If you do not have enough time, please choose \`${DISMISS}\`. You will be prompted again later. - If you have rebuilt the database on another device, please select \`${DISMISS}\` and try synchronizing again. Since a difference has been detected, you will be prompted again. `; - const options = [ - ENABLE_BOTH, - ENABLE_FILENAME_CASE_INSENSITIVE, - ENABLE_FIXED_REVISION_FOR_CHUNKS]; + const options = [ENABLE_BOTH, ENABLE_FILENAME_CASE_INSENSITIVE, ENABLE_FIXED_REVISION_FOR_CHUNKS]; if (remoteChecked) { options.push(ADJUST_TO_REMOTE); } @@ -152,13 +173,11 @@ ___However, to enable either of these changes, both remote and local databases n case DISMISS: default: return false; - } this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; await this.saveSettings(); await this.core.rebuilder.scheduleRebuild(); await this.core.$$performRestart(); - } async initialMessage() { @@ -174,16 +193,14 @@ Note: If you do not know what it is, please refer to the [documentation](${URI_D const USE_SETUP = "Yes, I have"; const NEXT = "No, I do not have"; - const ret = await this.core.confirm.askSelectStringDialogue(message, [ - USE_SETUP, NEXT], { + const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_SETUP, NEXT], { title: "Welcome to Self-hosted LiveSync", - defaultAction: USE_SETUP + defaultAction: USE_SETUP, }); if (ret === USE_SETUP) { eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI); return false; - } - else if (ret == NEXT) { + } else if (ret == NEXT) { return true; } return false; @@ -199,10 +216,9 @@ How do you want to set it up manually?`; const USE_SETUP = "Set it up all manually"; const NEXT = "Remind me at the next launch"; - const ret = await this.core.confirm.askSelectStringDialogue(message, [ - USE_MINIMAL, USE_SETUP, NEXT], { + const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], { title: "Recommendation to use Setup URI", - defaultAction: USE_MINIMAL + defaultAction: USE_MINIMAL, }); if (ret === USE_MINIMAL) { eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD); @@ -211,8 +227,7 @@ How do you want to set it up manually?`; if (ret === USE_SETUP) { eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS); return false; - } - else if (ret == NEXT) { + } else if (ret == NEXT) { return false; } return false; @@ -229,12 +244,14 @@ How do you want to set it up manually?`; } if (!this.settings.isConfigured) { // Case sensitivity - if (!await this.initialMessage() || !await this.askAgainForSetupURI()) { - this._log("The setup has been cancelled, Self-hosted LiveSync waiting for your setup!", LOG_LEVEL_NOTICE); + if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) { + this._log( + "The setup has been cancelled, Self-hosted LiveSync waiting for your setup!", + LOG_LEVEL_NOTICE + ); return false; } - } return true; } -} \ No newline at end of file +} diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 289ae27..45002d9 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -1,12 +1,9 @@ // This file is based on a file that was published by the @remotely-save, under the Apache 2 License. // I would love to express my deepest gratitude to the original authors for their hard work and dedication. Without their contributions, this project would not have been possible. -// +// // Original Implementation is here: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts -import { - FetchHttpHandler, - type FetchHttpHandlerOptions, -} from "@smithy/fetch-http-handler"; +import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler"; import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http"; //@ts-ignore import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout"; @@ -25,20 +22,13 @@ import { requestUrl, type RequestUrlParam } from "../../../deps.ts"; export class ObsHttpHandler extends FetchHttpHandler { requestTimeoutInMs: number | undefined; reverseProxyNoSignUrl: string | undefined; - constructor( - options?: FetchHttpHandlerOptions, - reverseProxyNoSignUrl?: string - ) { + constructor(options?: FetchHttpHandlerOptions, reverseProxyNoSignUrl?: string) { super(options); - this.requestTimeoutInMs = - options === undefined ? undefined : options.requestTimeout; + this.requestTimeoutInMs = options === undefined ? undefined : options.requestTimeout; this.reverseProxyNoSignUrl = reverseProxyNoSignUrl; } // eslint-disable-next-line require-await - async handle( - request: HttpRequest, - { abortSignal }: HttpHandlerOptions = {} - ): Promise<{ response: HttpResponse }> { + async handle(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse }> { if (abortSignal?.aborted) { const abortError = new Error("Request aborted"); abortError.name = "AbortError"; @@ -54,18 +44,13 @@ export class ObsHttpHandler extends FetchHttpHandler { } const { port, method } = request; - let url = `${request.protocol}//${request.hostname}${port ? `:${port}` : "" - }${path}`; - if ( - this.reverseProxyNoSignUrl !== undefined && - this.reverseProxyNoSignUrl !== "" - ) { + let url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ""}${path}`; + if (this.reverseProxyNoSignUrl !== undefined && this.reverseProxyNoSignUrl !== "") { const urlObj = new URL(url); urlObj.host = this.reverseProxyNoSignUrl; url = urlObj.href; } - const body = - method === "GET" || method === "HEAD" ? undefined : request.body; + const body = method === "GET" || method === "HEAD" ? undefined : request.body; const transformedHeaders: Record = {}; for (const key of Object.keys(request.headers)) { diff --git a/src/modules/essentialObsidian/ModuleObsidianAPI.ts b/src/modules/essentialObsidian/ModuleObsidianAPI.ts index 1160295..1ab1532 100644 --- a/src/modules/essentialObsidian/ModuleObsidianAPI.ts +++ b/src/modules/essentialObsidian/ModuleObsidianAPI.ts @@ -1,17 +1,22 @@ -import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts'; -import { LOG_LEVEL_DEBUG, LOG_LEVEL_VERBOSE } from 'octagonal-wheels/common/logger'; -import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from '../../deps.ts'; -import { type EntryDoc, type FilePathWithPrefix } from '../../lib/src/common/types.ts'; -import { getPathFromTFile } from '../../common/utils.ts'; -import { disableEncryption, enableEncryption, isCloudantURI, isValidRemoteCouchDBURI, replicationFilter } from '../../lib/src/pouchdb/utils_couchdb.ts'; -import { setNoticeClass } from '../../lib/src/mock_and_interop/wrapper.ts'; -import { ObsHttpHandler } from './APILib/ObsHttpHandler.ts'; -import { PouchDB } from '../../lib/src/pouchdb/pouchdb-browser.ts'; -import { reactive, reactiveSource } from 'octagonal-wheels/dataobject/reactive'; +import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; +import { LOG_LEVEL_DEBUG, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; +import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts"; +import { type EntryDoc, type FilePathWithPrefix } from "../../lib/src/common/types.ts"; +import { getPathFromTFile } from "../../common/utils.ts"; +import { + disableEncryption, + enableEncryption, + isCloudantURI, + isValidRemoteCouchDBURI, + replicationFilter, +} from "../../lib/src/pouchdb/utils_couchdb.ts"; +import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts"; +import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts"; +import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts"; +import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive"; setNoticeClass(Notice); - async function fetchByAPI(request: RequestUrlParam): Promise { const ret = await requestUrl(request); if (ret.status - (ret.status % 100) !== 200) { @@ -27,11 +32,11 @@ async function fetchByAPI(request: RequestUrlParam): Promise } export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidianModule { - _customHandler!: ObsHttpHandler; authHeaderSource = reactiveSource(""); authHeader = reactive(() => - this.authHeaderSource.value == "" ? "" : "Basic " + window.btoa(this.authHeaderSource.value)); + this.authHeaderSource.value == "" ? "" : "Basic " + window.btoa(this.authHeaderSource.value) + ); last_successful_post = false; $$customFetchHandler(): ObsHttpHandler { @@ -42,11 +47,20 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi return !this.last_successful_post; } - async $$connectRemoteCouchDB(uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean): Promise; info: PouchDB.Core.DatabaseInfo }> { + async $$connectRemoteCouchDB( + uri: string, + auth: { username: string; password: string }, + disableRequestURI: boolean, + passphrase: string | false, + useDynamicIterationCount: boolean, + performSetup: boolean, + skipInfo: boolean, + compression: boolean + ): Promise; info: PouchDB.Core.DatabaseInfo }> { if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid"; if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters."; if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces."; - const userNameAndPassword = (auth.username && auth.password) ? `${auth.username}:${auth.password}` : ""; + const userNameAndPassword = auth.username && auth.password ? `${auth.username}:${auth.password}` : ""; if (this.authHeaderSource.value != userNameAndPassword) { this.authHeaderSource.value = userNameAndPassword; } @@ -135,7 +149,9 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi if (Math.floor(response.status / 100) !== 2) { if (method != "GET" && localURL.indexOf("/_local/") === -1 && !localURL.endsWith("/")) { const r = response.clone(); - this._log(`The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}`); + this._log( + `The request may have failed. The reason sent by the server: ${r.status}: ${r.statusText}` + ); try { this._log(await (await r.blob()).text(), LOG_LEVEL_VERBOSE); @@ -144,7 +160,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi this._log(_, LOG_LEVEL_VERBOSE); } } else { - this._log(`Just checkpoint or some server information has been missing. The 404 error shown above is not an error.`, LOG_LEVEL_VERBOSE) + this._log( + `Just checkpoint or some server information has been missing. The 404 error shown above is not an error.`, + LOG_LEVEL_VERBOSE + ); } } return response; @@ -178,7 +197,8 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi } catch (ex: any) { let msg = `${ex?.name}:${ex?.message}`; if (ex?.name == "TypeError" && ex?.message == "Failed to fetch") { - msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector."; + msg += + "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector."; } this._log(ex, LOG_LEVEL_VERBOSE); return msg; @@ -194,7 +214,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi return this.app.vault.getName(); } $$getVaultName(): string { - return this.core.$$vaultName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : ""); + return ( + this.core.$$vaultName() + + (this.settings?.additionalSuffixOfDatabaseName ? "-" + this.settings.additionalSuffixOfDatabaseName : "") + ); } $$getActiveFilePath(): FilePathWithPrefix | undefined { const file = this.app.workspace.getActiveFile(); @@ -205,7 +228,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi } $anyGetAppId(): Promise { - return Promise.resolve(`${("appId" in this.app ? this.app.appId : "")}`); + return Promise.resolve(`${"appId" in this.app ? this.app.appId : ""}`); } - -} \ No newline at end of file +} diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.ts index c9da6ef..c420163 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.ts @@ -1,25 +1,35 @@ -import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts'; -import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from '../../common/events.js'; -import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from 'octagonal-wheels/common/logger'; -import { scheduleTask } from 'octagonal-wheels/concurrency/task'; -import { type TFile } from '../../deps.ts'; -import { fireAndForget } from 'octagonal-wheels/promises'; -import { type FilePathWithPrefix } from '../../lib/src/common/types.ts'; -import { reactive, reactiveSource } from 'octagonal-wheels/dataobject/reactive'; -import { collectingChunks, pluginScanningCount, hiddenFilesEventCount, hiddenFilesProcessingCount } from '../../lib/src/mock_and_interop/stores.ts'; +import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; +import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "../../common/events.js"; +import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; +import { scheduleTask } from "octagonal-wheels/concurrency/task"; +import { type TFile } from "../../deps.ts"; +import { fireAndForget } from "octagonal-wheels/promises"; +import { type FilePathWithPrefix } from "../../lib/src/common/types.ts"; +import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive"; +import { + collectingChunks, + pluginScanningCount, + hiddenFilesEventCount, + hiddenFilesProcessingCount, +} from "../../lib/src/mock_and_interop/stores.ts"; export class ModuleObsidianEvents extends AbstractObsidianModule implements IObsidianModule { - $everyOnloadStart(): Promise { // this.registerEvent(this.app.workspace.on("editor-change", )); - this.plugin.registerEvent(this.app.vault.on("rename", (file, oldPath) => { - eventHub.emitEvent(EVENT_FILE_RENAMED, { newPath: file.path as FilePathWithPrefix, old: oldPath as FilePathWithPrefix }); - })); - this.plugin.registerEvent(this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED))); + this.plugin.registerEvent( + this.app.vault.on("rename", (file, oldPath) => { + eventHub.emitEvent(EVENT_FILE_RENAMED, { + newPath: file.path as FilePathWithPrefix, + old: oldPath as FilePathWithPrefix, + }); + }) + ); + this.plugin.registerEvent( + this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED)) + ); return Promise.resolve(true); } - $$performRestart(): void { this._performAppReload(); } @@ -33,9 +43,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs swapSaveCommand() { this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE); - const saveCommandDefinition = (this.app as any).commands?.commands?.[ - "editor:save-file" - ]; + const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"]; const save = saveCommandDefinition?.callback; if (typeof save === "function") { this.initialCallback = save; @@ -60,7 +68,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs //@ts-ignore window.CodeMirrorAdapter.commands.save = () => { //@ts-ignore - _this.app.commands.executeCommandById('editor:save-file') + _this.app.commands.executeCommandById("editor:save-file"); // _this.app.performCommand('editor:save-file'); }; } @@ -158,7 +166,6 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs return Promise.resolve(true); } - $$askReload(message?: string) { if (this.core.$$isReloadingScheduled()) { this._log(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE); @@ -168,16 +175,17 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs const RESTART_NOW = "Yes, restart immediately"; const RESTART_AFTER_STABLE = "Yes, schedule a restart after stabilisation"; const RETRY_LATER = "No, Leave it to me"; - const ret = await this.core.confirm.askSelectStringDialogue(message || "Do you want to restart and reload Obsidian now?", [ - RESTART_AFTER_STABLE, - RESTART_NOW, - RETRY_LATER], { defaultAction: RETRY_LATER }); + const ret = await this.core.confirm.askSelectStringDialogue( + message || "Do you want to restart and reload Obsidian now?", + [RESTART_AFTER_STABLE, RESTART_NOW, RETRY_LATER], + { defaultAction: RETRY_LATER } + ); if (ret == RESTART_NOW) { this._performAppReload(); } else if (ret == RESTART_AFTER_STABLE) { this.core.$$scheduleAppReload(); } - }) + }); } $$scheduleAppReload() { if (!this.core._totalProcessingCount) { @@ -194,24 +202,39 @@ export class ModuleObsidianEvents extends AbstractObsidianModule implements IObs const proc = this.core.processingFileEventCount.value; // eslint-disable-next-line @typescript-eslint/no-unused-vars const __ = __tick.value; - return dbCount + replicationCount + storageApplyingCount + chunkCount + pluginScanCount + hiddenFilesCount + conflictProcessCount + e + proc; - }) - this.plugin.registerInterval(setInterval(() => { - __tick.value++; - }, 1000) as unknown as number); + return ( + dbCount + + replicationCount + + storageApplyingCount + + chunkCount + + pluginScanCount + + hiddenFilesCount + + conflictProcessCount + + e + + proc + ); + }); + this.plugin.registerInterval( + setInterval(() => { + __tick.value++; + }, 1000) as unknown as number + ); let stableCheck = 3; - this.core._totalProcessingCount.onChanged(e => { + this.core._totalProcessingCount.onChanged((e) => { if (e.value == 0) { if (stableCheck-- <= 0) { this._performAppReload(); } - this._log(`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`, LOG_LEVEL_NOTICE, "restart-notice"); + this._log( + `Obsidian will be restarted soon! (Within ${stableCheck} seconds)`, + LOG_LEVEL_NOTICE, + "restart-notice" + ); } else { stableCheck = 3; } - }) + }); } } - -} \ No newline at end of file +} diff --git a/src/modules/essentialObsidian/ModuleObsidianMenu.ts b/src/modules/essentialObsidian/ModuleObsidianMenu.ts index 778c3ae..a373c97 100644 --- a/src/modules/essentialObsidian/ModuleObsidianMenu.ts +++ b/src/modules/essentialObsidian/ModuleObsidianMenu.ts @@ -4,9 +4,7 @@ import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/ import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule { - $everyOnloadStart(): Promise { - // UI addIcon( "replicate", @@ -22,7 +20,6 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid await this.core.$$replicate(true); }).addClass("livesync-ribbon-replicate"); - this.addCommand({ id: "livesync-replicate", name: "Replicate now", @@ -84,9 +81,9 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid id: "livesync-scan-files", name: "Scan storage and database again", callback: async () => { - await this.core.$$performFullScan(true) - } - }) + await this.core.$$performFullScan(true); + }, + }); this.addCommand({ id: "livesync-runbatch", @@ -94,7 +91,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid callback: async () => { await this.core.$everyCommitPendingFileEvent(); }, - }) + }); // TODO, Replicator is possibly one of features. It should be moved to features. this.addCommand({ @@ -103,9 +100,8 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid callback: () => { this.core.replicator.terminateSync(); }, - }) + }); return Promise.resolve(true); - } $everyOnload(): Promise { this.app.workspace.onLayoutReady(this.core.$$onLiveSyncReady.bind(this.core)); @@ -124,13 +120,10 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid await leaves[0].setViewState({ type: viewType, active: true, - }) + }); } if (leaves.length > 0) { - this.app.workspace.revealLeaf( - leaves[0] - ); + this.app.workspace.revealLeaf(leaves[0]); } } - -} \ No newline at end of file +} diff --git a/src/modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts b/src/modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts index 0fe9d9d..e1fc597 100644 --- a/src/modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts +++ b/src/modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts @@ -1,4 +1,4 @@ -import { AbstractObsidianModule, type IObsidianModule } from '../AbstractObsidianModule.ts'; +import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; export class ModuleExtraSyncObsidian extends AbstractObsidianModule implements IObsidianModule { deviceAndVaultName: string = ""; @@ -9,5 +9,4 @@ export class ModuleExtraSyncObsidian extends AbstractObsidianModule implements I $$setDeviceAndVaultName(name: string): void { this.deviceAndVaultName = name; } - -} \ No newline at end of file +} diff --git a/src/modules/extras/ModuleDev.ts b/src/modules/extras/ModuleDev.ts index b88d1c7..abfb0bf 100644 --- a/src/modules/extras/ModuleDev.ts +++ b/src/modules/extras/ModuleDev.ts @@ -8,9 +8,8 @@ import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts"; import { writable } from "svelte/store"; export class ModuleDev extends AbstractObsidianModule implements IObsidianModule { - $everyOnloadStart(): Promise { - __onMissingTranslation(() => { }); + __onMissingTranslation(() => {}); return Promise.resolve(true); } $everyOnloadAfterLoadSettings(): Promise { @@ -18,70 +17,80 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule // eslint-disable-next-line no-unused-labels __onMissingTranslation((key) => { const now = new Date(); - const filename = `missing-translation-` + const filename = `missing-translation-`; const time = now.toISOString().split("T")[0]; const outFile = `${filename}${time}.jsonl`; - const piece = JSON.stringify( - { - [key]: {} - } - ) + const piece = JSON.stringify({ + [key]: {}, + }); const writePiece = piece.substring(1, piece.length - 1) + ","; fireAndForget(async () => { try { await this.core.storageAccess.ensureDir(this.app.vault.configDir + "/ls-debug/"); - await this.core.storageAccess.appendHiddenFile(this.app.vault.configDir + "/ls-debug/" + outFile, writePiece + "\n") + await this.core.storageAccess.appendHiddenFile( + this.app.vault.configDir + "/ls-debug/" + outFile, + writePiece + "\n" + ); } catch (ex) { this._log(`Could not write ${outFile}`, LOG_LEVEL_VERBOSE); this._log(`Missing translation: ${writePiece}`, LOG_LEVEL_VERBOSE); this._log(ex, LOG_LEVEL_VERBOSE); } }); - }) + }); type STUB = { - toc: Set, - stub: { [key: string]: { [key: string]: Map> } } + toc: Set; + stub: { [key: string]: { [key: string]: Map> } }; }; eventHub.onEvent("document-stub-created", (detail: STUB) => { fireAndForget(async () => { const stub = detail.stub; const toc = detail.toc; - const stubDocX = - Object.entries(stub).map(([key, value]) => { - return [`## ${key}`, Object.entries(value). - map(([key2, value2]) => { - return [`### ${key2}`, - ([...(value2.entries())].map(([key3, value3]) => { - // return `#### ${key3}` + "\n" + JSON.stringify(value3); - const isObsolete = value3["is_obsolete"] ? " (obsolete)" : ""; - const desc = value3["desc"] ?? ""; - const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : ""; - return `#### ${key3}${isObsolete}\n${key}${desc}\n` - }))].flat() - }).flat()].flat() - }).flat(); - const stubDocMD = ` + const stubDocX = Object.entries(stub) + .map(([key, value]) => { + return [ + `## ${key}`, + Object.entries(value) + .map(([key2, value2]) => { + return [ + `### ${key2}`, + [...value2.entries()].map(([key3, value3]) => { + // return `#### ${key3}` + "\n" + JSON.stringify(value3); + const isObsolete = value3["is_obsolete"] ? " (obsolete)" : ""; + const desc = value3["desc"] ?? ""; + const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : ""; + return `#### ${key3}${isObsolete}\n${key}${desc}\n`; + }), + ].flat(); + }) + .flat(), + ].flat(); + }) + .flat(); + const stubDocMD = + ` | Icon | Description | | :---: | ----------------------------------------------------------------- | ` + - [...toc.values()].map(e => `${e}`).join("\n") + "\n\n" + + [...toc.values()].map((e) => `${e}`).join("\n") + + "\n\n" + stubDocX.join("\n"); - await this.core.storageAccess.writeHiddenFileAuto(this.app.vault.configDir + "/ls-debug/stub-doc.md", stubDocMD); - }) + await this.core.storageAccess.writeHiddenFileAuto( + this.app.vault.configDir + "/ls-debug/stub-doc.md", + stubDocMD + ); + }); }); enableTestFunction(this.plugin); - this.registerView( - VIEW_TYPE_TEST, - (leaf) => new TestPaneView(leaf, this.plugin, this) - ); + this.registerView(VIEW_TYPE_TEST, (leaf) => new TestPaneView(leaf, this.plugin, this)); this.addCommand({ id: "view-test", name: "Open Test dialogue", callback: () => { void this.core.$$showView(VIEW_TYPE_TEST); - } + }, }); return Promise.resolve(true); } @@ -111,4 +120,4 @@ export class ModuleDev extends AbstractObsidianModule implements IObsidianModule // this.addTestResult("Test of test3", true); return this.testDone(); } -} \ No newline at end of file +} diff --git a/src/modules/extras/ModuleIntegratedTest.ts b/src/modules/extras/ModuleIntegratedTest.ts index 690d727..1e0710d 100644 --- a/src/modules/extras/ModuleIntegratedTest.ts +++ b/src/modules/extras/ModuleIntegratedTest.ts @@ -4,11 +4,10 @@ import { shareRunningResult } from "octagonal-wheels/concurrency/lock"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule"; export class ModuleIntegratedTest extends AbstractObsidianModule implements IObsidianModule { - async waitFor(proc: () => Promise, timeout = 10000): Promise { await delay(100); const start = Date.now(); - while (!await proc()) { + while (!(await proc())) { if (timeout > 0) { if (Date.now() - start > timeout) { this._log(`Timeout`); @@ -40,25 +39,27 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs } } async assert(proc: () => Promise): Promise { - if (!await proc()) { + if (!(await proc())) { this._log(`Assertion failed`); return false; } return true; } async _orDie(key: string, proc: () => Promise): Promise | never { - if (!await this._test(key, proc)) { + if (!(await this._test(key, proc))) { throw new Error(`${key}`); } return true; } tryReplicate() { if (!this.settings.liveSync) { - return shareRunningResult("replicate-test", async () => { await this.core.$$replicate() }); + return shareRunningResult("replicate-test", async () => { + await this.core.$$replicate(); + }); } } async readStorageContent(file: FilePathWithPrefix): Promise { - if (!await this.core.storageAccess.isExistsIncludeHidden(file)) { + if (!(await this.core.storageAccess.isExistsIncludeHidden(file))) { return undefined; } return await this.core.storageAccess.readHiddenFileText(file); @@ -70,13 +71,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs await this.core.$anyResolveConflictByNewest(stepFile); await this.core.storageAccess.writeFileAuto(stepFile, stepContent); await this._orDie(`Wait for acknowledge ${no}`, async () => { - if (!await this.waitWithReplicating( - async () => { - return await this.storageContentIsEqual(stepAckFile, stepContent) - }, 20000) - ) return false; + if ( + !(await this.waitWithReplicating(async () => { + return await this.storageContentIsEqual(stepAckFile, stepContent); + }, 20000)) + ) + return false; return true; - }) + }); return true; } async _join(no: number, title: string): Promise { @@ -86,14 +88,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs const stepContent = `Step ${no}`; await this._orDie(`Wait for step ${no} (${title})`, async () => { - if (!await this.waitWithReplicating( - async () => { - return await this.storageContentIsEqual(stepFile, stepContent) - }, 20000) - ) return false; + if ( + !(await this.waitWithReplicating(async () => { + return await this.storageContentIsEqual(stepFile, stepContent); + }, 20000)) + ) + return false; return true; - } - ) + }); await this.core.$anyResolveConflictByNewest(stepAckFile); await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent); await this.tryReplicate(); @@ -105,13 +107,13 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs title, isGameChanger, proc, - check + check, }: { - step: number, - title: string, - isGameChanger: boolean, - proc: () => Promise, - check: () => Promise, + step: number; + title: string; + isGameChanger: boolean; + proc: () => Promise; + check: () => Promise; }): Promise { if (isGameChanger) { await this._proceed(step, title); @@ -121,9 +123,7 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs this._log(`Error: ${e}`); return false; } - return await this._orDie(`Step ${step} - ${title}`, - async () => await this.waitWithReplicating(check) - ); + return await this._orDie(`Step ${step} - ${title}`, async () => await this.waitWithReplicating(check)); } else { return await this._join(step, title); } @@ -135,14 +135,21 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs // async testReceiver(testMain: (testFileName: FilePathWithPrefix) => Promise): Promise { // } - async nonLiveTestRunner(isLeader: boolean, testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise): Promise { + async nonLiveTestRunner( + isLeader: boolean, + testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise + ): Promise { const storage = this.core.storageAccess; // const database = this.core.databaseFileAccess; // const _orDie = this._orDie.bind(this); const testCommandFile = "IT.md" as FilePathWithPrefix; const textCommandResponseFile = "ITx.md" as FilePathWithPrefix; let testFileName: FilePathWithPrefix; - this.addTestResult("-------Starting ... ", true, `Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}`); + this.addTestResult( + "-------Starting ... ", + true, + `Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}` + ); if (isLeader) { await this._proceed(0, "start"); } @@ -154,14 +161,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs isGameChanger: isLeader, proc: async () => await storage.removeHidden(testCommandFile), check: async () => !(await storage.isExistsIncludeHidden(testCommandFile)), - }) + }); await this.performStep({ step: 1, title: "Make sure that command File Not Exists On Receiver", isGameChanger: !isLeader, proc: async () => await storage.removeHidden(textCommandResponseFile), check: async () => !(await storage.isExistsIncludeHidden(textCommandResponseFile)), - }) + }); await this.performStep({ step: 2, @@ -173,14 +180,14 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs await storage.writeFileAuto(testCommandFile, testFileName); }, check: () => Promise.resolve(true), - }) + }); await this.performStep({ step: 3, title: "Wait for the command file to be arrived", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await storage.isExistsIncludeHidden(testCommandFile), - }) + }); await this.performStep({ step: 4, @@ -190,34 +197,31 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs await storage.writeHiddenFileAuto(textCommandResponseFile, "!"); }, check: () => Promise.resolve(true), - }) + }); await this.performStep({ step: 5, title: "Wait for the response file to be arrived", isGameChanger: isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await storage.isExistsIncludeHidden(textCommandResponseFile), - }) + }); await this.performStep({ step: 6, title: "Proceed to begin the test", isGameChanger: isLeader, - proc: async () => { - - }, + proc: async () => {}, check: () => Promise.resolve(true), }); await this.performStep({ step: 6, title: "Begin the test", isGameChanger: !false, - proc: async () => { - }, + proc: async () => {}, check: () => { return Promise.resolve(true); }, - }) + }); // await this.step(0, isLeader, true); try { this.addTestResult("** Main------", true, ``); @@ -234,15 +238,18 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs } return true; - // Make sure the + // Make sure the } - async testBasic(filename: FilePathWithPrefix, isLeader: boolean): Promise { const storage = this.core.storageAccess; const database = this.core.databaseFileAccess; - await this.addTestResult(`---**Starting Basic Test**---`, true, `Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}`); + await this.addTestResult( + `---**Starting Basic Test**---`, + true, + `Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}` + ); // if (isLeader) { // await this._proceed(0); // } @@ -252,10 +259,9 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs step: 0, title: "Make sure that file is not exist", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => !(await storage.isExists(filename)), - }) - + }); await this.performStep({ step: 1, @@ -263,47 +269,47 @@ export class ModuleIntegratedTest extends AbstractObsidianModule implements IObs isGameChanger: isLeader, proc: async () => await storage.writeFileAuto(filename, "Hello World"), check: async () => await storage.isExists(filename), - }) + }); await this.performStep({ step: 2, title: "Make sure the file is arrived", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await storage.isExists(filename), - }) + }); await this.performStep({ step: 3, title: "Update to Hello World 2", isGameChanger: isLeader, proc: async () => await storage.writeFileAuto(filename, "Hello World 2"), check: async () => await this.storageContentIsEqual(filename, "Hello World 2"), - }) + }); await this.performStep({ step: 4, title: "Make sure the modified file is arrived", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await this.storageContentIsEqual(filename, "Hello World 2"), - }) + }); await this.performStep({ step: 5, title: "Update to Hello World 3", isGameChanger: !isLeader, proc: async () => await storage.writeFileAuto(filename, "Hello World 3"), check: async () => await this.storageContentIsEqual(filename, "Hello World 3"), - }) + }); await this.performStep({ step: 6, title: "Make sure the modified file is arrived", isGameChanger: isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await this.storageContentIsEqual(filename, "Hello World 3"), - }) + }); const multiLineContent = `Line1:A Line2:B Line3:C -Line4:D` +Line4:D`; await this.performStep({ step: 7, @@ -311,38 +317,35 @@ Line4:D` isGameChanger: isLeader, proc: async () => await storage.writeFileAuto(filename, multiLineContent), check: async () => await this.storageContentIsEqual(filename, multiLineContent), - }) + }); await this.performStep({ step: 8, title: "Make sure the modified file is arrived", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await this.storageContentIsEqual(filename, multiLineContent), - }) + }); // While LiveSync, possibly cannot cause the conflict. if (!this.settings.liveSync) { - - - // Step 9 Make Conflict But Resolvable const multiLineContentL = `Line1:A Line2:B Line3:C! -Line4:D` +Line4:D`; const multiLineContentC = `Line1:A Line2:bbbbb Line3:C -Line4:D` +Line4:D`; await this.performStep({ step: 9, title: "Progress to be conflicted", isGameChanger: isLeader, - proc: async () => { }, + proc: async () => {}, check: () => Promise.resolve(true), - }) + }); await storage.writeFileAuto(filename, isLeader ? multiLineContentL : multiLineContentC); @@ -350,62 +353,62 @@ Line4:D` step: 10, title: "Update As Conflicted", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: () => Promise.resolve(true), - }) + }); await this.performStep({ step: 10, title: "Make sure Automatically resolved", isGameChanger: isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => (await database.getConflictedRevs(filename)).length === 0, - }) + }); await this.performStep({ step: 11, title: "Make sure Automatically resolved", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => (await database.getConflictedRevs(filename)).length === 0, - }) - - + }); const sensiblyMergedContent = `Line1:A Line2:bbbbb Line3:C! -Line4:D` +Line4:D`; await this.performStep({ step: 12, title: "Make sure Sensibly Merged on Leader", isGameChanger: isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent), - }) + }); await this.performStep({ step: 13, title: "Make sure Sensibly Merged on Receiver", isGameChanger: !isLeader, - proc: async () => { }, + proc: async () => {}, check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent), - }) + }); } await this.performStep({ step: 14, title: "Delete File", isGameChanger: isLeader, - proc: async () => { await storage.removeHidden(filename) }, - check: async () => !await storage.isExists(filename), - }) + proc: async () => { + await storage.removeHidden(filename); + }, + check: async () => !(await storage.isExists(filename)), + }); await this.performStep({ step: 15, title: "Make sure File is deleted", isGameChanger: !isLeader, - proc: async () => { }, - check: async () => !await storage.isExists(filename), - }) + proc: async () => {}, + check: async () => !(await storage.isExists(filename)), + }); this._log(`The Basic Test has been completed`, LOG_LEVEL_NOTICE); return true; } @@ -429,13 +432,12 @@ Line4:D` this._log(`Starting Test`); await this.testBasicEvent(isLeader); if (this.settings.remoteType == REMOTE_MINIO) await this.testBasicLive(isLeader); - } catch (e) { - this._log(e) + this._log(e); this._log(`Error: ${e}`); return Promise.resolve(false); } return Promise.resolve(true); } -} \ No newline at end of file +} diff --git a/src/modules/extras/ModuleReplicateTest.ts b/src/modules/extras/ModuleReplicateTest.ts index 49bae86..291df80 100644 --- a/src/modules/extras/ModuleReplicateTest.ts +++ b/src/modules/extras/ModuleReplicateTest.ts @@ -15,8 +15,6 @@ declare global { } export class ModuleReplicateTest extends AbstractObsidianModule implements IObsidianModule { - - testRootPath = "_test/"; testInfoPath = "_testinfo/"; @@ -24,7 +22,6 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi return this.core.$$getVaultName().indexOf("dev") >= 0 && this.core.$$vaultName().indexOf("recv") < 0; } - get nameByKind() { if (!this.isLeader) { return "RECV"; @@ -51,7 +48,6 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi } } - async dumpList() { if (this.settings.syncInternalFiles) { this._log("Write file list (Include Hidden)"); @@ -75,39 +71,38 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi void this._dumpFileList("files.md").finally(() => { void this.refreshSyncStatus(); }); - - } - }) + }, + }); this.addCommand({ id: "dump-file-structure-ih", name: "Dump Structure (Include Hidden)", callback: () => { const d = "files.md"; void this._dumpFileListIncludeHidden(d); - } - }) + }, + }); this.addCommand({ id: "dump-file-structure-auto", name: "Dump Structure", callback: () => { void this.dumpList(); - } - }) + }, + }); this.addCommand({ id: "dump-file-test", name: `Perform Test (Dev) ${this.isLeader ? "(Leader)" : "(Recv)"}`, callback: () => { void this.performTestManually(); - } - }) + }, + }); this.addCommand({ id: "watch-sync-result", name: `Watch sync result is matched between devices`, callback: () => { this.watchIsSynchronised = !this.watchIsSynchronised; void this.refreshSyncStatus(); - } - }) + }, + }); this.app.vault.on("modify", async (file) => { if (file.path.startsWith(this.testInfoPath)) { await this.refreshSyncStatus(); @@ -117,7 +112,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi return true; }); } - }) + }); this.statusBarSyncStatus = this.plugin.addStatusBarItem(); return Promise.resolve(true); } @@ -164,12 +159,11 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi } } - async _dumpFileList(outFile?: string) { const files = this.core.storageAccess.getFiles(); const out = [] as any[]; for (const file of files) { - if (!await this.core.$$isTargetFile(file.path)) { + if (!(await this.core.$$isTargetFile(file.path))) { continue; } if (file.path.startsWith(this.testInfoPath)) continue; @@ -183,8 +177,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi name: file.name, size: stat.size, mtime: stat.mtime, - hash: hashStr - } + hash: hashStr, + }; // const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`; out.push(item); } @@ -203,7 +197,9 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi async _dumpFileListIncludeHidden(outFile?: string) { const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); + .split(",") + .filter((e) => e) + .map((e) => new RegExp(e, "i")); const out = [] as any[]; const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns); console.dir(files); @@ -222,8 +218,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi name: file.split("/").pop(), size: stat.size, mtime: stat.mtime, - hash: hashStr - } + hash: hashStr, + }; // const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`; out.push(item); } @@ -263,27 +259,27 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi "docs/tech_info.md", "docs/terms.md", "docs/troubleshooting.md", - 'images/1.png', - 'images/2.png', - 'images/corrupted_data.png', - 'images/hatch.png', - 'images/lock_pattern1.png', - 'images/lock_pattern2.png', - 'images/quick_setup_1.png', - 'images/quick_setup_2.png', - 'images/quick_setup_3.png', - 'images/quick_setup_3b.png', - 'images/quick_setup_4.png', - 'images/quick_setup_5.png', - 'images/quick_setup_6.png', - 'images/quick_setup_7.png', - 'images/quick_setup_8.png', - 'images/quick_setup_9_1.png', - 'images/quick_setup_9_2.png', - 'images/quick_setup_10.png', - 'images/remote_db_setting.png', - 'images/write_logs_into_the_file.png', - ] + "images/1.png", + "images/2.png", + "images/corrupted_data.png", + "images/hatch.png", + "images/lock_pattern1.png", + "images/lock_pattern2.png", + "images/quick_setup_1.png", + "images/quick_setup_2.png", + "images/quick_setup_3.png", + "images/quick_setup_3b.png", + "images/quick_setup_4.png", + "images/quick_setup_5.png", + "images/quick_setup_6.png", + "images/quick_setup_7.png", + "images/quick_setup_8.png", + "images/quick_setup_9_1.png", + "images/quick_setup_9_2.png", + "images/quick_setup_10.png", + "images/remote_db_setting.png", + "images/write_logs_into_the_file.png", + ]; for (const file of files) { const remote = remoteTopDir + file; const local = this.testRootPath + file; @@ -303,7 +299,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi async waitFor(proc: () => Promise, timeout = 10000): Promise { await delay(100); const start = Date.now(); - while (!await proc()) { + while (!(await proc())) { if (timeout > 0) { if (Date.now() - start > timeout) { this._log(`Timeout`); @@ -316,7 +312,6 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi } async testConflictedManually1() { - await this.core.$$replicate(); const commonFile = `Resolve! @@ -328,18 +323,20 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi await this.core.$$replicate(); await this.core.$$replicate(); - if (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", { timeout: 30, defaultOption: "Yes" }) == "no") { + if ( + (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", { + timeout: 30, + defaultOption: "Yes", + })) == "no" + ) { return; } - const fileA = `Resolve to KEEP THIS -Willy Wonka, Willy Wonka, the amazing chocolatier!!` +Willy Wonka, Willy Wonka, the amazing chocolatier!!`; const fileB = `Resolve to DISCARD THIS -Charlie Bucket, Charlie Bucket, the amazing chocolatier!!` - - +Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`; if (this.isLeader) { await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileA); @@ -347,25 +344,37 @@ Charlie Bucket, Charlie Bucket, the amazing chocolatier!!` await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileB); } - if (await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", { timeout: 30, defaultOption: "Yes" }) == "no") { + if ( + (await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", { + timeout: 30, + defaultOption: "Yes", + })) == "no" + ) { return; } await this.core.$$replicate(); await this.core.$$replicate(); - - if (!await this.waitFor(async () => { - await this.core.$$replicate(); - return await this.__assertStorageContent(this.testRootPath + "wonka.md" as FilePath, fileA, false, true) == true; - }, 30000)) { - return await this.__assertStorageContent(this.testRootPath + "wonka.md" as FilePath, fileA, false, true); + if ( + !(await this.waitFor(async () => { + await this.core.$$replicate(); + return ( + (await this.__assertStorageContent( + (this.testRootPath + "wonka.md") as FilePath, + fileA, + false, + true + )) == true + ); + }, 30000)) + ) { + return await this.__assertStorageContent((this.testRootPath + "wonka.md") as FilePath, fileA, false, true); } return true; // We have to check the result } async testConflictedManually2() { - await this.core.$$replicate(); const commonFile = `Resolve To concatenate @@ -377,56 +386,82 @@ ABCDEFG`; await this.core.$$replicate(); await this.core.$$replicate(); - if (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", { timeout: 30, defaultOption: "Yes" }) == "no") { + if ( + (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", { + timeout: 30, + defaultOption: "Yes", + })) == "no" + ) { return; } const fileA = `Resolve to Concatenate -ABCDEFGHIJKLMNOPQRSTYZ` +ABCDEFGHIJKLMNOPQRSTYZ`; const fileB = `Resolve to Concatenate -AJKLMNOPQRSTUVWXYZ` +AJKLMNOPQRSTUVWXYZ`; const concatenated = `Resolve to Concatenate -ABCDEFGHIJKLMNOPQRSTUVWXYZ` +ABCDEFGHIJKLMNOPQRSTUVWXYZ`; if (this.isLeader) { await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileA); } else { await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileB); } - if (await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", { timeout: 30, defaultOption: "Yes" }) == "no") { + if ( + (await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", { + timeout: 30, + defaultOption: "Yes", + })) == "no" + ) { return; } await this.core.$$replicate(); await this.core.$$replicate(); - - if (!await this.waitFor(async () => { - await this.core.$$replicate(); - return await this.__assertStorageContent(this.testRootPath + "concat.md" as FilePath, concatenated, false, true) == true; - }, 30000)) { - return await this.__assertStorageContent(this.testRootPath + "concat.md" as FilePath, concatenated, false, true); + if ( + !(await this.waitFor(async () => { + await this.core.$$replicate(); + return ( + (await this.__assertStorageContent( + (this.testRootPath + "concat.md") as FilePath, + concatenated, + false, + true + )) == true + ); + }, 30000)) + ) { + return await this.__assertStorageContent( + (this.testRootPath + "concat.md") as FilePath, + concatenated, + false, + true + ); } return true; - } async testConflictAutomatic() { - if (this.isLeader) { const baseDoc = `Tasks! - [ ] Task 1 - [ ] Task 2 - [ ] Task 3 - [ ] Task 4 -` +`; await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc); } - await delay(100) + await delay(100); await this.core.$$replicate(); await this.core.$$replicate(); - if (await this.core.confirm.askYesNoDialog("Ready to test conflict?", { timeout: 30, defaultOption: "Yes" }) == "no") { + if ( + (await this.core.confirm.askYesNoDialog("Ready to test conflict?", { + timeout: 30, + defaultOption: "Yes", + })) == "no" + ) { return; } const mod1Doc = `Tasks! @@ -434,14 +469,14 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ` - [v] Task 2 - [ ] Task 3 - [ ] Task 4 -` +`; const mod2Doc = `Tasks! - [ ] Task 1 - [ ] Task 2 - [v] Task 3 - [ ] Task 4 -` +`; if (this.isLeader) { await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod1Doc); } else { @@ -451,7 +486,10 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ` await this.core.$$replicate(); await this.core.$$replicate(); await delay(1000); - if (await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" }) == "no") { + if ( + (await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) == + "no" + ) { return; } await this.core.$$replicate(); @@ -461,8 +499,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ` - [v] Task 2 - [v] Task 3 - [ ] Task 4 -` - return this.__assertStorageContent(this.testRootPath + "task.md" as FilePath, mergedDoc, false, true); +`; + return this.__assertStorageContent((this.testRootPath + "task.md") as FilePath, mergedDoc, false, true); } async checkConflictResolution() { @@ -471,24 +509,27 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ` await this.core.rebuilder.resolveAllConflictedFilesByNewerOnes(); await this.core.$$replicate(); await delay(1000); - if (!await this.testConflictAutomatic()) { + if (!(await this.testConflictAutomatic())) { this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE); return false; } - if (!await this.testConflictedManually1()) { + if (!(await this.testConflictedManually1())) { this._log("Conflict resolution (Manual1) failed", LOG_LEVEL_NOTICE); return false; } - if (!await this.testConflictedManually2()) { + if (!(await this.testConflictedManually2())) { this._log("Conflict resolution (Manual2) failed", LOG_LEVEL_NOTICE); return false; } return true; - } - - async __assertStorageContent(fileName: FilePath, content: string, inverted = false, showResult = false): Promise { + async __assertStorageContent( + fileName: FilePath, + content: string, + inverted = false, + showResult = false + ): Promise { try { const fileContent = await this.core.storageAccess.readHiddenFileText(fileName); let result = fileContent === content; @@ -531,4 +572,4 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ` await this._test("Conflict resolution", async () => await this.checkConflictResolution()); return this.testDone(); } -} \ No newline at end of file +} diff --git a/src/modules/extras/devUtil/TestPaneView.ts b/src/modules/extras/devUtil/TestPaneView.ts index a0b9b17..40a7cdd 100644 --- a/src/modules/extras/devUtil/TestPaneView.ts +++ b/src/modules/extras/devUtil/TestPaneView.ts @@ -1,19 +1,15 @@ -import { - ItemView, - WorkspaceLeaf -} from "obsidian"; -import TestPaneComponent from "./TestPane.svelte" -import type ObsidianLiveSyncPlugin from "../../../main.ts" +import { ItemView, WorkspaceLeaf } from "obsidian"; +import TestPaneComponent from "./TestPane.svelte"; +import type ObsidianLiveSyncPlugin from "../../../main.ts"; import type { ModuleDev } from "../ModuleDev.ts"; export const VIEW_TYPE_TEST = "ols-pane-test"; //Log view export class TestPaneView extends ItemView { - component?: TestPaneComponent; plugin: ObsidianLiveSyncPlugin; moduleDev: ModuleDev; icon = "view-log"; - title: string = "Self-hosted LiveSync Test and Results" + title: string = "Self-hosted LiveSync Test and Results"; navigation = true; getIcon(): string { @@ -26,7 +22,6 @@ export class TestPaneView extends ItemView { this.moduleDev = moduleDev; } - getViewType() { return VIEW_TYPE_TEST; } @@ -41,7 +36,7 @@ export class TestPaneView extends ItemView { target: this.contentEl, props: { plugin: this.plugin, - moduleDev: this.moduleDev + moduleDev: this.moduleDev, }, }); await Promise.resolve(); diff --git a/src/modules/extras/devUtil/testUtils.ts b/src/modules/extras/devUtil/testUtils.ts index 869d1fa..30a32ac 100644 --- a/src/modules/extras/devUtil/testUtils.ts +++ b/src/modules/extras/devUtil/testUtils.ts @@ -7,40 +7,44 @@ export function enableTestFunction(plugin_: ObsidianLiveSyncPlugin) { plugin = plugin_; } export function addDebugFileLog(message: any, stackLog = false) { - fireAndForget(serialized("debug-log", async () => { - const now = new Date(); - const filename = `debug-log` - const time = now.toISOString().split("T")[0]; - const outFile = `${filename}${time}.jsonl`; - // const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); - const timestamp = now.toLocaleString(); - const timestampEpoch = now; - let out = { "timestamp": timestamp, epoch: timestampEpoch, } as Record; - if (message instanceof Error) { - // debugger; - // console.dir(message.stack); - out = { ...out, message }; - } else if (stackLog) { - if (stackLog) { - const stackE = new Error(); - const stack = stackE.stack; - out = { ...out, stack } + fireAndForget( + serialized("debug-log", async () => { + const now = new Date(); + const filename = `debug-log`; + const time = now.toISOString().split("T")[0]; + const outFile = `${filename}${time}.jsonl`; + // const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); + const timestamp = now.toLocaleString(); + const timestampEpoch = now; + let out = { timestamp: timestamp, epoch: timestampEpoch } as Record; + if (message instanceof Error) { + // debugger; + // console.dir(message.stack); + out = { ...out, message }; + } else if (stackLog) { + if (stackLog) { + const stackE = new Error(); + const stack = stackE.stack; + out = { ...out, stack }; + } } - } - if (typeof message == "object") { - out = { ...out, ...message, } - } else { - out = { - result: message + if (typeof message == "object") { + out = { ...out, ...message }; + } else { + out = { + result: message, + }; } - } - // const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || ""); - // const out - try { - await plugin.storageAccess.appendHiddenFile(plugin.app.vault.configDir + "/ls-debug/" + outFile, JSON.stringify(out) + "\n") - } catch { - - //NO OP - } - })); -} \ No newline at end of file + // const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || ""); + // const out + try { + await plugin.storageAccess.appendHiddenFile( + plugin.app.vault.configDir + "/ls-debug/" + outFile, + JSON.stringify(out) + "\n" + ); + } catch { + //NO OP + } + }) + ); +} diff --git a/src/modules/extras/devUtil/tests.ts b/src/modules/extras/devUtil/tests.ts index f4c5119..5235c77 100644 --- a/src/modules/extras/devUtil/tests.ts +++ b/src/modules/extras/devUtil/tests.ts @@ -7,7 +7,7 @@ const measures = new Map(); function clearResult(name: string) { measures.set(name, [0, 0]); } -async function measureEach(name: string, proc: () => (void | Promise)) { +async function measureEach(name: string, proc: () => void | Promise) { const [times, spent] = measures.get(name) ?? [0, 0]; const start = performance.now(); @@ -15,57 +15,76 @@ async function measureEach(name: string, proc: () => (void | Promise)) { if (result instanceof Promise) await result; const end = performance.now(); measures.set(name, [times + 1, spent + (end - start)]); - } function formatNumber(num: number) { - return num.toLocaleString('en-US', { maximumFractionDigits: 2 }); + return num.toLocaleString("en-US", { maximumFractionDigits: 2 }); } -async function measure(name: string, proc: () => (void | Promise), times: number = 10000, duration: number = 1000): Promise { +async function measure( + name: string, + proc: () => void | Promise, + times: number = 10000, + duration: number = 1000 +): Promise { const from = Date.now(); let last = times; clearResult(name); do { await measureEach(name, proc); - } while (last-- > 0 && (Date.now() - from) < duration) + } while (last-- > 0 && Date.now() - from < duration); return [name, measures.get(name) as MeasureResult]; } // eslint-disable-next-line require-await, @typescript-eslint/require-await async function formatPerfResults(items: NamedMeasureResult[]) { - return `| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` + items.map(e => `| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |`).join("\n"); - + return ( + `| Name | Runs | Each | Total |\n| --- | --- | --- | --- | \n` + + items + .map( + (e) => + `| ${e[0]} | ${e[1][0]} | ${e[1][0] != 0 ? formatNumber(e[1][1] / e[1][0]) : "-"} | ${formatNumber(e[1][0])} |` + ) + .join("\n") + ); } export async function perf_trench(plugin: ObsidianLiveSyncPlugin) { clearResult("trench"); const trench = new Trench(plugin.simpleStore); const result = [] as NamedMeasureResult[]; - result.push(await measure("trench-short-string", async () => { - const p = trench.evacuate("string"); - await p(); - })); + result.push( + await measure("trench-short-string", async () => { + const p = trench.evacuate("string"); + await p(); + }) + ); { const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/10kb.png"); const uint8Array = new Uint8Array(testBinary); - result.push(await measure("trench-binary-10kb", async () => { - const p = trench.evacuate(uint8Array); - await p(); - })); + result.push( + await measure("trench-binary-10kb", async () => { + const p = trench.evacuate(uint8Array); + await p(); + }) + ); } { const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg"); const uint8Array = new Uint8Array(testBinary); - result.push(await measure("trench-binary-100kb", async () => { - const p = trench.evacuate(uint8Array); - await p(); - })); + result.push( + await measure("trench-binary-100kb", async () => { + const p = trench.evacuate(uint8Array); + await p(); + }) + ); } { const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/1mb.png"); const uint8Array = new Uint8Array(testBinary); - result.push(await measure("trench-binary-1mb", async () => { - const p = trench.evacuate(uint8Array); - await p(); - })); + result.push( + await measure("trench-binary-1mb", async () => { + const p = trench.evacuate(uint8Array); + await p(); + }) + ); } return formatPerfResults(result); -} \ No newline at end of file +} diff --git a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts index 9d71e60..ff53ff4 100644 --- a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -2,7 +2,14 @@ import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_pat import { getPathFromTFile, isValidPath } from "../../../common/utils.ts"; import { decodeBinary, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts"; import ObsidianLiveSyncPlugin from "../../../main.ts"; -import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../../lib/src/common/types.ts"; +import { + type DocumentID, + type FilePathWithPrefix, + type LoadedEntry, + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, +} from "../../../lib/src/common/types.ts"; import { Logger } from "../../../lib/src/common/logger.ts"; import { isErrorOfMissingDoc } from "../../../lib/src/pouchdb/utils_couchdb.ts"; import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/utils.ts"; @@ -19,7 +26,7 @@ function isComparableText(path: string) { } function isComparableTextDecode(path: string) { const ext = path.split(".").splice(-1)[0].toLowerCase(); - return ["json"].includes(ext) + return ["json"].includes(ext); } function readDocument(w: LoadedEntry) { if (w.data.length == 0) return ""; @@ -54,10 +61,16 @@ export class DocumentHistoryModal extends Modal { currentDeleted = false; initialRev?: string; - constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string) { + constructor( + app: App, + plugin: ObsidianLiveSyncPlugin, + file: TFile | FilePathWithPrefix, + id?: DocumentID, + revision?: string + ) { super(app); this.plugin = plugin; - this.file = (file instanceof TFile) ? getPathFromTFile(file) : file; + this.file = file instanceof TFile ? getPathFromTFile(file) : file; this.id = id; this.initialRev = revision; if (!file && id) { @@ -95,7 +108,7 @@ export class DocumentHistoryModal extends Modal { async loadRevs(initialRev?: string) { if (this.revs_info.length == 0) return; if (initialRev) { - const rIndex = this.revs_info.findIndex(e => e.rev == initialRev); + const rIndex = this.revs_info.findIndex((e) => e.rev == initialRev); if (rIndex >= 0) { this.range.value = `${this.revs_info.length - 1 - rIndex}`; } @@ -163,8 +176,7 @@ export class DocumentHistoryModal extends Modal { } else if (isImage(this.file)) { const src = this.generateBlobURL("base", w1data); const overlay = this.generateBlobURL("overlay", readDocument(w2) as Uint8Array); - result = - `
+ result = `
@@ -174,14 +186,12 @@ export class DocumentHistoryModal extends Modal { } } } - } if (result == undefined) { if (typeof w1data != "string") { if (isImage(this.file)) { const src = this.generateBlobURL("base", w1data); - result = - `
+ result = `
@@ -193,7 +203,8 @@ export class DocumentHistoryModal extends Modal { } } if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file"; - this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result; + this.contentView.innerHTML = + (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result; } } @@ -257,9 +268,9 @@ export class DocumentHistoryModal extends Modal { const leaf = this.plugin.app.workspace.getLeaf(false); await leaf.openFile(targetFile); } else { - Logger("The file could not view on the editor", LOG_LEVEL_NOTICE) + Logger("The file could not view on the editor", LOG_LEVEL_NOTICE); } - } + }; buttons.createEl("button", { text: "Back to this revision" }, (e) => { e.addClass("mod-cta"); e.addEventListener("click", () => { @@ -285,9 +296,9 @@ export class DocumentHistoryModal extends Modal { onClose() { const { contentEl } = this; contentEl.empty(); - this.BlobURLs.forEach(value => { + this.BlobURLs.forEach((value) => { console.log(value); if (value) URL.revokeObjectURL(value); - }) + }); } } diff --git a/src/modules/features/GlobalHistory/GlobalHistoryView.ts b/src/modules/features/GlobalHistory/GlobalHistoryView.ts index 79c78e3..ea80e78 100644 --- a/src/modules/features/GlobalHistory/GlobalHistoryView.ts +++ b/src/modules/features/GlobalHistory/GlobalHistoryView.ts @@ -1,13 +1,9 @@ -import { - ItemView, - WorkspaceLeaf -} from "../../../deps.ts"; +import { ItemView, WorkspaceLeaf } from "../../../deps.ts"; import GlobalHistoryComponent from "./GlobalHistory.svelte"; import type ObsidianLiveSyncPlugin from "../../../main.ts"; export const VIEW_TYPE_GLOBAL_HISTORY = "global-history"; export class GlobalHistoryView extends ItemView { - component?: GlobalHistoryComponent; plugin: ObsidianLiveSyncPlugin; icon = "clock"; @@ -23,7 +19,6 @@ export class GlobalHistoryView extends ItemView { this.plugin = plugin; } - getViewType() { return VIEW_TYPE_GLOBAL_HISTORY; } diff --git a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts index 0c6b108..d87078d 100644 --- a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts +++ b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts @@ -27,7 +27,7 @@ export class ConflictResolveModal extends Modal { if (this.pluginPickMode) { this.title = "Pick a version"; this.remoteName = `Use ${remoteName || "Remote"}`; - this.localName = "Use Local" + this.localName = "Use Local"; } // Send cancel signal for the previous merge dialogue // if not there, simply be ignored. @@ -48,7 +48,7 @@ export class ConflictResolveModal extends Modal { this.sendResponse(CANCELLED); } }); - }, 10) + }, 10); // sendValue("close-resolve-conflict:" + this.filename, false); this.titleEl.setText(this.title); contentEl.empty(); @@ -60,28 +60,47 @@ export class ConflictResolveModal extends Modal { const x1 = v[0]; const x2 = v[1]; if (x1 == DIFF_DELETE) { - diff += "" + escapeStringToHTML(x2).replace(/\n/g, "\n") + ""; + diff += + "" + + escapeStringToHTML(x2).replace(/\n/g, "\n") + + ""; } else if (x1 == DIFF_EQUAL) { - diff += "" + escapeStringToHTML(x2).replace(/\n/g, "\n") + ""; + diff += + "" + + escapeStringToHTML(x2).replace(/\n/g, "\n") + + ""; } else if (x1 == DIFF_INSERT) { - diff += "" + escapeStringToHTML(x2).replace(/\n/g, "\n") + ""; + diff += + "" + + escapeStringToHTML(x2).replace(/\n/g, "\n") + + ""; } } diff = diff.replace(/\n/g, "
"); div.innerHTML = diff; const div2 = contentEl.createDiv(""); - const date1 = new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : ""); - const date2 = new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : ""); + const date1 = + new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : ""); + const date2 = + new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : ""); div2.innerHTML = ` A:${date1}
B:${date2}
`; - contentEl.createEl("button", { text: this.localName }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev))).style.marginRight = "4px"; - contentEl.createEl("button", { text: this.remoteName }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev))).style.marginRight = "4px"; + contentEl.createEl("button", { text: this.localName }, (e) => + e.addEventListener("click", () => this.sendResponse(this.result.right.rev)) + ).style.marginRight = "4px"; + contentEl.createEl("button", { text: this.remoteName }, (e) => + e.addEventListener("click", () => this.sendResponse(this.result.left.rev)) + ).style.marginRight = "4px"; if (!this.pluginPickMode) { - contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT))).style.marginRight = "4px"; + contentEl.createEl("button", { text: "Concat both" }, (e) => + e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)) + ).style.marginRight = "4px"; } - contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED))).style.marginRight = "4px"; + contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => + e.addEventListener("click", () => this.sendResponse(CANCELLED)) + ).style.marginRight = "4px"; } sendResponse(result: MergeDialogResult) { @@ -106,4 +125,4 @@ export class ConflictResolveModal extends Modal { if (r === RESULT_TIMED_OUT) return CANCELLED; return r; } -} \ No newline at end of file +} diff --git a/src/modules/features/Log/LogPaneView.ts b/src/modules/features/Log/LogPaneView.ts index bd1203f..a482cc8 100644 --- a/src/modules/features/Log/LogPaneView.ts +++ b/src/modules/features/Log/LogPaneView.ts @@ -1,13 +1,9 @@ -import { - ItemView, - WorkspaceLeaf -} from "obsidian"; +import { ItemView, WorkspaceLeaf } from "obsidian"; import LogPaneComponent from "./LogPane.svelte"; import type ObsidianLiveSyncPlugin from "../../../main.ts"; export const VIEW_TYPE_LOG = "log-log"; //Log view export class LogPaneView extends ItemView { - component?: LogPaneComponent; plugin: ObsidianLiveSyncPlugin; icon = "view-log"; @@ -23,7 +19,6 @@ export class LogPaneView extends ItemView { this.plugin = plugin; } - getViewType() { return VIEW_TYPE_LOG; } @@ -35,8 +30,7 @@ export class LogPaneView extends ItemView { async onOpen() { this.component = new LogPaneComponent({ target: this.contentEl, - props: { - }, + props: {}, }); await Promise.resolve(); } diff --git a/src/modules/features/ModuleGlobalHistory.ts b/src/modules/features/ModuleGlobalHistory.ts index b6805e2..261cd7d 100644 --- a/src/modules/features/ModuleGlobalHistory.ts +++ b/src/modules/features/ModuleGlobalHistory.ts @@ -1,23 +1,17 @@ import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts"; - export class ModuleObsidianGlobalHistory extends AbstractObsidianModule implements IObsidianModule { - $everyOnloadStart(): Promise { - this.addCommand({ id: "livesync-global-history", name: "Show vault history", callback: () => { - this.showGlobalHistory() - } - }) + this.showGlobalHistory(); + }, + }); - this.registerView( - VIEW_TYPE_GLOBAL_HISTORY, - (leaf) => new GlobalHistoryView(leaf, this.plugin) - ); + this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf) => new GlobalHistoryView(leaf, this.plugin)); return Promise.resolve(true); } @@ -25,5 +19,4 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule implemen showGlobalHistory() { void this.core.$$showView(VIEW_TYPE_GLOBAL_HISTORY); } - -} \ No newline at end of file +} diff --git a/src/modules/features/ModuleInteractiveConflictResolver.ts b/src/modules/features/ModuleInteractiveConflictResolver.ts index 5272d57..5d6d9f7 100644 --- a/src/modules/features/ModuleInteractiveConflictResolver.ts +++ b/src/modules/features/ModuleInteractiveConflictResolver.ts @@ -1,11 +1,20 @@ -import { CANCELLED, LEAVE_TO_SUBSEQUENT, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MISSING_OR_ERROR, type DocumentID, type FilePathWithPrefix, type diff_result } from "../../lib/src/common/types.ts"; +import { + CANCELLED, + LEAVE_TO_SUBSEQUENT, + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + MISSING_OR_ERROR, + type DocumentID, + type FilePathWithPrefix, + type diff_result, +} from "../../lib/src/common/types.ts"; import { ConflictResolveModal } from "./InteractiveConflictResolving/ConflictResolveModal.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts"; import { fireAndForget } from "octagonal-wheels/promises"; export class ModuleInteractiveConflictResolver extends AbstractObsidianModule implements IObsidianModule { - $everyOnloadStart(): Promise { this.addCommand({ id: "livesync-conflictcheck", @@ -13,14 +22,14 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im callback: async () => { await this.pickFileForResolve(); }, - }) + }); this.addCommand({ id: "livesync-all-conflictcheck", name: "Resolve all conflicted files", callback: async () => { await this.allConflictCheck(); }, - }) + }); return Promise.resolve(true); } @@ -50,18 +59,26 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im // Create a new file by concatenating both conflicted revisions. const p = conflictCheckResult.diff.map((e) => e[1]).join(""); const delRev = testDoc._conflicts[0]; - if (!await this.core.databaseFileAccess.storeContent(filename, p)) { + if (!(await this.core.databaseFileAccess.storeContent(filename, p))) { this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE); return false; } // 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage. - if (await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated") == MISSING_OR_ERROR) { - this._log(`Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`, LOG_LEVEL_NOTICE); + if ( + (await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) == + MISSING_OR_ERROR + ) { + this._log( + `Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`, + LOG_LEVEL_NOTICE + ); return false; } } else if (typeof toDelete === "string") { // Select one of the conflicted revision to delete. - if (await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected") == MISSING_OR_ERROR) { + if ( + (await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) == MISSING_OR_ERROR + ) { this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); return false; } @@ -83,22 +100,21 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im while (await this.pickFileForResolve()); } - async pickFileForResolve() { - const notes: { id: DocumentID, path: FilePathWithPrefix, dispPath: string, mtime: number }[] = []; + const notes: { id: DocumentID; path: FilePathWithPrefix; dispPath: string; mtime: number }[] = []; for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) { if (!("_conflicts" in doc)) continue; notes.push({ id: doc._id, path: getPath(doc), dispPath: getPathWithoutPrefix(doc), mtime: doc.mtime }); } notes.sort((a, b) => b.mtime - a.mtime); - const notesList = notes.map(e => e.dispPath); + const notesList = notes.map((e) => e.dispPath); if (notesList.length == 0) { this._log("There are no conflicted documents", LOG_LEVEL_NOTICE); return false; } const target = await this.core.confirm.askSelectString("File to resolve conflict", notesList); if (target) { - const targetItem = notes.find(e => e.dispPath == target)!; + const targetItem = notes.find((e) => e.dispPath == target)!; await this.core.$$queueConflictCheck(targetItem.path); await this.core.$$waitForAllConflictProcessed(); return true; @@ -107,21 +123,27 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im } async $allScanStat(): Promise { - const notes: { path: string, mtime: number }[] = []; + const notes: { path: string; mtime: number }[] = []; this._log(`Checking conflicted files`, LOG_LEVEL_VERBOSE); for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) { if (!("_conflicts" in doc)) continue; notes.push({ path: getPath(doc), mtime: doc.mtime }); } if (notes.length > 0) { - this.core.confirm.askInPopup(`conflicting-detected-on-safety`, `Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - fireAndForget(() => this.allConflictCheck()) - }); - } + this.core.confirm.askInPopup( + `conflicting-detected-on-safety`, + `Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`, + (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + fireAndForget(() => this.allConflictCheck()); + }); + } + ); + this._log( + `Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, + LOG_LEVEL_VERBOSE ); - this._log(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_VERBOSE); for (const note of notes) { this._log(`Conflicted: ${note.path}`); } @@ -130,5 +152,4 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im } return true; } - -} \ No newline at end of file +} diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index 2cb3968..bf803cf 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -1,8 +1,23 @@ import { computed, reactive, reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive"; -import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type LOG_LEVEL } from "../../lib/src/common/types.ts"; +import { + LOG_LEVEL_DEBUG, + LOG_LEVEL_INFO, + LOG_LEVEL_VERBOSE, + PREFIXMD_LOGFILE, + type DatabaseConnectingStatus, + type LOG_LEVEL, +} from "../../lib/src/common/types.ts"; import { cancelTask, scheduleTask } from "octagonal-wheels/concurrency/task"; import { fireAndForget, isDirty, throttle } from "../../lib/src/common/utils.ts"; -import { collectingChunks, pluginScanningCount, hiddenFilesEventCount, hiddenFilesProcessingCount, type LogEntry, logStore, logMessages } from "../../lib/src/mock_and_interop/stores.ts"; +import { + collectingChunks, + pluginScanningCount, + hiddenFilesEventCount, + hiddenFilesProcessingCount, + type LogEntry, + logStore, + logMessages, +} from "../../lib/src/mock_and_interop/stores.ts"; import { eventHub } from "../../lib/src/hub/hub.ts"; import { EVENT_FILE_RENAMED, EVENT_LAYOUT_READY, EVENT_LEAF_ACTIVE_CHANGED } from "../../common/events.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; @@ -22,15 +37,16 @@ setGlobalLogFunction((message: any, level?: number, key?: string) => { let recentLogs = [] as string[]; // Recent log splicer -const recentLogProcessor = new QueueProcessor((logs: string[]) => { - recentLogs = [...recentLogs, ...logs].splice(-200); - logMessages.value = recentLogs; -}, { batchSize: 25, delay: 10, suspended: false, concurrentLimit: 1 }).resumePipeLine(); +const recentLogProcessor = new QueueProcessor( + (logs: string[]) => { + recentLogs = [...recentLogs, ...logs].splice(-200); + logMessages.value = recentLogs; + }, + { batchSize: 25, delay: 10, suspended: false, concurrentLimit: 1 } +).resumePipeLine(); // logStore.intercept(e => e.slice(Math.min(e.length - 200, 0))); - export class ModuleLog extends AbstractObsidianModule implements IObsidianModule { - registerView = this.plugin.registerView.bind(this.plugin); statusBar?: HTMLElement; @@ -41,7 +57,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule logHistory?: HTMLDivElement; messageArea?: HTMLDivElement; - statusBarLabels!: ReactiveValue<{ message: string, status: string }>; + statusBarLabels!: ReactiveValue<{ message: string; status: string }>; statusLog = reactiveSource(""); notifies: { [key: string]: { notice: Notice; count: number } } = {}; @@ -52,7 +68,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule const formatted = reactiveSource(""); let timer: ReturnType | undefined = undefined; let maxLen = 1; - numI.onChanged(numX => { + numI.onChanged((numX) => { const num = numX.value; const numLen = `${Math.abs(num)}`.length + 1; maxLen = maxLen < numLen ? numLen : maxLen; @@ -63,8 +79,8 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule maxLen = 1; }, 3000); } - formatted.value = ` ${mark}${`${padSpaces}${num}`.slice(-(maxLen))}`; - }) + formatted.value = ` ${mark}${`${padSpaces}${num}`.slice(-maxLen)}`; + }); return computed(() => formatted.value); } const labelReplication = padLeftSpComputed(this.core.replicationResultCount, `📥`); @@ -74,16 +90,16 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule const labelPluginScanCount = padLeftSpComputed(pluginScanningCount, `🔌`); const labelConflictProcessCount = padLeftSpComputed(this.core.conflictProcessQueueCount, `🔩`); const hiddenFilesCount = reactive(() => hiddenFilesEventCount.value + hiddenFilesProcessingCount.value); - const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`) + const labelHiddenFilesCount = padLeftSpComputed(hiddenFilesCount, `⚙️`); const queueCountLabelX = reactive(() => { return `${labelReplication()}${labelDBCount()}${labelStorageCount()}${labelChunkCount()}${labelPluginScanCount()}${labelHiddenFilesCount()}${labelConflictProcessCount()}`; - }) + }); const queueCountLabel = () => queueCountLabelX.value; const requestingStatLabel = computed(() => { const diff = this.core.requestCount.value - this.core.responseCount.value; return diff != 0 ? "📲 " : ""; - }) + }); const replicationStatLabel = computed(() => { const e = this.core.replicationStat.value; @@ -97,10 +113,10 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule let pullLast = ""; let w = ""; const labels: Partial> = { - "CONNECTED": "⚡", - "JOURNAL_SEND": "📦↑", - "JOURNAL_RECEIVE": "📦↓", - } + CONNECTED: "⚡", + JOURNAL_SEND: "📦↑", + JOURNAL_RECEIVE: "📦↓", + }; switch (e.syncStatus) { case "CLOSED": case "COMPLETED": @@ -117,8 +133,18 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule case "JOURNAL_SEND": case "JOURNAL_RECEIVE": w = labels[e.syncStatus] || "⚡"; - pushLast = ((lastSyncPushSeq == 0) ? "" : (lastSyncPushSeq >= maxPushSeq ? " (LIVE)" : ` (${maxPushSeq - lastSyncPushSeq})`)); - pullLast = ((lastSyncPullSeq == 0) ? "" : (lastSyncPullSeq >= maxPullSeq ? " (LIVE)" : ` (${maxPullSeq - lastSyncPullSeq})`)); + pushLast = + lastSyncPushSeq == 0 + ? "" + : lastSyncPushSeq >= maxPushSeq + ? " (LIVE)" + : ` (${maxPushSeq - lastSyncPushSeq})`; + pullLast = + lastSyncPullSeq == 0 + ? "" + : lastSyncPullSeq >= maxPullSeq + ? " (LIVE)" + : ` (${maxPullSeq - lastSyncPullSeq})`; break; case "ERRORED": w = "⚠"; @@ -127,13 +153,13 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule w = "?"; } return { w, sent, pushLast, arrived, pullLast }; - }) + }); const labelProc = padLeftSpComputed(this.core.processing, `⏳`); const labelPend = padLeftSpComputed(this.core.totalQueued, `🛫`); const labelInBatchDelay = padLeftSpComputed(this.core.batched, `📬`); const waitingLabel = computed(() => { return `${labelProc()}${labelPend()}${labelInBatchDelay()}`; - }) + }); const statusLineLabel = computed(() => { const { w, sent, pushLast, arrived, pullLast } = replicationStatLabel(); const queued = queueCountLabel(); @@ -142,24 +168,26 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule return { message: `${networkActivity}Sync: ${w} ↑ ${sent}${pushLast} ↓ ${arrived}${pullLast}${waiting}${queued}`, }; - }) + }); const statusBarLabels = reactive(() => { - const scheduleMessage = this.core.$$isReloadingScheduled() ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` : ""; + const scheduleMessage = this.core.$$isReloadingScheduled() + ? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n` + : ""; const { message } = statusLineLabel(); const status = scheduleMessage + this.statusLog.value; return { - message, status - } - }) + message, + status, + }; + }); this.statusBarLabels = statusBarLabels; const applyToDisplay = throttle((label: typeof statusBarLabels.value) => { // const v = label; this.applyStatusBarText(); - }, 20); - statusBarLabels.onChanged(label => applyToDisplay(label.value)) + statusBarLabels.onChanged((label) => applyToDisplay(label.value)); } $everyOnload(): Promise { @@ -182,10 +210,13 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule if (!thisFile) return ""; // Case Sensitivity if (this.core.$$shouldCheckCaseInsensitive()) { - const f = this.core.storageAccess.getFiles().map(e => e.path).filter(e => e.toLowerCase() == thisFile.path.toLowerCase()); + const f = this.core.storageAccess + .getFiles() + .map((e) => e.path) + .filter((e) => e.toLowerCase() == thisFile.path.toLowerCase()); if (f.length > 1) return "Not synchronised: There are multiple files with the same name"; } - if (!await this.core.$$isTargetFile(thisFile.path)) return "Not synchronised: not a target file"; + if (!(await this.core.$$isTargetFile(thisFile.path))) return "Not synchronised: not a target file"; if (this.core.$$isFileSizeExceeded(thisFile.stat.size)) return "Not synchronised: File size exceeded"; return ""; } @@ -197,11 +228,10 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule this.adjustStatusDivPosition(); await this.setFileStatus(); }); - } nextFrameQueue: ReturnType | undefined = undefined; - logLines: { ttl: number, message: string }[] = []; + logLines: { ttl: number; message: string }[] = []; applyStatusBarText() { if (this.nextFrameQueue) { @@ -222,10 +252,13 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule if (this.settings?.showStatusOnEditor && this.statusDiv) { if (this.settings.showLongerLogInsideEditor) { const now = new Date().getTime(); - this.logLines = this.logLines.filter(e => e.ttl > now); - const minimumNext = this.logLines.reduce((a, b) => a < b.ttl ? a : b.ttl, Number.MAX_SAFE_INTEGER); + this.logLines = this.logLines.filter((e) => e.ttl > now); + const minimumNext = this.logLines.reduce( + (a, b) => (a < b.ttl ? a : b.ttl), + Number.MAX_SAFE_INTEGER + ); if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now); - const recent = this.logLines.map(e => e.message); + const recent = this.logLines.map((e) => e.message); const recentLogs = recent.reverse().join("\n"); if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs; } @@ -237,14 +270,16 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule } }); - scheduleTask("log-hide", 3000, () => { this.statusLog.value = "" }); + scheduleTask("log-hide", 3000, () => { + this.statusLog.value = ""; + }); } $allStartOnUnload(): Promise { if (this.statusDiv) { this.statusDiv.remove(); } - document.querySelectorAll(`.livesync-status`)?.forEach(e => e.remove()); + document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove()); return Promise.resolve(true); } $everyOnloadStart(): Promise { @@ -264,22 +299,28 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule name: "Show log", callback: () => { void this.core.$$showView(VIEW_TYPE_LOG); - } + }, }); - this.registerView( - VIEW_TYPE_LOG, - (leaf) => new LogPaneView(leaf, this.plugin) - ); + this.registerView(VIEW_TYPE_LOG, (leaf) => new LogPaneView(leaf, this.plugin)); return Promise.resolve(true); } $everyOnloadAfterLoadSettings(): Promise { - logStore.pipeTo(new QueueProcessor(logs => logs.forEach(e => this.core.$$addLog(e.message, e.level, e.key)), { suspended: false, batchSize: 20, concurrentLimit: 1, delay: 0 })).startPipeline(); + logStore + .pipeTo( + new QueueProcessor((logs) => logs.forEach((e) => this.core.$$addLog(e.message, e.level, e.key)), { + suspended: false, + batchSize: 20, + concurrentLimit: 1, + delay: 0, + }) + ) + .startPipeline(); eventHub.onEvent(EVENT_FILE_RENAMED, (data) => { void this.setFileStatus(); }); const w = document.querySelectorAll(`.livesync-status`); - w.forEach(e => e.remove()); + w.forEach((e) => e.remove()); this.observeForLogs(); @@ -298,15 +339,20 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule } writeLogToTheFile(now: Date, vaultName: string, newMessage: string) { - fireAndForget(() => serialized("writeLog", async () => { - const time = now.toISOString().split("T")[0]; - const logDate = `${PREFIXMD_LOGFILE}${time}.md`; - const file = await this.core.storageAccess.isExists(normalizePath(logDate)); - if (!file) { - await this.core.storageAccess.appendHiddenFile(normalizePath(logDate), "```\n"); - } - await this.core.storageAccess.appendHiddenFile(normalizePath(logDate), vaultName + ":" + newMessage + "\n"); - })); + fireAndForget(() => + serialized("writeLog", async () => { + const time = now.toISOString().split("T")[0]; + const logDate = `${PREFIXMD_LOGFILE}${time}.md`; + const file = await this.core.storageAccess.isExists(normalizePath(logDate)); + if (!file) { + await this.core.storageAccess.appendHiddenFile(normalizePath(logDate), "```\n"); + } + await this.core.storageAccess.appendHiddenFile( + normalizePath(logDate), + vaultName + ":" + newMessage + "\n" + ); + }) + ); } $$addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void { if (level == LOG_LEVEL_DEBUG) { @@ -321,7 +367,12 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule const vaultName = this.core.$$getVaultName(); const now = new Date(); const timestamp = now.toLocaleString(); - const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); + const messageContent = + typeof message == "string" + ? message + : message instanceof Error + ? `${message.name}:${message.message}` + : JSON.stringify(message, null, 2); if (message instanceof Error) { // debugger; console.dir(message.stack); @@ -342,7 +393,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule if (!key) key = messageContent; if (key in this.notifies) { // @ts-ignore - const isShown = this.notifies[key].notice.noticeEl?.isShown() + const isShown = this.notifies[key].notice.noticeEl?.isShown(); if (!isShown) { this.notifies[key].notice = new Notice(messageContent, 0); } @@ -369,9 +420,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule } catch { // NO OP } - }) + }); } } - } - diff --git a/src/modules/features/ModuleObsidianDocumentHistory.ts b/src/modules/features/ModuleObsidianDocumentHistory.ts index 52bf5b6..752aeba 100644 --- a/src/modules/features/ModuleObsidianDocumentHistory.ts +++ b/src/modules/features/ModuleObsidianDocumentHistory.ts @@ -7,20 +7,15 @@ import { DocumentHistoryModal } from "./DocumentHistory/DocumentHistoryModal.ts" import { getPath } from "../../common/utils.ts"; import { fireAndForget } from "octagonal-wheels/promises"; - - export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implements IObsidianModule { - - $everyOnloadStart(): Promise { - this.addCommand({ id: "livesync-history", name: "Show history", callback: () => { const file = this.core.$$getActiveFilePath(); if (file) this.showHistory(file, undefined); - } + }, }); this.addCommand({ @@ -30,9 +25,12 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implem fireAndForget(async () => await this.fileHistory()); }, }); - eventHub.onEvent(EVENT_REQUEST_SHOW_HISTORY, ({ file, fileOnDB }: { file: TFile | FilePathWithPrefix, fileOnDB: LoadedEntry }) => { - this.showHistory(file, fileOnDB._id); - }) + eventHub.onEvent( + EVENT_REQUEST_SHOW_HISTORY, + ({ file, fileOnDB }: { file: TFile | FilePathWithPrefix; fileOnDB: LoadedEntry }) => { + this.showHistory(file, fileOnDB._id); + } + ); return Promise.resolve(true); } @@ -41,17 +39,16 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule implem } async fileHistory() { - const notes: { id: DocumentID, path: FilePathWithPrefix, dispPath: string, mtime: number }[] = []; + const notes: { id: DocumentID; path: FilePathWithPrefix; dispPath: string; mtime: number }[] = []; for await (const doc of this.localDatabase.findAllDocs()) { notes.push({ id: doc._id, path: getPath(doc), dispPath: getPath(doc), mtime: doc.mtime }); } notes.sort((a, b) => b.mtime - a.mtime); - const notesList = notes.map(e => e.dispPath); + const notesList = notes.map((e) => e.dispPath); const target = await this.core.confirm.askSelectString("File to view History", notesList); if (target) { - const targetId = notes.find(e => e.dispPath == target)!; + const targetId = notes.find((e) => e.dispPath == target)!; this.showHistory(targetId.path, targetId.id); } } - -} \ No newline at end of file +} diff --git a/src/modules/features/ModuleObsidianSetting.ts b/src/modules/features/ModuleObsidianSetting.ts index 584d7ca..4d33d07 100644 --- a/src/modules/features/ModuleObsidianSetting.ts +++ b/src/modules/features/ModuleObsidianSetting.ts @@ -1,19 +1,25 @@ import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidianModule.ts"; // import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser"; import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events"; -import { type BucketSyncSetting, type ConfigPassphraseStore, type CouchDBConnection, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, SALT_OF_PASSPHRASE } from "../../lib/src/common/types"; +import { + type BucketSyncSetting, + type ConfigPassphraseStore, + type CouchDBConnection, + DEFAULT_SETTINGS, + type ObsidianLiveSyncSettings, + SALT_OF_PASSPHRASE, +} from "../../lib/src/common/types"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger"; import { encrypt, tryDecrypt } from "octagonal-wheels/encryption"; import { setLang } from "../../lib/src/common/i18n"; import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb"; export class ModuleObsidianSettings extends AbstractObsidianModule implements IObsidianModule { - getPassphrase(settings: ObsidianLiveSyncSettings) { - const methods: Record Promise)> = { + const methods: Record Promise> = { "": () => Promise.resolve("*"), - "LOCALSTORAGE": () => Promise.resolve(localStorage.getItem("ls-setting-passphrase") ?? false), - "ASK_AT_LAUNCH": () => this.core.confirm.askString("Passphrase", "passphrase", "") - } + LOCALSTORAGE: () => Promise.resolve(localStorage.getItem("ls-setting-passphrase") ?? false), + ASK_AT_LAUNCH: () => this.core.confirm.askString("Passphrase", "passphrase", ""), + }; const method = settings.configPassphraseStore; const methodFunc = method in methods ? methods[method] : methods[""]; return methodFunc(); @@ -29,7 +35,6 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO this.usedPassphrase = ""; } - async decryptConfigurationItem(encrypted: string, passphrase: string) { const dec = await tryDecrypt(encrypted, passphrase + SALT_OF_PASSPHRASE, false); if (dec) { @@ -39,7 +44,6 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO return false; } - async encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings) { if (this.usedPassphrase != "") { return await encrypt(src, this.usedPassphrase + SALT_OF_PASSPHRASE, false); @@ -47,7 +51,10 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO const passphrase = await this.getPassphrase(settings); if (passphrase === false) { - this._log("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT); + this._log( + "Could not determine passphrase to save data.json! You probably make the configuration sure again!", + LOG_LEVEL_URGENT + ); return ""; } const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE, false); @@ -60,17 +67,25 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO } get appId() { - return `${("appId" in this.app ? this.app.appId : "")}`; + return `${"appId" in this.app ? this.app.appId : ""}`; } async $$saveSettingData() { this.core.$$saveDeviceAndVaultName(); const settings = { ...this.settings }; settings.deviceAndVaultName = ""; - if (this.usedPassphrase == "" && !await this.getPassphrase(settings)) { - this._log("Could not determine passphrase for saving data.json! Our data.json have insecure items!", LOG_LEVEL_NOTICE); + if (this.usedPassphrase == "" && !(await this.getPassphrase(settings))) { + this._log( + "Could not determine passphrase for saving data.json! Our data.json have insecure items!", + LOG_LEVEL_NOTICE + ); } else { - if (settings.couchDB_PASSWORD != "" || settings.couchDB_URI != "" || settings.couchDB_USER != "" || settings.couchDB_DBNAME) { + if ( + settings.couchDB_PASSWORD != "" || + settings.couchDB_URI != "" || + settings.couchDB_USER != "" || + settings.couchDB_DBNAME + ) { const connectionSetting: CouchDBConnection & BucketSyncSetting = { couchDB_DBNAME: settings.couchDB_DBNAME, couchDB_PASSWORD: settings.couchDB_PASSWORD, @@ -81,9 +96,12 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO endpoint: settings.endpoint, region: settings.region, secretKey: settings.secretKey, - useCustomRequestHandler: settings.useCustomRequestHandler + useCustomRequestHandler: settings.useCustomRequestHandler, }; - settings.encryptedCouchDBConnection = await this.encryptConfigurationItem(JSON.stringify(connectionSetting), settings); + settings.encryptedCouchDBConnection = await this.encryptConfigurationItem( + JSON.stringify(connectionSetting), + settings + ); settings.couchDB_PASSWORD = ""; settings.couchDB_DBNAME = ""; settings.couchDB_URI = ""; @@ -98,7 +116,6 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO settings.encryptedPassphrase = await this.encryptConfigurationItem(settings.passphrase, settings); settings.passphrase = ""; } - } await this.core.saveData(settings); eventHub.emitEvent(EVENT_SETTING_SAVED, settings); @@ -127,7 +144,10 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO } const passphrase = await this.getPassphrase(settings); if (passphrase === false) { - this._log("Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT); + this._log( + "Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", + LOG_LEVEL_URGENT + ); } else { if (settings.encryptedCouchDBConnection) { const keys = [ @@ -139,17 +159,23 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO "bucket", "endpoint", "region", - "secretKey"] as (keyof CouchDBConnection | keyof BucketSyncSetting)[]; - const decrypted = this.tryDecodeJson(await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, passphrase)) as (CouchDBConnection & BucketSyncSetting); + "secretKey", + ] as (keyof CouchDBConnection | keyof BucketSyncSetting)[]; + const decrypted = this.tryDecodeJson( + await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, passphrase) + ) as CouchDBConnection & BucketSyncSetting; if (decrypted) { for (const key of keys) { if (key in decrypted) { //@ts-ignore - settings[key] = decrypted[key] + settings[key] = decrypted[key]; } } } else { - this._log("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT); + this._log( + "Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", + LOG_LEVEL_URGENT + ); for (const key of keys) { //@ts-ignore settings[key] = ""; @@ -162,11 +188,13 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO if (decrypted) { settings.passphrase = decrypted; } else { - this._log("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT); + this._log( + "Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", + LOG_LEVEL_URGENT + ); settings.passphrase = ""; } } - } this.settings = settings; setLang(this.settings.displayLanguage); @@ -191,7 +219,10 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO } } if (isCloudantURI(this.settings.couchDB_URI) && this.settings.customChunkSize != 0) { - this._log("Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.", LOG_LEVEL_NOTICE) + this._log( + "Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.", + LOG_LEVEL_NOTICE + ); this.settings.customChunkSize = 0; } this.core.$$setDeviceAndVaultName(localStorage.getItem(lsKey) || ""); @@ -204,4 +235,4 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO // this.core.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim()); eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB); } -} \ No newline at end of file +} diff --git a/src/modules/features/ModuleObsidianSettingAsMarkdown.ts b/src/modules/features/ModuleObsidianSettingAsMarkdown.ts index b5892b0..133c5db 100644 --- a/src/modules/features/ModuleObsidianSettingAsMarkdown.ts +++ b/src/modules/features/ModuleObsidianSettingAsMarkdown.ts @@ -9,7 +9,6 @@ import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } const SETTING_HEADER = "````yaml:livesync-setting\n"; const SETTING_FOOTER = "\n````"; export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule implements IObsidianModule { - $everyOnloadStart(): Promise { this.addCommand({ id: "livesync-export-config", @@ -21,8 +20,8 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp fireAndForget(async () => { await this.core.$$saveSettingData(); }); - } - }) + }, + }); this.addCommand({ id: "livesync-import-config", name: "Parse setting file", @@ -33,32 +32,33 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp return ret.body != ""; } if (ctx.file) { - const file = ctx.file + const file = ctx.file; fireAndForget(async () => await this.checkAndApplySettingFromMarkdown(file.path, false)); } }, - }) - eventHub.onEvent("event-file-changed", (info: { - file: FilePathWithPrefix, automated: boolean - }) => { + }); + eventHub.onEvent("event-file-changed", (info: { file: FilePathWithPrefix; automated: boolean }) => { fireAndForget(() => this.checkAndApplySettingFromMarkdown(info.file, info.automated)); }); eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => { if (settings.settingSyncFile != "") { fireAndForget(() => this.saveSettingToMarkdown(settings.settingSyncFile)); } - }) + }); return Promise.resolve(true); } - extractSettingFromWholeText(data: string): { - preamble: string, body: string, postscript: string + preamble: string; + body: string; + postscript: string; } { if (data.indexOf(SETTING_HEADER) === -1) { return { - preamble: data, body: "", postscript: "" - } + preamble: data, + body: "", + postscript: "", + }; } const startMarkerPos = data.indexOf(SETTING_HEADER); const dataStartPos = startMarkerPos == -1 ? data.length : startMarkerPos; @@ -68,27 +68,33 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp const ret = { preamble: data.substring(0, dataStartPos), body, - postscript: data.substring(dataEndPos + SETTING_FOOTER.length + 1) - } + postscript: data.substring(dataEndPos + SETTING_FOOTER.length + 1), + }; return ret; } async parseSettingFromMarkdown(filename: string, data?: string) { const file = await this.core.storageAccess.isExists(filename); - if (!file) return { - preamble: "", body: "", postscript: "", - }; + if (!file) + return { + preamble: "", + body: "", + postscript: "", + }; if (data) { return this.extractSettingFromWholeText(data); } - const parseData = data ?? await this.core.storageAccess.readFileText(filename); + const parseData = data ?? (await this.core.storageAccess.readFileText(filename)); return this.extractSettingFromWholeText(parseData); } async checkAndApplySettingFromMarkdown(filename: string, automated?: boolean) { if (automated && !this.settings.notifyAllSettingSyncFile) { if (!this.settings.settingSyncFile || this.settings.settingSyncFile != filename) { - this._log(`Setting file (${filename}) is not matched to the current configuration. skipped.`, LOG_LEVEL_DEBUG); + this._log( + `Setting file (${filename}) is not matched to the current configuration. skipped.`, + LOG_LEVEL_DEBUG + ); return; } } @@ -103,61 +109,80 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp } if ("settingSyncFile" in newSetting && newSetting.settingSyncFile != filename) { - this._log("This setting file seems to backed up one. Please fix the filename or settingSyncFile value.", automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE); + this._log( + "This setting file seems to backed up one. Please fix the filename or settingSyncFile value.", + automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE + ); return; } - let settingToApply = { ...DEFAULT_SETTINGS } as ObsidianLiveSyncSettings; - settingToApply = { ...settingToApply, ...newSetting } - if (!(settingToApply?.writeCredentialsForSettingSync)) { - //New setting does not contains credentials. + settingToApply = { ...settingToApply, ...newSetting }; + if (!settingToApply?.writeCredentialsForSettingSync) { + //New setting does not contains credentials. settingToApply.couchDB_USER = this.settings.couchDB_USER; settingToApply.couchDB_PASSWORD = this.settings.couchDB_PASSWORD; settingToApply.passphrase = this.settings.passphrase; } - const oldSetting = this.generateSettingForMarkdown(this.settings, settingToApply.writeCredentialsForSettingSync); + const oldSetting = this.generateSettingForMarkdown( + this.settings, + settingToApply.writeCredentialsForSettingSync + ); if (!isObjectDifferent(oldSetting, this.generateSettingForMarkdown(settingToApply))) { - this._log("Setting markdown has been detected, but not changed.", automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE); - return + this._log( + "Setting markdown has been detected, but not changed.", + automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE + ); + return; } const addMsg = this.settings.settingSyncFile != filename ? " (This is not-active file)" : ""; - this.core.confirm.askInPopup("apply-setting-from-md", `Setting markdown ${filename}${addMsg} has been detected. Apply this from {HERE}.`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - fireAndForget(async () => { - const APPLY_ONLY = "Apply settings"; - const APPLY_AND_RESTART = "Apply settings and restart obsidian"; - const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md"; - const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md"; - const CANCEL = "Cancel"; - const result = await this.core.confirm.askSelectStringDialogue("Ready for apply the setting.", [ - APPLY_AND_RESTART, - APPLY_ONLY, - APPLY_AND_FETCH, - APPLY_AND_REBUILD, - CANCEL], { defaultAction: APPLY_AND_RESTART }); - if (result == APPLY_ONLY || result == APPLY_AND_RESTART || result == APPLY_AND_REBUILD || result == APPLY_AND_FETCH) { - this.core.settings = settingToApply; - await this.core.$$saveSettingData(); - if (result == APPLY_ONLY) { - this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE); - return; + this.core.confirm.askInPopup( + "apply-setting-from-md", + `Setting markdown ${filename}${addMsg} has been detected. Apply this from {HERE}.`, + (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + fireAndForget(async () => { + const APPLY_ONLY = "Apply settings"; + const APPLY_AND_RESTART = "Apply settings and restart obsidian"; + const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md"; + const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md"; + const CANCEL = "Cancel"; + const result = await this.core.confirm.askSelectStringDialogue( + "Ready for apply the setting.", + [APPLY_AND_RESTART, APPLY_ONLY, APPLY_AND_FETCH, APPLY_AND_REBUILD, CANCEL], + { defaultAction: APPLY_AND_RESTART } + ); + if ( + result == APPLY_ONLY || + result == APPLY_AND_RESTART || + result == APPLY_AND_REBUILD || + result == APPLY_AND_FETCH + ) { + this.core.settings = settingToApply; + await this.core.$$saveSettingData(); + if (result == APPLY_ONLY) { + this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE); + return; + } + if (result == APPLY_AND_REBUILD) { + await this.core.rebuilder.scheduleRebuild(); + } + if (result == APPLY_AND_FETCH) { + await this.core.rebuilder.scheduleFetch(); + } + this.core.$$performRestart(); } - if (result == APPLY_AND_REBUILD) { - await this.core.rebuilder.scheduleRebuild(); - } - if (result == APPLY_AND_FETCH) { - await this.core.rebuilder.scheduleFetch(); - } - this.core.$$performRestart(); - } - }) - }) - }) + }); + }); + } + ); } - generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial { + generateSettingForMarkdown( + settings?: ObsidianLiveSyncSettings, + keepCredential?: boolean + ): Partial { const saveData = { ...(settings ? settings : this.settings) } as Partial; delete saveData.encryptedCouchDBConnection; delete saveData.encryptedPassphrase; @@ -174,7 +199,6 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractObsidianModule imp const saveData = this.generateSettingForMarkdown(); const file = await this.core.storageAccess.isExists(filename); - if (!file) { await this.core.storageAccess.ensureDir(filename); const initialContent = `This file contains Self-hosted LiveSync settings as YAML. @@ -188,8 +212,11 @@ We can perform a command in this file. **Note** Please handle it with all of your care if you have configured to write credentials in. -` - await this.core.storageAccess.writeFileAuto(filename, initialContent + SETTING_HEADER + "\n" + SETTING_FOOTER); +`; + await this.core.storageAccess.writeFileAuto( + filename, + initialContent + SETTING_HEADER + "\n" + SETTING_FOOTER + ); } // if (!(file instanceof TFile)) { // this._log(`Markdown Setting: ${filename} already exists as a folder`, LOG_LEVEL_NOTICE); @@ -203,9 +230,11 @@ We can perform a command in this file. if (newBody == body) { this._log("Markdown setting: Nothing had been changed", LOG_LEVEL_VERBOSE); } else { - await this.core.storageAccess.writeFileAuto(filename, preamble + SETTING_HEADER + newBody + SETTING_FOOTER + postscript); + await this.core.storageAccess.writeFileAuto( + filename, + preamble + SETTING_HEADER + newBody + SETTING_FOOTER + postscript + ); this._log(`Markdown setting: ${filename} has been updated!`, LOG_LEVEL_VERBOSE); } } - -} \ No newline at end of file +} diff --git a/src/modules/features/ModuleObsidianSettingTab.ts b/src/modules/features/ModuleObsidianSettingTab.ts index b095760..994859f 100644 --- a/src/modules/features/ModuleObsidianSettingTab.ts +++ b/src/modules/features/ModuleObsidianSettingTab.ts @@ -4,7 +4,6 @@ import { type IObsidianModule, AbstractObsidianModule } from "../AbstractObsidia import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "../../common/events.ts"; export class ModuleObsidianSettingDialogue extends AbstractObsidianModule implements IObsidianModule { - settingTab!: ObsidianLiveSyncSettingTab; $everyOnloadStart(): Promise { @@ -28,8 +27,6 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule implem } get appId() { - return `${("appId" in this.app ? this.app.appId : "")}`; + return `${"appId" in this.app ? this.app.appId : ""}`; } - - -} \ No newline at end of file +} diff --git a/src/modules/features/ModuleSetupObsidian.ts b/src/modules/features/ModuleSetupObsidian.ts index a223502..e00b185 100644 --- a/src/modules/features/ModuleSetupObsidian.ts +++ b/src/modules/features/ModuleSetupObsidian.ts @@ -1,4 +1,9 @@ -import { type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types.ts"; +import { + type ObsidianLiveSyncSettings, + DEFAULT_SETTINGS, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, +} from "../../lib/src/common/types.ts"; import { configURIBase } from "../../common/types.ts"; // import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js"; import { decrypt, encrypt } from "../../lib/src/encryption/e2ee_v2.ts"; @@ -6,10 +11,12 @@ import { fireAndForget } from "../../lib/src/common/utils.ts"; import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_SETUP_URI, eventHub } from "../../common/events.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; - export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsidianModule { $everyOnload(): Promise { - this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => await this.setupWizard(conf.settings)); + this.registerObsidianProtocolHandler( + "setuplivesync", + async (conf: any) => await this.setupWizard(conf.settings) + ); this.addCommand({ id: "livesync-copysetupuri", @@ -39,30 +46,55 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi } async command_copySetupURI(stripExtra = true) { - const encryptingPassphrase = await this.core.confirm.askString("Encrypt your settings", "The passphrase to encrypt the setup URI", "", true); - if (encryptingPassphrase === false) - return; - const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" } as Partial; + const encryptingPassphrase = await this.core.confirm.askString( + "Encrypt your settings", + "The passphrase to encrypt the setup URI", + "", + true + ); + if (encryptingPassphrase === false) return; + const setting = { + ...this.settings, + configPassphraseStore: "", + encryptedCouchDBConnection: "", + encryptedPassphrase: "", + } as Partial; if (stripExtra) { delete setting.pluginSyncExtendedSetting; } const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]; for (const k of keys) { - if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) { + if ( + JSON.stringify(k in setting ? setting[k] : "") == + JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*") + ) { delete setting[k]; } } - const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false)); + const encryptedSetting = encodeURIComponent( + await encrypt(JSON.stringify(setting), encryptingPassphrase, false) + ); const uri = `${configURIBase}${encryptedSetting}`; await navigator.clipboard.writeText(uri); this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); } async command_copySetupURIFull() { - const encryptingPassphrase = await this.core.confirm.askString("Encrypt your settings", "The passphrase to encrypt the setup URI", "", true); - if (encryptingPassphrase === false) - return; - const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" }; - const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false)); + const encryptingPassphrase = await this.core.confirm.askString( + "Encrypt your settings", + "The passphrase to encrypt the setup URI", + "", + true + ); + if (encryptingPassphrase === false) return; + const setting = { + ...this.settings, + configPassphraseStore: "", + encryptedCouchDBConnection: "", + encryptedPassphrase: "", + }; + const encryptedSetting = encodeURIComponent( + await encrypt(JSON.stringify(setting), encryptingPassphrase, false) + ); const uri = `${configURIBase}${encryptedSetting}`; await navigator.clipboard.writeText(uri); this._log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); @@ -72,8 +104,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi } async command_openSetupURI() { const setupURI = await this.core.confirm.askString("Easy setup", "Set up URI", `${configURIBase}aaaaa`); - if (setupURI === false) - return; + if (setupURI === false) return; if (!setupURI.startsWith(`${configURIBase}`)) { this._log("Set up URI looks wrong.", LOG_LEVEL_NOTICE); return; @@ -85,12 +116,19 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi async setupWizard(confString: string) { try { const oldConf = JSON.parse(JSON.stringify(this.settings)); - const encryptingPassphrase = await this.core.confirm.askString("Passphrase", "The passphrase to decrypt your setup URI", "", true); - if (encryptingPassphrase === false) - return; + const encryptingPassphrase = await this.core.confirm.askString( + "Passphrase", + "The passphrase to decrypt your setup URI", + "", + true + ); + if (encryptingPassphrase === false) return; const newConf = await JSON.parse(await decrypt(confString, encryptingPassphrase, false)); if (newConf) { - const result = await this.core.confirm.askYesNoDialog("Importing Configuration from the Setup-URI. Are you sure to proceed?", {}); + const result = await this.core.confirm.askYesNoDialog( + "Importing Configuration from the Setup-URI. Are you sure to proceed?", + {} + ); if (result == "yes") { const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf) as ObsidianLiveSyncSettings; this.core.replicator.closeReplication(); @@ -100,7 +138,7 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi newSettingW.configPassphraseStore = ""; newSettingW.encryptedPassphrase = ""; newSettingW.encryptedCouchDBConnection = ""; - newSettingW.additionalSuffixOfDatabaseName = `${("appId" in this.app ? this.app.appId : "")}` + newSettingW.additionalSuffixOfDatabaseName = `${"appId" in this.app ? this.app.appId : ""}`; const setupJustImport = "Just import setting"; const setupAsNew = "Set it up as secondary or subsequent device"; const setupAsMerge = "Secondary device but try keeping local changes"; @@ -114,7 +152,11 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi newSettingW.useIndexedDBAdapter = true; } - const setupType = await this.core.confirm.askSelectStringDialogue("How would you like to set it up?", [setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually], { defaultAction: setupAsNew }); + const setupType = await this.core.confirm.askSelectStringDialogue( + "How would you like to set it up?", + [setupAsNew, setupAgain, setupAsMerge, setupJustImport, setupManually], + { defaultAction: setupAsNew } + ); if (setupType == setupJustImport) { this.core.settings = newSettingW; this.core.$$clearUsedPassphrase(); @@ -128,16 +170,27 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi this.core.$$clearUsedPassphrase(); await this.core.rebuilder.$fetchLocal(true); } else if (setupType == setupAgain) { - const confirm = "I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed."; - if (await this.core.confirm.askSelectStringDialogue("Do you really want to do this?", ["Cancel", confirm], { defaultAction: "Cancel" }) != confirm) { + const confirm = + "I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed."; + if ( + (await this.core.confirm.askSelectStringDialogue( + "Do you really want to do this?", + ["Cancel", confirm], + { defaultAction: "Cancel" } + )) != confirm + ) { return; } this.core.settings = newSettingW; this.core.$$clearUsedPassphrase(); await this.core.rebuilder.$rebuildEverything(); } else if (setupType == setupManually) { - const keepLocalDB = await this.core.confirm.askYesNoDialog("Keep local DB?", { defaultOption: "No" }); - const keepRemoteDB = await this.core.confirm.askYesNoDialog("Keep remote DB?", { defaultOption: "No" }); + const keepLocalDB = await this.core.confirm.askYesNoDialog("Keep local DB?", { + defaultOption: "No", + }); + const keepRemoteDB = await this.core.confirm.askYesNoDialog("Keep remote DB?", { + defaultOption: "No", + }); if (keepLocalDB == "yes" && keepRemoteDB == "yes") { // nothing to do. so peaceful. this.core.settings = newSettingW; @@ -145,7 +198,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi await this.core.$allSuspendAllSync(); await this.core.$allSuspendExtraSync(); await this.core.saveSettings(); - const replicate = await this.core.confirm.askYesNoDialog("Unlock and replicate?", { defaultOption: "Yes" }); + const replicate = await this.core.confirm.askYesNoDialog("Unlock and replicate?", { + defaultOption: "Yes", + }); if (replicate == "yes") { await this.core.$$replicate(true); await this.core.$$markRemoteUnlocked(); @@ -154,7 +209,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi return; } if (keepLocalDB == "no" && keepRemoteDB == "no") { - const reset = await this.core.confirm.askYesNoDialog("Drop everything?", { defaultOption: "No" }); + const reset = await this.core.confirm.askYesNoDialog("Drop everything?", { + defaultOption: "No", + }); if (reset != "yes") { this._log("Cancelled", LOG_LEVEL_NOTICE); this.core.settings = oldConf; @@ -168,7 +225,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi if (keepLocalDB == "no") { await this.core.$$resetLocalDatabase(); await this.core.localDatabase.initializeDatabase(); - const rebuild = await this.core.confirm.askYesNoDialog("Rebuild the database?", { defaultOption: "Yes" }); + const rebuild = await this.core.confirm.askYesNoDialog("Rebuild the database?", { + defaultOption: "Yes", + }); if (rebuild == "yes") { initDB = this.core.$$initializeDatabase(true); } else { @@ -180,7 +239,9 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi await this.core.$$markRemoteLocked(); } if (keepLocalDB == "no" || keepRemoteDB == "no") { - const replicate = await this.core.confirm.askYesNoDialog("Replicate once?", { defaultOption: "Yes" }); + const replicate = await this.core.confirm.askYesNoDialog("Replicate once?", { + defaultOption: "Yes", + }); if (replicate == "yes") { if (initDB != null) { await initDB; @@ -200,6 +261,4 @@ export class ModuleSetupObsidian extends AbstractObsidianModule implements IObsi this._log(ex, LOG_LEVEL_VERBOSE); } } - - } diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index 9d9f823..acf9eec 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -1,9 +1,35 @@ -import { Setting, TextComponent, type ToggleComponent, type DropdownComponent, ButtonComponent, type TextAreaComponent, type ValueComponent } from "obsidian"; +import { + Setting, + TextComponent, + type ToggleComponent, + type DropdownComponent, + ButtonComponent, + type TextAreaComponent, + type ValueComponent, +} from "obsidian"; import { unique } from "octagonal-wheels/collection"; -import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "../../../lib/src/common/types.ts"; -import { type ObsidianLiveSyncSettingTab, type AutoWireOption, wrapMemo, type OnUpdateResult, createStub, findAttrFromParent } from "./ObsidianLiveSyncSettingTab.ts"; -import { type AllSettingItemKey, getConfig, type AllSettings, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey } from "./settingConstants.ts"; - +import { + LEVEL_ADVANCED, + LEVEL_POWER_USER, + statusDisplay, + type ConfigurationItem, +} from "../../../lib/src/common/types.ts"; +import { + type ObsidianLiveSyncSettingTab, + type AutoWireOption, + wrapMemo, + type OnUpdateResult, + createStub, + findAttrFromParent, +} from "./ObsidianLiveSyncSettingTab.ts"; +import { + type AllSettingItemKey, + getConfig, + type AllSettings, + type AllStringItemKey, + type AllNumericItemKey, + type AllBooleanItemKey, +} from "./settingConstants.ts"; export class LiveSyncSetting extends Setting { autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; @@ -29,8 +55,9 @@ export class LiveSyncSetting extends Setting { DEV: { const paneName = findAttrFromParent(this.settingEl, "data-pane"); const panelName = findAttrFromParent(this.settingEl, "data-panel"); - const itemName = typeof this.nameBuf == "string" ? this.nameBuf : this.nameBuf.textContent?.toString() ?? ""; - const strValue = typeof value == "string" ? value : value.textContent?.toString() ?? ""; + const itemName = + typeof this.nameBuf == "string" ? this.nameBuf : (this.nameBuf.textContent?.toString() ?? ""); + const strValue = typeof value == "string" ? value : (value.textContent?.toString() ?? ""); createStub(itemName, key, strValue, panelName, paneName); } @@ -111,9 +138,11 @@ export class LiveSyncSetting extends Setting { } autoWireText(key: AllStringItemKey, opt?: AutoWireOption) { const conf = this.autoWireSetting(key, opt); - this.addText(text => { + this.addText((text) => { this.autoWiredComponent = text; - const setValue = wrapMemo((value: string) => { text.setValue(value) }); + const setValue = wrapMemo((value: string) => { + text.setValue(value); + }); this.invalidateValue = () => setValue(`${LiveSyncSetting.env.editingSettings[key]}`); this.invalidateValue(); text.onChange(async (value) => { @@ -129,9 +158,11 @@ export class LiveSyncSetting extends Setting { } autoWireTextArea(key: AllStringItemKey, opt?: AutoWireOption) { const conf = this.autoWireSetting(key, opt); - this.addTextArea(text => { + this.addTextArea((text) => { this.autoWiredComponent = text; - const setValue = wrapMemo((value: string) => { text.setValue(value) }); + const setValue = wrapMemo((value: string) => { + text.setValue(value); + }); this.invalidateValue = () => setValue(`${LiveSyncSetting.env.editingSettings[key]}`); this.invalidateValue(); text.onChange(async (value) => { @@ -145,9 +176,12 @@ export class LiveSyncSetting extends Setting { }); return this; } - autoWireNumeric(key: AllNumericItemKey, opt: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean; }) { + autoWireNumeric( + key: AllNumericItemKey, + opt: AutoWireOption & { clampMin?: number; clampMax?: number; acceptZero?: boolean } + ) { const conf = this.autoWireSetting(key, opt); - this.addText(text => { + this.addText((text) => { this.autoWiredComponent = text; if (opt.clampMin) { text.inputEl.setAttribute("min", `${opt.clampMin}`); @@ -156,7 +190,9 @@ export class LiveSyncSetting extends Setting { text.inputEl.setAttribute("max", `${opt.clampMax}`); } let lastError = false; - const setValue = wrapMemo((value: string) => { text.setValue(value) }); + const setValue = wrapMemo((value: string) => { + text.setValue(value); + }); this.invalidateValue = () => { if (!lastError) setValue(`${LiveSyncSetting.env.editingSettings[key]}`); }; @@ -193,9 +229,11 @@ export class LiveSyncSetting extends Setting { } autoWireToggle(key: AllBooleanItemKey, opt?: AutoWireOption) { const conf = this.autoWireSetting(key, opt); - this.addToggle(toggle => { + this.addToggle((toggle) => { this.autoWiredComponent = toggle; - const setValue = wrapMemo((value: boolean) => { toggle.setValue(opt?.invert ? !value : value) }); + const setValue = wrapMemo((value: boolean) => { + toggle.setValue(opt?.invert ? !value : value); + }); this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] ?? false); this.invalidateValue(); @@ -207,16 +245,15 @@ export class LiveSyncSetting extends Setting { }); return this; } - autoWireDropDown(key: AllStringItemKey, opt: AutoWireOption & { options: Record; }) { + autoWireDropDown(key: AllStringItemKey, opt: AutoWireOption & { options: Record }) { const conf = this.autoWireSetting(key, opt); - this.addDropdown(dropdown => { + this.addDropdown((dropdown) => { this.autoWiredComponent = dropdown; const setValue = wrapMemo((value: string) => { dropdown.setValue(value); }); - dropdown - .addOptions(opt.options); + dropdown.addOptions(opt.options); this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] || ""); this.invalidateValue(); @@ -264,7 +301,6 @@ export class LiveSyncSetting extends Setting { const newConf = this._getComputedStatus(); const keys = Object.keys(newConf) as [keyof OnUpdateResult]; for (const k of keys) { - if (k in this.prevStatus && this.prevStatus[k] == newConf[k]) { continue; } @@ -277,8 +313,8 @@ export class LiveSyncSetting extends Setting { case "classes": break; case "disabled": - this.setDisabled((newConf[k] || false)); - this.settingEl.toggleClass("sls-setting-disabled", (newConf[k] || false)); + this.setDisabled(newConf[k] || false); + this.settingEl.toggleClass("sls-setting-disabled", newConf[k] || false); this.prevStatus[k] = newConf[k]; break; case "isCta": diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 8132119..43d4e62 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -1,26 +1,77 @@ import { App, PluginSettingTab, MarkdownRenderer, stringifyYaml } from "../../../deps.ts"; import { - DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO, type LoadedEntry, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, REMOTE_COUCHDB, REMOTE_MINIO, PREFERRED_JOURNAL_SYNC, FLAGMD_REDFLAG, type ConfigLevel, LEVEL_POWER_USER, LEVEL_ADVANCED, LEVEL_EDGE_CASE, type MetaEntry, type FilePath, + DEFAULT_SETTINGS, + type ObsidianLiveSyncSettings, + type ConfigPassphraseStore, + type RemoteDBSettings, + type FilePathWithPrefix, + type HashAlgorithm, + type DocumentID, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + LOG_LEVEL_INFO, + type LoadedEntry, + PREFERRED_SETTING_CLOUDANT, + PREFERRED_SETTING_SELF_HOSTED, + FLAGMD_REDFLAG2_HR, + FLAGMD_REDFLAG3_HR, + REMOTE_COUCHDB, + REMOTE_MINIO, + PREFERRED_JOURNAL_SYNC, + FLAGMD_REDFLAG, + type ConfigLevel, + LEVEL_POWER_USER, + LEVEL_ADVANCED, + LEVEL_EDGE_CASE, + type MetaEntry, + type FilePath, } from "../../../lib/src/common/types.ts"; -import { createBlob, delay, isDocContentSame, isObjectDifferent, readAsBlob, sizeToHumanReadable } from "../../../lib/src/common/utils.ts"; +import { + createBlob, + delay, + isDocContentSame, + isObjectDifferent, + readAsBlob, + sizeToHumanReadable, +} from "../../../lib/src/common/utils.ts"; import { versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts"; import { Logger } from "../../../lib/src/common/logger.ts"; -import { balanceChunkPurgedDBs, checkSyncInfo, isCloudantURI, purgeUnreferencedChunks } from "../../../lib/src/pouchdb/utils_couchdb.ts"; +import { + balanceChunkPurgedDBs, + checkSyncInfo, + isCloudantURI, + purgeUnreferencedChunks, +} from "../../../lib/src/pouchdb/utils_couchdb.ts"; import { testCrypt } from "../../../lib/src/encryption/e2ee_v2.ts"; import ObsidianLiveSyncPlugin from "../../../main.ts"; import { getPath, requestToCouchDB, scheduleTask } from "../../../common/utils.ts"; import { request } from "obsidian"; import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts"; -import MultipleRegExpControl from './MultipleRegExpControl.svelte'; +import MultipleRegExpControl from "./MultipleRegExpControl.svelte"; import { LiveSyncCouchDBReplicator } from "../../../lib/src/replication/couchdb/LiveSyncReplicator.ts"; -import { type AllSettingItemKey, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey, type AllSettings, OnDialogSettingsDefault, type OnDialogSettings, getConfName } from "./settingConstants.ts"; +import { + type AllSettingItemKey, + type AllStringItemKey, + type AllNumericItemKey, + type AllBooleanItemKey, + type AllSettings, + OnDialogSettingsDefault, + type OnDialogSettings, + getConfName, +} from "./settingConstants.ts"; import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../../lib/src/common/rosetta.ts"; import { $t } from "../../../lib/src/common/i18n.ts"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises"; import { confirmWithMessage } from "../../coreObsidian/UILib/dialogs.ts"; -import { EVENT_REQUEST_COPY_SETUP_URI, EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, EVENT_REQUEST_OPEN_SETUP_URI, EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "../../../common/events.ts"; +import { + EVENT_REQUEST_COPY_SETUP_URI, + EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, + EVENT_REQUEST_OPEN_SETUP_URI, + EVENT_REQUEST_RELOAD_SETTING_TAB, + eventHub, +} from "../../../common/events.ts"; import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock"; import { JournalSyncMinio } from "../../../lib/src/replication/journal/objectstore/JournalSyncMinio.ts"; import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts"; @@ -28,42 +79,49 @@ import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSy import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; export type OnUpdateResult = { - visibility?: boolean, - disabled?: boolean, - classes?: string[], - isCta?: boolean, - isWarning?: boolean, -} + visibility?: boolean; + disabled?: boolean; + classes?: string[]; + isCta?: boolean; + isWarning?: boolean; +}; type OnUpdateFunc = () => OnUpdateResult; type UpdateFunction = () => void; export type AutoWireOption = { - placeHolder?: string, - holdValue?: boolean, - isPassword?: boolean, - invert?: boolean, + placeHolder?: string; + holdValue?: boolean; + isPassword?: boolean; + invert?: boolean; onUpdate?: OnUpdateFunc; obsolete?: boolean; -} +}; function visibleOnly(cond: () => boolean): OnUpdateFunc { return () => ({ - visibility: cond() - }) + visibility: cond(), + }); } function enableOnly(cond: () => boolean): OnUpdateFunc { return () => ({ - disabled: !cond() - }) + disabled: !cond(), + }); } -type OnSavedHandlerFunc = (value: AllSettings[T]) => (Promise | void); +type OnSavedHandlerFunc = (value: AllSettings[T]) => Promise | void; type OnSavedHandler = { - key: T, handler: OnSavedHandlerFunc, -} + key: T; + handler: OnSavedHandlerFunc; +}; function getLevelStr(level: ConfigLevel) { - return level == LEVEL_POWER_USER ? " (Power User)" : level == LEVEL_ADVANCED ? " (Advanced)" : level == LEVEL_EDGE_CASE ? " (Edge Case)" : ""; + return level == LEVEL_POWER_USER + ? " (Power User)" + : level == LEVEL_ADVANCED + ? " (Advanced)" + : level == LEVEL_EDGE_CASE + ? " (Edge Case)" + : ""; } export function findAttrFromParent(el: HTMLElement, attr: string): string { @@ -78,11 +136,10 @@ export function findAttrFromParent(el: HTMLElement, attr: string): string { return ""; } - // For creating a document const toc = new Set(); const stubs = {} as { - [key: string]: { [key: string]: Map> } + [key: string]: { [key: string]: Map> }; }; export function createStub(name: string, key: string, value: string, panel: string, pane: string) { DEV: { @@ -107,7 +164,7 @@ export function wrapMemo(func: (arg: T) => void) { func(arg); buf = arg; } - } + }; } export class ObsidianLiveSyncSettingTab extends PluginSettingTab { @@ -136,7 +193,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { * Apply editing setting to the plug-in. * @param keys setting keys for applying */ - applySetting(keys: (AllSettingItemKey)[]) { + applySetting(keys: AllSettingItemKey[]) { for (const k of keys) { if (!this.isDirty(k)) continue; if (k in OnDialogSettingsDefault) { @@ -149,15 +206,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { //@ts-ignore this.initialSettings[k] = this.plugin.settings[k]; } - keys.forEach(e => this.refreshSetting(e)); + keys.forEach((e) => this.refreshSetting(e)); } applyAllSettings() { - const changedKeys = (Object.keys(this.editingSettings ?? {}) as AllSettingItemKey[]).filter(e => this.isDirty(e)); + const changedKeys = (Object.keys(this.editingSettings ?? {}) as AllSettingItemKey[]).filter((e) => + this.isDirty(e) + ); this.applySetting(changedKeys); this.reloadAllSettings(); } - async saveLocalSetting(key: (keyof typeof OnDialogSettingsDefault)) { + async saveLocalSetting(key: keyof typeof OnDialogSettingsDefault) { if (key == "configPassphrase") { localStorage.setItem("ls-setting-passphrase", this.editingSettings?.[key] ?? ""); return await Promise.resolve(); @@ -172,7 +231,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { * Apply and save setting to the plug-in. * @param keys setting keys for applying */ - async saveSettings(keys: (AllSettingItemKey)[]) { + async saveSettings(keys: AllSettingItemKey[]) { let hasChanged = false; const appliedKeys = [] as AllSettingItemKey[]; for (const k of keys) { @@ -196,11 +255,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } // if (runOnSaved) { - const handlers = this.onSavedHandlers.filter(e => appliedKeys.indexOf(e.key) !== -1).map(e => e.handler(this.editingSettings[e.key as AllSettingItemKey])); + const handlers = this.onSavedHandlers + .filter((e) => appliedKeys.indexOf(e.key) !== -1) + .map((e) => e.handler(this.editingSettings[e.key as AllSettingItemKey])); await Promise.all(handlers); // } - keys.forEach(e => this.refreshSetting(e)); - + keys.forEach((e) => this.refreshSetting(e)); } /** @@ -208,7 +268,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { * @param keys setting keys for applying */ async saveAllDirtySettings() { - const changedKeys = (Object.keys(this.editingSettings ?? {}) as AllSettingItemKey[]).filter(e => this.isDirty(e)); + const changedKeys = (Object.keys(this.editingSettings ?? {}) as AllSettingItemKey[]).filter((e) => + this.isDirty(e) + ); await this.saveSettings(changedKeys); this.reloadAllSettings(); } @@ -230,15 +292,19 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { reloadAllLocalSettings() { const ret = { ...OnDialogSettingsDefault }; ret.configPassphrase = localStorage.getItem("ls-setting-passphrase") || ""; - ret.preset = "" + ret.preset = ""; ret.deviceAndVaultName = this.plugin.$$getDeviceAndVaultName(); return ret; } computeAllLocalSettings(): Partial { - const syncMode = this.editingSettings?.liveSync ? "LIVESYNC" : this.editingSettings?.periodicReplication ? "PERIODIC" : "ONEVENTS"; + const syncMode = this.editingSettings?.liveSync + ? "LIVESYNC" + : this.editingSettings?.periodicReplication + ? "PERIODIC" + : "ONEVENTS"; return { - syncMode - } + syncMode, + }; } /** * Reread all settings and request invalidate @@ -247,7 +313,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { const localSetting = this.reloadAllLocalSettings(); this._editingSettings = { ...this.plugin.settings, ...localSetting }; this._editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() }; - this.initialSettings = { ...this.editingSettings, }; + this.initialSettings = { ...this.editingSettings }; if (!skipUpdate) this.requestUpdate(); } @@ -269,7 +335,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.editingSettings[key] = this.initialSettings[key]; } } - this.editingSettings = { ...(this.editingSettings), ...this.computeAllLocalSettings() }; + this.editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() }; // this.initialSettings = { ...this.initialSettings }; this.requestUpdate(); } @@ -277,24 +343,24 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { isDirty(key: AllSettingItemKey) { return isObjectDifferent(this.editingSettings[key], this.initialSettings?.[key]); } - isSomeDirty(keys: (AllSettingItemKey)[]) { + isSomeDirty(keys: AllSettingItemKey[]) { // if (debug) { // console.dir(keys); // console.dir(keys.map(e => this.isDirty(e))); // } - return keys.some(e => this.isDirty(e)); + return keys.some((e) => this.isDirty(e)); } - isConfiguredAs(key: AllStringItemKey, value: string): boolean - isConfiguredAs(key: AllNumericItemKey, value: number): boolean - isConfiguredAs(key: AllBooleanItemKey, value: boolean): boolean + isConfiguredAs(key: AllStringItemKey, value: string): boolean; + isConfiguredAs(key: AllNumericItemKey, value: number): boolean; + isConfiguredAs(key: AllBooleanItemKey, value: boolean): boolean; isConfiguredAs(key: AllSettingItemKey, value: AllSettings[typeof key]) { if (!this.editingSettings) { return false; } return this.editingSettings[key] == value; } - // UI Element Wrapper --> + // UI Element Wrapper --> settingComponents = [] as Setting[]; controlledElementFunc = [] as UpdateFunction[]; onSavedHandlers = [] as OnSavedHandler[]; @@ -307,7 +373,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { Setting.env = this; eventHub.onEvent(EVENT_REQUEST_RELOAD_SETTING_TAB, () => { this.requestReload(); - }) + }); } async testConnection(settingOverride: Partial = {}): Promise { @@ -324,10 +390,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { closeSetting() { // @ts-ignore - this.plugin.app.setting.close() + this.plugin.app.setting.close(); } - handleElement(element: HTMLElement, func: OnUpdateFunc) { const updateFunc = ((element, func) => { const prev = {} as OnUpdateResult; @@ -337,30 +402,42 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { for (const k of keys) { if (prev[k] !== newValue[k]) { if (k == "visibility") { - element.toggleClass("sls-setting-hidden", !(newValue[k] || false)) + element.toggleClass("sls-setting-hidden", !(newValue[k] || false)); } //@ts-ignore prev[k] = newValue[k]; } } - } + }; })(element, func); this.controlledElementFunc.push(updateFunc); updateFunc(); } - createEl(el: HTMLElement, tag: T, o?: string | DomElementInfo | undefined, callback?: ((el: HTMLElementTagNameMap[T]) => void), func?: OnUpdateFunc) { + createEl( + el: HTMLElement, + tag: T, + o?: string | DomElementInfo | undefined, + callback?: (el: HTMLElementTagNameMap[T]) => void, + func?: OnUpdateFunc + ) { const element = el.createEl(tag, o, callback); if (func) this.handleElement(element, func); return element; } - addEl(el: HTMLElement, tag: T, o?: string | DomElementInfo | undefined, callback?: ((el: HTMLElementTagNameMap[T]) => void), func?: OnUpdateFunc) { + addEl( + el: HTMLElement, + tag: T, + o?: string | DomElementInfo | undefined, + callback?: (el: HTMLElementTagNameMap[T]) => void, + func?: OnUpdateFunc + ) { const elm = this.createEl(el, tag, o, callback, func); return Promise.resolve(elm); } - addOnSaved(key: T, func: (value: AllSettings[T]) => (Promise | void)) { + addOnSaved(key: T, func: (value: AllSettings[T]) => Promise | void) { this.onSavedHandlers.push({ key, handler: func }); } resetEditingSettings() { @@ -384,13 +461,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { // Something has changed if (this.isDirty(k as AllSettingItemKey)) { // And modified. - this.plugin.confirm.askInPopup(`config-reloaded-${k}`, `The setting "${getConfName(k as AllSettingItemKey)}" being in editing has been changed from somewhere. We can discard modification and reload by clicking {HERE}. Click elsewhere to ignore changes`, (anchor) => { - anchor.text = "HERE"; - anchor.addEventListener("click", () => { - this.refreshSetting(k as AllSettingItemKey); - this.display(); - }); - }); + this.plugin.confirm.askInPopup( + `config-reloaded-${k}`, + `The setting "${getConfName(k as AllSettingItemKey)}" being in editing has been changed from somewhere. We can discard modification and reload by clicking {HERE}. Click elsewhere to ignore changes`, + (anchor) => { + anchor.text = "HERE"; + anchor.addEventListener("click", () => { + this.refreshSetting(k as AllSettingItemKey); + this.display(); + }); + } + ); } else { // not modified this.refreshSetting(k as AllSettingItemKey); @@ -421,10 +502,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.menuEl.querySelectorAll(`.sls-setting-label`).forEach((element) => { if (element.hasClass(`c-${screen}`)) { element.addClass("selected"); - (element.querySelector("input[type=radio]"))!.checked = true; + element.querySelector("input[type=radio]")!.checked = true; } else { element.removeClass("selected"); - (element.querySelector("input[type=radio]"))!.checked = false; + element.querySelector("input[type=radio]")!.checked = false; } }); } @@ -442,7 +523,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { await this.saveAllDirtySettings(); this.containerEl.addClass("isWizard"); this.inWizard = true; - this.changeDisplay("20") + this.changeDisplay("20"); } menuEl?: HTMLElement; @@ -474,7 +555,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { el.addClass(`${styleHead}-disabled`); el.removeClass(`${styleHead}-enabled`); } - } + }; setStyle(containerEl, "menu-setting-poweruser", () => this.isConfiguredAs("usePowerUserMode", true)); setStyle(containerEl, "menu-setting-advanced", () => this.isConfiguredAs("useAdvancedMode", true)); setStyle(containerEl, "menu-setting-edgecase", () => this.isConfiguredAs("useEdgeCaseMode", true)); @@ -501,7 +582,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { changeDisplay(value); } } - } + }; const isNeedRebuildLocal = () => { return this.isSomeDirty([ "useIndexedDBAdapter", @@ -512,8 +593,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { "usePathObfuscation", "encrypt", // "remoteType", - ]) - } + ]); + }; const isNeedRebuildRemote = () => { return this.isSomeDirty([ "doNotUseFixedRevisionForChunks", @@ -521,10 +602,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { "passphrase", "useDynamicIterationCount", "usePathObfuscation", - "encrypt"]) + "encrypt", + ]); }; const confirmRebuild = async () => { - if (!await isPassphraseValid()) { + if (!(await isPassphraseValid())) { Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE); return; } @@ -558,13 +640,21 @@ This case includes the case which you have rebuilt the remote database. ## ${OPTION_ONLY_SETTING} Store only the settings. **Caution: This may lead to data corruption**; database reconstruction is generally necessary.`; const buttons = [ - OPTION_FETCH, OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE, - OPTION_ONLY_SETTING, OPTION_CANCEL]; - const result = await confirmWithMessage(this.plugin, title, note, buttons, OPTION_CANCEL, 0) + OPTION_FETCH, + OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE, + OPTION_ONLY_SETTING, + OPTION_CANCEL, + ]; + const result = await confirmWithMessage(this.plugin, title, note, buttons, OPTION_CANCEL, 0); if (result == OPTION_CANCEL) return; if (result == OPTION_FETCH) { - if (!await checkWorkingPassphrase()) { - if (await this.plugin.confirm.askYesNoDialog("Are you sure to proceed?", { defaultOption: "No" }) != "yes") return; + if (!(await checkWorkingPassphrase())) { + if ( + (await this.plugin.confirm.askYesNoDialog("Are you sure to proceed?", { + defaultOption: "No", + })) != "yes" + ) + return; } } if (!this.editingSettings.encrypt) { @@ -584,15 +674,20 @@ Store only the settings. **Caution: This may lead to data corruption**; database } else if (result == OPTION_ONLY_SETTING) { await this.plugin.saveSettings(); } - - } - this.createEl(menuWrapper, "div", { cls: "sls-setting-menu-buttons" }, (el) => { - el.addClass("wizardHidden"); - el.createEl("label", { text: "Changes need to be applied!" }); - void this.addEl(el, "button", { text: "Apply", cls: "mod-warning" }, buttonEl => { - buttonEl.addEventListener("click", () => fireAndForget(async () => await confirmRebuild())) - }) - }, visibleOnly(() => isNeedRebuildLocal() || isNeedRebuildRemote())); + }; + this.createEl( + menuWrapper, + "div", + { cls: "sls-setting-menu-buttons" }, + (el) => { + el.addClass("wizardHidden"); + el.createEl("label", { text: "Changes need to be applied!" }); + void this.addEl(el, "button", { text: "Apply", cls: "mod-warning" }, (buttonEl) => { + buttonEl.addEventListener("click", () => fireAndForget(async () => await confirmRebuild())); + }); + }, + visibleOnly(() => isNeedRebuildLocal() || isNeedRebuildRemote()) + ); const setLevelClass = (el: HTMLElement, level?: ConfigLevel) => { switch (level) { case LEVEL_POWER_USER: @@ -607,40 +702,67 @@ Store only the settings. **Caution: This may lead to data corruption**; database default: // NO OP. } - } + }; let paneNo = 0; - const addPane = (parentEl: HTMLElement, title: string, icon: string, order: number, wizardHidden: boolean, level?: ConfigLevel) => { + const addPane = ( + parentEl: HTMLElement, + title: string, + icon: string, + order: number, + wizardHidden: boolean, + level?: ConfigLevel + ) => { const el = this.createEl(parentEl, "div", { text: "" }); DEV: { const mdTitle = `${paneNo++}. ${title}${getLevelStr(level ?? "")}`; el.setAttribute("data-pane", mdTitle); - toc.add(`| ${icon} | [${mdTitle}](#${mdTitle.toLowerCase().replace(/ /g, "-").replace(/[^\w\s-]/g, "")}) | `); + toc.add( + `| ${icon} | [${mdTitle}](#${mdTitle + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^\w\s-]/g, "")}) | ` + ); } - setLevelClass(el, level) + setLevelClass(el, level); el.createEl("h3", { text: title, cls: "sls-setting-pane-title" }); if (this.menuEl) { - this.menuEl.createEl("label", { cls: `sls-setting-label c-${order} ${wizardHidden ? "wizardHidden" : ""}` }, el => { - setLevelClass(el, level) - const inputEl = el.createEl("input", { - type: "radio", name: "disp", value: `${order}`, cls: "sls-setting-tab" - } as DomElementInfo); - el.createEl("div", { - cls: "sls-setting-menu-btn", text: icon, title: title - }); - inputEl.addEventListener("change", selectPane); - inputEl.addEventListener("click", selectPane); - }) + this.menuEl.createEl( + "label", + { cls: `sls-setting-label c-${order} ${wizardHidden ? "wizardHidden" : ""}` }, + (el) => { + setLevelClass(el, level); + const inputEl = el.createEl("input", { + type: "radio", + name: "disp", + value: `${order}`, + cls: "sls-setting-tab", + } as DomElementInfo); + el.createEl("div", { + cls: "sls-setting-menu-btn", + text: icon, + title: title, + }); + inputEl.addEventListener("change", selectPane); + inputEl.addEventListener("click", selectPane); + } + ); } addScreenElement(`${order}`, el); - const p = Promise.resolve(el) + const p = Promise.resolve(el); // fireAndForget // p.finally(() => { // // Recap at the end. // }); return p; - } + }; const panelNoMap = {} as { [key: string]: number }; - const addPanel = (parentEl: HTMLElement, title: string, callback?: ((el: HTMLDivElement) => void), func?: OnUpdateFunc, level?: ConfigLevel) => { + const addPanel = ( + parentEl: HTMLElement, + title: string, + callback?: (el: HTMLDivElement) => void, + func?: OnUpdateFunc, + level?: ConfigLevel + ) => { const el = this.createEl(parentEl, "div", { text: "" }, callback, func); DEV: { const paneNo = findAttrFromParent(parentEl, "data-pane"); @@ -651,15 +773,14 @@ Store only the settings. **Caution: This may lead to data corruption**; database const panelNo = panelNoMap[paneNo]; el.setAttribute("data-panel", `${panelNo}. ${title}${getLevelStr(level ?? "")}`); } - setLevelClass(el, level) + setLevelClass(el, level); this.createEl(el, "h4", { text: title, cls: "sls-setting-panel-title" }); const p = Promise.resolve(el); // p.finally(() => { // // Recap at the end. // }) return p; - } - + }; menuTabs.forEach((element) => { const e = element.querySelector(".sls-setting-tab"); @@ -692,14 +813,15 @@ Store only the settings. **Caution: This may lead to data corruption**; database if (this.plugin?.replicator?.syncStatus == "PAUSED") return true; return false; }; - const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled()) - const onlyOnCouchDB = () => ({ - visibility: this.isConfiguredAs('remoteType', REMOTE_COUCHDB) - }) as OnUpdateResult; - const onlyOnMinIO = () => ({ - visibility: this.isConfiguredAs('remoteType', REMOTE_MINIO) - }) as OnUpdateResult; - + const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled()); + const onlyOnCouchDB = () => + ({ + visibility: this.isConfiguredAs("remoteType", REMOTE_COUCHDB), + }) as OnUpdateResult; + const onlyOnMinIO = () => + ({ + visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO), + }) as OnUpdateResult; // E2EE Function const checkWorkingPassphrase = async (): Promise => { @@ -711,7 +833,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database const replicator = this.plugin.$anyNewReplicator(settingForCheck); if (!(replicator instanceof LiveSyncCouchDBReplicator)) return true; - const db = await replicator.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.$$isMobile(), true); + const db = await replicator.connectRemoteCouchDBWithSetting( + settingForCheck, + this.plugin.$$isMobile(), + true + ); if (typeof db === "string") { Logger(`ERROR: Failed to check passphrase with the remote server: \n${db}.`, LOG_LEVEL_NOTICE); return false; @@ -720,11 +846,14 @@ Store only the settings. **Caution: This may lead to data corruption**; database // Logger("Database connected", LOG_LEVEL_NOTICE); return true; } else { - Logger("ERROR: Passphrase is not compatible with the remote server! Please confirm it again!", LOG_LEVEL_NOTICE); + Logger( + "ERROR: Passphrase is not compatible with the remote server! Please confirm it again!", + LOG_LEVEL_NOTICE + ); return false; } } - } + }; const isPassphraseValid = async () => { if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL_NOTICE); @@ -735,9 +864,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database return false; } return true; - } + }; - const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks") => { + const rebuildDB = async ( + method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks" + ) => { if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL_NOTICE); return; @@ -754,12 +885,15 @@ Store only the settings. **Caution: This may lead to data corruption**; database await this.plugin.$allSuspendExtraSync(); this.reloadAllSettings(); this.editingSettings.isConfigured = true; - Logger("All synchronizations have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE) + Logger( + "All synchronizations have been temporarily disabled. Please enable them after the fetching, if you need them.", + LOG_LEVEL_NOTICE + ); await this.saveAllDirtySettings(); this.closeSetting(); await delay(2000); await this.plugin.rebuilder.$performRebuildDB(method); - } + }; // Panes void addPane(containerEl, "Update Information", "💬", 100, false).then((paneEl) => { @@ -767,7 +901,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database const tmpDiv = createDiv(); // tmpDiv.addClass("sls-header-button"); - tmpDiv.addClass("op-warn-info") + tmpDiv.addClass("op-warn-info"); tmpDiv.innerHTML = `

Did you come here because of an upgrade notification? Read the version history and, if you are satisfied, press the button. I will bring it out again in the next version.

`; if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) { @@ -780,11 +914,13 @@ Store only the settings. **Caution: This may lead to data corruption**; database }); }); } - fireAndForget(() => MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin)); + fireAndForget(() => + MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin) + ); }); void addPane(containerEl, "Setup", "🧙‍♂️", 110, false).then((paneEl) => { - void addPanel(paneEl, "Quick Setup").then(paneEl => { + void addPanel(paneEl, "Quick Setup").then((paneEl) => { new Setting(paneEl) .setName("Use the copied setup URI") .setDesc("To setup Self-hosted LiveSync, this method is the most preferred one.") @@ -792,16 +928,14 @@ Store only the settings. **Caution: This may lead to data corruption**; database text.setButtonText("Use").onClick(() => { this.closeSetting(); eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI); - }) - }) + }); + }); - new Setting(paneEl) - .setName("Minimal setup") - .addButton((text) => { - text.setButtonText("Start").onClick(async () => { - await this.enableMinimalSetup(); - }) - }) + new Setting(paneEl).setName("Minimal setup").addButton((text) => { + text.setButtonText("Start").onClick(async () => { + await this.enableMinimalSetup(); + }); + }); new Setting(paneEl) .setName("Enable LiveSync on this device as the setup was completed manually") .addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true))) @@ -810,47 +944,54 @@ Store only the settings. **Caution: This may lead to data corruption**; database this.editingSettings.isConfigured = true; await this.saveAllDirtySettings(); this.plugin.$$askReload(); - }) - }) + }); + }); }); - void addPanel(paneEl, "To setup the other devices", undefined, visibleOnly(() => this.isConfiguredAs("isConfigured", true))).then(paneEl => { - new Setting(paneEl) - .setName("Copy current settings as a new setup URI") - .addButton((text) => { - text.setButtonText("Copy").onClick(() => { - // await this.plugin.addOnSetup.command_copySetupURI(); - eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); - }) - }) + void addPanel( + paneEl, + "To setup the other devices", + undefined, + visibleOnly(() => this.isConfiguredAs("isConfigured", true)) + ).then((paneEl) => { + new Setting(paneEl).setName("Copy current settings as a new setup URI").addButton((text) => { + text.setButtonText("Copy").onClick(() => { + // await this.plugin.addOnSetup.command_copySetupURI(); + eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); + }); + }); }); - void addPanel(paneEl, "Reset").then(paneEl => { - + void addPanel(paneEl, "Reset").then((paneEl) => { new Setting(paneEl) .setName("Discard existing settings and databases") .addButton((text) => { - text.setButtonText("Discard").onClick(async () => { - if (await this.plugin.confirm.askYesNoDialog("Do you really want to discard existing settings and databases?", { defaultOption: "No" }) == "yes") { - this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS }; - await this.saveAllDirtySettings(); - this.plugin.settings = { ...DEFAULT_SETTINGS }; - await this.plugin.$$saveSettingData(); - await this.plugin.$$resetLocalDatabase(); - // await this.plugin.initializeDatabase(); - this.plugin.$$askReload(); - } - }).setWarning() - }).addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true))) + text.setButtonText("Discard") + .onClick(async () => { + if ( + (await this.plugin.confirm.askYesNoDialog( + "Do you really want to discard existing settings and databases?", + { defaultOption: "No" } + )) == "yes" + ) { + this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS }; + await this.saveAllDirtySettings(); + this.plugin.settings = { ...DEFAULT_SETTINGS }; + await this.plugin.$$saveSettingData(); + await this.plugin.$$resetLocalDatabase(); + // await this.plugin.initializeDatabase(); + this.plugin.$$askReload(); + } + }) + .setWarning(); + }) + .addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true))); // } }); void addPanel(paneEl, "Enable extra and advanced features").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("useAdvancedMode") + new Setting(paneEl).autoWireToggle("useAdvancedMode"); - new Setting(paneEl) - .autoWireToggle("usePowerUserMode") - new Setting(paneEl) - .autoWireToggle("useEdgeCaseMode") + new Setting(paneEl).autoWireToggle("usePowerUserMode"); + new Setting(paneEl).autoWireToggle("useEdgeCaseMode"); this.addOnSaved("useAdvancedMode", () => this.display()); this.addOnSaved("usePowerUserMode", () => this.display()); @@ -861,9 +1002,16 @@ Store only the settings. **Caution: This may lead to data corruption**; database const repo = "vrtmrz/obsidian-livesync"; const topPath = "/docs/troubleshooting.md"; const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`; - this.createEl(paneEl, "div", "", el => el.innerHTML = `Open in browser`); + this.createEl( + paneEl, + "div", + "", + (el) => + (el.innerHTML = `Open in browser`) + ); const troubleShootEl = this.createEl(paneEl, "div", { - text: "", cls: "sls-troubleshoot-preview" + text: "", + cls: "sls-troubleshoot-preview", }); const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => { troubleShootEl.style.minHeight = troubleShootEl.clientHeight + "px"; @@ -881,15 +1029,26 @@ Store only the settings. **Caution: This may lead to data corruption**; database } catch (ex: any) { remoteTroubleShootMDSrc = "An error occurred!!\n" + ex.toString(); } - const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(/\((.*?(.png)|(.jpg))\)/g, `(${rawRepoURI}${basePath}/$1)`) + const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace( + /\((.*?(.png)|(.jpg))\)/g, + `(${rawRepoURI}${basePath}/$1)` + ); // Render markdown - await MarkdownRenderer.render(this.plugin.app, ` [Tips and Troubleshooting](${topPath}) [PageTop](${filename})\n\n${remoteTroubleShootMD}`, troubleShootEl, `${rawRepoURI}`, this.plugin); + await MarkdownRenderer.render( + this.plugin.app, + ` [Tips and Troubleshooting](${topPath}) [PageTop](${filename})\n\n${remoteTroubleShootMD}`, + troubleShootEl, + `${rawRepoURI}`, + this.plugin + ); // Menu - troubleShootEl.querySelector(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({ - position: "sticky", - top: "-1em", - backgroundColor: "var(--modal-background)" - }); + troubleShootEl + .querySelector(".sls-troubleshoot-anchor") + ?.parentElement?.setCssStyles({ + position: "sticky", + top: "-1em", + backgroundColor: "var(--modal-background)", + }); // Trap internal links. troubleShootEl.querySelectorAll("a.internal-link").forEach((anchorEl) => { anchorEl.addEventListener("click", (evt) => { @@ -898,12 +1057,19 @@ Store only the settings. **Caution: This may lead to data corruption**; database if (!uri) return; if (uri.startsWith("#")) { evt.preventDefault(); - const elements = Array.from(troubleShootEl.querySelectorAll("[data-heading]")) - const p = elements.find(e => e.getAttr("data-heading")?.toLowerCase().split(" ").join("-") == uri.substring(1).toLowerCase()); + const elements = Array.from( + troubleShootEl.querySelectorAll("[data-heading]") + ); + const p = elements.find( + (e) => + e.getAttr("data-heading")?.toLowerCase().split(" ").join("-") == + uri.substring(1).toLowerCase() + ); if (p) { p.setCssStyles({ scrollMargin: "3em" }); p.scrollIntoView({ - behavior: "instant", block: "start" + behavior: "instant", + block: "start", }); } } else { @@ -911,28 +1077,32 @@ Store only the settings. **Caution: This may lead to data corruption**; database await loadMarkdownPage(uri, basePath); troubleShootEl.setCssStyles({ scrollMargin: "1em" }); troubleShootEl.scrollIntoView({ - behavior: "instant", block: "start" + behavior: "instant", + block: "start", }); } }); - }) - }) + }); + }); troubleShootEl.style.minHeight = ""; - } + }; void loadMarkdownPage(topPath); }); }); void addPane(containerEl, "General Settings", "⚙️", 20, false).then((paneEl) => { void addPanel(paneEl, "Appearance").then((paneEl) => { const languages = Object.fromEntries([ - ["", "Default"], ...SUPPORTED_I18N_LANGS.map(e => [ - e, $t(`lang-${e}`)])]) as Record; + ["", "Default"], + ...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]), + ]) as Record; new Setting(paneEl).autoWireDropDown("displayLanguage", { - options: languages - }) + options: languages, + }); this.addOnSaved("displayLanguage", () => this.display()); new Setting(paneEl).autoWireToggle("showStatusOnEditor"); - new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", { onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)) }); + new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", { + onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)), + }); new Setting(paneEl).autoWireToggle("showStatusOnStatusbar"); }); void addPanel(paneEl, "Logging").then((paneEl) => { @@ -940,19 +1110,19 @@ Store only the settings. **Caution: This may lead to data corruption**; database new Setting(paneEl).autoWireToggle("lessInformationInLog"); - new Setting(paneEl) - .autoWireToggle("showVerboseLog", { onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)) }); + new Setting(paneEl).autoWireToggle("showVerboseLog", { + onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)), + }); }); - new Setting(paneEl) - .setClass("wizardOnly") - .addButton((button) => button + new Setting(paneEl).setClass("wizardOnly").addButton((button) => + button .setButtonText("Next") .setCta() .onClick(() => { this.changeDisplay("0"); }) - ); - }) + ); + }); let checkResultDiv: HTMLDivElement; const checkConfig = async (checkResultDiv: HTMLDivElement | undefined) => { Logger(`Checking database configuration`, LOG_LEVEL_INFO); @@ -970,12 +1140,16 @@ Store only the settings. **Caution: This may lead to data corruption**; database checkResultDiv?.appendChild(tmpDiv); }; try { - if (isCloudantURI(this.editingSettings.couchDB_URI)) { Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE); return; } - const r = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, window.origin); + const r = await requestToCouchDB( + this.editingSettings.couchDB_URI, + this.editingSettings.couchDB_USER, + this.editingSettings.couchDB_PASSWORD, + window.origin + ); const responseConfig = r.json; const addConfigFixButton = (title: string, key: string, value: string) => { @@ -986,8 +1160,15 @@ Store only the settings. **Caution: This may lead to data corruption**; database const x = checkResultDiv.appendChild(tmpDiv); x.querySelector("button")?.addEventListener("click", () => { fireAndForget(async () => { - Logger(`CouchDB Configuration: ${title} -> Set ${key} to ${value}`) - const res = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, undefined, key, value); + Logger(`CouchDB Configuration: ${title} -> Set ${key} to ${value}`); + const res = await requestToCouchDB( + this.editingSettings.couchDB_URI, + this.editingSettings.couchDB_USER, + this.editingSettings.couchDB_PASSWORD, + undefined, + key, + value + ); if (res.status == 200) { Logger(`CouchDB Configuration: ${title} successfully updated`, LOG_LEVEL_NOTICE); checkResultDiv.removeChild(x); @@ -1000,7 +1181,10 @@ Store only the settings. **Caution: This may lead to data corruption**; database }); }; addResult("---Notice---", ["ob-btn-config-head"]); - addResult("If the server configuration is not persistent (e.g., running on docker), the values set from here will also be volatile. Once you are able to connect, please reflect the settings in the server's local.ini.", ["ob-btn-config-info"]); + addResult( + "If the server configuration is not persistent (e.g., running on docker), the values set from here will also be volatile. Once you are able to connect, please reflect the settings in the server's local.ini.", + ["ob-btn-config-info"] + ); addResult("--Config check--", ["ob-btn-config-head"]); @@ -1022,7 +1206,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database if (responseConfig?.chttpd_auth?.require_valid_user != "true") { isSuccessful = false; addResult("❗ chttpd_auth.require_valid_user is wrong."); - addConfigFixButton("Set chttpd_auth.require_valid_user = true", "chttpd_auth/require_valid_user", "true"); + addConfigFixButton( + "Set chttpd_auth.require_valid_user = true", + "chttpd_auth/require_valid_user", + "true" + ); } else { addResult("✔ chttpd_auth.require_valid_user is ok."); } @@ -1048,7 +1236,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) { isSuccessful = false; addResult("❗ chttpd.max_http_request_size is low)"); - addConfigFixButton("Set chttpd.max_http_request_size", "chttpd/max_http_request_size", "4294967296"); + addConfigFixButton( + "Set chttpd.max_http_request_size", + "chttpd/max_http_request_size", + "4294967296" + ); } else { addResult("✔ chttpd.max_http_request_size is ok."); } @@ -1070,28 +1262,40 @@ Store only the settings. **Caution: This may lead to data corruption**; database addResult("✔ cors.credentials is ok."); } const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(","); - if (responseConfig?.cors?.origins == "*" || (ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && ConfiguredOrigins.indexOf("http://localhost") !== -1)) { + if ( + responseConfig?.cors?.origins == "*" || + (ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 && + ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && + ConfiguredOrigins.indexOf("http://localhost") !== -1) + ) { addResult("✔ cors.origins is ok."); } else { addResult("❗ cors.origins is wrong"); - addConfigFixButton("Set cors.origins", "cors/origins", "app://obsidian.md,capacitor://localhost,http://localhost"); + addConfigFixButton( + "Set cors.origins", + "cors/origins", + "app://obsidian.md,capacitor://localhost,http://localhost" + ); isSuccessful = false; } addResult("--Connection check--", ["ob-btn-config-head"]); addResult(`Current origin:${window.location.origin}`); // Request header check - const origins = [ - "app://obsidian.md", - "capacitor://localhost", - "http://localhost"]; + const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"]; for (const org of origins) { - const rr = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, org); - const responseHeaders = Object.fromEntries(Object.entries(rr.headers) - .map((e) => { + const rr = await requestToCouchDB( + this.editingSettings.couchDB_URI, + this.editingSettings.couchDB_USER, + this.editingSettings.couchDB_PASSWORD, + org + ); + const responseHeaders = Object.fromEntries( + Object.entries(rr.headers).map((e) => { e[0] = `${e[0]}`.toLowerCase(); return e; - })); + }) + ); addResult(`Origin check:${org}`); if (responseHeaders["access-control-allow-credentials"] != "true") { addResult("❗ CORS is not allowing credentials"); @@ -1100,13 +1304,18 @@ Store only the settings. **Caution: This may lead to data corruption**; database addResult("✔ CORS credentials OK"); } if (responseHeaders["access-control-allow-origin"] != org) { - addResult(`⚠ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}`); + addResult( + `⚠ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}` + ); } else { addResult("✔ CORS origin OK"); } } addResult("--Done--", ["ob-btn-config-head"]); - addResult("If you have some trouble with Connection-check even though all Config-check has been passed, please check your reverse proxy's configuration.", ["ob-btn-config-info"]); + addResult( + "If you have some trouble with Connection-check even though all Config-check has been passed, please check your reverse proxy's configuration.", + ["ob-btn-config-info"] + ); Logger(`Checking configuration done`, LOG_LEVEL_INFO); } catch (ex: any) { if (ex?.status == 401) { @@ -1126,18 +1335,18 @@ Store only the settings. **Caution: This may lead to data corruption**; database void addPane(containerEl, "Remote Configuration", "🛰️", 0, false).then((paneEl) => { void addPanel(paneEl, "Remote Server").then((paneEl) => { // const containerRemoteDatabaseEl = containerEl.createDiv(); - new Setting(paneEl) - .autoWireDropDown("remoteType", { - holdValue: true, options: { - [REMOTE_COUCHDB]: "CouchDB", [REMOTE_MINIO]: "Minio,S3,R2", - }, onUpdate: enableOnlySyncDisabled - }) - - - void addPanel(paneEl, "Minio,S3,R2", undefined, onlyOnMinIO).then(paneEl => { + new Setting(paneEl).autoWireDropDown("remoteType", { + holdValue: true, + options: { + [REMOTE_COUCHDB]: "CouchDB", + [REMOTE_MINIO]: "Minio,S3,R2", + }, + onUpdate: enableOnlySyncDisabled, + }); + void addPanel(paneEl, "Minio,S3,R2", undefined, onlyOnMinIO).then((paneEl) => { const syncWarnMinio = this.createEl(paneEl, "div", { - text: "" + text: "", }); const ObjectStorageMessage = `Kindly notice: this is a pretty experimental feature, hence we have some limitations. - Append only architecture. It will not shrink used storage if we do not perform a rebuild. @@ -1148,14 +1357,21 @@ Store only the settings. **Caution: This may lead to data corruption**; database However, your report is needed to stabilise this. I appreciate you for your great dedication. `; - void MarkdownRenderer.render(this.plugin.app, ObjectStorageMessage, syncWarnMinio, "/", this.plugin); + void MarkdownRenderer.render( + this.plugin.app, + ObjectStorageMessage, + syncWarnMinio, + "/", + this.plugin + ); syncWarnMinio.addClass("op-warn-info"); - new Setting(paneEl).autoWireText("endpoint", { holdValue: true }) + new Setting(paneEl).autoWireText("endpoint", { holdValue: true }); new Setting(paneEl).autoWireText("accessKey", { holdValue: true }); new Setting(paneEl).autoWireText("secretKey", { - holdValue: true, isPassword: true + holdValue: true, + isPassword: true, }); new Setting(paneEl).autoWireText("region", { holdValue: true }); @@ -1163,14 +1379,14 @@ However, your report is needed to stabilise this. I appreciate you for your grea new Setting(paneEl).autoWireText("bucket", { holdValue: true }); new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true }); - new Setting(paneEl) - .setName("Test Connection") - .addButton((button) => button + new Setting(paneEl).setName("Test Connection").addButton((button) => + button .setButtonText("Test") .setDisabled(false) .onClick(async () => { await this.testConnection(this.editingSettings); - })); + }) + ); new Setting(paneEl) .setName("Apply Settings") .setClass("wizardHidden") @@ -1181,63 +1397,88 @@ However, your report is needed to stabilise this. I appreciate you for your grea "accessKey", "secretKey", "bucket", - "useCustomRequestHandler"]) - .addOnUpdate(onlyOnMinIO) - + "useCustomRequestHandler", + ]) + .addOnUpdate(onlyOnMinIO); }); - - void addPanel(paneEl, "CouchDB", undefined, onlyOnCouchDB).then(paneEl => { + void addPanel(paneEl, "CouchDB", undefined, onlyOnCouchDB).then((paneEl) => { if (this.plugin.$$isMobile()) { - this.createEl(paneEl, "div", { - text: `Configured as using non-HTTPS. We cannot connect to the remote. Please set up the credentials and use HTTPS for the remote URI.`, - }, undefined, visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://"))) - .addClass("op-warn"); + this.createEl( + paneEl, + "div", + { + text: `Configured as using non-HTTPS. We cannot connect to the remote. Please set up the credentials and use HTTPS for the remote URI.`, + }, + undefined, + visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) + ).addClass("op-warn"); } else { - this.createEl(paneEl, "div", { - text: `Configured as using non-HTTPS. We might fail on mobile devices.` - }, undefined, visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://"))) - .addClass("op-warn-info"); + this.createEl( + paneEl, + "div", + { + text: `Configured as using non-HTTPS. We might fail on mobile devices.`, + }, + undefined, + visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) + ).addClass("op-warn-info"); } - this.createEl(paneEl, "div", { text: `These settings are kept locked while any synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.` }, undefined, visibleOnly(() => isAnySyncEnabled())).addClass("sls-setting-hidden"); + this.createEl( + paneEl, + "div", + { + text: `These settings are kept locked while any synchronization options are enabled. Disable these options in the "Sync Settings" tab to unlock.`, + }, + undefined, + visibleOnly(() => isAnySyncEnabled()) + ).addClass("sls-setting-hidden"); new Setting(paneEl).autoWireText("couchDB_URI", { - holdValue: true, onUpdate: enableOnlySyncDisabled + holdValue: true, + onUpdate: enableOnlySyncDisabled, }); new Setting(paneEl).autoWireText("couchDB_USER", { - holdValue: true, onUpdate: enableOnlySyncDisabled + holdValue: true, + onUpdate: enableOnlySyncDisabled, }); new Setting(paneEl).autoWireText("couchDB_PASSWORD", { holdValue: true, isPassword: true, - onUpdate: enableOnlySyncDisabled + onUpdate: enableOnlySyncDisabled, }); new Setting(paneEl).autoWireText("couchDB_DBNAME", { - holdValue: true, onUpdate: enableOnlySyncDisabled + holdValue: true, + onUpdate: enableOnlySyncDisabled, }); - new Setting(paneEl) .setName("Test Database Connection") .setClass("wizardHidden") - .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.") - .addButton((button) => button - .setButtonText("Test") - .setDisabled(false) - .onClick(async () => { - await this.testConnection(); - })); + .setDesc( + "Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created." + ) + .addButton((button) => + button + .setButtonText("Test") + .setDisabled(false) + .onClick(async () => { + await this.testConnection(); + }) + ); new Setting(paneEl) .setName("Check and fix database configuration") .setDesc("Check the database configuration, and fix if there are any problems.") - .addButton((button) => button - .setButtonText("Check") - .setDisabled(false) - .onClick(async () => { - await checkConfig(checkResultDiv); - })); + .addButton((button) => + button + .setButtonText("Check") + .setDisabled(false) + .onClick(async () => { + await checkConfig(checkResultDiv); + }) + ); checkResultDiv = this.createEl(paneEl, "div", { text: "", }); @@ -1250,77 +1491,95 @@ However, your report is needed to stabilise this. I appreciate you for your grea "couchDB_URI", "couchDB_USER", "couchDB_PASSWORD", - "couchDB_DBNAME"]) - .addOnUpdate(onlyOnCouchDB) + "couchDB_DBNAME", + ]) + .addOnUpdate(onlyOnCouchDB); }); }); void addPanel(paneEl, "Notification").then((paneEl) => { - paneEl.addClass("wizardHidden") + paneEl.addClass("wizardHidden"); new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden"); }); void addPanel(paneEl, "Confidentiality").then((paneEl) => { + new Setting(paneEl).autoWireToggle("encrypt", { holdValue: true }); - new Setting(paneEl) - .autoWireToggle("encrypt", { holdValue: true }) + const isEncryptEnabled = visibleOnly(() => this.isConfiguredAs("encrypt", true)); - const isEncryptEnabled = visibleOnly(() => this.isConfiguredAs("encrypt", true)) + new Setting(paneEl).autoWireText("passphrase", { + holdValue: true, + isPassword: true, + onUpdate: isEncryptEnabled, + }); - new Setting(paneEl) - .autoWireText("passphrase", { - holdValue: true, isPassword: true, onUpdate: isEncryptEnabled - }) - - new Setting(paneEl) - .autoWireToggle("usePathObfuscation", { - holdValue: true, onUpdate: isEncryptEnabled - }) + new Setting(paneEl).autoWireToggle("usePathObfuscation", { + holdValue: true, + onUpdate: isEncryptEnabled, + }); new Setting(paneEl) .autoWireToggle("useDynamicIterationCount", { - holdValue: true, onUpdate: isEncryptEnabled - }).setClass("wizardHidden"); - + holdValue: true, + onUpdate: isEncryptEnabled, + }) + .setClass("wizardHidden"); }); void addPanel(paneEl, "Fetch settings").then((paneEl) => { new Setting(paneEl) .setName("Fetch tweaks from the remote") .setDesc("Fetch other necessary settings from already configured remote.") - .addButton((button) => button - .setButtonText("Fetch") - .setDisabled(false) - .onClick(async () => { - const trialSetting = { ...this.initialSettings, ...this.editingSettings, }; - const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); - if (newTweaks.result !== false) { - this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; - this.requestUpdate(); - } - })); + .addButton((button) => + button + .setButtonText("Fetch") + .setDisabled(false) + .onClick(async () => { + const trialSetting = { ...this.initialSettings, ...this.editingSettings }; + const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); + if (newTweaks.result !== false) { + this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; + this.requestUpdate(); + } + }) + ); }); - new Setting(paneEl) - .setClass("wizardOnly") - .addButton((button) => button + new Setting(paneEl).setClass("wizardOnly").addButton((button) => + button .setButtonText("Next") .setCta() .setDisabled(false) .onClick(async () => { - if (!await checkConfig(checkResultDiv)) { - if (await this.plugin.confirm.askYesNoDialog("The configuration check has failed. Do you want to continue anyway?", { defaultOption: "No", title: "Remote Configuration Check Failed" }) == "no") { + if (!(await checkConfig(checkResultDiv))) { + if ( + (await this.plugin.confirm.askYesNoDialog( + "The configuration check has failed. Do you want to continue anyway?", + { defaultOption: "No", title: "Remote Configuration Check Failed" } + )) == "no" + ) { return; } } - const isEncryptionFullyEnabled = !this.editingSettings.encrypt || !this.editingSettings.usePathObfuscation; + const isEncryptionFullyEnabled = + !this.editingSettings.encrypt || !this.editingSettings.usePathObfuscation; if (isEncryptionFullyEnabled) { - if (await this.plugin.confirm.askYesNoDialog("Enabling End-to-End Encryption and Path Obfuscation is strongly recommended. Do you surely want to continue without encryption?", { defaultOption: "No", title: "Encryption is not enabled" }) == "no") { + if ( + (await this.plugin.confirm.askYesNoDialog( + "Enabling End-to-End Encryption and Path Obfuscation is strongly recommended. Do you surely want to continue without encryption?", + { defaultOption: "No", title: "Encryption is not enabled" } + )) == "no" + ) { return; } } if (!this.editingSettings.encrypt) { this.editingSettings.passphrase = ""; } - if (!await isPassphraseValid()) { - if (await this.plugin.confirm.askYesNoDialog("End-to-End encryption seems to have trouble. Do you surely want to continue with the current settings?", { defaultOption: "No", title: "Encryption has some trouble" }) == "no") { + if (!(await isPassphraseValid())) { + if ( + (await this.plugin.confirm.askYesNoDialog( + "End-to-End encryption seems to have trouble. Do you surely want to continue with the current settings?", + { defaultOption: "No", title: "Encryption has some trouble" } + )) == "no" + ) { return; } } @@ -1331,8 +1590,13 @@ However, your report is needed to stabilise this. I appreciate you for your grea } else { this.editingSettings = { ...this.editingSettings, ...PREFERRED_SETTING_SELF_HOSTED }; } - if (await this.plugin.confirm.askYesNoDialog("Do you want to fetch the tweaks from the remote?", { defaultOption: "Yes", title: "Fetch tweaks" }) == "yes") { - const trialSetting = { ...this.initialSettings, ...this.editingSettings, }; + if ( + (await this.plugin.confirm.askYesNoDialog( + "Do you want to fetch the tweaks from the remote?", + { defaultOption: "Yes", title: "Fetch tweaks" } + )) == "yes" + ) { + const trialSetting = { ...this.initialSettings, ...this.editingSettings }; const newTweaks = await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); if (newTweaks.result !== false) { this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; @@ -1341,57 +1605,67 @@ However, your report is needed to stabilise this. I appreciate you for your grea // Messages should be already shown. } } - changeDisplay("30") - })); + changeDisplay("30"); + }) + ); }); void addPane(containerEl, "Sync Settings", "🔄", 30, false).then((paneEl) => { if (this.editingSettings.versionUpFlash != "") { - const c = this.createEl(paneEl, "div", { - text: this.editingSettings.versionUpFlash, - cls: "op-warn sls-setting-hidden" - }, el => { - this.createEl(el, "button", { text: "I got it and updated." }, (e) => { - e.addClass("mod-cta"); - e.addEventListener("click", () => { - fireAndForget(async () => { - this.editingSettings.versionUpFlash = ""; - await this.saveAllDirtySettings(); - c.remove(); + const c = this.createEl( + paneEl, + "div", + { + text: this.editingSettings.versionUpFlash, + cls: "op-warn sls-setting-hidden", + }, + (el) => { + this.createEl(el, "button", { text: "I got it and updated." }, (e) => { + e.addClass("mod-cta"); + e.addEventListener("click", () => { + fireAndForget(async () => { + this.editingSettings.versionUpFlash = ""; + await this.saveAllDirtySettings(); + c.remove(); + }); }); }); - }) - }, visibleOnly(() => !this.isConfiguredAs("versionUpFlash", ""))); + }, + visibleOnly(() => !this.isConfiguredAs("versionUpFlash", "")) + ); } this.createEl(paneEl, "div", { text: `Please select and apply any preset item to complete the wizard.`, - cls: "wizardOnly" + cls: "wizardOnly", }).addClasses(["op-warn-info"]); - void addPanel(paneEl, "Synchronization Preset").then((paneEl) => { - - const options: Record = this.editingSettings.remoteType == REMOTE_COUCHDB ? { - NONE: "", - LIVESYNC: "LiveSync", - PERIODIC: "Periodic w/ batch", - DISABLE: "Disable all automatic" - } : { - NONE: "", - PERIODIC: "Periodic w/ batch", - DISABLE: "Disable all automatic" - }; + const options: Record = + this.editingSettings.remoteType == REMOTE_COUCHDB + ? { + NONE: "", + LIVESYNC: "LiveSync", + PERIODIC: "Periodic w/ batch", + DISABLE: "Disable all automatic", + } + : { + NONE: "", + PERIODIC: "Periodic w/ batch", + DISABLE: "Disable all automatic", + }; new Setting(paneEl) .autoWireDropDown("preset", { - options: options, holdValue: true, - }).addButton(button => { + options: options, + holdValue: true, + }) + .addButton((button) => { button.setButtonText("Apply"); button.onClick(async () => { // await this.saveSettings(["preset"]); await this.saveAllDirtySettings(); - }) - }) + }); + }); this.addOnSaved("preset", async (currentPreset) => { if (currentPreset == "") { @@ -1409,7 +1683,8 @@ However, your report is needed to stabilise this. I appreciate you for your grea syncAfterMerge: false, } as Partial; const presetLiveSync = { - ...presetAllDisabled, liveSync: true + ...presetAllDisabled, + liveSync: true, } as Partial; const presetPeriodic = { ...presetAllDisabled, @@ -1424,19 +1699,25 @@ However, your report is needed to stabilise this. I appreciate you for your grea if (currentPreset == "LIVESYNC") { this.editingSettings = { - ...this.editingSettings, ...presetLiveSync - } + ...this.editingSettings, + ...presetLiveSync, + }; Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL_NOTICE); } else if (currentPreset == "PERIODIC") { this.editingSettings = { - ...this.editingSettings, ...presetPeriodic - } - Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL_NOTICE); + ...this.editingSettings, + ...presetPeriodic, + }; + Logger( + "Synchronization setting configured as Periodic sync with batch database update.", + LOG_LEVEL_NOTICE + ); } else { Logger("All synchronizations disabled.", LOG_LEVEL_NOTICE); this.editingSettings = { - ...this.editingSettings, ...presetAllDisabled - } + ...this.editingSettings, + ...presetAllDisabled, + }; } if (this.inWizard) { @@ -1448,10 +1729,12 @@ However, your report is needed to stabilise this. I appreciate you for your grea await this.plugin.$$realizeSettingSyncMode(); await rebuildDB("localOnly"); // this.resetEditingSettings(); - if (await this.plugin.confirm.askYesNoDialog( - "All done!, do you want to generate a setup URI to set up other devices?", - { defaultOption: "Yes", title: "Congratulations!" } - ) == "yes") { + if ( + (await this.plugin.confirm.askYesNoDialog( + "All done!, do you want to generate a setup URI to set up other devices?", + { defaultOption: "Yes", title: "Congratulations!" } + )) == "yes" + ) { eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); } } else { @@ -1467,8 +1750,7 @@ However, your report is needed to stabilise this. I appreciate you for your grea await this.saveAllDirtySettings(); await this.plugin.$$realizeSettingSyncMode(); } - }) - + }); }); void addPanel(paneEl, "Synchronization Methods").then((paneEl) => { paneEl.addClass("wizardHidden"); @@ -1477,19 +1759,21 @@ However, your report is needed to stabilise this. I appreciate you for your grea const onlyOnNonLiveSync = visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")); const onlyOnPeriodic = visibleOnly(() => this.isConfiguredAs("syncMode", "PERIODIC")); - const optionsSyncMode = this.editingSettings.remoteType == REMOTE_COUCHDB ? { - "ONEVENTS": "On events", - PERIODIC: "Periodic and On events", - "LIVESYNC": "LiveSync" - } : { "ONEVENTS": "On events", PERIODIC: "Periodic and On events" } - + const optionsSyncMode = + this.editingSettings.remoteType == REMOTE_COUCHDB + ? { + ONEVENTS: "On events", + PERIODIC: "Periodic and On events", + LIVESYNC: "LiveSync", + } + : { ONEVENTS: "On events", PERIODIC: "Periodic and On events" }; new Setting(paneEl) .autoWireDropDown("syncMode", { //@ts-ignore - options: optionsSyncMode + options: optionsSyncMode, }) - .setClass("wizardHidden") + .setClass("wizardHidden"); this.addOnSaved("syncMode", async (value) => { this.editingSettings.liveSync = false; this.editingSettings.periodicReplication = false; @@ -1501,77 +1785,59 @@ However, your report is needed to stabilise this. I appreciate you for your grea await this.saveSettings(["liveSync", "periodicReplication"]); await this.plugin.$$realizeSettingSyncMode(); - }) - + }); new Setting(paneEl) .autoWireNumeric("periodicReplicationInterval", { - clampMax: 5000, onUpdate: onlyOnPeriodic - }).setClass("wizardHidden") - + clampMax: 5000, + onUpdate: onlyOnPeriodic, + }) + .setClass("wizardHidden"); new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync }) + .autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync }); new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync }) + .autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync }); new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }) + .autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }); new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync }) + .autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync }); new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }) - + .autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }); }); void addPanel(paneEl, "Update thinning").then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("batchSave") - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("batchSaveMinimumDelay", { - acceptZero: true, - onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)) - }) - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("batchSaveMaximumDelay", { - acceptZero: true, - onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)) - }) + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave"); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", { + acceptZero: true, + onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), + }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMaximumDelay", { + acceptZero: true, + onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), + }); }); void addPanel(paneEl, "Deletion Propagation", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("trashInsteadDelete") + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("trashInsteadDelete"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("doNotDeleteFolder") + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); }); void addPanel(paneEl, "Conflict resolution", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { - paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("resolveConflictsByNewerFile") + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("checkConflictOnlyOnOpen") + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("checkConflictOnlyOnOpen"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("showMergeDialogOnlyOnActive") + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive"); }); void addPanel(paneEl, "Sync settings via markdown", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { @@ -1579,82 +1845,74 @@ However, your report is needed to stabilise this. I appreciate you for your grea new Setting(paneEl) .autoWireText("settingSyncFile", { holdValue: true }) - .addApplyButton(["settingSyncFile"]) + .addApplyButton(["settingSyncFile"]); - new Setting(paneEl) - .autoWireToggle("writeCredentialsForSettingSync"); + new Setting(paneEl).autoWireToggle("writeCredentialsForSettingSync"); - new Setting(paneEl) - .autoWireToggle("notifyAllSettingSyncFile") + new Setting(paneEl).autoWireToggle("notifyAllSettingSyncFile"); }); void addPanel(paneEl, "Hidden files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { paneEl.addClass("wizardHidden"); - const LABEL_ENABLED = "🔁 : Enabled"; - const LABEL_DISABLED = "⏹️ : Disabled" + const LABEL_DISABLED = "⏹️ : Disabled"; const hiddenFileSyncSetting = new Setting(paneEl) - .setName("Hidden file synchronization").setClass("wizardHidden") - const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl + .setName("Hidden file synchronization") + .setClass("wizardHidden"); + const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl; const hiddenFileSyncSettingDiv = hiddenFileSyncSettingEl.createDiv(""); - hiddenFileSyncSettingDiv.innerText = this.editingSettings.syncInternalFiles ? LABEL_ENABLED : LABEL_DISABLED; + hiddenFileSyncSettingDiv.innerText = this.editingSettings.syncInternalFiles + ? LABEL_ENABLED + : LABEL_DISABLED; if (this.editingSettings.syncInternalFiles) { new Setting(paneEl) .setName("Disable Hidden files sync") .setClass("wizardHidden") .addButton((button) => { - button.setButtonText("Disable") - .onClick(async () => { - this.editingSettings.syncInternalFiles = false; - await this.saveAllDirtySettings(); - this.display(); - }) - }) + button.setButtonText("Disable").onClick(async () => { + this.editingSettings.syncInternalFiles = false; + await this.saveAllDirtySettings(); + this.display(); + }); + }); } else { - new Setting(paneEl) .setName("Enable Hidden files sync") .setClass("wizardHidden") .addButton((button) => { - button.setButtonText("Merge") - .onClick(async () => { - this.closeSetting() - // this.resetEditingSettings(); - await this.plugin.$anyConfigureOptionalSyncFeature("MERGE"); - }) + button.setButtonText("Merge").onClick(async () => { + this.closeSetting(); + // this.resetEditingSettings(); + await this.plugin.$anyConfigureOptionalSyncFeature("MERGE"); + }); }) .addButton((button) => { - button.setButtonText("Fetch") - .onClick(async () => { - this.closeSetting() - // this.resetEditingSettings(); - await this.plugin.$anyConfigureOptionalSyncFeature("FETCH"); - }) + button.setButtonText("Fetch").onClick(async () => { + this.closeSetting(); + // this.resetEditingSettings(); + await this.plugin.$anyConfigureOptionalSyncFeature("FETCH"); + }); }) .addButton((button) => { - button.setButtonText("Overwrite") - .onClick(async () => { - this.closeSetting() - // this.resetEditingSettings(); - await this.plugin.$anyConfigureOptionalSyncFeature("OVERWRITE"); - }) + button.setButtonText("Overwrite").onClick(async () => { + this.closeSetting(); + // this.resetEditingSettings(); + await this.plugin.$anyConfigureOptionalSyncFeature("OVERWRITE"); + }); }); } - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncInternalFilesBeforeReplication", { onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", false)) }) - - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("syncInternalFilesInterval", { - clampMin: 10, acceptZero: true - }) + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", { + onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", false)), + }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncInternalFilesInterval", { + clampMin: 10, + acceptZero: true, + }); }); - }); void addPane(containerEl, "Selector", "🚦", 33, false, LEVEL_ADVANCED).then((paneEl) => { void addPanel(paneEl, "Normal Files").then((paneEl) => { @@ -1662,93 +1920,110 @@ However, your report is needed to stabilise this. I appreciate you for your grea const syncFilesSetting = new Setting(paneEl) .setName("Synchronising files") - .setDesc("(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.") - .setClass("wizardHidden") + .setDesc( + "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files." + ) + .setClass("wizardHidden"); new MultipleRegExpControl({ - target: syncFilesSetting.controlEl, props: { + target: syncFilesSetting.controlEl, + props: { patterns: this.editingSettings.syncOnlyRegEx.split("|[]|"), originals: [...this.editingSettings.syncOnlyRegEx.split("|[]|")], apply: async (newPatterns: string[]) => { - this.editingSettings.syncOnlyRegEx = newPatterns.map((e: string) => e.trim()).filter(e => e != "").join("|[]|"); + this.editingSettings.syncOnlyRegEx = newPatterns + .map((e: string) => e.trim()) + .filter((e) => e != "") + .join("|[]|"); await this.saveAllDirtySettings(); this.display(); - } - } - }) + }, + }, + }); const nonSyncFilesSetting = new Setting(paneEl) .setName("Non-Synchronising files") - .setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.") + .setDesc( + "(RegExp) If this is set, any changes to local and remote files that match this will be skipped." + ) .setClass("wizardHidden"); new MultipleRegExpControl({ - target: nonSyncFilesSetting.controlEl, props: { + target: nonSyncFilesSetting.controlEl, + props: { patterns: this.editingSettings.syncIgnoreRegEx.split("|[]|"), originals: [...this.editingSettings.syncIgnoreRegEx.split("|[]|")], apply: async (newPatterns: string[]) => { - this.editingSettings.syncIgnoreRegEx = newPatterns.map(e => e.trim()).filter(e => e != "").join("|[]|"); + this.editingSettings.syncIgnoreRegEx = newPatterns + .map((e) => e.trim()) + .filter((e) => e != "") + .join("|[]|"); await this.saveAllDirtySettings(); this.display(); - } - } - }) - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 }) + }, + }, + }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("useIgnoreFiles") - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireTextArea("ignoreFiles", { onUpdate: visibleOnly(() => this.isConfiguredAs("useIgnoreFiles", true)) }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useIgnoreFiles"); + new Setting(paneEl).setClass("wizardHidden").autoWireTextArea("ignoreFiles", { + onUpdate: visibleOnly(() => this.isConfiguredAs("useIgnoreFiles", true)), + }); }); void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { - const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, ^\\.git\\/, \\/obsidian-livesync\\/"; - const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$"; + const defaultSkipPatternXPlat = + defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$"; - const pat = this.editingSettings.syncInternalFilesIgnorePatterns.split(",").map(x => x.trim()).filter(x => x != ""); - const patSetting = new Setting(paneEl) - .setName("Ignore patterns") - .setClass("wizardHidden") - .setDesc(""); + const pat = this.editingSettings.syncInternalFilesIgnorePatterns + .split(",") + .map((x) => x.trim()) + .filter((x) => x != ""); + const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc(""); new MultipleRegExpControl({ - target: patSetting.controlEl, props: { + target: patSetting.controlEl, + props: { patterns: pat, originals: [...pat], apply: async (newPatterns: string[]) => { - this.editingSettings.syncInternalFilesIgnorePatterns = newPatterns.map(e => e.trim()).filter(e => e != "").join(", "); + this.editingSettings.syncInternalFilesIgnorePatterns = newPatterns + .map((e) => e.trim()) + .filter((e) => e != "") + .join(", "); await this.saveAllDirtySettings(); this.display(); - } - } - }) + }, + }, + }); const addDefaultPatterns = async (patterns: string) => { - const oldList = this.editingSettings.syncInternalFilesIgnorePatterns.split(",").map(x => x.trim()).filter(x => x != ""); - const newList = patterns.split(",").map(x => x.trim()).filter(x => x != ""); + const oldList = this.editingSettings.syncInternalFilesIgnorePatterns + .split(",") + .map((x) => x.trim()) + .filter((x) => x != ""); + const newList = patterns + .split(",") + .map((x) => x.trim()) + .filter((x) => x != ""); const allSet = new Set([...oldList, ...newList]); this.editingSettings.syncInternalFilesIgnorePatterns = [...allSet].join(", "); await this.saveAllDirtySettings(); this.display(); - } + }; new Setting(paneEl) .setName("Add default patterns") .setClass("wizardHidden") .addButton((button) => { - button.setButtonText("Default") - .onClick(async () => { - await addDefaultPatterns(defaultSkipPattern); - }) - }).addButton((button) => { - button.setButtonText("Cross-platform") - .onClick(async () => { - await addDefaultPatterns(defaultSkipPatternXPlat); - }) + button.setButtonText("Default").onClick(async () => { + await addDefaultPatterns(defaultSkipPattern); + }); }) + .addButton((button) => { + button.setButtonText("Cross-platform").onClick(async () => { + await addDefaultPatterns(defaultSkipPatternXPlat); + }); + }); }); }); @@ -1756,46 +2031,56 @@ However, your report is needed to stabilise this. I appreciate you for your grea // With great respect, thank you TfTHacker! // Refer: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts void addPanel(paneEl, "Customization Sync").then((paneEl) => { - const enableOnlyOnPluginSyncIsNotEnabled = enableOnly(() => this.isConfiguredAs("usePluginSync", false)); + const enableOnlyOnPluginSyncIsNotEnabled = enableOnly(() => + this.isConfiguredAs("usePluginSync", false) + ); const visibleOnlyOnPluginSyncEnabled = visibleOnly(() => this.isConfiguredAs("usePluginSync", true)); - this.createEl(paneEl, "div", { - text: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.", - cls: "op-warn" - }, c => { - }, visibleOnly(() => this.isConfiguredAs("deviceAndVaultName", ""))); - this.createEl(paneEl, "div", { - text: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.", - cls: "op-warn-info" - }, c => { - }, visibleOnly(() => this.isConfiguredAs("usePluginSync", true))); + this.createEl( + paneEl, + "div", + { + text: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.", + cls: "op-warn", + }, + (c) => {}, + visibleOnly(() => this.isConfiguredAs("deviceAndVaultName", "")) + ); + this.createEl( + paneEl, + "div", + { + text: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.", + cls: "op-warn-info", + }, + (c) => {}, + visibleOnly(() => this.isConfiguredAs("usePluginSync", true)) + ); - new Setting(paneEl) - .autoWireText("deviceAndVaultName", { - placeHolder: "desktop", onUpdate: enableOnlyOnPluginSyncIsNotEnabled - }); + new Setting(paneEl).autoWireText("deviceAndVaultName", { + placeHolder: "desktop", + onUpdate: enableOnlyOnPluginSyncIsNotEnabled, + }); - new Setting(paneEl) - .autoWireToggle("usePluginSyncV2") + new Setting(paneEl).autoWireToggle("usePluginSyncV2"); - new Setting(paneEl) - .autoWireToggle("usePluginSync", { - onUpdate: enableOnly(() => !this.isConfiguredAs("deviceAndVaultName", "")) - }); + new Setting(paneEl).autoWireToggle("usePluginSync", { + onUpdate: enableOnly(() => !this.isConfiguredAs("deviceAndVaultName", "")), + }); - new Setting(paneEl) - .autoWireToggle("autoSweepPlugins", { - onUpdate: visibleOnlyOnPluginSyncEnabled - }) + new Setting(paneEl).autoWireToggle("autoSweepPlugins", { + onUpdate: visibleOnlyOnPluginSyncEnabled, + }); - new Setting(paneEl) - .autoWireToggle("autoSweepPluginsPeriodic", { - onUpdate: visibleOnly(() => this.isConfiguredAs("usePluginSync", true) && this.isConfiguredAs("autoSweepPlugins", true)) - }) - new Setting(paneEl) - .autoWireToggle("notifyPluginOrSettingUpdated", { - onUpdate: visibleOnlyOnPluginSyncEnabled - }) + new Setting(paneEl).autoWireToggle("autoSweepPluginsPeriodic", { + onUpdate: visibleOnly( + () => + this.isConfiguredAs("usePluginSync", true) && this.isConfiguredAs("autoSweepPlugins", true) + ), + }); + new Setting(paneEl).autoWireToggle("notifyPluginOrSettingUpdated", { + onUpdate: visibleOnlyOnPluginSyncEnabled, + }); new Setting(paneEl) .setName("Open") @@ -1814,14 +2099,12 @@ However, your report is needed to stabilise this. I appreciate you for your grea }); }); - void addPane(containerEl, "Hatch", "🧰", 50, true).then((paneEl) => { // const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` }); // hatchWarn.addClass("op-warn-info"); void addPanel(paneEl, "Reporting Issue").then((paneEl) => { - new Setting(paneEl) - .setName("Make report to inform the issue") - .addButton((button) => button + new Setting(paneEl).setName("Make report to inform the issue").addButton((button) => + button .setButtonText("Make report") .setCta() .setDisabled(false) @@ -1830,7 +2113,12 @@ However, your report is needed to stabilise this. I appreciate you for your grea const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷"; if (this.editingSettings.remoteType == REMOTE_COUCHDB) { try { - const r = await requestToCouchDB(this.editingSettings.couchDB_URI, this.editingSettings.couchDB_USER, this.editingSettings.couchDB_PASSWORD, window.origin); + const r = await requestToCouchDB( + this.editingSettings.couchDB_URI, + this.editingSettings.couchDB_USER, + this.editingSettings.couchDB_PASSWORD, + window.origin + ); Logger(JSON.stringify(r.json, null, 2)); @@ -1840,20 +2128,28 @@ However, your report is needed to stabilise this. I appreciate you for your grea responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED; responseConfig["couchdb"].uuid = REDACTED; responseConfig["admins"] = REDACTED; - } catch (ex) { Logger(ex, LOG_LEVEL_VERBOSE); - responseConfig = "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour." + responseConfig = + "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour."; } } else if (this.editingSettings.remoteType == REMOTE_MINIO) { responseConfig = "Object Storage Synchronisation"; // } - const pluginConfig = JSON.parse(JSON.stringify(this.editingSettings)) as ObsidianLiveSyncSettings; + const pluginConfig = JSON.parse( + JSON.stringify(this.editingSettings) + ) as ObsidianLiveSyncSettings; pluginConfig.couchDB_DBNAME = REDACTED; pluginConfig.couchDB_PASSWORD = REDACTED; - const scheme = pluginConfig.couchDB_URI.startsWith("http:") ? "(HTTP)" : (pluginConfig.couchDB_URI.startsWith("https:")) ? "(HTTPS)" : "" - pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : `self-hosted${scheme}`; + const scheme = pluginConfig.couchDB_URI.startsWith("http:") + ? "(HTTP)" + : pluginConfig.couchDB_URI.startsWith("https:") + ? "(HTTPS)" + : ""; + pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) + ? "cloudant" + : `self-hosted${scheme}`; pluginConfig.couchDB_USER = REDACTED; pluginConfig.passphrase = REDACTED; pluginConfig.encryptedPassphrase = REDACTED; @@ -1867,7 +2163,11 @@ However, your report is needed to stabilise this. I appreciate you for your grea if (endpoint == "") { pluginConfig.endpoint = "Not configured or AWS"; } else { - const endpointScheme = pluginConfig.endpoint.startsWith("http:") ? "(HTTP)" : (pluginConfig.endpoint.startsWith("https:")) ? "(HTTPS)" : ""; + const endpointScheme = pluginConfig.endpoint.startsWith("http:") + ? "(HTTP)" + : pluginConfig.endpoint.startsWith("https:") + ? "(HTTPS)" + : ""; pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`; } const obsidianInfo = `Navigator: ${navigator.userAgent} @@ -1882,339 +2182,437 @@ ${stringifyYaml(pluginConfig)}`; console.log(msgConfig); await navigator.clipboard.writeText(msgConfig); Logger(`Information has been copied to clipboard`, LOG_LEVEL_NOTICE); - })); - new Setting(paneEl) - .autoWireToggle("writeLogToTheFile") + }) + ); + new Setting(paneEl).autoWireToggle("writeLogToTheFile"); }); void addPanel(paneEl, "Scram Switches").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("suspendFileWatching") + new Setting(paneEl).autoWireToggle("suspendFileWatching"); this.addOnSaved("suspendFileWatching", () => this.plugin.$$askReload()); - new Setting(paneEl) - .autoWireToggle("suspendParseReplicationResult") + new Setting(paneEl).autoWireToggle("suspendParseReplicationResult"); this.addOnSaved("suspendParseReplicationResult", () => this.plugin.$$askReload()); }); void addPanel(paneEl, "Recovery and Repair").then((paneEl) => { - - const addResult = async (path: string, file: FilePathWithPrefix | false, fileOnDB: LoadedEntry | false) => { + const addResult = async ( + path: string, + file: FilePathWithPrefix | false, + fileOnDB: LoadedEntry | false + ) => { const storageFileStat = file ? await this.plugin.storageAccess.statHidden(file) : null; - resultArea.appendChild(this.createEl(resultArea, "div", {}, el => { - el.appendChild(this.createEl(el, "h6", { text: path })); - el.appendChild(this.createEl(el, "div", {}, infoGroupEl => { - infoGroupEl.appendChild(this.createEl(infoGroupEl, "div", { text: `Storage : Modified: ${!storageFileStat ? `Missing:` : `${new Date(storageFileStat.mtime).toLocaleString()}, Size:${storageFileStat.size}`}` })) - infoGroupEl.appendChild(this.createEl(infoGroupEl, "div", { text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size}`}` })) - })); - if (fileOnDB && file) { - el.appendChild(this.createEl(el, "button", { text: "Show history" }, buttonEl => { - buttonEl.onClickEvent(() => { - eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { - file: file, fileOnDB: fileOnDB - }); + resultArea.appendChild( + this.createEl(resultArea, "div", {}, (el) => { + el.appendChild(this.createEl(el, "h6", { text: path })); + el.appendChild( + this.createEl(el, "div", {}, (infoGroupEl) => { + infoGroupEl.appendChild( + this.createEl(infoGroupEl, "div", { + text: `Storage : Modified: ${!storageFileStat ? `Missing:` : `${new Date(storageFileStat.mtime).toLocaleString()}, Size:${storageFileStat.size}`}`, + }) + ); + infoGroupEl.appendChild( + this.createEl(infoGroupEl, "div", { + text: `Database: Modified: ${!fileOnDB ? `Missing:` : `${new Date(fileOnDB.mtime).toLocaleString()}, Size:${fileOnDB.size}`}`, + }) + ); }) - })) - } - if (file) { - el.appendChild(this.createEl(el, "button", { text: "Storage -> Database" }, buttonEl => { - buttonEl.onClickEvent(async () => { - if (file.startsWith(".")) { - const addOn = this.plugin.getAddOn(HiddenFileSync.name); - if (addOn) { - const file = (await addOn.scanInternalFiles()).find(e => e.path == path); - if (!file) { - Logger(`Failed to find the file in the internal files: ${path}`, LOG_LEVEL_NOTICE); - return; + ); + if (fileOnDB && file) { + el.appendChild( + this.createEl(el, "button", { text: "Show history" }, (buttonEl) => { + buttonEl.onClickEvent(() => { + eventHub.emitEvent(EVENT_REQUEST_SHOW_HISTORY, { + file: file, + fileOnDB: fileOnDB, + }); + }); + }) + ); + } + if (file) { + el.appendChild( + this.createEl(el, "button", { text: "Storage -> Database" }, (buttonEl) => { + buttonEl.onClickEvent(async () => { + if (file.startsWith(".")) { + const addOn = this.plugin.getAddOn(HiddenFileSync.name); + if (addOn) { + const file = (await addOn.scanInternalFiles()).find( + (e) => e.path == path + ); + if (!file) { + Logger( + `Failed to find the file in the internal files: ${path}`, + LOG_LEVEL_NOTICE + ); + return; + } + if (!(await addOn.storeInternalFileToDatabase(file, true))) { + Logger( + `Failed to store the file to the database (Hidden file): ${file}`, + LOG_LEVEL_NOTICE + ); + return; + } + } + } else { + if ( + !(await this.plugin.fileHandler.storeFileToDB( + file as FilePath, + true + )) + ) { + Logger( + `Failed to store the file to the database: ${file}`, + LOG_LEVEL_NOTICE + ); + return; + } } - if (!await addOn.storeInternalFileToDatabase(file, true)) { - Logger(`Failed to store the file to the database (Hidden file): ${file}`, LOG_LEVEL_NOTICE); - return; + el.remove(); + }); + }) + ); + } + if (fileOnDB) { + el.appendChild( + this.createEl(el, "button", { text: "Database -> Storage" }, (buttonEl) => { + buttonEl.onClickEvent(async () => { + if (fileOnDB.path.startsWith(ICHeader)) { + const addOn = this.plugin.getAddOn(HiddenFileSync.name); + if (addOn) { + if ( + !(await addOn.extractInternalFileFromDatabase( + path as FilePath, + true + )) + ) { + Logger( + `Failed to store the file to the database (Hidden file): ${file}`, + LOG_LEVEL_NOTICE + ); + return; + } + } + } else { + if ( + !(await this.plugin.fileHandler.dbToStorage( + fileOnDB as MetaEntry, + null, + true + )) + ) { + Logger( + `Failed to store the file to the storage: ${fileOnDB.path}`, + LOG_LEVEL_NOTICE + ); + return; + } } - } - } else { - if (!await this.plugin.fileHandler.storeFileToDB(file as FilePath, true)) { - Logger(`Failed to store the file to the database: ${file}`, LOG_LEVEL_NOTICE); - return; - } - } - el.remove(); - }) - })) - } - if (fileOnDB) { - el.appendChild(this.createEl(el, "button", { text: "Database -> Storage" }, buttonEl => { - buttonEl.onClickEvent(async () => { - if (fileOnDB.path.startsWith(ICHeader)) { - const addOn = this.plugin.getAddOn(HiddenFileSync.name); - if (addOn) { - if (!await addOn.extractInternalFileFromDatabase(path as FilePath, true)) { - Logger(`Failed to store the file to the database (Hidden file): ${file}`, LOG_LEVEL_NOTICE); - return; - } - } - - } else { - if (!await this.plugin.fileHandler.dbToStorage(fileOnDB as MetaEntry, null, true)) { - Logger(`Failed to store the file to the storage: ${fileOnDB.path}`, LOG_LEVEL_NOTICE); - return; - } - } - el.remove(); - }) - })) - } - return el; - })) - } + el.remove(); + }); + }) + ); + } + return el; + }) + ); + }; const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => { const dataContent = readAsBlob(fileOnDB); - const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file)) + const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file)); if (await isDocContentSame(content, dataContent)) { - Logger(`Compare: SAME: ${file}`) + Logger(`Compare: SAME: ${file}`); } else { Logger(`Compare: CONTENT IS NOT MATCHED! ${file}`, LOG_LEVEL_NOTICE); - void addResult(file, file, fileOnDB) + void addResult(file, file, fileOnDB); } - } + }; new Setting(paneEl) .setName("Recreate missing chunks for all files") - .setDesc("This will recreate chunks for all files. If there were missing chunks, this may fix the errors.") - .addButton((button) => button.setButtonText("Recreate all") - .setCta() - .onClick(async () => { - await this.plugin.fileHandler.createAllChunks(true); - })) + .setDesc( + "This will recreate chunks for all files. If there were missing chunks, this may fix the errors." + ) + .addButton((button) => + button + .setButtonText("Recreate all") + .setCta() + .onClick(async () => { + await this.plugin.fileHandler.createAllChunks(true); + }) + ); new Setting(paneEl) .setName("Resolve All conflicted files by the newer one") - .setDesc("Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.") - .addButton((button) => button.setButtonText("Resolve All") - .setCta() - .onClick(async () => { - await this.plugin.rebuilder.resolveAllConflictedFilesByNewerOnes(); - })) + .setDesc( + "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one." + ) + .addButton((button) => + button + .setButtonText("Resolve All") + .setCta() + .onClick(async () => { + await this.plugin.rebuilder.resolveAllConflictedFilesByNewerOnes(); + }) + ); new Setting(paneEl) .setName("Verify and repair all files") - .setDesc("Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.") - .addButton((button) => button - .setButtonText("Verify all") - .setDisabled(false) - .setCta() - .onClick(async () => { - Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns - .replace(/\n| /g, "") - .split(",").filter(e => e).map(e => new RegExp(e, "i")); - this.plugin.localDatabase.hashCaches.clear(); - Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); - const files = this.plugin.settings.syncInternalFiles ? (await this.plugin.storageAccess.getFilesIncludeHidden("/", undefined, ignorePatterns)) : (await this.plugin.storageAccess.getFileNames()); - const documents = [] as FilePath[]; + .setDesc( + "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep." + ) + .addButton((button) => + button + .setButtonText("Verify all") + .setDisabled(false) + .setCta() + .onClick(async () => { + Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); + const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns + .replace(/\n| /g, "") + .split(",") + .filter((e) => e) + .map((e) => new RegExp(e, "i")); + this.plugin.localDatabase.hashCaches.clear(); + Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify"); + const files = this.plugin.settings.syncInternalFiles + ? await this.plugin.storageAccess.getFilesIncludeHidden( + "/", + undefined, + ignorePatterns + ) + : await this.plugin.storageAccess.getFileNames(); + const documents = [] as FilePath[]; - const adn = this.plugin.localDatabase.findAllDocs() - for await (const i of adn) { - const path = getPath(i); - if (path.startsWith(ICXHeader)) continue; - if (path.startsWith(PSCHeader)) continue; - if (!this.plugin.settings.syncInternalFiles && path.startsWith(ICHeader)) continue; - documents.push(stripAllPrefixes(path)); - } - const allPaths = [ - ...new Set([ - ...documents, - ...files])]; - let i = 0; - const incProc = () => { - i++; - if (i % 25 == 0) Logger(`Checking ${i}/${files.length} files \n`, LOG_LEVEL_NOTICE, "verify-processed"); - } - const semaphore = Semaphore(10); - const processes = allPaths.map(async path => { - try { - if (shouldBeIgnored(path)) { - return incProc(); - } - const stat = await this.plugin.storageAccess.isExistsIncludeHidden(path) ? await this.plugin.storageAccess.statHidden(path) : false; - const fileOnStorage = stat != null ? stat : false; - if (!await this.plugin.$$isTargetFile(path)) return incProc(); - const releaser = await semaphore.acquire(1) - if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.size)) return incProc(); + const adn = this.plugin.localDatabase.findAllDocs(); + for await (const i of adn) { + const path = getPath(i); + if (path.startsWith(ICXHeader)) continue; + if (path.startsWith(PSCHeader)) continue; + if (!this.plugin.settings.syncInternalFiles && path.startsWith(ICHeader)) continue; + documents.push(stripAllPrefixes(path)); + } + const allPaths = [...new Set([...documents, ...files])]; + let i = 0; + const incProc = () => { + i++; + if (i % 25 == 0) + Logger( + `Checking ${i}/${files.length} files \n`, + LOG_LEVEL_NOTICE, + "verify-processed" + ); + }; + const semaphore = Semaphore(10); + const processes = allPaths.map(async (path) => { try { - const isHiddenFile = path.startsWith("."); - const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path; - const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath); - if (fileOnDB && this.plugin.$$isFileSizeExceeded(fileOnDB.size)) return incProc(); + if (shouldBeIgnored(path)) { + return incProc(); + } + const stat = (await this.plugin.storageAccess.isExistsIncludeHidden(path)) + ? await this.plugin.storageAccess.statHidden(path) + : false; + const fileOnStorage = stat != null ? stat : false; + if (!(await this.plugin.$$isTargetFile(path))) return incProc(); + const releaser = await semaphore.acquire(1); + if (fileOnStorage && this.plugin.$$isFileSizeExceeded(fileOnStorage.size)) + return incProc(); + try { + const isHiddenFile = path.startsWith("."); + const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path; + const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath); + if (fileOnDB && this.plugin.$$isFileSizeExceeded(fileOnDB.size)) + return incProc(); - if (!fileOnDB && fileOnStorage) { - Logger(`Compare: Not found on the local database: ${path}`, LOG_LEVEL_NOTICE); - void addResult(path, path, false) - return incProc(); - } - if (fileOnDB && !fileOnStorage) { - Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE); - void addResult(path, false, fileOnDB) - return incProc(); - } - if (fileOnStorage && fileOnDB) { - await checkBetweenStorageAndDatabase(path, fileOnDB) + if (!fileOnDB && fileOnStorage) { + Logger( + `Compare: Not found on the local database: ${path}`, + LOG_LEVEL_NOTICE + ); + void addResult(path, path, false); + return incProc(); + } + if (fileOnDB && !fileOnStorage) { + Logger(`Compare: Not found on the storage: ${path}`, LOG_LEVEL_NOTICE); + void addResult(path, false, fileOnDB); + return incProc(); + } + if (fileOnStorage && fileOnDB) { + await checkBetweenStorageAndDatabase(path, fileOnDB); + } + } catch (ex) { + Logger(`Error while processing ${path}`, LOG_LEVEL_NOTICE); + Logger(ex, LOG_LEVEL_VERBOSE); + } finally { + releaser(); + incProc(); } } catch (ex) { - Logger(`Error while processing ${path}`, LOG_LEVEL_NOTICE); + Logger(`Error while processing without semaphore ${path}`, LOG_LEVEL_NOTICE); Logger(ex, LOG_LEVEL_VERBOSE); - } finally { - releaser(); - incProc(); } - } catch (ex) { - Logger(`Error while processing without semaphore ${path}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); - } - }); - await Promise.all(processes); - Logger("done", LOG_LEVEL_NOTICE, "verify"); - // Logger(`${i}/${files.length}\n`, LOG_LEVEL_NOTICE, "verify-processed"); - })); + }); + await Promise.all(processes); + Logger("done", LOG_LEVEL_NOTICE, "verify"); + // Logger(`${i}/${files.length}\n`, LOG_LEVEL_NOTICE, "verify-processed"); + }) + ); const resultArea = paneEl.createDiv({ text: "" }); new Setting(paneEl) .setName("Check and convert non-path-obfuscated files") .setDesc("") - .addButton((button) => button - .setButtonText("Perform") - .setDisabled(false) - .setWarning() - .onClick(async () => { - for await (const docName of this.plugin.localDatabase.findAllDocNames()) { - if (!docName.startsWith("f:")) { - const idEncoded = await this.plugin.$$path2id(docName as FilePathWithPrefix); - const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID); - if (!doc) continue; - if (doc.type != "newnote" && doc.type != "plain") { - continue; - } - if (doc?.deleted ?? false) continue; - const newDoc = { ...doc }; - //Prepare converted data - newDoc._id = idEncoded; - newDoc.path = docName as FilePathWithPrefix; - // @ts-ignore - delete newDoc._rev; - try { - const obfuscatedDoc = await this.plugin.localDatabase.getRaw(idEncoded, { revs_info: true }); - // Unfortunately we have to delete one of them. - // Just now, save it as a conflicted document. - obfuscatedDoc._revs_info?.shift(); // Drop latest revision. - const previousRev = obfuscatedDoc._revs_info?.shift(); // Use second revision. - if (previousRev) { - newDoc._rev = previousRev.rev; - } else { - //If there are no revisions, set the possibly unique one - newDoc._rev = "1-" + (`00000000000000000000000000000000${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}`.slice(-32)); + .addButton((button) => + button + .setButtonText("Perform") + .setDisabled(false) + .setWarning() + .onClick(async () => { + for await (const docName of this.plugin.localDatabase.findAllDocNames()) { + if (!docName.startsWith("f:")) { + const idEncoded = await this.plugin.$$path2id(docName as FilePathWithPrefix); + const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID); + if (!doc) continue; + if (doc.type != "newnote" && doc.type != "plain") { + continue; } - const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true }); - if (ret.ok) { - Logger(`${docName} has been converted as conflicted document`, LOG_LEVEL_NOTICE); - doc._deleted = true; - if ((await this.plugin.localDatabase.putRaw(doc)).ok) { - Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); + if (doc?.deleted ?? false) continue; + const newDoc = { ...doc }; + //Prepare converted data + newDoc._id = idEncoded; + newDoc.path = docName as FilePathWithPrefix; + // @ts-ignore + delete newDoc._rev; + try { + const obfuscatedDoc = await this.plugin.localDatabase.getRaw(idEncoded, { + revs_info: true, + }); + // Unfortunately we have to delete one of them. + // Just now, save it as a conflicted document. + obfuscatedDoc._revs_info?.shift(); // Drop latest revision. + const previousRev = obfuscatedDoc._revs_info?.shift(); // Use second revision. + if (previousRev) { + newDoc._rev = previousRev.rev; + } else { + //If there are no revisions, set the possibly unique one + newDoc._rev = + "1-" + + `00000000000000000000000000000000${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}${~~(Math.random() * 1e9)}`.slice( + -32 + ); } - await this.plugin.$$queueConflictCheckIfOpen(docName as FilePathWithPrefix); - } else { - Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE); - Logger(ret, LOG_LEVEL_VERBOSE); - } - } catch (ex: any) { - if (ex?.status == 404) { - // We can perform this safely - if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) { - Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE); + const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true }); + if (ret.ok) { + Logger( + `${docName} has been converted as conflicted document`, + LOG_LEVEL_NOTICE + ); doc._deleted = true; if ((await this.plugin.localDatabase.putRaw(doc)).ok) { Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); } + await this.plugin.$$queueConflictCheckIfOpen( + docName as FilePathWithPrefix + ); + } else { + Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE); + Logger(ret, LOG_LEVEL_VERBOSE); + } + } catch (ex: any) { + if (ex?.status == 404) { + // We can perform this safely + if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) { + Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE); + doc._deleted = true; + if ((await this.plugin.localDatabase.putRaw(doc)).ok) { + Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE); + } + } + } else { + Logger( + `Something went wrong while converting ${docName}`, + LOG_LEVEL_NOTICE + ); + Logger(ex, LOG_LEVEL_VERBOSE); + // Something wrong. } - } else { - Logger(`Something went wrong while converting ${docName}`, LOG_LEVEL_NOTICE); - Logger(ex, LOG_LEVEL_VERBOSE); - // Something wrong. } } } - } - Logger(`Converting finished`, LOG_LEVEL_NOTICE); - })); + Logger(`Converting finished`, LOG_LEVEL_NOTICE); + }) + ); }); void addPanel(paneEl, "Reset").then((paneEl) => { - new Setting(paneEl) - .setName("Back to non-configured") - .addButton((button) => button + new Setting(paneEl).setName("Back to non-configured").addButton((button) => + button .setButtonText("Back") .setDisabled(false) .onClick(async () => { this.editingSettings.isConfigured = false; await this.saveAllDirtySettings(); this.plugin.$$askReload(); - })); + }) + ); - new Setting(paneEl) - .setName("Delete all customization sync data") - .addButton((button) => button + new Setting(paneEl).setName("Delete all customization sync data").addButton((button) => + button .setButtonText("Delete") .setDisabled(false) .setWarning() .onClick(async () => { Logger(`Deleting customization sync data`, LOG_LEVEL_NOTICE); - const entriesToDelete = (await this.plugin.localDatabase.allDocsRaw({ - startkey: "ix:", endkey: "ix:\u{10ffff}", include_docs: true - })); - const newData = entriesToDelete.rows.map(e => ({ - ...e.doc, _deleted: true + const entriesToDelete = await this.plugin.localDatabase.allDocsRaw({ + startkey: "ix:", + endkey: "ix:\u{10ffff}", + include_docs: true, + }); + const newData = entriesToDelete.rows.map((e) => ({ + ...e.doc, + _deleted: true, })); const r = await this.plugin.localDatabase.bulkDocsRaw(newData as any[]); // Do not care about the result. - Logger(`${r.length} items have been removed, to confirm how many items are left, please perform it again.`, LOG_LEVEL_NOTICE); - })) + Logger( + `${r.length} items have been removed, to confirm how many items are left, please perform it again.`, + LOG_LEVEL_NOTICE + ); + }) + ); }); }); void addPane(containerEl, "Advanced", "🔧", 46, false, LEVEL_ADVANCED).then((paneEl) => { void addPanel(paneEl, "Memory cache").then((paneEl) => { - - new Setting(paneEl) - .autoWireNumeric("hashCacheMaxCount", { clampMin: 10 }); - new Setting(paneEl) - .autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 }); + new Setting(paneEl).autoWireNumeric("hashCacheMaxCount", { clampMin: 10 }); + new Setting(paneEl).autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 }); }); void addPanel(paneEl, "Local Database Tweak").then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("customChunkSize", { clampMin: 0 }) + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("customChunkSize", { clampMin: 0 }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("enableChunkSplitterV2", { onUpdate: enableOnly(() => this.isConfiguredAs("useSegmenter", false)) }) - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("useSegmenter", { onUpdate: enableOnly(() => this.isConfiguredAs("enableChunkSplitterV2", false)) }) + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("enableChunkSplitterV2", { + onUpdate: enableOnly(() => this.isConfiguredAs("useSegmenter", false)), + }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useSegmenter", { + onUpdate: enableOnly(() => this.isConfiguredAs("enableChunkSplitterV2", false)), + }); }); void addPanel(paneEl, "Transfer Tweak").then((paneEl) => { new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("readChunksOnline", { onUpdate: onlyOnCouchDB }) + .autoWireToggle("readChunksOnline", { onUpdate: onlyOnCouchDB }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("concurrencyOfReadChunksOnline", { - clampMin: 10, onUpdate: onlyOnCouchDB - }) + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("concurrencyOfReadChunksOnline", { + clampMin: 10, + onUpdate: onlyOnCouchDB, + }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("minimumIntervalOfReadChunksOnline", { - clampMin: 10, onUpdate: onlyOnCouchDB - }) + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("minimumIntervalOfReadChunksOnline", { + clampMin: 10, + onUpdate: onlyOnCouchDB, + }); // new Setting(paneEl) // .setClass("wizardHidden") // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) @@ -2223,55 +2621,60 @@ ${stringifyYaml(pluginConfig)}`; // .autoWireNumeric("sendChunksBulkMaxSize", { // clampMax: 100, clampMin: 1, onUpdate: onlyOnCouchDB // }) - }); }); void addPane(containerEl, "Power users", "💪", 47, true, LEVEL_POWER_USER).then((paneEl) => { - - void addPanel(paneEl, "Remote Database Tweak").then((paneEl) => { - new Setting(paneEl).autoWireToggle("useEden").setClass("wizardHidden"); const onlyUsingEden = visibleOnly(() => this.isConfiguredAs("useEden", true)); - new Setting(paneEl).autoWireNumeric("maxChunksInEden", { onUpdate: onlyUsingEden }).setClass("wizardHidden"); - new Setting(paneEl).autoWireNumeric("maxTotalLengthInEden", { onUpdate: onlyUsingEden }).setClass("wizardHidden"); - new Setting(paneEl).autoWireNumeric("maxAgeInEden", { onUpdate: onlyUsingEden }).setClass("wizardHidden"); + new Setting(paneEl) + .autoWireNumeric("maxChunksInEden", { onUpdate: onlyUsingEden }) + .setClass("wizardHidden"); + new Setting(paneEl) + .autoWireNumeric("maxTotalLengthInEden", { onUpdate: onlyUsingEden }) + .setClass("wizardHidden"); + new Setting(paneEl) + .autoWireNumeric("maxAgeInEden", { onUpdate: onlyUsingEden }) + .setClass("wizardHidden"); new Setting(paneEl).autoWireToggle("enableCompression").setClass("wizardHidden"); }); - void addPanel(paneEl, "CouchDB Connection Tweak", undefined, onlyOnCouchDB).then((paneEl) => { paneEl.addClass("wizardHidden"); - this.createEl(paneEl, "div", { - text: `If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.`, - }, undefined, onlyOnCouchDB).addClass("wizardHidden"); + this.createEl( + paneEl, + "div", + { + text: `If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.`, + }, + undefined, + onlyOnCouchDB + ).addClass("wizardHidden"); new Setting(paneEl) .setClass("wizardHidden") - .autoWireNumeric("batch_size", { clampMin: 2, onUpdate: onlyOnCouchDB }) - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("batches_limit", { - clampMin: 2, onUpdate: onlyOnCouchDB - }) - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("useTimeouts", { onUpdate: onlyOnCouchDB }); + .autoWireNumeric("batch_size", { clampMin: 2, onUpdate: onlyOnCouchDB }); + new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batches_limit", { + clampMin: 2, + onUpdate: onlyOnCouchDB, + }); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useTimeouts", { onUpdate: onlyOnCouchDB }); }); void addPanel(paneEl, "Configuration Encryption").then((paneEl) => { const passphrase_options: Record = { "": "Default", LOCALSTORAGE: "Use a custom passphrase", ASK_AT_LAUNCH: "Ask an passphrase at every launch", - } + }; new Setting(paneEl) .setName("Encrypting sensitive configuration items") .autoWireDropDown("configPassphraseStore", { - options: passphrase_options, holdValue: true + options: passphrase_options, + holdValue: true, }) .setClass("wizardHidden"); @@ -2279,70 +2682,54 @@ ${stringifyYaml(pluginConfig)}`; .autoWireText("configPassphrase", { isPassword: true, holdValue: true }) .setClass("wizardHidden") .addOnUpdate(() => ({ - disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE") - })) + disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE"), + })); new Setting(paneEl) .addApplyButton(["configPassphrase", "configPassphraseStore"]) - .setClass("wizardHidden") + .setClass("wizardHidden"); }); void addPanel(paneEl, "Developer").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("enableDebugTools") - .setClass("wizardHidden") + new Setting(paneEl).autoWireToggle("enableDebugTools").setClass("wizardHidden"); }); }); void addPane(containerEl, "Patches", "🩹", 51, false, LEVEL_EDGE_CASE).then((paneEl) => { - - void addPanel(paneEl, "Compatibility (Metadata)").then((paneEl) => { + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("deleteMetadataOfDeletedFiles"); new Setting(paneEl) .setClass("wizardHidden") - .autoWireToggle("deleteMetadataOfDeletedFiles") - - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", { onUpdate: visibleOnly(() => this.isConfiguredAs("deleteMetadataOfDeletedFiles", true)) }) - + .autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", { + onUpdate: visibleOnly(() => this.isConfiguredAs("deleteMetadataOfDeletedFiles", true)), + }); }); - void addPanel(paneEl, "Compatibility (Conflict Behaviour)").then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("disableMarkdownAutoMerge") - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("writeDocumentsIfConflicted") + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("disableMarkdownAutoMerge"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("writeDocumentsIfConflicted"); }); void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => { - - new Setting(paneEl) - .autoWireToggle("useIndexedDBAdapter", { invert: true, holdValue: true }) + new Setting(paneEl).autoWireToggle("useIndexedDBAdapter", { invert: true, holdValue: true }); new Setting(paneEl) .autoWireToggle("doNotUseFixedRevisionForChunks", { holdValue: true }) - .setClass("wizardHidden") + .setClass("wizardHidden"); new Setting(paneEl) .autoWireToggle("handleFilenameCaseSensitive", { holdValue: true }) - .setClass("wizardHidden") + .setClass("wizardHidden"); this.addOnSaved("useIndexedDBAdapter", async () => { await this.saveAllDirtySettings(); await rebuildDB("localOnly"); - }) + }); }); void addPanel(paneEl, "Compatibility (Internal API Usage)").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("watchInternalFileChanges", { invert: true }) - + new Setting(paneEl).autoWireToggle("watchInternalFileChanges", { invert: true }); }); - void addPanel(paneEl, "Edge case addressing (Database)").then((paneEl) => { new Setting(paneEl) .autoWireText("additionalSuffixOfDatabaseName", { holdValue: true }) @@ -2351,309 +2738,378 @@ ${stringifyYaml(pluginConfig)}`; this.addOnSaved("additionalSuffixOfDatabaseName", async (key) => { Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE); await this.plugin.$$initializeDatabase(); - }) + }); - new Setting(paneEl) - .autoWireDropDown("hashAlg", { - options: { - "": "Old Algorithm", - "xxhash32": "xxhash32 (Fast)", - "xxhash64": "xxhash64 (Fastest)", - "sha1": "Fallback (Without WebAssembly)" - } as Record - }) + new Setting(paneEl).autoWireDropDown("hashAlg", { + options: { + "": "Old Algorithm", + xxhash32: "xxhash32 (Fast)", + xxhash64: "xxhash64 (Fastest)", + sha1: "Fallback (Without WebAssembly)", + } as Record, + }); this.addOnSaved("hashAlg", async () => { await this.plugin.localDatabase._prepareHashFunctions(); - }) + }); }); void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("doNotSuspendOnFetching") - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("doNotDeleteFolder") + new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching"); + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); }); void addPanel(paneEl, "Edge case addressing (Processing)").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("disableWorkerForGeneratingChunks") + new Setting(paneEl).autoWireToggle("disableWorkerForGeneratingChunks"); - new Setting(paneEl) - .autoWireToggle("processSmallFilesInUIThread", { - onUpdate: visibleOnly(() => this.isConfiguredAs("disableWorkerForGeneratingChunks", false)) - }) + new Setting(paneEl).autoWireToggle("processSmallFilesInUIThread", { + onUpdate: visibleOnly(() => this.isConfiguredAs("disableWorkerForGeneratingChunks", false)), + }); }); void addPanel(paneEl, "Compatibility (Trouble addressed)").then((paneEl) => { - new Setting(paneEl) - .autoWireToggle("disableCheckingConfigMismatch") + new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch"); }); }); - void addPane(containerEl, "Maintenance", "🎛️", 70, true).then((paneEl) => { - const isRemoteLockedAndDeviceNotAccepted = () => this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted; const isRemoteLocked = () => this.plugin?.replicator?.remoteLocked; // if (this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted) { - this.createEl(paneEl, "div", { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. It caused by some operations like this. Re-initialized. Local database initialization should be required. Please back your vault up, reset the local database, and press 'Mark this device as resolved'. This warning kept showing until confirming the device is resolved by the replication.", - cls: "op-warn" - }, c => { - this.createEl(c, "button", { - text: "I'm ready, mark this device 'resolved'", cls: "mod-warning" - }, (e) => { - e.addEventListener("click", () => { - fireAndForget(async () => { - await this.plugin.$$markRemoteResolved(); - this.display(); - }); - }); - }) - }, visibleOnly(isRemoteLockedAndDeviceNotAccepted)); - this.createEl(paneEl, "div", { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database. This warning kept showing until confirming the device is resolved by the replication", - cls: "op-warn" - }, c => this.createEl(c, "button", { - text: "I'm ready, unlock the database", cls: "mod-warning" - }, (e) => { - e.addEventListener("click", () => { - fireAndForget(async () => { - await this.plugin.$$markRemoteUnlocked(); - this.display(); - }); - }); - }), visibleOnly(isRemoteLocked)); + this.createEl( + paneEl, + "div", + { + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. It caused by some operations like this. Re-initialized. Local database initialization should be required. Please back your vault up, reset the local database, and press 'Mark this device as resolved'. This warning kept showing until confirming the device is resolved by the replication.", + cls: "op-warn", + }, + (c) => { + this.createEl( + c, + "button", + { + text: "I'm ready, mark this device 'resolved'", + cls: "mod-warning", + }, + (e) => { + e.addEventListener("click", () => { + fireAndForget(async () => { + await this.plugin.$$markRemoteResolved(); + this.display(); + }); + }); + } + ); + }, + visibleOnly(isRemoteLockedAndDeviceNotAccepted) + ); + this.createEl( + paneEl, + "div", + { + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database. This warning kept showing until confirming the device is resolved by the replication", + cls: "op-warn", + }, + (c) => + this.createEl( + c, + "button", + { + text: "I'm ready, unlock the database", + cls: "mod-warning", + }, + (e) => { + e.addEventListener("click", () => { + fireAndForget(async () => { + await this.plugin.$$markRemoteUnlocked(); + this.display(); + }); + }); + } + ), + visibleOnly(isRemoteLocked) + ); void addPanel(paneEl, "Scram!").then((paneEl) => { new Setting(paneEl) .setName("Lock remote") .setDesc("Lock remote to prevent synchronization with other devices.") - .addButton((button) => button - .setButtonText("Lock") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.$$markRemoteLocked(); - })); + .addButton((button) => + button + .setButtonText("Lock") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.$$markRemoteLocked(); + }) + ); new Setting(paneEl) .setName("Emergency restart") .setDesc("place the flag file to prevent all operation and restart.") - .addButton((button) => button - .setButtonText("Flag and restart") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, ""); - this.plugin.$$performRestart(); - })); - + .addButton((button) => + button + .setButtonText("Flag and restart") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, ""); + this.plugin.$$performRestart(); + }) + ); }); void addPanel(paneEl, "Data-complementary Operations").then((paneEl) => { new Setting(paneEl) .setName("Resend") .setDesc("Resend all chunks to the remote.") - .addButton((button) => button - .setButtonText("Send chunks") - .setWarning() - .setDisabled(false) - .onClick(async () => { - if (this.plugin.replicator instanceof LiveSyncCouchDBReplicator) { - await this.plugin.replicator.sendChunks(this.plugin.settings, undefined, true, 0); - } - })) + .addButton((button) => + button + .setButtonText("Send chunks") + .setWarning() + .setDisabled(false) + .onClick(async () => { + if (this.plugin.replicator instanceof LiveSyncCouchDBReplicator) { + await this.plugin.replicator.sendChunks(this.plugin.settings, undefined, true, 0); + } + }) + ) .addOnUpdate(onlyOnCouchDB); new Setting(paneEl) .setName("Reset journal received history") - .setDesc("Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.") - .addButton((button) => button - .setButtonText("Reset received") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ - ...info, receivedFiles: new Set(), knownIDs: new Set() - })); - Logger(`Journal received history has been cleared.`, LOG_LEVEL_NOTICE); - })).addOnUpdate(onlyOnMinIO); + .setDesc( + "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again." + ) + .addButton((button) => + button + .setButtonText("Reset received") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ + ...info, + receivedFiles: new Set(), + knownIDs: new Set(), + })); + Logger(`Journal received history has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(onlyOnMinIO); new Setting(paneEl) .setName("Reset journal sent history") - .setDesc("Initialise journal sent history. On the next sync, every item except this device received will be sent again.") - .addButton((button) => button - .setButtonText("Reset sent history") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ - ...info, lastLocalSeq: 0, sentIDs: new Set(), sentFiles: new Set() - })); - Logger(`Journal sent history has been cleared.`, LOG_LEVEL_NOTICE); - })).addOnUpdate(onlyOnMinIO); - + .setDesc( + "Initialise journal sent history. On the next sync, every item except this device received will be sent again." + ) + .addButton((button) => + button + .setButtonText("Reset sent history") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ + ...info, + lastLocalSeq: 0, + sentIDs: new Set(), + sentFiles: new Set(), + })); + Logger(`Journal sent history has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(onlyOnMinIO); }); void addPanel(paneEl, "Rebuilding Operations (Local)").then((paneEl) => { - new Setting(paneEl) .setName("Fetch from remote") .setDesc("Restore or reconstruct local database from remote.") - .addButton((button) => button - .setButtonText("Fetch") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); - this.plugin.$$performRestart(); - })).addButton((button) => button + .addButton((button) => + button + .setButtonText("Fetch") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); + this.plugin.$$performRestart(); + }) + ) + .addButton((button) => + button .setButtonText("Fetch w/o restarting") .setWarning() .setDisabled(false) .onClick(async () => { await rebuildDB("localOnly"); - })) + }) + ); new Setting(paneEl) .setName("Fetch rebuilt DB (Save local documents before)") .setDesc("Restore or reconstruct local database from remote database but use local chunks.") - .addButton((button) => button - .setButtonText("Save and Fetch") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("localOnlyWithChunks"); - })).addOnUpdate(onlyOnCouchDB); - + .addButton((button) => + button + .setButtonText("Save and Fetch") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await rebuildDB("localOnlyWithChunks"); + }) + ) + .addOnUpdate(onlyOnCouchDB); }); void addPanel(paneEl, "Total Overhaul").then((paneEl) => { new Setting(paneEl) .setName("Rebuild everything") .setDesc("Rebuild local and remote database with local files.") - .addButton((button) => button - .setButtonText("Rebuild") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); - this.plugin.$$performRestart(); - })) - .addButton((button) => button - .setButtonText("Rebuild w/o restarting") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("rebuildBothByThisDevice"); - })) + .addButton((button) => + button + .setButtonText("Rebuild") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); + this.plugin.$$performRestart(); + }) + ) + .addButton((button) => + button + .setButtonText("Rebuild w/o restarting") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await rebuildDB("rebuildBothByThisDevice"); + }) + ); }); void addPanel(paneEl, "Rebuilding Operations (Remote Only)").then((paneEl) => { - new Setting(paneEl) .setName("Perform compaction") - .setDesc("Compaction discards all of Eden in the non-latest revisions, reducing the storage usage. However, this operation requires the same free space on the remote as the current database.") - .addButton((button) => button - .setButtonText("Perform") - .setDisabled(false) - .onClick(async () => { - const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator; - Logger(`Compaction has been began`, LOG_LEVEL_NOTICE, "compaction") - if (await replicator.compactRemote(this.editingSettings)) { - Logger(`Compaction has been completed!`, LOG_LEVEL_NOTICE, "compaction"); - } else { - Logger(`Compaction has been failed!`, LOG_LEVEL_NOTICE, "compaction"); - } - })).addOnUpdate(onlyOnCouchDB); - + .setDesc( + "Compaction discards all of Eden in the non-latest revisions, reducing the storage usage. However, this operation requires the same free space on the remote as the current database." + ) + .addButton((button) => + button + .setButtonText("Perform") + .setDisabled(false) + .onClick(async () => { + const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator; + Logger(`Compaction has been began`, LOG_LEVEL_NOTICE, "compaction"); + if (await replicator.compactRemote(this.editingSettings)) { + Logger(`Compaction has been completed!`, LOG_LEVEL_NOTICE, "compaction"); + } else { + Logger(`Compaction has been failed!`, LOG_LEVEL_NOTICE, "compaction"); + } + }) + ) + .addOnUpdate(onlyOnCouchDB); new Setting(paneEl) .setName("Overwrite remote") .setDesc("Overwrite remote with local DB and passphrase.") - .addButton((button) => button - .setButtonText("Send") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await rebuildDB("remoteOnly"); - })) + .addButton((button) => + button + .setButtonText("Send") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await rebuildDB("remoteOnly"); + }) + ); new Setting(paneEl) .setName("Reset all journal counter") .setDesc("Initialise all journal history, On the next sync, every item will be received and sent.") - .addButton((button) => button - .setButtonText("Reset all") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().resetCheckpointInfo(); - Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE); - })).addOnUpdate(onlyOnMinIO); + .addButton((button) => + button + .setButtonText("Reset all") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().resetCheckpointInfo(); + Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(onlyOnMinIO); new Setting(paneEl) .setName("Purge all journal counter") .setDesc("Purge all sending and downloading cache.") - .addButton((button) => button - .setButtonText("Reset all") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().resetAllCaches(); - Logger(`Journal sending and downloading cache has been cleared.`, LOG_LEVEL_NOTICE); - })).addOnUpdate(onlyOnMinIO); + .addButton((button) => + button + .setButtonText("Reset all") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().resetAllCaches(); + Logger(`Journal sending and downloading cache has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(onlyOnMinIO); new Setting(paneEl) .setName("Make empty the bucket") .setDesc("Delete all data on the remote.") - .addButton((button) => button - .setButtonText("Delete") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ - ...info, - receivedFiles: new Set(), - knownIDs: new Set(), - lastLocalSeq: 0, - sentIDs: new Set(), - sentFiles: new Set() - })); - await this.resetRemoteBucket(); - Logger(`the bucket has been cleared.`, LOG_LEVEL_NOTICE); - })).addOnUpdate(onlyOnMinIO); + .addButton((button) => + button + .setButtonText("Delete") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({ + ...info, + receivedFiles: new Set(), + knownIDs: new Set(), + lastLocalSeq: 0, + sentIDs: new Set(), + sentFiles: new Set(), + })); + await this.resetRemoteBucket(); + Logger(`the bucket has been cleared.`, LOG_LEVEL_NOTICE); + }) + ) + .addOnUpdate(onlyOnMinIO); }); void addPanel(paneEl, "Niches").then((paneEl) => { new Setting(paneEl) .setClass("sls-setting-obsolete") .setName("(Obsolete) Clean up databases") - .setDesc("Delete unused chunks to shrink the database. However, this feature could be not effective in some cases. Please use rebuild everything instead.") - .addButton((button) => button.setButtonText("DryRun") - .setDisabled(false) - .onClick(async () => { - await this.dryRunGC(); - })).addButton((button) => button.setButtonText("Perform cleaning") + .setDesc( + "Delete unused chunks to shrink the database. However, this feature could be not effective in some cases. Please use rebuild everything instead." + ) + .addButton((button) => + button + .setButtonText("DryRun") + .setDisabled(false) + .onClick(async () => { + await this.dryRunGC(); + }) + ) + .addButton((button) => + button + .setButtonText("Perform cleaning") .setDisabled(false) .setWarning() .onClick(async () => { - this.closeSetting() + this.closeSetting(); await this.dbGC(); - })).addOnUpdate(onlyOnCouchDB); + }) + ) + .addOnUpdate(onlyOnCouchDB); }); void addPanel(paneEl, "Reset").then((paneEl) => { new Setting(paneEl) .setName("Discard local database to reset or uninstall Self-hosted LiveSync") - .addButton((button) => button - .setButtonText("Discard") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.$$resetLocalDatabase(); - await this.plugin.$$initializeDatabase(); - })); + .addButton((button) => + button + .setButtonText("Discard") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.$$resetLocalDatabase(); + await this.plugin.$$initializeDatabase(); + }) + ); }); - - }); void yieldNextAnimationFrame().then(() => { if (this.selectedScreen == "") { @@ -2661,13 +3117,13 @@ ${stringifyYaml(pluginConfig)}`; if (this.editingSettings.isConfigured) { changeDisplay("100"); } else { - changeDisplay("110") + changeDisplay("110"); } } else { if (isAnySyncEnabled()) { changeDisplay("20"); } else { - changeDisplay("110") + changeDisplay("110"); } } } else { @@ -2681,8 +3137,11 @@ ${stringifyYaml(pluginConfig)}`; await skipIfDuplicated("cleanup", async () => { const replicator = this.plugin.$$getReplicator(); if (!(replicator instanceof LiveSyncCouchDBReplicator)) return; - const remoteDBConn = await replicator.connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.$$isMobile()) - if (typeof (remoteDBConn) == "string") { + const remoteDBConn = await replicator.connectRemoteCouchDBWithSetting( + this.plugin.settings, + this.plugin.$$isMobile() + ); + if (typeof remoteDBConn == "string") { Logger(remoteDBConn); return; } @@ -2698,8 +3157,11 @@ ${stringifyYaml(pluginConfig)}`; const replicator = this.plugin.$$getReplicator(); if (!(replicator instanceof LiveSyncCouchDBReplicator)) return; await this.plugin.$$getReplicator().markRemoteLocked(this.plugin.settings, true, true); - const remoteDBConnection = await replicator.connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.$$isMobile()) - if (typeof (remoteDBConnection) == "string") { + const remoteDBConnection = await replicator.connectRemoteCouchDBWithSetting( + this.plugin.settings, + this.plugin.$$isMobile() + ); + if (typeof remoteDBConnection == "string") { Logger(remoteDBConnection); return; } @@ -2708,22 +3170,32 @@ ${stringifyYaml(pluginConfig)}`; this.plugin.localDatabase.hashCaches.clear(); await balanceChunkPurgedDBs(this.plugin.localDatabase.localDatabase, remoteDBConnection.db); this.plugin.localDatabase.refreshSettings(); - Logger("The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation.") + Logger( + "The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation." + ); }); } getMinioJournalSyncClient() { - const id = this.plugin.settings.accessKey - const key = this.plugin.settings.secretKey - const bucket = this.plugin.settings.bucket - const region = this.plugin.settings.region - const endpoint = this.plugin.settings.endpoint + const id = this.plugin.settings.accessKey; + const key = this.plugin.settings.secretKey; + const bucket = this.plugin.settings.bucket; + const region = this.plugin.settings.region; + const endpoint = this.plugin.settings.endpoint; const useCustomRequestHandler = this.plugin.settings.useCustomRequestHandler; - return new JournalSyncMinio(id, key, endpoint, bucket, this.plugin.simpleStore, this.plugin, useCustomRequestHandler, region); + return new JournalSyncMinio( + id, + key, + endpoint, + bucket, + this.plugin.simpleStore, + this.plugin, + useCustomRequestHandler, + region + ); } async resetRemoteBucket() { const minioJournal = this.getMinioJournalSyncClient(); await minioJournal.resetBucket(); } } - diff --git a/src/modules/features/SettingDialogue/settingConstants.ts b/src/modules/features/SettingDialogue/settingConstants.ts index 7b7d0c2..adb99a8 100644 --- a/src/modules/features/SettingDialogue/settingConstants.ts +++ b/src/modules/features/SettingDialogue/settingConstants.ts @@ -1,13 +1,21 @@ import { $t } from "../../../lib/src/common/i18n.ts"; -import { DEFAULT_SETTINGS, configurationNames, type ConfigurationItem, type FilterBooleanKeys, type FilterNumberKeys, type FilterStringKeys, type ObsidianLiveSyncSettings } from "../../../lib/src/common/types.ts"; +import { + DEFAULT_SETTINGS, + configurationNames, + type ConfigurationItem, + type FilterBooleanKeys, + type FilterNumberKeys, + type FilterStringKeys, + type ObsidianLiveSyncSettings, +} from "../../../lib/src/common/types.ts"; export type OnDialogSettings = { - configPassphrase: string, - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE", - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC" - dummy: number, - deviceAndVaultName: string, -} + configPassphrase: string; + preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; + syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; + dummy: number; + deviceAndVaultName: string; +}; export const OnDialogSettingsDefault: OnDialogSettings = { configPassphrase: "", @@ -15,9 +23,8 @@ export const OnDialogSettingsDefault: OnDialogSettings = { syncMode: "ONEVENTS", dummy: 0, deviceAndVaultName: "", -} -export const AllSettingDefault = - { ...DEFAULT_SETTINGS, ...OnDialogSettingsDefault } +}; +export const AllSettingDefault = { ...DEFAULT_SETTINGS, ...OnDialogSettingsDefault }; export type AllSettings = ObsidianLiveSyncSettings & OnDialogSettings; export type AllStringItemKey = FilterStringKeys; @@ -25,324 +32,326 @@ export type AllNumericItemKey = FilterNumberKeys; export type AllBooleanItemKey = FilterBooleanKeys; export type AllSettingItemKey = AllStringItemKey | AllNumericItemKey | AllBooleanItemKey; -export type ValueOf = - T extends AllStringItemKey ? string : - T extends AllNumericItemKey ? number : - T extends AllBooleanItemKey ? boolean : - AllSettings[T]; +export type ValueOf = T extends AllStringItemKey + ? string + : T extends AllNumericItemKey + ? number + : T extends AllBooleanItemKey + ? boolean + : AllSettings[T]; export const SettingInformation: Partial> = { - "liveSync": { - "name": "Sync Mode" - }, - "couchDB_URI": { - "name": "URI", - "placeHolder": "https://........" - }, - "couchDB_USER": { - "name": "Username", - "desc": "username" - }, - "couchDB_PASSWORD": { - "name": "Password", - "desc": "password" - }, - "couchDB_DBNAME": { - "name": "Database name" - }, - "passphrase": { - "name": "Passphrase", - "desc": "Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended." - }, - "showStatusOnEditor": { - "name": "Show status inside the editor", - "desc": "Reflected after reboot" - }, - "showOnlyIconsOnEditor": { - "name": "Show status as icons only" - }, - "showStatusOnStatusbar": { - "name": "Show status on the status bar", - "desc": "Reflected after reboot." - }, - "lessInformationInLog": { - "name": "Show only notifications", - "desc": "Prevent logging and show only notification. Please disable when you report the logs" - }, - "showVerboseLog": { - "name": "Verbose Log", - "desc": "Show verbose log. Please enable when you report the logs" - }, - "hashCacheMaxCount": { - "name": "Memory cache size (by total items)" - }, - "hashCacheMaxAmount": { - "name": "Memory cache size (by total characters)", - "desc": "(Mega chars)" - }, - "writeCredentialsForSettingSync": { - "name": "Write credentials in the file", - "desc": "(Not recommended) If set, credentials will be stored in the file." - }, - "notifyAllSettingSyncFile": { - "name": "Notify all setting files" - }, - "configPassphrase": { - "name": "Passphrase of sensitive configuration items", - "desc": "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again." - }, - "configPassphraseStore": { - "name": "Encrypting sensitive configuration items" - }, - "syncOnSave": { - "name": "Sync on Save", - "desc": "When you save a file, sync automatically" - }, - "syncOnEditorSave": { - "name": "Sync on Editor Save", - "desc": "When you save a file in the editor, sync automatically" - }, - "syncOnFileOpen": { - "name": "Sync on File Open", - "desc": "When you open a file, sync automatically" - }, - "syncOnStart": { - "name": "Sync on Start", - "desc": "Start synchronization after launching Obsidian." - }, - "syncAfterMerge": { - "name": "Sync after merging file", - "desc": "Sync automatically after merging files" - }, - "trashInsteadDelete": { - "name": "Use the trash bin", - "desc": "Do not delete files that are deleted in remote, just move to trash." - }, - "doNotDeleteFolder": { - "name": "Keep empty folder", - "desc": "Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted" - }, - "resolveConflictsByNewerFile": { - "name": "Always overwrite with a newer file (beta)", - "desc": "(Def off) Resolve conflicts by newer files automatically." - }, - "checkConflictOnlyOnOpen": { - "name": "Postpone resolution of inactive files" - }, - "showMergeDialogOnlyOnActive": { - "name": "Postpone manual resolution of inactive files" - }, - "disableMarkdownAutoMerge": { - "name": "Always resolve conflicts manually", - "desc": "If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)" - }, - "writeDocumentsIfConflicted": { - "name": "Always reflect synchronized changes even if the note has a conflict", - "desc": "Turn on to previous behavior" - }, - "syncInternalFilesInterval": { - "name": "Scan hidden files periodically", - "desc": "Seconds, 0 to disable" - }, - "batchSave": { - "name": "Batch database update", - "desc": "Reducing the frequency with which on-disk changes are reflected into the DB" - }, - "readChunksOnline": { - "name": "Fetch chunks on demand", - "desc": "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended." - }, - "syncMaxSizeInMB": { - "name": "Maximum file size", - "desc": "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used." - }, - "useIgnoreFiles": { - "name": "(Beta) Use ignore files", - "desc": "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files." - }, - "ignoreFiles": { - "name": "Ignore files", - "desc": "We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`" - }, - "batch_size": { - "name": "Batch size", - "desc": "Number of change feed items to process at a time. Defaults to 50. Minimum is 2." - }, - "batches_limit": { - "name": "Batch limit", - "desc": "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time." - }, - "useTimeouts": { - "name": "Use timeouts instead of heartbeats", - "desc": "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage." - }, - "concurrencyOfReadChunksOnline": { - "name": "Batch size of on-demand fetching" - }, - "minimumIntervalOfReadChunksOnline": { - "name": "The delay for consecutive on-demand fetches" - }, - "suspendFileWatching": { - "name": "Suspend file watching", - "desc": "Stop watching for file change." - }, - "suspendParseReplicationResult": { - "name": "Suspend database reflecting", - "desc": "Stop reflecting database changes to storage files." - }, - "writeLogToTheFile": { - "name": "Write logs into the file", - "desc": "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information." - }, - "deleteMetadataOfDeletedFiles": { - "name": "Do not keep metadata of deleted files." - }, - "useIndexedDBAdapter": { - "name": "(Obsolete) Use an old adapter for compatibility", - "desc": "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.", - "obsolete": true - }, - "watchInternalFileChanges": { - "name": "Scan changes on customization sync", - "desc": "Do not use internal API" - }, - "doNotSuspendOnFetching": { - "name": "Fetch database with previous behaviour" - }, - "disableCheckingConfigMismatch": { - "name": "Do not check configuration mismatch before replication" - }, - "usePluginSync": { - "name": "Enable customization sync" - }, - "autoSweepPlugins": { - "name": "Scan customization automatically", - "desc": "Scan customization before replicating." - }, - "autoSweepPluginsPeriodic": { - "name": "Scan customization periodically", - "desc": "Scan customization every 1 minute." - }, - "notifyPluginOrSettingUpdated": { - "name": "Notify customized", - "desc": "Notify when other device has newly customized." - }, - "remoteType": { - "name": "Remote Type", - "desc": "Remote server type" - }, - "endpoint": { - "name": "Endpoint URL", - "placeHolder": "https://........" - }, - "accessKey": { - "name": "Access Key" - }, - "secretKey": { - "name": "Secret Key" - }, - "region": { - "name": "Region", - "placeHolder": "auto" - }, - "bucket": { - "name": "Bucket Name" - }, - "useCustomRequestHandler": { - "name": "Use Custom HTTP Handler", - "desc": "If your Object Storage could not configured accepting CORS, enable this." - }, - "maxChunksInEden": { - "name": "Maximum Incubating Chunks", - "desc": "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks." - }, - "maxTotalLengthInEden": { - "name": "Maximum Incubating Chunk Size", - "desc": "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks." - }, - "maxAgeInEden": { - "name": "Maximum Incubation Period", - "desc": "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks." - }, - "settingSyncFile": { - "name": "Filename", - "desc": "If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform." - }, - "preset": { - "name": "Presets", - "desc": "Apply preset configuration" - }, - "syncMode": { + liveSync: { name: "Sync Mode", }, - "periodicReplicationInterval": { - "name": "Periodic Sync interval", - "desc": "Interval (sec)" + couchDB_URI: { + name: "URI", + placeHolder: "https://........", }, - "syncInternalFilesBeforeReplication": { - "name": "Scan for hidden files before replication" + couchDB_USER: { + name: "Username", + desc: "username", }, - "automaticallyDeleteMetadataOfDeletedFiles": { - "name": "Delete old metadata of deleted files on start-up", - "desc": "(Days passed, 0 to disable automatic-deletion)" + couchDB_PASSWORD: { + name: "Password", + desc: "password", }, - "additionalSuffixOfDatabaseName": { - "name": "Database suffix", - "desc": "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured." + couchDB_DBNAME: { + name: "Database name", }, - "hashAlg": { - "name": configurationNames["hashAlg"]?.name || "", - "desc": "xxhash64 is the current default." + passphrase: { + name: "Passphrase", + desc: "Encrypting passphrase. If you change the passphrase of an existing database, overwriting the remote database is strongly recommended.", }, - "deviceAndVaultName": { - "name": "Device name", - "desc": "Unique name between all synchronized devices. To edit this setting, please disable customization sync once." + showStatusOnEditor: { + name: "Show status inside the editor", + desc: "Reflected after reboot", }, - "displayLanguage": { - "name": "Display Language", - "desc": "Not all messages have been translated. And, please revert to \"Default\" when reporting errors." + showOnlyIconsOnEditor: { + name: "Show status as icons only", + }, + showStatusOnStatusbar: { + name: "Show status on the status bar", + desc: "Reflected after reboot.", + }, + lessInformationInLog: { + name: "Show only notifications", + desc: "Prevent logging and show only notification. Please disable when you report the logs", + }, + showVerboseLog: { + name: "Verbose Log", + desc: "Show verbose log. Please enable when you report the logs", + }, + hashCacheMaxCount: { + name: "Memory cache size (by total items)", + }, + hashCacheMaxAmount: { + name: "Memory cache size (by total characters)", + desc: "(Mega chars)", + }, + writeCredentialsForSettingSync: { + name: "Write credentials in the file", + desc: "(Not recommended) If set, credentials will be stored in the file.", + }, + notifyAllSettingSyncFile: { + name: "Notify all setting files", + }, + configPassphrase: { + name: "Passphrase of sensitive configuration items", + desc: "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.", + }, + configPassphraseStore: { + name: "Encrypting sensitive configuration items", + }, + syncOnSave: { + name: "Sync on Save", + desc: "When you save a file, sync automatically", + }, + syncOnEditorSave: { + name: "Sync on Editor Save", + desc: "When you save a file in the editor, sync automatically", + }, + syncOnFileOpen: { + name: "Sync on File Open", + desc: "When you open a file, sync automatically", + }, + syncOnStart: { + name: "Sync on Start", + desc: "Start synchronization after launching Obsidian.", + }, + syncAfterMerge: { + name: "Sync after merging file", + desc: "Sync automatically after merging files", + }, + trashInsteadDelete: { + name: "Use the trash bin", + desc: "Do not delete files that are deleted in remote, just move to trash.", + }, + doNotDeleteFolder: { + name: "Keep empty folder", + desc: "Normally, a folder is deleted when it becomes empty after a synchronization. Enabling this will prevent it from getting deleted", + }, + resolveConflictsByNewerFile: { + name: "Always overwrite with a newer file (beta)", + desc: "(Def off) Resolve conflicts by newer files automatically.", + }, + checkConflictOnlyOnOpen: { + name: "Postpone resolution of inactive files", + }, + showMergeDialogOnlyOnActive: { + name: "Postpone manual resolution of inactive files", + }, + disableMarkdownAutoMerge: { + name: "Always resolve conflicts manually", + desc: "If this switch is turned on, a merge dialog will be displayed, even if the sensible-merge is possible automatically. (Turn on to previous behavior)", + }, + writeDocumentsIfConflicted: { + name: "Always reflect synchronized changes even if the note has a conflict", + desc: "Turn on to previous behavior", + }, + syncInternalFilesInterval: { + name: "Scan hidden files periodically", + desc: "Seconds, 0 to disable", + }, + batchSave: { + name: "Batch database update", + desc: "Reducing the frequency with which on-disk changes are reflected into the DB", + }, + readChunksOnline: { + name: "Fetch chunks on demand", + desc: "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.", + }, + syncMaxSizeInMB: { + name: "Maximum file size", + desc: "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.", + }, + useIgnoreFiles: { + name: "(Beta) Use ignore files", + desc: "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.", + }, + ignoreFiles: { + name: "Ignore files", + desc: "We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`", + }, + batch_size: { + name: "Batch size", + desc: "Number of change feed items to process at a time. Defaults to 50. Minimum is 2.", + }, + batches_limit: { + name: "Batch limit", + desc: "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.", + }, + useTimeouts: { + name: "Use timeouts instead of heartbeats", + desc: "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.", + }, + concurrencyOfReadChunksOnline: { + name: "Batch size of on-demand fetching", + }, + minimumIntervalOfReadChunksOnline: { + name: "The delay for consecutive on-demand fetches", + }, + suspendFileWatching: { + name: "Suspend file watching", + desc: "Stop watching for file change.", + }, + suspendParseReplicationResult: { + name: "Suspend database reflecting", + desc: "Stop reflecting database changes to storage files.", + }, + writeLogToTheFile: { + name: "Write logs into the file", + desc: "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.", + }, + deleteMetadataOfDeletedFiles: { + name: "Do not keep metadata of deleted files.", + }, + useIndexedDBAdapter: { + name: "(Obsolete) Use an old adapter for compatibility", + desc: "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.", + obsolete: true, + }, + watchInternalFileChanges: { + name: "Scan changes on customization sync", + desc: "Do not use internal API", + }, + doNotSuspendOnFetching: { + name: "Fetch database with previous behaviour", + }, + disableCheckingConfigMismatch: { + name: "Do not check configuration mismatch before replication", + }, + usePluginSync: { + name: "Enable customization sync", + }, + autoSweepPlugins: { + name: "Scan customization automatically", + desc: "Scan customization before replicating.", + }, + autoSweepPluginsPeriodic: { + name: "Scan customization periodically", + desc: "Scan customization every 1 minute.", + }, + notifyPluginOrSettingUpdated: { + name: "Notify customized", + desc: "Notify when other device has newly customized.", + }, + remoteType: { + name: "Remote Type", + desc: "Remote server type", + }, + endpoint: { + name: "Endpoint URL", + placeHolder: "https://........", + }, + accessKey: { + name: "Access Key", + }, + secretKey: { + name: "Secret Key", + }, + region: { + name: "Region", + placeHolder: "auto", + }, + bucket: { + name: "Bucket Name", + }, + useCustomRequestHandler: { + name: "Use Custom HTTP Handler", + desc: "If your Object Storage could not configured accepting CORS, enable this.", + }, + maxChunksInEden: { + name: "Maximum Incubating Chunks", + desc: "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.", + }, + maxTotalLengthInEden: { + name: "Maximum Incubating Chunk Size", + desc: "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.", + }, + maxAgeInEden: { + name: "Maximum Incubation Period", + desc: "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.", + }, + settingSyncFile: { + name: "Filename", + desc: "If you set this, all settings are saved in a markdown file. You will be notified when new settings arrive. You can set different files by the platform.", + }, + preset: { + name: "Presets", + desc: "Apply preset configuration", + }, + syncMode: { + name: "Sync Mode", + }, + periodicReplicationInterval: { + name: "Periodic Sync interval", + desc: "Interval (sec)", + }, + syncInternalFilesBeforeReplication: { + name: "Scan for hidden files before replication", + }, + automaticallyDeleteMetadataOfDeletedFiles: { + name: "Delete old metadata of deleted files on start-up", + desc: "(Days passed, 0 to disable automatic-deletion)", + }, + additionalSuffixOfDatabaseName: { + name: "Database suffix", + desc: "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.", + }, + hashAlg: { + name: configurationNames["hashAlg"]?.name || "", + desc: "xxhash64 is the current default.", + }, + deviceAndVaultName: { + name: "Device name", + desc: "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.", + }, + displayLanguage: { + name: "Display Language", + desc: 'Not all messages have been translated. And, please revert to "Default" when reporting errors.', }, enableChunkSplitterV2: { name: "Use splitting-limit-capped chunk splitter", - desc: "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker." + desc: "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.", }, disableWorkerForGeneratingChunks: { name: "Do not split chunks in the background", - desc: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour)." + desc: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).", }, processSmallFilesInUIThread: { name: "Process small files in the foreground", - desc: "If enabled, the file under 1kb will be processed in the UI thread." + desc: "If enabled, the file under 1kb will be processed in the UI thread.", }, batchSaveMinimumDelay: { name: "Minimum delay for batch database updating", - desc: "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving." + desc: "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.", }, batchSaveMaximumDelay: { name: "Maximum delay for batch database updating", - desc: "Saving will be performed forcefully after this number of seconds." + desc: "Saving will be performed forcefully after this number of seconds.", }, - "notifyThresholdOfRemoteStorageSize": { + notifyThresholdOfRemoteStorageSize: { name: "Notify when the estimated remote storage size exceeds on start up", - desc: "MB (0 to disable)." + desc: "MB (0 to disable).", }, - "usePluginSyncV2": { + usePluginSyncV2: { name: "Enable per-file customization sync", - desc: "If enabled, efficient per-file customization sync will be used. A minor migration is required when enabling this feature, and all devices must be updated to v0.23.18. Enabling this feature will result in losing compatibility with older versions." + desc: "If enabled, efficient per-file customization sync will be used. A minor migration is required when enabling this feature, and all devices must be updated to v0.23.18. Enabling this feature will result in losing compatibility with older versions.", }, - "handleFilenameCaseSensitive": { + handleFilenameCaseSensitive: { name: "Handle files as Case-Sensitive", - desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)." + desc: "If this enabled, All files are handled as case-Sensitive (Previous behaviour).", }, - "doNotUseFixedRevisionForChunks": { + doNotUseFixedRevisionForChunks: { name: "Compute revisions for chunks (Previous behaviour)", - desc: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)" + desc: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)", }, - "sendChunksBulkMaxSize": { + sendChunksBulkMaxSize: { name: "Maximum size of chunks to send in one request", - desc: "MB" + desc: "MB", }, - "useAdvancedMode": { + useAdvancedMode: { name: "Enable advanced features", // desc: "Enable advanced mode" }, @@ -354,11 +363,11 @@ export const SettingInformation: Partial; askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { title?: string, defaultOption?: "Yes" | "No", timeout?: number }): Promise<"yes" | "no">; + askYesNoDialog( + message: string, + opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } + ): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise + askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: string[], opt: { title?: string, defaultAction: (typeof buttons)[number], timeout?: number }): Promise<(typeof buttons)[number] | false>; + askSelectStringDialogue( + message: string, + buttons: string[], + opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number } + ): Promise<(typeof buttons)[number] | false>; askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} \ No newline at end of file + confirmWithMessage( + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number + ): Promise<(typeof buttons)[number] | false>; +} diff --git a/src/modules/interfaces/DatabaseFileAccess.ts b/src/modules/interfaces/DatabaseFileAccess.ts index 99099d1..7f151ac 100644 --- a/src/modules/interfaces/DatabaseFileAccess.ts +++ b/src/modules/interfaces/DatabaseFileAccess.ts @@ -1,18 +1,34 @@ -import type { FilePathWithPrefix, LoadedEntry, MetaEntry, UXFileInfo, UXFileInfoStub } from "../../lib/src/common/types"; +import type { + FilePathWithPrefix, + LoadedEntry, + MetaEntry, + UXFileInfo, + UXFileInfoStub, +} from "../../lib/src/common/types"; export interface DatabaseFileAccess { delete: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string) => Promise; store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise; storeContent(path: FilePathWithPrefix, content: string): Promise; createChunks: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise; - fetch: (file: UXFileInfoStub | FilePathWithPrefix, - rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise; - fetchEntryFromMeta: (meta: MetaEntry, - waitForReady?: boolean, skipCheck?: boolean) => Promise; - fetchEntryMeta: (file: UXFileInfoStub | FilePathWithPrefix, - rev?: string, skipCheck?: boolean) => Promise; - fetchEntry: (file: UXFileInfoStub | FilePathWithPrefix, - rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise; + fetch: ( + file: UXFileInfoStub | FilePathWithPrefix, + rev?: string, + waitForReady?: boolean, + skipCheck?: boolean + ) => Promise; + fetchEntryFromMeta: (meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean) => Promise; + fetchEntryMeta: ( + file: UXFileInfoStub | FilePathWithPrefix, + rev?: string, + skipCheck?: boolean + ) => Promise; + fetchEntry: ( + file: UXFileInfoStub | FilePathWithPrefix, + rev?: string, + waitForReady?: boolean, + skipCheck?: boolean + ) => Promise; getConflictedRevs: (file: UXFileInfoStub | FilePathWithPrefix) => Promise; // storeFromStorage: (file: UXFileInfoStub | FilePathWithPrefix, force?: boolean) => Promise; -} \ No newline at end of file +} diff --git a/src/modules/interfaces/DatabaseRebuilder.ts b/src/modules/interfaces/DatabaseRebuilder.ts index ee7226e..d55398b 100644 --- a/src/modules/interfaces/DatabaseRebuilder.ts +++ b/src/modules/interfaces/DatabaseRebuilder.ts @@ -1,5 +1,7 @@ export interface Rebuilder { - $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise; + $performRebuildDB( + method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks" + ): Promise; $rebuildRemote(): Promise; $rebuildEverything(): Promise; $fetchLocal(makeLocalChunkBeforeSync?: boolean): Promise; @@ -7,5 +9,4 @@ export interface Rebuilder { scheduleRebuild(): Promise; scheduleFetch(): Promise; resolveAllConflictedFilesByNewerOnes(): Promise; - -} \ No newline at end of file +} diff --git a/src/modules/interfaces/StorageAccess.ts b/src/modules/interfaces/StorageAccess.ts index e16ded3..010a1d6 100644 --- a/src/modules/interfaces/StorageAccess.ts +++ b/src/modules/interfaces/StorageAccess.ts @@ -1,43 +1,50 @@ -import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "../../lib/src/common/types" +import type { + FilePath, + FilePathWithPrefix, + UXDataWriteOptions, + UXFileInfo, + UXFileInfoStub, + UXFolderInfo, + UXStat, +} from "../../lib/src/common/types"; export interface StorageAccess { + deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise; - deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise + writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise + readFileAuto(path: string): Promise; + readFileText(path: string): Promise; + isExists(path: string): Promise; + writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; + appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise; - readFileAuto(path: string): Promise - readFileText(path: string): Promise - isExists(path: string): Promise - writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise - appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise - - stat(path: string): Promise - statHidden(path: string): Promise - removeHidden(path: string): Promise - readHiddenFileAuto(path: string): Promise - readHiddenFileBinary(path: string): Promise - readHiddenFileText(path: string): Promise - isExistsIncludeHidden(path: string): Promise + stat(path: string): Promise; + statHidden(path: string): Promise; + removeHidden(path: string): Promise; + readHiddenFileAuto(path: string): Promise; + readHiddenFileBinary(path: string): Promise; + readHiddenFileText(path: string): Promise; + isExistsIncludeHidden(path: string): Promise; // This could be work also for the hidden files. - ensureDir(path: string): Promise - triggerFileEvent(event: string, path: string): void - triggerHiddenFile(path: string): Promise + ensureDir(path: string): Promise; + triggerFileEvent(event: string, path: string): void; + triggerHiddenFile(path: string): Promise; - getFileStub(path: string): UXFileInfoStub | null + getFileStub(path: string): UXFileInfoStub | null; readStubContent(stub: UXFileInfoStub): Promise; - getStub(path: string): UXFileInfoStub | UXFolderInfo | null + getStub(path: string): UXFileInfoStub | UXFolderInfo | null; - getFiles(): UXFileInfoStub[] - getFileNames(): FilePathWithPrefix[] + getFiles(): UXFileInfoStub[]; + getFileNames(): FilePathWithPrefix[]; - touched(file: UXFileInfoStub | FilePathWithPrefix): void - recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean - clearTouched(): void + touched(file: UXFileInfoStub | FilePathWithPrefix): void; + recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean; + clearTouched(): void; - // -- Low-Level - delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise - trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise + // -- Low-Level + delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise; + trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise; getFilesIncludeHidden( basePath: string, @@ -45,4 +52,4 @@ export interface StorageAccess { excludeFilter?: RegExp[], skipFolder?: string[] ): Promise; -} \ No newline at end of file +} diff --git a/src/modules/main/ModuleLiveSyncMain.ts b/src/modules/main/ModuleLiveSyncMain.ts index a272c32..7040cc6 100644 --- a/src/modules/main/ModuleLiveSyncMain.ts +++ b/src/modules/main/ModuleLiveSyncMain.ts @@ -1,6 +1,13 @@ import { fireAndForget } from "octagonal-wheels/promises"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, VER, type ObsidianLiveSyncSettings } from "../../lib/src/common/types.ts"; -import { EVENT_LAYOUT_READY, EVENT_PLUGIN_LOADED, EVENT_PLUGIN_UNLOADED, EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts"; +import { + EVENT_LAYOUT_READY, + EVENT_PLUGIN_LOADED, + EVENT_PLUGIN_UNLOADED, + EVENT_REQUEST_RELOAD_SETTING_TAB, + EVENT_SETTING_SAVED, + eventHub, +} from "../../common/events.ts"; import { $f, setLang } from "../../lib/src/common/i18n.ts"; import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert.ts"; import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task"; @@ -9,9 +16,8 @@ import { AbstractModule } from "../AbstractModule.ts"; import type { ICoreModule } from "../ModuleTypes.ts"; export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule { - async $$onLiveSyncReady() { - if (!await this.core.$everyOnLayoutReady()) return; + if (!(await this.core.$everyOnLayoutReady())) return; eventHub.emitEvent(EVENT_LAYOUT_READY); if (this.settings.suspendFileWatching || this.settings.suspendParseReplicationResult) { const ANSWER_KEEP = "Keep this plug-in suspended"; @@ -29,7 +35,12 @@ Do you want to resume them and restart Obsidian? > These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended. > If you are not sure, you can try to rerun these processes. Make sure to back your vault up. `; - if (await this.core.confirm.askSelectStringDialogue(message, [ANSWER_KEEP, ANSWER_RESUME], { defaultAction: ANSWER_KEEP, title: "Scram Enabled" }) == ANSWER_RESUME) { + if ( + (await this.core.confirm.askSelectStringDialogue(message, [ANSWER_KEEP, ANSWER_RESUME], { + defaultAction: ANSWER_KEEP, + title: "Scram Enabled", + })) == ANSWER_RESUME + ) { this.settings.suspendFileWatching = false; this.settings.suspendParseReplicationResult = false; await this.saveSettings(); @@ -42,11 +53,11 @@ Do you want to resume them and restart Obsidian? //TODO:stop all sync. return false; } - if (!await this.core.$everyOnFirstInitialize()) return; + if (!(await this.core.$everyOnFirstInitialize())) return; await this.core.$$realizeSettingSyncMode(); fireAndForget(async () => { this._log(`Additional safety scan..`, LOG_LEVEL_VERBOSE); - if (!await this.core.$allScanStat()) { + if (!(await this.core.$allScanStat())) { this._log(`Additional safety scan has been failed on some module`, LOG_LEVEL_NOTICE); } else { this._log(`Additional safety scan done`, LOG_LEVEL_VERBOSE); @@ -62,7 +73,7 @@ Do you want to resume them and restart Obsidian? }); eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => { fireAndForget(() => this.core.$$realizeSettingSyncMode()); - }) + }); } async $$onLiveSyncLoad(): Promise { @@ -70,7 +81,7 @@ Do you want to resume them and restart Obsidian? // debugger; eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core); this._log("loading plugin"); - if (!await this.core.$everyOnloadStart()) { + if (!(await this.core.$everyOnloadStart())) { this._log("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE); return; } @@ -82,7 +93,7 @@ Do you want to resume them and restart Obsidian? this._log($f`Self-hosted LiveSync${" v"}${manifestVersion} ${packageVersion}`); await this.core.$$loadSettings(); - if (!await this.core.$everyOnloadAfterLoadSettings()) { + if (!(await this.core.$everyOnloadAfterLoadSettings())) { this._log("Plugin initialising has been cancelled by some module", LOG_LEVEL_NOTICE); return; } @@ -116,7 +127,7 @@ Do you want to resume them and restart Obsidian? // this.$$replicate = this.$$replicate.bind(this); this.core.$$onLiveSyncReady = this.core.$$onLiveSyncReady.bind(this); await this.core.$everyOnload(); - await Promise.all(this.core.addOns.map(e => e.onload())); + await Promise.all(this.core.addOns.map((e) => e.onload())); } async $$onLiveSyncUnload(): Promise { @@ -159,12 +170,17 @@ Do you want to resume them and restart Obsidian? isReady = false; - $$isReady(): boolean { return this.isReady; } + $$isReady(): boolean { + return this.isReady; + } - $$markIsReady(): void { this.isReady = true; } - - $$resetIsReady(): void { this.isReady = false; } + $$markIsReady(): void { + this.isReady = true; + } + $$resetIsReady(): void { + this.isReady = false; + } _suspended = false; $$isSuspended(): boolean { @@ -178,5 +194,4 @@ Do you want to resume them and restart Obsidian? $$isUnloaded(): boolean { return this._unloaded; } - -} \ No newline at end of file +} diff --git a/utils/flyio/generate_setupuri.ts b/utils/flyio/generate_setupuri.ts index 5ea8618..a6db192 100644 --- a/utils/flyio/generate_setupuri.ts +++ b/utils/flyio/generate_setupuri.ts @@ -1,42 +1,171 @@ import { encrypt } from "npm:octagonal-wheels@0.1.11/encryption/encryption.js"; -const noun = ["waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", "feather", "grass", "haze", "mountain", "night", "pond", "darkness", "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", "violet", "water", "wildflower", "wave", "water", "resonance", "sun", "log", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", "frog", "smoke", "star"]; -const adjectives = ["autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", "billowing", "broken", "cold", "damp", "falling", "frosty", "green", "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", "red", "rough", "still", "small", "sparkling", "thrumming", "shy", "wandering", "withered", "wild", "black", "young", "holy", "solitary", "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", "polished", "ancient", "purple", "lively", "nameless"]; +const noun = [ + "waterfall", + "river", + "breeze", + "moon", + "rain", + "wind", + "sea", + "morning", + "snow", + "lake", + "sunset", + "pine", + "shadow", + "leaf", + "dawn", + "glitter", + "forest", + "hill", + "cloud", + "meadow", + "sun", + "glade", + "bird", + "brook", + "butterfly", + "bush", + "dew", + "dust", + "field", + "fire", + "flower", + "firefly", + "feather", + "grass", + "haze", + "mountain", + "night", + "pond", + "darkness", + "snowflake", + "silence", + "sound", + "sky", + "shape", + "surf", + "thunder", + "violet", + "water", + "wildflower", + "wave", + "water", + "resonance", + "sun", + "log", + "dream", + "cherry", + "tree", + "fog", + "frost", + "voice", + "paper", + "frog", + "smoke", + "star", +]; +const adjectives = [ + "autumn", + "hidden", + "bitter", + "misty", + "silent", + "empty", + "dry", + "dark", + "summer", + "icy", + "delicate", + "quiet", + "white", + "cool", + "spring", + "winter", + "patient", + "twilight", + "dawn", + "crimson", + "wispy", + "weathered", + "blue", + "billowing", + "broken", + "cold", + "damp", + "falling", + "frosty", + "green", + "long", + "late", + "lingering", + "bold", + "little", + "morning", + "muddy", + "old", + "red", + "rough", + "still", + "small", + "sparkling", + "thrumming", + "shy", + "wandering", + "withered", + "wild", + "black", + "young", + "holy", + "solitary", + "fragrant", + "aged", + "snowy", + "proud", + "floral", + "restless", + "divine", + "polished", + "ancient", + "purple", + "lively", + "nameless", +]; function friendlyString() { return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${noun[Math.floor(Math.random() * noun.length)]}`; } const uri_passphrase = `${Deno.env.get("uri_passphrase") ?? friendlyString()}`; - const URIBASE = "obsidian://setuplivesync?settings="; async function main() { const conf = { - "couchDB_URI": `${Deno.env.get("hostname")}`, - "couchDB_USER": `${Deno.env.get("username")}`, - "couchDB_PASSWORD": `${Deno.env.get("password")}`, - "couchDB_DBNAME": `${Deno.env.get("database")}`, - "syncOnStart": true, - "gcDelay": 0, - "periodicReplication": true, - "syncOnFileOpen": true, - "encrypt": true, - "passphrase": `${Deno.env.get("passphrase")}`, - "usePathObfuscation": true, - "batchSave": true, - "batch_size": 50, - "batches_limit": 50, - "useHistory": true, - "disableRequestURI": true, - "customChunkSize": 50, - "syncAfterMerge": false, - "concurrencyOfReadChunksOnline": 100, - "minimumIntervalOfReadChunksOnline": 100, - } + couchDB_URI: `${Deno.env.get("hostname")}`, + couchDB_USER: `${Deno.env.get("username")}`, + couchDB_PASSWORD: `${Deno.env.get("password")}`, + couchDB_DBNAME: `${Deno.env.get("database")}`, + syncOnStart: true, + gcDelay: 0, + periodicReplication: true, + syncOnFileOpen: true, + encrypt: true, + passphrase: `${Deno.env.get("passphrase")}`, + usePathObfuscation: true, + batchSave: true, + batch_size: 50, + batches_limit: 50, + useHistory: true, + disableRequestURI: true, + customChunkSize: 50, + syncAfterMerge: false, + concurrencyOfReadChunksOnline: 100, + minimumIntervalOfReadChunksOnline: 100, + }; const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false)); const theURI = `${URIBASE}${encryptedConf}`; console.log("\nYour passphrase of Setup-URI is: ", uri_passphrase); - console.log("This passphrase is never shown again, so please note it in a safe place.") + console.log("This passphrase is never shown again, so please note it in a safe place."); console.log(theURI); } -await main(); \ No newline at end of file +await main();