Explorar as APIs FileSystem

HTML5 Rocks

Introdução

Sempre achei que seria útil se os aplicativos da web pudessem ler e gravar arquivos e diretórios. Quando passamos de off-line para on-line, os aplicativos se tornam mais complexos, e a falta das APIs do sistema de arquivos tem sido um obstáculo para o avanço da Web. O armazenamento ou a interação com dados binários não deveriam se limitar à área de trabalho. Felizmente, isso não acontece mais, graças à API FileSystem. Com a API FileSystem, um aplicativo da web pode criar, ler, navegar e gravar em uma seção em sandbox do sistema de arquivos local do usuário.

A API se subdivide em vários temas:

  • Leitura e manipulação de arquivos: File/Blob, FileList, FileReader
  • Criação e gravação: BlobBuilder, FileWriter
  • Acesso a diretórios e sistema de arquivos: DirectoryReader, FileEntry/DirectoryEntry, LocalFileSystem

Suporte de navegadores e limites de armazenamento

Até a data em que este artigo foi escrito, o Google Chrome tem a única implementação de trabalho da API FileSystem. Ainda não existe uma interface de usuário dedicada do navegador para o gerenciamento de arquivos/cotas. Para armazenar dados no sistema do usuário, seu aplicativo pode ter que solicitar cota. No entanto, para fins de teste, o Google Chrome pode ser executado com o sinalizador --unlimited-quota-for-files. Além disso, se você estiver criando um aplicativo ou uma extensão para a Chrome Web Store, a permissão do arquivo de manifesto unlimitedStorage pode ser usada em lugar da solicitação de cotas. No final, os usuários receberão uma caixa de diálogo de permissão para conceder, negar ou aumentar o armazenamento de um aplicativo.

O sinalizador --allow-file-access-from-files será necessário se você estiver depurando o aplicativo a partir de file://. A ausência desses sinalizadores resultará no erro de arquivo SECURITY_ERR ou QUOTA_EXCEEDED_ERR.

Como solicitar um sistema de arquivos

Um aplicativo da web pode solicitar acesso a um sistema de arquivos em sandbox, chamando 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
Se o armazenamento de arquivos precisa ser persistente. Os valores possíveis são window.TEMPORARY ou window.PERSISTENT. Os dados armazenados com o uso de TEMPORARY podem ser removidos a critério do navegador (por exemplo, se for necessário mais espaço). O armazenamento PERSISTENT só pode ser limpo se explicitamente autorizado pelo usuário ou pelo aplicativo e exige que o usuário conceda uma cota ao seu aplicativo. Consulte como solicitar cota.
size
Tamanho (em bytes) que o aplicativo exigirá para armazenamento.
successCallback
Retorno de chamada quando a solicitação de um sistema de arquivos é bem-sucedida. Seu argumento é um objeto FileSystem.
opt_errorCallback
Retorno opcional para o caso de erro ou quando a solicitação para obter o sistema de arquivos é negada. Seu argumento é um objeto FileError.

Se você estiver chamando requestFileSystem() pela primeira vez, um novo armazenamento será criado para seu aplicativo. É importante lembrar que esse sistema de arquivos fica em uma sandbox, o significa que um aplicativo da web não pode acessar arquivos de outro aplicativo. Isso também significa que você não pode ler/gravar arquivos em uma pasta arbitrária da unidade de disco rígido do usuário (por exemplo, Minhas imagens, Meus documentos, etc.).

Exemplo de uso:

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

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

A especificação FileSystem também define uma interface de API síncrona, LocalFileSystemSync,destinada ao uso no Web Workers. No entanto, este tutorial não aborda a API síncrona.

Ao longo deste documento, usaremos o mesmo manipulador para processar erros a partir de chamadas assíncronas:

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);
}

Embora esse retorno de erro seja muito genérico, dá para entender a ideia geral. Em vez disso, você pode fornecer mensagens legíveis aos usuários.

Como solicitar cotas de armazenamento

Para usar o armazenamento PERSISTENT, é necessário obter permissão do usuário para armazenar dados persistentes. A mesma restrição não se aplica a armazenamento TEMPORARY, porque o navegador pode optar por remover dados armazenados temporariamente, a seu critério.

Para usar o armazenamento PERSISTENT com a API FileSystem, o Google Chrome recorre a uma nova API em window.webkitStorageInfo para solicitar armazenamento:

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

Depois que o usuário dá permissão, não há necessidade de chamar requestQuota() no futuro. As chamadas subsequentes são um "no-op".

Há também uma API para consultar o uso e a alocação atuais de cotas de uma origem: window.webkitStorageInfo.queryUsageAndQuota()

Como trabalhar com arquivos

Os arquivos em um ambiente de sandbox são representados pela interface FileEntry. Uma interface FileEntry contém os tipos de propriedades (name, isFile, ...) e métodos (remove, moveTo, copyTo, ...) que se esperam de um sistema de arquivos padrão.

Propriedades e métodos de 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);
...

Para melhor entender a FileEntry, o restante desta seção contém um apanhado de dicas para a execução de tarefas comuns.

Como criar um arquivo

Você pode procurar ou criar um arquivo com getFile() do sistema de arquivos, um método da interface DirectoryEntry. Depois de solicitar um sistema de arquivos, o retorno de chamada é transmitido a um objeto FileSystem que contém um DirectoryEntry (fs.root) apontando para a raiz do sistema de arquivos do aplicativo.

O código a seguir cria um arquivo vazio chamado "log.txt" na raiz do sistema de arquivos do aplicativo:

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);

Depois que o sistema de arquivos é solicitado, o manipulador é transmitido a um objeto FileSystem. No retorno, podemos chamar fs.root.getFile() com o nome do arquivo a ser criado. Você pode transmitir um caminho absoluto ou relativo, mas ele deve ser válido. Por exemplo, é um erro tentar criar um arquivo cujo pai imediato não exista. O segundo argumento para getFile() é uma literal de objeto que descreve o comportamento da função, se o arquivo não existir. Nesse exemplo, create: true cria o arquivo, se ele não existe, e emite um erro, se ele existe (exclusive: true). Caso contrário (se create: false), o arquivo é simplesmente localizado e retornado. Em ambos os casos, o conteúdo do arquivo não é substituído, porque estamos apenas obtendo uma entrada de referência para o arquivo em questão.

Como ler um arquivo por nome

O código a seguir recupera o arquivo "log.txt", seu conteúdo é lido usando a API FileReader e é anexado à nova <área de texto> na página. Se o arquivo log.txt não existe, um erro é gerado.

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);

Como gravar um arquivo

O código a seguir cria um arquivo vazio chamado "log.txt" (se já não existir) e o preenche com o texto "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 bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Lorem Ipsum');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

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

Nesse momento, chamamos o métodocreateWriter() de FileEntry para obter um objeto FileWriter. No interior do retorno de chamada, os manipuladores de evento são configurados para os eventos error e writeend. Os dados do texto são gravados no arquivo, criando-se um blob, anexando texto a ele e transmitindo o blob para FileWriter.write().

Como anexar dados a um arquivo

O código a seguir anexa o texto "Hello World" ao final do arquivo de registro. Um erro é emitido se o arquivo não existir.

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 bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Hello World');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

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

Como duplicar arquivos selecionados pelo usuário

O código a seguir permite que um usuário selecione vários arquivos usando <input type="file" multiple /> e cria cópias desses arquivos no sistema de arquivos em sandbox do aplicativo.

<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(file.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);

};

Embora tenhamos usado uma entrada para a importação do arquivo, pode-se usar oArrastar e soltar do HTML5 para obter o mesmo efeito.

Como observado no comentário, FileWriter.write() pode aceitar um Blob ou File. Isso porque File é herdado de Blob, e portanto os objetos de arquivo são blobs.

Como remover um arquivo

O código a seguir exclui o arquivo "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);

Como trabalhar com diretórios

Os diretórios na sandbox são representados pela interface DirectoryEntry, que compartilha a maioria das propriedades de FileEntry (herdadas de uma interface Entry comum). No entanto, DirectoryEntry possui métodos adicionais para manipular diretórios.

Propriedades e métodos de 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);
...

Como criar diretórios

Use o método getDirectory() de DirectoryEntry para ler ou criar diretórios. Você pode transmitir um nome ou um caminho como o diretório a ser procurado ou criado.

Por exemplo, o código a seguir cria um diretório chamado "MyPictures" no diretório raiz:

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

Subdiretórios

A criação de um subdiretório é exatamente igual à criação de qualquer outro diretório. No entanto, a API emite um erro se você tentar criar um diretório cujo pai imediato não exista. A solução é criar cada diretório em sequência, o que é um pouco difícil de fazer com uma API assíncrona.

O código a seguir cria uma nova hierarquia (music/genre/jazz) na raiz de FileSystem do aplicativo, adicionando cada subdiretório de forma recursiva depois que a pasta pai é criada.

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);

Agora que "music/genre/jazz" está criada, podemos transmitir seu caminho completo para getDirectory() e criar novas subpastas ou arquivos abaixo dele. Por exemplo:

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

Como ler o conteúdo de um diretório

Para ler o conteúdo de um diretório, crie um DirectoryReader e chame seu método readEntries(). Não há nenhuma garantia de que todas as entradas de um diretório serão retornadas em uma única chamada de readEntries(). Isso significa que você precisa continuar a chamar DirectoryReader.readEntries() até que parem de ser retornados resultados. O código a seguir demonstra isso:

<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);

Como remover um diretório

O método DirectoryEntry.remove() se comporta exatamente como o FileEntry. A diferença é que a tentativa de excluir um diretório que não esteja vazio resulta em erro.

O exemplo a seguir remove o diretório vazio "jazz" de "/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);

Como remover um diretório de forma recursiva

Se você tiver um diretório que contenha entradas, removeRecursively() é um bom aliado. Ele exclui o diretório e seu conteúdo de forma recursiva.

O código a seguir remove recursivamente o diretório "music" e todos os arquivos e diretórios contidos nele:

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);

Como copiar, renomear e mover

FileEntry e DirectoryEntry compartilham operações comuns.

Como copiar uma entrada

Tanto FileEntry como DirectoryEntry possuem uma opção copyTo() para duplicar entradas já existentes. Esse método faz uma cópia recursiva nas pastas automaticamente.

O código a seguir copia o arquivo "me.png" de um diretório para outro:

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);

Como mover ou renomear uma entrada

FileEntry e DirectoryEntry moveTo() permitem mover ou renomear um arquivo ou diretório. O primeiro argumento é o diretório pai para o qual o arquivo deve ser movido, e o segundo argumento é um novo nome opcional para o arquivo. Se não for fornecido um novo nome, será usado o nome original do arquivo.

O exemplo a seguir renomeia o arquivo "me.png" para "you.png", mas não move o arquivo:

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);

O exemplo a seguir move o arquivo "me.png" (localizado no diretório raiz) para uma pasta chamada "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: URLs

A API FileSystem exibe um novo esquema de URL, filesystem:, que pode ser usado para preencher os atributos src e href. Por exemplo, para exibir uma imagem e ter fileEntry, chamar toURL() forneceria o URL filesystem: do arquivo:

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

Por outro lado, se você já tiver um URL filesystem:, resolveLocalFileSystemURL() retorna fileEntry:

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

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

Trocando em miúdos

Exemplo básico

Esta demonstração lista os arquivos e as pastas do sistema de arquivos.

Terminal HTML5

Esta shell reproduz algumas das operações comuns de um sistema de arquivos UNIX (por exemplo, cd, mkdir, rm, open e cat) abstraindo a API FileSystem. Para adicionar arquivos, arraste e solte-os da área de trabalho para o terminal a seguir.

Usos

Há várias opções de armazenamento no HTML5, mas o FileSystem é diferente, pois tem como objetivo satisfazer casos de uso de armazenamento do lado do cliente que não são bem veiculados por bancos de dados. Em geral, são aplicativos que lidam com grandes blobs binários e/ou compartilham dados com aplicativos fora do contexto do navegador.

A especificação traz vários casos de uso:

  1. Uploader persistente
    • Quando um arquivo ou diretório é selecionado para upload, ele copia os arquivos em uma sandbox local e faz upload de uma parte por vez.
    • Os uploads podem ser reiniciados após falhas do navegador, interrupções de rede, etc.
  2. Vídeo games, músicas ou outro aplicativo com vários recursos de mídia
    • Faz o download de um ou vários tarballs maiores e os expande localmente para uma estrutura de diretórios.
    • O mesmo download funciona em qualquer sistema operacional.
    • Pode fazer apenas a pré-busca dos próximos recursos a serem usados no segundo plano; assim, não é preciso esperar o download para passar para a próxima fase do jogo ou ativar um novo recurso.
    • Esses recursos são usados diretamente do cache local, por meio de leituras diretas do arquivo ou passando URIs locais para tags de imagens ou vídeo, carregadores de ativos WebGL, etc.
    • Os arquivos podem ter um formato binário arbitrário.
    • No servidor, um tarball compactado é, em geral, muito menor que um grupo de arquivos compactados separadamente. Além disso, um tarball, em vez de 1.000 pequenos arquivos, envolve menos buscas. O restante é igual.
  3. Editor de áudio/foto com acesso off-line ou cache local para velocidade
    • Os blobs de dados são, em geral, bem grandes e são de leitura e gravação.
    • Pode gravar arquivos apenas parcialmente (substituindo apenas as tags ID3/EXIF, por exemplo).
    • A capacidade de organizar arquivos de projeto criando diretórios seria útil.
    • Arquivos editados devem ficar acessíveis a aplicativos do lado do cliente [iTunes, Picasa].
  4. Visualizador de vídeos off-line
    • Faz download de arquivos grandes (>1GB) para posterior visualização.
    • Precisa de eficiência na busca e fluxo contínuo.
    • Precisa ser capaz de passar um URI à tag de vídeo.
    • Deve permitir o acesso a arquivos com download parcial, por exemplo, para que se possa assistir ao primeiro episódio do DVD, mesmo que o download não tenha sido concluído antes de entrar em um avião.
    • Deve permitir selecionar um único episódio do meio de um download e repassar para a tag de vídeo.
  5. Cliente de webmail off-line
    • Faz download de anexos e armazena no local.
    • Armazena em cache anexos selecionados pelo usuário para upload posterior.
    • Deve ser capaz de consultar anexos armazenados em cache e miniaturas de imagens para exibição e upload.
    • Deve ser capaz de disparar o gerenciador de download do UA como se estivesse se comunicando com um servidor.
    • Deve ser capaz de fazer upload de um e-mail com anexos como uma postagem de várias partes, em vez de enviar um arquivo por vez em um XHR.

Especificações de referência

Comments

0