• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rzane / file_store / 0e732ce3c9aaa3df7b299c4b403493f780b3c2e5

29 May 2024 02:43PM UTC coverage: 93.174% (-1.2%) from 94.403%
0e732ce3c9aaa3df7b299c4b403493f780b3c2e5

push

github

web-flow
Merge pull request #32 from warmwaffles/update-for-latest-elixir

Update for latest elixir

1 of 1 new or added line in 1 file covered. (100.0%)

8 existing lines in 5 files now uncovered.

273 of 293 relevant lines covered (93.17%)

138.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.53
/lib/file_store/adapters/s3.ex
1
if Code.ensure_loaded?(ExAws.S3) do
54✔
2
  defmodule FileStore.Adapters.S3 do
3
    @moduledoc """
4
    Stores files using Amazon S3.
5

6
    ### Dependencies
7

8
    To use this adapter, you'll need to install and configure `ExAws.S3`.
9

10
    ### Configuration
11

12
      * `bucket` - The name of your S3 bucket. This option
13
        is required.
14

15
      * `ex_aws` - A keyword list of options that can be
16
        used to override the default configuration for `ExAws`.
17

18
    ### Example
19

20
        iex> store = FileStore.Adapters.S3.new(
21
        ...>   bucket: "mybucket"
22
        ...> )
23
        %FileStore.Adapters.S3{...}
24

25
        iex> FileStore.write(store, "foo", "hello world")
26
        :ok
27

28
        iex> FileStore.read(store, "foo")
29
        {:ok, "hello world"}
30

31
    """
32

33
    @enforce_keys [:bucket]
34
    defstruct [:bucket, ex_aws: []]
35

36
    @doc "Create a new S3 adapter"
37
    @spec new(keyword) :: FileStore.t()
38
    def new(opts) do
39
      if is_nil(opts[:bucket]) do
297✔
40
        raise "missing configuration: :bucket"
×
41
      end
42

43
      struct(__MODULE__, opts)
297✔
44
    end
45

46
    defimpl FileStore do
47
      alias FileStore.Stat
48
      alias FileStore.Utils
49

50
      @query_params [
51
        content_type: "response-content-type",
52
        disposition: "response-content-disposition"
53
      ]
54

55
      def get_public_url(store, key, opts) do
56
        config = get_config(store)
27✔
57
        host = store.bucket <> "." <> config[:host]
27✔
58
        query = Utils.encode_query(get_url_query(opts))
27✔
59
        scheme = String.trim_trailing(config[:scheme], "://")
27✔
60

61
        uri = %URI{
27✔
62
          scheme: scheme,
63
          host: host,
64
          port: config[:port],
65
          path: "/" <> key,
66
          query: query
67
        }
68

69
        URI.to_string(uri)
27✔
70
      end
71

72
      def get_signed_url(store, key, opts) do
73
        config = get_config(store)
36✔
74

75
        opts =
36✔
76
          opts
77
          |> Keyword.take([:expires_in])
78
          |> Keyword.put(:virtual_host, true)
79
          |> Keyword.put(:query_params, get_url_query(opts))
80

81
        ExAws.S3.presigned_url(config, :get, store.bucket, key, opts)
36✔
82
      end
83

84
      def stat(store, key) do
85
        store.bucket
135✔
86
        |> ExAws.S3.head_object(key)
87
        |> request(store)
88
        |> case do
135✔
89
          {:ok, %{headers: headers}} ->
90
            headers = Enum.into(headers, %{})
54✔
91
            etag = headers |> Map.get("ETag") |> unwrap_etag()
54✔
92
            size = headers |> Map.get("Content-Length") |> to_integer()
54✔
93
            type = headers |> Map.get("Content-Type")
54✔
94
            {:ok, %Stat{key: key, etag: etag, size: size, type: type}}
95

96
          {:error, reason} ->
81✔
97
            {:error, reason}
98
        end
99
      end
100

101
      def delete(store, key) do
102
        store.bucket
45✔
103
        |> ExAws.S3.delete_object(key)
104
        |> acknowledge(store)
45✔
105
      end
106

107
      def delete_all(store, opts) do
27✔
108
        store.bucket
27✔
109
        |> ExAws.S3.delete_all_objects(list!(store, opts))
110
        |> acknowledge(store)
27✔
111
      rescue
112
        error -> {:error, error}
×
113
      end
114

115
      def write(store, key, content, opts \\ []) do
116
        opts =
234✔
117
          opts
118
          |> Keyword.take([:content_type, :disposition])
119
          |> Utils.rename_key(:disposition, :content_disposition)
120

121
        store.bucket
234✔
122
        |> ExAws.S3.put_object(key, content, opts)
123
        |> acknowledge(store)
234✔
124
      end
125

126
      def read(store, key) do
127
        store.bucket
90✔
128
        |> ExAws.S3.get_object(key)
129
        |> request(store)
130
        |> case do
90✔
131
          {:ok, %{body: body}} -> {:ok, body}
81✔
132
          {:error, reason} -> {:error, reason}
9✔
133
        end
134
      end
135

136
      def upload(store, source, key) do
36✔
137
        source
138
        |> ExAws.S3.Upload.stream_file()
139
        |> ExAws.S3.upload(store.bucket, key)
36✔
140
        |> acknowledge(store)
36✔
141
      rescue
142
        error in [File.Error] -> {:error, error.reason}
9✔
143
      end
144

145
      def download(store, key, destination) do
146
        store.bucket
9✔
147
        |> ExAws.S3.download_file(key, destination)
148
        |> acknowledge(store)
9✔
149
      end
150

151
      def list!(store, opts) do
152
        opts = Keyword.take(opts, [:prefix])
54✔
153

154
        store.bucket
54✔
155
        |> ExAws.S3.list_objects(opts)
156
        |> ExAws.stream!(store.ex_aws)
54✔
157
        |> Stream.map(fn file -> file.key end)
54✔
158
      end
159

160
      def copy(store, src, dest) do
161
        store.bucket
54✔
162
        |> ExAws.S3.put_object_copy(dest, store.bucket, src)
54✔
163
        |> acknowledge(store)
54✔
164
      end
165

166
      def rename(store, src, dest) do
167
        with :ok <- copy(store, src, dest) do
27✔
168
          delete(store, src)
18✔
169
        end
170
      end
171

172
      defp request(op, store) do
173
        ExAws.request(op, store.ex_aws)
630✔
174
      end
175

176
      defp acknowledge(op, store) do
177
        case request(op, store) do
405✔
178
          {:ok, _} -> :ok
378✔
179
          {:error, reason} -> {:error, reason}
18✔
180
        end
181
      end
182

183
      defp get_url_query(opts) do
184
        for {key, query_param} <- @query_params,
63✔
185
            value = Keyword.get(opts, key),
186
            into: [],
187
            do: {query_param, value}
188
      end
189

190
      defp get_config(store), do: ExAws.Config.new(:s3, store.ex_aws)
63✔
191

UNCOV
192
      defp unwrap_etag(nil), do: nil
×
193
      defp unwrap_etag(etag), do: String.trim(etag, ~s("))
54✔
194

UNCOV
195
      defp to_integer(nil), do: nil
×
196
      defp to_integer(value) when is_integer(value), do: value
×
197
      defp to_integer(value) when is_binary(value), do: String.to_integer(value)
54✔
198
    end
199
  end
200
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc