fragmenter 0.5.1 → 1.0.0.rc1
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/HISTORY.md +11 -0
- data/README.md +167 -23
- data/fragmenter.gemspec +12 -10
- data/lib/fragmenter.rb +30 -23
- data/lib/fragmenter/rails/controller.rb +50 -0
- data/lib/fragmenter/rails/model.rb +19 -0
- data/lib/fragmenter/request.rb +20 -0
- data/lib/fragmenter/services/storer.rb +41 -0
- data/lib/fragmenter/services/uploader.rb +58 -0
- data/lib/fragmenter/validators/checksum_validator.rb +34 -0
- data/lib/fragmenter/version.rb +1 -1
- data/lib/fragmenter/{base.rb → wrapper.rb} +1 -1
- data/spec/fragmenter/fragment_spec.rb +13 -13
- data/spec/fragmenter/rails/controller_spec.rb +85 -0
- data/spec/fragmenter/rails/model_spec.rb +18 -0
- data/spec/fragmenter/redis_spec.rb +2 -2
- data/spec/fragmenter/request_spec.rb +15 -0
- data/spec/fragmenter/services/storer_spec.rb +49 -0
- data/spec/fragmenter/services/uploader_spec.rb +73 -0
- data/spec/fragmenter/validators/checksum_validator_spec.rb +41 -0
- data/spec/fragmenter/{base_spec.rb → wrapper_spec.rb} +10 -10
- data/spec/fragmenter_spec.rb +4 -5
- data/spec/spec_helper.rb +8 -0
- metadata +36 -26
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
data/.rspec
ADDED
data/.travis.yml
ADDED
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
|
+
[](https://travis-ci.org/dscout/fragmenter)
|
2
|
+
[](https://codeclimate.com/github/dscout/fragmenter)
|
3
|
+
|
1
4
|
# Fragmenter
|
2
5
|
|
3
|
-
Fragmenter
|
4
|
-
|
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
|
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
|
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
|
-
|
35
|
+
Add this to your Gemfile:
|
23
36
|
|
24
|
-
|
37
|
+
```ruby
|
38
|
+
gem 'fragmenter'
|
39
|
+
```
|
25
40
|
|
26
|
-
|
41
|
+
## Configuration
|
27
42
|
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
39
|
-
fragmenter.complete? # => false
|
57
|
+
## Using Fragmenter with Rails
|
40
58
|
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
63
|
+
```ruby
|
64
|
+
class UploadControler < ApplicationController
|
65
|
+
include Fragmenter::Rails::Controller
|
46
66
|
|
47
|
-
|
67
|
+
private
|
48
68
|
|
49
|
-
|
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
|
-
|
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
|
8
|
-
gem.version
|
9
|
-
gem.authors
|
10
|
-
gem.email
|
11
|
-
gem.
|
12
|
-
gem.
|
13
|
-
|
14
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
13
|
+
class << self
|
14
|
+
def configure(&block)
|
15
|
+
yield self
|
16
|
+
end
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
24
|
+
def logger=(logger)
|
25
|
+
@logger = logger
|
26
|
+
end
|
21
27
|
|
22
|
-
|
23
|
-
|
24
|
-
|
28
|
+
def redis
|
29
|
+
@redis ||= ::Redis.new
|
30
|
+
end
|
25
31
|
|
26
|
-
|
27
|
-
|
28
|
-
|
32
|
+
def redis=(redis)
|
33
|
+
@redis = redis
|
34
|
+
end
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
def expiration=(expiration)
|
37
|
+
@expiration = expiration
|
38
|
+
end
|
33
39
|
|
34
|
-
|
35
|
-
|
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
|
data/lib/fragmenter/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
51
|
-
|
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
|
-
|
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) {
|
7
|
+
let(:fragmenter) { double(:fragmenter, key: 'abcdefg') }
|
8
8
|
let(:redis) { Fragmenter.redis }
|
9
9
|
|
10
|
-
subject(:engine) {
|
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/
|
1
|
+
require 'fragmenter/wrapper'
|
2
2
|
|
3
|
-
describe Fragmenter::
|
4
|
-
let(:object) {
|
5
|
-
let(:engine_class) {
|
6
|
-
let(: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 {
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/spec/fragmenter_spec.rb
CHANGED
@@ -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 =
|
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 =
|
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) {
|
56
|
-
let(: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
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
|
-
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:
|
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.
|
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.
|
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
|
-
-
|
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:
|
91
|
-
segments:
|
92
|
-
- 0
|
93
|
-
hash: -1322051639440920079
|
95
|
+
version: 1.3.1
|
94
96
|
requirements: []
|
95
97
|
rubyforge_project:
|
96
|
-
rubygems_version:
|
98
|
+
rubygems_version: 2.0.0
|
97
99
|
signing_key:
|
98
|
-
specification_version:
|
99
|
-
summary:
|
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
|