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.
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