UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

366 lines (327 loc) 10.8 kB
import { $inject, AlephaError, type FileLike, Json } from "alepha"; import { FileDetector } from "../services/FileDetector.ts"; import type { CpOptions, CreateFileOptions, FileSystemProvider, LsOptions, MkdirOptions, RmOptions, } from "./FileSystemProvider.ts"; // --------------------------------------------------------------------------------------------------------------------- /** * Web-standard implementation of FileSystemProvider for Cloudflare Workers and other edge runtimes. * * Uses only Web APIs (ReadableStream, TextEncoder, etc.) — no Node.js-specific APIs. * Provides working `createFile` with proper streaming support. * Filesystem operations (rm, cp, mv, etc.) are not available in edge runtimes and will throw. * * @example * ```typescript * const fs = alepha.inject(WorkerdFileSystemProvider); * * // Create from text (returns FileLike with web ReadableStream) * const file = fs.createFile({ text: "Hello!", name: "greeting.txt" }); * const stream = file.stream(); // ReadableStream (web standard) * ``` */ export class WorkerdFileSystemProvider implements FileSystemProvider { protected detector = $inject(FileDetector); protected json = $inject(Json); protected encoder = new TextEncoder(); protected decoder = new TextDecoder(); public join(...paths: string[]): string { const joined = paths.join("/").replace(/\/+/g, "/"); const parts = joined.split("/"); const resolved: string[] = []; for (const part of parts) { if (part === "..") { resolved.pop(); } else if (part !== ".") { resolved.push(part); } } return resolved.join("/") || "."; } public createFile(options: CreateFileOptions): FileLike { if ("text" in options) { return this.createFileFromText(options.text, { type: options.type, name: options.name, }); } if ("arrayBuffer" in options) { return this.createFileFromArrayBuffer(options.arrayBuffer, { type: options.type, name: options.name, }); } if ("buffer" in options) { const ab = options.buffer instanceof ArrayBuffer ? options.buffer : options.buffer.buffer.slice( options.buffer.byteOffset, options.buffer.byteOffset + options.buffer.byteLength, ); return this.createFileFromArrayBuffer(ab as ArrayBuffer, { type: options.type, name: options.name, }); } if ("file" in options) { return this.createFileFromWebFile(options.file, { type: options.type, name: options.name, size: options.size, }); } if ("response" in options) { return this.createFileFromResponse(options.response, { type: options.type, name: options.name, }); } if ("stream" in options) { return this.createFileFromStream(options.stream as ReadableStream, { type: options.type, name: options.name, size: options.size, }); } if ("url" in options) { return this.createFileFromUrl(options.url, { type: options.type, name: options.name, }); } if ("path" in options) { throw new AlephaError( "WorkerdFileSystemProvider.createFile: 'path' source is not supported in edge runtimes.", ); } throw new AlephaError( "WorkerdFileSystemProvider.createFile: unsupported options.", ); } // ------------------------------------------------------------------------------------------------------------------- protected createFileFromText( text: string, options: { type?: string; name?: string } = {}, ): FileLike { const encoded = this.encoder.encode(text); const name = options.name ?? "file.txt"; return { name, type: options.type ?? this.detector.getContentType(name), size: encoded.byteLength, lastModified: Date.now(), stream: () => new ReadableStream({ start(controller) { controller.enqueue(encoded); controller.close(); }, }), arrayBuffer: async () => encoded.buffer.slice( encoded.byteOffset, encoded.byteOffset + encoded.byteLength, ) as ArrayBuffer, text: async () => text, }; } protected createFileFromArrayBuffer( source: ArrayBuffer, options: { type?: string; name?: string } = {}, ): FileLike { const name = options.name ?? "file"; const bytes = new Uint8Array(source); return { name, type: options.type ?? this.detector.getContentType(name), size: source.byteLength, lastModified: Date.now(), stream: () => new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); }, }), arrayBuffer: async () => source, text: async () => this.decoder.decode(source), }; } protected createFileFromWebFile( source: File, options: { type?: string; name?: string; size?: number } = {}, ): FileLike { const name = options.name ?? source.name; return { name, type: options.type ?? (source.type || this.detector.getContentType(name)), size: options.size ?? source.size ?? 0, lastModified: source.lastModified || Date.now(), stream: () => source.stream(), arrayBuffer: async () => await source.arrayBuffer(), text: async () => await source.text(), }; } protected createFileFromResponse( response: Response, options: { type?: string; name?: string } = {}, ): FileLike { if (!response.body) { throw new AlephaError("Response has no body stream"); } const sizeHeader = response.headers.get("content-length"); const size = sizeHeader ? parseInt(sizeHeader, 10) : 0; let name = options.name; if (!name) { const cd = response.headers.get("content-disposition"); if (cd) { const match = cd.match(/filename="?([^"]+)"?/); if (match) { name = match[1]; } } } name ??= "file"; const type = options.type ?? response.headers.get("content-type") ?? undefined; return { name, type: type ?? this.detector.getContentType(name), size, lastModified: Date.now(), stream: () => response.body!, arrayBuffer: async () => await response.arrayBuffer(), text: async () => await response.text(), }; } protected createFileFromStream( source: ReadableStream, options: { type?: string; name?: string; size?: number } = {}, ): FileLike { const name = options.name ?? "file"; let buffer: ArrayBuffer | null = null; const consumeStream = async (): Promise<ArrayBuffer> => { if (buffer) return buffer; const reader = source.getReader(); const chunks: Uint8Array[] = []; let done = false; while (!done) { const result = await reader.read(); done = result.done; if (result.value) { chunks.push(result.value); } } const total = chunks.reduce((sum, c) => sum + c.byteLength, 0); const merged = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; } buffer = merged.buffer as ArrayBuffer; return buffer; }; return { name, type: options.type ?? this.detector.getContentType(name), size: options.size ?? 0, lastModified: Date.now(), stream: () => source, arrayBuffer: consumeStream, text: async () => this.decoder.decode(await consumeStream()), }; } protected createFileFromUrl( url: string, options: { type?: string; name?: string } = {}, ): FileLike { const parsedUrl = new URL(url); const name = options.name ?? parsedUrl.pathname.split("/").pop() ?? "file"; return { name, type: options.type ?? this.detector.getContentType(name), size: 0, lastModified: Date.now(), stream: () => { throw new AlephaError( "WorkerdFileSystemProvider: streaming from URL is not supported. Use fetch() and createFile({ response }) instead.", ); }, arrayBuffer: async () => { const res = await fetch(url); return await res.arrayBuffer(); }, text: async () => { const res = await fetch(url); return await res.text(); }, }; } // ------------------------------------------------------------------------------------------------------------------- // Filesystem operations — not available in edge runtimes // ------------------------------------------------------------------------------------------------------------------- public async rm(_path: string, _options?: RmOptions): Promise<void> { throw new AlephaError( "WorkerdFileSystemProvider: rm() is not available in edge runtimes.", ); } public async cp( _src: string, _dest: string, _options?: CpOptions, ): Promise<void> { throw new AlephaError( "WorkerdFileSystemProvider: cp() is not available in edge runtimes.", ); } public async mv(_src: string, _dest: string): Promise<void> { throw new AlephaError( "WorkerdFileSystemProvider: mv() is not available in edge runtimes.", ); } public async mkdir(_path: string, _options?: MkdirOptions): Promise<void> { throw new AlephaError( "WorkerdFileSystemProvider: mkdir() is not available in edge runtimes.", ); } public async ls(_path: string, _options?: LsOptions): Promise<string[]> { throw new AlephaError( "WorkerdFileSystemProvider: ls() is not available in edge runtimes.", ); } public async exists(_path: string): Promise<boolean> { throw new AlephaError( "WorkerdFileSystemProvider: exists() is not available in edge runtimes.", ); } public async readFile(_path: string): Promise<Buffer> { throw new AlephaError( "WorkerdFileSystemProvider: readFile() is not available in edge runtimes.", ); } public async writeFile( _path: string, _data: Uint8Array | Buffer | string | FileLike, ): Promise<void> { throw new AlephaError( "WorkerdFileSystemProvider: writeFile() is not available in edge runtimes.", ); } public async readTextFile(_path: string): Promise<string> { throw new AlephaError( "WorkerdFileSystemProvider: readTextFile() is not available in edge runtimes.", ); } public async readJsonFile<T = unknown>(_path: string): Promise<T> { throw new AlephaError( "WorkerdFileSystemProvider: readJsonFile() is not available in edge runtimes.", ); } }