Multi-cloud file storage app for the Frappe framework. Uploads Frappe File attachments to Amazon S3, Google Cloud Storage (GCS), or Azure Blob Storage and serves them from the cloud.
- Three providers: Amazon S3, Google Cloud Storage, and Azure Blob Storage; switch via single configuration.
- Enable/disable: All upload, delete, and migrate behaviour runs only when Cloud Storage Configuration is enabled.
- Automatic upload: New File attachments (via Attach or image fields) are uploaded to the configured bucket/container; local file is removed and
file_urlis updated to the cloud URL. - Two buckets/containers: Separate private and public buckets/containers. Private: no public ACL; all access via signed URL. Public: objects get public-read (S3), make_public (GCS), or Blob-level public access (Azure); direct URLs. Avoids permission errors when the bucket/container blocks public access.
- Private files: Uploaded to the private bucket/container; served via time-limited signed URLs only.
- Public files: Uploaded to the public bucket/container with public read;
file_urlis the direct public URL. - Delete from cloud: Optional "Delete file from cloud when File is deleted"; when enabled, deleting a File document also deletes the object from the bucket/container.
- Test connection: Toolbar button on Cloud Storage Configuration to verify bucket/container access.
- Migrate existing files: Toolbar button to upload all existing local File records to the configured cloud (skips files already on cloud).
cd $PATH_TO_YOUR_BENCH
bench get-app $URL_OF_THIS_REPO
bench install-app multi_cloud_storageInstall Python dependencies (if not installed by bench):
pip install -r apps/multi_cloud_storage/requirements.txtDependencies: boto3, google-cloud-storage, azure-storage-blob, azure-identity, python-magic.
Go to Cloud Storage Configuration.
| Field | Description |
|---|---|
| Enabled | Turn cloud storage on/off. When off, no upload/delete/migrate runs. |
| Delete file from cloud when File is deleted | If enabled, deleting a File document also deletes the object in the bucket/container. |
| Storage Provider | Amazon S3, Google Cloud Storage, or Azure Blob Storage. |
| Signed URL Expiry (seconds) | Expiry for private-file signed URLs (default 300). |
| Folder Prefix | Optional prefix for object keys (e.g. frappe-files). |
| Field | Description |
|---|---|
| Private Bucket Name | Bucket for private files (required). No public ACL; use signed URLs only. |
| Public Bucket Name | Bucket for public files (required). Objects get public-read ACL. |
| Region | AWS region (e.g. us-east-1). |
| Access Key ID | Optional; omit to use IAM role or env credentials. |
| Secret Access Key | Optional; required if Access Key ID is set. |
| Field | Description |
|---|---|
| Private Bucket Name | Bucket for private files (required). No make_public; signed URLs only. |
| Public Bucket Name | Bucket for public files (required). Objects get make_public. |
| Service Account JSON | Full JSON key for a service account with access to both buckets. |
| Field | Description |
|---|---|
| Storage Account Name | Azure storage account name (required). Used to construct all blob URLs. |
| Private Container Name | Container for private files (required). Must have no public access. |
| Public Container Name | Container for public files (required). Must have Blob-level public access enabled in the Azure portal. |
| Storage Account Key | Optional; leave blank to use Managed Identity (azure-identity must be installed). |
Managed Identity: When Storage Account Key is left blank, the backend uses DefaultAzureCredential from azure-identity (supports Managed Identity, environment variables, Azure CLI, etc.). Signed URLs in this mode use a User Delegation Key — the identity must have the Storage Blob Delegator role on the storage account.
You can use the same bucket/container for both by setting the same name for Private and Public; private files will still be served only via signed URL (no public ACL). Use Test Connection after saving to confirm access.
- Upload: On File
after_insert, if cloud storage is enabled and the file is on disk, it is uploaded to the private or public bucket/container according tois_private. The File row is updated with the cloudfile_urlandcontent_hash(stored asprivate:keyorpublic:keyso delete/URL generation know which bucket/container to use). The local file is removed. - Private files: Stored in the private bucket/container;
file_urlis/api/method/multi_cloud_storage.controller.generate_file?key=..., which redirects to a signed URL. - Public files: Stored in the public bucket/container with public read;
file_urlis the direct public URL. - Delete: On File
on_trash, if "Delete file from cloud" is enabled, the object is deleted from the correct bucket/container (parsed fromcontent_hash). - Migrate: Same logic; each file is uploaded to the private or public bucket/container by its
is_privateflag.
Object keys use a path like {folder_prefix}/{YYYY}/{MM}/{DD}/{doctype}/{random}_{filename} (or custom key if a hook is used).
- Ignore doctypes: In
site_config.jsonor environment, setignore_multi_cloud_storage_doctypeto a list of doctypes whose attachments should not be uploaded (e.g.["Data Import", "Prepared Report"]). "Prepared Report" is always ignored. - Custom key generator: In your app's
hooks.py, setmulti_cloud_storage_key_generator = ["your_app.utils.your_key_function"]. The function receivesfile_name,parent_doctype,parent_nameand should return the object key (string).
Pre-commit is used for formatting and linting:
cd apps/multi_cloud_storage
pre-commit installTools: ruff, eslint, prettier, pyupgrade.
CI (GitHub Actions): installs the app and runs tests on push to develop; runs Semgrep and pip-audit on pull requests.
MIT