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