Il file system privato di origine

Lo standard del file system introduce un file system privato di origine (OPFS) come endpoint di archiviazione privato per l'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 di origine è supportato dai browser moderni ed è standardizzato dal Web Hypertext Application Technology Working Group (WHATWG) nello standard File System Living.

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

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, il suo elenco di 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 nomi di cartelle. `C:` su Windows rappresenta la directory principale dell'unità.

Modo tipico di lavorare con i file sul web

Per modificare l'elenco di cose da fare in un'applicazione web, il flusso è il seguente:

  1. L'utente carica il file su un server o lo apre sul client con <input type="file">.
  2. L'utente apporta le modifiche, quindi scarica il file risultante con un inserito <a download="ToDo.txt> che fai click() con JavaScript.
  3. Per aprire le cartelle, utilizzi un attributo speciale in <input type="file" webkitdirectory>, che, nonostante il nome proprietario, ha un supporto browser praticamente universale.

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 avere copie scaricate dei file di input. Pertanto, l'API File System Access ha introdotto tre metodi di selezione:

Consentono un flusso come segue:

  1. Apri ToDo.txt con showOpenFilePicker() e ottieni un FileSystemFileHandle oggetto.
  2. Dall'FileSystemFileHandle oggetto, ottieni un File chiamando il metodo getFile() dell'handle del file.
  3. Modifica il file, quindi chiama requestPermission({mode: 'readwrite'}) sull'handle.
  4. Se l'utente accetta la richiesta di autorizzazione, salva le modifiche nel file originale.
  5. 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 del file.

Limitazioni relative all'utilizzo dei file sul web

I file e le cartelle accessibili con questi metodi si trovano in quello che può essere definito il file system visibile all'utente. I file salvati dal web e i file eseguibili in particolare sono contrassegnati con il segno 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 documento, 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 in loco, ma utilizzano un file temporaneo. Il file stesso non viene modificato a meno che non superi tutti questi controlli di sicurezza.

Questo lavoro rende le operazioni sui file relativamente lente, nonostante i miglioramenti applicati ove possibile, ad esempio su macOS. Tuttavia, ogni write() chiamata è autonoma, quindi sotto il cofano 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 modo eccellente per registrare i dati. Ad esempio, SQLite archivia interi database in un singolo 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, in che modo le applicazioni web possono ottenere i vantaggi dei file, ma senza i costi di prestazioni dell'elaborazione dei file basata sul web? La risposta è il file system privato di origine.

File system visibile all'utente e file system privato 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 di origine non è destinato a essere visto dagli utenti. I file e le cartelle nel file system privato di origine, come suggerisce il nome, 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. Scopri di più sulla teoria delle origini in Understanding "same-site" and "same-origin".

Tutte le pagine che condividono la stessa origine possono visualizzare gli stessi dati del file system privato di 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 di origine indipendente, il che significa che il file system privato di 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 di 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 di origine, consulta il seguente diagramma. Il diagramma mostra che, a parte la directory principale, tutto il resto è concettualmente lo stesso, con una gerarchia di file e cartelle da organizzare e disporre in base alle esigenze di dati e archiviazione.

Diagramma del file system visibile all'utente e del file system privato di origine con due gerarchie di file esemplificative.
Il punto di ingresso per il file system visibile all'utente è un disco rigido simbolico, il punto di ingresso per il file system privato di origine è la chiamata del metodo navigator.storage.getDirectory.

Specifiche del file system privato di origine

Come altri meccanismi di archiviazione nel browser (ad esempio, localStorage o IndexedDB), il file system privato di origine è soggetto a limitazioni di quota del browser. Quando un utente cancella tutti i dati di navigazione o tutti i dati del sito, viene eliminato anche il file system privato di origine.

Chiama navigator.storage.estimate() e nell'oggetto di risposta risultante visualizza la usage voce per vedere la quantità di spazio di archiviazione già utilizzata dall'app, suddivisa per meccanismo di archiviazione nell'oggetto usageDetails, dove devi esaminare in modo specifico la voce fileSystem. Poiché il file system privato di origine non è visibile all'utente, non vengono visualizzati prompt di autorizzazione né controlli di Navigazione sicura.

Ottenere l'accesso alla directory principale

Per accedere alla directory principale, esegui il seguente comando. Viene visualizzato 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 worker web

Esistono due modi per utilizzare il file system privato di origine: sul thread principale o in un worker web. I worker web 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 perché 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 delle operazioni sui file più veloci possibili o se utilizzi WebAssembly, vai a Utilizzare il file system privato di origine in un worker web.

Utilizzare il file system privato di origine sul thread principale

Creare nuovi file e cartelle

Dopo aver creato una cartella principale, crea file e cartelle utilizzando i metodi getFileHandle() e getDirectoryHandle() rispettivamente. Se passi {create: true}, il file o la cartella verrà creato se non esiste. Crea una gerarchia di file chiamando queste funzioni utilizzando come punto di partenza una directory appena creata.

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});

La gerarchia di file risultante dall'esempio di codice precedente.

Accedere a file e cartelle esistenti

Se conosci il nome, accedi ai file e alle cartelle creati in precedenza chiamando i metodi getFileHandle() o getDirectoryHandle(), passando il nome del file o della cartella.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Ottenere il file associato a un handle di file per la lettura

Un FileSystemFileHandle rappresenta un file nel file system. Per ottenere l' associato File, utilizza il getFile() metodo. 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 sia Files. 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 in 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' {recursive: true} opzione.

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 removeEntry() metodo.

directoryHandle.removeEntry('my first nested file');

Spostare e rinominare file e cartelle

Rinomina e sposta file e cartelle utilizzando il move() metodo. Lo spostamento e la ridenominazione possono avvenire insieme o in isolamento.

// 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 cartella rispetto a una directory di riferimento, utilizza il resolve() metodo, passandogli 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 utilizzando navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Verificare 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 è così, utilizza il isSameEntry() metodo.

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Elencare i contenuti di una cartella

FileSystemDirectoryHandle è un iteratore asincrono su cui puoi eseguire l'iterazione con un for await... of loop. Come iteratore asincrono, supporta anche i entries(), metodi values(), e keys(), tra cui puoi scegliere a seconda delle informazioni di cui hai bisogno:

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()) {}

Elencare in modo ricorsivo i contenuti di una cartella e di tutte le sottocartelle

È facile sbagliare quando si utilizzano loop e funzioni asincroni abbinati alla ricorsione. La seguente funzione 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 handle.getFile() promessa in directoryEntryPromises.push, ma il handle direttamente.

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 di origine in un worker web

Come indicato in precedenza, i worker web non possono bloccare il thread principale, motivo per cui in questo contesto sono consentiti i 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 di file sincroni in loco

Una volta che hai un handle di accesso sincrono, puoi accedere a metodi di file in loco veloci 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 di byte scritti restituiti 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 tramite write().
  • close(): chiude l'handle di accesso.

Ecco un esempio che utilizza ogni metodo.

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);

Copiare un file dal file system privato di origine al file system visibile all'utente

Come accennato in precedenza, non è possibile spostare i file dal file system privato di origine al file system visibile all'utente, ma puoi copiarli. Poiché showSaveFilePicker() è esposto solo sul thread principale, ma non nel thread del 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 di origine

Finché non viene aggiunto il supporto integrato di DevTools (vedi crbug/1284595), utilizza l'estensione di Chrome OPFS Explorer per eseguire il debug del file system privato di origine. Lo screenshot da la sezione Creare nuovi file e cartelle è stato tratto direttamente dall'estensione.

L'estensione OPFS Explorer Chrome DevTools nel Chrome Web Store.

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 al 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 di 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 controllare il codice sorgente su GitHub. Tieni presente che la versione incorporata non utilizza il backend del file system privato di origine (perché l'iframe è multiorigine), ma quando apri la demo in una scheda separata, lo fa.

Conclusione

Il file system privato di origine, come specificato dal WHATWG, ha plasmato il modo in cui utilizziamo e interagiamo con i file sul web. Ha consentito 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 di origine è uno sforzo di collaborazione 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.

Ringraziamenti

Questo documento è stato esaminato da Austin Sully, Etienne Noël e Rachel Andrew.