Reputation: 741
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.
Upvotes: 18
Views: 6635
Reputation: 4276
I made these functions exclusively for storing and retrieving a FileSystemDirectoryHandle
/** @type {Promise<IDBDatabase>} */
const dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open('handle', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('handles', { keyPath: 'id' });
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(event);
reject(event.target.error);
};
});
/**
* Saves a directory handle to the database.
* @param {FileSystemDirectoryHandle} dirHandle - The directory handle to save.
* @returns {Promise<void>} A promise that resolves when the handle is saved.
*/
export const saveDirHandle = (dirHandle) => {
return new Promise((resolve, reject) => {
dbPromise.then((db) => {
const transaction = db.transaction(['handles'], 'readwrite');
const store = transaction.objectStore('handles');
const request = store.put({ id: 'lastFolder', handle: dirHandle });
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
});
};
/**
* Retrieves the last saved folder handle from the database.
* @returns {Promise<FileSystemDirectoryHandle|undefined>} A promise that resolves with the folder handle or undefined if not found.
*/
export const getLastFolderHandle = () => {
return new Promise((resolve, reject) => {
dbPromise.then((db) => {
const transaction = db.transaction(['handles'], 'readonly');
const store = transaction.objectStore('handles');
const request = store.get('lastFolder');
request.onsuccess = (event) => resolve(event.target.result?.handle);
request.onerror = () => reject(request.error);
});
});
};
Upvotes: 0
Reputation: 7423
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.
Upvotes: 9
Reputation: 8086
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... }
This may be a bug in Dexie, as reported here: https://github.com/dfahlander/Dexie.js/issues/1236
Upvotes: 1
Reputation: 2539
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/[email protected]/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.
Upvotes: 22