UNPKG

file-system-access

Version:

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

249 lines (211 loc) 7.32 kB
import { Adapter, FileSystemFileHandleAdapter, FileSystemFolderHandleAdapter, WriteChunk } from '../interfaces.js' import { errors, isChunkObject } from '../util.js' const { INVALID, GONE, SYNTAX, DISALLOWED } = errors class Sink implements UnderlyingSink<WriteChunk> { private file: File private size: number private position: number constructor (private fileHandle: FileHandle, file: File, private writer: FileWriter, keepExistingData: boolean) { this.file = keepExistingData ? file : new File([], file.name, file) this.size = keepExistingData ? file.size : 0 this.position = 0 } async write (chunk: WriteChunk) { try { // This ensures an error is thrown if the file has been deleted await this.fileHandle.getFile() } catch (err) { throw new DOMException(...GONE) } let file = this.file if (isChunkObject(chunk)) { if (chunk.type === 'write') { if (typeof chunk.position === 'number' && chunk.position >= 0) { this.position = chunk.position if (this.size < chunk.position) { this.file = new File( [this.file, new ArrayBuffer(chunk.position - this.size)], this.file.name, this.file ) } } if (!('data' in chunk)) { 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 { throw new DOMException(...SYNTAX('seek requires a position argument')) } } else if (chunk.type === 'truncate') { if (Number.isInteger(chunk.size) && chunk.size >= 0) { file = chunk.size < this.size ? new File([file.slice(0, chunk.size)], file.name, file) : new File([file, new Uint8Array(chunk.size - this.size)], file.name, file) this.size = file.size if (this.position > file.size) { this.position = file.size } this.file = file return } else { throw new DOMException(...SYNTAX('truncate requires a size argument')) } } } chunk = new Blob([chunk]) let blob = this.file // Calc the head and tail fragments const head = blob.slice(0, this.position) const tail = blob.slice(this.position + chunk.size) // Calc the padding let padding = this.position - head.size if (padding < 0) { padding = 0 } blob = new File([ head, new Uint8Array(padding), chunk, tail ], blob.name) this.size = blob.size this.position += chunk.size this.file = blob } async close () { try { // This ensures an error is thrown if the file has been deleted await this.fileHandle.getFile() } catch (err) { throw new DOMException(...GONE) } // We need to work with a cloned file because writer.truncate() will destroy our original data const bufferCopy = await new Promise<ArrayBuffer>((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result as ArrayBuffer) reader.onerror = reject reader.readAsArrayBuffer(this.file) }) this.file = new File([bufferCopy], this.file.name, this.file) await new Promise((resolve, reject) => { this.writer.onwriteend = resolve this.writer.onerror = reject this.writer.truncate(0) }) await new Promise((resolve, reject) => { this.writer.onwriteend = resolve this.writer.onerror = reject this.writer.write(this.file) }) this.writer.onwriteend = null! this.writer.onerror = null! this.file = this.position = this.size = null! } } export class FileHandle implements FileSystemFileHandleAdapter { readonly kind = 'file' file: FileEntry writable: boolean readable = true constructor (file: FileEntry, writable = true) { this.file = file this.writable = writable } get name () { return this.file.name } async isSameEntry (other: FileHandle) { return this.file.toURL() === other.file.toURL() } getFile () { return new Promise(this.file.file.bind(this.file)) } async createWritable (opts: FileSystemCreateWritableOptions) { if (!this.writable) throw new DOMException(...DISALLOWED) const file = await this.getFile() return new Promise<Sink>((resolve, reject) => this.file.createWriter(fileWriter => { resolve(new Sink(this, file, fileWriter, !!opts.keepExistingData)) }, reject) ) } } export class FolderHandle implements FileSystemFolderHandleAdapter { readonly kind = 'directory' readonly name private dir: FileSystemDirectoryEntry writable: boolean readable = true constructor (dir: FileSystemDirectoryEntry, writable = true) { this.dir = dir this.writable = writable this.name = dir.name } async isSameEntry (other: FolderHandle) { return this.dir.fullPath === other.dir.fullPath } async * entries () { const reader = this.dir.createReader() const entries = await new Promise(reader.readEntries.bind(reader)) for (const x of entries) { yield [ x.name, x.isFile ? new FileHandle(x as FileEntry, this.writable) : new FolderHandle(x as FileSystemDirectoryEntry, this.writable) ] as [string, FileHandle | FolderHandle] } } getDirectoryHandle (name: string, opts: Flags = {}) { return new Promise<FolderHandle>((resolve, reject) => { this.dir.getDirectory(name, opts, dir => { resolve(new FolderHandle(dir)) }, reject) }) } getFileHandle (name: string, opts: Flags = {}) { return new Promise<FileHandle>((resolve, reject) => this.dir.getFile(name, opts, file => resolve(new FileHandle(file)), reject) ) } async removeEntry (name: string, opts?: { recursive?: boolean}) { let entry: FolderHandle | FileHandle try { entry = await this.getDirectoryHandle(name) } catch (err) { if (err.name === 'TypeMismatchError') { entry = await this.getFileHandle(name) } else { throw err } } return new Promise<void>((resolve, reject) => { if (entry instanceof FolderHandle) { opts?.recursive ? entry.dir.removeRecursively(() => resolve(), reject) : entry.dir.remove(() => resolve(), reject) } else if (entry.file) { entry.file.remove(() => resolve(), reject) } }) } } export interface SandboxOptions { _persistent?: 0 | 1 } const adapter: Adapter<SandboxOptions> = ({ _persistent = 0 }) => new Promise((resolve, reject) => window.webkitRequestFileSystem( _persistent, 0, e => resolve(new FolderHandle(e.root)), reject ) ) export default adapter