Firebaseを始めよう No.3 Storageの操作

この記事の完全なソースコードは次のリポジトリのpublic/storageフォルダにあります。

GitHub - webarata3/begin_Firebase: Firebaseのテストです。

Storage

Storageは、名前の通りクラウド上のストレージです。ファイルをフォルダ階層を作り保管していくことができます。また、Firebaseの他のサービスと同様にルールを作り、ストレージにアクセスできるユーザーを制限することができます。ルールではファイルのサイズやファイルの種類も指定できます。

プログラム

今回は、Storageを使って、ファイルのアップロード、ダウンロード、削除を紹介していきます。

画面としては次のようなものになります。

最初にコードの全体を確認します。

HTMLでは、認証用にauth-google、エラーメッセージ表示用にsnack-barの2つのCustom Elementsを使っています。

<!DOCTYPE html>
<html lang="ja">
 <head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ストレージ</title>
  <link href="/storage/storage.css" rel="stylesheet">
 </head>
 <body>
  <auth-google></auth-google>
  <div id="storageArea">
   <div id="uploadForm">
    <input type="file" id="fileButton" hidden>
    <button id="selectFileButton">ファイルを選択</button>
    <div id="uploadStatus" class="hidden">
     <div id="uploadFileName"></div>
     <div id="fileSize"></div>
     <div id="progressBar">
      <div id="progress"></div>
     </div>
    </div>
   </div>
   <div id="operationForm">
    <input type="text" size="30" id="fileName">
    <button type="button" id="viewButton">画像として表示</button>
    <button type="button" id="downloadButton">ファイルとしてダウンロード</button>
    <button type="button" id="deleteButton">ファイルの削除</button><br>
    <img id="image">
    <a href="#" id="downloadLink" download hidden></a>
   </div>
  </div>
  <snack-bar></snack-bar>
  <script src="/__/firebase/5.4.2/firebase-app.js"></script>
  <script src="/__/firebase/5.4.2/firebase-auth.js"></script>
  <script src="/__/firebase/5.4.2/firebase-storage.js"></script>
  <script src="/__/firebase/init.js"></script>
  <script src="/common/auth-google.js"></script>
  <script src="/common/snack-bar.js"></script>
  <script src="/storage/storage.js"></script>
 </body>
</html>

CSSは次のとおりです。

@charset "utf-8";

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
}

#uploadForm {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}

#selectFile {
  margin-right: 5px;
}

#uploadStatus {
  display: flex;
  align-items: center;
}

#uploadStatus.hidden {
  display: none;
}

#uploadStatus > div:not(:last-child) {
  margin-right: 5px;
}

#uploadFileName, #fileSize {
  font-size: 14px;
}

#progressBar {
  background-color: #eee;
  width: 200px;
  height: 20px;
  overflow: hidden;
}

#progress {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #007bff;
  font-size: 12px;
  color: #fff;
  text-align: center;
  width: 0;
  height: 100%;
}

#image {
  width: 300px;
}

JavaScriptは次のとおりです。

'use strict';

const fileButton = document.getElementById('fileButton');
const uploadStatusEl = document.getElementById('uploadStatus');
const uploadFileNameEl = document.getElementById('uploadFileName');
const fileSizeEl = document.getElementById('fileSize');
const progressEl = document.getElementById('progress');

document.getElementById('selectFileButton').addEventListener('click', event => {
  event.preventDefault();
  fileButton.click();
});

fileButton.addEventListener('change', event => {
  const file = event.currentTarget.files[0];
  const uploadTask = firebase.storage().ref(`files/${file.name}`).put(file);

  uploadStatusEl.classList.remove('hidden');
  uploadFileNameEl.textContent = file.name;
  fileSizeEl.textContent = `(${getDisplayFileSize(file.size)})`;

  uploadTask.on('state_changed', snapshot => {
    var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    progressEl.style.width = `${progress}%`;
    progressEl.textContent = `${parseInt(progress, 10)}%`;
  }, error => {
    snackBar('ファイルのアップロードエラーです。' + error.code);
  }, () => {
    snackBar('ファイルのアップロードが完了しました。');
  });
});

function getDisplayFileSize(plainSize) {
  const SIZE_UNIT = ['B', 'KB', 'MB', 'GB', 'TB'];

  let size = parseInt(plainSize, 10);

  let i;
  for (i = 0; i < SIZE_UNIT.length; i++) {
    if (size < 1000) break;
    size = size / 1024;
  }

  if (size === Math.floor(size)) {
    size = Math.floor(size);
  } else {
    size = size.toPrecision(3);
  }

  return size + SIZE_UNIT[i];
}

const fileNameEl = document.getElementById('fileName');
const viewButton = document.getElementById('viewButton');
const imageEl = document.getElementById('image');

var storage = firebase.storage();

viewButton.addEventListener('click', evnet => {
  const fileName = fileNameEl.value;
  if (!fileName) return;

  const storageRef = storage.ref(fileName);

  storageRef.getDownloadURL().then(url => {
    imageEl.src = url;
  }).catch(error => {
    snackBar('ファイルの表示エラーです。' + error.code);
  });
});

document.getElementById('downloadButton').addEventListener('click', event => {
  const fileName = fileNameEl.value;
  if (!fileName) return;

  const storageRef = storage.ref(fileName);

  storageRef.getDownloadURL().then(url => {
    const xhr = new XMLHttpRequest();

    xhr.responseType = 'blob';
    xhr.addEventListener('load', event => {
      const blob = xhr.response;
      const link = document.getElementById("downloadLink");
      link.href = URL.createObjectURL(blob);
      const splitName = fileName.split('/');
      link.download = splitName[splitName.length - 1];
      link.click();
    });
    xhr.open('GET', url);
    xhr.send();
  }).catch(error => {
    snackBar('ファイルのダウンロードエラーです。' + error.code);
  });
});

document.getElementById('deleteButton').addEventListener('click', event => {
  const fileName = fileNameEl.value;
  if (!fileName) return;

  const storageRef = storage.ref(fileName);

  storageRef.delete().then(() => {
    snackBar(fileName + 'を削除しました。');
  }).catch(error => {
    snackBar('ファイルの削除エラーです。' + error.code);
  });
});

ファイルのアップロード

ファイルのアップロードのファイル選択のHTMLは次のとおりです。ファイルサイズやアップロードの進捗率を表示するため、少し複雑なHTMLになっています。

<div id="uploadForm">
 <input type="file" id="fileButton" hidden>
 <button id="selectFileButton">ファイルを選択</button>
 <div id="uploadStatus" class="hidden">
  <div id="uploadFileName"></div>
  <div id="fileSize"></div>
  <div id="progressBar">
   <div id="progress"></div>
  </div>
 </div>
</div>

選択されたファイルの表示を独自にするためinput type=fileのボタンは非表示にしています。そのかわりにselectFileButtonが押されたときにselectFileButtonが押されるようにしています。

ファイルのアップロードの処理部分は次のとおりです。

fileButton.addEventListener('change', event => {
  const file = event.currentTarget.files[0];
  const uploadTask = firebase.storage().ref(`files/${file.name}`).put(file);

  uploadStatusEl.classList.remove('hidden');
  uploadFileNameEl.textContent = file.name;
  fileSizeEl.textContent = `(${getDisplayFileSize(file.size)})`;

  uploadTask.on('state_changed', snapshot => {
    var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    progressEl.style.width = `${progress}%`;
    progressEl.textContent = `${parseInt(progress, 10)}%`;
  }, error => {
    snackBar('ファイルのアップロードエラーです。' + error.code);
  }, () => {
    snackBar('ファイルのアップロードが完了しました。');
  });
});

ファイル名が変わったときにファイルをアップロードします。ファイルを格納する場所はfirebase.storege().ref()で取得する参照先になります。今回の例では、filesフォルダの中にファイルを格納します。格納するファイルは、putメソッドで指定します。今回アップロードする際にアップロードの進捗状況を取得するので、putメソッドの戻り値のUploadTaskを使用します。UploadTaskでは、state_changedイベントをリッスンすることで、アップロードの進捗状況を確認できます。リッスンする関数の引数に渡されるsnapshotを使用し、そのプロパティの、bytesTransferredで送信したバイト数、totalBytesで送信するファイルのサイズが取得できます。ファイルの送信が完了すると、UploadTaskのonメソッドの3番目の引数の関数が呼び出されます。

ファイルのダウンロード

ファイルのダウンロードには2種類あり、URLだけ取得して画像として表示する方法と、ファイルそのものをダウンロードする方法があります。

まず、画像として表示する方法を見ていきます。

viewButton.addEventListener('click', evnet => {
  const fileName = fileNameEl.value;
  if (!fileName) return;

  const storageRef = storage.ref(fileName);

  storageRef.getDownloadURL().then(url => {
    imageEl.src = url;
  }).catch(error => {
    snackBar('ファイルの表示エラーです。' + error.code);
  });
});

imgタグのsrc属性に設定するだけであれば、上記のようなプログラムだけでできます。ファイルのURLはfirebase.storage.ReferencegetDownloadURLメソッドのコールバック関数の引数で取得できます。

次に、ファイルとしてダウンロードする方法を見ていきます。コードは次のようになります。

document.getElementById('downloadButton').addEventListener('click', event => {
  const fileName = fileNameEl.value;
  if (!fileName) return;

  const storageRef = storage.ref(fileName);

  storageRef.getDownloadURL().then(url => {
    const xhr = new XMLHttpRequest();

    xhr.responseType = 'blob';
    xhr.addEventListener('load', event => {
      const blob = xhr.response;
      const link = document.getElementById("downloadLink");
      link.href = URL.createObjectURL(blob);
      const splitName = fileName.split('/');
      link.download = splitName[splitName.length - 1];
      link.click();
    });
    xhr.open('GET', url);
    xhr.send();
  }).catch(error => {
    snackBar('ファイルのダウンロードエラーです。' + error.code);
  });
});

ただし、このコードを書いてファイルをダウンロードしようとして次のようなエラーになります。

No 'Access-Control-Allow-Origin' header is present on the requested resource

これを解決するためには、サーバ側でどこからのアクセスを許可するかという設定をしなければいけません。その設定のためにまずは、gsutilというツールを使う必要があります。gsutilは次のサイトを参考にインストールしてください。

gsutil ツール  |  Cloud Storage ドキュメント  |  Google Cloud

gsutilをインストール後に、次のようなファイルを作成します。ファイル名は「cors.json」とします。

[
  {
    "origin": ["http://localhost:5000"],
    "method": ["GET"],
    "maxAgeSeconds": 3600
  }
]

その後、gsutilを使い次のコマンドを入力します。

gsutil cors set cors.json gs://sandbox-2915c.appspot.com

これでファイルのダウンロードができるようになります。

ファイルの削除

ファイルの削除は次のようになります。

document.getElementById('deleteButton').addEventListener('click', event => {
  const fileName = fileNameEl.value;
  if (!fileName) return;

  const storageRef = storage.ref(fileName);

  storageRef.delete().then(() => {
    snackBar(fileName + 'を削除しました。');
  }).catch(error => {
    snackBar('ファイルの削除エラーです。' + error.code);
  });
});

削除は特別難しいことはなく、参照からdeleteメソッドを呼ぶだけです。

コメント