saviour 0.5.2 → 0.5.3
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/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
|