mirror of
https://github.com/ichaly/cohere.git
synced 2026-07-22 07:47:20 +00:00
Address Obsidian review API compatibility
This commit is contained in:
parent
158967ccc8
commit
b453949419
5 changed files with 178 additions and 133 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"id": "obsync",
|
||||
"name": "S3 Vault Sync",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"minAppVersion": "1.6.6",
|
||||
"description": "Sync vault files through OSS / S3-compatible object storage.",
|
||||
"author": "Chaly",
|
||||
"authorUrl": "https://github.com/ichaly",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import ObsyncPlugin from "./main";
|
||||
import { ObsidianVaultIO } from "./vault-io";
|
||||
|
||||
vi.mock("obsidian", () => ({
|
||||
App: class {},
|
||||
|
|
@ -218,6 +220,43 @@ describe("device identity settings", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("vault IO compatibility", () => {
|
||||
test("uses stable vault APIs to find and delete files and folders", async () => {
|
||||
const file = Object.assign(new TFile(), { path: "note.md" });
|
||||
const folder = Object.assign(new TFolder(), { path: "empty", children: [] });
|
||||
const getAbstractFileByPath = vi.fn((path: string) => {
|
||||
if (path === "note.md") return file;
|
||||
if (path === "empty") return folder;
|
||||
return null;
|
||||
});
|
||||
const app = {
|
||||
fileManager: {
|
||||
trashFile: vi.fn(async () => {}),
|
||||
},
|
||||
vault: {
|
||||
adapter: { exists: vi.fn(async () => false) },
|
||||
createBinary: vi.fn(),
|
||||
createFolder: vi.fn(),
|
||||
getAbstractFileByPath,
|
||||
getAllLoadedFiles: vi.fn(() => [file, folder]),
|
||||
getFiles: vi.fn(() => [file]),
|
||||
modifyBinary: vi.fn(),
|
||||
readBinary: vi.fn(async () => new ArrayBuffer(0)),
|
||||
},
|
||||
};
|
||||
const io = new ObsidianVaultIO(app as never);
|
||||
|
||||
await io.read("note.md");
|
||||
await io.delete("note.md");
|
||||
await io.deleteDirectory("empty");
|
||||
|
||||
expect(getAbstractFileByPath).toHaveBeenCalledWith("note.md");
|
||||
expect(getAbstractFileByPath).toHaveBeenCalledWith("empty");
|
||||
expect(app.fileManager.trashFile).toHaveBeenCalledWith(file);
|
||||
expect(app.fileManager.trashFile).toHaveBeenCalledWith(folder);
|
||||
});
|
||||
});
|
||||
|
||||
function createPlugin(settings: Partial<TestSettings> = {}): TestPlugin {
|
||||
const plugin = Object.create(ObsyncPlugin.prototype) as TestPlugin;
|
||||
plugin.app = { vault: { getName: () => "ichaly" } };
|
||||
|
|
|
|||
139
src/main.ts
139
src/main.ts
|
|
@ -1,10 +1,11 @@
|
|||
import { App, Notice, Platform, Plugin, PluginSettingTab, requestUrl, type TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
import { App, Notice, Platform, Plugin, PluginSettingTab, requestUrl, type TAbstractFile, TFile } from "obsidian";
|
||||
import { createApp, reactive, type App as VueApp } from "vue";
|
||||
import { createRandomId, createVaultId, normalizeKey } from "./core/ids";
|
||||
import SettingsApp from "./settings/SettingsApp.vue";
|
||||
import "./styles.scss";
|
||||
import { releaseDeletedContent, syncOnce, type LocalSyncState, type VaultIO } from "./sync/engine";
|
||||
import { releaseDeletedContent, syncOnce, type LocalSyncState } from "./sync/engine";
|
||||
import { S3ObjectStore } from "./store/s3";
|
||||
import { ObsidianVaultIO } from "./vault-io";
|
||||
|
||||
interface ObsyncSettings {
|
||||
endpoint: string;
|
||||
|
|
@ -165,7 +166,7 @@ export default class ObsyncPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private moveRibbonIconToBottom(iconEl: HTMLElement): void {
|
||||
const leftRibbonActions = document.querySelector(".workspace-ribbon.mod-left .side-dock-actions");
|
||||
const leftRibbonActions = activeDocument.querySelector(".workspace-ribbon.mod-left .side-dock-actions");
|
||||
(leftRibbonActions ?? iconEl.parentElement)?.append(iconEl);
|
||||
}
|
||||
|
||||
|
|
@ -178,8 +179,8 @@ export default class ObsyncPlugin extends Plugin {
|
|||
this.queueAutoSync();
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, "visibilitychange", () => {
|
||||
if (!document.hidden) {
|
||||
this.registerDomEvent(activeDocument, "visibilitychange", () => {
|
||||
if (!activeDocument.hidden) {
|
||||
this.queueAutoSync();
|
||||
}
|
||||
});
|
||||
|
|
@ -332,7 +333,8 @@ export default class ObsyncPlugin extends Plugin {
|
|||
deviceId: this.settings.deviceId,
|
||||
now: () => Date.now(),
|
||||
request: async (request) => {
|
||||
const { host: _host, ...headers } = request.headers;
|
||||
const headers = { ...request.headers };
|
||||
delete headers.host;
|
||||
const response = await requestUrl({
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
|
|
@ -359,7 +361,7 @@ export default class ObsyncPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
const loaded = await this.loadData();
|
||||
const loaded = await this.loadData() as Partial<ObsyncSettings> | null;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
|
||||
|
||||
if (!this.settings.vaultKey) {
|
||||
|
|
@ -529,129 +531,6 @@ function readOptionalString(record: Record<string, unknown>, key: string, fallba
|
|||
return value.trim() || fallback;
|
||||
}
|
||||
|
||||
class ObsidianVaultIO implements VaultIO {
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async scan(): Promise<Array<{ path: string; mtime: number; size: number }>> {
|
||||
const files = this.app.vault.getFiles();
|
||||
const result: Array<{ path: string; mtime: number; size: number }> = [];
|
||||
|
||||
for (const file of files) {
|
||||
result.push({
|
||||
path: file.path,
|
||||
mtime: file.stat.mtime,
|
||||
size: file.stat.size,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async read(path: string): Promise<Uint8Array> {
|
||||
const file = this.app.vault.getFileByPath(path);
|
||||
|
||||
if (!file) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
|
||||
return new Uint8Array(await this.app.vault.readBinary(file));
|
||||
}
|
||||
|
||||
async scanEmptyDirectories(): Promise<string[]> {
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter((file): file is TFolder => file instanceof TFolder)
|
||||
.filter((folder) => folder.path && !isIgnoredVaultPath(folder.path) && folder.children.length === 0)
|
||||
.map((folder) => folder.path);
|
||||
}
|
||||
|
||||
async write(path: string, bytes: Uint8Array): Promise<void> {
|
||||
await this.ensureParentFolder(path);
|
||||
const file = this.app.vault.getFileByPath(path);
|
||||
const data = toArrayBuffer(bytes);
|
||||
|
||||
if (file instanceof TFile) {
|
||||
await this.app.vault.modifyBinary(file, data);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.app.vault.createBinary(path, data);
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<void> {
|
||||
const file = this.app.vault.getFileByPath(path);
|
||||
|
||||
if (file) {
|
||||
await this.app.vault.trash(file, false);
|
||||
}
|
||||
}
|
||||
|
||||
async createDirectory(path: string): Promise<void> {
|
||||
let current = "";
|
||||
|
||||
for (const part of path.split("/")) {
|
||||
current = current ? `${current}/${part}` : part;
|
||||
await this.ensureFolderExists(current);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDirectory(path: string): Promise<void> {
|
||||
const folder = this.app.vault.getFolderByPath(path);
|
||||
|
||||
if (folder instanceof TFolder && folder.children.length === 0) {
|
||||
await this.app.vault.trash(folder, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureParentFolder(path: string): Promise<void> {
|
||||
const parts = path.split("/").slice(0, -1);
|
||||
let current = "";
|
||||
|
||||
for (const part of parts) {
|
||||
current = current ? `${current}/${part}` : part;
|
||||
await this.ensureFolderExists(current);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureFolderExists(path: string): Promise<void> {
|
||||
if (this.app.vault.getFolderByPath(path) instanceof TFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.app.vault.getFileByPath(path) instanceof TFile) {
|
||||
throw new Error(`Path exists as a file: ${path}`);
|
||||
}
|
||||
|
||||
if (await this.app.vault.adapter.exists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.vault.createFolder(path);
|
||||
} catch (error) {
|
||||
if (await this.app.vault.adapter.exists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isIgnoredVaultPath(path: string): boolean {
|
||||
return path === ".obsidian" || path.startsWith(".obsidian/") || path === ".trash" || path.startsWith(".trash/");
|
||||
}
|
||||
|
||||
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
||||
const copy = new Uint8Array(bytes.byteLength);
|
||||
copy.set(bytes);
|
||||
return copy.buffer;
|
||||
}
|
||||
|
||||
class ObsyncSettingTab extends PluginSettingTab {
|
||||
plugin: ObsyncPlugin;
|
||||
vueApp: VueApp<Element> | null = null;
|
||||
|
|
|
|||
128
src/vault-io.ts
Normal file
128
src/vault-io.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { type App, TFile, TFolder } from "obsidian";
|
||||
import type { VaultIO } from "./sync/engine";
|
||||
|
||||
export class ObsidianVaultIO implements VaultIO {
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async scan(): Promise<Array<{ path: string; mtime: number; size: number }>> {
|
||||
const files = this.app.vault.getFiles();
|
||||
const result: Array<{ path: string; mtime: number; size: number }> = [];
|
||||
|
||||
for (const file of files) {
|
||||
result.push({
|
||||
path: file.path,
|
||||
mtime: file.stat.mtime,
|
||||
size: file.stat.size,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async read(path: string): Promise<Uint8Array> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!(file instanceof TFile)) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
|
||||
return new Uint8Array(await this.app.vault.readBinary(file));
|
||||
}
|
||||
|
||||
async scanEmptyDirectories(): Promise<string[]> {
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter((file): file is TFolder => file instanceof TFolder)
|
||||
.filter((folder) => folder.path && !this.isIgnoredVaultPath(folder.path) && folder.children.length === 0)
|
||||
.map((folder) => folder.path);
|
||||
}
|
||||
|
||||
async write(path: string, bytes: Uint8Array): Promise<void> {
|
||||
await this.ensureParentFolder(path);
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
const data = toArrayBuffer(bytes);
|
||||
|
||||
if (file instanceof TFile) {
|
||||
await this.app.vault.modifyBinary(file, data);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.app.vault.createBinary(path, data);
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<void> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (file instanceof TFile) {
|
||||
await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
async createDirectory(path: string): Promise<void> {
|
||||
let current = "";
|
||||
|
||||
for (const part of path.split("/")) {
|
||||
current = current ? `${current}/${part}` : part;
|
||||
await this.ensureFolderExists(current);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDirectory(path: string): Promise<void> {
|
||||
const folder = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (folder instanceof TFolder && folder.children.length === 0) {
|
||||
await this.app.fileManager.trashFile(folder);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureParentFolder(path: string): Promise<void> {
|
||||
const parts = path.split("/").slice(0, -1);
|
||||
let current = "";
|
||||
|
||||
for (const part of parts) {
|
||||
current = current ? `${current}/${part}` : part;
|
||||
await this.ensureFolderExists(current);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureFolderExists(path: string): Promise<void> {
|
||||
const existing = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (existing instanceof TFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing instanceof TFile) {
|
||||
throw new Error(`Path exists as a file: ${path}`);
|
||||
}
|
||||
|
||||
if (await this.app.vault.adapter.exists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.vault.createFolder(path);
|
||||
} catch (error) {
|
||||
if (await this.app.vault.adapter.exists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private isIgnoredVaultPath(path: string): boolean {
|
||||
const configDir = this.app.vault.configDir || ".obsidian";
|
||||
return path === configDir || path.startsWith(`${configDir}/`) || path === ".trash" || path.startsWith(".trash/");
|
||||
}
|
||||
}
|
||||
|
||||
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
||||
const copy = new Uint8Array(bytes.byteLength);
|
||||
copy.set(bytes);
|
||||
return copy.buffer;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
{
|
||||
"0.1.0": "1.5.0"
|
||||
"0.1.0": "1.6.6"
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue