mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-04 00:48:47 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8bc2806e0 | ||
|
|
62f78b4028 | ||
|
|
cf9d2720ce | ||
|
|
09115dfe15 | ||
|
|
4cbb833e9d | ||
|
|
7419d0d2a1 | ||
|
|
f3e83d4045 |
20
.github/workflows/unit-ci.yml
vendored
20
.github/workflows/unit-ci.yml
vendored
@@ -3,6 +3,10 @@ name: unit-ci
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -26,8 +30,16 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install test dependencies (Playwright Chromium)
|
||||
run: npm run test:install-dependencies
|
||||
# unit tests do not require Playwright, so we can skip installing its dependencies to save time
|
||||
# - name: Install test dependencies (Playwright Chromium)
|
||||
# run: npm run test:install-dependencies
|
||||
|
||||
- name: Run unit tests suite
|
||||
run: npm run test:unit
|
||||
- name: Run unit tests suite with coverage
|
||||
run: npm run test:unit:coverage
|
||||
|
||||
- name: Upload coverage report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/**
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.47",
|
||||
"version": "0.25.49",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.47",
|
||||
"version": "0.25.49",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.47",
|
||||
"version": "0.25.49",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.47",
|
||||
"version": "0.25.49",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -257,20 +257,8 @@ export function requestToCouchDBWithCredentials(
|
||||
import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
|
||||
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
|
||||
// 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;
|
||||
// Logger(`Resolution MTime ${truncatedBaseMTime} and ${truncatedTargetMTime} `, LOG_LEVEL_VERBOSE);
|
||||
if (truncatedBaseMTime == truncatedTargetMTime) return EVEN;
|
||||
if (truncatedBaseMTime > truncatedTargetMTime) return BASE_IS_NEW;
|
||||
if (truncatedBaseMTime < truncatedTargetMTime) return TARGET_IS_NEW;
|
||||
throw new Error("Unexpected error");
|
||||
}
|
||||
|
||||
import { compareMTime } from "@lib/common/utils.ts";
|
||||
export { compareMTime };
|
||||
function getKey(file: AnyEntry | string | UXFileInfoStub) {
|
||||
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);
|
||||
return key;
|
||||
|
||||
@@ -53,9 +53,7 @@ import {
|
||||
PeriodicProcessor,
|
||||
disposeMemoObject,
|
||||
isCustomisationSyncMetadata,
|
||||
isMarkedAsSameChanges,
|
||||
isPluginMetadata,
|
||||
markChangesAreSame,
|
||||
memoIfNotExist,
|
||||
memoObject,
|
||||
retrieveMemoObject,
|
||||
@@ -1308,7 +1306,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
eden: {},
|
||||
};
|
||||
} else {
|
||||
if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) {
|
||||
if (this.services.path.isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) {
|
||||
this._log(
|
||||
`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`,
|
||||
LOG_LEVEL_DEBUG
|
||||
@@ -1328,7 +1326,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`,
|
||||
LOG_LEVEL_VERBOSE
|
||||
);
|
||||
markChangesAreSame(prefixedFileName, old.mtime, mtime + 1);
|
||||
this.services.path.markChangesAreSame(prefixedFileName, old.mtime, mtime + 1);
|
||||
return true;
|
||||
}
|
||||
saveData = {
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
|
||||
{#if selectedObj != false}
|
||||
<div class="op-scrollable json-source">
|
||||
<div class="op-scrollable json-source ls-dialog">
|
||||
{#each diffs as diff}
|
||||
<span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}
|
||||
>{diff[1]}</span
|
||||
|
||||
@@ -29,9 +29,7 @@ import {
|
||||
} from "../../lib/src/common/utils.ts";
|
||||
import {
|
||||
compareMTime,
|
||||
unmarkChanges,
|
||||
isInternalMetadata,
|
||||
markChangesAreSame,
|
||||
PeriodicProcessor,
|
||||
TARGET_IS_NEW,
|
||||
scheduleTask,
|
||||
@@ -362,13 +360,13 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
const dbMTime = getComparingMTime(db);
|
||||
const storageMTime = getComparingMTime(stat);
|
||||
if (dbMTime == 0 || storageMTime == 0) {
|
||||
unmarkChanges(path);
|
||||
this.services.path.unmarkChanges(path);
|
||||
} else {
|
||||
markChangesAreSame(path, getComparingMTime(db), getComparingMTime(stat));
|
||||
this.services.path.markChangesAreSame(path, getComparingMTime(db), getComparingMTime(stat));
|
||||
}
|
||||
}
|
||||
updateLastProcessedDeletion(path: FilePath, db: MetaEntry | LoadedEntry | false) {
|
||||
unmarkChanges(path);
|
||||
this.services.path.unmarkChanges(path);
|
||||
if (db) this.updateLastProcessedDatabase(path, db);
|
||||
this.updateLastProcessedFile(path, this.statToKey(null));
|
||||
}
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 2ed1925ca7...258d9aca11
13
src/main.ts
13
src/main.ts
@@ -22,7 +22,7 @@ import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
|
||||
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver.ts";
|
||||
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
|
||||
import { ModuleLog } from "./modules/features/ModuleLog.ts";
|
||||
import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
|
||||
// import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
|
||||
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
||||
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
||||
import { SetupManager } from "./modules/features/SetupManager.ts";
|
||||
@@ -36,7 +36,7 @@ import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidian
|
||||
import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts";
|
||||
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
|
||||
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
|
||||
import { ModuleInitializerFile } from "./modules/essential/ModuleInitializerFile.ts";
|
||||
// import { ModuleInitializerFile } from "./modules/essential/ModuleInitializerFile.ts";
|
||||
import { ModuleReplicator } from "./modules/core/ModuleReplicator.ts";
|
||||
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB.ts";
|
||||
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO.ts";
|
||||
@@ -65,6 +65,8 @@ import type { ServiceModules } from "./types.ts";
|
||||
import { useTargetFilters } from "@lib/serviceFeatures/targetFilter.ts";
|
||||
import { setNoticeClass } from "@lib/mock_and_interop/wrapper.ts";
|
||||
import { useCheckRemoteSize } from "./lib/src/serviceFeatures/checkRemoteSize.ts";
|
||||
import { useRedFlagFeatures } from "./serviceFeatures/redFlag.ts";
|
||||
import { useOfflineScanner } from "./lib/src/serviceFeatures/offlineScanner.ts";
|
||||
|
||||
export default class ObsidianLiveSyncPlugin
|
||||
extends Plugin
|
||||
@@ -165,7 +167,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
this._registerModule(new ModuleReplicator(this));
|
||||
this._registerModule(new ModuleConflictResolver(this));
|
||||
this._registerModule(new ModulePeriodicProcess(this));
|
||||
this._registerModule(new ModuleInitializerFile(this));
|
||||
// this._registerModule(new ModuleInitializerFile(this));
|
||||
this._registerModule(new ModuleObsidianEvents(this, this));
|
||||
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
|
||||
this._registerModule(new ModuleObsidianSettingsAsMarkdown(this));
|
||||
@@ -175,7 +177,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
this._registerModule(new ModuleSetupObsidian(this));
|
||||
this._registerModule(new ModuleObsidianDocumentHistory(this, this));
|
||||
this._registerModule(new ModuleMigration(this));
|
||||
this._registerModule(new ModuleRedFlag(this));
|
||||
// this._registerModule(new ModuleRedFlag(this));
|
||||
this._registerModule(new ModuleInteractiveConflictResolver(this, this));
|
||||
this._registerModule(new ModuleObsidianGlobalHistory(this, this));
|
||||
// this._registerModule(new ModuleCheckRemoteSize(this));
|
||||
@@ -336,6 +338,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
vaultService: this.services.vault,
|
||||
settingService: this.services.setting,
|
||||
APIService: this.services.API,
|
||||
pathService: this.services.path,
|
||||
});
|
||||
const storageEventManager = new StorageEventManagerObsidian(this, this, {
|
||||
fileProcessing: this.services.fileProcessing,
|
||||
@@ -413,6 +416,8 @@ export default class ObsidianLiveSyncPlugin
|
||||
const curriedFeature = () => feature(this);
|
||||
this.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
|
||||
}
|
||||
useRedFlagFeatures(this);
|
||||
useOfflineScanner(this);
|
||||
// enable target filter feature.
|
||||
useTargetFilters(this);
|
||||
useCheckRemoteSize(this);
|
||||
|
||||
137
src/managers/ObsidianStorageEventManagerAdapter.ts
Normal file
137
src/managers/ObsidianStorageEventManagerAdapter.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { TFile, TFolder } from "@/deps";
|
||||
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types";
|
||||
import type { FileEventItem } from "@lib/common/types";
|
||||
import type { IStorageEventManagerAdapter } from "@lib/managers/adapters";
|
||||
import type {
|
||||
IStorageEventTypeGuardAdapter,
|
||||
IStorageEventPersistenceAdapter,
|
||||
IStorageEventWatchAdapter,
|
||||
IStorageEventStatusAdapter,
|
||||
IStorageEventConverterAdapter,
|
||||
IStorageEventWatchHandlers,
|
||||
} from "@lib/managers/adapters";
|
||||
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import type { FileProcessingService } from "@lib/services/base/FileProcessingService";
|
||||
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
|
||||
|
||||
/**
|
||||
* Obsidian-specific type guard adapter
|
||||
*/
|
||||
class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter<TFile, TFolder> {
|
||||
isFile(file: any): file is TFile {
|
||||
if (file instanceof TFile) {
|
||||
return true;
|
||||
}
|
||||
if (file && typeof file === "object" && "isFolder" in file) {
|
||||
return !file.isFolder;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is TFolder {
|
||||
if (item instanceof TFolder) {
|
||||
return true;
|
||||
}
|
||||
if (item && typeof item === "object" && "isFolder" in item) {
|
||||
return !!item.isFolder;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsidian-specific persistence adapter
|
||||
*/
|
||||
class ObsidianPersistenceAdapter implements IStorageEventPersistenceAdapter {
|
||||
constructor(private core: LiveSyncCore) {}
|
||||
|
||||
async saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise<void> {
|
||||
await this.core.kvDB.set("storage-event-manager-snapshot", snapshot);
|
||||
}
|
||||
|
||||
async loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null> {
|
||||
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
|
||||
"storage-event-manager-snapshot"
|
||||
);
|
||||
return snapShot;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsidian-specific status adapter
|
||||
*/
|
||||
class ObsidianStatusAdapter implements IStorageEventStatusAdapter {
|
||||
constructor(private fileProcessing: FileProcessingService) {}
|
||||
|
||||
updateStatus(status: { batched: number; processing: number; totalQueued: number }): void {
|
||||
this.fileProcessing.batched.value = status.batched;
|
||||
this.fileProcessing.processing.value = status.processing;
|
||||
this.fileProcessing.totalQueued.value = status.totalQueued;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsidian-specific converter adapter
|
||||
*/
|
||||
class ObsidianConverterAdapter implements IStorageEventConverterAdapter<TFile> {
|
||||
toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub {
|
||||
return TFileToUXFileInfoStub(file, deleted);
|
||||
}
|
||||
|
||||
toInternalFileInfo(path: FilePath): UXInternalFileInfoStub {
|
||||
return InternalFileToUXFileInfoStub(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsidian-specific watch adapter
|
||||
*/
|
||||
class ObsidianWatchAdapter implements IStorageEventWatchAdapter {
|
||||
constructor(private plugin: ObsidianLiveSyncPlugin) {}
|
||||
|
||||
beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||
const plugin = this.plugin;
|
||||
|
||||
const boundHandlers = {
|
||||
onCreate: handlers.onCreate.bind(handlers),
|
||||
onChange: handlers.onChange.bind(handlers),
|
||||
onDelete: handlers.onDelete.bind(handlers),
|
||||
onRename: handlers.onRename.bind(handlers),
|
||||
onRaw: handlers.onRaw.bind(handlers),
|
||||
onEditorChange: handlers.onEditorChange?.bind(handlers),
|
||||
};
|
||||
|
||||
plugin.registerEvent(plugin.app.vault.on("create", boundHandlers.onCreate));
|
||||
plugin.registerEvent(plugin.app.vault.on("modify", boundHandlers.onChange));
|
||||
plugin.registerEvent(plugin.app.vault.on("delete", boundHandlers.onDelete));
|
||||
plugin.registerEvent(plugin.app.vault.on("rename", boundHandlers.onRename));
|
||||
//@ts-ignore : Internal API
|
||||
plugin.registerEvent(plugin.app.vault.on("raw", boundHandlers.onRaw));
|
||||
if (boundHandlers.onEditorChange) {
|
||||
plugin.registerEvent(plugin.app.workspace.on("editor-change", boundHandlers.onEditorChange));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite adapter for Obsidian StorageEventManager
|
||||
*/
|
||||
export class ObsidianStorageEventManagerAdapter implements IStorageEventManagerAdapter<TFile, TFolder> {
|
||||
readonly typeGuard: ObsidianTypeGuardAdapter;
|
||||
readonly persistence: ObsidianPersistenceAdapter;
|
||||
readonly watch: ObsidianWatchAdapter;
|
||||
readonly status: ObsidianStatusAdapter;
|
||||
readonly converter: ObsidianConverterAdapter;
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, fileProcessing: FileProcessingService) {
|
||||
this.typeGuard = new ObsidianTypeGuardAdapter();
|
||||
this.persistence = new ObsidianPersistenceAdapter(core);
|
||||
this.watch = new ObsidianWatchAdapter(plugin);
|
||||
this.status = new ObsidianStatusAdapter(fileProcessing);
|
||||
this.converter = new ObsidianConverterAdapter();
|
||||
}
|
||||
}
|
||||
@@ -1,168 +1,29 @@
|
||||
import type { FileEventItem } from "@/common/types";
|
||||
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "@lib/common/types";
|
||||
import type { FileEvent } from "@lib/interfaces/StorageEventManager";
|
||||
import { TFile, type TAbstractFile, TFolder } from "@/deps";
|
||||
import { LOG_LEVEL_DEBUG } from "octagonal-wheels/common/logger";
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import {
|
||||
StorageEventManagerBase,
|
||||
type FileEventItemSentinel,
|
||||
type StorageEventManagerBaseDependencies,
|
||||
} from "@lib/managers/StorageEventManager";
|
||||
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
|
||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
|
||||
import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter";
|
||||
|
||||
export class StorageEventManagerObsidian extends StorageEventManagerBase {
|
||||
export class StorageEventManagerObsidian extends StorageEventManagerBase<ObsidianStorageEventManagerAdapter> {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
core: LiveSyncCore;
|
||||
|
||||
// Necessary evil.
|
||||
cmdHiddenFileSync: HiddenFileSync;
|
||||
|
||||
override isFile(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFile): boolean {
|
||||
if (file instanceof TFile) {
|
||||
return true;
|
||||
}
|
||||
if (super.isFile(file)) {
|
||||
return true;
|
||||
}
|
||||
return !file.isFolder;
|
||||
}
|
||||
override isFolder(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFolder): boolean {
|
||||
if (file instanceof TFolder) {
|
||||
return true;
|
||||
}
|
||||
if (super.isFolder(file)) {
|
||||
return true;
|
||||
}
|
||||
return !!file.isFolder;
|
||||
}
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) {
|
||||
super(dependencies);
|
||||
const adapter = new ObsidianStorageEventManagerAdapter(plugin, core, dependencies.fileProcessing);
|
||||
super(adapter, dependencies);
|
||||
this.plugin = plugin;
|
||||
this.core = core;
|
||||
this.cmdHiddenFileSync = this.plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
|
||||
}
|
||||
|
||||
async beginWatch() {
|
||||
await this.snapShotRestored;
|
||||
const plugin = this.plugin;
|
||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||
this.watchEditorChange = this.watchEditorChange.bind(this);
|
||||
plugin.registerEvent(plugin.app.vault.on("modify", this.watchVaultChange));
|
||||
plugin.registerEvent(plugin.app.vault.on("delete", this.watchVaultDelete));
|
||||
plugin.registerEvent(plugin.app.vault.on("rename", this.watchVaultRename));
|
||||
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
|
||||
//@ts-ignore : Internal API
|
||||
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
|
||||
plugin.registerEvent(plugin.app.workspace.on("editor-change", this.watchEditorChange));
|
||||
}
|
||||
watchEditorChange(editor: any, info: any) {
|
||||
if (!("path" in info)) {
|
||||
return;
|
||||
}
|
||||
if (!this.shouldBatchSave) {
|
||||
return;
|
||||
}
|
||||
const file = info?.file as TFile;
|
||||
if (!file) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// this._log(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
if (!this.isWaiting(file.path as FilePath)) {
|
||||
return;
|
||||
}
|
||||
const data = info?.data as string;
|
||||
const fi: FileEvent = {
|
||||
type: "CHANGED",
|
||||
file: TFileToUXFileInfoStub(file),
|
||||
cachedData: data,
|
||||
};
|
||||
void this.appendQueue([fi]);
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
if (file instanceof TFolder) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// this._log(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const fileInfo = TFileToUXFileInfoStub(file);
|
||||
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||
if (file instanceof TFolder) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// this._log(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const fileInfo = TFileToUXFileInfoStub(file);
|
||||
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||
if (file instanceof TFolder) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// this._log(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const fileInfo = TFileToUXFileInfoStub(file, true);
|
||||
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
|
||||
}
|
||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||
// vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename').
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
// Watch raw events (Internal API)
|
||||
watchVaultRawEvents(path: FilePath) {
|
||||
if (this.storageAccess.isFileProcessing(path)) {
|
||||
// this._log(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
// Only for internal files.
|
||||
if (!this.settings) return;
|
||||
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
|
||||
if (this.settings.useIgnoreFiles) {
|
||||
// If it is one of ignore files, refresh the cached one.
|
||||
// (Calling$$isTargetFile will refresh the cache)
|
||||
void this.vaultService.isTargetFile(path).then(() => this._watchVaultRawEvents(path));
|
||||
} else {
|
||||
void this._watchVaultRawEvents(path);
|
||||
}
|
||||
}
|
||||
|
||||
async _watchVaultRawEvents(path: FilePath) {
|
||||
/**
|
||||
* Override _watchVaultRawEvents to add Obsidian-specific logic
|
||||
*/
|
||||
protected override async _watchVaultRawEvents(path: FilePath) {
|
||||
if (!this.settings.syncInternalFiles && !this.settings.usePluginSync) return;
|
||||
if (!this.settings.watchInternalFileChanges) return;
|
||||
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
|
||||
@@ -177,34 +38,11 @@ export class StorageEventManagerObsidian extends StorageEventManagerBase {
|
||||
[
|
||||
{
|
||||
type: "INTERNAL",
|
||||
file: InternalFileToUXFileInfoStub(path),
|
||||
file: this.adapter.converter.toInternalFileInfo(path),
|
||||
skipBatchWait: true, // Internal files should be processed immediately.
|
||||
},
|
||||
],
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async _saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]) {
|
||||
await this.core.kvDB.set("storage-event-manager-snapshot", snapshot);
|
||||
this._log(`Storage operation snapshot saved: ${snapshot.length} items`, LOG_LEVEL_DEBUG);
|
||||
}
|
||||
|
||||
async _loadSnapshot() {
|
||||
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
|
||||
"storage-event-manager-snapshot"
|
||||
);
|
||||
return snapShot;
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
const allFileEventItems = this.bufferedQueuedItems.filter((e): e is FileEventItem => "args" in e);
|
||||
const allItems = allFileEventItems.filter((e) => !e.cancelled);
|
||||
const totalItems = allItems.length + this.concurrentProcessing.waiting;
|
||||
const processing = this.processingCount;
|
||||
const batchedCount = this._waitingMap.size;
|
||||
this.fileProcessing.batched.value = batchedCount;
|
||||
this.fileProcessing.processing.value = processing;
|
||||
this.fileProcessing.totalQueued.value = totalItems + batchedCount + processing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { throttle } from "octagonal-wheels/function";
|
||||
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts";
|
||||
import { BASE_IS_NEW, compareFileFreshness, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
|
||||
import { BASE_IS_NEW, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
|
||||
import {
|
||||
type FilePathWithPrefixLC,
|
||||
type FilePathWithPrefix,
|
||||
@@ -308,7 +308,7 @@ export class ModuleInitializerFile extends AbstractModule {
|
||||
}
|
||||
}
|
||||
|
||||
const compareResult = compareFileFreshness(file, doc);
|
||||
const compareResult = this.services.path.compareFileFreshness(file, doc);
|
||||
switch (compareResult) {
|
||||
case BASE_IS_NEW:
|
||||
if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
||||
@@ -423,7 +423,7 @@ export class ModuleInitializerFile extends AbstractModule {
|
||||
}
|
||||
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportDetectedErrors.bind(this));
|
||||
services.databaseEvents.initialiseDatabase.setHandler(this._initializeDatabase.bind(this));
|
||||
services.vault.scanVault.setHandler(this._performFullScan.bind(this));
|
||||
services.databaseEvents.initialiseDatabase.addHandler(this._initializeDatabase.bind(this));
|
||||
services.vault.scanVault.addHandler(this._performFullScan.bind(this));
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,11 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
|
||||
{#if entry.isDeleted}
|
||||
<span class="filename" style="text-decoration: line-through">{entry.filename}</span>
|
||||
{:else}
|
||||
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -63,6 +63,7 @@ export class ConflictResolveModal extends Modal {
|
||||
contentEl.createEl("span", { text: this.filename });
|
||||
const div = contentEl.createDiv("");
|
||||
div.addClass("op-scrollable");
|
||||
div.addClass("ls-dialog");
|
||||
let diff = "";
|
||||
for (const v of this.result.diff) {
|
||||
const x1 = v[0];
|
||||
@@ -86,6 +87,7 @@ export class ConflictResolveModal extends Modal {
|
||||
}
|
||||
|
||||
const div2 = contentEl.createDiv("");
|
||||
div2.addClass("ls-dialog");
|
||||
const date1 =
|
||||
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
||||
const date2 =
|
||||
|
||||
@@ -1,7 +1,40 @@
|
||||
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
|
||||
import { normalizePath } from "@/deps";
|
||||
import { PathService } from "@/lib/src/services/base/PathService";
|
||||
|
||||
import {
|
||||
type BASE_IS_NEW,
|
||||
type TARGET_IS_NEW,
|
||||
type EVEN,
|
||||
markChangesAreSame,
|
||||
unmarkChanges,
|
||||
compareFileFreshness,
|
||||
isMarkedAsSameChanges,
|
||||
} from "@/common/utils";
|
||||
import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@/lib/src/common/types";
|
||||
export class ObsidianPathService extends PathService<ObsidianServiceContext> {
|
||||
override markChangesAreSame(
|
||||
old: UXFileInfo | AnyEntry | FilePathWithPrefix,
|
||||
newMtime: number,
|
||||
oldMtime: number
|
||||
): boolean | undefined {
|
||||
return markChangesAreSame(old, newMtime, oldMtime);
|
||||
}
|
||||
override unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void {
|
||||
return unmarkChanges(file);
|
||||
}
|
||||
override compareFileFreshness(
|
||||
baseFile: UXFileInfoStub | AnyEntry | undefined,
|
||||
checkTarget: UXFileInfo | AnyEntry | undefined
|
||||
): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
|
||||
return compareFileFreshness(baseFile, checkTarget);
|
||||
}
|
||||
override isMarkedAsSameChanges(
|
||||
file: UXFileInfoStub | AnyEntry | FilePathWithPrefix,
|
||||
mtimes: number[]
|
||||
): undefined | typeof EVEN {
|
||||
return isMarkedAsSameChanges(file, mtimes);
|
||||
}
|
||||
protected normalizePath(path: string): string {
|
||||
return normalizePath(path);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getPathFromTFile } from "@/common/utils";
|
||||
import { getPathFromTFile, isValidPath } from "@/common/utils";
|
||||
import { InjectableVaultService } from "@/lib/src/services/implements/injectable/InjectableVaultService";
|
||||
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
|
||||
import type { FilePath } from "@/lib/src/common/types";
|
||||
@@ -30,4 +30,7 @@ export class ObsidianVaultService extends InjectableVaultService<ObsidianService
|
||||
if (this.isStorageInsensitive()) return false;
|
||||
return super.shouldCheckCaseInsensitively(); // Check the setting
|
||||
}
|
||||
override isValidPath(path: string): boolean {
|
||||
return isValidPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
387
src/serviceFeatures/redFlag.ts
Normal file
387
src/serviceFeatures/redFlag.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||
import { createInstanceLogFunction, type LogFunction } from "@lib/services/lib/logUtils";
|
||||
import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const";
|
||||
import FetchEverything from "../modules/features/SetupWizard/dialogs/FetchEverything.svelte";
|
||||
import RebuildEverything from "../modules/features/SetupWizard/dialogs/RebuildEverything.svelte";
|
||||
import { extractObject } from "octagonal-wheels/object";
|
||||
import { REMOTE_MINIO } from "@lib/common/models/setting.const";
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
|
||||
import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition";
|
||||
|
||||
/**
|
||||
* Flag file handler interface, similar to target filter pattern.
|
||||
*/
|
||||
interface FlagFileHandler {
|
||||
priority: number;
|
||||
check: () => Promise<boolean>;
|
||||
handle: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export async function isFlagFileExist(host: NecessaryServices<never, "storageAccess">, path: string) {
|
||||
const redFlagExist = await host.serviceModules.storageAccess.isExists(
|
||||
host.serviceModules.storageAccess.normalisePath(path)
|
||||
);
|
||||
if (redFlagExist) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deleteFlagFile(host: NecessaryServices<never, "storageAccess">, log: LogFunction, path: string) {
|
||||
try {
|
||||
const isFlagged = await host.serviceModules.storageAccess.isExists(
|
||||
host.serviceModules.storageAccess.normalisePath(path)
|
||||
);
|
||||
if (isFlagged) {
|
||||
await host.serviceModules.storageAccess.delete(path, true);
|
||||
}
|
||||
} catch (ex) {
|
||||
log(`Could not delete ${path}`);
|
||||
log(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Factory function to create a fetch all flag handler.
|
||||
* All logic related to fetch all flag is encapsulated here.
|
||||
*/
|
||||
export function createFetchAllFlagHandler(
|
||||
host: NecessaryServices<
|
||||
"vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle",
|
||||
"storageAccess" | "rebuilder"
|
||||
>,
|
||||
log: LogFunction
|
||||
): FlagFileHandler {
|
||||
// Check if fetch all flag is active
|
||||
const isFlagActive = async () =>
|
||||
(await isFlagFileExist(host, FlagFilesOriginal.FETCH_ALL)) ||
|
||||
(await isFlagFileExist(host, FlagFilesHumanReadable.FETCH_ALL));
|
||||
|
||||
// Cleanup fetch all flag files
|
||||
const cleanupFlag = async () => {
|
||||
await deleteFlagFile(host, log, FlagFilesOriginal.FETCH_ALL);
|
||||
await deleteFlagFile(host, log, FlagFilesHumanReadable.FETCH_ALL);
|
||||
};
|
||||
|
||||
// Handle the fetch all scheduled operation
|
||||
const onScheduled = async () => {
|
||||
const method = await host.services.UI.dialogManager.openWithExplicitCancel(FetchEverything);
|
||||
if (method === "cancelled") {
|
||||
log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
await cleanupFlag();
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
}
|
||||
const { vault, extra } = method;
|
||||
const settings = await host.services.setting.currentSettings();
|
||||
// If remote is MinIO, makeLocalChunkBeforeSync is not available. (because no-deduplication on sending).
|
||||
const makeLocalChunkBeforeSyncAvailable = settings.remoteType !== REMOTE_MINIO;
|
||||
const mapVaultStateToAction = {
|
||||
identical: {
|
||||
makeLocalChunkBeforeSync: makeLocalChunkBeforeSyncAvailable,
|
||||
makeLocalFilesBeforeSync: false,
|
||||
},
|
||||
independent: {
|
||||
makeLocalChunkBeforeSync: false,
|
||||
makeLocalFilesBeforeSync: false,
|
||||
},
|
||||
unbalanced: {
|
||||
makeLocalChunkBeforeSync: false,
|
||||
makeLocalFilesBeforeSync: true,
|
||||
},
|
||||
cancelled: {
|
||||
makeLocalChunkBeforeSync: false,
|
||||
makeLocalFilesBeforeSync: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
return await processVaultInitialisation(host, log, async () => {
|
||||
const settings = host.services.setting.currentSettings();
|
||||
await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
|
||||
const vaultStateToAction = mapVaultStateToAction[vault];
|
||||
const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = vaultStateToAction;
|
||||
log(
|
||||
`Fetching everything with settings: makeLocalChunkBeforeSync=${makeLocalChunkBeforeSync}, makeLocalFilesBeforeSync=${makeLocalFilesBeforeSync}`,
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
await host.serviceModules.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync);
|
||||
await cleanupFlag();
|
||||
log("Fetch everything operation completed. Vault files will be gradually synced.", LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
priority: 10,
|
||||
check: () => isFlagActive(),
|
||||
handle: async () => {
|
||||
const res = await onScheduled();
|
||||
if (res) {
|
||||
return await verifyAndUnlockSuspension(host, log);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust setting to remote configuration.
|
||||
* @param config current configuration to retrieve remote preferred config
|
||||
* @returns updated configuration if applied, otherwise null.
|
||||
*/
|
||||
export async function adjustSettingToRemote(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
|
||||
log: LogFunction,
|
||||
config: ObsidianLiveSyncSettings
|
||||
) {
|
||||
// Fetch remote configuration unless prevented.
|
||||
const SKIP_FETCH = "Skip and proceed";
|
||||
const RETRY_FETCH = "Retry (recommended)";
|
||||
let canProceed = false;
|
||||
do {
|
||||
const remoteTweaks = await host.services.tweakValue.fetchRemotePreferred(config);
|
||||
if (!remoteTweaks) {
|
||||
const choice = await host.services.UI.confirm.askSelectStringDialogue(
|
||||
"Could not fetch configuration from remote. If you are new to the Self-hosted LiveSync, this might be expected. If not, you should check your network or server settings.",
|
||||
[SKIP_FETCH, RETRY_FETCH] as const,
|
||||
{
|
||||
defaultAction: RETRY_FETCH,
|
||||
timeout: 0,
|
||||
title: "Fetch Remote Configuration Failed",
|
||||
}
|
||||
);
|
||||
if (choice === SKIP_FETCH) {
|
||||
canProceed = true;
|
||||
}
|
||||
} else {
|
||||
const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
|
||||
// Check if any necessary tweak value is different from current config.
|
||||
const differentItems = Object.entries(necessary).filter(([key, value]) => {
|
||||
return (config as any)[key] !== value;
|
||||
});
|
||||
if (differentItems.length === 0) {
|
||||
log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
await host.services.UI.confirm.askSelectStringDialogue(
|
||||
"Your settings differed slightly from the server's. The plug-in has supplemented the incompatible parts with the server settings!",
|
||||
["OK"] as const,
|
||||
{
|
||||
defaultAction: "OK",
|
||||
timeout: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
config = {
|
||||
...config,
|
||||
...Object.fromEntries(differentItems),
|
||||
} satisfies ObsidianLiveSyncSettings;
|
||||
await host.services.setting.applyPartial(config, true);
|
||||
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||
canProceed = true;
|
||||
const updatedConfig = host.services.setting.currentSettings();
|
||||
return updatedConfig;
|
||||
}
|
||||
} while (!canProceed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust setting to remote if needed.
|
||||
* @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything)
|
||||
* @param config current configuration to retrieve remote preferred config
|
||||
*/
|
||||
export async function adjustSettingToRemoteIfNeeded(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
|
||||
log: LogFunction,
|
||||
extra: { preventFetchingConfig: boolean },
|
||||
config: ObsidianLiveSyncSettings
|
||||
) {
|
||||
if (extra && extra.preventFetchingConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remote configuration fetched and applied.
|
||||
if (await adjustSettingToRemote(host, log, config)) {
|
||||
config = host.services.setting.currentSettings();
|
||||
} else {
|
||||
log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
log(JSON.stringify(config), LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process vault initialisation with suspending file watching and sync.
|
||||
* @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process.
|
||||
* @param keepSuspending whether to keep suspending file watching after the process.
|
||||
* @returns result of the process, or false if error occurs.
|
||||
*/
|
||||
export async function processVaultInitialisation(
|
||||
host: NecessaryServices<"setting", any>,
|
||||
log: LogFunction,
|
||||
proc: () => Promise<boolean>,
|
||||
keepSuspending = false
|
||||
) {
|
||||
try {
|
||||
// Disable batch saving and file watching during initialisation.
|
||||
await host.services.setting.applyPartial({ batchSave: false }, false);
|
||||
await host.services.setting.suspendAllSync();
|
||||
await host.services.setting.suspendExtraSync();
|
||||
await host.services.setting.applyPartial({ suspendFileWatching: true }, true);
|
||||
try {
|
||||
const result = await proc();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
log("Error during vault initialisation process.", LOG_LEVEL_NOTICE);
|
||||
log(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
} catch (ex) {
|
||||
log("Error during vault initialisation.", LOG_LEVEL_NOTICE);
|
||||
log(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
} finally {
|
||||
if (!keepSuspending) {
|
||||
// Re-enable file watching after initialisation.
|
||||
await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyAndUnlockSuspension(
|
||||
host: NecessaryServices<"setting" | "appLifecycle" | "UI", any>,
|
||||
log: LogFunction
|
||||
) {
|
||||
if (!host.services.setting.currentSettings().suspendFileWatching) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
(await host.services.UI.confirm.askYesNoDialog(
|
||||
"Do you want to resume file and database processing, and restart obsidian now?",
|
||||
{ defaultOption: "Yes", timeout: 15 }
|
||||
)) != "yes"
|
||||
) {
|
||||
// TODO: Confirm actually proceed to next process.
|
||||
return true;
|
||||
}
|
||||
await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a rebuild flag handler.
|
||||
* All logic related to rebuild flag is encapsulated here.
|
||||
*/
|
||||
export function createRebuildFlagHandler(
|
||||
host: NecessaryServices<"setting" | "appLifecycle" | "UI" | "tweakValue", "storageAccess" | "rebuilder">,
|
||||
log: LogFunction
|
||||
) {
|
||||
// Check if rebuild flag is active
|
||||
const isFlagActive = async () =>
|
||||
(await isFlagFileExist(host, FlagFilesOriginal.REBUILD_ALL)) ||
|
||||
(await isFlagFileExist(host, FlagFilesHumanReadable.REBUILD_ALL));
|
||||
|
||||
// Cleanup rebuild flag files
|
||||
const cleanupFlag = async () => {
|
||||
await deleteFlagFile(host, log, FlagFilesOriginal.REBUILD_ALL);
|
||||
await deleteFlagFile(host, log, FlagFilesHumanReadable.REBUILD_ALL);
|
||||
};
|
||||
|
||||
// Handle the rebuild everything scheduled operation
|
||||
const onScheduled = async () => {
|
||||
const method = await host.services.UI.dialogManager.openWithExplicitCancel(RebuildEverything);
|
||||
if (method === "cancelled") {
|
||||
log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
await cleanupFlag();
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
}
|
||||
const { extra } = method;
|
||||
const settings = host.services.setting.currentSettings();
|
||||
await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
|
||||
return await processVaultInitialisation(host, log, async () => {
|
||||
await host.serviceModules.rebuilder.$rebuildEverything();
|
||||
await cleanupFlag();
|
||||
log("Rebuild everything operation completed.", LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
priority: 20,
|
||||
check: () => isFlagActive(),
|
||||
handle: async () => {
|
||||
const res = await onScheduled();
|
||||
if (res) {
|
||||
return await verifyAndUnlockSuspension(host, log);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a suspend all flag handler.
|
||||
* All logic related to suspend flag is encapsulated here.
|
||||
*/
|
||||
export function createSuspendFlagHandler(
|
||||
host: NecessaryServices<"setting", "storageAccess">,
|
||||
log: LogFunction
|
||||
): FlagFileHandler {
|
||||
// Check if suspend flag is active
|
||||
const isFlagActive = async () => await isFlagFileExist(host, FlagFilesOriginal.SUSPEND_ALL);
|
||||
|
||||
// Handle the suspend all scheduled operation
|
||||
const onScheduled = async () => {
|
||||
log("SCRAM is detected. All operations are suspended.", LOG_LEVEL_NOTICE);
|
||||
return await processVaultInitialisation(
|
||||
host,
|
||||
log,
|
||||
async () => {
|
||||
log(
|
||||
"All operations are suspended as per SCRAM.\nLogs will be written to the file. This might be a performance impact.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
await host.services.setting.applyPartial({ writeLogToTheFile: true }, true);
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
priority: 5,
|
||||
check: () => isFlagActive(),
|
||||
handle: () => onScheduled(),
|
||||
};
|
||||
}
|
||||
|
||||
export function flagHandlerToEventHandler(flagHandler: FlagFileHandler) {
|
||||
return async () => {
|
||||
if (await flagHandler.check()) {
|
||||
return await flagHandler.handle();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function useRedFlagFeatures(
|
||||
host: NecessaryServices<
|
||||
"API" | "appLifecycle" | "UI" | "setting" | "tweakValue" | "fileProcessing" | "vault",
|
||||
"storageAccess" | "rebuilder"
|
||||
>
|
||||
) {
|
||||
const log = createInstanceLogFunction("SF:RedFlag", host.services.API);
|
||||
const handlerFetch = createFetchAllFlagHandler(host, log);
|
||||
const handlerRebuild = createRebuildFlagHandler(host, log);
|
||||
const handlerSuspend = createSuspendFlagHandler(host, log);
|
||||
host.services.appLifecycle.onLayoutReady.addHandler(flagHandlerToEventHandler(handlerFetch), handlerFetch.priority);
|
||||
host.services.appLifecycle.onLayoutReady.addHandler(
|
||||
flagHandlerToEventHandler(handlerRebuild),
|
||||
handlerRebuild.priority
|
||||
);
|
||||
host.services.appLifecycle.onLayoutReady.addHandler(
|
||||
flagHandlerToEventHandler(handlerSuspend),
|
||||
handlerSuspend.priority
|
||||
);
|
||||
}
|
||||
1140
src/serviceFeatures/redFlag.unit.spec.ts
Normal file
1140
src/serviceFeatures/redFlag.unit.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,8 @@
|
||||
import { markChangesAreSame } from "@/common/utils";
|
||||
import type { AnyEntry } from "@lib/common/types";
|
||||
|
||||
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts";
|
||||
import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase";
|
||||
|
||||
// markChangesAreSame uses persistent data implicitly, we should refactor it too.
|
||||
// For now, to make the refactoring done once, we just use them directly.
|
||||
// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB).
|
||||
// TODO: REFACTOR
|
||||
export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {
|
||||
markChangesAreSame(old: AnyEntry, newMtime: number, oldMtime: number): void {
|
||||
markChangesAreSame(old, newMtime, oldMtime);
|
||||
}
|
||||
}
|
||||
// Refactored, now migrating...
|
||||
export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {}
|
||||
|
||||
@@ -1,160 +1,14 @@
|
||||
import { markChangesAreSame } from "@/common/utils";
|
||||
import type { FilePath, UXDataWriteOptions, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
|
||||
import { TFolder, type TAbstractFile, TFile, type Stat, type App, type DataWriteOptions, normalizePath } from "@/deps";
|
||||
import { FileAccessBase, toArrayBuffer, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts";
|
||||
import { TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Vault {
|
||||
getAbstractFileByPathInsensitive(path: string): TAbstractFile | null;
|
||||
}
|
||||
interface DataAdapter {
|
||||
reconcileInternalFile?(path: string): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileAccessObsidian extends FileAccessBase<TAbstractFile, TFile, TFolder, Stat> {
|
||||
app: App;
|
||||
|
||||
override getPath(file: string | TAbstractFile): FilePath {
|
||||
return (typeof file === "string" ? file : file.path) as FilePath;
|
||||
}
|
||||
|
||||
override isFile(file: TAbstractFile | null): file is TFile {
|
||||
return file instanceof TFile;
|
||||
}
|
||||
override isFolder(file: TAbstractFile | null): file is TFolder {
|
||||
return file instanceof TFolder;
|
||||
}
|
||||
override _statFromNative(file: TFile): Promise<TFile["stat"]> {
|
||||
return Promise.resolve(file.stat);
|
||||
}
|
||||
|
||||
override nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub {
|
||||
return TFileToUXFileInfoStub(file);
|
||||
}
|
||||
override nativeFolderToUXFolder(folder: TFolder): UXFolderInfo {
|
||||
if (folder instanceof TFolder) {
|
||||
return this.nativeFolderToUXFolder(folder);
|
||||
} else {
|
||||
throw new Error(`Not a folder: ${(folder as TAbstractFile)?.name}`);
|
||||
}
|
||||
}
|
||||
import { type App } from "@/deps";
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts";
|
||||
import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* Obsidian-specific implementation of FileAccessBase
|
||||
* Uses ObsidianFileSystemAdapter for platform-specific operations
|
||||
*/
|
||||
export class FileAccessObsidian extends FileAccessBase<ObsidianFileSystemAdapter> {
|
||||
constructor(app: App, dependencies: FileAccessBaseDependencies) {
|
||||
super({
|
||||
storageAccessManager: dependencies.storageAccessManager,
|
||||
vaultService: dependencies.vaultService,
|
||||
settingService: dependencies.settingService,
|
||||
APIService: dependencies.APIService,
|
||||
});
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
protected override _normalisePath(path: string): string {
|
||||
return normalizePath(path);
|
||||
}
|
||||
|
||||
protected async _adapterMkdir(path: string) {
|
||||
await this.app.vault.adapter.mkdir(path);
|
||||
}
|
||||
protected _getAbstractFileByPath(path: FilePath) {
|
||||
return this.app.vault.getAbstractFileByPath(path);
|
||||
}
|
||||
protected _getAbstractFileByPathInsensitive(path: FilePath) {
|
||||
return this.app.vault.getAbstractFileByPathInsensitive(path);
|
||||
}
|
||||
|
||||
protected async _tryAdapterStat(path: FilePath) {
|
||||
if (!(await this.app.vault.adapter.exists(path))) return null;
|
||||
return await this.app.vault.adapter.stat(path);
|
||||
}
|
||||
|
||||
protected async _adapterStat(path: FilePath) {
|
||||
return await this.app.vault.adapter.stat(path);
|
||||
}
|
||||
|
||||
protected async _adapterExists(path: FilePath) {
|
||||
return await this.app.vault.adapter.exists(path);
|
||||
}
|
||||
protected async _adapterRemove(path: FilePath) {
|
||||
await this.app.vault.adapter.remove(path);
|
||||
}
|
||||
|
||||
protected async _adapterRead(path: FilePath) {
|
||||
return await this.app.vault.adapter.read(path);
|
||||
}
|
||||
|
||||
protected async _adapterReadBinary(path: FilePath) {
|
||||
return await this.app.vault.adapter.readBinary(path);
|
||||
}
|
||||
|
||||
_adapterWrite(file: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
return this.app.vault.adapter.write(file, data, options);
|
||||
}
|
||||
_adapterWriteBinary(file: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||
return this.app.vault.adapter.writeBinary(file, toArrayBuffer(data), options);
|
||||
}
|
||||
|
||||
protected _adapterList(basePath: string): Promise<{ files: string[]; folders: string[] }> {
|
||||
return Promise.resolve(this.app.vault.adapter.list(basePath));
|
||||
}
|
||||
|
||||
async _vaultCacheRead(file: TFile) {
|
||||
return await this.app.vault.cachedRead(file);
|
||||
}
|
||||
|
||||
protected async _vaultRead(file: TFile): Promise<string> {
|
||||
return await this.app.vault.read(file);
|
||||
}
|
||||
|
||||
protected async _vaultReadBinary(file: TFile): Promise<ArrayBuffer> {
|
||||
return await this.app.vault.readBinary(file);
|
||||
}
|
||||
|
||||
protected override markChangesAreSame(path: string, mtime: number, newMtime: number) {
|
||||
return markChangesAreSame(path, mtime, newMtime);
|
||||
}
|
||||
|
||||
protected override async _vaultModify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
return await this.app.vault.modify(file, data, options);
|
||||
}
|
||||
protected override async _vaultModifyBinary(
|
||||
file: TFile,
|
||||
data: ArrayBuffer,
|
||||
options?: UXDataWriteOptions
|
||||
): Promise<void> {
|
||||
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
|
||||
}
|
||||
protected override async _vaultCreate(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
|
||||
return await this.app.vault.create(path, data, options);
|
||||
}
|
||||
protected override async _vaultCreateBinary(
|
||||
path: string,
|
||||
data: ArrayBuffer,
|
||||
options?: UXDataWriteOptions
|
||||
): Promise<TFile> {
|
||||
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
|
||||
}
|
||||
|
||||
protected override _trigger(name: string, ...data: any[]) {
|
||||
return this.app.vault.trigger(name, ...data);
|
||||
}
|
||||
protected override async _reconcileInternalFile(path: string) {
|
||||
return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path));
|
||||
}
|
||||
protected override async _adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) {
|
||||
return await this.app.vault.adapter.append(normalizedPath, data, options);
|
||||
}
|
||||
protected override async _delete(file: TFile | TFolder, force = false) {
|
||||
return await this.app.vault.delete(file, force);
|
||||
}
|
||||
protected override async _trash(file: TFile | TFolder, force = false) {
|
||||
return await this.app.vault.trash(file, force);
|
||||
}
|
||||
|
||||
protected override _getFiles() {
|
||||
return this.app.vault.getFiles();
|
||||
const adapter = new ObsidianFileSystemAdapter(app);
|
||||
super(adapter, dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,7 @@
|
||||
import {
|
||||
compareFileFreshness,
|
||||
markChangesAreSame,
|
||||
type BASE_IS_NEW,
|
||||
type EVEN,
|
||||
type TARGET_IS_NEW,
|
||||
} from "@/common/utils";
|
||||
import type { AnyEntry } from "@lib/common/models/db.type";
|
||||
import type { UXFileInfo, UXFileInfoStub } from "@lib/common/models/fileaccess.type";
|
||||
import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase";
|
||||
|
||||
// markChangesAreSame uses persistent data implicitly, we should refactor it too.
|
||||
// also, compareFileFreshness depends on marked changes, so we should refactor it as well. For now, to make the refactoring done once, we just use them directly.
|
||||
// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB).
|
||||
// TODO: REFACTOR
|
||||
export class ServiceFileHandler extends ServiceFileHandlerBase {
|
||||
override markChangesAreSame(old: UXFileInfo | AnyEntry, newMtime: number, oldMtime: number) {
|
||||
return markChangesAreSame(old, newMtime, oldMtime);
|
||||
}
|
||||
override compareFileFreshness(
|
||||
baseFile: UXFileInfoStub | AnyEntry | undefined,
|
||||
checkTarget: UXFileInfo | AnyEntry | undefined
|
||||
): typeof TARGET_IS_NEW | typeof BASE_IS_NEW | typeof EVEN {
|
||||
return compareFileFreshness(baseFile, checkTarget);
|
||||
}
|
||||
}
|
||||
// Refactored: markChangesAreSame, unmarkChanges, compareFileFreshness, isMarkedAsSameChanges are now moved to PathService
|
||||
export class ServiceFileHandler extends ServiceFileHandlerBase {}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@/lib/src/common/types";
|
||||
import type { IConversionAdapter } from "@/lib/src/serviceModules/adapters";
|
||||
import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
|
||||
import type { TFile, TFolder } from "obsidian";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for Obsidian
|
||||
*/
|
||||
|
||||
export class ObsidianConversionAdapter implements IConversionAdapter<TFile, TFolder> {
|
||||
nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub {
|
||||
return TFileToUXFileInfoStub(file);
|
||||
}
|
||||
|
||||
nativeFolderToUXFolder(folder: TFolder): UXFolderInfo {
|
||||
return TFolderToUXFileInfoStub(folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { FilePath, UXStat } from "@/lib/src/common/types";
|
||||
import type {
|
||||
IFileSystemAdapter,
|
||||
IPathAdapter,
|
||||
ITypeGuardAdapter,
|
||||
IConversionAdapter,
|
||||
IStorageAdapter,
|
||||
IVaultAdapter,
|
||||
} from "@/lib/src/serviceModules/adapters";
|
||||
import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian";
|
||||
import { ObsidianConversionAdapter } from "./ObsidianConversionAdapter";
|
||||
import { ObsidianPathAdapter } from "./ObsidianPathAdapter";
|
||||
import { ObsidianStorageAdapter } from "./ObsidianStorageAdapter";
|
||||
import { ObsidianTypeGuardAdapter } from "./ObsidianTypeGuardAdapter";
|
||||
import { ObsidianVaultAdapter } from "./ObsidianVaultAdapter";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Vault {
|
||||
getAbstractFileByPathInsensitive(path: string): TAbstractFile | null;
|
||||
}
|
||||
interface DataAdapter {
|
||||
reconcileInternalFile?(path: string): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for Obsidian
|
||||
*/
|
||||
|
||||
export class ObsidianFileSystemAdapter implements IFileSystemAdapter<TAbstractFile, TFile, TFolder, Stat> {
|
||||
readonly path: IPathAdapter<TAbstractFile>;
|
||||
readonly typeGuard: ITypeGuardAdapter<TFile, TFolder>;
|
||||
readonly conversion: IConversionAdapter<TFile, TFolder>;
|
||||
readonly storage: IStorageAdapter<Stat>;
|
||||
readonly vault: IVaultAdapter<TFile>;
|
||||
|
||||
constructor(private app: App) {
|
||||
this.path = new ObsidianPathAdapter();
|
||||
this.typeGuard = new ObsidianTypeGuardAdapter();
|
||||
this.conversion = new ObsidianConversionAdapter();
|
||||
this.storage = new ObsidianStorageAdapter(app);
|
||||
this.vault = new ObsidianVaultAdapter(app);
|
||||
}
|
||||
|
||||
getAbstractFileByPath(path: FilePath | string): TAbstractFile | null {
|
||||
return this.app.vault.getAbstractFileByPath(path);
|
||||
}
|
||||
|
||||
getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null {
|
||||
return this.app.vault.getAbstractFileByPathInsensitive(path);
|
||||
}
|
||||
|
||||
getFiles(): TFile[] {
|
||||
return this.app.vault.getFiles();
|
||||
}
|
||||
|
||||
statFromNative(file: TFile): Promise<UXStat> {
|
||||
return Promise.resolve({ ...file.stat, type: "file" });
|
||||
}
|
||||
|
||||
async reconcileInternalFile(path: string): Promise<void> {
|
||||
return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path));
|
||||
}
|
||||
}
|
||||
16
src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts
Normal file
16
src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { type TAbstractFile, normalizePath } from "@/deps";
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Obsidian
|
||||
*/
|
||||
export class ObsidianPathAdapter implements IPathAdapter<TAbstractFile> {
|
||||
getPath(file: string | TAbstractFile): FilePath {
|
||||
return (typeof file === "string" ? file : file.path) as FilePath;
|
||||
}
|
||||
|
||||
normalisePath(path: string): string {
|
||||
return normalizePath(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { UXDataWriteOptions } from "@/lib/src/common/types";
|
||||
import type { IStorageAdapter } from "@/lib/src/serviceModules/adapters";
|
||||
import { toArrayBuffer } from "@/lib/src/serviceModules/FileAccessBase";
|
||||
import type { Stat, App } from "obsidian";
|
||||
|
||||
/**
|
||||
* Storage adapter implementation for Obsidian
|
||||
*/
|
||||
|
||||
export class ObsidianStorageAdapter implements IStorageAdapter<Stat> {
|
||||
constructor(private app: App) {}
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
return await this.app.vault.adapter.exists(path);
|
||||
}
|
||||
|
||||
async trystat(path: string): Promise<Stat | null> {
|
||||
if (!(await this.app.vault.adapter.exists(path))) return null;
|
||||
return await this.app.vault.adapter.stat(path);
|
||||
}
|
||||
|
||||
async stat(path: string): Promise<Stat | null> {
|
||||
return await this.app.vault.adapter.stat(path);
|
||||
}
|
||||
|
||||
async mkdir(path: string): Promise<void> {
|
||||
await this.app.vault.adapter.mkdir(path);
|
||||
}
|
||||
|
||||
async remove(path: string): Promise<void> {
|
||||
await this.app.vault.adapter.remove(path);
|
||||
}
|
||||
|
||||
async read(path: string): Promise<string> {
|
||||
return await this.app.vault.adapter.read(path);
|
||||
}
|
||||
|
||||
async readBinary(path: string): Promise<ArrayBuffer> {
|
||||
return await this.app.vault.adapter.readBinary(path);
|
||||
}
|
||||
|
||||
async write(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
return await this.app.vault.adapter.write(path, data, options);
|
||||
}
|
||||
|
||||
async writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||
return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options);
|
||||
}
|
||||
|
||||
async append(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
return await this.app.vault.adapter.append(path, data, options);
|
||||
}
|
||||
|
||||
list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
|
||||
return Promise.resolve(this.app.vault.adapter.list(basePath));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ITypeGuardAdapter } from "@/lib/src/serviceModules/adapters";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
|
||||
/**
|
||||
* Type guard adapter implementation for Obsidian
|
||||
*/
|
||||
|
||||
export class ObsidianTypeGuardAdapter implements ITypeGuardAdapter<TFile, TFolder> {
|
||||
isFile(file: any): file is TFile {
|
||||
return file instanceof TFile;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is TFolder {
|
||||
return item instanceof TFolder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { UXDataWriteOptions } from "@/lib/src/common/types";
|
||||
import type { IVaultAdapter } from "@/lib/src/serviceModules/adapters";
|
||||
import { toArrayBuffer } from "@/lib/src/serviceModules/FileAccessBase";
|
||||
import type { TFile, App, TFolder } from "obsidian";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for Obsidian
|
||||
*/
|
||||
export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
||||
constructor(private app: App) {}
|
||||
|
||||
async read(file: TFile): Promise<string> {
|
||||
return await this.app.vault.read(file);
|
||||
}
|
||||
|
||||
async cachedRead(file: TFile): Promise<string> {
|
||||
return await this.app.vault.cachedRead(file);
|
||||
}
|
||||
|
||||
async readBinary(file: TFile): Promise<ArrayBuffer> {
|
||||
return await this.app.vault.readBinary(file);
|
||||
}
|
||||
|
||||
async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
return await this.app.vault.modify(file, data, options);
|
||||
}
|
||||
|
||||
async modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
|
||||
}
|
||||
|
||||
async create(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
|
||||
return await this.app.vault.create(path, data, options);
|
||||
}
|
||||
|
||||
async createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<TFile> {
|
||||
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
|
||||
}
|
||||
|
||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||
return await this.app.vault.delete(file, force);
|
||||
}
|
||||
|
||||
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
||||
return await this.app.vault.trash(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
return this.app.vault.trigger(name, ...data);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { TAbstractFile, TFile, TFolder, Stat } from "@/deps";
|
||||
|
||||
import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||
import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter";
|
||||
|
||||
// For typechecking purpose
|
||||
export class ServiceFileAccessObsidian extends ServiceFileAccessBase<TAbstractFile, TFile, TFolder, Stat> {}
|
||||
// For now, this is just a re-export of ServiceFileAccess with the Obsidian-specific adapter type.
|
||||
export class ServiceFileAccessObsidian extends ServiceFileAccessBase<ObsidianFileSystemAdapter> {}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.added {
|
||||
.ls-dialog .added {
|
||||
color: var(--text-on-accent);
|
||||
background-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.normal {
|
||||
.ls-dialog .normal {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.deleted {
|
||||
.ls-dialog .deleted {
|
||||
color: var(--text-on-accent);
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
|
||||
95
updates.md
95
updates.md
@@ -3,6 +3,34 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.49
|
||||
|
||||
3rd March, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- No longer deleted files are not clickable in the Global History pane.
|
||||
- Diff view now uses more specific classes (#803).
|
||||
- A message of configuration mismatching slightly added for better understanding.
|
||||
- Now it says `When replication is initiated manually via the command palette or ribbon, a dialogue box will open to address this.` to make it clear that the user can fix the issue by themselves.
|
||||
|
||||
### Refactored
|
||||
|
||||
- `ModuleRedFlag` has been refactored to `serviceFeatures/redFlag` and also tested.
|
||||
- `ModuleInitializerFile` has been refactored to `lib/serviceFeatures/offlineScanner` and also tested.
|
||||
|
||||
## 0.25.48
|
||||
|
||||
2nd March, 2026
|
||||
|
||||
No behavioural changes except unidentified faults. Please report if you find any unexpected behaviour after this update.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Many storage-related functions have been refactored for better maintainability and testability.
|
||||
- Now all platform-specific logics are supplied as adapters, and the core logic has become platform-agnostic.
|
||||
- Quite a number of tests have been added for the core logic, and the platform-specific logics are also tested with mocked adapters.
|
||||
|
||||
## 0.25.47
|
||||
|
||||
27th February, 2026
|
||||
@@ -205,72 +233,5 @@ However, this is not a minor refactoring, so please be careful. Let me know if y
|
||||
|
||||
- Fixed an issue where indexedDB would not close correctly on some environments, causing unexpected errors during database operations.
|
||||
|
||||
## 0.25.37
|
||||
|
||||
15th January, 2026
|
||||
|
||||
Thank you for your patience until my return!
|
||||
|
||||
This release contains minor changes discovered and fixed during test implementation.
|
||||
There are no changes affecting usage.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Logging system has been slightly refactored to improve maintainability.
|
||||
- Some import statements have been unified.
|
||||
|
||||
## 0.25.36
|
||||
|
||||
25th December, 2025
|
||||
|
||||
### Improved
|
||||
|
||||
- Now the garbage collector (V3) has been implemented. (Beta)
|
||||
- This garbage collector ensures that all devices are synchronised to the latest progress to prevent inconsistencies.
|
||||
- In other words, it makes sure that no new conflicts would have arisen.
|
||||
- This feature requires additional information (via node information), but it should be more reliable.
|
||||
- This feature requires all devices have v0.25.36 or later.
|
||||
- After the garbage collector runs, the database size may be reduced (Compaction will be run automatically after GC).
|
||||
- We should have an administrative privilege on the remote database to run this garbage collector.
|
||||
- Now the plug-in and device information is stored in the remote database.
|
||||
- This information is used for the garbage collector (V3).
|
||||
- Some additional features may be added in the future using this information.
|
||||
|
||||
## 0.25.35
|
||||
|
||||
24th December, 2025
|
||||
|
||||
Sorry for a small release! I would like to keep things moving along like this if possible. After all, the holidays seem to be starting soon. I will be doubled by my business until the 27th though, indeed.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764).
|
||||
|
||||
## 0.25.34
|
||||
|
||||
10th December, 2025
|
||||
|
||||
### Behaviour change
|
||||
|
||||
- The plug-in automatically fetches the missing chunks even if `Fetch chunks on demand` is disabled.
|
||||
- This change is to avoid loss of data when receiving a bulk of revisions.
|
||||
- This can be prevented by enabling `Use Only Local Chunks` in the settings.
|
||||
- Storage application now saved during each event and restored on startup.
|
||||
- Synchronisation result application is also now saved during each event and restored on startup.
|
||||
- These may avoid some unexpected loss of data when the editor crashes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the plug-in waits for the application of pended batch changes before the synchronisation starts.
|
||||
- This may avoid some unexpected loss or unexpected conflicts.
|
||||
Plug-in sends custom headers correctly when RequestAPI is used.
|
||||
- No longer causing unexpected chunk creation during `Reset synchronisation on This Device` with bucket sync.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Synchronisation result application process has been refactored.
|
||||
- Storage application process has been refactored.
|
||||
- Please report if you find any unexpected behaviour after this update. A bit of large refactoring.
|
||||
|
||||
Full notes are in
|
||||
[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
|
||||
@@ -3,6 +3,59 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.48
|
||||
|
||||
2nd March, 2026
|
||||
|
||||
No behavioural changes except unidentified faults. Please report if you find any unexpected behaviour after this update.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Many storage-related functions have been refactored for better maintainability and testability.
|
||||
- Now all platform-specific logics are supplied as adapters, and the core logic has become platform-agnostic.
|
||||
- Quite a number of tests have been added for the core logic, and the platform-specific logics are also tested with mocked adapters.
|
||||
|
||||
## 0.25.47
|
||||
|
||||
27th February, 2026
|
||||
|
||||
Phew, the financial year is still not over yet, but I have got some time to work on the plug-in again!
|
||||
|
||||
### Fixed and refactored
|
||||
|
||||
- Fixed the inexplicable behaviour when retrieving chunks from the network.
|
||||
- The chunk manager has been layered to be responsible for its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`, and `ArrivalWaitLayer`.
|
||||
- All layers have been tested now!
|
||||
- `LayeredChunkManager` has been implemented to manage these layers. Also tested.
|
||||
- `EntryManager` has been mostly rewritten and also tested.
|
||||
|
||||
- Now we can configure `Never warn` for remote storage size notification again.
|
||||
|
||||
### Tests
|
||||
|
||||
- The following test has been added:
|
||||
- `ConflictManager`.
|
||||
|
||||
## 0.25.46
|
||||
|
||||
26th February, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unexpected errors no longer occurred when the plug-in was unloaded.
|
||||
- Hidden File Sync now respects selectors.
|
||||
- Registering protocol-handlers now works safely without causing unexpected errors.
|
||||
|
||||
### Refactored
|
||||
|
||||
- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and tests have also been added.
|
||||
- Some unnecessary things have been removed.
|
||||
- LiveSyncManagers has now explicit dependencies.
|
||||
- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies.
|
||||
- This is to avoid circular dependencies and clarify the ownership of the managers.
|
||||
- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly.
|
||||
- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage.
|
||||
|
||||
## 0.25.45
|
||||
|
||||
25th February, 2026
|
||||
|
||||
@@ -26,7 +26,7 @@ export default mergeConfig(
|
||||
...importOnlyFiles,
|
||||
],
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
reporter: ["text", "json", "html", ["text", { file: "coverage-text.txt" }]],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user