'How to store large files to web local storage?
I am developing ionic 2 app that downloads files from online servers, I want to store those files to the local storage (or caches I don't know).
I know that I can store data to local storage using:
localStorage.setItem('key','value');
But if it is file and specially a large one, how to store it in local storage?
Note: I am using TypeScript.
Solution 1:[1]
For storing relatively large data, I'd recommend using IndexedDB.
Advantages over local storage:
- Larger size limits
- Supports other data types than string (native typed arrays!)
- Asynchronous
Solution 2:[2]
For your consideration in 2022: Origin Private File System
As of early-2022 IndexedDB remains the best overall choice for persisting binary data (e.g. images and videos, user files from <input type="file">, JS File and Blob objects, etc) due to its support for both asynchronous IO and ability to save raw binary data without needing ugly hacks like Base64-encoding for data: URIs.
However, IndexedDB has probably one of the least well-designed API surfaces in HTML5 ("you wire-up event-handlers after you trigger the event? crazy!), and the browser-vendors agree: so Apple's Safari and Google's Chromium both now support a simpler data persistence API named Origin Private File System (OPFS) which was recently introduced in 2021.
- Google Chrome supports it since Chrome 102.
- Apple Safari supports it since macOS 12.2+ and iOS 15.2+.
- As of February 2022, Mozilla Firefox does not support it: Mozilla's devs have reservations but might still end-up supporting it, hopefully soon.
OPFS is a subset of the larger WHATWG File System API and is also related to the extant WHATWG Storage API (which defines
localStorage,indexedDB, etc).Despite OPFS's name containing the term "File System", OPFS does not involve any access to the user's actual local computer file-system: think of it more as an isolated virtual or "pretend" file-system that's scoped to a single website (specifically, scoped to a single origin), so different websites/origins cannot access other websites' OPFS storage.
OPFS lacks an initial fixed-size storage quota, whereas
localStorageandsessionStorageboth have an initial limit of 5MB.- However this is not an invitation to store gigabytes of junk on users' computers in OPFS: browsers impose their own soft limits, and if you hit the limit your write operations to OPFS will throw
QuotaExceededErrorwhich you should be prepared to handle gracefully.
- However this is not an invitation to store gigabytes of junk on users' computers in OPFS: browsers impose their own soft limits, and if you hit the limit your write operations to OPFS will throw
Because OPFS doesn't involve the user's actual local filesystem, and because the space is segregated by origin (URI-scheme + hostname + port), it means that OPFS doesn't present a risk of users' privacy or computer security, so browsers won't interrupt your script to show your users a potentially scary-looking security-prompt that the user has to click-through to explicitly grant or deny your website or page's ability to use the OPFS API.
- Though browsers could still add such a prompt in future if they choose, and of course, browsers can always just disable OPFS and reject all attempts to use it, just as with
indexedDBandlocalStoragetoday, or even pretend to persist data between page-loads or sessions (e.g. as in Chrome's Incognito mode).
- Though browsers could still add such a prompt in future if they choose, and of course, browsers can always just disable OPFS and reject all attempts to use it, just as with
Using OPFS:
Apple have detailed usage examples in the WebKit blog.
As a brief introduction: you use OPFS via navigator.storage and awaiting getDirectory() to get a FileSystemHandle object representing the root directory of your website (origin)'s storage space, and then await methods on the root to create and manipulate files and subdirectories.
For example, supposing you have a <canvas> element with some oh-so fantastic original art your user painted by hand and wants to save locally as a PNG blob in the browser's OPFS storage space, then you'd do something like this:
async function doOpfsDemo() {
// Open the "root" of the website's (origin's) private filesystem:
let storageRoot = null;
try {
storageRoot = await navigator.storage.getDirectory();
}
catch( err ) {
console.error( err );
alert( "Couldn't open OPFS. See browser console.\n\n" + err );
return;
}
// Get the <canvas> element from the page DOM:
const canvasElem = document.getElementById( 'myCanvas' );
// Save the image:
await saveCanvasToPngInOriginPrivateFileSystem( storageRoot, canvasElem );
// (Re-)load the image:
await loadPngFromOriginPrivateFileSystemIntoCanvas( storageRoot, canvasElem );
}
async function saveCanvasToPngInOriginPrivateFileSystem( storageRoot, canvasElem ) {
// Save the <canvas>'s image to a PNG file to an in-memory Blob object: (see https://stackoverflow.com/a/57942679/159145 ):
const imagePngBlob = await new Promise(resolve => canvasElem.toBlob( resolve, 'image/png' ) );
// Create an empty (zero-byte) file in a new subdirectory: "art/mywaifu.png":
const newSubDir = await storageRoot.getDirectoryHandle( "art", { "create" : true });
const newFile = await newSubDir.getFileHandle( "mywaifu.png", { "create" : true });
// Open the `mywaifu.png` file as a writable stream ( FileSystemWritableFileStream ):
const wtr = await newFile.createWritable();
try {
// Then write the Blob object directly:
await wtr.write( imagePngBlob );
}
finally {
// And safely close the file stream writer:
await wtr.close();
}
}
async function loadPngFromOriginPrivateFileSystemIntoCanvas( storageRoot, canvasElem ) {
const artSubDir = await storageRoot.getDirectoryHandle( "art" );
const savedFile = await artSubDir.getFileHandle( "mywaifu.png" ); // Surprisingly there isn't a "fileExists()" function: instead you need to iterate over all files, which is odd... https://wicg.github.io/file-system-access/
// Get the `savedFile` as a DOM `File` object (as opposed to a `FileSystemFileHandle` object):
const pngFile = await savedFile.getFile();
// Load it into an ImageBitmap object which can be painted directly onto the <canvas>. You don't need to use URL.createObjectURL and <img/> anymore. See https://developer.mozilla.org/en-US/docs/Web/API/createImageBitmap
// But you *do* still need to `.close()` the ImageBitmap after it's painted otherwise you'll leak memory. Use a try/finally block for that.
try {
const loadedBitmap = await createImageBitmap( pngFile ); // `createImageBitmap()` is a global free-function, like `parseInt()`. Which is unusual as most modern JS APIs are designed to not pollute the global scope.
try {
const ctx = canvasElem.getContext('2d');
ctx.clearRect( /*x:*/ 0, /*y:*/ 0, ctx.canvas.width, ctx.canvas.height ); // Clear the canvas before drawing the loaded image.
ctx.drawImage( loadedBitmap, /*x:*/ 0, /*y:*/ 0 ); // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
}
finally {
loadedBitmap.close(); // https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap/close
}
}
catch( err ) {
console.error( err );
alert( "Couldn't load previously saved image into <canvas>. See browser console.\n\n" + err );
return;
}
}
Remember that OPFS filesystem spaces are not representative of their underlying filesystem, if any, on the user's computer: an extant
FileSystemFileHandlein an OPFS space will not necessarily exist in the user's real file-system.- In fact, it's more likely they'll be physically persisted the same way as IndexedDB blobs are: as a range of bytes nested inside some other data-store file, e.g. Chromium-based browsers use their own database-engine named LevelDB while Safari seems to use a journaled Sqlite DB for their IndexedDB backing-store. You can see them yourself: On Windows, Chrome/Chromium/Edge stores LevelDB database files for IndexedDB at
C:\Users\{you}\AppData\Local\Google\Chrome\User Data\Default\IndexedDB.
- In fact, it's more likely they'll be physically persisted the same way as IndexedDB blobs are: as a range of bytes nested inside some other data-store file, e.g. Chromium-based browsers use their own database-engine named LevelDB while Safari seems to use a journaled Sqlite DB for their IndexedDB backing-store. You can see them yourself: On Windows, Chrome/Chromium/Edge stores LevelDB database files for IndexedDB at
OPFS is also very simple: while your computer's NTFS, ZFS, or APFS file-system will have features like permissions/ACLs, ownership, low-level (i.e. block-level) access, snapshotting and history, and more, OPFS lacks all of those: OPFS doesn't even have a file-locking mechanism, which is usually considered an essential primitive FS operation.
- Obviously this keeps OPFS's API surface simpler: making it easier to implement and easier to use than otherwise, not to mention shrinking the potential attack surface, but it does mean that OPFS really does come-across more as a kind-of "local AWS S3" or Azure Blob Storage, which ultimately is still good enough for 95% of applications out there... just don't expect to be able to run a high-performance concurrent write-ahead-logged database server in JavaScript on OPFS.
Comparison of browser-based JavaScript File System APIs:
But don't confuse OPFS with the other filesystem and filesystem-esque APIs:
- MDN has a detailed rundown on their "File System Access API" page (though I feel the page is misnamed, as it covers multiple distinct separate API surfaces, and the way it's written implies some features (like
Window.showOpenFilePicker()) have wide-support when the reality is quite the opposite. - Google's web.dev site also has a rundown of their own.
- Using an
<input type="file">in HTML withHTMLInputElement.filesin script allows pages to prompt the user to select a single extant file, or multiple files (with<input type="file" multiple>, or even an entire directory tree - all of which expose those files to page scripts via theFileinterface. - Google's older and now deprecated (but still supported)
chrome.fileSystemAPI, originally intended for Chrome Apps and browser-extensions, webpage scripts may also use it viawindow.requestFileSystem()orwindow.webkitRequestFileSystem(). - The File and Directory Entries API, which has wide browser support for read-only access to the user's local computer filesystem.
- Note that this API is distinct from, but extends, the original W3C File API which also defines the
Fileinterface.- Also note that both the File API and File and Directory Entries API are largely read-only: so you normally cannot edit, overwrite, delete, or otherwise mutate files nor the underlying filesystem: you can only load/read those files and mutate an in-memory copy of the file, and then explicitly prompt the user to save any changes or new output via
HTMLAnchorElement.download, which is about as awkward as it sounds. - You can create, truncate, edit, and append to existing files with
createWritable/FileSystemWritableFileStream, but only Chromium-based browsers currently support it (so there's no Firefox or Safari support).
- Also note that both the File API and File and Directory Entries API are largely read-only: so you normally cannot edit, overwrite, delete, or otherwise mutate files nor the underlying filesystem: you can only load/read those files and mutate an in-memory copy of the file, and then explicitly prompt the user to save any changes or new output via
- Note that this API is distinct from, but extends, the original W3C File API which also defines the
- The HTML Drag-and-Drop API (which concerns use-cases where users drag a file from their desktop, e.g. from Windows File Explorer or macOS Finder and drop it onto some
<div>in a web-page in a browser window): by listening to thedraganddragoverevents scripts can access the dragged data, which may be files, but can also be other types of data).- This API functions similarly to
HTMLInputElement.filesin that it doesn't confer any write-access to any files that were dragged-and-dropped by users.
- This API functions similarly to
Solution 3:[3]
Browsers have limitation on how much data you can store in local storage. It varies from browser to browser.
If your file size is under the limit, you could copy the file content as a string and save it as a key value pair of "filename":"file content"
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | |
| Solution 3 | Community |
