lib/aliyun_oss/object.ex

defmodule Aliyun.Oss.Object do
  @moduledoc """
  Object operations - basic operations.

  Other operations can be found in:

  - `Aliyun.Oss.Object`
  - `Aliyun.Oss.Object.MultipartUpload`
  - `Aliyun.Oss.Object.ACL`
  - `Aliyun.Oss.Object.Symlink`
  - `Aliyun.Oss.Object.Tagging`

  """

  alias Aliyun.Oss.{Config, Service, Sign}
  alias Aliyun.Oss.Client.{Request, Response}

  @doc """
  HeadObject - gets the metadata of an object.

  The content of the object is not returned.

  ## Options

  - `:headers` - Defaults to `%{}`

  ## Examples

      iex> Aliyun.Oss.Object.head_object(config, "some-bucket", "some-object")
      {:ok,
      %Aliyun.Oss.Client.Response{
        data: "",
        headers: %{
          "accept-ranges" => ["bytes"],
          "connection" => ["keep-alive"],
          "content-length" => ["19"],
          "content-md5" => ["wpqajJtzJSpf8lOY/W4Hqg=="],
          "content-type" => ["text/plain"],
          "date" => ["Fri, 04 Jul 2025 03:10:24 GMT"],
          "etag" => ["\"D4100000000000000000000000000000\""],
          "last-modified" => ["Fri, 15 Jan 2021 09:16:13 GMT"],
          "server" => ["AliyunOSS"],
          "x-oss-hash-crc64ecma" => ["587015626014620604"],
          "x-oss-object-type" => ["Normal"],
          "x-oss-request-id" => ["680000000000000000000CE1"],
          "x-oss-server-time" => ["24"],
          "x-oss-storage-class" => ["Standard"],
          "x-oss-version-id" => ["null"]
        }
      }}

      iex> Aliyun.Oss.Object.head_object(config, "some-bucket", "unknown-object")
      {:error, %Aliyun.Oss.Client.Error{status: 404, code: nil, message: "", details: nil}}

  """
  @spec head_object(Config.t(), String.t(), String.t(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def head_object(%Config{} = config, bucket, object, options \\ []) do
    Service.head(config, bucket, object, options)
  end

  @doc """
  GetObjectMeta - gets the metadata of an object, including ETag, Size, and LastModified.

  The content of the object is not returned.

  ## Examples

      iex> Aliyun.Oss.Object.get_object_meta(config, "some-bucket", "some-object")
      {:ok,
      %Aliyun.Oss.Client.Response{
        data: "",
        headers: %{
          "connection" => ["keep-alive"],
          "content-length" => ["19"],
          "date" => ["Fri, 04 Jul 2025 03:16:45 GMT"],
          "etag" => ["\"D4100000000000000000000000000000\""],
          "last-modified" => ["Fri, 15 Jan 2021 09:16:13 GMT"],
          "server" => ["AliyunOSS"],
          "x-oss-hash-crc64ecma" => ["587015626014620604"],
          "x-oss-request-id" => ["680000000000000000000F52"],
          "x-oss-server-time" => ["39"],
          "x-oss-version-id" => ["null"]
        }
      }}

  """
  @spec get_object_meta(Config.t(), String.t(), String.t()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def get_object_meta(%Config{} = config, bucket, object) do
    Service.head(config, bucket, object, query_params: %{"objectMeta" => nil})
  end

  @doc """
  GetObject - gets an object.

  ## Options

  - `:query_params` - Defaults to `%{}`
  - `:headers` - Defaults to `%{}`

  ## Examples

      iex> Aliyun.Oss.Object.get_object(config, "some-bucket", "some-object")
      {:ok, %Aliyun.Oss.Client.Response{
          data: <<208, 207, 17, 224, 161, 177, 26, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 62, 0, 3, 0, 254, 255, 9, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18,
            0, 0, 0, ...>>,
          headers: %{
            "accept-ranges" => ["bytes"],
            "connection" => ["keep-alive"],
            "content-md5" => ["wpqajJtzJSpf8lOY/W4Hqg=="],
            "content-type" => ["text/plain"],
            "date" => ["Fri, 04 Jul 2025 05:45:44 GMT"],
            "etag" => ["\"C29000000000000000000000000000AA\""],
            "last-modified" => ["Fri, 15 Jan 2021 09:16:13 GMT"],
            "server" => ["AliyunOSS"],
            "x-oss-hash-crc64ecma" => ["587015626014620604"],
            "x-oss-object-type" => ["Normal"],
            "x-oss-request-id" => ["6860000000000000000000A3"],
            "x-oss-server-time" => ["41"],
            "x-oss-storage-class" => ["Standard"],
            "x-oss-version-id" => ["null"]
          }
        }
      }
      iex> Aliyun.Oss.Object.get_object(config, "some-bucket", "some-object", headers: %{}, query_params: %{"response-cache-control" => "no-cache"})
      {:ok, %Aliyun.Oss.Client.Response{
          data: <<208, 207, 17, 224, 161, 177, 26, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 62, 0, 3, 0, 254, 255, 9, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18,
            0, 0, 0, ...>>,
          headers: %{
            "accept-ranges" => ["bytes"],
            "cache-control" => ["no-cache],
            "connection" => ["keep-alive"],
            "content-md5" => ["wpqajJtzJSpf8lOY/W4Hqg=="],
            "content-type" => ["text/plain"],
            "date" => ["Fri, 04 Jul 2025 05:45:44 GMT"],
            "etag" => ["\"C29000000000000000000000000000AA\""],
            "last-modified" => ["Fri, 15 Jan 2021 09:16:13 GMT"],
            "server" => ["AliyunOSS"],
            "x-oss-hash-crc64ecma" => ["587015626014620604"],
            "x-oss-object-type" => ["Normal"],
            "x-oss-request-id" => ["6860000000000000000000A3"],
            "x-oss-server-time" => ["41"],
            "x-oss-storage-class" => ["Standard"],
            "x-oss-version-id" => ["null"]
          }
        }
      }
      iex> Aliyun.Oss.Object.get_object(config, "some-bucket", "some-object", headers: %{"range" => "bytes=0-2"}, query_params: %{"response-cache-control" => "no-cache"})
      {:ok, %Aliyun.Oss.Client.Response{
          data: "abc",
          headers: %{
            "accept-ranges" => ["bytes"],
            "cache-control" => ["no-cache],
            "connection" => ["keep-alive"],
            "content-md5" => ["wpqajJtzJSpf8lOY/W4Hqg=="],
            "content-type" => ["text/plain"],
            "date" => ["Fri, 04 Jul 2025 05:45:44 GMT"],
            "etag" => ["\"C29000000000000000000000000000AA\""],
            "last-modified" => ["Fri, 15 Jan 2021 09:16:13 GMT"],
            "server" => ["AliyunOSS"],
            "x-oss-hash-crc64ecma" => ["587015626014620604"],
            "x-oss-object-type" => ["Normal"],
            "x-oss-request-id" => ["6860000000000000000000A3"],
            "x-oss-server-time" => ["41"],
            "x-oss-storage-class" => ["Standard"],
            "x-oss-version-id" => ["null"]
          }
        }
      }

  """
  @spec get_object(Config.t(), String.t(), String.t(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def get_object(%Config{} = config, bucket, object, options \\ []) do
    Service.get(config, bucket, object, options)
  end

  @doc """
  SelectObject - executes SQL statements to perform operations on an object and obtains the execution results.

  ## Options

  - `:format` - specifies the request syntax:
      - available values: `:json`, `:csv`
      - default value: `:csv`

  ## Examples

      iex> select_request = ~S[
        <SelectRequest>
            <Expression>c2VsZWN0ICogZnJvbSBvc3NvYmplY3Q=</Expression>
            <InputSerialization>
            <JSON>
                <Type>DOCUMENT</Type>
            </JSON>
            </InputSerialization>
            <OutputSerialization>
            <JSON>
                <RecordDelimiter>LA==</RecordDelimiter>
            </JSON>
            </OutputSerialization>
        </SelectRequest>
      ]
      iex> Aliyun.Oss.Object.select_object(config, "some-bucket", "file.json", select_request, format: :json)
      {:ok, %Aliyun.Oss.Client.Response{
          data: %{"contacts" => [...]},
          headers: %{
            "accept-ranges" => ["bytes"],
            "connection" => ["keep-alive"],
            ...
          }
        }
      }
      iex> select_request = ~S[
        <SelectRequest>
          <Expression>c2VsZWN0ICogZnJvbSBvc3NvYmplY3Q=</Expression>
        </SelectRequest>
      ]
      iex> Aliyun.Oss.Object.select_object(config, "some-bucket", "file.csv", select_request, format: :csv)
      {:ok, %Aliyun.Oss.Client.Response{
          data: "...",
          headers: %{
            "accept-ranges" => ["bytes"],
            "connection" => ["keep-alive"],
            ...
          }
        }
      }

  """
  @spec select_object(Config.t(), String.t(), String.t(), String.t(), keyword) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def select_object(config, bucket, object, select_request, options \\ []) do
    x_oss_process =
      case Keyword.get(options, :format, :csv) do
        :csv -> "csv/select"
        :json -> "json/select"
      end

    post_object(config, bucket, object, select_request,
      query_params: %{"x-oss-process" => x_oss_process}
    )
  end

  @doc """
  CreateSelectObjectMeta - 获取目标文件总的行数,总的列数(对于 CSV 文件),以及 Splits 个数。

  如果该信息不存在,则会扫描整个文件,分析并记录下 CSV 文件的上述信息。重复调用则会保存上述信息而不必重新扫描整个文件。

  TODO: fix the doc

  ## Options

  - `:format` - specifies the request syntax:
      - available values: `:json`, `:csv`
      - default value: `:csv`

  ## Examples

      iex> select_request = ~S[
        <?xml version="1.0"?>
        <JsonMetaRequest>
            <InputSerialization>
                <JSON>
                    <Type>LINES</Type>
                </JSON>
            </InputSerialization>
            <OverwriteIfExisting>false</OverwriteIfExisting>
        </JsonMetaRequest>
      ]
      iex> Aliyun.Oss.Object.select_object_meta(config, "some-bucket", "some-object", select_request, format: :json)
      {:ok, %Aliyun.Oss.Client.Response{
          data: <<208, 207, 17, 224, 161, 177, 26, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 62, 0, 3, 0, 254, 255, 9, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18,
            0, 0, 0, ...>>,
          headers: %{
            "accept-ranges" => ["bytes"],
            "connection" => ["keep-alive"],
            ...
          }
        }
      }

  """
  @spec select_object_meta(Config.t(), String.t(), String.t(), String.t(), keyword) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def select_object_meta(config, bucket, object, select_request, options \\ []) do
    x_oss_process =
      case Keyword.get(options, :format, :csv) do
        :csv -> "csv/meta"
        :json -> "json/meta"
      end

    post_object(config, bucket, object, select_request,
      query_params: %{"x-oss-process" => x_oss_process}
    )
  end

  @doc """
  Creates a signed URL for an object.

  ## Options

  - `:query_params` - Defaults to `%{}`
  - `:headers` - Defaults to `%{}`
  - `:method` - HTTP method to use for the signed URL, defaults to `:get`, accept values: `:get`, `:put`, `:post`, `:delete`, `:head`
  - `:expires` - number of seconds until the signed URL expires, defaults to 3600 seconds (1 hour)

  ## Examples

      iex> expires = 3600
      iex> Aliyun.Oss.Object.signed_url(config, "some-bucket", "some-object", expires: expires, method: :get)
      "https://some-bucket.oss-cn-hangzhou.aliyuncs.com/oss-api.pdf?x-oss-credential=LT**************%2F20250701%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20250701T070913Z&x-oss-expires=3600&x-oss-signature=2f64d***********************************************************&x-oss-signature-version=OSS4-HMAC-SHA256"
      iex> Aliyun.Oss.Object.signed_url(config, "some-bucket", "some-object", expires: expires, method: :put, headers: %{"Content-Type" => "text/plain"})
      "https://some-bucket.oss-cn-hangzhou.aliyuncs.com/oss-api.pdf?x-oss-credential=LT**************%2F20250701%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20250701T070944Z&x-oss-expires=3600&x-oss-signature=2a6e************************************************************&x-oss-signature-version=OSS4-HMAC-SHA256"

  """
  @spec signed_url(Config.t(), String.t(), String.t(), keyword()) ::
          String.t()
  def signed_url(config, bucket, object, options \\ []) do
    Request.build!(
      config,
      Keyword.get(options, :method, :get),
      bucket,
      object,
      Keyword.get(options, :headers, %{}),
      Keyword.get(options, :query_params, %{}),
      ""
    )
    |> Request.sign_url(Keyword.get(options, :expires, 3600))
    |> Request.to_url()
  end

  @doc """
  Creates a signed URL for accessing an object.

  ## Examples

      iex> expires = 3600
      iex> Aliyun.Oss.Object.object_url(config, "some-bucket", "some-object", expires)
      "http://some-bucket.oss-cn-hangzhou.aliyuncs.com/oss-api.pdf?OSSAccessKeyId=nz2pc56s936**9l&Expires=1141889120&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D"

  """
  @spec object_url(Config.t(), String.t(), String.t(), integer()) :: String.t()
  def object_url(config, bucket, object, expires) do
    signed_url(config, bucket, object, expires: expires, method: :get)
  end

  @doc """
  PutObject - uploads objects.

  ## Options

  - `:query_params` - Defaults to `%{}`
  - `:headers` - Defaults to `%{}`

  ## Examples

      iex> Aliyun.Oss.Object.put_object(config, "some-bucket", "some-object", "CONTENT")
      {:ok, %Aliyun.Oss.Client.Response{
          data: "",
          headers: %{
            "connection" => ["keep-alive"],
            "content-length" => ["0"],
            "content-md5" => ["plz3uTkI****************"],
            "date" => ["Fri, 11 Jul 2025 02:30:40 GMT"],
            "etag" => ["\"A65CF7B93908B3A*****************\""],
            "server" => ["AliyunOSS"],
            "x-oss-hash-crc64ecma" => ["162113910***********"],
            "x-oss-request-id" => ["687077508A8E************"],
            "x-oss-server-time" => ["107"]
          }
        }
      }

  """
  @spec put_object(Config.t(), String.t(), String.t(), String.t(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def put_object(config, bucket, object, body, options \\ []) do
    Service.put(config, bucket, object, body, options)
  end

  @doc """
  CopyObject - copies objects within a bucket or between buckets in the same region.

  ## Options

  - `:headers` - Defaults to `%{}`

  ## Examples

      iex> Aliyun.Oss.Object.copy_object(config, {"source-bucket", "source-object"}, {"target-bucket", "target-object"})
      {:ok, %Aliyun.Oss.Client.Response{
          data: %{
            "CopyObjectResult" => %{
              "ETag" => "\"D2D5****************************\"",
              "LastModified" => "2025-02-27T09:21:13.000Z"
            }
          },
          headers: %{
            "connection" => ["keep-alive"],
            "content-type" => ["application/xml"],
            "date" => ["Fri, 11 Jul 2025 02:35:21 GMT"],
            "etag" => ["\"A65CF7B9************************\""],
            "server" => ["AliyunOSS"],
            "x-oss-copied-size" => ["20"],
            "x-oss-hash-crc64ecma" => ["16211***************"],
            "x-oss-ia-retrieve-flow-type" => ["0"],
            "x-oss-request-id" => ["68707869D***************"],
            "x-oss-server-time" => ["71"],
            "x-oss-version-id" => ["null"]
          }
        }
      }

  """
  @spec copy_object(Config.t(), {String.t(), String.t()}, {String.t(), String.t()}, keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def copy_object(
        config,
        {source_bucket, source_object},
        {target_bucket, target_object},
        options \\ []
      ) do
    headers = %{"x-oss-copy-source" => "/#{source_bucket}/#{source_object}"}
    options = Keyword.update(options, :headers, headers, &Map.merge(&1, headers))
    put_object(config, target_bucket, target_object, "", options)
  end

  @doc """
  AppendObject - uploads an object by appending the content of the object to an existing object.

  ## Examples

      iex> Aliyun.Oss.Object.append_object(config, "some-bucket", "some-object", "CONTENT", 0)
      {:ok, %Aliyun.Oss.Client.Response{
          data: "",
          headers: %{
            "connection" => ["keep-alive"],
            "content-length" => ["0"],
            "date" => ["Fri, 11 Jul 2025 02:46:19 GMT"],
            "etag" => ["\"AE9A6C899***********************\""],
            "server" => ["AliyunOSS"],
            "x-oss-hash-crc64ecma" => ["178569**************"],
            "x-oss-next-append-position" => ["7"],
            "x-oss-request-id" => ["68707AFB****************"],
            "x-oss-server-time" => ["15"]
          }
        }
      }

  """
  @spec append_object(Config.t(), String.t(), String.t(), String.t(), integer(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def append_object(config, bucket, object, content, position, options \\ []) do
    query_params = %{"append" => nil, "position" => position}
    options = Keyword.update(options, :query_params, query_params, &Map.merge(&1, query_params))
    post_object(config, bucket, object, content, options)
  end

  @doc """
  RestoreObject - restores an Archive object or a Cold Archive object.

  ## Examples

      iex> Aliyun.Oss.Object.restore_object(config, "some-bucket", "some-object")
      {:ok, %Aliyun.Oss.Client.Response{
          data: "",
          headers: %{
            "connection" => ["keep-alive"],
            "content-length" => ["0"],
            "date" => ["Fri, 11 Jul 2025 03:13:57 GMT"],
            "server" => ["AliyunOSS"],
            "x-oss-request-id" => ["68708175A***************"],
            "x-oss-server-time" => ["6"]
          }
        }
      }

  """
  @spec restore_object(Config.t(), String.t(), String.t()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def restore_object(config, bucket, object) do
    post_object(config, bucket, object, "", query_params: %{"restore" => nil})
  end

  @doc """
  CleanRestoredObject

  ## Examples

      iex> Aliyun.Oss.Object.clean_restored_object(config, "some-bucket", "some-object")
      {:ok, %Aliyun.Oss.Client.Response{
          data: "",
          headers: %{
            "connection" => ["keep-alive"],
            "content-length" => ["0"],
            "date" => ["Fri, 11 Jul 2025 03:13:57 GMT"],
            "server" => ["AliyunOSS"],
            "x-oss-request-id" => ["68708175A***************"],
            "x-oss-server-time" => ["6"]
          }
        }
      }

  """
  @spec clean_restored_object(Config.t(), String.t(), String.t()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def clean_restored_object(config, bucket, object) do
    post_object(config, bucket, object, "", query_params: %{"cleanRestoredObject" => nil})
  end

  @doc """
  DeleteObject - deletes an object.

  ## Options

  - `:query_params` - Defaults to `%{}`
  - `:headers` - Defaults to `%{}`

  ## Examples

      iex> Aliyun.Oss.Object.delete_object(config, "some-bucket", "some-object")
      {:ok, %Aliyun.Oss.Client.Response{
          data: "",
          headers: %{
            "connection" => ["keep-alive"],
            "content-length" => ["0"],
            ...
          }
        }
      }

  """
  @spec delete_object(Config.t(), String.t(), String.t(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def delete_object(config, bucket, object, options \\ []) do
    Service.delete(config, bucket, object, options)
  end

  @doc """
  DeleteMultipleObjects - deletes multiple objects from a bucket.

  ## Options

  - `:encoding_type` - Accept value: `:url`
  - `:quiet` - Set `true` to enable the quiet mode, default is `false`. This option will be ignored if you pass the raw xml body as the 3rd argument.

  ## Examples

      iex> Aliyun.Oss.Object.delete_multiple_objects(config, "some-bucket", ["object1", "object2"])
      {:ok, %Aliyun.Oss.Client.Response{
          data: %{
            "DeleteResult" => %{
              "Deleted" => [
                %{"Key" => "object1"},
                %{"Key" => "object2"}
              ]
            }
          },
          headers: %{
            "connection" => ["keep-alive"],
            "content-type" => ["application/xml"],
            ...
          }
        }
      }
      iex> xml_body = ~S[
      <?xml version="1.0" encoding="UTF-8"?>
      <Delete>
        <Quiet>false</Quiet>
        <Object>
          <Key>multipart.data</Key>
          <VersionId>CAEQNRiBgIDyz.6C0BYiIGQ2NWEwNmVhNTA3ZTQ3MzM5ODliYjM1ZTdjYjA4****</VersionId>
        </Object>
      </Delete>
      ]
      iex> Aliyun.Oss.Object.delete_multiple_objects(config, "some-bucket", xml_body)
      {:ok, %Aliyun.Oss.Client.Response{
          data: %{
            "DeleteResult" => %{
              "Deleted" => [
                %{
                  "Key" => "multipart.data",
                  "VersionId" => "CAEQNRiBgIDyz.6C0BYiIGQ2NWEwNmVhNTA3ZTQ3MzM5ODliYjM1ZTdjYjA4****"
                },
              ]
            }
          },
          headers: %{
            "connection" => ["keep-alive"],
            "content-type" => ["application/xml"],
            ...
          }
        }
      }
  """
  @body_tmpl """
  <?xml version="1.0" encoding="UTF-8"?>
  <Delete>
    <Quiet><%= quiet %></Quiet>
    <%= for object <- objects do %>
      <Object><Key><%= object %></Key></Object>
    <% end %>
  </Delete>
  """
  @spec delete_multiple_objects(Config.t(), String.t(), [String.t()] | String.t(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def delete_multiple_objects(config, bucket, objects_or_xml_body, options \\ [])

  def delete_multiple_objects(config, bucket, objects, options) when is_list(objects) do
    quiet = Keyword.get(options, :quiet, false)
    body = EEx.eval_string(@body_tmpl, quiet: quiet, objects: objects)
    delete_multiple_objects(config, bucket, body, options)
  end

  def delete_multiple_objects(config, bucket, body, options) when is_binary(body) do
    headers =
      case Keyword.get(options, :encoding_type) do
        :url -> %{"encoding-type" => "url"}
        _ -> %{}
      end
      |> Map.put("content-length", String.length(body))
      |> Map.put("content-md5", :md5 |> :crypto.hash(body) |> Base.encode64())

    Service.post(config, bucket, nil, body, headers: headers, query_params: %{"delete" => nil})
  end

  @doc """
  Signs Post Policy, and returns the encoded Post Policy and its signature.

  ## Examples

      iex> policy = %{
      ...>  "conditions" => [
      ...>    ["content-length-range", 0, 10485760],
      ...>    %{"bucket" => "ahaha"},
      ...>    %{"A" => "a"},
      ...>    %{"key" => "ABC"}
      ...>  ],
      ...>  "expiration" => "2013-12-01T12:00:00Z"
      ...>}
      iex> Aliyun.Oss.Object.sign_post_policy(config, policy)
      %{
        policy: "eyJjb25kaXRpb25zIjpbWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MF0seyJidWNrZXQiOiJhaGFoYSJ9LHsiQSI6ImEifSx7ImtleSI6IkFCQyJ9XSwiZXhwaXJhdGlvbiI6IjIwMTMtMTItMDFUMTI6MDA6MDBaIn0=",
        signature: "d9ea2e7088a5a7189d2d1a84aa872a00b7078877ffbd4a24b8897c23f16bc1db"
      }

  """
  @spec sign_post_policy(Config.t(), map()) :: %{
          policy: String.t(),
          signature: String.t()
        }
  def sign_post_policy(config, %{} = policy) do
    today = Date.utc_today() |> Date.to_iso8601(:basic)
    encoded_policy = policy |> Jason.encode!() |> Base.encode64()

    %{
      policy: encoded_policy,
      signature: Sign.sign(encoded_policy, Sign.get_signing_key(config, today))
    }
  end

  @spec post_object(Config.t(), String.t(), String.t(), String.t(), keyword()) ::
          {:error, Exception.t()} | {:ok, Response.t()}
  def post_object(config, bucket, object, body, options) do
    Service.post(config, bucket, object, body, options)
  end
end