mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-25 10:33:21 +00:00
Merge pull request #946 from vrtmrz/cli_vaultpath
feat: decouple the database and vault directories
This commit is contained in:
7 files changed
+161
-14
No files matched your search
@@ -70,6 +70,7 @@ async function verifyRemoteState(
|
|||||||
|
|
||||||
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
|
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
|
||||||
const { databasePath, core, settingsPath } = context;
|
const { databasePath, core, settingsPath } = context;
|
||||||
|
const vaultPath = context.vaultPath || databasePath;
|
||||||
|
|
||||||
await core.services.control.activated;
|
await core.services.control.activated;
|
||||||
if (options.command === "daemon") {
|
if (options.command === "daemon") {
|
||||||
@@ -235,7 +236,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
throw new Error("push requires two arguments: <src> <dst>");
|
throw new Error("push requires two arguments: <src> <dst>");
|
||||||
}
|
}
|
||||||
const sourcePath = path.resolve(options.commandArgs[0]);
|
const sourcePath = path.resolve(options.commandArgs[0]);
|
||||||
const destinationDatabasePath = toDatabaseRelativePath(options.commandArgs[1], databasePath);
|
const destinationDatabasePath = toDatabaseRelativePath(options.commandArgs[1], vaultPath);
|
||||||
const sourceData = await fs.readFile(sourcePath);
|
const sourceData = await fs.readFile(sourcePath);
|
||||||
const sourceStat = await fs.stat(sourcePath);
|
const sourceStat = await fs.stat(sourcePath);
|
||||||
console.log(`[Command] push ${sourcePath} -> ${destinationDatabasePath}`);
|
console.log(`[Command] push ${sourcePath} -> ${destinationDatabasePath}`);
|
||||||
@@ -253,7 +254,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 2) {
|
if (options.commandArgs.length < 2) {
|
||||||
throw new Error("pull requires two arguments: <src> <dst>");
|
throw new Error("pull requires two arguments: <src> <dst>");
|
||||||
}
|
}
|
||||||
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
const destinationPath = path.resolve(options.commandArgs[1]);
|
const destinationPath = path.resolve(options.commandArgs[1]);
|
||||||
console.log(`[Command] pull ${sourceDatabasePath} -> ${destinationPath}`);
|
console.log(`[Command] pull ${sourceDatabasePath} -> ${destinationPath}`);
|
||||||
|
|
||||||
@@ -276,7 +277,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 3) {
|
if (options.commandArgs.length < 3) {
|
||||||
throw new Error("pull-rev requires three arguments: <src> <dst> <rev>");
|
throw new Error("pull-rev requires three arguments: <src> <dst> <rev>");
|
||||||
}
|
}
|
||||||
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
const destinationPath = path.resolve(options.commandArgs[1]);
|
const destinationPath = path.resolve(options.commandArgs[1]);
|
||||||
const rev = options.commandArgs[2].trim();
|
const rev = options.commandArgs[2].trim();
|
||||||
if (!rev) {
|
if (!rev) {
|
||||||
@@ -333,7 +334,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 1) {
|
if (options.commandArgs.length < 1) {
|
||||||
throw new Error("put requires one argument: <dst>");
|
throw new Error("put requires one argument: <dst>");
|
||||||
}
|
}
|
||||||
const destinationDatabasePath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const destinationDatabasePath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
const content = await readStdinAsUtf8();
|
const content = await readStdinAsUtf8();
|
||||||
console.log(`[Command] put stdin -> ${destinationDatabasePath}`);
|
console.log(`[Command] put stdin -> ${destinationDatabasePath}`);
|
||||||
return await core.serviceModules.databaseFileAccess.storeContent(
|
return await core.serviceModules.databaseFileAccess.storeContent(
|
||||||
@@ -346,7 +347,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 1) {
|
if (options.commandArgs.length < 1) {
|
||||||
throw new Error("cat requires one argument: <src>");
|
throw new Error("cat requires one argument: <src>");
|
||||||
}
|
}
|
||||||
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
console.error(`[Command] cat ${sourceDatabasePath}`);
|
console.error(`[Command] cat ${sourceDatabasePath}`);
|
||||||
const source = await core.serviceModules.databaseFileAccess.fetch(
|
const source = await core.serviceModules.databaseFileAccess.fetch(
|
||||||
sourceDatabasePath as FilePathWithPrefix,
|
sourceDatabasePath as FilePathWithPrefix,
|
||||||
@@ -370,7 +371,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 2) {
|
if (options.commandArgs.length < 2) {
|
||||||
throw new Error("cat-rev requires two arguments: <src> <rev>");
|
throw new Error("cat-rev requires two arguments: <src> <rev>");
|
||||||
}
|
}
|
||||||
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const sourceDatabasePath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
const rev = options.commandArgs[1].trim();
|
const rev = options.commandArgs[1].trim();
|
||||||
if (!rev) {
|
if (!rev) {
|
||||||
throw new Error("cat-rev requires a non-empty revision");
|
throw new Error("cat-rev requires a non-empty revision");
|
||||||
@@ -397,7 +398,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.command === "ls") {
|
if (options.command === "ls") {
|
||||||
const prefix =
|
const prefix =
|
||||||
options.commandArgs.length > 0 && options.commandArgs[0].trim() !== ""
|
options.commandArgs.length > 0 && options.commandArgs[0].trim() !== ""
|
||||||
? toDatabaseRelativePath(options.commandArgs[0], databasePath)
|
? toDatabaseRelativePath(options.commandArgs[0], vaultPath)
|
||||||
: "";
|
: "";
|
||||||
const rows: { path: string; line: string }[] = [];
|
const rows: { path: string; line: string }[] = [];
|
||||||
|
|
||||||
@@ -429,7 +430,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 1) {
|
if (options.commandArgs.length < 1) {
|
||||||
throw new Error("info requires one argument: <path>");
|
throw new Error("info requires one argument: <path>");
|
||||||
}
|
}
|
||||||
const targetPath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const targetPath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
|
|
||||||
for await (const doc of core.services.database.localDatabase.findAllNormalDocs({ conflicts: true })) {
|
for await (const doc of core.services.database.localDatabase.findAllNormalDocs({ conflicts: true })) {
|
||||||
if (doc._deleted || doc.deleted) continue;
|
if (doc._deleted || doc.deleted) continue;
|
||||||
@@ -473,7 +474,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 1) {
|
if (options.commandArgs.length < 1) {
|
||||||
throw new Error("rm requires one argument: <path>");
|
throw new Error("rm requires one argument: <path>");
|
||||||
}
|
}
|
||||||
const targetPath = toDatabaseRelativePath(options.commandArgs[0], databasePath);
|
const targetPath = toDatabaseRelativePath(options.commandArgs[0], vaultPath);
|
||||||
console.error(`[Command] rm ${targetPath}`);
|
console.error(`[Command] rm ${targetPath}`);
|
||||||
return await core.serviceModules.databaseFileAccess.delete(targetPath as FilePathWithPrefix);
|
return await core.serviceModules.databaseFileAccess.delete(targetPath as FilePathWithPrefix);
|
||||||
}
|
}
|
||||||
@@ -482,7 +483,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
if (options.commandArgs.length < 2) {
|
if (options.commandArgs.length < 2) {
|
||||||
throw new Error("resolve requires two arguments: <path> <revision-to-keep>");
|
throw new Error("resolve requires two arguments: <path> <revision-to-keep>");
|
||||||
}
|
}
|
||||||
const targetPath = toDatabaseRelativePath(options.commandArgs[0], databasePath) as FilePathWithPrefix;
|
const targetPath = toDatabaseRelativePath(options.commandArgs[0], vaultPath) as FilePathWithPrefix;
|
||||||
const revisionToKeep = options.commandArgs[1].trim();
|
const revisionToKeep = options.commandArgs[1].trim();
|
||||||
if (revisionToKeep === "") {
|
if (revisionToKeep === "") {
|
||||||
throw new Error("resolve requires a non-empty revision-to-keep");
|
throw new Error("resolve requires a non-empty revision-to-keep");
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import * as os from "os";
|
||||||
import * as processSetting from "@lib/API/processSetting";
|
import * as processSetting from "@lib/API/processSetting";
|
||||||
import { ConnectionStringParser } from "@lib/common/ConnectionString";
|
import { ConnectionStringParser } from "@lib/common/ConnectionString";
|
||||||
import { configURIBase } from "@lib/common/models/shared.const";
|
import { configURIBase } from "@lib/common/models/shared.const";
|
||||||
@@ -601,6 +604,45 @@ describe("runCommand abnormal cases", () => {
|
|||||||
expect(exported2).toBe(roundTripInput);
|
expect(exported2).toBe(roundTripInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("runCommand with decoupled vault path", () => {
|
||||||
|
it("push resolves target path relative to vaultPath, not databasePath", async () => {
|
||||||
|
const core = createCoreMock();
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "livesync-test-"));
|
||||||
|
const localVaultPath = path.join(tempDir, "vault");
|
||||||
|
const localDatabasePath = path.join(tempDir, "db");
|
||||||
|
await fs.mkdir(localVaultPath);
|
||||||
|
await fs.mkdir(localDatabasePath);
|
||||||
|
|
||||||
|
const fileInVault = path.join(localVaultPath, "existing.md");
|
||||||
|
await fs.writeFile(fileInVault, "hello", "utf-8");
|
||||||
|
|
||||||
|
const decoupledContext = {
|
||||||
|
databasePath: localDatabasePath,
|
||||||
|
vaultPath: localVaultPath,
|
||||||
|
settingsPath: path.join(localDatabasePath, ".livesync/settings.json"),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
command: "push" as const,
|
||||||
|
commandArgs: [fileInVault, fileInVault],
|
||||||
|
databasePath: localDatabasePath,
|
||||||
|
vaultPath: localVaultPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runCommand(options, { ...decoupledContext, core });
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(core.serviceModules.storageAccess.writeFileAuto).toHaveBeenCalledWith(
|
||||||
|
"existing.md",
|
||||||
|
expect.any(ArrayBuffer),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("mark-resolved and unlock-remote commands", () => {
|
describe("mark-resolved and unlock-remote commands", () => {
|
||||||
it("mark-resolved without args runs on active database", async () => {
|
it("mark-resolved without args runs on active database", async () => {
|
||||||
const core = createCoreMock();
|
const core = createCoreMock();
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export interface CLIOptions {
|
|||||||
|
|
||||||
export interface CLICommandContext {
|
export interface CLICommandContext {
|
||||||
databasePath: string;
|
databasePath: string;
|
||||||
|
vaultPath: string;
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
core: LiveSyncBaseCore<ServiceContext, any>;
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
originalSyncSettings: Pick<
|
originalSyncSettings: Pick<
|
||||||
|
|||||||
+15
-3
@@ -329,8 +329,20 @@ export async function main() {
|
|||||||
options.command === "mirror" && options.commandArgs[0]
|
options.command === "mirror" && options.commandArgs[0]
|
||||||
? path.resolve(options.commandArgs[0])
|
? path.resolve(options.commandArgs[0])
|
||||||
: options.vaultPath
|
: options.vaultPath
|
||||||
? path.resolve(options.vaultPath)
|
? path.resolve(options.vaultPath)
|
||||||
: databasePath!;
|
: databasePath!;
|
||||||
|
|
||||||
|
// Check if vault directory exists
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(vaultPath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
console.error(`Error: Vault path ${vaultPath} is not a directory`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: Vault directory ${vaultPath} does not exist`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
infoLog(`Self-hosted LiveSync CLI`);
|
infoLog(`Self-hosted LiveSync CLI`);
|
||||||
infoLog(`Database Path: ${databasePath}`);
|
infoLog(`Database Path: ${databasePath}`);
|
||||||
@@ -541,7 +553,7 @@ export async function main() {
|
|||||||
infoLog("");
|
infoLog("");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runCommand(options, { databasePath, core, settingsPath, originalSyncSettings });
|
const result = await runCommand(options, { databasePath, vaultPath, core, settingsPath, originalSyncSettings });
|
||||||
if (!result) {
|
if (!result) {
|
||||||
console.error(`[Error] Command '${options.command}' failed`);
|
console.error(`[Error] Command '${options.command}' failed`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
cd "$CLI_DIR"
|
||||||
|
source "$SCRIPT_DIR/test-helpers.sh"
|
||||||
|
display_test_info
|
||||||
|
|
||||||
|
RUN_BUILD="${RUN_BUILD:-1}"
|
||||||
|
REMOTE_PATH="${REMOTE_PATH:-test/push-pull-decoupled.txt}"
|
||||||
|
cli_test_init_cli_cmd
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-test.XXXXXX")"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
SETTINGS_FILE="${1:-$WORK_DIR/data.json}"
|
||||||
|
|
||||||
|
if [[ "$RUN_BUILD" == "1" ]]; then
|
||||||
|
echo "[INFO] building CLI..."
|
||||||
|
npm run build
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] generating settings from DEFAULT_SETTINGS -> $SETTINGS_FILE"
|
||||||
|
cli_test_init_settings_file "$SETTINGS_FILE"
|
||||||
|
|
||||||
|
if [[ -n "${COUCHDB_URI:-}" && -n "${COUCHDB_USER:-}" && -n "${COUCHDB_PASSWORD:-}" && -n "${COUCHDB_DBNAME:-}" ]]; then
|
||||||
|
echo "[INFO] applying CouchDB env vars to generated settings"
|
||||||
|
cli_test_apply_couchdb_settings "$SETTINGS_FILE" "$COUCHDB_URI" "$COUCHDB_USER" "$COUCHDB_PASSWORD" "$COUCHDB_DBNAME"
|
||||||
|
else
|
||||||
|
echo "[WARN] CouchDB env vars are not fully set. push/pull may fail unless generated settings are updated."
|
||||||
|
cli_test_mark_settings_configured "$SETTINGS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VAULT_DIR="$WORK_DIR/vault"
|
||||||
|
DB_DIR="$WORK_DIR/db"
|
||||||
|
mkdir -p "$VAULT_DIR/test"
|
||||||
|
mkdir -p "$DB_DIR"
|
||||||
|
|
||||||
|
SRC_FILE="$WORK_DIR/push-source.txt"
|
||||||
|
PULLED_FILE="$WORK_DIR/pull-result.txt"
|
||||||
|
printf 'push-pull-decoupled-test %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SRC_FILE"
|
||||||
|
|
||||||
|
# 1. Test push command with decoupled vault directory
|
||||||
|
echo "[INFO] push with decoupled vault -> $REMOTE_PATH"
|
||||||
|
run_cli "$DB_DIR" --vault "$VAULT_DIR" --settings "$SETTINGS_FILE" push "$SRC_FILE" "$REMOTE_PATH"
|
||||||
|
|
||||||
|
# 2. Test pull command with decoupled vault directory
|
||||||
|
echo "[INFO] pull with decoupled vault <- $REMOTE_PATH"
|
||||||
|
run_cli "$DB_DIR" --vault "$VAULT_DIR" --settings "$SETTINGS_FILE" pull "$REMOTE_PATH" "$PULLED_FILE"
|
||||||
|
|
||||||
|
if cmp -s "$SRC_FILE" "$PULLED_FILE"; then
|
||||||
|
echo "[PASS] push/pull roundtrip with decoupled vault matched"
|
||||||
|
else
|
||||||
|
echo "[FAIL] push/pull roundtrip with decoupled vault mismatch" >&2
|
||||||
|
echo "--- source ---" >&2
|
||||||
|
cat "$SRC_FILE" >&2
|
||||||
|
echo "--- pulled ---" >&2
|
||||||
|
cat "$PULLED_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Clean up pulled file and vault test directory to verify mirror
|
||||||
|
rm -f "$PULLED_FILE"
|
||||||
|
rm -rf "$VAULT_DIR/test"
|
||||||
|
|
||||||
|
# 4. Test mirror command with decoupled vault directory
|
||||||
|
echo "[INFO] mirror with decoupled vault"
|
||||||
|
run_cli "$DB_DIR" --vault "$VAULT_DIR" --settings "$SETTINGS_FILE" mirror
|
||||||
|
|
||||||
|
RESTORED_FILE="$VAULT_DIR/$REMOTE_PATH"
|
||||||
|
if cmp -s "$SRC_FILE" "$RESTORED_FILE"; then
|
||||||
|
echo "[PASS] mirror with decoupled vault matched"
|
||||||
|
else
|
||||||
|
echo "[FAIL] mirror with decoupled vault mismatch" >&2
|
||||||
|
echo "--- source ---" >&2
|
||||||
|
cat "$SRC_FILE" >&2
|
||||||
|
echo "--- mirrored/restored ---" >&2
|
||||||
|
cat "$RESTORED_FILE" 2>/dev/null || echo "<none>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[PASS] decoupled database/vault E2E tests successfully completed"
|
||||||
+1
-1
Submodule src/lib updated: 2eb8938ed5...82e15f2b9d.
@@ -18,7 +18,15 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
|||||||
|
|
||||||
I should also consider the version numbering for the CLI...
|
I should also consider the version numbering for the CLI...
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`.
|
- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`.
|
||||||
|
- Decoupled the database directory path from the actual vault directory path using the `--vault` (or `-V`) option.
|
||||||
|
|
||||||
|
### Fixed (preventive)
|
||||||
|
|
||||||
|
- Validated that the specified vault path exists and is indeed a directory before starting the CLI.
|
||||||
|
- Integrated path resolution and validations for one-off commands (such as `'push'`, `'pull'`, `'cat'`, `'rm'`, `'info'`, and `'resolve'`) against the decoupled vault path instead of the database path.
|
||||||
|
|
||||||
## 0.25.73
|
## 0.25.73
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user