Lettura e scrittura di file e directory con la libreria browser-fs-access

I browser sono in grado di gestire file e directory da molto tempo. L'API File fornisce funzionalità per la rappresentazione di oggetti file nelle applicazioni web, nonché per selezionarli e accedere ai relativi dati in modo programmatico. Tuttavia, se guardi più da vicino, scoprirai che non è tutto oro quel che luccica.

Apertura dei file

In qualità di sviluppatore, puoi aprire e leggere file tramite l'elemento <input type="file">. Nella sua forma più semplice, l'apertura di un file può essere simile all'esempio di codice riportato di seguito. L'oggetto input fornisce un FileList, che nel caso riportato di seguito è costituito da un solo File. Un File è un tipo specifico di Blob e può essere utilizzato in qualsiasi contesto utilizzabile da un BLOB.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Apertura directory

Per aprire cartelle (o directory), puoi impostare l'attributo <input webkitdirectory>. A parte questo, tutto il resto funziona come sopra. Nonostante il nome con prefisso del fornitore, webkitdirectory non è utilizzabile solo nei browser Chromium e WebKit, ma anche nell'Edge precedente basato su EdgeHTML e in Firefox.

Salvataggio (o download) di file

Per salvare un file, in genere, puoi solo scaricarlo, grazie all'attributo <a download>. Dato un blob, puoi impostare l'attributo href dell'ancora su un URL blob: che puoi ottenere dal metodo URL.createObjectURL().

const saveFile = async (blob) => {
  const a = document.createElement('a');
  a.download = 'my-file.txt';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

Il problema

Uno svantaggio enorme dell'approccio di download è che non è possibile eseguire un flusso classico di apertura, modifica e salvataggio, ovvero non è possibile sovrascrivere il file originale. Al contrario, ogni volta che "salvi" viene creata una nuova copia del file originale nella cartella Download predefinita del sistema operativo.

L'API File System Access

L'API File System Access semplifica notevolmente entrambe le operazioni, apertura e salvataggio. Consente inoltre il salvataggio reale, ovvero non solo puoi scegliere dove salvare un file, ma anche sovrascrivere un file esistente.

Apertura dei file

Con l'API File System Access, l'apertura di un file è una questione di una chiamata al metodo window.showOpenFilePicker(). Questa chiamata restituisce un handle di file, dal quale è possibile ottenere l'elemento File effettivo tramite il metodo getFile().

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Aprire le directory

Apri una directory chiamando window.showDirectoryPicker() che rende le directory selezionabili nella finestra di dialogo del file.

Salvataggio dei file

Anche il salvataggio dei file è altrettanto semplice. Dall'handle di un file, crei un flusso in scrittura tramite createWritable(), quindi scrivi i dati BLOB richiamando il metodo write() del flusso, quindi chiudi il flusso chiamando il relativo metodo close().

const saveFile = async (blob) => {
  try {
    const handle = await window.showSaveFilePicker({
      types: [{
        accept: {
          // Omitted
        },
      }],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Introduzione a browser-fs-access

L'API File System Access è perfettamente valida, ma non è ancora ampiamente disponibile.

Tabella relativa al supporto dei browser per l&#39;API File System Access. Tutti i browser sono contrassegnati come &quot;Nessun supporto&quot; o &quot;Con un flag&quot;.
Tabella relativa al supporto dei browser per l'API File System Access. (Fonte)

Per questo motivo, considero l'API File System Access un miglioramento progressivo. Pertanto, voglio utilizzarlo quando il browser lo supporta e, in caso contrario, utilizzare l'approccio tradizionale, senza mai punire l'utente con download non necessari di codice JavaScript non supportato. La libreria browser-fs-access è la mia risposta a questa sfida.

Filosofia del design

Poiché è probabile che l'API File System Access subisca delle modifiche in futuro, l'API browser-fs-access non viene modellata in base a questa. In altre parole, la libreria non è un polyfill, ma un ponyfill. Puoi importare (staticamente o dinamicamente) esclusivamente le funzionalità di cui hai bisogno per mantenere l'app il più piccola possibile. I metodi disponibili sono quelli denominati correttamente fileOpen(), directoryOpen() e fileSave(). All'interno, la funzionalità della libreria rileva se l'API Accesso al file system è supportata, quindi importa il percorso del codice corrispondente.

Utilizzo della libreria browser-fs-access

Si tratta di tre metodi intuitivi da usare. Puoi specificare il mimeTypes o il file extensions accettati dalla tua app e impostare un flag multiple per consentire o meno la selezione di più file o directory. Per tutti i dettagli, consulta la documentazione dell'API browser-fs-access. L'esempio di codice seguente mostra come aprire e salvare i file immagine.

// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from 'https://unpkg.com/browser-fs-access';

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ['image/*'],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ['image/*'],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true
  });

  // Save a file.
  await fileSave(blob, {
    fileName: 'Untitled.png',
  });
})();

Demo

Puoi vedere il codice riportato sopra in azione in una demo su Glitch. Anche il codice sorgente è disponibile lì. Poiché per motivi di sicurezza i frame secondari di origine diversa non sono autorizzati a mostrare un selettore di file, la demo non può essere incorporata in questo articolo.

La libreria browser-fs-access in uso

Nel tempo libero, do un piccolo contributo a una PWA installabile chiamata Excalidraw, uno strumento di lavagna che consente di disegnare facilmente diagrammi con la sensazione di un disegno a mano. È completamente adattabile e funziona bene su una serie di dispositivi, dai piccoli cellulari ai computer con schermi di grandi dimensioni. Ciò significa che deve gestire i file su tutte le varie piattaforme, indipendentemente dal fatto che supportino o meno l'API Accesso al file system. Questo lo rende un ottimo candidato per la libreria browser-fs-access.

Ad esempio, posso iniziare un disegno sul mio iPhone, salvarlo (tecnicamente: scaricarlo, dato che Safari non supporta l'API File System Access) nella cartella Download dell'iPhone, aprire il file sul desktop (dopo averlo trasferito dal telefono), modificarlo e sovrascriverlo con le mie modifiche, o persino salvarlo come nuovo file.

Un disegno di Excalidraw su un iPhone.
Avvio di un disegno di Excalidraw su un iPhone in cui l'API File System Access non è supportata, ma in cui è possibile salvare (scaricare) un file nella cartella Download.
Il disegno di Excalidraw modificato su Chrome sul computer.
Apertura e modifica del disegno Excalidraw sul desktop in cui è supportata l'API File System Access, consentendo l'accesso al file tramite l'API.
Sovrascrivendo il file originale con le modifiche.
Sostituzione del file originale con le modifiche al file del disegno Excalidraw originale. Il browser mostra una finestra di dialogo che mi chiede se va tutto bene.
Salvare le modifiche in un nuovo file di disegno Excalidraw.
Salvare le modifiche in un nuovo file Excalidraw. Il file originale rimane invariato.

Esempio di codice reale

Di seguito, puoi vedere un esempio reale di browser-fs-access utilizzato in Excalidraw. Questo estratto è tratto da /src/data/json.ts. Di particolare interesse è il modo in cui il metodo saveAsJSON() passa un handle file o null al metodo fileSave() di browser-fs-access, che lo sovrascrive quando viene fornito un handle o lo salva in un nuovo file se non viene fornito.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
  fileHandle: any,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: "application/json",
  });
  const name = `${appState.name}.excalidraw`;
  (window as any).handle = await fileSave(
    blob,
    {
      fileName: name,
      description: "Excalidraw file",
      extensions: ["excalidraw"],
    },
    fileHandle || null,
  );
};

export const loadFromJSON = async () => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: ["json", "excalidraw"],
    mimeTypes: ["application/json"],
  });
  return loadFromBlob(blob);
};

Considerazioni sull'interfaccia utente

Che si tratti di Excalidraw o della tua app, l'interfaccia utente deve adattarsi alla situazione di supporto del browser. Se l'API File System Access è supportata (if ('showOpenFilePicker' in window) {}), puoi mostrare un pulsante Salva come oltre a un pulsante Salva. Gli screenshot di seguito mostrano la differenza tra la barra degli strumenti dell'app principale adattabile di Excalidraw su iPhone e su Chrome per computer. Tieni presente che su iPhone manca il pulsante Salva come.

Barra degli strumenti dell&#39;app Excalidraw su iPhone con un solo pulsante &quot;Salva&quot;.
Barra degli strumenti dell'app Excalibur su iPhone con un solo pulsante Salva.
Barra degli strumenti dell&#39;app Excalidraw su Chrome per computer con i pulsanti &quot;Salva&quot; e &quot;Salva come&quot;.
Barra degli strumenti dell'app Excalibur su Chrome con i pulsanti Salva e Salva come attivi.

Conclusioni

Il lavoro con i file di sistema funziona tecnicamente su tutti i browser moderni. Sui browser che supportano l'API Accesso al file system, puoi migliorare l'esperienza consentendo un salvataggio e una sovrascrittura (non solo il download) effettivi dei file e consentendo agli utenti di creare nuovi file dove vogliono, il tutto rimanendo funzionale sui browser che non supportano l'API Accesso al file system. browser-fs-access semplifica la vita grazie al trattamento delle sfumature del miglioramento progressivo e alla semplificazione del codice al massimo.

Ringraziamenti

Questo articolo è stato esaminato da Joe Medley e Kayce Basques. Grazie ai collaboratori di Excalidraw per il loro lavoro sul progetto e per aver esaminato le mie richieste pull. Immagine hero di Ilya Pavlov su Unsplash.