Статьи

Файловый API PhoneGap

На этой неделе у меня была возможность записать несколько видео на PhoneGap для Adobe TV. Одно из видео было посвящено Файловому API, и, поскольку это было немного сложно, я решил поделиться своими результатами и примером кода с другими.

Моя основная борьба с File API заключалась в том, чтобы попытаться понять, как это работает. Документы были не совсем понятны для меня и были немного запутанными. Оказывается, есть веская причина для этого. (Хотя я работаю над улучшением документации.) Файловый API PhoneGap действительно является реализацией Файлового API W3 . Документы PhoneGap упоминают что-то похожее в области базы данных, поэтому имеет смысл также обновлять документы File. (И как я уже сказал — я работаю над этим. Я сделал свой первый запрос на добавление, чтобы добавить именно такое упоминание.)

После того, как я понял это, я нашел невероятно полезную статью о File API в HTML5 Rocks: Изучение API файловой системы . Я призываю всех прочитать статью Эрика Бидельмана. У него есть примеры практически для каждой части API.

На высоком уровне работа с File API сводится к нескольким основным понятиям:

  • Сначала вы запрашиваете файловую систему. Вы можете запросить постоянную или временную файловую систему. На рабочем столе они оба указывают на изолированную папку. На PhoneGap ваш доступ немного шире, по сути, вся система хранения.
  • API поддерживает базовые операции «CRUD» для файлов и папок.
  • API поддерживает чтение и запись в файлы, как двоичные, так и обычные текстовые.
  • Вероятно, самый сложный аспект (ну, не сложный, просто немного громоздкий) заключается в том, что каждая операция асинхронна. Таким образом, для получения и чтения файла требуется около 3 или 4 уровня обратных вызовов.

Для своего видео Adobe TV я создал простое приложение, демонстрирующее некоторые из этих принципов. Я начал с нескольких простых кнопок, которые позволили бы мне проверить основные операции с файлами:

Для того, чтобы что-то сделать, мне нужен доступ к файловой системе, и это нужно сделать после того, как PhoneGap запустит событие deviceready:

function onDeviceReady() {

    //request the persistent file system
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFSSuccess, onError);
    
}

function init() {
    document.addEventListener("deviceready", onDeviceReady, true);
} 

Если файловая система загружена, onFSSuccess будет обрабатывать сохранение указателя на нее, а также настраивать мои обработчики событий:

function onFSSuccess(fs) {
    fileSystem = fs;

    getById("#dirListingButton").addEventListener("touchstart",doDirectoryListing);            
    getById("#addFileButton").addEventListener("touchstart",doAppendFile);            
    getById("#readFileButton").addEventListener("touchstart",doReadFile);            
    getById("#metadataFileButton").addEventListener("touchstart",doMetadataFile);            
    getById("#deleteFileButton").addEventListener("touchstart",doDeleteFile);            
    
    logit( "Got the file system: "+fileSystem.name +"<br/>" +
                                    "root entry name is "+fileSystem.root.name + "<p/>")    

    doDirectoryListing();
} 

Кстати, getById — это просто оболочка для document.getElementById. (Попытка уменьшить мою зависимость от jQuery.) Наш объект fileSystem имеет несколько свойств, которые мы можем отобразить, например, имя. Он также имеет корневое свойство, которое является указателем на корневой каталог. (Дух.) Функция logit просто добавляет DIV на HTML-страницу в качестве метода быстрой отладки.

Этот обработчик событий затем запускает doDirectoryListing. Обычно он запускается кнопкой «Показать содержимое каталога», но я автоматически запускаю ее после открытия файловой системы.

function gotFiles(entries) {
    var s = "";
    for(var i=0,len=entries.length; i<len; i++) {
        //entry objects include: isFile, isDirectory, name, fullPath
        s+= entries[i].fullPath;
        if (entries[i].isFile) {
            s += " [F]";
        }
        else {
            s += " [D]";
        }
        s += "<br/>";
        
    }
    s+="<p/>";
    logit(s);
}

function doDirectoryListing(e) {
    //get a directory reader from our FS
    var dirReader = fileSystem.root.createReader();

    dirReader.readEntries(gotFiles,onError);        
}

Читая снизу вверх, обработчик событий запускается, создавая объект чтения из корневого свойства объекта файловой системы. Чтобы получить файлы, вы просто вызываете readEntries и используете обратный вызов для обработки результата. Записи (которые могут быть файлами или каталогами) представляют собой простой массив объектов. Вот пример вывода:

Так что насчет чтения и записи файлов? Открыть файл просто. Вы можете просто запустить getFile (name), и API может (если хотите) также создать файл, если он не существует. Это немного упрощает вещи. Вот обработчик событий и перезвоним для нажатия «Создание / добавление в тестовый файл».

function appendFile(f) {

    f.createWriter(function(writerOb) {
        writerOb.onwrite=function() {
            logit("Done writing to file.<p/>");
        }
        //go to the end of the file...
        writerOb.seek(writerOb.length);
        writerOb.write("Test at "+new Date().toString() + "\n");
    })

}

function doAppendFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, appendFile, onError);
} 

Снова — пожалуйста, прочитайте снизу вверх. Здесь вы можете увидеть использование getFile вместе с параметрами после него, чтобы гарантировать, что ошибка не будет выдана, если ее не существует. Присоединение к файлу осуществляется путем создания объекта записи. Обратите внимание — и я облажался сам — если вы не стремитесь к концу файла, вы на самом деле перезаписываете данные, а не добавляете их. Теперь давайте посмотрим на чтение:

function readFile(f) {
    reader = new FileReader();
    reader.onloadend = function(e) {
        console.log("go to end");
        logit("<pre>" + e.target.result + "</pre><p/>");
    }
    reader.readAsText(f);
}

function doReadFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, readFile, onError);
} 

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

Теперь давайте посмотрим на метаданные. Этот метод не возвращает много данных — только дата изменения файла / каталога.

function metadataFile(m) {
    logit("File was last modified "+m.modificationTime+"<p/>");    
}

function doMetadataFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, function(f) {
        f.getMetadata(metadataFile,onError);
    }, onError);
} 

Наконец — давайте посмотрим на операцию удаления:

function doDeleteFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, function(f) {
        f.remove(function() {
            logit("File removed<p/>");
        });
    }, onError);
} 

Я надеюсь, что эти примеры имеют смысл. Если это не очевидно, я немного подправил свой стиль, создавая каждый из разделов. Иногда я писал обратные вызовы в вызовах API, а иногда я делал это отдельно. Я включил полный код ниже, а также APK для тех, кто хочет проверить на Android.

<!DOCTYPE HTML>
<html>

<head>
<meta name="viewport" content="width=320; user-scalable=no" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Minimal AppLaud App</title>

<script type="text/javascript" charset="utf-8" src="phonegap-1.4.1.js"></script>
<script type="text/javascript" charset="utf-8">
var fileSystem;

//generic getById
function getById(id) {
    return document.querySelector(id);
}
//generic content logger
function logit(s) {
    getById("#content").innerHTML += s;
}

//generic error handler
function onError(e) {
    getById("#content").innerHTML = "<h2>Error</h2>"+e.toString();
}

function doDeleteFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, function(f) {
        f.remove(function() {
            logit("File removed<p/>");
        });
    }, onError);
}

function metadataFile(m) {
    logit("File was last modified "+m.modificationTime+"<p/>");    
}

function doMetadataFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, function(f) {
        f.getMetadata(metadataFile,onError);
    }, onError);
}

function readFile(f) {
    reader = new FileReader();
    reader.onloadend = function(e) {
        console.log("go to end");
        logit("<pre>" + e.target.result + "</pre><p/>");
    }
    reader.readAsText(f);
}

function doReadFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, readFile, onError);
}

function appendFile(f) {

    f.createWriter(function(writerOb) {
        writerOb.onwrite=function() {
            logit("Done writing to file.<p/>");
        }
        //go to the end of the file...
        writerOb.seek(writerOb.length);
        writerOb.write("Test at "+new Date().toString() + "\n");
    })

}

function doAppendFile(e) {
    fileSystem.root.getFile("test.txt", {create:true}, appendFile, onError);
}

function gotFiles(entries) {
    var s = "";
    for(var i=0,len=entries.length; i<len; i++) {
        //entry objects include: isFile, isDirectory, name, fullPath
        s+= entries[i].fullPath;
        if (entries[i].isFile) {
            s += " [F]";
        }
        else {
            s += " [D]";
        }
        s += "<br/>";
        
    }
    s+="<p/>";
    logit(s);
}

function doDirectoryListing(e) {
    //get a directory reader from our FS
    var dirReader = fileSystem.root.createReader();

    dirReader.readEntries(gotFiles,onError);        
}

function onFSSuccess(fs) {
    fileSystem = fs;

    getById("#dirListingButton").addEventListener("touchstart",doDirectoryListing);            
    getById("#addFileButton").addEventListener("touchstart",doAppendFile);            
    getById("#readFileButton").addEventListener("touchstart",doReadFile);            
    getById("#metadataFileButton").addEventListener("touchstart",doMetadataFile);            
    getById("#deleteFileButton").addEventListener("touchstart",doDeleteFile);            
    
    logit( "Got the file system: "+fileSystem.name +"<br/>" +
                                    "root entry name is "+fileSystem.root.name + "<p/>")    

    doDirectoryListing();
}

function onDeviceReady() {

    //request the persistent file system
    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFSSuccess, onError);
    
}

function init() {
    document.addEventListener("deviceready", onDeviceReady, true);
}
</script>

<style>
button { width: 100%; padding: 5px; }
</style>
</head>

<body onload="init();" id="stage" class="theme">

<button id="addFileButton">Create/Append to Test File</button>
<button id="readFileButton">Read Test File</button>
<button id="metadataFileButton">Get Test File Metadata</button>
<button id="deleteFileButton">Delete Test File</button>
<button id="dirListingButton">Show Directory Contents</button>

<div id="content"></div>

</body>
</html>