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.
- checksums.yaml +7 -0
- data/app/assets/javascripts/refile.js +125 -0
- data/config/locales/en.yml +10 -0
- data/config/routes.rb +5 -0
- data/lib/refile.rb +510 -0
- data/lib/refile/app.rb +186 -0
- data/lib/refile/attacher.rb +190 -0
- data/lib/refile/attachment.rb +108 -0
- data/lib/refile/attachment/active_record.rb +133 -0
- data/lib/refile/attachment_definition.rb +83 -0
- data/lib/refile/backend/file_system.rb +120 -0
- data/lib/refile/backend/s3.rb +1 -0
- data/lib/refile/backend_macros.rb +45 -0
- data/lib/refile/custom_logger.rb +48 -0
- data/lib/refile/file.rb +102 -0
- data/lib/refile/file_double.rb +13 -0
- data/lib/refile/image_processing.rb +1 -0
- data/lib/refile/rails.rb +54 -0
- data/lib/refile/rails/attachment_helper.rb +121 -0
- data/lib/refile/random_hasher.rb +11 -0
- data/lib/refile/signature.rb +36 -0
- data/lib/refile/simple_form.rb +17 -0
- data/lib/refile/type.rb +28 -0
- data/lib/refile/version.rb +3 -0
- data/spec/refile/active_record_helper.rb +35 -0
- data/spec/refile/app_spec.rb +424 -0
- data/spec/refile/attachment/active_record_spec.rb +568 -0
- data/spec/refile/attachment_helper_spec.rb +78 -0
- data/spec/refile/attachment_spec.rb +589 -0
- data/spec/refile/backend/file_system_spec.rb +5 -0
- data/spec/refile/backend_examples.rb +228 -0
- data/spec/refile/backend_macros_spec.rb +83 -0
- data/spec/refile/custom_logger_spec.rb +21 -0
- data/spec/refile/features/direct_upload_spec.rb +63 -0
- data/spec/refile/features/multiple_upload_spec.rb +122 -0
- data/spec/refile/features/normal_upload_spec.rb +144 -0
- data/spec/refile/features/presigned_upload_spec.rb +31 -0
- data/spec/refile/features/simple_form_spec.rb +8 -0
- data/spec/refile/fixtures/hello.txt +1 -0
- data/spec/refile/fixtures/image.jpg +0 -0
- data/spec/refile/fixtures/large.txt +44 -0
- data/spec/refile/fixtures/monkey.txt +1 -0
- data/spec/refile/fixtures/world.txt +1 -0
- data/spec/refile/spec_helper.rb +72 -0
- data/spec/refile_spec.rb +355 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -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"
|
data/config/routes.rb
ADDED
data/lib/refile.rb
ADDED
@@ -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
|