Changes
7 changed files (+178/-2)
-
-
@@ -9,3 +9,9 @@ # While this could be considered as an environment specific artifacts like `.DS_Store`# or `*.swp`, I decided to add this because this project is for Obsidian and `docs/` # directory is supposed to be edited using Obsidian. docs/.obsidian # What: Temporary directory for testing actual file writer # Why: Although test cleans up the temporary directory, there could be a chance the # cleanup code does not work and the directory (accidentally) remains. # This prevents VCS picking up the directory in that case. filesystem_writer/.test
-
-
-
@@ -32,6 +32,8 @@ "https://deno.land/std@0.221.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c","https://deno.land/std@0.221.0/assert/mod.ts": "7e41449e77a31fef91534379716971bebcfc12686e143d38ada5438e04d4a90e", "https://deno.land/std@0.221.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", "https://deno.land/std@0.221.0/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270", "https://deno.land/std@0.221.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", "https://deno.land/std@0.221.0/encoding/hex.ts": "e939f50d55be48a1fe42fecaaecdb54353df38e831c47f374be7e6fdbe61510e", "https://deno.land/std@0.221.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", "https://deno.land/std@0.221.0/front_matter/_formats.ts": "9a8ac1524f93b3ae093bd66864a49fc0088037920c6d60863da136d10f92e04d", "https://deno.land/std@0.221.0/front_matter/create_extractor.ts": "642e6e55cd07864b7c8068f88d271290d5d0a13d979ad335e10a7f52046b1f80",
-
-
-
@@ -0,0 +1,5 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 export * from "https://deno.land/std@0.221.0/encoding/hex.ts";
-
-
-
@@ -52,7 +52,8 @@The most straightforward way to run the tests is to run this command on the repository root: ``` $ deno test --allow-read=. $ deno test --allow-read=. --allow-write=./filesystem_writer/.test ``` If you omit the `--allow-read=.` permission, tests performing actual file I/O will be skipped. - If you omit the `--allow-read=.` permission, tests performing actual file I/O will be skipped. - If you omit the `--allow-write=./filesystem_writer/.test` permission, tests performing actual file write will be skipped.
-
-
-
@@ -0,0 +1,44 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { assertEquals } from "../deps/deno.land/std/assert/mod.ts"; import { DenoFsWriter } from "./deno_fs.ts"; const root = new URL("./.test/", import.meta.url); const writePermission = await Deno.permissions.query({ name: "write", path: root, }); // Read permission is also required in order to check the output file. const readPermission = await Deno.permissions.query({ name: "read", path: root, }); Deno.test("Should write a file", { // Skip this test if write permission is not granted. // Without this, simple `deno test` would fail or prompt permissions, which is annoying. ignore: writePermission.state !== "granted" || readPermission.state !== "granted", }, async () => { await Deno.mkdir(root, { recursive: true }); try { const writer = new DenoFsWriter(root); const content = new TextEncoder().encode("Hello, World!\n"); await writer.write(["foo", "bar.txt"], content); assertEquals( await Deno.readTextFile(new URL("foo/bar.txt", root)), "Hello, World!\n", ); } finally { await Deno.remove(root, { recursive: true }); } });
-
-
-
@@ -0,0 +1,100 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { dirname, SEPARATOR } from "../deps/deno.land/std/path/mod.ts"; import { encodeHex } from "../deps/deno.land/std/encoding/hex.ts"; import type { FileSystemWriter, WriteOptions } from "./interface.ts"; export class DenoFsWriter implements FileSystemWriter { #root: string; /** * Map of path and SHA-256 hash */ #wroteHashes: Map<string, string> = new Map(); /** * Map of path and SHA-256 hash */ #writingHashes: Map<string, string> = new Map(); constructor(rootDirectory: string | URL) { this.#root = typeof rootDirectory === "string" ? rootDirectory : rootDirectory.pathname; // Append trailing slash for easy path resolving if (!(/[\\/]$/.test(this.#root))) { this.#root += SEPARATOR; } Deno.permissions.request({ name: "write", path: this.#root }); } #resolve = (path: readonly string[]): string => { let normalized: string[] = []; for (const segment of path) { switch (segment) { case ".": break; case "..": // TODO: Abort for above the root traversal. normalized = normalized.slice(0, -1); break; default: normalized.push(segment); break; } } return this.#root + normalized.join(SEPARATOR); }; async write( path: readonly string[], content: Uint8Array, opts: WriteOptions = {}, ) { const resolvedPath = this.#resolve(path); const hash = encodeHex(await crypto.subtle.digest("SHA-256", content)); const wroteHash = this.#wroteHashes.get(resolvedPath); if (hash === wroteHash) { // Same content at same path, skip. return; } if (wroteHash && opts.errorOnOverwrite) { throw new Error( `Attempted to overwrite at "${resolvedPath}" (wrote: ${wroteHash}, attempt: ${hash})`, ); } const writingHash = this.#writingHashes.get(resolvedPath); if (hash === writingHash) { // Same content at same path, skip. return; } if (writingHash) { throw new Error( `Attempted to interrupt ongoing write at "${resolvedPath}" (writing: ${writingHash}, attempt: ${hash})`, ); } this.#writingHashes.set(resolvedPath, hash); try { await Deno.mkdir(dirname(resolvedPath), { recursive: true }); await Deno.writeFile(resolvedPath, content); this.#wroteHashes.set(resolvedPath, hash); } finally { this.#writingHashes.delete(resolvedPath); } } }
-
-
-
@@ -0,0 +1,18 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 export interface WriteOptions { /** * If this flag is on, the write operation aborts if the target path is already written. */ errorOnOverwrite?: boolean; } export interface FileSystemWriter { write( path: readonly string[], content: Uint8Array, options?: WriteOptions, ): Promise<void>; }
-