diff --git a/src/StorageAccessor/DirectVault.ts b/src/StorageAccessor/DirectVault.ts index d3e67ec..9ee770a 100644 --- a/src/StorageAccessor/DirectVault.ts +++ b/src/StorageAccessor/DirectVault.ts @@ -1,6 +1,6 @@ -import type { Stat } from "obsidian"; -import { FileType, StorageAccessorTypes } from "../storage.ts"; +import { normalizePath, type Stat } from "obsidian"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; export class DirectVault extends StorageAccessor { @@ -8,6 +8,10 @@ export class DirectVault extends StorageAccessor { sep = "/"; // Always use / as separator on vault. + normalizePath(path: string): string { + return normalizePath(path); + } + async createFolder(absolutePath: string): Promise { await this.app.vault.adapter.mkdir(absolutePath); } diff --git a/src/StorageAccessor/ExternalVaultFilesystem.test.ts b/src/StorageAccessor/ExternalVaultFilesystem.test.ts new file mode 100644 index 0000000..3617068 --- /dev/null +++ b/src/StorageAccessor/ExternalVaultFilesystem.test.ts @@ -0,0 +1,36 @@ +// eslint-disable-next-line import/no-nodejs-modules -- Electron file reads return Node Buffers. +import { Buffer } from "node:buffer"; +import { ExternalVaultFilesystem } from "./ExternalVaultFilesystem.ts"; +import type { StorageAccessorHost } from "./storage-accessor-types.ts"; + +declare const Deno: { + test: (name: string, fn: () => void | Promise) => void; +}; + +function assertEquals(actual: T, expected: T, message: string) { + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`${message}\nactual=${JSON.stringify(actual)}\nexpected=${JSON.stringify(expected)}`); + } +} + +Deno.test("ExternalVaultFilesystem: reads only the visible Node Buffer bytes", async () => { + const backing = Buffer.from([99, 1, 2, 3, 88]); + const visible = backing.subarray(1, 4); + const plugin = { + app: { + vault: { + adapter: { + fsPromises: { + readFile: async () => visible, + }, + }, + }, + }, + } as unknown as StorageAccessorHost; + const storage = new ExternalVaultFilesystem(plugin); + + const result = await storage._readBinary("archive.bin"); + if (result === false) throw new Error("Expected binary data"); + + assertEquals([...new Uint8Array(result)], [1, 2, 3], "Buffer byte offset and length should be preserved"); +}); diff --git a/src/StorageAccessor/ExternalVaultFilesystem.ts b/src/StorageAccessor/ExternalVaultFilesystem.ts index cfed4f5..ad7b0b5 100644 --- a/src/StorageAccessor/ExternalVaultFilesystem.ts +++ b/src/StorageAccessor/ExternalVaultFilesystem.ts @@ -1,8 +1,11 @@ import type { Stat } from "obsidian"; -import { type FsAPI, FileType, StorageAccessorTypes } from "../storage.ts"; +// eslint-disable-next-line import/no-nodejs-modules -- Electron exposes the Node filesystem API used here. +import type { promises } from "node:fs"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; import { toArrayBuffer } from "../util.ts"; +type FsAPI = Pick; export class ExternalVaultFilesystem extends StorageAccessor { type = StorageAccessorTypes.EXTERNAL; @@ -21,7 +24,7 @@ export class ExternalVaultFilesystem extends StorageAccessor { await this.fsPromises.mkdir(absolutePath, { recursive: true }); } - async ensureDirectory(fullPath: string) { + override async ensureDirectory(fullPath: string) { const delimiter = this.sep; const pathElements = fullPath.split(delimiter); pathElements.pop(); @@ -42,7 +45,7 @@ export class ExternalVaultFilesystem extends StorageAccessor { async _readBinary(path: string): Promise { const buffer = await this.fsPromises.readFile(path); - return toArrayBuffer(buffer.buffer) + return toArrayBuffer(buffer); } async deleteBinary(path: string): Promise { diff --git a/src/StorageAccessor/NormalVault.ts b/src/StorageAccessor/NormalVault.ts index 7298b42..a82dff1 100644 --- a/src/StorageAccessor/NormalVault.ts +++ b/src/StorageAccessor/NormalVault.ts @@ -1,6 +1,6 @@ -import { TFile, TFolder, type Stat } from "obsidian"; -import { FileType, StorageAccessorTypes } from "../storage.ts"; +import { normalizePath, TFile, TFolder, type Stat } from "obsidian"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; export class NormalVault extends StorageAccessor { @@ -8,6 +8,10 @@ export class NormalVault extends StorageAccessor { sep = "/"; // Always use / as separator on vault. + normalizePath(path: string): string { + return normalizePath(path); + } + async createFolder(absolutePath: string): Promise { await this.app.vault.createFolder(absolutePath); } diff --git a/src/StorageAccessor/S3Bucket.ts b/src/StorageAccessor/S3Bucket.ts index 2c40f3e..63e4ea3 100644 --- a/src/StorageAccessor/S3Bucket.ts +++ b/src/StorageAccessor/S3Bucket.ts @@ -1,8 +1,8 @@ import { S3 } from "@aws-sdk/client-s3"; -import type { Stat } from "obsidian"; +import { normalizePath, type Stat } from "obsidian"; import { ObsHttpHandler } from "../ObsHttpHandler.ts"; -import { FileType, StorageAccessorTypes } from "../storage.ts"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; import { toArrayBuffer } from "../util.ts"; @@ -10,11 +10,15 @@ export class S3Bucket extends StorageAccessor { type = StorageAccessorTypes.S3; sep = "/"; + normalizePath(path: string): string { + return normalizePath(path); + } + createFolder(absolutePath: string): Promise { // S3 does not have folder concept. So, we don't need to create folder. return Promise.resolve(); } - ensureDirectory(fullPath: string): Promise { + override ensureDirectory(fullPath: string): Promise { return Promise.resolve(); } diff --git a/src/StorageAccessor/StorageAccessor.ts b/src/StorageAccessor/StorageAccessor.ts index a0619ae..2fd4231 100644 --- a/src/StorageAccessor/StorageAccessor.ts +++ b/src/StorageAccessor/StorageAccessor.ts @@ -1,13 +1,19 @@ -import { normalizePath, type Stat } from "obsidian"; -import type DiffZipBackupPlugin from "../../main.ts"; -import { type StorageAccessorType, FileType, decryptCompatOpenSSL, encryptCompatOpenSSL } from "../storage.ts"; +import type { Stat } from "obsidian"; +import { OpenSSLCompat } from "octagonal-wheels/encryption"; +import { + type StorageAccessorHost, + type StorageAccessorType, + FileType, +} from "./storage-accessor-types.ts"; import { toArrayBuffer } from "../util.ts"; +const decryptCompatOpenSSL = OpenSSLCompat.CBC.decryptCBC; +const encryptCompatOpenSSL = OpenSSLCompat.CBC.encryptCBC; export abstract class StorageAccessor { abstract type: StorageAccessorType; abstract sep: string; - public plugin: DiffZipBackupPlugin; + public plugin: StorageAccessorHost; get app() { return this.plugin.app; } @@ -21,7 +27,7 @@ export abstract class StorageAccessor { } public isLocal: boolean = false; - constructor(plugin: DiffZipBackupPlugin, basePath?: string, isLocal?: boolean) { + constructor(plugin: StorageAccessorHost, basePath?: string, isLocal?: boolean) { this.basePath = basePath || ""; this.plugin = plugin; this.isLocal = isLocal || false; @@ -73,9 +79,7 @@ export abstract class StorageAccessor { abstract _readBinary(path: string, preventUseCache?: boolean): Promise; abstract deleteBinary(path: string): Promise; - normalizePath(path: string): string { - return normalizePath(path); - } + abstract normalizePath(path: string): string; abstract stat(path: string): Promise; async ensureDirectory(fullPath: string) { diff --git a/src/StorageAccessor/storage-accessor-types.ts b/src/StorageAccessor/storage-accessor-types.ts new file mode 100644 index 0000000..723e4ec --- /dev/null +++ b/src/StorageAccessor/storage-accessor-types.ts @@ -0,0 +1,30 @@ +import type { App } from "obsidian"; +import type { DiffZipBackupSettings } from "../types.ts"; + +/** + * DiffZip-owned context supplied to storage accessor implementations. + * + * This is an internal composition shape, not a platform-neutral storage contract. + */ +export interface StorageAccessorHost { + readonly app: App; + readonly settings: DiffZipBackupSettings; +} + +/** File kind reported by a storage accessor. */ +export enum FileType { + Missing, + File, + Folder, +} + +/** Stable storage accessor identifiers. */ +export const StorageAccessorTypes = { + NORMAL: "normal", + DIRECT: "direct", + EXTERNAL: "external", + S3: "s3", +} as const; + +/** Identifier for one DiffZip storage accessor implementation. */ +export type StorageAccessorType = typeof StorageAccessorTypes[keyof typeof StorageAccessorTypes]; diff --git a/src/storage.ts b/src/storage.ts index ba3a7ce..9316d8d 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -2,40 +2,21 @@ * Abstract class for storage accessors and its implementations. */ import type DiffZipBackupPlugin from "../main.ts"; -// Because of this is a type-only import. -// eslint-disable-next-line import/no-nodejs-modules -- type only import of promises -import type { promises } from "node:fs"; -import { OpenSSLCompat } from "octagonal-wheels/encryption"; import { NormalVault } from "./StorageAccessor/NormalVault.ts"; import { DirectVault } from "./StorageAccessor/DirectVault.ts"; import { ExternalVaultFilesystem } from "./StorageAccessor/ExternalVaultFilesystem.ts"; import { S3Bucket } from "./StorageAccessor/S3Bucket.ts"; import type { StorageAccessor } from "./StorageAccessor/StorageAccessor.ts"; -export const decryptCompatOpenSSL = OpenSSLCompat.CBC.decryptCBC; -export const encryptCompatOpenSSL = OpenSSLCompat.CBC.encryptCBC; +import { + StorageAccessorTypes, + type StorageAccessorType, +} from "./StorageAccessor/storage-accessor-types.ts"; -export enum FileType { - "Missing", - "File", - "Folder", -} - -export type FsAPI = { - mkdir: typeof promises.mkdir; - writeFile: typeof promises.writeFile; - readFile: typeof promises.readFile; - stat: typeof promises.stat; - rm: typeof promises.rm; -}; - -export const StorageAccessorTypes = { - NORMAL: "normal", - DIRECT: "direct", - EXTERNAL: "external", - S3: "s3", -} as const; - -export type StorageAccessorType = typeof StorageAccessorTypes[keyof typeof StorageAccessorTypes]; +export { + FileType, + StorageAccessorTypes, + type StorageAccessorType, +} from "./StorageAccessor/storage-accessor-types.ts"; export function getStorageTypeForBackupAccess(plugin: DiffZipBackupPlugin): StorageAccessorType { if (plugin.isDesktopMode) {