saviour 0.5.2 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/saviour/file.rb +6 -2
- data/lib/saviour/integrator.rb +41 -13
- data/lib/saviour/s3_storage.rb +12 -19
- data/lib/saviour/version.rb +1 -1
- data/saviour.gemspec +1 -1
- data/spec/feature/concurrent_processors_spec.rb +13 -7
- data/spec/feature/follow_file_spec.rb +68 -1
- data/spec/feature/introspection_spec.rb +1 -1
- data/spec/feature/remove_attachment_spec.rb +43 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e06f1497a5f51dab300598d70e8c31f3930255ed8398857725cf5a6d7789a7b2
|
4
|
+
data.tar.gz: 915adfc7973e402555b7edcf7fef4791227098c065f37aa867df519d5c73902d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f477a2b6baa7a031b8ddef7634b3d086651c309c67c9e41328654a42569ef9dc9ecbe096cf680b3ac7447ef5f29d3e807f136ea8f704ed406cac0de624a4366a
|
7
|
+
data.tar.gz: bf268771af642e63095e82abb1cee2bc28149a51d918b48a2dbd5228e9d7262785f2a5b66325b8339acc577fcf17fd3b3914419a5de2b61144e2d23318454195
|
data/lib/saviour/file.rb
CHANGED
@@ -71,8 +71,12 @@ module Saviour
|
|
71
71
|
def assign(object)
|
72
72
|
raise(SourceError, "given object to #assign or #<attach_as>= must respond to `read`") if object && !object.respond_to?(:read)
|
73
73
|
|
74
|
-
followers = @model.class.
|
75
|
-
|
74
|
+
followers = @model.class.followers_per_leader_config[@attached_as]
|
75
|
+
|
76
|
+
(followers || []).each do |x|
|
77
|
+
attachment = @model.send(x[:attachment])
|
78
|
+
attachment.assign(object) unless attachment.changed?
|
79
|
+
end
|
76
80
|
|
77
81
|
@source_data = nil
|
78
82
|
@source = object
|
data/lib/saviour/integrator.rb
CHANGED
@@ -10,8 +10,8 @@ module Saviour
|
|
10
10
|
|
11
11
|
@klass.class_attribute :attached_files
|
12
12
|
@klass.attached_files = []
|
13
|
-
@klass.class_attribute :
|
14
|
-
@klass.
|
13
|
+
@klass.class_attribute :followers_per_leader_config
|
14
|
+
@klass.followers_per_leader_config = {}
|
15
15
|
|
16
16
|
klass = @klass
|
17
17
|
persistence_klass = @persistence_klass
|
@@ -21,8 +21,14 @@ module Saviour
|
|
21
21
|
uploader_klass = maybe_uploader_klass[0]
|
22
22
|
|
23
23
|
if opts[:follow]
|
24
|
-
|
25
|
-
|
24
|
+
dependent = opts[:dependent]
|
25
|
+
|
26
|
+
if dependent.nil? || ![:destroy, :ignore].include?(dependent)
|
27
|
+
raise(ConfigurationError, "You must specify a :dependent option when using :follow. Use either :destroy or :ignore")
|
28
|
+
end
|
29
|
+
|
30
|
+
klass.followers_per_leader_config[opts[:follow]] ||= []
|
31
|
+
klass.followers_per_leader_config[opts[:follow]].push({ attachment: attach_as, dependent: dependent })
|
26
32
|
end
|
27
33
|
|
28
34
|
if uploader_klass.nil? && block.nil?
|
@@ -50,17 +56,33 @@ module Saviour
|
|
50
56
|
send(attach_as).changed?
|
51
57
|
end
|
52
58
|
|
53
|
-
define_method("remove_#{attach_as}!") do
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
59
|
+
define_method("remove_#{attach_as}!") do |dependent: nil|
|
60
|
+
if !dependent.nil? && ![:destroy, :ignore].include?(dependent)
|
61
|
+
raise ArgumentError, ":dependent option must be either :destroy or :ignore"
|
62
|
+
end
|
63
|
+
|
64
|
+
layer = persistence_klass.new(self)
|
65
|
+
|
66
|
+
attachment_remover = proc do |attach_as|
|
67
|
+
work = proc do
|
68
|
+
send(attach_as).delete
|
69
|
+
layer.write(attach_as, nil)
|
70
|
+
end
|
71
|
+
|
72
|
+
if ActiveRecord::Base.connection.current_transaction.open?
|
73
|
+
DbHelpers.run_after_commit &work
|
74
|
+
else
|
75
|
+
work.call
|
76
|
+
end
|
58
77
|
end
|
59
78
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
79
|
+
attachment_remover.call(attach_as)
|
80
|
+
|
81
|
+
(self.class.followers_per_leader_config[attach_as] || []).each do |follower|
|
82
|
+
dependent_option = dependent || follower[:dependent]
|
83
|
+
next if dependent_option == :ignore || send(follower[:attachment]).changed?
|
84
|
+
|
85
|
+
attachment_remover.call(follower[:attachment])
|
64
86
|
end
|
65
87
|
end
|
66
88
|
end
|
@@ -68,6 +90,12 @@ module Saviour
|
|
68
90
|
klass.include mod
|
69
91
|
end
|
70
92
|
|
93
|
+
@klass.define_singleton_method("attached_followers_per_leader") do
|
94
|
+
self.followers_per_leader_config.map do |leader, followers|
|
95
|
+
[leader, followers.map { |data| data[:attachment] }]
|
96
|
+
end.to_h
|
97
|
+
end
|
98
|
+
|
71
99
|
@klass.class_attribute :__saviour_validations
|
72
100
|
|
73
101
|
@klass.define_singleton_method("attach_validation") do |attach_as, method_name = nil, &block|
|
data/lib/saviour/s3_storage.rb
CHANGED
@@ -26,6 +26,7 @@ module Saviour
|
|
26
26
|
raise(KeyTooLarge, "The key in S3 must be at max 1024 bytes, this key is too big: #{path}")
|
27
27
|
end
|
28
28
|
|
29
|
+
# TODO: Use multipart api
|
29
30
|
client.put_object(@create_options.merge(body: file_or_contents, bucket: @bucket, key: path))
|
30
31
|
end
|
31
32
|
|
@@ -36,20 +37,23 @@ module Saviour
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def read_to_file(path, dest_file)
|
40
|
+
path = sanitize_leading_slash(path)
|
41
|
+
|
39
42
|
dest_file.binmode
|
40
43
|
dest_file.rewind
|
41
44
|
dest_file.truncate(0)
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
|
-
dest_file.flush
|
46
|
+
client.get_object({ bucket: @bucket, key: path }, target: dest_file)
|
47
|
+
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
|
48
|
+
raise FileNotPresent, "Trying to read an unexisting path: #{path}"
|
49
49
|
end
|
50
50
|
|
51
51
|
def read(path)
|
52
|
-
|
52
|
+
path = sanitize_leading_slash(path)
|
53
|
+
|
54
|
+
client.get_object(bucket: @bucket, key: path).body.read
|
55
|
+
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
|
56
|
+
raise FileNotPresent, "Trying to read an unexisting path: #{path}"
|
53
57
|
end
|
54
58
|
|
55
59
|
def delete(path)
|
@@ -57,7 +61,7 @@ module Saviour
|
|
57
61
|
|
58
62
|
client.delete_object(
|
59
63
|
bucket: @bucket,
|
60
|
-
key: path
|
64
|
+
key: path
|
61
65
|
)
|
62
66
|
end
|
63
67
|
|
@@ -101,17 +105,6 @@ module Saviour
|
|
101
105
|
|
102
106
|
private
|
103
107
|
|
104
|
-
def get_file_stringio(path)
|
105
|
-
path = sanitize_leading_slash(path)
|
106
|
-
|
107
|
-
client.get_object(
|
108
|
-
bucket: @bucket,
|
109
|
-
key: path
|
110
|
-
).body
|
111
|
-
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
|
112
|
-
raise FileNotPresent, "Trying to read an unexisting path: #{path}"
|
113
|
-
end
|
114
|
-
|
115
108
|
def public_url_prefix
|
116
109
|
if @public_url_prefix.respond_to?(:call)
|
117
110
|
@public_url_prefix.call
|
data/lib/saviour/version.rb
CHANGED
data/saviour.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
|
|
15
15
|
|
16
16
|
spec.required_ruby_version = ">= 2.2.0"
|
17
17
|
|
18
|
-
spec.add_dependency "activerecord", ">= 5.0"
|
18
|
+
spec.add_dependency "activerecord", ">= 5.0", "< 5.2"
|
19
19
|
spec.add_dependency "activesupport", ">= 5.0"
|
20
20
|
spec.add_dependency "concurrent-ruby", ">= 1.0.5"
|
21
21
|
|
@@ -3,7 +3,13 @@ require 'spec_helper'
|
|
3
3
|
describe "concurrent processors" 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']
|
7
|
+
WAIT_TIME = 2
|
8
|
+
THRESHOLD = 1.5
|
9
|
+
else
|
10
|
+
WAIT_TIME = 0.5
|
11
|
+
THRESHOLD = 0.1
|
12
|
+
end
|
7
13
|
|
8
14
|
let(:uploader) {
|
9
15
|
Class.new(Saviour::BaseUploader) {
|
@@ -52,7 +58,7 @@ describe "concurrent processors" do
|
|
52
58
|
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
53
59
|
|
54
60
|
start_times = a.times.values.sort
|
55
|
-
expect((start_times[0] - start_times[-1]).abs).to be_within(
|
61
|
+
expect((start_times[0] - start_times[-1]).abs).to be_within(THRESHOLD).of(0)
|
56
62
|
end
|
57
63
|
|
58
64
|
it 'works in serial with 1 worker' do
|
@@ -80,8 +86,8 @@ describe "concurrent processors" do
|
|
80
86
|
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
81
87
|
|
82
88
|
start_times = a.times.values.sort
|
83
|
-
expect((start_times[0] - start_times[1]).abs).to be_within(
|
84
|
-
expect((start_times[2] - start_times[3]).abs).to be_within(
|
89
|
+
expect((start_times[0] - start_times[1]).abs).to be_within(THRESHOLD).of(0)
|
90
|
+
expect((start_times[2] - start_times[3]).abs).to be_within(THRESHOLD).of(0)
|
85
91
|
expect((start_times[0] - start_times[-1]).abs).to be > WAIT_TIME
|
86
92
|
end
|
87
93
|
end
|
@@ -96,7 +102,7 @@ describe "concurrent processors" do
|
|
96
102
|
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
97
103
|
|
98
104
|
start_times = a.times.values.sort
|
99
|
-
expect((start_times[0] - start_times[-1]).abs).to be_within(
|
105
|
+
expect((start_times[0] - start_times[-1]).abs).to be_within(THRESHOLD).of(0)
|
100
106
|
end
|
101
107
|
|
102
108
|
it 'works in serial with 1 worker' do
|
@@ -120,8 +126,8 @@ describe "concurrent processors" do
|
|
120
126
|
file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
|
121
127
|
|
122
128
|
start_times = a.times.values.sort
|
123
|
-
expect((start_times[0] - start_times[1]).abs).to be_within(
|
124
|
-
expect((start_times[2] - start_times[3]).abs).to be_within(
|
129
|
+
expect((start_times[0] - start_times[1]).abs).to be_within(THRESHOLD).of(0)
|
130
|
+
expect((start_times[2] - start_times[3]).abs).to be_within(THRESHOLD).of(0)
|
125
131
|
expect((start_times[0] - start_times[-1]).abs).to be > WAIT_TIME
|
126
132
|
end
|
127
133
|
end
|
@@ -22,7 +22,7 @@ describe "Make one attachment follow another one" do
|
|
22
22
|
let(:klass) {
|
23
23
|
a = Class.new(Test) { include Saviour::Model }
|
24
24
|
a.attach_file :file, uploader
|
25
|
-
a.attach_file :file_thumb, uploader_for_version, follow: :file
|
25
|
+
a.attach_file :file_thumb, uploader_for_version, follow: :file, dependent: :ignore
|
26
26
|
a
|
27
27
|
}
|
28
28
|
|
@@ -50,4 +50,71 @@ describe "Make one attachment follow another one" do
|
|
50
50
|
expect(a.file_thumb.read.bytesize).to eq 32
|
51
51
|
end
|
52
52
|
end
|
53
|
+
|
54
|
+
describe "dependent destruction" do
|
55
|
+
context "with dependent: :destroy" do
|
56
|
+
let(:klass) {
|
57
|
+
a = Class.new(Test) { include Saviour::Model }
|
58
|
+
a.attach_file :file, uploader
|
59
|
+
a.attach_file :file_thumb, uploader_for_version, follow: :file, dependent: :destroy
|
60
|
+
a
|
61
|
+
}
|
62
|
+
|
63
|
+
it "removes followers" do
|
64
|
+
a = klass.create! file: StringIO.new("some contents without a filename")
|
65
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
66
|
+
|
67
|
+
a.remove_file!
|
68
|
+
expect(a.file_thumb?).to be_falsey
|
69
|
+
expect(a.file_thumb.read).to be_nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it "does not remove follower if it has been changed before destruction in a transaction" do
|
73
|
+
a = klass.create! file: StringIO.new("some contents without a filename")
|
74
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
75
|
+
|
76
|
+
ActiveRecord::Base.transaction do
|
77
|
+
a.file_thumb = StringIO.new("replaced contents")
|
78
|
+
a.remove_file!
|
79
|
+
a.save!
|
80
|
+
end
|
81
|
+
|
82
|
+
expect(a.file_thumb?).to be_truthy
|
83
|
+
expect(a.file_thumb.read).to eq "replaced contents"
|
84
|
+
expect(a.file?).to be_falsey
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not remove follower if it has been changed before destruction outside a transaction" do
|
88
|
+
a = klass.create! file: StringIO.new("some contents without a filename")
|
89
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
90
|
+
|
91
|
+
a.file_thumb = StringIO.new("replaced contents")
|
92
|
+
a.remove_file!
|
93
|
+
a.save!
|
94
|
+
|
95
|
+
expect(a.file_thumb?).to be_truthy
|
96
|
+
expect(a.file_thumb.read).to eq "replaced contents"
|
97
|
+
expect(a.file?).to be_falsey
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "with dependent: :ignore" do
|
102
|
+
let(:klass) {
|
103
|
+
a = Class.new(Test) { include Saviour::Model }
|
104
|
+
a.attach_file :file, uploader
|
105
|
+
a.attach_file :file_thumb, uploader_for_version, follow: :file, dependent: :ignore
|
106
|
+
a
|
107
|
+
}
|
108
|
+
|
109
|
+
it "leaves followers" do
|
110
|
+
a = klass.create! file: StringIO.new("some contents without a filename")
|
111
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
112
|
+
|
113
|
+
a.remove_file!
|
114
|
+
expect(a.file_thumb?).to be_truthy
|
115
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
116
|
+
expect(a.file.read).to be_nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
53
120
|
end
|
@@ -22,7 +22,7 @@ describe "Introspection of attached files" do
|
|
22
22
|
let(:klass) {
|
23
23
|
a = Class.new(Test) { include Saviour::Model }
|
24
24
|
a.attach_file :file, uploader
|
25
|
-
a.attach_file :file_thumb, uploader_for_version, follow: :file
|
25
|
+
a.attach_file :file_thumb, uploader_for_version, follow: :file, dependent: :ignore
|
26
26
|
a
|
27
27
|
}
|
28
28
|
|
@@ -5,7 +5,7 @@ describe "remove attachment" do
|
|
5
5
|
|
6
6
|
let(:uploader) {
|
7
7
|
Class.new(Saviour::BaseUploader) {
|
8
|
-
store_dir { "/store/dir" }
|
8
|
+
store_dir { "/store/dir/#{model.id}/#{attached_as}" }
|
9
9
|
}
|
10
10
|
}
|
11
11
|
|
@@ -80,4 +80,46 @@ describe "remove attachment" do
|
|
80
80
|
expect(a[:file]).to be_nil
|
81
81
|
end
|
82
82
|
end
|
83
|
+
|
84
|
+
context "with followers" do
|
85
|
+
context "when configured to destroy followers" do
|
86
|
+
let(:klass) {
|
87
|
+
a = Class.new(Test) { include Saviour::Model }
|
88
|
+
a.attach_file :file, uploader
|
89
|
+
a.attach_file :file_thumb, uploader, follow: :file, dependent: :destroy
|
90
|
+
a
|
91
|
+
}
|
92
|
+
|
93
|
+
it "does not remove followers when using dependent: :ignore on the remove call" do
|
94
|
+
a = klass.create! file: StringIO.new("some contents without a filename")
|
95
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
96
|
+
|
97
|
+
a.remove_file!(dependent: :ignore)
|
98
|
+
|
99
|
+
expect(a.file_thumb?).to be_truthy
|
100
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
101
|
+
expect(a.file?).to be_falsey
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "when configured to ignore followers" do
|
106
|
+
let(:klass) {
|
107
|
+
a = Class.new(Test) { include Saviour::Model }
|
108
|
+
a.attach_file :file, uploader
|
109
|
+
a.attach_file :file_thumb, uploader, follow: :file, dependent: :ignore
|
110
|
+
a
|
111
|
+
}
|
112
|
+
|
113
|
+
it "does remove followers when using dependent: :destroy on the remove call" do
|
114
|
+
a = klass.create! file: StringIO.new("some contents without a filename")
|
115
|
+
expect(a.file_thumb.read).to eq "some contents without a filename"
|
116
|
+
|
117
|
+
a.remove_file!(dependent: :destroy)
|
118
|
+
|
119
|
+
expect(a.file_thumb?).to be_falsey
|
120
|
+
expect(a.file_thumb.read).to be_nil
|
121
|
+
expect(a.file?).to be_falsey
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
83
125
|
end
|
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.5.
|
4
|
+
version: 0.5.3
|
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-04-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,6 +17,9 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '5.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5.2'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -24,6 +27,9 @@ dependencies:
|
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '5.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5.2'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: activesupport
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -253,7 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
253
259
|
version: '0'
|
254
260
|
requirements: []
|
255
261
|
rubyforge_project:
|
256
|
-
rubygems_version: 2.7.
|
262
|
+
rubygems_version: 2.7.6
|
257
263
|
signing_key:
|
258
264
|
specification_version: 4
|
259
265
|
summary: File storage handler following active record model lifecycle
|