jamesmacaulay-asset_cloud 0.5.0

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