leifcr-refile 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/refile.js +125 -0
  3. data/config/locales/en.yml +10 -0
  4. data/config/routes.rb +5 -0
  5. data/lib/refile.rb +510 -0
  6. data/lib/refile/app.rb +186 -0
  7. data/lib/refile/attacher.rb +190 -0
  8. data/lib/refile/attachment.rb +108 -0
  9. data/lib/refile/attachment/active_record.rb +133 -0
  10. data/lib/refile/attachment_definition.rb +83 -0
  11. data/lib/refile/backend/file_system.rb +120 -0
  12. data/lib/refile/backend/s3.rb +1 -0
  13. data/lib/refile/backend_macros.rb +45 -0
  14. data/lib/refile/custom_logger.rb +48 -0
  15. data/lib/refile/file.rb +102 -0
  16. data/lib/refile/file_double.rb +13 -0
  17. data/lib/refile/image_processing.rb +1 -0
  18. data/lib/refile/rails.rb +54 -0
  19. data/lib/refile/rails/attachment_helper.rb +121 -0
  20. data/lib/refile/random_hasher.rb +11 -0
  21. data/lib/refile/signature.rb +36 -0
  22. data/lib/refile/simple_form.rb +17 -0
  23. data/lib/refile/type.rb +28 -0
  24. data/lib/refile/version.rb +3 -0
  25. data/spec/refile/active_record_helper.rb +35 -0
  26. data/spec/refile/app_spec.rb +424 -0
  27. data/spec/refile/attachment/active_record_spec.rb +568 -0
  28. data/spec/refile/attachment_helper_spec.rb +78 -0
  29. data/spec/refile/attachment_spec.rb +589 -0
  30. data/spec/refile/backend/file_system_spec.rb +5 -0
  31. data/spec/refile/backend_examples.rb +228 -0
  32. data/spec/refile/backend_macros_spec.rb +83 -0
  33. data/spec/refile/custom_logger_spec.rb +21 -0
  34. data/spec/refile/features/direct_upload_spec.rb +63 -0
  35. data/spec/refile/features/multiple_upload_spec.rb +122 -0
  36. data/spec/refile/features/normal_upload_spec.rb +144 -0
  37. data/spec/refile/features/presigned_upload_spec.rb +31 -0
  38. data/spec/refile/features/simple_form_spec.rb +8 -0
  39. data/spec/refile/fixtures/hello.txt +1 -0
  40. data/spec/refile/fixtures/image.jpg +0 -0
  41. data/spec/refile/fixtures/large.txt +44 -0
  42. data/spec/refile/fixtures/monkey.txt +1 -0
  43. data/spec/refile/fixtures/world.txt +1 -0
  44. data/spec/refile/spec_helper.rb +72 -0
  45. data/spec/refile_spec.rb +355 -0
  46. metadata +143 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f781b9f6d4404dc29d2b92347798a84e7208a8ad
4
+ data.tar.gz: d3d68577da41b921cfa349aafc142bb8d5d5550e
5
+ SHA512:
6
+ metadata.gz: d605f5943011f2b8258b335b045f949f58aabf4eb66a4f1e2f3001387d682d3c60f0580543e22b8d426695b959def093858a1cb904a2915b98277c90f0d80748
7
+ data.tar.gz: 55e9254dc55bfccf60caa620bf1afe3257b4f630e9d459c826bc1ce88e0fecf137fa721bf13fcddc53da8a1223781648d0dd8dcdfd5b2359c5f8c2e3c3749b2e
@@ -0,0 +1,125 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ function isSuccess(xhr) {
5
+ return (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304
6
+ }
7
+
8
+ function formData(as, file, fields) {
9
+ var data = new FormData();
10
+
11
+ if(fields) {
12
+ Object.keys(fields).forEach(function(key) {
13
+ data.append(key, fields[key]);
14
+ });
15
+ }
16
+
17
+ data.append(as, file);
18
+
19
+ return data;
20
+ }
21
+
22
+ if(!document.addEventListener) { return; } // IE8
23
+
24
+ document.addEventListener("change", function(changeEvent) {
25
+ var input = changeEvent.target;
26
+ if(input.tagName === "INPUT" && input.type === "file" && input.getAttribute("data-direct")) {
27
+ if(!input.files) { return; } // IE9, bail out if file API is not supported.
28
+
29
+ var reference = input.getAttribute("data-reference");
30
+ var metadataField = document.querySelector("input[type=hidden][data-reference='" + reference + "']");
31
+
32
+ var url = input.getAttribute("data-url");
33
+ var fields = JSON.parse(input.getAttribute("data-fields") || "null");
34
+
35
+ var requests = [].map.call(input.files, function(file, index) {
36
+ function dispatchEvent(element, name, progress) {
37
+ var ev = document.createEvent('CustomEvent');
38
+ ev.initCustomEvent(name, true, false, { xhr: xhr, file: file, index: index, progress: progress });
39
+ element.dispatchEvent(ev);
40
+ }
41
+
42
+ var xhr = new XMLHttpRequest();
43
+
44
+ xhr.file = file;
45
+
46
+ xhr.addEventListener("load", function() {
47
+ xhr.complete = true;
48
+ if(requests.every(function(xhr) { return xhr.complete })) {
49
+ finalizeUpload();
50
+ }
51
+ if(isSuccess(xhr)) {
52
+ dispatchEvent(input, "upload:success");
53
+ } else {
54
+ dispatchEvent(input, "upload:failure");
55
+ }
56
+ dispatchEvent(input, "upload:complete");
57
+ });
58
+
59
+ xhr.upload.addEventListener("progress", function(progressEvent) {
60
+ dispatchEvent(input, "upload:progress", progressEvent);
61
+ });
62
+
63
+ if(input.getAttribute("data-presigned")) {
64
+ dispatchEvent(input, "presign:start");
65
+ var presignXhr = new XMLHttpRequest();
66
+ var presignUrl = url + "?t=" + Date.now() + "." + index;
67
+ presignXhr.addEventListener("load", function() {
68
+ dispatchEvent(input, "presign:complete");
69
+ if(isSuccess(presignXhr)) {
70
+ dispatchEvent(input, "presign:success");
71
+ var data = JSON.parse(presignXhr.responseText)
72
+ xhr.id = data.id;
73
+ xhr.open("POST", data.url, true);
74
+ xhr.send(formData(data.as, file, data.fields));
75
+ dispatchEvent(input, "upload:start");
76
+ } else {
77
+ dispatchEvent(input, "presign:failure");
78
+ xhr.complete = true;
79
+ };
80
+ });
81
+ presignXhr.open("GET", presignUrl, true);
82
+ presignXhr.send();
83
+ } else {
84
+ xhr.open("POST", url, true);
85
+ xhr.send(formData(input.getAttribute("data-as"), file, fields));
86
+ dispatchEvent(input, "upload:start");
87
+ }
88
+
89
+ return xhr;
90
+ });
91
+
92
+ if(requests.length) {
93
+ input.classList.add("uploading");
94
+ }
95
+
96
+ var finalizeUpload = function() {
97
+ input.classList.remove("uploading");
98
+
99
+ if(requests.every(isSuccess)) {
100
+ var dataObj = requests.map(function(xhr) {
101
+ var json, data = { filename: xhr.file.name, content_type: xhr.file.type, size: xhr.file.size };
102
+
103
+ try {
104
+ json = JSON.parse(xhr.responseText);
105
+ } catch(e) {
106
+ json = {};
107
+ }
108
+
109
+ var id = xhr.id || json.id;
110
+ var url = xhr.url || json.url;
111
+
112
+ if (id) data.id = id;
113
+ if (url) data.url = url;
114
+
115
+ return data;
116
+ });
117
+ if(!input.multiple) dataObj = dataObj[0];
118
+ if(metadataField) metadataField.value = JSON.stringify(dataObj);
119
+
120
+ input.removeAttribute("name");
121
+ }
122
+ }
123
+ }
124
+ });
125
+ })();
@@ -0,0 +1,10 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ too_large: "is too large"
6
+ download_failed: "could not be downloaded"
7
+ invalid_content_type: "You are not allowed to upload %{content} file format. Allowed types: %{permitted}."
8
+ invalid_extension: "You are not allowed to upload %{extension} file extension. Allowed types: %{permitted}."
9
+ refile:
10
+ empty_param: "an empty"
@@ -0,0 +1,5 @@
1
+ if Refile.automount
2
+ Rails.application.routes.draw do
3
+ mount Refile.app, at: Refile.mount_point, as: :refile_app
4
+ end
5
+ end
@@ -0,0 +1,510 @@
1
+ require "uri"
2
+ require "fileutils"
3
+ require "tempfile"
4
+ require "rest_client"
5
+ require "logger"
6
+ require "mime/types"
7
+
8
+ module Refile
9
+ # @api private
10
+ class Invalid < StandardError; end
11
+
12
+ # @api private
13
+ class InvalidID < Invalid; end
14
+
15
+ # @api private
16
+ class InvalidMaxSize < Invalid; end
17
+
18
+ # @api private
19
+ class InvalidFile < Invalid; end
20
+
21
+ # @api private
22
+ class Confirm < StandardError
23
+ def message
24
+ "are you sure? this will remove all files in the backend, call as \
25
+ `clear!(:confirm)` if you're sure you want to do this"
26
+ end
27
+ end
28
+
29
+ class << self
30
+ # A shortcut to the instance of the Rack application. This should be
31
+ # set when the application is initialized. `refile/rails` sets this
32
+ # value.
33
+ #
34
+ # @return [Refile::App, nil]
35
+ attr_accessor :app
36
+
37
+ # The host name of a CDN distribution that the Rack application can be
38
+ # reached at. If not set, Refile will use an absolute URL without hostname.
39
+ # It is strongly recommended to run Refile behind a CDN and to set this to
40
+ # the hostname of the CDN distribution.
41
+ #
42
+ # The `cdn_host` setting is used when retrieving files, but not when
43
+ # uploading new files, since uploads should normally not go through the
44
+ # CDN.
45
+ #
46
+ # A protocol relative URL is recommended for this value.
47
+ #
48
+ # @return [String, nil]
49
+ attr_accessor :cdn_host
50
+
51
+ # The host name that the Rack application can be reached at. If not set,
52
+ # Refile will use an absolute URL without hostname. You should only change
53
+ # this setting if you are running the Refile app on a different domain
54
+ # than your main application.
55
+ #
56
+ # If you are simply running the Refile app behind a CDN you'll want to
57
+ # change {Refile.cdn_host} instead.
58
+ #
59
+ # The difference between {Refile.app_host} and {Refile.cdn_host} is that the
60
+ # latter only affects URLs generated by {Refile.file_url} and the
61
+ # {Refile::AttachmentHelper#attachment_url} and
62
+ # {Refile::AttachmentHelper#attachment_image_tag} helpers, whereas the
63
+ # former also affects {Refile.upload_url}, {Refile.presign_url} and the
64
+ # {Refile::AttachmentHelper#attachment_field} helper.
65
+ #
66
+ # @return [String, nil]
67
+ attr_accessor :app_host
68
+
69
+ # @deprecated use {Refile.cdn_host} instead
70
+ def host
71
+ warn "Refile.host is deprecated, please use Refile.cdn_host instead"
72
+ cdn_host
73
+ end
74
+
75
+ # @deprecated use {Refile.cdn_host} instead
76
+ def host=(host)
77
+ warn "Refile.host is deprecated, please use Refile.cdn_host instead"
78
+ self.cdn_host = host
79
+ end
80
+
81
+ # A list of names which identify backends in the global backend registry.
82
+ # The Rack application allows POST requests to only the backends specified
83
+ # in this config option. This defaults to `["cache"]`, only allowing direct
84
+ # uploads to the cache backend.
85
+ #
86
+ # @return [Array[String], :all]
87
+ attr_accessor :allow_uploads_to
88
+
89
+ # A list of names which identify backends in the global backend registry.
90
+ # The Rack application allows GET requests to only the backends specified
91
+ # in this config option. This defaults to `:all`, allowing files from all
92
+ # backends to be downloaded.
93
+ #
94
+ # @return [Array[String], :all]
95
+ attr_accessor :allow_downloads_from
96
+
97
+ # Logger that should be used by rack application
98
+ #
99
+ # @return [Logger]
100
+ attr_accessor :logger
101
+
102
+ # Value for Access-Control-Allow-Origin header
103
+ #
104
+ # @return [String]
105
+ attr_accessor :allow_origin
106
+
107
+ # Value for Cache-Control: max-age=<value in seconds> header
108
+ #
109
+ # @return [Integer]
110
+ attr_accessor :content_max_age
111
+
112
+ # Where should the rack application be mounted? The default is 'attachments'.
113
+ #
114
+ # @return [String]
115
+ attr_accessor :mount_point
116
+
117
+ # Should the rack application be automounted in a Rails app?
118
+ #
119
+ # If set to false then Refile.app should be mounted in the Rails application
120
+ # routes.rb with the options `at: Refile.mount_point, as: :refile_app`
121
+ #
122
+ # The default is true.
123
+ #
124
+ # @return [Boolean]
125
+ attr_accessor :automount
126
+
127
+ # Value for generating signed attachment urls to protect from DoS
128
+ #
129
+ # @return [String]
130
+ attr_accessor :secret_key
131
+
132
+ # A global registry of backends.
133
+ #
134
+ # @return [Hash{String => Backend}]
135
+ def backends
136
+ @backends ||= {}
137
+ end
138
+
139
+ # A global registry of processors. These will be used by the Rack
140
+ # application to manipulate files prior to serving them up to the user,
141
+ # based on options sent trough the URL. This can be used for example to
142
+ # resize images or to convert files to another file format.
143
+ #
144
+ # @return [Hash{String => Proc}]
145
+ def processors
146
+ @processors ||= {}
147
+ end
148
+
149
+ # A global registry of types. Currently, types are simply aliases for a set
150
+ # of content types, but their functionality may expand in the future.
151
+ #
152
+ # @return [Hash{Symbol => Refile::Type}]
153
+ def types
154
+ @types ||= {}
155
+ end
156
+
157
+ # Adds a processor. The processor must respond to `call`, both receiving
158
+ # and returning an IO-like object. Alternatively a block can be given to
159
+ # this method which also receives and returns an IO-like object.
160
+ #
161
+ # An IO-like object is recommended to be an instance of the `IO` class or
162
+ # one of its subclasses, like `File` or a `StringIO`, or a `Refile::File`.
163
+ # It can also be any other object which responds to `size`, `read`, `eof`?
164
+ # `rewind` and `close` and mimics the behaviour of IO objects for these
165
+ # methods.
166
+ #
167
+ # @example With processor class
168
+ # class Reverse
169
+ # def call(file)
170
+ # StringIO.new(file.read.reverse)
171
+ # en
172
+ # end
173
+ # Refile.processor(:reverse, Reverse)
174
+ #
175
+ # @example With block
176
+ # Refile.processor(:reverse) do |file|
177
+ # StringIO.new(file.read.reverse)
178
+ # end
179
+ #
180
+ # @param [#to_s] name The name of the processor
181
+ # @param [Proc, nil] processor The processor, must respond to `call` and.
182
+ # @yield [Refile::File] The file to modify
183
+ # @yieldreturn [IO] An IO-like object representing the processed file
184
+ # @return [void]
185
+ def processor(name, processor = nil, &block)
186
+ processor ||= block
187
+ processors[name.to_s] = processor
188
+ end
189
+
190
+ # A shortcut to retrieving the backend named "store" from the global
191
+ # registry.
192
+ #
193
+ # @return [Backend]
194
+ def store
195
+ backends["store"]
196
+ end
197
+
198
+ # A shortcut to setting the backend named "store" in the global registry.
199
+ #
200
+ # @param [Backend] backend
201
+ def store=(backend)
202
+ backends["store"] = backend
203
+ end
204
+
205
+ # A shortcut to retrieving the backend named "cache" from the global
206
+ # registry.
207
+ #
208
+ # @return [Backend]
209
+ def cache
210
+ backends["cache"]
211
+ end
212
+
213
+ # A shortcut to setting the backend named "cache" in the global registry.
214
+ #
215
+ # @param [Backend] backend
216
+ def cache=(backend)
217
+ backends["cache"] = backend
218
+ end
219
+
220
+ # Yield the Refile module as a convenience for configuring multiple
221
+ # config options at once.
222
+ #
223
+ # @yield Refile
224
+ def configure
225
+ yield self
226
+ end
227
+
228
+ # Extract the filename from an uploadable object. If the filename cannot be
229
+ # determined, this method will return `nil`.
230
+ #
231
+ # @param [IO] uploadable The uploadable object to extract the filename from
232
+ # @return [String, nil] The extracted filename
233
+ def extract_filename(uploadable)
234
+ path = if uploadable.respond_to?(:original_filename)
235
+ uploadable.original_filename
236
+ elsif uploadable.respond_to?(:path)
237
+ uploadable.path
238
+ end
239
+ ::File.basename(path) if path
240
+ end
241
+
242
+ # Extract the content type from an uploadable object. If the content type
243
+ # cannot be determined, this method will return `nil`.
244
+ #
245
+ # @param [IO] uploadable The uploadable object to extract the content type from
246
+ # @return [String, nil] The extracted content type
247
+ def extract_content_type(uploadable)
248
+ if uploadable.respond_to?(:content_type)
249
+ uploadable.content_type
250
+ else
251
+ filename = extract_filename(uploadable)
252
+ if filename
253
+ content_type = MIME::Types.of(filename).first
254
+ content_type.to_s if content_type
255
+ end
256
+ end
257
+ end
258
+
259
+ # Generates a URL to the Refile application.
260
+ #
261
+ # The host defaults to {Refile.app_host}. You can also override the host via
262
+ # the `host` option. Normally the Refile app will not be mounted at the
263
+ # root but rather at some other path, the `prefix` option allows you to
264
+ # override this setting, and if not set it will fall back to
265
+ # {Refile.mount_point}.
266
+ #
267
+ # @example
268
+ # Refile.app_url
269
+ #
270
+ # @example With host and prefix
271
+ # Refile.app_url(host: "http://some.domain", prefix: "/refile")
272
+ #
273
+ # @param [String, nil] host Override the host
274
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
275
+ # @return [String] The generated URL
276
+ def app_url(host: nil, prefix: nil)
277
+ host ||= Refile.app_host
278
+ prefix ||= Refile.mount_point
279
+
280
+ uri = URI(host.to_s)
281
+ uri.path = prefix || "/"
282
+ uri.to_s
283
+ end
284
+
285
+ # Receives a {Refile::File} and generates a URL to it.
286
+ #
287
+ # Optionally the name of a processor and arguments to it can be appended.
288
+ #
289
+ # The `filename` option must be given.
290
+ #
291
+ # The host defaults to {Refile.cdn_host}, which is useful for serving all
292
+ # attachments from a CDN. You can also override the host via the `host`
293
+ # option.
294
+ #
295
+ # Returns `nil` if the supplied file is `nil`.
296
+ #
297
+ # @example
298
+ # Refile.file_url(Refile.store.get(id))
299
+ #
300
+ # @example With processor
301
+ # Refile.file_url(Refile.store.get(id), :image, :fill, 300, 300, format: "jpg")
302
+ #
303
+ # @param [Refile::File] file The file to generate a URL for
304
+ # @param [String] filename The filename to be appended to the URL
305
+ # @param [String, nil] format A file extension to be appended to the URL
306
+ # @param [String, nil] host Override the host
307
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
308
+ # @param [String, nil] expires_at Adds a sulfix to the URL that sets the expiration time of the URL
309
+ # @param [String, nil] force_download Adds a sulfix to the URL to force the download of the file when URL is accessed
310
+ # @return [String, nil] The generated URL
311
+ def file_url(file, *args, expires_at: nil, host: nil, prefix: nil, filename:, format: nil, force_download: nil)
312
+ return unless file
313
+
314
+ host ||= Refile.cdn_host
315
+ backend_name = Refile.backends.key(file.backend)
316
+
317
+ filename = Rack::Utils.escape(filename)
318
+ filename << "." << format.to_s if format && !filename.downcase.end_with?(format.to_s.downcase)
319
+
320
+ base_path = ::File.join("", backend_name, *args.map(&:to_s), file.id.to_s, filename)
321
+ if expires_at
322
+ base_path += "?expires_at=#{expires_at.to_i}" # UNIX timestamp
323
+ end
324
+
325
+ base_path += "?force_download=true" if force_download
326
+
327
+ ::File.join(app_url(prefix: prefix, host: host), token(base_path), base_path)
328
+ end
329
+
330
+ # Receives a Refile backend and returns a URL to the Refile application
331
+ # where files can be uploaded.
332
+ #
333
+ # @example
334
+ # Refile.upload_url(Refile.store)
335
+ #
336
+ # @param [Refile::Backend] backend The backend to generate a URL for
337
+ # @param [String, nil] host Override the host
338
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
339
+ # @return [String] The generated URL
340
+ def upload_url(backend, host: nil, prefix: nil)
341
+ backend_name = Refile.backends.key(backend)
342
+
343
+ ::File.join(app_url(host: host, prefix: prefix), backend_name)
344
+ end
345
+
346
+ # Receives a Refile backend and returns a URL to the Refile application
347
+ # where a presign object for the backend can be retrieved.
348
+ #
349
+ # @example
350
+ # Refile.upload_url(Refile.store)
351
+ #
352
+ # @param [Refile::Backend] backend The backend to generate a URL for
353
+ # @param [String, nil] host Override the host
354
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
355
+ # @return [String] The generated URL
356
+ def presign_url(backend, host: nil, prefix: nil)
357
+ ::File.join(upload_url(backend, host: host, prefix: prefix), "presign")
358
+ end
359
+
360
+ # Generate a URL to an attachment. Receives an instance of a class which
361
+ # has used the {Refile::Attachment#attachment} macro to generate an
362
+ # attachment column, and the name of this column, and based on this
363
+ # generates a URL to a {Refile::App}.
364
+ #
365
+ # Optionally the name of a processor and arguments to it can be appended.
366
+ #
367
+ # If the filename option is not given, the filename is taken from the
368
+ # metadata stored in the attachment, or eventually falls back to the
369
+ # `name`.
370
+ #
371
+ # The host defaults to {Refile.cdn_host}, which is useful for serving all
372
+ # attachments from a CDN. You can also override the host via the `host`
373
+ # option.
374
+ #
375
+ # Returns `nil` if there is no file attached.
376
+ #
377
+ # @example
378
+ # Refile.attachment_url(@post, :document)
379
+ #
380
+ # @example With processor
381
+ # Refile.attachment_url(@post, :image, :fill, 300, 300, format: "jpg")
382
+ #
383
+ # @param [Refile::Attachment] object Instance of a class which has an attached file
384
+ # @param [Symbol] name The name of the attachment column
385
+ # @param [String, nil] filename The filename to be appended to the URL
386
+ # @param [String, nil] format A file extension to be appended to the URL
387
+ # @param [String, nil] host Override the host
388
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
389
+ # @param [String, nil] expires_at Adds a sulfix to the URL that sets the expiration time of the URL
390
+ # @param [String, nil] force_download Adds a sulfix to the URL to force the download of the file when URL is accessed
391
+ # @return [String, nil] The generated URL
392
+ def attachment_url(object, name, *args, expires_at: nil, host: nil, prefix: nil, filename: nil, format: nil, force_download: nil)
393
+ attacher = object.send(:"#{name}_attacher")
394
+ file = attacher.get
395
+ return unless file
396
+
397
+ filename ||= attacher.basename || name.to_s
398
+ format ||= attacher.extension
399
+
400
+ file_url(file, *args, expires_at: expires_at, host: host, prefix: prefix, filename: filename, format: format, force_download: force_download)
401
+ end
402
+
403
+ # Receives an instance of a class which has used the
404
+ # {Refile::Attachment#attachment} macro to generate an attachment column,
405
+ # and the name of this column, and based on this generates a URL to a
406
+ # {Refile::App} where files can be uploaded.
407
+ #
408
+ # @example
409
+ # Refile.attachment_upload_url(@post, :document)
410
+ #
411
+ # @param [Refile::Attachment] object Instance of a class which has an attached file
412
+ # @param [Symbol] name The name of the attachment column
413
+ # @param [String, nil] host Override the host
414
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
415
+ # @return [String] The generated URL
416
+ def attachment_upload_url(object, name, host: nil, prefix: nil)
417
+ backend = object.send(:"#{name}_attachment_definition").cache
418
+
419
+ upload_url(backend, host: host, prefix: prefix)
420
+ end
421
+
422
+ # Receives an instance of a class which has used the
423
+ # {Refile::Attachment#attachment} macro to generate an attachment column,
424
+ # and the name of this column, and based on this generates a URL to a
425
+ # {Refile::App} where a presign object for the backend can be retrieved.
426
+ #
427
+ # @example
428
+ # Refile.attachment_presign_url(@post, :document)
429
+ #
430
+ # @param [Refile::Attachment] object Instance of a class which has an attached file
431
+ # @param [Symbol] name The name of the attachment column
432
+ # @param [String, nil] host Override the host
433
+ # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root
434
+ # @return [String] The generated URL
435
+ def attachment_presign_url(object, name, host: nil, prefix: nil)
436
+ backend = object.send(:"#{name}_attachment_definition").cache
437
+
438
+ presign_url(backend, host: host, prefix: prefix)
439
+ end
440
+
441
+ # Generate a signature for a given path concatenated with the configured secret token.
442
+ #
443
+ # Raises an error if no secret token is configured.
444
+ #
445
+ # @example
446
+ # Refile.token('/store/f5f2e4/document.pdf')
447
+ #
448
+ # @param [String] path The path to generate a token for
449
+ # @raise [RuntimeError] If {Refile.secret_key} is not set
450
+ # @return [String, nil] The generated token
451
+ def token(path)
452
+ if secret_key.nil?
453
+ error = "Refile.secret_key was not set.\n\n"
454
+ error << "Please add the following to your Refile configuration and restart your application:\n\n"
455
+ error << "```\nRefile.secret_key = '#{SecureRandom.hex(64)}'\n```\n\n"
456
+
457
+ raise error
458
+ end
459
+
460
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha1"), secret_key, path)
461
+ end
462
+
463
+ # Check if the given token is a valid token for the given path.
464
+ #
465
+ # @example
466
+ # Refile.valid_token?('/store/f5f2e4/document.pdf', 'abcd1234')
467
+ #
468
+ # @param [String] path The path to check validity for
469
+ # @param [String] token The token to check
470
+ # @raise [RuntimeError] If {Refile.secret_key} is not set
471
+ # @return [Boolean] Whether the token is valid
472
+ def valid_token?(path, token)
473
+ expected = Digest::SHA1.hexdigest(token(path))
474
+ actual = Digest::SHA1.hexdigest(token)
475
+
476
+ expected == actual
477
+ end
478
+
479
+ # @api private
480
+ def parse_json(data, *args)
481
+ JSON.parse(data.to_s, *args)
482
+ rescue JSON::ParserError
483
+ nil
484
+ end
485
+ end
486
+
487
+ require "refile/version"
488
+ require "refile/signature"
489
+ require "refile/type"
490
+ require "refile/backend_macros"
491
+ require "refile/attachment_definition"
492
+ require "refile/attacher"
493
+ require "refile/attachment"
494
+ require "refile/random_hasher"
495
+ require "refile/file"
496
+ require "refile/custom_logger"
497
+ require "refile/app"
498
+ require "refile/backend/file_system"
499
+ end
500
+
501
+ Refile.configure do |config|
502
+ config.allow_uploads_to = ["cache"]
503
+ config.allow_downloads_from = :all
504
+ config.allow_origin = "*"
505
+ config.logger = Logger.new(STDOUT) unless ENV["RACK_ENV"] == "test"
506
+ config.mount_point = "/attachments"
507
+ config.automount = true
508
+ config.content_max_age = 60 * 60 * 24 * 365
509
+ config.types[:image] = Refile::Type.new(:image, content_type: %w[image/jpeg image/gif image/png])
510
+ end