saviour 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -13
  4. data/DECOMPOSE.md +66 -0
  5. data/Gemfile +1 -0
  6. data/README.md +39 -8
  7. data/lib/saviour/attribute_name_calculator.rb +15 -0
  8. data/lib/saviour/base_integrator.rb +53 -0
  9. data/lib/saviour/basic_model.rb +7 -0
  10. data/lib/saviour/config.rb +0 -1
  11. data/lib/saviour/file.rb +13 -34
  12. data/lib/saviour/life_cycle.rb +57 -0
  13. data/lib/saviour/source_filename_extractor.rb +21 -0
  14. data/lib/saviour/url_source.rb +1 -1
  15. data/lib/saviour/utils/class_attribute.rb +26 -0
  16. data/lib/saviour/version.rb +1 -1
  17. data/lib/saviour.rb +7 -155
  18. data/saviour.gemspec +1 -5
  19. data/spec/feature/access_to_model_and_mounted_as_spec.rb +13 -5
  20. data/spec/feature/versions_spec.rb +72 -49
  21. data/spec/models/attribute_name_calculator_spec.rb +11 -0
  22. data/spec/models/basic_model_spec.rb +51 -0
  23. data/spec/models/file_spec.rb +32 -55
  24. data/spec/models/url_source_spec.rb +5 -5
  25. data/spec/spec_helper.rb +2 -30
  26. data/spec/support/models.rb +7 -2
  27. metadata +12 -72
  28. data/Appraisals +0 -19
  29. data/gemfiles/4.0.gemfile +0 -9
  30. data/gemfiles/4.1.gemfile +0 -9
  31. data/gemfiles/4.2.gemfile +0 -9
  32. data/gemfiles/5.0.gemfile +0 -9
  33. data/lib/saviour/processors/digest.rb +0 -16
  34. data/spec/feature/crud_workflows_spec.rb +0 -143
  35. data/spec/feature/persisted_path_spec.rb +0 -34
  36. data/spec/feature/reload_model_spec.rb +0 -24
  37. data/spec/feature/validations_spec.rb +0 -171
  38. data/spec/models/processors/digest_spec.rb +0 -22
  39. data/spec/models/saviour_spec.rb +0 -80
  40. data/spec/support/schema.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c2fca8190280a50014041a2989e1faa1ab76940
4
- data.tar.gz: 49c26d990899a05d0b10ce48196c26429a0e8bff
3
+ metadata.gz: 8673c1ddde4f8e16189bfd2d54ea7ad239155679
4
+ data.tar.gz: 8a97558346780d9105a5b2145fcdc3a038ae3bc6
5
5
  SHA512:
6
- metadata.gz: 06fa6cba605bc2d7f44043bd979189cf3b8ddeb27faee2dd14f7c77c6361910c5abe0de610562cdd5d21c56e41e636a8aefc5a1fe40a879a1e46c3175e3d08c3
7
- data.tar.gz: 0075fc54313974fe0d8ca044ad9b749bc92368ad1053d2844b60bd8e518d2d4f311d596423fbe0598e7a94e77b6a0f22e207efd99b754bbff77108f6d4d2f831
6
+ metadata.gz: 541052bdedf19b045140c57fa154d9f40007f56bd4fd4cd63895d99a2f577d24e4e10e39e876c597c1bc38fbd462797dcc59173d3b5f36d89aaa06e91c2cf52c
7
+ data.tar.gz: beea334842a6e686c7df91b933ed55a70298fb00818a950381c2d015bc138ff90ff7deff9b7d74f0a4c150a8069600800f5afc5ff002d77b92e3e1cb0fde2380
data/.gitignore CHANGED
@@ -18,4 +18,5 @@ tmp
18
18
  spec/support/test_data_dir/*
19
19
  .idea
20
20
  .rbenv-gemsets
21
- *.gemfile.lock
21
+ *.gemfile.lock
22
+ .DS_Store
data/.travis.yml CHANGED
@@ -1,23 +1,14 @@
1
1
  language: ruby
2
+ sudo: false
3
+ cache: bundler
2
4
  rvm:
3
- - 2.0.0
4
5
  - 2.1.8
5
6
  - 2.2.4
6
7
  - 2.3.0
7
8
 
8
- gemfile:
9
- - gemfiles/4.0.gemfile
10
- - gemfiles/4.1.gemfile
11
- - gemfiles/4.2.gemfile
12
- - gemfiles/5.0.gemfile
13
-
14
9
  addons:
15
10
  code_climate:
16
11
  repo_token: abb288da5fac3efc45be30ffb37085314b9189ddccedf2cc68282777477e21c5
17
12
 
18
- matrix:
19
- exclude:
20
- - rvm: 2.0.0
21
- gemfile: gemfiles/5.0.gemfile
22
- - rvm: 2.1.8
23
- gemfile: gemfiles/5.0.gemfile
13
+ after_success:
14
+ - bundle exec codeclimate-test-reporter
data/DECOMPOSE.md ADDED
@@ -0,0 +1,66 @@
1
+ - New saviour-ar gem will provide what's currently in saviour gem
2
+ - saviour gem will be data-storage independent. It will provide a way to manage files with processors, but without a
3
+ lifecycle attached to a database-model. It will provide a more low level api.
4
+ - saviour-ar will provide the specific integration with active record.
5
+
6
+
7
+ # Saviour agnostic gem API
8
+
9
+
10
+ First, you'll need to define what files can be saved in what objects (any Ruby class). You can do that by including the `Saviour::BasicModel` module, example:
11
+
12
+ ```
13
+ class MyObject
14
+ include Saviour::BasicModel
15
+
16
+ attach_file :image, ImageUploader
17
+ attach_file :scheme, FileUploader
18
+ end
19
+ ```
20
+
21
+ Now, you can assign and work with the files associated to instances of MyObject with the following api:
22
+
23
+ ```
24
+ # New file
25
+
26
+ a = MyObject.new
27
+ a.image = File.open('/path/image.jpg')
28
+ a.image.assign File.open('newfile.jpg')
29
+ a.image.changed? # => true
30
+ saved_path = a.image.write # => persists file in the storage and returns the path in which the file has been saved
31
+
32
+ b = MyObject.new
33
+ b.image.set_path!(saved_path) # => Link this image to the persisted image from before
34
+ b.image.exists? # => true
35
+ b.image.read # -> return bytes
36
+ b.image.delete # -> delete
37
+ b.image.exists? # => false
38
+
39
+ # ...
40
+ ```
41
+
42
+ If you want to work directly managing the files associated to those objects, you can use the `Saviour::File` public API directly.
43
+
44
+ However, Saviour is designed to work with models that are saved in some kind of persistent storage, like a database of some sort. This is why Saviour also provides a generic `LifeCycle` service which you can use to simulate the persistence lifecycle of the object. You can then use:
45
+
46
+ ```
47
+ a = MyObject.new image: File.open('image.jpg')
48
+
49
+ Saviour::LifeCycle.new(a).save!
50
+ Saviour::LifeCycle.new(a).delete!
51
+ ```
52
+
53
+ `save!` will have the effect of saving all the attachments associated with the object, and `delete!` will have the effect of removing all the files associated with this object from the file storage defined.
54
+
55
+ Using LifeCycle you consider the object as a whole, while working with individual files you have more control, but you always operate with individual files.
56
+
57
+ The feature of versions, for example, only applies when you use the LifeCycle approach, since, by definition, a version is automatically constructed from the original file while this one is saved, and this involved operating in two or more attachments at the same time over an specific object. Since in Saviour versions can be managed exactly like regular attachments, such behavior don't apply when you work with File instances directly.
58
+
59
+
60
+ # Saviour API for developers
61
+
62
+ If you want to develop a new gem to integrate Saviour with another persistence technology, you need to do two things.
63
+
64
+ 1) Write a class with the api #read, #write and #persisted? and give it to Savior, so he knows how to work with the persistence layer.
65
+ 2) Write a new module for the final users to include in their models, providing the expected hooks so that the usage of the `LifeCycle` is automatic and transparent.
66
+
data/Gemfile CHANGED
@@ -4,3 +4,4 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem "codeclimate-test-reporter", group: :test, require: nil
7
+ gem "simplecov", group: :test, require: nil
data/README.md CHANGED
@@ -56,12 +56,14 @@ to an ActiveRecord object) and processings. See the following example of a model
56
56
 
57
57
  ```
58
58
  class Post < ActiveRecord::Base
59
- # The posts table must have an `image` String column.
59
+ include Saviour::Model
60
+
61
+ # The posts table must have an `image` string column.
60
62
  attach_file :image, PostImageUploader
61
63
  end
62
64
 
63
65
  class PostImageUploader < Saviour::BaseUploader
64
- store_dir! { "/default/path/#{model.id}/#{attached_as}" }
66
+ store_dir { "/default/path/#{model.id}/#{attached_as}" }
65
67
 
66
68
  process :resize, width: 500, height: 500
67
69
 
@@ -281,7 +283,7 @@ and UrlSource.
281
283
 
282
284
  ### StringSource
283
285
 
284
- This is just a wrapper class that gives no additional behaviour except for implementing the required API. Use it as:
286
+ This is just a wrapper class that gives no additional behavior except for implementing the required API. Use it as:
285
287
 
286
288
  ```
287
289
  foo = Saviour::StringSource.new("my raw contents", "filename.jpg")
@@ -310,7 +312,7 @@ example:
310
312
 
311
313
  ```
312
314
  class ExampleUploader < Saviour::BaseUploader
313
- store_dir! { "/default/path/#{model.id}" }
315
+ store_dir { "/default/path/#{model.id}" }
314
316
 
315
317
  process :resize, width: 50, height: 50
316
318
 
@@ -324,7 +326,7 @@ class ExampleUploader < Saviour::BaseUploader
324
326
  end
325
327
 
326
328
  version(:thumb) do
327
- store_dir! { "/default/path/#{model.id}/versions" }
329
+ store_dir { "/default/path/#{model.id}/versions" }
328
330
  process :resize, with: 10, height: 10
329
331
  end
330
332
 
@@ -431,6 +433,7 @@ Example of validations:
431
433
 
432
434
  ```
433
435
  class Post < ActiveRecord::Base
436
+ include Saviour::Model
434
437
  attach_file :image, PostImageUploader
435
438
 
436
439
  attach_validation(:image) do |contents, filename|
@@ -447,6 +450,7 @@ Validations can also be declared passing a method name instead of a block, like
447
450
 
448
451
  ```
449
452
  class Post < ActiveRecord::Base
453
+ include Saviour::Model
450
454
  attach_file :image, PostImageUploader
451
455
  attach_validation :image, :check_size
452
456
 
@@ -481,8 +485,8 @@ This is a compilation of common questions or features regarding file uploads.
481
485
 
482
486
  ### Digested filename
483
487
 
484
- A common use case is to create a processor to include a digest of the file in the filename. The implementation is left
485
- for the user, but a simple example of such processor is this:
488
+ A common use case is to create a processor to include a digest of the file in the filename, in order to automatically
489
+ expire caches. The implementation is left for the user, but a simple example of such processor is this:
486
490
 
487
491
  ```
488
492
  def digest_filename(contents, filename, opts = {})
@@ -497,8 +501,35 @@ for the user, but a simple example of such processor is this:
497
501
  end
498
502
  ```
499
503
 
500
- ### Getting metadata from the file
501
504
  ### How to recreate versions
505
+
506
+ Recreating a version based on the master file can be easily done by just assigning the master file to the version and
507
+ saving the model. You just need a little bit more code in order to preserve the current version filename, for example,
508
+ if that's something you want.
509
+
510
+ An example service that can do that is the following:
511
+
512
+ ```
513
+ class SaviourRecreateVersionsService
514
+ def initialize(model)
515
+ @model = model
516
+ end
517
+
518
+ def recreate!(attached_as, *versions)
519
+ base = @model.send(attached_as).read
520
+
521
+ versions.each do |version|
522
+ current_filename = @model.send(attached_as, version).filename
523
+ @model.send(attached_as, version).assign(Saviour::StringSource.new(base, current_filename))
524
+ end
525
+
526
+ @model.save!
527
+ end
528
+ end
529
+ ```
530
+
531
+ ### Getting metadata from the file
532
+
502
533
  ### Caching across redisplays in normal forms
503
534
  ### Introspection (Class.attached_files)
504
535
  ### Processing in background
@@ -0,0 +1,15 @@
1
+ module Saviour
2
+ class AttributeNameCalculator
3
+ def initialize(attached_as, version = nil)
4
+ @attached_as, @version = attached_as, version
5
+ end
6
+
7
+ def name
8
+ if @version
9
+ "#{@attached_as}_#{@version}"
10
+ else
11
+ @attached_as.to_s
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Saviour
2
+ class BaseIntegrator
3
+ def initialize(klass)
4
+ @klass = klass
5
+ end
6
+
7
+ def file_instantiator_hook(model, file_instance, attach_as, version)
8
+ # noop
9
+ end
10
+
11
+ def attach_file_hook(klass, attach_as, uploader_klass)
12
+ # noop
13
+ end
14
+
15
+ def setup!
16
+ raise "You cannot include Saviour twice in the same class" if @klass.respond_to?(:attached_files)
17
+
18
+ @klass.send :extend, ClassAttribute
19
+ @klass.class_attribute :attached_files
20
+ @klass.attached_files = {}
21
+
22
+ a = self
23
+ b = @klass
24
+
25
+ @klass.define_singleton_method "attach_file" do |attach_as, uploader_klass|
26
+ a.attach_file_hook(self, attach_as, uploader_klass)
27
+
28
+ versions = uploader_klass.versions || []
29
+ b.attached_files[attach_as] ||= []
30
+ b.attached_files[attach_as] += versions
31
+
32
+ b.class_eval do
33
+ define_method(attach_as) do |version = nil|
34
+ instance_variable_get("@__uploader_#{version}_#{attach_as}") || begin
35
+ new_file = ::Saviour::File.new(uploader_klass, self, attach_as, version)
36
+ a.file_instantiator_hook(self, new_file, attach_as, version)
37
+
38
+ instance_variable_set("@__uploader_#{version}_#{attach_as}", new_file)
39
+ end
40
+ end
41
+
42
+ define_method("#{attach_as}=") do |value|
43
+ send(attach_as).assign(value)
44
+ end
45
+
46
+ define_method("#{attach_as}_changed?") do
47
+ send(attach_as).changed?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ module Saviour
2
+ module BasicModel
3
+ def self.included(klass)
4
+ BaseIntegrator.new(klass).setup!
5
+ end
6
+ end
7
+ end
@@ -17,7 +17,6 @@ module Saviour
17
17
  Thread.current["Saviour::Config"][:processing_enabled] = value
18
18
  end
19
19
 
20
-
21
20
  def storage
22
21
  Thread.current["Saviour::Config"] ||= {}
23
22
  Thread.current["Saviour::Config"][:storage] || Thread.main["Saviour::Config"][:storage] || NotImplemented.new
data/lib/saviour/file.rb CHANGED
@@ -2,47 +2,32 @@ require 'securerandom'
2
2
 
3
3
  module Saviour
4
4
  class File
5
- class SourceFilenameExtractor
6
- def initialize(source)
7
- @source = source
8
- end
9
-
10
- def detected_filename
11
- original_filename || path_filename
12
- end
13
-
14
- def original_filename
15
- @source.respond_to?(:original_filename) && @source.original_filename.present? && @source.original_filename
16
- end
17
-
18
- def path_filename
19
- @source.respond_to?(:path) && @source.path.present? && ::File.basename(@source.path)
20
- end
21
- end
22
-
5
+ attr_reader :persisted_path
23
6
 
24
7
  def initialize(uploader_klass, model, attached_as, version = nil)
25
8
  @uploader_klass, @model, @attached_as = uploader_klass, model, attached_as
26
9
  @version = version
27
-
28
- @persisted = !!persisted_path
29
10
  @source_was = @source = nil
30
11
  end
31
12
 
13
+ def set_path!(path)
14
+ @persisted_path = path
15
+ end
16
+
32
17
  def exists?
33
- persisted? && Config.storage.exists?(persisted_path)
18
+ persisted? && Config.storage.exists?(@persisted_path)
34
19
  end
35
20
 
36
21
  def read
37
- persisted? && exists? && Config.storage.read(persisted_path)
22
+ persisted? && exists? && Config.storage.read(@persisted_path)
38
23
  end
39
24
 
40
25
  def delete
41
- persisted? && exists? && Config.storage.delete(persisted_path)
26
+ persisted? && exists? && Config.storage.delete(@persisted_path)
42
27
  end
43
28
 
44
29
  def public_url
45
- persisted? && Config.storage.public_url(persisted_path)
30
+ persisted? && Config.storage.public_url(@persisted_path)
46
31
  end
47
32
 
48
33
  alias_method :url, :public_url
@@ -52,13 +37,13 @@ module Saviour
52
37
 
53
38
  @source_data = nil
54
39
  @source = object
55
- @persisted = !object
40
+ @persisted_path = nil if object
56
41
 
57
42
  object
58
43
  end
59
44
 
60
45
  def persisted?
61
- @persisted
46
+ !!@persisted_path
62
47
  end
63
48
 
64
49
  def changed?
@@ -66,7 +51,7 @@ module Saviour
66
51
  end
67
52
 
68
53
  def filename
69
- persisted? && ::File.basename(persisted_path)
54
+ ::File.basename(@persisted_path) if persisted?
70
55
  end
71
56
 
72
57
  def with_copy
@@ -96,7 +81,7 @@ module Saviour
96
81
 
97
82
  path = uploader.write(source_data, filename_to_be_assigned)
98
83
  @source_was = @source
99
- @persisted = true
84
+ @persisted_path = path
100
85
  path
101
86
  end
102
87
 
@@ -114,11 +99,5 @@ module Saviour
114
99
  def uploader
115
100
  @uploader ||= @uploader_klass.new(version: @version, data: {model: @model, attached_as: @attached_as})
116
101
  end
117
-
118
- def persisted_path
119
- if @model.persisted? || @model.destroyed?
120
- @model.read_attribute(ColumnNamer.new(@attached_as, @version).name)
121
- end
122
- end
123
102
  end
124
103
  end
@@ -0,0 +1,57 @@
1
+ module Saviour
2
+ class LifeCycle
3
+ def initialize(model, persistence_klass = nil)
4
+ raise "Please provide an object compatible with Saviour." unless model.class.respond_to?(:attached_files)
5
+
6
+ @persistence_klass = persistence_klass
7
+ @model = model
8
+ end
9
+
10
+ def delete!
11
+ attached_files.each do |column, versions|
12
+ (versions + [nil]).each { |version| @model.send(column, version).delete if @model.send(column, version).exists? }
13
+ end
14
+ end
15
+
16
+ def save!
17
+ attached_files.each do |column, versions|
18
+ base_file_changed = @model.send(column).changed?
19
+ original_content = @model.send(column).source_data if base_file_changed
20
+
21
+ versions.each do |version|
22
+ if @model.send(column, version).changed?
23
+ upload_file(column, version)
24
+ elsif base_file_changed
25
+ @model.send(column, version).assign(StringSource.new(original_content, default_version_filename(column, version)))
26
+ upload_file(column, version)
27
+ end
28
+ end
29
+
30
+ upload_file(column, nil) if base_file_changed
31
+ end
32
+ end
33
+
34
+
35
+ private
36
+
37
+ def default_version_filename(column, version)
38
+ filename = @model.send(column).filename_to_be_assigned
39
+ "#{::File.basename(filename, ".*")}_#{version}#{::File.extname(filename)}"
40
+ end
41
+
42
+ def upload_file(column, version)
43
+ name = AttributeNameCalculator.new(column, version).name
44
+ persistence_layer = @persistence_klass.new(@model) if @persistence_klass
45
+ current_path = persistence_layer.read(name) if persistence_layer
46
+
47
+ Config.storage.delete(current_path) if current_path
48
+
49
+ new_path = @model.send(column, version).write
50
+ persistence_layer.write(name, new_path) if persistence_layer
51
+ end
52
+
53
+ def attached_files
54
+ @model.class.attached_files || {}
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ module Saviour
2
+ class SourceFilenameExtractor
3
+ def initialize(source)
4
+ @source = source
5
+ end
6
+
7
+ def detected_filename
8
+ original_filename || path_filename
9
+ end
10
+
11
+ def original_filename
12
+ value = @source.original_filename if @source.respond_to?(:original_filename)
13
+ value if !value.nil? && value != ''
14
+ end
15
+
16
+ def path_filename
17
+ value = @source.path if @source.respond_to?(:path)
18
+ ::File.basename(value) if !value.nil? && value != ''
19
+ end
20
+ end
21
+ end
@@ -24,7 +24,7 @@ module Saviour
24
24
  private
25
25
 
26
26
  def resolve(uri, max_redirects = MAX_REDIRECTS)
27
- raise RuntimeError, "Max number of allowed redirects reached (#{MAX_REDIRECTS}) when resolving #{@uri}" if max_redirects == 0
27
+ raise RuntimeError, "Max number of allowed redirects reached (#{MAX_REDIRECTS}) when resolving #{uri}" if max_redirects == 0
28
28
 
29
29
  response = Net::HTTP.get_response(uri)
30
30
 
@@ -0,0 +1,26 @@
1
+ # Port and simplification of ActiveSupport class attribute
2
+ module Saviour
3
+ module ClassAttribute
4
+ def class_attribute(*attrs)
5
+ attrs.each do |name|
6
+ singleton_class.instance_eval do
7
+ undef_method(name) if method_defined?(name)
8
+ end
9
+
10
+ define_singleton_method(name) { nil }
11
+
12
+ singleton_class.instance_eval do
13
+ undef_method("#{name}=") if method_defined?("#{name}=")
14
+ end
15
+
16
+ define_singleton_method("#{name}=") do |val|
17
+ singleton_class.class_eval do
18
+ undef_method(name) if method_defined?(name)
19
+ define_method(name) { val }
20
+ end
21
+ val
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,3 @@
1
1
  module Saviour
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end