refile 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of refile might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -2
- data/.travis.yml +2 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +33 -0
- data/History.md +9 -0
- data/README.md +67 -16
- data/app/assets/javascripts/refile.js +19 -17
- data/lib/refile.rb +36 -6
- data/lib/refile/app.rb +15 -12
- data/lib/refile/attacher.rb +119 -49
- data/lib/refile/attachment.rb +29 -16
- data/lib/refile/attachment/active_record.rb +5 -2
- data/lib/refile/backend/file_system.rb +61 -1
- data/lib/refile/backend/s3.rb +66 -0
- data/lib/refile/custom_logger.rb +46 -0
- data/lib/refile/file.rb +32 -1
- data/lib/refile/image_processing.rb +72 -3
- data/lib/refile/rails.rb +2 -8
- data/lib/refile/rails/attachment_helper.rb +77 -19
- data/lib/refile/signature.rb +16 -1
- data/lib/refile/type.rb +28 -0
- data/lib/refile/version.rb +1 -1
- data/refile.gemspec +1 -1
- data/spec/refile/active_record_helper.rb +27 -0
- data/spec/refile/attachment/active_record_spec.rb +92 -0
- data/spec/refile/attachment_spec.rb +153 -28
- data/spec/refile/custom_logger_spec.rb +22 -0
- data/spec/refile/features/direct_upload_spec.rb +19 -2
- data/spec/refile/features/normal_upload_spec.rb +41 -11
- data/spec/refile/features/presigned_upload_spec.rb +1 -2
- data/spec/refile/rails/attachment_helper_spec.rb +1 -1
- data/spec/refile/test_app.rb +16 -14
- data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +1 -1
- data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +1 -1
- data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +1 -1
- data/spec/refile/test_app/app/views/direct_posts/new.html.erb +4 -0
- data/spec/refile/test_app/app/views/normal_posts/show.html.erb +5 -3
- metadata +27 -17
data/lib/refile/signature.rb
CHANGED
@@ -1,7 +1,21 @@
|
|
1
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.
|
2
5
|
class Signature
|
3
|
-
|
6
|
+
# @return [String] the name of the field that the file will be uploaded as.
|
7
|
+
attr_reader :as
|
4
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
|
5
19
|
def initialize(as:, id:, url:, fields:)
|
6
20
|
@as = as
|
7
21
|
@id = id
|
@@ -9,6 +23,7 @@ module Refile
|
|
9
23
|
@fields = fields
|
10
24
|
end
|
11
25
|
|
26
|
+
# @return [Hash{Symbol => Object}] an object suitable for serialization to JSON
|
12
27
|
def as_json(*)
|
13
28
|
{ as: @as, id: @id, url: @url, fields: @fields }
|
14
29
|
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
|
data/lib/refile/version.rb
CHANGED
data/refile.gemspec
CHANGED
@@ -19,7 +19,6 @@ Gem::Specification.new do |spec|
|
|
19
19
|
|
20
20
|
spec.required_ruby_version = ">= 2.1.0"
|
21
21
|
|
22
|
-
spec.add_dependency "rest-client", "~> 1.7.2"
|
23
22
|
spec.add_dependency "sinatra", "~> 1.4.5"
|
24
23
|
spec.add_dependency "mime-types"
|
25
24
|
|
@@ -38,4 +37,5 @@ Gem::Specification.new do |spec|
|
|
38
37
|
spec.add_development_dependency "selenium-webdriver"
|
39
38
|
spec.add_development_dependency "yard"
|
40
39
|
spec.add_development_dependency "rubocop"
|
40
|
+
spec.add_development_dependency "redcarpet"
|
41
41
|
end
|
@@ -0,0 +1,27 @@
|
|
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.column :title, :string
|
15
|
+
t.column :image_id, :string
|
16
|
+
t.column :image_content_type, :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
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
quietly do
|
26
|
+
TestMigration.up
|
27
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "refile/active_record_helper"
|
2
|
+
require "refile/attachment/active_record"
|
3
|
+
|
4
|
+
describe Refile::ActiveRecord::Attachment do
|
5
|
+
let(:options) { {} }
|
6
|
+
let(:klass) do
|
7
|
+
opts = options
|
8
|
+
Class.new(ActiveRecord::Base) do
|
9
|
+
self.table_name = :posts
|
10
|
+
|
11
|
+
def self.name
|
12
|
+
"Post"
|
13
|
+
end
|
14
|
+
|
15
|
+
attachment :document, **opts
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#valid?" do
|
20
|
+
let(:options) { { type: :image } }
|
21
|
+
|
22
|
+
context "with file" do
|
23
|
+
it "returns true when no file is assigned" do
|
24
|
+
post = klass.new
|
25
|
+
expect(post.valid?).to be_truthy
|
26
|
+
expect(post.errors[:document]).to be_empty
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns false when type is invalid" do
|
30
|
+
post = klass.new
|
31
|
+
post.document = Refile::FileDouble.new("hello", content_type: "text/plain")
|
32
|
+
expect(post.valid?).to be_falsy
|
33
|
+
expect(post.errors[:document].length).to eq(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "returns true when type is invalid" do
|
37
|
+
post = klass.new
|
38
|
+
post.document = Refile::FileDouble.new("hello", content_type: "image/png")
|
39
|
+
expect(post.valid?).to be_truthy
|
40
|
+
expect(post.errors[:document]).to be_empty
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "with metadata" do
|
45
|
+
it "returns false when metadata doesn't have an id" do
|
46
|
+
Refile.cache.upload(StringIO.new("hello"))
|
47
|
+
post = klass.new
|
48
|
+
post.document = { content_type: "text/png" }.to_json
|
49
|
+
expect(post.valid?).to be_falsy
|
50
|
+
expect(post.errors[:document].length).to eq(1)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns false when type is invalid" do
|
54
|
+
file = Refile.cache.upload(StringIO.new("hello"))
|
55
|
+
post = klass.new
|
56
|
+
post.document = { id: file.id, content_type: "text/png" }.to_json
|
57
|
+
expect(post.valid?).to be_falsy
|
58
|
+
expect(post.errors[:document].length).to eq(1)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns true when type is invalid" do
|
62
|
+
file = Refile.cache.upload(StringIO.new("hello"))
|
63
|
+
post = klass.new
|
64
|
+
post.document = { id: file.id, content_type: "image/png" }.to_json
|
65
|
+
expect(post.valid?).to be_truthy
|
66
|
+
expect(post.errors[:document]).to be_empty
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#save" do
|
72
|
+
it "stores the assigned file" do
|
73
|
+
post = klass.new
|
74
|
+
post.document = Refile::FileDouble.new("hello")
|
75
|
+
post.save
|
76
|
+
post = klass.find(post.id)
|
77
|
+
expect(post.document.read).to eq("hello")
|
78
|
+
expect(Refile.store.read(post.document.id)).to eq("hello")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#destroy" do
|
83
|
+
it "removes the stored file" do
|
84
|
+
post = klass.new
|
85
|
+
post.document = Refile::FileDouble.new("hello")
|
86
|
+
post.save
|
87
|
+
file = post.document
|
88
|
+
post.destroy
|
89
|
+
expect(file.exists?).to be_falsy
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -5,7 +5,7 @@ describe Refile::Attachment do
|
|
5
5
|
Class.new do
|
6
6
|
extend Refile::Attachment
|
7
7
|
|
8
|
-
attr_accessor :document_id, :
|
8
|
+
attr_accessor :document_id, :document_filename, :document_size, :document_content_type
|
9
9
|
|
10
10
|
attachment :document, **opts
|
11
11
|
end
|
@@ -14,10 +14,46 @@ describe Refile::Attachment do
|
|
14
14
|
|
15
15
|
describe ":name=" do
|
16
16
|
it "receives a file, caches it and sets the _id parameter" do
|
17
|
-
instance.document = Refile::FileDouble.new("hello")
|
17
|
+
instance.document = Refile::FileDouble.new("hello", "foo.txt", content_type: "text/plain")
|
18
|
+
|
19
|
+
expect(instance.document.read).to eq("hello")
|
20
|
+
|
21
|
+
expect(instance.document_attacher.data[:filename]).to eq("foo.txt")
|
22
|
+
expect(instance.document_attacher.data[:size]).to eq(5)
|
23
|
+
expect(instance.document_attacher.data[:content_type]).to eq("text/plain")
|
24
|
+
|
25
|
+
expect(instance.document_filename).to eq("foo.txt")
|
26
|
+
expect(instance.document_size).to eq(5)
|
27
|
+
expect(instance.document_content_type).to eq("text/plain")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "receives serialized data and retrieves file from it" do
|
31
|
+
file = Refile.cache.upload(Refile::FileDouble.new("hello"))
|
32
|
+
instance.document = { id: file.id, filename: "foo.txt", content_type: "text/plain", size: 5 }.to_json
|
33
|
+
|
34
|
+
expect(instance.document.read).to eq("hello")
|
35
|
+
|
36
|
+
expect(instance.document_attacher.data[:filename]).to eq("foo.txt")
|
37
|
+
expect(instance.document_attacher.data[:size]).to eq(5)
|
38
|
+
expect(instance.document_attacher.data[:content_type]).to eq("text/plain")
|
18
39
|
|
19
|
-
expect(
|
20
|
-
expect(
|
40
|
+
expect(instance.document_filename).to eq("foo.txt")
|
41
|
+
expect(instance.document_size).to eq(5)
|
42
|
+
expect(instance.document_content_type).to eq("text/plain")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "does nothing when assigned string lacks an id" do
|
46
|
+
instance.document = { size: 5 }.to_json
|
47
|
+
|
48
|
+
expect(instance.document).to be_nil
|
49
|
+
expect(instance.document_size).to be_nil
|
50
|
+
end
|
51
|
+
|
52
|
+
it "does nothing when assigned string is not valid JSON" do
|
53
|
+
instance.document = "size:f{oo}"
|
54
|
+
|
55
|
+
expect(instance.document).to be_nil
|
56
|
+
expect(instance.document_size).to be_nil
|
21
57
|
end
|
22
58
|
end
|
23
59
|
|
@@ -43,13 +79,26 @@ describe Refile::Attachment do
|
|
43
79
|
|
44
80
|
context "without redirects" do
|
45
81
|
before(:each) do
|
46
|
-
stub_request(:get, "http://www.example.com/some_file").to_return(
|
82
|
+
stub_request(:get, "http://www.example.com/some_file.txt").to_return(
|
83
|
+
status: 200,
|
84
|
+
body: "abc",
|
85
|
+
headers: { "Content-Length" => 3, "Content-Type" => "text/plain" }
|
86
|
+
)
|
47
87
|
end
|
48
88
|
|
49
|
-
it "downloads file, caches it and sets the _id parameter" do
|
50
|
-
instance.remote_document_url = "http://www.example.com/some_file"
|
89
|
+
it "downloads file, caches it and sets the _id parameter and metadata" do
|
90
|
+
instance.remote_document_url = "http://www.example.com/some_file.txt"
|
91
|
+
expect(instance.document.read).to eq("abc")
|
92
|
+
|
93
|
+
expect(instance.document_attacher.data[:filename]).to eq("some_file.txt")
|
94
|
+
expect(instance.document_attacher.data[:size]).to eq(3)
|
95
|
+
expect(instance.document_attacher.data[:content_type]).to eq("text/plain")
|
96
|
+
|
97
|
+
expect(instance.document_filename).to eq("some_file.txt")
|
98
|
+
expect(instance.document_size).to eq(3)
|
99
|
+
expect(instance.document_content_type).to eq("text/plain")
|
100
|
+
|
51
101
|
expect(Refile.cache.get(instance.document.id).read).to eq("abc")
|
52
|
-
expect(Refile.cache.get(instance.document_cache_id).read).to eq("abc")
|
53
102
|
end
|
54
103
|
end
|
55
104
|
|
@@ -62,8 +111,9 @@ describe Refile::Attachment do
|
|
62
111
|
|
63
112
|
it "follows redirects and fetches the file, caches it and sets the _id parameter" do
|
64
113
|
instance.remote_document_url = "http://www.example.com/1"
|
114
|
+
expect(instance.document_filename).to eq("2")
|
115
|
+
expect(instance.document.read).to eq("woop")
|
65
116
|
expect(Refile.cache.get(instance.document.id).read).to eq("woop")
|
66
|
-
expect(Refile.cache.get(instance.document_cache_id).read).to eq("woop")
|
67
117
|
end
|
68
118
|
|
69
119
|
context "when errors enabled" do
|
@@ -71,7 +121,7 @@ describe Refile::Attachment do
|
|
71
121
|
it "handles redirect loops by trowing errors" do
|
72
122
|
expect do
|
73
123
|
instance.remote_document_url = "http://www.example.com/loop"
|
74
|
-
end.to raise_error(
|
124
|
+
end.to raise_error(RuntimeError, /redirection loop/)
|
75
125
|
end
|
76
126
|
end
|
77
127
|
|
@@ -88,15 +138,6 @@ describe Refile::Attachment do
|
|
88
138
|
end
|
89
139
|
end
|
90
140
|
|
91
|
-
describe ":name_cache_id" do
|
92
|
-
it "doesn't overwrite a cached file" do
|
93
|
-
instance.document = Refile::FileDouble.new("hello")
|
94
|
-
instance.document_cache_id = "xyz"
|
95
|
-
|
96
|
-
expect(instance.document.read).to eq("hello")
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
141
|
describe ":name_attacher.store!" do
|
101
142
|
it "puts a cached file into the store" do
|
102
143
|
instance.document = Refile::FileDouble.new("hello")
|
@@ -107,7 +148,8 @@ describe Refile::Attachment do
|
|
107
148
|
expect(Refile.store.get(instance.document_id).read).to eq("hello")
|
108
149
|
expect(Refile.store.get(instance.document.id).read).to eq("hello")
|
109
150
|
|
110
|
-
expect(instance.
|
151
|
+
expect(instance.document.read).to eq("hello")
|
152
|
+
expect(instance.document_size).to eq(5)
|
111
153
|
expect(Refile.cache.get(cache.id).exists?).to be_falsy
|
112
154
|
end
|
113
155
|
|
@@ -133,7 +175,7 @@ describe Refile::Attachment do
|
|
133
175
|
expect(Refile.store.get(instance.document_id).read).to eq("world")
|
134
176
|
expect(Refile.store.get(instance.document.id).read).to eq("world")
|
135
177
|
|
136
|
-
expect(instance.
|
178
|
+
expect(instance.document.read).to eq("world")
|
137
179
|
expect(Refile.cache.get(cache.id).exists?).to be_falsy
|
138
180
|
expect(Refile.store.get(file.id).exists?).to be_falsy
|
139
181
|
end
|
@@ -146,29 +188,28 @@ describe Refile::Attachment do
|
|
146
188
|
instance.document_attacher.store!
|
147
189
|
|
148
190
|
expect(instance.document_id).to be_nil
|
191
|
+
expect(instance.document_size).to be_nil
|
149
192
|
expect(Refile.store.exists?(file.id)).to be_falsy
|
150
193
|
end
|
151
194
|
end
|
152
195
|
|
153
196
|
describe ":name_attacher.delete!" do
|
154
197
|
it "deletes a stored file" do
|
155
|
-
|
156
|
-
instance.
|
198
|
+
instance.document = Refile::FileDouble.new("hello")
|
199
|
+
instance.document_attacher.store!
|
200
|
+
file = instance.document
|
157
201
|
|
158
202
|
instance.document_attacher.delete!
|
159
203
|
|
160
|
-
expect(instance.document_id).to be_nil
|
161
204
|
expect(Refile.store.exists?(file.id)).to be_falsy
|
162
205
|
end
|
163
206
|
|
164
207
|
it "deletes a cached file" do
|
165
|
-
|
166
|
-
|
208
|
+
instance.document = Refile::FileDouble.new("hello")
|
209
|
+
file = instance.document
|
167
210
|
|
168
211
|
instance.document_attacher.delete!
|
169
212
|
|
170
|
-
expect(instance.document_id).to be_nil
|
171
|
-
expect(instance.document_cache_id).to be_nil
|
172
213
|
expect(Refile.cache.exists?(file.id)).to be_falsy
|
173
214
|
end
|
174
215
|
end
|
@@ -200,6 +241,83 @@ describe Refile::Attachment do
|
|
200
241
|
end
|
201
242
|
end
|
202
243
|
|
244
|
+
describe ":name_attacher.valid?" do
|
245
|
+
let(:options) { { type: :image, raise_errors: false } }
|
246
|
+
|
247
|
+
it "returns false if no file is attached" do
|
248
|
+
expect(instance.document_attacher.valid?).to be_falsy
|
249
|
+
end
|
250
|
+
|
251
|
+
it "returns false and if valid file is attached" do
|
252
|
+
file = Refile::FileDouble.new("hello", content_type: "image/png")
|
253
|
+
|
254
|
+
instance.document = file
|
255
|
+
|
256
|
+
expect(instance.document_attacher.valid?).to be_truthy
|
257
|
+
end
|
258
|
+
|
259
|
+
it "returns false and sets errors if invalid file is attached" do
|
260
|
+
file = Refile::FileDouble.new("hello", content_type: "text/plain")
|
261
|
+
|
262
|
+
instance.document = file
|
263
|
+
|
264
|
+
expect(instance.document_attacher.valid?).to be_falsy
|
265
|
+
expect(instance.document_attacher.errors).to eq([:invalid_content_type])
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe ":name_attacher.extension" do
|
270
|
+
it "is nil when not inferrable" do
|
271
|
+
file = Refile::FileDouble.new("hello")
|
272
|
+
instance.document = file
|
273
|
+
expect(instance.document_attacher.extension).to be_nil
|
274
|
+
end
|
275
|
+
|
276
|
+
it "is inferred from the filename" do
|
277
|
+
file = Refile::FileDouble.new("hello", "hello.txt")
|
278
|
+
instance.document = file
|
279
|
+
expect(instance.document_attacher.extension).to eq("txt")
|
280
|
+
end
|
281
|
+
|
282
|
+
it "is inferred from the content type" do
|
283
|
+
file = Refile::FileDouble.new("hello", content_type: "image/png")
|
284
|
+
instance.document = file
|
285
|
+
expect(instance.document_attacher.extension).to eq("png")
|
286
|
+
end
|
287
|
+
|
288
|
+
it "returns nil with unknown content type" do
|
289
|
+
file = Refile::FileDouble.new("hello", content_type: "foo/doesnotexist")
|
290
|
+
instance.document = file
|
291
|
+
expect(instance.document_attacher.extension).to be_nil
|
292
|
+
end
|
293
|
+
|
294
|
+
it "is nil when filename has no extension" do
|
295
|
+
file = Refile::FileDouble.new("hello", "hello")
|
296
|
+
instance.document = file
|
297
|
+
expect(instance.document_attacher.extension).to be_nil
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
describe ":name_attacher.basename" do
|
302
|
+
it "is nil when not inferrable" do
|
303
|
+
file = Refile::FileDouble.new("hello")
|
304
|
+
instance.document = file
|
305
|
+
expect(instance.document_attacher.basename).to be_nil
|
306
|
+
end
|
307
|
+
|
308
|
+
it "is inferred from the filename" do
|
309
|
+
file = Refile::FileDouble.new("hello", "hello.txt")
|
310
|
+
instance.document = file
|
311
|
+
expect(instance.document_attacher.basename).to eq("hello")
|
312
|
+
end
|
313
|
+
|
314
|
+
it "returns filename if filename has no extension" do
|
315
|
+
file = Refile::FileDouble.new("hello", "hello")
|
316
|
+
instance.document = file
|
317
|
+
expect(instance.document_attacher.basename).to eq("hello")
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
203
321
|
describe ":name_attacher.error" do
|
204
322
|
let(:options) { { cache: :limited_cache, raise_errors: false } }
|
205
323
|
|
@@ -358,4 +476,11 @@ describe Refile::Attachment do
|
|
358
476
|
expect(instance.document).to be_nil
|
359
477
|
end
|
360
478
|
end
|
479
|
+
|
480
|
+
it "includes the module with methods in an instrospectable way" do
|
481
|
+
expect { puts klass.ancestors }
|
482
|
+
.to output(/Refile::Attachment\(document\)/).to_stdout
|
483
|
+
expect { p klass.ancestors }
|
484
|
+
.to output(/Refile::Attachment\(document\)/).to_stdout
|
485
|
+
end
|
361
486
|
end
|