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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/refile.js +125 -0
  3. data/config/locales/en.yml +10 -0
  4. data/config/routes.rb +5 -0
  5. data/lib/refile.rb +510 -0
  6. data/lib/refile/app.rb +186 -0
  7. data/lib/refile/attacher.rb +190 -0
  8. data/lib/refile/attachment.rb +108 -0
  9. data/lib/refile/attachment/active_record.rb +133 -0
  10. data/lib/refile/attachment_definition.rb +83 -0
  11. data/lib/refile/backend/file_system.rb +120 -0
  12. data/lib/refile/backend/s3.rb +1 -0
  13. data/lib/refile/backend_macros.rb +45 -0
  14. data/lib/refile/custom_logger.rb +48 -0
  15. data/lib/refile/file.rb +102 -0
  16. data/lib/refile/file_double.rb +13 -0
  17. data/lib/refile/image_processing.rb +1 -0
  18. data/lib/refile/rails.rb +54 -0
  19. data/lib/refile/rails/attachment_helper.rb +121 -0
  20. data/lib/refile/random_hasher.rb +11 -0
  21. data/lib/refile/signature.rb +36 -0
  22. data/lib/refile/simple_form.rb +17 -0
  23. data/lib/refile/type.rb +28 -0
  24. data/lib/refile/version.rb +3 -0
  25. data/spec/refile/active_record_helper.rb +35 -0
  26. data/spec/refile/app_spec.rb +424 -0
  27. data/spec/refile/attachment/active_record_spec.rb +568 -0
  28. data/spec/refile/attachment_helper_spec.rb +78 -0
  29. data/spec/refile/attachment_spec.rb +589 -0
  30. data/spec/refile/backend/file_system_spec.rb +5 -0
  31. data/spec/refile/backend_examples.rb +228 -0
  32. data/spec/refile/backend_macros_spec.rb +83 -0
  33. data/spec/refile/custom_logger_spec.rb +21 -0
  34. data/spec/refile/features/direct_upload_spec.rb +63 -0
  35. data/spec/refile/features/multiple_upload_spec.rb +122 -0
  36. data/spec/refile/features/normal_upload_spec.rb +144 -0
  37. data/spec/refile/features/presigned_upload_spec.rb +31 -0
  38. data/spec/refile/features/simple_form_spec.rb +8 -0
  39. data/spec/refile/fixtures/hello.txt +1 -0
  40. data/spec/refile/fixtures/image.jpg +0 -0
  41. data/spec/refile/fixtures/large.txt +44 -0
  42. data/spec/refile/fixtures/monkey.txt +1 -0
  43. data/spec/refile/fixtures/world.txt +1 -0
  44. data/spec/refile/spec_helper.rb +72 -0
  45. data/spec/refile_spec.rb +355 -0
  46. metadata +143 -0
@@ -0,0 +1,5 @@
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
+ end
@@ -0,0 +1,228 @@
1
+ RSpec.shared_examples_for :backend do
2
+ def uploadable(data = "hello")
3
+ Refile::FileDouble.new(data)
4
+ end
5
+
6
+ describe "#upload" do
7
+ it "raises ArgumentError when invalid object is uploaded" do
8
+ expect { backend.upload(double(size: 123)) }.to raise_error(Refile::InvalidFile)
9
+ expect { backend.upload("hello") }.to raise_error(Refile::InvalidFile)
10
+ end
11
+
12
+ it "raises Refile::Invalid when object is too large" do
13
+ expect { backend.upload(uploadable("a" * 200)) }.to raise_error(Refile::Invalid)
14
+ end
15
+
16
+ it "stores file for later retrieval" do
17
+ file = backend.upload(uploadable)
18
+ retrieved = backend.get(file.id)
19
+
20
+ expect(retrieved.read).to eq("hello")
21
+ expect(retrieved.size).to eq(5)
22
+ expect(retrieved.exists?).to be_truthy
23
+ end
24
+
25
+ it "can store an uploaded file" do
26
+ file = backend.upload(uploadable)
27
+ file2 = backend.upload(file)
28
+
29
+ retrieved = backend.get(file2.id)
30
+
31
+ expect(retrieved.read).to eq("hello")
32
+ expect(retrieved.size).to eq(5)
33
+ expect(retrieved.exists?).to be_truthy
34
+ end
35
+ end
36
+
37
+ describe "#delete" do
38
+ it "removes a stored file" do
39
+ file = backend.upload(uploadable)
40
+
41
+ backend.delete(file.id)
42
+
43
+ expect(backend.get(file.id).exists?).to be_falsy
44
+ end
45
+
46
+ it "does not affect other files" do
47
+ file = backend.upload(uploadable)
48
+ other = backend.upload(uploadable)
49
+
50
+ backend.delete(file.id)
51
+
52
+ expect(backend.get(file.id).exists?).to be_falsy
53
+ expect(backend.get(other.id).exists?).to be_truthy
54
+ end
55
+
56
+ it "does nothing when file doesn't exist" do
57
+ file = backend.upload(uploadable)
58
+
59
+ backend.delete(file.id)
60
+ backend.delete(file.id)
61
+ end
62
+
63
+ it "can be called through file" do
64
+ file = backend.upload(uploadable)
65
+
66
+ file.delete
67
+
68
+ expect(backend.get(file.id).exists?).to be_falsy
69
+ end
70
+
71
+ it "raises error when called with invalid ID" do
72
+ expect { backend.delete("/evil") }.to raise_error(Refile::InvalidID)
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
+
91
+ it "raises error when called with invalid ID" do
92
+ expect { backend.read("/evil") }.to raise_error(Refile::InvalidID)
93
+ end
94
+ end
95
+
96
+ describe "#size" do
97
+ it "returns file size" do
98
+ file = backend.upload(uploadable)
99
+ expect(backend.size(file.id)).to eq(5)
100
+ end
101
+
102
+ it "returns nil when file doesn't exist" do
103
+ expect(backend.size("nosuchfile")).to be_nil
104
+ end
105
+
106
+ it "can be called through file" do
107
+ file = backend.upload(uploadable)
108
+ expect(file.size).to eq(5)
109
+ end
110
+
111
+ it "raises error when called with invalid ID" do
112
+ expect { backend.size("/evil") }.to raise_error(Refile::InvalidID)
113
+ end
114
+ end
115
+
116
+ describe "#exists?" do
117
+ it "returns true when file exists" do
118
+ file = backend.upload(uploadable)
119
+ expect(backend.exists?(file.id)).to eq(true)
120
+ end
121
+
122
+ it "returns false when file doesn't exist" do
123
+ expect(backend.exists?("nosuchfile")).to eq(false)
124
+ end
125
+
126
+ it "can be called through file" do
127
+ expect(backend.upload(uploadable).exists?).to eq(true)
128
+ expect(backend.get("nosuchfile").exists?).to eq(false)
129
+ end
130
+
131
+ it "raises error when called with invalid ID" do
132
+ expect { backend.exists?("/evil") }.to raise_error(Refile::InvalidID)
133
+ end
134
+ end
135
+
136
+ describe "#clear!" do
137
+ it "removes files when called with :confirm" do
138
+ file = backend.upload(uploadable)
139
+
140
+ backend.clear!(:confirm)
141
+
142
+ expect(backend.get(file.id).exists?).to be_falsy
143
+ end
144
+
145
+ it "complains when called without confirm" do
146
+ file = backend.upload(uploadable)
147
+
148
+ expect { backend.clear! }.to raise_error(Refile::Confirm)
149
+
150
+ expect(backend.get(file.id).exists?).to be_truthy
151
+ end
152
+ end
153
+
154
+ describe "#max_size" do
155
+ it "returns the given max size" do
156
+ expect(backend.max_size).to eq(100)
157
+ end
158
+ end
159
+
160
+ describe "File" do
161
+ it "is an io-like object" do
162
+ file = backend.upload(uploadable)
163
+
164
+ buffer = ""
165
+ result = ""
166
+
167
+ until file.eof?
168
+ chunk = file.read(2, buffer)
169
+ result << chunk
170
+ expect("hello").to include(buffer)
171
+ end
172
+
173
+ expect(result).to eq("hello")
174
+
175
+ file.close
176
+
177
+ expect { file.read(1, buffer) }.to raise_error
178
+ end
179
+
180
+ describe "#rewind" do
181
+ it "rewinds file to beginning" do
182
+ file = backend.upload(uploadable)
183
+
184
+ expect(file.read(2)).to eq("he")
185
+ expect(file.read(2)).to eq("ll")
186
+ file.rewind
187
+ expect(file.read(2)).to eq("he")
188
+ end
189
+ end
190
+
191
+ describe "#read" do
192
+ it "can read file contents" do
193
+ file = backend.upload(uploadable)
194
+
195
+ expect(file.read).to eq("hello")
196
+ end
197
+
198
+ it "can read entire file contents into buffer" do
199
+ file = backend.upload(uploadable)
200
+
201
+ buffer = ""
202
+
203
+ file.read(nil, buffer)
204
+
205
+ expect(buffer).to eq("hello")
206
+ end
207
+ end
208
+
209
+ describe "#download" do
210
+ it "returns a downloaded tempfile" do
211
+ file = backend.upload(uploadable)
212
+ download = file.download
213
+
214
+ expect(download).to be_an_instance_of(Tempfile)
215
+ expect(File.read(download.path)).to eq("hello")
216
+ end
217
+ end
218
+
219
+ describe "#as_json" do
220
+ it "returns id and stringified backend as a Hash" do
221
+ file = backend.upload(uploadable)
222
+ hash = file.as_json
223
+
224
+ expect(hash.keys).to eq [:id, :backend]
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,83 @@
1
+ RSpec.describe Refile::BackendMacros do
2
+ let(:klass) do
3
+ Class.new do
4
+ extend Refile::BackendMacros
5
+
6
+ attr_accessor :max_size
7
+
8
+ verify_uploadable def upload(uploadable)
9
+ uploadable
10
+ end
11
+
12
+ verify_id def get(id)
13
+ id
14
+ end
15
+ end
16
+ end
17
+ let(:instance) { klass.new }
18
+
19
+ describe "#verify_uploadable" do
20
+ let(:io) { StringIO.new("hello") }
21
+
22
+ it "works if it conforms to required API" do
23
+ expect(instance.upload(double(size: 444, read: io, eof?: true, rewind: true, close: nil))).to be_truthy
24
+ end
25
+
26
+ it "raises ArgumentError if argument does not respond to `size`" do
27
+ expect { instance.upload(double(read: io, eof?: true, rewind: true, close: nil)) }.to raise_error(Refile::InvalidFile)
28
+ end
29
+
30
+ it "raises ArgumentError if argument does not respond to `read`" do
31
+ expect { instance.upload(double(size: 444, eof?: true, rewind: true, close: nil)) }.to raise_error(Refile::InvalidFile)
32
+ end
33
+
34
+ it "raises ArgumentError if argument does not respond to `eof?`" do
35
+ expect { instance.upload(double(size: 444, read: true, rewind: true, close: nil)) }.to raise_error(Refile::InvalidFile)
36
+ end
37
+
38
+ it "raises ArgumentError if argument does not respond to `rewind`" do
39
+ expect { instance.upload(double(size: 444, read: true, eof?: true, close: nil)) }.to raise_error(Refile::InvalidFile)
40
+ end
41
+
42
+ it "raises ArgumentError if argument does not respond to `close`" do
43
+ expect { instance.upload(double(size: 444, read: true, rewind: true, eof?: true)) }.to raise_error(Refile::InvalidFile)
44
+ end
45
+
46
+ it "returns true if size is respeced" do
47
+ instance.max_size = 8
48
+ expect(instance.upload(Refile::FileDouble.new("hello"))).to be_truthy
49
+ end
50
+
51
+ it "raises Refile::Invalid if size is exceeded" do
52
+ instance.max_size = 8
53
+ expect { instance.upload(Refile::FileDouble.new("hello world")) }.to raise_error(Refile::InvalidMaxSize)
54
+ end
55
+ end
56
+
57
+ describe "#verify_id" do
58
+ it "works if it has a valid ID" do
59
+ expect(instance.get("1234aBCde123aee")).to be_truthy
60
+ end
61
+
62
+ it "raises ArgumentError if argument does not respond to `size`" do
63
+ expect { instance.get("ev/il") }.to raise_error(Refile::InvalidID)
64
+ end
65
+ end
66
+
67
+ describe "#valid_id?" do
68
+ it "returns true for valid ID" do
69
+ expect(klass.valid_id?("1234aBCde123aee")).to be_truthy
70
+ end
71
+
72
+ it "returns false for invalid ID" do
73
+ expect(klass.valid_id?("ev/il")).to be_falsey
74
+ end
75
+ end
76
+
77
+ describe "#decode_id" do
78
+ it "returns to_s" do
79
+ expect(klass.decode_id("1234aBCde123aee")).to eq "1234aBCde123aee"
80
+ expect(klass.decode_id(1234)).to eq "1234"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,21 @@
1
+ require "refile/custom_logger"
2
+
3
+ describe Refile::CustomLogger do
4
+ let(:rack_app) do
5
+ ->(_) { [200, {}, ["Success"]] }
6
+ end
7
+ let(:io) { StringIO.new }
8
+ let(:env) do
9
+ { "QUERY_STRING" => "",
10
+ "REQUEST_METHOD" => "POST",
11
+ "PATH_INFO" => "/" }
12
+ end
13
+
14
+ let(:expected_format) { %r{Prefix: \[[^\]]+\] POST "/" 200 \d+\.\d+ms\n\n$} }
15
+
16
+ it "uses a dynamic logger" do
17
+ _, _, body = described_class.new(rack_app, "Prefix", -> { Logger.new(io) }).call(env)
18
+ body.close
19
+ expect(io.tap(&:rewind).read).to match(expected_format)
20
+ end
21
+ end
@@ -0,0 +1,63 @@
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
+ expect(page).to have_content("All uploads complete")
13
+
14
+ click_button "Create"
15
+
16
+ expect(page).to have_selector("h1", text: "A cool post")
17
+ expect(page).to have_selector(".content-type", text: "text/plain")
18
+ expect(page).to have_selector(".size", text: "6")
19
+ expect(page).to have_selector(".filename", text: "hello.txt")
20
+ expect(download_link("Document")).to eq("hello")
21
+ end
22
+
23
+ scenario "Fail to upload a file that is too large" do
24
+ visit "/direct/posts/new"
25
+ fill_in "Title", with: "A cool post"
26
+ attach_file "Document", path("large.txt")
27
+
28
+ expect(page).to have_content("Upload started")
29
+ expect(page).to have_content("Upload failure error")
30
+ end
31
+
32
+ scenario "Upload a file after validation failure" do
33
+ visit "/direct/posts/new"
34
+ fill_in "Title", with: "A cool post"
35
+ check "Requires document"
36
+ click_button "Create"
37
+
38
+ attach_file "Document", path("hello.txt")
39
+
40
+ expect(page).to have_content("Upload started")
41
+ expect(page).to have_content("Upload success")
42
+ expect(page).to have_content("Upload complete")
43
+
44
+ click_button "Create"
45
+
46
+ expect(download_link("Document")).to eq("hello")
47
+ end
48
+
49
+ scenario "Fail to upload a file that has wrong format" do
50
+ visit "/direct/posts/new"
51
+ fill_in "Title", with: "A cool post"
52
+ attach_file "Image", path("large.txt")
53
+
54
+ expect(page).to have_content("Upload started")
55
+ expect(page).to have_content("Upload success")
56
+ expect(page).to have_content("Upload complete")
57
+
58
+ click_button "Create"
59
+
60
+ expect(page).to have_selector(".field_with_errors")
61
+ expect(page).to have_content("You are not allowed to upload text/plain file format. Allowed types: image/jpeg, image/gif, and image/png.")
62
+ end
63
+ end
@@ -0,0 +1,122 @@
1
+ require "refile/test_app"
2
+
3
+ feature "Multiple file uploads", :js do
4
+ scenario "Upload multiple files" do
5
+ visit "/multiple/posts/new"
6
+ fill_in "Title", with: "A cool post"
7
+ attach_file "Documents", [path("hello.txt"), path("world.txt")]
8
+ click_button "Create"
9
+
10
+ expect(download_link("Document: hello.txt")).to eq("hello")
11
+ expect(download_link("Document: world.txt")).to eq("world")
12
+ end
13
+
14
+ scenario "Fail to upload a file that is too large" do
15
+ visit "/multiple/posts/new"
16
+ fill_in "Title", with: "A cool post"
17
+ attach_file "Documents", [path("hello.txt"), path("large.txt")]
18
+ click_button "Create"
19
+
20
+ expect(page).to have_content("Documents is invalid")
21
+ end
22
+
23
+ scenario "Fail to upload a file that has the wrong format then submit" do
24
+ visit "/multiple/posts/new"
25
+ fill_in "Title", with: "A cool post"
26
+ attach_file "Documents", [path("hello.txt"), path("image.jpg")]
27
+ click_button "Create"
28
+
29
+ expect(page).to have_content("Documents is invalid")
30
+ click_button "Create"
31
+ expect(page).to have_selector("h1", text: "A cool post")
32
+ expect(page).not_to have_link("Document")
33
+ end
34
+
35
+ scenario "Upload files via form redisplay", js: true do
36
+ visit "/multiple/posts/new"
37
+ attach_file "Documents", [path("hello.txt"), path("world.txt")]
38
+ click_button "Create"
39
+ fill_in "Title", with: "A cool post"
40
+ click_button "Create"
41
+
42
+ expect(download_link("Document: hello.txt")).to eq("hello")
43
+ expect(download_link("Document: world.txt")).to eq("world")
44
+ end
45
+
46
+ scenario "Edit with changes" do
47
+ visit "/multiple/posts/new"
48
+ fill_in "Title", with: "A cool post"
49
+ attach_file "Documents", [path("hello.txt"), path("world.txt")]
50
+ click_button "Create"
51
+
52
+ click_link "Edit multiple"
53
+ attach_file "Documents", [path("monkey.txt")]
54
+ click_button "Update"
55
+
56
+ expect(download_link("Document: monkey.txt")).to eq("monkey")
57
+ expect(page).not_to have_link("Document: hello.txt")
58
+ expect(page).not_to have_link("Document: world.txt")
59
+ end
60
+
61
+ scenario "Edit without changes" do
62
+ visit "/multiple/posts/new"
63
+ fill_in "Title", with: "A cool post"
64
+ attach_file "Documents", [path("hello.txt"), path("world.txt")]
65
+ click_button "Create"
66
+
67
+ click_link "Edit multiple"
68
+ click_button "Update"
69
+
70
+ expect(download_link("Document: hello.txt")).to eq("hello")
71
+ expect(download_link("Document: world.txt")).to eq("world")
72
+ end
73
+
74
+ describe "with direct upload" do
75
+ scenario "Successfully upload a file" do
76
+ visit "/direct/posts/new"
77
+ fill_in "Title", with: "A cool post"
78
+ attach_file "Documents", [path("hello.txt"), path("world.txt")]
79
+
80
+ expect(page).to have_content("Upload started")
81
+ expect(page).to have_content("Upload success")
82
+ expect(page).to have_content("Upload complete")
83
+ expect(page).to have_content("All uploads complete")
84
+
85
+ click_button "Create"
86
+
87
+ expect(download_link("Document: hello.txt")).to eq("hello")
88
+ expect(download_link("Document: world.txt")).to eq("world")
89
+ end
90
+ end
91
+
92
+ describe "with presigned upload" do
93
+ scenario "Successfully upload a file" do
94
+ visit "/presigned/posts/new"
95
+ fill_in "Title", with: "A cool post"
96
+ attach_file "Documents", [path("hello.txt"), path("world.txt")]
97
+
98
+ expect(page).to have_content("Presign start")
99
+ expect(page).to have_content("Presign complete")
100
+ expect(page).to have_content("Upload started")
101
+ expect(page).to have_content("Upload complete token accepted")
102
+ expect(page).to have_content("Upload success token accepted")
103
+
104
+ click_button "Create"
105
+
106
+ expect(page).to have_selector("h1", text: "A cool post")
107
+ expect(download_link("Document: hello.txt")).to eq("hello")
108
+ expect(download_link("Document: world.txt")).to eq("world")
109
+ end
110
+
111
+ scenario "Fail to upload a file that is too large" do
112
+ visit "/presigned/posts/new"
113
+ fill_in "Title", with: "A cool post"
114
+ attach_file "Documents", [path("large.txt"), path("world.txt")]
115
+
116
+ expect(page).to have_content("Presign start")
117
+ expect(page).to have_content("Presign complete")
118
+ expect(page).to have_content("Upload started")
119
+ expect(page).to have_content("Upload failure too large")
120
+ end
121
+ end
122
+ end