How AppFlowy Implements File Storage and Upload/Download: A Technical Deep Dive
AppFlowy implements file storage and upload/download through a layered async architecture that separates storage abstraction, local temporary file caching, and cloud backend integration, enabling resumable multi-part uploads with real-time progress tracking.
AppFlowy-IO/AppFlowy manages user-generated files—images, attachments, and custom themes—using a modular Rust architecture designed for reliability and cloud agnosticism. The file storage and upload/download implementation leverages trait-based abstraction, temporary file staging, and a queue-driven worker system to handle concurrent transfers while insulating the Flutter UI from backend complexity.
Storage Service Abstraction Layer
At the core of AppFlowy’s file handling lies a dual-trait system that decouples the frontend from concrete storage implementations. This design allows the same UI code to target local mock storage, AF Cloud, or future S3-compatible backends without modification.
The Public API Contract
The primary interface for all storage operations is the StorageService trait defined in frontend/rust-lib/flowy-storage-pub/src/storage.rs. This trait exposes async methods for upload creation, deletion, download, and progress subscription:
#[async_trait]
pub trait StorageService: Send + Sync {
async fn delete_object(&self, url: String) -> FlowyResult<()>;
fn download_object(&self, url: String, local_file_path: String) -> FlowyResult<()>;
async fn create_upload(
&self,
workspace_id: &str,
parent_dir: &str,
local_file_path: &str,
) -> Result<(CreatedUpload, Option<FileProgressReceiver>), FlowyError>;
async fn start_upload(&self, record: &BoxAny) -> Result<(), FlowyError>;
async fn resume_upload(
&self,
workspace_id: &str,
parent_dir: &str,
file_id: &str,
) -> Result<(), FlowyError>;
}
All UI-layer components interact with the storage subsystem through an Arc<dyn StorageService>, ensuring the Flutter frontend remains agnostic of whether files live locally or in the cloud.
Cloud Backend Abstraction
The actual remote operations are governed by the StorageCloudService trait in frontend/rust-lib/flowy-storage-pub/src/cloud.rs. This interface defines the multi-part upload protocol and CRUD operations:
pub trait StorageCloudService: Send + Sync {
async fn create_upload(
&self,
workspace_id: &Uuid,
parent_dir: &str,
file_id: &str,
content_type: &str,
file_size: u64
) -> Result<CreateUploadResponse, FlowyError>;
async fn upload_part(
&self,
workspace_id: &Uuid,
parent_dir: &str,
upload_id: &str,
file_id: &str,
part_number: i32,
body: Vec<u8>,
) -> Result<UploadPartResponse, FlowyError>;
async fn complete_upload(
&self,
workspace_id: &Uuid,
parent_dir: &str,
upload_id: &str,
file_id: &str,
parts: Vec<CompletedPartRequest>,
) -> Result<(), FlowyError>;
}
The concrete implementation for AppFlowy’s hosted backend, AFCloudFileStorageServiceImpl, resides in frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs and maps these methods to the AF Cloud HTTP API.
Upload Pipeline Architecture
When a user initiates a file upload, the system executes a five-stage pipeline that guarantees data integrity and supports resumption after network interruptions.
Temporary File Staging
Before any network activity begins, the FileTempStorage struct (in frontend/rust-lib/flowy-storage/src/file_cache.rs) creates a defensive copy of the user’s file inside a .cache_files directory within the application root. This ensures the original file cannot be mutated during the chunked upload process.
Key methods include:
create_temp_file_from_existing– Copies the selected file into the temporary cachedelete_temp_file– Cleans up cached chunks after successful upload
Upload Queue Management
The StorageManager (in frontend/rust-lib/flowy-storage/src/manager.rs) orchestrates the upload flow. When create_upload() is invoked, it:
- Validates against the global
is_exceed_storage_limitflag - Stages the file via
FileTempStorage - Persists metadata to SQLite (
UploadFileTable) - Enqueues a task in the
UploadTaskQueue
The FileUploader worker (defined in frontend/rust-lib/flowy-storage/src/uploader.rs) processes the BinaryHeap of tasks with a concurrency limit of 3. It handles retries with exponential backoff and automatically pauses when storage quotas are exceeded. The FileUploaderRunner operates as an async loop reacting to Signal::Proceed and Signal::Stop events to drive the queue forward.
AF Cloud Backend Implementation
The AFCloudFileStorageServiceImpl<T> executes the actual multi-part upload against the remote API:
create_upload– Initializes the upload session via the/create-uploadendpoint, returning a pre-signed URL andupload_idupload_part– Transmits individual chunks (asVec<u8>) to the pre-signed URL, storing returned ETags and part numbers in the local databasecomplete_upload– Finalizes the object by sending the assembled parts list to the cloud, where the service reconstructs the final file
This implementation supports resumable uploads; if a transfer interrupts, the system can invoke resume_upload() using the persisted chunk metadata to restart from the last successful part.
Download Implementation
File retrieval follows a simpler path through StorageServiceImpl::download_object (in frontend/rust-lib/flowy-storage/src/manager.rs). The method spawns a background Tokio task that:
- Checks if
local_file_pathalready exists (idempotent behavior) - Calls
cloud_service.get_object(url)to fetch raw bytes - Writes the data to disk using
tokio::fs::OpenOptions
If the file already exists locally, the download is skipped, preventing redundant network traffic.
Progress Notification System
Throughout the upload lifecycle, the system emits FileProgress events containing completion ratios (0.0 to 1.0) or error states. These events flow through a global broadcast channel (global_notifier) to which the UI can subscribe. When create_upload() is called, it returns an optional FileProgressReceiver that streams updates to the Flutter frontend, enabling real-time progress bars and completion callbacks.
Flutter Frontend Integration
The Dart layer interacts with the Rust backend through the AppFlowy SDK (FIDL bridge). In frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart, the upload flow is triggered by:
await storageManager.createUpload(
workspaceId,
parentDir,
file.path,
);
The widget listens to the returned progress stream to display loading indicators and handles completion states when the receiver emits a progress value of 1.0. Additional upload interfaces, such as the theme customizer in theme_upload.dart, reuse the same storage manager methods, demonstrating the abstraction’s flexibility.
Summary
- Modular trait-based design –
StorageServiceandStorageCloudServicetraits enable swapping cloud providers without UI changes - Defensive temporary staging –
FileTempStoragecopies files to.cache_filesbefore upload to prevent mutation - Queue-driven concurrency –
FileUploaderprocesses up to 3 concurrent uploads with retry logic and backpressure handling - Resumable multi-part protocol – Chunked uploads with SQLite-backed progress tracking support interruption and recovery
- Real-time progress streaming –
FileProgressReceiverchannels broadcast updates to Flutter widgets - Idempotent downloads – Background tasks skip existing files to conserve bandwidth
Frequently Asked Questions
How does AppFlowy handle large file uploads?
AppFlowy handles large files through chunked multi-part uploads. The AFCloudFileStorageServiceImpl splits files into discrete parts and uploads them sequentially via upload_part(). Each part’s ETag and number are stored in SQLite, allowing the system to resume from the last successful chunk if the connection drops. A debug-only size limit (maximum_upload_file_size_in_bytes) is enforced during the create_upload phase.
Can AppFlowy resume interrupted uploads?
Yes. The upload system is fully resumable. When resume_upload() is called on the StorageService implementation, the system queries the UploadFileTable for existing chunk metadata and invokes the cloud backend’s upload_part method only for missing segments. The FileUploader queue automatically handles retry logic with exponential backoff until the complete_upload signal is sent.
What cloud storage backends does AppFlowy support?
Currently, AppFlowy ships with AF Cloud as the primary backend, implemented by AFCloudFileStorageServiceImpl in flowy-server/src/af_cloud/impls/file_storage.rs. However, the architecture supports any backend implementing the StorageCloudService trait. Developers can inject alternative implementations for S3-compatible services or local mock storage by providing a different Arc<dyn StorageCloudService> to the StorageManager.
How does the Flutter UI track upload progress?
The UI receives real-time updates through a FileProgressReceiver channel created during the create_upload() call. This receiver streams FileProgress structs containing a float between 0.0 and 1.0. The Flutter widget (e.g., upload_image_menu.dart) listens to this stream and updates progress indicators accordingly. When the value reaches 1.0, the UI displays a completion state and removes the loading indicator.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →