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,39 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
class AssetNotFoundError < StandardError
|
3
|
+
def initialize(key, version=nil)
|
4
|
+
super(version ? "Could not find version #{version} of asset #{key}" : "Could not find asset #{key}")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Bucket
|
9
|
+
attr_reader :name
|
10
|
+
attr_accessor :cloud
|
11
|
+
|
12
|
+
def initialize(cloud, name)
|
13
|
+
@cloud, @name = cloud, name
|
14
|
+
end
|
15
|
+
|
16
|
+
def ls(key = nil)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def read(key)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(key, data)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(key)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
# versioning
|
33
|
+
#
|
34
|
+
# implement #read_version(key, version) and #versions(key) in subclasses
|
35
|
+
def versioned?
|
36
|
+
respond_to?(:read_version)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
class ActiveRecordBucket < AssetCloud::Bucket
|
3
|
+
class_attribute :key_attribute, :value_attribute
|
4
|
+
self.key_attribute = 'key'
|
5
|
+
self.value_attribute = 'value'
|
6
|
+
|
7
|
+
def ls(key=name)
|
8
|
+
col = records.connection.quote_column_name(self.key_attribute)
|
9
|
+
records.all(:conditions => ["#{col} LIKE ?", "#{key}%"]).map do |r|
|
10
|
+
cloud[r.send(self.key_attribute)]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(key)
|
15
|
+
find_record!(key).send(self.value_attribute)
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(key, value)
|
19
|
+
record = records.send("find_or_initialize_by_#{self.key_attribute}", key.to_s)
|
20
|
+
record.send("#{self.value_attribute}=", value)
|
21
|
+
record.save!
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(key)
|
25
|
+
if record = find_record(key)
|
26
|
+
record.destroy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def stat(key)
|
31
|
+
if record = find_record(key)
|
32
|
+
AssetCloud::Metadata.new(true, record.send(self.value_attribute).size, record.created_at, record.updated_at)
|
33
|
+
else
|
34
|
+
AssetCloud::Metadata.new(false)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# override to return @cloud.user.assets or some other ActiveRecord Enumerable
|
41
|
+
# which responds to .connection, .find, etc.
|
42
|
+
#
|
43
|
+
# model must have columns for this class's key_attribute and value_attribute,
|
44
|
+
# plus created_at and updated_at.
|
45
|
+
def records
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_record(key)
|
50
|
+
records.first(:conditions => {self.key_attribute => key.to_s})
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_record!(key)
|
54
|
+
find_record(key) or raise(AssetCloud::AssetNotFoundError, key)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
class BlackholeBucket < Bucket
|
3
|
+
def ls(namespace = nil)
|
4
|
+
[]
|
5
|
+
end
|
6
|
+
|
7
|
+
def read(key)
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def write(key, data)
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete(key)
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def stat(key)
|
20
|
+
Metadata.new(false)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
class BucketChain < Bucket
|
3
|
+
# returns a new Bucket class which writes to each given Bucket
|
4
|
+
# but only uses the first one for reading
|
5
|
+
def self.chain(*klasses)
|
6
|
+
Class.new(self) do
|
7
|
+
attr_reader :chained_buckets
|
8
|
+
define_method 'initialize' do |cloud, name|
|
9
|
+
super(cloud, name)
|
10
|
+
@chained_buckets = klasses.map {|klass| klass.new(cloud,name)}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def ls(key=nil)
|
17
|
+
first_possible_bucket {|b| b.ls(key)}
|
18
|
+
end
|
19
|
+
def read(key)
|
20
|
+
first_possible_bucket {|b| b.read(key)}
|
21
|
+
end
|
22
|
+
def stat(key=nil)
|
23
|
+
first_possible_bucket {|b| b.stat(key)}
|
24
|
+
end
|
25
|
+
def read_version(key, version)
|
26
|
+
first_possible_bucket {|b| b.read_version(key, version)}
|
27
|
+
end
|
28
|
+
def versions(key)
|
29
|
+
first_possible_bucket {|b| b.versions(key)}
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def write(key, data)
|
34
|
+
every_bucket_with_transaction_on_key(key) {|b| b.write(key, data)}
|
35
|
+
end
|
36
|
+
def delete(key)
|
37
|
+
every_bucket_with_transaction_on_key(key) {|b| b.delete(key)}
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_to?(sym)
|
41
|
+
@chained_buckets.any? {|b| b.respond_to?(sym)}
|
42
|
+
end
|
43
|
+
def method_missing(sym, *args)
|
44
|
+
first_possible_bucket {|b| b.send(sym, *args)}
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def first_possible_bucket(&block)
|
50
|
+
@chained_buckets.each do |bucket|
|
51
|
+
begin
|
52
|
+
return yield(bucket)
|
53
|
+
rescue NoMethodError, NotImplementedError => e
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def every_bucket_with_transaction_on_key(key, i=0, &block)
|
60
|
+
return unless bucket = @chained_buckets[i]
|
61
|
+
|
62
|
+
old_value = begin
|
63
|
+
bucket.read(key)
|
64
|
+
rescue AssetCloud::AssetNotFoundError
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
result = yield(bucket)
|
68
|
+
|
69
|
+
begin
|
70
|
+
every_bucket_with_transaction_on_key(key, i+1, &block)
|
71
|
+
return result
|
72
|
+
rescue StandardError => e
|
73
|
+
if old_value
|
74
|
+
bucket.write(key, old_value)
|
75
|
+
else
|
76
|
+
bucket.delete(key)
|
77
|
+
end
|
78
|
+
raise e
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -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 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,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,42 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
|
3
|
+
class MemoryBucket < Bucket
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
@memory = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def ls(prefix=nil)
|
11
|
+
results = []
|
12
|
+
@memory.each do |k,v|
|
13
|
+
results.push(cloud[k]) if prefix.nil? || k.starts_with?(prefix)
|
14
|
+
end
|
15
|
+
results
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(key)
|
19
|
+
raise AssetCloud::AssetNotFoundError, key unless @memory.has_key?(key)
|
20
|
+
@memory[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
@memory.delete(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def write(key, data)
|
28
|
+
@memory[key] = data
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def stat(key)
|
34
|
+
return Metadata.non_existing unless @memory.has_key?(key)
|
35
|
+
|
36
|
+
Metadata.new(true, read(key).size)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AssetCloud
|
2
|
+
|
3
|
+
class VersionedMemoryBucket < MemoryBucket
|
4
|
+
|
5
|
+
def read(key)
|
6
|
+
raise AssetCloud::AssetNotFoundError, key unless @memory.has_key?(key)
|
7
|
+
read_version(key, latest_version(key))
|
8
|
+
end
|
9
|
+
|
10
|
+
def write(key, data)
|
11
|
+
@memory[key] ||= []
|
12
|
+
@memory[key] << data
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_version(key, version)
|
17
|
+
@memory[key][version - 1]
|
18
|
+
end
|
19
|
+
|
20
|
+
def versions(key)
|
21
|
+
(1..latest_version(key)).to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def latest_version(key)
|
27
|
+
@memory[key].size
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'class_inheritable_attributes'
|
2
|
+
|
3
|
+
module AssetCloud
|
4
|
+
module Callbacks
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def callback_methods(*symbols)
|
9
|
+
symbols.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
|
+
self.class_eval code, __FILE__, __LINE__
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute_callbacks(symbol, args)
|
40
|
+
callbacks_for(symbol).each do |callback|
|
41
|
+
|
42
|
+
result = case callback
|
43
|
+
when Symbol
|
44
|
+
self.send(callback, *args)
|
45
|
+
when Proc, Method
|
46
|
+
callback.call(self, *args)
|
47
|
+
else
|
48
|
+
if callback.respond_to?(method)
|
49
|
+
callback.send(method, self, *args)
|
50
|
+
else
|
51
|
+
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."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
return false if result == false
|
55
|
+
end
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def callbacks_for(symbol)
|
60
|
+
self.class.send(symbol) || []
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|