Skip to content

Commit

Permalink
Big update
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmywarting committed Jun 11, 2021
1 parent 1f0e85e commit 847b6c7
Show file tree
Hide file tree
Showing 32 changed files with 1,953 additions and 2,091 deletions.
Empty file added .github/FUNDING.yml
Empty file.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ dist
.tern-port

.idea
.history
15 changes: 15 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"editor.tabSize": 2,
"javascript.preferences.quoteStyle": "single",
"javascript.format.insertSpaceAfterConstructor": true,
"javascript.format.insertSpaceAfterCommaDelimiter": true,
"javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true,
"javascript.format.semicolons": "remove",
"javascript.suggest.completeFunctionCalls": true,
"javascript.suggest.completeJSDocs": true,
"javascript.suggest.autoImports": true,
"js/ts.implicitProjectConfig.checkJs": true,
}
85 changes: 50 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
# 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.
What is this?

This is file system that follows [native-file-system](https://wicg.github.io/native-file-system/) specification. Thanks to it we can have a unified way of handling data in all browser and even in NodeJS in a more secure way.

At a high level what we're providing is several bits:

1. A modernized version of `FileSystemFileHandle` and `FileSystemDirectoryHandle` interfaces.
2. A modernized version of `FileSystemWritableFileStream` to truncate and write data.
3. A way to not only use one location to read & write data to and from, but several other sources called adapters

### Adapters

This polyfill/ponyfill ships with 5 filesystem backends:
This polyfill/ponyfill ships with a few backends built in:

* `native`: Stores files the `Native Sandboxed` file storage
* `node`: Interact with filesystem using nodes `fs`
* `native`: Stores files the `Native Sandboxed` file file system storage
* `Sandbox`: Stores files into the Blinks `Sandboxed FileSystem` API.
* `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 storage`: Stores files in cache storage like a request/response a-like.

You can even load in your own underlying adapter and get the same set of API's

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.<br>
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 `_`
It's not trying to interfere with the changing spec by using other properties that may conflict with the feature changes to the spec.

( The current minium supported browser I have choosen to support is the ones that can handle import/export )<br>
( The current minium supported browser I have chosen to support is the ones that can handle import/export )<br>
( Some parts are lazy loaded when needed )

### Using
Expand All @@ -32,34 +43,42 @@ import {
getOriginPrivateDirectory
} from 'https://cdn.jsdelivr.net/gh/jimmywarting/native-file-system-adapter/src/es6.js'


// the getOriginPrivateDirectory is a legacy name that
// native filesystem added, have not bother to change it

getOriginPrivateDirectory() // same as calling navigator.storage.getDirectory()
// Blink's good old sandboxed file system API, can choose between persistent and temporary
getOriginPrivateDirectory(import('../src/adapters/sandbox.js'))
getOriginPrivateDirectory(import('../src/adapters/memory.js'))
getOriginPrivateDirectory(import('../src/adapters/indexeddb.js'))
getOriginPrivateDirectory(import('../src/adapters/cache.js'))
getOriginPrivateDirectory(import('../src/adapters/node.js'), './starting-path')
// same as calling navigator.storage.getDirectory()
handle = await getOriginPrivateDirectory()
// A write only adapter to save files to the disk
handle = await getOriginPrivateDirectory(import('../src/adapters/downloader.js'))
// Blinks old sandboxed api
handle = await getOriginPrivateDirectory(import('../src/adapters/sandbox.js'))
// fast in-memory file system
handle = await getOriginPrivateDirectory(import('../src/adapters/memory.js'))
// Using indexDB
handle = await getOriginPrivateDirectory(import('../src/adapters/indexeddb.js'))
// Store things in the new Cache API as request/responses (bad at mutating data)
handle = await getOriginPrivateDirectory(import('../src/adapters/cache.js'))

// Node only variant:
handle = await getOriginPrivateDirectory(import('native-file-system-adapter/src/adapters/memory.js'))
handle = await getOriginPrivateDirectory(import('native-file-system-adapter/src/adapters/node.js'), './starting-path')



// The polyfilled (file input) version will turn into a memory adapter
// You will have readwrite permission on the memory adapter,
// You will have read & write permission on the memory adapter,
// you might want to transfer (copy) the handle to another adapter
showOpenFilePicker({_preferPolyfill: boolean, ...sameOpts}).then(fileHandle => {})
showDirectoryPicker({_preferPolyfill: boolean, ...sameOpts}).then(directoryHandle => {})

// Supports drag and drop also
ondrop = evt => {
window.ondrop = evt => {
evt.preventDefault()
getOriginPrivateDirectory(evt.dataTransfer).then(directoryHandle => {
// This is kind of a hybrid memory & sandboxed (Entry api) adapter
// it works together with old Entry API rather then transferring all of it to a memory adapter
// This would allow you to monitor file changes by calling getFile()
// and compare the last modified date or file size
// You will have read access but, requesting write permission will reject.
})
for (let item of evt.dataTransfer.items) {
item.getAsFileSystemHandle(handle => {
console.log(handle)
})
}
}

// request user to select a file
Expand All @@ -73,10 +92,11 @@ const fileHandle = await showOpenFilePicker({
// returns a File Instance
const file = await fileHandle.getFile()

// copy the file over to a another adapter
const rootHandle = await getOriginPrivateDirectory() // same as navigator.store.getDirectory()
// copy the file over to a another place
const rootHandle = await getOriginPrivateDirectory()
const fileHandle = await rootHandle.getFileHandle(file.name, { create: true })
await fileHandle.write(file)
fileHandle.close()

// save/download a file
const fileHandle = await showSaveFilePicker({
Expand All @@ -90,7 +110,7 @@ const fileHandle = await showSaveFilePicker({
excludeAcceptAllOption: false // default
})

// Look at what extension they chosed
// Look at what extension they chosen
const extensionChosen = fileHandle.name.split('.').pop()

const blob = {
Expand All @@ -100,7 +120,7 @@ const blob = {
}[extensionChosen]

await blob.stream().pipeTo(fileHandle.getWriter())
// or
// or
var writer = fileHandle.getWriter()
writer.write(blob)
writer.close()
Expand All @@ -113,7 +133,7 @@ Will get to it at some point in the feature

Saving/downloading a file borrowing some of ideas from [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js).
The difference is:
- Using service worker is optional choice with this adapter. 🤷‍
- Using service worker is optional choice with this adapter.
- It dose 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.<br>(note that it requires https - but again worker is optional)
- You don't have to handle web-streams-polyfills it's lazy loaded when needed when you need that writable stream. 😴
Expand All @@ -126,17 +146,12 @@ 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 sequental order when using the polyfilled version.

-----

If you have chosen to `open-directory` when the polyfilled version is in use (`input[webkitdirectory]`)
than you can't get any write access to it. So unless you are using chanary with experimental flags or enabled the [Origin Trials](https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/developer-guide.md) for beta testing on your origin, then you better use `save-file` instead to be safe. It's also possible to query/request permission.
Seeking and truncating won't do anything. You should be writing all data in sequential order when using the polyfilled version.

### Testing

start up a server and open `/examples/test.html` in your browser.

- start up a server and open `/examples/test.html` in your browser.
- for node: `npm i && npm run test`

### Resources

Expand Down
98 changes: 0 additions & 98 deletions example/ps.html

This file was deleted.

19 changes: 15 additions & 4 deletions example/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,34 @@ const ABORT = 1
const CLOSE = 2
const PING = 3

/** @implements {UnderlyingSource} */
class MessagePortSource {

/** @type {ReadableStreamController<any>} controller */
controller

/** @param {MessagePort} port */
constructor (port) {
this.port = port;
this.port.onmessage = evt => this.onMessage(evt.data)
}

/**
* @param {ReadableStreamController<any>} controller
*/
start (controller) {
this.controller = controller
}
pull () {
this.port.postMessage({ type: PULL })
}

/** @param {Error} reason */
cancel (reason) {
// Firefox can notifiy a cancel event, chrome can't
// Firefox can notify a cancel event, chrome can't
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
this.port.postMessage({ type: ERROR, reason: reason.message })
this.port.close()
}

/** @param {{ type: number; chunk: Uint8Array; reason: any; }} message */
onMessage (message) {
// enqueue() will call pull() if needed when there's no backpressure
if (message.type === WRITE) this.controller.enqueue(message.chunk)
Expand Down
14 changes: 8 additions & 6 deletions example/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
// scope: './'
})
</script>
<script type=module src="./test.js"></script>
<script type="module" src="./test.js"></script>
<table id="table">
<caption>Browser storage</caption>
<thead><tr>
Expand Down Expand Up @@ -135,13 +135,15 @@
<tr>
<td width="1">
<center>Drag and drop</center><br>
The DataTransfer are very similar to Blinks sandboxed filesystem.
so it would be easy to just convert the event to a FileSystemDirectoryHandle
and use the sandbox adapter on it.
The DataTransfer have implemented <code>DataTransferItem.prototype.getAsFileSystemHandle()</code>
That lets you convert a item into a filesystem handle. This is polyfilled for you.
<pre style="background: #fff; box-sizing: border-box; color: #68615e; display: block; height: 100%; overflow-x: auto; padding: 0.5em;"><code style="height: 100%;">elm.ondragover = <span class="hljs-function"><span style="color: #df5320;">evt</span> =&gt;</span> evt.preventDefault()
elm.ondrop = <span class="hljs-function"><span style="color: #df5320;">evt</span> =&gt;</span> {
elm.ondrop = <span class="hljs-function"><span style="color: #df5320;">async evt</span> =&gt;</span> {
evt.preventDefault()
getOriginPrivateDirectory(evt.dataTransfer)
for (const dataTransferItem of dataTransfer.items) {
const handle = await dataTransferItem.getAsFileSystemHandle()
console.log(handle.kind)
}
}</code></pre>
</td>
<td>Drop anywhere<br>on page</td>
Expand Down
Loading

0 comments on commit 847b6c7

Please sign in to comment.