mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-28 06:17:06 +00:00
Merge current main into PR 1039
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts";
|
||||
import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase";
|
||||
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
|
||||
import { ServiceDatabaseFileAccessBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase";
|
||||
|
||||
// markChangesAreSame uses persistent data implicitly, we should refactor it too.
|
||||
// For now, to make the refactoring done once, we just use them directly.
|
||||
// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB).
|
||||
// Hence it remains in the plug-in rather than Commonlib. (markChangesAreSame is using indexedDB).
|
||||
// Refactored, now migrating...
|
||||
export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type App } from "@/deps";
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts";
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase";
|
||||
import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase";
|
||||
import { ServiceFileHandlerBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileHandlerBase";
|
||||
|
||||
// markChangesAreSame uses persistent data implicitly, we should refactor it too.
|
||||
// also, compareFileFreshness depends on marked changes, so we should refactor it as well. For now, to make the refactoring done once, we just use them directly.
|
||||
// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB).
|
||||
// Hence it remains in the plug-in rather than Commonlib. (markChangesAreSame is using indexedDB).
|
||||
// Refactored: markChangesAreSame, unmarkChanges, compareFileFreshness, isMarkedAsSameChanges are now moved to PathService
|
||||
export class ServiceFileHandler extends ServiceFileHandlerBase {}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
StoredFileReflectionProvenance,
|
||||
type FileReflectionProvenanceRecord,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/interfaces/FileReflectionProvenance";
|
||||
import type { SimpleStore } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
|
||||
export const FILE_REFLECTION_PROVENANCE_STORE = "file-reflection-provenance-v1";
|
||||
|
||||
export type FileReflectionProvenanceStoreFactory = {
|
||||
openSimpleStore<T>(kind: string): SimpleStore<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the device-local record which links a Vault file to the exact
|
||||
* database revision most recently reflected in that Vault.
|
||||
*
|
||||
* This runs during service composition, before KeyValueDB is opened. The
|
||||
* returned namespaced handle is inert until its first operation; normal hosts
|
||||
* complete the sequential onSettingLoaded lifecycle before Vault scanning,
|
||||
* watching, or replication can invoke it. Operations are never held waiting for
|
||||
* readiness; they fail on a lifecycle violation and may fail during reset.
|
||||
*/
|
||||
export function createFileReflectionProvenance(keyValueDB: FileReflectionProvenanceStoreFactory) {
|
||||
return new StoredFileReflectionProvenance(
|
||||
keyValueDB.openSimpleStore<FileReflectionProvenanceRecord>(FILE_REFLECTION_PROVENANCE_STORE)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { SimpleStore } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import type { FileReflectionProvenanceRecord } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileReflectionProvenance";
|
||||
import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
createFileReflectionProvenance,
|
||||
FILE_REFLECTION_PROVENANCE_STORE,
|
||||
} from "./FileReflectionProvenance";
|
||||
|
||||
describe("createFileReflectionProvenance", () => {
|
||||
it("uses one reset-scoped host store for exact reflected revisions", async () => {
|
||||
const values = new Map<string, FileReflectionProvenanceRecord>();
|
||||
const store = {
|
||||
get: vi.fn(async (key: string) => values.get(key)),
|
||||
set: vi.fn(async (key: string, value: FileReflectionProvenanceRecord) => {
|
||||
values.set(key, value);
|
||||
}),
|
||||
delete: vi.fn(async (key: string) => {
|
||||
values.delete(key);
|
||||
}),
|
||||
keys: vi.fn(async () => [...values.keys()]),
|
||||
db: undefined,
|
||||
} as unknown as SimpleStore<FileReflectionProvenanceRecord>;
|
||||
const openSimpleStore = vi.fn().mockReturnValue(store);
|
||||
const path = "note.md" as FilePathWithPrefix;
|
||||
|
||||
const provenance = createFileReflectionProvenance({ openSimpleStore });
|
||||
expect(openSimpleStore).toHaveBeenCalledWith(FILE_REFLECTION_PROVENANCE_STORE);
|
||||
await provenance.set(path, { revision: "3-displayed", observedStorageMtime: 123.456 });
|
||||
|
||||
expect(openSimpleStore).toHaveBeenCalledTimes(1);
|
||||
await expect(provenance.get(path)).resolves.toEqual({
|
||||
revision: "3-displayed",
|
||||
observedStorageMtime: 123.456,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
|
||||
import type { TFile, TFolder } from "obsidian";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FilePath, UXStat } from "@lib/common/types";
|
||||
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type {
|
||||
IFileSystemAdapter,
|
||||
IPathAdapter,
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
IConversionAdapter,
|
||||
IStorageAdapter,
|
||||
IVaultAdapter,
|
||||
} from "@lib/serviceModules/adapters";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian";
|
||||
import { ObsidianConversionAdapter } from "./ObsidianConversionAdapter";
|
||||
import { ObsidianPathAdapter } from "./ObsidianPathAdapter";
|
||||
@@ -54,6 +54,11 @@ export class ObsidianFileSystemAdapter implements IFileSystemAdapter<TAbstractFi
|
||||
return Promise.resolve(this.app.vault.getFiles());
|
||||
}
|
||||
|
||||
async renameFile(file: TFile, newPath: string): Promise<TFile> {
|
||||
await this.vault.rename(file, newPath);
|
||||
return file;
|
||||
}
|
||||
|
||||
statFromNative(file: TFile): Promise<UXStat> {
|
||||
return Promise.resolve({ ...file.stat, type: "file" });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type TAbstractFile, normalizePath } from "@/deps";
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Obsidian
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
||||
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
|
||||
import type { UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import { toArrayBuffer } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase";
|
||||
import type { Stat, App } from "obsidian";
|
||||
import { toIntegerTimestamps } from "./sanitizeWriteOptions";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||
import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase";
|
||||
import type { UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import { toArrayBuffer } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase";
|
||||
import type { TFile, App, TFolder } from "obsidian";
|
||||
import { toIntegerTimestamps } from "./sanitizeWriteOptions";
|
||||
|
||||
@@ -11,7 +11,8 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
|
||||
constructor(private app: App) {}
|
||||
|
||||
async read(file: TFile): Promise<string> {
|
||||
return await this.app.vault.read(file);
|
||||
// Vault.read strips a leading UTF-8 BOM, leaving the content size inconsistent with TFile.stat.
|
||||
return await this.app.vault.adapter.read(file.path);
|
||||
}
|
||||
|
||||
async cachedRead(file: TFile): Promise<string> {
|
||||
@@ -38,22 +39,16 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
|
||||
return await this.app.vault.createBinary(path, toArrayBuffer(data), toIntegerTimestamps(options));
|
||||
}
|
||||
|
||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||
if ("trashFile" in this.app.fileManager) {
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
|
||||
return await this.app.vault.delete(file, force);
|
||||
async rename(file: TFile, newPath: string): Promise<void> {
|
||||
return await this.app.vault.rename(file, newPath);
|
||||
}
|
||||
|
||||
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
||||
if ("trashFile" in this.app.fileManager) {
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
|
||||
return await this.app.vault.trash(file, force);
|
||||
async delete(file: TFile | TFolder): Promise<void> {
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
|
||||
async trash(file: TFile | TFolder): Promise<void> {
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { App, TFile } from "obsidian";
|
||||
import { ObsidianVaultAdapter } from "./ObsidianVaultAdapter";
|
||||
|
||||
describe("ObsidianVaultAdapter.read", () => {
|
||||
it("preserves a UTF-8 BOM so the content size matches the file stat", async () => {
|
||||
const path = "Transcripts/字幕.md";
|
||||
const contentWithoutBom = "字幕の検証行です。\n";
|
||||
const contentWithBom = `\ufeff${contentWithoutBom}`;
|
||||
const read = vi.fn().mockResolvedValue(contentWithoutBom);
|
||||
const adapterRead = vi.fn().mockResolvedValue(contentWithBom);
|
||||
const app = {
|
||||
vault: {
|
||||
read,
|
||||
adapter: {
|
||||
read: adapterRead,
|
||||
},
|
||||
},
|
||||
} as unknown as App;
|
||||
const file = {
|
||||
path,
|
||||
stat: {
|
||||
ctime: 1,
|
||||
mtime: 2,
|
||||
size: new Blob([contentWithBom]).size,
|
||||
},
|
||||
} as TFile;
|
||||
const adapter = new ObsidianVaultAdapter(app);
|
||||
|
||||
const result = await adapter.read(file);
|
||||
|
||||
expect(new Blob([result]).size).toBe(file.stat.size);
|
||||
expect(result.charCodeAt(0)).toBe(0xfeff);
|
||||
expect(adapterRead).toHaveBeenCalledWith(path);
|
||||
expect(read).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||
import { ServiceFileAccessBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase";
|
||||
import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter";
|
||||
|
||||
// For now, this is just a re-export of ServiceFileAccess with the Obsidian-specific adapter type.
|
||||
|
||||
Reference in New Issue
Block a user