saviour 0.4.14 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/saviour/base_uploader.rb +17 -1
- data/lib/saviour/config.rb +8 -0
- data/lib/saviour/db_helpers.rb +6 -6
- data/lib/saviour/file.rb +0 -3
- data/lib/saviour/life_cycle.rb +102 -48
- data/lib/saviour/persistence_layer.rb +4 -0
- data/lib/saviour/version.rb +1 -1
- data/lib/saviour.rb +1 -0
- data/saviour.gemspec +1 -0
- data/spec/feature/concurrent_processors_spec.rb +118 -0
- data/spec/feature/crud_workflows_spec.rb +42 -0
- data/spec/feature/processors_api_spec.rb +26 -0
- data/spec/feature/stash_spec.rb +86 -0
- data/spec/spec_helper.rb +6 -3
- data/spec/support/active_record_asserts.rb +35 -0
- data/spec/support/schema.rb +4 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '04095eaf0410d74e00e023a53cc473e8b5f2898f49673a329127d446a87f0080'
|
4
|
+
data.tar.gz: a3cba3ccb7d59f0f0e219475f003f21f102205132d3bc2cfb9ccc2ca6b0b5c54
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83428891e963c4a96bffe2865380bbd586af945029e23d50c86c8b6aacac99f2ee7b928d89383bba0e1845175396e0b6f0c972d18e2df03390579cbf197518f0
|
7
|
+
data.tar.gz: 3ba8709c6d680f74eb933c3336b3954cd284b8bd52445db2cab72e2c58e1534152667a482b5cab2b5c8cf49a95a63e22f2a0a33e4cebf1becd03f3e1dddc65a6
|
@@ -5,6 +5,7 @@ module Saviour
|
|
5
5
|
class BaseUploader
|
6
6
|
def initialize(opts = {})
|
7
7
|
@data = opts.fetch(:data, {})
|
8
|
+
@stash = {}
|
8
9
|
end
|
9
10
|
|
10
11
|
def method_missing(name, *args, &block)
|
@@ -51,6 +52,14 @@ module Saviour
|
|
51
52
|
throw(:halt_process)
|
52
53
|
end
|
53
54
|
|
55
|
+
def stash(hash)
|
56
|
+
@stash.merge!(hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
def stashed
|
60
|
+
@stash
|
61
|
+
end
|
62
|
+
|
54
63
|
def store_dir
|
55
64
|
@store_dir ||= Uploader::StoreDirExtractor.new(self).store_dir
|
56
65
|
end
|
@@ -76,10 +85,17 @@ module Saviour
|
|
76
85
|
process(name, opts, :file, &block)
|
77
86
|
end
|
78
87
|
|
79
|
-
|
80
88
|
def store_dir(name = nil, &block)
|
81
89
|
store_dirs.push(name || block)
|
82
90
|
end
|
91
|
+
|
92
|
+
def after_upload(&block)
|
93
|
+
after_upload_hooks.push(block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def after_upload_hooks
|
97
|
+
@after_upload ||= []
|
98
|
+
end
|
83
99
|
end
|
84
100
|
end
|
85
101
|
end
|
data/lib/saviour/config.rb
CHANGED
data/lib/saviour/db_helpers.rb
CHANGED
@@ -47,22 +47,22 @@ module Saviour
|
|
47
47
|
|
48
48
|
class << self
|
49
49
|
|
50
|
-
def run_after_commit(&block)
|
51
|
-
unless
|
50
|
+
def run_after_commit(connection = ActiveRecord::Base.connection, &block)
|
51
|
+
unless connection.current_transaction.open?
|
52
52
|
raise NotInTransaction, 'Trying to use `run_after_commit` but no transaction is currently open.'
|
53
53
|
end
|
54
54
|
|
55
55
|
dummy = CommitDummy.new(block)
|
56
|
-
|
56
|
+
connection.add_transaction_record(dummy)
|
57
57
|
end
|
58
58
|
|
59
|
-
def run_after_rollback(&block)
|
60
|
-
unless
|
59
|
+
def run_after_rollback(connection = ActiveRecord::Base.connection, &block)
|
60
|
+
unless connection.current_transaction.open?
|
61
61
|
raise NotInTransaction, 'Trying to use `run_after_commit` but no transaction is currently open.'
|
62
62
|
end
|
63
63
|
|
64
64
|
dummy = RollbackDummy.new(block)
|
65
|
-
|
65
|
+
connection.add_transaction_record(dummy)
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
data/lib/saviour/file.rb
CHANGED
data/lib/saviour/life_cycle.rb
CHANGED
@@ -1,5 +1,80 @@
|
|
1
1
|
module Saviour
|
2
2
|
class LifeCycle
|
3
|
+
class FileCreator
|
4
|
+
def initialize(current_path, file, column, connection)
|
5
|
+
@file = file
|
6
|
+
@column = column
|
7
|
+
@current_path = current_path
|
8
|
+
@connection = connection
|
9
|
+
end
|
10
|
+
|
11
|
+
def upload
|
12
|
+
@new_path = @file.write
|
13
|
+
|
14
|
+
return unless @new_path
|
15
|
+
|
16
|
+
DbHelpers.run_after_rollback(@connection) do
|
17
|
+
Config.storage.delete(@new_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
[@column, @new_path]
|
21
|
+
end
|
22
|
+
|
23
|
+
def uploader
|
24
|
+
@file.uploader
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class FileUpdater
|
29
|
+
def initialize(current_path, file, column, connection)
|
30
|
+
@file = file
|
31
|
+
@column = column
|
32
|
+
@current_path = current_path
|
33
|
+
@connection = connection
|
34
|
+
end
|
35
|
+
|
36
|
+
def upload
|
37
|
+
dup_temp_path = SecureRandom.hex
|
38
|
+
|
39
|
+
dup_file = proc do
|
40
|
+
Config.storage.cp @current_path, dup_temp_path
|
41
|
+
|
42
|
+
DbHelpers.run_after_commit(@connection) do
|
43
|
+
Config.storage.delete dup_temp_path
|
44
|
+
end
|
45
|
+
|
46
|
+
DbHelpers.run_after_rollback(@connection) do
|
47
|
+
Config.storage.mv dup_temp_path, @current_path
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@new_path = @file.write(
|
52
|
+
before_write: ->(path) { dup_file.call if @current_path == path }
|
53
|
+
)
|
54
|
+
|
55
|
+
return unless @new_path
|
56
|
+
|
57
|
+
if @current_path && @current_path != @new_path
|
58
|
+
DbHelpers.run_after_commit(@connection) do
|
59
|
+
Config.storage.delete(@current_path)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Delete the newly uploaded file only if it's an update in a different path
|
64
|
+
if @current_path.nil? || @current_path != @new_path
|
65
|
+
DbHelpers.run_after_rollback(@connection) do
|
66
|
+
Config.storage.delete(@new_path)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
[@column, @new_path]
|
71
|
+
end
|
72
|
+
|
73
|
+
def uploader
|
74
|
+
@file.uploader
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
3
78
|
def initialize(model, persistence_klass)
|
4
79
|
raise ConfigurationError, "Please provide an object compatible with Saviour." unless model.class.respond_to?(:attached_files)
|
5
80
|
|
@@ -16,71 +91,50 @@ module Saviour
|
|
16
91
|
end
|
17
92
|
|
18
93
|
def create!
|
19
|
-
|
20
|
-
next unless @model.send(column).changed?
|
21
|
-
|
22
|
-
persistence_layer = @persistence_klass.new(@model)
|
23
|
-
new_path = @model.send(column).write
|
24
|
-
|
25
|
-
if new_path
|
26
|
-
persistence_layer.write(column, new_path)
|
27
|
-
|
28
|
-
DbHelpers.run_after_rollback do
|
29
|
-
Config.storage.delete(new_path)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
94
|
+
process_upload(FileCreator)
|
33
95
|
end
|
34
96
|
|
35
97
|
def update!
|
36
|
-
|
37
|
-
next unless @model.send(column).changed?
|
38
|
-
|
39
|
-
update_file(column)
|
40
|
-
end
|
98
|
+
process_upload(FileUpdater)
|
41
99
|
end
|
42
100
|
|
43
|
-
|
44
101
|
private
|
45
102
|
|
46
|
-
|
47
|
-
def update_file(column)
|
103
|
+
def process_upload(klass)
|
48
104
|
persistence_layer = @persistence_klass.new(@model)
|
49
|
-
current_path = persistence_layer.read(column)
|
50
|
-
dup_temp_path = SecureRandom.hex
|
51
105
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
DbHelpers.run_after_commit do
|
56
|
-
Config.storage.delete dup_temp_path
|
57
|
-
end
|
106
|
+
uploaders = attached_files.map do |column|
|
107
|
+
next unless @model.send(column).changed?
|
58
108
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
109
|
+
klass.new(
|
110
|
+
persistence_layer.read(column),
|
111
|
+
@model.send(column),
|
112
|
+
column,
|
113
|
+
ActiveRecord::Base.connection
|
114
|
+
)
|
115
|
+
end.compact
|
63
116
|
|
64
|
-
|
65
|
-
|
66
|
-
)
|
117
|
+
pool = Concurrent::FixedThreadPool.new(Saviour::Config.concurrent_workers)
|
118
|
+
futures = uploaders.map { |uploader| Concurrent::Future.execute(executor: pool) { uploader.upload } }
|
67
119
|
|
68
|
-
|
69
|
-
|
120
|
+
pool.shutdown
|
121
|
+
pool.wait_for_termination
|
70
122
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
123
|
+
result = futures.map do |x|
|
124
|
+
x.value.tap do
|
125
|
+
raise(x.reason) if x.rejected?
|
75
126
|
end
|
127
|
+
end.compact
|
76
128
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
129
|
+
attrs = result.to_h
|
130
|
+
|
131
|
+
uploaders.map(&:uploader).select { |x| x.class.after_upload_hooks.any? }.each do |uploader|
|
132
|
+
uploader.class.after_upload_hooks.each do |hook|
|
133
|
+
uploader.instance_exec(uploader.stashed, &hook)
|
82
134
|
end
|
83
135
|
end
|
136
|
+
|
137
|
+
persistence_layer.write_attrs(attrs) if attrs.length > 0
|
84
138
|
end
|
85
139
|
|
86
140
|
def attached_files
|
data/lib/saviour/version.rb
CHANGED
data/lib/saviour.rb
CHANGED
data/saviour.gemspec
CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
|
18
18
|
spec.add_dependency "activerecord", ">= 5.0"
|
19
19
|
spec.add_dependency "activesupport", ">= 5.0"
|
20
|
+
spec.add_dependency "concurrent-ruby", ">= 1.0.5"
|
20
21
|
|
21
22
|
spec.add_development_dependency "bundler"
|
22
23
|
spec.add_development_dependency "rspec"
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "concurrent processors" do
|
4
|
+
before { allow(Saviour::Config).to receive(:storage).and_return(Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://domain.com")) }
|
5
|
+
|
6
|
+
WAIT_TIME = 0.5
|
7
|
+
|
8
|
+
let(:uploader) {
|
9
|
+
Class.new(Saviour::BaseUploader) {
|
10
|
+
store_dir { "/store/dir" }
|
11
|
+
|
12
|
+
process_with_file do |file, filename|
|
13
|
+
sleep WAIT_TIME
|
14
|
+
|
15
|
+
[file, filename]
|
16
|
+
end
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
let(:klass) {
|
21
|
+
klass = Class.new(Test) { include Saviour::Model }
|
22
|
+
klass.attach_file :file, uploader
|
23
|
+
klass.attach_file :file_thumb, uploader
|
24
|
+
klass.attach_file :file_thumb_2, uploader
|
25
|
+
klass.attach_file :file_thumb_3, uploader
|
26
|
+
klass
|
27
|
+
}
|
28
|
+
|
29
|
+
context 'on update' do
|
30
|
+
it 'works concurrently with 4 workers' do
|
31
|
+
a = klass.create!
|
32
|
+
|
33
|
+
Saviour::Config.concurrent_workers = 4
|
34
|
+
|
35
|
+
t0 = Time.now
|
36
|
+
a.update_attributes! file: Saviour::StringSource.new("contents", "file.txt"),
|
37
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
38
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
39
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
40
|
+
|
41
|
+
diff = Time.now - t0
|
42
|
+
expect(diff).to be_within(0.05).of(WAIT_TIME)
|
43
|
+
expect(diff).to be < WAIT_TIME * 4
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'works in serial with 1 worker' do
|
47
|
+
a = klass.create!
|
48
|
+
|
49
|
+
Saviour::Config.concurrent_workers = 1
|
50
|
+
|
51
|
+
t0 = Time.now
|
52
|
+
a.update_attributes! file: Saviour::StringSource.new("contents", "file.txt"),
|
53
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
54
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
55
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
56
|
+
|
57
|
+
expect(Time.now - t0).to be >= WAIT_TIME * 4
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'concurrency can be adjusted' do
|
61
|
+
a = klass.create!
|
62
|
+
|
63
|
+
Saviour::Config.concurrent_workers = 2
|
64
|
+
|
65
|
+
t0 = Time.now
|
66
|
+
a.update_attributes! file: Saviour::StringSource.new("contents", "file.txt"),
|
67
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
68
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
69
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
70
|
+
|
71
|
+
diff = Time.now - t0
|
72
|
+
expect(diff).to be_within(0.05).of(WAIT_TIME * 2)
|
73
|
+
expect(diff).to be < WAIT_TIME * 4
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'on create' do
|
78
|
+
it 'works concurrently with 4 workers' do
|
79
|
+
Saviour::Config.concurrent_workers = 4
|
80
|
+
|
81
|
+
t0 = Time.now
|
82
|
+
klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
|
83
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
84
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
85
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
86
|
+
|
87
|
+
diff = Time.now - t0
|
88
|
+
expect(diff).to be_within(0.05).of(WAIT_TIME)
|
89
|
+
expect(diff).to be < WAIT_TIME * 4
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'works in serial with 1 worker' do
|
93
|
+
Saviour::Config.concurrent_workers = 1
|
94
|
+
|
95
|
+
t0 = Time.now
|
96
|
+
klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
|
97
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
98
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
99
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
100
|
+
|
101
|
+
expect(Time.now - t0).to be >= WAIT_TIME * 4
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'concurrency can be adjusted' do
|
105
|
+
Saviour::Config.concurrent_workers = 2
|
106
|
+
|
107
|
+
t0 = Time.now
|
108
|
+
klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
|
109
|
+
file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
|
110
|
+
file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
|
111
|
+
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
112
|
+
|
113
|
+
diff = Time.now - t0
|
114
|
+
expect(diff).to be_within(0.05).of(WAIT_TIME * 2)
|
115
|
+
expect(diff).to be < WAIT_TIME * 4
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -103,6 +103,26 @@ describe "saving a new file" do
|
|
103
103
|
a.save
|
104
104
|
end
|
105
105
|
end
|
106
|
+
|
107
|
+
context do
|
108
|
+
let(:klass) {
|
109
|
+
a = Class.new(Test) { include Saviour::Model }
|
110
|
+
a.attach_file :file, uploader
|
111
|
+
a.attach_file :file_thumb, uploader
|
112
|
+
a
|
113
|
+
}
|
114
|
+
|
115
|
+
it "saves to db only once with multiple file attachments" do
|
116
|
+
# 1 create + 1 update with two attributes
|
117
|
+
expected_query = %Q{UPDATE "tests" SET "file" = '/store/dir/file.txt', "file_thumb" = '/store/dir/file.txt'}
|
118
|
+
expect_to_yield_queries(count: 2, including: [expected_query]) do
|
119
|
+
klass.create!(
|
120
|
+
file: Saviour::StringSource.new("foo", "file.txt"),
|
121
|
+
file_thumb: Saviour::StringSource.new("foo", "file.txt")
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
106
126
|
end
|
107
127
|
|
108
128
|
describe "deletion" do
|
@@ -142,6 +162,28 @@ describe "saving a new file" do
|
|
142
162
|
a.update_attributes! file: Saviour::StringSource.new("foo", "file.txt")
|
143
163
|
expect(Saviour::Config.storage.read(a[:file])).to eq "foo"
|
144
164
|
end
|
165
|
+
|
166
|
+
context do
|
167
|
+
let(:klass) {
|
168
|
+
a = Class.new(Test) { include Saviour::Model }
|
169
|
+
a.attach_file :file, uploader
|
170
|
+
a.attach_file :file_thumb, uploader
|
171
|
+
a
|
172
|
+
}
|
173
|
+
|
174
|
+
it "saves to db only once with multiple file attachments" do
|
175
|
+
a = klass.create!
|
176
|
+
|
177
|
+
# 2 update's, first empty and then 1 with the two attributes
|
178
|
+
expected_query = %Q{UPDATE "tests" SET "file" = '/store/dir/file.txt', "file_thumb" = '/store/dir/file.txt'}
|
179
|
+
expect_to_yield_queries(count: 2, including: [expected_query]) do
|
180
|
+
a.update_attributes!(
|
181
|
+
file: Saviour::StringSource.new("foo", "file.txt"),
|
182
|
+
file_thumb: Saviour::StringSource.new("foo", "file.txt")
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
145
187
|
end
|
146
188
|
|
147
189
|
describe "dupping" do
|
@@ -72,4 +72,30 @@ describe "processor's API" do
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
75
|
+
|
76
|
+
describe "errors raised from processors are propagated" do
|
77
|
+
let(:uploader) {
|
78
|
+
Class.new(Saviour::BaseUploader) do
|
79
|
+
store_dir { "/store/dir/#{model.id}" }
|
80
|
+
|
81
|
+
process do |contents, filename|
|
82
|
+
raise "custom problem!"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
}
|
86
|
+
|
87
|
+
it "on update" do
|
88
|
+
a = klass.create!
|
89
|
+
|
90
|
+
expect {
|
91
|
+
a.update_attributes! file: Saviour::StringSource.new("contents", "filename.txt")
|
92
|
+
}.to raise_error.with_message("custom problem!")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "on create" do
|
96
|
+
expect {
|
97
|
+
klass.create! file: Saviour::StringSource.new("contents", "filename.txt")
|
98
|
+
}.to raise_error.with_message("custom problem!")
|
99
|
+
end
|
100
|
+
end
|
75
101
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "stash data on process" do
|
4
|
+
before { allow(Saviour::Config).to receive(:storage).and_return(Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://domain.com")) }
|
5
|
+
|
6
|
+
it 'stores data on after upload on update' do
|
7
|
+
uploader = Class.new(Saviour::BaseUploader) {
|
8
|
+
store_dir { "/store/dir" }
|
9
|
+
|
10
|
+
process_with_file do |file, filename|
|
11
|
+
stash(file_size: File.size(file.path))
|
12
|
+
|
13
|
+
[file, filename]
|
14
|
+
end
|
15
|
+
|
16
|
+
after_upload do |stash|
|
17
|
+
model.update_attributes!(file_size: stash[:file_size])
|
18
|
+
end
|
19
|
+
}
|
20
|
+
|
21
|
+
klass = Class.new(Test) { include Saviour::Model }
|
22
|
+
klass.attach_file :file, uploader
|
23
|
+
|
24
|
+
a = klass.create!
|
25
|
+
|
26
|
+
a.update_attributes! file: Saviour::StringSource.new("a" * 74, "file.txt")
|
27
|
+
expect(a.file_size).to eq 74
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'stores data on after upload on create' do
|
31
|
+
uploader = Class.new(Saviour::BaseUploader) {
|
32
|
+
store_dir { "/store/dir" }
|
33
|
+
|
34
|
+
process_with_file do |file, filename|
|
35
|
+
stash(file_size: File.size(file.path))
|
36
|
+
|
37
|
+
[file, filename]
|
38
|
+
end
|
39
|
+
|
40
|
+
after_upload do |stash|
|
41
|
+
model.update_attributes!(file_size: stash[:file_size])
|
42
|
+
end
|
43
|
+
}
|
44
|
+
|
45
|
+
klass = Class.new(Test) { include Saviour::Model }
|
46
|
+
klass.attach_file :file, uploader
|
47
|
+
|
48
|
+
a = klass.create! file: Saviour::StringSource.new("a" * 74, "file.txt")
|
49
|
+
|
50
|
+
expect(a.file_size).to eq 74
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'stashes are independent per uploader' do
|
54
|
+
uploader = Class.new(Saviour::BaseUploader) {
|
55
|
+
store_dir { "/store/dir" }
|
56
|
+
|
57
|
+
process_with_file do |file, filename|
|
58
|
+
# same ':size' key in the stash
|
59
|
+
stash(size: File.size(file.path))
|
60
|
+
|
61
|
+
[file, filename]
|
62
|
+
end
|
63
|
+
|
64
|
+
after_upload do |stash|
|
65
|
+
model.update_attributes!("size_#{attached_as}" => stash[:size])
|
66
|
+
end
|
67
|
+
}
|
68
|
+
|
69
|
+
klass = Class.new(Test) { include Saviour::Model }
|
70
|
+
klass.attach_file :file, uploader
|
71
|
+
klass.attach_file :file_thumb, uploader
|
72
|
+
|
73
|
+
a = klass.create!
|
74
|
+
|
75
|
+
# - 1 initial empty query
|
76
|
+
# - 2 queries to update size
|
77
|
+
# - 1 query to assign stored paths
|
78
|
+
expect_to_yield_queries(count: 4) do
|
79
|
+
a.update_attributes! file: Saviour::StringSource.new("a" * 74, "file.txt"),
|
80
|
+
file_thumb: Saviour::StringSource.new("a" * 31, "file_2.txt")
|
81
|
+
end
|
82
|
+
|
83
|
+
expect(a.size_file).to eq 74
|
84
|
+
expect(a.size_file_thumb).to eq 31
|
85
|
+
end
|
86
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -7,15 +7,17 @@ require 'active_record'
|
|
7
7
|
require 'sqlite3'
|
8
8
|
require 'get_process_mem'
|
9
9
|
|
10
|
+
require 'support/active_record_asserts'
|
11
|
+
|
10
12
|
require File.expand_path("../../lib/saviour", __FILE__)
|
11
13
|
|
12
14
|
connection_opts = case ENV.fetch('DB', "sqlite")
|
13
15
|
when "sqlite"
|
14
|
-
{adapter: "sqlite3", database: ":memory:"}
|
16
|
+
{ adapter: "sqlite3", database: ":memory:" }
|
15
17
|
when "mysql"
|
16
|
-
{adapter: "mysql2", database: "saviour", username: "root", encoding: "utf8"}
|
18
|
+
{ adapter: "mysql2", database: "saviour", username: "root", encoding: "utf8" }
|
17
19
|
when "postgres"
|
18
|
-
{adapter: "postgresql", database: "saviour", username: "postgres"}
|
20
|
+
{ adapter: "postgresql", database: "saviour", username: "postgres" }
|
19
21
|
end
|
20
22
|
|
21
23
|
ActiveRecord::Base.establish_connection(connection_opts)
|
@@ -44,6 +46,7 @@ RSpec.configure do |config|
|
|
44
46
|
|
45
47
|
config.before do
|
46
48
|
Test.delete_all
|
49
|
+
Saviour::Config.concurrent_workers = 4
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActiveRecordAssertions
|
2
|
+
def expect_to_yield_queries(count: nil, including: [])
|
3
|
+
AssertionsTracker.clear!
|
4
|
+
yield
|
5
|
+
|
6
|
+
expect(AssertionsTracker.data.size).to eq(count) if count
|
7
|
+
|
8
|
+
including.each do |query|
|
9
|
+
expect(AssertionsTracker.data).to include a_string_matching(Regexp.new(query))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module AssertionsTracker
|
15
|
+
def self.data
|
16
|
+
@data ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.clear!
|
20
|
+
@data = []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec.configure do |config|
|
25
|
+
config.before do
|
26
|
+
AssertionsTracker.clear!
|
27
|
+
end
|
28
|
+
|
29
|
+
config.include ActiveRecordAssertions
|
30
|
+
end
|
31
|
+
|
32
|
+
ActiveSupport::Notifications.subscribe "sql.active_record" do |name, started, finished, unique_id, data|
|
33
|
+
AssertionsTracker.data.push(data[:sql]) if data[:name] == "SQL"
|
34
|
+
end
|
35
|
+
|
data/spec/support/schema.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saviour
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roger Campos
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: concurrent-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.0.5
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.5
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -186,6 +200,7 @@ files:
|
|
186
200
|
- lib/saviour/version.rb
|
187
201
|
- saviour.gemspec
|
188
202
|
- spec/feature/allow_overriding_attached_as_method_spec.rb
|
203
|
+
- spec/feature/concurrent_processors_spec.rb
|
189
204
|
- spec/feature/crud_workflows_spec.rb
|
190
205
|
- spec/feature/dirty_spec.rb
|
191
206
|
- spec/feature/follow_file_spec.rb
|
@@ -199,6 +214,7 @@ files:
|
|
199
214
|
- spec/feature/remove_attachment_spec.rb
|
200
215
|
- spec/feature/reopens_file_at_every_process_spec.rb
|
201
216
|
- spec/feature/rewind_source_before_read_spec.rb
|
217
|
+
- spec/feature/stash_spec.rb
|
202
218
|
- spec/feature/transactional_behavior_spec.rb
|
203
219
|
- spec/feature/uploader_declaration_spec.rb
|
204
220
|
- spec/feature/validations_spec.rb
|
@@ -211,6 +227,7 @@ files:
|
|
211
227
|
- spec/models/s3_storage_spec.rb
|
212
228
|
- spec/models/url_source_spec.rb
|
213
229
|
- spec/spec_helper.rb
|
230
|
+
- spec/support/active_record_asserts.rb
|
214
231
|
- spec/support/data/camaloon.jpg
|
215
232
|
- spec/support/data/example.xml
|
216
233
|
- spec/support/data/text.txt
|