Move file handling logic inside of client
This commit is contained in:
parent
db8e4bc2e7
commit
fde1fecbb6
7 changed files with 151 additions and 137 deletions
69
frontend/obsidian-plugin/src/obsidian-file-system.ts
Normal file
69
frontend/obsidian-plugin/src/obsidian-file-system.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { normalizePath, Stat, Vault } from "obsidian";
|
||||
import { FileSystemOperations, RelativePath } from "sync-client";
|
||||
|
||||
export class ObsidianFileSystemOperations implements FileSystemOperations {
|
||||
public constructor(private readonly vault: Vault) {}
|
||||
|
||||
public async listAllFiles(): Promise<RelativePath[]> {
|
||||
return this.vault.getFiles().map((file) => file.path);
|
||||
}
|
||||
|
||||
public async read(path: RelativePath): Promise<Uint8Array> {
|
||||
return new Uint8Array(
|
||||
await this.vault.adapter.readBinary(normalizePath(path))
|
||||
);
|
||||
}
|
||||
|
||||
public async write(path: RelativePath, content: Uint8Array): Promise<void> {
|
||||
return this.vault.adapter.writeBinary(
|
||||
normalizePath(path),
|
||||
content.buffer as ArrayBuffer
|
||||
);
|
||||
}
|
||||
|
||||
public async atomicUpdateText(
|
||||
path: RelativePath,
|
||||
updater: (currentContent: string) => string
|
||||
): Promise<string> {
|
||||
return this.vault.adapter.process(normalizePath(path), updater);
|
||||
}
|
||||
|
||||
public async getFileSize(path: RelativePath): Promise<number> {
|
||||
return (await this.statFile(path)).size;
|
||||
}
|
||||
|
||||
public async getModificationTime(path: RelativePath): Promise<Date> {
|
||||
return new Date((await this.statFile(path)).mtime);
|
||||
}
|
||||
|
||||
public async exists(path: RelativePath): Promise<boolean> {
|
||||
return this.vault.adapter.exists(normalizePath(path));
|
||||
}
|
||||
|
||||
public async createDirectory(path: RelativePath): Promise<void> {
|
||||
return this.vault.adapter.mkdir(normalizePath(path));
|
||||
}
|
||||
|
||||
public async delete(path: RelativePath): Promise<void> {
|
||||
if (!(await this.vault.adapter.trashSystem(normalizePath(path)))) {
|
||||
return this.vault.adapter.remove(normalizePath(path));
|
||||
}
|
||||
}
|
||||
|
||||
public async rename(
|
||||
oldPath: RelativePath,
|
||||
newPath: RelativePath
|
||||
): Promise<void> {
|
||||
return this.vault.adapter.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
private async statFile(path: string): Promise<Stat> {
|
||||
const file = await this.vault.adapter.stat(normalizePath(path));
|
||||
|
||||
if (!file) {
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,12 @@ import "../manifest.json";
|
|||
import { SyncSettingsTab } from "./views/settings-tab";
|
||||
import { HistoryView } from "./views/history-view";
|
||||
import { ObsidianFileEventHandler } from "./obisidan-event-handler";
|
||||
import { ObsidianFileOperations } from "./obsidian-file-operations";
|
||||
import { StatusBar } from "./views/status-bar";
|
||||
|
||||
import { LogsView } from "./views/logs-view";
|
||||
import { StatusDescription } from "./views/status-description";
|
||||
import { Logger, SyncClient } from "sync-client";
|
||||
import { ObsidianFileSystemOperations } from "./obsidian-file-system";
|
||||
|
||||
export default class VaultLinkPlugin extends Plugin {
|
||||
private settingsTab: SyncSettingsTab | undefined;
|
||||
|
|
@ -21,7 +21,7 @@ export default class VaultLinkPlugin extends Plugin {
|
|||
Logger.getInstance().info("Starting plugin");
|
||||
|
||||
this.client = await SyncClient.create(
|
||||
new ObsidianFileOperations(this.app.vault),
|
||||
new ObsidianFileSystemOperations(this.app.vault),
|
||||
{
|
||||
load: this.loadData.bind(this),
|
||||
save: this.saveData.bind(this)
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import type { RelativePath } from "src/persistence/database";
|
||||
|
||||
export interface FileOperations {
|
||||
listAllFiles: () => Promise<RelativePath[]>;
|
||||
|
||||
read: (path: RelativePath) => Promise<Uint8Array>;
|
||||
|
||||
getFileSize: (path: RelativePath) => Promise<number>;
|
||||
|
||||
exists: (path: RelativePath) => Promise<boolean>;
|
||||
|
||||
getModificationTime: (path: RelativePath) => Promise<Date>;
|
||||
|
||||
// Create and write the file if it doesn't exist. Otherwise, it has the same behavior as write.
|
||||
// All parent directories are created if they don't exist.
|
||||
create: (path: RelativePath, newContent: Uint8Array) => Promise<void>;
|
||||
|
||||
// Update the file at the given path.
|
||||
// If the file's content is different from `expectedContent`, the a 3-way merge is performed before writing.
|
||||
// If the file no longer exists, the file is not recreated and an empty array is returned.
|
||||
write: (
|
||||
path: RelativePath,
|
||||
expectedContent: Uint8Array,
|
||||
newContent: Uint8Array
|
||||
) => Promise<Uint8Array>;
|
||||
|
||||
remove: (path: RelativePath) => Promise<void>;
|
||||
|
||||
move: (oldPath: RelativePath, newPath: RelativePath) => Promise<void>;
|
||||
|
||||
isFileEligibleForSync: (path: RelativePath) => boolean;
|
||||
}
|
||||
|
|
@ -1,53 +1,56 @@
|
|||
import type { Stat, Vault } from "obsidian";
|
||||
import { normalizePath } from "obsidian";
|
||||
import { Platform } from "obsidian";
|
||||
import type { FileOperations, RelativePath } from "sync-client";
|
||||
import { Logger, isFileTypeMergable, mergeText } from "sync-client";
|
||||
import { Logger } from "src/tracing/logger";
|
||||
import { FileSystemOperations } from "./filesystem-operations";
|
||||
import { RelativePath } from "src/persistence/database";
|
||||
import { isBinary, isFileTypeMergable, mergeText } from "sync_lib";
|
||||
|
||||
export class ObsidianFileOperations implements FileOperations {
|
||||
public constructor(private readonly vault: Vault) {}
|
||||
export class FileOperations {
|
||||
public constructor(private readonly fs: FileSystemOperations) {}
|
||||
|
||||
public async listAllFiles(): Promise<RelativePath[]> {
|
||||
const files = this.vault.getFiles();
|
||||
const files = await this.fs.listAllFiles();
|
||||
Logger.getInstance().debug(`Listing all files, found ${files.length}`);
|
||||
return files.map((file) => file.path);
|
||||
return files;
|
||||
}
|
||||
|
||||
public async read(path: RelativePath): Promise<Uint8Array> {
|
||||
Logger.getInstance().debug(`Reading file: ${path}`);
|
||||
if (isFileTypeMergable(path)) {
|
||||
let text = await this.vault.adapter.read(normalizePath(path));
|
||||
const content = await this.fs.read(path);
|
||||
|
||||
text = text.replace(/\r\n/g, "\n");
|
||||
|
||||
return new TextEncoder().encode(text);
|
||||
if (isBinary(content)) {
|
||||
return content;
|
||||
}
|
||||
return new Uint8Array(
|
||||
await this.vault.adapter.readBinary(normalizePath(path))
|
||||
);
|
||||
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
let text = decoder.decode(content);
|
||||
text = text.replace(/\r\n/g, "\n");
|
||||
|
||||
return new TextEncoder().encode(text);
|
||||
}
|
||||
|
||||
public async getFileSize(path: RelativePath): Promise<number> {
|
||||
Logger.getInstance().debug(`Getting file size: ${path}`);
|
||||
return (await this.statFile(path)).size;
|
||||
return this.fs.getFileSize(path);
|
||||
}
|
||||
|
||||
public async getModificationTime(path: RelativePath): Promise<Date> {
|
||||
Logger.getInstance().debug(`Getting modification time: ${path}`);
|
||||
return new Date((await this.statFile(path)).mtime);
|
||||
return this.fs.getModificationTime(path);
|
||||
}
|
||||
|
||||
public async exists(path: RelativePath): Promise<boolean> {
|
||||
Logger.getInstance().debug(`Checking existance of ${path}`);
|
||||
return this.vault.adapter.exists(normalizePath(path));
|
||||
return this.fs.exists(path);
|
||||
}
|
||||
|
||||
// Create and write the file if it doesn't exist. Otherwise, it has the same behavior as write.
|
||||
// All parent directories are created if they don't exist.
|
||||
public async create(
|
||||
path: RelativePath,
|
||||
newContent: Uint8Array
|
||||
): Promise<void> {
|
||||
Logger.getInstance().debug(`Creating file: ${path}`);
|
||||
if (await this.vault.adapter.exists(normalizePath(path))) {
|
||||
if (await this.fs.exists(path)) {
|
||||
Logger.getInstance().debug(
|
||||
`Didn't expect ${path} to exist, when trying to create it, merging instead`
|
||||
);
|
||||
|
|
@ -55,50 +58,51 @@ export class ObsidianFileOperations implements FileOperations {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.createParentDirectories(normalizePath(path));
|
||||
await this.vault.adapter.writeBinary(
|
||||
normalizePath(path),
|
||||
newContent.buffer as ArrayBuffer
|
||||
);
|
||||
await this.createParentDirectories(path);
|
||||
await this.fs.write(path, newContent);
|
||||
}
|
||||
|
||||
// Update the file at the given path.
|
||||
// If the file's content is different from `expectedContent`, the a 3-way merge is performed before writing.
|
||||
// If the file no longer exists, the file is not recreated and an empty array is returned.
|
||||
public async write(
|
||||
path: RelativePath,
|
||||
expectedContent: Uint8Array,
|
||||
newContent: Uint8Array
|
||||
): Promise<Uint8Array> {
|
||||
Logger.getInstance().debug(`Writing file: ${path}`);
|
||||
if (!(await this.vault.adapter.exists(normalizePath(path)))) {
|
||||
if (!(await this.fs.exists(path))) {
|
||||
Logger.getInstance().debug(
|
||||
`The caller assumed ${path} exists, but it no longer, so we wont recreate it`
|
||||
);
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
if (!isFileTypeMergable(path)) {
|
||||
if (
|
||||
!isFileTypeMergable(path) ||
|
||||
isBinary(expectedContent) ||
|
||||
isBinary(newContent)
|
||||
) {
|
||||
Logger.getInstance().debug(
|
||||
`The expected content is not mergable, so we won't perform a 3-way merge, just overwrite it`
|
||||
);
|
||||
await this.vault.adapter.writeBinary(
|
||||
normalizePath(path),
|
||||
newContent.buffer as ArrayBuffer
|
||||
);
|
||||
await this.fs.write(path, newContent);
|
||||
return newContent;
|
||||
}
|
||||
|
||||
const expetedText = new TextDecoder().decode(expectedContent);
|
||||
const expectedText = new TextDecoder().decode(expectedContent);
|
||||
const newText = new TextDecoder().decode(newContent);
|
||||
|
||||
const resultText = await this.vault.adapter.process(
|
||||
normalizePath(path),
|
||||
const resultText = await this.fs.atomicUpdateText(
|
||||
path,
|
||||
(currentText) => {
|
||||
currentText = currentText.replace(/\r\n/g, "\n");
|
||||
if (currentText !== expetedText) {
|
||||
if (currentText !== expectedText) {
|
||||
Logger.getInstance().debug(
|
||||
`Performing a 3-way merge for ${path} with the expected content`
|
||||
);
|
||||
|
||||
return mergeText(expetedText, currentText, newText);
|
||||
return mergeText(expectedText, currentText, newText);
|
||||
}
|
||||
|
||||
Logger.getInstance().debug(
|
||||
|
|
@ -113,18 +117,13 @@ export class ObsidianFileOperations implements FileOperations {
|
|||
|
||||
public async remove(path: RelativePath): Promise<void> {
|
||||
Logger.getInstance().debug(`Removing file: ${path}`);
|
||||
if (await this.vault.adapter.exists(normalizePath(path))) {
|
||||
await this.vault.adapter.trashSystem(normalizePath(path));
|
||||
}
|
||||
return this.fs.delete(path);
|
||||
}
|
||||
|
||||
public async move(
|
||||
oldPath: RelativePath,
|
||||
newPath: RelativePath
|
||||
): Promise<void> {
|
||||
oldPath = normalizePath(oldPath);
|
||||
newPath = normalizePath(newPath);
|
||||
|
||||
Logger.getInstance().debug(`Moving file: ${oldPath} -> ${newPath}`);
|
||||
|
||||
if (oldPath === newPath) {
|
||||
|
|
@ -132,25 +131,16 @@ export class ObsidianFileOperations implements FileOperations {
|
|||
}
|
||||
|
||||
await this.createParentDirectories(newPath);
|
||||
await this.vault.adapter.rename(oldPath, newPath);
|
||||
await this.fs.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
public isFileEligibleForSync(path: RelativePath): boolean {
|
||||
if (Platform.isDesktopApp) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
// if (Platform.isDesktopApp) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return isFileTypeMergable(path);
|
||||
}
|
||||
|
||||
private async statFile(path: string): Promise<Stat> {
|
||||
const file = await this.vault.adapter.stat(normalizePath(path));
|
||||
|
||||
if (!file) {
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
|
||||
return file;
|
||||
// return isFileTypeMergable(path);
|
||||
}
|
||||
|
||||
private async createParentDirectories(path: string): Promise<void> {
|
||||
|
|
@ -160,8 +150,8 @@ export class ObsidianFileOperations implements FileOperations {
|
|||
}
|
||||
for (let i = 1; i < components.length; i++) {
|
||||
const parentDir = components.slice(0, i).join("/");
|
||||
if (!(await this.vault.adapter.exists(parentDir))) {
|
||||
await this.vault.adapter.mkdir(parentDir);
|
||||
if (!(await this.fs.exists(parentDir))) {
|
||||
await this.fs.createDirectory(parentDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { RelativePath } from "src/persistence/database";
|
||||
|
||||
export interface FileSystemOperations {
|
||||
listAllFiles(): Promise<RelativePath[]>;
|
||||
read(path: RelativePath): Promise<Uint8Array>;
|
||||
write(path: RelativePath, content: Uint8Array): Promise<void>;
|
||||
atomicUpdateText(
|
||||
path: RelativePath,
|
||||
updater: (currentContent: string) => string
|
||||
): Promise<string>;
|
||||
getFileSize(path: RelativePath): Promise<number>;
|
||||
getModificationTime(path: RelativePath): Promise<Date>;
|
||||
exists(path: RelativePath): Promise<boolean>;
|
||||
createDirectory(path: RelativePath): Promise<void>;
|
||||
delete(path: RelativePath): Promise<void>;
|
||||
rename(oldPath: RelativePath, newPath: RelativePath): Promise<void>;
|
||||
}
|
||||
|
|
@ -1,9 +1,3 @@
|
|||
export { Settings, type SyncSettings } from "./persistence/settings";
|
||||
|
||||
export { type CheckConnectionResult } from "./services/sync-service";
|
||||
|
||||
export { Syncer } from "./sync-operations/syncer";
|
||||
|
||||
export {
|
||||
SyncHistory,
|
||||
SyncType,
|
||||
|
|
@ -12,17 +6,14 @@ export {
|
|||
type HistoryStats,
|
||||
type HistoryEntry
|
||||
} from "./tracing/sync-history";
|
||||
|
||||
export { Logger, LogLevel } from "./tracing/logger";
|
||||
|
||||
export { SyncClient } from "./sync-client";
|
||||
export { type FileOperations } from "./file-operations";
|
||||
export { type RelativePath } from "./persistence/database";
|
||||
export type { PersistenceProvider } from "./persistence/persistence";
|
||||
export { Syncer } from "./sync-operations/syncer";
|
||||
export type { CheckConnectionResult } from "./services/sync-service";
|
||||
export { Settings, type SyncSettings } from "./persistence/settings";
|
||||
|
||||
export {
|
||||
isFileTypeMergable,
|
||||
mergeText,
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
merge
|
||||
} from "sync_lib";
|
||||
export type { RelativePath } from "./persistence/database";
|
||||
export type { FileSystemOperations } from "./file-operations/filesystem-operations";
|
||||
export type { PersistenceProvider } from "./persistence/persistence";
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import init from "sync_lib";
|
|||
import wasmBin from "sync_lib/sync_lib_bg.wasm";
|
||||
import type { PersistenceProvider } from "./persistence/persistence";
|
||||
import { SyncHistory } from "./tracing/sync-history";
|
||||
import type { FileOperations } from "./file-operations";
|
||||
import { Logger } from "./tracing/logger";
|
||||
import { Database } from "./persistence/database";
|
||||
import { Settings } from "./persistence/settings";
|
||||
import type { CheckConnectionResult } from "./services/sync-service";
|
||||
import { SyncService } from "./services/sync-service";
|
||||
import { Syncer } from "./sync-operations/syncer";
|
||||
import { applyRemoteChangesLocally } from "./sync-operations/apply-remote-changes-locally";
|
||||
import { FileSystemOperations } from "./file-operations/filesystem-operations";
|
||||
import { FileOperations } from "./file-operations/file-operations";
|
||||
|
||||
export class SyncClient {
|
||||
private remoteListenerIntervalId: number | null = null;
|
||||
|
|
@ -35,7 +35,7 @@ export class SyncClient {
|
|||
}
|
||||
|
||||
public static async create(
|
||||
operations: FileOperations,
|
||||
fs: FileSystemOperations,
|
||||
persistence: PersistenceProvider
|
||||
): Promise<SyncClient> {
|
||||
const history = new SyncHistory();
|
||||
|
|
@ -75,7 +75,7 @@ export class SyncClient {
|
|||
database,
|
||||
settings,
|
||||
syncService,
|
||||
operations,
|
||||
new FileOperations(fs),
|
||||
history
|
||||
);
|
||||
|
||||
|
|
@ -90,19 +90,11 @@ export class SyncClient {
|
|||
void syncer.scheduleSyncForOfflineChanges();
|
||||
|
||||
client.registerRemoteEventListener(
|
||||
settings,
|
||||
database,
|
||||
syncService,
|
||||
syncer,
|
||||
settings.getSettings().fetchChangesUpdateIntervalMs
|
||||
);
|
||||
|
||||
settings.addOnSettingsChangeHandlers((newSettings, oldSettings) => {
|
||||
client.registerRemoteEventListener(
|
||||
settings,
|
||||
database,
|
||||
syncService,
|
||||
syncer,
|
||||
newSettings.fetchChangesUpdateIntervalMs
|
||||
);
|
||||
|
||||
|
|
@ -142,26 +134,13 @@ export class SyncClient {
|
|||
}
|
||||
}
|
||||
|
||||
private registerRemoteEventListener(
|
||||
settings: Settings,
|
||||
database: Database,
|
||||
syncService: SyncService,
|
||||
syncer: Syncer,
|
||||
intervalMs: number
|
||||
): void {
|
||||
private registerRemoteEventListener(intervalMs: number): void {
|
||||
if (this.remoteListenerIntervalId !== null) {
|
||||
window.clearInterval(this.remoteListenerIntervalId);
|
||||
}
|
||||
|
||||
this.remoteListenerIntervalId = window.setInterval(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
async () =>
|
||||
applyRemoteChangesLocally({
|
||||
settings,
|
||||
database,
|
||||
syncService,
|
||||
syncer
|
||||
}),
|
||||
() => void this._syncer.applyRemoteChangesLocally(),
|
||||
intervalMs
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue