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