defmodule COS.Object do @moduledoc """ 对象(Object)是对象存储的基本单元,可理解为任何格式类型的数据,例如图片、文档和音视频文件等。 [腾讯云文档](https://cloud.tencent.com/document/product/436/13324) """ alias COS.{HTTPClient, Utils} @doc ~S""" 简单上传对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/7749) 可以上传一个对象至指定存储桶中,该操作需要请求者对存储桶有 WRITE 权限。 最大支持上传不超过5GB的对象,5GB以上对象请使用分块上传(Todo)或高级接口(Todo)上传。 ## 示例 iex> COS.Object.put("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "content") {:ok, %Tesla.Env{ body: "", headers: [ {"server", "tencent-cos"}, {"data", "Tue, 29 Mar 2022 16:39:58 GMT"}, ... ], ... }} # 设置传对象的内容类型 COS.Object.put( "https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.json", "{\"key\":\"value\"}", headers: [{"content-type", "application-json"}] ) # 设置 HTTP 响应的超时时间 COS.Object.put( "https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "content", tesla_opts: [adapter: [recv_timeout: 30_000]] ) # 创建“文件夹” COS.Object.put("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example/", "") """ @spec put( host :: binary(), key :: binary(), content :: binary(), opts :: [headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts()] ) :: Tesla.Env.t() def put(host, key, content, opts \\ []) do headers = opts[:headers] || [] HTTPClient.request( method: :put, url: host <> "/" <> key, body: content, headers: headers, opts: opts[:tesla_opts] ) end @doc """ 上传本地文件 ## 示例 iex> COS.Object.put_from_file("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "./example.txt") {:ok, %Tesla.Env{ body: "", headers: [ {"server", "tencent-cos"}, {"data", "Tue, 29 Mar 2022 16:39:58 GMT"}, ... ], ... }} """ @spec put_from_file( host :: binary(), key :: binary(), path :: binary(), opts :: [headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts()] ) :: Tesla.Env.t() def put_from_file(host, key, path, opts \\ []) do put(host, key, File.read!(path), opts) end @doc """ 追加上传对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/7741) ## 示例 iex> COS.Object.append("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", 0, "hello") {:ok, %Tesla.Env{ body: "", headers: [ {"x-cos-content-sha1", "5d41402abc4b2a76b9719d911017c592"}, {"x-cos-next-append-position", "5"}, ... ], ... }} iex> COS.Object.append("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", 5, " world") {:ok, %Tesla.Env{ body: "", headers: [ {"x-cos-content-sha1", "b7913aa15c43be7d534b4eec6e99e8a0"}, {"x-cos-next-append-position", "11"}, ... ], ... }} """ @spec append( host :: binary(), key :: binary(), position :: pos_integer(), content :: binary(), opts :: [headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts()] ) :: Tesla.Env.t() def append(host, key, position, content, opts \\ []) do headers = opts[:headers] || [] HTTPClient.request( method: :post, url: host <> "/" <> key, query: %{append: "", position: position}, headers: headers, body: content, opts: opts[:tesla_opts] ) end @doc """ 复制对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/10881) 创建一个已存在 COS 的对象的副本,即将一个对象从源路径(对象键)复制到目标路径(对象键)。 建议对象大小为1M到5G,超过5G的对象请使用分块上传(Todo)。 ## 示例 iex> COS.Object.copy( "https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "destination.txt", "other-bucket-1250000000.cos.ap-shanghai.myqcloud.com/source.txt" ) {:ok, %Tesla.Env{ body: %{ "crc64" => "1791993320000000000", "e_tag" => "\"6ae33dfd6c0c9fa03b77f75xxxxxxxxx\"", "last_modified" => "2022-03-29T17:20:24Z" }, headers: [ {"server", "tencent-cos"}, {"data", "Tue, 29 Mar 2022 16:39:58 GMT"}, ... ], ... }} """ @spec copy( host :: binary(), key :: binary(), source :: binary(), opts :: [headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts()] ) :: Tesla.Env.t() def copy(host, key, source, opts \\ []) do headers = opts[:headers] || [] HTTPClient.request( method: :put, url: host <> "/" <> key, headers: [{"x-cos-copy-source", source} | headers], opts: opts[:tesla_opts], result_key: "copy_object_result" ) end @doc """ 查询对象元数据 - [腾讯云文档](https://cloud.tencent.com/document/product/436/7745) """ @spec head( host :: binary(), key :: binary(), opts :: [ query: %{optional(:version_id) => binary()} | nil, headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts() ] ) :: Tesla.Env.t() def head(host, key, opts \\ []) do version_id = get_in(opts, [:query, :version_id]) headers = opts[:headers] || [] HTTPClient.request( method: :head, url: host <> "/" <> key, query: %{versionId: version_id}, headers: headers, opts: opts[:tesla_opts] ) end @doc """ 检查对象是否存在 ## 示例 iex> COS.Object.exists?("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt") true iex> COS.Object.exists?("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example/") true iex> COS.Object.exists?("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "missing.txt") false """ @spec exists?( host :: binary(), key :: binary(), opts :: [ query: %{optional(:version_id) => binary()} | nil, headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts() ] ) :: boolean() def exists?(host, key, opts \\ []) do with {:ok, _response} <- head(host, key, opts) do true else _ -> false end end @doc """ 删除单个对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/7743) ## 示例 COS.Object.delete("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt") # 删除“文件夹”,如果“文件夹”内有对象,则不会删除。 COS.Object.delete("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example/") """ @spec delete( host :: binary(), key :: binary(), opts :: [ query: %{optional(:version_id) => binary()} | nil, tesla_opts: Tesla.Env.opts() ] ) :: Tesla.Env.t() def delete(host, key, opts \\ []) do version_id = get_in(opts, [:query, :version_id]) HTTPClient.request( method: :delete, url: host <> "/" <> key, query: %{versionId: version_id}, opts: opts[:tesla_opts] ) end @doc """ 删除多个对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/8289) ## 示例 iex> COS.Object.multi_delete("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", %{ object: [%{key: "example.txt"}, %{key: "error.txt"}] }) {:ok, %Tesla.Env{ body: %{ "deleted" => [%{ "key" => "example.txt", ... }], "error" => [%{ "key" => "error.txt", ... }] }, ... }} # Quiet 模式,在响应中仅包含删除失败的对象信息和错误信息 iex> COS.Object.multi_delete("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", %{ object: [%{key: "example.txt"}, %{key: "error.txt"}], quiet: true }) {:ok, %Tesla.Env{ body: %{ "deleted" => [], "error" => [%{ "key" => "error.txt", ... }] }, ... }} """ @spec multi_delete( host :: binary(), body :: %{ :object => [%{:key => binary(), optional(:version_id) => binary()}], optional(:quiet) => boolean() }, opts :: [tesla_opts: Tesla.Env.opts()] ) :: Tesla.Env.t() def multi_delete(host, body, opts \\ []) do body = {:delete, body} with {:ok, response} <- HTTPClient.request( method: :post, url: host <> "/?delete", body: body, opts: opts[:tesla_opts] ) do body = Enum.reduce(["deleted", "error"], %{}, fn key, acc -> value = response.body |> get_in(["delete_result", key]) |> List.wrap() Map.put(acc, key, value) end) {:ok, %{response | body: body}} end end @doc """ 获取请求预签名 URL 说明: - 建议用户使用临时密钥生成预签名,通过临时授权的方式进一步提高预签名上传、下载等请求的安全性。 申请临时密钥时,请遵循[最小权限指引原则](https://cloud.tencent.com/document/product/436/38618),防止泄漏目标存储桶或对象之外的资源。 - 如果您一定要使用永久密钥来生成预签名,建议永久密钥的权限范围仅限于上传或下载操作,以规避风险。 - 获取对象的 URL 并下载对象参数,可在获取的 URL 后拼接参数 `response-content-disposition=attachment`。 ## 示例 iex> COS.Object.get_presigned_url("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt") "https://bucket-1250000000.cos.ap-beijing.myqcloud.com/example.txt?q-ak=AKIDb************************DmNIJ&q-header-list=&q-key-time=1649011703%3B1649012603&q-sign-algorithm=sha1&q-sign-time=1649011703%3B1649012603&q-signature=a9c13b2b1e09c5ce46df0242ac37b9cb828c0d6b&q-url-param-list=" """ @spec get_presigned_url( host :: binary(), key :: binary(), opts :: [ method: Tesla.Env.method(), query: Tesla.Env.query(), headers: Tesla.Env.headers(), expired_at: DateTime.t(), expire_in: COS.Utils.expire_in() ] ) :: binary() def get_presigned_url(host, key, opts \\ []) do method = opts[:method] || :get query = opts[:query] || %{} query = method |> COS.Auth.get("/" <> key, query: query, headers: opts[:headers], expire_in: opts[:expire_in], expired_at: opts[:expired_at] ) |> Map.new() |> Map.merge(query) |> URI.encode_query() encoded_key = key |> Utils.url_encode() |> String.replace("%2F", "/") %{URI.parse(host) | path: "/" <> encoded_key, query: query} |> URI.to_string() end @doc """ 下载对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/7753) ## 示例 iex> COS.Object.put("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "content") {:ok, ...} iex> COS.Object.get("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "content") {:ok, %Tesla.Env{ body: "content", headers: [ {"content-type", "application/octet-stream"}, {"content-length", "7"}, ... ], ... }} # 指定 Range 请求头部下载部分内容 iex> COS.Object.get("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "content", headers: [ {"range", "bytes=0-2"} ]) {:ok, %Tesla.Env{ body: "con", headers: [ {"content-type", "application/octet-stream"}, {"content-length", "3"}, {"accept-ranges", "bytes"}, {"content-range", "bytes 0-0/3"}, ... ], ... }} """ @spec get( host :: binary(), key :: binary(), opts :: [ query: %{optional(:version_id) => binary()} | nil, headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts() ] ) :: Tesla.Env.t() def get(host, key, opts \\ []) do version_id = get_in(opts, [:query, :version_id]) headers = opts[:headers] || [] HTTPClient.request( method: :get, url: host <> "/" <> key, query: %{versionId: version_id}, headers: headers, opts: opts[:tesla_opts] ) end @doc """ 下载对象到本地文件 ## 示例 iex> COS.Object.get_to_file("https://bucket-1250000000.cos.ap-beijing.myqcloud.com", "example.txt", "./example.txt") {:ok, %Tesla.Env{ body: "content", headers: [ {"content-type", "application/octet-stream"}, {"content-length", "7"}, ... ], ... }} """ @spec get_to_file( host :: binary(), key :: binary(), path :: binary(), opts :: [ query: %{optional(:version_id) => binary()} | nil, headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts() ] ) :: Tesla.Env.t() def get_to_file(host, key, path, opts \\ []) do with {:ok, response} <- get(host, key, opts), :ok <- File.write(path, response.body) do {:ok, response} end end @doc """ 恢复归档对象 - [腾讯云文档](https://cloud.tencent.com/document/product/436/12633) """ @spec restore( host :: binary(), key :: binary(), body :: %{days: pos_integer(), c_a_s_job_parameters: %{tier: binary()}}, opts :: [headers: Tesla.Env.headers(), tesla_opts: Tesla.Env.opts()] ) :: Tesla.Env.t() def restore(host, key, body, opts \\ []) do body = {:restore_request, body} version_id = get_in(opts, [:query, :version_id]) headers = opts[:headers] || [] HTTPClient.request( method: :post, url: host <> "/" <> key, query: %{restore: "", versionId: version_id}, headers: headers, body: body, opts: opts[:tesla_opts] ) end end