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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1da8130c0e682e7d8c0d86a936a498a8d70d85f5feb8b94d287217062bbb69c
4
- data.tar.gz: ce046b7ff42417a32229f5ba4ee717ecf4a49c1d07ab0923aae3b96152d998ec
3
+ metadata.gz: e06f1497a5f51dab300598d70e8c31f3930255ed8398857725cf5a6d7789a7b2
4
+ data.tar.gz: 915adfc7973e402555b7edcf7fef4791227098c065f37aa867df519d5c73902d
5
5
  SHA512:
6
- metadata.gz: 5b532b295d640352549307de69ac0bf4d4ca63cb1f8312d4e43f97f98d28a122b4249e31e44dc4ee5d99e24a811d31e63f1dfb22d4cda280f20ba634a077f571
7
- data.tar.gz: da792dd8c19baa075f43ba22062f7f8bc920e985f465c5b09660e31e762f5b8c13e93ef3847c9d7fdefe673bbba3dac60c58a920240f76cb91d3611536399741
6
+ metadata.gz: f477a2b6baa7a031b8ddef7634b3d086651c309c67c9e41328654a42569ef9dc9ecbe096cf680b3ac7447ef5f29d3e807f136ea8f704ed406cac0de624a4366a
7
+ data.tar.gz: bf268771af642e63095e82abb1cee2bc28149a51d918b48a2dbd5228e9d7262785f2a5b66325b8339acc577fcf17fd3b3914419a5de2b61144e2d23318454195
@@ -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.attached_followers_per_leader[@attached_as]
75
- followers.each { |x| @model.send(x).assign(object) unless @model.send(x).changed? } if followers
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
@@ -10,8 +10,8 @@ module Saviour
10
10
 
11
11
  @klass.class_attribute :attached_files
12
12
  @klass.attached_files = []
13
- @klass.class_attribute :attached_followers_per_leader
14
- @klass.attached_followers_per_leader = {}
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
- klass.attached_followers_per_leader[opts[:follow]] ||= []
25
- klass.attached_followers_per_leader[opts[:follow]].push(attach_as)
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
- work = proc do
55
- send(attach_as).delete
56
- layer = persistence_klass.new(self)
57
- layer.write(attach_as, nil)
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
- if ActiveRecord::Base.connection.current_transaction.open?
61
- DbHelpers.run_after_commit &work
62
- else
63
- work.call
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|
@@ -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
- io = get_file_stringio(path)
44
- while data = io.read(1024 * 1024)
45
- dest_file.write(data)
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
- get_file_stringio(path).read
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
@@ -1,3 +1,3 @@
1
1
  module Saviour
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.3"
3
3
  end
@@ -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
- WAIT_TIME = 0.5
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(0.1).of(0)
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(0.1).of(0)
84
- expect((start_times[2] - start_times[3]).abs).to be_within(0.1).of(0)
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(0.1).of(0)
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(0.1).of(0)
124
- expect((start_times[2] - start_times[3]).abs).to be_within(0.1).of(0)
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.2
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-03-14 00:00:00.000000000 Z
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.3
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