file-system-access
Version:
File System Access API implementation (ponyfill) with pluggable storage adapters via IndexedDB, Cache API, in-memory etc.
239 lines • 8.95 kB
JavaScript
import { errors, isChunkObject } from '../util.js';
const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors;
const DIR = { headers: { 'content-type': 'dir' } };
const FILE = () => ({ headers: { 'content-type': 'file', 'last-modified': '' + Date.now() } });
class Sink {
constructor(cache, path, file) {
this._cache = cache;
this.path = path;
this.size = file.size;
this.position = 0;
this.file = file;
}
async write(chunk) {
const [r] = await this._cache.keys(this.path);
if (!r)
throw new DOMException(...GONE);
if (isChunkObject(chunk)) {
if (chunk.type === 'write') {
if (typeof chunk.position === 'number' && chunk.position >= 0) {
if (this.size < chunk.position) {
const blob = new Blob([this.file, new ArrayBuffer(chunk.position - this.size)]);
this.file = new File([blob], this.file.name, this.file);
}
this.position = chunk.position;
}
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) {
let file = this.file;
file = new File(chunk.size < this.size ? [file.slice(0, chunk.size)] : [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() {
const [r] = await this._cache.keys(this.path);
if (!r)
throw new DOMException(...GONE);
return this._cache.put(this.path, new Response(this.file, FILE()));
}
}
export class FileHandle {
constructor(path, cache) {
this.kind = 'file';
this.writable = true;
this.readable = true;
this._cache = cache;
this.path = path;
}
get name() {
return this.path.split('/').pop();
}
async isSameEntry(other) {
return this.path === other.path;
}
async getFile() {
const res = await this._cache.match(this.path);
if (!res)
throw new DOMException(...GONE);
const blob = await res.blob();
const file = new File([blob], this.name, { lastModified: +res.headers.get('last-modified') });
return file;
}
async createWritable(opts) {
let file = await this.getFile();
if (!opts.keepExistingData) {
file = new File([], file.name, file);
}
return new Sink(this._cache, this.path, file);
// let p, rs
// p = new Promise(resolve => rs = resolve)
// const { readable, writable } = new TransformStream(new Sink(p))
// this._cache.put(this.path, new Response(readable, FILE())).then(rs)
// return writable.getWriter()
}
}
export class FolderHandle {
constructor(dir, cache) {
this.kind = 'directory';
this.writable = true;
this.readable = true;
this._dir = dir;
this._cache = cache;
this.name = dir.split('/').pop();
}
async *entries() {
for (const [path, isFile] of Object.entries(await this._tree)) {
yield [
path.split('/').pop(),
isFile ? new FileHandle(path, this._cache) : new FolderHandle(path, this._cache)
];
}
}
async isSameEntry(other) {
return this._dir === other._dir;
}
async getDirectoryHandle(name, opts = {}) {
const path = this._dir.endsWith('/') ? this._dir + name : `${this._dir}/${name}`;
const tree = await this._tree;
if (tree.hasOwnProperty(path)) {
const isFile = tree[path];
if (isFile)
throw new DOMException(...MISMATCH);
return new FolderHandle(path, this._cache);
}
else {
if (opts.create) {
tree[path] = false;
await this._cache.put(path, new Response('{}', DIR));
await this._save(tree);
return new FolderHandle(path, this._cache);
}
throw new DOMException(...GONE);
}
}
get _tree() {
return this._cache.match(this._dir).then(r => r.json()).catch(e => {
throw new DOMException(...GONE);
});
}
_save(tree) {
return this._cache.put(this._dir, new Response(JSON.stringify(tree), DIR));
}
async getFileHandle(name, opts = {}) {
const path = this._dir.endsWith('/') ? this._dir + name : `${this._dir}/${name}`;
const tree = await this._tree;
if (tree.hasOwnProperty(path)) {
const isFile = tree[path];
if (!isFile)
throw new DOMException(...MISMATCH);
return new FileHandle(path, this._cache);
}
else {
if (opts.create) {
const tree = await this._tree;
tree[path] = true;
await this._cache.put(path, new Response('', FILE()));
await this._save(tree);
return new FileHandle(path, this._cache);
}
else {
throw new DOMException(...GONE);
}
}
}
async removeEntry(name, opts) {
const tree = await this._tree;
const path = this._dir.endsWith('/') ? this._dir + name : `${this._dir}/${name}`;
if (tree.hasOwnProperty(path)) {
if (opts.recursive) {
const toDelete = [...Object.entries(tree)];
while (toDelete.length) {
const [path, isFile] = toDelete.pop();
if (isFile) {
await this._cache.delete(path);
}
else {
const e = await this._cache.match(path).then(r => r.json());
toDelete.push(...Object.entries(e));
}
}
delete tree[path];
}
else {
const isFile = tree[path];
delete tree[path];
if (isFile) {
await this._cache.delete(path);
}
else {
const e = await this._cache.match(path).then(r => r.json());
const keys = Object.keys(e);
if (keys.length) {
throw new DOMException(...MOD_ERR);
}
else {
await this._cache.delete(path);
}
}
}
await this._save(tree);
}
else {
throw new DOMException(...GONE);
}
}
}
const adapter = async () => {
const cache = await caches.open('sandboxed-fs');
if (!await cache.match('/'))
await cache.put('/', new Response('{}', DIR));
return new FolderHandle(location.origin + '/', cache);
};
export default adapter;
//# sourceMappingURL=cache.js.map