refile 0.5.5 → 0.6.0

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 (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")