UNPKG

file-system-access

Version:

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

227 lines (201 loc) 7.05 kB
import fs from 'fs/promises' import { errors, isChunkObject } from '../util.js' import { join } from 'path' import Blob from 'fetch-blob' import { fileFrom } from 'fetch-blob/from.js' import { Adapter, FileSystemFileHandleAdapter, FileSystemFolderHandleAdapter, WriteChunk } from '../interfaces.js' import DOMException from 'node-domexception' // import mime from 'mime-types' const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors export class Sink implements UnderlyingSink<WriteChunk> { private fileHandle: fs.FileHandle private size: number private path: string private position = 0 constructor (fileHandle: fs.FileHandle, path: string, size: number) { this.fileHandle = fileHandle this.path = path this.size = size this.position = 0 } async abort() { await this.fileHandle.close() } async write (chunk: WriteChunk) { try { await fs.stat(this.path) } catch(err) { if (err.code === 'ENOENT') { await this.fileHandle.close().catch() throw new DOMException(...GONE) } } if (isChunkObject(chunk)) { if (chunk.type === 'write') { if (typeof chunk.position === 'number' && chunk.position >= 0) { this.position = chunk.position } if (!('data' in chunk)) { await 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 { await 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 { await 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 = Buffer.from(chunk) } else if (chunk instanceof Blob) { for await (const data of (chunk as Blob).stream()) { const res = await this.fileHandle.writev([data], this.position) this.position += res.bytesWritten this.size += res.bytesWritten } return } const res = await this.fileHandle.writev([chunk as Uint8Array | DataView], this.position) this.position += res.bytesWritten this.size += res.bytesWritten } async close () { // First make sure we close the handle await this.fileHandle.close() await fs.stat(this.path).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) }) } } export class FileHandle implements FileSystemFileHandleAdapter { readonly kind = 'file' readonly name: string private _path: string writable = true constructor (path: string, name: string) { this._path = path this.name = name } async getFile () { await fs.stat(this._path).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) }) return (await fileFrom(this._path)) as any as globalThis.File } async isSameEntry (other: FileHandle) { return this._path === this.#getPath.apply(other) } #getPath() { return this._path } async createWritable () { const fileHandle = await fs.open(this._path, 'r+').catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) throw err }) const { size } = await fileHandle.stat() return new Sink(fileHandle, this._path, size) } } export class FolderHandle implements FileSystemFolderHandleAdapter { readonly kind = 'directory' readonly name: string private _path: string writable = true constructor (path = '', name = '') { this.name = name this._path = path } async isSameEntry (other: FolderHandle) { return this._path === other._path } async * entries () { const dir = this._path const items = await fs.readdir(dir).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) throw err }) for (let name of items) { const path = join(dir, name) const stat = await fs.lstat(path) if (stat.isFile()) { yield [name, new FileHandle(path, name)] as [string, FileHandle] } else if (stat.isDirectory()) { yield [name, new FolderHandle(path, name)] as [string, FolderHandle] } } } async getDirectoryHandle (name: string, opts: FileSystemGetDirectoryOptions = {}) { const path = join(this._path, name) const stat = await fs.lstat(path).catch(err => { if (err.code !== 'ENOENT') 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 fs.mkdir(path) return new FolderHandle(path, name) } async getFileHandle (name: string, opts: FileSystemGetFileOptions = {}) { const path = join(this._path, name) const stat = await fs.lstat(path).catch(err => { if (err.code !== 'ENOENT') 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) await (await fs.open(path, 'w')).close() return new FileHandle(path, name) } async queryPermission () { return 'granted' as PermissionState } async removeEntry (name: string, opts: FileSystemRemoveOptions) { const path = join(this._path, name) const stat = await fs.lstat(path).catch(err => { if (err.code === 'ENOENT') throw new DOMException(...GONE) throw err }) if (stat.isDirectory()) { if (opts.recursive) { await fs.rm(path, { recursive: true, }).catch(err => { if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR) throw err }) } else { await fs.rmdir(path).catch(err => { if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR) throw err }) } } else { await fs.unlink(path) } } } const adapter: Adapter<string> = path => new FolderHandle(path) export default adapter