fragmenter 0.5.1 → 1.0.0.rc1

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: 0d849a0a1482e71d93c6c90ea5a9336e2ced2be6
4
+ data.tar.gz: 1d87cae0a38cc7dddce3d76a08a5181a7d11587b
5
+ SHA512:
6
+ metadata.gz: 14a4a61707cf76f83ceff137473dd8343b2ec7a1a0f5c645900c78dbb472516a943075cdae551be02efdf2e7e527e87e7a01afe62e21299738d23f04914382e0
7
+ data.tar.gz: 3720a5b353199f1be826f663b758873d0311d0fa4a032a9ebdb99bcfeedcad5059bf4e9b180fea48e2b359dd5726551e6093a89bd718a3bb9b9c6c1b0ebaa47d
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ doc/
7
7
  bin/
8
8
  vendor/ruby
9
9
  tmp
10
+ TODO
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ -I spec/spec_helper.rb
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ services:
6
+ - redis-server
7
+ script: bundle exec rspec spec
data/HISTORY.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 1.0.0.rc1
2
+
3
+ * Add modules for easiy integration with Rails controllers and models.
4
+ * Add validators for fragment storage.
5
+ * Add services to help handle requests.
6
+ * Add a request object that wraps the various resource, fragment, and rack
7
+ request objects.
8
+ * Rename `Fragmenter::Base` to `Fragmenter::Wrapper`.
9
+ * Enhanced documentation so that people can actually see what to do with the
10
+ libary.
11
+
1
12
  # 0.5.1
2
13
 
3
14
  * Initial release!
data/README.md CHANGED
@@ -1,51 +1,195 @@
1
+ [![Build Status](https://travis-ci.org/dscout/fragmenter.png?branch=master)](https://travis-ci.org/dscout/fragmenter)
2
+ [![Code Climate](https://codeclimate.com/github/dscout/fragmenter.png)](https://codeclimate.com/github/dscout/fragmenter)
3
+
1
4
  # Fragmenter
2
5
 
3
- Fragmenter stores and rebuilds binary data in a distributed fashion. The only
4
- engine currently provided is Redis.
6
+ Fragmenter is a library for multipart upload support backed by Redis.
7
+ Fragmenter handles storing multiple parts of a larger binary and rebuilding it
8
+ back into the original after all parts have been stored.
5
9
 
6
- ## Why Fragments?
10
+ ## Why Fragment?
7
11
 
8
12
  It alleviates the problems posed by uploading large blocks of data from slow
9
13
  clients, notably mobile apps, by allowing the device to send multiple smaller
10
14
  blocks of data independently. Once all of the smaller blocks have been received
11
15
  they can quickly be rebuilt into the original file on the server.
12
16
 
17
+ Think of multipart uploading as blocky streaming. Nginx, Rack and Rails all
18
+ make it impossible to stream binary uploads. Breaking them into manageable
19
+ pieces is the simplest workaround.
20
+
21
+ Fragments are intended to be rather small, anywhere from 10-50k depending on
22
+ the underlying data size. There is a balance between connection overhead from
23
+ repeated server calls, being connection error tollerant, and not blocking the
24
+ server from handling other connections.
25
+
13
26
  ## Requirements
14
27
 
15
- Fragmenter is tested on Ruby 1.9.3, but any ruby implementation with 1.9 syntax
28
+ Fragmenter is tested on Ruby 2.0, but any ruby implementation with 1.9 syntax
16
29
  should be supported.
17
30
 
18
31
  Redis 2.0 or greater is required and version 2.6 is recommended.
19
32
 
20
33
  ## Installation
21
34
 
22
- $ gem install fragmenter
35
+ Add this to your Gemfile:
23
36
 
24
- ## Getting Started
37
+ ```ruby
38
+ gem 'fragmenter'
39
+ ```
25
40
 
26
- ### Configuration
41
+ ## Configuration
27
42
 
28
- Fragmenter.configure do |config|
29
- config.redis = $redis
30
- config.logger = Rails.logger
31
- config.expiration = 2.days.to_i
32
- end
43
+ You can configure the following components of `Fragmenter`:
33
44
 
34
- ### Usage
45
+ * **redis** - Redis instance to use for IO. Defaults to a new instance connected to `localhost`.
46
+ * **logger** - Logger instance to write out to. Defaults to `STDOUT` at the `INFO` level.
47
+ * **expiration** - The number of seconds until fragments will expire. Defaults to 86400, or 1 day.
35
48
 
36
- fragmenter = Fragmenter::Base.new(record)
49
+ ```ruby
50
+ Fragmenter.configure do |config|
51
+ config.redis = $redis
52
+ config.logger = Rails.logger
53
+ config.expiration = 2.days.to_i
54
+ end
55
+ ```
37
56
 
38
- fragmenter.store(binary_data, number: 1, total: 12, content_type: 'image/jpeg')
39
- fragmenter.complete? # => false
57
+ ## Using Fragmenter with Rails
40
58
 
41
- fragmenter.store(binary_data, number: 12, total: 12, content_type: 'image/jpeg')
42
- fragmenter.complete? # => true
59
+ However, it is designed to be used from within a Rails controller. Include the
60
+ provided `Fragmenter::Controller` module into any controller you wish to have
61
+ process uploads:
43
62
 
44
- rebuilt = fragmenter.rebuild # => binary data
45
- fragmenter.clean!
63
+ ```ruby
64
+ class UploadControler < ApplicationController
65
+ include Fragmenter::Rails::Controller
46
66
 
47
- More detailed examples will be added soon.
67
+ private
48
68
 
49
- ## License
69
+ def resource
70
+ @resource ||= Avatar.find(:avatar_id)
71
+ end
72
+ end
73
+ ```
74
+
75
+ The module adds methods for handling the GET, PUT, and DELETE requests needed
76
+ for handling fragment uploads. You must define a `resource` method that returns
77
+ an object implementing `fragmenter`. In the example above the `resource` is an
78
+ instance of the `Avatar` model, which could look something like this:
79
+
80
+ ```ruby
81
+ class Avatar < ActiveRecord::Base
82
+ include Fragmenter::Rails::Model
83
+
84
+ def rebuild_fragments
85
+ self.avatar = Fragmenter::DummyIO.new(fragmenter.rebuild).tap do |io|
86
+ io.content_type = fragmenter.meta['content_type']
87
+ end
50
88
 
51
- Please see LICENSE for licensing details.
89
+ save!
90
+ end
91
+ end
92
+ ```
93
+
94
+ You **must** provide a concrete `rebuild_fragments` method that will perform
95
+ rebuilding, saving, persisting etc. Without overriding `rebuild_fragments` a
96
+ `Fragmenter::AbstractMethodError` will be raised when storage is complete and
97
+ it attempts to rebuild.
98
+
99
+ The example above synchronous storage using a mounted CarrierWave style
100
+ uploader. You may want to perform rebuilding with a background worker instead
101
+ to keep response times speedy.
102
+
103
+ After you have configured your routes to map `show`, `update` and `destroy` to
104
+ the uploads controller:
105
+
106
+ ```ruby
107
+ MyApp::Application.routes.draw do
108
+ resource :avatar do
109
+ resource :upload, only: [:show, :update, :destroy]
110
+ end
111
+ end
112
+ ```
113
+
114
+ Then you can start sending `PUT` requests with successive fragments of data.
115
+ Each fragment will be stored uniquely to the parent object, an instance of
116
+ Avatar in this case. For each fragment that is stored the response will be the
117
+ JSON representation of the fragments along with a `200 OK` status code:
118
+
119
+ ```bash
120
+ curl -i
121
+ -X PUT /
122
+ -H 'X-Fragment-Number: 1' /
123
+ -H 'X-Fragment-Total: 2' /
124
+ --data-binary @blob-1 /
125
+ http://example.com/avatar/1/upload
126
+
127
+ #=> HTTP/1.1 200 OK
128
+ #=> { "content_type": "image/jpeg", "fragments": [1], "total": 2 }
129
+ ```
130
+
131
+ When the final part is uploaded the status code will be `202 Accepted` if the
132
+ fragment is valid and can be rebuilt:
133
+
134
+ ```bash
135
+ curl -i
136
+ -X PUT /
137
+ -H 'X-Fragment-Number: 2' /
138
+ -H 'X-Fragment-Total: 2' /
139
+ --data-binary @blob-2 /
140
+ http://example.com/avatar/1/upload
141
+
142
+ #=> HTTP/1.1 202 Accepted
143
+ #=> { "content_type": "image/jpeg", "fragments": [1,2], "total": 2 }
144
+ ```
145
+
146
+ ### Validation
147
+
148
+ Often you will want to be sure that all of the data is being stored without any
149
+ bytes missing. A standard way to handle this is by sending a checksum that is
150
+ verified after transfer. Fragmenter handles checksum matching using a validator
151
+ that verifies each fragment that is uploaded. Validation is handled for any request
152
+ where the `Content-MD5` header has been sent:
153
+
154
+ ```bash
155
+ curl -X PUT /
156
+ -H 'Content-MD5: ceba1b1ffc89e99abb54c1f8ab0c4157' /
157
+ -H 'X-Fragment-Number: 1' /
158
+ -H 'X-Fragment-Total: 1' /
159
+ --data-binary @blob /
160
+ http://example.com/avatar/1/upload
161
+ ```
162
+
163
+ Failure to match the checksum will result in a `422 Unprocessable Entity`
164
+ response with an accompanying message and errors:
165
+
166
+ ```json
167
+ { "message": "Upload of part failed.",
168
+ "errors": [
169
+ "Expected checksum {{expected}} to match {{calculated}}"
170
+ ]
171
+ }
172
+ ```
173
+ As images uploads are a common use-case for fragmented uploading an
174
+ ImageValidator is included, but not one of the default validators. You can
175
+ control with validators are used by overriding the `validators` method within
176
+ the controller:
177
+
178
+ ```ruby
179
+ class AvatarUploader < ApplicationController
180
+ ...
181
+
182
+ private
183
+
184
+ def validators
185
+ super + [ImageValidator, CustomValidator]
186
+ end
187
+ end
188
+ ```
189
+
190
+ To add a custom validator you must add it at some point in the validator chain.
191
+ A validator can be any class that responds to `valid?` with a boolean value and
192
+ provides a list of errors. See the [ImageValidator][1] for an example validator
193
+ that only performs validation when all fragments are complete.
194
+
195
+ [1]:lib/fragmenter/validators/image
data/fragmenter.gemspec CHANGED
@@ -4,20 +4,22 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'fragmenter/version'
5
5
 
6
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.homepage = 'https://github.com/dscout/fragmenter'
7
+ gem.name = 'fragmenter'
8
+ gem.version = Fragmenter::VERSION
9
+ gem.authors = ['Parker Selbert']
10
+ gem.email = ['parker@sorentwo.com']
11
+ gem.homepage = 'https://github.com/dscout/fragmenter'
12
+ gem.description = %q{Fragmentize and rebuild data}
13
+ gem.summary = <<-SUMMARY
14
+ Multipart upload support backed by Redis. Fragmenter handles storing
15
+ multiple parts of a larger binary and rebuilding it back into the original
16
+ after all parts have been stored.
17
+ SUMMARY
15
18
 
16
19
  gem.files = `git ls-files`.split($/)
17
20
  gem.test_files = gem.files.grep(%r{^(spec)/})
18
21
  gem.require_paths = ['lib']
19
22
 
20
23
  gem.add_dependency 'redis', '~> 3.0.0'
21
-
22
- gem.add_development_dependency 'rspec', '~> 2.11.0'
24
+ gem.add_development_dependency 'rspec', '~> 2.14.0'
23
25
  end
data/lib/fragmenter.rb CHANGED
@@ -1,37 +1,44 @@
1
1
  require 'logger'
2
2
  require 'redis'
3
- require 'fragmenter/version'
4
- require 'fragmenter/base'
5
3
  require 'fragmenter/redis'
4
+ require 'fragmenter/version'
5
+ require 'fragmenter/wrapper'
6
+ require 'fragmenter/rails/controller'
7
+ require 'fragmenter/rails/model'
8
+ require 'fragmenter/services/uploader'
9
+ require 'fragmenter/services/storer'
10
+ require 'fragmenter/validators/checksum_validator'
6
11
 
7
12
  module Fragmenter
8
- def self.configure(&block)
9
- yield self
10
- end
13
+ class << self
14
+ def configure(&block)
15
+ yield self
16
+ end
11
17
 
12
- def self.logger
13
- @logger ||= Logger.new(STDOUT).tap do |logger|
14
- logger.level = Logger::INFO
18
+ def logger
19
+ @logger ||= Logger.new(STDOUT).tap do |logger|
20
+ logger.level = Logger::INFO
21
+ end
15
22
  end
16
- end
17
23
 
18
- def self.logger=(logger)
19
- @logger = logger
20
- end
24
+ def logger=(logger)
25
+ @logger = logger
26
+ end
21
27
 
22
- def self.redis
23
- @redis ||= ::Redis.new
24
- end
28
+ def redis
29
+ @redis ||= ::Redis.new
30
+ end
25
31
 
26
- def self.redis=(redis)
27
- @redis = redis
28
- end
32
+ def redis=(redis)
33
+ @redis = redis
34
+ end
29
35
 
30
- def self.expiration=(expiration)
31
- @expiration = expiration
32
- end
36
+ def expiration=(expiration)
37
+ @expiration = expiration
38
+ end
33
39
 
34
- def self.expiration
35
- @expiration || 60 * 60 * 24
40
+ def expiration
41
+ @expiration || 60 * 60 * 24
42
+ end
36
43
  end
37
44
  end
@@ -0,0 +1,50 @@
1
+ module Fragmenter
2
+ module Rails
3
+ module Controller
4
+ def show
5
+ render json: fragmenter.as_json
6
+ end
7
+
8
+ def update
9
+ if uploader.store
10
+ render json: fragmenter.as_json, status: update_status
11
+ else
12
+ render json: {
13
+ message: 'Upload of part failed.', errors: uploader.errors
14
+ }, status: :unprocessable_entity
15
+ end
16
+ end
17
+
18
+ def destroy
19
+ fragmenter.clean!
20
+
21
+ render nothing: true, status: :no_content
22
+ end
23
+
24
+ private
25
+
26
+ def fragmenter
27
+ resource.fragmenter
28
+ end
29
+
30
+ def validators
31
+ [Fragmenter::Validators::ChecksumValidator]
32
+ end
33
+
34
+ def uploader
35
+ @uploader ||= Fragmenter::Services::Uploader.new(
36
+ Fragmenter::Request.new(
37
+ resource: resource,
38
+ fragmenter: fragmenter,
39
+ body: request.body,
40
+ headers: request.headers
41
+ ), validators
42
+ )
43
+ end
44
+
45
+ def update_status
46
+ uploader.complete? ? :accepted : :ok
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+ module Fragmenter
2
+ class AbstractMethodError < StandardError; end
3
+
4
+ module Rails
5
+ module Model
6
+ def fragmenter
7
+ @fragmenter ||= Fragmenter::Wrapper.new(self)
8
+ end
9
+
10
+ def store_fragment(*args)
11
+ fragmenter.store(*args)
12
+ end
13
+
14
+ def rebuild_fragments
15
+ raise Fragmenter::AbstractMethodError.new('This must be overriden on your model')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Fragmenter
2
+ class Request
3
+ attr_accessor :fragmenter, :headers, :resource
4
+
5
+ def initialize(options = {})
6
+ @body = options.fetch(:body, StringIO.new(''))
7
+ @fragmenter = options.fetch(:fragmenter, nil)
8
+ @headers = options.fetch(:headers, {})
9
+ @resource = options.fetch(:resource, nil)
10
+ end
11
+
12
+ def body
13
+ if @body.respond_to?(:read)
14
+ @body.read
15
+ else
16
+ @body
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ require 'fragmenter/request'
2
+
3
+ module Fragmenter
4
+ module Services
5
+ class Storer
6
+ attr_reader :request, :errors
7
+
8
+ def initialize(request)
9
+ @request = request
10
+ @errors = []
11
+ end
12
+
13
+ def store
14
+ stored = fragmenter.store(request.body, extracted_options)
15
+
16
+ unless stored
17
+ errors << 'Unable to store fragment'
18
+ end
19
+
20
+ stored
21
+ end
22
+
23
+ private
24
+
25
+ def fragmenter
26
+ request.fragmenter
27
+ end
28
+
29
+ def extracted_options
30
+ headers = request.headers
31
+
32
+ headers['HTTP_X_FRAGMENT_NUMBER'] ||= headers['HTTP_PART_NUMBER']
33
+ headers['HTTP_X_FRAGMENT_TOTAL'] ||= headers['HTTP_PARTS_TOTAL']
34
+
35
+ { content_type: headers.fetch('CONTENT_TYPE'),
36
+ number: headers.fetch('HTTP_X_FRAGMENT_NUMBER'),
37
+ total: headers.fetch('HTTP_X_FRAGMENT_TOTAL') }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,58 @@
1
+ require 'fragmenter/services/storer'
2
+
3
+ module Fragmenter
4
+ module Services
5
+ class Uploader
6
+ attr_reader :request, :validators
7
+ attr_writer :storer
8
+
9
+ def initialize(request, validators = [])
10
+ @request = request
11
+ @validators = validators
12
+ end
13
+
14
+ def storer
15
+ @storer ||= Fragmenter::Services::Storer.new(request)
16
+ end
17
+
18
+ def store
19
+ stored = valid? && storer.store
20
+ @complete = fragmenter.complete?
21
+
22
+ if stored && complete?
23
+ rebuild_fragments
24
+ end
25
+
26
+ stored
27
+ end
28
+
29
+ def errors
30
+ validator_instances.map(&:errors).flatten
31
+ end
32
+
33
+ def complete?
34
+ !!@complete
35
+ end
36
+
37
+ def valid?
38
+ validator_instances.all?(&:valid?)
39
+ end
40
+
41
+ private
42
+
43
+ def fragmenter
44
+ request.fragmenter
45
+ end
46
+
47
+ def rebuild_fragments
48
+ request.resource.rebuild_fragments
49
+ end
50
+
51
+ def validator_instances
52
+ @validator_instances ||= validators.map do |validator|
53
+ validator.new(request)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ require 'digest/md5'
2
+
3
+ module Fragmenter
4
+ module Validators
5
+ class ChecksumValidator
6
+ attr_reader :errors, :request
7
+
8
+ def initialize(request)
9
+ @request = request
10
+ @errors = []
11
+ end
12
+
13
+ def valid?
14
+ matches = expected.nil? || expected == calculated
15
+
16
+ unless matches
17
+ errors << "Expected checksum #{expected} to match #{calculated}"
18
+ end
19
+
20
+ matches
21
+ end
22
+
23
+ private
24
+
25
+ def expected
26
+ request.headers['HTTP_CONTENT_MD5']
27
+ end
28
+
29
+ def calculated
30
+ @calculated ||= Digest::MD5.hexdigest(request.body)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Fragmenter
2
- VERSION = '0.5.1'
2
+ VERSION = '1.0.0.rc1'
3
3
  end
@@ -1,5 +1,5 @@
1
1
  module Fragmenter
2
- class Base
2
+ class Wrapper
3
3
  extend Forwardable
4
4
 
5
5
  attr_reader :object, :engine
@@ -5,54 +5,54 @@ describe Fragmenter::Fragment do
5
5
 
6
6
  describe '#number' do
7
7
  it 'defaults the number to 1' do
8
- described_class.new(blob, {}).number.should == 1
8
+ Fragmenter::Fragment.new(blob, {}).number.should == 1
9
9
  end
10
10
  end
11
11
 
12
12
  describe '#total' do
13
13
  it 'defaults the total to 1' do
14
- described_class.new(blob, {}).total.should == 1
14
+ Fragmenter::Fragment.new(blob, {}).total.should == 1
15
15
  end
16
16
  end
17
17
 
18
18
  describe '#content_type' do
19
19
  it 'defaults the content_type to a binary format' do
20
- described_class.new(blob, {}).content_type.should == 'application/octet-stream'
20
+ Fragmenter::Fragment.new(blob, {}).content_type.should == 'application/octet-stream'
21
21
  end
22
22
  end
23
23
 
24
24
  describe '#padded_number' do
25
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'
26
+ Fragmenter::Fragment.new(blob, number: 1, total: 2000).padded_number.should == '0001'
27
27
  end
28
28
  end
29
29
 
30
30
  describe '#valid?' do
31
31
  it 'is valid with a complete blob and sensible options' do
32
- described_class.new(blob, number: 1, total: 2).should be_valid
32
+ Fragmenter::Fragment.new(blob, number: 1, total: 2).should be_valid
33
33
  end
34
34
 
35
35
  it 'is not valid with an empty blob' do
36
- described_class.new('', number: 1, total: 2).should_not be_valid
36
+ Fragmenter::Fragment.new('', number: 1, total: 2).should_not be_valid
37
37
  end
38
38
 
39
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
40
+ Fragmenter::Fragment.new(blob, number: -1, total: 2).should_not be_valid
41
+ Fragmenter::Fragment.new(blob, number: 'one', total: 2).should_not be_valid
42
42
  end
43
43
 
44
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
45
+ Fragmenter::Fragment.new(blob, number: 1, total: -2).should_not be_valid
46
+ Fragmenter::Fragment.new(blob, number: 1, total: 'two').should_not be_valid
47
47
  end
48
48
 
49
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
50
+ Fragmenter::Fragment.new(blob, number: 2, total: 1).should_not be_valid
51
+ Fragmenter::Fragment.new(blob, number: 2, total: 2).should be_valid
52
52
  end
53
53
 
54
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
55
+ Fragmenter::Fragment.new(blob, content_type: 'jpg').should_not be_valid
56
56
  end
57
57
  end
58
58
  end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+ require 'fragmenter/rails/controller'
3
+
4
+ describe Fragmenter::Rails::Controller do
5
+ UploadController = Struct.new(:resource) do
6
+ include Fragmenter::Rails::Controller
7
+ end
8
+
9
+ Resource = Struct.new(:id) do
10
+ def fragmenter
11
+ @fragmenter ||= Fragmenter::Wrapper.new(self)
12
+ end
13
+ end
14
+
15
+ describe '#show' do
16
+ it 'renders the JSON representation of the associated fragmenter' do
17
+ resource = Resource.new(100)
18
+ controller = UploadController.new(resource)
19
+
20
+ controller.stub(:render)
21
+
22
+ controller.show
23
+
24
+ expect(controller).to have_received(:render).with(
25
+ json: { 'fragments' => [] }
26
+ )
27
+ end
28
+ end
29
+
30
+ describe '#destroy' do
31
+ it 'commands the fragmenter to clean' do
32
+ resource = Resource.new(100)
33
+ controller = UploadController.new(resource)
34
+
35
+ controller.stub(:render)
36
+ resource.fragmenter.stub(:clean!)
37
+
38
+ controller.destroy
39
+
40
+ expect(resource.fragmenter).to have_received(:clean!)
41
+ expect(controller).to have_received(:render).with(
42
+ nothing: true,
43
+ status: :no_content
44
+ )
45
+ end
46
+ end
47
+
48
+ describe '#update' do
49
+ it 'stores the request body' do
50
+ resource = Resource.new(100)
51
+ controller = UploadController.new(resource)
52
+ uploader = double(:uploader, store: true, complete?: false)
53
+
54
+ controller.stub(:render)
55
+ controller.stub(uploader: uploader)
56
+
57
+ controller.update
58
+
59
+ expect(uploader).to have_received(:store)
60
+ expect(controller).to have_received(:render).with(
61
+ json: { 'fragments' => [] },
62
+ status: :ok
63
+ )
64
+ end
65
+
66
+ it 'renders error messages if storage fails' do
67
+ resource = Resource.new(100)
68
+ controller = UploadController.new(resource)
69
+ uploader = double(:uploader, store: false, errors: [], complete?: false)
70
+
71
+ controller.stub(:render)
72
+ controller.stub(uploader: uploader)
73
+
74
+ controller.update
75
+
76
+ expect(controller).to have_received(:render).with(
77
+ json: {
78
+ message: 'Upload of part failed.',
79
+ errors: []
80
+ },
81
+ status: :unprocessable_entity
82
+ )
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'fragmenter/rails/model'
3
+
4
+ describe Fragmenter::Rails::Model do
5
+ let(:model) do
6
+ double(:model).extend(Fragmenter::Rails::Model)
7
+ end
8
+
9
+ it 'adds a fragmenter wrapper around the underlying model' do
10
+ expect(model).to respond_to(:fragmenter)
11
+ expect(model.fragmenter).to be_instance_of(Fragmenter::Wrapper)
12
+ end
13
+
14
+ it 'adds an abstract rebuild_fragments method for compatibility' do
15
+ expect(model).to respond_to(:rebuild_fragments)
16
+ expect { model.rebuild_fragments }.to raise_error(Fragmenter::AbstractMethodError)
17
+ end
18
+ end
@@ -4,10 +4,10 @@ require 'fragmenter/redis'
4
4
  describe Fragmenter::Redis do
5
5
  let(:blob_1) { '00010110' }
6
6
  let(:blob_2) { '11101110' }
7
- let(:fragmenter) { mock(:fragmenter, key: 'abcdefg') }
7
+ let(:fragmenter) { double(:fragmenter, key: 'abcdefg') }
8
8
  let(:redis) { Fragmenter.redis }
9
9
 
10
- subject(:engine) { described_class.new(fragmenter) }
10
+ subject(:engine) { Fragmenter::Redis.new(fragmenter) }
11
11
 
12
12
  before do
13
13
  Fragmenter.logger = Logger.new('/dev/null')
@@ -0,0 +1,15 @@
1
+ require 'fragmenter/request'
2
+
3
+ describe Fragmenter::Request do
4
+ describe '#body' do
5
+ it 'attempts to read from the body if it is an IO object' do
6
+ request = Fragmenter::Request.new(body: StringIO.new('blob'))
7
+ expect(request.body).to eq('blob')
8
+ end
9
+
10
+ it 'does not attempt to read the body if it is not IO' do
11
+ request = Fragmenter::Request.new(body: 'blob')
12
+ expect(request.body).to eq('blob')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ require 'fragmenter/services/storer'
2
+
3
+ describe Fragmenter::Services::Storer do
4
+ Storer = Fragmenter::Services::Storer
5
+
6
+ describe '#store' do
7
+ it 'stores the request with the fragmenter' do
8
+ fragmenter = double(:fragmenter)
9
+
10
+ request = Fragmenter::Request.new(
11
+ fragmenter: fragmenter,
12
+ body: '00100',
13
+ headers: {
14
+ 'CONTENT_TYPE' => 'application/octet-stream',
15
+ 'HTTP_X_FRAGMENT_NUMBER' => 1,
16
+ 'HTTP_X_FRAGMENT_TOTAL' => 2
17
+ }
18
+ )
19
+
20
+ expect(fragmenter).to receive(:store).with(
21
+ '00100',
22
+ content_type: 'application/octet-stream',
23
+ number: 1,
24
+ total: 2
25
+ ).and_return(true)
26
+
27
+ expect(Storer.new(request).store).to be_true
28
+ end
29
+
30
+ it 'records an error if the body could not be stored' do
31
+ fragmenter = double(:fragmenter, store: false)
32
+
33
+ request = Fragmenter::Request.new(
34
+ fragmenter: fragmenter,
35
+ body: '00100',
36
+ headers: {
37
+ 'CONTENT_TYPE' => 'application/octet-stream',
38
+ 'HTTP_X_FRAGMENT_NUMBER' => 1,
39
+ 'HTTP_X_FRAGMENT_TOTAL' => 2
40
+ }
41
+ )
42
+
43
+ storer = Storer.new(request)
44
+
45
+ expect(storer.store).to be_false
46
+ expect(storer.errors.length).to be_nonzero
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,73 @@
1
+ require 'fragmenter/services/uploader'
2
+
3
+ describe Fragmenter::Services::Uploader do
4
+ Uploader = Fragmenter::Services::Uploader
5
+
6
+ describe '#store' do
7
+ it 'attempts to store the fragments without any validators' do
8
+ fragmenter = double(:fragmenter, complete?: false)
9
+ request = Fragmenter::Request.new(fragmenter: fragmenter)
10
+ uploader = Uploader.new(request)
11
+
12
+ expect(uploader.storer).to receive(:store).and_return(true)
13
+ expect(uploader.store).to be_true
14
+ end
15
+
16
+ it 'does not attempt to store fragments if any validators are invalid' do
17
+ validator = Struct.new(:request) do
18
+ def valid?
19
+ false
20
+ end
21
+ end
22
+
23
+ fragmenter = double(:fragmenter, complete?: false)
24
+ request = Fragmenter::Request.new(fragmenter: fragmenter)
25
+ uploader = Uploader.new(request, [validator])
26
+
27
+ expect(uploader.storer).to_not receive(:store)
28
+
29
+ expect(uploader.store).to be_false
30
+ end
31
+
32
+ it 'instructs the resource to rebuild if storage is complete' do
33
+ resource = double(:resource)
34
+ fragmenter = double(:fragmenter, complete?: true)
35
+ storer = double(:storer, store: true)
36
+ request = Fragmenter::Request.new(resource: resource, fragmenter: fragmenter)
37
+ uploader = Uploader.new(request)
38
+
39
+ uploader.storer = storer
40
+
41
+ expect(resource).to receive(:rebuild_fragments)
42
+
43
+ uploader.store
44
+ end
45
+ end
46
+
47
+ describe '#complete?' do
48
+ it 'is incomplete without store having been called' do
49
+ uploader = Uploader.new(Object.new)
50
+
51
+ expect(uploader).to_not be_complete
52
+ end
53
+ end
54
+
55
+ describe '#errors' do
56
+ it 'merges the errors from all validators' do
57
+ validator_a = Struct.new(:request) do
58
+ def valid?; false; end
59
+ def errors; ['bad']; end
60
+ end
61
+
62
+ validator_b = Struct.new(:request) do
63
+ def valid?; false; end
64
+ def errors; ['invalid']; end
65
+ end
66
+
67
+ uploader = Uploader.new({}, [validator_a, validator_b])
68
+ uploader.valid?
69
+
70
+ expect(uploader.errors).to eq(%w[bad invalid])
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,41 @@
1
+ require 'fragmenter/validators/checksum_validator'
2
+
3
+ describe Fragmenter::Validators::ChecksumValidator do
4
+ Validator = Fragmenter::Validators::ChecksumValidator
5
+
6
+ describe '#valid?' do
7
+ it 'is always valid if no expected checksum was given' do
8
+ expect(Validator.new(double(headers: {}))).to be_valid
9
+ end
10
+
11
+ it 'is valid if the expected checksum matches the body checksum' do
12
+ request = double(
13
+ body: '0010001000',
14
+ headers: { 'HTTP_CONTENT_MD5' => '4ac8660969d304047daa9c3539f63682' }
15
+ )
16
+
17
+ expect(Validator.new(request)).to be_valid
18
+ end
19
+
20
+ it 'is not valid if the expected checksum does not match the body checksum' do
21
+ request = double(
22
+ body: '0010001000',
23
+ headers: { 'HTTP_CONTENT_MD5' => 'a9c3539f636824ac8660969d304047da' }
24
+ )
25
+
26
+ expect(Validator.new(request)).to_not be_valid
27
+ end
28
+
29
+ it 'records an error when the checksums do not match' do
30
+ request = double(
31
+ body: '0010001000',
32
+ headers: { 'HTTP_CONTENT_MD5' => 'a9c3539f636824ac8660969d304047da' }
33
+ )
34
+
35
+ validator = Validator.new(request)
36
+ validator.valid?
37
+
38
+ expect(validator.errors.length).to be_nonzero
39
+ end
40
+ end
41
+ end
@@ -1,15 +1,15 @@
1
- require 'fragmenter/base'
1
+ require 'fragmenter/wrapper'
2
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') }
3
+ describe Fragmenter::Wrapper do
4
+ let(:object) { double(:object, id: 1001) }
5
+ let(:engine_class) { double(:engine_class, new: engine) }
6
+ let(:engine) { double(:engine) }
7
7
 
8
- subject { described_class.new(object, engine_class) }
8
+ subject(:base) { Fragmenter::Wrapper.new(object, engine_class) }
9
9
 
10
10
  describe '#key' do
11
11
  it 'composes a key from the object class and id value' do
12
- subject.key.should match(/[a-z]+-\d+/)
12
+ base.key.should match(/[a-z]+-\d+/)
13
13
  end
14
14
  end
15
15
 
@@ -20,12 +20,12 @@ describe Fragmenter::Base do
20
20
  it 'delegates #store to the storage engine' do
21
21
  engine.should_receive(:store).with(blob, headers)
22
22
 
23
- subject.store(blob, headers)
23
+ base.store(blob, headers)
24
24
  end
25
25
 
26
26
  it 'delegates #fragments to the storage engine' do
27
27
  engine.should_receive(:fragments)
28
- subject.fragments
28
+ base.fragments
29
29
  end
30
30
  end
31
31
 
@@ -34,7 +34,7 @@ describe Fragmenter::Base do
34
34
  engine.stub('meta' => { 'content_type' => 'application/octet-stream' },
35
35
  'fragments' => ['1', '2'])
36
36
 
37
- subject.as_json.tap do |json|
37
+ base.as_json.tap do |json|
38
38
  json.should have_key('content_type')
39
39
  json.should have_key('fragments')
40
40
  end
@@ -10,13 +10,12 @@ describe Fragmenter do
10
10
  describe '.logger' do
11
11
  it 'attempts to instantiate a standard logger to STDOUT' do
12
12
  Fragmenter.logger.should be_instance_of(Logger)
13
- Fragmenter.logger.level.should == Logger::INFO
14
13
  end
15
14
  end
16
15
 
17
16
  describe '.logger=' do
18
17
  it 'stores the logger instance on the module' do
19
- logger = mock(:logger)
18
+ logger = double(:logger)
20
19
 
21
20
  Fragmenter.logger = logger
22
21
  Fragmenter.logger.should be(logger)
@@ -31,7 +30,7 @@ describe Fragmenter do
31
30
 
32
31
  describe '.redis=' do
33
32
  it 'stores the redis instance on the module' do
34
- redis = mock(:redis)
33
+ redis = double(:redis)
35
34
 
36
35
  Fragmenter.redis = redis
37
36
  Fragmenter.redis.should be(redis)
@@ -52,8 +51,8 @@ describe Fragmenter do
52
51
  end
53
52
 
54
53
  describe '.configure' do
55
- let(:redis) { mock(:redis) }
56
- let(:logger) { mock(:logger) }
54
+ let(:redis) { double(:redis) }
55
+ let(:logger) { double(:logger) }
57
56
 
58
57
  it 'allows customization via passing a block' do
59
58
  Fragmenter.configure do |config|
data/spec/spec_helper.rb CHANGED
@@ -0,0 +1,8 @@
1
+ require 'fragmenter'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ config.order = 'random'
8
+ end
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fragmenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
5
- prerelease:
4
+ version: 1.0.0.rc1
6
5
  platform: ruby
7
6
  authors:
8
7
  - Parker Selbert
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-10-29 00:00:00.000000000 Z
11
+ date: 2013-07-18 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: redis
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ~>
28
25
  - !ruby/object:Gem::Version
@@ -30,19 +27,17 @@ dependencies:
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rspec
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
31
  - - ~>
36
32
  - !ruby/object:Gem::Version
37
- version: 2.11.0
33
+ version: 2.14.0
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
38
  - - ~>
44
39
  - !ruby/object:Gem::Version
45
- version: 2.11.0
40
+ version: 2.14.0
46
41
  description: Fragmentize and rebuild data
47
42
  email:
48
43
  - parker@sorentwo.com
@@ -51,6 +46,8 @@ extensions: []
51
46
  extra_rdoc_files: []
52
47
  files:
53
48
  - .gitignore
49
+ - .rspec
50
+ - .travis.yml
54
51
  - Gemfile
55
52
  - HISTORY.md
56
53
  - LICENSE.txt
@@ -58,48 +55,61 @@ files:
58
55
  - Rakefile
59
56
  - fragmenter.gemspec
60
57
  - lib/fragmenter.rb
61
- - lib/fragmenter/base.rb
62
58
  - lib/fragmenter/fragment.rb
59
+ - lib/fragmenter/rails/controller.rb
60
+ - lib/fragmenter/rails/model.rb
63
61
  - lib/fragmenter/redis.rb
62
+ - lib/fragmenter/request.rb
63
+ - lib/fragmenter/services/storer.rb
64
+ - lib/fragmenter/services/uploader.rb
65
+ - lib/fragmenter/validators/checksum_validator.rb
64
66
  - lib/fragmenter/version.rb
65
- - spec/fragmenter/base_spec.rb
67
+ - lib/fragmenter/wrapper.rb
66
68
  - spec/fragmenter/fragment_spec.rb
69
+ - spec/fragmenter/rails/controller_spec.rb
70
+ - spec/fragmenter/rails/model_spec.rb
67
71
  - spec/fragmenter/redis_spec.rb
72
+ - spec/fragmenter/request_spec.rb
73
+ - spec/fragmenter/services/storer_spec.rb
74
+ - spec/fragmenter/services/uploader_spec.rb
75
+ - spec/fragmenter/validators/checksum_validator_spec.rb
76
+ - spec/fragmenter/wrapper_spec.rb
68
77
  - spec/fragmenter_spec.rb
69
78
  - spec/spec_helper.rb
70
79
  homepage: https://github.com/dscout/fragmenter
71
80
  licenses: []
81
+ metadata: {}
72
82
  post_install_message:
73
83
  rdoc_options: []
74
84
  require_paths:
75
85
  - lib
76
86
  required_ruby_version: !ruby/object:Gem::Requirement
77
- none: false
78
87
  requirements:
79
- - - ! '>='
88
+ - - '>='
80
89
  - !ruby/object:Gem::Version
81
90
  version: '0'
82
- segments:
83
- - 0
84
- hash: -1322051639440920079
85
91
  required_rubygems_version: !ruby/object:Gem::Requirement
86
- none: false
87
92
  requirements:
88
- - - ! '>='
93
+ - - '>'
89
94
  - !ruby/object:Gem::Version
90
- version: '0'
91
- segments:
92
- - 0
93
- hash: -1322051639440920079
95
+ version: 1.3.1
94
96
  requirements: []
95
97
  rubyforge_project:
96
- rubygems_version: 1.8.23
98
+ rubygems_version: 2.0.0
97
99
  signing_key:
98
- specification_version: 3
99
- summary: Fragmentize and rebuild data
100
+ specification_version: 4
101
+ summary: Multipart upload support backed by Redis. Fragmenter handles storing multiple
102
+ parts of a larger binary and rebuilding it back into the original after all parts
103
+ have been stored.
100
104
  test_files:
101
- - spec/fragmenter/base_spec.rb
102
105
  - spec/fragmenter/fragment_spec.rb
106
+ - spec/fragmenter/rails/controller_spec.rb
107
+ - spec/fragmenter/rails/model_spec.rb
103
108
  - spec/fragmenter/redis_spec.rb
109
+ - spec/fragmenter/request_spec.rb
110
+ - spec/fragmenter/services/storer_spec.rb
111
+ - spec/fragmenter/services/uploader_spec.rb
112
+ - spec/fragmenter/validators/checksum_validator_spec.rb
113
+ - spec/fragmenter/wrapper_spec.rb
104
114
  - spec/fragmenter_spec.rb
105
115
  - spec/spec_helper.rb