mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-08 19:34:20 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a51c78011 | ||
|
|
7b9c0b011f | ||
|
|
26a050e6f6 | ||
|
|
34162f747c | ||
|
|
9e87ee4da1 | ||
|
|
ba5d4c434b | ||
|
|
cf173caf88 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.73",
|
||||
"version": "0.25.74",
|
||||
"minAppVersion": "1.7.2",
|
||||
"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",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.73",
|
||||
"version": "0.25.74",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.73",
|
||||
"version": "0.25.74",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.73",
|
||||
"version": "0.25.74",
|
||||
"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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface CLIOptions {
|
||||
|
||||
export interface CLICommandContext {
|
||||
databasePath: string;
|
||||
vaultPath: string;
|
||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
||||
settingsPath: string;
|
||||
originalSyncSettings: Pick<
|
||||
|
||||
@@ -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;
|
||||
|
||||
83
src/apps/cli/test/test-decoupled-vault-linux.sh
Normal file
83
src/apps/cli/test/test-decoupled-vault-linux.sh
Normal 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"
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 2eb8938ed5...82e15f2b9d
@@ -296,8 +296,9 @@ body {
|
||||
content: " ❓";
|
||||
}
|
||||
|
||||
.sls-item-invalid-value {
|
||||
background-color: rgba(var(--background-modifier-error-rgb), 0.3) !important;
|
||||
.sls-setting .setting-item-control input.sls-item-invalid-value,
|
||||
.sls-setting .setting-item-control textarea.sls-item-invalid-value {
|
||||
background-color: rgba(var(--background-modifier-error-rgb), 0.3);
|
||||
}
|
||||
|
||||
.sls-setting-disabled input[type=text],
|
||||
|
||||
11
updates.md
11
updates.md
@@ -3,7 +3,7 @@ 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
|
||||
## 0.25.74
|
||||
|
||||
8th June, 2026
|
||||
|
||||
@@ -11,6 +11,7 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
||||
|
||||
- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan (#941).
|
||||
- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911).
|
||||
- The startup sequence now saves the state more effectively (Thank you so much for @bmcyver)!
|
||||
|
||||
## Only CLI
|
||||
|
||||
@@ -18,7 +19,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user