# Alibaba Cloud OSS

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

```elixir
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:

```elixir
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:

```elixir
endpoint: "https://files.example.com",
endpoint_mode: :bucket
```

A 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

```elixir
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

```elixir
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:

```elixir
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:

```elixir
{: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

- [OSS V4 request signing](https://www.alibabacloud.com/help/en/oss/developer-reference/recommend-to-use-signature-version-4)
- [OSS V4 presigned URLs](https://www.alibabacloud.com/help/en/oss/developer-reference/add-signatures-to-urls)
- [PutObject headers and limitations](https://help.aliyun.com/en/oss/developer-reference/putobject)
- [CopyObject](https://www.alibabacloud.com/help/en/oss/developer-reference/copyobject)

## 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:

```sh
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](cdn-aliyun.html#console-setup-and-live-test) for console setup,
environment variables and a command to run only the CDN test. Run both with:

```sh
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.
