Skip to content

Instantly share code, notes, and snippets.

@petkaantonov
Last active January 17, 2019 20:32
Show Gist options
  • Save petkaantonov/37a9c64ea2bbd1aa3ca5 to your computer and use it in GitHub Desktop.
Save petkaantonov/37a9c64ea2bbd1aa3ca5 to your computer and use it in GitHub Desktop.

Better Web File Access

Even though web apps are becoming more and more offline capable, local file access (which is the only thing available when you're offline) is still needlessly restricted and limited. After the user has consented to give the app access to a file(s) from their local file system, the web app's read access to that file is limited until a reload and there is no write access at all.

To work around the read issue, a user would have to never refresh the app as otherwise they risk the inconvenience of having to repick all the files again. The app could put the File objects in IndexedDB, but this both duplicates the data, uses up the app's storage limits (very quickly with media files I might add) and doesn't address the write case at all. IndexedDB is by design unsuitable for this [1]: https://code.google.com/p/chromium/issues/detail?id=476959#c6 [2]: https://code.google.com/p/chromium/issues/detail?id=108012#c69.

To work around the write issue, the user is made to open a 'save as' dialog (using <a href="bloburl" download>), find the file they are modifying and overwrite. The web cannot currently do 'save' at all, even though it is more common action in native applications.

Use cases

Better UX for any kind of file editing task. For example: Drop an image to an app, rotate it and you're already done without having to go through the painful 'save as' dialog process.

Media player apps need persistent access to media files the user has added. The IndexedDB workaround is not even possible here because media libraries are huge, so you basically have to read your entire media library every time the app is started.

Proposal

Implement FileReference object that can be obtained from a native (as opposed to JS constructed) File object. FileReference represents a reference to a File rather than its contents. It grants the app a permission to access the referenced file as long as the file has not been modified from the point it was originally obtained. The FileReference object has internal fields to keep track of this (see Privacy and Security).

The FileReference object can be persisted in IndexedDB, so that access to files can be regained across page refreshes without user intervention. Because the FileReference is just a pointer, the IndexedDB transaction semantics don't need to worry about the file's contents at all.

API:

FileReference(File file) -> FileReference. Constructs a new file reference from file. If the file is not a native File with internal file reference to actual file system file, throw a type error. The file reference's internal write permission field is set to false.

FileReference.prototype.getFile() -> Promise<File>. Returns a promise that resolves to a File object exactly as if the user had selected the File with a file picker. If the file reference is invalid (file has been modified outside the app's control), the promise is rejected with ObsoleteFileReferenceError.

FileReference.prototype.update(bytes Blob|ArrayBuffer, [, start ]) -> Promise<void>. Modify the file by writing bytes starting at start or 0. If the file reference is invalid (file has been modified outside the app's control), the promise is rejected with ObsoleteFileReferenceError. If the file reference's write permission field is false, prompt for a permission for the write ("Site wants to make changes to C:\My Documents\todo.txt, yes/no?"). If permission is denied, reject the promise with FileNotWritable error (or PermissionDeniedError?).

Blob.prototype.saveAs([defaultFileName]) -> Promise<FileReference>. Starts a "save as" dialog with defaultFileName as suggested file name and allowed extension based on the blob's mime type. If the dialog and write succeeds, resolve the promise with a FileReference that references the newly created file. This FileReference has internal write permission set to true.

When a file reference is persisted to IndexedDB, its internal write permission is always persisted as false. This means a permission to change a file is only good for current session. FileReferences can only be obtained in privileged context (Https-only, like Service Worker).

Privacy and Security

Persistent read should not have any new privacy or security issues as the Blob URL and File apis are already guarding against outside modifications. The user can get rid of FileReferences any time by clearing IndexedDB data.

In-place writing to file definitely needs a permission. Writing should not be allowed on system files etc (are those even allowed to be read?). I think write permission should probably be on per-directory basis but not sure if that's too much trouble.

@petkaantonov
Copy link
Author

The write permission obtained doesn't have to be persisted even if the file reference is. When persisting, the internal write permission field could be set to null/false in the persisted version. But it is critical for both cases to be able to persist them, so that the app can be refreshed without having to repick all files and it would punish anyone who reboots/updates/restarts browser so without it you would surely see some banners like "never restart this app".

For persistent read case there is no new privacy/security issue because apps can already persist Files in IndexedDB, it's just that they are limited by size and it's duplicating data so it's very inefficient. It's only an optimization that enables something like a media library (or massive cloud uploads across multiple refreshes etc), there is no need to change FileSystem API for this case at all.

So to recap write security:

  • Single file based (perhaps there should be a possibility somehow to expand to a shallow directory?*)
  • New FileReference objects always need prompt
  • When storing FileReference objects in IndexedDB, the internal write permission field is stored with false/null
  • Prompt is yes/no, and response is only valid for current session (this is implied by previous point)
  • The FileReference obtained from blob.saveAs() has write permission (again, for the current session only as write permission field is not persisted)
  • The FileReference must be valid along with having write permission, for write to be possible. FileReference becomes invalid when the backing file is modified from outside the app

* Meaning other FileReferences to files residing directly in such a directory wouldn't prompt write permission, not meaning directory write permission or anything like that

.Other stuff to consider:

  • Write size limits
  • Should write permission be even obtainable for FileReferences that have not been touched by the app e.g. for a year or a month?
  • Should we in addition to https require the app to declare a CSP policy to access write feature? Not because it's foolproof security but it does give a slight signal that it's a serious app with at least basic security awareness.

@inexorabletash
Copy link

I'm liking the approach of starting with a notion of a read-only reference that can be persisted, then expand that to being writable with non-persisted permission. Small building blocks that can be reasoned about easily. (i.e. sold to the security team...)

Can we loop in @arunranga to bash on FileReference vs. FileHandle/FileHandleWritable ?

@petkaantonov
Copy link
Author

Sure

@petkaantonov
Copy link
Author

@inexorabletash
Copy link

Yep, I sent email.

@inexorabletash
Copy link

FYI, I'm still a fan! Planning to move forward in this space on the spec and implementation front, albeit slowly.

@petkaantonov
Copy link
Author

Awesome. Anywhere I can follow the progress?

@forresthopkinsa
Copy link

Hi -- this is a much-needed feature. Did this discussion continue elsewhere? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment