Pull to refresh

FileSystem API&File API: разбираемся и используем

Reading time14 min
Views95K
HTML5 Powered with Performance & Integration, and Offline & Storage
В данной статье я хочу рассмотреть FileSystem API и File API, разобраться с его методами и показать пару полезных штук. Эта статья является компиляцией материалов с html5rocks (1, 2, 3). Все представленные ниже демки можно посмотреть по первым двум ссылкам. Третья ссылка так же предлагает ряд интересных демо. Ну а теперь займемся изучением материала.

В качестве введения


С помощью FileSystem API и File API веб приложение может создавать, читать, просматривать и записывать файлы находящиеся в области пользовательской «песочницы».

Описание API разбито на следующие секции:
  • Чтение и управление файлами: File/Blob, FileList, FileReader
  • Создание и запись: BlobBuilder, FileWriter
  • Работа с директориями и права доступа: DirectoryReader, FileEntry/DirectoryEntry, LocalFileSystem

Пара слов об объектах, с которыми предстоит работать:
  1. File — собственно файл; позволяет получить такую доступную только для чтения информацию, как имя, размер, mimetype и прочее.
  2. FileList — «массив» объектов File.
  3. Blob — сущность, позволяющая разбирать файл по байтам.

Поддержка браузерами и ограничение на хранение

На момент написания статьи только Google Chrome 9+ имеет рабочую реализацию FileSystem API. И на данный момент пока нет никаких диалоговых окон для управления файлами и квотами на хранилище, поэтому нужно будет использовать флаг --unlimited-quota-for-files (в случае разработки приложений для Chrome Web Store будет достаточно манифеста с разрешением unlimitedStorage). Но все меняется и пользователи в скором времени получат возможность для управления правами по работе с файлами, которые будут требоваться приложению.

Вам может потребоваться использование флага --allow-file-access-from-files , если вы дебажите приложение с использованием file://. Если этот флаг не использовать, то будут выброшены исключения типа SECURITY_ERR или QUOTA_EXCEEDED_ERR.

Обращаемся к файловой системе


Проверим поддержку браузером нужных нам функций
// Проверяем поддержку File API 
if (window.File && window.FileReader && window.FileList && window.Blob) {
  // Работает
} else {
  alert('File API не поддерживается данным браузером');
}

Веб приложение может обратиться к файловой системе (естественно в ограниченной «песочнице») вызвав следующий метод window.requestFileSystem():
window.requestFileSystem(type, size, successCallback, opt_errorCallback)

type
Правила хранения, доступные значения window.TEMPORARY и window.PERSISTENT. данные, хранящиеся с использованием ключа TEMPORARY могут быть удаление по усмотрению браузером (например, если не хватает места). Если же выставлен ключPERSISTENT, то данные могут быть очищены только после действий пользователя или приложения.

size
Размер (в байтах) хранилища, которое потребуется приложению.

successCallback
Callback-функция, выполняемая в случае успешного обращения к файловой системе. Ее аргументом является объект типа FileSystem.

opt_errorCallback
Необязательная callback-функция для обработки ошибок. Так же вызывается, когда возникают ошибки обращения к файловой системе. Параметром является объект типа FileError.

Если вы вызываете метод requestFileSystem() в рамках вашего приложения в первый раз, то в этот момент и будет создано хранилище. Очень важно помнить, что данное хранилище является закрытым и другое приложение не будет иметь к нему доступа. Это так же значит, что приложение не может менять прочие файлы и папки, расположенные на жестком диске.

Пример использования
function onInitFs(fs) {
  console.log('Opened file system: ' + fs.name);
}

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

Спецификация по FileSystem так же описывает API для синхронной работы, а именно интерфейс LocalFileSystemSync, который предполагается использовать совместно Web Workers. Но в этой статье данное API не будет рассмотрено.

Возвращаясь к методу requestFileSystem() стоит описать возможные варианты возникающих ошибок:
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);
}

Описанный пример очень прост, но по сути является заготовкой для дальнейшей работы с возникающими ошибками.

Работа с файлам


Для работы с файлам предусмотрен интерфейс 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.toURI(opt_mimeType);  // Currently not implemented in Google Chrome 9.

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

Разберем на примерах азы работы с FileEntry.

Создание файла

Получить или создать файл можно с помощью метода getFile() у интерфейса DirectoryEntry. После обращения к хранилищу, callback возвращает нам объект FileSystem, содержащий в себе DirectoryEntry (fs.root), ссылающийся на корневую папку хранилища.

Следующий код создаст пустой файл «log.txt»:
function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {
    // fileEntry будет иметь следующие свойства
    // fileEntry.isFile === true
    // fileEntry.name == 'log.txt'
    // fileEntry.fullPath == '/log.txt'

  }, errorHandler);

}
window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler);

Итак, после обращения к файловому хранилищу, у нас в руках оказывается FileSystem. Внутри сallback-функции мы можем обратится к методу fs.root.getFile(), передав имя файла, который требуется создать. Можно передать как относительный, так и абсолютный путь — главное чтобы он был верным. Например, ошибочным будет создание файла, если его родительская папка не существует. Вторым аргументом метода getFile() является объект, описывающий параметры объекта, которые будут к нему применены, если он еще не создан. Более подробно можно прочитать в документации.

Чтение файла по имени

Следующий код обращается к файлу «log.txt» и читает его содержимое с помощью FileReader API, после чего записывает все содержимое в блок <textarea>. Если файл не существует, то будет выброшена ошибка.
function onInitFs(fs) {

  fs.root.getFile('log.txt', {}, function(fileEntry) {
    
    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.PERSISTENT, 1024*1024, onInitFs, errorHandler);

FileReader помимо прочего предоставляет следующие методы для чтения:
  • FileReader.readAsBinaryString(Blob|File) — результат будет содержать байтовую строку.
  • FileReader.readAsText(Blob|File, opt_encoding) — результат будет содержать текстовую строку. Кодировка по умолчанию — 'UTF-8', менять можно с помощью задания опционального параметра
  • FileReader.readAsDataURL(Blob|File) — на выходе имеем data URL.
  • FileReader.readAsArrayBuffer(Blob|File) — получаем данные в виде ArrayBuffer.

В следующем примере мы зачитаем изображение и выведем его thumbnail:
<style>
  .thumb {
    height: 75px;
    border: 1px solid #000;
    margin: 10px 5px 0 0;
  }
</style>

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // Loop through the FileList and render image files as thumbnails.
    for (var i = 0, f; f = files[i]; i++) {

      // Only process image files.
      if (!f.type.match('image.*')) {
        continue;
      }

      var reader = new FileReader();

      // Closure to capture the file information.
      reader.onload = (function(theFile) {
        return function(e) {
          // Render thumbnail.
          var span = document.createElement('span');
          span.innerHTML = ['<img class="thumb" src="', e.target.result,
                            '" title="', theFile.name, '"/>'].join('');
          document.getElementById('list').insertBefore(span, null);
        };
      })(f);

      // Read in the image file as a data URL.
      reader.readAsDataURL(f);
    }
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Иногда нам может потребоваться не весь файл, а лишь его часть, для этого удобно использовать File.slice(start_byte,length).
Выглядит это так:
var blob = file.slice(startingByte, length);
reader.readAsBinaryString(blob);

В следующем примере мы сможем прочитать либо нужные на байты, либо весь файл целиком. Особое внимание обратите на onloadend и evt.target.readyState, которые в данном случае будут заменять нам событие onload. (О событиях чуть ниже).
<style>
  #byte_content {
    margin: 5px 0;
    max-height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
  }
  #byte_range { margin-top: 5px; }
</style>

<input type="file" id="file" name="file" /> Read bytes: 
<span class="readBytesButtons">
  <button data-startbyte="0" data-endbyte="4">1-5</button>
  <button data-startbyte="5" data-endbyte="14">6-15</button>
  <button data-startbyte="6" data-endbyte="7">7-8</button>
  <button>entire file</button>
</span>
<div id="byte_range"></div>
<div id="byte_content"></div>

<script>
  function readBlob(opt_startByte, opt_stopByte) {

    var files = document.getElementById('files').files;
    if (!files.length) {
      alert('Please select a file!');
      return;
    }

    var file = files[0];
    var start = opt_startByte || 0;
    var stop = opt_stopByte || file.size - 1;

    var reader = new FileReader();

    // If we use onloadend, we need to check the readyState.
    reader.onloadend = function(evt) {
      if (evt.target.readyState == FileReader.DONE) { // DONE == 2
        document.getElementById('byte_content').textContent = evt.target.result;
        document.getElementById('byte_range').textContent = 
            ['Read bytes: ', start + 1, ' - ', stop + 1,
             ' of ', file.size, ' byte file'].join('');
      }
    };
    var length =  (stop - start) + 1;
    var blob = file.slice(start, length);
    reader.readAsBinaryString(blob);
  }
  
  document.querySelector('.readBytesButtons').addEventListener('click', function(evt) {
    if (evt.target.tagName.toLowerCase() == 'button') {
      var startByte = evt.target.getAttribute('data-startbyte');
      var endByte = evt.target.getAttribute('data-endbyte');
      readBlob(startByte, endByte);
    }
  }, false);
</script>

Теперь о событиях. FileReader предоставляет нам следующие типы событий:
  • onloadstart
  • onprogress
  • onload,
  • onabort
  • onerror
  • onloadend

Их можно использовать, когда нужно отобразить процесс загрузки файла. Например так:
<style>
  #progress_bar {
    margin: 10px 0;
    padding: 3px;
    border: 1px solid #000;
    font-size: 14px;
    clear: both;
    opacity: 0;
    -moz-transition: opacity 1s linear;
    -o-transition: opacity 1s linear;
    -webkit-transition: opacity 1s linear;
  }
  #progress_bar.loading {
    opacity: 1.0;
  }
  #progress_bar .percent {
    background-color: #99ccff;
    height: auto;
    width: 0;
  }
</style>

<input type="file" id="file" name="file" />
<button onclick="abortRead();">Cancel read</button>
<div id="progress_bar"><div class="percent">0%</div></div>

<script>
  var reader;
  var progress = document.querySelector('.percent');

  function abortRead() {
    reader.abort();
  }

  function errorHandler(evt) {
    switch(evt.target.error.code) {
      case evt.target.error.NOT_FOUND_ERR:
        alert('File Not Found!');
        break;
      case evt.target.error.NOT_READABLE_ERR:
        alert('File is not readable');
        break;
      case evt.target.error.ABORT_ERR:
        break; // noop
      default:
        alert('An error occurred reading this file.');
    };
  }

  function updateProgress(evt) {
    // evt is an ProgressEvent.
    if (evt.lengthComputable) {
      var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
      // Increase the progress bar length.
      if (percentLoaded < 100) {
        progress.style.width = percentLoaded + '%';
        progress.textContent = percentLoaded + '%';
      }
    }
  }

  function handleFileSelect(evt) {
    // Reset progress indicator on new file selection.
    progress.style.width = '0%';
    progress.textContent = '0%';

    reader = new FileReader();
    reader.onerror = errorHandler;
    reader.onprogress = updateProgress;
    reader.onabort = function(e) {
      alert('File read cancelled');
    };
    reader.onloadstart = function(e) {
      document.getElementById('progress_bar').className = 'loading';
    };
    reader.onload = function(e) {
      // Ensure that the progress bar displays 100% at the end.
      progress.style.width = '100%';
      progress.textContent = '100%';
      setTimeout("document.getElementById('progress_bar').className='';", 2000);
    }

    // Read in the image file as a binary string.
    reader.readAsBinaryString(evt.target.files[0]);
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>


Запись в файл

С помощью следующего кода мы создадим файл «log.txt» (если он не существует) и запишем в него 'Ipsum Lorem'.
function onInitFs(fs) {

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

    fileEntry.createWriter(function(fileWriter) {

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

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

      var bb = new BlobBuilder();
      bb.append('Ipsum Lorem');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

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

Как видно, мы обращаемся к методу createWriter() для получения объекта. Кроме этого, мы обрабатываем события окончания записи в файл и возможного создания ошибки.

Дописываем данные в файл

Следующий код допишет 'Hello World' в конец файла. Если файла нет, то выброситься ошибка.
function onInitFs(fs) {

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

    fileEntry.createWriter(function(fileWriter) {

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

      var bb = new BlobBuilder();
      bb.append('Hello World');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

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


Создание копий выбранных файлов

Следующий код позволяет пользователю выбирать несколько файлов, используя <input type=«file» multiple> и создает копии этих файлов.
<input type="file" id="myfile" multiple />

document.querySelector('#myfile').onchange = function(e) {
  var files = this.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {    
    for (var i = 0, file; file = files[i]; ++i) {

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

};

Для упрощения выбора файлов можно использовать HTML5 Drag and Drop.

<div id="drop_zone">Drop files here</div>
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.dataTransfer.files; // FileList object.

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', f.name, '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate.toLocaleDateString(), '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  function handleDragOver(evt) {
    evt.stopPropagation();
    evt.preventDefault();
  }

  // Setup the dnd listeners.
  var dropZone = document.getElementById('drop_zone');
  dropZone.addEventListener('dragover', handleDragOver, false);
  dropZone.addEventListener('drop', handleFileSelect, false);
</script>


Удаление файлов

Следующий код удалит 'log.txt'.
window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) {
  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

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

  }, errorHandler);
}, errorHandler);


Работа с директориями


Работа с директориями осуществляется за счет использования DirectoryEntry, который обладает большинством свойств FileEntry ( они оба наследуют интерфейс Entry). Перечислим ниже свойства и методы DirectoryEntry.
dirEntry.isDirectory === true
// Остальное так же как в FileEntry
...

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


Создание директорий

Для создания и обращения к директориям используется getDirectory() интерфейса DirectoryEntry. Можно передавать как имя, так и путь до директории.

Создадим в корне директорию «MyPictures»:
window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) {
  fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) {
    ...
  }, errorHandler);
}, errorHandler);


Поддиректории

Создание поддиректорий по сути то же самое, что и создание директорий, однако надо помнить, что если родительская директория не существует, то будет выброшена ошибка. Следующий код показывает, как можно обойти это ограничение:
var path = 'music/genres/jazz/';

function createDir(rootDirEntry, folders) {
  //Фильтруем './' и '/' 
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    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.PERSISTENT, 1024*1024, onInitFs, errorHandler);

Теперь у нас создана директория «music/genres/jazz» и мы можем обращаться к любому ее уровню и создавать в них новые файлы. Например:
window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) {
  fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) {
    ...
  }, errorHandler);
}, errorHandler);


Разбираем содержимое директории

Чтобы узнать, что содержится в директории нужно создать DirectoryReader и вызвать его метод readEntries(), но при этом нет гарантии, что вернется все содержимое выбранной директории (!!!). Это значит, что надо обращаться к методу DirectoryReader.readEntries(), пока результат не станет пустым. Пример:
<ul id="filelist"></ul>

function toArray(list) {
  return Array.prototype.slice.call(list || [], 0);
}

function listResults(entries) {
  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 = [];

  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries();

}

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


Удаляем директорию

Для удаления следует вызвать метод DirectoryEntry.remove(). Важно знать, что если попытаться удалить не пустую директорию, то будет выброшена ошибка.

Удалим пустую директорию «jazz» из "/music/genres/":
window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) {
  fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) {

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

  }, errorHandler);
}, errorHandler);


Рекурсивно удаляем директории

Если у вас есть не пустая директория и вы все же хотите ее удалить, то вам поможет метод removeRecursively(), который удалить и директорию и все ее содержимое.

Произведем эту операцию с директорией «music»:
window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) {
  fs.root.getDirectory('/misc/../music', {}, function(dirEntry) {

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

  }, errorHandler);
}, errorHandler);


Копируем, переименовываем и перемещаем


FileEntry и DirectoryEntry полностью идентичны в этом аспекте.

Копируем

И FileEntry и DirectoryEntry имеют метод copyTo() для копирования. В случае директорий, метод рекурсивно создаст и все содержимое.

Скопируем «me.png» из одной директории в другую:
function copy(cwd, src, dest) {
  cwd.getFile(src, {}, function(fileEntry) {

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

  }, errorHandler);
}

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


Перемещение или переименование

У FileEntry и DirectoryEntry есть метод moveTo(), позволяющие перемещать и переименовывать файлы и директории. Первым аргументом является родительская папка, вторым (опциональным) параметром является новое имя.

Переименуем «me.png» в «you.png»:
function rename(cwd, src, newName) {
  cwd.getFile(src, {}, function(fileEntry) {
    fileEntry.moveTo(cwd, newName);
  }, errorHandler);
}

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


Переместим «me.png» из корневой директории в «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.PERSISTENT, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);


Use Cases

HTML5 предлагает несколько вариантов локального хранения данных, но FileSystem отличается тем, что позволяет удовлетворить те потребности, которые не удовлетворяют базы данных. В основном, это нужды приложение, которые хранят большой объем бинарных данных и/или предоставляет доступ к файлам приложениям вне браузера.

Приведем список возможных применений:
  1. Uploader файлов
  2. Игры и приложение, работающие с медиа файлами
  3. Аудио/фото редактор, с оффлайновым режимом работы
  4. Оффлановый видео проигрыватель
  5. Оффлайновый почтовый клиент
Tags:
Hubs:
+89
Comments35

Articles