ExOss supports file transfer operations via ExOss.StorageTask structs
and the run/1 macro function (or ExOss.Runner.run/2).
StorageTask
An ExOss.StorageTask struct has source, target, and opts fields.
Location Types
| Type | Format | Description |
|---|---|---|
| Cloud | {:cloud, bucket, key} | Object in cloud storage |
| Remote URL | {:remote, url} | Download from a URL |
| Local file | {:file, path} | Upload from filesystem |
| Content | {:content, binary} | In-memory binary |
Operations
Upload from Local File
MyApp.Storage.run(%ExOss.StorageTask{
source: {:file, "/path/to/photo.jpg"},
target: {:cloud, "bucket", "photos/photo.jpg"}
})
# => {:ok, "photos/photo.jpg"}Local files use streaming multipart uploads, even for small files. S3 uploads
without raw headers use ExAws.S3.upload; S3 uploads with opts[:headers] and
native OSS uploads use ExOss's multipart coordinator so object headers
are set before signing initiation. The coordinator accepts chunk_size (at least
5 MiB), max_concurrency (default 4 for S3, 1 for OSS), and per-part timeout
(default 30000 milliseconds or :infinity). It aborts on part failures/timeouts.
S3 credentials are resolved for each request on this path, including each part. sweet_xml parses the
service's XML responses to read the initial UploadId and the completion
result; it does not parse your file's contents or handle HTTP requests.
Req handles HTTP transport, and ExOss automatically includes sweet_xml
as a required dependency—no separate installation is needed.
Content and remote URL uploads use a regular PUT rather than multipart upload, so they do not rely on this multipart response parsing.
Upload from Content
MyApp.Storage.run(%ExOss.StorageTask{
source: {:content, "Hello, World!"},
target: {:cloud, "bucket", "hello.txt"}
})
# => {:ok, "hello.txt"}Upload from Remote URL
MyApp.Storage.run(%ExOss.StorageTask{
source: {:remote, "https://example.com/photo.jpg"},
target: {:cloud, "bucket", "photos/photo.jpg"}
})
# => {:ok, "photos/photo.jpg"}The remote content is downloaded via Req.get/2 and then uploaded to
the configured storage provider. The response's Content-Type is used unless
the caller supplies a content type or an explicit header.
Cloud-to-Cloud Copy
MyApp.Storage.run(%ExOss.StorageTask{
source: {:cloud, "source-bucket", "file.jpg"},
target: {:cloud, "target-bucket", "copies/file.jpg"}
})
# => {:ok, "copies/file.jpg"}This uses server-side CopyObject (S3 or OSS). OSS copying requires both buckets to be in the same region.
Delete Immediately
MyApp.Storage.run(%ExOss.StorageTask{
source: {:cloud, "bucket", "file.jpg"},
target: nil
})
# => {:ok, nil}Delete After N Days (Delayed Delete)
MyApp.Storage.run(%ExOss.StorageTask{
source: {:cloud, "bucket", "file.jpg"},
target: nil,
opts: [days: 30]
})
# => {:ok, nil}This adds a delete_after_days tag to the object. A lifecycle rule must
be configured on the bucket to actually expire tagged objects.
Content-Type Detection
For {:file, path} and {:content, binary} sources, ExOss automatically
detects the content type using MIME.from_path/1. To override:
MyApp.Storage.run(%ExOss.StorageTask{
source: {:content, binary_data},
target: {:cloud, "bucket", "file.dat"},
opts: [content_type: "application/x-custom"]
})Error Handling
All operations return {:ok, result} or {:error, reason}:
case MyApp.Storage.run(task) do
{:ok, res_key} -> IO.puts("Uploaded to #{res_key}")
{:error, reason} -> IO.puts("Failed: #{reason}")
endUsing with Explicit Client
For multi-client scenarios:
client = ExOss.Client.new(provider: :aws, ...)
ExOss.Runner.run(client, %ExOss.StorageTask{...})Raw Headers and Presigned Uploads
Uploads accept opts[:headers] as a map or list of string pairs:
opts: [
headers: %{"Content-Type" => "image/png", "Cache-Control" => "max-age=3600"}
]The outer options may also be a map. Both opts[:content_type] and
opts["content_type"] place their value in headers["content-type"]. This applies
to all storage providers (:aws, :qiniu, :custom, :aliyun) for server
uploads and presigned PUT credentials. It is an option-to-header mapping, not a
change to the MIME value or a provider-specific conversion. For example:
opts = %{"content_type" => "image/png", "headers" => %{"cache-control" => "max-age=3600"}}
# Effective headers: %{"content-type" => "image/png", "cache-control" => "max-age=3600"}Only "content_type" and "headers" have string-key aliases; other outer option
keys remain atoms. Supplying both a string and atom alias is rejected as ambiguous.
Existing keyword options continue to work.
Names are case-insensitive. Explicit headers override opts[:content_type],
which remains supported, and automatic inference. Duplicate names differing
only by case are rejected. ExOss reserves authentication, host, date, length and
transport framing headers. Provider-specific headers are not translated; check
the provider's documentation for supported headers on each operation.
For presigned uploads, pass the same options as the new final argument:
credential = MyApp.Storage.upload_credential("bucket", "photo.png", 3600,
content_type: "image/png",
headers: %{"cache-control" => "max-age=3600"}
)
Req.put!(credential.presigned_url, body: png_bytes, headers: credential.headers)Existing calls without options still work. No Content-Type is automatically inferred for a presigned URL; explicitly supplied headers are included in the storage signature. Changing their values can invalidate the signature. Browser uploads need a CORS rule permitting PUT and the supplied headers.
See Aliyun OSS for native OSS details and Aliyun CDN for CDN upload fallback and URL signing.
The default Req transport preserves signed fields even if application/global
Req options specify auth, params, body encoders, or replacement headers.
Use opts[:headers] for business headers, not transport configuration. Explicit
custom ExAws HTTP clients are still honored and must preserve signed requests.