Exploring the FileSystem APIs

HTML5 Rocks

Introduzione

Ho sempre detto che sarebbe comodo se le web application potessero leggere e scrivere file e directory. Non appena ci spostiamo dall'offline all'online, le applicazioni diventano più complesse e la mancanza di API per il file system costituisce un ostacolo all'avanzata del web. Salvare dati binari o interagire con essi non dovrebbe essere limitato dal desktop. Fortunatamente non è più così grazie alle API FileSystem. Con le FileSystem API una web app può creare, leggere, navigare e scrivere in una sezione sandboxed del file system locale.

Le API sono suddivise per funzionalità:

  • Lettura e manipolazione dei file: File/Blob, FileList, FileReader
  • Creazione e scrittura: Blob(), FileWriter
  • Accesso a directory e al file system: DirectoryReader, FileEntry/DirectoryEntry, LocalFileSystem

Supporto del browser & limitazioni sulla memorizzazione

Al momento della scrittura di questo articolo, Google Chrome fornisce l'unica implementazione funzionante delle FileSystem API. Non esiste ancora un'interfaccia browser dedicata per la gestione dei file/quota. Per salvare i dati sul sistema dell'utente, si può fare in modo che la tua applicazione richieda quota. Comunque, per test, Chrome può essere eseguito con il flag --unlimited-quota-for-files. Inoltre, se stai scrivendo un'applicazione o un'estensione per il Chrome Web Store, invece di utilizzare la richiesta di quota, può essere usato la permission unlimitedStorage nel file manifest. Prima o poi l'utente riceverà una permission dialog nella quale potrà consentire, rifiutare o incrementare lo storage per l'app.

Potresti aver bisogno del flag --allow-file-access-from-files se stai effettuando il debug della tua app da file://. Se non usi questo flag riceverai un errore FileError di tipo SECURITY_ERR o QUOTA_EXCEEDED_ERR.

Richiedere un file system

Una web app può richiedere accesso ad un file system sandboxed chiamando window.requestFileSystem():

// Note: The file system has been prefixed as of Google Chrome 12:
window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

window.requestFileSystem(type, size, successCallback, opt_errorCallback)
type
Indica se lo storage deve essere persistente. Valori possibili sono window.TEMPORARY o window.PERSISTENT. I dati memorizzati con l'opzione TEMPORARY possono essere rimossi a discrezione del browser (ad esempio se necessita di più spazio). La memorizzazione PERSISTENT non può essere cancellata se non sotto esplicita autorizzazione dell'utente o dell'app e richiede che l'utente conceda la quota all'app. Fai riferimento a richiedere quota.
size
Dimensione (in byte) dello spazio di memorizzazione richiesto dall'app.
successCallback
Callback che viene invocata al successo della richiesta del file system. Il suo argomento è un oggetto FileSystem.
opt_errorCallback
Callback opzionale utilizzata per la gestione degli errori o quando la richiesta di ottenere il file system viene rifiutata. Il suo argomento è un oggetto FileError.

Se invochi requestFileSystem() per la prima volta, viene creato un nuovo storage per la tua applicazione. È importante ricordare che questo file system è sandboxed, il che significa che una web app non può accedere ai file di un'altra app. Questo inoltre significa che non puoi leggere o scrivere file in una cartella arbitraria dell'hard disk dell'utente (per esempio My Pictures, My Documents, etc.).

Esempi di utilizzo:

function onInitFs(fs) {
  console.log('Opened file system: ' + fs.name);
}

window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, onInitFs, errorHandler);

La specifica del FileSystem, inoltre, definisce un'API sincrona LocalFileSystemSync, un'interfaccia che è destinata ad essere usata in Web Workers. Comunque questo tutorial non copre le API sincrone.

In tutto il resto di questo documento utilizzeremo lo stesso handler per processare gli errori derivanti dalle chiamate asincrone:

function errorHandler(e) {
  var msg = '';

  switch (e.code) {
    case FileError.QUOTA_EXCEEDED_ERR:
      msg = 'QUOTA_EXCEEDED_ERR';
      break;
    case FileError.NOT_FOUND_ERR:
      msg = 'NOT_FOUND_ERR';
      break;
    case FileError.SECURITY_ERR:
      msg = 'SECURITY_ERR';
      break;
    case FileError.INVALID_MODIFICATION_ERR:
      msg = 'INVALID_MODIFICATION_ERR';
      break;
    case FileError.INVALID_STATE_ERR:
      msg = 'INVALID_STATE_ERR';
      break;
    default:
      msg = 'Unknown Error';
      break;
  };

  console.log('Error: ' + msg);
}

Questa callback di errore è molto generica, ma rende l'idea. Tu, invece, potresti voler fornire un messaggio agli utenti che sia human-readable.

Richiedere quota di memorizzazione

Per utilizzare la memorizzazione PERSISTENT, devi ottenere dall'utente il permesso di memorizzare dati persistenti. La stessa restrizione non viene applicata alla memorizzazione TEMPORARY poichè il browser può scegliere a sua discrezione di eliminare dati temporaneamente memorizzati.

Per usare la memorizzazione PERSISTENT con le FileSystem API, Chrome espone una nuova API sotto window.webkitStorageInfo per la richiesta di storage:

window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024, function(grantedBytes) {
  window.requestFileSystem(PERSISTENT, grantedBytes, onInitFs, errorHandler);
}, function(e) {
  console.log('Error', e);
});

Una volta che l'utente ha concesso il permesso, non è più necessario chiamare requestQuota() in futuro (a meno che non desideri incrementare la quota della tua app). Le chiamate successive per una quota uguale o inferiore non saranno valutate come operazioni.

C'è anche una API per ricevere informazioni riguardo l'utilizzo e l'allocazione della quota corrente: window.webkitStorageInfo.queryUsageAndQuota()

Lavorare con i file

I file nell'ambiente sandboxed sono rappresentati attraverso l'interfaccia FileEntry. Un FileEntry contiene le proprietà (name, isFile, ...) e i metodi (remove, moveTo, copyTo, ...) che ti aspetti da un file system standard.

Proprietà e metodi di FileEntry:

fileEntry.isFile === true
fileEntry.isDirectory === false
fileEntry.name
fileEntry.fullPath
...

fileEntry.getMetadata(successCallback, opt_errorCallback);
fileEntry.remove(successCallback, opt_errorCallback);
fileEntry.moveTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.copyTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.getParent(successCallback, opt_errorCallback);
fileEntry.toURL(opt_mimeType);

fileEntry.file(successCallback, opt_errorCallback);
fileEntry.createWriter(successCallback, opt_errorCallback);
...

Per comprendere meglio FileEntry, il resto di questa sezione conterrà un mucchio di ricette per svolgere operazioni comuni.

Creare un file

Puoi cercare o creare un file attraverso il metodo del file system getFile(), un metodo dell'interfaccia DirectoryEntry. Dopo aver richiesto un file system, alla callback di successo viene passato un oggetto FileSystem che contiene una DirectoryEntry (fs.root) la quale punta alla root del file system dell'app.

Il codice seguente crea un file vuoto chiamato "log.txt" nella root del file system dell'app:

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {

    // fileEntry.isFile === true
    // fileEntry.name == 'log.txt'
    // fileEntry.fullPath == '/log.txt'

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Una volta che il file system è stato richiesto, all'handler di successo viene passato un oggetto FileSystem. All'interno della callback, possiamo chiamare fs.root.getFile() con il nome del file da creare. Puoi passare un path assoluto o relativo, ma deve essere valido. Per esempio, è un errore tentare di creare un file il cui immediato parent non esiste. Il secondo argomento di getFile() è un oggetto che descrive il comportamento della funzione se il file non esiste. In questo esempio, create: true crea il file se non esiste e solleva un errore se esso esiste (exclusive: true). Invece (se create: false), il file viene semplicemente caricato e restituito. In entrambi i casi, il contenuto del file non viene sovrascritto poichè stiamo ottenendo un riferimento al file in questione.

Leggere un file tramite il nome

Il codice seguente ritrova il file chiamato "log.txt", il suo contenuto viene letto utilizzando le API FileReader e accodato ad una nuova <textarea> sulla pagina. Se log.txt non esiste viene sollevato un errore.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {}, function(fileEntry) {

    // Get a File object representing the file,
    // then use FileReader to read its contents.
    fileEntry.file(function(file) {
       var reader = new FileReader();

       reader.onloadend = function(e) {
         var txtArea = document.createElement('textarea');
         txtArea.value = this.result;
         document.body.appendChild(txtArea);
       };

       reader.readAsText(file);
    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Scrivere su un file

Il codice seguente crea un file vuoto chiamato "log.txt" (se non esiste) e vi scrive del testo 'Lorem Ipsum'.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.onwriteend = function(e) {
        console.log('Write completed.');
      };

      fileWriter.onerror = function(e) {
        console.log('Write failed: ' + e.toString());
      };

      // Create a new Blob and write it to log.txt.
      var blob = new Blob(['Lorem Ipsum'], {type: 'text/plain'});

      fileWriter.write(blob);

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Questa volta chiamiamo il metodo di FileEntry createWriter() per ottenere un oggetto FileWriter. All'interno della callback di successo, l'event handler è settato per gli eventi error e writeend. I dati testuali sono scritti sul file attraverso la creazione di un blob, accodando il testo ad esso e passando il blob a FileWriter.write().

Accodare dati ad un file

Il codice seguente accoda il testo 'Hello World' alla fine del nostro file di log. Un errore è sollevato se il file non esiste.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.seek(fileWriter.length); // Start write position at EOF.

      // Create a new Blob and write it to log.txt.
      var blob = new Blob(['Hello World'], {type: 'text/plain'});

      fileWriter.write(blob);

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Duplicare file selezionati dall'utente

Il codice seguente permette ad un utente di selezionare una sequenza di file utilizzando <input type="file" multiple /> e di creare copie di questi file nel file system sandboxed dell'app.

<input type="file" id="myfile" multiple />
document.querySelector('#myfile').onchange = function(e) {
  var files = this.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
    // Duplicate each file the user selected to the app's fs.
    for (var i = 0, file; file = files[i]; ++i) {

      // Capture current iteration's file in local scope for the getFile() callback.
      (function(f) {
        fs.root.getFile(f.name, {create: true, exclusive: true}, function(fileEntry) {
          fileEntry.createWriter(function(fileWriter) {
            fileWriter.write(f); // Note: write() can take a File or Blob object.
          }, errorHandler);
        }, errorHandler);
      })(file);

    }
  }, errorHandler);

};

Nonostante abbiamo utilizzato un input per l'import dei file, si può facilmente far leva sul Drag and Drop di HTML5 per raggiungere lo stesso obiettivo.

Come notato nel commento, FileWriter.write() può accettare un Blob o un File. Questo poichè File è ereditato da Blob. Quindi tutti gli oggetti file sono blob.

Rimuovere un file

Il codice seguente elimina il file 'log.txt'.

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    fileEntry.remove(function() {
      console.log('File removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Lavorare con le directory

Le directory nella sandbox sono rappresentate attraverso l'interfaccia DirectoryEntry, che condivide molte delle proprietà di FileEntry (entrambi sono ereditati da un'interfaccia Entry comune). Comunque DirectoryEntry ha metodi addizionali per manipolare le directory.

Proprietà e metodi di DirectoryEntry:

dirEntry.isDirectory === true
// See the section on FileEntry for other inherited properties/methods.
...

var dirReader = dirEntry.createReader();
dirEntry.getFile(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.getDirectory(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.removeRecursively(successCallback, opt_errorCallback);
...

Creare directory

Utilizza il metodo getDirectory() di DirectoryEntry per leggere o creare le directory. Puoi passare sia un nome che un path come directory da cercare o da creare.

Per esempio il codice seguente crea una directory chiamata "MyPictures" nella directory root:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) {
    ...
  }, errorHandler);
}, errorHandler);
  

Sottodirectory

Creare una sottodirectory è esattamente uguale a creare qualsiasi altra directory. Comunque la API solleva un errore se si cerca di creare una directory il cui immediato parent non esiste. La soluzione è creare ciascuna directory sequenzialmente, che è leggermente più complicato con le API asincrone.

Il codice seguente crea una nuova gerarchia (music/genres/jazz) nella root del FileSystem dell'app aggiungento ricorsivamente ciascuna sottodirectory dopo che il suo parent è stato creato.

var path = 'music/genres/jazz/';

function createDir(rootDirEntry, folders) {
  // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'.
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    // Recursively add the new subfolder (if we still have another to create).
    if (folders.length) {
      createDir(dirEntry, folders.slice(1));
    }
  }, errorHandler);
};

function onInitFs(fs) {
  createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry.
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Ora che "music/genres/jazz" è a posto, possiamo passare il suo path intero a getDirectory() e creare nuove sottodirectory o file al suo interno. Ad esempio:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) {
    ...
  }, errorHandler);
}, errorHandler);

Leggere il contenuto di una directory

Per leggere il contenuto di una directory, crea un DirectoryReader e chiama il suo metodo readEntries(). Non c'è garanzia che tutte le entry della directory vengano restituite in una singola chiamata a readEntries(). Ciò significa che tu hai bisogno di continuare a chiamare DirectoryReader.readEntries() finchè nuovi risultati non sono più ritornati. Il codice seguente lo dimostra:

<ul id="filelist"></ul>
function toArray(list) {
  return Array.prototype.slice.call(list || [], 0);
}

function listResults(entries) {
  // Document fragments can improve performance since they're only appended
  // to the DOM once. Only one browser reflow occurs.
  var fragment = document.createDocumentFragment();

  entries.forEach(function(entry, i) {
    var img = entry.isDirectory ? '<img src="folder-icon.gif">' :
                                  '<img src="file-icon.gif">';
    var li = document.createElement('li');
    li.innerHTML = [img, '<span>', entry.name, '</span>'].join('');
    fragment.appendChild(li);
  });

  document.querySelector('#filelist').appendChild(fragment);
}

function onInitFs(fs) {

  var dirReader = fs.root.createReader();
  var entries = [];

  // Call the reader.readEntries() until no more results are returned.
  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries(); // Start reading dirs.

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Rimuovere una directory

Il metodo DirectoryEntry.remove() si comporta proprio come FileEntry. La differenza: cercare di cancellare una directory non vuota ottiene come risultato un errore.

Il codice seguente rimuove la directory vuota "jazz" da "/music/genres/":

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) {

    dirEntry.remove(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Rimuovere ricorsivamente una directory

Se hai una directory noiosa che contiene entry, removeRecursively() è il tuo alleato. Esso elimina la directory ed il suo contenuto, ricorsivamente.

Il codice seguente rimuove ricorsivamente la directory "music" e tutti i file e le directory che essa contiene:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('/misc/../music', {}, function(dirEntry) {

    dirEntry.removeRecursively(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Copiare, rinominare e spostare

FileEntry e DirectoryEntry condividono operazioni comuni.

Copiare una entry

Sia FileEntry che DirectoryEntry hanno un copyTo() per duplicare le entry esistenti. Questo metodo automaticamente effettua una copia ricorsiva sulle cartelle.

Il codice seguente copia il file "me.png" da una directory ad un'altra:

function copy(cwd, src, dest) {
  cwd.getFile(src, {}, function(fileEntry) {

    cwd.getDirectory(dest, {}, function(dirEntry) {
      fileEntry.copyTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  copy(fs.root, '/folder1/me.png', 'folder2/mypics/');
}, errorHandler);

Spostare o rinominare una entry

il metodo moveTo() presente in FileEntry e DirectoryEntry ti permette di spostare o rinominare un file o una directory. Il suo primo argomento è la directory parent nella quale spostare il file e il secondo è un nuovo nome opzionale per il file. Se non viene fornito un nuovo nome, viene utilizzato il nome originale del file.

Il seguente esempio rinomina "me.png" in "you.png", ma non sposta il file:

function rename(cwd, src, newName) {
  cwd.getFile(src, {}, function(fileEntry) {
    fileEntry.moveTo(cwd, newName);
  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  rename(fs.root, 'me.png', 'you.png');
}, errorHandler);

Il seguente esempio sposta "me.png" (situato nella directory root) in una cartella chiamata "newfolder".

function move(src, dirName) {
  fs.root.getFile(src, {}, function(fileEntry) {

    fs.root.getDirectory(dirName, {}, function(dirEntry) {
      fileEntry.moveTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);

filesystem: URL

L'API FileSystem espone un nuovo schema per le URL, filesystem:, che può essere usato per riempire gli attributi src o href. Per esempio, se vuoi mostrare un'immagine ed hai il suo fileEntry, chiamando toURL() otterrai l'URL filesystem: del file:

var img = document.createElement('img');
img.src = fileEntry.toURL(); // filesystem:http://example.com/temporary/myfile.png
document.body.appendChild(img);

In alternativa, se hai già un URL filesystem:, resolveLocalFileSystemURL() ti restituirà il fileEntry:

window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                   window.webkitResolveLocalFileSystemURL;

var url = 'filesystem:http://example.com/temporary/myfile.png';
window.resolveLocalFileSystemURL(url, function(fileEntry) {
  ...
});

Uniamo il tutto

Esempio base

Questa demo elenca i file/cartelle nel filesystem.

Terminale HTML5

Questa shell replica alcune delle comuni operazioni in un filesystem UNIX (come cd, mkdir, rm, open e cat) astraendo le API FileSystem. Per aggiungere un contenuto, apri l'app e trascina i file dal tuo desktop nella finestra del terminale.

Click to open the HTML5 Wow Terminal
Open the HTML5 Terminal

Casi d'uso

Ci sono diverse alternative disponibili in HTML5, ma il FileSystem è differente poichè mira a soddisfare casi d'uso di memorizzazione lato client che non vengono gestiti bene dai database. Generalmente queste sono applicazioni che trattano grossi blob binari e/o condividono dati con applicazioni al di fuori del contesto del browser.

L'elenco seguente illustra alcuni casi d'uso:

  1. Uploader persistente
    • Quando viene selezionato un file o una directory per l'upload, copiare i file in una sandbox locale e caricare un chunk per volta.
    • L'upload deve essere riavviato dopo l'occorrenza di crash del browser, interruzioni della linea, etc.
  2. Video game, player musicali o altre app con un gran numero di risorse multimediali
    • Scaricare uno o più grossi archivi e espanderli localmente in una directory.
    • Lo stesso download deve funzionare su ogni sistema operativo.
    • Gestire in background il prefetching della prossima risorsa di cui si avrà bisogno così che l'ingresso nel nuovo livello o l'attivazione di una nuova feature non richieda l'attesa del download.
    • Usare le risorse direttamente dalla cache locale tramite letture dirette dei file o gestione degli URI locali a immagini, loader video WebGL, etc.
    • I file possono essere file binari arbitrari.
    • Lato server un archivio è generalmente più piccolo di una collezione di file separatamente compressi. Inoltre un archivio invoca meno seek di mille piccoli file, il resto rimane invariato.
  3. Editor audio/foto con accesso offline o cache locale per velocizzare l'accesso alle risorse
    • I blob dati sono potenzialmente abbastanza grandi e vengono letti e scritti.
    • Può essere richiesto una scrittura parziale su file (sovrascrivendo solo il tag ID3/EXIF per esempio).
    • Organizzare i file dei progetti attraverso la creazione di directory potrebbe essere utile.
    • I file modificati potrebbero essere accessibili da applicazioni lato client [iTunes, Picasa].
  4. Visualizzatore di video offline
    • Scaricare grossi file (>1GB) per vederli successivamente.
    • Seek e straming efficiente.
    • Capacità di gestire un URI ad un tag video.
    • Accesso a file parzialmente scaricati ad esempio permettere di guardare il primo episodio di un DVD anche se il download non è stato completato prima che tu prenda il volo.
    • Possibilità di estrarre un singolo episodio nel mezzo di un download e passarlo al tag video.
  5. Client di Web Mail offline
    • Scaricare gli allegati e memorizzarli localmente.
    • Caching di allegati selezionati dall'utente per il loro successivo caricamento.
    • Necessità di potersi riferire ad allegati presenti nella cache e ad anteprime di immagini per la visualizzazione o l'upload.
    • Possibilità di avviare il download manager dello user agent proprio come se si stesse parlando con un server.
    • Possibilità di caricare mail con allegati attraverso una post multiparte invece di inviare un file per volta attraverso XHR.

Riferimenti per le specifiche

Comments

0