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