leifcr-refile 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/refile/app.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
require "json"
|
2
|
+
require "sinatra/base"
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
module Refile
|
6
|
+
# A Rack application which can be mounted or run on its own.
|
7
|
+
#
|
8
|
+
# @example mounted in Rails
|
9
|
+
# Rails.application.routes.draw do
|
10
|
+
# mount Refile::App.new, at: "attachments", as: :refile_app
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @example as standalone app
|
14
|
+
# require "refile"
|
15
|
+
#
|
16
|
+
# run Refile::App.new
|
17
|
+
class App < Sinatra::Base
|
18
|
+
configure do
|
19
|
+
set :show_exceptions, false
|
20
|
+
set :raise_errors, false
|
21
|
+
set :sessions, false
|
22
|
+
set :logging, false
|
23
|
+
set :dump_errors, false
|
24
|
+
use CustomLogger, "Refile::App", proc { Refile.logger }
|
25
|
+
end
|
26
|
+
|
27
|
+
before do
|
28
|
+
if Refile.allow_origin
|
29
|
+
response["Access-Control-Allow-Origin"] = Refile.allow_origin
|
30
|
+
response["Access-Control-Allow-Headers"] = request.env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
|
31
|
+
response["Access-Control-Allow-Method"] = request.env["HTTP_ACCESS_CONTROL_REQUEST_METHOD"].to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# This will match all token authenticated requests
|
36
|
+
before "/:token/:backend/*" do
|
37
|
+
halt 403 unless verified?
|
38
|
+
end
|
39
|
+
|
40
|
+
get "/:token/:backend/:id/:filename" do
|
41
|
+
halt 404 unless download_allowed?
|
42
|
+
stream_file file
|
43
|
+
end
|
44
|
+
|
45
|
+
get "/:token/:backend/:processor/:id/:file_basename.:extension" do
|
46
|
+
halt 404 unless download_allowed?
|
47
|
+
stream_file processor.call(file, format: params[:extension])
|
48
|
+
end
|
49
|
+
|
50
|
+
get "/:token/:backend/:processor/:id/:filename" do
|
51
|
+
halt 404 unless download_allowed?
|
52
|
+
stream_file processor.call(file)
|
53
|
+
end
|
54
|
+
|
55
|
+
get "/:token/:backend/:processor/*/:id/:file_basename.:extension" do
|
56
|
+
halt 404 unless download_allowed?
|
57
|
+
stream_file processor.call(file, *params[:splat].first.split("/"), format: params[:extension])
|
58
|
+
end
|
59
|
+
|
60
|
+
get "/:token/:backend/:processor/*/:id/:filename" do
|
61
|
+
halt 404 unless download_allowed?
|
62
|
+
stream_file processor.call(file, *params[:splat].first.split("/"))
|
63
|
+
end
|
64
|
+
|
65
|
+
options "/:backend" do
|
66
|
+
""
|
67
|
+
end
|
68
|
+
|
69
|
+
post "/:backend" do
|
70
|
+
halt 404 unless upload_allowed?
|
71
|
+
tempfile = request.params.fetch("file").fetch(:tempfile)
|
72
|
+
filename = request.params.fetch("file").fetch(:filename)
|
73
|
+
file = backend.upload(tempfile)
|
74
|
+
url = Refile.file_url(file, filename: filename)
|
75
|
+
content_type :json
|
76
|
+
{ id: file.id, url: url }.to_json
|
77
|
+
end
|
78
|
+
|
79
|
+
get "/:backend/presign" do
|
80
|
+
halt 404 unless upload_allowed?
|
81
|
+
content_type :json
|
82
|
+
backend.presign.to_json
|
83
|
+
end
|
84
|
+
|
85
|
+
not_found do
|
86
|
+
content_type :text
|
87
|
+
"not found"
|
88
|
+
end
|
89
|
+
|
90
|
+
error 403 do
|
91
|
+
content_type :text
|
92
|
+
"forbidden"
|
93
|
+
end
|
94
|
+
|
95
|
+
error Refile::InvalidFile do
|
96
|
+
status 400
|
97
|
+
"Upload failure error"
|
98
|
+
end
|
99
|
+
|
100
|
+
error Refile::InvalidMaxSize do
|
101
|
+
status 413
|
102
|
+
"Upload failure error"
|
103
|
+
end
|
104
|
+
|
105
|
+
error do |error_thrown|
|
106
|
+
log_error("Error -> #{error_thrown}")
|
107
|
+
error_thrown.backtrace.each do |line|
|
108
|
+
log_error(line)
|
109
|
+
end
|
110
|
+
content_type :text
|
111
|
+
"error"
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def download_allowed?
|
117
|
+
Refile.allow_downloads_from == :all or Refile.allow_downloads_from.include?(params[:backend])
|
118
|
+
end
|
119
|
+
|
120
|
+
def upload_allowed?
|
121
|
+
Refile.allow_uploads_to == :all or Refile.allow_uploads_to.include?(params[:backend])
|
122
|
+
end
|
123
|
+
|
124
|
+
def logger
|
125
|
+
Refile.logger
|
126
|
+
end
|
127
|
+
|
128
|
+
def stream_file(file)
|
129
|
+
expires Refile.content_max_age, :public
|
130
|
+
|
131
|
+
if file.respond_to?(:path)
|
132
|
+
path = file.path
|
133
|
+
else
|
134
|
+
path = Dir::Tmpname.create(params[:id]) {}
|
135
|
+
IO.copy_stream file, path
|
136
|
+
end
|
137
|
+
|
138
|
+
filename = Rack::Utils.unescape(request.path.split("/").last)
|
139
|
+
disposition = force_download?(params) ? "attachment" : "inline"
|
140
|
+
|
141
|
+
send_file path, filename: filename, disposition: disposition, type: ::File.extname(filename)
|
142
|
+
end
|
143
|
+
|
144
|
+
def backend
|
145
|
+
Refile.backends.fetch(params[:backend]) do |name|
|
146
|
+
log_error("Could not find backend: #{name}")
|
147
|
+
halt 404
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def file
|
152
|
+
file = backend.get(params[:id])
|
153
|
+
unless file.exists?
|
154
|
+
log_error("Could not find attachment by id: #{params[:id]}")
|
155
|
+
halt 404
|
156
|
+
end
|
157
|
+
file.download
|
158
|
+
end
|
159
|
+
|
160
|
+
def processor
|
161
|
+
Refile.processors.fetch(params[:processor]) do |name|
|
162
|
+
log_error("Could not find processor: #{name}")
|
163
|
+
halt 404
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def log_error(message)
|
168
|
+
logger.error "#{self.class.name}: #{message}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def verified?
|
172
|
+
base_path = request.fullpath.gsub(::File.join(request.script_name, params[:token]), "")
|
173
|
+
|
174
|
+
Refile.valid_token?(base_path, params[:token]) && not_expired?(params)
|
175
|
+
end
|
176
|
+
|
177
|
+
def not_expired?(params)
|
178
|
+
params["expires_at"].nil? ||
|
179
|
+
(Time.at(params["expires_at"].to_i) > Time.now)
|
180
|
+
end
|
181
|
+
|
182
|
+
def force_download?(params)
|
183
|
+
!params["force_download"].nil?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module Refile
|
2
|
+
# @api private
|
3
|
+
class Attacher
|
4
|
+
attr_reader :definition, :record, :errors
|
5
|
+
attr_accessor :remove
|
6
|
+
|
7
|
+
Presence = ->(val) { val if val != "" }
|
8
|
+
|
9
|
+
def initialize(definition, record)
|
10
|
+
@definition = definition
|
11
|
+
@record = record
|
12
|
+
@errors = []
|
13
|
+
@metadata = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
@definition.name
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache
|
21
|
+
@definition.cache
|
22
|
+
end
|
23
|
+
|
24
|
+
def store
|
25
|
+
@definition.store
|
26
|
+
end
|
27
|
+
|
28
|
+
def id
|
29
|
+
Presence[read(:id, true)]
|
30
|
+
end
|
31
|
+
|
32
|
+
def size
|
33
|
+
Presence[@metadata[:size] || read(:size)]
|
34
|
+
end
|
35
|
+
|
36
|
+
def filename
|
37
|
+
Presence[@metadata[:filename] || read(:filename)]
|
38
|
+
end
|
39
|
+
|
40
|
+
def content_type
|
41
|
+
Presence[@metadata[:content_type] || read(:content_type)]
|
42
|
+
end
|
43
|
+
|
44
|
+
def cache_id
|
45
|
+
Presence[@metadata[:id]]
|
46
|
+
end
|
47
|
+
|
48
|
+
def basename
|
49
|
+
if filename and extension
|
50
|
+
::File.basename(filename, "." << extension)
|
51
|
+
else
|
52
|
+
filename
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def extension
|
57
|
+
if filename
|
58
|
+
Presence[::File.extname(filename).sub(/^\./, "")]
|
59
|
+
elsif content_type
|
60
|
+
type = MIME::Types[content_type][0]
|
61
|
+
type.extensions[0] if type
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get
|
66
|
+
if remove?
|
67
|
+
nil
|
68
|
+
elsif cache_id
|
69
|
+
cache.get(cache_id)
|
70
|
+
elsif id
|
71
|
+
store.get(id)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def set(value)
|
76
|
+
case value
|
77
|
+
when nil then self.remove = true
|
78
|
+
when String, Hash then retrieve!(value)
|
79
|
+
else cache!(value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def retrieve!(value)
|
84
|
+
if value.is_a?(String)
|
85
|
+
@metadata = Refile.parse_json(value, symbolize_names: true) || {}
|
86
|
+
elsif value.is_a?(Hash)
|
87
|
+
@metadata = value
|
88
|
+
end
|
89
|
+
write_metadata if cache_id
|
90
|
+
end
|
91
|
+
|
92
|
+
def cache!(uploadable)
|
93
|
+
@metadata = {
|
94
|
+
size: uploadable.size,
|
95
|
+
content_type: Refile.extract_content_type(uploadable),
|
96
|
+
filename: Refile.extract_filename(uploadable)
|
97
|
+
}
|
98
|
+
if valid?
|
99
|
+
@metadata[:id] = cache.upload(uploadable).id
|
100
|
+
write_metadata
|
101
|
+
elsif @definition.raise_errors?
|
102
|
+
raise Refile::Invalid, @errors.join(", ")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def download(url)
|
107
|
+
unless url.to_s.empty?
|
108
|
+
response = RestClient::Request.new(method: :get, url: url, raw_response: true).execute
|
109
|
+
@metadata = {
|
110
|
+
size: response.file.size,
|
111
|
+
filename: URI.parse(url).path.split("/").last,
|
112
|
+
content_type: response.headers[:content_type]
|
113
|
+
}
|
114
|
+
if valid?
|
115
|
+
response.file.open if response.file.closed? # https://github.com/refile/refile/pull/210
|
116
|
+
@metadata[:id] = cache.upload(response.file).id
|
117
|
+
write_metadata
|
118
|
+
elsif @definition.raise_errors?
|
119
|
+
raise Refile::Invalid, @errors.join(", ")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
rescue RestClient::Exception
|
123
|
+
@errors = [:download_failed]
|
124
|
+
raise if @definition.raise_errors?
|
125
|
+
end
|
126
|
+
|
127
|
+
def store!
|
128
|
+
if remove?
|
129
|
+
delete!
|
130
|
+
write(:id, nil, true)
|
131
|
+
remove_metadata
|
132
|
+
elsif cache_id
|
133
|
+
file = store.upload(get)
|
134
|
+
delete!
|
135
|
+
write(:id, file.id, true)
|
136
|
+
write_metadata
|
137
|
+
end
|
138
|
+
@metadata = {}
|
139
|
+
end
|
140
|
+
|
141
|
+
def delete!
|
142
|
+
cache.delete(cache_id) if cache_id
|
143
|
+
store.delete(id) if id
|
144
|
+
@metadata = {}
|
145
|
+
end
|
146
|
+
|
147
|
+
def remove?
|
148
|
+
remove and remove != "" and remove !~ /\A0|false$\z/
|
149
|
+
end
|
150
|
+
|
151
|
+
def present?
|
152
|
+
not @metadata.empty?
|
153
|
+
end
|
154
|
+
|
155
|
+
def data
|
156
|
+
@metadata if valid?
|
157
|
+
end
|
158
|
+
|
159
|
+
def valid?
|
160
|
+
@errors = @definition.validate(self)
|
161
|
+
@errors.empty?
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def read(column, strict = false)
|
167
|
+
m = "#{name}_#{column}"
|
168
|
+
value ||= record.send(m) if strict or record.respond_to?(m)
|
169
|
+
value
|
170
|
+
end
|
171
|
+
|
172
|
+
def write(column, value, strict = false)
|
173
|
+
return if record.frozen?
|
174
|
+
m = "#{name}_#{column}="
|
175
|
+
record.send(m, value) if strict or record.respond_to?(m)
|
176
|
+
end
|
177
|
+
|
178
|
+
def write_metadata
|
179
|
+
write(:size, size)
|
180
|
+
write(:content_type, content_type)
|
181
|
+
write(:filename, filename)
|
182
|
+
end
|
183
|
+
|
184
|
+
def remove_metadata
|
185
|
+
write(:size, nil)
|
186
|
+
write(:content_type, nil)
|
187
|
+
write(:filename, nil)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Refile
|
2
|
+
module Attachment
|
3
|
+
# Macro which generates accessors for the given column which make it
|
4
|
+
# possible to upload and retrieve previously uploaded files through the
|
5
|
+
# generated accessors.
|
6
|
+
#
|
7
|
+
# The `raise_errors` option controls whether assigning an invalid file
|
8
|
+
# should immediately raise an error, or save the error and defer handling
|
9
|
+
# it until later.
|
10
|
+
#
|
11
|
+
# Given a record with an attachment named `image`, the following methods
|
12
|
+
# will be added:
|
13
|
+
#
|
14
|
+
# - `image`
|
15
|
+
# - `image=`
|
16
|
+
# - `remove_image`
|
17
|
+
# - `remove_image=`
|
18
|
+
# - `remote_image_url`
|
19
|
+
# - `remote_image_url=`
|
20
|
+
# - `image_url`
|
21
|
+
# - `image_presigned_url`
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# class User
|
25
|
+
# extend Refile::Attachment
|
26
|
+
#
|
27
|
+
# attachment :image
|
28
|
+
# attr_accessor :image_id
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# @param [String] name Name of the column which accessor are generated for
|
32
|
+
# @param [#to_s] cache Name of a backend in {Refile.backends} to use as transient cache
|
33
|
+
# @param [#to_s] store Name of a backend in {Refile.backends} to use as permanent store
|
34
|
+
# @param [true, false] raise_errors Whether to raise errors in case an invalid file is assigned
|
35
|
+
# @param [Symbol, nil] type The type of file that can be uploaded, see {Refile.types}
|
36
|
+
# @param [String, Array<String>, nil] extension Limit the uploaded file to the given extension or list of extensions
|
37
|
+
# @param [String, Array<String>, nil] content_type Limit the uploaded file to the given content type or list of content types
|
38
|
+
# @return [void]
|
39
|
+
def attachment(name, cache: :cache, store: :store, raise_errors: true, type: nil, extension: nil, content_type: nil)
|
40
|
+
definition = AttachmentDefinition.new(name,
|
41
|
+
cache: cache,
|
42
|
+
store: store,
|
43
|
+
raise_errors: raise_errors,
|
44
|
+
type: type,
|
45
|
+
extension: extension,
|
46
|
+
content_type: content_type
|
47
|
+
)
|
48
|
+
|
49
|
+
define_singleton_method :"#{name}_attachment_definition" do
|
50
|
+
definition
|
51
|
+
end
|
52
|
+
|
53
|
+
mod = Module.new do
|
54
|
+
attacher = :"#{name}_attacher"
|
55
|
+
|
56
|
+
define_method :"#{name}_attachment_definition" do
|
57
|
+
definition
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method attacher do
|
61
|
+
ivar = :"@#{attacher}"
|
62
|
+
instance_variable_get(ivar) or instance_variable_set(ivar, Attacher.new(definition, self))
|
63
|
+
end
|
64
|
+
|
65
|
+
define_method "#{name}=" do |value|
|
66
|
+
send(attacher).set(value)
|
67
|
+
end
|
68
|
+
|
69
|
+
define_method name do
|
70
|
+
send(attacher).get
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method "remove_#{name}=" do |remove|
|
74
|
+
send(attacher).remove = remove
|
75
|
+
end
|
76
|
+
|
77
|
+
define_method "remove_#{name}" do
|
78
|
+
send(attacher).remove
|
79
|
+
end
|
80
|
+
|
81
|
+
define_method "remote_#{name}_url=" do |url|
|
82
|
+
send(attacher).download(url)
|
83
|
+
end
|
84
|
+
|
85
|
+
define_method "remote_#{name}_url" do
|
86
|
+
end
|
87
|
+
|
88
|
+
define_method "#{name}_url" do |*args|
|
89
|
+
Refile.attachment_url(self, name, *args)
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method "presigned_#{name}_url" do |expires_in = 900|
|
93
|
+
attachment = send(attacher)
|
94
|
+
attachment.store.object(attachment.id).presigned_url(:get, expires_in: expires_in) unless attachment.id.nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
define_method "#{name}_data" do
|
98
|
+
send(attacher).data
|
99
|
+
end
|
100
|
+
|
101
|
+
define_singleton_method("to_s") { "Refile::Attachment(#{name})" }
|
102
|
+
define_singleton_method("inspect") { "Refile::Attachment(#{name})" }
|
103
|
+
end
|
104
|
+
|
105
|
+
include mod
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|