Lo standard del file system introduce un file system privato dell'origine (OPFS) come endpoint di archiviazione privato dell'origine della pagina e non visibile all'utente che fornisce l'accesso facoltativo a un tipo speciale di file altamente ottimizzato per le prestazioni.
Supporto browser
Il file system privato dell'origine è supportato dai browser moderni ed è standardizzato dal Web Hypertext Application Technology Working Group (WHATWG) nello standard vivente del file system.
Motivazione
Quando pensi ai file sul computer, probabilmente pensi a una gerarchia di file: file organizzati in cartelle che puoi esplorare con Esplora file del sistema operativo. Ad esempio, su Windows, per un utente di nome Tom, l'elenco delle cose da fare potrebbe trovarsi in C:\Users\Tom\Documents\ToDo.txt
. In questo esempio, ToDo.txt
è il nome del file e Users
, Tom
e Documents
sono i nomi delle cartelle. `C:` su Windows rappresenta la directory principale dell'unità.
Modo tradizionale di lavorare con i file sul web
Per modificare l'elenco delle cose da fare in un'applicazione web, il flusso è il seguente:
- L'utente carica il file su un server o lo apre sul client con
<input type="file">
. - L'utente apporta le modifiche e poi scarica il file risultante con un
<a download="ToDo.txt>
inserito checlick()
in modo programmatico tramite JavaScript. - Per aprire le cartelle, utilizzi un attributo speciale in
<input type="file" webkitdirectory>
che, nonostante il nome proprietario, è supportato da quasi tutti i browser.
Un modo moderno di lavorare con i file sul web
Questo flusso non è rappresentativo di come gli utenti pensano di modificare i file e significa che gli utenti finiscono per scaricare copie dei file di input. Pertanto, l'API File System Access ha introdotto tre metodi di selezione: showOpenFilePicker()
, showSaveFilePicker()
e showDirectoryPicker()
, che fanno esattamente ciò che suggerisce il loro nome. Consentono un flusso come segue:
- Apri
ToDo.txt
conshowOpenFilePicker()
e ottieni un oggettoFileSystemFileHandle
. - Dall'oggetto
FileSystemFileHandle
, ottieni unFile
chiamando il metodogetFile()
dell'handle del file. - Modifica il file, poi chiama
requestPermission({mode: 'readwrite'})
sull'handle. - Se l'utente accetta la richiesta di autorizzazione, salva le modifiche nel file originale.
- In alternativa, chiama
showSaveFilePicker()
e lascia che l'utente scelga un nuovo file. Se l'utente sceglie un file aperto in precedenza, i relativi contenuti verranno sovrascritti. Per i salvataggi ripetuti, puoi conservare l'handle del file, in modo da non dover mostrare di nuovo la finestra di dialogo di salvataggio.
Limitazioni relative all'utilizzo dei file sul web
I file e le cartelle accessibili tramite questi metodi si trovano in quello che può essere definito il file system visibile all'utente. I file salvati dal web, e in particolare i file eseguibili, sono contrassegnati con il contrassegno del web, quindi il sistema operativo può mostrare un avviso aggiuntivo prima dell'esecuzione di un file potenzialmente pericoloso. Come funzionalità di sicurezza aggiuntiva, i file ottenuti dal web sono protetti anche da Navigazione sicura, che, per semplicità e nel contesto di questo articolo, puoi considerare come una scansione antivirus basata sul cloud. Quando scrivi dati in un file utilizzando l'API File System Access, le scritture non vengono eseguite sul posto, ma utilizzano un file temporaneo. Il file stesso non viene modificato a meno che non superi tutti questi controlli di sicurezza. Come puoi immaginare, questo lavoro rende le operazioni sui file relativamente lente, nonostante i miglioramenti applicati ove possibile, ad esempio su macOS. Tuttavia, ogni chiamata write()
è autonoma, quindi apre il file, cerca l'offset specificato e infine scrive i dati.
I file come base dell'elaborazione
Allo stesso tempo, i file sono un ottimo modo per registrare i dati. Ad esempio, SQLite archivia interi database in un unico file. Un altro esempio sono le mipmap utilizzate nell'elaborazione delle immagini. Le mipmap sono sequenze di immagini precalcolate e ottimizzate, ognuna delle quali è una rappresentazione a risoluzione progressivamente inferiore della precedente, il che rende più veloci molte operazioni come lo zoom. Quindi, come possono le applicazioni web ottenere i vantaggi dei file, ma senza i costi di rendimento dell'elaborazione dei file basata sul web? La risposta è il file system privato dell'origine.
Il file system privato visibile all'utente rispetto a quello di origine
A differenza del file system visibile all'utente esplorato utilizzando Esplora file del sistema operativo, con file e cartelle che puoi leggere, scrivere, spostare e rinominare, il file system privato dell'origine non è destinato a essere visto dagli utenti. Come suggerisce il nome, i file e le cartelle nel file system privato dell'origine sono privati e, più concretamente, privati per l'origine di un sito. Scopri l'origine di una pagina digitando location.origin
nella console DevTools. Ad esempio, l'origine della pagina https://developer.chrome.com/articles/
è https://developer.chrome.com
(ovvero la parte /articles
non fa parte dell'origine). Per saperne di più sulla teoria delle origini, consulta l'articolo Informazioni su "stesso sito" e "stessa origine". Tutte le pagine che condividono la stessa origine possono visualizzare gli stessi dati del file system privato dell'origine, quindi https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
può visualizzare gli stessi dettagli dell'esempio precedente. Ogni origine ha il proprio file system privato dell'origine indipendente, il che significa che il file system privato dell'origine di https://developer.chrome.com
è completamente distinto da quello di, ad esempio, https://web.dev
. Su Windows, la directory principale del file system visibile all'utente è C:\\
.
L'equivalente per il file system privato dell'origine è una directory principale inizialmente vuota per origine a cui si accede chiamando il metodo asincrono
navigator.storage.getDirectory()
.
Per un confronto tra il file system visibile all'utente e il file system privato dell'origine, vedi il seguente diagramma. Il diagramma mostra che, a parte la directory principale, tutto il resto è concettualmente uguale, con una gerarchia di file e cartelle da organizzare e disporre in base alle esigenze di dati e spazio di archiviazione.
Dettagli del file system privato dell'origine
Come altri meccanismi di archiviazione nel browser (ad esempio localStorage o IndexedDB), il file system privato dell'origine è soggetto a limitazioni di quota del browser. Quando un utente cancella tutti i dati di navigazione o tutti i dati dei siti, viene eliminato anche il file system privato dell'origine. Chiama navigator.storage.estimate()
e nell'oggetto di risposta risultante visualizza la voce usage
per vedere la quantità di spazio di archiviazione già consumato dalla tua app, suddiviso per meccanismo di archiviazione nell'oggetto usageDetails
, in cui devi esaminare in particolare la voce fileSystem
. Poiché il file system privato di origine non è visibile all'utente, non vengono visualizzati prompt di autorizzazione e non vengono eseguiti controlli di Navigazione sicura.
Ottenere l'accesso alla directory principale
Per accedere alla directory principale, esegui il comando seguente. Il risultato è un handle di directory vuoto, più precisamente un FileSystemDirectoryHandle
.
const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);
Thread principale o Web Worker
Esistono due modi per utilizzare il file system privato dell'origine: nel thread principale o in un Web Worker. I web worker non possono bloccare il thread principale, il che significa che in questo contesto le API possono essere sincrone, un pattern generalmente non consentito sul thread principale. Le API sincrone possono essere più veloci in quanto evitano di dover gestire le promesse e le operazioni sui file sono in genere sincrone in linguaggi come C che possono essere compilati in WebAssembly.
// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);
Se hai bisogno di operazioni sui file più rapide possibili o se utilizzi WebAssembly, vai a Utilizzare il file system privato dell'origine in un web worker. Altrimenti, puoi continuare a leggere.
Utilizza il file system privato dell'origine sul thread principale
Creare nuovi file e cartelle
Una volta creata una cartella principale, crea file e cartelle utilizzando rispettivamente i metodi getFileHandle()
e getDirectoryHandle()
. Se passi {create: true}
, il file o la cartella verrà creato se non esiste. Crea una gerarchia di file chiamando queste funzioni utilizzando una directory appena creata come punto di partenza.
const fileHandle = await opfsRoot
.getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
.getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
.getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
.getDirectoryHandle('my first nested folder', {create: true});
Accedere a file e cartelle esistenti
Se conosci il nome, accedi a file e cartelle creati in precedenza chiamando i metodi getFileHandle()
o getDirectoryHandle()
e passando il nome del file o della cartella.
const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
.getDirectoryHandle('my first folder');
Recupero del file associato a un handle di file per la lettura
Un FileSystemFileHandle
rappresenta un file nel file system. Per ottenere il File
associato, utilizza il metodo getFile()
. Un oggetto File
è un tipo specifico di Blob
e può essere utilizzato in qualsiasi contesto in cui può essere utilizzato un Blob
. In particolare, FileReader
, URL.createObjectURL()
, createImageBitmap()
e XMLHttpRequest.send()
accettano sia Blobs
che Files
. Se vuoi, l'ottenimento di un File
da un FileSystemFileHandle
"libera" i dati, in modo che tu possa accedervi e renderli disponibili al file system visibile all'utente.
const file = await fileHandle.getFile();
console.log(await file.text());
Scrivere in un file tramite streaming
Trasmetti i dati in streaming in un file chiamando createWritable()
, che crea un FileSystemWritableFileStream
a cui poi write()
i contenuti. Alla fine, devi close()
lo stream.
const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();
Eliminare file e cartelle
Elimina file e cartelle chiamando il metodo remove()
specifico dell'handle del file o della directory. Per eliminare una cartella, incluse tutte le sottocartelle, passa l'opzione {recursive: true}
.
await fileHandle.remove();
await directoryHandle.remove({recursive: true});
In alternativa, se conosci il nome del file o della cartella da eliminare in una directory, utilizza il metodo removeEntry()
.
directoryHandle.removeEntry('my first nested file');
Spostamento e ridenominazione di file e cartelle
Rinomina e sposta file e cartelle utilizzando il metodo move()
. Lo spostamento e la ridenominazione possono avvenire insieme o separatamente.
// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
.move(nestedDirectoryHandle, 'my first renamed and now nested file');
Risolvere il percorso di un file o di una cartella
Per scoprire dove si trova un determinato file o una determinata cartella rispetto a una directory di riferimento, utilizza il metodo resolve()
, passando un FileSystemHandle
come argomento. Per ottenere il percorso completo di un file o di una cartella nel file system privato di origine, utilizza la directory principale come directory di riferimento ottenuta tramite navigator.storage.getDirectory()
.
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
Controllare se due handle di file o cartelle puntano allo stesso file o alla stessa cartella
A volte hai due handle e non sai se puntano allo stesso file o alla stessa cartella. Per verificare se è questo il caso, utilizza il metodo isSameEntry()
.
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
Elencare i contenuti di una cartella
FileSystemDirectoryHandle
è un iteratore asincrono su cui iterare con un ciclo for await…of
. In quanto iteratore asincrono, supporta anche i metodi entries()
, values()
e keys()
, tra cui puoi scegliere a seconda delle informazioni che ti servono:
for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}
Elenca in modo ricorsivo i contenuti di una cartella e di tutte le sottocartelle
Gestire cicli e funzioni asincroni abbinati alla ricorsione è facile da sbagliare. La funzione riportata di seguito può fungere da punto di partenza per elencare i contenuti di una cartella e di tutte le relative sottocartelle, inclusi tutti i file e le relative dimensioni. Puoi semplificare la funzione se non hai bisogno delle dimensioni dei file, non inserendo la promessa handle.getFile()
, ma direttamente handle
, dove è indicato directoryEntryPromises.push
.
const getDirectoryEntriesRecursive = async (
directoryHandle,
relativePath = '.',
) => {
const fileHandles = [];
const directoryHandles = [];
const entries = {};
// Get an iterator of the files and folders in the directory.
const directoryIterator = directoryHandle.values();
const directoryEntryPromises = [];
for await (const handle of directoryIterator) {
const nestedPath = `${relativePath}/${handle.name}`;
if (handle.kind === 'file') {
fileHandles.push({ handle, nestedPath });
directoryEntryPromises.push(
handle.getFile().then((file) => {
return {
name: handle.name,
kind: handle.kind,
size: file.size,
type: file.type,
lastModified: file.lastModified,
relativePath: nestedPath,
handle
};
}),
);
} else if (handle.kind === 'directory') {
directoryHandles.push({ handle, nestedPath });
directoryEntryPromises.push(
(async () => {
return {
name: handle.name,
kind: handle.kind,
relativePath: nestedPath,
entries:
await getDirectoryEntriesRecursive(handle, nestedPath),
handle,
};
})(),
);
}
}
const directoryEntries = await Promise.all(directoryEntryPromises);
directoryEntries.forEach((directoryEntry) => {
entries[directoryEntry.name] = directoryEntry;
});
return entries;
};
Utilizzare il file system privato dell'origine in un web worker
Come indicato in precedenza, i web worker non possono bloccare il thread principale, motivo per cui in questo contesto sono consentiti metodi sincroni.
Ottenere un handle di accesso sincrono
Il punto di ingresso per le operazioni sui file più veloci possibili è un FileSystemSyncAccessHandle
, ottenuto da un normale FileSystemFileHandle
chiamando createSyncAccessHandle()
.
const fileHandle = await opfsRoot
.getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
Metodi sincroni di file in loco
Una volta ottenuto un handle di accesso sincrono, puoi accedere a metodi di file rapidi in loco che sono tutti sincroni.
getSize()
: restituisce le dimensioni del file in byte.write()
: scrive il contenuto di un buffer nel file, facoltativamente a un determinato offset, e restituisce il numero di byte scritti. Il controllo del numero restituito di byte scritti consente ai chiamanti di rilevare e gestire errori e scritture parziali.read()
: legge i contenuti del file in un buffer, facoltativamente a un determinato offset.truncate()
: ridimensiona il file in base alle dimensioni specificate.flush()
: garantisce che i contenuti del file contengano tutte le modifiche apportate tramitewrite()
.close()
: chiude la maniglia di accesso.
Di seguito è riportato un esempio che utilizza tutti i metodi menzionati sopra.
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();
// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();
// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));
// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));
// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));
// Truncate the file after 4 bytes.
accessHandle.truncate(4);
Copia un file dal file system privato di origine al file system visibile all'utente
Come accennato in precedenza, lo spostamento dei file dal file system privato di origine al file system visibile all'utente non è possibile, ma puoi copiarli. Poiché showSaveFilePicker()
è esposto solo nel thread principale, ma non nel thread Worker, assicurati di eseguire il codice lì.
// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
// Obtain a file handle to a new file in the user-visible file system
// with the same name as the file in the origin private file system.
const saveHandle = await showSaveFilePicker({
suggestedName: fileHandle.name || ''
});
const writable = await saveHandle.createWritable();
await writable.write(await fileHandle.getFile());
await writable.close();
} catch (err) {
console.error(err.name, err.message);
}
Eseguire il debug del file system privato dell'origine
Finché non viene aggiunto il supporto integrato di DevTools (vedi crbug/1284595), utilizza l'estensione Chrome OPFS Explorer per eseguire il debug del file system privato dell'origine. Lo screenshot riportato sopra nella sezione Creazione di nuovi file e cartelle è stato preso direttamente dall'estensione.
Dopo aver installato l'estensione, apri Chrome DevTools, seleziona la scheda OPFS Explorer e potrai ispezionare la gerarchia dei file. Salva i file dal file system privato di origine nel file system visibile all'utente facendo clic sul nome del file ed elimina file e cartelle facendo clic sull'icona del cestino.
Demo
Guarda il file system privato dell'origine in azione (se installi l'estensione OPFS Explorer) in una demo che lo utilizza come backend per un database SQLite compilato in WebAssembly. Assicurati di consultare il codice sorgente su Glitch. Tieni presente che la versione incorporata riportata di seguito non utilizza il backend del file system privato dell'origine (perché l'iframe è multiorigine), ma quando apri la demo in una scheda separata, lo fa.
Conclusioni
Il file system privato dell'origine, come specificato da WHATWG, ha plasmato il modo in cui utilizziamo e interagiamo con i file sul web. Ha reso possibili nuovi casi d'uso che erano impossibili da ottenere con il file system visibile all'utente. Tutti i principali fornitori di browser, Apple, Mozilla e Google, sono a bordo e condividono una visione comune. Lo sviluppo del file system privato dell'origine è un'iniziativa collaborativa e il feedback di sviluppatori e utenti è essenziale per il suo progresso. Mentre continuiamo a perfezionare e migliorare lo standard, sono graditi i feedback sul repository whatwg/fs sotto forma di problemi o richieste di pull.
Link correlati
- Specifiche standard del file system
- File System Standard repo
- Post sull'API File System con Origin Private File System WebKit
- Estensione OPFS Explorer
Ringraziamenti
Questo articolo è stato esaminato da Austin Sully, Etienne Noël e Rachel Andrew. Immagine hero di Christina Rumpf su Unsplash.