saviour 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +491 -0
  7. data/Rakefile +6 -0
  8. data/lib/saviour.rb +170 -0
  9. data/lib/saviour/base_uploader.rb +81 -0
  10. data/lib/saviour/config.rb +13 -0
  11. data/lib/saviour/file.rb +124 -0
  12. data/lib/saviour/local_storage.rb +72 -0
  13. data/lib/saviour/processors/digest.rb +16 -0
  14. data/lib/saviour/s3_storage.rb +77 -0
  15. data/lib/saviour/string_source.rb +15 -0
  16. data/lib/saviour/uploader/element.rb +19 -0
  17. data/lib/saviour/uploader/processors_runner.rb +88 -0
  18. data/lib/saviour/uploader/store_dir_extractor.rb +41 -0
  19. data/lib/saviour/url_source.rb +55 -0
  20. data/lib/saviour/version.rb +3 -0
  21. data/saviour.gemspec +26 -0
  22. data/spec/feature/access_to_model_and_mounted_as.rb +30 -0
  23. data/spec/feature/crud_workflows_spec.rb +138 -0
  24. data/spec/feature/persisted_path_spec.rb +35 -0
  25. data/spec/feature/reload_model_spec.rb +25 -0
  26. data/spec/feature/validations_spec.rb +172 -0
  27. data/spec/feature/versions_spec.rb +186 -0
  28. data/spec/models/base_uploader_spec.rb +396 -0
  29. data/spec/models/config_spec.rb +16 -0
  30. data/spec/models/file_spec.rb +210 -0
  31. data/spec/models/local_storage_spec.rb +154 -0
  32. data/spec/models/processors/digest_spec.rb +22 -0
  33. data/spec/models/s3_storage_spec.rb +170 -0
  34. data/spec/models/saviour_spec.rb +80 -0
  35. data/spec/models/url_source_spec.rb +76 -0
  36. data/spec/spec_helper.rb +93 -0
  37. data/spec/support/data/camaloon.jpg +0 -0
  38. data/spec/support/data/example.xml +21 -0
  39. data/spec/support/data/text.txt +1 -0
  40. data/spec/support/models.rb +3 -0
  41. data/spec/support/schema.rb +9 -0
  42. metadata +196 -0
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saviour::Config do
4
+ after { Saviour::Config.storage = nil }
5
+
6
+ describe "#storage" do
7
+ it do
8
+ expect { Saviour::Config.storage }.to raise_error(RuntimeError)
9
+ end
10
+
11
+ it do
12
+ Saviour::Config.storage = :test
13
+ expect(Saviour::Config.storage).to eq :test
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,210 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saviour::File do
4
+ let(:mocked_storage) {
5
+ Class.new {
6
+ def write(content, filename)
7
+ end
8
+
9
+ def read(path)
10
+ end
11
+
12
+ def delete(path)
13
+ end
14
+
15
+ def exists?(path)
16
+ end
17
+ }.new
18
+ }
19
+
20
+ before { allow(Saviour::Config).to receive(:storage).and_return(mocked_storage) }
21
+
22
+ let(:uploader_klass) { Class.new(Saviour::BaseUploader) {
23
+ store_dir { "/store/dir" }
24
+ } }
25
+
26
+ let(:example_file) { double(read: "some file contents", path: "/my/path") }
27
+
28
+ describe "initialization" do
29
+ describe "derives persisted from the model" do
30
+ it do
31
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
32
+ expect(file).not_to be_persisted
33
+ end
34
+
35
+ it do
36
+ file = Saviour::File.new(uploader_klass, Test.create!, :file)
37
+ expect(file).not_to be_persisted
38
+ end
39
+
40
+ it do
41
+ file = Saviour::File.new(uploader_klass, Test.create!(file: "/mocked/path"), :file)
42
+ expect(file).to be_persisted
43
+ end
44
+ end
45
+
46
+ describe "is not changed" do
47
+ it do
48
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
49
+ expect(file).not_to be_changed
50
+ end
51
+
52
+ it do
53
+ file = Saviour::File.new(uploader_klass, Test.create!, :file)
54
+ expect(file).not_to be_changed
55
+ end
56
+
57
+ it do
58
+ file = Saviour::File.new(uploader_klass, Test.create!(file: "/mocked/path"), :file)
59
+ expect(file).not_to be_changed
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "#assign" do
65
+ it "returns the assigned object" do
66
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
67
+ expect(file.assign(example_file)).to eq example_file
68
+ end
69
+
70
+ it "allows to reset the internal source" do
71
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
72
+ file.assign(example_file)
73
+ expect(file.assign(nil)).to be_nil
74
+ end
75
+
76
+ it "shows error if assigned object do not respond to :read" do
77
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
78
+ expect { file.assign(6) }.to raise_error(RuntimeError)
79
+ end
80
+ end
81
+
82
+ describe "#write" do
83
+ it "fails without source" do
84
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
85
+ expect { file.write }.to raise_error(RuntimeError)
86
+ end
87
+
88
+ describe "filename used" do
89
+ it "is extracted from original_filename if possible" do
90
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
91
+ file.assign(double(read: "contents", original_filename: 'original.jpg', path: "/my/path/my_file.zip"))
92
+ uploader = double
93
+ allow(file).to receive(:uploader).and_return(uploader)
94
+ expect(uploader).to receive(:write).with("contents", "original.jpg")
95
+ file.write
96
+ end
97
+
98
+ it "is extracted from path if possible" do
99
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
100
+ file.assign(double(read: "contents", path: "/my/path/my_file.zip"))
101
+ uploader = double
102
+ allow(file).to receive(:uploader).and_return(uploader)
103
+ expect(uploader).to receive(:write).with("contents", "my_file.zip")
104
+ file.write
105
+ end
106
+
107
+ it "is random if cannot be guessed" do
108
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
109
+ file.assign(double(read: "contents"))
110
+ allow(SecureRandom).to receive(:hex).and_return("stubbed-random")
111
+ uploader = double
112
+ allow(file).to receive(:uploader).and_return(uploader)
113
+ expect(uploader).to receive(:write).with("contents", "stubbed-random")
114
+ file.write
115
+ end
116
+ end
117
+
118
+ it "returns the path" do
119
+ object = Test.new
120
+ file = Saviour::File.new(uploader_klass, object, :file)
121
+ file.assign(double(read: "contents", path: "/my/path/my_file.zip"))
122
+ uploader = double
123
+ allow(file).to receive(:uploader).and_return(uploader)
124
+ expect(uploader).to receive(:write).with("contents", "my_file.zip").and_return("/store/dir/my_file.zip")
125
+ expect(file.write).to eq "/store/dir/my_file.zip"
126
+ end
127
+ end
128
+
129
+ describe "#changed?" do
130
+ it "is cleared after removing the assignation" do
131
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
132
+ expect(file).not_to be_changed
133
+ file.assign(example_file)
134
+ expect(file).to be_changed
135
+ file.assign(nil)
136
+ expect(file).not_to be_changed
137
+ end
138
+
139
+ it "is cleared after persisting" do
140
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
141
+ file.assign(double(read: "contents", path: "/my/path/my_file.zip"))
142
+ expect(file).to be_changed
143
+
144
+ uploader = double
145
+ allow(file).to receive(:uploader).and_return(uploader)
146
+ expect(uploader).to receive(:write)
147
+ file.write
148
+
149
+ expect(file).not_to be_changed
150
+ end
151
+ end
152
+
153
+ describe "#filename" do
154
+ it "returns the filename of the persisted file" do
155
+ file = Saviour::File.new(uploader_klass, Test.create!(file: "/mocked/path/file.rar"), :file)
156
+ expect(file.filename).to eq "file.rar"
157
+ end
158
+ end
159
+
160
+ describe "#with_copy" do
161
+ it "provides a copy of the stored file" do
162
+ file = Saviour::File.new(uploader_klass, Test.create!(file: "/mocked/path/file.rar"), :file)
163
+ allow(file).to receive(:read).and_return("some contents")
164
+
165
+ file.with_copy do |tmpfile|
166
+ expect(tmpfile.read).to eq "some contents"
167
+ end
168
+ end
169
+
170
+ it "deletes the copied file even on exception" do
171
+ file = Saviour::File.new(uploader_klass, Test.create!(file: "/mocked/path/file.rar"), :file)
172
+ allow(file).to receive(:read).and_return("some contents")
173
+ mocked_tmpfile = double(binmode: "", rewind: "", flush: "", write: "")
174
+ allow(Tempfile).to receive(:open).and_yield(mocked_tmpfile)
175
+
176
+ expect(mocked_tmpfile).to receive(:close)
177
+ expect(mocked_tmpfile).to receive(:delete)
178
+
179
+ test_exception = Class.new(Exception)
180
+
181
+ begin
182
+ file.with_copy {|_| raise(test_exception, "some exception within the block") }
183
+ rescue test_exception
184
+ end
185
+ end
186
+ end
187
+
188
+ describe "#blank?" do
189
+ it "it's true when not yet assigned nor persisted" do
190
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
191
+ expect(file).to be_blank
192
+ end
193
+
194
+ it "it's true when not yet assigned but persisted" do
195
+ file = Saviour::File.new(uploader_klass, Test.create!, :file)
196
+ expect(file).to be_blank
197
+ end
198
+
199
+ it "it's false when not persisted but assigned" do
200
+ file = Saviour::File.new(uploader_klass, Test.new, :file)
201
+ file.assign example_file
202
+ expect(file).not_to be_blank
203
+ end
204
+
205
+ it "it's false when persisted and assigned" do
206
+ file = Saviour::File.new(uploader_klass, Test.create!(file: example_file), :file)
207
+ expect(file).not_to be_blank
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,154 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saviour::LocalStorage do
4
+ # operate only inside @tmpdir to not mess with test setup, Dir.mktmpdir
5
+ subject { Saviour::LocalStorage.new(local_prefix: @tmpdir) }
6
+
7
+ describe "#write" do
8
+ let(:filename) { "output.jpg" }
9
+ let(:destination_path) { File.join("/my/folder", filename) }
10
+
11
+ it "writting a new file" do
12
+ with_test_file("camaloon.jpg") do |file, _|
13
+ file_destination = File.join(@tmpdir, destination_path)
14
+ expect(File.file?(file_destination)).to be_falsey
15
+
16
+ contents = file.read
17
+ subject.write(contents, destination_path)
18
+
19
+ expect(File.file?(file_destination)).to be_truthy
20
+ expect(File.read(file_destination)).to eq contents
21
+ end
22
+ end
23
+
24
+ describe "overwritting an existing file" do
25
+ context "with overwrite protection" do
26
+ it "raises an error" do
27
+ path = File.join(@tmpdir, destination_path)
28
+
29
+ FileUtils.mkdir_p(File.dirname(path))
30
+ File.write(path, "some dummy content")
31
+ expect(File.file?(path)).to be_truthy
32
+
33
+ with_test_file("camaloon.jpg") do |file, _|
34
+ contents = file.read
35
+ expect { subject.write(contents, destination_path) }.to raise_error(RuntimeError)
36
+ end
37
+ end
38
+ end
39
+
40
+ context "without overwrite protection" do
41
+ subject { Saviour::LocalStorage.new(local_prefix: @tmpdir, overwrite_protection: false) }
42
+
43
+ it do
44
+ path = File.join(@tmpdir, destination_path)
45
+
46
+ FileUtils.mkdir_p(File.dirname(path))
47
+ File.write(path, "some dummy content")
48
+ expect(File.file?(path)).to be_truthy
49
+
50
+ with_test_file("camaloon.jpg") do |file, _|
51
+ contents = file.read
52
+ subject.write(contents, destination_path)
53
+
54
+ expect(File.file?(path)).to be_truthy
55
+ expect(File.read(path)).to eq contents
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#read" do
63
+ it "reads an existing file" do
64
+ with_test_file("camaloon.jpg") do |file, _|
65
+ expect(subject.read(File.basename(file.path))).to eq file.read
66
+ end
67
+ end
68
+
69
+ it "fails if the file do not exists" do
70
+ expect { subject.read("nope.rar") }.to raise_error(RuntimeError)
71
+ end
72
+ end
73
+
74
+ describe "#delete" do
75
+ it "deletes an existing file" do
76
+ with_test_file("camaloon.jpg") do |file, _|
77
+ expect(File.file?(file.path)).to be_truthy
78
+
79
+ subject.delete(File.basename(file.path))
80
+ expect(File.file?(file.path)).to be_falsey
81
+ end
82
+ end
83
+
84
+ it "fails if the file do not exists" do
85
+ expect { subject.delete("nope.rar") }.to raise_error(RuntimeError)
86
+ end
87
+
88
+ it "does not leave an empty dir behind" do
89
+ with_test_file("camaloon.jpg") do |file, _|
90
+ final_path = File.join(@tmpdir, "some/folder/dest.jpg")
91
+
92
+ FileUtils.mkdir_p(File.dirname(final_path))
93
+ FileUtils.cp file.path, final_path
94
+ expect(File.file?(final_path)).to be_truthy
95
+
96
+ subject.delete("/some/folder/dest.jpg")
97
+
98
+ expect(File.file?(final_path)).to be_falsey
99
+ expect(File.directory?(File.join(@tmpdir, "some/folder"))).to be_falsey
100
+ expect(File.directory?(File.join(@tmpdir, "some"))).to be_falsey
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "#exists?" do
106
+ it "with existing file" do
107
+ with_test_file("camaloon.jpg") do |file, _|
108
+ expect(subject.exists?(File.basename(file.path))).to be_truthy
109
+ end
110
+ end
111
+
112
+ it "with no file" do
113
+ expect(subject.exists?("unexisting_file.zip")).to be_falsey
114
+ end
115
+ end
116
+
117
+ describe "#public_url" do
118
+ let(:destination_path) { "dest/file.jpeg" }
119
+
120
+ context do
121
+ subject { Saviour::LocalStorage.new(local_prefix: @tmpdir) }
122
+
123
+ it "fails if not provided the prefix" do
124
+ with_test_file("camaloon.jpg") do |file, _|
125
+ expect {
126
+ subject.public_url(File.basename(file.path))
127
+ }.to raise_error(RuntimeError)
128
+ end
129
+ end
130
+ end
131
+
132
+ context do
133
+ subject { Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: -> { "http://mywebsite.com/#{Time.now.hour}/uploads" }) }
134
+
135
+ it do
136
+ allow(Time).to receive(:now).and_return(Time.new(2015, 1, 1, 13, 2, 1))
137
+
138
+ with_test_file("camaloon.jpg") do |file, filename|
139
+ expect(subject.public_url(File.basename(file.path))).to eq "http://mywebsite.com/13/uploads/#{filename}"
140
+ end
141
+ end
142
+ end
143
+
144
+ context do
145
+ subject { Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://mywebsite.com/uploads") }
146
+
147
+ it do
148
+ with_test_file("camaloon.jpg") do |file, filename|
149
+ expect(subject.public_url(File.basename(file.path))).to eq "http://mywebsite.com/uploads/#{filename}"
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saviour::Processors::Digest do
4
+ describe "#digest_filename" do
5
+ subject {
6
+ Class.new { include Saviour::Processors::Digest }.new
7
+ }
8
+
9
+ let(:filename) { "name.jpg" }
10
+ let(:contents) { "bynary contents for a file" }
11
+
12
+ it do
13
+ _, new_name = subject.digest_filename(contents, filename)
14
+ expect(new_name).to eq "name-ab54d187b7909ff4bba34777073d4654.jpg"
15
+ end
16
+
17
+ it do
18
+ _, new_name = subject.digest_filename(contents, filename, separator: '/')
19
+ expect(new_name).to eq "name/ab54d187b7909ff4bba34777073d4654.jpg"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saviour::S3Storage do
4
+ subject { Saviour::S3Storage.new(bucket: "fake-bucket", aws_access_key_id: "stub", aws_secret_access_key: "stub", public_url_prefix: "https://fake-bucket.s3.amazonaws.com") }
5
+ let!(:mocked_s3) { MockedS3Helper.new }
6
+ before { mocked_s3.start!(bucket_name: "fake-bucket") }
7
+
8
+ context do
9
+ it "fails when no keys are provided" do
10
+ expect {
11
+ Saviour::S3Storage.new(bucket: "fake-bucket")
12
+ }.to raise_error(ArgumentError)
13
+ end
14
+
15
+ it "fails when the bucket doesn't exists" do
16
+ expect {
17
+ Saviour::S3Storage.new(bucket: "no-bucket", aws_access_key_id: "stub", aws_secret_access_key: "stub")
18
+ }.to raise_error(ArgumentError)
19
+ end
20
+ end
21
+
22
+ describe "#write" do
23
+ let(:destination_path) { "my/folder/file.jpeg" }
24
+
25
+ it "writting a new file" do
26
+ with_test_file("camaloon.jpg") do |file, _|
27
+ expect(mocked_s3.exists?(destination_path)).to be_falsey
28
+
29
+ contents = file.read
30
+ subject.write(contents, destination_path)
31
+
32
+ expect(mocked_s3.read(destination_path)).to eq contents
33
+ end
34
+ end
35
+
36
+ describe "overwritting" do
37
+ context "without overwrite protection" do
38
+ subject {
39
+ Saviour::S3Storage.new(
40
+ bucket: "fake-bucket",
41
+ aws_access_key_id: "stub",
42
+ aws_secret_access_key: "stub",
43
+ public_url_prefix: "https://fake-bucket.s3.amazonaws.com",
44
+ overwrite_protection: false
45
+ )
46
+ }
47
+
48
+ it "overwrites the existing file" do
49
+ mocked_s3.write("some dummy contents", destination_path)
50
+ expect(mocked_s3.exists?(destination_path)).to be_truthy
51
+
52
+ with_test_file("camaloon.jpg") do |file, _|
53
+ contents = file.read
54
+ subject.write(contents, destination_path)
55
+ expect(mocked_s3.read(destination_path)).to eq contents
56
+ end
57
+ end
58
+ end
59
+
60
+ context "with overwrite protection" do
61
+ it "raises an exception" do
62
+ mocked_s3.write("some dummy contents", destination_path)
63
+ expect(mocked_s3.exists?(destination_path)).to be_truthy
64
+
65
+ with_test_file("camaloon.jpg") do |file, _|
66
+ contents = file.read
67
+ expect { subject.write(contents, destination_path) }.to raise_error(RuntimeError)
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ it "ignores leading slash" do
74
+ subject.write("trash contents", "/folder/file.out")
75
+ expect(subject.exists?("folder/file.out")).to be_truthy
76
+ expect(subject.exists?("/folder/file.out")).to be_truthy
77
+ expect(subject.exists?("////folder/file.out")).to be_truthy
78
+ end
79
+ end
80
+
81
+ describe "#read" do
82
+ let(:destination_path) { "dest/file.jpeg" }
83
+
84
+ it "reads an existing file" do
85
+ with_test_file("camaloon.jpg") do |file, _|
86
+ contents = file.read
87
+
88
+ mocked_s3.write(contents, destination_path)
89
+ expect(subject.read(destination_path)).to eq contents
90
+ end
91
+ end
92
+
93
+ it "fails if the file do not exists" do
94
+ expect { subject.read("nope.rar") }.to raise_error(RuntimeError)
95
+ end
96
+ end
97
+
98
+ describe "#delete" do
99
+ let(:destination_path) { "dest/file.jpeg" }
100
+
101
+ it "deletes an existing file" do
102
+ with_test_file("camaloon.jpg") do |file, _|
103
+ contents = file.read
104
+ mocked_s3.write(contents, destination_path)
105
+
106
+ expect(mocked_s3.exists?(destination_path)).to be_truthy
107
+ subject.delete("/dest/file.jpeg")
108
+ expect(mocked_s3.exists?(destination_path)).to be_falsey
109
+ end
110
+ end
111
+
112
+ it "fails if the file do not exists" do
113
+ expect { subject.delete("nope.rar") }.to raise_error(RuntimeError)
114
+ end
115
+ end
116
+
117
+ describe "#exists?" do
118
+ let(:destination_path) { "dest/file.jpeg" }
119
+
120
+ it "with existing file" do
121
+ with_test_file("camaloon.jpg") do |file, _|
122
+ contents = file.read
123
+ mocked_s3.write(contents, destination_path)
124
+ expect(subject.exists?(destination_path)).to be_truthy
125
+ end
126
+ end
127
+
128
+ it "with no file" do
129
+ expect(subject.exists?("unexisting_file.zip")).to be_falsey
130
+ end
131
+ end
132
+
133
+ describe "#public_url" do
134
+ let(:destination_path) { "dest/file.jpeg" }
135
+
136
+ context do
137
+ subject { Saviour::S3Storage.new(bucket: "fake-bucket", aws_access_key_id: "stub", aws_secret_access_key: "stub") }
138
+
139
+ it "fails if not provided the prefix" do
140
+ with_test_file("camaloon.jpg") do |file, _|
141
+ contents = file.read
142
+ mocked_s3.write(contents, destination_path)
143
+ expect { subject.public_url(destination_path) }.to raise_error(RuntimeError)
144
+ end
145
+ end
146
+ end
147
+
148
+ context do
149
+ subject { Saviour::S3Storage.new(bucket: "fake-bucket", aws_access_key_id: "stub", aws_secret_access_key: "stub", public_url_prefix: -> { "https://#{Time.now.hour}.s3.amazonaws.com" }) }
150
+
151
+ it "allow to use a lambda for dynamic url prefixes" do
152
+ allow(Time).to receive(:now).and_return(Time.new(2015, 1, 1, 13, 2, 1))
153
+
154
+ with_test_file("camaloon.jpg") do |file, _|
155
+ contents = file.read
156
+ mocked_s3.write(contents, destination_path)
157
+ expect(subject.public_url(destination_path)).to eq "https://13.s3.amazonaws.com/dest/file.jpeg"
158
+ end
159
+ end
160
+ end
161
+
162
+ it do
163
+ with_test_file("camaloon.jpg") do |file, _|
164
+ contents = file.read
165
+ mocked_s3.write(contents, destination_path)
166
+ expect(subject.public_url(destination_path)).to eq "https://fake-bucket.s3.amazonaws.com/dest/file.jpeg"
167
+ end
168
+ end
169
+ end
170
+ end