UNPKG

file-system-access

Version:

File System Access API implementation (ponyfill) with pluggable storage adapters via IndexedDB, Cache API, in-memory etc.

269 lines (244 loc) 7.41 kB
import { join, basename } from 'https://deno.land/std@0.98.0/path/mod.ts' import { errors } from '../util.js' const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors // TODO: // - either depend on fetch-blob. // - push for https://github.com/denoland/deno/pull/10969 // - or extend the File class like i did in that PR /** @param {string} path */ async function fileFrom (path) { const e = Deno.readFileSync(path) const s = await Deno.stat(path) return new File([e], basename(path), { lastModified: Number(s.mtime) }) } export class Sink { /** * @param {Deno.File} fileHandle * @param {string} path * @param {number} size */ constructor (fileHandle, path, size) { this.fileHandle = fileHandle this.path = path this.size = size this.position = 0 } async abort() { this.fileHandle.close() } async write (chunk) { try { await Deno.stat(this.path) } catch (err) { if (err.name === 'NotFound') { this.fileHandle.close() throw new DOMException(...GONE) } } if (typeof chunk === 'object') { if (chunk.type === 'write') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { this.position = chunk.position } if (!('data' in chunk)) { this.fileHandle.close() throw new DOMException(...SYNTAX('write requires a data argument')) } chunk = chunk.data } else if (chunk.type === 'seek') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { if (this.size < chunk.position) { throw new DOMException(...INVALID) } this.position = chunk.position return } else { this.fileHandle.close() throw new DOMException(...SYNTAX('seek requires a position argument')) } } else if (chunk.type === 'truncate') { if (Number.isInteger(chunk.size) && chunk.size >= 0) { await this.fileHandle.truncate(chunk.size) this.size = chunk.size if (this.position > this.size) { this.position = this.size } return } else { this.fileHandle.close() throw new DOMException(...SYNTAX('truncate requires a size argument')) } } } if (chunk instanceof ArrayBuffer) { chunk = new Uint8Array(chunk) } else if (typeof chunk === 'string') { chunk = new TextEncoder().encode(chunk) } else if (chunk instanceof Blob) { await this.fileHandle.seek(this.position, Deno.SeekMode.Start) for await (const data of chunk.stream()) { const written = await this.fileHandle.write(data) this.position += written this.size += written } return } await this.fileHandle.seek(this.position, Deno.SeekMode.Start) const written = await this.fileHandle.write(chunk) this.position += written this.size += written } async close () { // First make sure the handle is closed this.fileHandle.close() try { await Deno.stat(this.path) } catch (err) { if (err.name === 'NotFound') { throw new DOMException(...GONE) } } } } export class FileHandle { #path name /** @readonly */ kind = 'file' writable = true /** * @param {string} path * @param {string} name */ constructor (path, name) { this.#path = path this.name = name } async getFile () { await Deno.stat(this.#path).catch(err => { if (err.name === 'NotFound') throw new DOMException(...GONE) }) return fileFrom(this.#path) } async isSameEntry (other) { return this.#path === this.#getPath.apply(other) } #getPath() { return this.#path } async createWritable () { const fileHandle = await Deno.open(this.#path, {write: true}).catch(err => { if (err.name === 'NotFound') throw new DOMException(...GONE) throw err }) const { size } = await fileHandle.stat() return new Sink(fileHandle, this.#path, size) } } export class FolderHandle { #path = '' name /** @readonly */ kind = 'directory' writable = true /** @param {string} path */ constructor (path, name = '') { this.name = name this.#path = join(path) } async isSameEntry (other) { return this.#path === this.#getPath.apply(other) } #getPath() { return this.#path } /** * @returns {AsyncGenerator<[string, FileHandle | FolderHandle], void, unknown>} */ async * entries () { const dir = this.#path try { for await (const dirEntry of Deno.readDir(dir)) { const { name } = dirEntry const path = join(dir, name) const stat = await Deno.lstat(path) if (stat.isFile) { yield [name, new FileHandle(path, name)] } else if (stat.isDirectory) { yield [name, new FolderHandle(path, name)] } } } catch (err) { throw err.name === 'NotFound' ? new DOMException(...GONE) : err } } /** * @param {string} name * @param {{create?: boolean}} opts */ async getDirectoryHandle (name, opts = {}) { const path = join(this.#path, name) const stat = await Deno.lstat(path).catch(err => { if (err.name !== 'NotFound') throw err }) const isDirectory = stat?.isDirectory if (stat && isDirectory) return new FolderHandle(path, name) if (stat && !isDirectory) throw new DOMException(...MISMATCH) if (!opts.create) throw new DOMException(...GONE) await Deno.mkdir(path) return new FolderHandle(path, name) } /** * @param {string} name * @param {{ create: any; }} opts */ async getFileHandle (name, opts) { const path = join(this.#path, name) const stat = await Deno.lstat(path).catch(err => { if (err.name !== 'NotFound') throw err }) const isFile = stat?.isFile if (stat && isFile) return new FileHandle(path, name) if (stat && !isFile) throw new DOMException(...MISMATCH) if (!opts.create) throw new DOMException(...GONE) const c = await Deno.open(path, { create: true, write: true, }) c.close() return new FileHandle(path, name) } /** * @returns {Promise<PermissionState>} */ async queryPermission () { return 'granted' } /** * @param {string} name * @param {{ recursive: boolean; }} opts */ async removeEntry (name, opts) { const path = join(this.#path, name) const stat = await Deno.lstat(path).catch(err => { if (err.name === 'NotFound') throw new DOMException(...GONE) throw err }) if (stat.isDirectory) { if (opts.recursive) { await Deno.remove(path, { recursive: true }).catch(err => { if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR) throw err }) } else { await Deno.remove(path).catch(() => { throw new DOMException(...MOD_ERR) }) } } else { await Deno.remove(path) } } } /** @type import('../interfaces.js').Adapter<string> */ export default path => new FolderHandle(join(Deno.cwd(), path))