Changes
6 changed files (+101/-67)
-
-
@@ -3,22 +3,11 @@ //// 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"; import type { FileSystemWriter } 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"
-
@@ -56,45 +45,10 @@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); } await Deno.mkdir(dirname(resolvedPath), { recursive: true }); await Deno.writeFile(resolvedPath, content); } }
-
-
-
@@ -2,17 +2,9 @@ // 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>; }
-
-
-
@@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// // SPDX-License-Identifier: Apache-2.0 import type { FileSystemWriter, WriteOptions } from "./interface.ts"; import type { FileSystemWriter } from "./interface.ts"; const SEP = "/";
-
@@ -12,15 +12,8 @@async write( path: readonly string[], content: Uint8Array, opts: WriteOptions = {}, ): Promise<void> { const key = path.join(SEP); if (opts.errorOnOverwrite) { if (this.#files.has(key)) { throw new Error(`Attempt to overwrite file at "${key}"`); } } this.#files.set(key, content); }
-
-
-
@@ -0,0 +1,57 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { assertEquals, assertExists, assertRejects, } from "../deps/deno.land/std/assert/mod.ts"; import { MemoryFsWriter } from "./memory_fs.ts"; import { noOverwrite } from "./no_overwrite.ts"; Deno.test("Should prevent write to different contents to same path", async () => { const writer = noOverwrite(new MemoryFsWriter()); const enc = new TextEncoder(); await writer.write(["foo", "bar", "baz"], enc.encode("Foo Bar Baz\n")); await assertRejects(async () => // No final newline await writer.write(["foo", "bar", "baz"], enc.encode("Foo Bar Baz")) ); }); Deno.test("Should not reject if the contents are same", async () => { const memoryWriter = new MemoryFsWriter(); const writer = noOverwrite(memoryWriter); const enc = new TextEncoder(); await writer.write(["foo", "bar", "baz"], enc.encode("Foo Bar Baz\n")); await writer.write(["foo", "bar", "baz"], enc.encode("Foo Bar Baz\n")); const file = memoryWriter.get(["foo", "bar", "baz"]); assertExists(file); assertEquals(new TextDecoder().decode(file), "Foo Bar Baz\n"); }); Deno.test("Should not reject if paths are different", async () => { const memoryWriter = new MemoryFsWriter(); const writer = noOverwrite(memoryWriter); const enc = new TextEncoder(); await writer.write(["foo", "bar", "baz"], enc.encode("Foo Bar Baz\n")); await writer.write(["foo", "bar", "qux"], enc.encode("Foo Bar Qux\n")); const baz = memoryWriter.get(["foo", "bar", "baz"]); assertExists(baz); assertEquals(new TextDecoder().decode(baz), "Foo Bar Baz\n"); const qux = memoryWriter.get(["foo", "bar", "qux"]); assertExists(qux); assertEquals(new TextDecoder().decode(qux), "Foo Bar Qux\n"); });
-
-
-
@@ -0,0 +1,38 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { encodeHex } from "../deps/deno.land/std/encoding/hex.ts"; import type { FileSystemWriter } from "./interface.ts"; const SEP = "/"; /** * Wraps the given FileSystem Writer and returns a new FileSystem Writer * that prevents writes to the same location with different content. * It also suppress redundant writes if the content hash is same. */ export function noOverwrite(childWriter: FileSystemWriter): FileSystemWriter { const hashes = new Map<string, string>(); return { async write(path, content) { const key = path.join(SEP); const hash = encodeHex(await crypto.subtle.digest("SHA-256", content)); const prev = hashes.get(key); if (!prev) { hashes.set(key, hash); return childWriter.write(path, content); } if (prev !== hash) { throw new Error( `Detected an attempt to write different content to "${key}":` + `first hash = ${prev}, second hash = ${hash}`, ); } }, }; }
-
-
-
@@ -29,10 +29,10 @@ export function validateTree(childWriter: FileSystemWriter): FileSystemWriter {const nodes = new Map<string, NodeType>(); return { write(path, content, opts) { write(path, content) { setNodeTypeRecur(path, NodeType.File, nodes); return childWriter.write(path, content, opts); return childWriter.write(path, content); }, }; }
-