fragmenter 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.
- data/.gitignore +9 -0
- data/Gemfile +2 -0
- data/HISTORY.md +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/Rakefile +7 -0
- data/fragmenter.gemspec +21 -0
- data/lib/fragmenter.rb +37 -0
- data/lib/fragmenter/base.rb +27 -0
- data/lib/fragmenter/fragment.rb +50 -0
- data/lib/fragmenter/redis.rb +102 -0
- data/lib/fragmenter/version.rb +3 -0
- data/spec/fragmenter/base_spec.rb +43 -0
- data/spec/fragmenter/fragment_spec.rb +58 -0
- data/spec/fragmenter/redis_spec.rb +153 -0
- data/spec/fragmenter_spec.rb +68 -0
- data/spec/spec_helper.rb +0 -0
- metadata +105 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/HISTORY.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Dscout, Inc
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Fragmenter
|
2
|
+
|
3
|
+
Fragmenter stores and rebuilds binary data in a distributed fashion. The only
|
4
|
+
engine currently provided is Redis.
|
5
|
+
|
6
|
+
## Why Fragments?
|
7
|
+
|
8
|
+
It alleviates the problems posed by uploading large blocks of data from slow
|
9
|
+
clients, notably mobile apps, by allowing the device to send multiple smaller
|
10
|
+
blocks of data independently. Once all of the smaller blocks have been received
|
11
|
+
they can quickly be rebuilt into the original file on the server.
|
12
|
+
|
13
|
+
## Requirements
|
14
|
+
|
15
|
+
Fragmenter is tested on Ruby 1.9.3, but any ruby implementation with 1.9 syntax
|
16
|
+
should be supported.
|
17
|
+
|
18
|
+
Redis 2.0 or greater is required and version 2.6 is recommended.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
$ gem install fragmenter
|
23
|
+
|
24
|
+
## Getting Started
|
25
|
+
|
26
|
+
### Configuration
|
27
|
+
|
28
|
+
Fragmenter.configure do |config|
|
29
|
+
config.redis = $redis
|
30
|
+
config.logger = Rails.logger
|
31
|
+
config.expiration = 2.days.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
### Usage
|
35
|
+
|
36
|
+
fragmenter = Fragmenter::Base.new(record)
|
37
|
+
|
38
|
+
fragmenter.store(binary_data, number: 1, total: 12, content_type: 'image/jpeg')
|
39
|
+
fragmenter.complete? # => false
|
40
|
+
|
41
|
+
fragmenter.store(binary_data, number: 12, total: 12, content_type: 'image/jpeg')
|
42
|
+
fragmenter.complete? # => true
|
43
|
+
|
44
|
+
rebuilt = fragmenter.rebuild # => binary data
|
45
|
+
fragmenter.clean!
|
46
|
+
|
47
|
+
More detailed examples will be added soon.
|
48
|
+
|
49
|
+
## License
|
50
|
+
|
51
|
+
Please see LICENSE for licensing details.
|
data/Rakefile
ADDED
data/fragmenter.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fragmenter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'fragmenter'
|
8
|
+
gem.version = Fragmenter::VERSION
|
9
|
+
gem.authors = ['Parker Selbert']
|
10
|
+
gem.email = ['parker@sorentwo.com']
|
11
|
+
gem.description = %q{Fragmentize and rebuild data}
|
12
|
+
gem.summary = %q{Fragmentize and rebuild data}
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
16
|
+
gem.require_paths = ['lib']
|
17
|
+
|
18
|
+
gem.add_dependency 'redis', '~> 3.0.0'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rspec', '~> 2.11.0'
|
21
|
+
end
|
data/lib/fragmenter.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'redis'
|
3
|
+
require 'fragmenter/version'
|
4
|
+
require 'fragmenter/base'
|
5
|
+
require 'fragmenter/redis'
|
6
|
+
|
7
|
+
module Fragmenter
|
8
|
+
def self.configure(&block)
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.logger
|
13
|
+
@logger ||= Logger.new(STDOUT).tap do |logger|
|
14
|
+
logger.level = Logger::INFO
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.logger=(logger)
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.redis
|
23
|
+
@redis ||= ::Redis.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.redis=(redis)
|
27
|
+
@redis = redis
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.expiration=(expiration)
|
31
|
+
@expiration = expiration
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.expiration
|
35
|
+
@expiration || 60 * 60 * 24
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Fragmenter
|
2
|
+
class Base
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
attr_reader :object, :engine
|
6
|
+
|
7
|
+
delegate clean!: :engine
|
8
|
+
delegate complete?: :engine
|
9
|
+
delegate fragments: :engine
|
10
|
+
delegate meta: :engine
|
11
|
+
delegate rebuild: :engine
|
12
|
+
delegate store: :engine
|
13
|
+
|
14
|
+
def initialize(object, engine_class = Fragmenter::Redis)
|
15
|
+
@object = object
|
16
|
+
@engine = engine_class.new(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def key
|
20
|
+
[object.class.to_s.downcase, object.id].join('-')
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_json
|
24
|
+
engine.meta.merge('fragments' => engine.fragments)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Fragmenter
|
2
|
+
class Fragment
|
3
|
+
attr_reader :blob, :options
|
4
|
+
|
5
|
+
def initialize(blob, options)
|
6
|
+
@blob = blob
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def number
|
11
|
+
@number ||= options.fetch(:number, 1).to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
def total
|
15
|
+
@total ||= options.fetch(:total, 1).to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def content_type
|
19
|
+
@content_type ||= options[:content_type] || 'application/octet-stream'
|
20
|
+
end
|
21
|
+
|
22
|
+
def padded_number
|
23
|
+
digits = total.to_s.length
|
24
|
+
|
25
|
+
"%0#{digits}d" % number.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?
|
29
|
+
valid_blob? && valid_number? && valid_total? && valid_content_type?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def valid_blob?
|
35
|
+
blob.size > 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def valid_number?
|
39
|
+
number.kind_of?(Integer) && number > 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_total?
|
43
|
+
total.kind_of?(Integer) && total > 0 && total >= number
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_content_type?
|
47
|
+
content_type =~ /\w+\/\w+/
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'fragmenter/fragment'
|
2
|
+
|
3
|
+
module Fragmenter
|
4
|
+
class Redis
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
delegate expiration: Fragmenter
|
8
|
+
delegate logger: Fragmenter
|
9
|
+
delegate redis: Fragmenter
|
10
|
+
|
11
|
+
attr_reader :fragmenter
|
12
|
+
|
13
|
+
def initialize(fragmenter)
|
14
|
+
@fragmenter = fragmenter
|
15
|
+
end
|
16
|
+
|
17
|
+
def store_key
|
18
|
+
fragmenter.key
|
19
|
+
end
|
20
|
+
|
21
|
+
def meta_key
|
22
|
+
[store_key, 'options'].join('-')
|
23
|
+
end
|
24
|
+
|
25
|
+
def store(blob, options)
|
26
|
+
fragment = Fragmenter::Fragment.new(blob, options)
|
27
|
+
|
28
|
+
if fragment.valid?
|
29
|
+
persist_fragment(fragment)
|
30
|
+
else
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def meta
|
36
|
+
redis.hgetall meta_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def fragments
|
40
|
+
redis.hkeys(store_key).sort
|
41
|
+
end
|
42
|
+
|
43
|
+
def complete?
|
44
|
+
redis.hlen(store_key).to_s == redis.hget(meta_key, :total)
|
45
|
+
end
|
46
|
+
|
47
|
+
def rebuild
|
48
|
+
benchmark_rebuilding do
|
49
|
+
redis.hmget(store_key, *fragments).join('')
|
50
|
+
end
|
51
|
+
rescue ::Redis::CommandError
|
52
|
+
log 'Failure rebuilding, most likely there are no fragments to rebuild'
|
53
|
+
|
54
|
+
''
|
55
|
+
end
|
56
|
+
|
57
|
+
def clean!
|
58
|
+
redis.del store_key, meta_key
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def log(message)
|
64
|
+
logger.info "Fragmenter: #{message}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def persist_fragment(fragment)
|
68
|
+
benchmark_persistence(fragment) do
|
69
|
+
redis.multi do
|
70
|
+
redis.hset store_key, fragment.padded_number, fragment.blob
|
71
|
+
redis.hset meta_key, :content_type, fragment.content_type
|
72
|
+
redis.hset meta_key, :total, fragment.total
|
73
|
+
|
74
|
+
redis.expire store_key, expiration
|
75
|
+
redis.expire meta_key, expiration
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def benchmark_persistence(fragment, &block)
|
81
|
+
log %(Storing #{fragment.number}/#{fragment.total}...)
|
82
|
+
start_time = Time.now
|
83
|
+
|
84
|
+
yield
|
85
|
+
|
86
|
+
end_time = Time.now
|
87
|
+
log %(Stored (#{end_time - start_time}) #{fragment.number}/#{fragment.total} #{fragment.blob.size} bytes)
|
88
|
+
end
|
89
|
+
|
90
|
+
def benchmark_rebuilding(&block)
|
91
|
+
log %(Rebuilding #{fragments.length} fragments...)
|
92
|
+
start_time = Time.now
|
93
|
+
|
94
|
+
rebuilt = yield
|
95
|
+
|
96
|
+
end_time = Time.now
|
97
|
+
log %(Rebuilt (#{end_time - start_time}) #{fragments.length} fragments #{rebuilt.size} bytes)
|
98
|
+
|
99
|
+
rebuilt
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'fragmenter/base'
|
2
|
+
|
3
|
+
describe Fragmenter::Base do
|
4
|
+
let(:object) { mock('object', id: 1001) }
|
5
|
+
let(:engine_class) { mock('engine_class', new: engine) }
|
6
|
+
let(:engine) { mock('engine') }
|
7
|
+
|
8
|
+
subject { described_class.new(object, engine_class) }
|
9
|
+
|
10
|
+
describe '#key' do
|
11
|
+
it 'composes a key from the object class and id value' do
|
12
|
+
subject.key.should match(/[a-z]+-\d+/)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'engine delegation' do
|
17
|
+
let(:blob) { '0101' }
|
18
|
+
let(:headers) { {} }
|
19
|
+
|
20
|
+
it 'delegates #store to the storage engine' do
|
21
|
+
engine.should_receive(:store).with(blob, headers)
|
22
|
+
|
23
|
+
subject.store(blob, headers)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'delegates #fragments to the storage engine' do
|
27
|
+
engine.should_receive(:fragments)
|
28
|
+
subject.fragments
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#as_json' do
|
33
|
+
it 'merges the stored meta and fragments' do
|
34
|
+
engine.stub('meta' => { 'content_type' => 'application/octet-stream' },
|
35
|
+
'fragments' => ['1', '2'])
|
36
|
+
|
37
|
+
subject.as_json.tap do |json|
|
38
|
+
json.should have_key('content_type')
|
39
|
+
json.should have_key('fragments')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'fragmenter/fragment'
|
2
|
+
|
3
|
+
describe Fragmenter::Fragment do
|
4
|
+
let(:blob) { '1010101' }
|
5
|
+
|
6
|
+
describe '#number' do
|
7
|
+
it 'defaults the number to 1' do
|
8
|
+
described_class.new(blob, {}).number.should == 1
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#total' do
|
13
|
+
it 'defaults the total to 1' do
|
14
|
+
described_class.new(blob, {}).total.should == 1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#content_type' do
|
19
|
+
it 'defaults the content_type to a binary format' do
|
20
|
+
described_class.new(blob, {}).content_type.should == 'application/octet-stream'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#padded_number' do
|
25
|
+
it 'zero pads the number with as many zeros as the total has places' do
|
26
|
+
described_class.new(blob, number: 1, total: 2000).padded_number.should == '0001'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#valid?' do
|
31
|
+
it 'is valid with a complete blob and sensible options' do
|
32
|
+
described_class.new(blob, number: 1, total: 2).should be_valid
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is not valid with an empty blob' do
|
36
|
+
described_class.new('', number: 1, total: 2).should_not be_valid
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'is not valid without an integer part number greater than 1' do
|
40
|
+
described_class.new(blob, number: -1, total: 2).should_not be_valid
|
41
|
+
described_class.new(blob, number: 'one', total: 2).should_not be_valid
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'is not valid without an integer part total' do
|
45
|
+
described_class.new(blob, number: 1, total: -2).should_not be_valid
|
46
|
+
described_class.new(blob, number: 1, total: 'two').should_not be_valid
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'is not valid when the number is greater the total' do
|
50
|
+
described_class.new(blob, number: 2, total: 1).should_not be_valid
|
51
|
+
described_class.new(blob, number: 2, total: 2).should be_valid
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'is not valid without a content type resembling a mime type' do
|
55
|
+
described_class.new(blob, content_type: 'jpg').should_not be_valid
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'fragmenter'
|
2
|
+
require 'fragmenter/redis'
|
3
|
+
|
4
|
+
describe Fragmenter::Redis do
|
5
|
+
let(:blob_1) { '00010110' }
|
6
|
+
let(:blob_2) { '11101110' }
|
7
|
+
let(:fragmenter) { mock(:fragmenter, key: 'abcdefg') }
|
8
|
+
let(:redis) { Fragmenter.redis }
|
9
|
+
|
10
|
+
subject(:engine) { described_class.new(fragmenter) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
Fragmenter.logger = Logger.new('/dev/null')
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
redis.del engine.store_key, engine.meta_key
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#store_key' do
|
21
|
+
it 'delegates store key to the fragmenter key' do
|
22
|
+
engine.store_key.should == fragmenter.key
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#meta_key' do
|
27
|
+
it 'combines the base store key with options' do
|
28
|
+
engine.meta_key.should include(fragmenter.key)
|
29
|
+
engine.meta_key.should include('options')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#store' do
|
34
|
+
it 'does not store empty blobs' do
|
35
|
+
engine.store('', number: 1, total: 2).should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'writes the provided blob to the fragmenter key and the provided number' do
|
39
|
+
engine.store(blob_1, number: 1, total: 48)
|
40
|
+
|
41
|
+
redis.hget(fragmenter.key, '01').should == blob_1
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'overwrites existing data at the key + number location' do
|
45
|
+
engine.store(blob_1, number: 1, total: 48)
|
46
|
+
engine.store(blob_2, number: 1, total: 48)
|
47
|
+
|
48
|
+
redis.hget(fragmenter.key, '01').should == blob_2
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'sets the fragment to expire' do
|
52
|
+
engine.store(blob_1, number: 1, total: 48)
|
53
|
+
|
54
|
+
redis.ttl(engine.store_key).should == Fragmenter.expiration
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'stores meta-data in a spearate key' do
|
58
|
+
subject.store(blob_1, number: 1, total: 48)
|
59
|
+
|
60
|
+
redis.exists(engine.meta_key).should be_true
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'sets the meta to expire' do
|
64
|
+
engine.store(blob_1, number: 1, total: 48)
|
65
|
+
|
66
|
+
redis.ttl(engine.meta_key).should == Fragmenter.expiration
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'defaults the stored content-type to application/octet-stream' do
|
70
|
+
subject.store(blob_1, number: 1, total: 48)
|
71
|
+
|
72
|
+
redis.hget(engine.meta_key, :content_type).should == 'application/octet-stream'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#meta' do
|
77
|
+
it 'returns an empty hash when nothing has been stored' do
|
78
|
+
engine.meta.should == {}
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns the accumulated metadata when data has been stored' do
|
82
|
+
engine.store(blob_1, content_type: 'image/jpeg', number: 1, total: 2)
|
83
|
+
engine.meta.should eq(
|
84
|
+
'content_type' => 'image/jpeg',
|
85
|
+
'total' => '2'
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#fragments' do
|
91
|
+
context 'without any fragments' do
|
92
|
+
it 'returns an empty array' do
|
93
|
+
engine.fragments.should == []
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'when fragments have been stored' do
|
98
|
+
before do
|
99
|
+
engine.store(blob_2, number: 3, total: 30)
|
100
|
+
engine.store(blob_1, number: 1, total: 30)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns an array of the stored fragment indecies' do
|
104
|
+
engine.fragments.should == ['01', '03']
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#complete?' do
|
110
|
+
it 'is incomplete if the number of stored fragments does not match the total' do
|
111
|
+
engine.store(blob_1, number: 1, total: 2)
|
112
|
+
|
113
|
+
engine.should_not be_complete
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'is complete if the stored fragments matches all values between 1 and the total' do
|
117
|
+
engine.store(blob_1, number: 1, total: 2)
|
118
|
+
engine.store(blob_2, number: 2, total: 2)
|
119
|
+
|
120
|
+
engine.should be_complete
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#rebuild' do
|
125
|
+
before do
|
126
|
+
engine.store(blob_1, number: 1, total: 2)
|
127
|
+
engine.store(blob_2, number: 2, total: 2)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'returns the aggregated values from all stored fragments' do
|
131
|
+
engine.rebuild.should == [blob_1, blob_2].join('')
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'returns nothing when no fragments are present' do
|
135
|
+
redis.del engine.store_key
|
136
|
+
|
137
|
+
engine.rebuild.should == ''
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#clean!' do
|
142
|
+
before do
|
143
|
+
engine.store(blob_1, number: 1, total: 2)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'deletes the storage and meta data' do
|
147
|
+
engine.clean!
|
148
|
+
|
149
|
+
redis.exists(engine.meta_key).should be_false
|
150
|
+
redis.exists(engine.store_key).should be_false
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'fragmenter'
|
2
|
+
|
3
|
+
describe Fragmenter do
|
4
|
+
after do
|
5
|
+
Fragmenter.redis = nil
|
6
|
+
Fragmenter.logger = nil
|
7
|
+
Fragmenter.expiration = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.logger' do
|
11
|
+
it 'attempts to instantiate a standard logger to STDOUT' do
|
12
|
+
Fragmenter.logger.should be_instance_of(Logger)
|
13
|
+
Fragmenter.logger.level.should == Logger::INFO
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.logger=' do
|
18
|
+
it 'stores the logger instance on the module' do
|
19
|
+
logger = mock(:logger)
|
20
|
+
|
21
|
+
Fragmenter.logger = logger
|
22
|
+
Fragmenter.logger.should be(logger)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.redis' do
|
27
|
+
it 'attempts to create a redis connection with default values' do
|
28
|
+
Fragmenter.redis.should be_instance_of(Redis)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '.redis=' do
|
33
|
+
it 'stores the redis instance on the module' do
|
34
|
+
redis = mock(:redis)
|
35
|
+
|
36
|
+
Fragmenter.redis = redis
|
37
|
+
Fragmenter.redis.should be(redis)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '.expiration' do
|
42
|
+
it 'defaults expiration to one day' do
|
43
|
+
Fragmenter.expiration.should == 86400
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '.expiration=' do
|
48
|
+
it 'stoires the expiration value on the module' do
|
49
|
+
Fragmenter.expiration = 10000
|
50
|
+
Fragmenter.expiration.should eq(10000)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '.configure' do
|
55
|
+
let(:redis) { mock(:redis) }
|
56
|
+
let(:logger) { mock(:logger) }
|
57
|
+
|
58
|
+
it 'allows customization via passing a block' do
|
59
|
+
Fragmenter.configure do |config|
|
60
|
+
config.redis = redis
|
61
|
+
config.logger = logger
|
62
|
+
end
|
63
|
+
|
64
|
+
Fragmenter.redis.should == redis
|
65
|
+
Fragmenter.logger.should == logger
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/spec/spec_helper.rb
ADDED
File without changes
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fragmenter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Parker Selbert
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.11.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.11.0
|
46
|
+
description: Fragmentize and rebuild data
|
47
|
+
email:
|
48
|
+
- parker@sorentwo.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- Gemfile
|
55
|
+
- HISTORY.md
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- fragmenter.gemspec
|
60
|
+
- lib/fragmenter.rb
|
61
|
+
- lib/fragmenter/base.rb
|
62
|
+
- lib/fragmenter/fragment.rb
|
63
|
+
- lib/fragmenter/redis.rb
|
64
|
+
- lib/fragmenter/version.rb
|
65
|
+
- spec/fragmenter/base_spec.rb
|
66
|
+
- spec/fragmenter/fragment_spec.rb
|
67
|
+
- spec/fragmenter/redis_spec.rb
|
68
|
+
- spec/fragmenter_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage:
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
hash: -818571163033621587
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
hash: -818571163033621587
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.23
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Fragmentize and rebuild data
|
100
|
+
test_files:
|
101
|
+
- spec/fragmenter/base_spec.rb
|
102
|
+
- spec/fragmenter/fragment_spec.rb
|
103
|
+
- spec/fragmenter/redis_spec.rb
|
104
|
+
- spec/fragmenter_spec.rb
|
105
|
+
- spec/spec_helper.rb
|