saviour 0.5.10 → 0.5.11
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.
- 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
|