saviour 0.4.14 → 0.5.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.
- 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
|