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
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
|