AngularJS で Promise (defer)を使ってファイルシステムからファイルを読み込む (cordova filesystem)

# Monaca & OnsenUI でハイブリッドアプリ

OnsenUI = AngularJSなので、MonacaでAngularJSの形に従いながら、アプリケーションを作成している。

端末にファイルを保存＆読み取りする場合、PhoneGap(Cordova) FileAPI のお世話になる。

## HTML5 File API でファイルを扱う

ファイルを読み書きするだけならコールバック関数に仕事してもらって・・で良いんだけど、ファイルの中身をリスト形式で持たせて、画面に表示・・を使用とすると、表示したいタイミングでデータが取得出来てるわけではない非同期処理の壁に阻まれた。。

### Promise でやってみる

そんな時はPromise!

- [AngularJS - Promiseを使おう - Qiita](http://qiita.com/teyosh/items/d7d3c17f954318c88882)
- [JavaScript - 今更だけどPromise入門 - Qiita](http://qiita.com/koki_cheese/items/c559da338a3d307c9d88?utm_content=buffer5bfea&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer)

AngularJS で Promise するには、``` $q.defer() ``` する。

#### 基本パターン

``` 
function slowCall() {
  var df = $q.defer();
  // 非同期処理を担当する関数を定義
  function fn() {
    var msg = 'now : ' + new Date().getTime();
    alert('inside async function.');
    df.resolve(msg);
  };
  $timeout(fn, 1000);
  return df.promise;
};
```

この、定義した ``` slowCall ``` 関数はPromiseを返してくれる。
非同期処理した後に、その結果を用いて更に処理を続けたい時は・・

#### 呼び出し側

```
var testDeferSample = function () {
  alert('test defer sample start.');
  slowCall().then(function () {
    alert('END!');
  });
  alert('Opps.. outside');
};
```

呼び出すとこんな感じ。

```
test defer sample start.
Opps.. outside
inside async function.
END!
```

非同期処理が無事終わった後のEND!

#### 幾つも非同期呼び出しして、最後の最後になんか処理をはさみたい時

``` $q.all ``` を使う。

```
var testAllSample = function() {
  alert('test sample start.');
  var p1 = slowCall().then(function(msg) {
    alert('LAST 1' + msg);
    return msg;
  });
  var p2 = slowCall().then(function(msg) {
    alert('LAST 2' + msg);
    return msg;
  });
  $q.all([p1,p2]).then(function(res) {
    alert('LAST MOST ' + res[0] + ', ' + res[1]);
  });
};
```

呼び出すとこんな感じ

```
test all sample start.
inside async function.
LAST 1 1440643852941
inside async function.
LAST 2 1440643876515
LAST MOST 1440643852941, 1440643876515
```

p1, p2で返してきた値が最後のall呼び出しで受け取れる。

p1, p2で何も ``` return ``` しない場合、 undefined が返ってくる。
resはallで呼び出した関数の数分のオブジェクトが入っている。

## ファイル操作の実際

### ファイルシステムを取得

```
var getFileSystem = function() {
  var deferred = $q.defer();
  window.requestFileSystem(window.PERSISTENT, 1024*1024, function(filesystem) {
    deferred.resolve(filesystem);
  });
  return deferred.promise;
};
```

### パスを指定してディレクトリを取得

```
var getTargetDirectory = function(dirPath) {
  var deferred = $q.defer();
  getFileSystem().then(function(filesystem) {
    filesystem.root.getDirectory(dirPath, { create: true, exclusive: false },
      function(dirEntry) {
        deferred.resolve(dirEntry);
      },
      failFiles
    );
  });
  return deferred.promise;
};
```

ディレクトリをオープンする時に、指定されたディレクトリがない場合、作成するオプションを渡している。
``` { create: true, exclusive: false }```
作成したくない場合、 ``` { create: false }``` にする。

``` failFiles ``` はディレクトリの取得に失敗した場合を扱うエラー処理関数。

こんな感じでエラーがあったことを表示してるだけ。

```
function failFiles(error) {
  alert('failFiles' + error.code);
};
```

### 指定されたディレクトリにあるファイル名を一覧で返す

```
var listDir = function (dirPath) {
  var deferred = $q.defer();
  getTargetDirectory(dirPath).then(function (dirEntry) {
    var reader = dirEntry.createReader();
    var fileNames = [];
    reader.readEntries(
      function (entries) {
        for (var i = 0; i < entries.length; i++) {
          fileNames.push(entries[i].name);
        }
        deferred.resolve(fileNames);
      },
      function (error) {
        console.log("reading target directory error");
      }
    );
  });
  return deferred.promise;
};
```



### ファイルの中身を読む場合

```
var readFile = function(dirPath, fileName) {
  var deferred = $q.defer();
  getTargetDirectory(dirPath).then(function(dirEntry) {
    dirEntry.getFile(fileName, { create: false },
      function(fileEntry) {
        fileEntry.file(function(file) {
          var reader = new FileReader();
          reader.onloadend = function() {
            deferred.resolve(this.result);
          };
          reader.readAsText(file);
        });
      },
      function(error) {
        console.log("reading target directory error");
      }
    );
  });
  return deferred.promise;
};
```

### ファイルを保存する場合

```
var _writeFile = function(dirPath, fileName, fileText) {
  var deferred = $q.defer();
  getTargetDirectory(dirPath).then(function(dirEntry) {
    dirEntry.getFile(fileName, { create: true },
      function(fileEntry) {
        fileEntry.createWriter( 
          function(writer) {
            var cb = function() {
              //console.log("write end");
              alert("保存しました。");
              deferred.resolve(fileName);
            }
            writer.onwrite = cb;
            writer.onerror = function() { console.log("保存に失敗しました。"); }
            //console.log( fileText );
            writer.write( fileText );
          } ,
          function() {
            console.log("create writer error");
          }
        );
      },
      failFiles
    );
  });
  return deferred.promise;
};
```

## ディレクトリを指定したら、ファイル名とファイルの中身をJSONにして返して欲しい

### ディレクトリパスとファイル名を指定されたら、JSONオブジェクトにして返すPromise.
```
var createJsonObject = function (targetDirPath, fileName) {
  var deferred = $q.defer();
  readFile(targetDirPath, fileName).then(function (content) {
    var json = { name: fileName, data: content };
    deferred.resolve(json);
  });
  return deferred.promise;
};
```

### 後でJSONオブジェクトを$q.all()呼び出しするために、関数を生成する関数をリストで保持する

```
var createCallbackFunctionList = function (targetDirPath, fileNames) {
  var deferred = $q.defer();
  var functions = [];
  fileNames.map(function (fileName) {
    functions.push(createJsonObject(targetDirPath, fileName));
  });
  deferred.resolve(functions);
  return deferred.promise;
};
```

### 本体

前提：ファイル名を保存時に ``` new Date().getTime() ``` で作ってる。
ゴール：ファイル名の降順に並び替えしてデータを取得

```
var getFileAsJsonList = function () {
  var jsonList = [];
  var targetDirPath = 'YOUR_TARGET/DIRECTORY/HERE';

  var deferred = $q.defer();
  // まずディレクトリにあるファイル名を元に、JSONオブジェクトを取ってくる関数を作る
  FileUtilService.listDir(targetDirPath).then(function (fileNames) {
    createCallbackFunctionList(targetDirPath, fileNames).then(function (fns) {
      deferred.resolve(fns);
    })
  });

  // ファイルコンテンツ取得時に非同期処理のため、ファイルの順序がバラバラになる
  // 全ファイルコンテンツの取得後に、ファイル名（日時）で並べ替え。
  deferred.promise.then(function (promises) {
    // 全部実行したい関数の配列を取得 then の後にくる関数は、
    // promisesが全て実行し終わったら一番最後に実行される
    $q.all(promises).then(function (photos) {
      jsonList = photos.sort(function (a, b) {
        return parseInt(b.name) - parseInt(a.name);
      });
      alert('got' + jsonList.length);
      angular.forEach(jsonList, function (value, key) {
        console.log('FileInfo : ' + value.name + ', content length=' + value.data.length);
      })
    });
  });
};
```

## 参考資料

- [AngularJS file service for ionic (org.apache.cordova.file)](https://gist.github.com/maleeb/3bed0cf3d51831909a61#file-fileservice-js)
- [ファイルプラグイン - Monacaドキュメント](http://docs.monaca.mobi/ja/reference/phonegap_34/ja/file/)
- [ファイル - Monacaドキュメント](http://docs.monaca.mobi/2.9/ja/reference/phonegap_29/ja/file/file/)
- [PhoneGap(Cordova) FileAPI を読む - 憧れ駆動開発](http://atasatamatara.hatenablog.jp/entry/2012/11/01/185325)