File System Access API implementation (ponyfill) with pluggable storage adapters via IndexedDB, Cache API, in-memory etc.
npm install file-system-accessshowDirectoryPicker, showOpenFilePicker and showSaveFilePicker, with fallbacks to regular input elements.
FileSystemFileHandle and FileSystemDirectoryHandle interfaces.
FileSystemWritableFileStream to truncate and write data.
navigator.storage.getDirectory() (getOriginPrivateDirectory) which can read & write data to and from several sources called adapters, not just the browser sandboxed file system
DataTransferItem.prototype.getAsFileSystemHandle()
getOriginPrivateDirectory is called with no arguments, the browser's native sandboxed file system is used, just like calling navigator.storage.getDirectory().
node: Uses NodeJS's fs module
deno: Interact with filesystem using Deno
sandbox (deprecated): Uses requestFileSystem. Only supported in Chromium-based browsers using the Blink engine.
indexeddb: Stores files into the browser's IndexedDB object database.
memory: Stores files in-memory. Thus, it is a temporary file store that clears when the user navigates away.
cache: Stores files with the browser's Cache API in request/response pairs.
html
`
$3
Works in Node.JS v14.8+ or in the browser, with a module bundler such as Webpack.
`
npm i file-system-access
`
`js
import { getOriginPrivateDirectory } from 'file-system-access'
import indexedDbAdapter from 'file-system-access/lib/adapters/indexeddb.js'
import nodeAdapter from 'file-system-access/lib/adapters/node.js'
const dirHandle = await getOriginPrivateDirectory(indexedDbAdapter)
const nodeDirHandle = await getOriginPrivateDirectory(nodeAdapter, './real-dir')
`
Examples
$3
You can get a directory handle to a sandboxed virtual file system using the getOriginPrivateDirectory function.
This is a legacy name introduced by an older Native File System specification and is kept for simplicity.
It is equivalent to the navigator.storage.getDirectory() method introduced by the later File System Access spec.
`js
import { getOriginPrivateDirectory, support } from 'file-system-access'
// Uses only native implementation - same as calling navigator.storage.getDirectory()
if (support.adapter.native) {
handle = await getOriginPrivateDirectory()
}
// Blinks old sandboxed api
if (support.adapter.sandbox) {
handle = await getOriginPrivateDirectory(import('file-system-access/lib/adapters/sandbox.js'))
}
// fast in-memory file system
handle = await getOriginPrivateDirectory(import('file-system-access/lib/adapters/memory.js'))
// Using indexDB
handle = await getOriginPrivateDirectory(import('file-system-access/lib/adapters/indexeddb.js'))
// Store things in the new Cache API as request/responses (bad at mutating data)
if (support.adapter.cache) {
handle = await getOriginPrivateDirectory(import('file-system-access/lib/adapters/cache.js'))
}
// Node only variant:
handle = await getOriginPrivateDirectory(import('file-system-access/lib/adapters/memory.js'))
handle = await getOriginPrivateDirectory(import('file-system-access/lib/adapters/node.js'), './starting-path')
// Deno only variant:
handle = await getOriginPrivateDirectory(import('file-system-access/src/adapters/memory.js'))
handle = await getOriginPrivateDirectory(import('file-system-access/src/adapters/deno.js'), './starting-path')
`
$3
`js
import { showDirectoryPicker, showOpenFilePicker } from 'file-system-access'
// The polyfilled (file input) version will turn into a memory adapter
// You will have read & write permission on the memory adapter,
// you might want to transfer (copy) the handle to another adapter
const [fileHandle] = await showOpenFilePicker({_preferPolyfill: boolean, ...sameOpts})
const dirHandle = await showDirectoryPicker({_preferPolyfill: boolean, ...sameOpts})
`
$3
`js
// Apply polyfill for DataTransferItem.prototype.getAsFileSystemHandle()
import { polyfillDataTransferItem } from 'file-system-access'
await polyfillDataTransferItem();
// or just use a static import
import 'file-system-access/lib/polyfillDataTransferItem.js'
window.ondrop = async evt => {
evt.preventDefault()
for (let item of evt.dataTransfer.items) {
const handle = await item.getAsFileSystemHandle()
console.log(handle)
}
}
`
$3
`js
import { showOpenFilePicker, getOriginPrivateDirectory } from 'file-system-access'
// request user to select a file
const [fileHandle] = await showOpenFilePicker({
types: [], // default
multiple: false, // default
excludeAcceptAllOption: false, // default
_preferPolyfill: false // default
})
// returns a File Instance
const file = await fileHandle.getFile()
// copy the file over to a another place
const rootHandle = await getOriginPrivateDirectory()
const fileHandle = await rootHandle.getFileHandle(file.name, { create: true })
const writable = await fileHandle.createWritable()
await writable.write(file)
await writable.close()
`
$3
`js
import { showSaveFilePicker } from 'file-system-access'
const fileHandle = await showSaveFilePicker({
_preferPolyfill: false,
suggestedName: 'Untitled.png',
types: [
{ accept: { "image/png": [ ".png" ] } },
{ accept: { "image/jpg": [ ".jpg" ] } },
{ accept: { "image/webp": [ ".webp" ] } }
],
excludeAcceptAllOption: false // default
})
// Look at what extension they have chosen
const extensionChosen = fileHandle.name.split('.').pop()
const blob = {
jpg: generateCanvasBlob({ type: 'blob', format: 'jpg' }),
png: generateCanvasBlob({ type: 'blob', format: 'png' }),
webp: generateCanvasBlob({ type: 'blob', format: 'webp' })
}[extensionChosen]
await blob.stream().pipeTo(fileHandle.createWritable())
// or
var writer = fileHandle.getWritable()
await writer.write(blob)
await writer.close()
`
Supported browsers
When importing as an ES module, browsers that support dynamic imports and ES2018 features are a minimum requirement. When using a bundler, this restriction is no longer applicable.
When the directory picker falls back to input elements, the browser must support webkitdirectory and webkitRelativePath. Because of this, support for picking directories is generally poor on Mobile browsers.
For drag and drop, the getAsFileSystemHandle() polyfill depends on the File and Directory Entries API support, more specifically FileSystemDirectoryEntry, FileSystemFileEntry and webkitGetAsEntry.
Limitations
- Storing a file handle in IndexedDB or sharing it with postMessage isn't currently possible unless you use native.
- showDirectoryPicker and showOpenFilePicker will not throw any AbortErrors (e.g. user cancellations) when using a fallback input element
- showDirectoryPicker will return a flat hierarchy when a fallback input element is used and webkitRelativePath is not supported (e.g. mobile Safari). This can be detected by checking if the name attribute of the root directory handle is an empty string.
- showSaveFilePicker may not actually show any prompt when using a fallback input (e.g. on Chrome the file is auto-saved to the browser's preferred download folder)
- Cache adapter only works in secure (HTTPS) contexts
- IndexedDB adapter may not work in some browsers in Private mode
- Mobile Safari scrambles original file names
A note when downloading with the polyfilled version
Saving/downloading a file is borrowing some of ideas from StreamSaver.js.
The difference is:
- Using service worker is optional choice with this adapter.
- It does not rely on some man-in-the-middle or external hosted service worker.
- If you want to stream large data to the disk directly instead of taking up much RAM you need to set up a service worker yourself.
(note that it requires https - but again worker is optional)
to set up a service worker you have to basically copy the example and register it:
`js
navigator.serviceWorker.register('sw.js')
`
Without service worker you are going to write all data to the memory and download it once it closes.
Seeking and truncating won't do anything. You should be writing all data in sequential order when using the polyfilled version.
Testing
- In project folder, run npx http-server -p 3000 .
- For browser tests, open http://localhost:3000/example/test.html in your browser.
- For node: npm run test-node
- For deno: npm run test-deno
Resources
I recommend to follow up on this links for you to learn more about the API and how it works
- https://web.dev/file-system-access/
- https://wicg.github.io/file-system-access/
- https://github.com/WICG/file-system-access
Alternatives
- native-file-system-adapter: The original implementation in vanilla JavaScript by @jimmywarting.
- browser-fs-access by @tomayac: A similar, more like a shim (without getSystemDirectory).
- StreamSaver by @jimmywarting: A way to save large data to the disk directly with a writable stream
(same technique can be achieved if service worker are setup properly)
- FileSaver by @eligrey: One among the first libs to save blobs to the disk
License
file-system-access is licensed under the MIT License. See LICENSE` for details.