Chapter 14

Storage:ファイルの移動

masalib
masalib
2020.12.26に更新

私が勤めている会社のシステムだと

編集 → 確認 → 更新

のような編集画面の構成になっている事が多いです。

そして2つの方式があります

  • 画像URL切り替え方式
  • 画像ID固定でテンプファイル画像で切り替え方式

画像URL切り替え方式

  1. 編集画面でimageをアップして画像URLを発行する
  2. 確認画面で1で発行したURLをもとに確認する
  3. 更新処理で旧画像URLから1で発行したURLに切り替える。旧画像URLを削除する。

画像ID固定方式

  1. 編集画面でimageをテンプフォルダにアップする
  2. 確認画面で1でアップしたテンプ画像を表示する
  3. 更新処理で1でアップした画像を上書きコピーする。1でアップした画像を消す

FirebaseStorageではファイル移動という関数はありません。たぶん開発陣は画像URL切り替え方式を推奨していると思います。私みたいな古い人間はついつい画像ID固定方式で作ってしまいます。

ファイル移動がないなら作る。

概要

  1. 移動元のファイル(テンプファイル)をダウンロードする。
  2. ダウンロードしたファイルをもとにファイルをアップロードする。
  3. 移動元のファイルを削除する

概要だけみると簡単に見えるのですが、1が手間がかかります。

事前作業

gsutil のインストール

javascriptでデータをダウンロードするには対象のサーバー(Firebase)にCORSの設定が必要です。
設定をかえるのはGUIでは提供されておらず、CUIのgsutilが必要です。

https://cloud.google.com/storage/docs/gsutil_install?hl=ja

私の場合はWindowsなのでファイルダウンロードしてインストールするだけでした。
Macは3年前にインストールしましたが問題なくインストールできました(エビデンスがないです)

Windows場合はインストールが終了ページで

  • Start Google Cloud SDK Shell
  • Run 'gcloud init'

チェックして終了するとターミナル ウィンドウが開いて gcloud init コマンドが実行されます。使用しているユーザーで認証する

gsutil を使ってCORSの設定ファイルをアップする

下記ような設定ファイル(JSON)を作成する

[
    {
      "origin": ["*"],
      "method": ["GET"],
      "maxAgeSeconds": 3600
    }
  ]

作成後に設定ファイルをアップする

gsutil cors set cors.json gs://<your-cloud-storage-bucket>

gsutil cors set cors.json gs://learn-firebase-masalib.appspot.com  
// => Setting CORS on gs://learn-firebase-masalib.appspot.com/...  

確認は以下のコマンド

gsutil cors get gs://<your-cloud-storage-bucket>

gsutil cors get gs://learn-firebase-masalib.appspot.com  
// => [{"maxAgeSeconds": 3600, "method": ["GET"], "origin": ["*"]}]    

プログラム

//更新ボタンが押された時を想定
const sourcePath   = "images/users/"+ currentUser.uid + "/tempprofilePicture.png" 
const destinationPath  = "images/users/"+ currentUser.uid + "/filePicture.png" 
let statusData = moveFirebaseFile(sourcePath, destinationPath)
console.log(statusData)
//・
//・
//・
//・
function moveFirebaseFile(currentPath, destinationPath) {
    let oldRef = storage.ref().child(currentPath)

    let metadata = {
        contentType: 'image/jpeg',
    };
	
    oldRef.getDownloadURL().then(url => {
        fetch(url).then(htmlReturn => {
            let fileArray = new Uint8Array()
            const reader = htmlReturn.body.getReader()

            //get the reader that reads the readable stream of data
            reader
                .read()
                .then(function appendStreamChunk({ done, value }) {
                    //If the reader doesn't return "done = true" append the chunk that was returned to us
                    // rinse and repeat until it is done.
                    if (value) {
                        fileArray = mergeTypedArrays(fileArray, value)
                    }
                    if (done) {
                        //console.log(fileArray)
                        return fileArray
                    } else {
                        // "Readout not complete, reading next chunk"
                        return reader.read().then(appendStreamChunk)
                    }
                })
                .then(file => {
                    //Write the file to the new storage place
                    let status = storage
                        .ref()
                        .child(destinationPath)
                        .put(file,metadata)
                    //Remove the old reference
                    oldRef.delete()

                    return status
                })
        })
    })
}

function mergeTypedArrays(a, b) {
    // Checks for truthy values on both arrays
    //if(!a && !b) throw 'Please specify valid arguments for parameters a and b.';  
        // Checks for truthy values or empty arrays on each argument
    // to avoid the unnecessary construction of a new array and
    // the type comparison
    if(!b || b.length === 0) return a;
    if(!a || a.length === 0) return b;

    // Make sure that both typed arrays are of the same type
    if(Object.prototype.toString.call(a) !== Object.prototype.toString.call(b)){
        console.log(Error,'The types of the two arguments passed for parameters a and b do not match.')
    }

    var c = new a.constructor(a.length + b.length);
    c.set(a);
    c.set(b, a.length);

    return c;
}

解説

oldRef.getDownloadURL().thenは具体的なURLのパスを取得しています

source:images/users/9f5oVBzuPuZrKibnLgWPM61UYcL2/tempprofilePicture.pngの場合は
 ↓

https://firebasestorage.googleapis.com/v0/b/learn-firebase-masalib.appspot.com/o/images%2Fusers%2F9f5oVBzuPuZrKibnLgWPM61UYcL2%2FtempprofilePicture.png?alt=media&token=fe155cf4-5da4-4e56-861f-72dbf3d1eed5

になります

let fileArray = new Uint8Array()はバイナリーデータを扱うために
準備しています。

Uint8Arrayとは

Uint8Array は型付き配列であり、 8 ビット符号なし整数値の配列を表します。中身は 0 で初期化されます。生成されると、配列の中の要素はオブジェクトのメソッドを使用するか、配列の標準的な添字の構文を使用するか (すなわち、ブラケット構文を使用するか) して参照することができます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array#:~:text=Uint8Array は型付き配列,することができます。

元ファイルを読み込んで型付き配列に突っ込んでいます。

if (value) {
    fileArray = mergeTypedArrays(fileArray, value)
}
if (done) {
    //console.log(fileArray)
    return fileArray
} 

mergeTypedArraysは結合しています。

結論

このように画像ID固定方式だと、難しい移動処理を作らないといけません。関数化すれば手間は減るかと思いますが画像URL切り替え方式で設計した方がいいです。今回はソースはアップしません。

参考URL

https://stackoverflow.com/questions/38601548/how-to-move-files-with-firebase-storage

https://www.it-swarm-ja.tech/ja/javascript/javascriptでtypedarrayをマージするにはどうすればよいですか?/1069708231/