diff --git a/src/apps/cli/Dockerfile b/src/apps/cli/Dockerfile index 7e85bdb..f1a24cf 100644 --- a/src/apps/cli/Dockerfile +++ b/src/apps/cli/Dockerfile @@ -108,4 +108,4 @@ RUN chmod +x /usr/local/bin/livesync-cli # Mount your vault / local database directory here VOLUME ["/data"] -ENTRYPOINT ["livesync-cli"] +ENTRYPOINT ["/usr/local/bin/livesync-cli"] diff --git a/src/apps/cli/README.md b/src/apps/cli/README.md index 08f8493..8ea8db4 100644 --- a/src/apps/cli/README.md +++ b/src/apps/cli/README.md @@ -45,6 +45,10 @@ CLI Main - Settings management (JSON file) - Graceful shutdown handling +## Something I realised later that could lead to misunderstandings + +The term `vault` in this README refers to the directory containing your local database and settings file. Not the actual files you want to sync. I will fix this later, but please be mind this for now. + ## Docker A Docker image is provided for headless / server deployments. Build from the repository root: diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index fac1072..ecd8afa 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -3,25 +3,10 @@ * Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian */ -if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") { - const store = new Map(); - (globalThis as any).localStorage = { - getItem: (key: string) => (store.has(key) ? store.get(key)! : null), - setItem: (key: string, value: string) => { - store.set(key, value); - }, - removeItem: (key: string) => { - store.delete(key); - }, - clear: () => { - store.clear(); - }, - }; -} - import * as fs from "fs/promises"; import * as path from "path"; import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub"; +import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage"; import { LiveSyncBaseCore } from "../../LiveSyncBaseCore"; import { ModuleReplicatorP2P } from "../../modules/core/ModuleReplicatorP2P"; import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules"; @@ -43,6 +28,7 @@ import { getPathFromUXFileInfo } from "@lib/common/typeUtils"; import { stripAllPrefixes } from "@lib/string_and_binary/path"; const SETTINGS_FILE = ".livesync/settings.json"; +ensureGlobalNodeLocalStorage(); defaultLoggerEnv.minLogLevel = LOG_LEVEL_DEBUG; function printHelp(): void { @@ -252,6 +238,7 @@ export async function main() { const settingsPath = options.settingsPath ? path.resolve(options.settingsPath) : path.join(vaultPath, SETTINGS_FILE); + configureNodeLocalStorage(path.join(vaultPath, ".livesync", "runtime", "local-storage.json")); infoLog(`Self-hosted LiveSync CLI`); infoLog(`Vault: ${vaultPath}`); diff --git a/src/apps/cli/services/NodeLocalStorage.ts b/src/apps/cli/services/NodeLocalStorage.ts new file mode 100644 index 0000000..c92338a --- /dev/null +++ b/src/apps/cli/services/NodeLocalStorage.ts @@ -0,0 +1,111 @@ +import * as nodeFs from "node:fs"; +import * as nodePath from "node:path"; + +type LocalStorageShape = { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; + clear(): void; +}; + +class PersistentNodeLocalStorage { + private storagePath: string | undefined; + private localStore: Record = {}; + + configure(storagePath: string) { + if (this.storagePath === storagePath) { + return; + } + this.storagePath = storagePath; + this.loadFromFile(); + } + + private loadFromFile() { + if (!this.storagePath) { + this.localStore = {}; + return; + } + try { + const loaded = JSON.parse(nodeFs.readFileSync(this.storagePath, "utf-8")) as Record; + this.localStore = { ...loaded }; + } catch { + this.localStore = {}; + } + } + + private flushToFile() { + if (!this.storagePath) { + return; + } + nodeFs.mkdirSync(nodePath.dirname(this.storagePath), { recursive: true }); + nodeFs.writeFileSync(this.storagePath, JSON.stringify(this.localStore, null, 2), "utf-8"); + } + + getItem(key: string): string | null { + return this.localStore[key] ?? null; + } + + setItem(key: string, value: string) { + this.localStore[key] = value; + this.flushToFile(); + } + + removeItem(key: string) { + if (!(key in this.localStore)) { + return; + } + delete this.localStore[key]; + this.flushToFile(); + } + + clear() { + this.localStore = {}; + this.flushToFile(); + } +} + +const persistentNodeLocalStorage = new PersistentNodeLocalStorage(); + +function createNodeLocalStorageShim(): LocalStorageShape { + return { + getItem(key: string) { + return persistentNodeLocalStorage.getItem(key); + }, + setItem(key: string, value: string) { + persistentNodeLocalStorage.setItem(key, value); + }, + removeItem(key: string) { + persistentNodeLocalStorage.removeItem(key); + }, + clear() { + persistentNodeLocalStorage.clear(); + }, + }; +} + +export function ensureGlobalNodeLocalStorage() { + if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") { + (globalThis as any).localStorage = createNodeLocalStorageShim(); + } +} + +export function configureNodeLocalStorage(storagePath: string) { + persistentNodeLocalStorage.configure(storagePath); + ensureGlobalNodeLocalStorage(); +} + +export function getNodeLocalStorageItem(key: string): string { + return persistentNodeLocalStorage.getItem(key) ?? ""; +} + +export function setNodeLocalStorageItem(key: string, value: string) { + persistentNodeLocalStorage.setItem(key, value); +} + +export function deleteNodeLocalStorageItem(key: string) { + persistentNodeLocalStorage.removeItem(key); +} + +export function clearNodeLocalStorage() { + persistentNodeLocalStorage.clear(); +} \ No newline at end of file diff --git a/src/apps/cli/services/NodeLocalStorage.unit.spec.ts b/src/apps/cli/services/NodeLocalStorage.unit.spec.ts new file mode 100644 index 0000000..1911508 --- /dev/null +++ b/src/apps/cli/services/NodeLocalStorage.unit.spec.ts @@ -0,0 +1,60 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + clearNodeLocalStorage, + configureNodeLocalStorage, + ensureGlobalNodeLocalStorage, + getNodeLocalStorageItem, + setNodeLocalStorageItem, +} from "./NodeLocalStorage"; + +describe("NodeLocalStorage", () => { + const tempDirs: string[] = []; + + afterEach(() => { + clearNodeLocalStorage(); + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("persists values to the configured file", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "livesync-node-local-storage-")); + tempDirs.push(tempDir); + const storagePath = path.join(tempDir, "runtime", "local-storage.json"); + + configureNodeLocalStorage(storagePath); + setNodeLocalStorageItem("checkpoint", "42"); + + const saved = JSON.parse(fs.readFileSync(storagePath, "utf-8")) as Record; + expect(saved.checkpoint).toBe("42"); + }); + + it("reloads persisted values when configured again", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "livesync-node-local-storage-")); + tempDirs.push(tempDir); + const storagePath = path.join(tempDir, "runtime", "local-storage.json"); + + fs.mkdirSync(path.dirname(storagePath), { recursive: true }); + fs.writeFileSync(storagePath, JSON.stringify({ persisted: "value" }, null, 2), "utf-8"); + + configureNodeLocalStorage(storagePath); + + expect(getNodeLocalStorageItem("persisted")).toBe("value"); + }); + + it("installs a global localStorage shim backed by the same store", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "livesync-node-local-storage-")); + tempDirs.push(tempDir); + const storagePath = path.join(tempDir, "runtime", "local-storage.json"); + + configureNodeLocalStorage(storagePath); + ensureGlobalNodeLocalStorage(); + + globalThis.localStorage.setItem("shared", "state"); + + expect(getNodeLocalStorageItem("shared")).toBe("state"); + }); +}); \ No newline at end of file diff --git a/src/apps/cli/services/NodeSettingService.ts b/src/apps/cli/services/NodeSettingService.ts index f231fba..0859183 100644 --- a/src/apps/cli/services/NodeSettingService.ts +++ b/src/apps/cli/services/NodeSettingService.ts @@ -5,17 +5,17 @@ import { handlers } from "@lib/services/lib/HandlerUtils"; import type { ObsidianLiveSyncSettings } from "@lib/common/types"; import type { ServiceContext } from "@lib/services/base/ServiceBase"; import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import * as nodeFs from "node:fs"; -import * as nodePath from "node:path"; +import { + configureNodeLocalStorage, + deleteNodeLocalStorageItem, + getNodeLocalStorageItem, + setNodeLocalStorageItem, +} from "./NodeLocalStorage"; export class NodeSettingService extends SettingService { - private storagePath: string; - private localStore: Record = {}; - constructor(context: T, dependencies: SettingServiceDependencies, storagePath: string) { super(context, dependencies); - this.storagePath = storagePath; - this.loadLocalStoreFromFile(); + configureNodeLocalStorage(storagePath); this.onSettingSaved.addHandler((settings) => { eventHub.emitEvent(EVENT_SETTING_SAVED, settings); return Promise.resolve(true); @@ -26,34 +26,16 @@ export class NodeSettingService extends SettingService }); } - private loadLocalStoreFromFile() { - try { - const loaded = JSON.parse(nodeFs.readFileSync(this.storagePath, "utf-8")) as Record; - this.localStore = { ...loaded }; - } catch { - this.localStore = {}; - } - } - - private flushLocalStoreToFile() { - nodeFs.mkdirSync(nodePath.dirname(this.storagePath), { recursive: true }); - nodeFs.writeFileSync(this.storagePath, JSON.stringify(this.localStore, null, 2), "utf-8"); - } - protected setItem(key: string, value: string) { - this.localStore[key] = value; - this.flushLocalStoreToFile(); + setNodeLocalStorageItem(key, value); } protected getItem(key: string): string { - return this.localStore[key] ?? ""; + return getNodeLocalStorageItem(key); } protected deleteItem(key: string): void { - if (key in this.localStore) { - delete this.localStore[key]; - this.flushLocalStoreToFile(); - } + deleteNodeLocalStorageItem(key); } public saveData = handlers<{ saveData: (data: ObsidianLiveSyncSettings) => Promise }>().binder("saveData"); diff --git a/updates.md b/updates.md index a0b2ab1..dbc9a70 100644 --- a/updates.md +++ b/updates.md @@ -3,6 +3,18 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +## Unreleased + +2nd April, 2026 + +### CLI + +#### (may) Fixed, to be confirmed + +- Replication progress is now correctly saved and restored in the CLI. + + + ## ~~0.25.55~~ 0.25.56 30th March, 2026 @@ -206,91 +218,6 @@ As a result of recent refactoring, we are able to write tests more easily now! - `ModuleObsidianAPI` has been removed and implemented in `APIService` and `RemoteService`. - Now `APIService` is responsible for the network-online-status, not `databaseService.managers.networkManager`. -## 0.25.44 - -24th February, 2026 - -This release represents a significant architectural overhaul of the plug-in, focusing on modularity, testability, and stability. While many changes are internal, they pave the way for more robust features and easier maintenance. -However, as this update is very substantial, please do feel free to let me know if you encounter any issues. - -### Fixed - -- Ignore files (e.g., `.ignore`) are now handled efficiently. -- Replication & Database: - - Replication statistics are now correctly reset after switching replicators. -- Fixed `File already exists` for .md files has been merged (PR #802) So thanks @waspeer for the contribution! - -### Improved - -- Now we can configure network-error banners as icons, or hide them completely with the new `Network Warning Style` setting in the `General` pane of the settings dialogue. (#770, PR #804) - - Thanks so much to @A-wry! - -### Refactored - -#### Architectural Overhaul: - -- A major transition from Class-based Modules to a Service/Middleware architecture has begun. - - Many modules (for example, `ModulePouchDB`, `ModuleLocalDatabaseObsidian`, `ModuleKeyValueDB`) have been removed or integrated into specific Services (`database`, `keyValueDB`, etc.). - - Reduced reliance on dynamic binding and inverted dependencies; dependencies are now explicit. - - `ObsidianLiveSyncPlugin` properties (`replicator`, `localDatabase`, `storageAccess`, etc.) have been moved to their respective services for better separation of concerns. - - In this refactoring, the Service will henceforth, as a rule, cease to use setHandler, that is to say, simple lazy binding. - - They will be implemented directly in the service. - - However, not everything will be middlewarised. Modules that maintain state or make decisions based on the results of multiple handlers are permitted. -- Lifecycle: - - Application LifeCycle now starts in `Main` rather than `ServiceHub` or `ObsidianMenuModule`, ensuring smoother startup coordination. - -#### New Services & Utilities: - -- Added a `control` service to orchestrate other services (for example, handling stop/start logic during settings realisation). -- Added `UnresolvedErrorManager` to handle and display unresolved errors in a unified way. -- Added `logUtils` to unify logging injection and formatting. -- `VaultService.isTargetFile` now uses multiple, distinct checkers for better extensibility. - -#### Code Separation: - -- Separated Obsidian-specific logic from base logic for `StorageEventManager` and `FileAccess` modules. -- Moved reactive state values and statistics from the main plug-in instance to the services responsible for them. - -#### Internal Cleanups: - -- Many functions have been renamed for clarity (for example, `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`). -- Added `override` keywords to overridden items and removed dynamic binding for clearer code inheritance. -- Moved common functions to the common library. - -#### Dependencies: - -- Bumped dependencies simply to a point where they can be considered problem-free (by human-powered-artefacts-diff). - - Svelte, terser, and more something will be bumped later. They have a significant impact on the diff and paint it totally. - - You may be surprised, but when I bump the library, I am actually checking for any unintended code. - -## 0.25.43 - -5th, February, 2026 - -### Fixed - -- Encryption/decryption issues when using Object Storage as remote have been fixed. - - Now the plug-in falls back to V1 encryption/decryption when V2 fails (if not configured as ForceV1). - - This may fix the issue reported in #772. - -### Notice - -Quite a few packages have been updated in this release. Please report if you find any unexpected behaviour after this update. - -## 0.25.42 - -2nd, February, 2026 - -This release is identical to 0.25.41-patched-3, except for the version number. - -### Refactored - -- Now the service context is `protected` instead of `private` in `ServiceBase`. - - This change allows derived classes to access the context directly. -- Some dynamically bound services have been moved to services for better dependency management. -- `WebPeer` has been moved to the main repository from the sub repository `livesync-commonlib` for correct dependency management. -- Migrated from the outdated, unstable platform abstraction layer to services. - - A bit more services will be added in the future for better maintainability. Full notes are in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). diff --git a/updates_old.md b/updates_old.md index bb8fa75..0e219f7 100644 --- a/updates_old.md +++ b/updates_old.md @@ -3,6 +3,47 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. + + +## ~~0.25.55~~ 0.25.56 + +30th March, 2026 + +### Fixed + +- No longer `Peer-to-Peer Sync is not enabled. We cannot open a new connection.` error occurs when we have not enabled P2P sync and are not expected to use it (#830). + +### CLI + +- Fixed incomplete localStorage support in the CLI (#831). Thank you so much @rewse ! +- Fixed the issue where the CLI could not be connected to the remote which had been locked once (#833), also thanks to @rewse ! + +## 0.25.54 + +18th March, 2026 + +### Fixed + +- Remote storage size check now works correctly again (#818). +- Some buttons on the settings dialogue now respond correctly again (#827). + +### Refactored + +- P2P replicator has been refactored to be a little more robust and easier to understand. +- Delete items which are no longer used that might cause potential problems + +### CLI + +- Fixed the corrupted display of the help message. +- Remove some unnecessary code. + +### WebApp + +- Fixed the issue where the detail level was not being applied in the log pane. +- Pop-ups are now shown. +- Add coverage for the test. +- Pop-ups are now shown in the web app as well. + ## 0.25.53 17th March, 2026