From 822d9579762b528e634a4c5560bc396d17eedf22 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 12 Mar 2026 19:41:10 +0900 Subject: [PATCH] Refactor: separate entrypoint and main, Fix: readlng binary file --- src/apps/cli/README.md | 3 -- .../adapters/NodeStorageAdapter.unit.spec.ts | 40 +++++++++++++++++++ src/apps/cli/entrypoint.ts | 7 ++++ src/apps/cli/main.ts | 22 +--------- .../cli/test/test-e2e-two-vaults-common.sh | 32 +++++++++++---- src/apps/cli/vite.config.ts | 4 +- src/lib | 2 +- updates.md | 22 +++++++++- 8 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 src/apps/cli/adapters/NodeStorageAdapter.unit.spec.ts create mode 100644 src/apps/cli/entrypoint.ts diff --git a/src/apps/cli/README.md b/src/apps/cli/README.md index ca9a383..c12c2ee 100644 --- a/src/apps/cli/README.md +++ b/src/apps/cli/README.md @@ -187,9 +187,6 @@ TODO: Conflict and resolution checks for real local databases. - `serve`: Start CLI in server mode, exposing REST APIs for remote, and batch operations. - `cause-conflicted `: Mark a file as conflicted without changing its content, to trigger conflict resolution in Obsidian. -## Current Limitations and known issues -- Binary files are not supported yet (it seems... but I haven't tested this yet). - ## Use Cases ### 1. Bootstrap a new headless vault diff --git a/src/apps/cli/adapters/NodeStorageAdapter.unit.spec.ts b/src/apps/cli/adapters/NodeStorageAdapter.unit.spec.ts new file mode 100644 index 0000000..a1b0977 --- /dev/null +++ b/src/apps/cli/adapters/NodeStorageAdapter.unit.spec.ts @@ -0,0 +1,40 @@ +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { NodeStorageAdapter } from "./NodeStorageAdapter"; + +describe("NodeStorageAdapter binary I/O", () => { + const tempDirs: string[] = []; + + async function createAdapter() { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "livesync-cli-node-storage-")); + tempDirs.push(tempDir); + return new NodeStorageAdapter(tempDir); + } + + afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); + }); + + it("writes and reads binary data without corruption", async () => { + const adapter = await createAdapter(); + const expected = Uint8Array.from([0x00, 0x7f, 0x80, 0xff, 0x42]); + + await adapter.writeBinary("files/blob.bin", expected.buffer.slice(0)); + const result = await adapter.readBinary("files/blob.bin"); + + expect(Array.from(new Uint8Array(result))).toEqual(Array.from(expected)); + }); + + it("returns an ArrayBuffer with the exact file length", async () => { + const adapter = await createAdapter(); + const expected = Uint8Array.from([0x10, 0x20, 0x30]); + + await adapter.writeBinary("files/small.bin", expected.buffer.slice(0)); + const result = await adapter.readBinary("files/small.bin"); + + expect(result.byteLength).toBe(expected.byteLength); + expect(Array.from(new Uint8Array(result))).toEqual([0x10, 0x20, 0x30]); + }); +}); diff --git a/src/apps/cli/entrypoint.ts b/src/apps/cli/entrypoint.ts new file mode 100644 index 0000000..b8a1177 --- /dev/null +++ b/src/apps/cli/entrypoint.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { main } from "./main"; + +main().catch((error) => { + console.error(`[Fatal Error]`, error); + process.exit(1); +}); diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index 58817b3..43d3109 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Self-hosted LiveSync CLI * Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian @@ -22,7 +21,6 @@ if (!("localStorage" in globalThis)) { import * as fs from "fs/promises"; import * as path from "path"; -import { pathToFileURL } from "node:url"; import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub"; import { LiveSyncBaseCore } from "../../LiveSyncBaseCore"; import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules"; @@ -201,7 +199,7 @@ async function createDefaultSettingsFile(options: CLIOptions) { console.log(`[Done] Created settings file: ${targetPath}`); } -async function main() { +export async function main() { const options = parseArgs(); const avoidStdoutNoise = options.command === "cat" || @@ -373,21 +371,3 @@ async function main() { process.exit(1); } } - -// Run main only when invoked as the entrypoint, not when imported by tests. -const isEntryPoint = (() => { - const argv1 = process.argv[1]; - if (!argv1) return false; - try { - return import.meta.url === pathToFileURL(argv1).href; - } catch { - return false; - } -})(); - -if (isEntryPoint) { - main().catch((error) => { - console.error(`[Fatal Error]`, error); - process.exit(1); - }); -} diff --git a/src/apps/cli/test/test-e2e-two-vaults-common.sh b/src/apps/cli/test/test-e2e-two-vaults-common.sh index 03a5400..0745275 100755 --- a/src/apps/cli/test/test-e2e-two-vaults-common.sh +++ b/src/apps/cli/test/test-e2e-two-vaults-common.sh @@ -134,6 +134,18 @@ assert_command_fails() { fi } +assert_files_equal() { + local expected_file="$1" + local actual_file="$2" + local message="$3" + if ! cmp -s "$expected_file" "$actual_file"; then + echo "[FAIL] $message" >&2 + echo "[FAIL] expected sha256: $(sha256sum "$expected_file" | awk '{print $1}')" >&2 + echo "[FAIL] actual sha256: $(sha256sum "$actual_file" | awk '{print $1}')" >&2 + exit 1 + fi +} + sanitise_cat_stdout() { sed '/^\[CLIWatchAdapter\] File watching is not enabled in CLI version$/d' } @@ -295,6 +307,7 @@ TARGET_A_ONLY="e2e/a-only-info.md" TARGET_SYNC="e2e/sync-info.md" TARGET_PUSH="e2e/pushed-from-a.md" TARGET_PUT="e2e/put-from-a.md" +TARGET_PUSH_BINARY="e2e/pushed-from-a.bin" TARGET_CONFLICT="e2e/conflict.md" echo "[CASE] A puts and A can get info" @@ -318,18 +331,21 @@ run_cli_a push "$PUSH_SRC" "$TARGET_PUSH" >/dev/null printf 'put-content-%s\n' "$DB_SUFFIX" | run_cli_a put "$TARGET_PUT" >/dev/null sync_both run_cli_b pull "$TARGET_PUSH" "$PULL_DST" >/dev/null -if ! cmp -s "$PUSH_SRC" "$PULL_DST"; then - echo "[FAIL] B pull result does not match pushed source" >&2 - echo "--- source ---" >&2 - cat "$PUSH_SRC" >&2 - echo "--- pulled ---" >&2 - cat "$PULL_DST" >&2 - exit 1 -fi +assert_files_equal "$PUSH_SRC" "$PULL_DST" "B pull result does not match pushed source" CAT_B_PUT="$(run_cli_b cat "$TARGET_PUT" | sanitise_cat_stdout)" assert_equal "put-content-$DB_SUFFIX" "$CAT_B_PUT" "B cat should return A put content" echo "[PASS] push/pull and put/cat across vaults" +echo "[CASE] A pushes binary, both sync, and B can pull identical bytes" +PUSH_BINARY_SRC="$WORK_DIR/push-source.bin" +PULL_BINARY_DST="$WORK_DIR/pull-destination.bin" +head -c 4096 /dev/urandom > "$PUSH_BINARY_SRC" +run_cli_a push "$PUSH_BINARY_SRC" "$TARGET_PUSH_BINARY" >/dev/null +sync_both +run_cli_b pull "$TARGET_PUSH_BINARY" "$PULL_BINARY_DST" >/dev/null +assert_files_equal "$PUSH_BINARY_SRC" "$PULL_BINARY_DST" "B pull result does not match pushed binary source" +echo "[PASS] binary push/pull across vaults" + echo "[CASE] A removes, both sync, and B can no longer cat" run_cli_a rm "$TARGET_PUT" >/dev/null sync_both diff --git a/src/apps/cli/vite.config.ts b/src/apps/cli/vite.config.ts index 42e6202..d3bdea0 100644 --- a/src/apps/cli/vite.config.ts +++ b/src/apps/cli/vite.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ minify: false, rollupOptions: { input: { - index: path.resolve(__dirname, "main.ts"), + index: path.resolve(__dirname, "entrypoint.ts"), }, external: (id) => { if (defaultExternal.includes(id)) return true; @@ -48,7 +48,7 @@ export default defineConfig({ }, }, lib: { - entry: path.resolve(__dirname, "main.ts"), + entry: path.resolve(__dirname, "entrypoint.ts"), formats: ["cjs"], fileName: "index", }, diff --git a/src/lib b/src/lib index 4346ead..d94c9b3 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 4346ead9c86574c3f370d458a59ea48b502e6d33 +Subproject commit d94c9b3ed74f10ca1ce704f54518485f646aa225 diff --git a/updates.md b/updates.md index 4be0b2f..5106031 100644 --- a/updates.md +++ b/updates.md @@ -3,7 +3,25 @@ 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. -## -- Unreleased2 -- +## Unnamed 12th March, 2026 + +12th March, 2026 + +### Fixed + +- Fixed Journal Sync had not been working on some timing, due to a compatibility issue (for a long time). + +### Internal behaviour change (or fix) + +- Journal Replicator now yields true after the replication is done. + +### CLI + +- Add more tests. +- Object Storage support has also been confirmed (and fixed) in CLI. + - Yes, we have finally managed to 'get one file'. + +## Unnamed 11th March, 2026 11th March, 2026 (second commit). @@ -23,7 +41,7 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid ### New something - Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless, and a dedicated version. -## -- Unreleased -- +## Unnamed 11th March, 2026 11th March, 2026