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.
- checksums.yaml +7 -0
- data/app/assets/javascripts/refile.js +125 -0
- data/config/locales/en.yml +10 -0
- data/config/routes.rb +5 -0
- data/lib/refile.rb +510 -0
- data/lib/refile/app.rb +186 -0
- data/lib/refile/attacher.rb +190 -0
- data/lib/refile/attachment.rb +108 -0
- data/lib/refile/attachment/active_record.rb +133 -0
- data/lib/refile/attachment_definition.rb +83 -0
- data/lib/refile/backend/file_system.rb +120 -0
- data/lib/refile/backend/s3.rb +1 -0
- data/lib/refile/backend_macros.rb +45 -0
- data/lib/refile/custom_logger.rb +48 -0
- data/lib/refile/file.rb +102 -0
- data/lib/refile/file_double.rb +13 -0
- data/lib/refile/image_processing.rb +1 -0
- data/lib/refile/rails.rb +54 -0
- data/lib/refile/rails/attachment_helper.rb +121 -0
- data/lib/refile/random_hasher.rb +11 -0
- data/lib/refile/signature.rb +36 -0
- data/lib/refile/simple_form.rb +17 -0
- data/lib/refile/type.rb +28 -0
- data/lib/refile/version.rb +3 -0
- data/spec/refile/active_record_helper.rb +35 -0
- data/spec/refile/app_spec.rb +424 -0
- data/spec/refile/attachment/active_record_spec.rb +568 -0
- data/spec/refile/attachment_helper_spec.rb +78 -0
- data/spec/refile/attachment_spec.rb +589 -0
- data/spec/refile/backend/file_system_spec.rb +5 -0
- data/spec/refile/backend_examples.rb +228 -0
- data/spec/refile/backend_macros_spec.rb +83 -0
- data/spec/refile/custom_logger_spec.rb +21 -0
- data/spec/refile/features/direct_upload_spec.rb +63 -0
- data/spec/refile/features/multiple_upload_spec.rb +122 -0
- data/spec/refile/features/normal_upload_spec.rb +144 -0
- data/spec/refile/features/presigned_upload_spec.rb +31 -0
- data/spec/refile/features/simple_form_spec.rb +8 -0
- data/spec/refile/fixtures/hello.txt +1 -0
- data/spec/refile/fixtures/image.jpg +0 -0
- data/spec/refile/fixtures/large.txt +44 -0
- data/spec/refile/fixtures/monkey.txt +1 -0
- data/spec/refile/fixtures/world.txt +1 -0
- data/spec/refile/spec_helper.rb +72 -0
- data/spec/refile_spec.rb +355 -0
- 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"
|
data/lib/refile/rails.rb
ADDED
@@ -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,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
|
data/lib/refile/type.rb
ADDED
@@ -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,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
|