Writable Files

Editor’s Draft,

This version:
https://wicg.github.io/writable-files/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google)

Abstract

This document defines a web platform API that lets websites gain write access to the native file system. It builds on [FILE-API], but adds lots of new functionality on top.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

TODO

This provides similar functionality as earlier drafts of the [file-system-api] as well as the [entries-api], but with a more modern API.

2. Files and Directories

2.1. Concepts

A entry is either a file entry or a directory entry.

Each entry has an associated name.

A file entry additionally consists of binary data and a modification timestamp.

A directory entry additionally consists of a set of entries. Each member is either a file or a directory.

The parent directory of a entry child is the directory parent for which parent’s entries contains child, if such a directory exist. Each directory entry must be contained by the entries of at most one directory entry, while each file entry must be contained by the entries of exactly one directory entry.

If a directory does not have a parent directory, it is called a root directory.

Each entry entry has an associated file system, which is either entry itself if entry is a root directory, or otherwise is equal to the file system of entry’s parent directory.

TODO: Explain how entries map to files on disk (multiple entries can map to the same file or directory on disk but doesn’t have to map to any file on disk). Root directories typically don’t map to files on disk at all. Same file represented by different entries can have different parents etc.

2.2. The FileSystemHandle interface

interface FileSystemHandle {
  readonly attribute boolean isFile;
  readonly attribute boolean isDirectory;
  readonly attribute USVString name;

  Promise<FileSystemDirectoryHandle> getParent();

  Promise<FileSystemHandle> moveTo(
      FileSystemDirectoryHandle parent, optional USVString name);
  Promise<FileSystemHandle> copyTo(
      FileSystemDirectoryHandle parent, optional USVString name);
  Promise<void> remove();
};

A FileSystemHandle object represents a entry. Each FileSystemHandle object is assocaited with a entry (a entry). Multiple separate objects implementing the FileSystemHandle interface can all be associated with the same entry simultaneously.

handle . isFile

Returns true iff handle is a FileSystemFileHandle.

handle . isDirectory

Returns true iff handle is a FileSystemDirectoryHandle.

handle . name

Returns the name of the entry represented by handle.

The isFile attribute must return true if the associated entry is a file entry, and false otherwise.

The isDirectory attribute must return true if the associated entry is a directory entry, and false otherwise.

The name attribute must return the name of the associated entry.

2.2.1. The getParent() method

parent = await handle . getParent()

Returns a promise resolved to the parent directory of handle. If handle is the root directory of its file system the promise resolves to null.

Can reject if the entry no longer exists in the underlying file system, or if some error prevents reading the entry or its parent.

The getParent() method, when invoked, must run these steps:
  1. TODO

2.2.2. The moveTo() method

newHandle = await handle . moveTo(await handle.getParent(), newName)

Renames the entry represented by handle to newName.

newHandle = await handle . moveTo(otherDir)

Moves the entry represented by handle to otherDir, while keeping its existing name.

newHandle = await handle . moveTo(otherDir, newName)

Moves the entry represented by handle to otherDir, while renaming it to newName.

In all of these cases, handle will no longer represent a valid entry, and thus any further operations on it will fail.

Attempting to move an entry to the directory it already exists in, without renaming it is an error. In all other cases if the target entry already exists, it is overwritten.

The moveTo(parent, name) method, when invoked, must run these steps:
  1. TODO

2.2.3. The copyTo() method

newHandle = await handle . copyTo(await handle.getParent(), newName)

Creates a copy of the entry represented by handle with name newName.

newHandle = await handle . copyTo(otherDir)

Creates a copy of the entry represented by handle in otherDir, while keeping its existing name.

newHandle = await handle . copyTo(otherDir, newName)

Creates a copy of the entry represented by handle in otherDir, using newName for the name of the new entry.

In all of these cases, handle will no longer represent a valid entry, and thus any further operations on it will fail.

Attempting to copy an entry on top of itself will fail. In all other cases if the target entry already exists, it is overwritten.

The copyTo(parent, name) method, when invoked, must run these steps:
  1. TODO

2.2.4. The remove() method

await handle . remove()

Attempts to remove the entry represented by handle from the underlying file system.

The remove() method, when invoked, must run these steps:
  1. TODO

2.3. The FileSystemFileHandle interface

interface FileSystemFileHandle : FileSystemHandle {
  Promise<File> getFile();
  Promise<FileSystemWriter> createWriter();
};

2.3.1. The getFile() method

file = await fileHandle . getFile()

Returns a File representing the state on disk of the entry represented by handle.

The getFile() method, when invoked, must run these steps:
  1. TODO

2.3.2. The createWriter() method

writer = await fileHandle . createWriter()

Returns a FileSystemWriter that can be used to write to the file.

The createWriter() method, when invoked, must run these steps:
  1. TODO

2.4. The FileSystemDirectoryHandle interface

dictionary FileSystemGetFileOptions {
  boolean create = false;
};

dictionary FileSystemGetDirectoryOptions {
  boolean create = false;
};

interface FileSystemDirectoryHandle : FileSystemHandle {
  Promise<FileSystemFileHandle> getFile(USVString name, optional FileSystemGetFileOptions options);
  Promise<FileSystemDirectoryHandle> getDirectory(USVString name, optional FileSystemGetDirectoryOptions options);

  // This really returns an async iterable, but that is not yet expressable in WebIDL.
  object getEntries();

  Promise<void> removeRecursively();
};

Should we have separate getFile and getDirectory methods, or just a single getChild/getEntry method?

Having getFile methods in both FileSystemDirectoryHandle and FileSystemFileHandle, but with very different behavior might be confusing? Perhaps rename at least one of them (but see also previous issue).

Should getEntries be its own method, or should FileSystemDirectoryHandle just be an async iterable itself?

2.4.1. The getFile() method

fileHandle = await directoryHandle . getFile(name)

Returns a handle for a file named name in the directory represented by directoryHandle. If no such file exists, this rejects.

fileHandle = await directoryHandle . getFile(name, { create: true })

Returns a handle for a file named name in the directory represented by directoryHandle. If no such file exists, this creates a new file. If no file with named name can be created this rejects. Creation can fail because there already is a directory with the same name, because the name uses characters that aren’t supported in file names on the underlying file system, or because the user agent for security reasons decided not to allow creation of the file.

See TODO section for possible security reasons.

The getFile(name, options) method, when invoked, must run these steps:
  1. TODO

2.4.2. The getDirectory() method

subdirHandle = await directoryHandle . getDirectory(name)

Returns a handle for a directory named name in the directory represented by directoryHandle. If no such directory exists, this rejects.

subdirHandle = await directoryHandle . getDirectory(name, { create: true })

Returns a handle for a directory named name in the directory represented by directoryHandle. If no such directory exists, this creates a new directory. If creating the directory failed, this rejects. Creation can fail because there already is a file with the same name, or because the name uses characters that aren’t supported in file names on the underlying file system.

The getDirectory(name, options) method, when invoked, must run these steps:
  1. TODO

2.4.3. The getEntries() method

for await (const handle of directoryHandle . getEntries()) {}

Iterates over all entries whose parent is the entry represented by directoryHandle.

The getEntries() method, when invoked, must run these steps:
  1. TODO

2.4.4. The removeRecursively() method

await directoryHandle . removeRecursively()

Removes the entry represented by directoryHandle, and all entries contained within it, recursively.

2.5. The FileSystemWriter interface

interface FileSystemWriter {
  Promise<void> write(unsigned long long position, (Blob or ReadableStream) data);
  Promise<void> truncate(unsigned long long size);
  Promise<void> close();
};

2.5.1. The write() method

await writer . write(position, data)

Writes the content of data into the file associated with writer at position position. If position is past the end of the file writing will fail and this method rejects.

The write(position, data) method, when invoked, must run these steps:
  1. TODO

2.5.2. The truncate() method

await writer . truncate(size)

Resizes the file associated with writer to be size bytes long. If size is larger than the current file size this pads the file with zero bytes, otherwise it truncates the file.

The truncate(size) method, when invoked, must run these steps:
  1. TODO

3. Accessing native filesystem

3.1. The chooseFileSystemEntries() method

enum ChooseFileSystemEntriesType { "openFile", "saveFile", "openDirectory" };

dictionary ChooseFileSystemEntriesOptionsAccepts {
  USVString description;
  sequence<USVString> mimeTypes;
  sequence<USVString> extensions;
};

dictionary ChooseFileSystemEntriesOptions {
    ChooseFileSystemEntriesType type = "openFile";
    boolean multiple = false;
    sequence<ChooseFileSystemEntriesOptionsAccepts> accepts;
    boolean excludeAcceptAllOption = false;
};

partial interface Window {
    [SecureContext]
    Promise<(FileSystemHandle or sequence<FileSystemHandle>)>
        chooseFileSystemEntries(optional ChooseFileSystemEntriesOptions options);
};
result = await window . chooseFileSystemEntries(options)

Shows a file picker dialog to the user and returns handles for the selected files or directories.

The options argument sets options that influence the behavior of the shown file picker.

options.type specifies the type of the entry the website wants the user to pick. When set to "openFile" (the default), the user can select only existing files. When set to "saveFile" the dialog will additionally let the user select files that don’t yet exist. Finally when set to "openDirectory", the dialog will let the user select directories instead of files.

If options.multiple is false (or absent) the user can only select a single file, and the result will be a single FileSystemHandle. If on the other hand options.multiple is true, the dialog can let the user select more than one file, and result will be an array of FileSystemHandle instances (even if the user did select a single file, if multiple is true this will be returned as a single-element array).

Finally options.accepts and options.excludeAcceptAllOption specify the types of files the dialog will let the user select. Each entry in options.accepts describes a single type of file, consisting of a description, zero or more mimeTypes and zero or more extensions. Options with no valid mimeTypes and no extensions are invalid and are ignored. If no description is provided one will be generated.

If options.excludeAcceptAllOption is true, or if no valid entries exist in options.accepts, a option matching all files will be included in the file types the dialog lets the user select.

The chooseFileSystemEntries(options) method, when invoked, must run these steps:
  1. TODO

4. Accessing special filesystems

4.1. The getSystemDirectory() method

enum SystemDirectoryType {
  "sandbox"
};

dictionary GetSystemDirectoryOptions {
  required SystemDirectoryType type;
};

partial interface FileSystemDirectoryHandle {
  static Promise<FileSystemDirectoryHandle> getSystemDirectory(GetSystemDirectoryOptions options);
};
directoryHandle = FileSystemDirectoryHandle . getSystemDirectory({ type: "sandbox" })

Returns the sandboxed filesystem.

getSystemDirectory might not be the best name. Also perhaps should be on Window rather than on FileSystemDirectoryHandle. <https://github.com/wicg/writable-files/issues/27>

The getSystemDirectory(options) method, when invoked, must run these steps:
  1. TODO

5. Security Considerations

5.1. Secure Context

5.2. Limiting types of files that can be written to

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[FILE-API]
Marijn Kruisselbrink; Arun Ranganathan. File API. 6 November 2018. WD. URL: https://www.w3.org/TR/FileAPI/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[ENTRIES-API]
File and Directory Entries API. Living Standard. URL: https://wicg.github.io/entries-api/
[FILE-SYSTEM-API]
Eric Uhrhane. File API: Directories and System. 24 April 2014. NOTE. URL: https://www.w3.org/TR/file-system-api/

IDL Index

interface FileSystemHandle {
  readonly attribute boolean isFile;
  readonly attribute boolean isDirectory;
  readonly attribute USVString name;

  Promise<FileSystemDirectoryHandle> getParent();

  Promise<FileSystemHandle> moveTo(
      FileSystemDirectoryHandle parent, optional USVString name);
  Promise<FileSystemHandle> copyTo(
      FileSystemDirectoryHandle parent, optional USVString name);
  Promise<void> remove();
};

interface FileSystemFileHandle : FileSystemHandle {
  Promise<File> getFile();
  Promise<FileSystemWriter> createWriter();
};

dictionary FileSystemGetFileOptions {
  boolean create = false;
};

dictionary FileSystemGetDirectoryOptions {
  boolean create = false;
};

interface FileSystemDirectoryHandle : FileSystemHandle {
  Promise<FileSystemFileHandle> getFile(USVString name, optional FileSystemGetFileOptions options);
  Promise<FileSystemDirectoryHandle> getDirectory(USVString name, optional FileSystemGetDirectoryOptions options);

  // This really returns an async iterable, but that is not yet expressable in WebIDL.
  object getEntries();

  Promise<void> removeRecursively();
};

interface FileSystemWriter {
  Promise<void> write(unsigned long long position, (Blob or ReadableStream) data);
  Promise<void> truncate(unsigned long long size);
  Promise<void> close();
};

enum ChooseFileSystemEntriesType { "openFile", "saveFile", "openDirectory" };

dictionary ChooseFileSystemEntriesOptionsAccepts {
  USVString description;
  sequence<USVString> mimeTypes;
  sequence<USVString> extensions;
};

dictionary ChooseFileSystemEntriesOptions {
    ChooseFileSystemEntriesType type = "openFile";
    boolean multiple = false;
    sequence<ChooseFileSystemEntriesOptionsAccepts> accepts;
    boolean excludeAcceptAllOption = false;
};

partial interface Window {
    [SecureContext]
    Promise<(FileSystemHandle or sequence<FileSystemHandle>)>
        chooseFileSystemEntries(optional ChooseFileSystemEntriesOptions options);
};

enum SystemDirectoryType {
  "sandbox"
};

dictionary GetSystemDirectoryOptions {
  required SystemDirectoryType type;
};

partial interface FileSystemDirectoryHandle {
  static Promise<FileSystemDirectoryHandle> getSystemDirectory(GetSystemDirectoryOptions options);
};

Issues Index

TODO: Explain how entries map to files on disk (multiple entries can map to the same file or directory on disk but doesn’t have to map to any file on disk). Root directories typically don’t map to files on disk at all. Same file represented by different entries can have different parents etc.
Should we have separate getFile and getDirectory methods, or just a single getChild/getEntry method?
Having getFile methods in both FileSystemDirectoryHandle and FileSystemFileHandle, but with very different behavior might be confusing? Perhaps rename at least one of them (but see also previous issue).
Should getEntries be its own method, or should FileSystemDirectoryHandle just be an async iterable itself?
getSystemDirectory might not be the best name. Also perhaps should be on Window rather than on FileSystemDirectoryHandle. <https://github.com/wicg/writable-files/issues/27>