Storage Configuration
All platform files (site assets, reconciliation files, etc.) are stored through the S3-compatible protocol. MinIO, Aliyun OSS, Tencent COS, Huawei OBS, AWS S3 and other S3-compatible object storage services are supported. Configuration entry: Admin panel → System Config → Platform Config → Storage.
How Upload Works
Files are uploaded via presigned direct upload, in three steps:
- The admin frontend asks the backend for a presigned upload URL;
- The browser uploads the file directly to the object storage (the file never passes through the backend server);
- The backend confirms the file exists and registers the file record.
Therefore one action is mandatory before anything else: configure CORS rules on the bucket, otherwise "Test Connection" passes but real uploads still fail. Configuration and troubleshooting: see Storage CORS Configuration and Upload Troubleshooting.
Parameters
| Parameter | Required | Description |
|---|---|---|
| Endpoint | Yes | API address of the storage service. Must include https:// and must NOT include the bucket name |
| Region | Depends | See "Do I need the region" below |
| Access Key / Secret Key | Yes | Credentials of the storage service |
| Path Style Access | Yes | Address style switch, see below |
| Public Bucket / Private Bucket | Yes | Bucket names; the same bucket can be used for both, see "Single bucket" |
| Public Base URL | Recommended | External address for public bucket files. Must start with http:// or https://; what to fill depends on the URL join mode, see below |
| URL Join Mode | Recommended | How public file URLs are composed: bucket-level domain (default) / with bucket, see below |
| Base Storage Path | No | Common prefix of object keys, e.g. daxpay |
| Upload presign expiration | No | Validity of upload URLs in minutes, default 10 |
| Download presign expiration | No | Validity of private file URLs in hours, default 72 |
Do I need the region
The region only participates in request signing — it does not decide which data center is accessed; that is entirely determined by the endpoint. So in most cases it can be left empty:
| Storage service | Required | Example |
|---|---|---|
| MinIO / self-hosted S3-compatible | Not required, leave empty | — |
| Aliyun OSS | Leave empty; fill it if a signature-region error occurs | cn-beijing |
| Huawei OBS | Recommended | cn-north-4 |
| Tencent COS | Required (validates the signing region) | ap-shanghai, ap-guangzhou |
| AWS S3 | Required | us-east-1, ap-northeast-1 |
| Qiniu Kodo | Required | cn-east-1 |
| Cloudflare R2 | Fixed value | auto |
When left empty, us-east-1 is used for signing. MinIO and other services that do not validate the signing region are not affected. Refer to each provider's S3-compatibility documentation for exact region codes.
Path Style Access
One bucket has two address styles:
- Path style (on):
https://endpoint/bucket/object— required for MinIO and most self-hosted storage - Virtual-hosted style (off):
https://bucket.endpoint-host/object— the native style of cloud providers
How to choose
Always enable it for self-hosted MinIO. Cloud providers usually support both; follow the recommended values in the examples below.
About Public Base URL and URL Join Mode
The final public file URL is decided by "Public Base URL + URL Join Mode". Two join modes:
- Bucket-level domain (default): URL =
domain/object-key. For domains bound directly to a bucket — R2 bucket-level custom domains, a CDN domain bound to a single bucket. Fill in the bucket-level domain. - With bucket: URL =
domain/bucket/object-key. For roots hosting multiple buckets — MinIO and the native domains of Aliyun OSS / Tencent COS. Fill in the root address; the bucket name is appended by the system.
The URL must start with http:// or https:// (validated by both the config page and the backend): an address without the protocol prefix is treated as a relative path by browsers during file-access redirects, making public files unreachable.
The config page previews the composed full URL in real time from the current form values, so you can verify the mode directly. When "Test Connection" gets a 404 on public access, it automatically retries with the other join mode and suggests switching if that one works.
Single bucket
The public and private bucket fields accept the same bucket name. In that case set the bucket ACL to public-read: public files are directly accessible and private files still work through presigned URLs.
The trade-off: "private" becomes nominal — anyone holding the full URL can read the file. For production, create two separate buckets so the permission semantics actually hold.
Examples
TIP
Endpoint addresses, region codes and other values in these examples are taken from each provider's official documentation. Providers may adjust their S3-compatible endpoint forms and configuration requirements over time — always follow the latest official docs when filling in the form.
Example 1: Self-hosted MinIO
| Parameter | Value |
|---|---|
| Endpoint | https://minio.example.com (the MinIO API port, not the console port) |
| Region | Leave empty |
| Path Style Access | On |
| Public Base URL | https://minio.example.com (or its public access domain) |
| URL Join Mode | With bucket (MinIO hosts multiple buckets under one root; the bucket name is appended by the system) |
The keys are the MinIO AccessKey / SecretKey (created via mc admin).
Example 2: Aliyun OSS (Beijing)
| Parameter | Value |
|---|---|
| Endpoint | https://s3.oss-cn-beijing.aliyuncs.com (the S3-compatible endpoint with the s3. prefix, see "S3 Compatibility Notes" below) |
| Region | Leave empty; fill cn-beijing if a signature-region error occurs |
| Path Style Access | On |
| Public / Private Bucket | e.g. daxpay-pub / daxpay-priv (or the same bucket name) |
| Public Base URL | https://oss-cn-beijing.aliyuncs.com (the native domain, without the s3. prefix) |
| URL Join Mode | With bucket (multiple buckets under the native root; if the bucket has its own custom domain, switch to bucket-level domain and fill that domain) |
The keys are the AccessKey of a RAM user. Minimal permissions: PutObject, GetObject, DeleteObject, ListObjects, GetBucketInfo on both buckets.
Note: even if the backend runs on Aliyun ECS, do not switch the endpoint to the internal address (ending with -internal) — uploads go from the user's browser directly, and browsers cannot reach internal addresses.
Example 3: Tencent COS (Shanghai)
| Parameter | Value |
|---|---|
| Endpoint | https://cos.ap-shanghai.myqcloud.com |
| Region | ap-shanghai (required) |
| Path Style Access | Off |
| Public Base URL | https://cos.ap-shanghai.myqcloud.com |
| URL Join Mode | With bucket (multiple buckets under the native root; the default CDN acceleration domain works the same way) |
The keys are the SecretId / SecretKey of a Tencent Cloud sub-account.
Example 4: Cloudflare R2 (bucket-level custom domain)
| Parameter | Value |
|---|---|
| Endpoint | https://<account-id>.r2.cloudflarestorage.com (the S3-compatible endpoint; the account ID is on the R2 overview page) |
| Region | auto |
| Path Style Access | On |
| Public Base URL | https://cdn.example.com (the custom domain bound to the bucket in the R2 console) |
| URL Join Mode | Bucket-level domain (default) — the R2 custom domain points directly at the bucket, the URL contains no bucket segment |
The keys are the R2 AccessKey / SecretKey (Object Storage → Manage API Tokens). Note that "bucket-level domain" exists exactly for the R2 shape: the custom domain is bound to the bucket and the address is simply domain/object-key; do not select "With bucket", otherwise the URL gains a bucket segment and always 404s. Without a custom domain, R2 also offers a public access domain of the form https://pub-<hash>.r2.dev (enable it in the bucket settings) — also fill it as a bucket-level domain.
S3 Compatibility Notes
Every provider has its own native storage API, but S3 is the de-facto standard and all mainstream providers offer a compatibility layer. The platform talks to every storage through the S3 protocol, so keep two distinct addresses apart:
- API endpoint (Endpoint): where the platform sends S3 requests (test connection, presigned uploads). On most providers the native address is itself the S3-compatible endpoint — just fill it in.
- File access domain (Public Base URL): the address browsers use to fetch files directly. It is independent of the API endpoint; use the provider's native domain or your CDN domain.
Why Aliyun OSS needs the s3. prefix
Aliyun OSS is the exception. In the early days it "incidentally" accepted S3 on its native address, and the behavioral gaps caused real problems — for example, the checksum and chunked transfer encoding that newer AWS SDK versions enable by default are rejected by the OSS compatibility layer. Aliyun later introduced a dedicated S3-compatible endpoint (https://s3.oss-<region>.aliyuncs.com); the official docs now require it and list the prefix-less native address as a typical misconfiguration.
Two pitfalls from practice are already handled by the platform, so normally you don't need to care:
- The SDK-default checksum and chunked transfer encoding are disabled on the upload path, so the OSS error no longer occurs.
- When Region is left empty, a default value is used for signing. If "Test Connection" reports
SignatureDoesNotMatch, fill the region code (e.g.cn-beijing) in the Region field.
After switching the endpoint to the s3. prefixed address, keep the native address (e.g. https://oss-cn-beijing.aliyuncs.com) as the Public Base URL — the former is for API requests from the platform, the latter is for browsers accessing files; they serve different purposes.
For comparison: Tencent COS addresses (cos.ap-xxx.myqcloud.com) are already S3-compatible endpoints — fill them in directly, there is no separate prefixed form.
Testing and Troubleshooting
After saving, click "Test Connection". The system runs a full loop on both buckets: bucket exists → write probe file → read → delete. A pass means read/write permissions are ready. The test uses the current form values (unsaved edits are probed immediately); saving first is not required.
Common errors:
| Symptom | Cause | Fix |
|---|---|---|
| Test passes, upload fails | Bucket has no CORS rules | See FAQ - Storage CORS Configuration and Upload Troubleshooting |
| Write failed, check upload permission | Key lacks write permission / bucket policy denies writes / bucket enforces KMS encryption without KMS permission | Search 探测写入失败 (probe write failed) in backend logs for the real status code; temporarily grant full permissions to narrow it down |
| File uploaded but image not displayed | URL Join Mode does not match the domain shape (bucket-level domain with "with bucket", or shared-root domain with "bucket-level"), or the public bucket is private | Switch the join mode to match the domain (Test Connection auto-corrects and suggests); set the public bucket to public-read |
| Public file 404 after a 302 | Public Base URL lacks the http(s):// protocol prefix; the redirect is parsed as a relative path by browsers | Add the protocol prefix to the URL (enforced by the config page; watch out for legacy stored configs) |
| Upload succeeds but the record stays pending | Key can write but cannot read | Add read permission (GetObject) |
"Test Connection" reports SignatureDoesNotMatch | The region used for signing does not match the region expected by the storage | Fill the region code in Region, e.g. cn-beijing |
WARNING
"A record exists in the file list" does not mean the upload succeeded — the record is created as soon as the upload URL is requested. Only a truly accessible file counts.