blobby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4a0b96e9f498791cc7df4c6dfe2a16fe3333495f
4
+ data.tar.gz: c45aa9cff2b917fed75bb174a7ca02482b39ff4e
5
+ SHA512:
6
+ metadata.gz: 6e1643a9675acfdfa5a64ea97a8d1e7fd98557f150b243dd75838be5a5ddbe9a7b8e8a312b5c4560b0097eef0f99423c94a1a5e359e5b5b7b7f3365d5c71fc61
7
+ data.tar.gz: 5a010f2432a9a6dd3caa22e5eed35b8b6986180c5b6ace5ca2d49c4b2f52d9d1100a8965913c4d693d6464d49f8c3c9a86cf3598526755ba86f15e4a38ea5b58
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rbenv-*
6
+ .ruby-version
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,47 @@
1
+ Eval:
2
+ Exclude:
3
+ - "Rakefile"
4
+
5
+ Metrics/AbcSize:
6
+ Enabled: false
7
+
8
+ Metrics/LineLength:
9
+ Max: 120
10
+
11
+ Metrics/MethodLength:
12
+ Max: 30
13
+
14
+ Style/ClassAndModuleChildren:
15
+ EnforcedStyle: nested
16
+ Exclude:
17
+ - "spec/**/*"
18
+
19
+ Style/Documentation:
20
+ Exclude:
21
+ - "spec/**/*"
22
+
23
+ Style/EmptyLinesAroundBlockBody:
24
+ Enabled: false
25
+
26
+ Style/EmptyLinesAroundClassBody:
27
+ EnforcedStyle: empty_lines
28
+
29
+ Style/EmptyLinesAroundModuleBody:
30
+ Enabled: false
31
+
32
+ Style/Encoding:
33
+ EnforcedStyle: when_needed
34
+ Enabled: true
35
+
36
+ Style/FileName:
37
+ Exclude:
38
+ - "bin/*"
39
+
40
+ Style/HashSyntax:
41
+ EnforcedStyle: hash_rockets
42
+
43
+ Style/StringLiterals:
44
+ EnforcedStyle: double_quotes
45
+
46
+ Style/WordArray:
47
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.5
5
+ - 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Runtime dependencies in blobby.gemspec
4
+ gemspec
5
+
6
+ # Development/test dependencies below
7
+ gem "rake", "~> 10.0"
8
+ gem "rspec", "~> 3.1"
9
+ gem "sinatra"
10
+ gem "sham_rack", "~> 1.3.5"
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Blobby
2
+
3
+ [![Build Status](https://travis-ci.org/realestate-com-au/blobby.svg?branch=master)](https://travis-ci.org/realestate-com-au/blobby)
4
+
5
+ This gem provides a standard interface for storing big chunks of data.
6
+
7
+ ## Usage
8
+
9
+ It supports popular BLOBs operations such as reading:
10
+
11
+ store["key"].read
12
+
13
+ writing:
14
+
15
+ store["key"].write("some content")
16
+ store["key"].write(File.open("kitty.png"))
17
+
18
+ checking for existance:
19
+
20
+ store["key"].exists?
21
+
22
+ and even deleting:
23
+
24
+ store["key"].delete
25
+
26
+ This gem provides several "store" implementations:
27
+
28
+ # on disk
29
+ Blobby::FilesystemStore.new("/big/data")
30
+
31
+ # in memory
32
+ Blobby::InMemoryStore.new
33
+
34
+ # generic HTTP
35
+ Blobby::HttpStore.new("http://attachment-store/objects")
36
+
37
+ # fake success
38
+ Blobby::FakeSuccessStore.new
39
+
40
+ Other gems provide additional implementations:
41
+
42
+ # gem "blobby-s3"
43
+ Blobby::S3Store.new("mybucket")
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler"
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ require "rspec/core/rake_task"
8
+
9
+ task "default" => "spec"
10
+
11
+ RSpec::Core::RakeTask.new(:spec) do |t|
12
+ t.rspec_opts = ["--colour"]
13
+ end
data/blobby.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
4
+ require "blobby/version"
5
+
6
+ Gem::Specification.new do |gem|
7
+
8
+ gem.authors = ["Mike Williams"]
9
+ gem.email = ["mdub@dogbiscuit.org"]
10
+ gem.summary = "Various ways of storing BLOBs"
11
+ gem.homepage = "https://github.com/realestate-com.au/blobby"
12
+
13
+ gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.name = "blobby"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Blobby::VERSION
19
+
20
+ end
@@ -0,0 +1,64 @@
1
+ module Blobby
2
+
3
+ # Compose a number of stores.
4
+ #
5
+ # Writes go to all stores. Reads use the first store to respond.
6
+ #
7
+ class CompositeStore
8
+
9
+ def initialize(stores)
10
+ @stores = stores
11
+ end
12
+
13
+ def [](key)
14
+ KeyConstraint.must_allow!(key)
15
+ objects = stores.map { |store| store[key] }
16
+ StoredObject.new(objects)
17
+ end
18
+
19
+ def available?
20
+ stores.all?(&:available?)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :stores
26
+
27
+ class StoredObject
28
+
29
+ def initialize(objects)
30
+ @objects = objects
31
+ end
32
+
33
+ def exists?
34
+ objects.any?(&:exists?)
35
+ end
36
+
37
+ def read(&block)
38
+ objects.each do |o|
39
+ return o.read(&block) if o.exists?
40
+ end
41
+ nil
42
+ end
43
+
44
+ def write(content)
45
+ content = content.read if content.respond_to?(:read)
46
+ objects.each do |o|
47
+ o.write(content)
48
+ end
49
+ nil
50
+ end
51
+
52
+ def delete
53
+ objects.all?(&:delete)
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :objects
59
+
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,52 @@
1
+ require "blobby/key_constraint"
2
+
3
+ module Blobby
4
+
5
+ # A BLOB store that is always successful.
6
+ #
7
+ class FakeSuccessStore
8
+
9
+ def available?
10
+ true
11
+ end
12
+
13
+ def [](key)
14
+ KeyConstraint.must_allow!(key)
15
+ StoredObject.new
16
+ end
17
+
18
+ class StoredObject
19
+
20
+ def exists?
21
+ true
22
+ end
23
+
24
+ def read
25
+ image_path = Pathname(File.dirname(__FILE__)) + "placeholder.png"
26
+ image_path.open("rb") do |io|
27
+ if block_given?
28
+ while chunk = io.read(512)
29
+ yield chunk
30
+ end
31
+ nil
32
+ else
33
+ io.read
34
+ end
35
+ end
36
+ rescue Errno::ENOENT
37
+ nil
38
+ end
39
+
40
+ def write(_content)
41
+ nil
42
+ end
43
+
44
+ def delete
45
+ true
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,133 @@
1
+ require "blobby/key_constraint"
2
+ require "fileutils"
3
+ require "forwardable"
4
+ require "pathname"
5
+ require "tempfile"
6
+
7
+ module Blobby
8
+
9
+ # A BLOB store backed by a file-system.
10
+ #
11
+ class FilesystemStore
12
+
13
+ def initialize(dir, options = {}, &sharding_strategy)
14
+ @dir = Pathname(dir)
15
+ @umask = options[:umask] || File.umask
16
+ @sharding_strategy = sharding_strategy || noop_sharding_strategy
17
+ end
18
+
19
+ attr_reader :dir
20
+ attr_reader :umask
21
+
22
+ def available?
23
+ dir.directory? && dir.readable? && dir.writable?
24
+ end
25
+
26
+ def [](key)
27
+ KeyConstraint.must_allow!(key)
28
+ relative_path = @sharding_strategy.call(key)
29
+ StoredObject.new(dir + relative_path, umask)
30
+ end
31
+
32
+ private
33
+
34
+ def noop_sharding_strategy
35
+ ->(key) { key }
36
+ end
37
+
38
+ class StoredObject
39
+
40
+ def initialize(path, umask)
41
+ @path = path
42
+ @umask = umask
43
+ end
44
+
45
+ extend Forwardable
46
+
47
+ def_delegator :@path, :exist?, :exists?
48
+
49
+ def read
50
+ @path.open("rb") do |io|
51
+ if block_given?
52
+ while chunk = io.read(512)
53
+ yield chunk
54
+ end
55
+ nil
56
+ else
57
+ io.read
58
+ end
59
+ end
60
+ rescue Errno::ENOENT
61
+ nil
62
+ end
63
+
64
+ def write(content)
65
+ atomic_create(@path) do |out|
66
+ if content.respond_to?(:read)
67
+ FileUtils.copy_stream(content, out)
68
+ else
69
+ out << content
70
+ end
71
+ end
72
+ nil
73
+ end
74
+
75
+ def delete
76
+ !!FileUtils.rm(@path)
77
+ rescue Errno::ENOENT
78
+ false
79
+ end
80
+
81
+ private
82
+
83
+ def apply_umask(mode)
84
+ mode & ~@umask
85
+ end
86
+
87
+ def using_default_umask?
88
+ @umask == File.umask
89
+ end
90
+
91
+ RAND_MAX = ("F" * 10).to_i(16)
92
+
93
+ def tmp_name
94
+ sprintf("tmp-%X", rand(RAND_MAX))
95
+ end
96
+
97
+ def atomic_create(store_path)
98
+ store_dir = store_path.parent
99
+ tmp_path = store_dir + tmp_name
100
+
101
+ tmp = nil
102
+ begin
103
+ tmp = tmp_path.open(File::CREAT | File::EXCL | File::WRONLY, 0666)
104
+ tmp.binmode
105
+ rescue Errno::ENOENT => e
106
+ FileUtils.mkdir_p(store_dir.to_s, :mode => apply_umask(0777))
107
+ retry
108
+ end
109
+
110
+ begin
111
+ yield tmp
112
+ tmp.chmod(apply_umask(0666)) unless using_default_umask?
113
+ ensure
114
+ tmp.close
115
+ end
116
+
117
+ first_try = true
118
+ begin
119
+ tmp_path.rename(store_path)
120
+ rescue Errno::ESTALE => e
121
+ raise unless first_try
122
+ first_try = false
123
+ now = Time.now
124
+ File.utime(now, now, store_dir.to_s)
125
+ retry
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+
133
+ end
@@ -0,0 +1,135 @@
1
+ require "blobby/key_constraint"
2
+ require "net/http"
3
+
4
+ module Blobby
5
+
6
+ # A BLOB store backed by HTTP.
7
+ #
8
+ class HttpStore
9
+
10
+ def initialize(base_url, options = {})
11
+ @base_url = base_url
12
+ @base_url += "/" unless @base_url.end_with?("/")
13
+ @max_retries = options.fetch(:max_retries, 2)
14
+ end
15
+
16
+ attr_reader :base_url
17
+ attr_reader :max_retries
18
+
19
+ def available?
20
+ with_http_connection do
21
+ true
22
+ end
23
+ rescue
24
+ false
25
+ end
26
+
27
+ def [](key)
28
+ KeyConstraint.must_allow!(key)
29
+ StoredObject.new(self, key)
30
+ end
31
+
32
+ def base_uri
33
+ URI(base_url)
34
+ end
35
+
36
+ def with_http_connection
37
+ remaining_retry_intervals = retry_intervals(max_retries)
38
+ begin
39
+ Net::HTTP.start(base_uri.host, base_uri.port) do |http|
40
+ yield http, base_uri.path
41
+ end
42
+ rescue *retryable_exceptions => e
43
+ raise e if remaining_retry_intervals.empty?
44
+ sleep(remaining_retry_intervals.shift) && retry
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def retryable_exceptions
51
+ [EOFError, Errno::ECONNRESET]
52
+ end
53
+
54
+ def retry_intervals(n)
55
+ # exponential backoff: [0.5, 1, 2, 4, 8, ...]
56
+ scaling_factor = (0.5 + Kernel.rand * 0.1) # a little random avoids throbbing
57
+ Array.new(n) { |i| (2**i) * scaling_factor }
58
+ end
59
+
60
+ class StoredObject
61
+
62
+ def initialize(store, key)
63
+ @store = store
64
+ @key = key
65
+ end
66
+
67
+ attr_reader :key
68
+
69
+ def exists?
70
+ with_http_connection do |http, path|
71
+ response = http.head(path)
72
+ response.code == "200"
73
+ end
74
+ end
75
+
76
+ def read(&block)
77
+ with_http_connection do |http, path|
78
+ http.request_get(path) do |response|
79
+ case response
80
+ when Net::HTTPNotFound then
81
+ return nil
82
+ when Net::HTTPSuccess then
83
+ if block_given?
84
+ response.read_body(&block)
85
+ return nil
86
+ else
87
+ return response.read_body
88
+ end
89
+ end
90
+ response.error!
91
+ end
92
+ end
93
+ end
94
+
95
+ def write(content)
96
+ content = content.read if content.respond_to?(:read)
97
+ with_http_connection do |http, path|
98
+ put = Net::HTTP::Put.new(path)
99
+ put.body = content
100
+ put["Content-Type"] = "application/octet-stream"
101
+ response = http.request(put)
102
+ response.error! unless response.is_a?(Net::HTTPSuccess)
103
+ true
104
+ end
105
+ nil
106
+ end
107
+
108
+ def delete
109
+ with_http_connection do |http, path|
110
+ delete = Net::HTTP::Delete.new(path)
111
+ response = http.request(delete)
112
+ case response
113
+ when Net::HTTPSuccess then
114
+ true
115
+ when Net::HTTPNotFound then
116
+ false
117
+ else
118
+ response.error!
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def with_http_connection
126
+ @store.with_http_connection do |http, base_path|
127
+ yield http, base_path + key
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ end
134
+
135
+ end