asset_cloud 1.0.2

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +13 -0
  6. data/Rakefile +24 -0
  7. data/asset_cloud.gemspec +24 -0
  8. data/lib/asset_cloud.rb +54 -0
  9. data/lib/asset_cloud/asset.rb +187 -0
  10. data/lib/asset_cloud/asset_extension.rb +42 -0
  11. data/lib/asset_cloud/base.rb +247 -0
  12. data/lib/asset_cloud/bucket.rb +39 -0
  13. data/lib/asset_cloud/buckets/active_record_bucket.rb +57 -0
  14. data/lib/asset_cloud/buckets/blackhole_bucket.rb +23 -0
  15. data/lib/asset_cloud/buckets/bucket_chain.rb +84 -0
  16. data/lib/asset_cloud/buckets/file_system_bucket.rb +79 -0
  17. data/lib/asset_cloud/buckets/invalid_bucket.rb +28 -0
  18. data/lib/asset_cloud/buckets/memory_bucket.rb +42 -0
  19. data/lib/asset_cloud/buckets/versioned_memory_bucket.rb +33 -0
  20. data/lib/asset_cloud/callbacks.rb +63 -0
  21. data/lib/asset_cloud/free_key_locator.rb +28 -0
  22. data/lib/asset_cloud/metadata.rb +29 -0
  23. data/lib/asset_cloud/validations.rb +52 -0
  24. data/spec/active_record_bucket_spec.rb +95 -0
  25. data/spec/asset_extension_spec.rb +103 -0
  26. data/spec/asset_spec.rb +177 -0
  27. data/spec/base_spec.rb +114 -0
  28. data/spec/blackhole_bucket_spec.rb +41 -0
  29. data/spec/bucket_chain_spec.rb +158 -0
  30. data/spec/callbacks_spec.rb +125 -0
  31. data/spec/file_system_spec.rb +74 -0
  32. data/spec/files/products/key.txt +1 -0
  33. data/spec/files/versioned_stuff/foo +1 -0
  34. data/spec/find_free_key_spec.rb +39 -0
  35. data/spec/memory_bucket_spec.rb +52 -0
  36. data/spec/spec_helper.rb +5 -0
  37. data/spec/validations_spec.rb +53 -0
  38. data/spec/versioned_memory_bucket_spec.rb +36 -0
  39. metadata +151 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a3f0421040a068165fb6219c055d4d490c0d1b56
4
+ data.tar.gz: 539fc589e72c4b3be9eb61e45fb895d75f96f0ae
5
+ SHA512:
6
+ metadata.gz: 67092177460792fa3892e585bb939b97969b80b96e621d32a619fe24022caccc781db5426c6bc3b8921ed8b950886259ab57a910472cbee922ae27ff9e2f3a28
7
+ data.tar.gz: b532387d3a18ceceb4710f8a3bd006693e063171ec1792f040d0ee3d1c2534aa3d95f31edc26ad05a604bbfd916ed387571d08785fdb5829c403ce3a058908c6
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ Gemfile.lock
3
+ *.orig
4
+ .dotest
5
+ .rvmrc
6
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2013 Tobias Lütke & Shopify, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,13 @@
1
+ = AssetCloud
2
+
3
+ An abstraction layer around arbitrary and diverse asset stores.
4
+
5
+ == Installation
6
+
7
+ === as a Gem
8
+
9
+ gem install asset_cloud
10
+
11
+ == Copyright
12
+
13
+ Copyright (c) 2008-2013 Tobias Lütke & Shopify, Inc. Released under the MIT license (see LICENSE for details).
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rspec'
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+ require 'rdoc/task'
6
+ require 'rspec/core/rake_task'
7
+
8
+ desc 'Default: run unit tests.'
9
+ task :default => [:spec]
10
+
11
+ desc "Run all spec examples"
12
+ RSpec::Core::RakeTask.new do |t|
13
+ t.pattern = 'spec/**/*_spec.rb'
14
+ t.rspec_opts = ['--color']
15
+ end
16
+
17
+ desc 'Generate documentation for the asset_cloud plugin.'
18
+ Rake::RDocTask.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = 'AssetCloud'
21
+ rdoc.options << '--line-numbers' << '--inline-source'
22
+ rdoc.rdoc_files.include('README')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{asset_cloud}
5
+ s.version = "1.0.2"
6
+
7
+ s.authors = %w(Shopify)
8
+ s.date = %q{2009-08-04}
9
+ s.summary = %q{An abstraction layer around arbitrary and diverse asset stores.}
10
+ s.description = %q{An abstraction layer around arbitrary and diverse asset stores.}
11
+
12
+ s.email = %q{developers@shopify.com}
13
+ s.homepage = %q{http://github.com/Shopify/asset_cloud}
14
+ s.require_paths = %w(lib)
15
+
16
+ s.files = `git ls-files`.split($/)
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ s.add_dependency 'activesupport'
20
+ s.add_dependency 'class_inheritable_attributes'
21
+
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'rake'
24
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support'
2
+
3
+ # Core
4
+ require 'asset_cloud/asset'
5
+ require 'asset_cloud/metadata'
6
+ require 'asset_cloud/bucket'
7
+ require 'asset_cloud/buckets/active_record_bucket'
8
+ require 'asset_cloud/buckets/blackhole_bucket'
9
+ require 'asset_cloud/buckets/bucket_chain'
10
+ require 'asset_cloud/buckets/file_system_bucket'
11
+ require 'asset_cloud/buckets/invalid_bucket'
12
+ require 'asset_cloud/buckets/memory_bucket'
13
+ require 'asset_cloud/buckets/versioned_memory_bucket'
14
+ require 'asset_cloud/base'
15
+
16
+
17
+ # Extensions
18
+ require 'asset_cloud/free_key_locator'
19
+ require 'asset_cloud/callbacks'
20
+ require 'asset_cloud/validations'
21
+
22
+ require 'asset_cloud/asset_extension'
23
+
24
+
25
+ AssetCloud::Base.class_eval do
26
+ include AssetCloud::FreeKeyLocator
27
+ include AssetCloud::Callbacks
28
+ callback_methods :write, :delete
29
+ end
30
+
31
+ AssetCloud::Asset.class_eval do
32
+ include AssetCloud::Callbacks
33
+ callback_methods :store, :delete
34
+
35
+ include AssetCloud::Validations
36
+ callback_methods :validate
37
+ validate :valid_key
38
+
39
+ def execute_callbacks(symbol, args)
40
+ super
41
+ @extensions.each {|ext| ext.execute_callbacks(symbol, args)}
42
+ end
43
+
44
+ private
45
+
46
+ def valid_key
47
+ if key.blank?
48
+ add_error "key cannot be empty"
49
+ elsif key !~ AssetCloud::Base::VALID_PATHS
50
+ add_error "#{key.inspect} contains illegal characters"
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,187 @@
1
+ module AssetCloud
2
+
3
+ class AssetError < StandardError
4
+ end
5
+
6
+ class AssetNotSaved < AssetError
7
+ end
8
+
9
+ class Asset
10
+ include Comparable
11
+ attr_accessor :key, :value, :cloud, :metadata, :new_asset
12
+ attr_reader :extensions
13
+
14
+ def initialize(cloud, key, value = nil, metadata = Metadata.non_existing)
15
+ @new_asset = true
16
+ @cloud = cloud
17
+ @key = key
18
+ @value = value
19
+ @metadata = metadata
20
+
21
+ apply_extensions
22
+
23
+ if @cloud.blank?
24
+ raise ArgumentError, "cloud is not a valid AssetCloud::Base"
25
+ end
26
+
27
+ yield self if block_given?
28
+ end
29
+
30
+ def self.at(cloud, key, value = nil, metadata = nil, &block)
31
+ file = self.new(cloud, key, value, metadata, &block)
32
+ file.new_asset = false
33
+ file
34
+ end
35
+
36
+ def <=>(other)
37
+ cloud.object_id <=> other.cloud.object_id && key <=> other.key
38
+ end
39
+
40
+ def new_asset?
41
+ @new_asset
42
+ end
43
+
44
+ def relative_key
45
+ @key.split("/",2).last
46
+ end
47
+
48
+ def relative_key_without_ext
49
+ relative_key.gsub(/\.[^.]+$/,"")
50
+ end
51
+
52
+ def dirname
53
+ File.dirname(@key)
54
+ end
55
+
56
+ def extname
57
+ File.extname(@key)
58
+ end
59
+
60
+ def format
61
+ extname.sub('.', '')
62
+ end
63
+
64
+ def basename
65
+ File.basename(@key)
66
+ end
67
+
68
+ def basename_without_ext
69
+ File.basename(@key, extname)
70
+ end
71
+
72
+ def size
73
+ metadata.size
74
+ end
75
+
76
+ def exist?
77
+ metadata.exist?
78
+ end
79
+
80
+ def created_at
81
+ metadata.created_at
82
+ end
83
+
84
+ def updated_at
85
+ metadata.updated_at
86
+ end
87
+
88
+ def value_hash
89
+ metadata.value_hash
90
+ end
91
+
92
+ def delete
93
+ if new_asset?
94
+ false
95
+ else
96
+ cloud.delete(key)
97
+ end
98
+ end
99
+
100
+ def metadata
101
+ @metadata ||= cloud.stat(key)
102
+ end
103
+
104
+ def value
105
+ @value ||= if new_asset?
106
+ nil
107
+ else
108
+ cloud.read(key)
109
+ end
110
+ end
111
+
112
+ def store
113
+ unless @value.nil?
114
+ @new_asset = false
115
+ @metadata = nil
116
+ cloud.write(key, value)
117
+ end
118
+ end
119
+
120
+ def store!
121
+ store or raise AssetNotSaved
122
+ end
123
+
124
+ def to_param
125
+ basename
126
+ end
127
+
128
+ def handle
129
+ basename.to_handle
130
+ end
131
+
132
+ def url(options = {})
133
+ cloud.url_for key, options
134
+ end
135
+
136
+ def bucket_name
137
+ @key.split('/').first
138
+ end
139
+
140
+ def bucket
141
+ cloud.buckets[bucket_name.to_sym]
142
+ end
143
+
144
+ def inspect
145
+ "#<#{self.class.name}: #{key}>"
146
+ end
147
+
148
+ # versioning
149
+
150
+ def versioned?
151
+ bucket.versioned?
152
+ end
153
+
154
+ def rollback(version)
155
+ self.value = cloud.read_version(key, version)
156
+ self
157
+ end
158
+
159
+ def versions
160
+ cloud.versions(key)
161
+ end
162
+
163
+ def version_details
164
+ cloud.version_details(key)
165
+ end
166
+
167
+ def method_missing(method, *args)
168
+ @extensions.each do |ext|
169
+ begin
170
+ return ext.send(method, *args)
171
+ rescue NoMethodError, NotImplementedError => e
172
+ nil
173
+ end
174
+ end
175
+ super
176
+ end
177
+
178
+ private
179
+
180
+ def apply_extensions
181
+ @extensions ||= []
182
+ @cloud.asset_extension_classes_for_bucket(bucket_name).each do |ext|
183
+ @extensions << ext.new(self) if ext.applies_to_asset?(self)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,42 @@
1
+ module AssetCloud
2
+ class AssetExtension
3
+ class AssetMismatch < StandardError
4
+ end
5
+ def store; true; end
6
+ def delete; true; end
7
+
8
+ include AssetCloud::Callbacks
9
+ include AssetCloud::Validations
10
+
11
+ callback_methods :store, :delete, :validate
12
+
13
+ attr_reader :asset
14
+ delegate :add_error, :to => :asset
15
+
16
+ class_attribute :extnames
17
+
18
+ def self.applies_to(*args)
19
+ extnames = args.map do |arg|
20
+ arg = arg.to_s.downcase
21
+ arg = ".#{arg}" unless arg.starts_with?('.')
22
+ arg
23
+ end
24
+ self.extnames = extnames
25
+ end
26
+
27
+ def self.applies_to_asset?(asset)
28
+ extnames = self.extnames || []
29
+ extnames.each do |extname|
30
+ return true if asset.key.downcase.ends_with?(extname)
31
+ end
32
+ false
33
+ end
34
+
35
+ def initialize(asset)
36
+ unless self.class.applies_to_asset?(asset)
37
+ raise AssetMismatch, "Instances of #{self.class.name} cannot be applied to asset #{asset.key.inspect}"
38
+ end
39
+ @asset = asset
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,247 @@
1
+ require 'uri'
2
+ require 'class_inheritable_attributes'
3
+
4
+ module AssetCloud
5
+
6
+ class IllegalPath < StandardError
7
+ end
8
+
9
+ class Base
10
+ cattr_accessor :logger
11
+
12
+ VALID_PATHS = /\A[a-z0-9][a-z0-9_\-\/]+([a-z0-9][\w\-\ \.\@]*\.\w{2,6})?\z/i
13
+ MATCH_BUCKET = /^(\w+)(\/|$)/
14
+
15
+ attr_accessor :url, :root
16
+
17
+ class_attribute :root_bucket_class
18
+ self.root_bucket_class = 'AssetCloud::FileSystemBucket'
19
+ class_attribute :root_asset_class
20
+ self.root_asset_class = 'AssetCloud::Asset'
21
+
22
+ class_inheritable_hash :bucket_classes
23
+ self.bucket_classes = {}
24
+ class_inheritable_hash :asset_classes
25
+ self.asset_classes = {}
26
+ class_inheritable_hash :asset_extension_classes
27
+ self.asset_extension_classes = {}
28
+
29
+ def self.bucket(*args)
30
+ asset_class = if args.last.is_a? Hash
31
+ convert_to_class_name_if_possible(args.pop[:asset_class])
32
+ end
33
+
34
+ bucket_class = if args.last.is_a? Class
35
+ convert_to_class_name_if_possible(args.pop)
36
+ else
37
+ raise ArgumentError, 'requires a bucket class'
38
+ end
39
+
40
+ if bucket_name = args.first
41
+ self.bucket_classes[bucket_name.to_sym] = bucket_class
42
+ self.asset_classes[bucket_name.to_sym] = asset_class if asset_class
43
+ else
44
+ self.root_bucket_class = bucket_class
45
+ self.root_asset_class = asset_class if asset_class
46
+ end
47
+ end
48
+
49
+ def self.asset_extensions(*args)
50
+ opts = args.last.is_a?(Hash) ? args.pop.slice(:only, :except) : {}
51
+ opts.each do |k,v|
52
+ opts[k] = [v].flatten.map(&:to_sym)
53
+ end
54
+
55
+ args.each do |klass|
56
+ klass = convert_to_class_name_if_possible(klass)
57
+ self.asset_extension_classes[klass] = opts
58
+ end
59
+ end
60
+
61
+ def buckets
62
+ @buckets ||= Hash.new do |hash, key|
63
+ if klass = self.class.bucket_classes[key]
64
+ hash[key] = constantize_if_necessary(klass).new(self, key)
65
+ else
66
+ hash[key] = nil
67
+ end
68
+ end
69
+ end
70
+
71
+ def initialize(root, url = '/')
72
+ @root, @url = root, url
73
+ end
74
+
75
+ def url_for(key, options={})
76
+ File.join(@url, URI.encode(key))
77
+ end
78
+
79
+ def path_for(key)
80
+ File.join(path, key)
81
+ end
82
+
83
+ def path
84
+ root
85
+ end
86
+
87
+ def find(key)
88
+ asset = asset_at(key)
89
+ asset.value
90
+ asset
91
+ end
92
+
93
+ def asset_at(*args)
94
+ asset_class_for(args.first).at(self, *args)
95
+ end
96
+
97
+ def asset_at!(*args)
98
+ check_key_for_errors(args.first)
99
+ asset_at(*args)
100
+ end
101
+
102
+ def move(source, destination)
103
+ return if source == destination
104
+
105
+ object = copy(source, destination)
106
+ if object.errors.none?
107
+ asset_at(source).delete
108
+ end
109
+ object
110
+ end
111
+
112
+ def copy(source, destination)
113
+ return if source == destination
114
+
115
+ object = build(destination, read(source))
116
+ object.store
117
+ object
118
+ end
119
+
120
+ def build(key, value = nil, &block)
121
+ logger.info { " [#{self.class.name}] Building asset #{key}" } if logger
122
+ asset_class_for(key).new(self, key, value, Metadata.non_existing, &block)
123
+ end
124
+
125
+ def write(key, value)
126
+ check_key_for_errors(key)
127
+ logger.info { " [#{self.class.name}] Writing #{value.size} bytes to #{key}" } if logger
128
+
129
+ bucket_for(key).write(key, value)
130
+ end
131
+
132
+ def read(key)
133
+ logger.info { " [#{self.class.name}] Reading from #{key}" } if logger
134
+
135
+ bucket_for(key).read(key)
136
+ end
137
+
138
+ def stat(key)
139
+ logger.info { " [#{self.class.name}] Statting #{key}" } if logger
140
+
141
+ bucket_for(key).stat(key)
142
+ end
143
+
144
+ def ls(key)
145
+ logger.info { " [#{self.class.name}] Listing objects in #{key}" } if logger
146
+
147
+ bucket_for(key).ls(key)
148
+ end
149
+
150
+ def exist?(key)
151
+ if fp = stat(key)
152
+ fp.exist?
153
+ else
154
+ false
155
+ end
156
+ end
157
+
158
+ def supports?(key)
159
+ key =~ VALID_PATHS
160
+ end
161
+
162
+ def delete(key)
163
+ logger.info { " [#{self.class.name}] Deleting #{key}" } if logger
164
+
165
+ bucket_for(key).delete(key)
166
+ end
167
+
168
+ def bucket_for(key)
169
+ bucket = buckets[bucket_symbol_for_key(key)]
170
+ bucket ? bucket : root_bucket
171
+ end
172
+
173
+ def []=(key, value)
174
+ asset = self[key]
175
+ asset.value = value
176
+ asset.store
177
+ end
178
+
179
+ def [](key)
180
+ asset_at!(key)
181
+ end
182
+
183
+ # versioning
184
+
185
+ def read_version(key, version)
186
+ logger.info { " [#{self.class.name}] Reading from #{key} at version #{version}" } if logger
187
+ bucket_for(key).read_version(key, version)
188
+ end
189
+
190
+ def versions(key)
191
+ logger.info { " [#{self.class.name}] Getting all versions for #{key}" } if logger
192
+ bucket_for(key).versions(key)
193
+ end
194
+
195
+ def version_details(key)
196
+ logger.info { " [#{self.class.name}] Getting all version details for #{key}" } if logger
197
+ bucket_for(key).version_details(key)
198
+ end
199
+
200
+ def asset_class_for(key)
201
+ klass = self.class.asset_classes[bucket_symbol_for_key(key)] || self.class.root_asset_class
202
+ constantize_if_necessary(klass)
203
+ end
204
+
205
+ def asset_extension_classes_for_bucket(bucket)
206
+ bucket = bucket.to_sym
207
+ extensions = self.class.asset_extension_classes
208
+ klasses = extensions.keys.select do |ext|
209
+ opts = extensions[ext]
210
+ (opts.key?(:only) ? opts[:only].include?(bucket) : true) &&
211
+ (opts.key?(:except) ? !opts[:except].include?(bucket) : true)
212
+ end
213
+ klasses.map {|klass| constantize_if_necessary(klass)}
214
+ end
215
+
216
+ protected
217
+
218
+ def bucket_symbol_for_key(key)
219
+ $1.to_sym if key =~ MATCH_BUCKET
220
+ end
221
+
222
+ def root_bucket
223
+ @default_bucket ||= constantize_if_necessary(self.class.root_bucket_class).new(self, '')
224
+ end
225
+
226
+ def constantize_if_necessary(klass)
227
+ klass.is_a?(Class) ? klass : klass.constantize
228
+ end
229
+
230
+ def self.convert_to_class_name_if_possible(klass)
231
+ if klass.is_a?(Class) && klass.name.present?
232
+ klass.name
233
+ else
234
+ klass
235
+ end
236
+ end
237
+
238
+ def check_key_for_errors(key)
239
+ raise IllegalPath, "key cannot be empty" if key.blank?
240
+ raise IllegalPath, "#{key.inspect} contains illegal characters" unless supports?(key)
241
+ rescue => e
242
+ logger.info { " [#{self.class.name}] bad key #{e.message}" } if logger
243
+ raise
244
+ end
245
+
246
+ end
247
+ end