mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-26 13:27:05 +00:00
187 lines
7.0 KiB
TypeScript
187 lines
7.0 KiB
TypeScript
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
|
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
|
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
|
|
|
/**
|
|
* Vault adapter implementation for FileSystem API
|
|
*/
|
|
export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
|
|
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
|
|
|
async read(file: FSAPIFile): Promise<string> {
|
|
const fileObject = await file.handle.getFile();
|
|
return await fileObject.text();
|
|
}
|
|
|
|
async cachedRead(file: FSAPIFile): Promise<string> {
|
|
// No caching in webapp version, just read directly
|
|
return await this.read(file);
|
|
}
|
|
|
|
async readBinary(file: FSAPIFile): Promise<ArrayBuffer> {
|
|
const fileObject = await file.handle.getFile();
|
|
return await fileObject.arrayBuffer();
|
|
}
|
|
|
|
async modify(file: FSAPIFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
|
const writable = await file.handle.createWritable();
|
|
await writable.write(data);
|
|
await writable.close();
|
|
}
|
|
|
|
async modifyBinary(file: FSAPIFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
|
const writable = await file.handle.createWritable();
|
|
await writable.write(data);
|
|
await writable.close();
|
|
}
|
|
|
|
async create(p: string, data: string, options?: UXDataWriteOptions): Promise<FSAPIFile> {
|
|
const parts = p.split("/").filter((part) => part !== "");
|
|
const fileName = parts[parts.length - 1];
|
|
|
|
// Navigate to parent directory, creating as needed
|
|
let currentHandle = this.rootHandle;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
currentHandle = await currentHandle.getDirectoryHandle(parts[i], { create: true });
|
|
}
|
|
|
|
// Create the file
|
|
const fileHandle = await currentHandle.getFileHandle(fileName, { create: true });
|
|
const writable = await fileHandle.createWritable();
|
|
await writable.write(data);
|
|
await writable.close();
|
|
|
|
// Get file metadata
|
|
const fileObject = await fileHandle.getFile();
|
|
|
|
return {
|
|
path: p as FilePath,
|
|
stat: {
|
|
size: fileObject.size,
|
|
mtime: fileObject.lastModified,
|
|
ctime: fileObject.lastModified,
|
|
type: "file",
|
|
},
|
|
handle: fileHandle,
|
|
};
|
|
}
|
|
|
|
async createBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<FSAPIFile> {
|
|
const parts = p.split("/").filter((part) => part !== "");
|
|
const fileName = parts[parts.length - 1];
|
|
|
|
// Navigate to parent directory, creating as needed
|
|
let currentHandle = this.rootHandle;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
currentHandle = await currentHandle.getDirectoryHandle(parts[i], { create: true });
|
|
}
|
|
|
|
// Create the file
|
|
const fileHandle = await currentHandle.getFileHandle(fileName, { create: true });
|
|
const writable = await fileHandle.createWritable();
|
|
await writable.write(data);
|
|
await writable.close();
|
|
|
|
// Get file metadata
|
|
const fileObject = await fileHandle.getFile();
|
|
|
|
return {
|
|
path: p as FilePath,
|
|
stat: {
|
|
size: fileObject.size,
|
|
mtime: fileObject.lastModified,
|
|
ctime: fileObject.lastModified,
|
|
type: "file",
|
|
},
|
|
handle: fileHandle,
|
|
};
|
|
}
|
|
|
|
private async deletePathIfExists(path: string): Promise<void> {
|
|
const parts = path.split("/").filter((part) => part !== "");
|
|
const name = parts.pop();
|
|
if (!name) return;
|
|
|
|
try {
|
|
let currentHandle = this.rootHandle;
|
|
for (const part of parts) {
|
|
currentHandle = await currentHandle.getDirectoryHandle(part);
|
|
}
|
|
await currentHandle.removeEntry(name);
|
|
} catch (error) {
|
|
if ((error as { name?: unknown })?.name === "NotFoundError") return;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async rename(file: FSAPIFile, newPath: string): Promise<void> {
|
|
const source = await file.handle.getFile();
|
|
const data = await source.arrayBuffer();
|
|
const oldPath = file.path;
|
|
const oldPathParts = oldPath.split("/");
|
|
const oldName = oldPathParts.pop() ?? "file";
|
|
const temporaryName = `.${oldName}.livesync-rename-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
|
|
const temporaryPath = [...oldPathParts, temporaryName].join("/");
|
|
const temporaryFile = await this.createBinary(temporaryPath, data);
|
|
|
|
try {
|
|
await this.delete(file);
|
|
} catch (error) {
|
|
await this.delete(temporaryFile, true);
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
const renamedFile = await this.createBinary(newPath, data);
|
|
file.path = renamedFile.path;
|
|
file.stat = renamedFile.stat;
|
|
file.handle = renamedFile.handle;
|
|
} catch (error) {
|
|
try {
|
|
await this.deletePathIfExists(newPath);
|
|
} catch (cleanupError) {
|
|
throw new Error(
|
|
`Could not rename ${oldPath} to ${newPath}, or remove the incomplete target. A temporary copy remains at ${temporaryPath}. Rename error: ${String(error)}. Cleanup error: ${String(cleanupError)}`
|
|
);
|
|
}
|
|
try {
|
|
const restoredFile = await this.createBinary(oldPath, data);
|
|
file.path = restoredFile.path;
|
|
file.stat = restoredFile.stat;
|
|
file.handle = restoredFile.handle;
|
|
await this.delete(temporaryFile, true);
|
|
} catch (restoreError) {
|
|
throw new Error(
|
|
`Could not rename ${oldPath} to ${newPath}, or restore it. A temporary copy remains at ${temporaryPath}. Rename error: ${String(error)}. Restore error: ${String(restoreError)}`
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
await this.delete(temporaryFile, true);
|
|
}
|
|
|
|
async delete(file: FSAPIFile | FSAPIFolder, force = false): Promise<void> {
|
|
const parts = file.path.split("/").filter((part) => part !== "");
|
|
const name = parts[parts.length - 1];
|
|
|
|
// Navigate to parent directory
|
|
let currentHandle = this.rootHandle;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
|
|
}
|
|
|
|
// Remove the entry
|
|
await currentHandle.removeEntry(name, { recursive: force });
|
|
}
|
|
|
|
async trash(file: FSAPIFile | FSAPIFolder, force = false): Promise<void> {
|
|
// In webapp, trash is the same as delete (no recycle bin)
|
|
await this.delete(file, force);
|
|
}
|
|
|
|
trigger(name: string, ...data: unknown[]): void {
|
|
// No-op in webapp version (no event system yet)
|
|
}
|
|
}
|