Merge branch 'main' into asch/fix-everything
This commit is contained in:
commit
3a20a7c2f8
6 changed files with 120 additions and 298 deletions
|
|
@ -150,25 +150,6 @@ test("parseArgs - default log level is INFO", () => {
|
||||||
assert.equal(args.logLevel, LogLevel.INFO);
|
assert.equal(args.logLevel, LogLevel.INFO);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseArgs - parse DEBUG log level", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"--log-level",
|
|
||||||
"DEBUG"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.logLevel, LogLevel.DEBUG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - parse ERROR log level", () => {
|
test("parseArgs - parse ERROR log level", () => {
|
||||||
const args = parseArgs([
|
const args = parseArgs([
|
||||||
"node",
|
"node",
|
||||||
|
|
@ -188,43 +169,6 @@ test("parseArgs - parse ERROR log level", () => {
|
||||||
assert.equal(args.logLevel, LogLevel.ERROR);
|
assert.equal(args.logLevel, LogLevel.ERROR);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseArgs - log level is case insensitive", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"--log-level",
|
|
||||||
"debug"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.logLevel, LogLevel.DEBUG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - throws on invalid log level", () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"--log-level",
|
|
||||||
"INVALID"
|
|
||||||
]);
|
|
||||||
}, /Invalid log level/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - reads required options from environment variables", () => {
|
test("parseArgs - reads required options from environment variables", () => {
|
||||||
process.env.VAULTLINK_LOCAL_PATH = "/env/path";
|
process.env.VAULTLINK_LOCAL_PATH = "/env/path";
|
||||||
|
|
@ -267,184 +211,3 @@ test("parseArgs - CLI arguments take precedence over environment variables", ()
|
||||||
delete process.env.VAULTLINK_TOKEN;
|
delete process.env.VAULTLINK_TOKEN;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseArgs - reads log level from environment variable", () => {
|
|
||||||
process.env.VAULTLINK_LOG_LEVEL = "DEBUG";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default"
|
|
||||||
]);
|
|
||||||
assert.equal(args.logLevel, LogLevel.DEBUG);
|
|
||||||
} finally {
|
|
||||||
delete process.env.VAULTLINK_LOG_LEVEL;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - quiet defaults to false", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.quiet, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - parse --quiet flag", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"--quiet"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.quiet, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - parse -q short flag", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"-q"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.quiet, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - line-endings defaults to auto", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.lineEndings, "auto");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - parse --line-endings lf", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"--line-endings",
|
|
||||||
"lf"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.lineEndings, "lf");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - parse --line-endings crlf", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"https://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default",
|
|
||||||
"--line-endings",
|
|
||||||
"crlf"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.lineEndings, "crlf");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - throws on invalid remote URI protocol", () => {
|
|
||||||
assert.throws(() => {
|
|
||||||
parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"ftp://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default"
|
|
||||||
]);
|
|
||||||
}, /Invalid remote URI/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - accepts http:// remote URI", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.remoteUri, "http://localhost:3000");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseArgs - accepts wss:// remote URI", () => {
|
|
||||||
const args = parseArgs([
|
|
||||||
"node",
|
|
||||||
"cli.js",
|
|
||||||
"-l",
|
|
||||||
"/path/to/vault",
|
|
||||||
"-r",
|
|
||||||
"wss://sync.example.com",
|
|
||||||
"-t",
|
|
||||||
"mytoken",
|
|
||||||
"-v",
|
|
||||||
"default"
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.remoteUri, "wss://sync.example.com");
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { Command, Option } from "commander";
|
||||||
import packageJson from "../package.json";
|
import packageJson from "../package.json";
|
||||||
import { LogLevel } from "sync-client";
|
import { LogLevel } from "sync-client";
|
||||||
|
|
||||||
type LineEndingMode = "auto" | "lf" | "crlf";
|
export const LINE_ENDING_MODES = ["auto", "lf", "crlf"] as const;
|
||||||
|
export type LineEndingMode = (typeof LINE_ENDING_MODES)[number];
|
||||||
|
|
||||||
interface CliArgs {
|
interface CliArgs {
|
||||||
remoteUri: string;
|
remoteUri: string;
|
||||||
|
|
@ -21,6 +22,35 @@ interface CliArgs {
|
||||||
|
|
||||||
const VALID_PROTOCOLS = ["http://", "https://", "ws://", "wss://"];
|
const VALID_PROTOCOLS = ["http://", "https://", "ws://", "wss://"];
|
||||||
|
|
||||||
|
const REQUIRED_OPTIONS = {
|
||||||
|
localPath: {
|
||||||
|
flags: "-l, --local-path <path>",
|
||||||
|
env: "VAULTLINK_LOCAL_PATH"
|
||||||
|
},
|
||||||
|
remoteUri: {
|
||||||
|
flags: "-r, --remote-uri <uri>",
|
||||||
|
env: "VAULTLINK_REMOTE_URI"
|
||||||
|
},
|
||||||
|
token: { flags: "-t, --token <token>", env: "VAULTLINK_TOKEN" },
|
||||||
|
vaultName: {
|
||||||
|
flags: "-v, --vault-name <name>",
|
||||||
|
env: "VAULTLINK_VAULT_NAME"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function requireOption<T>(
|
||||||
|
value: T | undefined,
|
||||||
|
name: keyof typeof REQUIRED_OPTIONS
|
||||||
|
): T {
|
||||||
|
if (value === undefined) {
|
||||||
|
const { flags, env } = REQUIRED_OPTIONS[name];
|
||||||
|
throw new Error(
|
||||||
|
`required option '${flags}' not specified (or set ${env})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseArgs(argv: string[]): CliArgs {
|
export function parseArgs(argv: string[]): CliArgs {
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
|
@ -32,23 +62,25 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||||
.version(packageJson.version)
|
.version(packageJson.version)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option(
|
new Option(
|
||||||
"-l, --local-path <path>",
|
REQUIRED_OPTIONS.localPath.flags,
|
||||||
"Local directory path to sync"
|
"Local directory path to sync"
|
||||||
).env("VAULTLINK_LOCAL_PATH")
|
).env(REQUIRED_OPTIONS.localPath.env)
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("-r, --remote-uri <uri>", "Remote server URI").env(
|
new Option(
|
||||||
"VAULTLINK_REMOTE_URI"
|
REQUIRED_OPTIONS.remoteUri.flags,
|
||||||
)
|
"Remote server URI"
|
||||||
|
).env(REQUIRED_OPTIONS.remoteUri.env)
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("-t, --token <token>", "Authentication token").env(
|
new Option(
|
||||||
"VAULTLINK_TOKEN"
|
REQUIRED_OPTIONS.token.flags,
|
||||||
)
|
"Authentication token"
|
||||||
|
).env(REQUIRED_OPTIONS.token.env)
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("-v, --vault-name <name>", "Vault name").env(
|
new Option(REQUIRED_OPTIONS.vaultName.flags, "Vault name").env(
|
||||||
"VAULTLINK_VAULT_NAME"
|
REQUIRED_OPTIONS.vaultName.env
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
|
|
@ -105,7 +137,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||||
"[OPTIONAL] Line ending style: auto (platform default), lf, crlf"
|
"[OPTIONAL] Line ending style: auto (platform default), lf, crlf"
|
||||||
)
|
)
|
||||||
.default("auto")
|
.default("auto")
|
||||||
.choices(["auto", "lf", "crlf"])
|
.choices([...LINE_ENDING_MODES])
|
||||||
.env("VAULTLINK_LINE_ENDINGS")
|
.env("VAULTLINK_LINE_ENDINGS")
|
||||||
)
|
)
|
||||||
.addHelpText(
|
.addHelpText(
|
||||||
|
|
@ -144,22 +176,6 @@ Environment variables:
|
||||||
const lineEndingsStr = (opts.lineEndings as string | undefined) ?? "auto";
|
const lineEndingsStr = (opts.lineEndings as string | undefined) ?? "auto";
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
|
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
|
||||||
|
|
||||||
const requireOption = <T>(value: T | undefined, name: string): T => {
|
|
||||||
if (value === undefined) {
|
|
||||||
const option = program.options.find(
|
|
||||||
(o) => o.attributeName() === name
|
|
||||||
);
|
|
||||||
const envHint =
|
|
||||||
option?.envVar !== undefined
|
|
||||||
? ` (or set ${option.envVar})`
|
|
||||||
: "";
|
|
||||||
throw new Error(
|
|
||||||
`required option '${option?.flags ?? name}' not specified${envHint}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const requiredLocalPath = requireOption(localPath, "localPath");
|
const requiredLocalPath = requireOption(localPath, "localPath");
|
||||||
const requiredRemoteUri = requireOption(remoteUri, "remoteUri");
|
const requiredRemoteUri = requireOption(remoteUri, "remoteUri");
|
||||||
const requiredToken = requireOption(token, "token");
|
const requiredToken = requireOption(token, "token");
|
||||||
|
|
@ -187,13 +203,11 @@ Environment variables:
|
||||||
}
|
}
|
||||||
const logLevel = logLevelUpper;
|
const logLevel = logLevelUpper;
|
||||||
|
|
||||||
const validLineEndings: readonly string[] = ["auto", "lf", "crlf"];
|
const isLineEndingMode = (value: string): value is LineEndingMode =>
|
||||||
const isLineEndingMode = (value: string): value is LineEndingMode => {
|
(LINE_ENDING_MODES as readonly string[]).includes(value);
|
||||||
return validLineEndings.includes(value);
|
|
||||||
};
|
|
||||||
if (!isLineEndingMode(lineEndingsStr)) {
|
if (!isLineEndingMode(lineEndingsStr)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid line endings mode '${lineEndingsStr}'. Valid values are: ${validLineEndings.join(", ")}`
|
`Invalid line endings mode '${lineEndingsStr}'. Valid values are: ${LINE_ENDING_MODES.join(", ")}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const lineEndings = lineEndingsStr;
|
const lineEndings = lineEndingsStr;
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ import {
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
Logger,
|
Logger,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
type LogLine,
|
LogLine,
|
||||||
type SyncSettings,
|
type SyncSettings,
|
||||||
type StoredDatabase
|
type StoredDatabase
|
||||||
} from "sync-client";
|
} from "sync-client";
|
||||||
import { parseArgs } from "./args";
|
import { parseArgs, type LineEndingMode } from "./args";
|
||||||
import { NodeFileSystemOperations } from "./node-filesystem";
|
import { NodeFileSystemOperations, VAULTLINK_DIR } from "./node-filesystem";
|
||||||
import { FileWatcher } from "./file-watcher";
|
import { FileWatcher } from "./file-watcher";
|
||||||
import { formatLogLine } from "./logger-formatter";
|
import { formatLogLine } from "./logger-formatter";
|
||||||
import packageJson from "../package.json";
|
import packageJson from "../package.json";
|
||||||
|
|
@ -50,7 +50,7 @@ function createLogHandler(minLevel: LogLevel): (logLine: LogLine) => void {
|
||||||
const HEALTH_CHECK_INTERVAL_MS = 30 * 1000;
|
const HEALTH_CHECK_INTERVAL_MS = 30 * 1000;
|
||||||
const PROGRESS_LOG_INTERVAL_MS = 2000;
|
const PROGRESS_LOG_INTERVAL_MS = 2000;
|
||||||
|
|
||||||
function resolveLineEndings(mode: "auto" | "lf" | "crlf"): string {
|
function resolveLineEndings(mode: LineEndingMode): string {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "lf":
|
case "lf":
|
||||||
return "\n";
|
return "\n";
|
||||||
|
|
@ -65,9 +65,13 @@ async function main(): Promise<void> {
|
||||||
const args = parseArgs(process.argv);
|
const args = parseArgs(process.argv);
|
||||||
const absolutePath = path.resolve(args.localPath);
|
const absolutePath = path.resolve(args.localPath);
|
||||||
|
|
||||||
const logger = new Logger();
|
|
||||||
const logHandler = createLogHandler(args.logLevel);
|
const logHandler = createLogHandler(args.logLevel);
|
||||||
logger.onLogEmitted.add(logHandler);
|
// Boot-time messages are emitted directly through logHandler before the
|
||||||
|
// SyncClient (and its Logger) exist; afterwards every log line flows
|
||||||
|
// through client.logger.
|
||||||
|
const emitBoot = (level: LogLevel, message: string): void => {
|
||||||
|
logHandler(new LogLine(level, message));
|
||||||
|
};
|
||||||
|
|
||||||
if (!fsSync.existsSync(absolutePath)) {
|
if (!fsSync.existsSync(absolutePath)) {
|
||||||
fsSync.mkdirSync(absolutePath, { recursive: true });
|
fsSync.mkdirSync(absolutePath, { recursive: true });
|
||||||
|
|
@ -76,27 +80,31 @@ async function main(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(absolutePath);
|
const stats = await fs.stat(absolutePath);
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
logger.error(`${absolutePath} is not a directory`);
|
emitBoot(LogLevel.ERROR, `${absolutePath} is not a directory`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
emitBoot(
|
||||||
|
LogLevel.ERROR,
|
||||||
`Cannot access directory ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`
|
`Cannot access directory ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
logger.info(`VaultLink Local CLI v${packageJson.version}`);
|
emitBoot(LogLevel.INFO, `VaultLink Local CLI v${packageJson.version}`);
|
||||||
logger.info(`Local path: ${absolutePath}`);
|
emitBoot(LogLevel.INFO, `Local path: ${absolutePath}`);
|
||||||
logger.info(`Remote URI: ${args.remoteUri}`);
|
emitBoot(LogLevel.INFO, `Remote URI: ${args.remoteUri}`);
|
||||||
logger.info(`Vault name: ${args.vaultName}`);
|
emitBoot(LogLevel.INFO, `Vault name: ${args.vaultName}`);
|
||||||
if (args.lineEndings !== "auto") {
|
if (args.lineEndings !== "auto") {
|
||||||
logger.info(`Line endings: ${args.lineEndings.toUpperCase()}`);
|
emitBoot(
|
||||||
|
LogLevel.INFO,
|
||||||
|
`Line endings: ${args.lineEndings.toUpperCase()}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataDir = path.join(absolutePath, ".vaultlink");
|
const dataDir = path.join(absolutePath, VAULTLINK_DIR);
|
||||||
const dataFile = path.join(dataDir, "sync-data.json");
|
const dataFile = path.join(dataDir, "sync-data.json");
|
||||||
|
|
||||||
await fs.mkdir(dataDir, { recursive: true });
|
await fs.mkdir(dataDir, { recursive: true });
|
||||||
|
|
@ -105,8 +113,7 @@ async function main(): Promise<void> {
|
||||||
|
|
||||||
const ignorePatterns = [
|
const ignorePatterns = [
|
||||||
...(args.ignorePatterns ?? []),
|
...(args.ignorePatterns ?? []),
|
||||||
".vaultlink/**",
|
`${VAULTLINK_DIR}/**`
|
||||||
".git/**"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const settings: SyncSettings = {
|
const settings: SyncSettings = {
|
||||||
|
|
@ -134,7 +141,10 @@ async function main(): Promise<void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
database = JSON.parse(content) as Partial<StoredDatabase>;
|
database = JSON.parse(content) as Partial<StoredDatabase>;
|
||||||
} catch {
|
} catch {
|
||||||
logger.warn(`Cannot read data file at ${dataFile}`);
|
emitBoot(
|
||||||
|
LogLevel.WARNING,
|
||||||
|
`Cannot read data file at ${dataFile}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -269,7 +279,6 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error: unknown) => {
|
main().catch((error: unknown) => {
|
||||||
// Last-resort handler before the logger exists
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
`Unexpected error: ${error instanceof Error ? error.message : String(error)}`
|
`Unexpected error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import type { Dirent } from "fs";
|
import type { Dirent } from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import type {
|
import type {
|
||||||
FileSystemOperations,
|
FileSystemOperations,
|
||||||
RelativePath,
|
RelativePath,
|
||||||
|
|
@ -8,8 +9,13 @@ import type {
|
||||||
} from "sync-client";
|
} from "sync-client";
|
||||||
import { toUnixPath } from "./path-utils";
|
import { toUnixPath } from "./path-utils";
|
||||||
|
|
||||||
|
// VaultLink's per-vault metadata directory. Holds the persisted sync database
|
||||||
|
// and the tmp files atomicWrite renames into place; the matching `${VAULTLINK_DIR}/**`
|
||||||
|
// ignore pattern keeps everything in here invisible to the file watcher.
|
||||||
|
export const VAULTLINK_DIR = ".vaultlink";
|
||||||
|
|
||||||
export class NodeFileSystemOperations implements FileSystemOperations {
|
export class NodeFileSystemOperations implements FileSystemOperations {
|
||||||
public constructor(private readonly basePath: string) {}
|
public constructor(private readonly basePath: string) { }
|
||||||
|
|
||||||
public async listFilesRecursively(
|
public async listFilesRecursively(
|
||||||
directory: RelativePath | undefined
|
directory: RelativePath | undefined
|
||||||
|
|
@ -132,12 +138,37 @@ export class NodeFileSystemOperations implements FileSystemOperations {
|
||||||
content: Uint8Array | string,
|
content: Uint8Array | string,
|
||||||
encoding?: BufferEncoding
|
encoding?: BufferEncoding
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const tmpPath = fullPath + ".tmp";
|
const tmpDir = path.join(this.basePath, VAULTLINK_DIR);
|
||||||
await fs.writeFile(tmpPath, content, encoding);
|
await fs.mkdir(tmpDir, { recursive: true });
|
||||||
const fd = await fs.open(tmpPath, "r");
|
const tmpPath = path.join(tmpDir, `atomic-write-${randomUUID()}.tmp`);
|
||||||
await fd.datasync();
|
try {
|
||||||
await fd.close();
|
await fs.writeFile(tmpPath, content, encoding);
|
||||||
await fs.rename(tmpPath, fullPath);
|
const fd = await fs.open(tmpPath, "r");
|
||||||
|
try {
|
||||||
|
await fd.datasync();
|
||||||
|
} finally {
|
||||||
|
await fd.close();
|
||||||
|
}
|
||||||
|
await fs.rename(tmpPath, fullPath);
|
||||||
|
await this.syncDirectory(path.dirname(fullPath));
|
||||||
|
} catch (error) {
|
||||||
|
await fs.unlink(tmpPath).catch(() => undefined);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the rename durable by fsync'ing the destination's parent directory.
|
||||||
|
// Skipped on Windows: fsync on a directory handle isn't supported there
|
||||||
|
private async syncDirectory(dir: string): Promise<void> {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fd = await fs.open(dir, "r");
|
||||||
|
try {
|
||||||
|
await fd.sync();
|
||||||
|
} finally {
|
||||||
|
await fd.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async walkDirectory(
|
private async walkDirectory(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,14 @@ export function toUnixPath(nativePath: string): string {
|
||||||
return nativePath.split(path.sep).join(path.posix.sep);
|
return nativePath.split(path.sep).join(path.posix.sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match a file path against a glob pattern
|
// Match a file path against a glob pattern.
|
||||||
// Extends path.matchesGlob so that "dir/**" also matches the directory itself
|
//
|
||||||
|
// Behaves like Node's path.matchesGlob with one extension: `dir/**` matches
|
||||||
|
// the directory `dir` itself, not only its descendants. The watcher feeds us
|
||||||
|
// a directory's relative path (e.g. ".git") at the same time it's about to
|
||||||
|
// recurse into it, and the natural way for users to write the ignore pattern
|
||||||
|
// is `.git/**` — under stdlib semantics that pattern would let the directory
|
||||||
|
// through and only block its children, defeating the prune.
|
||||||
export function matchesGlob(filePath: string, pattern: string): boolean {
|
export function matchesGlob(filePath: string, pattern: string): boolean {
|
||||||
if (pattern.endsWith("/**") && filePath === pattern.slice(0, -3)) {
|
if (pattern.endsWith("/**") && filePath === pattern.slice(0, -3)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ module.exports = (env, argv) => ({
|
||||||
const destinations = [
|
const destinations = [
|
||||||
"/volumes/syncthing/Desktop/test/test/.obsidian/plugins/vault-link",
|
"/volumes/syncthing/Desktop/test/test/.obsidian/plugins/vault-link",
|
||||||
"/volumes/syncthing/Desktop/test/test2/.obsidian/plugins/vault-link"
|
"/volumes/syncthing/Desktop/test/test2/.obsidian/plugins/vault-link"
|
||||||
// "/home/andras/obsidian-test/.obsidian/plugins/vault-link"
|
|
||||||
];
|
];
|
||||||
destinations.forEach((destination) => {
|
destinations.forEach((destination) => {
|
||||||
fs.copy(source, destination)
|
fs.copy(source, destination)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue