jamesmacaulay-asset_cloud 0.5.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.
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ *.orig
3
+ .dotest
data/CHANGELOG ADDED
@@ -0,0 +1,7 @@
1
+ June 2009:
2
+
3
+ * Use different Asset subclasses for each bucket in a cloud (james)
4
+ * AssetCloud::Base#find now calls asset_at but will raise errors if the asset doesn't exist (james)
5
+ * original AssetCloud::Base#find renamed to #asset_at (james)
6
+ * Added simple bucket multiplexing with AssetCloud::Bucket.chain (james)
7
+ * extraction from Shopify
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 Tobias Lütke & Jaded Pixel, 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,17 @@
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 Shopify-asset_cloud -s http://gems.github.com
10
+
11
+ === as a Rails plugin
12
+
13
+ script/plugin install git://github.com/Shopify/asset_cloud.git
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2008-2009 Tobias Lütke & Jaded Pixel, Inc. Released under the MIT license (see LICENSE for details).
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'spec/rake/spectask'
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => [:spec]
8
+
9
+ desc "Run all spec examples"
10
+ Spec::Rake::SpecTask.new do |t|
11
+ t.libs << "spec"
12
+ t.spec_files = FileList['spec/**/*_spec.rb']
13
+ t.spec_opts = ['--options', %\"#{File.dirname(__FILE__)}/spec/spec.opts"\]
14
+ end
15
+
16
+ desc 'Generate documentation for the asset_cloud plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'AssetCloud'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ begin
26
+ require 'jeweler'
27
+ Jeweler::Tasks.new do |gemspec|
28
+ gemspec.name = "asset_cloud"
29
+ gemspec.summary = "An abstraction layer around arbitrary and diverse asset stores."
30
+ gemspec.email = "developers@shopify.com"
31
+ gemspec.homepage = "http://github.com/Shopify/asset_cloud"
32
+ gemspec.description = "An abstraction layer around arbitrary and diverse asset stores."
33
+ gemspec.authors = ["Shopify"]
34
+ end
35
+ rescue LoadError
36
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
37
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.1
@@ -0,0 +1,79 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{asset_cloud}
5
+ s.version = "0.5.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Shopify"]
9
+ s.date = %q{2009-06-16}
10
+ s.description = %q{An abstraction layer around arbitrary and diverse asset stores.}
11
+ s.email = %q{developers@shopify.com}
12
+ s.extra_rdoc_files = [
13
+ "LICENSE",
14
+ "README.rdoc"
15
+ ]
16
+ s.files = [
17
+ ".document",
18
+ ".gitignore",
19
+ "CHANGELOG",
20
+ "LICENSE",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "asset_cloud.gemspec",
25
+ "init.rb",
26
+ "install.rb",
27
+ "lib/asset_cloud.rb",
28
+ "lib/asset_cloud/asset.rb",
29
+ "lib/asset_cloud/base.rb",
30
+ "lib/asset_cloud/blackhole_bucket.rb",
31
+ "lib/asset_cloud/bucket.rb",
32
+ "lib/asset_cloud/callbacks.rb",
33
+ "lib/asset_cloud/file_system_bucket.rb",
34
+ "lib/asset_cloud/free_key_locator.rb",
35
+ "lib/asset_cloud/invalid_bucket.rb",
36
+ "lib/asset_cloud/memory_bucket.rb",
37
+ "lib/asset_cloud/metadata.rb",
38
+ "spec/asset_spec.rb",
39
+ "spec/base_spec.rb",
40
+ "spec/blackhole_bucket_spec.rb",
41
+ "spec/bucket_spec.rb",
42
+ "spec/callbacks_spec.rb",
43
+ "spec/file_system_spec.rb",
44
+ "spec/files/products/key.txt",
45
+ "spec/find_free_key_spec.rb",
46
+ "spec/memory_bucket_spec.rb",
47
+ "spec/regexp_spec.rb",
48
+ "spec/spec.opts",
49
+ "spec/spec_helper.rb"
50
+ ]
51
+ s.has_rdoc = true
52
+ s.homepage = %q{http://github.com/Shopify/asset_cloud}
53
+ s.rdoc_options = ["--charset=UTF-8"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.2}
56
+ s.summary = %q{An abstraction layer around arbitrary and diverse asset stores.}
57
+ s.test_files = [
58
+ "spec/asset_spec.rb",
59
+ "spec/base_spec.rb",
60
+ "spec/blackhole_bucket_spec.rb",
61
+ "spec/bucket_spec.rb",
62
+ "spec/callbacks_spec.rb",
63
+ "spec/file_system_spec.rb",
64
+ "spec/find_free_key_spec.rb",
65
+ "spec/memory_bucket_spec.rb",
66
+ "spec/regexp_spec.rb",
67
+ "spec/spec_helper.rb"
68
+ ]
69
+
70
+ if s.respond_to? :specification_version then
71
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
72
+ s.specification_version = 3
73
+
74
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
75
+ else
76
+ end
77
+ else
78
+ end
79
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ # Include hook code here
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,119 @@
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
12
+ attr_accessor :new_asset
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
+ if @cloud.blank?
22
+ raise ArgumentError, "cloud is not a valid AssetCloud::Base"
23
+ end
24
+
25
+ yield self if block_given?
26
+ end
27
+
28
+ def self.at(cloud, key, value = nil, metadata = nil, &block)
29
+ file = self.new(cloud, key, value, metadata, &block)
30
+ file.new_asset = false
31
+ file
32
+ end
33
+
34
+ def <=>(other)
35
+ cloud.object_id <=> other.cloud.object_id && key <=> other.key
36
+ end
37
+
38
+ def new_asset?
39
+ @new_asset
40
+ end
41
+
42
+ def dirname
43
+ File.dirname(@key)
44
+ end
45
+
46
+ def extname
47
+ File.extname(@key)
48
+ end
49
+
50
+ def basename
51
+ File.basename(@key)
52
+ end
53
+
54
+ def basename_without_ext
55
+ File.basename(@key, extname)
56
+ end
57
+
58
+ def size
59
+ metadata.size
60
+ end
61
+
62
+ def exist?
63
+ metadata.exist?
64
+ end
65
+
66
+ def created_at
67
+ metadata.created_at
68
+ end
69
+
70
+ def delete
71
+ if new_asset?
72
+ false
73
+ else
74
+ cloud.delete(key)
75
+ end
76
+ end
77
+
78
+ def metadata
79
+ @metadata ||= cloud.stat(key)
80
+ end
81
+
82
+ def value
83
+ @value ||= if new_asset?
84
+ nil
85
+ else
86
+ cloud.read(key)
87
+ end
88
+ end
89
+
90
+ def store
91
+ unless @value.nil?
92
+ @new_asset = false
93
+ @metadata = nil
94
+ cloud.write(key, value)
95
+ end
96
+ end
97
+
98
+ def store!
99
+ store or raise AssetNotSaved
100
+ end
101
+
102
+ def to_param
103
+ basename
104
+ end
105
+
106
+ def handle
107
+ basename.to_handle
108
+ end
109
+
110
+ def url
111
+ cloud.url_for key
112
+ end
113
+
114
+
115
+ def inspect
116
+ "#<#{self.class.name}: #{key}>"
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,178 @@
1
+
2
+ module AssetCloud
3
+
4
+ class IllegalPath < StandardError
5
+ end
6
+
7
+ class Base
8
+ cattr_accessor :logger
9
+
10
+ VALID_PATHS = /^[a-z0-9][a-z0-9_\-\/]+([a-z0-9][\w\-\ \.]*\.\w{2,6})?$/i
11
+
12
+ attr_accessor :url, :root
13
+
14
+ class_inheritable_accessor :root_bucket_class
15
+ self.root_bucket_class = FileSystemBucket
16
+ class_inheritable_accessor :root_asset_class
17
+ self.root_asset_class = Asset
18
+
19
+ class_inheritable_hash :bucket_classes
20
+ self.bucket_classes = {}
21
+ class_inheritable_hash :asset_classes
22
+ self.asset_classes = {}
23
+
24
+ def self.bucket(*args)
25
+ asset_class = if args.last.is_a? Hash
26
+ args.pop[:asset_class]
27
+ end
28
+
29
+ if args.last.is_a? Class
30
+ bucket_class = args.pop
31
+ else
32
+ raise ArgumentError, 'requires a bucket class'
33
+ end
34
+
35
+ if bucket_name = args.first
36
+ self.bucket_classes[bucket_name.to_sym] = bucket_class
37
+ self.asset_classes[bucket_name.to_sym] = asset_class if asset_class
38
+ else
39
+ self.root_bucket_class = bucket_class
40
+ self.root_asset_class = asset_class if asset_class
41
+ end
42
+ end
43
+
44
+ def buckets
45
+ @buckets ||= Hash.new do |hash, key|
46
+ if klass = self.class.bucket_classes[key]
47
+ hash[key] = klass.new(self, key)
48
+ else
49
+ hash[key] = nil
50
+ end
51
+ end
52
+ end
53
+
54
+ def initialize(root, url = '/')
55
+ @root, @url = root, url
56
+ end
57
+
58
+ def url_for(key, secure = false)
59
+ File.join(@url, key)
60
+ end
61
+
62
+ def path_for(key)
63
+ File.join(path, key)
64
+ end
65
+
66
+ def path
67
+ root
68
+ end
69
+
70
+ def find(key)
71
+ returning asset_at(key) do |asset|
72
+ asset.value
73
+ end
74
+ end
75
+
76
+ def asset_at(key)
77
+ check_key_for_errors(key)
78
+
79
+ asset_class_for(key).at(self, key)
80
+ end
81
+
82
+ def move(source, destination)
83
+ return if source == destination
84
+
85
+ object = copy(source, destination)
86
+ asset_at(source).delete
87
+ object
88
+ end
89
+
90
+ def copy(source, destination)
91
+ return if source == destination
92
+
93
+ object = build(destination, read(source))
94
+ object.store
95
+ object
96
+ end
97
+
98
+ def build(key, value = nil, &block)
99
+ logger.info { " [#{self.class.name}] Building asset #{key}" } if logger
100
+ asset_class_for(key).new(self, key, value, Metadata.non_existing, &block)
101
+ end
102
+
103
+ def write(key, value)
104
+ check_key_for_errors(key)
105
+ logger.info { " [#{self.class.name}] Writing #{value.size} bytes to #{key}" } if logger
106
+
107
+ bucket_for(key).write(key, value)
108
+ end
109
+
110
+ def read(key)
111
+ logger.info { " [#{self.class.name}] Reading from #{key}" } if logger
112
+
113
+ bucket_for(key).read(key)
114
+ end
115
+
116
+ def stat(key)
117
+ logger.info { " [#{self.class.name}] Statting #{key}" } if logger
118
+
119
+ bucket_for(key).stat(key)
120
+ end
121
+
122
+ def ls(key)
123
+ logger.info { " [#{self.class.name}] Listing objects in #{key}" } if logger
124
+
125
+ bucket_for(key).ls(key)
126
+ end
127
+
128
+ def exist?(key)
129
+ if fp = stat(key)
130
+ fp.exist?
131
+ else
132
+ false
133
+ end
134
+ end
135
+
136
+ def delete(key)
137
+ logger.info { " [#{self.class.name}] Deleting #{key}" } if logger
138
+
139
+ bucket_for(key).delete(key)
140
+ end
141
+
142
+ def bucket_for(key)
143
+ bucket = buckets[bucket_symbol_for_key(key)]
144
+ bucket ? bucket : root_bucket
145
+ end
146
+
147
+ def []=(key, value)
148
+ write(key, value)
149
+ end
150
+
151
+ def [](key)
152
+ asset_at(key)
153
+ end
154
+
155
+ protected
156
+
157
+ def asset_class_for(key)
158
+ self.class.asset_classes[bucket_symbol_for_key(key)] || self.class.root_asset_class
159
+ end
160
+
161
+ def bucket_symbol_for_key(key)
162
+ $1.to_sym if key =~ /^(\w+)(\/|$)/
163
+ end
164
+
165
+ def root_bucket
166
+ @default_bucket ||= self.class.root_bucket_class.new(self, '')
167
+ end
168
+
169
+ def check_key_for_errors(key)
170
+ raise IllegalPath, "key cannot be empty" if key.blank?
171
+ raise IllegalPath, "#{key.inspect} contains illegal characters" unless key =~ VALID_PATHS
172
+ rescue => e
173
+ logger.info { " [#{self.class.name}] bad key #{e.message}" } if logger
174
+ raise
175
+ end
176
+
177
+ end
178
+ end
@@ -0,0 +1,24 @@
1
+
2
+ module AssetCloud
3
+ class BlackholeBucket < Bucket
4
+ def ls(namespace = nil)
5
+ []
6
+ end
7
+
8
+ def read(key)
9
+ nil
10
+ end
11
+
12
+ def write(key, data)
13
+ nil
14
+ end
15
+
16
+ def delete(key)
17
+ nil
18
+ end
19
+
20
+ def stat(key)
21
+ Metadata.new(false)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+
2
+ module AssetCloud
3
+ class AssetNotFoundError < StandardError
4
+ def initialize(key_or_message, message=false)
5
+ super(message ? key_or_message : "Could not find asset #{key_or_message.to_s.inspect}")
6
+ end
7
+ end
8
+
9
+ class Bucket
10
+ attr_reader :name
11
+ attr_accessor :cloud
12
+
13
+ # returns a new Bucket class which writes to each given Bucket
14
+ # but only uses the first one for reading
15
+ def self.chain(*klasses)
16
+ Class.new(self) do
17
+ attr_reader :chained_buckets
18
+ define_method 'initialize' do |cloud, name|
19
+ super
20
+ @chained_buckets = klasses.map {|klass| klass.new(cloud,name)}
21
+ end
22
+ def ls(key=nil)
23
+ @chained_buckets.first.ls(key)
24
+ end
25
+ def read(key)
26
+ @chained_buckets.first.read(key)
27
+ end
28
+ def write(key, data)
29
+ @chained_buckets.each { |b| b.write(key, data)}
30
+ end
31
+ def delete(key)
32
+ @chained_buckets.each { |b| b.delete(key)}
33
+ end
34
+ end
35
+ end
36
+
37
+ def initialize(cloud, name)
38
+ @cloud, @name = cloud, name
39
+ end
40
+
41
+ def ls(key = nil)
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def read(key)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def write(key, data)
50
+ raise NotImplementedError
51
+ end
52
+
53
+ def delete(key)
54
+ raise NotImplementedError
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,62 @@
1
+ module AssetCloud
2
+
3
+ module Callbacks
4
+
5
+ CALLBACKS = [:delete, :write]
6
+
7
+ def self.included(base)
8
+
9
+ CALLBACKS.each do |method|
10
+ code = <<-"end_eval"
11
+
12
+ def self.before_#{method}(*callbacks, &block)
13
+ callbacks << block if block_given?
14
+ write_inheritable_array(:before_#{method}, callbacks)
15
+ end
16
+
17
+ def self.after_#{method}(*callbacks, &block)
18
+ callbacks << block if block_given?
19
+ write_inheritable_array(:after_#{method}, callbacks)
20
+ end
21
+
22
+
23
+ def #{method}_with_callbacks(*args)
24
+ if execute_callbacks(:before_#{method}, args)
25
+ result = #{method}_without_callbacks(*args)
26
+ execute_callbacks(:after_#{method}, args)
27
+ end
28
+ result
29
+ end
30
+
31
+ alias_method_chain :#{method}, 'callbacks'
32
+ end_eval
33
+
34
+ base.class_eval code, __FILE__, __LINE__
35
+ end
36
+ end
37
+
38
+ def execute_callbacks(symbol, args)
39
+ callbacks_for(symbol).each do |callback|
40
+
41
+ result = case callback
42
+ when Symbol
43
+ self.send(callback, *args)
44
+ when Proc, Method
45
+ callback.call(self, *args)
46
+ else
47
+ if callback.respond_to?(method)
48
+ callback.send(method, self, *args)
49
+ else
50
+ raise StandardError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
51
+ end
52
+ end
53
+ return false if result == false
54
+ end
55
+ true
56
+ end
57
+
58
+ def callbacks_for(symbol)
59
+ self.class.read_inheritable_attribute(symbol) || []
60
+ end
61
+ end
62
+ end