Use provider: :aliyun for the native OSS API with V4 authentication.
This does not require OSS's optional S3-compatible authentication mode.
Existing AWS, Qiniu S3 and custom S3 providers continue to use ExAws.
Configuration
config :my_app, MyApp.Storage,
provider: :aliyun,
access_key_id: System.fetch_env!("OSS_ACCESS_KEY_ID"),
secret_access_key: System.fetch_env!("OSS_ACCESS_KEY_SECRET"),
region: "cn-hangzhou"Region is required even with a custom endpoint. Explicit us-east-1 is supported
for US (Virginia). Use the region ID without the
oss- prefix. The default endpoint is
https://{bucket}.oss-{region}.aliyuncs.com; only virtual bucket addressing is
supported. Long-term credentials and STS credentials are supported. For STS,
set security_token as well as the temporary access key and secret. ExOss does
not obtain or refresh STS credentials. Credentials and tokens are redacted from
client inspection.
Endpoints
By default, endpoint_mode: :service prefixes the requested bucket to the host:
endpoint: "https://oss-cn-hangzhou-internal.aliyuncs.com"
# bucket.oss-cn-hangzhou-internal.aliyuncs.com
endpoint: "https://oss-accelerate.aliyuncs.com"
# bucket.oss-accelerate.aliyuncs.com (requires acceleration enabled on OSS)An endpoint already prefixed with the requested bucket is not prefixed twice. For a bucket endpoint or an OSS-bound CNAME, explicitly use:
endpoint: "https://files.example.com",
endpoint_mode: :bucketA bucket endpoint must refer to the bucket supplied to the operation. Endpoints must be HTTP(S) origins without paths, query strings, fragments or user info. Internal endpoints require network access from the application to OSS.
Upload headers
MyApp.Storage.run(%ExOss.StorageTask{
source: {:content, png_bytes},
target: {:cloud, "my-bucket", "images/avatar.png"},
opts: [
headers: %{
"Content-Type" => "image/png",
"Cache-Control" => "max-age=3600",
"x-oss-meta-source" => "avatar"
}
]
})headers accepts a map or list of string name/value pairs. Names are normalized
to lowercase. Values are passed through without converting S3 headers to OSS
headers. Consult the operation's official API reference for allowed headers and
values. ExOss does not maintain a complete list of unsupported provider headers.
opts[:content_type] and its string-key alias opts["content_type"] both populate
headers["content-type"]. This same mapping is used by every storage provider. Precedence:
explicit header, then legacy content_type, then automatic inference. Local
file/content uploads infer from the destination key; remote uploads prefer the
source response's Content-Type and otherwise infer from the key for OSS.
Case-insensitive duplicate header names are rejected. ExOss manages Authorization, Host, Date, Content-Length, Transfer-Encoding, Connection, and OSS/AWS signing dates, tokens and payload hashes; callers cannot override these. CR/LF and invalid header names are rejected before signing.
Presigned PUT and GET
credential = MyApp.Storage.upload_credential(
"my-bucket", "images/avatar.png", 3600,
headers: %{"content-type" => "image/png", "x-oss-meta-source" => "avatar"}
)
Req.put!(credential.presigned_url,
body: png_bytes,
headers: credential.headers
)The original calls with two or three arguments still work. The optional fourth
argument accepts headers and the legacy content_type. No Content-Type is
inferred for presigned uploads; only explicitly supplied values are bound to the
signature. UploadCredential.headers defaults to %{} and contains the headers
the caller must send. OSS V4 signs Content-Type, Content-MD5, all supplied
x-oss-* headers and all additional supplied headers. The URL also binds Host.
Changing or omitting signed headers invalidates the signature. Setting a
Content-Type does not validate the file's bytes.
Downloads accept headers and query_params in the existing options argument:
url = MyApp.Storage.authorize_download_url("my-bucket", "images/avatar.png", 60,
headers: %{"range" => "bytes=0-1023"},
query_params: [{"response-content-type", "image/png"}]
)
Req.get!(url, headers: %{"range" => "bytes=0-1023"})The maximum URL lifetime is 604800 seconds for long-term credentials or 43200 seconds for STS credentials, and at least one second. STS credential expiration may shorten the actual lifetime. Browser direct uploads also require an OSS CORS rule allowing the origin, PUT, and the headers sent by the browser.
Operations and errors
Content and remote uploads use PutObject. Local files use streaming multipart
upload with 5 MiB chunks by default (opts[:chunk_size] can increase this).
Object headers are sent on multipart initiation; they are not copied blindly to
part or completion requests. Parts run sequentially by default; max_concurrency can increase parallelism and
timeout controls each part (default 30000 milliseconds). Empty files send
one empty part. A failed upload attempts AbortMultipartUpload while preserving
the original error; process termination can still leave incomplete uploads, so
configure a bucket rule to remove them.
Copy uses OSS CopyObject with x-oss-copy-source and requires source and target
buckets in the same region. The API's size and storage-class restrictions apply.
Delete and metadata use DeleteObject and HeadObject. Delayed deletion writes
delete_after_days; configure the bucket lifecycle rule separately. This is not
a deletion scheduler.
Successful run calls retain {:ok, key} / {:ok, nil}. OSS HTTP errors and
embedded XML errors return:
{:error, {:oss_error, %{
status_code: 403,
code: "AccessDenied",
message: "...",
request_id: "..."
}}}Transport errors return {:error, reason}. Metadata retains the existing
{:error, :no_metadata} failure contract. OSS transport options such as
receive_timeout or a Req test adapter can be configured under
config :ex_oss, :req_opts, .... Signed method, URL, body and headers cannot be
overridden through these options. Req auth/query/body transformation options are
also ignored after merging global defaults, to protect signed requests. OSS requests do not automatically redirect or
retry; callers can decide whether a failed operation is safe to retry.
References
Optional live tests
Default tests use local adapters and deterministic signing vectors. Live tests are excluded by default. To verify PutObject, HEAD, presigned PUT with headers, and presigned GET against a dedicated test bucket:
export OSS_TEST_REGION='cn-hangzhou'
export OSS_TEST_BUCKET='your-test-bucket'
export OSS_TEST_ACCESS_KEY_ID='your-access-key-id'
export OSS_TEST_ACCESS_KEY_SECRET='your-access-key-secret'
# Required with STS temporary credentials:
# export OSS_TEST_SECURITY_TOKEN='your-sts-token'
# Optional service endpoint override:
# export OSS_TEST_ENDPOINT='https://oss-cn-hangzhou.aliyuncs.com'
mix test test/ex_oss/aliyun_integration_test.exs --include aliyun_integration --only oss_live --trace
The CDN test additionally requires OSS_TEST_CDN_ENDPOINT
and OSS_TEST_CDN_AUTH_KEY; optional OSS_TEST_CDN_AUTH_TYPE is a, b, c
or f, and OSS_TEST_CDN_TTL defaults to 1800. The CDN test uses the default
path format for C and default decimal/query parameter names for F.
See Aliyun CDN for console setup,
environment variables and a command to run only the CDN test. Run both with:
mix test test/ex_oss/aliyun_integration_test.exs --include aliyun_integration
To open a signed download URL manually, set OSS_TEST_KEEP_OBJECTS=1. This
prints the URL (valid for 300 seconds from generation) and the object key, and
disables automatic cleanup. Delete the printed object from the test bucket
after checking it. Signed URLs grant temporary access; avoid sharing the output.
These tests create uniquely named objects below ex-oss-tests/ with a filename
containing Chinese characters, a space, + and %. Unless retention is enabled,
they attempt to delete them on exit. Use a test bucket with matching CDN origin configuration.
The signing key needs object upload, read and delete permissions. Running the
live tests performs real cloud requests; unit test success alone does not
confirm a particular account's permissions, CORS or CDN configuration.
The live tests do not cover multipart uploads, STS expiration or browser CORS.