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,13 @@
1
+ module Refile
2
+ class FileDouble
3
+ attr_reader :original_filename, :content_type
4
+ def initialize(data, name = nil, content_type: nil)
5
+ @io = StringIO.new(data)
6
+ @original_filename = name
7
+ @content_type = content_type
8
+ end
9
+
10
+ extend Forwardable
11
+ def_delegators :@io, :read, :rewind, :size, :eof?, :close
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ raise "[Refile] image processing has been extracted into a separate gem, see https://github.com/refile/refile-mini_magick"
@@ -0,0 +1,54 @@
1
+ require "refile"
2
+ require "refile/rails/attachment_helper"
3
+
4
+ module Refile
5
+ # @api private
6
+ class Engine < Rails::Engine
7
+ initializer "refile.setup", before: :load_environment_config do
8
+ if RUBY_PLATFORM == "java"
9
+ # Work around a bug in JRuby, see: https://github.com/jruby/jruby/issues/2779
10
+ Encoding.default_internal = nil
11
+ end
12
+
13
+ Refile.store ||= Refile::Backend::FileSystem.new(Rails.root.join("tmp/uploads/store").to_s)
14
+ Refile.cache ||= Refile::Backend::FileSystem.new(Rails.root.join("tmp/uploads/cache").to_s)
15
+
16
+ ActiveSupport.on_load :active_record do
17
+ require "refile/attachment/active_record"
18
+ end
19
+
20
+ ActionView::Base.send(:include, Refile::AttachmentHelper)
21
+ ActionView::Helpers::FormBuilder.send(:include, AttachmentHelper::FormBuilder)
22
+ end
23
+
24
+ initializer "refile.app" do
25
+ Refile.logger = Rails.logger
26
+ Refile.app = Refile::App.new
27
+ end
28
+
29
+ initializer "refile.secret_key" do |app|
30
+ Refile.secret_key ||= if app.respond_to?(:secrets)
31
+ app.secrets.secret_key_base
32
+ elsif app.config.respond_to?(:secret_key_base)
33
+ app.config.secret_key_base
34
+ elsif app.config.respond_to?(:secret_token)
35
+ app.config.secret_token
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Add in missing methods for file uploads in Rails < 4
42
+ ActionDispatch::Http::UploadedFile.class_eval do
43
+ unless instance_methods.include?(:eof?)
44
+ def eof?
45
+ @tempfile.eof?
46
+ end
47
+ end
48
+
49
+ unless instance_methods.include?(:close)
50
+ def close
51
+ @tempfile.close
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,121 @@
1
+ module Refile
2
+ # Rails view helpers which aid in using Refile from views.
3
+ module AttachmentHelper
4
+ # Form builder extension
5
+ module FormBuilder
6
+ # @see AttachmentHelper#attachment_field
7
+ def attachment_field(method, options = {})
8
+ self.multipart = true
9
+ @template.attachment_field(@object_name, method, objectify_options(options))
10
+ end
11
+
12
+ # @see AttachmentHelper#attachment_cache_field
13
+ def attachment_cache_field(method, options = {})
14
+ self.multipart = true
15
+ @template.attachment_cache_field(@object_name, method, objectify_options(options))
16
+ end
17
+ end
18
+
19
+ # View helper which generates a url for an attachment. This generates a URL
20
+ # to the {Refile::App} which is assumed to be mounted in the Rails
21
+ # application.
22
+ #
23
+ # @see Refile.attachment_url
24
+ #
25
+ # @param [Refile::Attachment] object Instance of a class which has an attached file
26
+ # @param [Symbol] name The name of the attachment column
27
+ # @param [String, nil] filename The filename to be appended to the URL
28
+ # @param [String, nil] fallback The path to an asset to be used as a fallback
29
+ # @param [String, nil] format A file extension to be appended to the URL
30
+ # @param [String, nil] host Override the host
31
+ # @return [String, nil] The generated URL
32
+ def attachment_url(record, name, *args, fallback: nil, **opts)
33
+ file = record && record.public_send(name)
34
+ if file
35
+ Refile.attachment_url(record, name, *args, **opts)
36
+ elsif fallback
37
+ asset_url(fallback)
38
+ end
39
+ end
40
+
41
+ # Generates an image tag for the given attachment, adding appropriate
42
+ # classes and optionally falling back to the given fallback image if there
43
+ # is no file attached.
44
+ #
45
+ # Returns `nil` if there is no file attached and no fallback specified.
46
+ #
47
+ # @param [String] fallback The path to an image asset to be used as a fallback
48
+ # @param [Hash] options Additional options for the image tag
49
+ # @see #attachment_url
50
+ # @return [ActiveSupport::SafeBuffer, nil] The generated image tag
51
+ def attachment_image_tag(record, name, *args, fallback: nil, host: nil, prefix: nil, format: nil, **options)
52
+ file = record && record.public_send(name)
53
+ classes = ["attachment", (record.class.model_name.singular if record), name, *options[:class]]
54
+
55
+ if file
56
+ image_tag(attachment_url(record, name, *args, host: host, prefix: prefix, format: format), options.merge(class: classes))
57
+ elsif fallback
58
+ classes << "fallback"
59
+ image_tag(fallback, options.merge(class: classes))
60
+ end
61
+ end
62
+
63
+ # Generates a form field which can be used with records which have
64
+ # attachments. This will generate both a file field as well as a hidden
65
+ # field which tracks the id of the file in the cache before it is
66
+ # permanently stored.
67
+ #
68
+ # @param object_name The name of the object to generate a field for
69
+ # @param method The name of the field
70
+ # @param [Hash] options
71
+ # @option options [Object] object Set by the form builder, currently required for direct/presigned uploads to work.
72
+ # @option options [Boolean] direct If set to true, adds the appropriate data attributes for direct uploads with refile.js.
73
+ # @option options [Boolean] presign If set to true, adds the appropriate data attributes for presigned uploads with refile.js.
74
+ # @return [ActiveSupport::SafeBuffer] The generated form field
75
+ def attachment_field(object_name, method, object:, **options)
76
+ options[:data] ||= {}
77
+
78
+ definition = object.send(:"#{method}_attachment_definition")
79
+ options[:accept] = definition.accept
80
+
81
+ if options[:direct]
82
+ url = Refile.attachment_upload_url(object, method, host: options[:host], prefix: options[:prefix])
83
+ options[:data].merge!(direct: true, as: "file", url: url)
84
+ end
85
+
86
+ if options[:presigned] and definition.cache.respond_to?(:presign)
87
+ url = Refile.attachment_presign_url(object, method, host: options[:host], prefix: options[:prefix])
88
+ options[:data].merge!(direct: true, presigned: true, url: url)
89
+ end
90
+
91
+ options[:data][:reference] = SecureRandom.hex
92
+ options[:include_hidden] = false
93
+
94
+ attachment_cache_field(object_name, method, object: object, **options) + file_field(object_name, method, options)
95
+ end
96
+
97
+ # Generates a hidden form field which tracks the id of the file in the cache
98
+ # before it is permanently stored.
99
+ #
100
+ # @param object_name The name of the object to generate a field for
101
+ # @param method The name of the field
102
+ # @param [Hash] options
103
+ # @option options [Object] object Set by the form builder
104
+ # @return [ActiveSupport::SafeBuffer] The generated hidden form field
105
+ def attachment_cache_field(object_name, method, object:, **options)
106
+ options[:data] ||= {}
107
+ options[:data][:reference] ||= SecureRandom.hex
108
+
109
+ hidden_options = {
110
+ multiple: options[:multiple],
111
+ value: object.send("#{method}_data").try(:to_json),
112
+ object: object,
113
+ id: nil,
114
+ data: { reference: options[:data][:reference] }
115
+ }
116
+ hidden_options.merge!(index: options[:index]) if options.key?(:index)
117
+
118
+ hidden_field(object_name, method, hidden_options)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,11 @@
1
+ module Refile
2
+ # A file hasher which ignores the file contents and always returns a random string.
3
+ class RandomHasher
4
+ # Generate a random string
5
+ #
6
+ # @return [String]
7
+ def hash(_uploadable = nil)
8
+ SecureRandom.hex(30)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ module Refile
2
+ # A signature summarizes an HTTP request a client can make to upload a file
3
+ # to directly upload a file to a backend. This signature is usually generated
4
+ # by a backend's `presign` method.
5
+ class Signature
6
+ # @return [String] the name of the field that the file will be uploaded as.
7
+ attr_reader :as
8
+
9
+ # @return [String] the id the file will receive once uploaded.
10
+ attr_reader :id
11
+
12
+ # @return [String] the url the file should be uploaded to.
13
+ attr_reader :url
14
+
15
+ # @return [String] additional fields to be sent alongside the file.
16
+ attr_reader :fields
17
+
18
+ # @api private
19
+ def initialize(as:, id:, url:, fields:)
20
+ @as = as
21
+ @id = id
22
+ @url = url
23
+ @fields = fields
24
+ end
25
+
26
+ # @return [Hash{Symbol => Object}] an object suitable for serialization to JSON
27
+ def as_json(*)
28
+ { as: @as, id: @id, url: @url, fields: @fields }
29
+ end
30
+
31
+ # @return [String] the signature serialized as JSON
32
+ def to_json(*)
33
+ as_json.to_json
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # make attachment_field behave like a normal input type so we get nice wrapper and labels
2
+ # <%= f.input :cover_image, as: :attachment, direct: true, presigned: true %>
3
+ module SimpleForm
4
+ module Inputs
5
+ class AttachmentInput < Base
6
+ def input(wrapper_options = nil)
7
+ refile_options = [:presigned, :direct, :multiple]
8
+ merged_input_options = merge_wrapper_options(input_options.slice(*refile_options).merge(input_html_options), wrapper_options)
9
+ @builder.attachment_field(attribute_name, merged_input_options)
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ SimpleForm::FormBuilder.class_eval do
16
+ map_type :attachment, to: SimpleForm::Inputs::AttachmentInput
17
+ end
@@ -0,0 +1,28 @@
1
+ module Refile
2
+ # A type represents an alias for one or multiple content types.
3
+ # By adding types, you could simplify this:
4
+ #
5
+ # attachment :document, content_type: %w[text/plain application/pdf]
6
+ #
7
+ # To this:
8
+ #
9
+ # attachment :document, type: :document
10
+ #
11
+ # Simply define a new type like this:
12
+ #
13
+ # Refile.types[:document] = Refile::Type.new(:document,
14
+ # content_type: %w[text/plain application/pdf]
15
+ # )
16
+ #
17
+ class Type
18
+ # @return [String, Array<String>] The type's content types
19
+ attr_accessor :content_type
20
+
21
+ # @param [Symbol] name the name of the type
22
+ # @param [String, Array<String>] content_type content types which are valid for this type
23
+ def initialize(name, content_type: nil)
24
+ @name = name
25
+ @content_type = content_type
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Refile
2
+ VERSION = "0.6.3"
3
+ end
@@ -0,0 +1,35 @@
1
+ require "active_record"
2
+
3
+ I18n.enforce_available_locales = true
4
+
5
+ ActiveRecord::Base.establish_connection(
6
+ adapter: "sqlite3",
7
+ database: File.expand_path("test_app/db/db.sqlite", File.dirname(__FILE__)),
8
+ verbosity: "quiet"
9
+ )
10
+
11
+ class TestMigration < ActiveRecord::Migration
12
+ def self.up
13
+ create_table :posts, force: true do |t|
14
+ t.integer :user_id
15
+ t.column :title, :string
16
+ t.column :image_id, :string
17
+ t.column :document_id, :string
18
+ t.column :document_filename, :string
19
+ t.column :document_content_type, :string
20
+ t.column :document_size, :integer
21
+ end
22
+
23
+ create_table :users, force: true
24
+
25
+ create_table :documents, force: true do |t|
26
+ t.belongs_to :post, null: false
27
+ t.column :file_id, :string, null: false
28
+ t.column :file_filename, :string
29
+ t.column :file_content_type, :string
30
+ t.column :file_size, :integer, null: false
31
+ end
32
+ end
33
+ end
34
+
35
+ TestMigration.up
@@ -0,0 +1,424 @@
1
+ require "rack/test"
2
+
3
+ describe Refile::App do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ res = Refile::App.new
8
+ res.settings.set :public_folder, ""
9
+ res
10
+ end
11
+
12
+ before do
13
+ allow(Refile).to receive(:token).and_return("token")
14
+ end
15
+
16
+ describe "GET /:backend/:id/:filename" do
17
+ it "returns a stored file" do
18
+ file = Refile.store.upload(StringIO.new("hello"))
19
+ get "/token/store/#{file.id}/hello"
20
+
21
+ expect(last_response.status).to eq(200)
22
+ expect(last_response.body).to eq("hello")
23
+ end
24
+
25
+ it "sets appropriate filename from URL" do
26
+ file = Refile.store.upload(StringIO.new("hello"))
27
+ get "/token/store/#{file.id}/logo@2x.png"
28
+
29
+ expect(last_response.status).to eq(200)
30
+ expect(last_response.headers["Content-Disposition"]).to eq 'inline; filename="logo@2x.png"'
31
+ end
32
+
33
+ it "sets appropriate content type from extension" do
34
+ file = Refile.store.upload(StringIO.new("hello"))
35
+ get "/token/store/#{file.id}/hello.html"
36
+
37
+ expect(last_response.status).to eq(200)
38
+ expect(last_response.body).to eq("hello")
39
+ expect(last_response.headers["Content-Type"]).to include("text/html")
40
+ end
41
+
42
+ it "downloads the uploaded file if force download is enabled" do
43
+ file = Refile.store.upload(StringIO.new("hello"))
44
+ get "/token/store/#{file.id}/hello?force_download=true"
45
+
46
+ expect(last_response.status).to eq(200)
47
+ expect(last_response.headers["Content-Disposition"]).to include("attachment")
48
+ end
49
+
50
+ it "returns a 404 if the file doesn't exist" do
51
+ Refile.store.upload(StringIO.new("hello"))
52
+
53
+ get "/token/store/doesnotexist/hello"
54
+
55
+ expect(last_response.status).to eq(404)
56
+ expect(last_response.content_type).to eq("text/plain;charset=utf-8")
57
+ expect(last_response.body).to eq("not found")
58
+ end
59
+
60
+ it "returns a 404 if the backend doesn't exist" do
61
+ file = Refile.store.upload(StringIO.new("hello"))
62
+
63
+ get "/token/doesnotexist/#{file.id}/hello"
64
+
65
+ expect(last_response.status).to eq(404)
66
+ expect(last_response.content_type).to eq("text/plain;charset=utf-8")
67
+ expect(last_response.body).to eq("not found")
68
+ end
69
+
70
+ context "with allow origin" do
71
+ before(:each) do
72
+ allow(Refile).to receive(:allow_origin).and_return("example.com")
73
+ end
74
+
75
+ it "sets CORS header" do
76
+ file = Refile.store.upload(StringIO.new("hello"))
77
+
78
+ get "/token/store/#{file.id}/hello"
79
+
80
+ expect(last_response.status).to eq(200)
81
+ expect(last_response.body).to eq("hello")
82
+ expect(last_response.headers["Access-Control-Allow-Origin"]).to eq("example.com")
83
+ end
84
+ end
85
+
86
+ it "returns a 200 for head requests" do
87
+ file = Refile.store.upload(StringIO.new("hello"))
88
+
89
+ head "/token/store/#{file.id}/hello"
90
+
91
+ expect(last_response.status).to eq(200)
92
+ expect(last_response.body).to be_empty
93
+ end
94
+
95
+ it "returns a 404 for head requests if the file doesn't exist" do
96
+ Refile.store.upload(StringIO.new("hello"))
97
+
98
+ head "/token/store/doesnotexist/hello"
99
+
100
+ expect(last_response.status).to eq(404)
101
+ expect(last_response.body).to be_empty
102
+ end
103
+
104
+ it "returns a 404 for non get requests" do
105
+ file = Refile.store.upload(StringIO.new("hello"))
106
+
107
+ post "/token/store/#{file.id}/hello"
108
+
109
+ expect(last_response.status).to eq(404)
110
+ expect(last_response.content_type).to eq("text/plain;charset=utf-8")
111
+ expect(last_response.body).to eq("not found")
112
+ end
113
+
114
+ context "verification" do
115
+ before do
116
+ allow(Refile).to receive(:token).and_call_original
117
+ end
118
+
119
+ context "with a valid token" do
120
+ let(:file) { Refile.store.upload(StringIO.new("hello")) }
121
+ it "accepts the token" do
122
+ token = Refile.token("/store/#{file.id}/hello")
123
+
124
+ get "/#{token}/store/#{file.id}/hello"
125
+
126
+ expect(last_response.status).to eq(200)
127
+ expect(last_response.body).to eq("hello")
128
+ end
129
+
130
+ context "with a valid `expires_at`" do
131
+ let(:expires_at) { (Time.now + 1.seconds).to_i }
132
+
133
+ it "accepts the expires at" do
134
+ token =
135
+ Refile.token("/store/#{file.id}/hello?expires_at=#{expires_at}")
136
+
137
+ get "/#{token}/store/#{file.id}/hello?expires_at=#{expires_at}"
138
+
139
+ expect(last_response.status).to eq(200)
140
+ expect(last_response.body).to eq("hello")
141
+ end
142
+ end
143
+
144
+ context "with an `expires_at` in the past" do
145
+ let(:expires_at) { (Time.now - 1.seconds).to_i }
146
+
147
+ it "returns a 403" do
148
+ token =
149
+ Refile.token("/store/#{file.id}/hello?expires_at=#{expires_at}")
150
+
151
+ get "/#{token}/store/#{file.id}/hello?expires_at=#{expires_at}"
152
+
153
+ expect(last_response.status).to eq(403)
154
+ expect(last_response.body).to eq("forbidden")
155
+ end
156
+ end
157
+
158
+ context "missing `expires_at` in requiest when it was part of token" do
159
+ let(:expires_at) { (Time.now + 1.seconds).to_i }
160
+
161
+ it "returns a 403" do
162
+ token =
163
+ Refile.token("/store/#{file.id}/hello?expires_at=#{expires_at}")
164
+
165
+ get "/#{token}/store/#{file.id}/hello"
166
+
167
+ expect(last_response.status).to eq(403)
168
+ expect(last_response.body).to eq("forbidden")
169
+ end
170
+ end
171
+ end
172
+
173
+ it "returns a 403 for unsigned get requests" do
174
+ file = Refile.store.upload(StringIO.new("hello"))
175
+
176
+ get "/eviltoken/store/#{file.id}/hello"
177
+
178
+ expect(last_response.status).to eq(403)
179
+ expect(last_response.body).to eq("forbidden")
180
+ end
181
+
182
+ it "does not retrieve nor process files for unauthenticated requests" do
183
+ file = Refile.store.upload(StringIO.new("hello"))
184
+
185
+ expect(Refile.store).not_to receive(:get)
186
+ get "/eviltoken/store/#{file.id}/hello"
187
+
188
+ expect(last_response.status).to eq(403)
189
+ expect(last_response.body).to eq("forbidden")
190
+ end
191
+ end
192
+
193
+ context "when unrestricted" do
194
+ before do
195
+ allow(Refile).to receive(:allow_downloads_from).and_return(:all)
196
+ end
197
+
198
+ it "gets signatures from all backends" do
199
+ file = Refile.store.upload(StringIO.new("hello"))
200
+ get "/token/store/#{file.id}/test.txt"
201
+ expect(last_response.status).to eq(200)
202
+ end
203
+ end
204
+
205
+ context "when restricted" do
206
+ before do
207
+ allow(Refile).to receive(:allow_downloads_from).and_return(["store"])
208
+ end
209
+
210
+ it "gets signatures from allowed backend" do
211
+ file = Refile.store.upload(StringIO.new("hello"))
212
+ get "/token/store/#{file.id}/test.txt"
213
+ expect(last_response.status).to eq(200)
214
+ end
215
+
216
+ it "returns 404 if backend is not allowed" do
217
+ file = Refile.store.upload(StringIO.new("hello"))
218
+ get "/token/cache/#{file.id}/test.txt"
219
+ expect(last_response.status).to eq(404)
220
+ end
221
+ end
222
+ end
223
+
224
+ describe "GET /:backend/:processor/:id/:filename" do
225
+ it "returns 404 if processor does not exist" do
226
+ file = Refile.store.upload(StringIO.new("hello"))
227
+
228
+ get "/token/store/doesnotexist/#{file.id}/hello"
229
+
230
+ expect(last_response.status).to eq(404)
231
+ expect(last_response.content_type).to eq("text/plain;charset=utf-8")
232
+ expect(last_response.body).to eq("not found")
233
+ end
234
+
235
+ it "applies block processor to file" do
236
+ file = Refile.store.upload(StringIO.new("hello"))
237
+
238
+ get "/token/store/reverse/#{file.id}/hello"
239
+
240
+ expect(last_response.status).to eq(200)
241
+ expect(last_response.body).to eq("olleh")
242
+ end
243
+
244
+ it "applies object processor to file" do
245
+ file = Refile.store.upload(StringIO.new("hello"))
246
+
247
+ get "/token/store/upcase/#{file.id}/hello"
248
+
249
+ expect(last_response.status).to eq(200)
250
+ expect(last_response.body).to eq("HELLO")
251
+ end
252
+
253
+ it "applies processor with arguments" do
254
+ file = Refile.store.upload(StringIO.new("hello"))
255
+
256
+ get "/token/store/concat/foo/bar/baz/#{file.id}/hello"
257
+
258
+ expect(last_response.status).to eq(200)
259
+ expect(last_response.body).to eq("hellofoobarbaz")
260
+ end
261
+
262
+ it "applies processor with format" do
263
+ file = Refile.store.upload(StringIO.new("hello"))
264
+
265
+ get "/token/store/convert_case/#{file.id}/hello.up"
266
+
267
+ expect(last_response.status).to eq(200)
268
+ expect(last_response.body).to eq("HELLO")
269
+ end
270
+
271
+ it "returns a 403 for unsigned request" do
272
+ file = Refile.store.upload(StringIO.new("hello"))
273
+
274
+ get "/eviltoken/store/reverse/#{file.id}/hello"
275
+
276
+ expect(last_response.status).to eq(403)
277
+ expect(last_response.body).to eq("forbidden")
278
+ end
279
+ end
280
+
281
+ describe "POST /:backend" do
282
+ it "uploads a file for direct upload backends" do
283
+ file = Rack::Test::UploadedFile.new(path("hello.txt"))
284
+ post "/cache", file: file
285
+
286
+ expect(last_response.status).to eq(200)
287
+ expect(JSON.parse(last_response.body)["id"]).not_to be_empty
288
+ end
289
+
290
+ it "does not require signed request param to upload" do
291
+ allow(Refile).to receive(:secret_key).and_return("abcd1234")
292
+
293
+ file = Rack::Test::UploadedFile.new(path("hello.txt"))
294
+ post "/cache", file: file
295
+
296
+ expect(last_response.status).to eq(200)
297
+ expect(JSON.parse(last_response.body)["id"]).not_to be_empty
298
+ end
299
+
300
+ it "returns the url of the uploaded file" do
301
+ file = Rack::Test::UploadedFile.new(path("hello.txt"))
302
+ post "/cache", file: file
303
+
304
+ expect(last_response.status).to eq(200)
305
+ expect(JSON.parse(last_response.body)["url"]).not_to be_empty
306
+ expect(JSON.parse(last_response.body)["url"]).to include("hello.txt")
307
+ expect(JSON.parse(last_response.body)["url"]).to include("cache")
308
+ end
309
+
310
+ context "when unrestricted" do
311
+ before do
312
+ allow(Refile).to receive(:allow_uploads_to).and_return(:all)
313
+ end
314
+
315
+ it "allows uploads to all backends" do
316
+ post "/store", file: Rack::Test::UploadedFile.new(path("hello.txt"))
317
+ expect(last_response.status).to eq(200)
318
+ end
319
+ end
320
+
321
+ context "when restricted" do
322
+ before do
323
+ allow(Refile).to receive(:allow_uploads_to).and_return(["cache"])
324
+ end
325
+
326
+ it "allows uploads to allowed backends" do
327
+ post "/cache", file: Rack::Test::UploadedFile.new(path("hello.txt"))
328
+ expect(last_response.status).to eq(200)
329
+ end
330
+
331
+ it "returns 404 if backend is not allowed" do
332
+ post "/store", file: Rack::Test::UploadedFile.new(path("hello.txt"))
333
+ expect(last_response.status).to eq(404)
334
+ end
335
+ end
336
+
337
+ context "when file is invalid" do
338
+ before do
339
+ allow(Refile).to receive(:allow_uploads_to).and_return(:all)
340
+ end
341
+
342
+ context "when file is too big" do
343
+ before do
344
+ backend = double
345
+ allow(backend).to receive(:upload).with(anything).and_raise(Refile::InvalidMaxSize)
346
+ allow_any_instance_of(Refile::App).to receive(:backend).and_return(backend)
347
+ end
348
+
349
+ it "returns 413 if file is too big" do
350
+ post "/store_max_size", file: Rack::Test::UploadedFile.new(path("hello.txt"))
351
+ expect(last_response.status).to eq(413)
352
+ end
353
+ end
354
+
355
+ context "when other unexpected exception happens" do
356
+ before do
357
+ backend = double
358
+ allow(backend).to receive(:upload).with(anything).and_raise(Refile::InvalidFile)
359
+ allow_any_instance_of(Refile::App).to receive(:backend).and_return(backend)
360
+ end
361
+
362
+ it "returns 400 if file is too big" do
363
+ post "/store_max_size", file: Rack::Test::UploadedFile.new(path("hello.txt"))
364
+ expect(last_response.status).to eq(400)
365
+ end
366
+ end
367
+ end
368
+ end
369
+
370
+ describe "GET /:backend/presign" do
371
+ it "returns presign signature" do
372
+ get "/limited_cache/presign"
373
+
374
+ expect(last_response.status).to eq(200)
375
+ result = JSON.parse(last_response.body)
376
+ expect(result["id"]).not_to be_empty
377
+ expect(result["url"]).to eq("/presigned/posts/upload")
378
+ expect(result["as"]).to eq("file")
379
+ end
380
+
381
+ context "when unrestricted" do
382
+ before do
383
+ allow(Refile).to receive(:allow_uploads_to).and_return(:all)
384
+ end
385
+
386
+ it "gets signatures from all backends" do
387
+ get "/limited_cache/presign"
388
+ expect(last_response.status).to eq(200)
389
+ end
390
+ end
391
+
392
+ context "when restricted" do
393
+ before do
394
+ allow(Refile).to receive(:allow_uploads_to).and_return(["limited_cache"])
395
+ end
396
+
397
+ it "gets signatures from allowed backend" do
398
+ get "/limited_cache/presign"
399
+ expect(last_response.status).to eq(200)
400
+ end
401
+
402
+ it "returns 404 if backend is not allowed" do
403
+ get "/store/presign"
404
+ expect(last_response.status).to eq(404)
405
+ end
406
+ end
407
+ end
408
+
409
+ it "returns a 404 if id not given" do
410
+ get "/token/store"
411
+
412
+ expect(last_response.status).to eq(404)
413
+ expect(last_response.content_type).to eq("text/plain;charset=utf-8")
414
+ expect(last_response.body).to eq("not found")
415
+ end
416
+
417
+ it "returns a 404 for root" do
418
+ get "/"
419
+
420
+ expect(last_response.status).to eq(404)
421
+ expect(last_response.content_type).to eq("text/plain;charset=utf-8")
422
+ expect(last_response.body).to eq("not found")
423
+ end
424
+ end