---
# System prepended metadata

title: Firebase Storage Note

---

# Firebase Storage Note
## Firebase Storage 大小限制

免費方案的空間大小限制是 5GB
每日最多下載流量 1GB
一天下載次數最多5萬次 上傳次數最多2萬次
就一般的開發者寫寫 side project 是非常夠用的
![](https://i.imgur.com/izLxKzS.png)
## 新建專案
首先到 [Firebase](https://firebase.google.com/) 官網新增專案
![](https://i.imgur.com/R7NRHtj.png)
專案命名 按自己喜好取名
![](https://i.imgur.com/s8H3yYe.png)
這邊會問要不要啟用 GA 功能，因為不是今天重點，所以不啟用
同學們不用擔心若是之後有需要仍可以到設定內開啟 GA 相關功能

開始建立專案 會花一點時間
![](https://i.imgur.com/GfxqMkq.png)
![](https://i.imgur.com/vTeqnPR.png)

點選繼續可以進入控制台介面
左側會有許多功能可以設定，一般開發者基本上只會用到 **開發** 功能
剛剛沒開啟的 GA 功能若是之後有需求可以到 **數據分析** 這邊開啟

簡單介紹一下控制台開發功能列表
![](https://i.imgur.com/DkMiur0.png)

* Authentication
身份驗證：顧名思義就是一些權限驗證的設定
* Database
資料庫：字面上翻過來就是資料庫 可以在這裡檢查設定我們寫進來的東西
目前區分成 Cloud Firestore 和 Realtime Database 兩種，差異可見：
[GCP專門家: Firebase Cloud Firestore 及 Realtime Database 介紹及比較](https://blog.gcp.expert/compare-firebase-cloud-firestore-with-realtime-database/)

> 附帶一提六角學院 Node.js 課程有 Firebase Database 的 Realtime Database 詳細教學，有興趣同學們可以回去複習一下

* Storage
儲存：字面上翻過來就是儲存、存儲 可以上傳一些圖片與影片 檔案都東西
可以想成類似 google 那種雲端的感覺
* Hosting
託管功能：是 Firebase 的網站部署功能
可以讓自己的 Firebase 作品部署託管在上面，就不用再用 github Page 或 Heroku 部署自己的網站
* Functions
後端功能 整合成一個一個自己的  Fumctions 就是 JavaScript Methods
開發者可以透過操作這些 Fumction Methods 開發和管理我們的後端功能
* ML Kit
這是和機器學習有關的服務 不在我們這次的主題內呦

## 開始使用 Firebase Storage
點選控制台左側列表 Storage
![](https://i.imgur.com/RYgMk1D.png)
點選開始使用

### Step1
![](https://i.imgur.com/oiT7gXP.png)
有用過 database 功能的人應該對這個挺熟悉的，同樣是安全權限的部分，這個步驟只提示安全權限，當前步驟無法修改，我們點下一步繼續

### Step2
![](https://i.imgur.com/AsaTe1v.png)
接著要設定 Cloud Storage 的(機房)位置，一但選了就不能變更，有分成多區域與單區域，多區域如美國和歐洲就代表實際選擇後，這地區有好幾個機房位置可以使用，(代表歐美有不只一個機房)

基本上選哪裡都還是可以用 Firebase Storage 的服務，這裡當然選離我們比較近的地區，點選 **了解詳情** 可以看近一步說明
![](https://i.imgur.com/oBVov3g.png)
亞洲有四個選項分別是代表
* 孟買
* 香港
* 東京
* 大阪

這裡我選擇香港 asia-east2
![](https://i.imgur.com/KBJu0wV.png)
稍等幾秒就建立好了

建立好進來 storage 頁面，我們可以看三個分頁選項
![](https://i.imgur.com/Xx6lauG.png)
* 檔案
可以在這裡觀看上傳上來的檔案與資料夾結構，可以想成條列式的 google 雲端那種感覺
* 規則
就是剛剛說的安全權限設定規則
* 用量
可以觀看目前使用到的空間大小與流量

## 新增應用程式
從 Project Overview 處點選 `</>` 新增網頁應用程式
![](https://i.imgur.com/PvPCmQ9.png)


或者是點選 Project Overview 旁邊的小齒輪 選專案設定
![](https://i.imgur.com/z3dnn2s.png)

![](https://i.imgur.com/tXJW1Jc.png)

點選 `</>` 新增網頁應用程式

進入網頁應用程式畫面
### Step1
![](https://i.imgur.com/E6P5Tug.png)


輸入我們的應用程式名稱，這裡也是按自己喜好取
應用程式名稱和專案名稱是不一樣的東西，一個專案底下可以有數個應用程式

這裡也可以選擇一併為這個應用程式設定代管功能
這裡先不選擇，代管功能之後還可以設定
### Step2
![](https://i.imgur.com/WDDp32J.png)
這邊有兩個重點
1. 檔案引入的方式：
要使用 Firebase 功能，他的新版 SDK，預設引入的 firebase-app.js 是 Firebase 的主要核心，其他的功能都被拆分成各個子項目，我們要在 script 引入 Firebase 對應的 Storage Library 檔案才可以使用
2. firebaseConfig
firebaseConfig 包含了我們專案應用程式的金鑰，透過這個金鑰，你的網頁服務才能找到對應的 Firebase 對應的 database 或 storage 應用，所以每個人金鑰參數都會不一樣呦

## 引入檔案
接著開始開發吧，使用 codepen 或在自己的電腦內新增一個 `index.html`，並 CDN 引入核心檔案與 Storage 檔案

### CDN 引入
```
<!-- Firebase 核心 -->
<script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-app.js"></script>

<!-- Firebase storage 模組檔案 -->
<script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-storage.js"></script>
```
若是在自己電腦上 html 開發，將上面兩個檔案放在`</body>`前面

### Vue Cli 或 Create-React-App
如果是使用 Vue cli 或 Create-React-App，需要透過 npm 或 yarn 來安裝

使用終端機 cd 到專案資料夾後下指令
```
npm install --save firebase
```
到我們的專案 `app.js` 或 `app.vue` 開頭加入
```
import * as firebase from "firebase/app";
import "firebase/storage";
```

## 初始化設定
### 貼入金鑰
> 這裡的金鑰設定檔晚點我會砍掉，你們替換成自己的 firebaseConfig 版本就好了

新開一個 `.js` 檔案引入你的 HTML，並在裡面貼入我們的金鑰
```
  var firebaseConfig = {
    apiKey: "AIzaSyBDLr5ffr1l85D6rF3hsBRjALgumfZm8ZQ",
    authDomain: "test-storage-1150d.firebaseapp.com",
    databaseURL: "https://test-storage-1150d.firebaseio.com",
    projectId: "test-storage-1150d",
    storageBucket: "test-storage-1150d.appspot.com",
    messagingSenderId: "661329561283",
    appId: "1:661329561283:web:2a5a297384c6542a"
  };
  
```
如果是使用 Vue cli 或 Create-React-App 就貼進你的 `app.js` 或 `app.vue`
想在專門檔案管理也可以將金鑰抽出來成一隻檔案，再 `import` 進 `app.js` 或 `app.vue` 也可以

### 初始化 Firebase
在你的 `.js` 檔案或  `app.js` 或 `app.vue` 加入這行
```
 firebase.initializeApp(firebaseConfig);
```
這樣就完成初始化
也可以指派給一個變數並 console.log 印出來檢查看看
```
const db = firebase.initializeApp(firebaseConfig);
console.log(db)
```
## 檔案上傳
接著終於可以開始時做檔案上傳功能了，我們先從**單檔上傳**開始，為了方便解釋我會用 codepen 來說明：

### 單檔上傳
HTML
```
<progress value="0" max="100" id="uploader">0%</progress></br>
<input type="file" value="upload" id="uploadBtn"></br>
<div id="msg"></div>
```
JS
先透過 getElementById 取得對應的元素
```
const uploader = document.getElementById("uploader");
const uploadBtn = document.getElementById("uploadBtn");
const msg = document.getElementById("msg");
```
我們要做的是：當我們選擇檔案後，就將其上傳至雲端
```
uploadBtn.addEventListener("change", event => {
  
  // 取得檔案資訊
  const file = event.target.files[0];
  const path = file.name;
  
  // 取得 storage 對應的位置
  const storageReference = firebase.storage().ref(path);

  // .put() 方法把東西丟到該位置裡
  const task = storageReference.put(file);
});
```
實際操作會發現...沒反應
打開 console 檢查

![](https://i.imgur.com/X2PTLmc.png)
403沒有權限，原因是因為我們規則設定，權限的部分現在是只有認證(例如登入後)的權限才可以上傳，我們到規則這邊
![](https://i.imgur.com/udvQV0Q.png)
把不等於
```
allow read, write: if request.auth != null;
```
改成
```
allow read, write: if true;
```
然後發布
![](https://i.imgur.com/DKN1sL8.png)
這裡方便說明我就先改成 true，但這麼做的缺點就是門戶大開，只要有你的金鑰設定檔，大家都可以上傳東西上來，實際開發時一定會會和登入功能去做權限控管

回到 codpen 重新上傳，會發現這次就成功了

在我們上傳檔案的程式碼內，我們利用
```
firebase.storage()
```
來取得 Firebase storage 對應的 API 和功能，利用 `.ref()` 來指向 storage 雲端裡對應的位置

若是有寫過 Firebase database 的人，應該會知道`.ref()` 不傳入路徑參數，會指向 root 根目錄，但是，我們現在上傳檔案 **這個對應位置字串需要包含檔案名稱**
```
firebase.storage().ref(相對路徑包含檔名);
```
什麼意思呢？
來看看如果我們這樣做，會發生什麼事情：

```
.ref("folder")
```
你以為結果是沒上傳成功或者新建一個資料夾名叫 folder 嗎？
實際上是上傳到 root 根目錄的檔案名稱變成 folder 了
因為我們真正推東西上去的 API 是 `.put()`
而`.ref()`若沒有 `"/"` 來區分資料夾層級，那麼字串參數就會變成我們檔案上傳進去的檔案名稱

但是如果我們改成這樣
```
.ref("folder/newName")
```
來看看結果
這邊自動新增了資料夾 folder，我們剛剛上傳的檔案名稱變成 newName

實際上 Firebase storage 沒有新增資料夾的 API，所謂的資料夾實際上是我們的路徑，storage 會替我們在上傳時自動產生：
* 如果上傳時路徑的資料夾層級已經存在 storage，`.ref()`就按照這個路徑指過去
* 如果上傳時路徑資料夾層級不存在，`.ref()`就按照這個路徑新建資料夾並指過去

而另一個重點 API 當然是
```
.put(檔案)
```
我們透過 `.put()` 來上傳檔案
要注意的是必須傳入參數，這個參數就是我們要上傳的檔案，也就是從`event.target.file[0]` 取到的東西

[codepen 範例1](https://codepen.io/SkyrimTomato/pen/QWLgxNr?editors=1011)

了解上述資料後，可以小小優化一下，讓讀取條可以跟著跑動
```
uploadBtn.addEventListener("change", event => {
  msg.textContent = "";
  
  // 取得檔案資訊
  const file = event.target.files[0];
  const path = file.name;
  
  // 取得 storage 對應的位置
  const storageReference = firebase.storage().ref(path)

  // .put() 方法把東西丟到該位置裡
  const task = storageReference.put(file);

  // .on()監聽並連動 progress 讀取條
  task.on(
    "state_changed",
    function progress(snapshot) {
      let uploadValue = snapshot.bytesTransferred / snapshot.totalBytes * 100;
      uploader.value = uploadValue;
    },
    function error(err) {
      msg.textContent = "上傳失敗";
    },
    function complete() {
      msg.textContent = "上傳成功";
    }
  );
});

```
當然大家都知道，實際上檔案在傳輸並不是同步的，而是**非同步**傳輸，在傳輸的過程一定需要時間，所以我們需要"監聽"上傳的過程，如何監聽呢？這裡可以使用 `.on()`
```
.on()
```
我們把`.put()`上傳這段程式碼指派給一個變數 task

```
const task = storageReference.put(file);
```
用點`.on()`接續在 task 變數後面監聽，`.on()`可以帶入以下參數：

```
.on("state_changed", callback1, callback2, callback3）
```
* `"state_changed"`
字串格式 表示監聽`.put()` 上傳狀態的變動

* callback1
上傳**中**觸發的 callback function 函式，上傳過程會不停觸發

* callback2
上傳**失敗**觸發的 callback function 函式，只在失敗後觸發一次

* callback3
上傳**結束**觸發的 callback function 函式，只在完成後觸發一次

callback1 這邊會帶入一個當前資料快照 snapshot 作為參數，我們從 snapshot 取出 bytesTransferred 和 totalBytes 兩個數值：
* bytesTransferred 表示當前已上傳的檔案大小
* totalBytes 表示預期要上傳的檔案大小

然後將
```
bytesTransferred/totalBytes * 100
```
計算出來的值指派給 `<progress>` 的 `value` 屬性
這邊是簡單的數學計算:
在看`<progress>` 這個 HTML5 讀取條時，可以用％數來想，100% 就是完成，假如我們今天有個 `10MB` 檔案在上傳，並且網路速度固定 `0.1秒` 上傳 `1MB`
* 0.1 秒時
```
1/10 * 100
```
已上傳`1MB` progress 讀取條這時就是 10(%)

* 0.2 秒時
```
2/10 * 100
```
已上傳`2MB` progress 讀取條這時就是 20(%)

* 到 0.5 秒時
```
5/10 * 100
```
已上傳`5MB` progress 讀取條這時就是 50(%)

直到最後..
* 1秒時
```
10/10 * 100
```
已上傳`10MB` progress 讀取條這時就是 100(%)

[codepen 範例2](https://codepen.io/SkyrimTomato/pen/QWLgxNr?editors=1011)

### 拖曳上傳
接著來說拖曳上傳，拖曳上傳會需要用到 HTML5 新增的 Event 事件 `drop` 和 `dragover`
這裡一樣以 codepen 為範例

HTML
```
<div id="dropContainer" class="drop-container">
    <span>拖曳檔案至此上傳</span>
</div>

<div id="msg"></div>
```

CSS
```
.drop-container {
  width: 600px;
  height: 400px;
  border: 4px dashed #000;
  line-height: 400px;
  text-align: center;
}
```
JS
一樣要引入 Firebase Library 和金鑰設定
```
const dropContainer = document.getElementById("dropContainer");
const msg = document.getElementById("msg");

// 將這個範例存進 Storage drag-and-drop/ 資料夾
const folder = "drag-and-drop/"
```
接著對我們的 dropContainer 拖曳區域下 drop 和 dragover 事件監聽 
```
// dragover 拖曳檔案至範圍
dropContainer.addEventListener("dragover", event => {
  event.preventDefault();
});

// drop 拖曳時放開檔案
dropContainer.addEventListener("drop", event => {
  event.preventDefault();
});
```
* dragover 監聽事件是滑鼠拖曳東西進入被監聽範圍內就會觸發
* drop 監聽事件是滑鼠在該監聽範圍內左鍵放開時就會觸發

那..為什麼 dragover 和 drop 都要加 `event.preventDefault();` 阻止預設行為呢？
因為 dragover 會瘋狂被觸發嗎？這倒不是最主要考量

> 正常使用瀏覽器網站，隨便將一個圖片拖曳進瀏覽器會發生什麼事情?
> 瀏覽器會離開當前網站並開啟這張圖片

為了讓瀏覽器不去做預設開啟這張圖片的行為，我們在拖曳和放開的這兩個過程，必須加上`.preventDefault();` 去阻止它的預設行為

理解這點後，我們繼續修改程式碼
在 drop 的 callback 函式內加上
```
  const file = event.dataTransfer.files[0];
```
注意現在是**拖曳事件**，要從 callback 參數取得檔案資訊，要用 `.dataTransfer` 而不是 `.target`

接下來如同單檔上傳那邊的範例一樣，透過 `.ref()` 指向位置，透過 `.put()` 將 file 檔案上傳
```
  const name = file.name
  const fullPath = `${folder}${name}`;
  const storageReference = firebase.storage().ref(fullPath);
  const task = storageReference.put(file);
```
[codepen 單檔拖曳上傳範例](https://codepen.io/SkyrimTomato/pen/abowjVw?editors=0011)

### 多檔拖曳上傳
多檔拖曳其實很簡單

應該有人注意到我用事件 event 參數取 file 檔案時是用 `files[0]` 去取
實際上 `event.dataTransfer.files` 是一個 `Array-like` 物件，他是個類陣列的物件，我們可以用 `[0]` 取到這個 files 物件的第一筆檔案資料

> 當然，若你對 JavaScript 夠熟悉，應該知道 Array 就是 Object 

我們可以下 console.log() 來檢查 files
![](https://i.imgur.com/bqzQUES.png)
可以發現 files 是個物件，其 key 像陣列一樣是由 ０ 開始數，並且有個隱藏屬性 `length` 列出有多少數量的 *File* 檔案

我們沒辦法用 Array 原生方法例如 `forEach` 來迭代 *File* 物件
用 `for-in` 還會把 `length` 給印出來多此一舉，雖然可以用 `Object.key` 或 `Array.from` 來轉換成 Array，但既然 `length` 這個隱藏屬性表示實際上 *File* 數量..

那我們就可以用一般的 for 迴圈來迭代就可以了

```
const files = event.dataTransfer.files;

for(let i = 0; i < files.length; i++) {
    const path = folder + files[i].name;
    const storageReference = firebase.storage().ref(path);
    const task = storageReference.put(files[i]);
}
```
這個一來就可以在 for 迴圈迭代的過程中，個別將上傳的檔案透過 `.ref()` 和 `.put()` 上傳至雲端

[codepen 多檔拖曳上傳範例](https://codepen.io/SkyrimTomato/pen/mdbwzdr?editors=0011)

### 資料夾上傳
最後一個上傳來講資料夾上傳，資料夾上傳就比較要複雜一點

上傳一個資料夾，裡頭可能有複數資料夾，有的層級有檔案，有的層級還是資料夾，面對這種巢狀結構，我們需要用到 **遞迴** 來迭代資料夾樹狀的路徑

先來看看 drop 事件這邊
```
dropContainer.addEventListener("drop", event => {
  event.preventDefault();
  
  //  用 dataTransfer.items 取得拖曳進來的資料物件 注意這裡不是使用 dataTransfer.file
  const filesData = event.dataTransfer.items;
})
```
我們現在上傳的資料分類不只是 file 檔案，同時還會有資料夾，所以我們要用 `.items` 來取得拖曳進來的資料，然後再篩選分類出哪些是資料夾或檔案

```
dropContainer.addEventListener("drop", event => {
  event.preventDefault();
  
  //  用 dataTransfer.items 取得拖曳進來的資料物件 注意這裡不是使用 dataTransfer.file
  const filesData = event.dataTransfer.items;
  
    // 考量到可能會一次拖曳好幾個資料夾，這裡要先 for 迭代一次
    for (var i=0; i < filesData.length; i++) {    
      
      // 使用 webkitGetAsEntry 來取得傳入進來的檔案分層列表
      const files = filesData[i].webkitGetAsEntry();

      if (files) {
        sortFileTree(files);
      }
    }
})
```
而 sortFileTree 函式長這樣
```
// 遞迴檢查是檔案還是資料夾
const sortFileTree = (item, path = "") => {

    if (item.isFile) { 
      item.file( file => {
        const fullPath = `${path}${file.name}`
        console.log(fullPath)
        const storageReference = firebase.storage().ref(fullPath);
        
        // .put() 方法把東西丟到雲端裡
        var task = storageReference.put(file);
      });
    } else if (item.isDirectory) {
      
      // 透過 File API createReader 去創建這層資料夾物件已供我們後續讀取
      const dirReader = item.createReader();
      
      // 透過 readEntries 去讀取這個資料夾目錄裡頭的東西
      dirReader.readEntries(entries => {
        console.log("entries.length",entries.length)
        // 使用 entries.length 知道該層級往下共有多少檔案數量 為了 loop 讀取這些檔案 這裡要跑 for 迭代
        for (let i = 0; i < entries.length; i++) {

          // loop 讀到該檔案時，再呼叫一次 sortFileTree 並傳入該檔案與路徑
          sortFileTree(entries[i], path + item.name + "/");
        }
      });
    }
}

```

使用遞迴和使用 for 迴圈一樣:要小心無窮遞迴 囧
我們要在裡頭做個 `if` 來判斷什麼情境下呼叫自己、什麼時候結束

我們傳入遞迴函式的檔案是透過 webkitGetAsEntry 取得的目錄物件，我們可以透過 `isFile` 和 `isDirectory` 來幫助我們判斷
* isFile
判斷是否是檔案：如果是檔案就將其上傳到 storage，遞迴結束

* isDirectory
判斷是否為資料夾：如果是資料夾就就條列取得裡頭所有項目，然後再呼叫 sortFileTree 往下遞迴去判斷

[firebase storage 拖曳資料夾上傳](https://codepen.io/SkyrimTomato/pen/oNvwRdz?editors=0011)


## 下載
拖曳資料夾上傳這邊可能很多人聽不懂，沒關係接下來拉回 storage 功能，來講講下載
下載的範例會沿用單檔上傳的部分

HTML
新增下載按鈕
```
<progress value="0" max="100" id="uploader">0%</progress></br>
<input type="file" value="upload" id="uploadBtn"></br></br>
<button id="downloadBtn">download</button></br>
<div id="msg"></div>
```
![](https://i.imgur.com/tqaNqml.png)

JS
新增下面這段事件
```

downloadBtn.addEventListener("click",event => {
  // 取得 storage 中對應的檔案位置
  const fileRef = firebase.storage().ref(fullPath)
  
    // .ref() 指向已存在 storage 中的檔案位置後 可以透過 getDownloadURL 取得連結
    fileRef.getDownloadURL().then(function (url) {
        console.log(url) 
    })
  
})
```

### 取得檔案連結
我們在下載點擊事件中，用到了 `getDownloadURL`
* `getDownloadURL()`
回傳字串連結，可取得檔案在 storage 上的連結位置

getDownloadURL 是個 promise ，我們可以後面接個 `.then` 去接它返傳的 url
這裡搭配 `console.log` 來檢查

![](https://i.imgur.com/byvF4Qp.png)
可以看到 url 就是檔案在 Firebase storage 上的位置連結

接著我們就可以拿 `getDownloadURL` return 的 url 去做些事情，例如是圖片連結就指派其給 `<img>` 的 `src` 屬性



### 跨域問題
若遇到跨域問題
![](https://i.imgur.com/Du9EogA.png)

見下方 Firebase Storage Cors 跨域設定

### 下載
取得檔案連結後，我們想要將實體化下載成本地檔案，這時遇到幾個知識點：

* 若今天檔案下載網站和檔案來源是同源同網域，可以直接透過 `<a>` 標籤配合 `download` 屬性去抓
* 若檔案下載網站是不同源 (例如 codepen 或 localhost )，解決了跨域問題後，我們還要把它轉成 *blob* 格式才可以下載

這裡 AJAX 取檔案，用 `fetch` 和 `blob()` 來示範
當我們轉成 blob 格式後，要用 `createObjectURL()` 將  *blob* 轉成實際檔案與取得在本地瀏覽器中的位置連結

```
    fetch(url)
    .then(res => res.blob())
    .then(blob => {
        let a = document.createElement("a");
        let url = window.URL.createObjectURL(blob);
        a.href = url;
        a.download = name;

        // Firefox 需要將 JS 建立出的 element appendChild 到 DOM 上才可以 work
        a.style.display = "none";
        document.body.appendChild(a);

        a.click();

        // 刪除多餘的 DOM 與 釋放記憶體
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);
      });
```

[codepen 範例: Firebase storage 檔案下載](https://codepen.io/SkyrimTomato/pen/ExYXqGG?editors=0010)


## 刪除
刪除的部分最簡單 只會用到 `delete()`
* `.delete()`
一樣用 `.ref` 取得的檔案位置，後面再接上`.delete()`就可以刪除

HTML 新增刪除按鈕
```
<progress value="0" max="100" id="uploader">0%</progress></br>
<input type="file" value="upload" id="uploadBtn"></br></br>
<button id="downloadBtn">download</button></br></br>
<button id="deleteBtn">delete</button></br>
<div id="msg"></div>
```

JS
```
deleteBtn.addEventListener("click",() => {

  // 取得 storage 中對應的檔案位置
  const fileRef = firebase.storage().ref(fullPath);
  
  fileRef.delete().then( () => {
        msg.textContent = "刪除成功";
        uploadBtn.value = "";
        uploader.value = 0;
    }).catch(function (error) {
        msg.textContent = "刪除失敗";
    })
})
```


[codepen 範例: Firebase storage 下載＆刪除](https://codepen.io/SkyrimTomato/pen/NWKvWGo?editors=0010)


## 條列式讀取 Firebase Storage 上的檔案
最後可能會有人想，那怎麼沒講到讀取？
製作雲端系統時，頁面顯示時要怎麼**條列式列出這層有哪些檔案或資料夾？**

這部分，Firebase Storage 有提供便捷的 API 來幫助我們列出

* `.listAll()`
* `.prefixes`
* `.items`


`listAll()` 後方傳入的參數 res
透過`.prefixes` 取得這層所有資料夾
透過 `.items` 取得這層所有檔案與資訊

[codepen 範例: 條列出資料夾與檔案名稱](https://codepen.io/SkyrimTomato/pen/mdbMdGQ?editors=1011)

> 備註：請將 firebaseConfig 替換成你自己的設定呦

## Firebase Storage Cors 跨域設定
當我們要下載或上傳 Storage 東西時，若瀏覽器 console 出現 CORS 報錯，代表我們沒設定跨域設定，見 [Firebase 文件](https://firebase.google.com/docs/storage/web/download-files#cors_configuration) 與 [Cloud Storage CORS](https://cloud.google.com/storage/docs/configuring-cors?hl=zh-tw) 說明
要做到跨域的設定與白名單，我們需要安裝 gsutil 這個套件
[gsutil 安裝方法](https://cloud.google.com/storage/docs/gsutil_install)

接著在自己電腦新增一支名為 cors.json 的檔案，其內容為
```
[
    {
      "origin": ["*"],
      "method": ["GET"],
      "maxAgeSeconds": 3600
    }
]
```
* origin
值為陣列，裡頭可以存放多個要設為白名單的網域(字串格式)，這裡 `"*"` 字號代表不限制網域都可以的意思
* method
值為陣列，裡頭存放我們想設定允許的方法(字串格式)，例如 GET、POST，這裡先設定`"GET"`
* maxAgeSeconds
值為數值，maxAgeSeconds 指的是指定時間秒數，瀏覽器對特定資源的預取（OPTIONS）請求返回結果的快取緩存時間，單位為秒，藉由快取回應的方式，瀏覽器便不需要在原始要求重複時，將再次向 Firebase 傳送要求。


> cors.json 檔案存放位置不拘，但要記住你放在哪個資料夾位置

因為接著要開啟終端機，cd 指到你存有 cors.json 的資料夾位置後，輸入

```
gsutil cors set cors.json 你的Storage資料夾網址
```
Storage 資料夾網址可以到 Firebase 後台 > Storage > 檔案，就是附圖中的 `gs://xxxx`
![](https://i.imgur.com/CjHrZEA.png)

若是終端機出現 401 訊息 代表沒登入，在終端機輸入
```
gcloud auth login
```
有安裝 gsutil 這邊 gcloud 指令就會正常執行

選擇你的帳戶，注意是對應你當前 Firebase 應用程式的 google 帳號登入
接著再輸入
```
gsutil cors set cors.json 你的Storage資料夾網址
```
看到終端機出現 `Setting CORS on gs:/xxxx` 就成功了！


若是出現 403 訊息，代表當前可能已經登入但對應的 Storage 帳號不一樣，就要先登出
```
gcloud auth revoke --all
```

然後再登入一次
```
gcloud auth login
```
接著登入後一樣回終端機輸入
```
gsutil cors set cors.json 你的Storage資料夾網址
```
看到終端機出現 `Setting CORS on gs:/xxxx` 就成功了！

## 進階 TODO
瞭解 Firebase Storage 基本上傳、下載、刪除、條列顯示功能，各位可以思考進階雲端系統開發，自己的雲端系統還要添加什麼功能和設計

例如
* 檔案存入 Storage 時，是否也要存入檔案資訊到 Firebase Database?
* listAll 條列出檔案時，是否將檔案/資料夾路徑資訊透過 `data-` 綁在 HTML tag 上供點擊事件取用？
* 雲端資料夾與路由該如何分層？
* 麵包選單設定？
* 分享功能的網址哪裡來的？
* 刪除與 undo 功能？
 
### Enjoy Coding!