fragmenter 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ Gemfile.lock
6
+ doc/
7
+ bin/
8
+ vendor/ruby
9
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,3 @@
1
+ # 0.5.0
2
+
3
+ * Initial release!
@@ -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.
@@ -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.
@@ -0,0 +1,7 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new :spec
6
+
7
+ task :default => :spec
@@ -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
@@ -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,3 @@
1
+ module Fragmenter
2
+ VERSION = '0.5.0'
3
+ 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
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