refile 0.2.2
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/.gitignore +27 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +476 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/refile.js +50 -0
- data/app/helpers/attachment_helper.rb +52 -0
- data/config.ru +8 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +3 -0
- data/lib/refile.rb +72 -0
- data/lib/refile/app.rb +97 -0
- data/lib/refile/attachment.rb +89 -0
- data/lib/refile/attachment/active_record.rb +24 -0
- data/lib/refile/backend/file_system.rb +70 -0
- data/lib/refile/backend/s3.rb +129 -0
- data/lib/refile/file.rb +65 -0
- data/lib/refile/image_processing.rb +73 -0
- data/lib/refile/rails.rb +36 -0
- data/lib/refile/random_hasher.rb +5 -0
- data/lib/refile/version.rb +3 -0
- data/refile.gemspec +34 -0
- data/spec/refile/app_spec.rb +151 -0
- data/spec/refile/attachment_spec.rb +141 -0
- data/spec/refile/backend/file_system_spec.rb +30 -0
- data/spec/refile/backend/s3_spec.rb +11 -0
- data/spec/refile/backend_examples.rb +215 -0
- data/spec/refile/features/direct_upload_spec.rb +29 -0
- data/spec/refile/features/normal_upload_spec.rb +36 -0
- data/spec/refile/features/presigned_upload_spec.rb +29 -0
- data/spec/refile/fixtures/hello.txt +1 -0
- data/spec/refile/fixtures/large.txt +44 -0
- data/spec/refile/spec_helper.rb +58 -0
- data/spec/refile/test_app.rb +46 -0
- data/spec/refile/test_app/app/assets/javascripts/application.js +40 -0
- data/spec/refile/test_app/app/controllers/application_controller.rb +2 -0
- data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +15 -0
- data/spec/refile/test_app/app/controllers/home_controller.rb +4 -0
- data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +19 -0
- data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +30 -0
- data/spec/refile/test_app/app/models/post.rb +5 -0
- data/spec/refile/test_app/app/views/direct_posts/new.html.erb +16 -0
- data/spec/refile/test_app/app/views/home/index.html.erb +1 -0
- data/spec/refile/test_app/app/views/layouts/application.html.erb +14 -0
- data/spec/refile/test_app/app/views/normal_posts/new.html.erb +20 -0
- data/spec/refile/test_app/app/views/normal_posts/show.html.erb +9 -0
- data/spec/refile/test_app/app/views/presigned_posts/new.html.erb +16 -0
- data/spec/refile/test_app/config/database.yml +7 -0
- data/spec/refile/test_app/config/routes.rb +17 -0
- data/spec/refile/test_app/public/favicon.ico +0 -0
- data/spec/refile_spec.rb +35 -0
- metadata +294 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
describe Refile::Attachment do
|
2
|
+
let(:options) { { } }
|
3
|
+
let(:post) { Post.new }
|
4
|
+
let(:klass) do
|
5
|
+
opts = options
|
6
|
+
Class.new do
|
7
|
+
extend Refile::Attachment
|
8
|
+
|
9
|
+
attr_accessor :document_id, :document_name, :document_size
|
10
|
+
|
11
|
+
attachment :document, **opts
|
12
|
+
end
|
13
|
+
end
|
14
|
+
let(:instance) { klass.new }
|
15
|
+
|
16
|
+
describe ":name=" do
|
17
|
+
it "receives a file, caches it and sets the _id parameter" do
|
18
|
+
instance.document = Refile::FileDouble.new("hello")
|
19
|
+
|
20
|
+
expect(Refile.cache.get(instance.document.id).read).to eq("hello")
|
21
|
+
expect(Refile.cache.get(instance.document_cache_id).read).to eq("hello")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ":name" do
|
26
|
+
it "gets a file from the store" do
|
27
|
+
file = Refile.store.upload(Refile::FileDouble.new("hello"))
|
28
|
+
instance.document_id = file.id
|
29
|
+
|
30
|
+
expect(instance.document.id).to eq(file.id)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe ":name_cache_id" do
|
35
|
+
it "doesn't overwrite a cached file" do
|
36
|
+
instance.document = Refile::FileDouble.new("hello")
|
37
|
+
instance.document_cache_id = "xyz"
|
38
|
+
|
39
|
+
expect(instance.document.read).to eq("hello")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe ":name_attachment.store!" do
|
44
|
+
it "puts a cached file into the store" do
|
45
|
+
instance.document = Refile::FileDouble.new("hello")
|
46
|
+
cache = instance.document
|
47
|
+
|
48
|
+
instance.document_attachment.store!
|
49
|
+
|
50
|
+
expect(Refile.store.get(instance.document_id).read).to eq("hello")
|
51
|
+
expect(Refile.store.get(instance.document.id).read).to eq("hello")
|
52
|
+
|
53
|
+
expect(instance.document_cache_id).to be_nil
|
54
|
+
expect(Refile.cache.get(cache.id).exists?).to be_falsy
|
55
|
+
end
|
56
|
+
|
57
|
+
it "does nothing when not cached" do
|
58
|
+
file = Refile.store.upload(Refile::FileDouble.new("hello"))
|
59
|
+
instance.document_id = file.id
|
60
|
+
|
61
|
+
instance.document_attachment.store!
|
62
|
+
|
63
|
+
expect(Refile.store.get(instance.document_id).read).to eq("hello")
|
64
|
+
expect(Refile.store.get(instance.document.id).read).to eq("hello")
|
65
|
+
end
|
66
|
+
|
67
|
+
it "overwrites previously stored file" do
|
68
|
+
file = Refile.store.upload(Refile::FileDouble.new("hello"))
|
69
|
+
instance.document_id = file.id
|
70
|
+
|
71
|
+
instance.document = Refile::FileDouble.new("world")
|
72
|
+
cache = instance.document
|
73
|
+
|
74
|
+
instance.document_attachment.store!
|
75
|
+
|
76
|
+
expect(Refile.store.get(instance.document_id).read).to eq("world")
|
77
|
+
expect(Refile.store.get(instance.document.id).read).to eq("world")
|
78
|
+
|
79
|
+
expect(instance.document_cache_id).to be_nil
|
80
|
+
expect(Refile.cache.get(cache.id).exists?).to be_falsy
|
81
|
+
expect(Refile.store.get(file.id).exists?).to be_falsy
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe ":name_attachment.error" do
|
86
|
+
let(:options) { { cache: :limited_cache, raise_errors: false } }
|
87
|
+
|
88
|
+
it "is blank when valid file uploaded" do
|
89
|
+
file = Refile::FileDouble.new("hello")
|
90
|
+
instance.document = file
|
91
|
+
|
92
|
+
expect(instance.document_attachment.errors).to be_empty
|
93
|
+
expect(Refile.cache.get(instance.document.id).exists?).to be_truthy
|
94
|
+
end
|
95
|
+
|
96
|
+
it "contains a list of errors when invalid file uploaded" do
|
97
|
+
file = Refile::FileDouble.new("a"*120)
|
98
|
+
instance.document = file
|
99
|
+
|
100
|
+
expect(instance.document_attachment.errors).to eq([:too_large])
|
101
|
+
expect(instance.document).to be_nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it "is reset when valid file uploaded" do
|
105
|
+
file = Refile::FileDouble.new("a"*120)
|
106
|
+
instance.document = file
|
107
|
+
|
108
|
+
file = Refile::FileDouble.new("hello")
|
109
|
+
instance.document = file
|
110
|
+
|
111
|
+
expect(instance.document_attachment.errors).to be_empty
|
112
|
+
expect(Refile.cache.get(instance.document.id).exists?).to be_truthy
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "with option `raise_errors: true" do
|
117
|
+
let(:options) { { cache: :limited_cache, raise_errors: true } }
|
118
|
+
|
119
|
+
it "raises an error when invalid file assigned" do
|
120
|
+
file = Refile::FileDouble.new("a"*120)
|
121
|
+
expect do
|
122
|
+
instance.document = file
|
123
|
+
end.to raise_error(Refile::Invalid)
|
124
|
+
|
125
|
+
expect(instance.document_attachment.errors).to eq([:too_large])
|
126
|
+
expect(instance.document).to be_nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "with option `raise_errors: false" do
|
131
|
+
let(:options) { { cache: :limited_cache, raise_errors: false } }
|
132
|
+
|
133
|
+
it "does not raise an error when invalid file assigned" do
|
134
|
+
file = Refile::FileDouble.new("a"*120)
|
135
|
+
instance.document = file
|
136
|
+
|
137
|
+
expect(instance.document_attachment.errors).to eq([:too_large])
|
138
|
+
expect(instance.document).to be_nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
RSpec.describe Refile::Backend::FileSystem do
|
2
|
+
let(:backend) { Refile::Backend::FileSystem.new(File.expand_path("tmp/store1", Dir.pwd), max_size: 100) }
|
3
|
+
|
4
|
+
it_behaves_like :backend
|
5
|
+
|
6
|
+
describe "#upload" do
|
7
|
+
it "efficiently copies a file if it has a path" do
|
8
|
+
path = File.expand_path("tmp/test.txt", Dir.pwd)
|
9
|
+
File.write(path, "hello")
|
10
|
+
|
11
|
+
uploadable = Refile::FileDouble.new("wrong")
|
12
|
+
allow(uploadable).to receive(:path).and_return(path)
|
13
|
+
|
14
|
+
file = backend.upload(uploadable)
|
15
|
+
|
16
|
+
expect(backend.get(file.id).read).to eq("hello")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "ignores path if it doesn't exist" do
|
20
|
+
path = File.expand_path("tmp/doesnotexist.txt", Dir.pwd)
|
21
|
+
|
22
|
+
uploadable = Refile::FileDouble.new("yes")
|
23
|
+
allow(uploadable).to receive(:path).and_return(path)
|
24
|
+
|
25
|
+
file = backend.upload(uploadable)
|
26
|
+
|
27
|
+
expect(backend.get(file.id).read).to eq("yes")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
if ENV["S3"]
|
2
|
+
require "refile/backend/s3"
|
3
|
+
|
4
|
+
config = YAML.load_file("s3.yml").map { |k, v| [k.to_sym, v] }.to_h
|
5
|
+
|
6
|
+
RSpec.describe Refile::Backend::S3 do
|
7
|
+
let(:backend) { Refile::Backend::S3.new(max_size: 100, **config) }
|
8
|
+
|
9
|
+
it_behaves_like :backend
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
RSpec.shared_examples_for :backend do
|
2
|
+
def uploadable(data = "hello")
|
3
|
+
Refile::FileDouble.new(data)
|
4
|
+
end
|
5
|
+
|
6
|
+
def open_files
|
7
|
+
ObjectSpace.each_object(File).reject { |f| f.closed? } if defined?(ObjectSpace)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#upload" do
|
11
|
+
it "raises ArgumentError when invalid object is uploaded" do
|
12
|
+
expect { backend.upload(double(size: 123)) }.to raise_error(ArgumentError)
|
13
|
+
expect { backend.upload("hello") }.to raise_error(ArgumentError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "raises Refile::Invalid when object is too large" do
|
17
|
+
expect { backend.upload(uploadable("a" * 200)) }.to raise_error(Refile::Invalid)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "stores file for later retrieval" do
|
21
|
+
file = backend.upload(uploadable)
|
22
|
+
retrieved = backend.get(file.id)
|
23
|
+
|
24
|
+
expect(retrieved.read).to eq("hello")
|
25
|
+
expect(retrieved.size).to eq(5)
|
26
|
+
expect(retrieved.exists?).to be_truthy
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can store an uploaded file" do
|
30
|
+
file = backend.upload(uploadable)
|
31
|
+
file2 = backend.upload(file)
|
32
|
+
|
33
|
+
retrieved = backend.get(file2.id)
|
34
|
+
|
35
|
+
expect(retrieved.read).to eq("hello")
|
36
|
+
expect(retrieved.size).to eq(5)
|
37
|
+
expect(retrieved.exists?).to be_truthy
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#delete" do
|
42
|
+
it "removes a stored file" do
|
43
|
+
file = backend.upload(uploadable)
|
44
|
+
|
45
|
+
backend.delete(file.id)
|
46
|
+
|
47
|
+
expect(backend.get(file.id).exists?).to be_falsy
|
48
|
+
end
|
49
|
+
|
50
|
+
it "does not affect other files" do
|
51
|
+
file = backend.upload(uploadable)
|
52
|
+
other = backend.upload(uploadable)
|
53
|
+
|
54
|
+
backend.delete(file.id)
|
55
|
+
|
56
|
+
expect(backend.get(file.id).exists?).to be_falsy
|
57
|
+
expect(backend.get(other.id).exists?).to be_truthy
|
58
|
+
end
|
59
|
+
|
60
|
+
it "does nothing when file doesn't exist" do
|
61
|
+
file = backend.upload(uploadable)
|
62
|
+
|
63
|
+
backend.delete(file.id)
|
64
|
+
backend.delete(file.id)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can be called through file" do
|
68
|
+
file = backend.upload(uploadable)
|
69
|
+
|
70
|
+
file.delete
|
71
|
+
|
72
|
+
expect(backend.get(file.id).exists?).to be_falsy
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#read" do
|
77
|
+
it "returns file contents" do
|
78
|
+
file = backend.upload(uploadable)
|
79
|
+
expect(backend.read(file.id)).to eq("hello")
|
80
|
+
end
|
81
|
+
|
82
|
+
it "returns nil when file doesn't exist" do
|
83
|
+
expect(backend.read("nosuchfile")).to be_nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it "can be called through file" do
|
87
|
+
file = backend.upload(uploadable)
|
88
|
+
expect(file.read).to eq("hello")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#size" do
|
93
|
+
it "returns file size" do
|
94
|
+
file = backend.upload(uploadable)
|
95
|
+
expect(backend.size(file.id)).to eq(5)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns nil when file doesn't exist" do
|
99
|
+
expect(backend.size("nosuchfile")).to be_nil
|
100
|
+
end
|
101
|
+
|
102
|
+
it "can be called through file" do
|
103
|
+
file = backend.upload(uploadable)
|
104
|
+
expect(file.size).to eq(5)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#exists?" do
|
109
|
+
it "returns true when file exists" do
|
110
|
+
file = backend.upload(uploadable)
|
111
|
+
expect(backend.exists?(file.id)).to eq(true)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "returns false when file doesn't exist" do
|
115
|
+
expect(backend.exists?("nosuchfile")).to eq(false)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "can be called through file" do
|
119
|
+
expect(backend.upload(uploadable).exists?).to eq(true)
|
120
|
+
expect(backend.get("nosuchfile").exists?).to eq(false)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#clear!" do
|
125
|
+
it "removes files when called with :confirm" do
|
126
|
+
file = backend.upload(uploadable)
|
127
|
+
|
128
|
+
backend.clear!(:confirm)
|
129
|
+
|
130
|
+
expect(backend.get(file.id).exists?).to be_falsy
|
131
|
+
end
|
132
|
+
|
133
|
+
it "complains when called without confirm" do
|
134
|
+
file = backend.upload(uploadable)
|
135
|
+
|
136
|
+
expect { backend.clear! }.to raise_error(ArgumentError)
|
137
|
+
|
138
|
+
expect(backend.get(file.id).exists?).to be_truthy
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "File" do
|
143
|
+
it "is an io-like object" do
|
144
|
+
before = open_files
|
145
|
+
|
146
|
+
file = backend.upload(uploadable)
|
147
|
+
|
148
|
+
buffer = ""
|
149
|
+
result = ""
|
150
|
+
|
151
|
+
until file.eof?
|
152
|
+
chunk = file.read(2, buffer)
|
153
|
+
result << chunk
|
154
|
+
expect("hello").to include(buffer)
|
155
|
+
end
|
156
|
+
|
157
|
+
expect(result).to eq("hello")
|
158
|
+
|
159
|
+
file.close
|
160
|
+
|
161
|
+
expect { file.read(1, buffer) }.to raise_error
|
162
|
+
|
163
|
+
expect(open_files).to eq(before)
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "#read" do
|
167
|
+
it "can read file contents" do
|
168
|
+
file = backend.upload(uploadable)
|
169
|
+
|
170
|
+
buffer = ""
|
171
|
+
|
172
|
+
expect(file.read).to eq("hello")
|
173
|
+
end
|
174
|
+
|
175
|
+
it "can read entire file contents into buffer" do
|
176
|
+
file = backend.upload(uploadable)
|
177
|
+
|
178
|
+
buffer = ""
|
179
|
+
|
180
|
+
file.read(nil, buffer)
|
181
|
+
|
182
|
+
expect(buffer).to eq("hello")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "#each" do
|
187
|
+
it "can read file contents" do
|
188
|
+
file = backend.upload(uploadable)
|
189
|
+
|
190
|
+
buffer = ""
|
191
|
+
file.each do |chunk|
|
192
|
+
buffer << chunk
|
193
|
+
end
|
194
|
+
|
195
|
+
expect(buffer).to eq("hello")
|
196
|
+
end
|
197
|
+
|
198
|
+
it "returns an enumerator when no block given" do
|
199
|
+
file = backend.upload(uploadable)
|
200
|
+
|
201
|
+
expect(file.each.to_a.join).to eq("hello")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "#download" do
|
206
|
+
it "returns a downloaded tempfile" do
|
207
|
+
file = backend.upload(uploadable)
|
208
|
+
download = file.download
|
209
|
+
|
210
|
+
expect(download).to be_an_instance_of(Tempfile)
|
211
|
+
expect(File.read(download.path)).to eq("hello")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "refile/test_app"
|
2
|
+
|
3
|
+
feature "Direct HTTP post file uploads", :js do
|
4
|
+
scenario "Successfully upload a file" do
|
5
|
+
visit "/direct/posts/new"
|
6
|
+
fill_in "Title", with: "A cool post"
|
7
|
+
attach_file "Document", path("hello.txt")
|
8
|
+
|
9
|
+
expect(page).to have_content("Upload started")
|
10
|
+
expect(page).to have_content("Upload success")
|
11
|
+
expect(page).to have_content("Upload complete")
|
12
|
+
|
13
|
+
click_button "Create"
|
14
|
+
|
15
|
+
expect(page).to have_selector("h1", text: "A cool post")
|
16
|
+
result = Net::HTTP.get_response(URI(find_link("Document")[:href])).body.chomp
|
17
|
+
expect(result).to eq("hello")
|
18
|
+
end
|
19
|
+
|
20
|
+
scenario "Fail to upload a file that is too large" do
|
21
|
+
visit "/direct/posts/new"
|
22
|
+
fill_in "Title", with: "A cool post"
|
23
|
+
attach_file "Document", path("large.txt")
|
24
|
+
|
25
|
+
expect(page).to have_content("Upload started")
|
26
|
+
expect(page).to have_content("Upload failure error")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "refile/test_app"
|
2
|
+
|
3
|
+
feature "Normal HTTP Post file uploads" do
|
4
|
+
scenario "Successfully upload a file" do
|
5
|
+
visit "/normal/posts/new"
|
6
|
+
fill_in "Title", with: "A cool post"
|
7
|
+
attach_file "Document", path("hello.txt")
|
8
|
+
click_button "Create"
|
9
|
+
|
10
|
+
expect(page).to have_selector("h1", text: "A cool post")
|
11
|
+
click_link("Document")
|
12
|
+
expect(page.source.chomp).to eq("hello")
|
13
|
+
end
|
14
|
+
|
15
|
+
scenario "Fail to upload a file that is too large" do
|
16
|
+
visit "/normal/posts/new"
|
17
|
+
fill_in "Title", with: "A cool post"
|
18
|
+
attach_file "Document", path("large.txt")
|
19
|
+
click_button "Create"
|
20
|
+
|
21
|
+
expect(page).to have_selector(".field_with_errors")
|
22
|
+
expect(page).to have_content("Document is too large")
|
23
|
+
end
|
24
|
+
|
25
|
+
scenario "Upload a file via form redisplay" do
|
26
|
+
visit "/normal/posts/new"
|
27
|
+
attach_file "Document", path("hello.txt")
|
28
|
+
click_button "Create"
|
29
|
+
fill_in "Title", with: "A cool post"
|
30
|
+
click_button "Create"
|
31
|
+
|
32
|
+
expect(page).to have_selector("h1", text: "A cool post")
|
33
|
+
click_link("Document")
|
34
|
+
expect(page.source.chomp).to eq("hello")
|
35
|
+
end
|
36
|
+
end
|