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