New filesystem/directory API proposal

As mentioned in my previous email [1], here's my proposal for an API
covering directories and read-write access to a local, sandboxed,
per-origin filesystem.  As with that API, if people like the general
gist of this one, I'll write it up nicely, get it hosted somewhere
official, and start rolling in feedback.

We have a lot of app developers asking for this sort of a capability,
and are quite interested in discussing it further and getting to the
point where we can experiment with implementations.

Thanks,

    Eric Uhrhane
    ericu@google.com

How to get a FileSystem in the device world:

[Supplemental, NoInterfaceObject]
interface DeviceFS {
  void fileSystems(FileSystemsCB successCallback,
                   [Optional] Callback errorCallback);
  void resolveURI(DOMString uri,
                  EntryCB successCallback,
                  [Optional] Callback errorCallback);
}

[NoInterfaceObject, Callback=FunctionOnly]
interface FileSystemsCB {
    void onSuccess(FileSystem[] fileSystems);
};
This is pretty much the same as [2], but I moved resolveURI() up to the top
level so that you don't need to know which filesystem a URI is in before trying
to look it up.

How to get a FileSystem in the browser world:

[Supplemental, NoInterfaceObject]
interface WindowLocalFS {
  void requestTemporaryFilesystem(FileSystemCB successCallback,
                                  [Optional] errorCallback);
  void requestPersistentFilesystem(long long requestedQuota,
                                   FileSystemCB successCallback,
                                   [Optional] errorCallback);
  void resolveURI(DOMString uri,
                  EntryCB successCallback,
                  [Optional] Callback errorCallback);
}

[NoInterfaceObject, Callback=FunctionOnly]
interface FileSystemCB {
    void onSuccess(FileSystem fileSystem);
};

An application can request temporary or persistent storage space.  Temporary
storage may be easier to get at the UA's discretion [looser quota restrictions,
available without prompting the user], but the data stored there may be deleted
at the UA's convenience.

Once persistent storage has been granted, data stored there by the application
must not be deleted by the UA without user intervention.  The application may of
course delete it itself.  The UA should require permission from the user before
granting persistent storage space to the application.

As with any other client-side storage, filesystem access allows for
cookie-resurrection attacks.  UAs should present the option of clearing it when
the user clears any other origin-specific storage, blocking access to it when
cookies are blocked, etc.  This is especially important if temporary storage
space is permitted without explicit user permission.

[NoInterfaceObject]
interface FileSystem {
  readonly attribute DOMString name;
  readonly attribute DirectoryEntry root;
  readonly attribute long long quota;
};

[NoInterfaceObject, Callback=FunctionOnly]
interface EntryCB {
    void onSuccess(Entry entry);
};

Cross-platform compatibility:

I've taken out the filesystem attributes for case-sensitivity and -preservation.
We don't want to pass the buck on those attributes to the application developer.
We just want the same code to work everywhere.  What the app sees should be a
case-insensitive, case-preserving filesystem.  That's easy to implement on top
of the most-commonly-used filesystems, and not hard to emulate on case-sensitive
systems.  Problems due to interactions with applications running outside the
browser should be extremely rare and minor.

The directory separator is '/'.

No file or directory may be named any of [CON, PRN, AUX, NUL, COM1, COM2, COM3,
COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7,
LPT8, LPT9], nor may a file or directory's name begin with any of those strings
followed by a period.  Filenames may not end in period or space.

Paths can't contain any character in the set [<>:"/\|?*] or whose representation
in UTF-8 is in [0-31].

"./", as a path segment, refers to the currently-specified directory.  If it's
not at the beginning of the supplied path, it's therefore a noop.

"../" as a path segment will refer to the parent of the currently-specified
directory; if it's not the beginning of the supplied path, it therefore
effectively deletes the previously-supplied path segment from the path being
computed.

The parent directory of the root directory of a filesystem is the root directory
of the filesystem.


[NoInterfaceObject]
interface Entry {
  const unsigned short DIRECTORY = 1;
  const unsigned short FILE = 2;
  readonly attribute unsigned short entryType;
  readonly attribute DOMString      name;
  readonly attribute DOMString      fullPath;
  readonly attribute FileSystem     fileSystem;

  void moveTo(DirectoryEntry parent, optional DOMString newName,
              [Optional] EntryCB successCallback,
              [Optional] Callback errorCallback);
  void copyTo(DirectoryEntry parent, optional DOMString newName,
              [Optional] EntryCB successCallback,
              [Optional] Callback errorCallback);
  void getParent(EntryCB successCallback, [Optional] Callback errorCallback);

  DOMString toURI(); // Valid only in this domain.

  // Note that this does not recurse; if this is a DirectoryEntrySync and the
  // directory it represents isn't empty, this will fail.  If it turns out to be
  // important, we can add it, but there's nothing to stop a JavaScript library
  // from implementing it using this API.
  void remove([Optional] Callback successCallback,
              [Optional] Callback errorCallback);
};

[NoInterfaceObject]
interface DirectoryEntry : Entry {
  DirectoryReader createReader();

  const unsigned short CREATE = 1;
  // If CREATE is specified and the Entry already exists, fail.
  // If CREATE is specified and the Entry doesn't exist, create it as a
  // zero-length file.
  // If CREATE is not specified and the Entry doesn't exist, fail.
  // If CREATE is not specified and the path exists, but is a directory, fail.
  void getFile(DOMString path, unsigned short options,
               [Optional] EntryCB successCallback,
               [Optional] Callback errorCallback);
  void makeDir(DOMString name,
               [Optional] EntryCB successCallback,
               [Optional] Callback errorCallback);
};

[Constructor]
interface DirectoryReader {
  // Holds state; subsequent calls return later Entries.
  // Returns success and zero entries when all Entries have been returned.
  void readEntries(EntriesCB successCallback,
                   [Optional] Callback errorCallback);
};

[NoInterfaceObject, Callback=FunctionOnly]
interface EntriesCB {
    void onSuccess(Entry[] entries);
};

[NoInterfaceObject]
interface FileEntry : Entry, File {
  // If APPEND is not specified, the file is truncated to length 0 before its
  // first write.
  const unsigned short APPEND = 1;
  FileWriter createWriter([Optional] unsigned short options);
};

I've made FileEntry derive from File in [3], but there are a couple of small
issues to iron out:
  1) An Entry's URI is persistent across page reloads, although of course the
  file or directory to which it refers may be deleted or moved.  A File's URN is
  valid only until the page goes away.  That's not necessarily a problem, but it
  seems odd to be able to ask for both from the same object.
  2) File's "type" [formerly mediaType] might confuse folks looking for the
  entryType.  That's not a show-stopper, but it might lead to some coding
  errors.

As all operations will generally map to a single underlying platform-specific
call, I've given them callbacks, rather than using an EventTarget.  There's not
really a way to get progress events, and it seems cumbersome to use an
EventTarget without getting any extra benefit from it.  This means we've got two
different styles of async calls [between FileReader/Writer and this proposal],
but I think the simplification is well worth the contrast.

I took out the zip operation for now, as I'm not sure what the use cases are.
Zip and unzip can be written in JavaScript [e.g.  http://jszip.stuartk.co.uk/].
If speed will be an issue, please respond with use cases.

Synchronous APIs for Web Workers, directly modeled on those above.

[Supplemental, NoInterfaceObject]
interface WorkerLocalFS {
  FileSystemSync requestTemporaryFilesystem();
  FileSystemSync requestPersistentFilesystem(long long requestedQuota);
  EntrySync resolveURI(DOMString uri);
}

[NoInterfaceObject]
interface FileSystemSync {
    readonly attribute DOMString name;
    readonly attribute DirectoryEntrySync root;
    readonly attribute long long quota;
};

[NoInterfaceObject]
interface EntrySync {
  const unsigned short DIRECTORY = 1;
  const unsigned short FILE = 2;
  readonly attribute unsigned short type;
  readonly attribute DOMString      name;
  readonly attribute DOMString      fullPath;
  readonly attribute FileSystem     fileSystem;

  void moveTo(DirectoryEntrySync parent, optional DOMString newName);
  void copyTo(DirectoryEntrySync parent, optional DOMString newName);
  DirectoryEntrySync getParent();

  DOMString toURI(); // Valid only in this domain.  Same issues as fullPath.

  // Note that this does not recurse; if this is a DirectoryEntrySync and the
  // directory it represents isn't empty, this will fail.  If it turns out to be
  // important, we can add it, but there's nothing to stop a JavaScript library
  // from implementing it using this API.
  void remove();
};

[NoInterfaceObject]
interface DirectoryEntrySync : EntrySync {
  DirectoryReaderSync createReader();

  const unsigned short CREATE = 1;
  // If CREATE is specified and the Entry already exists, fail.
  // If CREATE is specified and the Entry doesn't exist, create it as a
  // zero-length file.
  // If CREATE is not specified and the Entry doesn't exist, fail.
  // If CREATE is not specified and the path exists, but is a directory, fail.
  EntrySync getFile(DOMString path, unsigned short options);
  DirectoryEntrySync makeDir(DOMString name);
};

[Constructor]
interface DirectoryReaderSync {
  // Holds state; subsequent calls return later Entries.
  // Returns null when all Entries have been returned.
  EntrySync[] readEntries();
};

[NoInterfaceObject]
interface FileEntrySync : EntrySync, File {
  // If APPEND is not specified, the file is truncated to length 0 as it's
  // opened.
  const unsigned short APPEND = 1;
  FileWriterSync createWriter([Optional] unsigned short options);
};

// TODO: Some way to query quota info.  Enforcing eventual and approximate quota
// correctness is a lot easier to do efficiently than doing it per-call, so I
// plan to leave some wiggle room there.

[1] http://lists.w3.org/Archives/Public/public-device-apis/2010Jan/0228.html
[2] http://dev.w3.org/2009/dap/file-system/file-dir-sys.html
[3] http://dev.w3.org/2006/webapi/FileAPI/

Received on Friday, 29 January 2010 21:33:03 UTC