mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-03 06:11:50 +00:00
Add self-hosted-livesync-cli to src/apps/cli as a headless, and a dedicated version.
This commit is contained in:
28
src/apps/cli/adapters/NodeConversionAdapter.ts
Normal file
28
src/apps/cli/adapters/NodeConversionAdapter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as path from "path";
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for Node.js
|
||||
*/
|
||||
export class NodeConversionAdapter implements IConversionAdapter<NodeFile, NodeFolder> {
|
||||
nativeFileToUXFileInfoStub(file: NodeFile): UXFileInfoStub {
|
||||
return {
|
||||
name: path.basename(file.path),
|
||||
path: file.path,
|
||||
stat: file.stat,
|
||||
isFolder: false,
|
||||
};
|
||||
}
|
||||
|
||||
nativeFolderToUXFolder(folder: NodeFolder): UXFolderInfo {
|
||||
return {
|
||||
name: path.basename(folder.path),
|
||||
path: folder.path,
|
||||
isFolder: true,
|
||||
children: [],
|
||||
parent: path.dirname(folder.path) as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
153
src/apps/cli/adapters/NodeFileSystemAdapter.ts
Normal file
153
src/apps/cli/adapters/NodeFileSystemAdapter.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import type { FilePath, UXStat } from "@lib/common/types";
|
||||
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
|
||||
import { NodePathAdapter } from "./NodePathAdapter";
|
||||
import { NodeTypeGuardAdapter } from "./NodeTypeGuardAdapter";
|
||||
import { NodeConversionAdapter } from "./NodeConversionAdapter";
|
||||
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for Node.js
|
||||
*/
|
||||
export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeFile, NodeFolder, NodeStat> {
|
||||
readonly path: NodePathAdapter;
|
||||
readonly typeGuard: NodeTypeGuardAdapter;
|
||||
readonly conversion: NodeConversionAdapter;
|
||||
readonly storage: NodeStorageAdapter;
|
||||
readonly vault: NodeVaultAdapter;
|
||||
|
||||
private fileCache = new Map<string, NodeFile>();
|
||||
|
||||
constructor(private basePath: string) {
|
||||
this.path = new NodePathAdapter();
|
||||
this.typeGuard = new NodeTypeGuardAdapter();
|
||||
this.conversion = new NodeConversionAdapter();
|
||||
this.storage = new NodeStorageAdapter(basePath);
|
||||
this.vault = new NodeVaultAdapter(basePath);
|
||||
}
|
||||
|
||||
private resolvePath(p: FilePath | string): string {
|
||||
return path.join(this.basePath, p);
|
||||
}
|
||||
|
||||
private normalisePath(p: FilePath | string): string {
|
||||
return this.path.normalisePath(p as string);
|
||||
}
|
||||
|
||||
async getAbstractFileByPath(p: FilePath | string): Promise<NodeFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
|
||||
const cached = this.fileCache.get(pathStr);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return await this.refreshFile(pathStr);
|
||||
}
|
||||
|
||||
async getAbstractFileByPathInsensitive(p: FilePath | string): Promise<NodeFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
|
||||
const exact = await this.getAbstractFileByPath(pathStr);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
const lowerPath = pathStr.toLowerCase();
|
||||
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
|
||||
if (cachedPath.toLowerCase() === lowerPath) {
|
||||
return cachedFile;
|
||||
}
|
||||
}
|
||||
|
||||
await this.scanDirectory();
|
||||
|
||||
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
|
||||
if (cachedPath.toLowerCase() === lowerPath) {
|
||||
return cachedFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFiles(): Promise<NodeFile[]> {
|
||||
if (this.fileCache.size === 0) {
|
||||
await this.scanDirectory();
|
||||
}
|
||||
return Array.from(this.fileCache.values());
|
||||
}
|
||||
|
||||
async statFromNative(file: NodeFile): Promise<UXStat> {
|
||||
return file.stat;
|
||||
}
|
||||
|
||||
async reconcileInternalFile(p: string): Promise<void> {
|
||||
// No-op in Node.js version
|
||||
// This is used by Obsidian to sync internal file metadata
|
||||
}
|
||||
|
||||
async refreshFile(p: string): Promise<NodeFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
try {
|
||||
const fullPath = this.resolvePath(pathStr);
|
||||
const stat = await fs.stat(fullPath);
|
||||
if (!stat.isFile()) {
|
||||
this.fileCache.delete(pathStr);
|
||||
return null;
|
||||
}
|
||||
|
||||
const file: NodeFile = {
|
||||
path: pathStr as FilePath,
|
||||
stat: {
|
||||
size: stat.size,
|
||||
mtime: stat.mtimeMs,
|
||||
ctime: stat.ctimeMs,
|
||||
type: "file",
|
||||
},
|
||||
};
|
||||
this.fileCache.set(pathStr, file);
|
||||
return file;
|
||||
} catch {
|
||||
this.fileCache.delete(pathStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to recursively scan directory and populate file cache
|
||||
*/
|
||||
async scanDirectory(relativePath: string = ""): Promise<void> {
|
||||
const fullPath = this.resolvePath(relativePath);
|
||||
try {
|
||||
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryRelativePath = path.join(relativePath, entry.name).replace(/\\/g, "/");
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await this.scanDirectory(entryRelativePath);
|
||||
} else if (entry.isFile()) {
|
||||
const entryFullPath = this.resolvePath(entryRelativePath);
|
||||
const stat = await fs.stat(entryFullPath);
|
||||
const file: NodeFile = {
|
||||
path: entryRelativePath as FilePath,
|
||||
stat: {
|
||||
size: stat.size,
|
||||
mtime: stat.mtimeMs,
|
||||
ctime: stat.ctimeMs,
|
||||
type: "file",
|
||||
},
|
||||
};
|
||||
this.fileCache.set(entryRelativePath, file);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory doesn't exist or is not readable
|
||||
console.error(`Error scanning directory ${fullPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/apps/cli/adapters/NodePathAdapter.ts
Normal file
18
src/apps/cli/adapters/NodePathAdapter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as path from "path";
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile } from "./NodeTypes";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Node.js
|
||||
*/
|
||||
export class NodePathAdapter implements IPathAdapter<NodeFile> {
|
||||
getPath(file: string | NodeFile): FilePath {
|
||||
return (typeof file === "string" ? file : file.path) as FilePath;
|
||||
}
|
||||
|
||||
normalisePath(p: string): string {
|
||||
// Normalize path separators to forward slashes (like Obsidian)
|
||||
return path.normalize(p).replace(/\\/g, "/");
|
||||
}
|
||||
}
|
||||
124
src/apps/cli/adapters/NodeStorageAdapter.ts
Normal file
124
src/apps/cli/adapters/NodeStorageAdapter.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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: stat.mtimeMs,
|
||||
ctime: 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: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/apps/cli/adapters/NodeTypeGuardAdapter.ts
Normal file
15
src/apps/cli/adapters/NodeTypeGuardAdapter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
|
||||
/**
|
||||
* Type guard adapter implementation for Node.js
|
||||
*/
|
||||
export class NodeTypeGuardAdapter implements ITypeGuardAdapter<NodeFile, NodeFolder> {
|
||||
isFile(file: any): file is NodeFile {
|
||||
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is NodeFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true;
|
||||
}
|
||||
}
|
||||
22
src/apps/cli/adapters/NodeTypes.ts
Normal file
22
src/apps/cli/adapters/NodeTypes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { FilePath, UXStat } from "@lib/common/types";
|
||||
|
||||
/**
|
||||
* Node.js file representation
|
||||
*/
|
||||
export type NodeFile = {
|
||||
path: FilePath;
|
||||
stat: UXStat;
|
||||
};
|
||||
|
||||
/**
|
||||
* Node.js folder representation
|
||||
*/
|
||||
export type NodeFolder = {
|
||||
path: FilePath;
|
||||
isFolder: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Node.js stat type (compatible with UXStat)
|
||||
*/
|
||||
export type NodeStat = UXStat;
|
||||
118
src/apps/cli/adapters/NodeVaultAdapter.ts
Normal file
118
src/apps/cli/adapters/NodeVaultAdapter.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for Node.js
|
||||
*/
|
||||
export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
constructor(private basePath: string) {}
|
||||
|
||||
private resolvePath(p: string): string {
|
||||
return path.join(this.basePath, p);
|
||||
}
|
||||
|
||||
async read(file: NodeFile): Promise<string> {
|
||||
return await fs.readFile(this.resolvePath(file.path), "utf-8");
|
||||
}
|
||||
|
||||
async cachedRead(file: NodeFile): Promise<string> {
|
||||
// No caching in CLI version, just read directly
|
||||
return await this.read(file);
|
||||
}
|
||||
|
||||
async readBinary(file: NodeFile): Promise<ArrayBuffer> {
|
||||
const buffer = await fs.readFile(this.resolvePath(file.path));
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
async modify(file: NodeFile, data: string, options?: UXDataWriteOptions): Promise<void> {
|
||||
const fullPath = this.resolvePath(file.path);
|
||||
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 modifyBinary(file: NodeFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
|
||||
const fullPath = this.resolvePath(file.path);
|
||||
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 create(p: string, data: string, options?: UXDataWriteOptions): Promise<NodeFile> {
|
||||
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);
|
||||
}
|
||||
|
||||
const stat = await fs.stat(fullPath);
|
||||
return {
|
||||
path: p as any,
|
||||
stat: {
|
||||
size: stat.size,
|
||||
mtime: stat.mtimeMs,
|
||||
ctime: stat.ctimeMs,
|
||||
type: "file",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<NodeFile> {
|
||||
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);
|
||||
}
|
||||
|
||||
const stat = await fs.stat(fullPath);
|
||||
return {
|
||||
path: p as any,
|
||||
stat: {
|
||||
size: stat.size,
|
||||
mtime: stat.mtimeMs,
|
||||
ctime: stat.ctimeMs,
|
||||
type: "file",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async delete(file: NodeFile | NodeFolder, force = false): Promise<void> {
|
||||
const fullPath = this.resolvePath(file.path);
|
||||
const stat = await fs.stat(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
await fs.rm(fullPath, { recursive: true, force });
|
||||
} else {
|
||||
await fs.unlink(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
async trash(file: NodeFile | NodeFolder, force = false): Promise<void> {
|
||||
// In CLI, trash is the same as delete (no recycle bin)
|
||||
await this.delete(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
// No-op in CLI version (no event system)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user