native-file-system-adapter
Advanced tools
Comparing version 3.0.0 to 3.0.1
@@ -0,1 +1,4 @@ | ||
// Want to remove this postMessage hack, tell them u want transferable streams: | ||
// https://bugs.webkit.org/show_bug.cgi?id=215485 | ||
const WRITE = 0 | ||
@@ -27,2 +30,6 @@ const PULL = 0 | ||
pull () { | ||
this.port.postMessage({ type: PULL }) | ||
} | ||
/** @param {Error} reason */ | ||
@@ -41,3 +48,2 @@ cancel (reason) { | ||
this.controller.enqueue(message.chunk) | ||
this.port.postMessage({ type: PULL }) | ||
} | ||
@@ -44,0 +50,0 @@ if (message.type === ABORT) { |
{ | ||
"name": "native-file-system-adapter", | ||
"version": "3.0.0", | ||
"version": "3.0.1", | ||
"description": "Native File System API", | ||
@@ -27,3 +27,3 @@ "main": "src/es6.js", | ||
}, | ||
"types": "/types/mod.d.ts", | ||
"types": "types/mod.d.ts", | ||
"keywords": [ | ||
@@ -30,0 +30,0 @@ "filesystem", |
@@ -1,3 +0,1 @@ | ||
/* global Blob, DOMException, Response, MessageChannel */ | ||
import { errors } from '../util.js' | ||
@@ -14,4 +12,4 @@ import config from '../config.js' | ||
const { GONE } = errors | ||
// @ts-ignore | ||
const isSafari = /constructor/i.test(window.HTMLElement) || window.safari || window.WebKitPoint | ||
// @ts-ignore - Don't match newer versions of Safari, but that's okay | ||
const isOldSafari = /constructor/i.test(window.HTMLElement) | ||
@@ -43,3 +41,3 @@ export class FileHandle { | ||
if (isSafari || !sw) { | ||
if (isOldSafari || !sw) { | ||
/** @type {Blob[]} */ | ||
@@ -100,2 +98,5 @@ let chunks = [] | ||
// Want to remove this postMessage hack, tell them u want transferable streams: | ||
// https://bugs.webkit.org/show_bug.cgi?id=215485 | ||
const WRITE = 0 | ||
@@ -102,0 +103,0 @@ const PULL = 0 |
@@ -5,4 +5,13 @@ /* global indexedDB, Blob, File, DOMException */ | ||
const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors | ||
const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, ABORT } = errors | ||
/** | ||
* @param {IDBTransaction} tx | ||
* @param {(e) => {}} onerror | ||
*/ | ||
function setupTxErrorHandler (tx, onerror) { | ||
tx.onerror = () => onerror(tx.error) | ||
tx.onabort = () => onerror(tx.error || new DOMException(...ABORT)) | ||
} | ||
class Sink { | ||
@@ -9,0 +18,0 @@ /** |
@@ -5,6 +5,7 @@ import showDirectoryPicker from './showDirectoryPicker.js' | ||
import getOriginPrivateDirectory from './getOriginPrivateDirectory.js' | ||
// FileSystemWritableFileStream must be loaded before FileSystemFileHandle | ||
import FileSystemWritableFileStream from './FileSystemWritableFileStream.js' | ||
import FileSystemDirectoryHandle from './FileSystemDirectoryHandle.js' | ||
import FileSystemFileHandle from './FileSystemFileHandle.js' | ||
import FileSystemHandle from './FileSystemHandle.js' | ||
import FileSystemWritableFileStream from './FileSystemWritableFileStream.js' | ||
@@ -11,0 +12,0 @@ export { |
import FileSystemHandle from './FileSystemHandle.js' | ||
import { errors } from './util.js' | ||
const { GONE, MOD_ERR } = errors | ||
const kAdapter = Symbol('adapter') | ||
@@ -93,2 +96,3 @@ | ||
let { handle: current, path } = openSet.pop() | ||
for await (const entry of current.values()) { | ||
@@ -136,3 +140,60 @@ if (await entry.isSameEntry(possibleDescendant)) { | ||
if (globalThis.FileSystemDirectoryHandle) { | ||
const proto = globalThis.FileSystemDirectoryHandle.prototype | ||
proto.resolve = async function resolve (possibleDescendant) { | ||
if (await possibleDescendant.isSameEntry(this)) { | ||
return [] | ||
} | ||
const openSet = [{ handle: this, path: [] }] | ||
while (openSet.length) { | ||
let { handle: current, path } = openSet.pop() | ||
for await (const entry of current.values()) { | ||
if (await entry.isSameEntry(possibleDescendant)) { | ||
return [...path, entry.name] | ||
} | ||
if (entry.kind === 'directory') { | ||
openSet.push({ handle: entry, path: [...path, entry.name] }) | ||
} | ||
} | ||
} | ||
return null | ||
} | ||
// Safari allows us operate on deleted files, | ||
// so we need to check if they still exist. | ||
// Hope to remove this one day. | ||
async function ensureDoActuallyStillExist (handle) { | ||
const root = await navigator.storage.getDirectory() | ||
const path = await root.resolve(handle) | ||
if (path === null) { throw new DOMException(...GONE) } | ||
} | ||
const entries = proto.entries | ||
proto.entries = async function * () { | ||
await ensureDoActuallyStillExist(this) | ||
yield * entries.call(this) | ||
} | ||
proto[Symbol.asyncIterator] = async function * () { | ||
yield * this.entries() | ||
} | ||
const removeEntry = proto.removeEntry | ||
proto.removeEntry = async function (name, options = {}) { | ||
return removeEntry.call(this, name, options).catch(async err => { | ||
const unknown = err instanceof DOMException && err.name === 'UnknownError' | ||
if (unknown && !options.recursive) { | ||
const empty = (await entries.call(this).next()).done | ||
if (!empty) { throw new DOMException(...MOD_ERR) } | ||
} | ||
throw err | ||
}) | ||
} | ||
} | ||
export default FileSystemDirectoryHandle | ||
export { FileSystemDirectoryHandle } |
import FileSystemHandle from './FileSystemHandle.js' | ||
import FileSystemWritableFileStream from './FileSystemWritableFileStream.js' | ||
import { errors } from './util.js' | ||
const { INVALID, SYNTAX, GONE } = errors | ||
const kAdapter = Symbol('adapter') | ||
@@ -46,3 +49,182 @@ | ||
// Safari doesn't support async createWritable streams yet. | ||
if ( | ||
globalThis.FileSystemFileHandle && | ||
!globalThis.FileSystemFileHandle.prototype.createWritable | ||
) { | ||
const wm = new WeakMap() | ||
let workerUrl | ||
// Worker code that should be inlined (can't use any external functions) | ||
const code = () => { | ||
let fileHandle, handle | ||
onmessage = async evt => { | ||
const port = evt.ports[0] | ||
const cmd = evt.data | ||
switch (cmd.type) { | ||
case 'open': | ||
const file = cmd.name | ||
let dir = await navigator.storage.getDirectory() | ||
for (const folder of cmd.path) { | ||
dir = await dir.getDirectoryHandle(folder) | ||
} | ||
fileHandle = await dir.getFileHandle(file) | ||
handle = await fileHandle.createSyncAccessHandle() | ||
break | ||
case 'write': | ||
handle.write(cmd.data, { at: cmd.position }) | ||
handle.flush() | ||
break | ||
case 'truncate': | ||
handle.truncate(cmd.size) | ||
break | ||
case 'abort': | ||
case 'close': | ||
handle.close() | ||
break | ||
} | ||
port.postMessage(0) | ||
} | ||
} | ||
globalThis.FileSystemFileHandle.prototype.createWritable = async function (options) { | ||
// Safari only support writing data in a worker with sync access handle. | ||
if (!workerUrl) { | ||
const stringCode = `(${code.toString()})()` | ||
const blob = new Blob([stringCode], { | ||
type: 'text/javascript' | ||
}) | ||
workerUrl = URL.createObjectURL(blob) | ||
} | ||
const worker = new Worker(workerUrl, { type: 'module' }) | ||
let position = 0 | ||
const textEncoder = new TextEncoder() | ||
let size = await this.getFile().then(file => file.size) | ||
const send = message => new Promise((resolve, reject) => { | ||
const mc = new MessageChannel() | ||
mc.port1.onmessage = evt => { | ||
if (evt.data instanceof Error) reject(evt.data) | ||
else resolve(evt.data) | ||
mc.port1.close() | ||
mc.port2.close() | ||
mc.port1.onmessage = null | ||
} | ||
worker.postMessage(message, [mc.port2]) | ||
}) | ||
// Safari also don't support transferable file system handles. | ||
// So we need to pass the path to the worker. This is a bit hacky and ugly. | ||
const root = await navigator.storage.getDirectory() | ||
const parent = await wm.get(this) | ||
const path = await root.resolve(parent) | ||
// Should likely never happen, but just in case... | ||
if (path === null) throw new DOMException(...GONE) | ||
let controller | ||
await send({ type: 'open', path, name: this.name }) | ||
if (options?.keepExistingData === false) { | ||
await send({ type: 'truncate', size: 0 }) | ||
size = 0 | ||
} | ||
const ws = new FileSystemWritableFileStream({ | ||
start: ctrl => { | ||
controller = ctrl | ||
}, | ||
async write(chunk) { | ||
const isPlainObject = chunk?.constructor === Object | ||
if (isPlainObject) { | ||
chunk = { ...chunk } | ||
} else { | ||
chunk = { type: 'write', data: chunk, position } | ||
} | ||
if (chunk.type === 'write') { | ||
if (!('data' in chunk)) { | ||
await send({ type: 'close' }) | ||
throw new DOMException(...SYNTAX('write requires a data argument')) | ||
} | ||
chunk.position ??= position | ||
if (typeof chunk.data === 'string') { | ||
chunk.data = textEncoder.encode(chunk.data) | ||
} | ||
else if (chunk.data instanceof ArrayBuffer) { | ||
chunk.data = new Uint8Array(chunk.data) | ||
} | ||
else if (!(chunk.data instanceof Uint8Array) && ArrayBuffer.isView(chunk.data)) { | ||
chunk.data = new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength) | ||
} | ||
else if (!(chunk.data instanceof Uint8Array)) { | ||
const ab = await new Response(chunk.data).arrayBuffer() | ||
chunk.data = new Uint8Array(ab) | ||
} | ||
if (Number.isInteger(chunk.position) && chunk.position >= 0) { | ||
position = chunk.position | ||
} | ||
position += chunk.data.byteLength | ||
size += chunk.data.byteLength | ||
} else if (chunk.type === 'seek') { | ||
if (Number.isInteger(chunk.position) && chunk.position >= 0) { | ||
if (size < chunk.position) { | ||
throw new DOMException(...INVALID) | ||
} | ||
console.log('seeking', chunk) | ||
position = chunk.position | ||
return // Don't need to enqueue seek... | ||
} else { | ||
await send({ type: 'close' }) | ||
throw new DOMException(...SYNTAX('seek requires a position argument')) | ||
} | ||
} else if (chunk.type === 'truncate') { | ||
if (Number.isInteger(chunk.size) && chunk.size >= 0) { | ||
size = chunk.size | ||
if (position > size) { position = size } | ||
} else { | ||
await send({ type: 'close' }) | ||
throw new DOMException(...SYNTAX('truncate requires a size argument')) | ||
} | ||
} | ||
await send(chunk) | ||
}, | ||
async close () { | ||
await send({ type: 'close' }) | ||
worker.terminate() | ||
}, | ||
async abort (reason) { | ||
await send({ type: 'abort', reason }) | ||
worker.terminate() | ||
}, | ||
}) | ||
return ws | ||
} | ||
const orig = FileSystemDirectoryHandle.prototype.getFileHandle | ||
FileSystemDirectoryHandle.prototype.getFileHandle = async function (...args) { | ||
const handle = await orig.call(this, ...args) | ||
wm.set(handle, this) | ||
return handle | ||
} | ||
} | ||
export default FileSystemFileHandle | ||
export { FileSystemFileHandle } |
const kAdapter = Symbol('adapter') | ||
/** | ||
* @typedef {Object} FileSystemHandlePermissionDescriptor | ||
* @property {('read'|'readwrite')} [mode='read'] | ||
*/ | ||
class FileSystemHandle { | ||
@@ -19,3 +23,5 @@ /** @type {FileSystemHandle} */ | ||
async queryPermission ({mode = 'read'} = {}) { | ||
/** @param {FileSystemHandlePermissionDescriptor} descriptor */ | ||
async queryPermission (descriptor = {}) { | ||
const { mode = 'read' } = descriptor | ||
const handle = this[kAdapter] | ||
@@ -83,3 +89,10 @@ | ||
// Safari safari doesn't support writable streams yet. | ||
if (globalThis.FileSystemHandle) { | ||
globalThis.FileSystemHandle.prototype.queryPermission ??= function (descriptor) { | ||
return 'granted' | ||
} | ||
} | ||
export default FileSystemHandle | ||
export { FileSystemHandle } |
@@ -6,5 +6,6 @@ import config from './config.js' | ||
class FileSystemWritableFileStream extends WritableStream { | ||
constructor (...args) { | ||
super(...args) | ||
#writer | ||
constructor (writer) { | ||
super(writer) | ||
this.#writer = writer | ||
// Stupid Safari hack to extend native classes | ||
@@ -18,3 +19,3 @@ // https://bugs.webkit.org/show_bug.cgi?id=226201 | ||
close () { | ||
async close () { | ||
this._closed = true | ||
@@ -38,2 +39,3 @@ const w = this.getWriter() | ||
// The write(data) method steps are: | ||
write (data) { | ||
@@ -44,6 +46,13 @@ if (this._closed) { | ||
// 1. Let writer be the result of getting a writer for this. | ||
const writer = this.getWriter() | ||
const p = writer.write(data) | ||
// 2. Let result be the result of writing a chunk to writer given data. | ||
const result = writer.write(data) | ||
// 3. Release writer. | ||
writer.releaseLock() | ||
return p | ||
// 4. Return result. | ||
return result | ||
} | ||
@@ -66,3 +75,12 @@ } | ||
// Safari safari doesn't support writable streams yet. | ||
if ( | ||
globalThis.FileSystemFileHandle && | ||
!globalThis.FileSystemFileHandle.prototype.createWritable && | ||
!globalThis.FileSystemWritableFileStream | ||
) { | ||
globalThis.FileSystemWritableFileStream = FileSystemWritableFileStream | ||
} | ||
export default FileSystemWritableFileStream | ||
export { FileSystemWritableFileStream } |
@@ -17,8 +17,6 @@ /** @typedef {import('./FileSystemDirectoryHandle.js').default} FileSystemDirectoryHandle */ | ||
input.type = 'file' | ||
input.webkitdirectory = true | ||
// Fallback to multiple files input for iOS Safari | ||
input.multiple = true | ||
// Even with this check, the browser may support the attribute, but not the functionality (e.g. iOS Safari) | ||
if (!('webkitdirectory' in input)) { | ||
throw new Error(`HTMLInputElement.webkitdirectory is not supported`) | ||
} | ||
// See https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only | ||
@@ -30,4 +28,2 @@ input.style.position = 'fixed' | ||
input.webkitdirectory = true | ||
// Lazy load while the user is choosing the directory | ||
@@ -34,0 +30,0 @@ const p = import('./util.js') |
@@ -33,5 +33,8 @@ /** @typedef {import('./FileSystemFileHandle.js').default} FileSystemFileHandle */ | ||
// See https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only | ||
input.style.position = 'fixed' | ||
input.style.top = '-100000px' | ||
input.style.left = '-100000px' | ||
Object.assign(input.style, { | ||
position: 'fixed', | ||
top: '-100000px', | ||
left: '-100000px' | ||
}) | ||
document.body.appendChild(input) | ||
@@ -43,5 +46,6 @@ | ||
await new Promise(resolve => { | ||
input.addEventListener('change', resolve) | ||
input.addEventListener('change', resolve, { once: true }) | ||
input.click() | ||
}) | ||
input.remove() | ||
@@ -48,0 +52,0 @@ return p.then(m => m.getFileHandlesFromInput(input)) |
@@ -8,3 +8,3 @@ /** @typedef {import('./FileSystemFileHandle.js').default} FileSystemFileHandle */ | ||
* @param {boolean} [options.excludeAcceptAllOption=false] Prevent user for selecting any | ||
* @param {Object[]} [options.accepts] Files you want to accept | ||
* @param {Object[]} [options.types] Files you want to accept | ||
* @param {string} [options.suggestedName] the name to fall back to when using polyfill | ||
@@ -11,0 +11,0 @@ * @param {string} [options._name] the name to fall back to when using polyfill |
@@ -46,2 +46,1 @@ export class Sink { | ||
export default _default; | ||
declare const File_1: typeof window.File; |
@@ -5,3 +5,3 @@ export default showSaveFilePicker; | ||
excludeAcceptAllOption?: boolean; | ||
accepts?: any[]; | ||
types?: any[]; | ||
suggestedName?: string; | ||
@@ -8,0 +8,0 @@ _name?: string; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
117814
45
2974