Il file system privato di origine

Lo standard File System introduce un file system privato di origine (OPFS) come endpoint di archiviazione privato rispetto all'origine della pagina e non visibile all'utente che fornisce 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) nel File System Living Standard.

Supporto dei browser

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

Origine

Motivazione

Quando pensi ai file sul computer, probabilmente ti viene in mente una gerarchia di file: i file organizzati in cartelle che puoi esplorare con l'esplorazione dei file del tuo sistema operativo. Ad esempio, su Windows, l'elenco delle cose da fare di un utente chiamato Mario potrebbe essere 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. Su Windows, "C:" rappresenta la directory radice dell'unità.

Modalità tradizionale di lavorare con i file sul web

Per modificare l'elenco delle cose da fare in un'applicazione web, segui questa procedura:

  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 elemento <a download="ToDo.txt> inserito che tu hai inserito in modo programmatico click() tramite JavaScript.
  3. Per aprire le cartelle, utilizzi un attributo speciale in <input type="file" webkitdirectory> che, nonostante il suo nome proprietario, ha un supporto browser praticamente universale.

Un modo moderno di lavorare con i file sul web

Questo flusso non è rappresentativo di come gli utenti pensano della modifica dei file e significa che gli utenti finiscono per scaricare copie dei loro 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:

  1. Apri ToDo.txt con showOpenFilePicker() e recupera un oggetto FileSystemFileHandle.
  2. Dall'oggetto FileSystemFileHandle, ottieni un File chiamando il metodo getFile() dell'handle di 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 sceglie un file aperto in precedenza, i suoi contenuti vengono sovrascritti. Per i salvataggi ripetuti, puoi tenere l'handle del file a portata di mano, in modo da non dover mostrare di nuovo la finestra di dialogo per il salvataggio del file.

Restrizioni relative all'utilizzo di file sul web

I file e le cartelle accessibili con questi metodi risiedono in quello che può essere chiamato il file system user-visible. I file salvati dal web, e in particolare i file eseguibili, sono contrassegnati con il marchio del web, quindi il sistema operativo può 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 considerato come una scansione antivirus basata su 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 non viene modificato a meno che non superi tutti questi controlli di sicurezza. Come potete 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() è indipendente, 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 modo eccellente 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, ciascuna delle quali è una rappresentazione a risoluzione progressiva più bassa di quella precedente, il che rende più veloce molte operazioni, come l'aumento dello zoom. Quindi, in che modo le applicazioni web possono ottenere i vantaggi dei file, ma senza i costi delle 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 quello privato di origine

A differenza del file system visibile all'utente, esplorato utilizzando l'esplorazione dei file del sistema operativo, con file e cartelle che puoi leggere, scrivere, spostare e rinominare, il file system privato di origine non deve 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 (ovvero, la parte /articles non fa parte dell'origine). Per ulteriori informazioni sulla teoria delle origini, consulta la sezione Informazioni sulla teoria delle origini e "same-origin". 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, di https://web.dev. Su Windows, la directory radice del file system visibile all'utente è C:\\. L'equivalente per il file system privato dell'origine è una directory radice 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 diagramma seguente. Il diagramma mostra che, a parte la directory principale, tutto il resto è concettualmente la stessa, con una gerarchia di file e cartelle da organizzare e organizzare in base alle esigenze di dati e 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 del file system visibile all&#39;utente è un disco rigido simbolico, il punto di ingresso del 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 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 risposta risultante vedrai la voce usage per vedere la quantità di spazio di archiviazione già consumata dalla tua app, che è suddivisa per meccanismo di archiviazione nell'oggetto usageDetails, dove vuoi esaminare nello specifico la voce fileSystem. Poiché il file system privato di origine non è visibile all'utente, non vengono visualizzate richieste di autorizzazioni e controlli di Navigazione sicura.

Ottenere l'accesso alla directory radice

Per ottenere l'accesso alla directory radice, esegui questo comando. 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 worker web

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 in quanto evitano di dover gestire le promesse, mentre le operazioni con i 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 devi gestire WebAssembly, vai a Utilizzare il file system privato di origine in un web worker. In caso contrario, potete continuare a leggere.

Usa 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(). Passando {create: true}, il file o la cartella verranno creati se non esistono. Crea una gerarchia di file richiamando 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.

Accedi a file e cartelle esistenti

Se ne conosci il nome, accedi ai file e alle cartelle creati in precedenza chiamando i metodi getFileHandle() o getDirectoryHandle(), fornendo 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 all'handle di un file per la lettura

L'istruzione FileSystemFileHandle rappresenta un file nel file system. Per ottenere il valore File associato, utilizza il metodo getFile(). Un oggetto File è un tipo specifico di Blob e può essere utilizzato in qualsiasi contesto consentito da un Blob. In particolare, FileReader, URL.createObjectURL(), createImageBitmap() e XMLHttpRequest.send() accettano sia Blobs che Files. Se vuoi, ottieni un File da un FileSystemFileHandle "senza costi" per potervi accedere e renderli disponibili al file system visibile all'utente.

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

Scrittura in un file tramite flusso di dati

Trasmetti lo streaming di dati in un file chiamando createWritable(), che crea un elemento FileSystemWritableFileStream in modo da 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 del file o dell'handle di 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

Rinomina e sposta file e cartelle utilizzando 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 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']`.

Verificare 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`.

Elenca i contenuti di una cartella

FileSystemDirectoryHandle è un iteratore asincrono che esegui l'iterazione con un loop 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()) {}

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

La gestione di cicli e funzioni asincroni in coppia con la ricorsione è facile da sbagliare. La funzione riportata di seguito 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. Per semplificare la funzione se non hai bisogno delle dimensioni del file, dove è indicato directoryEntryPromises.push, non forzando 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;
  };

Utilizza il file system privato di origine in un web worker

Come spiegato in precedenza, i web worker non possono bloccare il thread principale, motivo per cui in questo contesto sono consentiti i metodi sincroni.

Ottieni un handle di accesso sincrono

Il punto di accesso alle operazioni su file più veloci possibile è un FileSystemSyncAccessHandle, ottenuto da FileSystemFileHandle normale chiamando createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Metodi sincronizzati sul file in loco

Una volta creato un handle di accesso sincrono, puoi accedere a metodi file veloci sul posto che sono tutti sincroni.

  • getSize(): restituisce la dimensione 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. La verifica del numero di byte scritti restituito consente ai chiamanti di rilevare e gestire errori e scritture parziali.
  • read(): legge i contenuti del file in un buffer, facoltativamente in base a 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 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);

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

Come accennato in precedenza, lo spostamento di file dal file system privato di origine a quello visibile all'utente non è possibile, ma puoi copiare i file. 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);
}

Esegui il debug del file system privato di origine

Fino a quando non verrà aggiunto il supporto integrato per DevTools (vedi crbug/1284595), utilizza l'estensione di Chrome OPFS Explorer per eseguire il debug del file system privato di origine. Lo screenshot della sezione Creazione di nuovi file e cartelle riportato sopra viene acquisito direttamente dall'estensione.

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

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

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 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 modellato il modo in cui usiamo e interagiamo con i file sul web. Ha abilitato 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 coinvolti e condividono una visione comune. Lo sviluppo del file system privato di origine è molto uno sforzo collaborativo e il feedback di sviluppatori e utenti è essenziale per il suo progresso. Mentre continuiamo a perfezionare e migliorare lo standard, ti invitiamo a dare un feedback sul repository Whatwg/fs sotto forma di Problemi o Richieste di pull.

Ringraziamenti

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