leifcr-refile 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
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,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