asset_cloud 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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