asset_cloud 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +13 -0
- data/Rakefile +24 -0
- data/asset_cloud.gemspec +24 -0
- data/lib/asset_cloud.rb +54 -0
- data/lib/asset_cloud/asset.rb +187 -0
- data/lib/asset_cloud/asset_extension.rb +42 -0
- data/lib/asset_cloud/base.rb +247 -0
- data/lib/asset_cloud/bucket.rb +39 -0
- data/lib/asset_cloud/buckets/active_record_bucket.rb +57 -0
- data/lib/asset_cloud/buckets/blackhole_bucket.rb +23 -0
- data/lib/asset_cloud/buckets/bucket_chain.rb +84 -0
- data/lib/asset_cloud/buckets/file_system_bucket.rb +79 -0
- data/lib/asset_cloud/buckets/invalid_bucket.rb +28 -0
- data/lib/asset_cloud/buckets/memory_bucket.rb +42 -0
- data/lib/asset_cloud/buckets/versioned_memory_bucket.rb +33 -0
- data/lib/asset_cloud/callbacks.rb +63 -0
- data/lib/asset_cloud/free_key_locator.rb +28 -0
- data/lib/asset_cloud/metadata.rb +29 -0
- data/lib/asset_cloud/validations.rb +52 -0
- data/spec/active_record_bucket_spec.rb +95 -0
- data/spec/asset_extension_spec.rb +103 -0
- data/spec/asset_spec.rb +177 -0
- data/spec/base_spec.rb +114 -0
- data/spec/blackhole_bucket_spec.rb +41 -0
- data/spec/bucket_chain_spec.rb +158 -0
- data/spec/callbacks_spec.rb +125 -0
- data/spec/file_system_spec.rb +74 -0
- data/spec/files/products/key.txt +1 -0
- data/spec/files/versioned_stuff/foo +1 -0
- data/spec/find_free_key_spec.rb +39 -0
- data/spec/memory_bucket_spec.rb +52 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/validations_spec.rb +53 -0
- data/spec/versioned_memory_bucket_spec.rb +36 -0
- metadata +151 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module AssetCloud
|
4
|
+
|
5
|
+
module FreeKeyLocator
|
6
|
+
|
7
|
+
def find_free_key_like(key, options = {})
|
8
|
+
# Check weather the suggested key name is free. If so we
|
9
|
+
# simply return it.
|
10
|
+
|
11
|
+
if not exist?(key)
|
12
|
+
key
|
13
|
+
else
|
14
|
+
|
15
|
+
ext = File.extname(key)
|
16
|
+
dirname = File.dirname(key)
|
17
|
+
base = dirname == '.' ? File.basename(key, ext) : File.join(File.dirname(key), File.basename(key, ext))
|
18
|
+
base = base.gsub(/_[\h]{8}-[\h]{4}-4[\h]{3}-[\h]{4}-[\h]{12}/, "")
|
19
|
+
|
20
|
+
# Attach UUID to avoid name collision
|
21
|
+
key = "#{base}_#{SecureRandom.uuid}#{ext}"
|
22
|
+
return key unless exist?(key)
|
23
|
+
|
24
|
+
raise StandardError, 'Filesystem out of free filenames'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
class Metadata
|
3
|
+
attr_accessor :exist, :size, :created_at, :updated_at, :value_hash
|
4
|
+
|
5
|
+
def new?
|
6
|
+
!self.exist
|
7
|
+
end
|
8
|
+
|
9
|
+
def exist?
|
10
|
+
self.exist
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(exist, size = nil, created_at = nil, updated_at = nil, value_hash = nil)
|
14
|
+
self.exist, self.size, self.created_at, self.updated_at, self.value_hash = exist, size, created_at, updated_at, value_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.existing
|
18
|
+
self.new(true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.non_existing
|
22
|
+
self.new false
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"#<#{self.class.name}: exist:#{exist} size:#{size.inspect} bytes>"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
module Validations
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.class_eval do
|
6
|
+
include AssetCloud::Callbacks
|
7
|
+
|
8
|
+
alias_method_chain :store, :validation
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def validate(*validations, &block)
|
14
|
+
validations << block if block_given?
|
15
|
+
write_inheritable_array(:validate, validations)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def store_with_validation
|
20
|
+
validate
|
21
|
+
errors.empty? ? store_without_validation : false
|
22
|
+
end
|
23
|
+
|
24
|
+
def errors
|
25
|
+
@errors ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def warnings
|
29
|
+
@warnings ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
validate
|
34
|
+
errors.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_error(msg)
|
38
|
+
errors << msg
|
39
|
+
errors.uniq!
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_warning(*msgs)
|
43
|
+
warnings.concat(msgs)
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate
|
47
|
+
errors.clear
|
48
|
+
warnings.clear
|
49
|
+
execute_callbacks(:validate, [])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
MockRecords = Object.new
|
4
|
+
|
5
|
+
class MockActiveRecordBucket < AssetCloud::ActiveRecordBucket
|
6
|
+
self.key_attribute = 'name'
|
7
|
+
self.value_attribute = 'body'
|
8
|
+
protected
|
9
|
+
def records
|
10
|
+
MockRecords
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class RecordCloud < AssetCloud::Base
|
15
|
+
bucket :stuff, MockActiveRecordBucket
|
16
|
+
end
|
17
|
+
|
18
|
+
describe AssetCloud::ActiveRecordBucket do
|
19
|
+
directory = File.dirname(__FILE__) + '/files'
|
20
|
+
|
21
|
+
before do
|
22
|
+
@cloud = RecordCloud.new(directory , 'http://assets/files' )
|
23
|
+
@bucket = @cloud.buckets[:stuff]
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#ls' do
|
27
|
+
before do
|
28
|
+
MockRecords.should_receive(:connection).and_return(@mock_connection = double("connection"))
|
29
|
+
@mock_connection.should_receive(:quote_column_name).with('name').and_return("`name`")
|
30
|
+
(@mock_record = double("record")).should_receive(:name).and_return('stuff/a1')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return a list of assets which start with the given prefix" do
|
34
|
+
MockRecords.should_receive(:all).with(:conditions => ["`name` LIKE ?", "stuff/a%"]).and_return([@mock_record])
|
35
|
+
|
36
|
+
@bucket.ls('stuff/a').size.should == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return a list of all assets when a prefix is not given" do
|
40
|
+
MockRecords.should_receive(:all).with(:conditions => ["`name` LIKE ?", "stuff%"]).and_return([@mock_record])
|
41
|
+
|
42
|
+
@bucket.ls.size.should == 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#read' do
|
47
|
+
it "should return the value of a key when it exists" do
|
48
|
+
(@mock_record = double("record")).should_receive(:body).and_return('foo')
|
49
|
+
MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(@mock_record)
|
50
|
+
|
51
|
+
@bucket.read('stuff/a1')
|
52
|
+
end
|
53
|
+
it "should raise AssetNotFoundError when nothing is there" do
|
54
|
+
MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(nil)
|
55
|
+
|
56
|
+
lambda {@bucket.read('stuff/a1')}.should raise_error(AssetCloud::AssetNotFoundError)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#write' do
|
61
|
+
it "should write to the DB" do
|
62
|
+
(@mock_record = double("record")).should_receive(:body=).with('foo').and_return('foo')
|
63
|
+
@mock_record.should_receive(:save!).and_return(true)
|
64
|
+
MockRecords.should_receive(:find_or_initialize_by_name).with('stuff/a1').and_return(@mock_record)
|
65
|
+
|
66
|
+
@bucket.write('stuff/a1', 'foo')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#delete' do
|
71
|
+
it "should destroy records" do
|
72
|
+
(@mock_record = double("record")).should_receive(:destroy).and_return(true)
|
73
|
+
MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(@mock_record)
|
74
|
+
|
75
|
+
@bucket.delete('stuff/a1')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#stat' do
|
80
|
+
it "should return appropriate metadata" do
|
81
|
+
(@mock_record = double("record")).should_receive(:created_at).and_return(1982)
|
82
|
+
@mock_record.should_receive(:updated_at).and_return(2002)
|
83
|
+
@mock_record.should_receive(:body).and_return('foo')
|
84
|
+
MockRecords.should_receive(:first).with(:conditions => {'name' => 'stuff/a1'}).and_return(@mock_record)
|
85
|
+
|
86
|
+
metadata = @bucket.stat('stuff/a1')
|
87
|
+
metadata.created_at.should == 1982
|
88
|
+
metadata.updated_at.should == 2002
|
89
|
+
metadata.size.should == 3
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class NoCatsAsset < AssetCloud::Asset
|
4
|
+
validate :no_cats
|
5
|
+
before_store :asset_callback
|
6
|
+
|
7
|
+
private
|
8
|
+
def no_cats
|
9
|
+
add_error('no cats allowed!') if value =~ /cat/i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class CssAssetExtension < AssetCloud::AssetExtension
|
14
|
+
applies_to :css
|
15
|
+
|
16
|
+
validate :valid_css
|
17
|
+
|
18
|
+
private
|
19
|
+
def valid_css
|
20
|
+
add_error "not enough curly brackets!" unless asset.value =~ /\{.*\}/
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class XmlAssetExtension < AssetCloud::AssetExtension
|
25
|
+
applies_to :xml
|
26
|
+
|
27
|
+
validate :valid_xml
|
28
|
+
before_store :xml_callback
|
29
|
+
|
30
|
+
def turn_into_xml
|
31
|
+
asset.value = "<xml>#{asset.value}</xml>"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def valid_xml
|
36
|
+
add_error "not enough angle brackets!" unless asset.value =~ /\<.*\>/
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class CatsAndDogsCloud < AssetCloud::Base
|
41
|
+
bucket :dog_pound, AssetCloud::MemoryBucket, :asset_class => NoCatsAsset
|
42
|
+
bucket :cat_pen, AssetCloud::MemoryBucket
|
43
|
+
|
44
|
+
asset_extensions CssAssetExtension, :only => :cat_pen
|
45
|
+
asset_extensions XmlAssetExtension, :except => :cat_pen
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "AssetExtension" do
|
49
|
+
include AssetCloud
|
50
|
+
|
51
|
+
before do
|
52
|
+
@cloud = CatsAndDogsCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "applicability" do
|
56
|
+
it "should work" do
|
57
|
+
asset = @cloud['cat_pen/cats.xml']
|
58
|
+
XmlAssetExtension.applies_to_asset?(asset).should == true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "validations" do
|
63
|
+
it "should be added to assets in the right bucket with the right extension" do
|
64
|
+
asset = @cloud['cat_pen/cats.css']
|
65
|
+
asset.value = 'foo'
|
66
|
+
asset.store.should == false
|
67
|
+
asset.errors.should == ["not enough curly brackets!"]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should not squash existing validations on the asset" do
|
71
|
+
asset = @cloud['dog_pound/cats.xml']
|
72
|
+
asset.value = 'cats!'
|
73
|
+
asset.store.should == false
|
74
|
+
asset.errors.should == ['no cats allowed!', "not enough angle brackets!"]
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should not apply to non-matching assets or those in exempted buckets" do
|
78
|
+
asset = @cloud['cat_pen/cats.xml']
|
79
|
+
asset.value = "xml"
|
80
|
+
asset.store.should == true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "callbacks" do
|
85
|
+
it "should run alongside the asset's callbacks" do
|
86
|
+
asset = @cloud['dog_pound/dogs.xml']
|
87
|
+
asset.should_receive(:asset_callback)
|
88
|
+
asset.extensions.first.should_receive(:xml_callback)
|
89
|
+
asset.value = '<dogs/>'
|
90
|
+
asset.store.should == true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#method_missing" do
|
95
|
+
it "should try to run method on extensions" do
|
96
|
+
asset = @cloud['dog_pound/dogs.xml']
|
97
|
+
asset.value = 'dogs'
|
98
|
+
asset.turn_into_xml
|
99
|
+
asset.value.should == '<xml>dogs</xml>'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
data/spec/asset_spec.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Asset" do
|
4
|
+
include AssetCloud
|
5
|
+
|
6
|
+
before do
|
7
|
+
@cloud = double('Cloud', :asset_extension_classes_for_bucket => [])
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "when first created (without a value)" do
|
11
|
+
before do
|
12
|
+
@asset = AssetCloud::Asset.new(@cloud, "products/key.txt")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should be return new_asset? => true" do
|
16
|
+
@asset.new_asset?.should == true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should have a key" do
|
20
|
+
@asset.key.should == 'products/key.txt'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have a value of nil" do
|
24
|
+
|
25
|
+
@asset.value.should == nil
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have a basename" do
|
29
|
+
@asset.basename.should == 'key.txt'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have a basename without ext (if required)" do
|
33
|
+
@asset.basename_without_ext.should == 'key'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have an ext" do
|
37
|
+
@asset.extname.should == '.txt'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have a relative_key_without_ext" do
|
41
|
+
@asset.relative_key_without_ext.should == 'key'
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should have a bucket_name" do
|
45
|
+
@asset.bucket_name.should == 'products'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should have a bucket" do
|
49
|
+
@cloud.should_receive(:buckets).and_return(:products => :products_bucket)
|
50
|
+
@asset.bucket.should == :products_bucket
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should store data to the bucket" do
|
54
|
+
@cloud.should_receive(:write).with("products/key.txt", 'value')
|
55
|
+
|
56
|
+
@asset.value = 'value'
|
57
|
+
@asset.store
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should not try to store data when it's value is nil" do
|
61
|
+
@cloud.should_receive(:write).never
|
62
|
+
|
63
|
+
@asset.store
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should not try to read data from bucket if its a new_asset" do
|
67
|
+
@cloud.should_receive(:read).never
|
68
|
+
|
69
|
+
@asset.value.should == nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should simply ignore calls to delete" do
|
73
|
+
@cloud.should_receive(:delete).never
|
74
|
+
|
75
|
+
@asset.delete
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
describe "when first created (without a value) with subdirectory" do
|
82
|
+
before do
|
83
|
+
@asset = AssetCloud::Asset.new(@cloud, "products/retail/key.txt")
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should have a relative_key_without_ext" do
|
87
|
+
@asset.relative_key_without_ext.should == 'retail/key'
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should have a relative_key" do
|
91
|
+
@asset.relative_key.should == 'retail/key.txt'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
describe "when first created with value" do
|
97
|
+
before do
|
98
|
+
@asset = AssetCloud::Asset.new(@cloud, "products/key.txt", 'value')
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should be return new_asset? => true" do
|
102
|
+
@asset.new_asset?.should == true
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
it "should have a value of 'value'" do
|
107
|
+
@asset.value.should == 'value'
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should return false when asked if it exists because its still a new_asset" do
|
111
|
+
@asset.exist?.should == false
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
it "should not try to read data from bucket if its a new_asset" do
|
116
|
+
@cloud.should_receive(:read).never
|
117
|
+
|
118
|
+
@asset.value.should == 'value'
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should write data to the bucket" do
|
122
|
+
@cloud.should_receive(:write).with("products/key.txt", 'value')
|
123
|
+
@asset.store
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "when fetched from the bucket" do
|
129
|
+
before do
|
130
|
+
@asset = AssetCloud::Asset.at(@cloud, "products/key.txt", 'value', AssetCloud::Metadata.new(true, 'value'.size, Time.now, Time.now))
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should be return new_asset? => false" do
|
134
|
+
@asset.new_asset?.should == false
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should indicate that it exists" do
|
138
|
+
|
139
|
+
@asset.exist?.should == true
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
it "should read the value from the bucket" do
|
144
|
+
@asset.value.should == 'value'
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
it "should simply ignore calls to delete" do
|
149
|
+
@cloud.should_receive(:delete).and_return(true)
|
150
|
+
|
151
|
+
@asset.delete
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should ask the bucket to create a full url" do
|
155
|
+
@cloud.should_receive(:url_for).with('products/key.txt', {}).and_return('http://assets/products/key.txt')
|
156
|
+
|
157
|
+
@asset.url.should == 'http://assets/products/key.txt'
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should ask the bucket whether or not it is versioned" do
|
161
|
+
bucket = double('Bucket')
|
162
|
+
@cloud.should_receive(:buckets).and_return(:products => bucket)
|
163
|
+
bucket.should_receive(:versioned?).and_return(true)
|
164
|
+
|
165
|
+
@asset.versioned?.should == true
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should validate its key" do
|
169
|
+
asset = AssetCloud::Asset.new(@cloud, "products/foo, bar.txt", "data")
|
170
|
+
asset.store.should == false
|
171
|
+
asset.errors.size.should == 1
|
172
|
+
asset.errors.first.should =~ /illegal characters/
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
end
|