refile 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +476 -0
  8. data/Rakefile +11 -0
  9. data/app/assets/javascripts/refile.js +50 -0
  10. data/app/helpers/attachment_helper.rb +52 -0
  11. data/config.ru +8 -0
  12. data/config/locales/en.yml +5 -0
  13. data/config/routes.rb +3 -0
  14. data/lib/refile.rb +72 -0
  15. data/lib/refile/app.rb +97 -0
  16. data/lib/refile/attachment.rb +89 -0
  17. data/lib/refile/attachment/active_record.rb +24 -0
  18. data/lib/refile/backend/file_system.rb +70 -0
  19. data/lib/refile/backend/s3.rb +129 -0
  20. data/lib/refile/file.rb +65 -0
  21. data/lib/refile/image_processing.rb +73 -0
  22. data/lib/refile/rails.rb +36 -0
  23. data/lib/refile/random_hasher.rb +5 -0
  24. data/lib/refile/version.rb +3 -0
  25. data/refile.gemspec +34 -0
  26. data/spec/refile/app_spec.rb +151 -0
  27. data/spec/refile/attachment_spec.rb +141 -0
  28. data/spec/refile/backend/file_system_spec.rb +30 -0
  29. data/spec/refile/backend/s3_spec.rb +11 -0
  30. data/spec/refile/backend_examples.rb +215 -0
  31. data/spec/refile/features/direct_upload_spec.rb +29 -0
  32. data/spec/refile/features/normal_upload_spec.rb +36 -0
  33. data/spec/refile/features/presigned_upload_spec.rb +29 -0
  34. data/spec/refile/fixtures/hello.txt +1 -0
  35. data/spec/refile/fixtures/large.txt +44 -0
  36. data/spec/refile/spec_helper.rb +58 -0
  37. data/spec/refile/test_app.rb +46 -0
  38. data/spec/refile/test_app/app/assets/javascripts/application.js +40 -0
  39. data/spec/refile/test_app/app/controllers/application_controller.rb +2 -0
  40. data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +15 -0
  41. data/spec/refile/test_app/app/controllers/home_controller.rb +4 -0
  42. data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +19 -0
  43. data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +30 -0
  44. data/spec/refile/test_app/app/models/post.rb +5 -0
  45. data/spec/refile/test_app/app/views/direct_posts/new.html.erb +16 -0
  46. data/spec/refile/test_app/app/views/home/index.html.erb +1 -0
  47. data/spec/refile/test_app/app/views/layouts/application.html.erb +14 -0
  48. data/spec/refile/test_app/app/views/normal_posts/new.html.erb +20 -0
  49. data/spec/refile/test_app/app/views/normal_posts/show.html.erb +9 -0
  50. data/spec/refile/test_app/app/views/presigned_posts/new.html.erb +16 -0
  51. data/spec/refile/test_app/config/database.yml +7 -0
  52. data/spec/refile/test_app/config/routes.rb +17 -0
  53. data/spec/refile/test_app/public/favicon.ico +0 -0
  54. data/spec/refile_spec.rb +35 -0
  55. 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