jamesmacaulay-asset_cloud 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ module AssetCloud
2
+
3
+ class FileSystemBucket < Bucket
4
+
5
+ def ls(key = nil)
6
+ objects = []
7
+ base_path = File.join(path_for(key), '*')
8
+
9
+ Dir.glob(base_path).each do |f|
10
+ next unless File.file?(f)
11
+ objects.push Asset.at(cloud, relative_path_for(f) )
12
+ end
13
+ objects
14
+ end
15
+
16
+ def read(key)
17
+ File.read(path_for(key))
18
+ rescue Errno::ENOENT => e
19
+ raise AssetCloud::AssetNotFoundError, key
20
+ end
21
+
22
+ def delete(key)
23
+ File.delete(path_for(key))
24
+ rescue Errno::ENOENT
25
+ end
26
+
27
+ def write(key, data)
28
+ full_path = path_for(key)
29
+
30
+ retried = false
31
+
32
+ begin
33
+ File.open(full_path, "wb+") { |fp| fp << data }
34
+ true
35
+ rescue Errno::ENOENT => e
36
+ if retried == false
37
+ directory = File.dirname(full_path)
38
+ FileUtils.mkdir_p(File.dirname(full_path))
39
+ retried = true
40
+ retry
41
+ else
42
+ raise
43
+ end
44
+ false
45
+ end
46
+ end
47
+
48
+ def stat(key)
49
+ begin
50
+ stat = File.stat(path_for(key))
51
+ Metadata.new(true, stat.size, stat.ctime, stat.mtime)
52
+ rescue Errno::ENOENT => e
53
+ Metadata.new(false)
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def path_for(key)
60
+ cloud.path_for(key)
61
+ end
62
+
63
+ def path
64
+ cloud.path
65
+ end
66
+
67
+ private
68
+
69
+ def remove_full_path_regexp
70
+ @regexp ||= /^#{path}\//
71
+ end
72
+
73
+ def relative_path_for(f)
74
+ f.sub(remove_full_path_regexp, '')
75
+ end
76
+ end
77
+
78
+
79
+ end
@@ -0,0 +1,39 @@
1
+ module AssetCloud
2
+
3
+ module FreeKeyLocator
4
+
5
+ def find_free_key_like(key, options = {})
6
+ # Check weather the suggested key name is free. If so we
7
+ # simply return it.
8
+
9
+ if not exist?(key)
10
+ key
11
+ else
12
+
13
+ ext = File.extname(key)
14
+ dirname = File.dirname(key)
15
+ base = dirname == '.' ? File.basename(key, ext) : File.join(File.dirname(key), File.basename(key, ext))
16
+ count = base.scan(/\d+$/).flatten.first.to_i
17
+ base = base.gsub(/([\-\_]?)\d+$/,'')
18
+ separator = $1 || '_'
19
+
20
+
21
+ # increase the count until you find a unused key
22
+ 10.times do
23
+ count += 1
24
+ key = "#{base}#{separator}#{count}#{ext}"
25
+ return key unless exist?(key)
26
+ end
27
+
28
+ # Ok we have to go random here...
29
+ 100.times do
30
+ count += rand(9999999)
31
+ key = "#{base}#{separator}#{count}#{ext}"
32
+ return key unless exist?(key)
33
+ end
34
+
35
+ raise StandardError, 'Filesystem out of free filenames'
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ module AssetCloud
2
+ class InvalidBucketError < StandardError
3
+ end
4
+
5
+ class InvalidBucket < Bucket
6
+ Error = "No such namespace: %s".freeze
7
+
8
+ def ls(namespace)
9
+ raise InvalidBucketError, Error % key
10
+ end
11
+
12
+ def read(key)
13
+ raise InvalidBucketError, Error % key
14
+ end
15
+
16
+ def write(key, data)
17
+ raise InvalidBucketError, Error % key
18
+ end
19
+
20
+ def delete(key)
21
+ raise InvalidBucketError, Error % key
22
+ end
23
+
24
+ def stat(key)
25
+ raise InvalidBucketError, Error % key
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ module AssetCloud
2
+
3
+ class MemoryBucket < Bucket
4
+
5
+ def initialize(*args)
6
+ super
7
+ @memory = {}
8
+ end
9
+
10
+ def ls(key = nil)
11
+ @memory.find_all do |key, value|
12
+ key.left(key.size) == namespace
13
+ end
14
+ end
15
+
16
+ def read(key)
17
+ raise AssetCloud::AssetNotFoundError, key unless @memory.has_key?(key)
18
+ @memory[key]
19
+ end
20
+
21
+ def delete(key)
22
+ @memory.delete(key)
23
+ end
24
+
25
+ def write(key, data)
26
+ @memory[key] = data
27
+
28
+ true
29
+ end
30
+
31
+ def stat(key)
32
+ return Metadata.non_existing unless @memory.has_key?(key)
33
+
34
+ Metadata.new(true, @memory[key].size)
35
+ end
36
+
37
+ end
38
+
39
+
40
+ end
@@ -0,0 +1,30 @@
1
+ module AssetCloud
2
+
3
+ class Metadata
4
+ attr_accessor :exist, :size, :created_at, :updated_at
5
+
6
+ def new?
7
+ !self.exist
8
+ end
9
+
10
+ def exist?
11
+ self.exist
12
+ end
13
+
14
+ def initialize(exist, size = nil, created_at = nil, updated_at = nil)
15
+ self.exist, self.size, self.created_at, self.updated_at = exist, size, created_at, updated_at
16
+ end
17
+
18
+ def self.existing
19
+ self.new(true)
20
+ end
21
+
22
+ def self.non_existing
23
+ self.new false
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class.name}: exist:#{exist} size:#{size.inspect} bytes>"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_support'
2
+
3
+ # Core
4
+ require File.dirname(__FILE__) + '/asset_cloud/asset'
5
+ require File.dirname(__FILE__) + '/asset_cloud/metadata'
6
+ require File.dirname(__FILE__) + '/asset_cloud/bucket'
7
+ require File.dirname(__FILE__) + '/asset_cloud/invalid_bucket'
8
+ require File.dirname(__FILE__) + '/asset_cloud/blackhole_bucket'
9
+ require File.dirname(__FILE__) + '/asset_cloud/memory_bucket'
10
+ require File.dirname(__FILE__) + '/asset_cloud/file_system_bucket'
11
+ require File.dirname(__FILE__) + '/asset_cloud/base'
12
+
13
+
14
+ # Extensions
15
+ require File.dirname(__FILE__) + '/asset_cloud/free_key_locator'
16
+ require File.dirname(__FILE__) + '/asset_cloud/callbacks'
17
+
18
+
19
+ AssetCloud::Base.class_eval do
20
+ include AssetCloud::FreeKeyLocator
21
+ include AssetCloud::Callbacks
22
+ end
23
+
@@ -0,0 +1,135 @@
1
+
2
+ require File.dirname(__FILE__) + '/spec_helper'
3
+
4
+ describe "Asset" do
5
+ include AssetCloud
6
+
7
+ before do
8
+ @cloud = mock('Bucket')
9
+ end
10
+
11
+ describe "when first created (without a value)" do
12
+ before do
13
+ @asset = AssetCloud::Asset.new(@cloud, "products/key.txt")
14
+ end
15
+
16
+ it "should be return new_asset? => true" do
17
+ @asset.new_asset?.should == true
18
+ end
19
+
20
+ it "should have a key" do
21
+ @asset.key.should == 'products/key.txt'
22
+ end
23
+
24
+ it "should have a value of nil" do
25
+
26
+ @asset.value.should == nil
27
+ end
28
+
29
+ it "should have a basename" do
30
+ @asset.basename.should == 'key.txt'
31
+ end
32
+
33
+ it "should have a basename without ext (if required)" do
34
+ @asset.basename_without_ext.should == 'key'
35
+ end
36
+
37
+ it "should have an ext" do
38
+ @asset.extname.should == '.txt'
39
+ end
40
+
41
+
42
+ it "should store data to the bucket" do
43
+ @cloud.should_receive(:write).with("products/key.txt", 'value')
44
+
45
+ @asset.value = 'value'
46
+ @asset.store
47
+ end
48
+
49
+ it "should not try to store data when it's value is nil" do
50
+ @cloud.should_receive(:write).never
51
+
52
+ @asset.store
53
+ end
54
+
55
+ it "should not try to read data from bucket if its a new_asset" do
56
+ @cloud.should_receive(:read).never
57
+
58
+ @asset.value.should == nil
59
+ end
60
+
61
+ it "should simply ignore calls to delete" do
62
+ @cloud.should_receive(:delete).never
63
+
64
+ @asset.delete
65
+ end
66
+
67
+ end
68
+
69
+ describe "when first created with value" do
70
+ before do
71
+ @asset = AssetCloud::Asset.new(@cloud, "products/key.txt", 'value')
72
+ end
73
+
74
+ it "should be return new_asset? => true" do
75
+ @asset.new_asset?.should == true
76
+ end
77
+
78
+
79
+ it "should have a value of 'value'" do
80
+ @asset.value.should == 'value'
81
+ end
82
+
83
+ it "should return false when asked if it exists because its still a new_asset" do
84
+ @asset.exist?.should == false
85
+ end
86
+
87
+
88
+ it "should not try to read data from bucket if its a new_asset" do
89
+ @cloud.should_receive(:read).never
90
+
91
+ @asset.value.should == 'value'
92
+ end
93
+
94
+ it "should write data to the bucket" do
95
+ @cloud.should_receive(:write).with("products/key.txt", 'value')
96
+ @asset.store
97
+ end
98
+
99
+ end
100
+
101
+ describe "when fetched from the bucket" do
102
+ before do
103
+ @asset = AssetCloud::Asset.at(@cloud, "products/key.txt", 'value', AssetCloud::Metadata.new(true, 'value'.size, Time.now, Time.now))
104
+ end
105
+
106
+ it "should be return new_asset? => false" do
107
+ @asset.new_asset?.should == false
108
+ end
109
+
110
+ it "should indicate that it exists" do
111
+
112
+ @asset.exist?.should == true
113
+ end
114
+
115
+
116
+ it "should read the value from the bucket" do
117
+ @asset.value.should == 'value'
118
+ end
119
+
120
+
121
+ it "should simply ignore calls to delete" do
122
+ @cloud.should_receive(:delete).and_return(true)
123
+
124
+ @asset.delete
125
+ end
126
+
127
+ it "should ask the bucket to create a full url" do
128
+ @cloud.should_receive(:url_for).with('products/key.txt').and_return('http://assets/products/key.txt')
129
+
130
+ @asset.url.should == 'http://assets/products/key.txt'
131
+ end
132
+ end
133
+
134
+
135
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,77 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class SpecialAsset < AssetCloud::Asset
4
+ end
5
+
6
+ class BasicCloud < AssetCloud::Base
7
+ bucket :special, AssetCloud::MemoryBucket, :asset_class => SpecialAsset
8
+ end
9
+
10
+
11
+ describe BasicCloud do
12
+ directory = File.dirname(__FILE__) + '/files'
13
+
14
+ before do
15
+ @fs = BasicCloud.new(directory , 'http://assets/files' )
16
+ end
17
+
18
+ it "should raise invalid bucket if none is given" do
19
+ @fs['image.jpg'].exist?.should == false
20
+ end
21
+
22
+
23
+ it "should be backed by a file system bucket" do
24
+ @fs['products/key.txt'].exist?.should == true
25
+ end
26
+
27
+ it "should raise when listing non existing buckets" do
28
+ @fs.ls('products').should == [AssetCloud::Asset.new(@fs, 'products/key.txt')]
29
+ end
30
+
31
+
32
+ it "should allow you to create new assets" do
33
+ obj = @fs.build('new_file.test')
34
+ obj.should be_an_instance_of(AssetCloud::Asset)
35
+ obj.cloud.should be_an_instance_of(BasicCloud)
36
+ end
37
+
38
+ it "should raise error when using with minus relative or absolute paths" do
39
+ lambda { @fs['../test'] }.should raise_error(AssetCloud::IllegalPath)
40
+ lambda { @fs['/test'] }.should raise_error(AssetCloud::IllegalPath)
41
+ lambda { @fs['.../test'] }.should raise_error(AssetCloud::IllegalPath)
42
+ lambda { @fs['./test'] }.should raise_error(AssetCloud::IllegalPath)
43
+ end
44
+
45
+ it "should allow sensible relative filenames" do
46
+ @fs['assets/rails_logo.gif']
47
+ @fs['assets/rails-2.gif']
48
+ @fs['assets/223434.gif']
49
+ @fs['files/1.JPG']
50
+ end
51
+
52
+ it "should compute complete urls to assets" do
53
+ @fs.url_for('products/key.txt').should == 'http://assets/files/products/key.txt'
54
+ end
55
+
56
+ describe "#find" do
57
+ it "should return the appropriate asset when one exists" do
58
+ asset = @fs.find('products/key.txt')
59
+ asset.key.should == 'products/key.txt'
60
+ asset.value.should == 'value'
61
+ end
62
+ it "should raise AssetNotFoundError when the asset doesn't exist" do
63
+ lambda { @fs.find('products/not-there.txt') }.should raise_error(AssetCloud::AssetNotFoundError)
64
+ end
65
+ end
66
+
67
+ describe "#bucket" do
68
+ it "should allow specifying a class to use for assets in this bucket" do
69
+ @fs['assets/rails_logo.gif'].should be_instance_of(AssetCloud::Asset)
70
+ @fs['special/fancy.txt'].should be_instance_of(SpecialAsset)
71
+
72
+ @fs.build('assets/foo').should be_instance_of(AssetCloud::Asset)
73
+ @fs.build('special/foo').should be_instance_of(SpecialAsset)
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class BlackholeCloud < AssetCloud::Base
4
+ bucket AssetCloud::BlackholeBucket
5
+ end
6
+
7
+ describe BlackholeCloud do
8
+ directory = File.dirname(__FILE__) + '/files'
9
+
10
+ before do
11
+ @fs = BlackholeCloud.new(directory , 'http://assets/files' )
12
+ end
13
+
14
+ it "should allow access to files using the [] operator" do
15
+ @fs['tmp/image.jpg']
16
+ end
17
+
18
+ it "should return nil for non existent files" do
19
+ @fs['tmp/image.jpg'].exist?.should == false
20
+ end
21
+
22
+ it "should still return nil, even if you wrote something there" do
23
+ @fs['tmp/image.jpg'] = 'test'
24
+ @fs['tmp/image.jpg'].exist?.should == false
25
+ end
26
+
27
+ describe "when using a sub path" do
28
+ it "should allow access to files using the [] operator" do
29
+ @fs['tmp/image.jpg']
30
+ end
31
+
32
+ it "should return nil for non existent files" do
33
+ @fs['tmp/image.jpg'].exist?.should == false
34
+ end
35
+
36
+ it "should still return nil, even if you wrote something there" do
37
+ @fs['tmp/image.jpg'] = 'test'
38
+ @fs['tmp/image.jpg'].exist?.should == false
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class ChainedCloud < AssetCloud::Base
4
+ bucket :stuff, AssetCloud::Bucket.chain(AssetCloud::MemoryBucket, AssetCloud::BlackholeBucket)
5
+ end
6
+
7
+ describe AssetCloud::Bucket do
8
+ directory = File.dirname(__FILE__) + '/files'
9
+
10
+ before(:each) do
11
+ @cloud = ChainedCloud.new(directory , 'http://assets/files' )
12
+ @bucket_chain = @cloud.buckets[:stuff]
13
+ @memory_bucket, @blackhole_bucket = @bucket_chain.chained_buckets
14
+ end
15
+
16
+ describe "#chain" do
17
+ it 'should take multiple Bucket classes and return a new Bucket class' do
18
+ @bucket_chain.should be_a_kind_of(AssetCloud::Bucket)
19
+ end
20
+
21
+ it 'should return a Bucket which writes to each sub-bucket' do
22
+ @bucket_chain.chained_buckets.each do |bucket|
23
+ bucket.should_receive(:write).with('stuff/foo', 'bar').and_return(true)
24
+ bucket.should_receive(:delete).with('stuff/foo').and_return(true)
25
+ end
26
+
27
+ @bucket_chain.write('stuff/foo', 'bar')
28
+ @bucket_chain.delete('stuff/foo')
29
+ end
30
+
31
+ it 'should return a Bucket which reads from only the first sub-bucket' do
32
+ @memory_bucket.should_receive(:read).with('stuff/foo').and_return('bar')
33
+ @memory_bucket.should_receive(:ls).with(nil).and_return(:some_assets)
34
+ @blackhole_bucket.should_not_receive(:read)
35
+ @blackhole_bucket.should_not_receive(:ls)
36
+
37
+ @bucket_chain.read('stuff/foo').should == 'bar'
38
+ @bucket_chain.ls.should == :some_assets
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,78 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class CallbackCloud < AssetCloud::Base
4
+ bucket :tmp, AssetCloud::MemoryBucket
5
+
6
+
7
+ after_delete :callback_after_delete
8
+ before_delete :callback_before_delete
9
+
10
+ after_write :callback_after_write
11
+ before_write :callback_before_write
12
+ end
13
+
14
+ class MethodRecordingCloud < AssetCloud::Base
15
+ attr_accessor :run_callbacks
16
+
17
+ bucket :tmp, AssetCloud::MemoryBucket
18
+
19
+ before_write :callback_before_write
20
+ after_write :callback_before_write
21
+
22
+
23
+ def method_missing(method, *args)
24
+ @run_callbacks << method.to_sym
25
+ end
26
+ end
27
+
28
+ describe CallbackCloud do
29
+ before { @fs = CallbackCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/') }
30
+
31
+ it "should invoke callbacks after store" do
32
+
33
+ @fs.should_receive(:callback_before_write).with('tmp/file.txt', 'text').and_return(true)
34
+ @fs.should_receive(:callback_after_write).with('tmp/file.txt', 'text').and_return(true)
35
+
36
+
37
+ @fs.write 'tmp/file.txt', 'text'
38
+
39
+ end
40
+
41
+ it "should invoke callbacks after delete" do
42
+
43
+ @fs.should_receive(:callback_before_delete).with('tmp/file.txt').and_return(true)
44
+ @fs.should_receive(:callback_after_delete).with('tmp/file.txt').and_return(true)
45
+
46
+
47
+ @fs.delete 'tmp/file.txt'
48
+ end
49
+
50
+ it "should invoke callbacks even when constructing a new asset" do
51
+ @fs.should_receive(:callback_before_write).with('tmp/file.txt', 'hello').and_return(true)
52
+ @fs.should_receive(:callback_after_write).with('tmp/file.txt', 'hello').and_return(true)
53
+
54
+
55
+ asset = @fs.build('tmp/file.txt')
56
+ asset.value = 'hello'
57
+ asset.store
58
+
59
+ end
60
+
61
+ end
62
+
63
+ describe MethodRecordingCloud do
64
+ before do
65
+ @fs = MethodRecordingCloud.new(File.dirname(__FILE__) + '/files', 'http://assets/')
66
+ @fs.run_callbacks = []
67
+ end
68
+
69
+ it 'should record event when invoked' do
70
+ @fs.write('tmp/file.txt', 'random data')
71
+ @fs.run_callbacks.should == [:callback_before_write, :callback_before_write]
72
+ end
73
+
74
+ it 'should record event when assignment operator is used' do
75
+ @fs['tmp/file.txt'] = 'random data'
76
+ @fs.run_callbacks.should == [:callback_before_write, :callback_before_write]
77
+ end
78
+ end
@@ -0,0 +1,73 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class FileSystemCloud < AssetCloud::Base
4
+ bucket AssetCloud::InvalidBucket
5
+ bucket :products, AssetCloud::FileSystemBucket
6
+ bucket :tmp, AssetCloud::FileSystemBucket
7
+ end
8
+
9
+
10
+ describe FileSystemCloud do
11
+ directory = File.dirname(__FILE__) + '/files'
12
+
13
+ before do
14
+ @fs = FileSystemCloud.new(directory , 'http://assets/files' )
15
+ FileUtils.mkdir_p(directory + '/tmp')
16
+ end
17
+
18
+ after do
19
+ FileUtils.rm_rf(directory + '/tmp')
20
+ end
21
+
22
+ it "should use invalid bucket for random directories" do
23
+ @fs.bucket_for('does-not-exist/file.txt').should be_an_instance_of(AssetCloud::InvalidBucket)
24
+ end
25
+
26
+ it "should use filesystem bucekt for products/ and tmp/ directories" do
27
+ @fs.bucket_for('products/file.txt').should be_an_instance_of(AssetCloud::FileSystemBucket)
28
+ @fs.bucket_for('tmp/file.txt').should be_an_instance_of(AssetCloud::FileSystemBucket)
29
+ end
30
+
31
+ it "should return Asset for existing files" do
32
+ @fs['products/key.txt'].exist?.should == true
33
+ @fs['products/key.txt'].should be_an_instance_of(AssetCloud::Asset)
34
+ end
35
+
36
+ it "should be able to test if a file exists or not" do
37
+ @fs.stat('products/key.txt').exist?.should == true
38
+ @fs.stat('products/key2.txt').exist?.should == false
39
+ end
40
+
41
+ it "should be able to list files" do
42
+ @fs.ls('products').collect(&:key).should == ['products/key.txt']
43
+ end
44
+
45
+ describe 'when modifying file system' do
46
+
47
+ it "should call write after storing an asset" do
48
+ @fs.buckets[:tmp].should_receive(:write).with('tmp/new_file.test', 'hello world').and_return(true)
49
+
50
+ @fs.build('tmp/new_file.test', 'hello world').store
51
+ end
52
+
53
+ it "should be able to create new files" do
54
+ @fs.build('tmp/new_file.test', 'hello world').store
55
+
56
+ @fs.stat('tmp/new_file.test').exist.should == true
57
+ end
58
+
59
+ it "should be able to create new files with simple assignment" do
60
+ @fs['tmp/new_file.test'] = 'hello world'
61
+
62
+ @fs.stat('tmp/new_file.test').exist.should == true
63
+ end
64
+
65
+ it "should create directories as needed" do
66
+ @fs.build('tmp/new_file.test', 'hello world').store
67
+
68
+ @fs['tmp/new_file.test'].exist?.should == true
69
+ @fs['tmp/new_file.test'].value.should == 'hello world'
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1 @@
1
+ value