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 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