saviour 0.2.0

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