saviour 0.5.10 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "codeclimate-test-reporter", :group => :test, :require => nil
6
- gem "activesupport", "~> 5.0.0"
7
- gem "activerecord", "~> 5.0.0"
6
+ gem "activesupport", "~> 5.2.0"
7
+ gem "activerecord", "~> 5.2.0"
8
8
 
9
9
  gemspec :path => "../"
@@ -1,6 +1,7 @@
1
1
  require 'saviour/version'
2
2
  require 'saviour/base_uploader'
3
3
  require 'saviour/file'
4
+ require 'saviour/read_only_file'
4
5
  require 'saviour/local_storage'
5
6
  require 'saviour/s3_storage'
6
7
  require 'saviour/config'
@@ -5,13 +5,14 @@ module Saviour
5
5
  attr_reader :persisted_path
6
6
  attr_reader :source
7
7
 
8
- def initialize(uploader_klass, model, attached_as)
8
+ def initialize(uploader_klass, model, attached_as, persisted_path = nil)
9
9
  @uploader_klass, @model, @attached_as = uploader_klass, model, attached_as
10
10
  @source_was = @source = nil
11
- end
11
+ @persisted_path = persisted_path
12
12
 
13
- def set_path!(path)
14
- @persisted_path = path
13
+ if persisted_path
14
+ @model.instance_variable_set("@__uploader_#{@attached_as}_was", ReadOnlyFile.new(persisted_path))
15
+ end
15
16
  end
16
17
 
17
18
  def exists?
@@ -24,7 +25,6 @@ module Saviour
24
25
  end
25
26
 
26
27
  def delete
27
- persisted? && Config.storage.delete(@persisted_path)
28
28
  @persisted_path = nil
29
29
  @source_was = nil
30
30
  @source = nil
@@ -48,10 +48,7 @@ module Saviour
48
48
 
49
49
  def clone
50
50
  return nil unless persisted?
51
-
52
- new_file = Saviour::File.new(@uploader_klass, @model, @attached_as)
53
- new_file.set_path! @persisted_path
54
- new_file
51
+ Saviour::File.new(@uploader_klass, @model, @attached_as, @persisted_path)
55
52
  end
56
53
 
57
54
  def dup(new_model)
@@ -68,6 +65,7 @@ module Saviour
68
65
 
69
66
  def reload
70
67
  @model.instance_variable_set("@__uploader_#{@attached_as}", nil)
68
+ @model.instance_variable_set("@__uploader_#{@attached_as}_was", nil)
71
69
  end
72
70
 
73
71
  alias_method :url, :public_url
@@ -85,8 +83,8 @@ module Saviour
85
83
  @source_data = nil
86
84
  @source = object
87
85
 
88
- if changed? && @model.respond_to?("#{@attached_as}_will_change!")
89
- @model.send "#{@attached_as}_will_change!"
86
+ if changed? && @model.instance_variable_get("@__uploader_#{@attached_as}_was").nil?
87
+ @model.instance_variable_set("@__uploader_#{@attached_as}_was", clone)
90
88
  end
91
89
 
92
90
  @persisted_path = nil if object
@@ -162,6 +160,7 @@ module Saviour
162
160
  end
163
161
 
164
162
  @persisted_path = path
163
+ @model.instance_variable_set("@__uploader_#{@attached_as}_was", ReadOnlyFile.new(persisted_path))
165
164
  path
166
165
  end
167
166
  end
@@ -177,7 +176,7 @@ module Saviour
177
176
 
178
177
  def source_data
179
178
  @source_data ||= begin
180
- @source.rewind
179
+ @source.rewind if @source.respond_to?(:rewind)
181
180
  @source.read
182
181
  end
183
182
  end
@@ -40,10 +40,8 @@ module Saviour
40
40
  define_method(attach_as) do
41
41
  instance_variable_get("@__uploader_#{attach_as}") || begin
42
42
  uploader_klass = Class.new(Saviour::BaseUploader, &block) if block
43
- new_file = ::Saviour::File.new(uploader_klass, self, attach_as)
44
-
45
43
  layer = persistence_klass.new(self)
46
- new_file.set_path!(layer.read(attach_as))
44
+ new_file = ::Saviour::File.new(uploader_klass, self, attach_as, layer.read(attach_as))
47
45
 
48
46
  instance_variable_set("@__uploader_#{attach_as}", new_file)
49
47
  end
@@ -57,10 +55,34 @@ module Saviour
57
55
  send(attach_as).present?
58
56
  end
59
57
 
58
+ define_method("#{attach_as}_was") do
59
+ instance_variable_get("@__uploader_#{attach_as}_was")
60
+ end
61
+
60
62
  define_method("#{attach_as}_changed?") do
61
63
  send(attach_as).changed?
62
64
  end
63
65
 
66
+ define_method(:changed_attributes) do
67
+ if send("#{attach_as}_changed?")
68
+ super().merge(attach_as => send("#{attach_as}_was"))
69
+ else
70
+ super()
71
+ end
72
+ end
73
+
74
+ define_method(:changes) do
75
+ if send("#{attach_as}_changed?")
76
+ super().merge(attach_as => send("#{attach_as}_change"))
77
+ else
78
+ super()
79
+ end
80
+ end
81
+
82
+ define_method("#{attach_as}_change") do
83
+ [send("#{attach_as}_was"), send(attach_as)]
84
+ end
85
+
64
86
  define_method("remove_#{attach_as}!") do |dependent: nil|
65
87
  if !dependent.nil? && ![:destroy, :ignore].include?(dependent)
66
88
  raise ArgumentError, ":dependent option must be either :destroy or :ignore"
@@ -69,9 +91,12 @@ module Saviour
69
91
  layer = persistence_klass.new(self)
70
92
 
71
93
  attachment_remover = proc do |attach_as|
94
+ layer.write(attach_as, nil)
95
+ deletion_path = send(attach_as).persisted_path
96
+ send(attach_as).delete
97
+
72
98
  work = proc do
73
- send(attach_as).delete
74
- layer.write(attach_as, nil)
99
+ Config.storage.delete(deletion_path) if deletion_path && send(attach_as).persisted_path.nil?
75
100
  end
76
101
 
77
102
  if ActiveRecord::Base.connection.current_transaction.open?
@@ -84,9 +84,25 @@ module Saviour
84
84
 
85
85
  def delete!
86
86
  DbHelpers.run_after_commit do
87
- attached_files.each do |column|
88
- @model.send(column).delete
87
+ pool = Concurrent::FixedThreadPool.new(Saviour::Config.concurrent_workers)
88
+
89
+ futures = attached_files.map do |column|
90
+ Concurrent::Future.execute(executor: pool) {
91
+ path = @model.send(column).persisted_path
92
+ Config.storage.delete(path) if path
93
+ @model.send(column).delete
94
+ }
89
95
  end
96
+
97
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
98
+ futures.each do |future|
99
+ future.value
100
+ raise(future.reason) if future.rejected?
101
+ end
102
+ end
103
+
104
+ pool.shutdown
105
+ pool.wait_for_termination
90
106
  end
91
107
  end
92
108
 
@@ -26,6 +26,7 @@ module Saviour
26
26
  self.class.attached_files.each do |attach_as|
27
27
  duped[attach_as] = nil
28
28
  duped.instance_variable_set("@__uploader_#{attach_as}", send(attach_as).dup(duped))
29
+ duped.instance_variable_set("@__uploader_#{attach_as}_was", nil)
29
30
  end
30
31
 
31
32
  duped
@@ -0,0 +1,35 @@
1
+ module Saviour
2
+ class ReadOnlyFile
3
+ attr_reader :persisted_path
4
+
5
+ def initialize(persisted_path)
6
+ @persisted_path = persisted_path
7
+ end
8
+
9
+ def exists?
10
+ persisted? && Config.storage.exists?(@persisted_path)
11
+ end
12
+
13
+ def read
14
+ return nil unless persisted?
15
+ Config.storage.read(@persisted_path)
16
+ end
17
+
18
+ def public_url
19
+ return nil unless persisted?
20
+ Config.storage.public_url(@persisted_path)
21
+ end
22
+ alias_method :url, :public_url
23
+
24
+ def ==(another_file)
25
+ return false unless another_file.is_a?(Saviour::File) || another_file.is_a?(ReadOnlyFile)
26
+ return false unless another_file.persisted?
27
+
28
+ another_file.persisted_path == persisted_path
29
+ end
30
+
31
+ def persisted?
32
+ true
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module Saviour
2
- VERSION = "0.5.10"
2
+ VERSION = "0.5.11"
3
3
  end
@@ -15,8 +15,8 @@ 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", "< 5.2"
19
- spec.add_dependency "activesupport", ">= 5.0"
18
+ spec.add_dependency "activerecord", ">= 5.1"
19
+ spec.add_dependency "activesupport", ">= 5.1"
20
20
  spec.add_dependency "concurrent-ruby", ">= 1.0.5"
21
21
 
22
22
  spec.add_development_dependency "bundler"
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "concurrent processors" do
3
+ describe "concurrency on operations" 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']
@@ -131,4 +131,52 @@ describe "concurrent processors" do
131
131
  expect((start_times[0] - start_times[-1]).abs).to be > WAIT_TIME
132
132
  end
133
133
  end
134
+
135
+ context 'on destroy' do
136
+ before do
137
+ allow(Saviour::Config.storage).to receive(:delete) { sleep WAIT_TIME }
138
+ end
139
+
140
+ def measure
141
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
142
+ yield
143
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting
144
+ end
145
+
146
+ it 'works concurrently with 4 workers' do
147
+ Saviour::Config.concurrent_workers = 4
148
+
149
+ a = klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
150
+ file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
151
+ file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
152
+ file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
153
+
154
+ time = measure { a.destroy! }
155
+ expect(time).to be_within(THRESHOLD).of(WAIT_TIME)
156
+ end
157
+
158
+ it 'works in serial with 1 worker' do
159
+ Saviour::Config.concurrent_workers = 1
160
+
161
+ a = klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
162
+ file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
163
+ file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
164
+ file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
165
+
166
+ time = measure { a.destroy! }
167
+ expect(time).to be_within(THRESHOLD).of(WAIT_TIME * 4)
168
+ end
169
+
170
+ it 'concurrency can be adjusted' do
171
+ Saviour::Config.concurrent_workers = 2
172
+
173
+ a = klass.create! file: Saviour::StringSource.new("contents", "file.txt"),
174
+ file_thumb: Saviour::StringSource.new("contents", "file_2.txt"),
175
+ file_thumb_2: Saviour::StringSource.new("contents", "file_3.txt"),
176
+ file_thumb_3: Saviour::StringSource.new("contents", "file_4.txt")
177
+
178
+ time = measure { a.destroy! }
179
+ expect(time).to be_within(THRESHOLD).of(WAIT_TIME * 2)
180
+ end
181
+ end
134
182
  end
@@ -123,6 +123,14 @@ describe "CRUD" do
123
123
  end
124
124
  end
125
125
  end
126
+
127
+ it "can be created from another saviour attachment" do
128
+ a = klass.create! file: Saviour::StringSource.new("contents", "file.txt")
129
+ b = klass.create! file: a.file
130
+
131
+ expect(b.file.read).to eq "contents"
132
+ expect(b.file.filename).to eq "file.txt"
133
+ end
126
134
  end
127
135
 
128
136
  describe "deletion" do
@@ -163,6 +171,23 @@ describe "CRUD" do
163
171
  expect(Saviour::Config.storage.read(a[:file])).to eq "foo"
164
172
  end
165
173
 
174
+ it "does not generate an extra query when saving the file with only attached changes" do
175
+ a = klass.create!
176
+
177
+ expect_to_yield_queries(count: 1) do
178
+ a.update_attributes! file: Saviour::StringSource.new("foo", "file.txt")
179
+ end
180
+ end
181
+
182
+ it "does generate an extra query when saving the file with an extra change" do
183
+ a = klass.create!
184
+
185
+ expect_to_yield_queries(count: 2) do
186
+ a.update_attributes! name: "Text",
187
+ file: Saviour::StringSource.new("foo", "file.txt")
188
+ end
189
+ end
190
+
166
191
  context do
167
192
  let(:klass) {
168
193
  a = Class.new(Test) { include Saviour::Model }
@@ -174,9 +199,8 @@ describe "CRUD" do
174
199
  it "saves to db only once with multiple file attachments" do
175
200
  a = klass.create!
176
201
 
177
- # 2 update's, first empty and then 1 with the two attributes
178
202
  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
203
+ expect_to_yield_queries(count: 1, including: [expected_query]) do
180
204
  a.update_attributes!(
181
205
  file: Saviour::StringSource.new("foo", "file.txt"),
182
206
  file_thumb: Saviour::StringSource.new("foo", "file.txt")
@@ -3,11 +3,14 @@ require 'spec_helper'
3
3
  describe "dirty model" 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
- it "provides changes and previous file" do
6
+ before do
7
7
  uploader = Class.new(Saviour::BaseUploader) { store_dir { "/store/dir" } }
8
- klass = Class.new(Test) { include Saviour::Model }
9
- klass.attach_file :file, uploader
10
- a = klass.create!
8
+ @klass = Class.new(Test) { include Saviour::Model }
9
+ @klass.attach_file :file, uploader
10
+ end
11
+
12
+ it "provides changes and previous file" do
13
+ a = @klass.create!
11
14
 
12
15
  with_test_file("example.xml") do |xml_file|
13
16
  with_test_file("camaloon.jpg") do |jpg_file|
@@ -28,17 +31,76 @@ describe "dirty model" do
28
31
  expect(a.file_was.persisted?).to be_truthy
29
32
 
30
33
  expect(a.changed_attributes).to include("file" => a.file_was)
34
+ expect(a.changed).to eq ["file"]
35
+
36
+ expect(a.changed?).to be_truthy
37
+
38
+ expect(a.changes).to eq({"file" => [a.file_was, a.file]})
31
39
  end
32
40
  end
33
41
  end
34
42
 
43
+ it "provides changes on two changed attributes" do
44
+ a = @klass.create!
45
+
46
+ a.name = "Foo bar"
47
+ a.file = Saviour::StringSource.new("contents", "file.txt")
48
+
49
+ expect(a.changed.sort).to eq ["file", "name"]
50
+ a.save!
51
+
52
+ a.name = "Johny"
53
+ a.file = Saviour::StringSource.new("contents", "file_2.txt")
54
+
55
+ expect(a.changed.sort).to eq ["file", "name"]
56
+ expect(a.changes).to eq({ "file" => [a.file_was, a.file], "name" => ["Foo bar", "Johny"]})
57
+ end
58
+
59
+ it "returns always the first ever known value as was" do
60
+ a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt")
61
+ url_was = a.file.url
62
+
63
+ a.file = Saviour::StringSource.new("contents", "file_45.txt")
64
+ expect(a.file_was.url).to eq url_was
65
+
66
+ a.file = Saviour::StringSource.new("contents", "file_95.txt")
67
+ expect(a.file_was.url).to eq url_was
68
+ end
69
+
35
70
  it "changes are nil when not persisted" do
36
- uploader = Class.new(Saviour::BaseUploader) { store_dir { "/store/dir" } }
37
- klass = Class.new(Test) { include Saviour::Model }
38
- klass.attach_file :file, uploader
71
+ a = @klass.new file: Saviour::StringSource.new("contents", "file.txt")
39
72
 
40
- a = klass.new file: Saviour::StringSource.new("contents", "file.txt")
41
73
  expect(a.file_changed?).to be_truthy
42
74
  expect(a.file_was).to be_nil
43
75
  end
76
+
77
+ it "changes are nil when persisted but no file was assigned" do
78
+ a = @klass.create!
79
+ a.file = Saviour::StringSource.new("contents", "file.txt")
80
+ expect(a.file_was).to be_nil
81
+ end
82
+
83
+ it "was data is kept after reload" do
84
+ a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt"), name: "Cuca"
85
+ expect(a.name_was).to eq "Cuca"
86
+ expect(a.file_was).to eq a.file
87
+
88
+ a.reload
89
+ expect(a.file_was).to eq a.file
90
+ end
91
+
92
+ it "was data is refreshed on persisting a new assignation" do
93
+ a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt")
94
+ a.file = Saviour::StringSource.new("contents", "file_2.txt")
95
+ a.save!
96
+ expect(a.file_was.url).to match(/file_2\.txt/)
97
+ end
98
+
99
+ it "was file is cleared on dup" do
100
+ a = @klass.create! file: Saviour::StringSource.new("contents", "file.txt")
101
+ expect(a.file_was).to eq a.file
102
+
103
+ b = a.dup
104
+ expect(b.file_was).to be_nil
105
+ end
44
106
  end