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
text/typescript
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