Обзор различных API файловой системы

HTML5 Rocks

Введение

Я всегда считал, что было бы очень удобно, если бы веб-приложения могли читать и записывать файлы и каталоги. По мере перехода из офлайн в онлайн приложения становятся все более сложными, но дальнейшему развитию Интернета мешает отсутствие API файловых систем. Хранение и использование двоичных данных не должно ограничиваться рабочим столом. К счастью, FileSystem API решает эту задачу. С его помощью любое веб-приложение может создавать, считывать, просматривать и записывать данные в тестовой среде локальной файловой системы пользователя.

В API можно выделить несколько аспектов.

  • Чтение файлов и операции с ними: File/Blob, FileList, FileReader.
  • Создание и запись: BlobBuilder, FileWriter.
  • Доступ к каталогам и файловым системам: DirectoryReader, FileEntry/DirectoryEntry, LocalFileSystem.

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

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

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

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

Веб-приложение может запрашивать доступ к файловой системе тестовой среды с помощью метода 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
Доступность хранилища файлов. Возможные значения: window.TEMPORARY и window.PERSISTENT. Данные, которые сохраняются с ключом TEMPORARY, могут быть удалены на усмотрение браузера (напимер, при нехватке места). Ключ PERSISTENT не позволяет удалять данные без разрешения пользователя или приложения, и для него необходима квота, предоставляемая приложению пользователем. См. запрос квоты
size
Размер необходимого приложению хранилища (в байтах).
successCallback
Функция обратного вызова, которая вызывается при успешном запросе файловой системы. В качестве аргумента выступает объект FileSystem.
opt_errorCallback
Необязательный вызов для обработки ошибок или при отказе в ответ на запрос файловой системы. В качестве аргумента выступает объект FileError.

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

Пример использования:

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

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

Спецификация FileSystem также определяет API для синхронной работы: интерфейс LocalFileSystemSync, предназначенный для использования вместе с объектами Web Worker. В этом руководстве API для синхронной работы не рассматриваются.

Далее в настоящем документе мы будем использовать для обработки ошибок, связанных с асинхронными вызовами, один и тот же обработчик:

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

Конечно, обратный вызов для этой ошибки носит общий характер, но идея понятна. Сообщения для пользователей должны быть осмысленными.

Запрос квоты хранилища

Для использования постоянного хранилища (ключ PERSISTENT) необходимо получить разрешение пользователя на хранение постоянных данных. На временное хранилище (ключ TEMPORARY) это правило не распространяется, поскольку браузер может удалять такую информацию по собственному усмотрению.

Для использования постоянного хранилища (ключ PERSISTENT) с FileSystem API браузер Chrome запрашивает хранилище, указывая новый API в атрибуте window.webkitStorageInfo:

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

Как только пользователь дает разрешение, необходимость в вызове функции requestQuota() отпадает. Все последующие вызовы являются пустыми операциями.

Существует также API для запроса текущей степени использования и распределения исходного хранилища: window.webkitStorageInfo.queryUsageAndQuota()

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

Для работы с файлами в тестовой среде предназначен интерфейс FileEntry. Он содержит типы свойств (name, isFile и т. д.) и методов (remove, moveTo, copyTo и т. д.), характерных для стандартной файловой системы.

Свойства и методы 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);
...

Чтобы показать, что такое FileEntry, в этом разделе мы разберем несколько примеров выполнения распространенных задач.

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

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

Приведенный ниже код создает в корневой папке файловой системы приложения пустой файл log.txt.

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

После запроса файловой системы обработчику передается объект FileSystem. В пределах обратного вызова можно вызвать функцию fs.root.getFile() с именем файла, который нужно создать. Путь может быть абсолютным или относительным (но он должен быть действительным). Например, не пытайтесь создать файл в несуществующей родительской папке. Вторым аргументом функции getFile() является объект, описывающий поведение функции в случае, если файл не существует. В нашем примере атрибут create: true создает файл, если он не существует, и выдает ошибку в противном случае (exclusive: true). Атрибут (create: false) приводит только к поиску и выдаче файла. Его содержание не перезаписывается ни при каких обстоятельствах, поскольку мы получаем только ссылку на соответствующий файл.

Чтение файла по названию

Приведенный ниже код извлекает файл под названием log.txt, читает его содержание с помощью FileReader API и записывает в новый блок <textarea> на странице. Если файл log.txt не существует, выдается ошибка.

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

Запись в файл

Приведенный ниже код создает пустой файл под названием log.txt (если он не существует) и заполняет его текстом 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);

На этот раз мы вызываем метод createWriter() объекта FileEntry и получаем элемент FileWriter. В обратном вызове настроены обработчики событий error и writeend. Для записи текстовых данных в файл создается объект blob, в него добавляется текст, после чего он передается функции FileWriter.write().

Прикрепление данных к файлу

Приведенный ниже код добавляет в конец файла log.txt текст Hello World. Если файл не существует, выдается ошибка.

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

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

Приведенный ниже код позволяет пользователю выбрать несколько файлов с помощью атрибута <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) {
    // 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);

};

Мы использовали для импорта файла элемент input, но ту же задачу можно решить с помощью функции перетаскивания в HTML5.

Как указано в комментарии, метод FileWriter.write() может принимать объект Blob или File. Это связано с тем, что File создается на основе Blob, а значит, оба файловых параметра являются blob-объектами.

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

Приведенный ниже код удаляет файл 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);

Работа с каталогами

Для работы с каталогами в тестовой среде предназначен интерфейс DirectoryEntry, который обладает большинством свойств FileEntry (они наследуются из общего интерфейса Entry). При этом DirectoryEntry поддерживает дополнительные методы для работы с каталогами.

Свойства и методы 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);
...

Создание каталогов

С помощью метода getDirectory() DirectoryEntry можно считывать и создавать каталоги. Для их поиска или создания можно указывать название или путь.

Например, приведенный ниже код создает в корневом каталоге папку под названием MyPictures.

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

Подкаталоги

Подкаталоги создаются точно так же, как и каталоги. В то же время при попытке создать каталог с несуществующим родительским элементом API выдает ошибку. Решение заключается в том, чтобы создавать каталоги последовательно, что не так просто сделать с асинхронным API.

Приведенный ниже код создает новую иерархию (music/genres/jazz) в корневой папке файловой системы приложения, добавляя очередной подкаталог после создания его родительской папки.

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

После этого полный путь к структуре music/genres/jazz можно передавать в функцию getDirectory() для создания новых подпапок и файлов. Например:

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

Чтение содержания каталога

Чтобы прочитать содержание каталога, создайте объект DirectoryReader и вызовите его метод readEntries(). Разовый вызов readEntries() не гарантирует получение всех содержащихся в каталоге элементов. Это значит, что метод DirectoryReader.readEntries() необходимо вызывать до тех пор, пока не перестанут появляться новые результаты. Это демонстрирует приведенный ниже код.

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

Удаление каталога

Метод DirectoryEntry.remove() действует точно так же, как и FileEntry. Разница только в том, что при попытке удаления несуществующего каталога выдается ошибка.

Приведенный ниже код удаляет пустой каталог jazz из папки /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);

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

Для удаления ненужного каталога, содержащего элементы, предназначен метод removeRecursively(). Он рекурсивно удаляет каталог и его содержание.

Приведенный ниже код рекурсивно удаляет папку music, в также все файлы и каталоги, которые она содержит.

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

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

FileEntry и DirectoryEntry выполняют аналогичные операции.

Копирование элемента

Для дублирования существующих элементов в обоих объектах (FileEntry и DirectoryEntry) предусмотрен метод copyTo(). Он автоматически создает рекурсивную копию папок.

Код в приведенном ниже примере копирует файл me.png из одного каталога в другой.

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

Перемещение и переименование записи

Для перемещения и переименования файлов и каталогов в объектах 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.TEMPORARY, 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.TEMPORARY, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);

filesystem: URL

FileSystem API вводит новую схему URL, filesystem:, которую можно использовать для заполнения атрибутов src и href. Например, чтобы вывести на экран изображение и получить для него объект fileEntry, вызовите метод toURL(), который возвращает filesystem:-URL соответствующего файла.

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

Если у вас уже есть filesystem:-URL, метод resolveLocalFileSystemURL() возвращает объект fileEntry.

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

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

Комбинация возможностей

Простой пример

Этот демонстрационный код выдает список файлов и папок в файловой системе.

Терминал HTML5

Эта оболочка воспроизводит некоторые из наиболее распространенных операций в файловой системе UNIX (например, cd, mkdir, rm, open и cat), используя для этого FileSystem API. Чтобы добавить файлы, перетащите их с рабочего стола в открытый ниже терминал.

Примеры использования

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

В спецификации приведено несколько примеров использования.

  1. Инструмент для постоянной загрузки
    • Когда для загрузки выбирается файл или каталог, файлы копируются в локальную тестовую среду и загружаются по частям.
    • В случае сбоя браузера, сети и т. д. загрузку можно возобновить.
  2. Видеоигры, музыка и другие приложения с большим объемом мультимедийных данных
    • Инструмент загружает один или несколько больших архивов и локально разворачивает их в структуру каталогов.
    • Общий способ загрузки должен работать в любой операционной системе.
    • Данные, которые вскоре понадобятся, могут выбираться в фоновом режиме, чтобы их загрузка при переходе на следующий уровень игры или при активации новой функции не занимала слишком много времени.
    • Загрузчик выбирает элементы непосредственно из локального кэша путем прямого считывания файлов или обработки локальных URL изображений или видеотегов, с помощью программ для загрузки объектов WebGL и т. д.
    • Файлы могут иметь любой двоичный формат.
    • Сжатый архив на сервере обычно намного меньше, чем набор файлов, сжатых по отдельности. Кроме того, один архив вместо 1000 небольших файлов требует меньше обращений. В остальном никакой разницы нет.
  3. Аудио- или фоторедактор с автономным доступом или локальным кэшем для скорости
    • Blob-объекты могут быть довольно большими и использоваться для чтения и записи.
    • Файлы могут перезаписываться частично (например, только теги ID3/EXIF).
    • Полезной является возможность систематизации файлов проекта с помощью структуры каталогов.
    • У клиентских приложений [iTunes, Picasa] должен быть доступ к измененным файлам.
  4. Офлайн-программа для просмотра видео
    • Загружает большие файлы (больше 1 ГБ) для их последующего просмотра.
    • Требует эффективного поиска и потоковой передачи.
    • Должна уметь передавать URL в видеотеги.
    • Требует доступа к частично загруженным файлам (например, чтобы можно было смотреть первый эпизод DVD-фильма, не дожидаясь полной загрузки).
    • Должна уметь извлекать один эпизод из загруженных данных и передавать его непосредственно в видеотег.
  5. Автономный почтовый веб-клиент
    • Загружает прикрепленные файлы и хранит их локально.
    • Кэширует выбранные пользователем прикрепленные файлы для их последующей загрузки.
    • Должен уметь обращаться к кэшированным прикрепленным файлам и миниатюрам изображений для отображения и загрузки.
    • Должен уметь вызывать менеджер загрузок UA так же, как при обращении к серверу.
    • Должен уметь загружать электронные письма с прикрепленными файлами как сообщения из нескольких частей, а не отправлять сразу весь файл с помощью XHR-запроса.

Справочные спецификации

Comments

0