file-system-access

Interface

The exact interfaces involved here are yet to be decided. But at a high level what we’re providing is several bits:

  1. A modernized version of the existing (but not really standardized) Entry API in the form of new (names TBD) FileSystemFileHandle and FileSystemDirectoryHandle interfaces.
  2. A modernized version of the existing (and also not really standardized) FileWriter interface.
  3. Various entry points to get a handle representing a limited view of the native file system. I.e. either via a file picker, or to get access to certain well known directories, or even to get access to the whole native file system. Mimicing things such as chrome’s chrome.fileSystem.chooseEntry API.

Example code

// Show a file picker to open a file.
const file_ref = await FileSystemFileHandle.choose({
    type: 'open',
    multiple: false, // If true, returns an array rather than a single handle.
    
    // If true, the resulting file reference won't be writable. Note that there
    // is no guarantee that the resulting file reference will be writable when
    // readOnly is set to false. Both filesystem level permissions as well as
    // browser UI/user intent might result in a file reference that isn't usable
    // for writing, even if the website asked for a writable reference.
    readOnly: false,
    
    accepts: [{description: 'Images', extensions: ['jpg', 'gif', 'png']}],
    suggestedStartLocation: 'pictures-library'
});
if (!file_ref) {
    // User cancelled, or otherwise failed to open a file.
    return;
}

// Read the contents of the file.
const file_reader = new FileReader();
file_reader.onload = (event) => {
    // File contents will appear in event.target.result.  See
    // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/onload for
    // more info.

    // ...

    // Write changed contents back to the file. Rejects if file reference is not
    // writable. Note that it is not generally possible to know if a file
    // reference is going to be writable without actually trying to write to it.
    // For example, both the underlying filesystem level permissions for the
    // file might have changed, or the user/user agent might have revoked write
    // access for this website to this file after it acquired the file
    // reference.
    const file_writer = await file_ref.createWriter();
    await file_writer.write(new Blob(['foobar']));
    file_writer.seek(1024);
    await file_writer.write(new Blob(['bla']));

    // Can also write contents of a ReadableStream.
    let response = await fetch('foo');
    await file_writer.write(response.body);
};

// file_ref.file() method will reject if site (no longer) has access to the
// file.
let file = await file_ref.file();

// readAsArrayBuffer() is async and returns immediately.  |file_reader|'s onload
// handler will be called with the result of the file read.
file_reader.readAsArrayBuffer(file);

Also possible to store file references in IDB to re-read and write to them later.

// Open a db instance to save file references for later sessions
let db;
let request = indexedDB.open("WritableFilesDemo");
request.onerror = function(e) { console.log(e); }
request.onsuccess = function(e) { db = e.target.result; }

// Show file picker UI.
const file_ref = await FileSystemFileHandle.choose();

if (file_ref) {
    // Save the reference to open the file later.
    let transaction = db.transaction(["filerefs"], "readwrite");
    let request = transaction.objectStore("filerefs").add( file_ref );
    request.onsuccess = function(e) { console.log(e); }

    // Do other useful things with the opened file.
};

// ...

// Retrieve a file you've opened before. Show's no filepicker UI.
// The browser can choose when to allow or not allow this open.
let file_id = "123"; // Some logic to determine which file you'd like to open
let transaction = db.transaction(["filerefs"], "readonly");
let request = transaction.objectStore("filerefs").get(file_id);
request.onsuccess = function(e) {
    let ref = e.result;

    // Rejects if file is no longer readable, either because it doesn't exist
    // anymore or because the website no longer has permission to read it.
    let file = await ref.file();
    // ... read from file

    // Rejects if file is no longer writable, because the website no longer has
    // permission to write to it.
    let file_writer = await ref.createWriter({createIfNotExists: true});
    // ... write to file_writer
}

The fact that handles are serializable also means you can postMessage them around:

// In a service worker:
self.addEventListener('some-hypothetical-launch-event', e => {
  // e.file is a FileSystemFileHandle representing the file this SW was launched with.
  let win = await clients.openWindow('bla.html');
  if (win)
    win.postMessage({openFile: e.file});
});

// In bla.html
navigator.serviceWorker.addEventListener('message', e => {
  let file_ref = e.openFile;
  // Do something useful with the file reference.
});

Also possible to get access to an entire directory.

const dir_ref = await FileSystemDirectoryHandle.choose();
if (!dir_ref) {
    // User cancelled, or otherwise failed to open a directory.
    return;
}
// Read directory contents.
for await (const entry of dir_ref.entries()) {
    // entry is a FileSystemFileHandle or a FileSystemDirectoryHandle.
}

// Get a specific file.
const file_ref = await dir_ref.getFile('foo.js');
// Do something useful with the file.

// Get a subdirectory.
const subdir = await dir_ref.getDirectory('bla', {createIfNotExists: true});

// And you can possibly do stuff like move and/or copy files around.
await file_ref.copyTo(dir_ref, 'new_name', {overwrite: true});

// You can also navigate the file system:
const dir2 = await file_ref.getParent();
// dir2.fullPath == dir_ref.fullPath.

// But you can't escape from the directory you've been given access to.
// await dir_ref.getParent() == null

And perhaps even possible to get access to certain “well-known” directories, without showing a file picker, i.e. to get access to all fonts, all photos, or similar. Could still include some kind of permission prompt if needed.

const font_dir = await FileSystemDirectoryHandle.getSystemDirectory({type: 'fonts'});
for await (const entry of font_dir.entries()) {
    // Use font entry.
};

Proposed security models

The spec has hooks for the browser to customize the security model:

Native app like
One proposed security model is to allow sites to always open files as writable. Files could be reopened on later visits to the site. If the file had been modified since the last time the site opened it, the browser could show non blocking UI notifying the user:

Warning text when reading a file</img>

When the file is written, the browser could show another non blocking UI notifying the user:

Warning text when writing a file</img>

Alternatively, the browser could choose not to show any UI. This would match the model that users expect from native apps, while restricting the site’s actual access only to files that the user has explicitly granted access for.

More restictive
Alternatively, the site could choose to only allow files to be writable if the user accepts some additional permission (such as a “Allow modifications” checkbox on the filepicker). The non blocking UI mentioned above could be a blocking UI that the user has to accept. The UI could be shown for any read, rather than just reads on modified files.

Known weirdness