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.
- 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
|