15

Working on an app that uses the new(ish) File System Access API, and I wanted to save the fileHandles of recently loaded files, to display a "Recent Files..." menu option and let a user load one of these files without opening the system file selection window.

This article has a paragraph about storing fileHandles in IndexedDB and it mentions that the handles returned from the API are "serializable," but it doesn't have any example code, and JSON.stringify won't do it.

File handles are serializable, which means that you can save a file handle to IndexedDB, or call postMessage() to send them between the same top-level origin.

Is there a way to serialize the handle other than JSON? I thought maybe IndexedDB would do it automatically but that doesn't seem to work, either.

Charles Johnson
  • 691
  • 1
  • 7
  • 19

3 Answers3

18

Here is a minimal example that demonstrates how to store and retrieve a file handle (a FileSystemHandle to be precise) in IndexedDB (the code uses the idb-keyval library for brevity):

import { get, set } from 'https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js';

const pre = document.querySelector('pre');
const button = document.querySelector('button');

button.addEventListener('click', async () => {
  try {
    const fileHandleOrUndefined = await get('file');    
    if (fileHandleOrUndefined) {      
      pre.textContent =
          `Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    // This always returns an array, but we just need the first entry.
    const [fileHandle] = await window.showOpenFilePicker();
    await set('file', fileHandle);    
    pre.textContent =
        `Stored file handle for "${fileHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

I have created a demo that shows the above code in action.

DenverCoder9
  • 2,024
  • 11
  • 32
  • Thank you, that's very helpful. One more question: do you know if it's possible to store the handle in the chrome.storage.local DB? Or is IndexedDB the only way that will preserve the full object? – Charles Johnson Jan 28 '21 at 18:12
  • 2
    Most probably not, since it is modeled after local storage, which converts to string, which means it can’t serialize a file handle. – DenverCoder9 Jan 28 '21 at 22:13
  • DenverCoder9 I tested this demo on my android mobile(chrome) but, It did not work out. Any idea how can I get the same effect using Chrome on my mobile? – JohanEcAv Sep 09 '22 at 17:30
  • 1
    @JohanEcAv The File System Access API is currently not supported on Android. – DenverCoder9 Sep 11 '22 at 09:50
  • @DenverCoder9 There is another way to get it? another API or library? :( – JohanEcAv Sep 12 '22 at 13:38
  • 1
    @JohanEcAv None that I'd be aware of, since simply the underlying concept isn't there currently. – DenverCoder9 Sep 13 '22 at 17:04
8

When a platform interface is [Serializable], it means it has associated internal serialization and deserialization rules that will be used by APIs that perform the “structured clone” algorithm to create “copies” of JS values. Structured cloning is used by the Message API, as mentioned. It’s also used by the History API, so at least in theory you can persist FileHandle objects in association with history entries.

In Chromium at the time of writing, FileHandle objects appear to serialize and deserialize successfully when used with history.state in general, e.g. across reloads and backwards navigation. Curiously, it seems deserialization may silently fail when returning to a forward entry: popStateEvent.state and history.state always return null when traversing forwards to an entry whose associated state includes one or more FileHandles. This appears to be a bug.

History entries are part of the “session” storage “shelf”. Session here refers to (roughly) “the lifetime of the tab/window”. This can sometimes be exactly what you want for FileHandle (e.g. upon traversing backwards, reopen the file that was open in the earlier state). However it doesn’t help with “origin shelf” lifetime storage that sticks around across multiple sessions. The only API that can serialize and deserialize FileHandle for origin-level storage is, as far as I’m aware, IndexedDB.

Semicolon
  • 6,793
  • 2
  • 30
  • 38
1

For those using Dexie to interface with IndexedDB, you will get an empty object unless you leave the primary key unnamed ('not inbound'):

db.version(1).stores({
  test: '++id'
});

const [fileHandle] = await window.showOpenFilePicker();
db.test.add({ fileHandle })

This results in a record with { fileHandle: {} } (empty object)

However, if you do not name the primary key, it serializes the object properly:

db.version(1).stores({
  test: '++'
});

const [fileHandle] = await window.showOpenFilePicker();
db.test.add({ fileHandle })

Result: { fileHandle: FileSystemFileHandle... }

image

This may be a bug in Dexie, as reported here: https://github.com/dfahlander/Dexie.js/issues/1236

James Gentes
  • 7,528
  • 7
  • 44
  • 64