mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-31 07:47:05 +00:00
fix(storage): floor write-option timestamps in Obsidian adapters
Obsidian's mobile storage layer forwards mtime/ctime to Capacitor's Filesystem.setTimes, whose native binding casts the value to a Java Long. A non-integer (float) timestamp makes that cast throw (ClassCastException: Double cannot be cast to Long), which crashes the app on launch as soon as such a document is replicated in. Float timestamps can reach the database from any client that stores fs.Stats.mtimeMs without flooring. Coerce mtime/ctime to integer ms at the Obsidian vault and storage adapter write boundary, so a float already present in the mesh can't crash the app regardless of where it came from. The truly central choke point is dbToStorage in livesync-commonlib; a matching guard there would cover the CLI and webapp too. This change protects the platform that actually crashes. Claude-Session: https://claude.ai/code/session_0123E9jVQrsgu3zb82Csuwhi
This commit is contained in:
@@ -2,6 +2,7 @@ import type { UXDataWriteOptions } from "@lib/common/types";
|
|||||||
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
||||||
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
|
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
|
||||||
import type { Stat, App } from "obsidian";
|
import type { Stat, App } from "obsidian";
|
||||||
|
import { toIntegerTimestamps } from "./sanitizeWriteOptions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage adapter implementation for Obsidian
|
* Storage adapter implementation for Obsidian
|
||||||
@@ -40,15 +41,15 @@ export class ObsidianStorageAdapter implements IStorageAdapter<Stat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async write(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
async write(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||||
return await this.app.vault.adapter.write(path, data, options);
|
return await this.app.vault.adapter.write(path, data, toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
async writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||||
return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options);
|
return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async append(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
async append(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||||
return await this.app.vault.adapter.append(path, data, options);
|
return await this.app.vault.adapter.append(path, data, toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
|
list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { UXDataWriteOptions } from "@lib/common/types";
|
|||||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||||
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
|
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
|
||||||
import type { TFile, App, TFolder } from "obsidian";
|
import type { TFile, App, TFolder } from "obsidian";
|
||||||
|
import { toIntegerTimestamps } from "./sanitizeWriteOptions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vault adapter implementation for Obsidian
|
* Vault adapter implementation for Obsidian
|
||||||
@@ -22,19 +23,19 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||||
return await this.app.vault.modify(file, data, options);
|
return await this.app.vault.modify(file, data, toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
async modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||||
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
|
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
|
async create(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
|
||||||
return await this.app.vault.create(path, data, options);
|
return await this.app.vault.create(path, data, toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<TFile> {
|
async createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<TFile> {
|
||||||
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
|
return await this.app.vault.createBinary(path, toArrayBuffer(data), toIntegerTimestamps(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce the timestamp fields of a write-options object to integer milliseconds.
|
||||||
|
*
|
||||||
|
* On mobile, Obsidian forwards `mtime`/`ctime` to Capacitor's
|
||||||
|
* Filesystem.setTimes, whose native binding casts the value to a Java `Long`.
|
||||||
|
* A non-integer (float) timestamp makes that cast throw
|
||||||
|
* `ClassCastException: Double cannot be cast to Long`, which crashes the app on
|
||||||
|
* launch as soon as such a document is replicated in. Float timestamps can
|
||||||
|
* enter the database from any client that stores `fs.Stats.mtimeMs` without
|
||||||
|
* flooring. Flooring at the storage boundary guarantees every Obsidian write
|
||||||
|
* carries an integer, so a float timestamp already present in the mesh cannot
|
||||||
|
* brick the app.
|
||||||
|
*
|
||||||
|
* Returns a shallow copy so the caller's options object is not mutated; passes
|
||||||
|
* `undefined` through unchanged.
|
||||||
|
*/
|
||||||
|
export function toIntegerTimestamps(options?: UXDataWriteOptions): UXDataWriteOptions | undefined {
|
||||||
|
if (!options) return options;
|
||||||
|
const sanitized: UXDataWriteOptions = { ...options };
|
||||||
|
if (typeof sanitized.mtime === "number") sanitized.mtime = Math.floor(sanitized.mtime);
|
||||||
|
if (typeof sanitized.ctime === "number") sanitized.ctime = Math.floor(sanitized.ctime);
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user