saviour 0.5.10 → 0.5.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/Gemfile +1 -1
- data/README.md +939 -346
- data/gemfiles/{5.0.gemfile → 5.2.gemfile} +2 -2
- data/lib/saviour.rb +1 -0
- data/lib/saviour/file.rb +11 -12
- data/lib/saviour/integrator.rb +30 -5
- data/lib/saviour/life_cycle.rb +18 -2
- data/lib/saviour/model.rb +1 -0
- data/lib/saviour/read_only_file.rb +35 -0
- data/lib/saviour/version.rb +1 -1
- data/saviour.gemspec +2 -2
- data/spec/feature/{concurrent_processors_spec.rb → concurrency_spec.rb} +49 -1
- data/spec/feature/crud_workflows_spec.rb +26 -2
- data/spec/feature/dirty_spec.rb +70 -8
- data/spec/feature/remove_attachment_spec.rb +50 -3
- data/spec/feature/stash_spec.rb +1 -2
- data/spec/models/file_spec.rb +6 -16
- data/spec/support/active_record_asserts.rb +10 -1
- metadata +9 -14
data/lib/saviour.rb
CHANGED
data/lib/saviour/file.rb
CHANGED
@@ -5,13 +5,14 @@ module Saviour
|
|
5
5
|
attr_reader :persisted_path
|
6
6
|
attr_reader :source
|
7
7
|
|
8
|
-
def initialize(uploader_klass, model, attached_as)
|
8
|
+
def initialize(uploader_klass, model, attached_as, persisted_path = nil)
|
9
9
|
@uploader_klass, @model, @attached_as = uploader_klass, model, attached_as
|
10
10
|
@source_was = @source = nil
|
11
|
-
|
11
|
+
@persisted_path = persisted_path
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
if persisted_path
|
14
|
+
@model.instance_variable_set("@__uploader_#{@attached_as}_was", ReadOnlyFile.new(persisted_path))
|
15
|
+
end
|
15
16
|
end
|
16
17
|
|
17
18
|
def exists?
|
@@ -24,7 +25,6 @@ module Saviour
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def delete
|
27
|
-
persisted? && Config.storage.delete(@persisted_path)
|
28
28
|
@persisted_path = nil
|
29
29
|
@source_was = nil
|
30
30
|
@source = nil
|
@@ -48,10 +48,7 @@ module Saviour
|
|
48
48
|
|
49
49
|
def clone
|
50
50
|
return nil unless persisted?
|
51
|
-
|
52
|
-
new_file = Saviour::File.new(@uploader_klass, @model, @attached_as)
|
53
|
-
new_file.set_path! @persisted_path
|
54
|
-
new_file
|
51
|
+
Saviour::File.new(@uploader_klass, @model, @attached_as, @persisted_path)
|
55
52
|
end
|
56
53
|
|
57
54
|
def dup(new_model)
|
@@ -68,6 +65,7 @@ module Saviour
|
|
68
65
|
|
69
66
|
def reload
|
70
67
|
@model.instance_variable_set("@__uploader_#{@attached_as}", nil)
|
68
|
+
@model.instance_variable_set("@__uploader_#{@attached_as}_was", nil)
|
71
69
|
end
|
72
70
|
|
73
71
|
alias_method :url, :public_url
|
@@ -85,8 +83,8 @@ module Saviour
|
|
85
83
|
@source_data = nil
|
86
84
|
@source = object
|
87
85
|
|
88
|
-
if changed? && @model.
|
89
|
-
@model.
|
86
|
+
if changed? && @model.instance_variable_get("@__uploader_#{@attached_as}_was").nil?
|
87
|
+
@model.instance_variable_set("@__uploader_#{@attached_as}_was", clone)
|
90
88
|
end
|
91
89
|
|
92
90
|
@persisted_path = nil if object
|
@@ -162,6 +160,7 @@ module Saviour
|
|
162
160
|
end
|
163
161
|
|
164
162
|
@persisted_path = path
|
163
|
+
@model.instance_variable_set("@__uploader_#{@attached_as}_was", ReadOnlyFile.new(persisted_path))
|
165
164
|
path
|
166
165
|
end
|
167
166
|
end
|
@@ -177,7 +176,7 @@ module Saviour
|
|
177
176
|
|
178
177
|
def source_data
|
179
178
|
@source_data ||= begin
|
180
|
-
@source.rewind
|
179
|
+
@source.rewind if @source.respond_to?(:rewind)
|
181
180
|
@source.read
|
182
181
|
end
|
183
182
|
end
|
data/lib/saviour/integrator.rb
CHANGED
@@ -40,10 +40,8 @@ module Saviour
|
|
40
40
|
define_method(attach_as) do
|
41
41
|
instance_variable_get("@__uploader_#{attach_as}") || begin
|
42
42
|
uploader_klass = Class.new(Saviour::BaseUploader, &block) if block
|
43
|
-
new_file = ::Saviour::File.new(uploader_klass, self, attach_as)
|
44
|
-
|
45
43
|
layer = persistence_klass.new(self)
|
46
|
-
new_file.
|
44
|
+
new_file = ::Saviour::File.new(uploader_klass, self, attach_as, layer.read(attach_as))
|
47
45
|
|
48
46
|
instance_variable_set("@__uploader_#{attach_as}", new_file)
|
49
47
|
end
|
@@ -57,10 +55,34 @@ module Saviour
|
|
57
55
|
send(attach_as).present?
|
58
56
|
end
|
59
57
|
|
58
|
+
define_method("#{attach_as}_was") do
|
59
|
+
instance_variable_get("@__uploader_#{attach_as}_was")
|
60
|
+
end
|
61
|
+
|
60
62
|
define_method("#{attach_as}_changed?") do
|
61
63
|
send(attach_as).changed?
|
62
64
|
end
|
63
65
|
|
66
|
+
define_method(:changed_attributes) do
|
67
|
+
if send("#{attach_as}_changed?")
|
68
|
+
super().merge(attach_as => send("#{attach_as}_was"))
|
69
|
+
else
|
70
|
+
super()
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
define_method(:changes) do
|
75
|
+
if send("#{attach_as}_changed?")
|
76
|
+
super().merge(attach_as => send("#{attach_as}_change"))
|
77
|
+
else
|
78
|
+
super()
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method("#{attach_as}_change") do
|
83
|
+
[send("#{attach_as}_was"), send(attach_as)]
|
84
|
+
end
|
85
|
+
|
64
86
|
define_method("remove_#{attach_as}!") do |dependent: nil|
|
65
87
|
if !dependent.nil? && ![:destroy, :ignore].include?(dependent)
|
66
88
|
raise ArgumentError, ":dependent option must be either :destroy or :ignore"
|
@@ -69,9 +91,12 @@ module Saviour
|
|
69
91
|
layer = persistence_klass.new(self)
|
70
92
|
|
71
93
|
attachment_remover = proc do |attach_as|
|
94
|
+
layer.write(attach_as, nil)
|
95
|
+
deletion_path = send(attach_as).persisted_path
|
96
|
+
send(attach_as).delete
|
97
|
+
|
72
98
|
work = proc do
|
73
|
-
send(attach_as).
|
74
|
-
layer.write(attach_as, nil)
|
99
|
+
Config.storage.delete(deletion_path) if deletion_path && send(attach_as).persisted_path.nil?
|
75
100
|
end
|
76
101
|
|
77
102
|
if ActiveRecord::Base.connection.current_transaction.open?
|
data/lib/saviour/life_cycle.rb
CHANGED
@@ -84,9 +84,25 @@ module Saviour
|
|
84
84
|
|
85
85
|
def delete!
|
86
86
|
DbHelpers.run_after_commit do
|
87
|
-
|
88
|
-
|
87
|
+
pool = Concurrent::FixedThreadPool.new(Saviour::Config.concurrent_workers)
|
88
|
+
|
89
|
+
futures = attached_files.map do |column|
|
90
|
+
Concurrent::Future.execute(executor: pool) {
|
91
|
+
path = @model.send(column).persisted_path
|
92
|
+
Config.storage.delete(path) if path
|
93
|
+
@model.send(column).delete
|
94
|
+
}
|
89
95
|
end
|
96
|
+
|
97
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
98
|
+
futures.each do |future|
|
99
|
+
future.value
|
100
|
+
raise(future.reason) if future.rejected?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
pool.shutdown
|
105
|
+
pool.wait_for_termination
|
90
106
|
end
|
91
107
|
end
|
92
108
|
|
data/lib/saviour/model.rb
CHANGED
@@ -26,6 +26,7 @@ module Saviour
|
|
26
26
|
self.class.attached_files.each do |attach_as|
|
27
27
|
duped[attach_as] = nil
|
28
28
|
duped.instance_variable_set("@__uploader_#{attach_as}", send(attach_as).dup(duped))
|
29
|
+
duped.instance_variable_set("@__uploader_#{attach_as}_was", nil)
|
29
30
|
end
|
30
31
|
|
31
32
|
duped
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Saviour
|
2
|
+
class ReadOnlyFile
|
3
|
+
attr_reader :persisted_path
|
4
|
+
|
5
|
+
def initialize(persisted_path)
|
6
|
+
@persisted_path = persisted_path
|
7
|
+
end
|
8
|
+
|
9
|
+
def exists?
|
10
|
+
persisted? && Config.storage.exists?(@persisted_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read
|
14
|
+
return nil unless persisted?
|
15
|
+
Config.storage.read(@persisted_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def public_url
|
19
|
+
return nil unless persisted?
|
20
|
+
Config.storage.public_url(@persisted_path)
|
21
|
+
end
|
22
|
+
alias_method :url, :public_url
|
23
|
+
|
24
|
+
def ==(another_file)
|
25
|
+
return false unless another_file.is_a?(Saviour::File) || another_file.is_a?(ReadOnlyFile)
|
26
|
+
return false unless another_file.persisted?
|
27
|
+
|
28
|
+
another_file.persisted_path == persisted_path
|
29
|
+
end
|
30
|
+
|
31
|
+
def persisted?
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/saviour/version.rb
CHANGED
data/saviour.gemspec
CHANGED
@@ -15,8 +15,8 @@ Gem::Specification.new do |spec|
|
|
15
15
|
|
16
16
|
spec.required_ruby_version = ">= 2.2.0"
|
17
17
|
|
18
|
-
spec.add_dependency "activerecord", ">= 5.
|
19
|
-
spec.add_dependency "activesupport", ">= 5.
|
18
|
+
spec.add_dependency "activerecord", ">= 5.1"
|
19
|
+
spec.add_dependency "activesupport", ">= 5.1"
|
20
20
|
spec.add_dependency "concurrent-ruby", ">= 1.0.5"
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe "
|
3
|
+
describe "concurrency on operations" do
|
4
4
|
before { allow(Saviour::Config).to receive(:storage).and_return(Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://domain.com")) }
|
5
5
|
|
6
6
|
if ENV['TRAVIS']
|
@@ -131,4 +131,52 @@ describe "concurrent processors" do
|
|
131
131
|
expect((start_times[0] - start_times[-1]).abs).to be > WAIT_TIME
|
132
132
|
end
|
133
133
|
end
|
134
|
+
|
135
|
+
context 'on destroy' do
|
136
|
+
before do
|
137
|
+
allow(Saviour::Config.storage).to receive(:delete) { sleep WAIT_TIME }
|
138
|
+
end
|
139
|
+
|
140
|
+
def measure
|
141
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
142
|
+
yield
|
143
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'works concurrently with 4 workers' do
|
147
|
+
Saviour::Config.concurrent_workers = 4
|
148
|
+
|
149
|
+
a = klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
|
150
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
151
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
152
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
153
|
+
|
154
|
+
time = measure { a.destroy! }
|
155
|
+
expect(time).to be_within(THRESHOLD).of(WAIT_TIME)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'works in serial with 1 worker' do
|
159
|
+
Saviour::Config.concurrent_workers = 1
|
160
|
+
|
161
|
+
a = klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
|
162
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
163
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
164
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
165
|
+
|
166
|
+
time = measure { a.destroy! }
|
167
|
+
expect(time).to be_within(THRESHOLD).of(WAIT_TIME * 4)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'concurrency can be adjusted' do
|
171
|
+
Saviour::Config.concurrent_workers = 2
|
172
|
+
|
173
|
+
a = klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
|
174
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
175
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
176
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
177
|
+
|
178
|
+
time = measure { a.destroy! }
|
179
|
+
expect(time).to be_within(THRESHOLD).of(WAIT_TIME * 2)
|
180
|
+
end
|
181
|
+
end
|
134
182
|
end
|
@@ -123,6 +123,14 @@ describe "CRUD" do
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
end
|
126
|
+
|
127
|
+
it "can be created from another saviour attachment" do
|
128
|
+
a = klass.create! file: Saviour::StringSource.new("contents", "file.txt")
|
129
|
+
b = klass.create! file: a.file
|
130
|
+
|
131
|
+
expect(b.file.read).to eq "contents"
|
132
|
+
expect(b.file.filename).to eq "file.txt"
|
133
|
+
end
|
126
134
|
end
|
127
135
|
|
128
136
|
describe "deletion" do
|
@@ -163,6 +171,23 @@ describe "CRUD" do
|
|
163
171
|
expect(Saviour::Config.storage.read(a[:file])).to eq "foo"
|
164
172
|
end
|
165
173
|
|
174
|
+
it "does not generate an extra query when saving the file with only attached changes" do
|
175
|
+
a = klass.create!
|
176
|
+
|
177
|
+
expect_to_yield_queries(count: 1) do
|
178
|
+
a.update_attributes! file: Saviour::StringSource.new("foo", "file.txt")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
it "does generate an extra query when saving the file with an extra change" do
|
183
|
+
a = klass.create!
|
184
|
+
|
185
|
+
expect_to_yield_queries(count: 2) do
|
186
|
+
a.update_attributes! name: "Text",
|
187
|
+
file: Saviour::StringSource.new("foo", "file.txt")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
166
191
|
context do
|
167
192
|
let(:klass) {
|
168
193
|
a = Class.new(Test) { include Saviour::Model }
|
@@ -174,9 +199,8 @@ describe "CRUD" do
|
|
174
199
|
it "saves to db only once with multiple file attachments" do
|
175
200
|
a = klass.create!
|
176
201
|
|
177
|
-
# 2 update's, first empty and then 1 with the two attributes
|
178
202
|
expected_query = %Q{UPDATE "tests" SET "file" = '/store/dir/file.txt', "file_thumb" = '/store/dir/file.txt'}
|
179
|
-
expect_to_yield_queries(count:
|
203
|
+
expect_to_yield_queries(count: 1, including: [expected_query]) do
|
180
204
|
a.update_attributes!(
|
181
205
|
file: Saviour::StringSource.new("foo", "file.txt"),
|
182
206
|
file_thumb: Saviour::StringSource.new("foo", "file.txt")
|
data/spec/feature/dirty_spec.rb
CHANGED
@@ -3,11 +3,14 @@ require 'spec_helper'
|
|
3
3
|
describe "dirty model" do
|
4
4
|
before { allow(Saviour::Config).to receive(:storage).and_return(Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://domain.com")) }
|
5
5
|
|
6
|
-
|
6
|
+
before do
|
7
7
|
uploader = Class.new(Saviour::BaseUploader) { store_dir { "/store/dir" } }
|
8
|
-
klass = Class.new(Test) { include Saviour::Model }
|
9
|
-
klass.attach_file :file, uploader
|
10
|
-
|
8
|
+
@klass = Class.new(Test) { include Saviour::Model }
|
9
|
+
@klass.attach_file :file, uploader
|
10
|
+
end
|
11
|
+
|
12
|
+
it "provides changes and previous file" do
|
13
|
+
a = @klass.create!
|
11
14
|
|
12
15
|
with_test_file("example.xml") do |xml_file|
|
13
16
|
with_test_file("camaloon.jpg") do |jpg_file|
|
@@ -28,17 +31,76 @@ describe "dirty model" do
|
|
28
31
|
expect(a.file_was.persisted?).to be_truthy
|
29
32
|
|
30
33
|
expect(a.changed_attributes).to include("file" => a.file_was)
|
34
|
+
expect(a.changed).to eq ["file"]
|
35
|
+
|
36
|
+
expect(a.changed?).to be_truthy
|
37
|
+
|
38
|
+
expect(a.changes).to eq({"file" => [a.file_was, a.file]})
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
43
|
+
it "provides changes on two changed attributes" do
|
44
|
+
a = @klass.create!
|
45
|
+
|
46
|
+
a.name = "Foo bar"
|
47
|
+
a.file = Saviour::StringSource.new("contents", "file.txt")
|
48
|
+
|
49
|
+
expect(a.changed.sort).to eq ["file", "name"]
|
50
|
+
a.save!
|
51
|
+
|
52
|
+
a.name = "Johny"
|
53
|
+
a.file = Saviour::StringSource.new("contents", "file_2.txt")
|
54
|
+
|
55
|
+
expect(a.changed.sort).to eq ["file", "name"]
|
56
|
+
expect(a.changes).to eq({ "file" => [a.file_was, a.file], "name" => ["Foo bar", "Johny"]})
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns always the first ever known value as was" do
|
60
|
+
a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt")
|
61
|
+
url_was = a.file.url
|
62
|
+
|
63
|
+
a.file = Saviour::StringSource.new("contents", "file_45.txt")
|
64
|
+
expect(a.file_was.url).to eq url_was
|
65
|
+
|
66
|
+
a.file = Saviour::StringSource.new("contents", "file_95.txt")
|
67
|
+
expect(a.file_was.url).to eq url_was
|
68
|
+
end
|
69
|
+
|
35
70
|
it "changes are nil when not persisted" do
|
36
|
-
|
37
|
-
klass = Class.new(Test) { include Saviour::Model }
|
38
|
-
klass.attach_file :file, uploader
|
71
|
+
a = @klass.new file: Saviour::StringSource.new("contents", "file.txt")
|
39
72
|
|
40
|
-
a = klass.new file: Saviour::StringSource.new("contents", "file.txt")
|
41
73
|
expect(a.file_changed?).to be_truthy
|
42
74
|
expect(a.file_was).to be_nil
|
43
75
|
end
|
76
|
+
|
77
|
+
it "changes are nil when persisted but no file was assigned" do
|
78
|
+
a = @klass.create!
|
79
|
+
a.file = Saviour::StringSource.new("contents", "file.txt")
|
80
|
+
expect(a.file_was).to be_nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it "was data is kept after reload" do
|
84
|
+
a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt"), name: "Cuca"
|
85
|
+
expect(a.name_was).to eq "Cuca"
|
86
|
+
expect(a.file_was).to eq a.file
|
87
|
+
|
88
|
+
a.reload
|
89
|
+
expect(a.file_was).to eq a.file
|
90
|
+
end
|
91
|
+
|
92
|
+
it "was data is refreshed on persisting a new assignation" do
|
93
|
+
a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt")
|
94
|
+
a.file = Saviour::StringSource.new("contents", "file_2.txt")
|
95
|
+
a.save!
|
96
|
+
expect(a.file_was.url).to match(/file_2\.txt/)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "was file is cleared on dup" do
|
100
|
+
a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt")
|
101
|
+
expect(a.file_was).to eq a.file
|
102
|
+
|
103
|
+
b = a.dup
|
104
|
+
expect(b.file_was).to be_nil
|
105
|
+
end
|
44
106
|
end
|