diff --git a/README.md b/README.md index d3b50da..fc3b16c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Native File System adapter (polyfill) +# Native File System adapter (ponyfill) > This is an in-browser file system that follows [native-file-system](https://wicg.github.io/native-file-system/) and supports storing and retrieving files from various backends. @@ -13,7 +13,7 @@ This polyfill/ponyfill ships with 5 filesystem backends: * `Cache storage`: Stores files in cache storage like a request/response a-like. The api is designed in such a way that it can work with or without the ponyfill if you choose to remove or add this.
-It's not trying to interfear with the changing spec by using other arguments/properties that may conflict with the feature changes to the spec. A few none spec options are prefixed with a `_` +It's not trying to interfear with the changing spec by using other properties that may conflict with the feature changes to the spec. A few none spec options are prefixed with a `_` ( The current minium supported browser I have choosen to support is the ones that can handle import/export )
( Some parts are lazy loaded when needed ) @@ -21,12 +21,23 @@ It's not trying to interfear with the changing spec by using other arguments/pro ### Using ```js -import { chooseFileSystemEntries, FileSystemDirectoryHandle } +import { showOpenFilePicker, getOriginPrivateDirectory } from 'https://cdn.jsdelivr.net/gh/jimmywarting/native-file-system-adapter/src/es6.js' +export { + showDirectoryPicker, + showOpenFilePicker, + showSaveFilePicker, + , + FileSystemDirectoryHandle, + FileSystemFileHandle, + FileSystemHandle, + FileSystemWritableFileStream +} + + // pick a file const fileHandle = await chooseFileSystemEntries({ - type: 'open-file', // default accepts: [ { extensions: ['jpg'] }, { extensions: ['webp'] }, @@ -40,40 +51,30 @@ const fileHandle = await chooseFileSystemEntries({ const file = await fileHandle.getFile() // store a file -const folderHandle = await FileSystemDirectoryHandle.getSystemDirectory({ - type: 'sandbox', - _driver: 'native', // native|sandbox|memory|indexeddb|cache - _persistent: true, // option for when using blink's sandboxed storage (default=temporary) -}) - -const fileHandle = await folderHandle.getFile(file.name, { create: true }) +const folderHandle = await getOriginPrivateDirectory() +const fileHandle = await folderHandle.getFileHandle(file.name, { create: true }) await fileHandle.write(file) // save/download a file -const fileHandle = await chooseFileSystemEntries({ - type: 'save-file' - accepts: [ - { extensions: ['jpg'] }, - { extensions: ['webp'] }, - { mimeTypes: ['image/png'] } - ], - excludeAcceptAllOption: true, +const fileHandle = await showSaveFilePicker({ _preferPolyfill: false, _name: 'Untitled.png', // the name being used when preferPolyfill is true or native is unavalible + types: {}, + excludeAcceptAllOption: false, }) const extensionChosen = fileHandle.name.split('.').pop() -const image = { +const blob = { jpg: generateCanvas({ type: 'blob', format: 'jpg' }), png: generateCanvas({ type: 'blob', format: 'png' }), webp: generateCanvas({ type: 'blob', format: 'webp' }) }[extensionChosen] -await image.stream().pipeTo(fileHandle.getWriter()) +await blob.stream().pipeTo(fileHandle.getWriter()) ``` -PS: storing a file handle in IndexedDB or sharing it with postMessage isn't currently possible. +PS: storing a file handle in IndexedDB or sharing it with postMessage isn't currently possible unless you use native. ### A note when downloading with the polyfilled version diff --git a/example/test.html b/example/test.html index 45f290e..779673c 100644 --- a/example/test.html +++ b/example/test.html @@ -63,32 +63,74 @@ Total +
+      getOriginPrivateDirectory(),
+      getOriginPrivateDirectory(import('./adapters/sandbox.js'))
+      getOriginPrivateDirectory(import('./adapters/memory.js'))
+      getOriginPrivateDirectory(import('./adapters/indexeddb.js'))
+      getOriginPrivateDirectory(import('./adapters/cache.js'))
+    
+
+
+
- + + - - + + + + + + + + + diff --git a/example/test.js b/example/test.js index 334d753..03a7528 100644 --- a/example/test.js +++ b/example/test.js @@ -1,7 +1,10 @@ // @ts-check import { WritableStream, ReadableStream as Readable } from 'https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.1.0/dist/ponyfill.es2018.mjs' import { - chooseFileSystemEntries, + showDirectoryPicker, + showOpenFilePicker, + showSaveFilePicker, + getOriginPrivateDirectory, FileSystemDirectoryHandle } from '../src/es6.js' import { fromDataTransfer } from '../src/util.js' @@ -11,8 +14,6 @@ const ReadableStream = globalThis.WritableStream ? globalThis.ReadableStream : Readable -globalThis.chooseFileSystemEntries = chooseFileSystemEntries - if (!Blob.prototype.text) { Blob.prototype.text = function () { return new Response(this).text() @@ -50,166 +51,160 @@ function tt (n, html) { tr.insertCell().appendChild(html()) } -t('getDirectory(create=false) rejects for non-existing directories', async (root) => { - err = await root.getDirectory('non-existing-dir').catch(a=>a) +t('getDirectoryHandle(create=false) rejects for non-existing directories', async (root) => { + err = await root.getDirectoryHandle('non-existing-dir').catch(a=>a) assert(err instanceof DOMException) assert(err.name === 'NotFoundError') }) -t('getDirectory(create=true) creates an empty directory', async (root) => { - handle = await root.getDirectory('non-existing-dir', { create: true }) - assert(handle.isFile === false) - assert(handle.isDirectory === true) +t('getDirectoryHandle(create=true) creates an empty directory', async (root) => { + handle = await root.getDirectoryHandle('non-existing-dir', { create: true }) + assert(handle.kind === 'directory') assert(handle.name === 'non-existing-dir') assert(await getDirectoryEntryCount(handle) === 0) arrayEqual(await getSortedDirectoryEntries(root), ['non-existing-dir/']) }) -t('getDirectory(create=false) returns existing directories', async (root) => { - existing_handle = await root.getDirectory('dir-with-contents', { create: true }) +t('getDirectoryHandle(create=false) returns existing directories', async (root) => { + existing_handle = await root.getDirectoryHandle('dir-with-contents', { create: true }) file_handle = await createEmptyFile('test-file', existing_handle) - handle = await root.getDirectory('dir-with-contents', { create: false }) - assert(handle.isFile === false) - assert(handle.isDirectory === true) + handle = await root.getDirectoryHandle('dir-with-contents', { create: false }) + assert(handle.kind === 'directory') assert(handle.name === 'dir-with-contents') arrayEqual(await getSortedDirectoryEntries(handle), ['test-file']) }) -t('getDirectory(create=true) returns existing directories without erasing', async (root) => { - existing_handle = await root.getDirectory('dir-with-contents', { create: true }) - file_handle = await existing_handle.getFile('test-file', { create: true }) - handle = await root.getDirectory('dir-with-contents', { create: true }) - assert(handle.isFile === false) - assert(handle.isDirectory === true) +t('getDirectoryHandle(create=true) returns existing directories without erasing', async (root) => { + existing_handle = await root.getDirectoryHandle('dir-with-contents', { create: true }) + file_handle = await existing_handle.getFileHandle('test-file', { create: true }) + handle = await root.getDirectoryHandle('dir-with-contents', { create: true }) + assert(handle.kind === 'directory') assert(handle.name === 'dir-with-contents') arrayEqual(await getSortedDirectoryEntries(handle), ['test-file']) }) -t('getDirectory() when a file already exists with the same name', async (root) => { +t('getDirectoryHandle() when a file already exists with the same name', async (root) => { await createEmptyFile('file-name', root) - err = await root.getDirectory('file-name').catch(e=>e) + err = await root.getDirectoryHandle('file-name').catch(e=>e) assert(err.name === 'TypeMismatchError') - err = await root.getDirectory('file-name', { create: false }).catch(e=>e) + err = await root.getDirectoryHandle('file-name', { create: false }).catch(e=>e) assert(err.name === 'TypeMismatchError') - err = await root.getDirectory('file-name', {create: true}).catch(e=>e) + err = await root.getDirectoryHandle('file-name', {create: true}).catch(e=>e) }) -t('getDirectory() with empty name', async (root) => { - err = await root.getDirectory('', { create: true }).catch(e=>e) +t('getDirectoryHandle() with empty name', async (root) => { + err = await root.getDirectoryHandle('', { create: true }).catch(e=>e) assert(err instanceof TypeError) - err = await root.getDirectory('', {create: false}).catch(e=>e) + err = await root.getDirectoryHandle('', {create: false}).catch(e=>e) assert(err instanceof TypeError) }) -t('getDirectory(create=true) with empty name', async (root) => { - err = await root.getDirectory('.').catch(e=>e) +t('getDirectoryHandle(create=true) with empty name', async (root) => { + err = await root.getDirectoryHandle('.').catch(e=>e) assert(err instanceof TypeError) - err = await root.getDirectory('.', { create: true }).catch(e=>e) + err = await root.getDirectoryHandle('.', { create: true }).catch(e=>e) assert(err instanceof TypeError) }) -t('getDirectory() with ".." name', async (root) => { +t('getDirectoryHandle() with ".." name', async (root) => { subdir = await createDirectory('subdir-name', root) - err = await subdir.getDirectory('..').catch(e=>e) + err = await subdir.getDirectoryHandle('..').catch(e=>e) assert(err instanceof TypeError) - err = await subdir.getDirectory('..', { create: true }).catch(e=>e) + err = await subdir.getDirectoryHandle('..', { create: true }).catch(e=>e) assert(err instanceof TypeError) }) -t('getDirectory(create=false) with a path separator when the directory exists', async (root) => { +t('getDirectoryHandle(create=false) with a path separator when the directory exists', async (root) => { const first_subdir_name = 'first-subdir-name' const first_subdir = await createDirectory(first_subdir_name, root) const second_subdir_name = 'second-subdir-name' await createDirectory(second_subdir_name, first_subdir) const path_with_separator = `${first_subdir_name}/${second_subdir_name}` - err = await root.getDirectory(path_with_separator).catch(e=>e) + err = await root.getDirectoryHandle(path_with_separator).catch(e=>e) assert(err instanceof TypeError) }) -t('getDirectory(create=true) with a path separator', async (root) => { +t('getDirectoryHandle(create=true) with a path separator', async (root) => { const subdir_name = 'subdir-name'; const subdir = await createDirectory(subdir_name, root); const path_with_separator = `${subdir_name}/file_name`; - err = await root.getDirectory(path_with_separator, { create: true }).catch(e=>e) + err = await root.getDirectoryHandle(path_with_separator, { create: true }).catch(e=>e) assert(err instanceof TypeError) }) -t('getFile(create=false) rejects for non-existing files', async (root) => { - err = await root.getFile('non-existing-file').catch(e=>e) +t('getFileHandle(create=false) rejects for non-existing files', async (root) => { + err = await root.getFileHandle('non-existing-file').catch(e=>e) assert(err.name === 'NotFoundError') }) -t('getFile(create=true) creates an empty file for non-existing files', async (root) => { - handle = await root.getFile('non-existing-file', { create: true }) - assert(handle.isFile === true) - assert(handle.isDirectory === false) +t('getFileHandle(create=true) creates an empty file for non-existing files', async (root) => { + handle = await root.getFileHandle('non-existing-file', { create: true }) + assert(handle.kind === 'file') assert(handle.name === 'non-existing-file') assert(await getFileSize(handle) === 0) assert(await getFileContents(handle) === '') }) -t('getFile(create=false) returns existing files', async (root) => { +t('getFileHandle(create=false) returns existing files', async (root) => { existing_handle = await createFileWithContents('existing-file', '1234567890', root) - handle = await root.getFile('existing-file') - assert(handle.isFile === true) - assert(handle.isDirectory === false) + handle = await root.getFileHandle('existing-file') + assert(handle.kind === 'file') assert(handle.name === 'existing-file') assert(await getFileSize(handle) === 10) assert(await getFileContents(handle) === '1234567890') }) -t('getFile(create=true) returns existing files without erasing', async (root) => { +t('getFileHandle(create=true) returns existing files without erasing', async (root) => { existing_handle = await createFileWithContents('file-with-contents', '1234567890', root) - handle = await root.getFile('file-with-contents', { create: true }) - assert(handle.isFile === true) - assert(handle.isDirectory === false) + handle = await root.getFileHandle('file-with-contents', { create: true }) + assert(handle.kind === 'file') assert(handle.name === 'file-with-contents') assert(await getFileSize(handle) === 10) assert(await getFileContents(handle) === '1234567890') }) -t('getFile(create=false) when a directory already exists with the same name', async (root) => { - await root.getDirectory('dir-name', { create: true }) - err = await root.getFile('dir-name').catch(e=>e) +t('getFileHandle(create=false) when a directory already exists with the same name', async (root) => { + await root.getDirectoryHandle('dir-name', { create: true }) + err = await root.getFileHandle('dir-name').catch(e=>e) assert(err.name === 'TypeMismatchError') }) -t('getFile(create=true) when a directory already exists with the same name', async (root) => { - await root.getDirectory('dir-name', { create: true }) - err = await root.getFile('dir-name', { create: true }).catch(e=>e) +t('getFileHandle(create=true) when a directory already exists with the same name', async (root) => { + await root.getDirectoryHandle('dir-name', { create: true }) + err = await root.getFileHandle('dir-name', { create: true }).catch(e=>e) assert(err.name === 'TypeMismatchError') }) -t('getFile() with empty name', async (root) => { - err = await root.getFile('', {create: true}).catch(e=>e) +t('getFileHandle() with empty name', async (root) => { + err = await root.getFileHandle('', {create: true}).catch(e=>e) assert(err instanceof TypeError) - err = await root.getFile('', {create: false}).catch(e=>e) + err = await root.getFileHandle('', {create: false}).catch(e=>e) assert(err instanceof TypeError) }) -t('getFile() with "." name', async (root) => { - err = await root.getFile('.').catch(e=>e) +t('getFileHandle() with "." name', async (root) => { + err = await root.getFileHandle('.').catch(e=>e) assert(err instanceof TypeError) - err = await root.getFile('.', { create: true }).catch(e=>e) + err = await root.getFileHandle('.', { create: true }).catch(e=>e) assert(err instanceof TypeError) }) -t('getFile() with ".." name', async (root) => { - err = await root.getFile('..').catch(e=>e) +t('getFileHandle() with ".." name', async (root) => { + err = await root.getFileHandle('..').catch(e=>e) assert(err instanceof TypeError) - err = await root.getFile('..', { create: true }).catch(e=>e) + err = await root.getFileHandle('..', { create: true }).catch(e=>e) assert(err instanceof TypeError) }) -t('getFile(create=false) with a path separator when the file exists.', async (root) => { +t('getFileHandle(create=false) with a path separator when the file exists.', async (root) => { await createDirectory('subdir-name', root) - err = await root.getFile(`subdir-name/file_name`).catch(e=>e) + err = await root.getFileHandle(`subdir-name/file_name`).catch(e=>e) assert(err instanceof TypeError) }) -t('getFile(create=true) with a path separator', async (root) => { +t('getFileHandle(create=true) with a path separator', async (root) => { await createDirectory('subdir-name', root) - err = await root.getFile(`subdir-name/file_name`, {create: true}).catch(e=>e) + err = await root.getFileHandle(`subdir-name/file_name`, {create: true}).catch(e=>e) assert(err instanceof TypeError) }) @@ -226,11 +221,12 @@ t('removeEntry() on an already removed file should fail', async (root) => { handle = await createFileWithContents('file-to-remove', '12345', root) await root.removeEntry('file-to-remove') err = await root.removeEntry('file-to-remove').catch(e=>e) + console.log(err) assert(err.name === 'NotFoundError') }) t('removeEntry() to remove an empty directory', async (root) => { - handle = await root.getDirectory('dir-to-remove', { create: true }) + handle = await root.getDirectoryHandle('dir-to-remove', { create: true }) await createFileWithContents('file-to-keep', 'abc', root) await root.removeEntry('dir-to-remove') arrayEqual(await getSortedDirectoryEntries(root), ['file-to-keep']) @@ -239,7 +235,7 @@ t('removeEntry() to remove an empty directory', async (root) => { }) t('removeEntry() on a non-empty directory should fail', async (root) => { - handle = await root.getDirectory('dir-to-remove', { create: true }) + handle = await root.getDirectoryHandle('dir-to-remove', { create: true }) await createEmptyFile('file-in-dir', handle) err = await root.removeEntry('dir-to-remove').catch(e=>e) assert(err.name === 'InvalidModificationError') @@ -546,9 +542,9 @@ t('write() with a string with unix line ending preserved', async (root) => { await wfs.close() assert(await getFileContents(handle) === 'foo\n') assert(await getFileSize(handle) === 4) -}, -async () => { - 'write() with a string with windows line ending preserved' +}), + +t('write() with a string with windows line ending preserved', async (root) => { handle = await createEmptyFile('string_with_windows_line_ending', root) wfs = await handle.createWritable() await wfs.write('foo\r\n') @@ -862,66 +858,69 @@ function img (format) { }) } -const wrapper = manualTest.querySelector('tbody tr') -$test.onclick = async () => { - const opts = { - type: $type.value, - multiple: $multiple.querySelector('input').checked, - accepts: eval($accept.querySelector('textarea').value), - excludeAcceptAllOption: $exclude.querySelector('input').checked, - _name: $name.querySelector('input').value, - _preferPolyfill: $preferPolyfill.querySelector('input').checked - } - const handle = await chooseFileSystemEntries(opts) - assert(Array.isArray(handle) === !!opts.multiple) - if (opts.multiple) handle = handle[0] - assert(handle.isFile === ['open-file', 'save-file'].includes(opts.type)) - assert(handle.isDirectory === (opts.type === 'open-directory')) - if (opts.type === 'save-file') { - const format = handle.name.split('.').pop() - const image = await img(format) - const ws = await handle.createWritable() - ws.write(image) - ws.close() +$types1.value = JSON.stringify([ + { + description: 'Text Files', + accept: { + 'text/plain': ['txt', 'text'], + 'text/html': ['html', 'htm'] + } + }, + { + description: 'Images', + accept: { + 'image/*': ['png', 'gif', 'jpeg', 'jpg'] + } } - - console.log('assert succeeded') +], null, 2) + +$types2.value = JSON.stringify([ + { + accept: { 'image/jpg': ['jpg'] } + }, + { + accept: { 'image/png': ['png'] } + }, + { + accept: { 'image/webp': ['webp'] } + }, +], null, 2) + +form_showDirectoryPicker.onsubmit = evt => { + evt.preventDefault() + const opts = Object.fromEntries([...new FormData(evt.target)]) + opts._preferPolyfill = !!opts._preferPolyfill + showDirectoryPicker(opts).then(console.log, console.error) } -$type.onchange = () => { - switch ($type.value) { - case 'save-file': - $accept.hidden = - $exclude.hidden = - $name.hidden = false - $multiple.hidden = true - return - case 'open-directory': - $exclude.hidden = - $multiple.hidden = - $accept.hidden = - $name.hidden = true - return - case 'open-file': - $exclude.hidden = - $multiple.hidden = - $accept.hidden = false - $name.hidden = true - } +form_showOpenFilePicker.onsubmit = evt => { + evt.preventDefault() + const opts = Object.fromEntries([...new FormData(evt.target)]) + JSON.parse(new FormData(evt.target).get('foo')) + opts.types = JSON.parse(opts.types || '""') + opts._preferPolyfill = !!opts._preferPolyfill + showOpenFilePicker(opts).then(console.log, console.error) +} +form_showSaveFilePicker.onsubmit = async evt => { + evt.preventDefault() + const opts = Object.fromEntries([...new FormData(evt.target)]) + opts.types = JSON.parse(opts.types || '""') + opts._preferPolyfill = !!opts._preferPolyfill + const handle = await showSaveFilePicker(opts) + const format = handle.name.split('.').pop() + const image = await img(format) + const ws = await handle.createWritable() + ws.write(image) + ws.close() } -$accept.querySelector('textarea').value = `[ - { extensions: ['jpg'] }, - { extensions: ['webp'] }, - { mimeTypes: ['image/png'] } -]` async function init () { const drivers = await Promise.allSettled([ - FileSystemDirectoryHandle.getSystemDirectory({ type: 'sandbox', _driver: 'native' }), - FileSystemDirectoryHandle.getSystemDirectory({ type: 'sandbox', _driver: 'sandbox', _persistent: false }), - FileSystemDirectoryHandle.getSystemDirectory({ type: 'sandbox', _driver: 'memory' }), - FileSystemDirectoryHandle.getSystemDirectory({ type: 'sandbox', _driver: 'indexeddb' }), - FileSystemDirectoryHandle.getSystemDirectory({ type: 'sandbox', _driver: 'cache' }) + getOriginPrivateDirectory(), + getOriginPrivateDirectory(import('../src/adapters/sandbox.js')), + getOriginPrivateDirectory(import('../src/adapters/memory.js')), + getOriginPrivateDirectory(import('../src/adapters/indexeddb.js')), + getOriginPrivateDirectory(import('../src/adapters/cache.js')) ]) let j = 0 for (let driver of drivers) { @@ -952,7 +951,7 @@ function arrayEqual(a1, a2) { async function cleanupSandboxedFileSystem (root) { for await (let entry of root.getEntries()) { - await root.removeEntry(entry.name, { recursive: entry.isDirectory }) + await root.removeEntry(entry.name, { recursive: entry.kind === 'directory' }) } } @@ -977,7 +976,7 @@ async function getDirectoryEntryCount (handle) { async function getSortedDirectoryEntries(handle) { let result = []; for await (let entry of handle.getEntries()) { - if (entry.isDirectory) + if (entry.kind === 'directory') result.push(entry.name + '/') else result.push(entry.name) @@ -987,11 +986,11 @@ async function getSortedDirectoryEntries(handle) { } async function createDirectory(name, parent) { - return parent.getDirectory(name, {create: true}) + return parent.getDirectoryHandle(name, {create: true}) } async function createEmptyFile(name, parent) { - const handle = await parent.getFile(name, { create: true }) + const handle = await parent.getFileHandle(name, { create: true }) // Make sure the file is empty. assert(await getFileSize(handle) === 0) return handle @@ -1016,7 +1015,7 @@ function streamFromFetch(data) { }) } -init() +init().catch(console.error) globalThis.ondragover = evt => evt.preventDefault() globalThis.ondrop = async evt => { diff --git a/package.json b/package.json new file mode 100644 index 0000000..c427945 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "native-file-system-adapter", + "version": "0.1.0", + "description": "Native File System API", + "main": "src/es6.js", + "directories": { + "example": "example" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jimmywarting/native-file-system-adapter.git" + }, + "author": "Jimmy Wärting", + "license": "MIT", + "bugs": { + "url": "https://github.com/jimmywarting/native-file-system-adapter/issues" + }, + "homepage": "https://github.com/jimmywarting/native-file-system-adapter#readme" +} diff --git a/src/FileSystemDirectoryHandle.js b/src/FileSystemDirectoryHandle.js index b66d4bc..f793727 100644 --- a/src/FileSystemDirectoryHandle.js +++ b/src/FileSystemDirectoryHandle.js @@ -1,5 +1,3 @@ -// @ts-check - import FileSystemHandle from './FileSystemHandle.js' import FileSystemFileHandle from './FileSystemFileHandle.js' @@ -18,15 +16,15 @@ class FileSystemDirectoryHandle extends FileSystemHandle { * @param {boolean} [options.create] create the directory if don't exist * @returns {Promise} */ - async getDirectory (name, options = {}) { + async getDirectoryHandle (name, options = {}) { if (name === '') throw new TypeError(`Name can't be an empty string.`) if (name === '.' || name === '..' || name.includes('/')) throw new TypeError(`Name contains invalid characters.`) - return new FileSystemDirectoryHandle(await wm.get(this).getDirectory(name, options)) + return new FileSystemDirectoryHandle(await wm.get(this).getDirectoryHandle(name, options)) } async * getEntries () { for await (let entry of wm.get(this).getEntries()) - yield entry.isFile ? new FileSystemFileHandle(entry) : new FileSystemDirectoryHandle(entry) + yield entry.kind === 'file' ? new FileSystemFileHandle(entry) : new FileSystemDirectoryHandle(entry) } /** @@ -35,10 +33,10 @@ class FileSystemDirectoryHandle extends FileSystemHandle { * @param {boolean} [options.create] create the file if don't exist * @returns {Promise} */ - async getFile (name, options) { + async getFileHandle (name, options) { if (name === '') throw new TypeError(`Name can't be an empty string.`) if (name === '.' || name === '..' || name.includes('/')) throw new TypeError(`Name contains invalid characters.`) - return new FileSystemFileHandle(await wm.get(this).getFile(name, options)) + return new FileSystemFileHandle(await wm.get(this).getFileHandle(name, options)) } /** @@ -50,55 +48,10 @@ class FileSystemDirectoryHandle extends FileSystemHandle { if (name === '.' || name === '..' || name.includes('/')) throw new TypeError(`Name contains invalid characters.`) return wm.get(this).removeEntry(name, options) } - - /** - * @param {object} options - * @param {('sandbox')} options.type - type of system to get - * @param {('indexeddb'|'memory'|'sandbox'|'native')} [options._driver] - type of system to get - * @return {Promise} - */ - static async getSystemDirectory (options) { - const err = `Failed to execute 'getSystemDirectory' on 'FileSystemDirectoryHandle': ` - const { _driver = 'native' } = options - - if (!arguments.length) { - throw new TypeError(err + '1 argument required, but only 0 present.') - } - if (typeof options !== 'object') { - throw new TypeError(err + `parameter 1 ('options') is not an object.`) - } - if (!options.hasOwnProperty('type')) { - throw new TypeError(err + 'required member type is undefined.') - } - - if (options._driver instanceof DataTransfer) { - const entries = [...options._driver.items].map(item => - item.webkitGetAsEntry() - ) - return import('./util.js').then(m => m.fromDataTransfer(entries)) - } - - if (options.type !== 'sandbox') { - throw new TypeError(err + `The provided value '${options.type}' is not a valid enum value of type SystemDirectoryType.`) - } - - if (_driver === 'native') { - return globalThis.FileSystemDirectoryHandle.getSystemDirectory(options) - } - - if (!['indexeddb', 'memory', 'sandbox', 'native', 'cache'].includes(_driver)) { - throw new TypeError('the adapter dont exist') - } - - let module = await import(`./adapters/${_driver}.js`) - const sandbox = await module.default(options) - return new FileSystemDirectoryHandle(sandbox) - } } -FileSystemDirectoryHandle.prototype.isFile = false +FileSystemDirectoryHandle.prototype.kind = '' FileSystemDirectoryHandle.prototype.name = '' -FileSystemDirectoryHandle.prototype.isDirectory = true Object.defineProperty(FileSystemDirectoryHandle.prototype, Symbol.toStringTag, { value: 'FileSystemDirectoryHandle', @@ -108,9 +61,9 @@ Object.defineProperty(FileSystemDirectoryHandle.prototype, Symbol.toStringTag, { }) Object.defineProperties(FileSystemDirectoryHandle.prototype, { - getDirectory: { enumerable: true }, + getDirectoryHandle: { enumerable: true }, getEntries: { enumerable: true }, - getFile: { enumerable: true }, + getFileHandle: { enumerable: true }, removeEntry: { enumerable: true } }) diff --git a/src/FileSystemHandle.js b/src/FileSystemHandle.js index 6c894f6..34dc57a 100644 --- a/src/FileSystemHandle.js +++ b/src/FileSystemHandle.js @@ -2,8 +2,9 @@ const wm = new WeakMap() class FileSystemHandle { constructor (meta) { - this.isDirectory = !meta.isFile - this.isFile = !!meta.isFile + /** @type {"file|directory"} */ + this.kind = meta.kind + /** @type {string} */ this.name = meta.name wm.set(this, meta) } diff --git a/src/FileSystemWritableFileStream.js b/src/FileSystemWritableFileStream.js index 07f5b5b..fd1f679 100644 --- a/src/FileSystemWritableFileStream.js +++ b/src/FileSystemWritableFileStream.js @@ -1,15 +1,19 @@ /* global globalThis */ -import { WritableStream } from 'https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.1.0/dist/ponyfill.es2018.mjs' +import {WritableStream as _WritableStream} from 'https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.1.0/dist/ponyfill.es2018.mjs' -const Writable = globalThis.WritableStream || WritableStream /** - * @lends WritableStream + * @alias {WritableStream} */ -class FileSystemWritableFileStream extends Writable { +var F = _WritableStream + + +class FileSystemWritableFileStream extends (globalThis.WritableStream || _WritableStream) { constructor (sink) { super(sink) + + /** @private */ this._closed = false } close () { @@ -20,9 +24,17 @@ class FileSystemWritableFileStream extends Writable { return p // return super.close ? super.close() : this.getWriter().close() } + + /** + * @param {number} position + */ seek (position) { return this.write({ type: 'seek', position }) } + + /** + * @param {number} size + */ truncate (size) { return this.write({ type: 'truncate', size }) } diff --git a/src/adapters/cache.js b/src/adapters/cache.js index d9d983c..1f2e268 100644 --- a/src/adapters/cache.js +++ b/src/adapters/cache.js @@ -1,46 +1,10 @@ import { errors } from '../util.js' -const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, SECURITY, DISALLOWED } = errors +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 (p) { -// this.p = p -// this.position = 0 -// } -// flush (ctrl) { -// ctrl.terminate() -// return this.p -// } -// async transform (chunk, ctrl) { -// if (typeof chunk === 'object') { -// if (chunk.type === 'write') { -// if (Number.isInteger(chunk.position) && chunk.position >= 0) { -// throw new Error('writing with position is not supported') -// } -// if (!('data' in chunk)) { -// throw new DOMException(`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. write requires a data argument`, 'SyntaxError') -// } -// chunk = chunk.data -// } else if (chunk.type === 'seek') { -// throw new Error('seeking is not supported') -// } else if (chunk.type === 'truncate') { -// throw new Error('truncate is not supported') -// } -// } - -// const reader = new Response(chunk).body.getReader() -// while (true) { -// const { value, done } = await reader.read() -// if (done) return -// this.position += value.length -// ctrl.enqueue(value) -// } -// } -// } - class Sink { constructor (cache, path, file) { this.cache = cache @@ -124,7 +88,7 @@ export class FileHandle { constructor (path, cache) { this.cache = cache this.path = path - this.isFile = true + this.kind = 'file' this.writable = true this.readable = true } @@ -154,19 +118,15 @@ export class FolderHandle { this.writable = true this.readable = true this.cache = cache - } - get isFile () { - return false - } - get name () { - return this.dir.split('/').pop() + this.kind = 'directory' + this.name = dir.split('/').pop() } async * getEntries () { for (let [path, isFile] of Object.entries(await this._tree)) { yield isFile ? new FileHandle(path, this.cache) : new FolderHandle(path, this.cache) } } - async getDirectory (name, opts = {}) { + async getDirectoryHandle (name, opts = {}) { const path = this.dir.endsWith('/') ? this.dir + name : `${this.dir}/${name}` const tree = await this._tree if (tree.hasOwnProperty(path)) { @@ -196,7 +156,7 @@ export class FolderHandle { _save (tree) { return this.cache.put(this.dir, new Response(JSON.stringify(tree), DIR)) } - async getFile (name, opts = {}) { + async getFileHandle (name, opts = {}) { const path = this.dir.endsWith('/') ? this.dir + name : `${this.dir}/${name}` const tree = await this._tree if (tree.hasOwnProperty(path)) { diff --git a/src/adapters/downloader.js b/src/adapters/downloader.js index 4456b62..ac6cd84 100644 --- a/src/adapters/downloader.js +++ b/src/adapters/downloader.js @@ -5,13 +5,14 @@ const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, SECURITY, DISALLOWED } = error export class FileHandle { constructor (name, file) { this.name = name - this.isFile = true + this.kind = 'file' } getFile () { throw new DOMException(...GONE) } async createWritable (opts) { const sw = await navigator.serviceWorker.getRegistration() + // @ts-ignore const useBlobFallback = !sw || /constructor/i.test(window.HTMLElement) || !!window.safari let sink @@ -56,6 +57,7 @@ export class FileHandle { } }) sink = ts.writable.getWriter() + // @ts-ignore sw.active.postMessage({ rs: ts.readable, url: sw.scope + name, @@ -75,7 +77,7 @@ export class FileHandle { sink = { async write (chunk) { const reader = new Response(chunk).body.getReader() - const pump = () => reader.read() + const pump = _ => reader.read() .then(res => res.done ? '' : pump(mc.port1.postMessage(res.value))) return pump() }, diff --git a/src/adapters/dropbox.js b/src/adapters/dropbox.js deleted file mode 100644 index b14df64..0000000 --- a/src/adapters/dropbox.js +++ /dev/null @@ -1 +0,0 @@ -Hi diff --git a/src/adapters/gdrive.js b/src/adapters/gdrive.js deleted file mode 100644 index b14df64..0000000 --- a/src/adapters/gdrive.js +++ /dev/null @@ -1 +0,0 @@ -Hi diff --git a/src/adapters/indexeddb.js b/src/adapters/indexeddb.js index 0086fce..e7a6726 100644 --- a/src/adapters/indexeddb.js +++ b/src/adapters/indexeddb.js @@ -1,6 +1,6 @@ import { errors } from '../util.js' -const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, SECURITY, DISALLOWED } = errors +const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors class Sink { constructor (db, id, size, file) { @@ -10,7 +10,7 @@ class Sink { this.position = 0 this.file = file } - write (chunk,c) { + write (chunk) { if (typeof chunk === 'object') { if (chunk.type === 'write') { if (Number.isInteger(chunk.position) && chunk.position >= 0) { @@ -92,7 +92,7 @@ class FileHandle { this.db = db this.id = id this.name = name - this.isFile = true + this.kind = 'file' this.readable = true this.writable = true } @@ -134,10 +134,10 @@ function rimraf(evt, toDelete, recursive = true) { class FolderHandle { constructor(db, id, name) { - this.id = id this.db = db + this.id = id + this.kind = 'directory' this.name = name - this.isFile = false this.readable = true this.writable = true } @@ -151,7 +151,7 @@ class FolderHandle { : new FolderHandle(this.db, id, name) } } - getDirectory (name, opts = {}) { + getDirectoryHandle (name, opts = {}) { return new Promise((rs, rj) => { const [ tx, table ] = store(this.db) table.get(this.id).onsuccess = evt => { @@ -172,7 +172,7 @@ class FolderHandle { } }) } - getFile (name, opts = {}) { + getFileHandle (name, opts = {}) { return new Promise((rs, rj) => { const [tx, table] = store(this.db) const query = table.get(this.id) @@ -198,7 +198,7 @@ class FolderHandle { } async removeEntry (name, opts) { return new Promise((rs, rj) => { - const [tx, table] = store(this.db, 1) + const [tx, table] = store(this.db) const cwdQ = table.get(this.id) cwdQ.onsuccess = (evt) => { const cwd = cwdQ.result @@ -212,7 +212,7 @@ class FolderHandle { } tx.oncomplete = rs tx.onerror = rj - tx.onabort = (e) => { + tx.onabort = () => { rj(new DOMException(...MOD_ERR)) } }) @@ -224,7 +224,7 @@ export default (opts = { persistent: false }) => new Promise((rs,rj) => { request.onupgradeneeded = evt => { const db = evt.target.result - const os = db.createObjectStore('entries', { autoIncrement: true }).transaction.oncomplete = evt => { + db.createObjectStore('entries', { autoIncrement: true }).transaction.oncomplete = evt => { db.transaction('entries', 'readwrite').objectStore('entries').add({}) } } diff --git a/src/adapters/memory.js b/src/adapters/memory.js index 4a867ee..59dfd99 100644 --- a/src/adapters/memory.js +++ b/src/adapters/memory.js @@ -94,20 +94,23 @@ export class FileHandle { constructor (name, file, writable = true) { this.file = file || new File([], name) this.name = name - this.isFile = true + this.kind = 'file' this.deleted = false this.writable = writable this.readable = true } + getFile () { if (this.deleted) throw new DOMException(...GONE) return this.file } + createWritable (opts) { if (!this.writable) throw new DOMException(...DISALLOWED) if (this.deleted) throw new DOMException(...GONE) return new Sink(this) } + destroy () { this.deleted = true this.file = null @@ -115,19 +118,22 @@ export class FileHandle { } export class FolderHandle { + constructor (name, writable = true) { this.name = name - this.isFile = false + this.kind = 'directory' this.deleted = false this.entries = {} this.writable = writable this.readable = true } + async * getEntries () { if (this.deleted) throw new DOMException(...GONE) yield* Object.values(this.entries) } - getDirectory (name, opts = {}) { + + getDirectoryHandle (name, opts = {}) { if (this.deleted) throw new DOMException(...GONE) const entry = this.entries[name] if (entry) { // entry exist @@ -144,7 +150,8 @@ export class FolderHandle { } } } - getFile (name, opts = {}) { + + getFileHandle (name, opts = {}) { const entry = this.entries[name] const isFile = entry instanceof FileHandle if (entry && isFile) return entry @@ -154,12 +161,14 @@ export class FolderHandle { return (this.entries[name] = new FileHandle(name)) } } + removeEntry (name, opts) { const entry = this.entries[name] if (!entry) throw new DOMException(...GONE) entry.destroy(opts.recursive) delete this.entries[name] } + destroy (recursive) { for (let x of Object.values(this.entries)) { if (!recursive) throw new DOMException(...MOD_ERR) diff --git a/src/adapters/node.js b/src/adapters/node.js new file mode 100644 index 0000000..59dfd99 --- /dev/null +++ b/src/adapters/node.js @@ -0,0 +1,184 @@ +import { errors } from '../util.js' + +const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, SECURITY, DISALLOWED } = errors + +export class Sink { + constructor (fileHandle) { + this.fileHandle = fileHandle + this.file = fileHandle.file + this.size = fileHandle.file.size + this.position = 0 + } + write (chunk) { + if (typeof chunk === 'object') { + if (chunk.type === 'write') { + if (Number.isInteger(chunk.position) && chunk.position >= 0) { + if (this.size < chunk.position) { + throw new DOMException(...INVALID) + } + 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 = chunk.size < this.size + ? file.slice(0, chunk.size) + : new File([file, new Uint8Array(chunk.size - this.size)], file.name) + + 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 + + // Maybe shouldn't do this: + // this.fileHandle.file = this.file + } + close () { + if (this.fileHandle.deleted) throw new DOMException(...GONE) + this.fileHandle.file = this.file + this.file = + this.position = + this.size = null + if (this.fileHandle.onclose) { + this.fileHandle.onclose(this.fileHandle) + } + } +} + +export class FileHandle { + constructor (name, file, writable = true) { + this.file = file || new File([], name) + this.name = name + this.kind = 'file' + this.deleted = false + this.writable = writable + this.readable = true + } + + getFile () { + if (this.deleted) throw new DOMException(...GONE) + return this.file + } + + createWritable (opts) { + if (!this.writable) throw new DOMException(...DISALLOWED) + if (this.deleted) throw new DOMException(...GONE) + return new Sink(this) + } + + destroy () { + this.deleted = true + this.file = null + } +} + +export class FolderHandle { + + constructor (name, writable = true) { + this.name = name + this.kind = 'directory' + this.deleted = false + this.entries = {} + this.writable = writable + this.readable = true + } + + async * getEntries () { + if (this.deleted) throw new DOMException(...GONE) + yield* Object.values(this.entries) + } + + getDirectoryHandle (name, opts = {}) { + if (this.deleted) throw new DOMException(...GONE) + const entry = this.entries[name] + if (entry) { // entry exist + if (entry instanceof FileHandle) { + throw new DOMException(...MISMATCH) + } else { + return entry + } + } else { + if (opts.create) { + return (this.entries[name] = new FolderHandle(name)) + } else { + throw new DOMException(...GONE) + } + } + } + + getFileHandle (name, opts = {}) { + const entry = this.entries[name] + const isFile = entry instanceof FileHandle + if (entry && isFile) return entry + if (entry && !isFile) throw new DOMException(...MISMATCH) + if (!entry && !opts.create) throw new DOMException(...GONE) + if (!entry && opts.create) { + return (this.entries[name] = new FileHandle(name)) + } + } + + removeEntry (name, opts) { + const entry = this.entries[name] + if (!entry) throw new DOMException(...GONE) + entry.destroy(opts.recursive) + delete this.entries[name] + } + + destroy (recursive) { + for (let x of Object.values(this.entries)) { + if (!recursive) throw new DOMException(...MOD_ERR) + x.destroy(recursive) + } + this.entries = {} + this.deleted = true + } +} + +const fs = new FolderHandle('') + +export default opts => fs diff --git a/src/adapters/sandbox.js b/src/adapters/sandbox.js index f515010..de5148f 100644 --- a/src/adapters/sandbox.js +++ b/src/adapters/sandbox.js @@ -1,6 +1,6 @@ import { errors } from '../util.js' -const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX, SECURITY, DISALLOWED } = errors +const { DISALLOWED } = errors class Sink { constructor(writer, someklass) { @@ -57,7 +57,7 @@ class Sink { export class FileHandle { constructor (file, writable = true) { this.file = file - this.isFile = true + this.kind = 'file' this.writable = writable this.readable = true } @@ -88,12 +88,8 @@ export class FolderHandle { this.dir = dir this.writable = writable this.readable = true - } - get isFile () { - return false - } - get name () { - return this.dir.name + this.kind = 'directory' + this.name = dir.name } async * getEntries () { const entries = await new Promise((rs, rj) => this.dir.createReader().readEntries(rs, rj)) @@ -101,45 +97,41 @@ export class FolderHandle { yield x.isFile ? new FileHandle(x, this.writable) : new FolderHandle(x, this.writable) } } - getDirectory (name, opts = {}) { + getDirectoryHandle (name, opts = {}) { return new Promise((rs, rj) => { this.dir.getDirectory(name, opts, dir => { rs(new FolderHandle(dir)) }, rj) }) } - getFile (name, opts = {}) { + getFileHandle (name, opts = {}) { return new Promise((rs, rj) => this.dir.getFile(name, opts, file => rs(new FileHandle(file)), rj)) } removeEntry (name, opts) { return new Promise(async (rs, rj) => { - const entry = await this.getDirectory(name).catch(err => - err.name === 'TypeMismatchError' ? this.getFile(name) : err + const entry = await this.getDirectoryHandle(name).catch(err => + err.name === 'TypeMismatchError' ? this.getFileHandle(name) : err ) - if (entry.isFile === false) { + if (entry instanceof Error) { + rj(entry) + } + + if (entry.kind === 'directory') { opts.recursive ? entry.dir.removeRecursively(rs, rj) : entry.dir.remove(rs, rj) - } else if (entry.isFile) { + } else if (entry.file) { entry.file.remove(rs, rj) - } else { - rj(entry) } }) } } -export default (opts = {}) => new Promise((rs,rj) => +export default (opts = {}) => new Promise((rs, rj) => globalThis.webkitRequestFileSystem( !!opts._persistent, 0, - e => rs(new FolderHandle(e.root), console.log(opts)), + e => rs(new FolderHandle(e.root)), rj ) ) - -// Used for creating a new FileList in a round-about way -function FileListItem(files) { - for (var b = new ClipboardEvent("").clipboardData || new DataTransfer; c--;) b.items.add(a[c]) - return b.files -} diff --git a/src/es6.js b/src/es6.js index 609c78d..16140d9 100644 --- a/src/es6.js +++ b/src/es6.js @@ -1,11 +1,17 @@ -import chooseFileSystemEntries from './filesystem.js' +import showDirectoryPicker from './showDirectoryPicker.js' +import showOpenFilePicker from './showOpenFilePicker.js' +import showSaveFilePicker from './showSaveFilePicker.js' +import getOriginPrivateDirectory from './getOriginPrivateDirectory.js' import FileSystemDirectoryHandle from './FileSystemDirectoryHandle.js' import FileSystemFileHandle from './FileSystemFileHandle.js' import FileSystemHandle from './FileSystemHandle.js' import FileSystemWritableFileStream from './FileSystemWritableFileStream.js' export { - chooseFileSystemEntries, + showDirectoryPicker, + showOpenFilePicker, + showSaveFilePicker, + getOriginPrivateDirectory, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, diff --git a/src/filesystem.js b/src/filesystem.js deleted file mode 100644 index 6cef455..0000000 --- a/src/filesystem.js +++ /dev/null @@ -1,57 +0,0 @@ -const ChooseFileSystemEntriesType = [ - 'open-file', - 'save-file', - 'open-directory' -] - -const def = { - type: 'openFile', - accepts: [] -} -const native = globalThis.chooseFileSystemEntries - -/** - * @param {Object} [options] - * @param {('openFile'|'saveFile'|'openDirectory')} [options.type] type of operation to make - * @param {boolean} [options.multiple] If you want to allow more than one file - * @param {boolean} [options.excludeAcceptAllOption=false] Prevent user for selecting any - * @param {Object[]} [options.accepts] Files you want to accept - * @param {string} [options._name] the name to fall back to when using polyfill - * @param {boolean} [options._preferPolyfill] If you rather want to use the polyfill instead of the native - * @returns Promise - */ -async function chooseFileSystemEntries (options = {}) { - const opts = { ...def, ...options } - if (native && opts._preferPolyfill !== true) { - return native(opts) - } - if (!ChooseFileSystemEntriesType.includes(opts.type)) { - throw new TypeError(`The provided value '${ - opts.type - }' is not a valid enum value of type ChooseFileSystemEntriesType.`) - } - - if (opts.type === 'save-file') { - const FileSystemFileHandle = await import('./FileSystemFileHandle.js').then(d => d.default) - const { FileHandle } = await import('./adapters/downloader.js') - return new FileSystemFileHandle(new FileHandle(opts._name)) - } - - const input = document.createElement('input') - input.type = 'file' - input.multiple = opts.multiple - input.webkitdirectory = opts.type === 'open-directory' - // input.accepts = opts.accepts[0].extensions - input.accept = opts.accepts.map(e => [...(e.extensions || []).map(e=>'.'+e), ...e.mimeTypes || []]).flat().join(',') - - return new Promise(rs => { - const p = import('./util.js').then(m => m.fromInput) - // Detecting cancel btn is hard :[ - // there exist some browser hacks but they are vary diffrent, - // hacky or no good. - input.onchange = () => rs(p.then(fn => fn(input))) - input.click() - }) -} - -export default chooseFileSystemEntries diff --git a/src/getOriginPrivateDirectory.js b/src/getOriginPrivateDirectory.js new file mode 100644 index 0000000..ae7d614 --- /dev/null +++ b/src/getOriginPrivateDirectory.js @@ -0,0 +1,21 @@ +import FileSystemDirectoryHandle from './FileSystemDirectoryHandle.js' +/** + * @param {object=} driver + * @return {Promise} + */ +async function getOriginPrivateDirectory (driver, options = {}) { + if (driver instanceof DataTransfer) { + const entries = [driver.items].map(item => + item.webkitGetAsEntry() + ) + return import('./util.js').then(m => m.fromDataTransfer(entries)) + } + if (!driver) { + return globalThis.getOriginPrivateDirectory() + } + let module = await driver + const sandbox = module.default ? await module.default(options) : module(options) + return new FileSystemDirectoryHandle(sandbox) +} + +export default getOriginPrivateDirectory diff --git a/src/showDirectoryPicker.js b/src/showDirectoryPicker.js new file mode 100644 index 0000000..db4c0c1 --- /dev/null +++ b/src/showDirectoryPicker.js @@ -0,0 +1,27 @@ +const native = globalThis.showDirectoryPicker + +/** + * @param {Object} [options] + * @param {boolean} [options.multiple] If you want to allow more than one file + * @param {string} [options._name] the name to fall back to when using polyfill + * @param {boolean} [options._preferPolyfill] If you rather want to use the polyfill instead of the native + * @returns Promise + */ +async function showDirectoryPicker (options = {}) { + if (native && !options._preferPolyfill) { + return native(options) + } + + const input = document.createElement('input') + input.type = 'file' + input.multiple = !!options.multiple + input.webkitdirectory = true + + return new Promise(rs => { + const p = import('./util.js').then(m => m.fromInput) + input.onchange = () => rs(p.then(fn => fn(input))) + input.click() + }) +} + +export default showDirectoryPicker diff --git a/src/showOpenFilePicker.js b/src/showOpenFilePicker.js new file mode 100644 index 0000000..08016d2 --- /dev/null +++ b/src/showOpenFilePicker.js @@ -0,0 +1,33 @@ +const def = { + accepts: [] +} +const native = globalThis.showOpenFilePicker + +/** + * @param {Object} [options] + * @param {boolean} [options.multiple] If you want to allow more than one file + * @param {boolean} [options.excludeAcceptAllOption=false] Prevent user for selecting any + * @param {Object[]} [options.accepts] Files you want to accept + * @param {boolean} [options._preferPolyfill] If you rather want to use the polyfill instead of the native + * @returns Promise + */ +async function showOpenFilePicker (options = {}) { + const opts = { ...def, ...options } + + if (native && !options._preferPolyfill) { + return native(opts) + } + + const input = document.createElement('input') + input.type = 'file' + input.multiple = opts.multiple + input.accept = opts.accepts.map(e => [...(e.extensions || []).map(e=>'.'+e), ...e.mimeTypes || []]).flat().join(',') + + return new Promise(rs => { + const p = import('./util.js').then(m => m.fromInput) + input.onchange = () => rs(p.then(fn => fn(input))) + input.click() + }) +} + +export default showOpenFilePicker diff --git a/src/showSaveFilePicker.js b/src/showSaveFilePicker.js new file mode 100644 index 0000000..7d1d10e --- /dev/null +++ b/src/showSaveFilePicker.js @@ -0,0 +1,23 @@ +const native = globalThis.showSaveFilePicker +const def = { + accepts: [] +} +/** + * @param {Object} [options] + * @param {boolean} [options.excludeAcceptAllOption=false] Prevent user for selecting any + * @param {Object[]} [options.accepts] Files you want to accept + * @param {string} [options._name] the name to fall back to when using polyfill + * @param {boolean} [options._preferPolyfill] If you rather want to use the polyfill instead of the native + * @returns Promise + */ +async function showSaveFilePicker (options = {}) { + if (native && !options._preferPolyfill) { + return native(options) + } + + const FileSystemFileHandle = await import('./FileSystemFileHandle.js').then(d => d.default) + const { FileHandle } = await import('./adapters/downloader.js') + return new FileSystemFileHandle(new FileHandle(options._name)) +} + +export default showSaveFilePicker diff --git a/src/util.js b/src/util.js index 59194c3..60f6666 100644 --- a/src/util.js +++ b/src/util.js @@ -1,6 +1,6 @@ export const errors = { - INVALID: ['seeking position failed', 'InvalidStateError'], - GONE: ['A requested file or directory could not be found at the time an operation was processed', 'NotFoundError'], + INVALID: ['seeking position failed.', 'InvalidStateError'], + GONE: ['A requested file or directory could not be found at the time an operation was processed.', 'NotFoundError'], MISMATCH: ['The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError'], MOD_ERR: ['The object can not be modified in this way.', 'InvalidModificationError'], SYNTAX: m => [`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${m}`, 'SyntaxError'], diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7c1ff34 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "noEmit": false, + "target": "esnext", + "module": "esnext", + "declaration": true, + "allowJs": true, + "checkJs": true, + "removeComments": true, + "emitDeclarationOnly": true, + "lib": ["es2019", "dom"], + "outDir": "./types/", + }, + "include": ["src/*.js"], +} diff --git a/types/FileSystemDirectoryHandle.d.ts b/types/FileSystemDirectoryHandle.d.ts new file mode 100644 index 0000000..4cf172e --- /dev/null +++ b/types/FileSystemDirectoryHandle.d.ts @@ -0,0 +1,14 @@ +export default FileSystemDirectoryHandle; +declare class FileSystemDirectoryHandle extends FileSystemHandle { + constructor(meta: any); + getDirectoryHandle(name: string, options?: { + create: boolean; + }): Promise; + getEntries(): AsyncGenerator; + getFileHandle(name: string, options?: { + create: boolean; + }): Promise; + removeEntry(name: string, options?: object): Promise; +} +import FileSystemHandle from "./FileSystemHandle.js"; +import FileSystemFileHandle from "./FileSystemFileHandle.js"; diff --git a/types/FileSystemFileHandle.d.ts b/types/FileSystemFileHandle.d.ts new file mode 100644 index 0000000..7f6c49b --- /dev/null +++ b/types/FileSystemFileHandle.d.ts @@ -0,0 +1,8 @@ +export default FileSystemFileHandle; +declare class FileSystemFileHandle extends FileSystemHandle { + constructor(meta: any); + createWritable(options?: any): Promise; + getFile(): Promise; +} +import FileSystemHandle from "./FileSystemHandle.js"; +import FileSystemWritableFileStream from "./FileSystemWritableFileStream.js"; diff --git a/types/FileSystemHandle.d.ts b/types/FileSystemHandle.d.ts new file mode 100644 index 0000000..c818e08 --- /dev/null +++ b/types/FileSystemHandle.d.ts @@ -0,0 +1,8 @@ +export default FileSystemHandle; +declare class FileSystemHandle { + constructor(meta: any); + kind: "file|directory"; + name: string; + queryPermission(options?: {}): Promise<"denied" | "granted">; + requestPermission(options?: {}): Promise<"denied" | "granted">; +} diff --git a/types/FileSystemWritableFileStream.d.ts b/types/FileSystemWritableFileStream.d.ts new file mode 100644 index 0000000..97ce148 --- /dev/null +++ b/types/FileSystemWritableFileStream.d.ts @@ -0,0 +1,11 @@ +export default FileSystemWritableFileStream; +declare const FileSystemWritableFileStream_base: any; +declare class FileSystemWritableFileStream extends FileSystemWritableFileStream_base { + [x: string]: any; + constructor(sink: any); + private _closed; + close(): any; + seek(position: number): any; + truncate(size: number): any; + write(data: any): any; +} diff --git a/types/adapters/downloader.d.ts b/types/adapters/downloader.d.ts new file mode 100644 index 0000000..0de9d1d --- /dev/null +++ b/types/adapters/downloader.d.ts @@ -0,0 +1,10 @@ +export class FileHandle { + constructor(name: any, file: any); + name: any; + kind: string; + getFile(): void; + createWritable(opts: any): Promise | { + write(chunk: any): void; + close(something: any): void; + }>; +} diff --git a/types/adapters/memory.d.ts b/types/adapters/memory.d.ts new file mode 100644 index 0000000..be358e8 --- /dev/null +++ b/types/adapters/memory.d.ts @@ -0,0 +1,37 @@ +export class Sink { + constructor(fileHandle: any); + fileHandle: any; + file: any; + size: any; + position: number; + write(chunk: any): void; + close(): void; +} +export class FileHandle { + constructor(name: any, file: any, writable?: boolean); + file: any; + name: any; + kind: string; + deleted: boolean; + writable: boolean; + readable: boolean; + getFile(): any; + createWritable(opts: any): Sink; + destroy(): void; +} +export class FolderHandle { + constructor(name: any, writable?: boolean); + name: any; + kind: string; + deleted: boolean; + entries: {}; + writable: boolean; + readable: boolean; + getEntries(): AsyncGenerator; + getDirectoryHandle(name: any, opts?: {}): any; + getFileHandle(name: any, opts?: {}): any; + removeEntry(name: any, opts: any): void; + destroy(recursive: any): void; +} +declare function _default(opts: any): FolderHandle; +export default _default; diff --git a/types/adapters/sandbox.d.ts b/types/adapters/sandbox.d.ts new file mode 100644 index 0000000..6915631 --- /dev/null +++ b/types/adapters/sandbox.d.ts @@ -0,0 +1,24 @@ +export class FileHandle { + constructor(file: any, writable?: boolean); + file: any; + kind: string; + writable: boolean; + readable: boolean; + get name(): any; + getFile(): Promise; + createWritable(opts: any): Promise; +} +export class FolderHandle { + constructor(dir: any, writable?: boolean); + dir: any; + writable: boolean; + readable: boolean; + kind: string; + name: any; + getEntries(): AsyncGenerator; + getDirectoryHandle(name: any, opts?: {}): Promise; + getFileHandle(name: any, opts?: {}): Promise; + removeEntry(name: any, opts: any): Promise; +} +declare function _default(opts?: {}): Promise; +export default _default; diff --git a/types/es6.d.ts b/types/es6.d.ts new file mode 100644 index 0000000..34c83e6 --- /dev/null +++ b/types/es6.d.ts @@ -0,0 +1,9 @@ +import showDirectoryPicker from "./showDirectoryPicker.js"; +import showOpenFilePicker from "./showOpenFilePicker.js"; +import showSaveFilePicker from "./showSaveFilePicker.js"; +import getOriginPrivateDirectory from "./getOriginPrivateDirectory.js"; +import FileSystemDirectoryHandle from "./FileSystemDirectoryHandle.js"; +import FileSystemFileHandle from "./FileSystemFileHandle.js"; +import FileSystemHandle from "./FileSystemHandle.js"; +import FileSystemWritableFileStream from "./FileSystemWritableFileStream.js"; +export { showDirectoryPicker, showOpenFilePicker, showSaveFilePicker, getOriginPrivateDirectory, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, FileSystemWritableFileStream }; diff --git a/types/getOriginPrivateDirectory.d.ts b/types/getOriginPrivateDirectory.d.ts new file mode 100644 index 0000000..a4894b3 --- /dev/null +++ b/types/getOriginPrivateDirectory.d.ts @@ -0,0 +1,3 @@ +export default getOriginPrivateDirectory; +declare function getOriginPrivateDirectory(driver?: object | undefined, options?: {}): Promise; +import FileSystemDirectoryHandle from "./FileSystemDirectoryHandle.js"; diff --git a/types/showDirectoryPicker.d.ts b/types/showDirectoryPicker.d.ts new file mode 100644 index 0000000..f0712f3 --- /dev/null +++ b/types/showDirectoryPicker.d.ts @@ -0,0 +1,6 @@ +export default showDirectoryPicker; +declare function showDirectoryPicker(options?: { + multiple: boolean; + _name: string; + _preferPolyfill: boolean; +}): Promise; diff --git a/types/showOpenFilePicker.d.ts b/types/showOpenFilePicker.d.ts new file mode 100644 index 0000000..f95a247 --- /dev/null +++ b/types/showOpenFilePicker.d.ts @@ -0,0 +1,7 @@ +export default showOpenFilePicker; +declare function showOpenFilePicker(options?: { + multiple: boolean; + excludeAcceptAllOption: boolean; + accepts: any[]; + _preferPolyfill: boolean; +}): Promise; diff --git a/types/showSaveFilePicker.d.ts b/types/showSaveFilePicker.d.ts new file mode 100644 index 0000000..6659056 --- /dev/null +++ b/types/showSaveFilePicker.d.ts @@ -0,0 +1,7 @@ +export default showSaveFilePicker; +declare function showSaveFilePicker(options?: { + excludeAcceptAllOption: boolean; + accepts: any[]; + _name: string; + _preferPolyfill: boolean; +}): Promise; diff --git a/types/util.d.ts b/types/util.d.ts new file mode 100644 index 0000000..f1eee38 --- /dev/null +++ b/types/util.d.ts @@ -0,0 +1,11 @@ +export function fromDataTransfer(entries: any): Promise; +export function fromInput(input: any): Promise; +export namespace errors { + export const INVALID: string[]; + export const GONE: string[]; + export const MISMATCH: string[]; + export const MOD_ERR: string[]; + export function SYNTAX(m: any): string[]; + export const SECURITY: string[]; + export const DISALLOWED: string[]; +}
Manual save & open file(s)/directory
ManualManual Testing
-chooseFileSystemEntries( - type: - accepts: - excludeAcceptAllOption: - _name: - _preferPolyfill: -) + + showDirectoryPicker({ +
+   _preferPolyfill:
+
+ })
+ showOpenFilePicker({
+
+   _preferPolyfill: +
+
+   multiple: +
+
+   excludeAcceptAllOption: +
+
+   types:
+
+ }) +
+ showSaveFilePicker({
+
+   _preferPolyfill: +
+
+   _name: +
+
+   excludeAcceptAllOption: +
+
+   types:
+
+ }) +
@@ -98,11 +140,8 @@ and use the sandbox adapter on it.
elm.ondragover = evt => evt.preventDefault()
 elm.ondrop = evt => {
-  evt => evt.preventDefault()
-  FileSystemDirectoryHandle.getSystemDirectory({
-    type: 'sandbox',
-    _driver: evt.dataTransfer
-  })
+  evt.preventDefault()
+  getOriginPrivateDirectory(evt.dataTransfer)
 }
Drop anywhere
on page