chore(format): no intentional behaviour change - runs pretty

This commit is contained in:
Frank Harrison
2024-11-11 09:39:45 +00:00
parent 6e1eb36f3b
commit 5c97e5b672
71 changed files with 6029 additions and 3740 deletions
@@ -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<boolean> {
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<UXStat | null> {
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<boolean> {
return await this.vaultAccess.adapterStat(path) !== null;
return (await this.vaultAccess.adapterStat(path)) !== null;
}
async ensureDir(path: string): Promise<boolean> {
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);
}
}
}
}
@@ -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<boolean> {
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<Notice>(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);
}
}
}
+96 -53
View File
@@ -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<string> {
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<string> {
}
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<string | false> => {
export const askString = (
app: App,
title: string,
key: string,
placeholder: string,
isPassword: boolean = false
): Promise<string | false> => {
return new Promise((res) => {
const dialog = new InputStringDialog(app, title, key, placeholder, isPassword, (result) => res(result));
dialog.open();
});
};
@@ -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<T>(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<TFile> {
if (typeof (data) === "string") {
async vaultCreate(
path: string,
data: string | ArrayBuffer | Uint8Array,
options?: DataWriteOptions
): Promise<TFile> {
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 = [];
}
}
}
@@ -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<void>;
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);
}
}
}
@@ -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<UXFileInfo> {
export async function TFileToUXFileInfo(
core: LiveSyncCore,
file: TFile,
prefix?: string,
deleted?: boolean
): Promise<UXFileInfo> {
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<UXFileInfo> {
export async function InternalFileToUXFileInfo(
fullPath: string,
vaultAccess: SerializedFileAccess,
prefix: string = ICHeader
): Promise<UXFileInfo> {
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;
}
}