saviour 0.2.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +491 -0
  7. data/Rakefile +6 -0
  8. data/lib/saviour.rb +170 -0
  9. data/lib/saviour/base_uploader.rb +81 -0
  10. data/lib/saviour/config.rb +13 -0
  11. data/lib/saviour/file.rb +124 -0
  12. data/lib/saviour/local_storage.rb +72 -0
  13. data/lib/saviour/processors/digest.rb +16 -0
  14. data/lib/saviour/s3_storage.rb +77 -0
  15. data/lib/saviour/string_source.rb +15 -0
  16. data/lib/saviour/uploader/element.rb +19 -0
  17. data/lib/saviour/uploader/processors_runner.rb +88 -0
  18. data/lib/saviour/uploader/store_dir_extractor.rb +41 -0
  19. data/lib/saviour/url_source.rb +55 -0
  20. data/lib/saviour/version.rb +3 -0
  21. data/saviour.gemspec +26 -0
  22. data/spec/feature/access_to_model_and_mounted_as.rb +30 -0
  23. data/spec/feature/crud_workflows_spec.rb +138 -0
  24. data/spec/feature/persisted_path_spec.rb +35 -0
  25. data/spec/feature/reload_model_spec.rb +25 -0
  26. data/spec/feature/validations_spec.rb +172 -0
  27. data/spec/feature/versions_spec.rb +186 -0
  28. data/spec/models/base_uploader_spec.rb +396 -0
  29. data/spec/models/config_spec.rb +16 -0
  30. data/spec/models/file_spec.rb +210 -0
  31. data/spec/models/local_storage_spec.rb +154 -0
  32. data/spec/models/processors/digest_spec.rb +22 -0
  33. data/spec/models/s3_storage_spec.rb +170 -0
  34. data/spec/models/saviour_spec.rb +80 -0
  35. data/spec/models/url_source_spec.rb +76 -0
  36. data/spec/spec_helper.rb +93 -0
  37. data/spec/support/data/camaloon.jpg +0 -0
  38. data/spec/support/data/example.xml +21 -0
  39. data/spec/support/data/text.txt +1 -0
  40. data/spec/support/models.rb +3 -0
  41. data/spec/support/schema.rb +9 -0
  42. metadata +196 -0
@@ -0,0 +1,16 @@
1
+ module Saviour
2
+ module Processors
3
+ module Digest
4
+ def digest_filename(contents, filename, opts = {})
5
+ separator = opts.fetch(:separator, "-")
6
+
7
+ digest = ::Digest::MD5.hexdigest(contents)
8
+ extension = ::File.extname(filename)
9
+
10
+ new_filename = "#{[::File.basename(filename, ".*"), digest].join(separator)}#{extension}"
11
+
12
+ [contents, new_filename]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ module Saviour
2
+ class S3Storage
3
+ def initialize(conf = {})
4
+ @bucket = conf.delete(:bucket)
5
+ @public_url_prefix = conf.delete(:public_url_prefix)
6
+ @conf = conf
7
+ @overwrite_protection = conf.delete(:overwrite_protection) { true }
8
+ assert_directory_exists!
9
+ end
10
+
11
+ def write(contents, path)
12
+ raise(RuntimeError, "The path you're trying to write already exists!") if @overwrite_protection && exists?(path)
13
+
14
+ path = sanitize_leading_slash(path)
15
+ directory.files.create(
16
+ key: path,
17
+ body: contents,
18
+ public: true
19
+ )
20
+ end
21
+
22
+ def read(path)
23
+ path = sanitize_leading_slash(path)
24
+ assert_exists(path)
25
+ directory.files.get(path).body
26
+ end
27
+
28
+ def delete(path)
29
+ path = sanitize_leading_slash(path)
30
+ assert_exists(path)
31
+ directory.files.get(path).destroy
32
+ end
33
+
34
+ def exists?(path)
35
+ path = sanitize_leading_slash(path)
36
+ !!directory.files.head(path)
37
+ end
38
+
39
+ def public_url(path)
40
+ raise(RuntimeError, "You must provide a `public_url_prefix`") unless public_url_prefix
41
+
42
+ path = sanitize_leading_slash(path)
43
+ ::File.join(public_url_prefix, path)
44
+ end
45
+
46
+
47
+ private
48
+
49
+ def public_url_prefix
50
+ if @public_url_prefix.respond_to?(:call)
51
+ @public_url_prefix.call
52
+ else
53
+ @public_url_prefix
54
+ end
55
+ end
56
+
57
+ def sanitize_leading_slash(path)
58
+ path.gsub(/\A\/*/, '')
59
+ end
60
+
61
+ def assert_directory_exists!
62
+ directory || raise(ArgumentError, "The bucket #{@bucket} doesn't exists or misconfigured connection.")
63
+ end
64
+
65
+ def assert_exists(path)
66
+ raise RuntimeError, "File does not exists: #{path}" unless exists?(path)
67
+ end
68
+
69
+ def directory
70
+ @directory ||= connection.directories.get(@bucket)
71
+ end
72
+
73
+ def connection
74
+ @connection ||= Fog::Storage.new({provider: 'AWS'}.merge(@conf))
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,15 @@
1
+ module Saviour
2
+ class StringSource
3
+ def initialize(value, filename = nil)
4
+ @value, @filename = value, filename
5
+ end
6
+
7
+ def read
8
+ @value
9
+ end
10
+
11
+ def original_filename
12
+ @filename
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Saviour
2
+ module Uploader
3
+ class Element
4
+ attr_reader :version, :method_or_block
5
+
6
+ def initialize(version, method_or_block)
7
+ @version, @method_or_block = version, method_or_block
8
+ end
9
+
10
+ def versioned?
11
+ !!@version
12
+ end
13
+
14
+ def block?
15
+ @method_or_block.respond_to?(:call)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,88 @@
1
+ module Saviour
2
+ module Uploader
3
+ class ProcessorsRunner
4
+ attr_writer :file
5
+ attr_accessor :contents
6
+ attr_accessor :filename
7
+
8
+ def initialize(uploader, version_name)
9
+ @uploader, @version_name = uploader, version_name
10
+ end
11
+
12
+ def matching_processors
13
+ @uploader.class.processors.select { |processor| !processor[:element].versioned? || processor[:element].version == @version_name }
14
+ end
15
+
16
+ def file
17
+ @file ||= Tempfile.new([SecureRandom.hex, ::File.extname(filename)]).tap { |x| x.binmode }
18
+ end
19
+
20
+ def run_element(element, opts, data)
21
+ if element.block?
22
+ @uploader.instance_exec(data, filename, &element.method_or_block)
23
+ else
24
+ if opts.empty?
25
+ @uploader.send(element.method_or_block, data, filename)
26
+ else
27
+ @uploader.send(element.method_or_block, data, filename, opts)
28
+ end
29
+ end
30
+ end
31
+
32
+ def advance!(processor, previous_type)
33
+ if processor[:type] != previous_type
34
+ if processor[:type] == :memory
35
+ self.contents = ::File.read(file)
36
+ else
37
+ file.rewind
38
+ file.write(contents)
39
+ file.flush
40
+ file.rewind
41
+ end
42
+ end
43
+ end
44
+
45
+ def run_processor(processor)
46
+ element = processor[:element]
47
+ opts = processor[:opts]
48
+
49
+ if processor[:type] == :memory
50
+ result = run_element(element, opts, contents)
51
+
52
+ self.contents = result[0]
53
+ self.filename = result[1]
54
+
55
+ else
56
+ file.rewind
57
+
58
+ result = run_element(element, opts, file)
59
+
60
+ self.file = result[0]
61
+ self.filename = result[1]
62
+ end
63
+ end
64
+
65
+ def run!(content_data, initial_filename)
66
+ self.contents = content_data
67
+ self.filename = initial_filename
68
+ previous_type = :memory
69
+
70
+ matching_processors.each do |processor|
71
+ advance!(processor, previous_type)
72
+
73
+ run_processor(processor)
74
+ previous_type = processor[:type]
75
+ end
76
+
77
+ if previous_type == :file
78
+ file.rewind
79
+ self.contents = ::File.read(file)
80
+ end
81
+
82
+ file.delete
83
+
84
+ [contents, filename]
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,41 @@
1
+ module Saviour
2
+ module Uploader
3
+ class StoreDirExtractor
4
+ def initialize(uploader)
5
+ @uploader = uploader
6
+ end
7
+
8
+ def candidate_store_dirs
9
+ @candidate_store_dirs ||= @uploader.class.store_dirs
10
+ end
11
+
12
+ def versioned_store_dirs?
13
+ candidate_store_dirs.any? { |x| x.versioned? && x.version == @uploader.version_name }
14
+ end
15
+
16
+ def versioned_store_dir
17
+ candidate_store_dirs.select { |x| x.versioned? && x.version == @uploader.version_name }.last if versioned_store_dirs?
18
+ end
19
+
20
+ def non_versioned_store_dir
21
+ candidate_store_dirs.select { |x| !x.versioned? }.last
22
+ end
23
+
24
+ def store_dir_handler
25
+ @store_dir_handler ||= versioned_store_dir || non_versioned_store_dir
26
+ end
27
+
28
+ def store_dir
29
+ @store_dir ||= begin
30
+ if store_dir_handler
31
+ if store_dir_handler.block?
32
+ @uploader.instance_eval(&store_dir_handler.method_or_block)
33
+ else
34
+ @uploader.send(store_dir_handler.method_or_block)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ require 'uri'
2
+
3
+ module Saviour
4
+ class UrlSource
5
+ MAX_REDIRECTS = 10
6
+
7
+ def initialize(url)
8
+ @uri = wrap_uri_string(url)
9
+ end
10
+
11
+ def read
12
+ with_retry(3) { resolve(@uri) }
13
+ end
14
+
15
+ def original_filename
16
+ ::File.basename(@uri.path)
17
+ end
18
+
19
+ def path
20
+ @uri.path
21
+ end
22
+
23
+
24
+ private
25
+
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
28
+
29
+ response = Net::HTTP.get_response(uri)
30
+
31
+ case response
32
+ when Net::HTTPSuccess
33
+ response.body
34
+ when Net::HTTPRedirection
35
+ resolve(wrap_uri_string(response['location']), max_redirects - 1)
36
+ else
37
+ false
38
+ end
39
+ end
40
+
41
+ def wrap_uri_string(url)
42
+ begin
43
+ URI(url)
44
+ rescue URI::InvalidURIError
45
+ raise ArgumentError, "'#{url}' is not a valid URI"
46
+ end
47
+ end
48
+
49
+ def with_retry(n = 3, &block)
50
+ raise(RuntimeError, "Connection to #{@uri} failed after 3 attempts.") if n == 0
51
+
52
+ block.call || with_retry(n - 1, &block)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module Saviour
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ require './lib/saviour/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "saviour"
5
+ spec.version = Saviour::VERSION
6
+ spec.authors = ["Roger Campos"]
7
+ spec.email = ["roger@rogercampos.com"]
8
+ spec.description = %q{File storage handler following active record model lifecycle}
9
+ spec.summary = %q{File storage handler following active record model lifecycle}
10
+ spec.homepage = "https://github.com/rogercampos/saviour"
11
+ spec.license = "MIT"
12
+
13
+ spec.files = `git ls-files`.split($/)
14
+ spec.require_paths = ["lib"]
15
+
16
+ spec.required_ruby_version = ">= 2.0.0"
17
+
18
+ spec.add_dependency "activerecord", ">= 3.0"
19
+ spec.add_dependency "activesupport", ">= 3.0"
20
+ spec.add_dependency "fog-aws"
21
+ spec.add_dependency "mime-types"
22
+ spec.add_development_dependency "bundler"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "sqlite3"
26
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe "access to model data from uploaders" do
4
+ before { Saviour::Config.storage = Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://domain.com") }
5
+ after { Saviour::Config.storage = nil }
6
+
7
+ let(:uploader) {
8
+ Class.new(Saviour::BaseUploader) do
9
+ store_dir { "/store/dir/#{model.id}" }
10
+ process { |contents, name| [contents, "#{model.id}-#{attached_as}-#{name}"] }
11
+ end
12
+ }
13
+
14
+ let(:klass) {
15
+ a = Class.new(Text)
16
+ a.attach_file :file, uploader
17
+ a
18
+ }
19
+
20
+ describe "file store" do
21
+ it do
22
+ with_test_file("example.xml") do |example, name|
23
+ a = klass.create! id: 87
24
+ expect(a.update_attributes(file: example)).to be_truthy
25
+ expect(Saviour::Config.storage.exists?(a[:file])).to be_truthy
26
+ expect(a[:file]).to eq "/store/dir/87/87-file-#{name}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe "saving a new file" do
4
+ before { Saviour::Config.storage = Saviour::LocalStorage.new(local_prefix: @tmpdir, public_url_prefix: "http://domain.com") }
5
+ after { Saviour::Config.storage = nil }
6
+
7
+ let(:uploader) {
8
+ Class.new(Saviour::BaseUploader) {
9
+ store_dir { "/store/dir" }
10
+ }
11
+ }
12
+
13
+ let(:klass) {
14
+ a = Class.new(Test) { include Saviour }
15
+ a.attach_file :file, uploader
16
+ a
17
+ }
18
+
19
+ describe "creation" do
20
+ it do
21
+ with_test_file("example.xml") do |example|
22
+ a = klass.create!
23
+ expect(a.update_attributes(file: example)).to be_truthy
24
+ end
25
+ end
26
+
27
+ it do
28
+ with_test_file("example.xml") do |example|
29
+ a = klass.create!
30
+ a.update_attributes(file: example)
31
+
32
+ expect(Saviour::Config.storage.exists?(a[:file])).to be_truthy
33
+ end
34
+ end
35
+
36
+ it do
37
+ with_test_file("example.xml") do |example, real_filename|
38
+ a = klass.create!
39
+ a.update_attributes(file: example)
40
+ expect(a[:file]).to eq "/store/dir/#{real_filename}"
41
+ end
42
+ end
43
+
44
+ it do
45
+ with_test_file("example.xml") do |example|
46
+ a = klass.create!
47
+ a.update_attributes(file: example)
48
+
49
+ example.rewind
50
+ expect(a.file.read).to eq example.read
51
+ end
52
+ end
53
+
54
+ it do
55
+ with_test_file("example.xml") do |example|
56
+ a = klass.create!
57
+ a.update_attributes(file: example)
58
+
59
+ expect(a.file.exists?).to be_truthy
60
+ end
61
+ end
62
+
63
+ it do
64
+ with_test_file("example.xml") do |example, real_filename|
65
+ a = klass.create!
66
+ a.update_attributes(file: example)
67
+
68
+ expect(a.file.filename).to eq real_filename
69
+ end
70
+ end
71
+
72
+ it do
73
+ with_test_file("example.xml") do |example, real_filename|
74
+ a = klass.create!
75
+ a.update_attributes(file: example)
76
+
77
+ expect(a.file.url).to eq "http://domain.com/store/dir/#{real_filename}"
78
+ expect(a.file.public_url).to eq a.file.url
79
+ end
80
+ end
81
+
82
+ it "don't create anything if save do not completes (halt during before_save)" do
83
+ klass = Class.new(Test) do
84
+ attr_accessor :fail_at_save
85
+ before_save { !fail_at_save }
86
+ include Saviour
87
+ end
88
+ klass.attach_file :file, uploader
89
+
90
+ expect {
91
+ a = klass.new
92
+ a.fail_at_save = true
93
+ a.save!
94
+ }.to raise_error(ActiveRecord::RecordNotSaved)
95
+
96
+ with_test_file("example.xml") do |example, _|
97
+ a = klass.new
98
+ a.fail_at_save = true
99
+ a.file = example
100
+
101
+ expect(Saviour::Config.storage).not_to receive(:write)
102
+ a.save
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "deletion" do
108
+ it do
109
+ with_test_file("example.xml") do |example|
110
+ a = klass.create!
111
+ a.update_attributes(file: example)
112
+ expect(a.file.exists?).to be_truthy
113
+ expect(a.destroy).to be_truthy
114
+
115
+ expect(Saviour::Config.storage.exists?(a[:file])).to be_falsey
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "updating" do
121
+ it do
122
+ with_test_file("example.xml") do |example|
123
+ a = klass.create!
124
+ a.update_attributes(file: example)
125
+
126
+ expect(Saviour::Config.storage.exists?(a[:file])).to be_truthy
127
+ previous_location = a[:file]
128
+
129
+ with_test_file("camaloon.jpg") do |example_2|
130
+ a.update_attributes(file: example_2)
131
+ expect(Saviour::Config.storage.exists?(a[:file])).to be_truthy
132
+
133
+ expect(Saviour::Config.storage.exists?(previous_location)).to be_falsey
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end