mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-11 12:28:49 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dfd42259d | ||
|
|
9cf630320c | ||
|
|
584adc9296 | ||
|
|
f7dba6854f | ||
|
|
1d83e0ee31 | ||
|
|
d0244bd6d0 | ||
|
|
79bb5e1c77 | ||
|
|
3403712e24 | ||
|
|
8faa19629b | ||
|
|
7ff9c666ce | ||
|
|
d8bc2806e0 | ||
|
|
62f78b4028 | ||
|
|
cf9d2720ce | ||
|
|
09115dfe15 | ||
|
|
4cbb833e9d | ||
|
|
7419d0d2a1 | ||
|
|
f3e83d4045 | ||
|
|
28e06a21e4 | ||
|
|
e08fbbd223 | ||
|
|
a1e331d452 | ||
|
|
646f8af680 | ||
|
|
392f76fd36 |
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/**
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,4 +27,5 @@ data.json
|
||||
|
||||
cov_profile/**
|
||||
|
||||
coverage
|
||||
coverage
|
||||
src/apps/cli/dist/*
|
||||
8
devs.md
8
devs.md
@@ -52,6 +52,7 @@ Hence, the new feature should be implemented as follows:
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
npm run test:unit # Run unit tests with vitest (or `npm run test:unit:coverage` for coverage)
|
||||
npm run check # TypeScript and svelte type checking
|
||||
npm run dev # Development build with auto-rebuild (uses .env for test vault paths)
|
||||
npm run build # Production build
|
||||
@@ -67,8 +68,11 @@ npm test # Run vitest tests (requires Docker services)
|
||||
|
||||
### Testing Infrastructure
|
||||
|
||||
- **Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)
|
||||
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright
|
||||
- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~
|
||||
- This is now obsolete, migrated to vitest.
|
||||
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests.
|
||||
- Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`).
|
||||
|
||||
- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services:
|
||||
```bash
|
||||
npm run test:docker-all:start # Start all test services
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.45",
|
||||
"version": "0.25.52",
|
||||
"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",
|
||||
|
||||
2639
package-lock.json
generated
2639
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.45",
|
||||
"version": "0.25.52",
|
||||
"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",
|
||||
|
||||
287
src/LiveSyncBaseCore.ts
Normal file
287
src/LiveSyncBaseCore.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
|
||||
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
|
||||
import type { Confirm } from "./lib/src/interfaces/Confirm";
|
||||
import type { DatabaseFileAccess } from "./lib/src/interfaces/DatabaseFileAccess";
|
||||
import type { Rebuilder } from "./lib/src/interfaces/DatabaseRebuilder";
|
||||
import type { IFileHandler } from "./lib/src/interfaces/FileHandler";
|
||||
import type { StorageAccess } from "./lib/src/interfaces/StorageAccess";
|
||||
import type { LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB";
|
||||
import type { LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes";
|
||||
import type { LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicatorEnv";
|
||||
import type { LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { useCheckRemoteSize } from "./lib/src/serviceFeatures/checkRemoteSize";
|
||||
import { useOfflineScanner } from "./lib/src/serviceFeatures/offlineScanner";
|
||||
import { useTargetFilters } from "./lib/src/serviceFeatures/targetFilter";
|
||||
import type { ServiceContext } from "./lib/src/services/base/ServiceBase";
|
||||
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices";
|
||||
import { AbstractModule } from "./modules/AbstractModule";
|
||||
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
|
||||
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
|
||||
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB";
|
||||
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO";
|
||||
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker";
|
||||
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver";
|
||||
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks";
|
||||
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain";
|
||||
import type { ServiceModules } from "./lib/src/interfaces/ServiceModule";
|
||||
import { useRedFlagFeatures } from "./serviceFeatures/redFlag";
|
||||
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
|
||||
|
||||
export class LiveSyncBaseCore<
|
||||
T extends ServiceContext = ServiceContext,
|
||||
TCommands extends IMinimumLiveSyncCommands = IMinimumLiveSyncCommands,
|
||||
>
|
||||
implements
|
||||
LiveSyncLocalDBEnv,
|
||||
LiveSyncReplicatorEnv,
|
||||
LiveSyncJournalReplicatorEnv,
|
||||
LiveSyncCouchDBReplicatorEnv,
|
||||
HasSettings<ObsidianLiveSyncSettings>
|
||||
{
|
||||
addOns = [] as TCommands[];
|
||||
|
||||
/**
|
||||
* register an add-onn to the plug-in.
|
||||
* Add-ons are features that are not essential to the core functionality of the plugin,
|
||||
* @param addOn
|
||||
*/
|
||||
private _registerAddOn(addOn: TCommands) {
|
||||
this.addOns.push(addOn);
|
||||
this.services.appLifecycle.onUnload.addHandler(() => Promise.resolve(addOn.onunload()).then(() => true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an add-on by its class name. Returns undefined if not found.
|
||||
* @param cls
|
||||
* @returns
|
||||
*/
|
||||
getAddOn<T extends TCommands>(cls: string) {
|
||||
for (const addon of this.addOns) {
|
||||
if (addon.constructor.name == cls) return addon as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(
|
||||
serviceHub: InjectableServiceHub<T>,
|
||||
serviceModuleInitialiser: (
|
||||
core: LiveSyncBaseCore<T, TCommands>,
|
||||
serviceHub: InjectableServiceHub<T>
|
||||
) => ServiceModules,
|
||||
extraModuleInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => AbstractModule[],
|
||||
addOnsInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => TCommands[],
|
||||
featuresInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => void
|
||||
) {
|
||||
this._services = serviceHub;
|
||||
this._serviceModules = serviceModuleInitialiser(this, serviceHub);
|
||||
const extraModules = extraModuleInitialiser(this);
|
||||
this.registerModules(extraModules);
|
||||
this.initialiseServiceFeatures();
|
||||
featuresInitialiser(this);
|
||||
const addOns = addOnsInitialiser(this);
|
||||
for (const addOn of addOns) {
|
||||
this._registerAddOn(addOn);
|
||||
}
|
||||
this.bindModuleFunctions();
|
||||
}
|
||||
/**
|
||||
* The service hub for managing all services.
|
||||
*/
|
||||
_services: InjectableServiceHub<T> | undefined = undefined;
|
||||
|
||||
get services() {
|
||||
if (!this._services) {
|
||||
throw new Error("Services not initialised yet");
|
||||
}
|
||||
return this._services;
|
||||
}
|
||||
/**
|
||||
* Service Modules
|
||||
*/
|
||||
protected _serviceModules: ServiceModules;
|
||||
|
||||
get serviceModules() {
|
||||
return this._serviceModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
|
||||
*/
|
||||
private modules = [
|
||||
// Move to registerModules
|
||||
] as AbstractModule[];
|
||||
|
||||
/**
|
||||
* Get a module by its class. Throws an error if not found.
|
||||
* Mostly used for getting SetupManager.
|
||||
* @param constructor
|
||||
* @returns
|
||||
*/
|
||||
getModule<T extends AbstractModule>(constructor: new (...args: any[]) => T): T {
|
||||
for (const module of this.modules) {
|
||||
if (module.constructor === constructor) return module as T;
|
||||
}
|
||||
throw new Error(`Module ${constructor} not found or not loaded.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module to the plug-in.
|
||||
* @param module The module to register.
|
||||
*/
|
||||
private _registerModule(module: AbstractModule) {
|
||||
this.modules.push(module);
|
||||
}
|
||||
|
||||
public registerModules(extraModules: AbstractModule[] = []) {
|
||||
this._registerModule(new ModuleLiveSyncMain(this));
|
||||
this._registerModule(new ModuleConflictChecker(this));
|
||||
this._registerModule(new ModuleReplicatorMinIO(this));
|
||||
this._registerModule(new ModuleReplicatorCouchDB(this));
|
||||
this._registerModule(new ModuleReplicator(this));
|
||||
this._registerModule(new ModuleConflictResolver(this));
|
||||
this._registerModule(new ModulePeriodicProcess(this));
|
||||
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
|
||||
this._registerModule(new ModuleBasicMenu(this));
|
||||
|
||||
for (const module of extraModules) {
|
||||
this._registerModule(module);
|
||||
}
|
||||
// Test and Dev Modules
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind module functions to services.
|
||||
*/
|
||||
public bindModuleFunctions() {
|
||||
for (const module of this.modules) {
|
||||
if (module instanceof AbstractModule) {
|
||||
module.onBindFunction(this, this.services);
|
||||
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
|
||||
} else {
|
||||
this.services.API.addLog(
|
||||
`Module ${(module as any)?.constructor?.name ?? "unknown"} does not have onBindFunction, skipping binding.`,
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user.
|
||||
*/
|
||||
get confirm(): Confirm {
|
||||
return this.services.UI.confirm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in.
|
||||
*/
|
||||
get settings() {
|
||||
return this.services.setting.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.setting.settings instead. Set the settings of the plug-in.
|
||||
*/
|
||||
set settings(value: ObsidianLiveSyncSettings) {
|
||||
this.services.setting.settings = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in.
|
||||
* @returns The current settings of the plug-in.
|
||||
*/
|
||||
getSettings(): ObsidianLiveSyncSettings {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.localDatabase instead. The local database instance.
|
||||
*/
|
||||
get localDatabase() {
|
||||
return this.services.database.localDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database.
|
||||
* @returns The PouchDB database instance.
|
||||
*/
|
||||
getDatabase(): PouchDB.Database<EntryDoc> {
|
||||
return this.localDatabase.localDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc.
|
||||
*/
|
||||
get simpleStore() {
|
||||
return this.services.keyValueDB.simpleStore as SimpleStore<CheckPointInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time.
|
||||
*/
|
||||
get replicator() {
|
||||
return this.services.replicator.getActiveReplicator()!;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc.
|
||||
*/
|
||||
get kvDB() {
|
||||
return this.services.keyValueDB.kvDB;
|
||||
}
|
||||
|
||||
/// Modules which were relied on services
|
||||
/**
|
||||
* Storage Accessor for handling file operations.
|
||||
* @obsolete Use serviceModules.storageAccess instead.
|
||||
*/
|
||||
get storageAccess(): StorageAccess {
|
||||
return this.serviceModules.storageAccess;
|
||||
}
|
||||
/**
|
||||
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
|
||||
* @obsolete Use serviceModules.databaseFileAccess instead.
|
||||
*/
|
||||
get databaseFileAccess(): DatabaseFileAccess {
|
||||
return this.serviceModules.databaseFileAccess;
|
||||
}
|
||||
/**
|
||||
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
|
||||
* @obsolete Use serviceModules.fileHandler instead.
|
||||
*/
|
||||
get fileHandler(): IFileHandler {
|
||||
return this.serviceModules.fileHandler;
|
||||
}
|
||||
/**
|
||||
* Rebuilder for handling database rebuilding operations.
|
||||
* @obsolete Use serviceModules.rebuilder instead.
|
||||
*/
|
||||
get rebuilder(): Rebuilder {
|
||||
return this.serviceModules.rebuilder;
|
||||
}
|
||||
|
||||
// private initialiseServices<T extends ServiceContext>(serviceHub: InjectableServiceHub<T>) {
|
||||
// this._services = serviceHub;
|
||||
// }
|
||||
/**
|
||||
* Initialise ServiceFeatures.
|
||||
* (Please refer `serviceFeatures` for more details)
|
||||
*/
|
||||
initialiseServiceFeatures() {
|
||||
useRedFlagFeatures(this);
|
||||
useOfflineScanner(this);
|
||||
|
||||
// enable target filter feature.
|
||||
useTargetFilters(this);
|
||||
useCheckRemoteSize(this);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IMinimumLiveSyncCommands {
|
||||
onunload(): void;
|
||||
onload(): void | Promise<void>;
|
||||
constructor: { name: string };
|
||||
}
|
||||
4
src/apps/webapp/.gitignore
vendored
Normal file
4
src/apps/webapp/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.log
|
||||
34
src/apps/webapp/adapters/FSAPIConversionAdapter.ts
Normal file
34
src/apps/webapp/adapters/FSAPIConversionAdapter.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "../../../lib/src/common/types";
|
||||
import type { IConversionAdapter } from "../../../lib/src/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSAPIFolder> {
|
||||
nativeFileToUXFileInfoStub(file: FSAPIFile): UXFileInfoStub {
|
||||
const pathParts = file.path.split("/");
|
||||
const name = pathParts[pathParts.length - 1] || file.handle.name;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
path: file.path,
|
||||
stat: file.stat,
|
||||
isFolder: false,
|
||||
};
|
||||
}
|
||||
|
||||
nativeFolderToUXFolder(folder: FSAPIFolder): UXFolderInfo {
|
||||
const pathParts = folder.path.split("/");
|
||||
const name = pathParts[pathParts.length - 1] || folder.handle.name;
|
||||
const parentPath = pathParts.slice(0, -1).join("/");
|
||||
|
||||
return {
|
||||
name: name,
|
||||
path: folder.path,
|
||||
isFolder: true,
|
||||
children: [],
|
||||
parent: parentPath as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
214
src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts
Normal file
214
src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { FilePath, UXStat } from "../../../lib/src/common/types";
|
||||
import type { IFileSystemAdapter } from "../../../lib/src/serviceModules/adapters";
|
||||
import { FSAPIPathAdapter } from "./FSAPIPathAdapter";
|
||||
import { FSAPITypeGuardAdapter } from "./FSAPITypeGuardAdapter";
|
||||
import { FSAPIConversionAdapter } from "./FSAPIConversionAdapter";
|
||||
import { FSAPIStorageAdapter } from "./FSAPIStorageAdapter";
|
||||
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
|
||||
import type { FSAPIFile, FSAPIFolder, FSAPIStat } from "./FSAPITypes";
|
||||
import { shareRunningResult } from "octagonal-wheels/concurrency/lock_v2";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSAPIFile, FSAPIFolder, FSAPIStat> {
|
||||
readonly path: FSAPIPathAdapter;
|
||||
readonly typeGuard: FSAPITypeGuardAdapter;
|
||||
readonly conversion: FSAPIConversionAdapter;
|
||||
readonly storage: FSAPIStorageAdapter;
|
||||
readonly vault: FSAPIVaultAdapter;
|
||||
|
||||
private fileCache = new Map<string, FSAPIFile>();
|
||||
private handleCache = new Map<string, FileSystemFileHandle>();
|
||||
|
||||
constructor(private rootHandle: FileSystemDirectoryHandle) {
|
||||
this.path = new FSAPIPathAdapter();
|
||||
this.typeGuard = new FSAPITypeGuardAdapter();
|
||||
this.conversion = new FSAPIConversionAdapter();
|
||||
this.storage = new FSAPIStorageAdapter(rootHandle);
|
||||
this.vault = new FSAPIVaultAdapter(rootHandle);
|
||||
}
|
||||
|
||||
private normalisePath(path: FilePath | string): string {
|
||||
return this.path.normalisePath(path as string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file handle for a given path
|
||||
*/
|
||||
private async getFileHandleByPath(p: FilePath | string): Promise<FileSystemFileHandle | null> {
|
||||
const pathStr = p as string;
|
||||
|
||||
// Check cache first
|
||||
const cached = this.handleCache.get(pathStr);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const parts = pathStr.split("/").filter((part) => part !== "");
|
||||
if (parts.length === 0) return null;
|
||||
|
||||
let currentHandle: FileSystemDirectoryHandle = this.rootHandle;
|
||||
const fileName = parts[parts.length - 1];
|
||||
|
||||
// Navigate to the parent directory
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
|
||||
}
|
||||
|
||||
const fileHandle = await currentHandle.getFileHandle(fileName);
|
||||
this.handleCache.set(pathStr, fileHandle);
|
||||
return fileHandle;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getAbstractFileByPath(p: FilePath | string): Promise<FSAPIFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
|
||||
const cached = this.fileCache.get(pathStr);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return await this.refreshFile(pathStr);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async getAbstractFileByPathInsensitive(p: FilePath | string): Promise<FSAPIFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
const exact = await this.getAbstractFileByPath(pathStr);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
// TODO: Refactor: Very, Very heavy.
|
||||
|
||||
const lowerPath = pathStr.toLowerCase();
|
||||
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
|
||||
if (cachedPath.toLowerCase() === lowerPath) {
|
||||
return cachedFile;
|
||||
}
|
||||
}
|
||||
|
||||
await this.scanDirectory();
|
||||
|
||||
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
|
||||
if (cachedPath.toLowerCase() === lowerPath) {
|
||||
return cachedFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFiles(): Promise<FSAPIFile[]> {
|
||||
if (this.fileCache.size === 0) {
|
||||
await this.scanDirectory();
|
||||
}
|
||||
return Array.from(this.fileCache.values());
|
||||
}
|
||||
|
||||
async statFromNative(file: FSAPIFile): Promise<UXStat> {
|
||||
// Refresh stat from the file handle
|
||||
try {
|
||||
const fileObject = await file.handle.getFile();
|
||||
return {
|
||||
size: fileObject.size,
|
||||
mtime: fileObject.lastModified,
|
||||
ctime: fileObject.lastModified,
|
||||
type: "file",
|
||||
};
|
||||
} catch {
|
||||
return file.stat;
|
||||
}
|
||||
}
|
||||
|
||||
async reconcileInternalFile(p: string): Promise<void> {
|
||||
// No-op in webapp version
|
||||
// This is used by Obsidian to sync internal file metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh file cache for a specific path
|
||||
*/
|
||||
async refreshFile(p: string): Promise<FSAPIFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
const handle = await this.getFileHandleByPath(pathStr);
|
||||
if (!handle) {
|
||||
this.fileCache.delete(pathStr);
|
||||
this.handleCache.delete(pathStr);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileObject = await handle.getFile();
|
||||
const file: FSAPIFile = {
|
||||
path: pathStr as FilePath,
|
||||
stat: {
|
||||
size: fileObject.size,
|
||||
mtime: fileObject.lastModified,
|
||||
ctime: fileObject.lastModified,
|
||||
type: "file",
|
||||
},
|
||||
handle: handle,
|
||||
};
|
||||
|
||||
this.fileCache.set(pathStr, file);
|
||||
this.handleCache.set(pathStr, handle);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to recursively scan directory and populate file cache
|
||||
*/
|
||||
async scanDirectory(relativePath: string = ""): Promise<void> {
|
||||
return shareRunningResult("scanDirectory:" + relativePath, async () => {
|
||||
try {
|
||||
const parts = relativePath.split("/").filter((part) => part !== "");
|
||||
let currentHandle = this.rootHandle;
|
||||
|
||||
for (const part of parts) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(part);
|
||||
}
|
||||
|
||||
// Use AsyncIterator instead of .values() for better compatibility
|
||||
for await (const [name, entry] of (currentHandle as any).entries()) {
|
||||
const entryPath = relativePath ? `${relativePath}/${name}` : name;
|
||||
|
||||
if (entry.kind === "directory") {
|
||||
// Recursively scan subdirectories
|
||||
await this.scanDirectory(entryPath);
|
||||
} else if (entry.kind === "file") {
|
||||
const fileHandle = entry as FileSystemFileHandle;
|
||||
const fileObject = await fileHandle.getFile();
|
||||
|
||||
const file: FSAPIFile = {
|
||||
path: entryPath as FilePath,
|
||||
stat: {
|
||||
size: fileObject.size,
|
||||
mtime: fileObject.lastModified,
|
||||
ctime: fileObject.lastModified,
|
||||
type: "file",
|
||||
},
|
||||
handle: fileHandle,
|
||||
};
|
||||
|
||||
this.fileCache.set(entryPath, file);
|
||||
this.handleCache.set(entryPath, fileHandle);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning directory ${relativePath}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.fileCache.clear();
|
||||
this.handleCache.clear();
|
||||
}
|
||||
}
|
||||
18
src/apps/webapp/adapters/FSAPIPathAdapter.ts
Normal file
18
src/apps/webapp/adapters/FSAPIPathAdapter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { FilePath } from "../../../lib/src/common/types";
|
||||
import type { IPathAdapter } from "../../../lib/src/serviceModules/adapters";
|
||||
import type { FSAPIFile } from "./FSAPITypes";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPIPathAdapter implements IPathAdapter<FSAPIFile> {
|
||||
getPath(file: string | FSAPIFile): FilePath {
|
||||
return (typeof file === "string" ? file : file.path) as FilePath;
|
||||
}
|
||||
|
||||
normalisePath(p: string): string {
|
||||
// Normalize path separators to forward slashes (like Obsidian)
|
||||
// Remove leading/trailing slashes
|
||||
return p.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
}
|
||||
210
src/apps/webapp/adapters/FSAPIStorageAdapter.ts
Normal file
210
src/apps/webapp/adapters/FSAPIStorageAdapter.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { UXDataWriteOptions } from "../../../lib/src/common/types";
|
||||
import type { IStorageAdapter } from "../../../lib/src/serviceModules/adapters";
|
||||
import type { FSAPIStat } from "./FSAPITypes";
|
||||
|
||||
/**
|
||||
* Storage adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
|
||||
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
||||
|
||||
/**
|
||||
* Resolve a path to directory and file handles
|
||||
*/
|
||||
private async resolvePath(p: string): Promise<{
|
||||
dirHandle: FileSystemDirectoryHandle;
|
||||
fileName: string;
|
||||
} | null> {
|
||||
try {
|
||||
const parts = p.split("/").filter((part) => part !== "");
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentHandle = this.rootHandle;
|
||||
const fileName = parts[parts.length - 1];
|
||||
|
||||
// Navigate to the parent directory
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
|
||||
}
|
||||
|
||||
return { dirHandle: currentHandle, fileName };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file handle for a given path
|
||||
*/
|
||||
private async getFileHandle(p: string): Promise<FileSystemFileHandle | null> {
|
||||
const resolved = await this.resolvePath(p);
|
||||
if (!resolved) return null;
|
||||
|
||||
try {
|
||||
return await resolved.dirHandle.getFileHandle(resolved.fileName);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get directory handle for a given path
|
||||
*/
|
||||
private async getDirectoryHandle(p: string): Promise<FileSystemDirectoryHandle | null> {
|
||||
try {
|
||||
const parts = p.split("/").filter((part) => part !== "");
|
||||
if (parts.length === 0) {
|
||||
return this.rootHandle;
|
||||
}
|
||||
|
||||
let currentHandle = this.rootHandle;
|
||||
for (const part of parts) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(part);
|
||||
}
|
||||
|
||||
return currentHandle;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(p: string): Promise<boolean> {
|
||||
const fileHandle = await this.getFileHandle(p);
|
||||
if (fileHandle) return true;
|
||||
|
||||
const dirHandle = await this.getDirectoryHandle(p);
|
||||
return dirHandle !== null;
|
||||
}
|
||||
|
||||
async trystat(p: string): Promise<FSAPIStat | null> {
|
||||
// Try as file first
|
||||
const fileHandle = await this.getFileHandle(p);
|
||||
if (fileHandle) {
|
||||
const file = await fileHandle.getFile();
|
||||
return {
|
||||
size: file.size,
|
||||
mtime: file.lastModified,
|
||||
ctime: file.lastModified,
|
||||
type: "file",
|
||||
};
|
||||
}
|
||||
|
||||
// Try as directory
|
||||
const dirHandle = await this.getDirectoryHandle(p);
|
||||
if (dirHandle) {
|
||||
return {
|
||||
size: 0,
|
||||
mtime: Date.now(),
|
||||
ctime: Date.now(),
|
||||
type: "folder",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async stat(p: string): Promise<FSAPIStat | null> {
|
||||
return await this.trystat(p);
|
||||
}
|
||||
|
||||
async mkdir(p: string): Promise<void> {
|
||||
const parts = p.split("/").filter((part) => part !== "");
|
||||
let currentHandle = this.rootHandle;
|
||||
|
||||
for (const part of parts) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(part, { create: true });
|
||||
}
|
||||
}
|
||||
|
||||
async remove(p: string): Promise<void> {
|
||||
const resolved = await this.resolvePath(p);
|
||||
if (!resolved) return;
|
||||
|
||||
await resolved.dirHandle.removeEntry(resolved.fileName, { recursive: true });
|
||||
}
|
||||
|
||||
async read(p: string): Promise<string> {
|
||||
const fileHandle = await this.getFileHandle(p);
|
||||
if (!fileHandle) {
|
||||
throw new Error(`File not found: ${p}`);
|
||||
}
|
||||
|
||||
const file = await fileHandle.getFile();
|
||||
return await file.text();
|
||||
}
|
||||
|
||||
async readBinary(p: string): Promise<ArrayBuffer> {
|
||||
const fileHandle = await this.getFileHandle(p);
|
||||
if (!fileHandle) {
|
||||
throw new Error(`File not found: ${p}`);
|
||||
}
|
||||
|
||||
const file = await fileHandle.getFile();
|
||||
return await file.arrayBuffer();
|
||||
}
|
||||
|
||||
async write(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
const resolved = await this.resolvePath(p);
|
||||
if (!resolved) {
|
||||
throw new Error(`Invalid path: ${p}`);
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
await this.mkdir(p.split("/").slice(0, -1).join("/"));
|
||||
|
||||
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async writeBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||
const resolved = await this.resolvePath(p);
|
||||
if (!resolved) {
|
||||
throw new Error(`Invalid path: ${p}`);
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
await this.mkdir(p.split("/").slice(0, -1).join("/"));
|
||||
|
||||
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async append(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
const existing = await this.exists(p);
|
||||
if (existing) {
|
||||
const currentContent = await this.read(p);
|
||||
await this.write(p, currentContent + data, options);
|
||||
} else {
|
||||
await this.write(p, data, options);
|
||||
}
|
||||
}
|
||||
|
||||
async list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
|
||||
const dirHandle = await this.getDirectoryHandle(basePath);
|
||||
if (!dirHandle) {
|
||||
return { files: [], folders: [] };
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
const folders: string[] = [];
|
||||
|
||||
// Use AsyncIterator instead of .values() for better compatibility
|
||||
for await (const [name, entry] of (dirHandle as any).entries()) {
|
||||
const entryPath = basePath ? `${basePath}/${name}` : name;
|
||||
|
||||
if (entry.kind === "directory") {
|
||||
folders.push(entryPath);
|
||||
} else if (entry.kind === "file") {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return { files, folders };
|
||||
}
|
||||
}
|
||||
17
src/apps/webapp/adapters/FSAPITypeGuardAdapter.ts
Normal file
17
src/apps/webapp/adapters/FSAPITypeGuardAdapter.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ITypeGuardAdapter } from "../../../lib/src/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
|
||||
/**
|
||||
* Type guard adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||
isFile(file: any): file is FSAPIFile {
|
||||
return (
|
||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
||||
);
|
||||
}
|
||||
|
||||
isFolder(item: any): item is FSAPIFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
||||
}
|
||||
}
|
||||
24
src/apps/webapp/adapters/FSAPITypes.ts
Normal file
24
src/apps/webapp/adapters/FSAPITypes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { FilePath, UXStat } from "../../../lib/src/common/types";
|
||||
|
||||
/**
|
||||
* FileSystem API file representation
|
||||
*/
|
||||
export type FSAPIFile = {
|
||||
path: FilePath;
|
||||
stat: UXStat;
|
||||
handle: FileSystemFileHandle;
|
||||
};
|
||||
|
||||
/**
|
||||
* FileSystem API folder representation
|
||||
*/
|
||||
export type FSAPIFolder = {
|
||||
path: FilePath;
|
||||
isFolder: true;
|
||||
handle: FileSystemDirectoryHandle;
|
||||
};
|
||||
|
||||
/**
|
||||
* FileSystem API stat type (compatible with UXStat)
|
||||
*/
|
||||
export type FSAPIStat = UXStat;
|
||||
123
src/apps/webapp/adapters/FSAPIVaultAdapter.ts
Normal file
123
src/apps/webapp/adapters/FSAPIVaultAdapter.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { FilePath, UXDataWriteOptions } from "../../../lib/src/common/types";
|
||||
import type { IVaultAdapter } from "../../../lib/src/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
|
||||
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
||||
|
||||
async read(file: FSAPIFile): Promise<string> {
|
||||
const fileObject = await file.handle.getFile();
|
||||
return await fileObject.text();
|
||||
}
|
||||
|
||||
async cachedRead(file: FSAPIFile): Promise<string> {
|
||||
// No caching in webapp version, just read directly
|
||||
return await this.read(file);
|
||||
}
|
||||
|
||||
async readBinary(file: FSAPIFile): Promise<ArrayBuffer> {
|
||||
const fileObject = await file.handle.getFile();
|
||||
return await fileObject.arrayBuffer();
|
||||
}
|
||||
|
||||
async modify(file: FSAPIFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
const writable = await file.handle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async modifyBinary(file: FSAPIFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||
const writable = await file.handle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
async create(p: string, data: string, options?: UXDataWriteOptions): Promise<FSAPIFile> {
|
||||
const parts = p.split("/").filter((part) => part !== "");
|
||||
const fileName = parts[parts.length - 1];
|
||||
|
||||
// Navigate to parent directory, creating as needed
|
||||
let currentHandle = this.rootHandle;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(parts[i], { create: true });
|
||||
}
|
||||
|
||||
// Create the file
|
||||
const fileHandle = await currentHandle.getFileHandle(fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
|
||||
// Get file metadata
|
||||
const fileObject = await fileHandle.getFile();
|
||||
|
||||
return {
|
||||
path: p as FilePath,
|
||||
stat: {
|
||||
size: fileObject.size,
|
||||
mtime: fileObject.lastModified,
|
||||
ctime: fileObject.lastModified,
|
||||
type: "file",
|
||||
},
|
||||
handle: fileHandle,
|
||||
};
|
||||
}
|
||||
|
||||
async createBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<FSAPIFile> {
|
||||
const parts = p.split("/").filter((part) => part !== "");
|
||||
const fileName = parts[parts.length - 1];
|
||||
|
||||
// Navigate to parent directory, creating as needed
|
||||
let currentHandle = this.rootHandle;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(parts[i], { create: true });
|
||||
}
|
||||
|
||||
// Create the file
|
||||
const fileHandle = await currentHandle.getFileHandle(fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
|
||||
// Get file metadata
|
||||
const fileObject = await fileHandle.getFile();
|
||||
|
||||
return {
|
||||
path: p as FilePath,
|
||||
stat: {
|
||||
size: fileObject.size,
|
||||
mtime: fileObject.lastModified,
|
||||
ctime: fileObject.lastModified,
|
||||
type: "file",
|
||||
},
|
||||
handle: fileHandle,
|
||||
};
|
||||
}
|
||||
|
||||
async delete(file: FSAPIFile | FSAPIFolder, force = false): Promise<void> {
|
||||
const parts = file.path.split("/").filter((part) => part !== "");
|
||||
const name = parts[parts.length - 1];
|
||||
|
||||
// Navigate to parent directory
|
||||
let currentHandle = this.rootHandle;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
|
||||
}
|
||||
|
||||
// Remove the entry
|
||||
await currentHandle.removeEntry(name, { recursive: force });
|
||||
}
|
||||
|
||||
async trash(file: FSAPIFile | FSAPIFolder, force = false): Promise<void> {
|
||||
// In webapp, trash is the same as delete (no recycle bin)
|
||||
await this.delete(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
// No-op in webapp version (no event system yet)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
209
src/apps/webapp/index.html
Normal file
209
src/apps/webapp/index.html
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Self-hosted LiveSync WebApp</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#status {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#status.error {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
#status.warning {
|
||||
background: #ffeaa7;
|
||||
color: #d63031;
|
||||
border: 1px solid #fdcb6e;
|
||||
}
|
||||
|
||||
#status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
#status.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-section h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
padding: 8px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-section li::before {
|
||||
content: "•";
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-left: -1em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.feature-list h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.console-link {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔄 Self-hosted LiveSync</h1>
|
||||
<p class="subtitle">Browser-based Self-hosted LiveSync using FileSystem API</p>
|
||||
|
||||
<div id="status" class="info">
|
||||
Initialising...
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>About This Application</h2>
|
||||
<ul>
|
||||
<li>Runs entirely in your browser</li>
|
||||
<li>Uses FileSystem API to access your local vault</li>
|
||||
<li>Syncs with CouchDB server (like Obsidian plugin)</li>
|
||||
<li>Settings stored in <code>.livesync/settings.json</code></li>
|
||||
<li>Real-time file watching with FileSystemObserver (Chrome 124+)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>How to Use</h2>
|
||||
<ul>
|
||||
<li>Grant directory access when prompted</li>
|
||||
<li>Create <code>.livesync/settings.json</code> in your vault folder. (Compatible with Obsidian's Self-hosted LiveSync)</li>
|
||||
<li>Add your CouchDB connection details</li>
|
||||
<li>Your files will be synced automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="console-link">
|
||||
💡 Open browser console (F12) for detailed logs
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
Powered by
|
||||
<a href="https://github.com/vrtmrz/obsidian-livesync" target="_blank">
|
||||
Self-hosted LiveSync
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
345
src/apps/webapp/main.ts
Normal file
345
src/apps/webapp/main.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Self-hosted LiveSync WebApp
|
||||
* Browser-based version of Self-hosted LiveSync plugin using FileSystem API
|
||||
*/
|
||||
|
||||
import { BrowserServiceHub } from "../../lib/src/services/BrowserServices";
|
||||
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
|
||||
import { ServiceContext } from "../../lib/src/services/base/ServiceBase";
|
||||
import { initialiseServiceModulesFSAPI } from "./serviceModules/FSAPIServiceModules";
|
||||
import type { ObsidianLiveSyncSettings } from "../../lib/src/common/types";
|
||||
import type { BrowserAPIService } from "../../lib/src/services/implements/browser/BrowserAPIService";
|
||||
import type { InjectableSettingService } from "../../lib/src/services/implements/injectable/InjectableSettingService";
|
||||
// import { SetupManager } from "@/modules/features/SetupManager";
|
||||
// import { ModuleObsidianSettingsAsMarkdown } from "@/modules/features/ModuleObsidianSettingAsMarkdown";
|
||||
// import { ModuleSetupObsidian } from "@/modules/features/ModuleSetupObsidian";
|
||||
// import { ModuleObsidianMenu } from "@/modules/essentialObsidian/ModuleObsidianMenu";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
const DB_NAME = "livesync-webapp";
|
||||
|
||||
/**
|
||||
* Default settings for the webapp
|
||||
*/
|
||||
const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
|
||||
liveSync: false,
|
||||
syncOnSave: true,
|
||||
syncOnStart: false,
|
||||
savingDelay: 200,
|
||||
lessInformationInLog: false,
|
||||
gcDelay: 0,
|
||||
periodicReplication: false,
|
||||
periodicReplicationInterval: 60,
|
||||
isConfigured: false,
|
||||
// CouchDB settings - user needs to configure these
|
||||
couchDB_URI: "",
|
||||
couchDB_USER: "",
|
||||
couchDB_PASSWORD: "",
|
||||
couchDB_DBNAME: "",
|
||||
// Disable features not needed in webapp
|
||||
usePluginSync: false,
|
||||
autoSweepPlugins: false,
|
||||
autoSweepPluginsPeriodic: false,
|
||||
};
|
||||
|
||||
class LiveSyncWebApp {
|
||||
private rootHandle: FileSystemDirectoryHandle | null = null;
|
||||
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
|
||||
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
|
||||
|
||||
async initialize() {
|
||||
console.log("Self-hosted LiveSync WebApp");
|
||||
console.log("Initializing...");
|
||||
|
||||
// Request directory access
|
||||
await this.requestDirectoryAccess();
|
||||
|
||||
if (!this.rootHandle) {
|
||||
throw new Error("Failed to get directory access");
|
||||
}
|
||||
|
||||
console.log(`Vault directory: ${this.rootHandle.name}`);
|
||||
|
||||
// Create service context and hub
|
||||
const context = new ServiceContext();
|
||||
this.serviceHub = new BrowserServiceHub<ServiceContext>();
|
||||
|
||||
// Setup API service
|
||||
(this.serviceHub.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
|
||||
() => this.rootHandle?.name || "livesync-webapp"
|
||||
);
|
||||
|
||||
// Setup settings handlers - save to .livesync folder
|
||||
const settingService = this.serviceHub.setting as InjectableSettingService<ServiceContext>;
|
||||
|
||||
settingService.saveData.setHandler(async (data: ObsidianLiveSyncSettings) => {
|
||||
try {
|
||||
await this.saveSettingsToFile(data);
|
||||
console.log("[Settings] Saved to .livesync/settings.json");
|
||||
} catch (error) {
|
||||
console.error("[Settings] Failed to save:", error);
|
||||
}
|
||||
});
|
||||
|
||||
settingService.loadData.setHandler(async (): Promise<ObsidianLiveSyncSettings | undefined> => {
|
||||
try {
|
||||
const data = await this.loadSettingsFromFile();
|
||||
if (data) {
|
||||
console.log("[Settings] Loaded from .livesync/settings.json");
|
||||
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[Settings] Failed to load, using defaults");
|
||||
}
|
||||
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
|
||||
});
|
||||
|
||||
// Create LiveSync core
|
||||
this.core = new LiveSyncBaseCore(
|
||||
this.serviceHub,
|
||||
(core, serviceHub) => {
|
||||
return initialiseServiceModulesFSAPI(this.rootHandle!, core, serviceHub);
|
||||
},
|
||||
(core) => [
|
||||
// new ModuleObsidianEvents(this, core),
|
||||
// new ModuleObsidianSettingDialogue(this, core),
|
||||
// new ModuleObsidianMenu(core),
|
||||
// new ModuleSetupObsidian(core),
|
||||
// new ModuleObsidianSettingsAsMarkdown(core),
|
||||
// new ModuleLog(this, core),
|
||||
// new ModuleObsidianDocumentHistory(this, core),
|
||||
// new ModuleInteractiveConflictResolver(this, core),
|
||||
// new ModuleObsidianGlobalHistory(this, core),
|
||||
// new ModuleDev(this, core),
|
||||
// new ModuleReplicateTest(this, core),
|
||||
// new ModuleIntegratedTest(this, core),
|
||||
// new SetupManager(core),
|
||||
],
|
||||
() => [],// No add-ons
|
||||
() => [],
|
||||
);
|
||||
|
||||
// Start the core
|
||||
await this.start();
|
||||
}
|
||||
|
||||
private async saveSettingsToFile(data: ObsidianLiveSyncSettings): Promise<void> {
|
||||
if (!this.rootHandle) return;
|
||||
|
||||
try {
|
||||
// Create .livesync directory if it doesn't exist
|
||||
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR, { create: true });
|
||||
|
||||
// Create/overwrite settings.json
|
||||
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(JSON.stringify(data, null, 2));
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
console.error("[Settings] Error saving to file:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSettingsFromFile(): Promise<Partial<ObsidianLiveSyncSettings> | null> {
|
||||
if (!this.rootHandle) return null;
|
||||
|
||||
try {
|
||||
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR);
|
||||
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE);
|
||||
const file = await fileHandle.getFile();
|
||||
const text = await file.text();
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
// File doesn't exist yet
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async requestDirectoryAccess() {
|
||||
try {
|
||||
// Check if we have a cached directory handle
|
||||
const cached = await this.loadCachedDirectoryHandle();
|
||||
if (cached) {
|
||||
// Verify permission (cast to any for compatibility)
|
||||
try {
|
||||
const permission = await (cached as any).queryPermission({ mode: "readwrite" });
|
||||
if (permission === "granted") {
|
||||
this.rootHandle = cached;
|
||||
console.log("[Directory] Using cached directory handle");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// queryPermission might not be supported, try to use anyway
|
||||
console.log("[Directory] Could not verify permission, requesting new access");
|
||||
}
|
||||
}
|
||||
|
||||
// Request new directory access
|
||||
console.log("[Directory] Requesting directory access...");
|
||||
this.rootHandle = await (window as any).showDirectoryPicker({
|
||||
mode: "readwrite",
|
||||
startIn: "documents",
|
||||
});
|
||||
|
||||
// Save the handle for next time
|
||||
await this.saveCachedDirectoryHandle(this.rootHandle);
|
||||
console.log("[Directory] Directory access granted");
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to get directory access:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCachedDirectoryHandle(handle: FileSystemDirectoryHandle) {
|
||||
try {
|
||||
// Use IndexedDB to store the directory handle
|
||||
const db = await this.openHandleDB();
|
||||
const transaction = db.transaction(["handles"], "readwrite");
|
||||
const store = transaction.objectStore("handles");
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = store.put(handle, "rootHandle");
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
});
|
||||
db.close();
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to cache handle:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCachedDirectoryHandle(): Promise<FileSystemDirectoryHandle | null> {
|
||||
try {
|
||||
const db = await this.openHandleDB();
|
||||
const transaction = db.transaction(["handles"], "readonly");
|
||||
const store = transaction.objectStore("handles");
|
||||
const handle = await new Promise<FileSystemDirectoryHandle | null>((resolve, reject) => {
|
||||
const request = store.get("rootHandle");
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = reject;
|
||||
});
|
||||
db.close();
|
||||
return handle;
|
||||
} catch (error) {
|
||||
console.error("[Directory] Failed to load cached handle:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async openHandleDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("livesync-webapp-handles", 1);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains("handles")) {
|
||||
db.createObjectStore("handles");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async start() {
|
||||
if (!this.core) {
|
||||
throw new Error("Core not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[Starting] Initializing LiveSync...");
|
||||
|
||||
const loadResult = await this.core.services.control.onLoad();
|
||||
if (!loadResult) {
|
||||
console.error("[Error] Failed to initialize LiveSync");
|
||||
this.showError("Failed to initialize LiveSync");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.core.services.control.onReady();
|
||||
|
||||
console.log("[Ready] LiveSync is running");
|
||||
|
||||
// Check if configured
|
||||
const settings = this.core.services.setting.currentSettings();
|
||||
if (!settings.isConfigured) {
|
||||
console.warn("[Warning] LiveSync is not configured yet");
|
||||
this.showWarning("Please configure CouchDB connection in settings");
|
||||
} else {
|
||||
console.log("[Info] LiveSync is configured and ready");
|
||||
console.log(`[Info] Database: ${settings.couchDB_URI}/${settings.couchDB_DBNAME}`);
|
||||
this.showSuccess("LiveSync is ready!");
|
||||
}
|
||||
|
||||
// Scan the directory to populate file cache
|
||||
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
|
||||
if (fileAccess?.fsapiAdapter) {
|
||||
console.log("[Scanning] Scanning vault directory...");
|
||||
await fileAccess.fsapiAdapter.scanDirectory();
|
||||
const files = await fileAccess.fsapiAdapter.getFiles();
|
||||
console.log(`[Scanning] Found ${files.length} files`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Error] Failed to start:", error);
|
||||
this.showError(`Failed to start: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.core) {
|
||||
console.log("[Shutdown] Shutting down...");
|
||||
|
||||
// Stop file watching
|
||||
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
|
||||
if (storageEventManager?.cleanup) {
|
||||
await storageEventManager.cleanup();
|
||||
}
|
||||
|
||||
await this.core.services.control.onUnload();
|
||||
console.log("[Shutdown] Complete");
|
||||
}
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
const statusEl = document.getElementById("status");
|
||||
if (statusEl) {
|
||||
statusEl.className = "error";
|
||||
statusEl.textContent = `Error: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
private showWarning(message: string) {
|
||||
const statusEl = document.getElementById("status");
|
||||
if (statusEl) {
|
||||
statusEl.className = "warning";
|
||||
statusEl.textContent = `Warning: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
private showSuccess(message: string) {
|
||||
const statusEl = document.getElementById("status");
|
||||
if (statusEl) {
|
||||
statusEl.className = "success";
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
const app = new LiveSyncWebApp();
|
||||
|
||||
window.addEventListener("load", async () => {
|
||||
try {
|
||||
await app.initialize();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle page unload
|
||||
window.addEventListener("beforeunload", () => {
|
||||
void app.shutdown();
|
||||
});
|
||||
|
||||
// Export for debugging
|
||||
(window as any).livesyncApp = app;
|
||||
281
src/apps/webapp/managers/FSAPIStorageEventManagerAdapter.ts
Normal file
281
src/apps/webapp/managers/FSAPIStorageEventManagerAdapter.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "../../../lib/src/common/types";
|
||||
import type { FileEventItem } from "../../../lib/src/common/types";
|
||||
import type { IStorageEventManagerAdapter } from "../../../lib/src/managers/adapters";
|
||||
import type {
|
||||
IStorageEventTypeGuardAdapter,
|
||||
IStorageEventPersistenceAdapter,
|
||||
IStorageEventWatchAdapter,
|
||||
IStorageEventStatusAdapter,
|
||||
IStorageEventConverterAdapter,
|
||||
IStorageEventWatchHandlers,
|
||||
} from "../../../lib/src/managers/adapters";
|
||||
import type { FileEventItemSentinel } from "../../../lib/src/managers/StorageEventManager";
|
||||
import type { FSAPIFile, FSAPIFolder } from "../adapters/FSAPITypes";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific type guard adapter
|
||||
*/
|
||||
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||
isFile(file: any): file is FSAPIFile {
|
||||
return (
|
||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
||||
);
|
||||
}
|
||||
|
||||
isFolder(item: any): item is FSAPIFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FileSystem API-specific persistence adapter (IndexedDB-based snapshot)
|
||||
*/
|
||||
class FSAPIPersistenceAdapter implements IStorageEventPersistenceAdapter {
|
||||
private dbName = "livesync-webapp-snapshot";
|
||||
private storeName = "snapshots";
|
||||
private snapshotKey = "file-events";
|
||||
|
||||
private async openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise<void> {
|
||||
try {
|
||||
const db = await this.openDB();
|
||||
const transaction = db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = store.put(snapshot, this.snapshotKey);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
db.close();
|
||||
} catch (error) {
|
||||
console.error("Failed to save snapshot:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null> {
|
||||
try {
|
||||
const db = await this.openDB();
|
||||
const transaction = db.transaction([this.storeName], "readonly");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
const result = await new Promise<(FileEventItem | FileEventItemSentinel)[] | null>((resolve, reject) => {
|
||||
const request = store.get(this.snapshotKey);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
db.close();
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FileSystem API-specific status adapter (console logging)
|
||||
*/
|
||||
class FSAPIStatusAdapter implements IStorageEventStatusAdapter {
|
||||
private lastUpdate = 0;
|
||||
private updateInterval = 5000; // Update every 5 seconds
|
||||
|
||||
updateStatus(status: { batched: number; processing: number; totalQueued: number }): void {
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdate > this.updateInterval) {
|
||||
if (status.totalQueued > 0 || status.processing > 0) {
|
||||
console.log(
|
||||
`[StorageEventManager] Batched: ${status.batched}, Processing: ${status.processing}, Total Queued: ${status.totalQueued}`
|
||||
);
|
||||
}
|
||||
this.lastUpdate = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FileSystem API-specific converter adapter
|
||||
*/
|
||||
class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile> {
|
||||
toFileInfo(file: FSAPIFile, deleted?: boolean): UXFileInfoStub {
|
||||
const pathParts = file.path.split("/");
|
||||
const name = pathParts[pathParts.length - 1] || file.handle.name;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
path: file.path,
|
||||
stat: file.stat,
|
||||
deleted: deleted,
|
||||
isFolder: false,
|
||||
};
|
||||
}
|
||||
|
||||
toInternalFileInfo(p: FilePath): UXInternalFileInfoStub {
|
||||
const pathParts = p.split("/");
|
||||
const name = pathParts[pathParts.length - 1] || "";
|
||||
|
||||
return {
|
||||
name: name,
|
||||
path: p,
|
||||
isInternal: true,
|
||||
stat: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
|
||||
*/
|
||||
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
private observer: any = null; // FileSystemObserver type
|
||||
|
||||
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
||||
|
||||
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||
// Use FileSystemObserver if available (Chrome 124+)
|
||||
if (typeof (window as any).FileSystemObserver === "undefined") {
|
||||
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
||||
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
const FileSystemObserver = (window as any).FileSystemObserver;
|
||||
|
||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
||||
for (const record of records) {
|
||||
const handle = record.root;
|
||||
const changedHandle = record.changedHandle;
|
||||
const relativePathComponents = record.relativePathComponents;
|
||||
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
|
||||
|
||||
// Build relative path
|
||||
const relativePath = relativePathComponents ? relativePathComponents.join("/") : "";
|
||||
|
||||
// Skip .livesync directory to avoid infinite loops
|
||||
if (relativePath.startsWith(".livesync/") || relativePath === ".livesync") {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[FileSystemObserver] ${type}: ${relativePath}`);
|
||||
|
||||
// Convert to our event handlers
|
||||
try {
|
||||
if (type === "appeared" || type === "modified") {
|
||||
if (changedHandle && changedHandle.kind === "file") {
|
||||
const file = await changedHandle.getFile();
|
||||
const fileInfo = {
|
||||
path: relativePath as any,
|
||||
stat: {
|
||||
size: file.size,
|
||||
mtime: file.lastModified,
|
||||
ctime: file.lastModified,
|
||||
type: "file" as const,
|
||||
},
|
||||
handle: changedHandle,
|
||||
};
|
||||
|
||||
if (type === "appeared") {
|
||||
await handlers.onCreate(fileInfo, undefined);
|
||||
} else {
|
||||
await handlers.onChange(fileInfo, undefined);
|
||||
}
|
||||
}
|
||||
} else if (type === "disappeared") {
|
||||
const fileInfo = {
|
||||
path: relativePath as any,
|
||||
stat: {
|
||||
size: 0,
|
||||
mtime: Date.now(),
|
||||
ctime: Date.now(),
|
||||
type: "file" as const,
|
||||
},
|
||||
handle: null as any,
|
||||
};
|
||||
await handlers.onDelete(fileInfo, undefined);
|
||||
} else if (type === "moved") {
|
||||
// Handle as delete + create
|
||||
// Note: FileSystemObserver provides both old and new paths in some cases
|
||||
// For simplicity, we'll treat it as a modification
|
||||
if (changedHandle && changedHandle.kind === "file") {
|
||||
const file = await changedHandle.getFile();
|
||||
const fileInfo = {
|
||||
path: relativePath as any,
|
||||
stat: {
|
||||
size: file.size,
|
||||
mtime: file.lastModified,
|
||||
ctime: file.lastModified,
|
||||
type: "file" as const,
|
||||
},
|
||||
handle: changedHandle,
|
||||
};
|
||||
await handlers.onChange(fileInfo, undefined);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[FileSystemObserver] Error processing ${type} event for ${relativePath}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing
|
||||
await this.observer.observe(this.rootHandle, { recursive: true });
|
||||
console.log("[FSAPIWatchAdapter] FileSystemObserver started successfully");
|
||||
} catch (error) {
|
||||
console.error("[FSAPIWatchAdapter] Failed to start FileSystemObserver:", error);
|
||||
console.log("[FSAPIWatchAdapter] Falling back to manual sync mode");
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async stopWatch(): Promise<void> {
|
||||
if (this.observer) {
|
||||
try {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
console.log("[FSAPIWatchAdapter] FileSystemObserver stopped");
|
||||
} catch (error) {
|
||||
console.error("[FSAPIWatchAdapter] Error stopping observer:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite adapter for FileSystem API StorageEventManager
|
||||
*/
|
||||
export class FSAPIStorageEventManagerAdapter implements IStorageEventManagerAdapter<FSAPIFile, FSAPIFolder> {
|
||||
readonly typeGuard: FSAPITypeGuardAdapter;
|
||||
readonly persistence: FSAPIPersistenceAdapter;
|
||||
readonly watch: FSAPIWatchAdapter;
|
||||
readonly status: FSAPIStatusAdapter;
|
||||
readonly converter: FSAPIConverterAdapter;
|
||||
|
||||
constructor(rootHandle: FileSystemDirectoryHandle) {
|
||||
this.typeGuard = new FSAPITypeGuardAdapter();
|
||||
this.persistence = new FSAPIPersistenceAdapter();
|
||||
this.watch = new FSAPIWatchAdapter(rootHandle);
|
||||
this.status = new FSAPIStatusAdapter();
|
||||
this.converter = new FSAPIConverterAdapter();
|
||||
}
|
||||
}
|
||||
39
src/apps/webapp/managers/StorageEventManagerFSAPI.ts
Normal file
39
src/apps/webapp/managers/StorageEventManagerFSAPI.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
StorageEventManagerBase,
|
||||
type StorageEventManagerBaseDependencies,
|
||||
} from "../../../lib/src/managers/StorageEventManager";
|
||||
import { FSAPIStorageEventManagerAdapter } from "./FSAPIStorageEventManagerAdapter";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "../../../lib/src/services/base/ServiceBase";
|
||||
|
||||
export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStorageEventManagerAdapter> {
|
||||
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>;
|
||||
private fsapiAdapter: FSAPIStorageEventManagerAdapter;
|
||||
|
||||
constructor(
|
||||
rootHandle: FileSystemDirectoryHandle,
|
||||
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>,
|
||||
dependencies: StorageEventManagerBaseDependencies
|
||||
) {
|
||||
const adapter = new FSAPIStorageEventManagerAdapter(rootHandle);
|
||||
super(adapter, dependencies);
|
||||
this.fsapiAdapter = adapter;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override _watchVaultRawEvents for webapp-specific logic
|
||||
* In webapp, we don't have internal files like Obsidian's .obsidian folder
|
||||
*/
|
||||
protected override async _watchVaultRawEvents(path: string) {
|
||||
// No-op in webapp version
|
||||
// Internal file handling is not needed
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
// Stop file watching
|
||||
if (this.fsapiAdapter?.watch) {
|
||||
await (this.fsapiAdapter.watch as any).stopWatch?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/apps/webapp/package.json
Normal file
21
src/apps/webapp/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "Browser-based Obsidian LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"imports": {
|
||||
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
||||
"@lib/worker/bgWorker.ts": "@lib/worker/bgWorker.mock.ts"
|
||||
}
|
||||
}
|
||||
15
src/apps/webapp/serviceModules/DatabaseFileAccess.ts
Normal file
15
src/apps/webapp/serviceModules/DatabaseFileAccess.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ServiceDatabaseFileAccessBase,
|
||||
type ServiceDatabaseFileAccessDependencies,
|
||||
} from "../../../lib/src/serviceModules/ServiceDatabaseFileAccessBase";
|
||||
import type { DatabaseFileAccess } from "../../../lib/src/interfaces/DatabaseFileAccess";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific implementation of ServiceDatabaseFileAccess
|
||||
* Same as Obsidian version, no platform-specific changes needed
|
||||
*/
|
||||
export class ServiceDatabaseFileAccessFSAPI extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {
|
||||
constructor(services: ServiceDatabaseFileAccessDependencies) {
|
||||
super(services);
|
||||
}
|
||||
}
|
||||
104
src/apps/webapp/serviceModules/FSAPIServiceModules.ts
Normal file
104
src/apps/webapp/serviceModules/FSAPIServiceModules.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { InjectableServiceHub } from "../../../lib/src/services/implements/injectable/InjectableServiceHub";
|
||||
import { ServiceRebuilder } from "../../../lib/src/serviceModules/Rebuilder";
|
||||
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
|
||||
import { StorageAccessManager } from "../../../lib/src/managers/StorageProcessingManager";
|
||||
import type { ServiceModules } from "../../../types";
|
||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "../../../lib/src/services/base/ServiceBase";
|
||||
import { FileAccessFSAPI } from "./FileAccessFSAPI";
|
||||
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
|
||||
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
|
||||
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
|
||||
|
||||
/**
|
||||
* Initialize service modules for FileSystem API webapp version
|
||||
* This is the webapp equivalent of ObsidianLiveSyncPlugin.initialiseServiceModules
|
||||
*
|
||||
* @param rootHandle - The root FileSystemDirectoryHandle for the vault
|
||||
* @param core - The LiveSyncBaseCore instance
|
||||
* @param services - The service hub
|
||||
* @returns ServiceModules containing all initialized service modules
|
||||
*/
|
||||
export function initialiseServiceModulesFSAPI(
|
||||
rootHandle: FileSystemDirectoryHandle,
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
services: InjectableServiceHub<ServiceContext>
|
||||
): ServiceModules {
|
||||
const storageAccessManager = new StorageAccessManager();
|
||||
|
||||
// FileSystem API-specific file access
|
||||
const vaultAccess = new FileAccessFSAPI(rootHandle, {
|
||||
storageAccessManager: storageAccessManager,
|
||||
vaultService: services.vault,
|
||||
settingService: services.setting,
|
||||
APIService: services.API,
|
||||
pathService: services.path,
|
||||
});
|
||||
|
||||
// FileSystem API-specific storage event manager
|
||||
const storageEventManager = new StorageEventManagerFSAPI(rootHandle, core, {
|
||||
fileProcessing: services.fileProcessing,
|
||||
setting: services.setting,
|
||||
vaultService: services.vault,
|
||||
storageAccessManager: storageAccessManager,
|
||||
APIService: services.API,
|
||||
});
|
||||
|
||||
// Storage access using FileSystem API adapter
|
||||
const storageAccess = new ServiceFileAccessFSAPI({
|
||||
API: services.API,
|
||||
setting: services.setting,
|
||||
fileProcessing: services.fileProcessing,
|
||||
vault: services.vault,
|
||||
appLifecycle: services.appLifecycle,
|
||||
storageEventManager: storageEventManager,
|
||||
storageAccessManager: storageAccessManager,
|
||||
vaultAccess: vaultAccess,
|
||||
});
|
||||
|
||||
// Database file access (platform-independent)
|
||||
const databaseFileAccess = new ServiceDatabaseFileAccessFSAPI({
|
||||
API: services.API,
|
||||
database: services.database,
|
||||
path: services.path,
|
||||
storageAccess: storageAccess,
|
||||
vault: services.vault,
|
||||
});
|
||||
|
||||
// File handler (platform-independent)
|
||||
const fileHandler = new (ServiceFileHandler as any)({
|
||||
API: services.API,
|
||||
databaseFileAccess: databaseFileAccess,
|
||||
conflict: services.conflict,
|
||||
setting: services.setting,
|
||||
fileProcessing: services.fileProcessing,
|
||||
vault: services.vault,
|
||||
path: services.path,
|
||||
replication: services.replication,
|
||||
storageAccess: storageAccess,
|
||||
});
|
||||
|
||||
// Rebuilder (platform-independent)
|
||||
const rebuilder = new ServiceRebuilder({
|
||||
API: services.API,
|
||||
database: services.database,
|
||||
appLifecycle: services.appLifecycle,
|
||||
setting: services.setting,
|
||||
remote: services.remote,
|
||||
databaseEvents: services.databaseEvents,
|
||||
replication: services.replication,
|
||||
replicator: services.replicator,
|
||||
UI: services.UI,
|
||||
vault: services.vault,
|
||||
fileHandler: fileHandler,
|
||||
storageAccess: storageAccess,
|
||||
control: services.control,
|
||||
});
|
||||
|
||||
return {
|
||||
rebuilder,
|
||||
fileHandler,
|
||||
databaseFileAccess,
|
||||
storageAccess,
|
||||
};
|
||||
}
|
||||
20
src/apps/webapp/serviceModules/FileAccessFSAPI.ts
Normal file
20
src/apps/webapp/serviceModules/FileAccessFSAPI.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "../../../lib/src/serviceModules/FileAccessBase";
|
||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific implementation of FileAccessBase
|
||||
* Uses FSAPIFileSystemAdapter for browser file operations
|
||||
*/
|
||||
export class FileAccessFSAPI extends FileAccessBase<FSAPIFileSystemAdapter> {
|
||||
constructor(rootHandle: FileSystemDirectoryHandle, dependencies: FileAccessBaseDependencies) {
|
||||
const adapter = new FSAPIFileSystemAdapter(rootHandle);
|
||||
super(adapter, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the adapter for accessing scanDirectory and other methods
|
||||
*/
|
||||
get fsapiAdapter(): FSAPIFileSystemAdapter {
|
||||
return this.adapter;
|
||||
}
|
||||
}
|
||||
15
src/apps/webapp/serviceModules/ServiceFileAccessImpl.ts
Normal file
15
src/apps/webapp/serviceModules/ServiceFileAccessImpl.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ServiceFileAccessBase,
|
||||
type StorageAccessBaseDependencies,
|
||||
} from "../../../lib/src/serviceModules/ServiceFileAccessBase";
|
||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific implementation of ServiceFileAccess
|
||||
* Uses FSAPIFileSystemAdapter for platform-specific operations
|
||||
*/
|
||||
export class ServiceFileAccessFSAPI extends ServiceFileAccessBase<FSAPIFileSystemAdapter> {
|
||||
constructor(services: StorageAccessBaseDependencies<FSAPIFileSystemAdapter>) {
|
||||
super(services);
|
||||
}
|
||||
}
|
||||
7
src/apps/webapp/svelte.config.js
Normal file
7
src/apps/webapp/svelte.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
32
src/apps/webapp/tsconfig.json
Normal file
32
src/apps/webapp/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
34
src/apps/webapp/vite.config.ts
Normal file
34
src/apps/webapp/vite.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "../../"),
|
||||
"@lib": path.resolve(__dirname, "../../lib/src"),
|
||||
},
|
||||
},
|
||||
base: "./",
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: path.resolve(__dirname, "index.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"),
|
||||
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"),
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { PouchDB } from "@lib/pouchdb/pouchdb-browser";
|
||||
import {
|
||||
type EntryDoc,
|
||||
type LOG_LEVEL,
|
||||
type ObsidianLiveSyncSettings,
|
||||
type P2PSyncSetting,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
@@ -39,6 +40,7 @@ import type { InjectableVaultServiceCompat } from "@lib/services/implements/inje
|
||||
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
|
||||
import type { InjectableAPIService } from "@/lib/src/services/implements/injectable/InjectableAPIService";
|
||||
import type { BrowserAPIService } from "@/lib/src/services/implements/browser/BrowserAPIService";
|
||||
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
|
||||
|
||||
function addToList(item: string, list: string) {
|
||||
return unique(
|
||||
@@ -87,6 +89,22 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
|
||||
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
|
||||
() => "p2p-livesync-web-peer"
|
||||
);
|
||||
this.services.API.addLog.setHandler(Logger);
|
||||
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
|
||||
this._simpleStore = repStore;
|
||||
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
|
||||
this.services.setting.settings = _settings as any;
|
||||
(this.services.setting as InjectableSettingService<any>).saveData.setHandler(async (data) => {
|
||||
await repStore.set("settings", data);
|
||||
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
|
||||
});
|
||||
(this.services.setting as InjectableSettingService<any>).loadData.setHandler(async () => {
|
||||
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
get settings() {
|
||||
return this.services.setting.currentSettings() as P2PSyncSetting;
|
||||
}
|
||||
async init() {
|
||||
// const { simpleStoreAPI } = await getWrappedSynchromesh();
|
||||
@@ -102,23 +120,27 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
|
||||
this._simpleStore = repStore;
|
||||
let _settings = (await repStore.get("settings")) || ({ ...P2P_DEFAULT_SETTINGS } as P2PSyncSetting);
|
||||
this.services.setting.settings = _settings as any;
|
||||
|
||||
await this.services.setting.loadSettings();
|
||||
this.plugin = {
|
||||
saveSettings: async () => {
|
||||
await repStore.set("settings", _settings);
|
||||
eventHub.emitEvent(EVENT_SETTING_SAVED, _settings);
|
||||
},
|
||||
get settings() {
|
||||
return _settings;
|
||||
},
|
||||
set settings(newSettings: P2PSyncSetting) {
|
||||
_settings = { ..._settings, ...newSettings };
|
||||
},
|
||||
rebuilder: null,
|
||||
// saveSettings: async () => {
|
||||
// await repStore.set("settings", _settings);
|
||||
// eventHub.emitEvent(EVENT_SETTING_SAVED, _settings);
|
||||
// },
|
||||
// get settings() {
|
||||
// return _settings;
|
||||
// },
|
||||
// set settings(newSettings: P2PSyncSetting) {
|
||||
// _settings = { ..._settings, ...newSettings };
|
||||
// },
|
||||
// rebuilder: null,
|
||||
// core: {
|
||||
// settings: this.services.setting.settings,
|
||||
// },
|
||||
services: this.services,
|
||||
core: {
|
||||
services: this.services,
|
||||
},
|
||||
// $$scheduleAppReload: () => {},
|
||||
// $$getVaultName: () => "p2p-livesync-web-peer",
|
||||
};
|
||||
@@ -132,9 +154,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
|
||||
}, 1000);
|
||||
return this;
|
||||
}
|
||||
get settings() {
|
||||
return this.plugin.settings;
|
||||
}
|
||||
|
||||
_log(msg: any, level?: LOG_LEVEL): void {
|
||||
Logger(msg, level);
|
||||
}
|
||||
@@ -334,12 +354,11 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.plugin.settings = remoteConfig;
|
||||
await this.plugin.saveSettings();
|
||||
await this.services.setting.applyPartial(remoteConfig, true);
|
||||
if (yn === DROP) {
|
||||
await this.plugin.rebuilder.scheduleFetch();
|
||||
// await this.plugin.rebuilder.scheduleFetch();
|
||||
} else {
|
||||
await this.plugin.services.appLifecycle.scheduleRestart();
|
||||
await this.plugin.core.services.appLifecycle.scheduleRestart();
|
||||
}
|
||||
} else {
|
||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||
@@ -357,13 +376,18 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
|
||||
} as const;
|
||||
|
||||
const targetSetting = settingMap[prop];
|
||||
const currentSettingAll = this.plugin.core.services.setting.currentSettings();
|
||||
const currentSetting = {
|
||||
[targetSetting]: currentSettingAll ? currentSettingAll[targetSetting] : "",
|
||||
};
|
||||
if (peer[prop]) {
|
||||
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
// this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
|
||||
// await this.plugin.saveSettings();
|
||||
currentSetting[targetSetting] = removeFromList(peer.name, currentSetting[targetSetting]);
|
||||
} else {
|
||||
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
currentSetting[targetSetting] = addToList(peer.name, currentSetting[targetSetting]);
|
||||
}
|
||||
await this.plugin.core.services.setting.applyPartial(currentSetting, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<main>
|
||||
<div class="control">
|
||||
{#await synchronised then cmdSync}
|
||||
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync}></P2PReplicatorPane>
|
||||
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync} core={cmdSync.plugin.core}></P2PReplicatorPane>
|
||||
{:catch error}
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
||||
|
||||
45
src/common/PeriodicProcessor.ts
Normal file
45
src/common/PeriodicProcessor.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Logger } from "octagonal-wheels/common/logger";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { eventHub, EVENT_PLUGIN_UNLOADED } from "./events";
|
||||
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||
type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>;
|
||||
export class PeriodicProcessor {
|
||||
_process: () => Promise<any>;
|
||||
_timer?: number = undefined;
|
||||
_core: PeriodicProcessorHost;
|
||||
constructor(core: PeriodicProcessorHost, process: () => Promise<any>) {
|
||||
// this._plugin = plugin;
|
||||
this._core = core;
|
||||
this._process = process;
|
||||
eventHub.onceEvent(EVENT_PLUGIN_UNLOADED, () => {
|
||||
this.disable();
|
||||
});
|
||||
}
|
||||
async process() {
|
||||
try {
|
||||
await this._process();
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
enable(interval: number) {
|
||||
this.disable();
|
||||
if (interval == 0) return;
|
||||
this._timer = this._core.services.API.setInterval(
|
||||
() =>
|
||||
fireAndForget(async () => {
|
||||
await this.process();
|
||||
if (this._core.services?.control?.hasUnloaded()) {
|
||||
this.disable();
|
||||
}
|
||||
}),
|
||||
interval
|
||||
);
|
||||
}
|
||||
disable() {
|
||||
if (this._timer !== undefined) {
|
||||
this._core.services.API.clearInterval(this._timer);
|
||||
this._timer = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { eventHub } from "../lib/src/hub/hub";
|
||||
import type ObsidianLiveSyncPlugin from "../main";
|
||||
// import type ObsidianLiveSyncPlugin from "../main";
|
||||
|
||||
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
|
||||
export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded";
|
||||
@@ -24,12 +24,12 @@ export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
|
||||
|
||||
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
|
||||
export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3";
|
||||
export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
|
||||
// export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
|
||||
declare global {
|
||||
interface LSEvents {
|
||||
[EVENT_PLUGIN_LOADED]: ObsidianLiveSyncPlugin;
|
||||
[EVENT_PLUGIN_LOADED]: undefined;
|
||||
[EVENT_PLUGIN_UNLOADED]: undefined;
|
||||
[EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined;
|
||||
[EVENT_REQUEST_OPEN_SETTINGS]: undefined;
|
||||
@@ -44,7 +44,6 @@ declare global {
|
||||
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
||||
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
|
||||
[EVENT_ANALYSE_DB_USAGE]: undefined;
|
||||
[EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined;
|
||||
[EVENT_REQUEST_PERFORM_GC_V3]: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,10 @@ import {
|
||||
type UXFileInfoStub,
|
||||
} from "../lib/src/common/types.ts";
|
||||
export { ICHeader, ICXHeader } from "./types.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../main.ts";
|
||||
import { writeString } from "../lib/src/string_and_binary/convert.ts";
|
||||
import { fireAndForget } from "../lib/src/common/utils.ts";
|
||||
import { sameChangePairs } from "./stores.ts";
|
||||
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { EVENT_PLUGIN_UNLOADED, eventHub } from "./events.ts";
|
||||
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
|
||||
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
||||
|
||||
@@ -126,47 +123,6 @@ export {
|
||||
stripInternalMetadataPrefix,
|
||||
} from "@lib/common/typeUtils.ts";
|
||||
|
||||
export class PeriodicProcessor {
|
||||
_process: () => Promise<any>;
|
||||
_timer?: number = undefined;
|
||||
_plugin: ObsidianLiveSyncPlugin;
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, process: () => Promise<any>) {
|
||||
this._plugin = plugin;
|
||||
this._process = process;
|
||||
eventHub.onceEvent(EVENT_PLUGIN_UNLOADED, () => {
|
||||
this.disable();
|
||||
});
|
||||
}
|
||||
async process() {
|
||||
try {
|
||||
await this._process();
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
enable(interval: number) {
|
||||
this.disable();
|
||||
if (interval == 0) return;
|
||||
this._timer = window.setInterval(
|
||||
() =>
|
||||
fireAndForget(async () => {
|
||||
await this.process();
|
||||
if (this._plugin.services?.control?.hasUnloaded()) {
|
||||
this.disable();
|
||||
}
|
||||
}),
|
||||
interval
|
||||
);
|
||||
this._plugin.registerInterval(this._timer);
|
||||
}
|
||||
disable() {
|
||||
if (this._timer !== undefined) {
|
||||
window.clearInterval(this._timer);
|
||||
this._timer = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const _requestToCouchDBFetch = async (
|
||||
baseUri: string,
|
||||
username: string,
|
||||
@@ -257,20 +213,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;
|
||||
@@ -385,11 +329,6 @@ export function disposeAllMemo() {
|
||||
_cached.clear();
|
||||
}
|
||||
|
||||
export function displayRev(rev: string) {
|
||||
const [number, hash] = rev.split("-");
|
||||
return `${number}-${hash.substring(0, 6)}`;
|
||||
}
|
||||
|
||||
export function getLogLevel(showNotice: boolean) {
|
||||
return showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
}
|
||||
@@ -458,3 +397,5 @@ export function onlyInNTimes(n: number, proc: (progress: number) => any) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { displayRev } from "@lib/common/utils.ts";
|
||||
|
||||
@@ -50,17 +50,15 @@ import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||
import {
|
||||
EVEN,
|
||||
PeriodicProcessor,
|
||||
disposeMemoObject,
|
||||
isCustomisationSyncMetadata,
|
||||
isMarkedAsSameChanges,
|
||||
isPluginMetadata,
|
||||
markChangesAreSame,
|
||||
memoIfNotExist,
|
||||
memoObject,
|
||||
retrieveMemoObject,
|
||||
scheduleTask,
|
||||
} from "../../common/utils.ts";
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
|
||||
@@ -394,29 +392,32 @@ export type PluginDataEx = {
|
||||
};
|
||||
|
||||
export class ConfigSync extends LiveSyncCommands {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
super(plugin);
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||
super(plugin, core);
|
||||
pluginScanningCount.onChanged((e) => {
|
||||
const total = e.value;
|
||||
pluginIsEnumerating.set(total != 0);
|
||||
});
|
||||
}
|
||||
get configDir() {
|
||||
return this.core.services.API.getSystemConfigDir();
|
||||
}
|
||||
get kvDB() {
|
||||
return this.plugin.kvDB;
|
||||
return this.core.kvDB;
|
||||
}
|
||||
|
||||
get useV2() {
|
||||
return this.plugin.settings.usePluginSyncV2;
|
||||
return this.core.settings.usePluginSyncV2;
|
||||
}
|
||||
get useSyncPluginEtc() {
|
||||
return this.plugin.settings.usePluginEtc;
|
||||
return this.core.settings.usePluginEtc;
|
||||
}
|
||||
isThisModuleEnabled() {
|
||||
return this.plugin.settings.usePluginSync;
|
||||
return this.core.settings.usePluginSync;
|
||||
}
|
||||
|
||||
pluginDialog?: PluginDialogModal = undefined;
|
||||
periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false));
|
||||
periodicPluginSweepProcessor = new PeriodicProcessor(this.core, async () => await this.scanAllConfigFiles(false));
|
||||
|
||||
pluginList: IPluginDataExDisplay[] = [];
|
||||
showPluginSyncModal() {
|
||||
@@ -441,7 +442,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
this.hidePluginSyncModal();
|
||||
this.periodicPluginSweepProcessor?.disable();
|
||||
}
|
||||
addRibbonIcon = this.plugin.addRibbonIcon.bind(this.plugin);
|
||||
addRibbonIcon = this.services.API.addRibbonIcon.bind(this.services.API);
|
||||
onload() {
|
||||
addIcon(
|
||||
"custom-sync",
|
||||
@@ -449,7 +450,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
<path d="m272 166-9.38 9.38 9.38 9.38 9.38-9.38c1.96-1.93 5.11-1.9 7.03 0.058 1.91 1.94 1.91 5.04 0 6.98l-9.38 9.38 5.86 5.86-11.7 11.7c-8.34 8.35-21.4 9.68-31.3 3.19l-3.84 3.98c-8.45 8.7-20.1 13.6-32.2 13.6h-5.55v-9.95h5.55c9.43-0.0182 18.5-3.84 25-10.6l3.95-4.09c-6.54-9.86-5.23-23 3.14-31.3l11.7-11.7 5.86 5.86 9.38-9.38c1.96-1.93 5.11-1.9 7.03 0.0564 1.91 1.93 1.91 5.04 2e-3 6.98z"/>
|
||||
</g>`
|
||||
);
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "livesync-plugin-dialog-ex",
|
||||
name: "Show customization sync dialog",
|
||||
callback: () => {
|
||||
@@ -466,10 +467,9 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
filePath: string
|
||||
): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" {
|
||||
if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG";
|
||||
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`))
|
||||
return "THEME";
|
||||
if (filePath.startsWith(`${this.app.vault.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET";
|
||||
if (filePath.startsWith(`${this.app.vault.configDir}/plugins/`)) {
|
||||
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.configDir}/themes/`)) return "THEME";
|
||||
if (filePath.startsWith(`${this.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET";
|
||||
if (filePath.startsWith(`${this.configDir}/plugins/`)) {
|
||||
if (
|
||||
filePath.endsWith("/styles.css") ||
|
||||
filePath.endsWith("/manifest.json") ||
|
||||
@@ -487,7 +487,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return "";
|
||||
}
|
||||
isTargetPath(filePath: string): boolean {
|
||||
if (!filePath.startsWith(this.app.vault.configDir)) return false;
|
||||
if (!filePath.startsWith(this.configDir)) return false;
|
||||
// Idea non-filter option?
|
||||
return this.getFileCategory(filePath) != "";
|
||||
}
|
||||
@@ -856,7 +856,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
children: [],
|
||||
eden: {},
|
||||
};
|
||||
const r = await this.plugin.localDatabase.putDBEntry(saving);
|
||||
const r = await this.core.localDatabase.putDBEntry(saving);
|
||||
if (r && r.ok) {
|
||||
this._log(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO);
|
||||
const delR = await this.deleteConfigOnDatabase(v1Path);
|
||||
@@ -998,16 +998,16 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
}
|
||||
async applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise<boolean> {
|
||||
const baseDir = this.app.vault.configDir;
|
||||
const baseDir = this.configDir;
|
||||
try {
|
||||
if (content) {
|
||||
// const dt = createBlob(content);
|
||||
const filename = data.files[0].filename;
|
||||
this._log(`Applying ${filename} of ${data.displayName || data.name}..`);
|
||||
const path = `${baseDir}/${filename}` as FilePath;
|
||||
await this.plugin.storageAccess.ensureDir(path);
|
||||
await this.core.storageAccess.ensureDir(path);
|
||||
// If the content has applied, modified time will be updated to the current time.
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, content);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(path, content);
|
||||
await this.storeCustomisationFileV2(path, this.services.setting.getDeviceAndVaultName());
|
||||
} else {
|
||||
const files = data.files;
|
||||
@@ -1017,12 +1017,12 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
const path = `${baseDir}/${f.filename}` as FilePath;
|
||||
this._log(`Applying ${f.filename} of ${data.displayName || data.name}..`);
|
||||
// const contentEach = createBlob(f.data);
|
||||
await this.plugin.storageAccess.ensureDir(path);
|
||||
await this.core.storageAccess.ensureDir(path);
|
||||
|
||||
if (f.datatype == "newnote") {
|
||||
let oldData;
|
||||
try {
|
||||
oldData = await this.plugin.storageAccess.readHiddenFileBinary(path);
|
||||
oldData = await this.core.storageAccess.readHiddenFileBinary(path);
|
||||
} catch (ex) {
|
||||
this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE);
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
@@ -1033,11 +1033,11 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(path, content, stat);
|
||||
} else {
|
||||
let oldData;
|
||||
try {
|
||||
oldData = await this.plugin.storageAccess.readHiddenFileText(path);
|
||||
oldData = await this.core.storageAccess.readHiddenFileText(path);
|
||||
} catch (ex) {
|
||||
this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE);
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
@@ -1048,7 +1048,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(path, content, stat);
|
||||
}
|
||||
this._log(`Applied ${f.filename} of ${data.displayName || data.name}..`);
|
||||
await this.storeCustomisationFileV2(path, this.services.setting.getDeviceAndVaultName());
|
||||
@@ -1067,7 +1067,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
if (data instanceof PluginDataExDisplayV2) {
|
||||
return this.applyDataV2(data, content);
|
||||
}
|
||||
const baseDir = this.app.vault.configDir;
|
||||
const baseDir = this.configDir;
|
||||
try {
|
||||
if (!data.documentPath) throw "InternalError: Document path not exist";
|
||||
const dx = await this.localDatabase.getDBEntry(data.documentPath);
|
||||
@@ -1080,12 +1080,12 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
try {
|
||||
// console.dir(f);
|
||||
const path = `${baseDir}/${f.filename}`;
|
||||
await this.plugin.storageAccess.ensureDir(path);
|
||||
await this.core.storageAccess.ensureDir(path);
|
||||
if (!content) {
|
||||
const dt = decodeBinary(f.data);
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, dt);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(path, dt);
|
||||
} else {
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, content);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(path, content);
|
||||
}
|
||||
this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`);
|
||||
} catch (ex) {
|
||||
@@ -1174,7 +1174,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
(docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath(docs as AnyEntry)
|
||||
);
|
||||
}
|
||||
if (this.isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) {
|
||||
if (this.isThisModuleEnabled() && this.core.settings.notifyPluginOrSettingUpdated) {
|
||||
if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) {
|
||||
const fragment = createFragment((doc) => {
|
||||
doc.createEl("span", undefined, (a) => {
|
||||
@@ -1232,13 +1232,13 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
|
||||
recentProcessedInternalFiles = [] as string[];
|
||||
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
|
||||
const stat = await this.plugin.storageAccess.statHidden(path);
|
||||
const stat = await this.core.storageAccess.statHidden(path);
|
||||
let version: string | undefined;
|
||||
let displayName: string | undefined;
|
||||
if (!stat) {
|
||||
return false;
|
||||
}
|
||||
const contentBin = await this.plugin.storageAccess.readHiddenFileBinary(path);
|
||||
const contentBin = await this.core.storageAccess.readHiddenFileBinary(path);
|
||||
let content: string[];
|
||||
try {
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
@@ -1267,7 +1267,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
const mtime = stat.mtime;
|
||||
return {
|
||||
filename: path.substring(this.app.vault.configDir.length + 1),
|
||||
filename: path.substring(this.configDir.length + 1),
|
||||
data: content,
|
||||
mtime,
|
||||
size: stat.size,
|
||||
@@ -1282,12 +1282,12 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
const prefixedFileName = vf;
|
||||
|
||||
const id = await this.path2id(prefixedFileName);
|
||||
const stat = await this.plugin.storageAccess.statHidden(path);
|
||||
const stat = await this.core.storageAccess.statHidden(path);
|
||||
if (!stat) {
|
||||
return false;
|
||||
}
|
||||
const mtime = stat.mtime;
|
||||
const content = await this.plugin.storageAccess.readHiddenFileBinary(path);
|
||||
const content = await this.core.storageAccess.readHiddenFileBinary(path);
|
||||
const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...(await arrayBufferToBase64(content))]);
|
||||
// const contentBlob = createBlob(content);
|
||||
try {
|
||||
@@ -1308,7 +1308,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 +1328,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 = {
|
||||
@@ -1506,11 +1506,11 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
if (this._isMainSuspended()) return false;
|
||||
if (!this.isThisModuleEnabled()) return false;
|
||||
// if (!this.isTargetPath(path)) return false;
|
||||
const stat = await this.plugin.storageAccess.statHidden(path);
|
||||
const stat = await this.core.storageAccess.statHidden(path);
|
||||
// Make sure that target is a file.
|
||||
if (stat && stat.type != "file") return false;
|
||||
|
||||
const configDir = normalizePath(this.app.vault.configDir);
|
||||
const configDir = normalizePath(this.configDir);
|
||||
const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting)
|
||||
.filter((e) => e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY)
|
||||
.map((e) => e.files)
|
||||
@@ -1676,7 +1676,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async scanInternalFiles(): Promise<FilePath[]> {
|
||||
const filenames = (await this.getFiles(this.app.vault.configDir, 2))
|
||||
const filenames = (await this.getFiles(this.configDir, 2))
|
||||
.filter((e) => e.startsWith("."))
|
||||
.filter((e) => !e.startsWith(".trash"));
|
||||
return filenames as FilePath[];
|
||||
@@ -1707,7 +1707,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
choices.push(CHOICE_DISABLE);
|
||||
choices.push(CHOICE_DISMISS);
|
||||
|
||||
const ret = await this.plugin.confirm.askSelectStringDialogue(message, choices, {
|
||||
const ret = await this.core.confirm.askSelectStringDialogue(message, choices, {
|
||||
defaultAction: CHOICE_DISMISS,
|
||||
timeout: 40,
|
||||
title: "Customisation sync",
|
||||
@@ -1730,13 +1730,13 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
private _allSuspendExtraSync(): Promise<boolean> {
|
||||
if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) {
|
||||
if (this.core.settings.usePluginSync || this.core.settings.autoSweepPlugins) {
|
||||
this._log(
|
||||
"Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this.plugin.settings.usePluginSync = false;
|
||||
this.plugin.settings.autoSweepPlugins = false;
|
||||
this.core.settings.usePluginSync = false;
|
||||
this.core.settings.autoSweepPlugins = false;
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -1747,14 +1747,20 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||
if (mode == "DISABLE") {
|
||||
this.plugin.settings.usePluginSync = false;
|
||||
await this.plugin.saveSettings();
|
||||
// this.plugin.settings.usePluginSync = false;
|
||||
// await this.plugin.saveSettings();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
usePluginSync: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == "CUSTOMIZE") {
|
||||
if (!this.services.setting.getDeviceAndVaultName()) {
|
||||
let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`);
|
||||
let name = await this.core.confirm.askString("Device name", "Please set this device name", `desktop`);
|
||||
if (!name) {
|
||||
if (Platform.isAndroidApp) {
|
||||
name = "android-app";
|
||||
@@ -1779,9 +1785,16 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
this.services.setting.setDeviceAndVaultName(name);
|
||||
}
|
||||
this.plugin.settings.usePluginSync = true;
|
||||
this.plugin.settings.useAdvancedMode = true;
|
||||
await this.plugin.saveSettings();
|
||||
// this.core.settings.usePluginSync = true;
|
||||
// this.core.settings.useAdvancedMode = true;
|
||||
// await this.core.saveSettings();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
usePluginSync: true,
|
||||
useAdvancedMode: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
await this.scanAllConfigFiles(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
export let isMaintenanceMode: boolean = false;
|
||||
export let isFlagged: boolean = false;
|
||||
const addOn = plugin.getAddOn<ConfigSync>(ConfigSync.name)!;
|
||||
$: core = plugin.core;
|
||||
const addOn = plugin.core.getAddOn<ConfigSync>(ConfigSync.name)!;
|
||||
if (!addOn) {
|
||||
Logger(`Could not load the add-on ${ConfigSync.name}`, LOG_LEVEL_INFO);
|
||||
throw new Error(`Could not load the add-on ${ConfigSync.name}`);
|
||||
@@ -334,13 +335,13 @@
|
||||
Logger(`Could not find local item`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const duplicateTermName = await plugin.confirm.askString("Duplicate", "device name", "");
|
||||
const duplicateTermName = await core.confirm.askString("Duplicate", "device name", "");
|
||||
if (duplicateTermName) {
|
||||
if (duplicateTermName.contains("/")) {
|
||||
Logger(`We can not use "/" to the device name`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const key = `${plugin.app.vault.configDir}/${local.files[0].filename}`;
|
||||
const key = `${plugin.core.services.API.getSystemConfigDir()}/${local.files[0].filename}`;
|
||||
await addOn.storeCustomizationFiles(key as FilePath, duplicateTermName);
|
||||
await addOn.updatePluginList(false, addOn.filenameToUnifiedKey(key, duplicateTermName));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class PluginDialogModal extends Modal {
|
||||
if (!this.component) {
|
||||
this.component = mount(PluginPane, {
|
||||
target: contentEl,
|
||||
props: { plugin: this.plugin },
|
||||
props: { plugin: this.plugin, core: this.plugin.core },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,19 +22,22 @@
|
||||
import { normalizePath } from "../../deps";
|
||||
import { HiddenFileSync } from "../HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
export let core :LiveSyncBaseCore;
|
||||
// $: core = plugin.core;
|
||||
|
||||
$: hideNotApplicable = false;
|
||||
$: thisTerm = plugin.services.setting.getDeviceAndVaultName();
|
||||
$: thisTerm = core.services.setting.getDeviceAndVaultName();
|
||||
|
||||
const addOn = plugin.getAddOn(ConfigSync.name) as ConfigSync;
|
||||
const addOn = core.getAddOn<ConfigSync>(ConfigSync.name)!;
|
||||
if (!addOn) {
|
||||
const msg =
|
||||
"AddOn Module (ConfigSync) has not been loaded. This is very unexpected situation. Please report this issue.";
|
||||
Logger(msg, LOG_LEVEL_NOTICE);
|
||||
throw new Error(msg);
|
||||
}
|
||||
const addOnHiddenFileSync = plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
|
||||
const addOnHiddenFileSync = core.getAddOn<HiddenFileSync>(HiddenFileSync.name) as HiddenFileSync;
|
||||
if (!addOnHiddenFileSync) {
|
||||
const msg =
|
||||
"AddOn Module (HiddenFileSync) has not been loaded. This is very unexpected situation. Please report this issue.";
|
||||
@@ -98,7 +101,7 @@
|
||||
await requestUpdate();
|
||||
}
|
||||
async function replicate() {
|
||||
await plugin.services.replication.replicate(true);
|
||||
await core.services.replication.replicate(true);
|
||||
}
|
||||
function selectAllNewest(selectMode: boolean) {
|
||||
selectNewestPulse++;
|
||||
@@ -147,8 +150,8 @@
|
||||
}
|
||||
function applyAutomaticSync(key: string, direction: "pushForce" | "pullForce" | "safe") {
|
||||
setMode(key, MODE_AUTOMATIC);
|
||||
const configDir = normalizePath(plugin.app.vault.configDir);
|
||||
const files = (plugin.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`);
|
||||
const configDir = normalizePath(plugin.core.services.API.getSystemConfigDir());
|
||||
const files = (plugin.core.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`);
|
||||
addOnHiddenFileSync.initialiseInternalFileSync(direction, true, files);
|
||||
}
|
||||
function askOverwriteModeForAutomatic(evt: MouseEvent, key: string) {
|
||||
@@ -222,22 +225,22 @@
|
||||
);
|
||||
if (mode == MODE_SELECTIVE) {
|
||||
automaticList.delete(key);
|
||||
delete plugin.settings.pluginSyncExtendedSetting[key];
|
||||
delete plugin.core.settings.pluginSyncExtendedSetting[key];
|
||||
automaticListDisp = automaticList;
|
||||
} else {
|
||||
automaticList.set(key, mode);
|
||||
automaticListDisp = automaticList;
|
||||
if (!(key in plugin.settings.pluginSyncExtendedSetting)) {
|
||||
plugin.settings.pluginSyncExtendedSetting[key] = {
|
||||
if (!(key in plugin.core.settings.pluginSyncExtendedSetting)) {
|
||||
plugin.core.settings.pluginSyncExtendedSetting[key] = {
|
||||
key,
|
||||
mode,
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
plugin.settings.pluginSyncExtendedSetting[key].files = files;
|
||||
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||
plugin.core.settings.pluginSyncExtendedSetting[key].files = files;
|
||||
plugin.core.settings.pluginSyncExtendedSetting[key].mode = mode;
|
||||
}
|
||||
plugin.services.setting.saveSettingData();
|
||||
core.services.setting.saveSettingData();
|
||||
}
|
||||
function getIcon(mode: SYNC_MODE) {
|
||||
if (mode in ICONS) {
|
||||
@@ -250,7 +253,7 @@
|
||||
let automaticListDisp = new Map<string, SYNC_MODE>();
|
||||
|
||||
// apply current configuration to the dialogue
|
||||
for (const { key, mode } of Object.values(plugin.settings.pluginSyncExtendedSetting)) {
|
||||
for (const { key, mode } of Object.values(plugin.core.settings.pluginSyncExtendedSetting)) {
|
||||
automaticList.set(key, mode);
|
||||
}
|
||||
|
||||
@@ -259,7 +262,7 @@
|
||||
let displayKeys: Record<string, string[]> = {};
|
||||
|
||||
function computeDisplayKeys(list: IPluginDataExDisplay[]) {
|
||||
const extraKeys = Object.keys(plugin.settings.pluginSyncExtendedSetting);
|
||||
const extraKeys = Object.keys(plugin.core.settings.pluginSyncExtendedSetting);
|
||||
return [
|
||||
...list,
|
||||
...extraKeys
|
||||
@@ -321,7 +324,7 @@
|
||||
$: {
|
||||
pluginEntries = groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name");
|
||||
}
|
||||
let useSyncPluginEtc = plugin.settings.usePluginEtc;
|
||||
let useSyncPluginEtc = plugin.core.settings.usePluginEtc;
|
||||
</script>
|
||||
|
||||
<div class="buttonsWrap">
|
||||
|
||||
@@ -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,10 +29,7 @@ import {
|
||||
} from "../../lib/src/common/utils.ts";
|
||||
import {
|
||||
compareMTime,
|
||||
unmarkChanges,
|
||||
isInternalMetadata,
|
||||
markChangesAreSame,
|
||||
PeriodicProcessor,
|
||||
TARGET_IS_NEW,
|
||||
scheduleTask,
|
||||
getLogLevel,
|
||||
@@ -43,6 +40,7 @@ import {
|
||||
EVEN,
|
||||
displayRev,
|
||||
} from "../../common/utils.ts";
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||
@@ -80,25 +78,25 @@ function getComparingMTime(
|
||||
|
||||
export class HiddenFileSync extends LiveSyncCommands {
|
||||
isThisModuleEnabled() {
|
||||
return this.plugin.settings.syncInternalFiles;
|
||||
return this.core.settings.syncInternalFiles;
|
||||
}
|
||||
|
||||
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(
|
||||
this.plugin,
|
||||
this.core,
|
||||
async () => this.isThisModuleEnabled() && this._isDatabaseReady() && (await this.scanAllStorageChanges(false))
|
||||
);
|
||||
|
||||
get kvDB() {
|
||||
return this.plugin.kvDB;
|
||||
return this.core.kvDB;
|
||||
}
|
||||
getConflictedDoc(path: FilePathWithPrefix, rev: string) {
|
||||
return this.plugin.managers.conflictManager.getConflictedDoc(path, rev);
|
||||
return this.core.localDatabase.managers.conflictManager.getConflictedDoc(path, rev);
|
||||
}
|
||||
onunload() {
|
||||
this.periodicInternalFileScanProcessor?.disable();
|
||||
}
|
||||
onload() {
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "livesync-sync-internal",
|
||||
name: "(re)initialise hidden files between storage and database",
|
||||
callback: () => {
|
||||
@@ -107,7 +105,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "livesync-scaninternal-storage",
|
||||
name: "Scan hidden file changes on the storage",
|
||||
callback: () => {
|
||||
@@ -116,7 +114,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "livesync-scaninternal-database",
|
||||
name: "Scan hidden file changes on the local database",
|
||||
callback: () => {
|
||||
@@ -125,7 +123,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
},
|
||||
});
|
||||
this.plugin.addCommand({
|
||||
this.services.API.addCommand({
|
||||
id: "livesync-internal-scan-offline-changes",
|
||||
name: "Scan and apply all offline hidden-file changes",
|
||||
callback: () => {
|
||||
@@ -244,13 +242,23 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (this.isThisModuleEnabled()) {
|
||||
//system file
|
||||
const filename = this.getPath(doc);
|
||||
if (await this.services.vault.isTargetFile(filename)) {
|
||||
// this.procInternalFile(filename);
|
||||
await this.processReplicationResult(doc);
|
||||
const unprefixedPath = stripAllPrefixes(filename);
|
||||
// No need to check via vaultService
|
||||
// if (!await this.services.vault.isTargetFile(unprefixedPath)) {
|
||||
// this._log(`Skipped processing sync file:${unprefixedPath} (Not target)`, LOG_LEVEL_VERBOSE);
|
||||
// return true;
|
||||
// }
|
||||
if (!(await this.isTargetFile(stripAllPrefixes(unprefixedPath)))) {
|
||||
this._log(
|
||||
`Skipped processing sync file:${unprefixedPath} (Not Hidden File Sync target)`,
|
||||
LOG_LEVEL_VERBOSE
|
||||
);
|
||||
// We should return true, we made sure that document is a internalMetadata.
|
||||
return true;
|
||||
} else {
|
||||
this._log(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
if (!(await this.processReplicationResult(doc))) {
|
||||
this._log(`Failed to process sync file:${unprefixedPath}`, LOG_LEVEL_NOTICE);
|
||||
// Do not yield false, this file had been processed.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -259,7 +267,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async loadFileWithInfo(path: FilePath): Promise<UXFileInfo> {
|
||||
const stat = await this.plugin.storageAccess.statHidden(path);
|
||||
const stat = await this.core.storageAccess.statHidden(path);
|
||||
if (!stat)
|
||||
return {
|
||||
name: path.split("/").pop() ?? "",
|
||||
@@ -274,7 +282,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
deleted: true,
|
||||
body: createBlob(new Uint8Array(0)),
|
||||
};
|
||||
const content = await this.plugin.storageAccess.readHiddenFileAuto(path);
|
||||
const content = await this.core.storageAccess.readHiddenFileAuto(path);
|
||||
return {
|
||||
name: path.split("/").pop() ?? "",
|
||||
path,
|
||||
@@ -296,7 +304,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
return `${doc.mtime}-${doc.size}-${doc._rev}-${doc._deleted || doc.deleted || false ? "-0" : "-1"}`;
|
||||
}
|
||||
async fileToStatKey(file: FilePath, stat: UXStat | null = null) {
|
||||
if (!stat) stat = await this.plugin.storageAccess.statHidden(file);
|
||||
if (!stat) stat = await this.core.storageAccess.statHidden(file);
|
||||
return this.statToKey(stat);
|
||||
}
|
||||
|
||||
@@ -310,7 +318,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async updateLastProcessedAsActualFile(file: FilePath, stat?: UXStat | null | undefined) {
|
||||
if (!stat) stat = await this.plugin.storageAccess.statHidden(file);
|
||||
if (!stat) stat = await this.core.storageAccess.statHidden(file);
|
||||
this._fileInfoLastProcessed.set(file, this.statToKey(stat));
|
||||
}
|
||||
|
||||
@@ -352,38 +360,38 @@ 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));
|
||||
}
|
||||
async ensureDir(path: FilePath) {
|
||||
const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(path);
|
||||
const isExists = await this.core.storageAccess.isExistsIncludeHidden(path);
|
||||
if (!isExists) {
|
||||
await this.plugin.storageAccess.ensureDir(path);
|
||||
await this.core.storageAccess.ensureDir(path);
|
||||
}
|
||||
}
|
||||
|
||||
async writeFile(path: FilePath, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<UXStat | null> {
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(path, data, opt);
|
||||
const stat = await this.plugin.storageAccess.statHidden(path);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(path, data, opt);
|
||||
const stat = await this.core.storageAccess.statHidden(path);
|
||||
// this.updateLastProcessedFile(path, this.statToKey(stat));
|
||||
return stat;
|
||||
}
|
||||
|
||||
async __removeFile(path: FilePath): Promise<"OK" | "ALREADY" | false> {
|
||||
try {
|
||||
if (!(await this.plugin.storageAccess.isExistsIncludeHidden(path))) {
|
||||
if (!(await this.core.storageAccess.isExistsIncludeHidden(path))) {
|
||||
// Already deleted
|
||||
// this.updateLastProcessedFile(path, this.statToKey(null));
|
||||
return "ALREADY";
|
||||
}
|
||||
if (await this.plugin.storageAccess.removeHidden(path)) {
|
||||
if (await this.core.storageAccess.removeHidden(path)) {
|
||||
// this.updateLastProcessedFile(path, this.statToKey(null));
|
||||
return "OK";
|
||||
}
|
||||
@@ -396,7 +404,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
async triggerEvent(path: FilePath) {
|
||||
try {
|
||||
// await this.app.vault.adapter.reconcileInternalFile(filename);
|
||||
await this.plugin.storageAccess.triggerHiddenFile(path);
|
||||
await this.core.storageAccess.triggerHiddenFile(path);
|
||||
} catch (ex) {
|
||||
this._log("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE);
|
||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||
@@ -510,7 +518,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
LOG_LEVEL_VERBOSE
|
||||
);
|
||||
const taskNameAndMeta = [...files].map(
|
||||
async (e) => [e, await this.plugin.storageAccess.statHidden(e)] as const
|
||||
async (e) => [e, await this.core.storageAccess.statHidden(e)] as const
|
||||
);
|
||||
const nameAndMeta = await Promise.all(taskNameAndMeta);
|
||||
const processFiles = nameAndMeta
|
||||
@@ -552,7 +560,7 @@ Offline Changed files: ${processFiles.length}`;
|
||||
}
|
||||
try {
|
||||
return await this.serializedForEvent(path, async () => {
|
||||
let stat = await this.plugin.storageAccess.statHidden(path);
|
||||
let stat = await this.core.storageAccess.statHidden(path);
|
||||
// sometimes folder is coming.
|
||||
if (stat != null && stat.type != "file") {
|
||||
return false;
|
||||
@@ -700,7 +708,7 @@ Offline Changed files: ${processFiles.length}`;
|
||||
revFrom._revs_info
|
||||
?.filter((e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo)
|
||||
.first()?.rev ?? "";
|
||||
const result = await this.plugin.managers.conflictManager.mergeObject(
|
||||
const result = await this.localDatabase.managers.conflictManager.mergeObject(
|
||||
doc.path,
|
||||
commonBase,
|
||||
doc._rev,
|
||||
@@ -807,9 +815,9 @@ Offline Changed files: ${processFiles.length}`;
|
||||
}
|
||||
}
|
||||
if (!keep && result) {
|
||||
const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(storageFilePath);
|
||||
const isExists = await this.core.storageAccess.isExistsIncludeHidden(storageFilePath);
|
||||
if (!isExists) {
|
||||
await this.plugin.storageAccess.ensureDir(storageFilePath);
|
||||
await this.core.storageAccess.ensureDir(storageFilePath);
|
||||
}
|
||||
const stat = await this.writeFile(storageFilePath, result);
|
||||
if (!stat) {
|
||||
@@ -886,7 +894,7 @@ Offline Changed files: ${processFiles.length}`;
|
||||
* @returns An object containing the ignore and target filters.
|
||||
*/
|
||||
parseRegExpSettings() {
|
||||
const regExpKey = `${this.plugin.settings.syncInternalFilesTargetPatterns}||${this.plugin.settings.syncInternalFilesIgnorePatterns}`;
|
||||
const regExpKey = `${this.core.settings.syncInternalFilesTargetPatterns}||${this.core.settings.syncInternalFilesIgnorePatterns}`;
|
||||
let ignoreFilter: CustomRegExp[];
|
||||
let targetFilter: CustomRegExp[];
|
||||
if (this.cacheFileRegExps.has(regExpKey)) {
|
||||
@@ -894,8 +902,8 @@ Offline Changed files: ${processFiles.length}`;
|
||||
ignoreFilter = cached[1];
|
||||
targetFilter = cached[0];
|
||||
} else {
|
||||
ignoreFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
|
||||
targetFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
|
||||
ignoreFilter = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
|
||||
targetFilter = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
|
||||
this.cacheFileRegExps.clear();
|
||||
this.cacheFileRegExps.set(regExpKey, [targetFilter, ignoreFilter]);
|
||||
}
|
||||
@@ -933,7 +941,7 @@ Offline Changed files: ${processFiles.length}`;
|
||||
* @returns An array of ignored file paths (lowercase).
|
||||
*/
|
||||
getCustomisationSynchronizationIgnoredFiles(): string[] {
|
||||
const configDir = this.plugin.app.vault.configDir;
|
||||
const configDir = this.services.API.getSystemConfigDir();
|
||||
const key =
|
||||
JSON.stringify(this.settings.pluginSyncExtendedSetting) + `||${this.settings.usePluginSync}||${configDir}`;
|
||||
if (this.cacheCustomisationSyncIgnoredFiles.has(key)) {
|
||||
@@ -1050,7 +1058,7 @@ Common untracked files: ${bothUntracked.length}`;
|
||||
notifyProgress();
|
||||
const rel = await semaphores.acquire();
|
||||
try {
|
||||
const fileStat = await this.plugin.storageAccess.statHidden(file);
|
||||
const fileStat = await this.core.storageAccess.statHidden(file);
|
||||
if (fileStat == null) {
|
||||
// This should not be happened. But, if it happens, we should skip this.
|
||||
this._log(`Unexpected error: Failed to stat file during applyOfflineChange :${file}`);
|
||||
@@ -1198,7 +1206,7 @@ Offline Changed files: ${files.length}`;
|
||||
// If notified about plug-ins, reloading Obsidian may not be necessary.
|
||||
const updatePluginId = manifest.id;
|
||||
const updatePluginName = manifest.name;
|
||||
this.plugin.confirm.askInPopup(
|
||||
this.core.confirm.askInPopup(
|
||||
`updated-${updatePluginId}`,
|
||||
`Files in ${updatePluginName} has been updated!\nPress {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`,
|
||||
(anchor) => {
|
||||
@@ -1230,9 +1238,9 @@ Offline Changed files: ${files.length}`;
|
||||
}
|
||||
|
||||
// If something changes left, notify for reloading Obsidian.
|
||||
if (updatedFolders.indexOf(this.plugin.app.vault.configDir) >= 0) {
|
||||
if (updatedFolders.indexOf(this.services.API.getSystemConfigDir()) >= 0) {
|
||||
if (!this.services.appLifecycle.isReloadingScheduled()) {
|
||||
this.plugin.confirm.askInPopup(
|
||||
this.core.confirm.askInPopup(
|
||||
`updated-any-hidden`,
|
||||
`Some setting files have been modified\nPress {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`,
|
||||
(anchor) => {
|
||||
@@ -1250,7 +1258,7 @@ Offline Changed files: ${files.length}`;
|
||||
if (this.settings.suppressNotifyHiddenFilesChange) {
|
||||
return;
|
||||
}
|
||||
const configDir = this.plugin.app.vault.configDir;
|
||||
const configDir = this.services.API.getSystemConfigDir();
|
||||
if (!key.startsWith(configDir)) return;
|
||||
const dirName = key.split("/").slice(0, -1).join("/");
|
||||
this.queuedNotificationFiles.add(dirName);
|
||||
@@ -1288,7 +1296,7 @@ Offline Changed files: ${files.length}`;
|
||||
const eachProgress = onlyInNTimes(100, (progress) => p.log(`Checking ${progress}/${allFileNames.size}`));
|
||||
for (const file of allFileNames) {
|
||||
eachProgress();
|
||||
const storageMTime = await this.plugin.storageAccess.statHidden(file);
|
||||
const storageMTime = await this.core.storageAccess.statHidden(file);
|
||||
const mtimeStorage = getComparingMTime(storageMTime);
|
||||
const dbEntry = allDatabaseMap.get(file)!;
|
||||
const mtimeDB = getComparingMTime(dbEntry);
|
||||
@@ -1608,7 +1616,7 @@ Offline Changed files: ${files.length}`;
|
||||
if (onlyNew) {
|
||||
// Check the file is new or not.
|
||||
const dbMTime = getComparingMTime(metaOnDB, includeDeletion); // metaOnDB.mtime;
|
||||
const storageStat = await this.plugin.storageAccess.statHidden(storageFilePath);
|
||||
const storageStat = await this.core.storageAccess.statHidden(storageFilePath);
|
||||
const storageMTimeActual = storageStat?.mtime ?? 0;
|
||||
const storageMTime =
|
||||
storageMTimeActual == 0 ? this.getLastProcessedFileMTime(storageFilePath) : storageMTimeActual;
|
||||
@@ -1662,7 +1670,7 @@ Offline Changed files: ${files.length}`;
|
||||
|
||||
async __checkIsNeedToWriteFile(storageFilePath: FilePath, content: string | ArrayBuffer): Promise<boolean> {
|
||||
try {
|
||||
const storageContent = await this.plugin.storageAccess.readHiddenFileAuto(storageFilePath);
|
||||
const storageContent = await this.core.storageAccess.readHiddenFileAuto(storageFilePath);
|
||||
const needWrite = !(await isDocContentSame(storageContent, content));
|
||||
return needWrite;
|
||||
} catch (ex) {
|
||||
@@ -1674,7 +1682,7 @@ Offline Changed files: ${files.length}`;
|
||||
|
||||
async __writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise<false | UXStat> {
|
||||
try {
|
||||
const statBefore = await this.plugin.storageAccess.statHidden(storageFilePath);
|
||||
const statBefore = await this.core.storageAccess.statHidden(storageFilePath);
|
||||
const isExist = statBefore != null;
|
||||
const writeContent = readContent(fileOnDB);
|
||||
await this.ensureDir(storageFilePath);
|
||||
@@ -1760,7 +1768,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
choices.push(CHOICE_MERGE);
|
||||
choices.push(CHOICE_DISABLE);
|
||||
|
||||
const ret = await this.plugin.confirm.confirmWithMessage(
|
||||
const ret = await this.core.confirm.confirmWithMessage(
|
||||
"Hidden file sync",
|
||||
message,
|
||||
choices,
|
||||
@@ -1779,12 +1787,12 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
}
|
||||
|
||||
private _allSuspendExtraSync(): Promise<boolean> {
|
||||
if (this.plugin.settings.syncInternalFiles) {
|
||||
if (this.core.settings.syncInternalFiles) {
|
||||
this._log(
|
||||
"Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this.plugin.settings.syncInternalFiles = false;
|
||||
this.core.settings.syncInternalFiles = false;
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -1807,9 +1815,15 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
}
|
||||
|
||||
if (mode == "DISABLE" || mode == "DISABLE_HIDDEN") {
|
||||
// await this.plugin.$allSuspendExtraSync();
|
||||
this.plugin.settings.syncInternalFiles = false;
|
||||
await this.plugin.saveSettings();
|
||||
// await this.core.$allSuspendExtraSync();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
syncInternalFiles: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
// this.core.settings.syncInternalFiles = false;
|
||||
// await this.core.saveSettings();
|
||||
return;
|
||||
}
|
||||
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
|
||||
@@ -1820,10 +1834,17 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
} else if (mode == "MERGE") {
|
||||
await this.initialiseInternalFileSync("safe", true);
|
||||
}
|
||||
this.plugin.settings.useAdvancedMode = true;
|
||||
this.plugin.settings.syncInternalFiles = true;
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
useAdvancedMode: true,
|
||||
syncInternalFiles: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
// this.plugin.settings.useAdvancedMode = true;
|
||||
// this.plugin.settings.syncInternalFiles = true;
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
// await this.plugin.saveSettings();
|
||||
this._log(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
// <-- Configuration handling
|
||||
@@ -1843,7 +1864,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
const files = fileNames.map(async (e) => {
|
||||
return {
|
||||
path: e,
|
||||
stat: await this.plugin.storageAccess.statHidden(e), // this.plugin.vaultAccess.adapterStat(e)
|
||||
stat: await this.core.storageAccess.statHidden(e), // this.plugin.vaultAccess.adapterStat(e)
|
||||
};
|
||||
});
|
||||
const result: InternalFileInfo[] = [];
|
||||
@@ -1948,5 +1969,6 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
services.setting.suspendExtraSync.addHandler(this._allSuspendExtraSync.bind(this));
|
||||
services.setting.suggestOptionalFeatures.addHandler(this._allAskUsingOptionalSyncFeature.bind(this));
|
||||
services.setting.enableOptionalFeature.addHandler(this._allConfigureOptionalSyncFeature.bind(this));
|
||||
services.vault.isTargetFileInExtra.addHandler(this.isTargetFile.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,22 @@ import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils.ts";
|
||||
|
||||
let noticeIndex = 0;
|
||||
export abstract class LiveSyncCommands {
|
||||
/**
|
||||
* @deprecated This class is deprecated. Please use core
|
||||
*/
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
core: LiveSyncCore;
|
||||
get app() {
|
||||
return this.plugin.app;
|
||||
}
|
||||
get settings() {
|
||||
return this.plugin.settings;
|
||||
return this.core.settings;
|
||||
}
|
||||
get localDatabase() {
|
||||
return this.plugin.localDatabase;
|
||||
return this.core.localDatabase;
|
||||
}
|
||||
get services() {
|
||||
return this.plugin.services;
|
||||
return this.core.services;
|
||||
}
|
||||
|
||||
// id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
|
||||
@@ -41,9 +45,10 @@ export abstract class LiveSyncCommands {
|
||||
return this.services.path.getPath(entry);
|
||||
}
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||
this.plugin = plugin;
|
||||
this.onBindFunction(plugin, plugin.services);
|
||||
this.core = core;
|
||||
this.onBindFunction(this.core, this.core.services);
|
||||
this._log = createInstanceLogFunction(this.constructor.name, this.services.API);
|
||||
__$checkInstanceBinding(this);
|
||||
}
|
||||
@@ -51,7 +56,7 @@ export abstract class LiveSyncCommands {
|
||||
abstract onload(): void | Promise<void>;
|
||||
|
||||
_isMainReady() {
|
||||
return this.plugin.services.appLifecycle.isReady();
|
||||
return this.services.appLifecycle.isReady();
|
||||
}
|
||||
_isMainSuspended() {
|
||||
return this.services.appLifecycle.isSuspended();
|
||||
|
||||
@@ -71,7 +71,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
|
||||
|
||||
async confirm(title: string, message: string, affirmative = "Yes", negative = "No") {
|
||||
return (
|
||||
(await this.plugin.confirm.askSelectStringDialogue(message, [affirmative, negative], {
|
||||
(await this.core.confirm.askSelectStringDialogue(message, [affirmative, negative], {
|
||||
title,
|
||||
defaultAction: affirmative,
|
||||
})) === affirmative
|
||||
@@ -302,7 +302,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
}
|
||||
|
||||
async scanUnusedChunks() {
|
||||
const kvDB = this.plugin.kvDB;
|
||||
const kvDB = this.core.kvDB;
|
||||
const chunkSet = (await kvDB.get<Set<DocumentID>>(DB_KEY_CHUNK_SET)) || new Set();
|
||||
const chunkUsageMap = (await kvDB.get<ChunkUsageMap>(DB_KEY_DOC_USAGE_MAP)) || new Map();
|
||||
const KEEP_MAX_REVS = 10;
|
||||
@@ -328,7 +328,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
async trackChanges(fromStart: boolean = false, showNotice: boolean = false) {
|
||||
if (!this.isAvailable()) return;
|
||||
const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
const kvDB = this.plugin.kvDB;
|
||||
const kvDB = this.core.kvDB;
|
||||
|
||||
const previousSeq = fromStart ? "" : await kvDB.get<string>(DB_KEY_SEQ);
|
||||
const chunkSet = (await kvDB.get<Set<DocumentID>>(DB_KEY_CHUNK_SET)) || new Set();
|
||||
@@ -457,7 +457,7 @@ Are you ready to delete unused chunks?`;
|
||||
const BUTTON_OK = `Yes, delete chunks`;
|
||||
const BUTTON_CANCEL = "Cancel";
|
||||
|
||||
const result = await this.plugin.confirm.askSelectStringDialogue(
|
||||
const result = await this.core.confirm.askSelectStringDialogue(
|
||||
confirmMessage,
|
||||
[BUTTON_OK, BUTTON_CANCEL] as const,
|
||||
{
|
||||
@@ -506,7 +506,7 @@ Are you ready to delete unused chunks?`;
|
||||
const message = `Garbage Collection completed.
|
||||
Success: ${successCount}, Errored: ${errored}`;
|
||||
this._log(message, logLevel);
|
||||
const kvDB = this.plugin.kvDB;
|
||||
const kvDB = this.core.kvDB;
|
||||
await kvDB.set(DB_KEY_CHUNK_SET, chunkSet);
|
||||
}
|
||||
|
||||
@@ -723,7 +723,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
}
|
||||
|
||||
async compactDatabase() {
|
||||
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
|
||||
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
|
||||
const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true);
|
||||
if (!remote) {
|
||||
this._notice("Failed to connect to remote for compaction.", "gc-compact");
|
||||
@@ -767,7 +767,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
// Temporarily set revs_limit to 1, perform compaction, and restore the original revs_limit.
|
||||
// Very dangerous operation, so now suppressed.
|
||||
return false;
|
||||
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
|
||||
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
|
||||
const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true);
|
||||
if (!remote) {
|
||||
this._notice("Failed to connect to remote for compaction.");
|
||||
@@ -822,7 +822,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
}
|
||||
async gcv3() {
|
||||
if (!this.isAvailable()) return;
|
||||
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
|
||||
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
|
||||
// Start one-shot replication to ensure all changes are synced before GC.
|
||||
const r0 = await replicator.openOneShotReplication(this.settings, false, false, "sync");
|
||||
if (!r0) {
|
||||
@@ -835,7 +835,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
// Delete the chunk, but first verify the following:
|
||||
// Fetch the list of accepted nodes from the replicator.
|
||||
const OPTION_CANCEL = "Cancel Garbage Collection";
|
||||
const info = await this.plugin.replicator.getConnectedDeviceList();
|
||||
const info = await this.core.replicator.getConnectedDeviceList();
|
||||
if (!info) {
|
||||
this._notice("No connected device information found. Cancelling Garbage Collection.");
|
||||
return;
|
||||
@@ -855,7 +855,7 @@ It is preferable to update all devices if possible. If you have any devices that
|
||||
const OPTION_IGNORE = "Ignore and Proceed";
|
||||
// const OPTION_DELETE = "Delete them and proceed";
|
||||
const buttons = [OPTION_CANCEL, OPTION_IGNORE] as const;
|
||||
const result = await this.plugin.confirm.askSelectStringDialogue(message, buttons, {
|
||||
const result = await this.core.confirm.askSelectStringDialogue(message, buttons, {
|
||||
title: "Node Information Missing",
|
||||
defaultAction: OPTION_CANCEL,
|
||||
});
|
||||
@@ -896,7 +896,7 @@ This may indicate that some devices have not completed synchronisation, which co
|
||||
: `All devices have the same progress value (${maxProgress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.`;
|
||||
const buttons = [OPTION_PROCEED, OPTION_CANCEL] as const;
|
||||
const defaultAction = progressDifference != 0 ? OPTION_CANCEL : OPTION_PROCEED;
|
||||
const result = await this.plugin.confirm.askSelectStringDialogue(message + "\n\n" + detail, buttons, {
|
||||
const result = await this.core.confirm.askSelectStringDialogue(message + "\n\n" + detail, buttons, {
|
||||
title: "Garbage Collection Confirmation",
|
||||
defaultAction,
|
||||
});
|
||||
|
||||
@@ -38,14 +38,14 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
storeP2PStatusLine = reactiveSource("");
|
||||
|
||||
getSettings(): P2PSyncSetting {
|
||||
return this.plugin.settings;
|
||||
return this.core.settings;
|
||||
}
|
||||
getDB() {
|
||||
return this.plugin.localDatabase.localDatabase;
|
||||
return this.core.localDatabase.localDatabase;
|
||||
}
|
||||
|
||||
get confirm(): Confirm {
|
||||
return this.plugin.confirm;
|
||||
return this.core.confirm;
|
||||
}
|
||||
_simpleStore!: SimpleStore<any>;
|
||||
|
||||
@@ -53,8 +53,8 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
return this._simpleStore;
|
||||
}
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
super(plugin);
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||
super(plugin, core);
|
||||
setReplicatorFunc(() => this._replicatorInstance);
|
||||
addP2PEventHandlers(this);
|
||||
this.afterConstructor();
|
||||
@@ -72,7 +72,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
|
||||
const settings = { ...this.settings, ...settingOverride };
|
||||
if (settings.remoteType == REMOTE_P2P) {
|
||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
|
||||
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin.core));
|
||||
}
|
||||
return undefined!;
|
||||
}
|
||||
@@ -183,12 +183,12 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
}
|
||||
|
||||
private async _allSuspendExtraSync() {
|
||||
this.plugin.settings.P2P_Enabled = false;
|
||||
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
|
||||
this.plugin.settings.P2P_AutoBroadcast = false;
|
||||
this.plugin.settings.P2P_AutoStart = false;
|
||||
this.plugin.settings.P2P_AutoSyncPeers = "";
|
||||
this.plugin.settings.P2P_AutoWatchPeers = "";
|
||||
this.plugin.core.settings.P2P_Enabled = false;
|
||||
this.plugin.core.settings.P2P_AutoAccepting = AutoAccepting.NONE;
|
||||
this.plugin.core.settings.P2P_AutoBroadcast = false;
|
||||
this.plugin.core.settings.P2P_AutoStart = false;
|
||||
this.plugin.core.settings.P2P_AutoSyncPeers = "";
|
||||
this.plugin.core.settings.P2P_AutoWatchPeers = "";
|
||||
return await Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -201,7 +201,10 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
|
||||
}
|
||||
|
||||
async _everyOnloadStart(): Promise<boolean> {
|
||||
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
|
||||
this.plugin.registerView(
|
||||
VIEW_TYPE_P2P,
|
||||
(leaf) => new P2PReplicatorPaneView(leaf, this.plugin.core, this.plugin)
|
||||
);
|
||||
this.plugin.addCommand({
|
||||
id: "open-p2p-replicator",
|
||||
name: "P2P Sync : Open P2P Replicator",
|
||||
|
||||
@@ -20,17 +20,18 @@
|
||||
import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator";
|
||||
import { $msg as _msg } from "../../../lib/src/common/i18n";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../lib/src/common/types";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
|
||||
interface Props {
|
||||
plugin: PluginShim;
|
||||
cmdSync: CommandShim;
|
||||
core: LiveSyncBaseCore;
|
||||
}
|
||||
|
||||
let { plugin, cmdSync }: Props = $props();
|
||||
let { cmdSync, core }: Props = $props();
|
||||
// const cmdSync = plugin.getAddOn<P2PReplicator>("P2PReplicator")!;
|
||||
setContext("getReplicator", () => cmdSync);
|
||||
|
||||
const initialSettings = { ...plugin.settings };
|
||||
const currentSettings = () => core.services.setting.currentSettings() as P2PSyncSetting;
|
||||
const initialSettings = { ...currentSettings() } as P2PSyncSetting;
|
||||
|
||||
let settings = $state<P2PSyncSetting>(initialSettings);
|
||||
|
||||
@@ -70,21 +71,33 @@
|
||||
);
|
||||
|
||||
async function saveAndApply() {
|
||||
const newSettings = {
|
||||
...plugin.settings,
|
||||
P2P_Enabled: eP2PEnabled,
|
||||
P2P_relays: eRelay,
|
||||
P2P_roomID: eRoomId,
|
||||
P2P_passphrase: ePassword,
|
||||
P2P_AppID: eAppId,
|
||||
P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
|
||||
P2P_AutoStart: eAutoStart,
|
||||
P2P_AutoBroadcast: eAutoBroadcast,
|
||||
};
|
||||
plugin.settings = newSettings;
|
||||
// const newSettings = {
|
||||
// ...currentSettings(),
|
||||
// P2P_Enabled: eP2PEnabled,
|
||||
// P2P_relays: eRelay,
|
||||
// P2P_roomID: eRoomId,
|
||||
// P2P_passphrase: ePassword,
|
||||
// P2P_AppID: eAppId,
|
||||
// P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
|
||||
// P2P_AutoStart: eAutoStart,
|
||||
// P2P_AutoBroadcast: eAutoBroadcast,
|
||||
// };
|
||||
await core.services.setting.applyPartial(
|
||||
{
|
||||
P2P_Enabled: eP2PEnabled,
|
||||
P2P_relays: eRelay,
|
||||
P2P_roomID: eRoomId,
|
||||
P2P_passphrase: ePassword,
|
||||
P2P_AppID: eAppId,
|
||||
P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
|
||||
P2P_AutoStart: eAutoStart,
|
||||
P2P_AutoBroadcast: eAutoBroadcast,
|
||||
},
|
||||
true
|
||||
);
|
||||
cmdSync.setConfig(SETTING_KEY_P2P_DEVICE_NAME, eDeviceName);
|
||||
deviceName = eDeviceName;
|
||||
await plugin.saveSettings();
|
||||
// await plugin.saveSettings();
|
||||
}
|
||||
async function revert() {
|
||||
eP2PEnabled = settings.P2P_Enabled;
|
||||
@@ -100,8 +113,9 @@
|
||||
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
|
||||
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
|
||||
const applyLoadSettings = (d: P2PSyncSetting, force: boolean) => {
|
||||
if(force){
|
||||
const initDeviceName = cmdSync.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? plugin.services.vault.getVaultName();
|
||||
if (force) {
|
||||
const initDeviceName =
|
||||
cmdSync.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? core.services.vault.getVaultName();
|
||||
deviceName = initDeviceName;
|
||||
eDeviceName = initDeviceName;
|
||||
}
|
||||
@@ -124,7 +138,7 @@
|
||||
closeServer();
|
||||
});
|
||||
const rx = eventHub.onEvent(EVENT_LAYOUT_READY, () => {
|
||||
applyLoadSettings(plugin.settings, true);
|
||||
applyLoadSettings(currentSettings(), true);
|
||||
});
|
||||
const r2 = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
|
||||
serverInfo = status;
|
||||
@@ -254,7 +268,7 @@
|
||||
cmdSync.setConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
|
||||
});
|
||||
let isObsidian = $derived.by(() => {
|
||||
return plugin.services.API.getPlatform() === "obsidian";
|
||||
return core.services.API.getPlatform() === "obsidian";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
||||
type PeerStatus,
|
||||
} from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
export const VIEW_TYPE_P2P = "p2p-replicator";
|
||||
|
||||
function addToList(item: string, list: string) {
|
||||
@@ -34,7 +35,8 @@ function removeFromList(item: string, list: string) {
|
||||
}
|
||||
|
||||
export class P2PReplicatorPaneView extends SvelteItemView {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
// plugin: ObsidianLiveSyncPlugin;
|
||||
core: LiveSyncBaseCore;
|
||||
override icon = "waypoints";
|
||||
title: string = "";
|
||||
override navigation = false;
|
||||
@@ -43,7 +45,7 @@ export class P2PReplicatorPaneView extends SvelteItemView {
|
||||
return "waypoints";
|
||||
}
|
||||
get replicator() {
|
||||
const r = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
|
||||
const r = this.core.getAddOn<P2PReplicator>(P2PReplicator.name);
|
||||
if (!r || !r._replicatorInstance) {
|
||||
throw new Error("Replicator not found");
|
||||
}
|
||||
@@ -66,7 +68,7 @@ export class P2PReplicatorPaneView extends SvelteItemView {
|
||||
const DROP = "Yes, and drop local database";
|
||||
const KEEP = "Yes, but keep local database";
|
||||
const CANCEL = "No, cancel";
|
||||
const yn = await this.plugin.confirm.askSelectStringDialogue(
|
||||
const yn = await this.core.confirm.askSelectStringDialogue(
|
||||
`Do you really want to apply the remote config? This will overwrite your current config immediately and restart.
|
||||
And you can also drop the local database to rebuild from the remote device.`,
|
||||
[DROP, KEEP, CANCEL] as const,
|
||||
@@ -78,7 +80,7 @@ And you can also drop the local database to rebuild from the remote device.`,
|
||||
if (yn === DROP || yn === KEEP) {
|
||||
if (yn === DROP) {
|
||||
if (remoteConfig.remoteType !== REMOTE_P2P) {
|
||||
const yn2 = await this.plugin.confirm.askYesNoDialog(
|
||||
const yn2 = await this.core.confirm.askYesNoDialog(
|
||||
`Do you want to set the remote type to "P2P Sync" to rebuild by "P2P replication"?`,
|
||||
{
|
||||
title: "Rebuild from remote device",
|
||||
@@ -90,12 +92,14 @@ And you can also drop the local database to rebuild from the remote device.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
this.plugin.settings = remoteConfig;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// this.plugin.settings = remoteConfig;
|
||||
// await this.plugin.saveSettings();
|
||||
await this.core.services.setting.applyPartial(remoteConfig);
|
||||
if (yn === DROP) {
|
||||
await this.plugin.rebuilder.scheduleFetch();
|
||||
await this.core.rebuilder.scheduleFetch();
|
||||
} else {
|
||||
this.plugin.services.appLifecycle.scheduleRestart();
|
||||
this.core.services.appLifecycle.scheduleRestart();
|
||||
}
|
||||
} else {
|
||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||
@@ -113,19 +117,24 @@ And you can also drop the local database to rebuild from the remote device.`,
|
||||
} as const;
|
||||
|
||||
const targetSetting = settingMap[prop];
|
||||
const currentSettingAll = this.core.services.setting.currentSettings();
|
||||
const currentSetting = {
|
||||
[targetSetting]: currentSettingAll ? currentSettingAll[targetSetting] : "",
|
||||
};
|
||||
if (peer[prop]) {
|
||||
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
// this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
|
||||
// await this.plugin.saveSettings();
|
||||
currentSetting[targetSetting] = removeFromList(peer.name, currentSetting[targetSetting]);
|
||||
} else {
|
||||
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
|
||||
await this.plugin.saveSettings();
|
||||
currentSetting[targetSetting] = addToList(peer.name, currentSetting[targetSetting]);
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
await this.core.services.setting.applyPartial(currentSetting, true);
|
||||
}
|
||||
m?: Menu;
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
|
||||
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, plugin: ObsidianLiveSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
// this.plugin = plugin;
|
||||
this.core = core;
|
||||
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
|
||||
if (this.m) {
|
||||
this.m.hide();
|
||||
@@ -183,15 +192,15 @@ And you can also drop the local database to rebuild from the remote device.`,
|
||||
}
|
||||
}
|
||||
instantiateComponent(target: HTMLElement) {
|
||||
const cmdSync = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
|
||||
const cmdSync = this.core.getAddOn<P2PReplicator>(P2PReplicator.name);
|
||||
if (!cmdSync) {
|
||||
throw new Error("Replicator not found");
|
||||
}
|
||||
return mount(ReplicatorPaneComponent, {
|
||||
target: target,
|
||||
props: {
|
||||
plugin: cmdSync.plugin,
|
||||
cmdSync: cmdSync,
|
||||
core: this.core,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 4af350bb67...7989f57e06
466
src/main.ts
466
src/main.ts
@@ -1,399 +1,110 @@
|
||||
import { Notice, Plugin, type App, type PluginManifest } from "./deps";
|
||||
import {
|
||||
type EntryDoc,
|
||||
type ObsidianLiveSyncSettings,
|
||||
type HasSettings,
|
||||
LOG_LEVEL_INFO,
|
||||
} from "./lib/src/common/types.ts";
|
||||
import { type SimpleStore } from "./lib/src/common/utils.ts";
|
||||
import { type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
|
||||
import { type LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator.js";
|
||||
|
||||
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
||||
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
||||
import { type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
|
||||
import { type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
|
||||
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
|
||||
import type { IObsidianModule } from "./modules/AbstractObsidianModule.ts";
|
||||
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
||||
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
|
||||
|
||||
import { ModuleCheckRemoteSize } from "./modules/essentialObsidian/ModuleCheckRemoteSize.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 { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
||||
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
||||
import { SetupManager } from "./modules/features/SetupManager.ts";
|
||||
import type { StorageAccess } from "@lib/interfaces/StorageAccess.ts";
|
||||
import type { Confirm } from "./lib/src/interfaces/Confirm.ts";
|
||||
import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder.ts";
|
||||
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts";
|
||||
import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidianEvents.ts";
|
||||
import { AbstractModule } from "./modules/AbstractModule.ts";
|
||||
import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts";
|
||||
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 { ModuleReplicator } from "./modules/core/ModuleReplicator.ts";
|
||||
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB.ts";
|
||||
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO.ts";
|
||||
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess.ts";
|
||||
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker.ts";
|
||||
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks.ts";
|
||||
import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts";
|
||||
import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
|
||||
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
|
||||
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
|
||||
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
|
||||
import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts";
|
||||
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
|
||||
import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts";
|
||||
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder.ts";
|
||||
import type { IFileHandler } from "@lib/interfaces/FileHandler.ts";
|
||||
import { ServiceDatabaseFileAccess } from "@/serviceModules/DatabaseFileAccess.ts";
|
||||
import { ServiceFileAccessObsidian } from "@/serviceModules/ServiceFileAccessImpl.ts";
|
||||
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager.ts";
|
||||
import { __$checkInstanceBinding } from "./lib/src/dev/checks.ts";
|
||||
import { ServiceFileHandler } from "./serviceModules/FileHandler.ts";
|
||||
import { FileAccessObsidian } from "./serviceModules/FileAccessObsidian.ts";
|
||||
import { StorageEventManagerObsidian } from "./managers/StorageEventManagerObsidian.ts";
|
||||
import { onLayoutReadyFeatures } from "./serviceFeatures/onLayoutReady.ts";
|
||||
import type { ServiceModules } from "./types.ts";
|
||||
import { useTargetFilters } from "@lib/serviceFeatures/targetFilter.ts";
|
||||
import { setNoticeClass } from "@lib/mock_and_interop/wrapper.ts";
|
||||
import type { ObsidianServiceContext } from "./lib/src/services/implements/obsidian/ObsidianServiceContext.ts";
|
||||
import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts";
|
||||
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
|
||||
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
|
||||
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
|
||||
import { SetupManager } from "./modules/features/SetupManager.ts";
|
||||
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
|
||||
import { enableI18nFeature } from "./serviceFeatures/onLayoutReady/enablei18n.ts";
|
||||
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
|
||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
core: LiveSyncCore;
|
||||
|
||||
export default class ObsidianLiveSyncPlugin
|
||||
extends Plugin
|
||||
implements
|
||||
LiveSyncLocalDBEnv,
|
||||
LiveSyncReplicatorEnv,
|
||||
LiveSyncJournalReplicatorEnv,
|
||||
LiveSyncCouchDBReplicatorEnv,
|
||||
HasSettings<ObsidianLiveSyncSettings>
|
||||
{
|
||||
/**
|
||||
* The service hub for managing all services.
|
||||
*/
|
||||
_services: InjectableServiceHub<ServiceContext> | undefined = undefined;
|
||||
|
||||
get services() {
|
||||
if (!this._services) {
|
||||
throw new Error("Services not initialised yet");
|
||||
}
|
||||
return this._services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service Modules
|
||||
*/
|
||||
protected _serviceModules: ServiceModules;
|
||||
|
||||
get serviceModules() {
|
||||
return this._serviceModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* addOns: Non-essential and graphically features
|
||||
*/
|
||||
addOns = [] as LiveSyncCommands[];
|
||||
|
||||
/**
|
||||
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
|
||||
*/
|
||||
private modules = [
|
||||
// Move to registerModules
|
||||
] as (IObsidianModule | AbstractModule)[];
|
||||
|
||||
/**
|
||||
* register an add-onn to the plug-in.
|
||||
* Add-ons are features that are not essential to the core functionality of the plugin,
|
||||
* @param addOn
|
||||
*/
|
||||
private _registerAddOn(addOn: LiveSyncCommands) {
|
||||
this.addOns.push(addOn);
|
||||
this.services.appLifecycle.onUnload.addHandler(() => Promise.resolve(addOn.onunload()).then(() => true));
|
||||
}
|
||||
|
||||
private registerAddOns() {
|
||||
this._registerAddOn(new ConfigSync(this));
|
||||
this._registerAddOn(new HiddenFileSync(this));
|
||||
this._registerAddOn(new LocalDatabaseMaintenance(this));
|
||||
this._registerAddOn(new P2PReplicator(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an add-on by its class name. Returns undefined if not found.
|
||||
* @param cls
|
||||
* @returns
|
||||
*/
|
||||
getAddOn<T extends LiveSyncCommands>(cls: string) {
|
||||
for (const addon of this.addOns) {
|
||||
if (addon.constructor.name == cls) return addon as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a module by its class. Throws an error if not found.
|
||||
* Mostly used for getting SetupManager.
|
||||
* @param constructor
|
||||
* @returns
|
||||
*/
|
||||
getModule<T extends IObsidianModule>(constructor: new (...args: any[]) => T): T {
|
||||
for (const module of this.modules) {
|
||||
if (module.constructor === constructor) return module as T;
|
||||
}
|
||||
throw new Error(`Module ${constructor} not found or not loaded.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module to the plug-in.
|
||||
* @param module The module to register.
|
||||
*/
|
||||
private _registerModule(module: IObsidianModule) {
|
||||
this.modules.push(module);
|
||||
}
|
||||
private registerModules() {
|
||||
this._registerModule(new ModuleLiveSyncMain(this));
|
||||
this._registerModule(new ModuleConflictChecker(this));
|
||||
this._registerModule(new ModuleReplicatorMinIO(this));
|
||||
this._registerModule(new ModuleReplicatorCouchDB(this));
|
||||
this._registerModule(new ModuleReplicator(this));
|
||||
this._registerModule(new ModuleConflictResolver(this));
|
||||
this._registerModule(new ModulePeriodicProcess(this));
|
||||
this._registerModule(new ModuleInitializerFile(this));
|
||||
this._registerModule(new ModuleObsidianEvents(this, this));
|
||||
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
|
||||
this._registerModule(new ModuleObsidianSettingsAsMarkdown(this));
|
||||
this._registerModule(new ModuleObsidianSettingDialogue(this, this));
|
||||
this._registerModule(new ModuleLog(this, this));
|
||||
this._registerModule(new ModuleObsidianMenu(this));
|
||||
this._registerModule(new ModuleSetupObsidian(this));
|
||||
this._registerModule(new ModuleObsidianDocumentHistory(this, this));
|
||||
this._registerModule(new ModuleMigration(this));
|
||||
this._registerModule(new ModuleRedFlag(this));
|
||||
this._registerModule(new ModuleInteractiveConflictResolver(this, this));
|
||||
this._registerModule(new ModuleObsidianGlobalHistory(this, this));
|
||||
this._registerModule(new ModuleCheckRemoteSize(this));
|
||||
// Test and Dev Modules
|
||||
this._registerModule(new ModuleDev(this, this));
|
||||
this._registerModule(new ModuleReplicateTest(this, this));
|
||||
this._registerModule(new ModuleIntegratedTest(this, this));
|
||||
this._registerModule(new SetupManager(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind module functions to services.
|
||||
*/
|
||||
private bindModuleFunctions() {
|
||||
for (const module of this.modules) {
|
||||
if (module instanceof AbstractModule) {
|
||||
module.onBindFunction(this, this.services);
|
||||
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
|
||||
} else {
|
||||
this.services.API.addLog(
|
||||
`Module ${module.constructor.name} does not have onBindFunction, skipping binding.`,
|
||||
LOG_LEVEL_INFO
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user.
|
||||
*/
|
||||
get confirm(): Confirm {
|
||||
return this.services.UI.confirm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in.
|
||||
*/
|
||||
get settings() {
|
||||
return this.services.setting.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.setting.settings instead. Set the settings of the plug-in.
|
||||
*/
|
||||
set settings(value: ObsidianLiveSyncSettings) {
|
||||
this.services.setting.settings = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in.
|
||||
* @returns The current settings of the plug-in.
|
||||
*/
|
||||
getSettings(): ObsidianLiveSyncSettings {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.localDatabase instead. The local database instance.
|
||||
*/
|
||||
get localDatabase() {
|
||||
return this.services.database.localDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.managers instead. The database managers, including entry manager, revision manager, etc.
|
||||
*/
|
||||
get managers() {
|
||||
return this.services.database.managers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database.
|
||||
* @returns The PouchDB database instance.
|
||||
*/
|
||||
getDatabase(): PouchDB.Database<EntryDoc> {
|
||||
return this.localDatabase.localDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc.
|
||||
*/
|
||||
get simpleStore() {
|
||||
return this.services.keyValueDB.simpleStore as SimpleStore<CheckPointInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time.
|
||||
*/
|
||||
get replicator() {
|
||||
return this.services.replicator.getActiveReplicator()!;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc.
|
||||
*/
|
||||
get kvDB() {
|
||||
return this.services.keyValueDB.kvDB;
|
||||
}
|
||||
|
||||
/// Modules which were relied on services
|
||||
/**
|
||||
* Storage Accessor for handling file operations.
|
||||
* @obsolete Use serviceModules.storageAccess instead.
|
||||
*/
|
||||
get storageAccess(): StorageAccess {
|
||||
return this.serviceModules.storageAccess;
|
||||
}
|
||||
/**
|
||||
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
|
||||
* @obsolete Use serviceModules.databaseFileAccess instead.
|
||||
*/
|
||||
get databaseFileAccess(): DatabaseFileAccess {
|
||||
return this.serviceModules.databaseFileAccess;
|
||||
}
|
||||
/**
|
||||
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
|
||||
* @obsolete Use serviceModules.fileHandler instead.
|
||||
*/
|
||||
get fileHandler(): IFileHandler {
|
||||
return this.serviceModules.fileHandler;
|
||||
}
|
||||
/**
|
||||
* Rebuilder for handling database rebuilding operations.
|
||||
* @obsolete Use serviceModules.rebuilder instead.
|
||||
*/
|
||||
get rebuilder(): Rebuilder {
|
||||
return this.serviceModules.rebuilder;
|
||||
}
|
||||
|
||||
// requestCount = reactiveSource(0);
|
||||
// responseCount = reactiveSource(0);
|
||||
// totalQueued = reactiveSource(0);
|
||||
// batched = reactiveSource(0);
|
||||
// processing = reactiveSource(0);
|
||||
// databaseQueueCount = reactiveSource(0);
|
||||
// storageApplyingCount = reactiveSource(0);
|
||||
// replicationResultCount = reactiveSource(0);
|
||||
|
||||
// pendingFileEventCount = reactiveSource(0);
|
||||
// processingFileEventCount = reactiveSource(0);
|
||||
|
||||
// _totalProcessingCount?: ReactiveValue<number>;
|
||||
|
||||
// replicationStat = reactiveSource({
|
||||
// sent: 0,
|
||||
// arrived: 0,
|
||||
// maxPullSeq: 0,
|
||||
// maxPushSeq: 0,
|
||||
// lastSyncPullSeq: 0,
|
||||
// lastSyncPushSeq: 0,
|
||||
// syncStatus: "CLOSED" as DatabaseConnectingStatus,
|
||||
// });
|
||||
|
||||
private initialiseServices() {
|
||||
this._services = new ObsidianServiceHub(this);
|
||||
}
|
||||
/**
|
||||
* Initialise service modules.
|
||||
*/
|
||||
private initialiseServiceModules() {
|
||||
private initialiseServiceModules(
|
||||
core: LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>,
|
||||
services: InjectableServiceHub<ObsidianServiceContext>
|
||||
): ServiceModules {
|
||||
const storageAccessManager = new StorageAccessManager();
|
||||
// If we want to implement to the other platform, implement ObsidianXXXXXService.
|
||||
const vaultAccess = new FileAccessObsidian(this.app, {
|
||||
storageAccessManager: storageAccessManager,
|
||||
vaultService: this.services.vault,
|
||||
settingService: this.services.setting,
|
||||
APIService: this.services.API,
|
||||
vaultService: services.vault,
|
||||
settingService: services.setting,
|
||||
APIService: services.API,
|
||||
pathService: services.path,
|
||||
});
|
||||
const storageEventManager = new StorageEventManagerObsidian(this, this, {
|
||||
fileProcessing: this.services.fileProcessing,
|
||||
setting: this.services.setting,
|
||||
vaultService: this.services.vault,
|
||||
const storageEventManager = new StorageEventManagerObsidian(this, core, {
|
||||
fileProcessing: services.fileProcessing,
|
||||
setting: services.setting,
|
||||
vaultService: services.vault,
|
||||
storageAccessManager: storageAccessManager,
|
||||
APIService: this.services.API,
|
||||
APIService: services.API,
|
||||
});
|
||||
const storageAccess = new ServiceFileAccessObsidian({
|
||||
API: this.services.API,
|
||||
setting: this.services.setting,
|
||||
fileProcessing: this.services.fileProcessing,
|
||||
vault: this.services.vault,
|
||||
appLifecycle: this.services.appLifecycle,
|
||||
API: services.API,
|
||||
setting: services.setting,
|
||||
fileProcessing: services.fileProcessing,
|
||||
vault: services.vault,
|
||||
appLifecycle: services.appLifecycle,
|
||||
storageEventManager: storageEventManager,
|
||||
storageAccessManager: storageAccessManager,
|
||||
vaultAccess: vaultAccess,
|
||||
});
|
||||
|
||||
const databaseFileAccess = new ServiceDatabaseFileAccess({
|
||||
API: this.services.API,
|
||||
database: this.services.database,
|
||||
path: this.services.path,
|
||||
API: services.API,
|
||||
database: services.database,
|
||||
path: services.path,
|
||||
storageAccess: storageAccess,
|
||||
vault: this.services.vault,
|
||||
vault: services.vault,
|
||||
});
|
||||
|
||||
const fileHandler = new ServiceFileHandler({
|
||||
API: this.services.API,
|
||||
API: services.API,
|
||||
databaseFileAccess: databaseFileAccess,
|
||||
conflict: this.services.conflict,
|
||||
setting: this.services.setting,
|
||||
fileProcessing: this.services.fileProcessing,
|
||||
vault: this.services.vault,
|
||||
path: this.services.path,
|
||||
replication: this.services.replication,
|
||||
conflict: services.conflict,
|
||||
setting: services.setting,
|
||||
fileProcessing: services.fileProcessing,
|
||||
vault: services.vault,
|
||||
path: services.path,
|
||||
replication: services.replication,
|
||||
storageAccess: storageAccess,
|
||||
});
|
||||
const rebuilder = new ServiceRebuilder({
|
||||
API: this.services.API,
|
||||
database: this.services.database,
|
||||
appLifecycle: this.services.appLifecycle,
|
||||
setting: this.services.setting,
|
||||
remote: this.services.remote,
|
||||
databaseEvents: this.services.databaseEvents,
|
||||
replication: this.services.replication,
|
||||
replicator: this.services.replicator,
|
||||
UI: this.services.UI,
|
||||
vault: this.services.vault,
|
||||
API: services.API,
|
||||
database: services.database,
|
||||
appLifecycle: services.appLifecycle,
|
||||
setting: services.setting,
|
||||
remote: services.remote,
|
||||
databaseEvents: services.databaseEvents,
|
||||
replication: services.replication,
|
||||
replicator: services.replicator,
|
||||
UI: services.UI,
|
||||
vault: services.vault,
|
||||
fileHandler: fileHandler,
|
||||
storageAccess: storageAccess,
|
||||
control: this.services.control,
|
||||
control: services.control,
|
||||
});
|
||||
return {
|
||||
rebuilder,
|
||||
@@ -407,20 +118,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
* @obsolete Use services.setting.saveSettingData instead. Save the settings to the disk. This is usually called after changing the settings in the code, to persist the changes.
|
||||
*/
|
||||
async saveSettings() {
|
||||
await this.services.setting.saveSettingData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise ServiceFeatures.
|
||||
* (Please refer `serviceFeatures` for more details)
|
||||
*/
|
||||
initialiseServiceFeatures() {
|
||||
for (const feature of onLayoutReadyFeatures) {
|
||||
const curriedFeature = () => feature(this);
|
||||
this.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
|
||||
}
|
||||
// enable target filter feature.
|
||||
useTargetFilters(this);
|
||||
await this.core.services.setting.saveSettingData();
|
||||
}
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
@@ -428,26 +126,60 @@ export default class ObsidianLiveSyncPlugin
|
||||
// Maybe no more need to setNoticeClass, but for safety, set it in the constructor of the main plugin class.
|
||||
// TODO: remove this.
|
||||
setNoticeClass(Notice);
|
||||
this.initialiseServices();
|
||||
this.registerModules();
|
||||
this.registerAddOns();
|
||||
this._serviceModules = this.initialiseServiceModules();
|
||||
this.initialiseServiceFeatures();
|
||||
this.bindModuleFunctions();
|
||||
|
||||
const serviceHub = new ObsidianServiceHub(this);
|
||||
|
||||
this.core = new LiveSyncBaseCore(
|
||||
serviceHub,
|
||||
(core, serviceHub) => {
|
||||
return this.initialiseServiceModules(core, serviceHub);
|
||||
},
|
||||
(core) => {
|
||||
const extraModules = [
|
||||
new ModuleObsidianEvents(this, core),
|
||||
new ModuleObsidianSettingDialogue(this, core),
|
||||
new ModuleObsidianMenu(core),
|
||||
new ModuleSetupObsidian(core),
|
||||
new ModuleObsidianSettingsAsMarkdown(core),
|
||||
new ModuleLog(this, core),
|
||||
new ModuleObsidianDocumentHistory(this, core),
|
||||
new ModuleInteractiveConflictResolver(this, core),
|
||||
new ModuleObsidianGlobalHistory(this, core),
|
||||
new ModuleDev(this, core),
|
||||
new ModuleReplicateTest(this, core),
|
||||
new ModuleIntegratedTest(this, core),
|
||||
new SetupManager(core), // this should be moved to core?
|
||||
new ModuleMigration(core),
|
||||
];
|
||||
return extraModules;
|
||||
},
|
||||
(core) => {
|
||||
const addOns = [
|
||||
new ConfigSync(this, core),
|
||||
new HiddenFileSync(this, core),
|
||||
new LocalDatabaseMaintenance(this, core),
|
||||
new P2PReplicator(this, core),
|
||||
];
|
||||
return addOns;
|
||||
},
|
||||
(core) => {
|
||||
//TODO Fix: useXXXX
|
||||
const featuresInitialiser = enableI18nFeature;
|
||||
const curriedFeature = () => featuresInitialiser(core);
|
||||
core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _startUp() {
|
||||
if (!(await this.services.control.onLoad())) return;
|
||||
const onReady = this.services.control.onReady.bind(this.services.control);
|
||||
if (!(await this.core.services.control.onLoad())) return;
|
||||
const onReady = this.core.services.control.onReady.bind(this.core.services.control);
|
||||
this.app.workspace.onLayoutReady(onReady);
|
||||
}
|
||||
override onload() {
|
||||
void this._startUp();
|
||||
}
|
||||
override onunload() {
|
||||
return void this.services.control.onUnload();
|
||||
return void this.core.services.control.onUnload();
|
||||
}
|
||||
}
|
||||
|
||||
// For now,
|
||||
export type LiveSyncCore = ObsidianLiveSyncPlugin;
|
||||
|
||||
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,210 +1,44 @@
|
||||
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 {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
export class StorageEventManagerObsidian extends StorageEventManagerBase<ObsidianStorageEventManagerAdapter> {
|
||||
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;
|
||||
}
|
||||
// cmdHiddenFileSync: HiddenFileSync;
|
||||
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) {
|
||||
super(dependencies);
|
||||
this.plugin = plugin;
|
||||
const adapter = new ObsidianStorageEventManagerAdapter(plugin, core, dependencies.fileProcessing);
|
||||
super(adapter, dependencies);
|
||||
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;
|
||||
if (!path.startsWith(this.core.services.API.getSystemConfigDir())) return;
|
||||
if (path.endsWith("/")) {
|
||||
// Folder
|
||||
return;
|
||||
}
|
||||
const isTargetFile = await this.cmdHiddenFileSync.isTargetFile(path);
|
||||
const isTargetFile = await this.vaultService.isTargetFileInExtra(path);
|
||||
if (!isTargetFile) return;
|
||||
|
||||
void this.appendQueue(
|
||||
[
|
||||
{
|
||||
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,10 +1,16 @@
|
||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
||||
import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase";
|
||||
|
||||
export abstract class AbstractModule {
|
||||
export abstract class AbstractModule<
|
||||
T extends LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands> = LiveSyncBaseCore<
|
||||
ServiceContext,
|
||||
IMinimumLiveSyncCommands
|
||||
>,
|
||||
> {
|
||||
_log = createInstanceLogFunction(this.constructor.name, this.services.API);
|
||||
get services() {
|
||||
if (!this.core._services) {
|
||||
@@ -36,13 +42,13 @@ export abstract class AbstractModule {
|
||||
return stripAllPrefixes(this.services.path.getPath(entry));
|
||||
}
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
|
||||
onBindFunction(core: T, services: typeof core.services) {
|
||||
// Override if needed.
|
||||
}
|
||||
constructor(public core: LiveSyncCore) {
|
||||
constructor(public core: T) {
|
||||
Logger(`[${this.constructor.name}] Loaded`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
saveSettings = this.core.saveSettings.bind(this.core);
|
||||
saveSettings = this.core.services.setting.saveSettingData.bind(this.core.services.setting);
|
||||
|
||||
addTestResult(key: string, value: boolean, summary?: string, message?: string) {
|
||||
this.services.test.addTestResult(`${this.constructor.name}`, key, value, summary, message);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PeriodicProcessor } from "../../common/utils";
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor";
|
||||
import type { LiveSyncCore } from "../../main";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import { balanceChunkPurgedDBs } from "@lib/pouchdb/chunks";
|
||||
import { purgeUnreferencedChunks } from "@lib/pouchdb/chunks";
|
||||
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
|
||||
import { scheduleTask } from "../../common/utils";
|
||||
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||
|
||||
import { $msg } from "../../lib/src/common/i18n";
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
type MetaEntry,
|
||||
} from "@lib/common/types";
|
||||
import type { ModuleReplicator } from "./ModuleReplicator";
|
||||
import { isChunk, isValidPath } from "@/common/utils";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import { isChunk } from "@/lib/src/common/typeUtils";
|
||||
import {
|
||||
LOG_LEVEL_DEBUG,
|
||||
LOG_LEVEL_INFO,
|
||||
@@ -22,6 +21,7 @@ import { fireAndForget, isAnyNote, throttle } from "@lib/common/utils";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore_v2";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
|
||||
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
||||
type ReplicateResultProcessorState = {
|
||||
@@ -54,7 +54,7 @@ export class ReplicateResultProcessor {
|
||||
get services() {
|
||||
return this.replicator.core.services;
|
||||
}
|
||||
get core(): LiveSyncCore {
|
||||
get core(): LiveSyncBaseCore {
|
||||
return this.replicator.core;
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ export class ReplicateResultProcessor {
|
||||
if (await this.services.replication.processOptionalSynchroniseResult(dbDoc)) {
|
||||
// Already processed
|
||||
this.log(`Processed by other processor: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
} else if (isValidPath(this.getPath(doc))) {
|
||||
} else if (this.services.vault.isValidPath(this.getPath(doc))) {
|
||||
// Apply to storage if the path is valid
|
||||
await this.applyToStorage(doc as MetaEntry);
|
||||
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
|
||||
@@ -11,13 +11,9 @@ import {
|
||||
type diff_check_result,
|
||||
type FilePathWithPrefix,
|
||||
} from "../../lib/src/common/types";
|
||||
import {
|
||||
compareMTime,
|
||||
displayRev,
|
||||
isCustomisationSyncMetadata,
|
||||
isPluginMetadata,
|
||||
TARGET_IS_NEW,
|
||||
} from "../../common/utils";
|
||||
import { isCustomisationSyncMetadata, isPluginMetadata } from "@lib/common/typeUtils.ts";
|
||||
import { TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
|
||||
import { compareMTime, displayRev } from "@lib/common/utils.ts";
|
||||
import diff_match_patch from "diff-match-patch";
|
||||
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
|
||||
import { eventHub } from "../../common/events.ts";
|
||||
@@ -214,7 +210,7 @@ export class ModuleConflictResolver extends AbstractModule {
|
||||
private async _resolveAllConflictedFilesByNewerOnes() {
|
||||
this._log(`Resolving conflicts by newer ones`, LOG_LEVEL_NOTICE);
|
||||
|
||||
const files = this.core.storageAccess.getFileNames();
|
||||
const files = await this.core.storageAccess.getFileNames();
|
||||
|
||||
let i = 0;
|
||||
for (const file of files) {
|
||||
|
||||
86
src/modules/essential/ModuleBasicMenu.ts
Normal file
86
src/modules/essential/ModuleBasicMenu.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
// Separated Module for basic menu commands, which are not related to obsidian specific features. It is expected to be used in other platforms with minimal changes.
|
||||
// However, it is odd that it has here at all; it really ought to be in each respective feature. It will likely be moved eventually. Until now, addCommand pointed to Obsidian's version.
|
||||
export class ModuleBasicMenu extends AbstractModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
this.addCommand({
|
||||
id: "livesync-replicate",
|
||||
name: "Replicate now",
|
||||
callback: async () => {
|
||||
await this.services.replication.replicate();
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-dump",
|
||||
name: "Dump information of this doc ",
|
||||
callback: () => {
|
||||
const file = this.services.vault.getActiveFilePath();
|
||||
if (!file) return;
|
||||
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-toggle",
|
||||
name: "Toggle LiveSync",
|
||||
callback: async () => {
|
||||
if (this.settings.liveSync) {
|
||||
this.settings.liveSync = false;
|
||||
this._log("LiveSync Disabled.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this.settings.liveSync = true;
|
||||
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.services.control.applySettings();
|
||||
await this.services.setting.saveSettingData();
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-suspendall",
|
||||
name: "Toggle All Sync.",
|
||||
callback: async () => {
|
||||
if (this.services.appLifecycle.isSuspended()) {
|
||||
this.services.appLifecycle.setSuspended(false);
|
||||
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this.services.appLifecycle.setSuspended(true);
|
||||
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.services.control.applySettings();
|
||||
await this.services.setting.saveSettingData();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-scan-files",
|
||||
name: "Scan storage and database again",
|
||||
callback: async () => {
|
||||
await this.services.vault.scanVault(true);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-runbatch",
|
||||
name: "Run pended batch processes",
|
||||
callback: async () => {
|
||||
await this.services.fileProcessing.commitPendingFileEvents();
|
||||
},
|
||||
});
|
||||
|
||||
// TODO, Replicator is possibly one of features. It should be moved to features.
|
||||
this.addCommand({
|
||||
id: "livesync-abortsync",
|
||||
name: "Abort synchronization immediately",
|
||||
callback: () => {
|
||||
this.core.replicator.terminateSync();
|
||||
},
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -73,7 +73,7 @@ export class ModuleInitializerFile extends AbstractModule {
|
||||
await this.collectDeletedFiles();
|
||||
|
||||
this._log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
|
||||
const filesStorageSrc = this.core.storageAccess.getFiles();
|
||||
const filesStorageSrc = await this.core.storageAccess.getFiles();
|
||||
|
||||
const _filesStorage = [] as typeof filesStorageSrc;
|
||||
|
||||
@@ -300,7 +300,7 @@ export class ModuleInitializerFile extends AbstractModule {
|
||||
throw new Error(`Missing doc:${(file as any).path}`);
|
||||
}
|
||||
if ("path" in file) {
|
||||
const w = this.core.storageAccess.getFileStub((file as any).path);
|
||||
const w = await this.core.storageAccess.getFileStub((file as any).path);
|
||||
if (w) {
|
||||
file = w;
|
||||
} else {
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
eventHub,
|
||||
} from "../../common/events.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import { performDoctorConsultation, RebuildOptions } from "../../lib/src/common/configForDoc.ts";
|
||||
import { isValidPath } from "../../common/utils.ts";
|
||||
import { isMetaEntry } from "../../lib/src/common/types.ts";
|
||||
@@ -40,7 +40,7 @@ export class ModuleMigration extends AbstractModule {
|
||||
);
|
||||
if (isModified) {
|
||||
this.settings = settings;
|
||||
await this.core.saveSettings();
|
||||
await this.saveSettings();
|
||||
}
|
||||
if (!skipRebuild) {
|
||||
if (shouldRebuild) {
|
||||
@@ -231,7 +231,7 @@ export class ModuleMigration extends AbstractModule {
|
||||
if (ret == FIX) {
|
||||
for (const file of recoverable) {
|
||||
// Overwrite the database with the files on the storage
|
||||
const stubFile = this.core.storageAccess.getFileStub(file.path);
|
||||
const stubFile = await this.core.storageAccess.getFileStub(file.path);
|
||||
if (stubFile == null) {
|
||||
Logger(`Could not find stub file for ${file.path}`, LOG_LEVEL_NOTICE);
|
||||
continue;
|
||||
|
||||
@@ -35,13 +35,13 @@ export class ModuleCheckRemoteSize extends AbstractModule {
|
||||
);
|
||||
if (ret == ANSWER_0) {
|
||||
this.settings.notifyThresholdOfRemoteStorageSize = 0;
|
||||
await this.core.saveSettings();
|
||||
await this.saveSettings();
|
||||
} else if (ret == ANSWER_800) {
|
||||
this.settings.notifyThresholdOfRemoteStorageSize = 800;
|
||||
await this.core.saveSettings();
|
||||
await this.saveSettings();
|
||||
} else if (ret == ANSWER_2000) {
|
||||
this.settings.notifyThresholdOfRemoteStorageSize = 2000;
|
||||
await this.core.saveSettings();
|
||||
await this.saveSettings();
|
||||
}
|
||||
}
|
||||
if (this.settings.notifyThresholdOfRemoteStorageSize > 0) {
|
||||
@@ -88,7 +88,8 @@ export class ModuleCheckRemoteSize extends AbstractModule {
|
||||
}),
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
await this.core.saveSettings();
|
||||
// await this.core.saveSettings();
|
||||
await this.core.services.setting.saveSettingData();
|
||||
} else {
|
||||
// Dismiss or Close the dialog
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
|
||||
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
||||
import { $msg } from "src/lib/src/common/i18n.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { type Editor, type MarkdownFileInfo, type MarkdownView } from "@/deps.ts";
|
||||
import { addIcon } from "@/deps.ts";
|
||||
import { type FilePathWithPrefix } from "@lib/common/types.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
|
||||
// Obsidian specific menu commands.
|
||||
export class ModuleObsidianMenu extends AbstractModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
// UI
|
||||
@@ -22,22 +22,6 @@ export class ModuleObsidianMenu extends AbstractModule {
|
||||
await this.services.replication.replicate(true);
|
||||
}).addClass("livesync-ribbon-replicate");
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-replicate",
|
||||
name: "Replicate now",
|
||||
callback: async () => {
|
||||
await this.services.replication.replicate();
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-dump",
|
||||
name: "Dump information of this doc ",
|
||||
callback: () => {
|
||||
const file = this.services.vault.getActiveFilePath();
|
||||
if (!file) return;
|
||||
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-checkdoc-conflicted",
|
||||
name: "Resolve if conflicted.",
|
||||
@@ -48,61 +32,6 @@ export class ModuleObsidianMenu extends AbstractModule {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-toggle",
|
||||
name: "Toggle LiveSync",
|
||||
callback: async () => {
|
||||
if (this.settings.liveSync) {
|
||||
this.settings.liveSync = false;
|
||||
this._log("LiveSync Disabled.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this.settings.liveSync = true;
|
||||
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.services.control.applySettings();
|
||||
await this.services.setting.saveSettingData();
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-suspendall",
|
||||
name: "Toggle All Sync.",
|
||||
callback: async () => {
|
||||
if (this.services.appLifecycle.isSuspended()) {
|
||||
this.services.appLifecycle.setSuspended(false);
|
||||
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this.services.appLifecycle.setSuspended(true);
|
||||
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.services.control.applySettings();
|
||||
await this.services.setting.saveSettingData();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-scan-files",
|
||||
name: "Scan storage and database again",
|
||||
callback: async () => {
|
||||
await this.services.vault.scanVault(true);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "livesync-runbatch",
|
||||
name: "Run pended batch processes",
|
||||
callback: async () => {
|
||||
await this.services.fileProcessing.commitPendingFileEvents();
|
||||
},
|
||||
});
|
||||
|
||||
// TODO, Replicator is possibly one of features. It should be moved to features.
|
||||
this.addCommand({
|
||||
id: "livesync-abortsync",
|
||||
name: "Abort synchronization immediately",
|
||||
callback: () => {
|
||||
this.core.replicator.terminateSync();
|
||||
},
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// I intend to discontinue maintenance of this class. It seems preferable to test it externally.
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
@@ -169,7 +170,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule {
|
||||
this._log("No storage access", LOG_LEVEL_INFO);
|
||||
return;
|
||||
}
|
||||
const files = this.core.storageAccess.getFiles();
|
||||
const files = await this.core.storageAccess.getFiles();
|
||||
const out = [] as any[];
|
||||
const webcrypto = await getWebCrypto();
|
||||
for (const file of files) {
|
||||
@@ -205,8 +206,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule {
|
||||
}
|
||||
|
||||
async __dumpFileListIncludeHidden(outFile?: string) {
|
||||
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
|
||||
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
|
||||
const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
|
||||
const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
|
||||
const out = [] as any[];
|
||||
const files = await this.core.storageAccess.getFilesIncludeHidden("", targetPatterns, ignorePatterns);
|
||||
// console.dir(files);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
export let moduleDev: ModuleDev;
|
||||
$: core = plugin.core;
|
||||
let performanceTestResult = "";
|
||||
let functionCheckResult = "";
|
||||
let testRunning = false;
|
||||
@@ -42,7 +43,7 @@
|
||||
// performTest();
|
||||
|
||||
eventHub.onceEvent(EVENT_LAYOUT_READY, async () => {
|
||||
if (await plugin.storageAccess.isExistsIncludeHidden("_AUTO_TEST.md")) {
|
||||
if (await core.storageAccess.isExistsIncludeHidden("_AUTO_TEST.md")) {
|
||||
new Notice("Auto test file found, running tests...");
|
||||
fireAndForget(async () => {
|
||||
await allTest();
|
||||
@@ -57,14 +58,14 @@
|
||||
function moduleMultiDeviceTest() {
|
||||
if (moduleTesting) return;
|
||||
moduleTesting = true;
|
||||
plugin.services.test.testMultiDevice().finally(() => {
|
||||
core.services.test.testMultiDevice().finally(() => {
|
||||
moduleTesting = false;
|
||||
});
|
||||
}
|
||||
function moduleSingleDeviceTest() {
|
||||
if (moduleTesting) return;
|
||||
moduleTesting = true;
|
||||
plugin.services.test.test().finally(() => {
|
||||
core.services.test.test().finally(() => {
|
||||
moduleTesting = false;
|
||||
});
|
||||
}
|
||||
@@ -72,8 +73,8 @@
|
||||
if (moduleTesting) return;
|
||||
moduleTesting = true;
|
||||
try {
|
||||
await plugin.services.test.test();
|
||||
await plugin.services.test.testMultiDevice();
|
||||
await core.services.test.test();
|
||||
await core.services.test.testMultiDevice();
|
||||
} finally {
|
||||
moduleTesting = false;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ export function addDebugFileLog(message: any, stackLog = false) {
|
||||
// const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || "");
|
||||
// const out
|
||||
try {
|
||||
await plugin.storageAccess.appendHiddenFile(
|
||||
plugin.app.vault.configDir + "/ls-debug/" + outFile,
|
||||
await plugin.core.storageAccess.appendHiddenFile(
|
||||
plugin.core.services.API.getSystemConfigDir() + "/ls-debug/" + outFile,
|
||||
JSON.stringify(out) + "\n"
|
||||
);
|
||||
} catch {
|
||||
|
||||
@@ -48,7 +48,7 @@ async function formatPerfResults(items: NamedMeasureResult[]) {
|
||||
}
|
||||
export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
|
||||
clearResult("trench");
|
||||
const trench = new Trench(plugin.simpleStore);
|
||||
const trench = new Trench(plugin.core.simpleStore);
|
||||
const result = [] as NamedMeasureResult[];
|
||||
result.push(
|
||||
await measure("trench-short-string", async () => {
|
||||
@@ -57,7 +57,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
|
||||
})
|
||||
);
|
||||
{
|
||||
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/10kb.png");
|
||||
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/10kb.png");
|
||||
const uint8Array = new Uint8Array(testBinary);
|
||||
result.push(
|
||||
await measure("trench-binary-10kb", async () => {
|
||||
@@ -67,7 +67,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
|
||||
);
|
||||
}
|
||||
{
|
||||
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg");
|
||||
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg");
|
||||
const uint8Array = new Uint8Array(testBinary);
|
||||
result.push(
|
||||
await measure("trench-binary-100kb", async () => {
|
||||
@@ -77,7 +77,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
|
||||
);
|
||||
}
|
||||
{
|
||||
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/1mb.png");
|
||||
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/1mb.png");
|
||||
const uint8Array = new Uint8Array(testBinary);
|
||||
result.push(
|
||||
await measure("trench-binary-1mb", async () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { isErrorOfMissingDoc } from "../../../lib/src/pouchdb/utils_couchdb.ts";
|
||||
import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/utils.ts";
|
||||
import { isPlainText, stripPrefix } from "../../../lib/src/string_and_binary/path.ts";
|
||||
import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
|
||||
function isImage(path: string) {
|
||||
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||
@@ -46,8 +47,9 @@ function readDocument(w: LoadedEntry) {
|
||||
}
|
||||
export class DocumentHistoryModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
core: LiveSyncBaseCore;
|
||||
get services() {
|
||||
return this.plugin.services;
|
||||
return this.core.services;
|
||||
}
|
||||
range!: HTMLInputElement;
|
||||
contentView!: HTMLDivElement;
|
||||
@@ -66,6 +68,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
core: LiveSyncBaseCore,
|
||||
plugin: ObsidianLiveSyncPlugin,
|
||||
file: TFile | FilePathWithPrefix,
|
||||
id?: DocumentID,
|
||||
@@ -73,6 +76,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.core = core;
|
||||
this.file = file instanceof TFile ? getPathFromTFile(file) : file;
|
||||
this.id = id;
|
||||
this.initialRev = revision;
|
||||
@@ -88,7 +92,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (!this.id) {
|
||||
this.id = await this.services.path.path2id(this.file);
|
||||
}
|
||||
const db = this.plugin.localDatabase;
|
||||
const db = this.core.localDatabase;
|
||||
try {
|
||||
const w = await db.getRaw(this.id, { revs_info: true });
|
||||
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
|
||||
@@ -137,7 +141,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
|
||||
async showExactRev(rev: string) {
|
||||
const db = this.plugin.localDatabase;
|
||||
const db = this.core.localDatabase;
|
||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||
this.currentText = "";
|
||||
this.currentDeleted = false;
|
||||
@@ -292,7 +296,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
return;
|
||||
}
|
||||
const d = readContent(this.currentDoc);
|
||||
await this.plugin.storageAccess.writeHiddenFileAuto(pathToWrite, d);
|
||||
await this.core.storageAccess.writeHiddenFileAuto(pathToWrite, d);
|
||||
await focusFile(pathToWrite);
|
||||
this.close();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { diff_match_patch } from "../../../deps.ts";
|
||||
import { DocumentHistoryModal } from "../DocumentHistory/DocumentHistoryModal.ts";
|
||||
import { isPlainText, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
export let core: LiveSyncBaseCore;
|
||||
|
||||
let showDiffInfo = false;
|
||||
let showChunkCorrected = false;
|
||||
@@ -44,12 +46,12 @@
|
||||
let history = [] as HistoryData[];
|
||||
let loading = false;
|
||||
function getPath(entry: AnyEntry): FilePathWithPrefix {
|
||||
return plugin.services.path.getPath(entry);
|
||||
return core.services.path.getPath(entry);
|
||||
}
|
||||
|
||||
async function fetchChanges(): Promise<HistoryData[]> {
|
||||
try {
|
||||
const db = plugin.localDatabase;
|
||||
const db = core.localDatabase;
|
||||
let result = [] as typeof history;
|
||||
for await (const docA of db.findAllNormalDocs()) {
|
||||
if (docA.mtime < range_from_epoch) {
|
||||
@@ -112,11 +114,11 @@
|
||||
}
|
||||
if (rev == docA._rev) {
|
||||
if (checkStorageDiff) {
|
||||
const isExist = await plugin.storageAccess.isExistsIncludeHidden(
|
||||
const isExist = await core.storageAccess.isExistsIncludeHidden(
|
||||
stripAllPrefixes(getPath(docA))
|
||||
);
|
||||
if (isExist) {
|
||||
const data = await plugin.storageAccess.readHiddenFileBinary(
|
||||
const data = await core.storageAccess.readHiddenFileBinary(
|
||||
stripAllPrefixes(getPath(docA))
|
||||
);
|
||||
const d = readAsBlob(doc);
|
||||
@@ -189,7 +191,7 @@
|
||||
onDestroy(() => {});
|
||||
|
||||
function showHistory(file: string, rev: string) {
|
||||
new DocumentHistoryModal(plugin.app, plugin, file as unknown as FilePathWithPrefix, undefined, rev).open();
|
||||
new DocumentHistoryModal(plugin.app, plugin.core, plugin, file as unknown as FilePathWithPrefix, undefined, rev).open();
|
||||
}
|
||||
function openFile(file: string) {
|
||||
plugin.app.workspace.openLinkText(file, file);
|
||||
@@ -250,7 +252,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>
|
||||
|
||||
@@ -11,6 +11,7 @@ export class GlobalHistoryView extends SvelteItemView {
|
||||
target: target,
|
||||
props: {
|
||||
plugin: this.plugin,
|
||||
core: this.plugin.core,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -254,8 +254,7 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
}
|
||||
// Case Sensitivity
|
||||
if (this.services.vault.shouldCheckCaseInsensitively()) {
|
||||
const f = this.core.storageAccess
|
||||
.getFiles()
|
||||
const f = (await this.core.storageAccess.getFiles())
|
||||
.map((e) => e.path)
|
||||
.filter((e) => e.toLowerCase() == thisFile.path.toLowerCase());
|
||||
if (f.length > 1) {
|
||||
@@ -405,8 +404,8 @@ export class ModuleLog extends AbstractObsidianModule {
|
||||
this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" });
|
||||
eventHub.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
|
||||
if (this.settings?.showStatusOnStatusbar) {
|
||||
this.statusBar = this.core.addStatusBarItem();
|
||||
this.statusBar.addClass("syncstatusbar");
|
||||
this.statusBar = this.services.API.addStatusBarItem();
|
||||
this.statusBar?.addClass("syncstatusbar");
|
||||
}
|
||||
this.adjustStatusDivPosition();
|
||||
return Promise.resolve(true);
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
|
||||
}
|
||||
|
||||
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
|
||||
new DocumentHistoryModal(this.app, this.plugin, file, id).open();
|
||||
new DocumentHistoryModal(this.app, this.core, this.plugin, file, id).open();
|
||||
}
|
||||
|
||||
async fileHistory() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSe
|
||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "../../common/events.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
|
||||
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
||||
settingTab!: ObsidianLiveSyncSettingTab;
|
||||
@@ -29,7 +30,7 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
||||
get appId() {
|
||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||
}
|
||||
override onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
|
||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE } from "../../lib/src/common/types.ts";
|
||||
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types.ts";
|
||||
import { configURIBase } from "../../common/types.ts";
|
||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
|
||||
import { fireAndForget } from "../../lib/src/common/utils.ts";
|
||||
@@ -25,16 +25,24 @@ export class ModuleSetupObsidian extends AbstractModule {
|
||||
private _setupManager!: SetupManager;
|
||||
private _everyOnload(): Promise<boolean> {
|
||||
this._setupManager = this.core.getModule(SetupManager);
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||
if (conf.settings) {
|
||||
await this._setupManager.onUseSetupURI(
|
||||
UserMode.Unknown,
|
||||
`${configURIBase}${encodeURIComponent(conf.settings)}`
|
||||
);
|
||||
} else if (conf.settingsQR) {
|
||||
await this._setupManager.decodeQR(conf.settingsQR);
|
||||
}
|
||||
});
|
||||
try {
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||
if (conf.settings) {
|
||||
await this._setupManager.onUseSetupURI(
|
||||
UserMode.Unknown,
|
||||
`${configURIBase}${encodeURIComponent(conf.settings)}`
|
||||
);
|
||||
} else if (conf.settingsQR) {
|
||||
await this._setupManager.decodeQR(conf.settingsQR);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this._log(
|
||||
"Failed to register protocol handler. This feature may not work in some environments.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this._log(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
this.addCommand({
|
||||
id: "livesync-setting-qr",
|
||||
name: "Show settings as a QR code",
|
||||
|
||||
@@ -86,8 +86,11 @@ export function createStub(name: string, key: string, value: string, panel: stri
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
get core() {
|
||||
return this.plugin.core;
|
||||
}
|
||||
get services() {
|
||||
return this.plugin.services;
|
||||
return this.core.services;
|
||||
}
|
||||
selectedScreen = "";
|
||||
|
||||
@@ -122,9 +125,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
continue;
|
||||
}
|
||||
//@ts-ignore
|
||||
this.plugin.settings[k] = this.editingSettings[k];
|
||||
this.core.settings[k] = this.editingSettings[k];
|
||||
//@ts-ignore
|
||||
this.initialSettings[k] = this.plugin.settings[k];
|
||||
this.initialSettings[k] = this.core.settings[k];
|
||||
}
|
||||
keys.forEach((e) => this.refreshSetting(e));
|
||||
}
|
||||
@@ -164,14 +167,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
continue;
|
||||
}
|
||||
//@ts-ignore
|
||||
this.plugin.settings[k] = this.editingSettings[k];
|
||||
this.core.settings[k] = this.editingSettings[k];
|
||||
//@ts-ignore
|
||||
this.initialSettings[k] = this.plugin.settings[k];
|
||||
this.initialSettings[k] = this.core.settings[k];
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
await this.plugin.saveSettings();
|
||||
await this.services.setting.saveSettingData();
|
||||
}
|
||||
|
||||
// if (runOnSaved) {
|
||||
@@ -231,7 +234,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
*/
|
||||
reloadAllSettings(skipUpdate: boolean = false) {
|
||||
const localSetting = this.reloadAllLocalSettings();
|
||||
this._editingSettings = { ...this.plugin.settings, ...localSetting };
|
||||
this._editingSettings = { ...this.core.settings, ...localSetting };
|
||||
this._editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() };
|
||||
this.initialSettings = { ...this.editingSettings };
|
||||
if (!skipUpdate) this.requestUpdate();
|
||||
@@ -242,7 +245,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
*/
|
||||
refreshSetting(key: AllSettingItemKey) {
|
||||
const localSetting = this.reloadAllLocalSettings();
|
||||
if (key in this.plugin.settings) {
|
||||
if (key in this.core.settings) {
|
||||
if (key in localSetting) {
|
||||
//@ts-ignore
|
||||
this.initialSettings[key] = localSetting[key];
|
||||
@@ -250,7 +253,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.editingSettings[key] = localSetting[key];
|
||||
} else {
|
||||
//@ts-ignore
|
||||
this.initialSettings[key] = this.plugin.settings[key];
|
||||
this.initialSettings[key] = this.core.settings[key];
|
||||
//@ts-ignore
|
||||
this.editingSettings[key] = this.initialSettings[key];
|
||||
}
|
||||
@@ -319,7 +322,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
closeSetting() {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close();
|
||||
this.core.app.setting.close();
|
||||
}
|
||||
|
||||
handleElement(element: HTMLElement, func: OnUpdateFunc) {
|
||||
@@ -381,7 +384,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
requestReload() {
|
||||
if (this.isShown) {
|
||||
const newConf = this.plugin.settings;
|
||||
const newConf = this.core.settings;
|
||||
const keys = Object.keys(newConf) as (keyof ObsidianLiveSyncSettings)[];
|
||||
let hasLoaded = false;
|
||||
for (const k of keys) {
|
||||
@@ -389,7 +392,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// Something has changed
|
||||
if (this.isDirty(k as AllSettingItemKey)) {
|
||||
// And modified.
|
||||
this.plugin.confirm.askInPopup(
|
||||
this.core.confirm.askInPopup(
|
||||
`config-reloaded-${k}`,
|
||||
$msg("obsidianLiveSyncSettingTab.msgSettingModified", {
|
||||
setting: getConfName(k as AllSettingItemKey),
|
||||
@@ -457,7 +460,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.editingSettings.syncOnStart = false;
|
||||
this.editingSettings.syncOnFileOpen = false;
|
||||
this.editingSettings.syncAfterMerge = false;
|
||||
this.plugin.replicator.closeReplication();
|
||||
this.core.replicator.closeReplication();
|
||||
await this.saveAllDirtySettings();
|
||||
this.containerEl.addClass("isWizard");
|
||||
this.inWizard = true;
|
||||
@@ -514,8 +517,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (this.isConfiguredAs("syncOnStart", true)) return true;
|
||||
if (this.isConfiguredAs("syncAfterMerge", true)) return true;
|
||||
if (this.isConfiguredAs("syncOnFileOpen", true)) return true;
|
||||
if (this.plugin?.replicator?.syncStatus == "CONNECTED") return true;
|
||||
if (this.plugin?.replicator?.syncStatus == "PAUSED") return true;
|
||||
if (this.core?.replicator?.syncStatus == "CONNECTED") return true;
|
||||
if (this.core?.replicator?.syncStatus == "PAUSED") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -605,7 +608,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.saveAllDirtySettings();
|
||||
this.closeSetting();
|
||||
await delay(2000);
|
||||
await this.plugin.rebuilder.$performRebuildDB(method);
|
||||
await this.core.rebuilder.$performRebuildDB(method);
|
||||
};
|
||||
async confirmRebuild() {
|
||||
if (!(await this.isPassphraseValid())) {
|
||||
@@ -633,7 +636,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (result == OPTION_FETCH) {
|
||||
if (!(await this.checkWorkingPassphrase())) {
|
||||
if (
|
||||
(await this.plugin.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
|
||||
(await this.core.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
|
||||
defaultOption: "No",
|
||||
})) != "yes"
|
||||
)
|
||||
@@ -646,16 +649,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.saveAllDirtySettings();
|
||||
await this.applyAllSettings();
|
||||
if (result == OPTION_FETCH) {
|
||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
|
||||
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
|
||||
this.services.appLifecycle.scheduleRestart();
|
||||
this.closeSetting();
|
||||
// await rebuildDB("localOnly");
|
||||
} else if (result == OPTION_REBUILD_BOTH) {
|
||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
|
||||
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
|
||||
this.services.appLifecycle.scheduleRestart();
|
||||
this.closeSetting();
|
||||
} else if (result == OPTION_ONLY_SETTING) {
|
||||
await this.plugin.saveSettings();
|
||||
await this.services.setting.saveSettingData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,7 +871,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
getMinioJournalSyncClient() {
|
||||
return new JournalSyncMinio(this.plugin.settings, this.plugin.simpleStore, this.plugin);
|
||||
return new JournalSyncMinio(this.core.settings, this.core.simpleStore, this.core);
|
||||
}
|
||||
async resetRemoteBucket() {
|
||||
const minioJournal = this.getMinioJournalSyncClient();
|
||||
|
||||
@@ -165,7 +165,7 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
}
|
||||
const obsidianInfo = {
|
||||
navigator: navigator.userAgent,
|
||||
fileSystem: this.plugin.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
|
||||
fileSystem: this.core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
|
||||
};
|
||||
const msgConfig = `# ---- Obsidian info ----
|
||||
${stringifyYaml(obsidianInfo)}
|
||||
@@ -221,7 +221,7 @@ ${stringifyYaml({
|
||||
|
||||
void addPanel(paneEl, "Recovery and Repair").then((paneEl) => {
|
||||
const addResult = async (path: string, file: FilePathWithPrefix | false, fileOnDB: LoadedEntry | false) => {
|
||||
const storageFileStat = file ? await this.plugin.storageAccess.statHidden(file) : null;
|
||||
const storageFileStat = file ? await this.core.storageAccess.statHidden(file) : null;
|
||||
resultArea.appendChild(
|
||||
this.createEl(resultArea, "div", {}, (el) => {
|
||||
el.appendChild(this.createEl(el, "h6", { text: path }));
|
||||
@@ -256,7 +256,7 @@ ${stringifyYaml({
|
||||
this.createEl(el, "button", { text: "Storage -> Database" }, (buttonEl) => {
|
||||
buttonEl.onClickEvent(async () => {
|
||||
if (file.startsWith(".")) {
|
||||
const addOn = this.plugin.getAddOn<HiddenFileSync>(HiddenFileSync.name);
|
||||
const addOn = this.core.getAddOn<HiddenFileSync>(HiddenFileSync.name);
|
||||
if (addOn) {
|
||||
const file = (await addOn.scanInternalFiles()).find((e) => e.path == path);
|
||||
if (!file) {
|
||||
@@ -275,7 +275,7 @@ ${stringifyYaml({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!(await this.plugin.fileHandler.storeFileToDB(file as FilePath, true))) {
|
||||
if (!(await this.core.fileHandler.storeFileToDB(file as FilePath, true))) {
|
||||
Logger(
|
||||
`Failed to store the file to the database: ${file}`,
|
||||
LOG_LEVEL_NOTICE
|
||||
@@ -293,7 +293,7 @@ ${stringifyYaml({
|
||||
this.createEl(el, "button", { text: "Database -> Storage" }, (buttonEl) => {
|
||||
buttonEl.onClickEvent(async () => {
|
||||
if (fileOnDB.path.startsWith(ICHeader)) {
|
||||
const addOn = this.plugin.getAddOn<HiddenFileSync>(HiddenFileSync.name);
|
||||
const addOn = this.core.getAddOn<HiddenFileSync>(HiddenFileSync.name);
|
||||
if (addOn) {
|
||||
if (
|
||||
!(await addOn.extractInternalFileFromDatabase(path as FilePath, true))
|
||||
@@ -307,7 +307,7 @@ ${stringifyYaml({
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!(await this.plugin.fileHandler.dbToStorage(
|
||||
!(await this.core.fileHandler.dbToStorage(
|
||||
fileOnDB as MetaEntry,
|
||||
null,
|
||||
true
|
||||
@@ -332,7 +332,7 @@ ${stringifyYaml({
|
||||
|
||||
const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => {
|
||||
const dataContent = readAsBlob(fileOnDB);
|
||||
const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file));
|
||||
const content = createBlob(await this.core.storageAccess.readHiddenFileBinary(file));
|
||||
if (await isDocContentSame(content, dataContent)) {
|
||||
Logger(`Compare: SAME: ${file}`);
|
||||
} else {
|
||||
@@ -348,7 +348,7 @@ ${stringifyYaml({
|
||||
.setButtonText("Recreate all")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
await this.plugin.fileHandler.createAllChunks(true);
|
||||
await this.core.fileHandler.createAllChunks(true);
|
||||
})
|
||||
);
|
||||
new Setting(paneEl)
|
||||
@@ -377,21 +377,21 @@ ${stringifyYaml({
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
|
||||
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
|
||||
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
|
||||
this.plugin.localDatabase.clearCaches();
|
||||
const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
|
||||
const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
|
||||
this.core.localDatabase.clearCaches();
|
||||
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
|
||||
const files = this.plugin.settings.syncInternalFiles
|
||||
? await this.plugin.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns)
|
||||
: await this.plugin.storageAccess.getFileNames();
|
||||
const files = this.core.settings.syncInternalFiles
|
||||
? await this.core.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns)
|
||||
: await this.core.storageAccess.getFileNames();
|
||||
const documents = [] as FilePath[];
|
||||
|
||||
const adn = this.plugin.localDatabase.findAllDocs();
|
||||
const adn = this.core.localDatabase.findAllDocs();
|
||||
for await (const i of adn) {
|
||||
const path = this.services.path.getPath(i);
|
||||
if (path.startsWith(ICXHeader)) continue;
|
||||
if (path.startsWith(PSCHeader)) continue;
|
||||
if (!this.plugin.settings.syncInternalFiles && path.startsWith(ICHeader)) continue;
|
||||
if (!this.core.settings.syncInternalFiles && path.startsWith(ICHeader)) continue;
|
||||
documents.push(stripAllPrefixes(path));
|
||||
}
|
||||
const allPaths = [...new Set([...documents, ...files])];
|
||||
@@ -411,8 +411,8 @@ ${stringifyYaml({
|
||||
if (shouldBeIgnored(path)) {
|
||||
return incProc();
|
||||
}
|
||||
const stat = (await this.plugin.storageAccess.isExistsIncludeHidden(path))
|
||||
? await this.plugin.storageAccess.statHidden(path)
|
||||
const stat = (await this.core.storageAccess.isExistsIncludeHidden(path))
|
||||
? await this.core.storageAccess.statHidden(path)
|
||||
: false;
|
||||
const fileOnStorage = stat != null ? stat : false;
|
||||
if (!(await this.services.vault.isTargetFile(path))) return incProc();
|
||||
@@ -422,7 +422,7 @@ ${stringifyYaml({
|
||||
try {
|
||||
const isHiddenFile = path.startsWith(".");
|
||||
const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path;
|
||||
const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath);
|
||||
const fileOnDB = await this.core.localDatabase.getDBEntry(dbPath);
|
||||
if (fileOnDB && this.services.vault.isFileSizeTooLarge(fileOnDB.size))
|
||||
return incProc();
|
||||
|
||||
@@ -466,10 +466,10 @@ ${stringifyYaml({
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
for await (const docName of this.plugin.localDatabase.findAllDocNames()) {
|
||||
for await (const docName of this.core.localDatabase.findAllDocNames()) {
|
||||
if (!docName.startsWith("f:")) {
|
||||
const idEncoded = await this.services.path.path2id(docName as FilePathWithPrefix);
|
||||
const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID);
|
||||
const doc = await this.core.localDatabase.getRaw(docName as DocumentID);
|
||||
if (!doc) continue;
|
||||
if (doc.type != "newnote" && doc.type != "plain") {
|
||||
continue;
|
||||
@@ -482,7 +482,7 @@ ${stringifyYaml({
|
||||
// @ts-ignore
|
||||
delete newDoc._rev;
|
||||
try {
|
||||
const obfuscatedDoc = await this.plugin.localDatabase.getRaw(idEncoded, {
|
||||
const obfuscatedDoc = await this.core.localDatabase.getRaw(idEncoded, {
|
||||
revs_info: true,
|
||||
});
|
||||
// Unfortunately we have to delete one of them.
|
||||
@@ -499,14 +499,14 @@ ${stringifyYaml({
|
||||
-32
|
||||
);
|
||||
}
|
||||
const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true });
|
||||
const ret = await this.core.localDatabase.putRaw(newDoc, { force: true });
|
||||
if (ret.ok) {
|
||||
Logger(
|
||||
`${docName} has been converted as conflicted document`,
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
doc._deleted = true;
|
||||
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
||||
if ((await this.core.localDatabase.putRaw(doc)).ok) {
|
||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.services.conflict.queueCheckForIfOpen(docName as FilePathWithPrefix);
|
||||
@@ -517,10 +517,10 @@ ${stringifyYaml({
|
||||
} catch (ex: any) {
|
||||
if (ex?.status == 404) {
|
||||
// We can perform this safely
|
||||
if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) {
|
||||
if ((await this.core.localDatabase.putRaw(newDoc)).ok) {
|
||||
Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE);
|
||||
doc._deleted = true;
|
||||
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
||||
if ((await this.core.localDatabase.putRaw(doc)).ok) {
|
||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
@@ -555,7 +555,7 @@ ${stringifyYaml({
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
Logger(`Deleting customization sync data`, LOG_LEVEL_NOTICE);
|
||||
const entriesToDelete = await this.plugin.localDatabase.allDocsRaw({
|
||||
const entriesToDelete = await this.core.localDatabase.allDocsRaw({
|
||||
startkey: "ix:",
|
||||
endkey: "ix:\u{10ffff}",
|
||||
include_docs: true,
|
||||
@@ -564,7 +564,7 @@ ${stringifyYaml({
|
||||
...e.doc,
|
||||
_deleted: true,
|
||||
}));
|
||||
const r = await this.plugin.localDatabase.bulkDocsRaw(newData as any[]);
|
||||
const r = await this.core.localDatabase.bulkDocsRaw(newData as any[]);
|
||||
// Do not care about the result.
|
||||
Logger(
|
||||
`${r.length} items have been removed, to confirm how many items are left, please perform it again.`,
|
||||
|
||||
@@ -11,8 +11,8 @@ export function paneMaintenance(
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
const isRemoteLockedAndDeviceNotAccepted = () => this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted;
|
||||
const isRemoteLocked = () => this.plugin?.replicator?.remoteLocked;
|
||||
const isRemoteLockedAndDeviceNotAccepted = () => this.core?.replicator?.remoteLockedAndDeviceNotAccepted;
|
||||
const isRemoteLocked = () => this.core?.replicator?.remoteLocked;
|
||||
// if (this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted) {
|
||||
this.createEl(
|
||||
paneEl,
|
||||
@@ -92,7 +92,7 @@ export function paneMaintenance(
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
|
||||
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
|
||||
this.services.appLifecycle.performRestart();
|
||||
})
|
||||
);
|
||||
@@ -108,7 +108,7 @@ export function paneMaintenance(
|
||||
.setCta()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.plugin.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
|
||||
await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
|
||||
this.services.appLifecycle.performRestart();
|
||||
})
|
||||
);
|
||||
@@ -121,7 +121,7 @@ export function paneMaintenance(
|
||||
.setCta()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.plugin.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
|
||||
await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
|
||||
this.services.appLifecycle.performRestart();
|
||||
})
|
||||
);
|
||||
@@ -137,8 +137,8 @@ export function paneMaintenance(
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
if (this.plugin.replicator instanceof LiveSyncCouchDBReplicator) {
|
||||
await this.plugin.replicator.sendChunks(this.plugin.settings, undefined, true, 0);
|
||||
if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
|
||||
await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -299,7 +299,7 @@ export function paneMaintenance(
|
||||
.setButtonText("Perform")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
|
||||
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
|
||||
Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction");
|
||||
if (await replicator.compactRemote(this.editingSettings)) {
|
||||
Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction");
|
||||
|
||||
@@ -31,7 +31,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
|
||||
void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => {
|
||||
const migrateAllToIndexedDB = async () => {
|
||||
const dbToName = this.plugin.localDatabase.dbname + SuffixDatabaseName + ExtraSuffixIndexedDB;
|
||||
const dbToName = this.core.localDatabase.dbname + SuffixDatabaseName + ExtraSuffixIndexedDB;
|
||||
const options = {
|
||||
adapter: "indexeddb",
|
||||
//@ts-ignore :missing def
|
||||
@@ -42,18 +42,19 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
const openTo = () => {
|
||||
return new PouchDB(dbToName, options);
|
||||
};
|
||||
if (await migrateDatabases("to IndexedDB", this.plugin.localDatabase.localDatabase, openTo)) {
|
||||
if (await migrateDatabases("to IndexedDB", this.core.localDatabase.localDatabase, openTo)) {
|
||||
Logger(
|
||||
"Migration to IndexedDB completed. Obsidian will be restarted with new configuration immediately.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this.plugin.settings.useIndexedDBAdapter = true;
|
||||
await this.services.setting.saveSettingData();
|
||||
// this.plugin.settings.useIndexedDBAdapter = true;
|
||||
// await this.services.setting.saveSettingData();
|
||||
await this.core.services.setting.applyPartial({ useIndexedDBAdapter: true }, true);
|
||||
this.services.appLifecycle.performRestart();
|
||||
}
|
||||
};
|
||||
const migrateAllToIDB = async () => {
|
||||
const dbToName = this.plugin.localDatabase.dbname + SuffixDatabaseName;
|
||||
const dbToName = this.core.localDatabase.dbname + SuffixDatabaseName;
|
||||
const options = {
|
||||
adapter: "idb",
|
||||
auto_compaction: false,
|
||||
@@ -62,13 +63,14 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
const openTo = () => {
|
||||
return new PouchDB(dbToName, options);
|
||||
};
|
||||
if (await migrateDatabases("to IDB", this.plugin.localDatabase.localDatabase, openTo)) {
|
||||
if (await migrateDatabases("to IDB", this.core.localDatabase.localDatabase, openTo)) {
|
||||
Logger(
|
||||
"Migration to IDB completed. Obsidian will be restarted with new configuration immediately.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this.plugin.settings.useIndexedDBAdapter = false;
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.core.services.setting.applyPartial({ useIndexedDBAdapter: false }, true);
|
||||
// this.core.settings.useIndexedDBAdapter = false;
|
||||
// await this.services.setting.saveSettingData();
|
||||
this.services.appLifecycle.performRestart();
|
||||
}
|
||||
};
|
||||
@@ -151,7 +153,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
} as Record<HashAlgorithm, string>,
|
||||
});
|
||||
this.addOnSaved("hashAlg", async () => {
|
||||
await this.plugin.localDatabase._prepareHashFunctions();
|
||||
await this.core.localDatabase._prepareHashFunctions();
|
||||
});
|
||||
});
|
||||
void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => {
|
||||
@@ -215,7 +217,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
||||
|
||||
this.addOnSaved("maxMTimeForReflectEvents", async (key) => {
|
||||
const buttons = ["Restart Now", "Later"] as const;
|
||||
const reboot = await this.plugin.confirm.askSelectStringDialogue(
|
||||
const reboot = await this.core.confirm.askSelectStringDialogue(
|
||||
"Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?",
|
||||
buttons,
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ export function paneRemoteConfig(
|
||||
.addButton((button) =>
|
||||
button
|
||||
.onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
await setupManager.onlyE2EEConfiguration(UserMode.Update, originalSettings);
|
||||
updateE2EESummary();
|
||||
@@ -79,7 +79,7 @@ export function paneRemoteConfig(
|
||||
.addButton((button) =>
|
||||
button
|
||||
.onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
await setupManager.onConfigureManually(originalSettings, UserMode.Update);
|
||||
updateE2EESummary();
|
||||
@@ -101,7 +101,7 @@ export function paneRemoteConfig(
|
||||
.setButtonText("Change Remote and Setup")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
await setupManager.onSelectServer(originalSettings, UserMode.Update);
|
||||
})
|
||||
@@ -127,7 +127,7 @@ export function paneRemoteConfig(
|
||||
.setButtonText("Configure")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
await setupManager.onCouchDBManualSetup(
|
||||
UserMode.Update,
|
||||
@@ -162,7 +162,7 @@ export function paneRemoteConfig(
|
||||
.setButtonText("Configure")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
await setupManager.onBucketManualSetup(
|
||||
UserMode.Update,
|
||||
@@ -202,7 +202,7 @@ export function paneRemoteConfig(
|
||||
.setButtonText("Configure")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||
await setupManager.onP2PManualSetup(
|
||||
UserMode.Update,
|
||||
|
||||
@@ -35,7 +35,7 @@ export function paneSetup(
|
||||
.setDesc($msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("Rerun Wizard")).onClick(async () => {
|
||||
const setupManager = this.plugin.getModule(SetupManager);
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
await setupManager.onOnboard(UserMode.ExistingUser);
|
||||
// await this.plugin.moduleSetupObsidian.onBoardingWizard(true);
|
||||
});
|
||||
@@ -86,14 +86,14 @@ export function paneSetup(
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard"))
|
||||
.onClick(async () => {
|
||||
if (
|
||||
(await this.plugin.confirm.askYesNoDialog(
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
|
||||
{ defaultOption: "No" }
|
||||
)) == "yes"
|
||||
) {
|
||||
this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS };
|
||||
await this.saveAllDirtySettings();
|
||||
this.plugin.settings = { ...DEFAULT_SETTINGS };
|
||||
this.core.settings = { ...DEFAULT_SETTINGS };
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.database.resetDatabase();
|
||||
// await this.plugin.initializeDatabase();
|
||||
|
||||
@@ -109,7 +109,7 @@ export function paneSyncSettings(
|
||||
await this.rebuildDB("localOnly");
|
||||
// this.resetEditingSettings();
|
||||
if (
|
||||
(await this.plugin.confirm.askYesNoDialog(
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgGenerateSetupURI"),
|
||||
{
|
||||
defaultOption: "Yes",
|
||||
|
||||
@@ -80,7 +80,7 @@ export class ModuleLiveSyncMain extends AbstractModule {
|
||||
initialiseWorkerModule();
|
||||
await this.services.appLifecycle.onWireUpEvents();
|
||||
// debugger;
|
||||
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core);
|
||||
eventHub.emitEvent(EVENT_PLUGIN_LOADED);
|
||||
this._log($msg("moduleLiveSyncMain.logLoadingPlugin"));
|
||||
if (!(await this.services.appLifecycle.onInitialise())) {
|
||||
this._log($msg("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);
|
||||
|
||||
@@ -171,4 +171,18 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
||||
statusText: `${r.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
override addStatusBarItem(): HTMLElement | undefined {
|
||||
return this.context.plugin.addStatusBarItem();
|
||||
}
|
||||
|
||||
override setInterval(handler: () => void, timeout: number): number {
|
||||
const timerId = globalThis.setInterval(handler, timeout) as unknown as number;
|
||||
this.context.plugin.registerInterval(timerId);
|
||||
return timerId;
|
||||
}
|
||||
|
||||
override getSystemConfigDir() {
|
||||
return this.app.vault.configDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
|
||||
path: path,
|
||||
vault: vault,
|
||||
setting: setting,
|
||||
API: API,
|
||||
});
|
||||
const keyValueDB = new ObsidianKeyValueDBService(context, {
|
||||
appLifecycle: appLifecycle,
|
||||
@@ -77,7 +78,6 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
|
||||
const replication = new ObsidianReplicationService(context, {
|
||||
APIService: API,
|
||||
appLifecycleService: appLifecycle,
|
||||
databaseEventService: databaseEvents,
|
||||
replicatorService: replicator,
|
||||
settingService: setting,
|
||||
fileProcessingService: fileProcessing,
|
||||
|
||||
@@ -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): Promise<TAbstractFile | null> {
|
||||
return Promise.resolve(this.app.vault.getAbstractFileByPath(path));
|
||||
}
|
||||
|
||||
getAbstractFileByPathInsensitive(path: FilePath | string): Promise<TAbstractFile | null> {
|
||||
return Promise.resolve(this.app.vault.getAbstractFileByPathInsensitive(path));
|
||||
}
|
||||
|
||||
getFiles(): Promise<TFile[]> {
|
||||
return Promise.resolve(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);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function generateHarness(
|
||||
}
|
||||
export async function waitForReady(harness: LiveSyncHarness): Promise<void> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (harness.plugin.services.appLifecycle.isReady()) {
|
||||
if (harness.plugin.core.services.appLifecycle.isReady()) {
|
||||
console.log("App Lifecycle is ready");
|
||||
return;
|
||||
}
|
||||
@@ -122,11 +122,11 @@ export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await delay(25);
|
||||
const processing =
|
||||
harness.plugin.services.replication.databaseQueueCount.value +
|
||||
harness.plugin.services.fileProcessing.totalQueued.value +
|
||||
harness.plugin.services.fileProcessing.batched.value +
|
||||
harness.plugin.services.fileProcessing.processing.value +
|
||||
harness.plugin.services.replication.storageApplyingCount.value;
|
||||
harness.plugin.core.services.replication.databaseQueueCount.value +
|
||||
harness.plugin.core.services.fileProcessing.totalQueued.value +
|
||||
harness.plugin.core.services.fileProcessing.batched.value +
|
||||
harness.plugin.core.services.fileProcessing.processing.value +
|
||||
harness.plugin.core.services.replication.storageApplyingCount.value;
|
||||
|
||||
if (processing === 0) {
|
||||
if (i > 0) {
|
||||
@@ -139,7 +139,7 @@ export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
|
||||
export async function waitForClosed(harness: LiveSyncHarness): Promise<void> {
|
||||
await delay(100);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (harness.plugin.services.control.hasUnloaded()) {
|
||||
if (harness.plugin.core.services.control.hasUnloaded()) {
|
||||
console.log("App has unloaded");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ export async function storeFile(
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
}
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
return file;
|
||||
}
|
||||
export async function readFromLocalDB(harness: LiveSyncHarness, path: string) {
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
return entry;
|
||||
}
|
||||
@@ -95,11 +95,11 @@ export async function testFileWrite(
|
||||
) {
|
||||
const file = await storeFile(harness, path, content, false, fileOptions);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
const vaultFile = await readFromVault(harness, path, content instanceof Blob, fileOptions);
|
||||
expect(await isDocContentSame(vaultFile, content)).toBe(true);
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
if (skipCheckToBeWritten) {
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -28,12 +28,12 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
});
|
||||
|
||||
it("should have services initialized", async () => {
|
||||
expect(harness.plugin.services).toBeDefined();
|
||||
expect(harness.plugin.core.services).toBeDefined();
|
||||
return await Promise.resolve();
|
||||
});
|
||||
it("should have local database initialized", async () => {
|
||||
expect(harness.plugin.localDatabase).toBeDefined();
|
||||
expect(harness.plugin.localDatabase.isReady).toBe(true);
|
||||
expect(harness.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harness.plugin.core.localDatabase.isReady).toBe(true);
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
@@ -54,11 +54,11 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
// await delay(100); // Wait a bit for the local database to process
|
||||
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
expect(readContent(entry)).toBe(content);
|
||||
@@ -80,10 +80,10 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
expect(readContent(entry)).toBe(content);
|
||||
@@ -108,9 +108,9 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
expect(await isDocContentSame(readContent, content)).toBe(true);
|
||||
}
|
||||
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
const entryContent = await readContent(entry);
|
||||
|
||||
@@ -68,7 +68,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
await waitForIdle(harnessInit);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harnessInit.plugin.services.replicator.getActiveReplicator()?.closeReplication();
|
||||
await harnessInit.plugin.core.services.replicator.getActiveReplicator()?.closeReplication();
|
||||
await harnessInit.dispose();
|
||||
await delay(1000);
|
||||
});
|
||||
@@ -81,7 +81,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
it("should be prepared for replication", async () => {
|
||||
await waitForReady(harnessInit);
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
const status = await harnessInit.plugin.services.replicator
|
||||
const status = await harnessInit.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(sync_test_setting_init);
|
||||
console.log("Connected devices after reset:", status);
|
||||
@@ -120,12 +120,12 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessUpload.plugin.services).toBeDefined();
|
||||
expect(harnessUpload.plugin.core.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessUpload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.localDatabase.isReady).toBe(true);
|
||||
expect(harnessUpload.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should prepare remote database", async () => {
|
||||
@@ -138,7 +138,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
// Perform replication
|
||||
// await harness.plugin.services.replication.replicate(true);
|
||||
// await harness.plugin.core.services.replication.replicate(true);
|
||||
});
|
||||
it("should different content of several files have been created correctly", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
|
||||
@@ -149,7 +149,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
test.each(FILE_SIZE_MD)("should large file of size %i bytes has been created", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
@@ -162,7 +162,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
const path = nameFile("binary", "bin", size);
|
||||
await testFileWrite(harnessUpload, path, content, true, fileOptions);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
@@ -210,12 +210,12 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessDownload.plugin.services).toBeDefined();
|
||||
expect(harnessDownload.plugin.core.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessDownload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.localDatabase.isReady).toBe(true);
|
||||
expect(harnessDownload.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should a file has been synchronised", async () => {
|
||||
@@ -232,9 +232,9 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
test.each(FILE_SIZE_MD)("should the file %i bytes had been synchronised", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
@@ -245,9 +245,9 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes had been synchronised", async (size) => {
|
||||
const path = nameFile("binary", "bin", size);
|
||||
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
|
||||
@@ -7,11 +7,11 @@ import { commands } from "vitest/browser";
|
||||
import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { waitTaskWithFollowups } from "../lib/util";
|
||||
async function waitForP2PPeers(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// Wait for peers to connect
|
||||
const maxRetries = 20;
|
||||
let retries = maxRetries;
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
@@ -38,8 +38,8 @@ async function waitForP2PPeers(harness: LiveSyncHarness) {
|
||||
}
|
||||
}
|
||||
export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
@@ -58,9 +58,9 @@ export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
|
||||
export async function performReplication(harness: LiveSyncHarness) {
|
||||
await waitForP2PPeers(harness);
|
||||
await delay(500);
|
||||
const p = harness.plugin.services.replication.replicate(true);
|
||||
const p = harness.plugin.core.services.replication.replicate(true);
|
||||
const task =
|
||||
harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P
|
||||
harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P
|
||||
? waitTaskWithFollowups(
|
||||
p,
|
||||
() => {
|
||||
@@ -74,17 +74,17 @@ export async function performReplication(harness: LiveSyncHarness) {
|
||||
: p;
|
||||
const result = await task;
|
||||
// await waitForIdle(harness);
|
||||
// if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// await closeP2PReplicatorConnections(harness);
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function closeReplication(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
return await closeP2PReplicatorConnections(harness);
|
||||
}
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
console.log("No active replicator to close");
|
||||
return;
|
||||
@@ -98,19 +98,21 @@ export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianL
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
if (shouldReset) {
|
||||
await delay(1000);
|
||||
await harness.plugin.services.replicator
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryResetRemoteDatabase(harness.plugin.settings);
|
||||
?.tryResetRemoteDatabase(harness.plugin.core.settings);
|
||||
} else {
|
||||
await harness.plugin.services.replicator
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryCreateRemoteDatabase(harness.plugin.settings);
|
||||
?.tryCreateRemoteDatabase(harness.plugin.core.settings);
|
||||
}
|
||||
await harness.plugin.services.replicator.getActiveReplicator()?.markRemoteResolved(harness.plugin.settings);
|
||||
// No exceptions should be thrown
|
||||
const status = await harness.plugin.services.replicator
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.settings);
|
||||
?.markRemoteResolved(harness.plugin.core.settings);
|
||||
// No exceptions should be thrown
|
||||
const status = await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.core.settings);
|
||||
console.log("Remote status:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("Dialog Tests", async () => {
|
||||
it("should show copy to clipboard dialog and confirm", async () => {
|
||||
const testString = "This is a test string to copy to clipboard.";
|
||||
const title = "Copy Test";
|
||||
const result = harness.plugin.services.UI.promptCopyToClipboard(title, testString);
|
||||
const result = harness.plugin.core.services.UI.promptCopyToClipboard(title, testString);
|
||||
const isDialogShown = await waitForDialogShown(title, 500);
|
||||
expect(isDialogShown).toBe(true);
|
||||
const copyButton = page.getByText("📋");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user