asset_cloud 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +13 -0
  6. data/Rakefile +24 -0
  7. data/asset_cloud.gemspec +24 -0
  8. data/lib/asset_cloud.rb +54 -0
  9. data/lib/asset_cloud/asset.rb +187 -0
  10. data/lib/asset_cloud/asset_extension.rb +42 -0
  11. data/lib/asset_cloud/base.rb +247 -0
  12. data/lib/asset_cloud/bucket.rb +39 -0
  13. data/lib/asset_cloud/buckets/active_record_bucket.rb +57 -0
  14. data/lib/asset_cloud/buckets/blackhole_bucket.rb +23 -0
  15. data/lib/asset_cloud/buckets/bucket_chain.rb +84 -0
  16. data/lib/asset_cloud/buckets/file_system_bucket.rb +79 -0
  17. data/lib/asset_cloud/buckets/invalid_bucket.rb +28 -0
  18. data/lib/asset_cloud/buckets/memory_bucket.rb +42 -0
  19. data/lib/asset_cloud/buckets/versioned_memory_bucket.rb +33 -0
  20. data/lib/asset_cloud/callbacks.rb +63 -0
  21. data/lib/asset_cloud/free_key_locator.rb +28 -0
  22. data/lib/asset_cloud/metadata.rb +29 -0
  23. data/lib/asset_cloud/validations.rb +52 -0
  24. data/spec/active_record_bucket_spec.rb +95 -0
  25. data/spec/asset_extension_spec.rb +103 -0
  26. data/spec/asset_spec.rb +177 -0
  27. data/spec/base_spec.rb +114 -0
  28. data/spec/blackhole_bucket_spec.rb +41 -0
  29. data/spec/bucket_chain_spec.rb +158 -0
  30. data/spec/callbacks_spec.rb +125 -0
  31. data/spec/file_system_spec.rb +74 -0
  32. data/spec/files/products/key.txt +1 -0
  33. data/spec/files/versioned_stuff/foo +1 -0
  34. data/spec/find_free_key_spec.rb +39 -0
  35. data/spec/memory_bucket_spec.rb +52 -0
  36. data/spec/spec_helper.rb +5 -0
  37. data/spec/validations_spec.rb +53 -0
  38. data/spec/versioned_memory_bucket_spec.rb +36 -0
  39. 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