雲端儲存 (cloud storage) 已成為跨裝置檔案協作的基礎。Google Drive、Dropbox、Microsoft OneDrive 與 Apple iCloud 等服務透過將雲端儲存無縫整合進日常工作流程,徹底改變了檔案管理方式。本章將探討 Google Drive 行動客戶端的設計。

Figure 1 展示了 Google Drive 的行動介面,說明複雜需求如何轉化為簡潔、易用的體驗。

Figure 1: Google Drive screenshots

Step 1: 理解問題並確立設計範圍#

以下是與面試官的對話範例:

  • Candidate: 我想先界定 Google Drive 行動客戶端的核心功能。基本功能應包含上傳與下載檔案 (並可取消進行中的操作)、建立與刪除資料夾以組織內容,並支援常見的檔案類型 (照片、影片、文件)。
  • Interviewer: 可以。既然是行動 app,網路可靠性是個問題。讓上傳支援可恢復 (resumable),讓使用者在連線中斷後能接續上傳。
  • Candidate: 沒問題。關於檔案大小,是否要設定上限?
  • Interviewer: 就設為 10 GB
  • Candidate: 使用者可能會多次更新同一檔案,是否需要支援版本歷史 (file versioning)?
  • Interviewer: 要,版本歷史是雲端儲存的重要功能。
  • Candidate: 使用者常從多裝置存取 Google Drive,我們要設計單一使用者多裝置,還是多使用者 (含檔案分享)?
  • Interviewer: 聚焦在單一使用者多裝置即可,暫不處理分享與權限管理。
  • Candidate: 非功能需求方面,應包含大規模使用者的可擴展性,以及在網路不佳時的資料持久性?
  • Interviewer: 是的。規模方面,設計支援約 1,000 萬 DAU
  • Candidate: 最後一個問題,Google Drive 的 Docs/Sheets 即時協作是否要涵蓋?
  • Interviewer: 將 Docs/Sheets 當作一般檔案處理,包含即時協作。

Requirements#

根據討論,以下是 Google Drive app 的核心功能需求:

  • 使用者可上傳與下載檔案 (最大 10 GB),並可取消進行中的操作。
  • 使用者可建立與刪除資料夾以組織內容。
  • 使用者可檢視並還原檔案的歷史版本。
  • 支援可恢復上傳 (resumable uploads)
  • 檔案可跨使用者的多個裝置同步。

非功能需求 (non-functional requirements)

  • 可擴展性 (scalability):支援 1,000 萬 DAU、龐大資料量,並涵蓋各種網路條件與裝置類型。
  • 資料持久性 (data durability):即使網路不佳,仍能可靠存取已下載的檔案。

範圍外 (out of scope)

  • 即時協作 (Docs/Sheets 多人編輯)。
  • 檔案分享與權限管理。

UI sketch#

Figure 2 展示 Google Drive app 的核心畫面:

  • Files List screen (中):app 的主要入口,瀏覽與管理檔案與資料夾。
  • Upload Modal (左):上傳檔案的專注介面。
  • File Revisions screen (右):顯示特定檔案的版本歷史以供檢視與還原。

Figure 2: Basic sketch of our Google Drive app

Step 2: API 設計#

定義好需求與 UI 後,接著設計連接行動客戶端與後端的 API。

網路協定 (Network protocol)#

Google Drive app 主要是客戶端驅動 (client-driven) 的操作,例如上傳、下載、組織資料夾等。對這類行為而言,HTTP + REST API 是直接且廣泛採用的方案。

但為了處理即時通知 (例如共享檔案被更新),我們會搭配 WebSockets 或 Server-Sent Events (SSE)。由於這類通知只是告知變更訊號、而非傳輸實際檔案內容,兩者皆可勝任。

本設計採用 REST,但在此規模下 gRPC + Protocol Buffers 也是不錯的選擇,具備以下優勢:

  • 二進位編碼 (binary encoding) 提升效能。
  • 強型別 (strong type safety) 保障 API 可靠性。
  • 高效的雙向串流 (bidirectional streaming),適合即時檔案更新。

熟悉 gRPC 的讀者可自行替換協定,本章討論的模式不受協定選擇影響。

Endpoints 與資料模型#

接著定義核心 API 端點與資料結構,從最基本的檔案與資料夾開始。

Files and folders#

由於 Google Drive 的資料夾本質上就是帶有特殊類型的檔案,我們將兩者的管理統一在同一組端點下。

  • List filesGET /v1/files?parentId={fileId}&page={page}&limit={limit}
    • Response: 200 OK with GetFilesResponse
    • 選擇性 query parameter:parentId (預設為 root)。
  • Get file detailsGET /v1/files/{fileId} → 200 OK with File
  • Download fileGET {downloadUrl} (由 File 物件提供)。
  • Create file or folderPOST /v1/files (Body: metadata,如 name、mimeType、parentId)。
  • Delete file or folderDELETE /v1/files/{fileId}
  • Update file/folder metadataPATCH /v1/files/{fileId}
資料模型 (Kotlin / Swift)
data class GetFilesResponse(
    val files: List<File>,
    val pagination: PaginationMetadata,
)

data class File(
    val id: String,
    val name: String,
    val mimeType: String,
    val size: Long,
    val downloadUrl: String?,
    val checksum: String?,
    // ...
)
struct GetFilesResponse {
    let files: [File]
    let pagination: PaginationMetadata
}

struct File {
    let id: String
    let name: String
    let mimeType: String
    let size: Int64
    let downloadUrl: String?
    let checksum: String?
    // ...
}

這些端點遵循標準 REST 慣例,所有請求都需要:

  • JWT bearer token 於 Authorization header 中進行驗證。
  • JSON content type (application/json)。

端點可能回傳 4xx (client error:驗證失敗、資源不存在、格式錯誤) 或 5xx (server error)。

File revisions#

為了支援檢視與還原檔案歷史,我們加入對應的版本端點:

  • List revisionsGET /v1/files/{fileId}/revisions?page={page}&limit={limit}
  • Get revision detailsGET /v1/files/{fileId}/revisions/{revisionId}
  • Restore revisionPOST /v1/files/{fileId}/revisions/{revisionId}/restore
FileRevision 資料模型
data class FileRevision(
    val revisionId: String,
    val fileId: String,
    val mimeType: String,
    val size: Long,
    val modifiedAt: String,
    val downloadUrl: String,
    val checksum: String,
)
struct FileRevision {
    let revisionId: String
    let fileId: String
    let mimeType: String
    let size: Int64
    let modifiedAt: String
    let downloadUrl: String
    let checksum: String
}

這樣客戶端就能輕鬆檢視與還原先前版本。

Step 3: 高階客戶端架構#

定義好 API 後,接著聚焦於行動客戶端的架構。目標是打造模組化、可擴展的設計,既能支援現有功能,也為未來成長保留彈性。

以下先看外部依賴,再進入客戶端本身,如 Figure 3 所示。

Figure 3: High-level architecture diagram of our Google Drive app

External server-side components#

  • Backend:核心系統,負責檔案儲存、metadata 與商業邏輯,提供 REST API (上傳/刪除) 與 SSE/WebSockets (即時更新)。
  • CDN:將靜態檔案快取至離使用者更近的節點,降低下載延遲。
  • API Gateway:客戶端進入後端的單一入口,處理驗證、路由、安全性。

Client architecture#

客戶端分為兩個核心層級:UI layerData layer,確保程式碼乾淨、可測試、易維護。

  • UI layer 包含三個主要畫面:
    • File List Screen:瀏覽內容。
    • File Upload Modal:新增檔案。
    • File Revisions Screen:檢視檔案歷史。
  • Data layer 透過 Files 與 Revisions Repositories 管理核心功能,搭配:
    • Network Dispatcher:處理與後端的 HTTP 通訊。
    • Revisions Remote Data Source:透過 SSE/WebSockets 監聽即時更新。

這個架構為 Google Drive app 打下穩固基礎。接下來深入探討系統中較複雜的面向。

Step 4: Design deep dive#

以下聚焦於 Google Drive 系統中幾個值得關注的主題:資料儲存可恢復上傳版本歷史資料加密

Data storage#

在 Google Drive 這類雲端儲存 app 中,客戶端需同時管理 metadata (檔名、資料夾階層、版本資訊) 與已下載的檔案內容。以下說明如何在本機有效且安全地儲存兩者。

File metadata storage#

app 會頻繁處理兩類資料:

  1. 瀏覽目錄時的檔案與資料夾 metadata (File)。
  2. 追蹤變更與即時更新的版本資訊 (FileRevision)。

這類 metadata 需要頻繁查詢與更新,因此關聯式資料庫 (relational database) 是理想選擇:

  • 結構化查詢:可快速執行「列出檔案」「尋找版本」等操作。
  • 關聯性:透過 parentId 維護檔案與資料夾的階層完整性。
  • 可擴展性:metadata 增長時仍能維持效能。
Database design#

FileFileRevision 資料模型結構清晰,可自然對應到資料庫表格,便於跨階層查詢、追蹤版本關係與維持資料一致性。

Figure 4 顯示更新後的架構,每個資料表都有獨立的 DAO (Data Access Object) 元件。

Figure 4: Data layer design update

Caching approach#

為了平衡速度與儲存空間,我們將 metadata DB 上限設為可調整值 (例如 200 MB),並採用 LRU (Least Recently Used) 驅逐策略:

  • 保留最近存取過的 metadata。
  • 清除較舊的項目。
  • 單筆 metadata 很小,故可快取大量資料卻佔用極少空間。

Managing downloaded files#

為了支援離線存取,使用者可下載檔案,這些檔案需安全且便於管理地儲存在本機。

app 的私有內部儲存 (private internal storage) (iOS/Android 沙盒) 是最佳選擇:

  • 檔案受保護,不被外部存取。
  • app 可管理檔案 (刪除、更新) 而不影響其他 app 或使用者目錄。
  • app 內直接存取,必要時可匯出至共享儲存。
How it works#
  • 檔案儲存在特定資料夾 (例如 Android 的 /data/user/0/com.google.android.apps.drive/files/downloads/<fileId>),使用 fileId 作為唯一檔名避免衝突。
  • 下載前 app 會檢查可用空間,若不足則提示使用者。

Industry insights:Box Drive 支援將資料夾標記為離線使用。被選取後,內容會於背景自動下載並儲存至本機供離線使用。

Resumable file uploads#

可恢復上傳 (resumable uploads) 是行動雲端儲存 app 的基石,尤其適合大檔案與不穩定的網路。它提供幾項關鍵優勢:

  • 使用者可暫停並恢復上傳,無需重新開始。
  • 連線中斷後可無縫復原。
  • 可依網路或電量條件管理上傳,提升效率與體驗。

Upload types#

許多主流 app 使用 resumable uploads 處理大檔案,包含 Google Photos、X (Twitter)、Box、Dropbox、Microsoft OneDrive 等。

以 Google Drive 為例,其文件定義了三種上傳方式:

  • Simple upload (uploadType=media):傳輸小於 5 MB 的媒體檔,不含 metadata。
  • Multipart upload (uploadType=multipart):傳輸小於 5 MB 的檔案,並於單一 request 內包含 metadata。
  • Resumable upload (uploadType=resumable):適用於大於 5 MB 的大檔案,或網路不穩的行動情境。Google 也建議將其用於大多數應用,因為它同樣適用於小檔案,僅多出一次 HTTP request 的成本。

以下聚焦於可恢復上傳的實作。

High-level steps#

  1. Initiate:client 請求建立 upload session,取得唯一 upload ID 與 session 詳細資訊。
  2. Upload chunks:檔案以較小的 chunk 分次上傳。若連線中斷,從最後成功的 chunk 恢復。
  3. Complete:所有 chunk 上傳完成後,server 最終化檔案並可選擇性驗證完整性。

Figure 5: Resumable uploads steps

各家實作的主要差異在端點結構:

  • 單一統一端點 (single unified endpoint):X (Twitter) 與 Google (Drive/Photos) 採此方式,透過 header 或 query parameter 管理 metadata。例如 Google Photos:
    • POST /files?uploadType=resumable&uploadId={uploadId}
    • Header:X-Goog-Upload-Command=[start|upload|finalize]X-Goog-Upload-Offset={bytes_offset}
  • 多端點 (multiple endpoints):Dropbox、Box 將各步驟拆到不同端點,例如 Dropbox:
    • POST /files/upload_session/start
    • POST /files/upload_session/append
    • POST /files/upload_session/finish

兩種方式皆可,以下以多端點為例詳細說明。

Implementation details#

Initiate upload#

POST /v1/upload?uploadType=resumable

UploadRequest / UploadResponse 資料模型
data class UploadRequest(
    val requestId: String,
    val name: String,
    val mimeType: String,
    val parentId: String,
    val size: Long,
)

data class UploadResponse(
    val uploadId: String,
    val totalChunks: Int,
    val chunksProcessed: Int,
    val chunkSize: Long,
    val uploadUrl: String,
    val uploadExpiresAt: String,
)
struct UploadRequest {
    let requestId: String
    let name: String
    let mimeType: String
    let parentId: String
    let size: Int64
}

struct UploadResponse {
    let uploadId: String
    let totalChunks: Int
    let chunksProcessed: Int
    let chunkSize: Int64
    let uploadUrl: String
    let uploadExpiresAt: String
}

UploadResponse 透過追蹤「檔案如何被切分」「已上傳了什麼」「下一個 chunk 要從哪開始」,指引整個上傳流程,無論是新上傳或恢復中的上傳。

Upload chunks and verify#
PUT /v1/upload/{uploadId}/chunk
Content-Range: bytes start-end/total

<binary chunk data>
  • Body:chunk 的原始二進位資料。
  • Response:最後一個 chunk 回傳 201 Created,否則回傳 308 Resume Incomplete 搭配 UploadResponse payload。

Content-Range header 告知後端目前傳送哪一段,當最後一段抵達時,後端組合整個檔案並回傳完成訊息,client 依回應碼驗證成功。

Resume upload#
GET /v1/upload/{uploadId}
  • Description:取得 upload session 資訊。
  • Response:200 OK with UploadResponse

client 從 chunksProcessed 之後的下一個 chunk 繼續上傳。若 session 已過期,則需重新發起上傳。

Cancel upload#
DELETE /v1/upload/{uploadId}

允許 client 中止上傳並釋放 server 資源。

Mobile implementation challenges#

實作 resumable uploads 時需要面對幾個平台特有的挑戰。

Managing background tasks#
  • 上傳應在背景執行,避免阻塞 UI thread。
  • 行動 OS 可能隨時終止背景任務,為此需將待完成的 UploadResponse 持久化到磁碟。
  • app 啟動時檢查未完成上傳並恢復,同時監控電量與網路狀態。
Limited storage and permissions#
  • 儲存:上傳前會暫時快取檔案。空間不足時應讓使用者清除上傳快取、移除已下載的檔案或檢視儲存使用量。
  • 檔案存取:使用系統 file picker 取得暫時讀取權限,或請求儲存權限。若使用者拒絕,應停用上傳功能並引導至系統設定。
Error handling and recovery#

後端處理了大部分複雜度 (如決定 chunkSize 與追蹤進度),但 client 仍需優雅處理錯誤:

  • 連線問題:儲存當前狀態,恢復連線後從最後成功的 chunk 繼續,並顯示「Connection lost, attempting to resume」之類的訊息。
  • 無效狀態:若偵測到 uploadUrl 過期、回應不一致 (chunksProcessed > totalChunks)、或 uploadExpiresAt 已到期,應重新發起上傳。

使用指數退避 (exponential backoff) 重試,逐步增加重試間隔,避免短時間內大量重試壓垮 server。

User experience and feedback#

即使使用者重啟 app 也應清楚看到上傳進度。client 可以:

  • chunksProcessed / totalChunks 計算進度。
  • 顯示有意義的進度指示器。
  • app 在背景時透過系統通知告知狀態。

Architecture changes in the diagram#

Figure 6 展示加入新元件後的架構:

  • UI layer:新增 File PickerFile Permissions Helper,處理使用者與檔案系統的互動。
  • Data layer:新增 File Splitter 切割大檔案,以及 File Uploader RepositoryFile Uploader Service 管理背景上傳流程。

Figure 6: Resumable file upload-related updates to the high-level architecture diagram

Version history#

版本歷史讓使用者在需要時能還原至先前版本。以下討論如何上傳新版本,以及如何處理變更重疊時的衝突。

Uploading a new version of an existing file#

當使用者修改並上傳檔案時,app 需有效處理新版本。主流做法有兩種:full copy uploadblock-level sync

Full copy upload#

每次變更都上傳整個檔案。

  • Client:無論改動大小,都傳送整份檔案。
  • Backend:每個版本都完整獨立儲存。
  • 優點:
    • 實作簡單,無需追蹤細部變更。
    • 每個版本完整,容易還原。
  • 缺點:
    • 儲存量大,大檔案多版本累積尤甚。
    • 耗費頻寬。

Google Drive、Box、OneDrive、iCloud 等多採此法,因為簡單可靠。

Block-level sync#

Block-level sync 更為精巧,只上傳變更的部分。

  • Client:將檔案切成 block,偵測差異,只傳送變動的 block。
  • Backend:維護 block registry 並在需要時重組版本。
  • 優點:節省頻寬與儲存,適合大檔案的小幅編輯。
  • 缺點:
    • 需進階邏輯管理並重組 block,複雜度高。
    • 兩端皆需額外運算資源。

Dropbox 透過 Broccoli 編碼系統與 block sync 協定開創了此技術。

Managing file conflicts#

檔案衝突發生在多裝置同時修改同一檔案、版本不一致時。常見情境:

  • 使用者在某裝置離線編輯,同時另一裝置修改了同一檔案。
  • 兩裝置幾乎同時上傳同一檔案的變更。
  • 同步中斷導致檔案處於不一致狀態。

處理方式分為兩部分:conflict detection (偵測) 與 conflict resolution (解決)。以下分別探討廣泛採用的偵測方式 optimistic concurrency control,以及常見的解決策略 Last-Write-Wins (LWW)version forking

Conflict detection#

Optimistic concurrency control 假設衝突罕見,只在更新時才驗證。運作流程如下:

  1. 每個檔案都有版本識別 (版本號或 hash),由 client 與 server 共同追蹤。
  2. Client 上傳新版本時,附上當前版本識別 (例如 If-Match: <version> header)。
  3. Server 比對:
    • 相同:接受上傳,版本號遞增。
    • 不同:回傳 409 Conflict
  4. 衝突時,client:
    • 透過 GET /v1/files/{fileId} 取得最新版本。
    • 自動或由使用者手動合併變更。
    • 以更新後的版本重試上傳。

優點:

  • 確保上傳建立於最新版本之上,避免遺失更新。
  • 可進行細粒度的衝突解決,提升資料完整性。

缺點:

  • 衝突解決流程會增加延遲,在不穩定的行動網路上尤為明顯。
  • 可能需要使用者手動合併,造成使用體驗摩擦。
Conflict resolution#

Google Drive 等雲端儲存常採最終一致性 (eventual consistency) 模型,以可用性為優先,允許更新先行、之後再解決衝突。兩種常見策略:

  • Last-Write-Wins (LWW):自動以最新版本取代舊版本。
  • Version forking:保留兩個衝突版本,通常透過檔名後綴區分 (如 filename.pngfilename (1).png)。
Real-world examples#

策略選擇因 app 需求而異:

  • Dropbox:預設使用 LWW,自動儲存最新版本,不詢問使用者。
  • Google Drive:交由使用者決定,偵測到衝突時顯示對話框 (見 Figure 7),詢問是「同時保留」或「取代」。

Figure 7: Google Drive user experience when uploading a file whose name already exists in the folder

Dropbox 的公開 API 透過上傳參數同時支援兩種策略:

  • strict_conflict:啟用嚴格驗證,防止併發上傳並回傳衝突錯誤。
  • mode:控制衝突發生時的行為:
    • add:自動重新命名,加上唯一計數器。
    • overwrite:覆蓋既有檔案,即 LWW。

Data encryption#

面試官有時會追加需求,例如「若要支援企業客戶,需要將儲存在裝置上的檔案加密,該怎麼做?」

Files and metadata encryption#

若無加密,裝置遺失或被破解時,攻擊者可掛載儲存存取敏感檔案。我們使用業界標準 AES-256,兼顧安全性與行動硬體效能:

  • Metadata 保護:檔案名稱、資料夾結構等存於加密資料庫。
  • 檔案保護:下載後的檔案加密儲存在 app 私有空間。

即使儲存內容被存取,沒有解密金鑰也無法解讀。

Key management for offline access#

為了支援離線存取,加密金鑰必須在本機存放。善用平台安全機制:

  • Android:Android Keystore 提供硬體支援的金鑰儲存,防止被提取。
  • iOS:Keychain 提供類似保護,並於支援裝置整合 Secure Enclave

為了額外保障,可以使用 PBKDF2 從使用者登入憑證派生金鑰,確保只有經過驗證的使用者才能解鎖資料,將加密與使用者身份緊密綁定。

Mobile implementation considerations#

加密雖提供安全,卻在行動裝置上帶來獨特挑戰。

Performance and resources#
  • 加解密對 CPU 負擔重,尤其大檔案。應於背景執行緒處理以保持 UI 流暢。
  • 加密檔案因 padding 與 metadata 而略微變大。
  • 需留意記憶體消耗,特別是同時持有加密與解密版本時。
Memory management#
  • 實作逾時清理 (timeout),自動釋放解密後的內容。
  • 記憶體吃緊時釋放快取的解密資料。
  • 留意加解密循環中的記憶體洩漏。
Error handling and recovery#

金鑰損毀、無法存取或加解密失敗時,需有明確的 fallback 路徑,並通知使用者採取正確行動。

Backup strategy#

加密檔案與裝置特定金鑰綁定,備份需特別處理:

  • 預設將加密檔案排除在系統備份之外
  • 若需備份,實作自訂邏輯以備份專用金鑰重新加密。
Device compatibility#

並非所有裝置都有相同的安全硬體:

  • 初始化時檢查安全硬體可用性
  • 在較舊裝置上優雅地 fallback 至 app sandbox 儲存。
  • 告知使用者目前以降級的安全能力運行。

Step 5: Wrap-up#

本章我們設計了一個 Google Drive 客戶端,解決雲端儲存 app 的關鍵挑戰:

  • 讓使用者安全地儲存、存取與同步檔案,並保留完整的版本歷史。
  • 以 REST API 處理核心操作,並透過 SSE/WebSockets 傳遞即時更新。
  • 透過清晰的 client 結構與後端互動,處理從基本檔案操作到 resumable uploads、版本歷史等進階功能。

若面試時間充裕,可考慮延伸以下題目:

  • Selective sync:設計讓使用者可選擇哪些資料夾或檔案類型在本機儲存、哪些只留在雲端的系統。
  • File sharing system:設計含細粒度權限與存取控管的完整檔案分享系統。
  • Compression:以 Google 的 Brotli 等壓縮技術優化資料傳輸與儲存,降低頻寬與儲存成本。