mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-13 19:11:15 +00:00
On Linux, fs.Stats.mtimeMs and ctimeMs return floats with sub-millisecond precision derived from the kernel's nanosecond filesystem mtime. Stored raw, this produces document timestamps like 1778511180024.462 in CouchDB rather than integer milliseconds. Mobile clients running LiveSync 0.25.60 have been observed to crash when processing change-feed updates carrying non-integer millisecond timestamps from CLI-written documents. Desktop and mobile GUI plugins write integer milliseconds, so the crash only manifests when the headless CLI on Linux is the source. Whether the issue was introduced in 0.25.60 or had been latent in earlier versions hasn't been investigated; 0.25.60 is the version where the crash was confirmed and the fix verified. Floor the values at every stat-read site (six across three adapters and one command) so CLI-written documents carry integer-millisecond timestamps consistent with the rest of the mesh.
125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
import * as fs from "fs/promises";
|
|
import * as path from "path";
|
|
import type { UXDataWriteOptions } from "@lib/common/types";
|
|
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
|
import type { NodeStat } from "./NodeTypes";
|
|
|
|
/**
|
|
* Storage adapter implementation for Node.js
|
|
*/
|
|
export class NodeStorageAdapter implements IStorageAdapter<NodeStat> {
|
|
constructor(private basePath: string) {}
|
|
|
|
private resolvePath(p: string): string {
|
|
return path.join(this.basePath, p);
|
|
}
|
|
|
|
async exists(p: string): Promise<boolean> {
|
|
try {
|
|
await fs.access(this.resolvePath(p));
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async trystat(p: string): Promise<NodeStat | null> {
|
|
try {
|
|
const stat = await fs.stat(this.resolvePath(p));
|
|
return {
|
|
size: stat.size,
|
|
mtime: Math.floor(stat.mtimeMs),
|
|
ctime: Math.floor(stat.ctimeMs),
|
|
type: stat.isDirectory() ? "folder" : "file",
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async stat(p: string): Promise<NodeStat | null> {
|
|
return await this.trystat(p);
|
|
}
|
|
|
|
async mkdir(p: string): Promise<void> {
|
|
await fs.mkdir(this.resolvePath(p), { recursive: true });
|
|
}
|
|
|
|
async remove(p: string): Promise<void> {
|
|
const fullPath = this.resolvePath(p);
|
|
const stat = await fs.stat(fullPath);
|
|
if (stat.isDirectory()) {
|
|
await fs.rm(fullPath, { recursive: true, force: true });
|
|
} else {
|
|
await fs.unlink(fullPath);
|
|
}
|
|
}
|
|
|
|
async read(p: string): Promise<string> {
|
|
return await fs.readFile(this.resolvePath(p), "utf-8");
|
|
}
|
|
|
|
async readBinary(p: string): Promise<ArrayBuffer> {
|
|
const buffer = await fs.readFile(this.resolvePath(p));
|
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
|
|
}
|
|
|
|
async write(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
|
const fullPath = this.resolvePath(p);
|
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
await fs.writeFile(fullPath, data, "utf-8");
|
|
|
|
if (options?.mtime || options?.ctime) {
|
|
const atime = options.mtime ? new Date(options.mtime) : new Date();
|
|
const mtime = options.mtime ? new Date(options.mtime) : new Date();
|
|
await fs.utimes(fullPath, atime, mtime);
|
|
}
|
|
}
|
|
|
|
async writeBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
|
const fullPath = this.resolvePath(p);
|
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
await fs.writeFile(fullPath, new Uint8Array(data));
|
|
|
|
if (options?.mtime || options?.ctime) {
|
|
const atime = options.mtime ? new Date(options.mtime) : new Date();
|
|
const mtime = options.mtime ? new Date(options.mtime) : new Date();
|
|
await fs.utimes(fullPath, atime, mtime);
|
|
}
|
|
}
|
|
|
|
async append(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
|
const fullPath = this.resolvePath(p);
|
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
await fs.appendFile(fullPath, data, "utf-8");
|
|
|
|
if (options?.mtime || options?.ctime) {
|
|
const atime = options.mtime ? new Date(options.mtime) : new Date();
|
|
const mtime = options.mtime ? new Date(options.mtime) : new Date();
|
|
await fs.utimes(fullPath, atime, mtime);
|
|
}
|
|
}
|
|
|
|
async list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
|
|
const fullPath = this.resolvePath(basePath);
|
|
try {
|
|
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
const files: string[] = [];
|
|
const folders: string[] = [];
|
|
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(basePath, entry.name).replace(/\\/g, "/");
|
|
if (entry.isDirectory()) {
|
|
folders.push(entryPath);
|
|
} else if (entry.isFile()) {
|
|
files.push(entryPath);
|
|
}
|
|
}
|
|
|
|
return { files, folders };
|
|
} catch {
|
|
return { files: [], folders: [] };
|
|
}
|
|
}
|
|
}
|