Compare commits

...

4 Commits

Author SHA1 Message Date
vorotamoroz
34162f747c fix corrupted test, update submodule 2026-06-08 11:10:37 +01:00
vorotamoroz
9e87ee4da1 Merge branch 'main' into cli_vaultpath 2026-06-08 19:07:02 +09:00
vorotamoroz
ba5d4c434b Merge pull request #945 from vrtmrz/fix_941
fixed: ignore hidden files completely if hidden file sync disabled
2026-06-08 18:57:09 +09:00
vorotamoroz
cf173caf88 feat: decouple the database and vault directories 2026-06-08 10:43:10 +01:00
7 changed files with 161 additions and 14 deletions

View File

@@ -70,6 +70,7 @@ async function verifyRemoteState(
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
const { databasePath, core, settingsPath } = context;
const vaultPath = context.vaultPath || databasePath;
await core.services.control.activated;
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>");
}
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 sourceStat = await fs.stat(sourcePath);
console.log(`[Command] push ${sourcePath} -> ${destinationDatabasePath}`);
@@ -253,7 +254,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 2) {
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]);
console.log(`[Command] pull ${sourceDatabasePath} -> ${destinationPath}`);
@@ -276,7 +277,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 3) {
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 rev = options.commandArgs[2].trim();
if (!rev) {
@@ -333,7 +334,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 1) {
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();
console.log(`[Command] put stdin -> ${destinationDatabasePath}`);
return await core.serviceModules.databaseFileAccess.storeContent(
@@ -346,7 +347,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 1) {
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}`);
const source = await core.serviceModules.databaseFileAccess.fetch(
sourceDatabasePath as FilePathWithPrefix,
@@ -370,7 +371,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 2) {
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();
if (!rev) {
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") {
const prefix =
options.commandArgs.length > 0 && options.commandArgs[0].trim() !== ""
? toDatabaseRelativePath(options.commandArgs[0], databasePath)
? toDatabaseRelativePath(options.commandArgs[0], vaultPath)
: "";
const rows: { path: string; line: string }[] = [];
@@ -429,7 +430,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 1) {
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 })) {
if (doc._deleted || doc.deleted) continue;
@@ -473,7 +474,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.commandArgs.length < 1) {
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}`);
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) {
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();
if (revisionToKeep === "") {
throw new Error("resolve requires a non-empty revision-to-keep");

View File

@@ -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 { ConnectionStringParser } from "@lib/common/ConnectionString";
import { configURIBase } from "@lib/common/models/shared.const";
@@ -601,6 +604,45 @@ describe("runCommand abnormal cases", () => {
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", () => {
it("mark-resolved without args runs on active database", async () => {
const core = createCoreMock();

View File

@@ -46,6 +46,7 @@ export interface CLIOptions {
export interface CLICommandContext {
databasePath: string;
vaultPath: string;
core: LiveSyncBaseCore<ServiceContext, any>;
settingsPath: string;
originalSyncSettings: Pick<

View File

@@ -329,8 +329,20 @@ export async function main() {
options.command === "mirror" && options.commandArgs[0]
? path.resolve(options.commandArgs[0])
: options.vaultPath
? path.resolve(options.vaultPath)
: databasePath!;
? path.resolve(options.vaultPath)
: 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(`Database Path: ${databasePath}`);
@@ -541,7 +553,7 @@ export async function main() {
infoLog("");
}
const result = await runCommand(options, { databasePath, core, settingsPath, originalSyncSettings });
const result = await runCommand(options, { databasePath, vaultPath, core, settingsPath, originalSyncSettings });
if (!result) {
console.error(`[Error] Command '${options.command}' failed`);
process.exitCode = 1;

View File

@@ -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"

Submodule src/lib updated: 2eb8938ed5...82e15f2b9d

View File

@@ -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...
### Improved
- 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