Il file system privato di origine

Lo standard di 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 del 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

  • 86
  • 86
  • 111
  • 15.2

Origine

Motivazione

Quando pensi ai file sul tuo computer, probabilmente ti viene in mente una gerarchia di file: file organizzati in cartelle che puoi esplorare con l'esplorazione file del tuo sistema operativo. Ad esempio, su Windows, per un utente chiamato Tommaso, l'elenco delle cose da fare potrebbe trovarsi in C:\Users\Tom\Documents\ToDo.txt. In questo esempio, ToDo.txt è il nome del file, mentre Users, Tom e Documents sono nomi di cartella. "C:" su Windows rappresenta la directory principale dell'unità.

Modo tradizionale di lavorare con i file sul web

Per modificare l'elenco di cose da fare in un'applicazione web, segui la procedura solita:

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

Un modo moderno di lavorare con i file sul web

Questo flusso non è rappresentativo del modo in cui gli utenti pensano alla modifica dei file, pertanto gli utenti finiranno per ricevere copie dei file di input scaricate. Pertanto, l'API File System Access ha introdotto tre metodi di selettore: showOpenFilePicker(), showSaveFilePicker() e showDirectoryPicker(), che fanno esattamente ciò che suggerisce il loro nome. Attivano un flusso come segue:

  1. Apri ToDo.txt con showOpenFilePicker() e recupera un oggetto FileSystemFileHandle.
  2. Dall'oggetto FileSystemFileHandle, ottieni 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 consenti all'utente di scegliere un nuovo file. Se l'utente seleziona un file aperto in precedenza, i relativi contenuti verranno sovrascritti. Per i salvataggi ripetuti, puoi mantenere l'handle del file a portata di mano, senza dover visualizzare di nuovo la finestra di dialogo per il salvataggio.

Limitazioni relative all'utilizzo dei file sul web

I file e le cartelle accessibili tramite questi metodi risiedono 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 marchio del web, quindi il sistema operativo potrebbe visualizzare un ulteriore avviso prima che venga eseguito un file potenzialmente pericoloso. Come ulteriore funzionalità di sicurezza, i file ottenuti dal web sono protetti anche da Navigazione sicura che, per semplicità e nel contesto di questo articolo, può essere considerata una scansione antivirus basata su cloud. Quando scrivi dati in un file utilizzando l'API File System Access, le scritture non vengono eseguite, ma utilizzano un file temporaneo. Il file non viene modificato se non supera tutti questi controlli di sicurezza. Come puoi immaginare, questo lavoro rende le operazioni con i file relativamente lente, nonostante i miglioramenti applicati ove possibile, ad esempio su macOS. Tuttavia, ogni chiamata a write() è autonoma, quindi in background 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 unico file. Un altro esempio sono i mipmaps utilizzati nell'elaborazione delle immagini. Le Mipmap sono sequenze di immagini precalcolate e ottimizzate, ciascuna delle quali è una rappresentazione a risoluzione progressivamente inferiore della precedente, il che rende più veloci molte operazioni come lo zoom. Dunque, in che modo le applicazioni web possono ottenere i vantaggi dei file, ma senza i costi in termini di prestazioni dell'elaborazione di file basata sul web? La risposta è il file system privato dell'origine.

Confronto tra il file system visibile all'utente e il file system privato di origine

A differenza del file system visibile agli utenti navigato utilizzando Esplora file del sistema operativo, con file e cartelle che puoi leggere, scrivere, spostare e rinominare, il file system privato dell'origine non è visibile agli utenti. I file e le cartelle nel file system privato di origine, come suggerito dal 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 (ovvero, la parte /articles non fa parte dell'origine). Per saperne di più sulla teoria delle origini, consulta Informazioni su "stesso sito" e "stesso-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 di origine indipendente, il che significa che il file system privato di origine di https://developer.chrome.com è completamente distinto da quello, 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 root 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, vedi il seguente diagramma. Il diagramma mostra che, a parte la directory radice, tutto il resto è concettualmente lo stesso, con una gerarchia di file e cartelle da organizzare e organizzare in base alle esigenze dei dati e 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 ingresso per il file system visibile all&#39;utente è un disco rigido simbolico, il punto di ingresso per il file system privato di origine chiama il metodo &quot;navigator.storage.getDirectory&quot;.

Specifiche del file system privato di origine

Proprio 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 dei siti, viene eliminato anche il file system privato di origine. Chiama navigator.storage.estimate() e nell'oggetto risposta risultante, vedi la voce usage per vedere la quantità di spazio di archiviazione già utilizzata dalla tua app, suddivisa per meccanismo di archiviazione nell'oggetto usageDetails, in cui vuoi controllare la voce fileSystem in modo specifico. Poiché il file system privato di origine non è visibile all'utente, non vengono visualizzati richieste di autorizzazione né controlli di Navigazione sicura.

Ottenere l'accesso alla directory root

Per accedere alla directory root, esegui questo comando. Al termine, l'handle della directory è vuoto, più nello specifico, una 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 di 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 poiché evitano di dover gestire le promesse e le operazioni sui file sono tipicamente 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ù rapide possibili o se ti occupi di WebAssembly, passa a Utilizzare il file system privato dell'origine in un web worker. Altrimenti, puoi continuare a leggere.

Utilizza il file system privato di 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 verranno creati se non esistono. 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 di 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(), trasmettendo 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 l'oggetto File associato, usa il metodo getFile(). Un oggetto File è un tipo specifico di Blob e può essere utilizzato in qualsiasi contesto a disposizione di Blob. In particolare, FileReader, URL.createObjectURL(), createImageBitmap() e XMLHttpRequest.send() accettano sia Blobs che Files. In tal caso, ottenere un File da un FileSystemFileHandle "libera" i dati, in modo da poterli accedere e renderli disponibili al file system visibile all'utente.

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

Scrivi in un file tramite streaming

Trasmetti il flusso di dati in un file chiamando createWritable(), che crea un FileSystemWritableFileStream, quindi 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

Eliminare file e cartelle chiamando il metodo remove() specifico dell'handle di file o directory. Per eliminare una cartella che include 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

Rinominare e spostare file e cartelle con il metodo move(). Lo spostamento e la ridenominazione possono avvenire insieme o in modo isolato.

// 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 rispetto a una directory di riferimento, usa 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 root 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 puntano allo stesso file o cartella

A volte hai due handle e non sai se puntano allo stesso file o cartella. Per verificare se è questo il caso, utilizza il metodo isSameEntry().

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

Elenco dei contenuti di una cartella

FileSystemDirectoryHandle è un iteratore asincrono che esegui l'iterazione con un loop for await…of. In quanto iteratore asincrono, supporta anche i metodi entries(), 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()) {}

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

È facile sbagliare se ci sono loop e funzioni asincroni abbinati alla ricorsione. La funzione seguente può servire 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 del file, dove è indicato directoryEntryPromises.push, non mantenendo 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;
  };

Usa il file system privato dell'origine in un web worker

Come descritto prima, 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 è una 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 sincronizzati per i file in loco

Una volta ottenuto l'handle di accesso sincrono, puoi accedere a metodi rapidi con file in loco tutti sincroni.

  • getSize(): restituisce le dimensioni del file in byte.
  • write(): scrive il contenuto di un buffer nel file, facoltativamente con 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 gli errori e le scritture parziali.
  • read(): legge i contenuti del file in un buffer, facoltativamente in corrispondenza di un determinato offset.
  • truncate(): ridimensiona il file alle dimensioni specificate.
  • flush(): verifica che i contenuti del file contengano tutte le modifiche apportate tramite write().
  • close(): chiude l'handle di accesso.

Ecco un esempio che utilizza tutti i metodi indicati in precedenza.

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, non è possibile spostare file dal file system privato di origine al file system visibile all'utente, ma puoi copiare i file. Poiché showSaveFilePicker() è esposto solo nel 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);
}

Esegui il debug del file system privato dell'origine

Fino a quando non verrà aggiunto il supporto DevTools integrato (vedi crbug/1284595), utilizza l'estensione di Chrome OPFS Explorer per eseguire il debug del file system privato dell'origine. Lo screenshot in alto nella sezione Creazione di nuovi file e cartelle viene 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 Explorer OPFS. A questo punto potrai controllare 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

Osserva 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. Nota che la versione incorporata di seguito 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.

Conclusioni

Il file system privato di origine, come specificato da WHATWG, ha plasmato il modo in cui usiamo e interagiamo con i file sul web. Ha permesso di creare 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, adottano e condividono una visione congiunta. Lo sviluppo del file system privato di origine è uno sforzo molto collaborativo e il feedback di sviluppatori e utenti è essenziale per il suo progresso. Mentre continuiamo a perfezionare e migliorare lo standard, è gradito fornire feedback sul repository whatwg/fs sotto forma di problemi o richieste di pull.

Ringraziamenti

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