refile 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/refile.rb +252 -27
  3. data/lib/refile/app.rb +55 -14
  4. data/lib/refile/attacher.rb +39 -40
  5. data/lib/refile/attachment.rb +28 -13
  6. data/lib/refile/attachment/active_record.rb +90 -1
  7. data/lib/refile/attachment_definition.rb +47 -0
  8. data/lib/refile/backend/s3.rb +1 -147
  9. data/lib/refile/backend_macros.rb +13 -5
  10. data/lib/refile/custom_logger.rb +3 -1
  11. data/lib/refile/file.rb +9 -0
  12. data/lib/refile/image_processing.rb +1 -143
  13. data/lib/refile/rails.rb +30 -0
  14. data/lib/refile/rails/attachment_helper.rb +27 -16
  15. data/lib/refile/signature.rb +5 -0
  16. data/lib/refile/simple_form.rb +17 -0
  17. data/lib/refile/version.rb +1 -1
  18. data/spec/refile/active_record_helper.rb +11 -0
  19. data/spec/refile/app_spec.rb +197 -20
  20. data/spec/refile/attachment/active_record_spec.rb +298 -1
  21. data/spec/refile/attachment_helper_spec.rb +39 -0
  22. data/spec/refile/attachment_spec.rb +53 -5
  23. data/spec/refile/backend_examples.rb +13 -2
  24. data/spec/refile/backend_macros_spec.rb +27 -6
  25. data/spec/refile/custom_logger_spec.rb +2 -3
  26. data/spec/refile/features/direct_upload_spec.rb +18 -0
  27. data/spec/refile/features/multiple_upload_spec.rb +122 -0
  28. data/spec/refile/features/normal_upload_spec.rb +5 -3
  29. data/spec/refile/features/presigned_upload_spec.rb +4 -0
  30. data/spec/refile/features/simple_form_spec.rb +8 -0
  31. data/spec/refile/fixtures/monkey.txt +1 -0
  32. data/spec/refile/fixtures/world.txt +1 -0
  33. data/spec/refile/spec_helper.rb +21 -11
  34. data/spec/refile_spec.rb +253 -24
  35. metadata +12 -303
  36. data/.gitignore +0 -27
  37. data/.rspec +0 -2
  38. data/.rubocop.yml +0 -68
  39. data/.travis.yml +0 -21
  40. data/.yardopts +0 -1
  41. data/CONTRIBUTING.md +0 -33
  42. data/Gemfile +0 -3
  43. data/History.md +0 -96
  44. data/LICENSE.txt +0 -22
  45. data/README.md +0 -651
  46. data/Rakefile +0 -19
  47. data/app/assets/javascripts/refile.js +0 -63
  48. data/config.ru +0 -8
  49. data/config/locales/en.yml +0 -8
  50. data/config/routes.rb +0 -5
  51. data/refile.gemspec +0 -42
  52. data/spec/refile/backend/s3_spec.rb +0 -11
  53. data/spec/refile/test_app.rb +0 -65
  54. data/spec/refile/test_app/app/assets/javascripts/application.js +0 -42
  55. data/spec/refile/test_app/app/controllers/application_controller.rb +0 -2
  56. data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +0 -15
  57. data/spec/refile/test_app/app/controllers/home_controller.rb +0 -4
  58. data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +0 -48
  59. data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +0 -31
  60. data/spec/refile/test_app/app/models/post.rb +0 -5
  61. data/spec/refile/test_app/app/views/direct_posts/new.html.erb +0 -20
  62. data/spec/refile/test_app/app/views/home/index.html.erb +0 -1
  63. data/spec/refile/test_app/app/views/layouts/application.html.erb +0 -14
  64. data/spec/refile/test_app/app/views/normal_posts/_form.html.erb +0 -28
  65. data/spec/refile/test_app/app/views/normal_posts/edit.html.erb +0 -1
  66. data/spec/refile/test_app/app/views/normal_posts/index.html +0 -5
  67. data/spec/refile/test_app/app/views/normal_posts/new.html.erb +0 -1
  68. data/spec/refile/test_app/app/views/normal_posts/show.html.erb +0 -19
  69. data/spec/refile/test_app/app/views/presigned_posts/new.html.erb +0 -16
  70. data/spec/refile/test_app/config/database.yml +0 -7
  71. data/spec/refile/test_app/config/routes.rb +0 -17
  72. data/spec/refile/test_app/public/favicon.ico +0 -0
@@ -6,8 +6,8 @@ module Refile
6
6
  def verify_id(method)
7
7
  mod = Module.new do
8
8
  define_method(method) do |id|
9
- id = id.to_s
10
- if id =~ /\A[a-z0-9]+\z/i
9
+ id = self.class.decode_id(id)
10
+ if self.class.valid_id?(id)
11
11
  super(id)
12
12
  else
13
13
  raise Refile::InvalidID
@@ -20,18 +20,26 @@ module Refile
20
20
  def verify_uploadable(method)
21
21
  mod = Module.new do
22
22
  define_method(method) do |uploadable|
23
- [:size, :read, :eof?, :close].each do |m|
23
+ [:size, :read, :eof?, :rewind, :close].each do |m|
24
24
  unless uploadable.respond_to?(m)
25
- raise ArgumentError, "does not respond to `#{m}`."
25
+ raise Refile::InvalidFile, "does not respond to `#{m}`."
26
26
  end
27
27
  end
28
28
  if max_size and uploadable.size > max_size
29
- raise Refile::Invalid, "#{uploadable.inspect} is too large"
29
+ raise Refile::InvalidMaxSize, "#{uploadable.inspect} is too large"
30
30
  end
31
31
  super(uploadable)
32
32
  end
33
33
  end
34
34
  prepend mod
35
35
  end
36
+
37
+ def valid_id?(id)
38
+ id =~ /\A[a-z0-9]+\z/i
39
+ end
40
+
41
+ def decode_id(id)
42
+ id.to_s
43
+ end
36
44
  end
37
45
  end
@@ -6,7 +6,9 @@ module Refile
6
6
  LOG_FORMAT = %(%s: [%s] %s "%s%s" %d %0.1fms\n)
7
7
 
8
8
  def initialize(app, prefix, logger_proc)
9
- @app, @prefix, @logger_proc = app, prefix, logger_proc
9
+ @app = app
10
+ @prefix = prefix
11
+ @logger_proc = logger_proc
10
12
  end
11
13
 
12
14
  def call(env)
@@ -71,9 +71,18 @@ module Refile
71
71
 
72
72
  Tempfile.new(id, binmode: true).tap do |tempfile|
73
73
  IO.copy_stream(io, tempfile)
74
+ tempfile.rewind
75
+ tempfile.fsync
74
76
  end
75
77
  end
76
78
 
79
+ # Rewind to beginning of file.
80
+ #
81
+ # @return [nil]
82
+ def rewind
83
+ @io = nil
84
+ end
85
+
77
86
  private
78
87
 
79
88
  def io
@@ -1,143 +1 @@
1
- require "refile"
2
- require "mini_magick"
3
-
4
- module Refile
5
- # Processes images via MiniMagick, resizing cropping and padding them.
6
- class ImageProcessor
7
- # @param [Symbol] method The method to invoke on {#call}
8
- def initialize(method)
9
- @method = method
10
- end
11
-
12
- # Changes the image encoding format to the given format
13
- #
14
- # @see http://www.imagemagick.org/script/command-line-options.php#format
15
- # @param [MiniMagick::Image] img the image to convert
16
- # @param [String] format the format to convert to
17
- # @return [void]
18
- def convert(img, format)
19
- img.format(format.to_s.downcase, nil)
20
- end
21
-
22
- # Resize the image to fit within the specified dimensions while retaining
23
- # the original aspect ratio. Will only resize the image if it is larger
24
- # than the specified dimensions. The resulting image may be shorter or
25
- # narrower than specified in either dimension but will not be larger than
26
- # the specified values.
27
- #
28
- # @param [MiniMagick::Image] img the image to convert
29
- # @param [#to_s] width the maximum width
30
- # @param [#to_s] height the maximum height
31
- # @return [void]
32
- def limit(img, width, height)
33
- img.resize "#{width}x#{height}>"
34
- end
35
-
36
- # Resize the image to fit within the specified dimensions while retaining
37
- # the original aspect ratio. The image may be shorter or narrower than
38
- # specified in the smaller dimension but will not be larger than the
39
- # specified values.
40
- #
41
- # @param [MiniMagick::Image] img the image to convert
42
- # @param [#to_s] width the width to fit into
43
- # @param [#to_s] height the height to fit into
44
- # @return [void]
45
- def fit(img, width, height)
46
- img.resize "#{width}x#{height}"
47
- end
48
-
49
- # Resize the image so that it is at least as large in both dimensions as
50
- # specified, then crops any excess outside the specified dimensions.
51
- #
52
- # The resulting image will always be exactly as large as the specified
53
- # dimensions.
54
- #
55
- # By default, the center part of the image is kept, and the remainder
56
- # cropped off, but this can be changed via the `gravity` option.
57
- #
58
- # @param [MiniMagick::Image] img the image to convert
59
- # @param [#to_s] width the width to fill out
60
- # @param [#to_s] height the height to fill out
61
- # @param [String] gravity which part of the image to focus on
62
- # @return [void]
63
- # @see http://www.imagemagick.org/script/command-line-options.php#gravity
64
- def fill(img, width, height, gravity = "Center")
65
- # FIXME: test and rewrite to simpler implementation!
66
- width = width.to_i
67
- height = height.to_i
68
- cols, rows = img[:dimensions]
69
- img.combine_options do |cmd|
70
- if width != cols || height != rows
71
- scale_x = width / cols.to_f
72
- scale_y = height / rows.to_f
73
- if scale_x >= scale_y
74
- cols = (scale_x * (cols + 0.5)).round
75
- rows = (scale_x * (rows + 0.5)).round
76
- cmd.resize "#{cols}"
77
- else
78
- cols = (scale_y * (cols + 0.5)).round
79
- rows = (scale_y * (rows + 0.5)).round
80
- cmd.resize "x#{rows}"
81
- end
82
- end
83
- cmd.gravity gravity
84
- cmd.background "rgba(255,255,255,0.0)"
85
- cmd.extent "#{width}x#{height}" if cols != width || rows != height
86
- end
87
- end
88
-
89
- # resize the image to fit within the specified dimensions while retaining
90
- # the original aspect ratio in the same way as {#fill}. unlike {#fill} it
91
- # will, if necessary, pad the remaining area with the given color, which
92
- # defaults to transparent where supported by the image format and white
93
- # otherwise.
94
- #
95
- # the resulting image will always be exactly as large as the specified
96
- # dimensions.
97
- #
98
- # by default, the image will be placed in the center but this can be
99
- # changed via the `gravity` option.
100
- #
101
- # @param [minimagick::image] img the image to convert
102
- # @param [#to_s] width the width to fill out
103
- # @param [#to_s] height the height to fill out
104
- # @param [string] background the color to use as a background
105
- # @param [string] gravity which part of the image to focus on
106
- # @return [void]
107
- # @see http://www.imagemagick.org/script/color.php
108
- # @see http://www.imagemagick.org/script/command-line-options.php#gravity
109
- def pad(img, width, height, background = "transparent", gravity = "Center")
110
- img.combine_options do |cmd|
111
- cmd.thumbnail "#{width}x#{height}>"
112
- if background == "transparent"
113
- cmd.background "rgba(255, 255, 255, 0.0)"
114
- else
115
- cmd.background background
116
- end
117
- cmd.gravity gravity
118
- cmd.extent "#{width}x#{height}"
119
- end
120
- end
121
-
122
- # Process the given file. The file will be processed via one of the
123
- # instance methods of this class, depending on the `method` argument passed
124
- # to the constructor on initialization.
125
- #
126
- # If the format is given it will convert the image to the given file format.
127
- #
128
- # @param [Tempfile] file the file to manipulate
129
- # @param [String] format the file format to convert to
130
- # @return [File] the processed file
131
- def call(file, *args, format: nil)
132
- img = ::MiniMagick::Image.new(file.path)
133
- img.format(format.to_s.downcase, nil) if format
134
- send(@method, img, *args)
135
-
136
- ::File.open(img.path, "rb")
137
- end
138
- end
139
- end
140
-
141
- [:fill, :fit, :limit, :pad, :convert].each do |name|
142
- Refile.processor(name, Refile::ImageProcessor.new(name))
143
- end
1
+ raise "[Refile] image processing has been extracted into a separate gem, see https://github.com/refile/refile-mini_magick"
@@ -5,6 +5,11 @@ module Refile
5
5
  # @api private
6
6
  class Engine < Rails::Engine
7
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
+
8
13
  Refile.store ||= Refile::Backend::FileSystem.new(Rails.root.join("tmp/uploads/store").to_s)
9
14
  Refile.cache ||= Refile::Backend::FileSystem.new(Rails.root.join("tmp/uploads/cache").to_s)
10
15
 
@@ -20,5 +25,30 @@ module Refile
20
25
  Refile.logger = Rails.logger
21
26
  Refile.app = Refile::App.new
22
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
23
53
  end
24
54
  end
@@ -19,11 +19,17 @@ module Refile
19
19
  # @param [Refile::Attachment] object Instance of a class which has an attached file
20
20
  # @param [Symbol] name The name of the attachment column
21
21
  # @param [String, nil] filename The filename to be appended to the URL
22
+ # @param [String, nil] fallback The path to an asset to be used as a fallback
22
23
  # @param [String, nil] format A file extension to be appended to the URL
23
24
  # @param [String, nil] host Override the host
24
25
  # @return [String, nil] The generated URL
25
- def attachment_url(record, name, *args, **opts)
26
- Refile.attachment_url(record, name, *args, **opts)
26
+ def attachment_url(record, name, *args, fallback: nil, **opts)
27
+ file = record && record.public_send(name)
28
+ if file
29
+ Refile.attachment_url(record, name, *args, **opts)
30
+ elsif fallback
31
+ asset_url(fallback)
32
+ end
27
33
  end
28
34
 
29
35
  # Generates an image tag for the given attachment, adding appropriate
@@ -36,12 +42,12 @@ module Refile
36
42
  # @param [Hash] options Additional options for the image tag
37
43
  # @see #attachment_url
38
44
  # @return [ActiveSupport::SafeBuffer, nil] The generated image tag
39
- def attachment_image_tag(record, name, *args, fallback: nil, format: nil, host: nil, **options)
40
- file = record.send(name)
41
- classes = ["attachment", record.class.model_name.singular, name, *options[:class]]
45
+ def attachment_image_tag(record, name, *args, fallback: nil, host: nil, prefix: nil, format: nil, **options)
46
+ file = record && record.public_send(name)
47
+ classes = ["attachment", (record.class.model_name.singular if record), name, *options[:class]]
42
48
 
43
49
  if file
44
- image_tag(attachment_url(record, name, *args, format: format, host: host), options.merge(class: classes))
50
+ image_tag(attachment_url(record, name, *args, host: host, prefix: prefix, format: format), options.merge(class: classes))
45
51
  elsif fallback
46
52
  classes << "fallback"
47
53
  image_tag(fallback, options.merge(class: classes))
@@ -63,23 +69,28 @@ module Refile
63
69
  def attachment_field(object_name, method, object:, **options)
64
70
  options[:data] ||= {}
65
71
 
66
- attacher = object.send(:"#{method}_attacher")
67
- options[:accept] = attacher.accept
72
+ definition = object.send(:"#{method}_attachment_definition")
73
+ options[:accept] = definition.accept
68
74
 
69
75
  if options[:direct]
70
- host = options[:host] || Refile.host || request.base_url
71
- backend_name = Refile.backends.key(attacher.cache)
72
-
73
- url = ::File.join(host, main_app.refile_app_path, backend_name)
76
+ url = Refile.attachment_upload_url(object, method, host: options[:host], prefix: options[:prefix])
74
77
  options[:data].merge!(direct: true, as: "file", url: url)
75
78
  end
76
79
 
77
- if options[:presigned] and attacher.cache.respond_to?(:presign)
78
- options[:data].merge!(direct: true).merge!(attacher.cache.presign.as_json)
80
+ if options[:presigned] and definition.cache.respond_to?(:presign)
81
+ url = Refile.attachment_presign_url(object, method, host: options[:host], prefix: options[:prefix])
82
+ options[:data].merge!(direct: true, presigned: true, url: url)
79
83
  end
80
84
 
81
- html = hidden_field(object_name, method, value: attacher.data.to_json, object: object, id: nil)
82
- html + file_field(object_name, method, options)
85
+ options[:data][:reference] = SecureRandom.hex
86
+
87
+ hidden_field(object_name, method,
88
+ multiple: options[:multiple],
89
+ value: object.send("#{method}_data").try(:to_json),
90
+ object: object,
91
+ id: nil,
92
+ data: { reference: options[:data][:reference] }
93
+ ) + file_field(object_name, method, options)
83
94
  end
84
95
  end
85
96
  end
@@ -27,5 +27,10 @@ module Refile
27
27
  def as_json(*)
28
28
  { as: @as, id: @id, url: @url, fields: @fields }
29
29
  end
30
+
31
+ # @return [String] the signature serialized as JSON
32
+ def to_json(*)
33
+ as_json.to_json
34
+ end
30
35
  end
31
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
@@ -1,3 +1,3 @@
1
1
  module Refile
2
- VERSION = "0.5.5"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -11,6 +11,7 @@ ActiveRecord::Base.establish_connection(
11
11
  class TestMigration < ActiveRecord::Migration
12
12
  def self.up
13
13
  create_table :posts, force: true do |t|
14
+ t.integer :user_id
14
15
  t.column :title, :string
15
16
  t.column :image_id, :string
16
17
  t.column :document_id, :string
@@ -18,6 +19,16 @@ class TestMigration < ActiveRecord::Migration
18
19
  t.column :document_content_type, :string
19
20
  t.column :document_size, :integer
20
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
21
32
  end
22
33
  end
23
34
 
@@ -7,11 +7,15 @@ describe Refile::App do
7
7
  Refile::App.new
8
8
  end
9
9
 
10
+ before do
11
+ allow(Refile).to receive(:token).and_return("token")
12
+ end
13
+
10
14
  describe "GET /:backend/:id/:filename" do
11
15
  it "returns a stored file" do
12
16
  file = Refile.store.upload(StringIO.new("hello"))
13
17
 
14
- get "/store/#{file.id}/hello"
18
+ get "/token/store/#{file.id}/hello"
15
19
 
16
20
  expect(last_response.status).to eq(200)
17
21
  expect(last_response.body).to eq("hello")
@@ -20,7 +24,7 @@ describe Refile::App do
20
24
  it "sets appropriate content type from extension" do
21
25
  file = Refile.store.upload(StringIO.new("hello"))
22
26
 
23
- get "/store/#{file.id}/hello.html"
27
+ get "/token/store/#{file.id}/hello.html"
24
28
 
25
29
  expect(last_response.status).to eq(200)
26
30
  expect(last_response.body).to eq("hello")
@@ -30,7 +34,7 @@ describe Refile::App do
30
34
  it "returns a 404 if the file doesn't exist" do
31
35
  Refile.store.upload(StringIO.new("hello"))
32
36
 
33
- get "/store/doesnotexist/hello"
37
+ get "/token/store/doesnotexist/hello"
34
38
 
35
39
  expect(last_response.status).to eq(404)
36
40
  expect(last_response.content_type).to eq("text/plain;charset=utf-8")
@@ -40,7 +44,7 @@ describe Refile::App do
40
44
  it "returns a 404 if the backend doesn't exist" do
41
45
  file = Refile.store.upload(StringIO.new("hello"))
42
46
 
43
- get "/doesnotexist/#{file.id}/hello"
47
+ get "/token/doesnotexist/#{file.id}/hello"
44
48
 
45
49
  expect(last_response.status).to eq(404)
46
50
  expect(last_response.content_type).to eq("text/plain;charset=utf-8")
@@ -55,7 +59,7 @@ describe Refile::App do
55
59
  it "sets CORS header" do
56
60
  file = Refile.store.upload(StringIO.new("hello"))
57
61
 
58
- get "/store/#{file.id}/hello"
62
+ get "/token/store/#{file.id}/hello"
59
63
 
60
64
  expect(last_response.status).to eq(200)
61
65
  expect(last_response.body).to eq("hello")
@@ -66,7 +70,7 @@ describe Refile::App do
66
70
  it "returns a 200 for head requests" do
67
71
  file = Refile.store.upload(StringIO.new("hello"))
68
72
 
69
- head "/store/#{file.id}/hello"
73
+ head "/token/store/#{file.id}/hello"
70
74
 
71
75
  expect(last_response.status).to eq(200)
72
76
  expect(last_response.body).to be_empty
@@ -75,7 +79,7 @@ describe Refile::App do
75
79
  it "returns a 404 for head requests if the file doesn't exist" do
76
80
  Refile.store.upload(StringIO.new("hello"))
77
81
 
78
- head "/store/doesnotexist/hello"
82
+ head "/token/store/doesnotexist/hello"
79
83
 
80
84
  expect(last_response.status).to eq(404)
81
85
  expect(last_response.body).to be_empty
@@ -84,19 +88,84 @@ describe Refile::App do
84
88
  it "returns a 404 for non get requests" do
85
89
  file = Refile.store.upload(StringIO.new("hello"))
86
90
 
87
- post "/store/#{file.id}/hello"
91
+ post "/token/store/#{file.id}/hello"
88
92
 
89
93
  expect(last_response.status).to eq(404)
90
94
  expect(last_response.content_type).to eq("text/plain;charset=utf-8")
91
95
  expect(last_response.body).to eq("not found")
92
96
  end
97
+
98
+ context "verification" do
99
+ before do
100
+ allow(Refile).to receive(:token).and_call_original
101
+ end
102
+
103
+ it "accepts valid token" do
104
+ file = Refile.store.upload(StringIO.new("hello"))
105
+ token = Refile.token("/store/#{file.id}/hello")
106
+
107
+ get "/#{token}/store/#{file.id}/hello"
108
+
109
+ expect(last_response.status).to eq(200)
110
+ expect(last_response.body).to eq("hello")
111
+ end
112
+
113
+ it "returns a 403 for unsigned get requests" do
114
+ file = Refile.store.upload(StringIO.new("hello"))
115
+
116
+ get "/eviltoken/store/#{file.id}/hello"
117
+
118
+ expect(last_response.status).to eq(403)
119
+ expect(last_response.body).to eq("forbidden")
120
+ end
121
+
122
+ it "does not retrieve nor process files for unauthenticated requests" do
123
+ file = Refile.store.upload(StringIO.new("hello"))
124
+
125
+ expect(Refile.store).not_to receive(:get)
126
+ get "/eviltoken/store/#{file.id}/hello"
127
+
128
+ expect(last_response.status).to eq(403)
129
+ expect(last_response.body).to eq("forbidden")
130
+ end
131
+ end
132
+
133
+ context "when unrestricted" do
134
+ before do
135
+ allow(Refile).to receive(:allow_downloads_from).and_return(:all)
136
+ end
137
+
138
+ it "gets signatures from all backends" do
139
+ file = Refile.store.upload(StringIO.new("hello"))
140
+ get "/token/store/#{file.id}/test.txt"
141
+ expect(last_response.status).to eq(200)
142
+ end
143
+ end
144
+
145
+ context "when restricted" do
146
+ before do
147
+ allow(Refile).to receive(:allow_downloads_from).and_return(["store"])
148
+ end
149
+
150
+ it "gets signatures from allowed backend" do
151
+ file = Refile.store.upload(StringIO.new("hello"))
152
+ get "/token/store/#{file.id}/test.txt"
153
+ expect(last_response.status).to eq(200)
154
+ end
155
+
156
+ it "returns 404 if backend is not allowed" do
157
+ file = Refile.store.upload(StringIO.new("hello"))
158
+ get "/token/cache/#{file.id}/test.txt"
159
+ expect(last_response.status).to eq(404)
160
+ end
161
+ end
93
162
  end
94
163
 
95
164
  describe "GET /:backend/:processor/:id/:filename" do
96
165
  it "returns 404 if processor does not exist" do
97
166
  file = Refile.store.upload(StringIO.new("hello"))
98
167
 
99
- get "/store/doesnotexist/#{file.id}/hello"
168
+ get "/token/store/doesnotexist/#{file.id}/hello"
100
169
 
101
170
  expect(last_response.status).to eq(404)
102
171
  expect(last_response.content_type).to eq("text/plain;charset=utf-8")
@@ -106,7 +175,7 @@ describe Refile::App do
106
175
  it "applies block processor to file" do
107
176
  file = Refile.store.upload(StringIO.new("hello"))
108
177
 
109
- get "/store/reverse/#{file.id}/hello"
178
+ get "/token/store/reverse/#{file.id}/hello"
110
179
 
111
180
  expect(last_response.status).to eq(200)
112
181
  expect(last_response.body).to eq("olleh")
@@ -115,7 +184,7 @@ describe Refile::App do
115
184
  it "applies object processor to file" do
116
185
  file = Refile.store.upload(StringIO.new("hello"))
117
186
 
118
- get "/store/upcase/#{file.id}/hello"
187
+ get "/token/store/upcase/#{file.id}/hello"
119
188
 
120
189
  expect(last_response.status).to eq(200)
121
190
  expect(last_response.body).to eq("HELLO")
@@ -124,7 +193,7 @@ describe Refile::App do
124
193
  it "applies processor with arguments" do
125
194
  file = Refile.store.upload(StringIO.new("hello"))
126
195
 
127
- get "/store/concat/foo/bar/baz/#{file.id}/hello"
196
+ get "/token/store/concat/foo/bar/baz/#{file.id}/hello"
128
197
 
129
198
  expect(last_response.status).to eq(200)
130
199
  expect(last_response.body).to eq("hellofoobarbaz")
@@ -133,34 +202,142 @@ describe Refile::App do
133
202
  it "applies processor with format" do
134
203
  file = Refile.store.upload(StringIO.new("hello"))
135
204
 
136
- get "/store/convert_case/#{file.id}/hello.up"
205
+ get "/token/store/convert_case/#{file.id}/hello.up"
137
206
 
138
207
  expect(last_response.status).to eq(200)
139
208
  expect(last_response.body).to eq("HELLO")
140
209
  end
210
+
211
+ it "returns a 403 for unsigned request" do
212
+ file = Refile.store.upload(StringIO.new("hello"))
213
+
214
+ get "/eviltoken/store/reverse/#{file.id}/hello"
215
+
216
+ expect(last_response.status).to eq(403)
217
+ expect(last_response.body).to eq("forbidden")
218
+ end
141
219
  end
142
220
 
143
221
  describe "POST /:backend" do
144
- it "returns 404 if backend is not marked as direct upload" do
222
+ it "uploads a file for direct upload backends" do
145
223
  file = Rack::Test::UploadedFile.new(path("hello.txt"))
146
- post "/store", file: file
224
+ post "/cache", file: file
147
225
 
148
- expect(last_response.status).to eq(404)
149
- expect(last_response.content_type).to eq("text/plain;charset=utf-8")
150
- expect(last_response.body).to eq("not found")
226
+ expect(last_response.status).to eq(200)
227
+ expect(JSON.parse(last_response.body)["id"]).not_to be_empty
151
228
  end
152
229
 
153
- it "uploads a file for direct upload backends" do
230
+ it "does not require signed request param to upload" do
231
+ allow(Refile).to receive(:secret_key).and_return("abcd1234")
232
+
154
233
  file = Rack::Test::UploadedFile.new(path("hello.txt"))
155
234
  post "/cache", file: file
156
235
 
157
236
  expect(last_response.status).to eq(200)
158
237
  expect(JSON.parse(last_response.body)["id"]).not_to be_empty
159
238
  end
239
+
240
+ context "when unrestricted" do
241
+ before do
242
+ allow(Refile).to receive(:allow_uploads_to).and_return(:all)
243
+ end
244
+
245
+ it "allows uploads to all backends" do
246
+ post "/store", file: Rack::Test::UploadedFile.new(path("hello.txt"))
247
+ expect(last_response.status).to eq(200)
248
+ end
249
+ end
250
+
251
+ context "when restricted" do
252
+ before do
253
+ allow(Refile).to receive(:allow_uploads_to).and_return(["cache"])
254
+ end
255
+
256
+ it "allows uploads to allowed backends" do
257
+ post "/cache", file: Rack::Test::UploadedFile.new(path("hello.txt"))
258
+ expect(last_response.status).to eq(200)
259
+ end
260
+
261
+ it "returns 404 if backend is not allowed" do
262
+ post "/store", file: Rack::Test::UploadedFile.new(path("hello.txt"))
263
+ expect(last_response.status).to eq(404)
264
+ end
265
+ end
266
+
267
+ context "when file is invalid" do
268
+ before do
269
+ allow(Refile).to receive(:allow_uploads_to).and_return(:all)
270
+ end
271
+
272
+ context "when file is too big" do
273
+ before do
274
+ backend = double
275
+ allow(backend).to receive(:upload).with(anything).and_raise(Refile::InvalidMaxSize)
276
+ allow_any_instance_of(Refile::App).to receive(:backend).and_return(backend)
277
+ end
278
+
279
+ it "returns 413 if file is too big" do
280
+ post "/store_max_size", file: Rack::Test::UploadedFile.new(path("hello.txt"))
281
+ expect(last_response.status).to eq(413)
282
+ end
283
+ end
284
+
285
+ context "when other unexpected exception happens" do
286
+ before do
287
+ backend = double
288
+ allow(backend).to receive(:upload).with(anything).and_raise(Refile::InvalidFile)
289
+ allow_any_instance_of(Refile::App).to receive(:backend).and_return(backend)
290
+ end
291
+
292
+ it "returns 400 if file is too big" do
293
+ post "/store_max_size", file: Rack::Test::UploadedFile.new(path("hello.txt"))
294
+ expect(last_response.status).to eq(400)
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ describe "GET /:backend/presign" do
301
+ it "returns presign signature" do
302
+ get "/limited_cache/presign"
303
+
304
+ expect(last_response.status).to eq(200)
305
+ result = JSON.parse(last_response.body)
306
+ expect(result["id"]).not_to be_empty
307
+ expect(result["url"]).to eq("/presigned/posts/upload")
308
+ expect(result["as"]).to eq("file")
309
+ end
310
+
311
+ context "when unrestricted" do
312
+ before do
313
+ allow(Refile).to receive(:allow_uploads_to).and_return(:all)
314
+ end
315
+
316
+ it "gets signatures from all backends" do
317
+ get "/limited_cache/presign"
318
+ expect(last_response.status).to eq(200)
319
+ end
320
+ end
321
+
322
+ context "when restricted" do
323
+ before do
324
+ allow(Refile).to receive(:allow_uploads_to).and_return(["limited_cache"])
325
+ end
326
+
327
+ it "gets signatures from allowed backend" do
328
+ get "/limited_cache/presign"
329
+ expect(last_response.status).to eq(200)
330
+ end
331
+
332
+ it "returns 404 if backend is not allowed" do
333
+ get "/store/presign"
334
+ expect(last_response.status).to eq(404)
335
+ end
336
+ end
160
337
  end
161
338
 
162
339
  it "returns a 404 if id not given" do
163
- get "/store"
340
+ get "/token/store"
164
341
 
165
342
  expect(last_response.status).to eq(404)
166
343
  expect(last_response.content_type).to eq("text/plain;charset=utf-8")