Cordova(Android)でファイルを扱うクラスを作成した

Cordovaでファイルを扱うためのクラスを作成しました。

ファイルは、私の場合、以下のプラグインが関連しました。

・cordova-plugin-camera
・cordova-plugin-file

# File System Access API

ファイルアクセスのための仕様が少し特殊で、File System Access APIというAPI仕様のようのようです。
ちょっとわかりにくいなあと思ったら、以下の方がまとめてくださっていました。（早く見つけたかった。。。）

https://developer.chrome.com/docs/capabilities/web-apis/file-system-access?hl=ja
https://doc.applican.com/FileSystem/index.html
https://github.com/apache/cordova-plugin-file

# ソースコード

```js:lib_file.js
class LibFile{
  constructor(){
  }

  error_message(code){
    switch(code){
        case 1: return "NOT_FOUND_ERR";
        case 2: return "SECURITY_ERR";
        case 3: return "ABORT_ERR";
        case 4: return "NOT_READABLE_ERR";
        case 5: return "ENCODING_ERR";
        case 6: return "NO_MODIFICATION_ALLOWED_ERR";
        case 7: return "INVALID_STATE_ERR";
        case 8: return "SYNTAX_ERR";
        case 9: return "INVALID_MODIFICATION_ERR";
        case 10: return "QUOTA_EXCEEDED_ERR";
        case 11: return "TYPE_MISMATCH_ERR";
        case 12: return "PATH_EXISTS_ERR";
        default: "Unknown error";
    }
  }

  static async newInstance(target){
    var lib = new LibFile();
    await lib.initialize(target);
    return lib;
  }

  async initialize(target = "data"){
    this.target = target;
    if( target == "root" )
      this.target_dir = cordova.file.externalRootDirectory;
    else if( target == "storage" )
      this.target_dir = cordova.file.applicationStorageDirectory;
    else if( target == "data" )
      this.target_dir = cordova.file.externalDataDirectory;
    else
      throw new Error("unknown target");
      
    return new Promise((resolve, reject) =>{
      window.resolveLocalFileSystemURL(this.target_dir, (dirEntry) => {
        this.dir = dirEntry;
        resolve(this);
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async createFile(dirName, fileName, data){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: true}, (dirEntry) =>{
        dirEntry.getFile(fileName, {create: true, exclusive: false}, (fileEntry) =>{
          fileEntry.createWriter((fileWriter) =>{
            fileWriter.write(data);
            fileWriter.onwriteend = () =>{
              resolve(fileEntry);
            };
            fileWriter.onerror = (error) =>{
              reject(this.error_message(error.code));
            };
          }, (error) =>{
            reject(this.error_message(error.code));
          });
        }, (error) =>{
          reject(this.error_message(error.code));
        });
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async createDirectory(dirName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: true, exclusive: true}, (dirEntry) =>{
        resolve(dirEntry);
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async removeDirectory(dirName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        dirEntry.removeRecursively(() =>{
          resolve();
        }, (error) =>{
          reject(error);
        });
      }, (error) =>{
        reject(error);
      });
      //   var dirReader = dirEntry.createReader();
      //   dirReader.readEntries((fileEntries) =>{
      //     for (let fileEntry of fileEntries) {
      //       fileEntry.remove();
      //     }
      //     dirEntry.remove(() =>{
      //       resolve();
      //     }, (error) =>{
      //       reject(this.error_message(error.code));
      //     });
      //   }, error =>{
      //     reject(this.error_message(error.code));
      //   });
      // }, (error) =>{
      //   reject(this.error_message(error.code));
      // });
    });
  }

  async getFileEntry(dirName, fileName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        dirEntry.getFile(fileName, {create: false, reclusive: false}, (fileEntry) =>{
          resolve(fileEntry);
        }, (error) =>{
          resolve(null);
        });
      }, (error) =>{
        resolve(null);
      });
    });
  }

  async isFile(dirName, fileName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        var dirReader = dirEntry.createReader();
        dirReader.readEntries((fileEntries) =>{
          var target = fileEntries.find(item => item.name == fileName && !item.isDirectory);
          resolve(target);
        }, error =>{
          resolve(null);
        });
      }, (error) =>{
        resolve(null);
      });
    });
  }

  async isDirectory(dirName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        resolve(dirEntry);
      }, error =>{
        resolve(null);
      });
    });
  }

  async listFiles(dirName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        var dirReader = dirEntry.createReader();
        dirReader.readEntries((fileEntries) =>{
          resolve(fileEntries);
        }, error =>{
          reject(this.error_message(error.code));
        });
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }
  
  async removeFile(dirName, fileName){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        dirEntry.getFile(fileName, {create: false, reclusive: false}, (fileEntry) =>{
          fileEntry.remove(() =>{
            resolve();
          }, (error) =>{
            reject(this.error_message(error.code));
          });
        }, (error) =>{
          reject(this.error_message(error.code));
        });
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async writeFile(dirName, fileName, data){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        dirEntry.getFile(fileName, {create: false, reclusive: false}, (fileEntry) =>{
          fileEntry.createWriter((fileWriter) =>{
            fileWriter.write(data);
            fileWriter.onwriteend = () =>{
              resolve();
            };
            fileWrite.onerror = (error) =>{
              reject(error);
            };
          }, (error) =>{
            reject(this.error_message(error.code));
          });
        }, (error) =>{
          reject(this.error_message(error.code));
        });
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async readFile(dirName, fileName, response_type = "text"){
    return new Promise((resolve, reject) =>{
      this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
        dirEntry.getFile(fileName, {create: false, reclusive: false}, (fileEntry) =>{
          fileEntry.file((file) => {
            var reader = new FileReader();
            reader.onloadend = () => {
              resolve(reader.result);
            };
            reader.onerror = () =>{
              reject(error);
            }
            if( response_type == "binary" )
              reader.readAsArrayBuffer(file);
            else if( response_type == "text" )
              reader.readAsText(file);
            else if( response_type == "dataurl" )
              reader.readAsDataURL(file);
            else
              reject(new Error("unknown response_type"));
          }, (error) =>{
            reject(this.error_message(error.code));
          });
        }, (error) =>{
          reject(this.error_message(error.code));
        });
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async copyFile(nativeUrl, dirName, fileName){
    return await new Promise((resolve, reject) =>{
        window.resolveLocalFileSystemURL(nativeUrl, (sourceEntry) => {
            this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
                sourceEntry.copyTo(dirEntry, fileName, (newFileEntry) => {
                  resolve(newFileEntry);
                }, (error) => {
                  reject(this.error_message(error.code));
                });
            }, (error) => {
              reject(this.error_message(error.code));
            });
        }, (error) => {
            reject(this.error_message(error.code));
        });
    });
  }

  async moveFile(nativeUrl, dirName, fileName){
    return await new Promise((resolve, reject) =>{
        window.resolveLocalFileSystemURL(nativeUrl, (sourceEntry) => {
          this.dir.getDirectory(dirName, {create: false}, (dirEntry) =>{
            sourceEntry.moveTo(dirEntry, fileName, (newFileEntry) => {
                resolve(newFileEntry);
              }, (error) => {
                reject(this.error_message(error.code));
              });
          }, (error) => {
            reject(this.error_message(error.code));
          });
        }, (error) =>{
            reject(this.error_message(error.code));
        });
    });
  }

  async readFromNativeUrl(nativeURL, response_type = "text"){
    return new Promise((resolve, reject) =>{
      resolveLocalFileSystemURL(nativeURL, (fileEntry) =>{
        fileEntry.file((file) => {
          var reader = new FileReader();
          reader.onloadend = () => {
            resolve(reader.result);
          };
          reader.onerror = (error) => {
            reject(this.error_message(error.code));
          }
          if( response_type == "binary" )
            reader.readAsArrayBuffer(file);
          else if( response_type == "text" )
            reader.readAsText(file);
          else if( response_type == "dataurl" )
            reader.readAsDataURL(file);
          else
            reject(new Error("unknown response_type"));
        }, (error) =>{
          reject(this.error_message(error.code));
        });
      }, (error) =>{
        reject(this.error_message(error.code));
      });
    });
  }

  async getFileEntry(nativeURL){
    return new Promise((resolve, reject) =>{
      resolveLocalFileSystemURL(nativeURL, (fileEntry) =>{
        resolve(fileEntry);
      }, (error) =>{
        reject(error);
      })
    });
  }

  toUrl(dirName, fileName){
    if( this.target == "root" )
      return "https://localhost/__cdvfile_sdcard__/" + dirName + "/" + fileName;
    else if( this.target == "storage" )
      return "https://localhost/__cdvfile_files__/" + dirName + "/" + fileName;
    else if( this.target == "data" )
      return "https://localhost/__cdvfile_files-external__/" + dirName + "/" + fileName;
    else
      throw new Error("unknown target");
  }
}
```

# Cordovaでの注意点

1点だけ、注意があります。

本来であれば、FileEntryで示されるファイルをHTMLでimgタグに使う場合は以下のようにしていたかと思います。

```
var image_url = fileEntry.toURL()
<img v-bind:src="image_url">
```

この場合、Webコンテンツ（HTMLやJavascript）をサーバに配置せずアプリケーションにアセットとして埋め込んでいる場合は問題ありません。サーバに配置して読み込んでいる場合、これではうまく動作しません。
なぜならば、fileEntry.toURL()は以下のように展開されるためです。

image_url="https://[ホスティングURL]/__cdvfile_sdcard__/test.png

ホスティングURLは、Webコンテンツを配置したホスト名です。アセットとして埋め込んでいる場合はlocalhostとなるため問題ないのですが、サーバに配置すると、ローカルにあるファイルをリモートのホスティングサーバからとってこようとします。

そこで、以下の関数を用意しました。これにより、ローカルにあるファイルをローカルからとってきます。

```js:lib_file.js
  toUrl(dirName, fileName){
    if( this.target == "root" )
      return "https://localhost/__cdvfile_sdcard__/" + dirName + "/" + fileName;
    else if( this.target == "storage" )
      return "https://localhost/__cdvfile_files__/" + dirName + "/" + fileName;
    else if( this.target == "data" )
      return "https://localhost/__cdvfile_files-external__/" + dirName + "/" + fileName;
    else
      throw new Error("unknown target");
  }
```

以下のように使います。

```
var image_url = libfile.toURL("ディレクトリ名", "ファイル名");
<img v-bind:src="image_url">
```

# Permission

以下の記載がありました。
・適切な READ_EXTERNAL または READ_MEDIA_* 権限による読み取り専用アクセス。
・読み取り専用アクセスはメディア ファイルに限定されますが、ドキュメントには適用されません。

あと、以下も

```xml:config.xml
<config-file target="AndroidManifest.xml" parent="/*" xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
</config-file>
```

# 参考
以下も参考にしてください。

[Cordovaアプリ開発の備忘録](https://qiita.com/poruruba/items/f03b1691fd0f113735b3)
[Cordovaアプリ開発の備忘録(プラグイン編)](https://qiita.com/poruruba/items/a454f1b90c5d8b0cb2f8)

以上