Il file system privato di origine

Lo standard 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 di origine è supportato dai browser moderni ed è standardizzato dal Web Hypertext Application Technology Working Group (WHATWG) nel File System Living Standard.

Supporto dei browser

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

Origine

Motivazione

Quando pensi ai file sul tuo computer, probabilmente pensi a una gerarchia di file: file organizzati in cartelle che puoi esplorare con l'esploratore file del tuo sistema operativo. Ad esempio, su Windows, l'elenco di cose da fare di un utente di nome Tom 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à.

Metodo tradizionale di lavoro con i file sul web

Per modificare la lista di cose da fare in un'applicazione web, il flusso abituale è 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 e poi scarica il file risultante con un <a download="ToDo.txt> iniettato che click() in modo programmatico tramite JavaScript.
  3. Per aprire le cartelle, utilizza un attributo speciale in <input type="file" webkitdirectory>, che, nonostante il nome proprietario, ha un supporto del browser praticamente universale.

Un modo moderno di lavorare con i file sul web

Questo flusso non è rappresentativo del modo in cui gli utenti pensano di modificare i file e significa che gli utenti finiscono per scaricare copie dei file di input. Di conseguenza, l'API File System Access ha introdotto tre metodi di selettore, showOpenFilePicker(), showSaveFilePicker() e showDirectoryPicker(), che fanno esattamente ciò che suggerisce il nome. Consentono un flusso come segue:

  1. Apri ToDo.txt con showOpenFilePicker() e ottieni un oggetto FileSystemFileHandle.
  2. Dall'oggetto FileSystemFileHandle, ottieni un File chiamando il metodo getFile() dell'handle 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 il 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 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 dal segno del web, pertanto il sistema operativo può mostrare un avviso aggiuntivo prima che venga eseguito 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 su cloud. Quando scrivi dati in un file utilizzando l'API File System Access, le scritture non sono in-place, 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() è autosufficiente, 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 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 ottimizzate e precalcolate, ognuna delle quali è una rappresentazione con risoluzione progressivamente inferiore della precedente, il che rende più veloci molte operazioni come lo zoom. Quindi, come possono le applicazioni web usufruire dei vantaggi dei file, ma senza i costi in termini di prestazioni dell'elaborazione dei file basata sul web? La risposta è il file system privato di origine.

Il file system visibile all'utente rispetto al file system privato dell'origine

A differenza del file system visibile all'utente che viene visualizzato 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. Come suggerisce il nome, i file e le cartelle nel file system privato dell'origine sono privati e, più concretamente, privati dell'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 Informazioni su "stessa proprietà" e "stessa origine". Tutte le pagine che condividono la stessa origine possono vedere gli stessi dati del file system privato dell'origine, quindi https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ può vedere gli stessi dettagli dell'esempio precedente. Ogni origine ha il proprio file system privato indipendente, il che significa che il file system privato di https://developer.chrome.com è completamente distinto da quello di, ad esempio, https://web.dev. Su Windows, la home directory del file system visibile all'utente è C:\\. L'equivalente per il file system privato dell'origine è una directory principale inizialmente vuota per ogni 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 uguale, con una gerarchia di file e cartelle da organizzare e disporre in base alle tue esigenze di dati e spazio di archiviazione.

Diagramma del file system visibile all&#39;utente e del file system privato di origine con due gerarchie di file di esempio. Il punto di contatto per il file system visibile all&#39;utente è un hard disk simbolico, mentre il punto di contatto per il file system privato di origine è la chiamata del metodo &quot;navigator.storage.getDirectory&quot;.

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 alle limitazioni delle quote del browser. Quando un utente cancella tutti i dati di navigazione o tutti i dati dei siti, viene eliminato anche il file system privato di origine. Chiama navigator.storage.estimate() e nell'oggetto di risposta risultante controlla la voce usage per vedere quanto spazio di archiviazione consuma già la tua app, suddiviso per meccanismo di archiviazione nell'oggetto usageDetails, dove vuoi esaminare specificamente la voce fileSystem. Poiché il file system privato di origine non è visibile all'utente, non vengono visualizzate richieste di autorizzazione né controlli di Navigazione sicura.

Accedere alla home directory

Per ottenere l'accesso alla home directory, esegui il seguente comando. Il risultato è un handle della 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 di 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 nel 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 utilizzi WebAssembly, vai a Utilizzare il file system privato di origine in un worker web. In caso contrario, puoi continuare a leggere.

Utilizza il file system privato dell'origine nel 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});

La gerarchia dei file risultante dall&#39;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 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. In altre parole, l'ottenimento di un File da un FileSystemFileHandle "libera" i dati, in modo da potervi accedere e renderli disponibili per il file system visibile all'utente.

const file = await fileHandle.getFile();
console.log(await file.text());

Scrivere in un file tramite streaming

Esegui lo streaming dei dati in un file chiamando createWritable(), che crea un FileSystemWritableFileStream a cui poi write() i contenuti. Al termine, 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');

Spostare e rinominare file e cartelle

Rinomina e sposta file e cartelle utilizzando il metodo move(). Lo spostamento e la ridenominazione possono essere eseguiti insieme o singolarmente.

// 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 sapere dove si trova un determinato file o una determinata cartella in relazione 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']`.

Controlla se due handle di file o cartelle rimandano allo stesso file o alla stessa cartella

A volte hai due handle e non sai se rimandano allo stesso file o alla stessa cartella. Per verificare se è così, utilizza il metodo isSameEntry().

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

Elencare i contenuti di una cartella

FileSystemDirectoryHandle è un iteratore asincrono su cui esegui l'iterazione con un ciclo for await…of. In qualità di iteratore asincrono, supporta anche i metodi entries(), values() e keys(), tra cui puoi scegliere in base alle 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()) {}

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

È facile commettere errori quando si utilizzano funzioni e loop asincroni abbinati alla ricorsione. La funzione riportata di seguito può essere utilizzata come 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, dove è indicato directoryEntryPromises.push, non inviando la promessa handle.getFile(), ma direttamente handle.

  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 worker web

Come accennato in precedenza, i web worker 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 in-place sincroni

Una volta ottenuto un handle di accesso sincrono, puoi accedere a metodi di file in-place rapidi che sono tutti sincronici.

  • getSize(): restituisce le dimensioni del file in byte.
  • write(): scrive il contenuto di un buffer nel file, eventualmente 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, eventualmente a un determinato offset.
  • truncate(): ridimensiona il file alle dimensioni specificate.
  • flush(): garantisce che i contenuti del file contengano tutte le modifiche apportate tramite write().
  • close(): chiude l'handle di accesso.

Di seguito è riportato un esempio che utilizza tutti i metodi sopra menzionati.

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() è visibile solo nel thread principale, ma non nel thread di lavoro, 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

Fino all'aggiunta del supporto di DevTools integrato (vedi crbug/1284595), utilizza l'estensione di Chrome OPFS Explorer per eseguire il debug del file system privato di origine. A proposito, lo screenshot sopra riportato nella sezione Creazione di nuovi file e cartelle è stato preso direttamente dall'estensione.

L&#39;estensione OPFS Explorer di Chrome DevTools nel Chrome Web Store.

Dopo aver installato l'estensione, apri Chrome DevTools, seleziona la scheda OPFS Explorer e potrai iniziare a 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

Prova il file system privato di origine (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 Glitch. Tieni presente che la versione incorporata di seguito non utilizza il backend del file system privato di origine (poiché l'iframe è cross-origin), ma lo fa quando apri la demo in una scheda separata.

Conclusioni

Il file system privato di origine, come specificato da 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, ovvero Apple, Mozilla e Google, sono coinvolti e condividono una visione comune. Lo sviluppo del file system privato di origine è un lavoro collaborativo e il feedback di sviluppatori e utenti è essenziale per il suo avanzamento. Mentre continuiamo a perfezionare e migliorare lo standard, sono ben accetti i feedback sul repository whatwg/fs sotto forma di problemi o richieste pull.

Ringraziamenti

Questo articolo è stato esaminato da Austin Sully, Etienne Noël e Rachel Andrew. Immagine hero di Christina Rumpf su Unsplash.