rubytus 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.editorconfig +10 -0
- data/.gitignore +18 -0
- data/.simplecov +6 -0
- data/.travis.yml +8 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.md +57 -0
- data/Rakefile +10 -0
- data/bin/rubytusd +13 -0
- data/lib/rubytus.rb +3 -0
- data/lib/rubytus/api.rb +79 -0
- data/lib/rubytus/command.rb +72 -0
- data/lib/rubytus/common.rb +11 -0
- data/lib/rubytus/constants.rb +33 -0
- data/lib/rubytus/error.rb +4 -0
- data/lib/rubytus/helpers.rb +82 -0
- data/lib/rubytus/info.rb +31 -0
- data/lib/rubytus/middlewares/storage_barrier.rb +36 -0
- data/lib/rubytus/middlewares/tus_barrier.rb +36 -0
- data/lib/rubytus/request.rb +102 -0
- data/lib/rubytus/storage.rb +117 -0
- data/lib/rubytus/version.rb +3 -0
- data/rubytus.gemspec +31 -0
- data/test/rubytus/test_command.rb +249 -0
- data/test/rubytus/test_command_runner.rb +123 -0
- data/test/rubytus/test_helpers.rb +57 -0
- data/test/rubytus/test_storage.rb +94 -0
- data/test/rubytus/test_version.rb +7 -0
- data/test/test_helper.rb +79 -0
- metadata +227 -0
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/.simplecov
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Rubytus
|
2
|
+
|
3
|
+
Resumable upload protocol implementation in Ruby
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/picocandy/rubytus.png)](https://travis-ci.org/picocandy/rubytus)
|
6
|
+
[![Coverage Status](https://coveralls.io/repos/picocandy/rubytus/badge.png?branch=master)](https://coveralls.io/r/picocandy/rubytus?branch=master)
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
```bash
|
11
|
+
$ gem install rubytus
|
12
|
+
```
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
```
|
17
|
+
$ rubytusd --help
|
18
|
+
Usage: <server> [options]
|
19
|
+
|
20
|
+
Server options:
|
21
|
+
-e, --environment NAME Set the execution environment (default: development)
|
22
|
+
-a, --address HOST Bind to HOST address (default: 0.0.0.0)
|
23
|
+
-p, --port PORT Use PORT (default: 9000)
|
24
|
+
-S, --socket FILE Bind to unix domain socket
|
25
|
+
|
26
|
+
Daemon options:
|
27
|
+
-u, --user USER Run as specified user
|
28
|
+
-c, --config FILE Config file (default: ./config/<server>.rb)
|
29
|
+
-d, --daemonize Run daemonized in the background (default: false)
|
30
|
+
-l, --log FILE Log to file (default: off)
|
31
|
+
-s, --stdout Log to stdout (default: false)
|
32
|
+
-P, --pid FILE Pid file (default: off)
|
33
|
+
|
34
|
+
SSL options:
|
35
|
+
--ssl Enables SSL (default: off)
|
36
|
+
--ssl-key FILE Path to private key
|
37
|
+
--ssl-cert FILE Path to certificate
|
38
|
+
--ssl-verify Enables SSL certificate verification
|
39
|
+
|
40
|
+
Common options:
|
41
|
+
-C, --console Start a console
|
42
|
+
-v, --verbose Enable verbose logging (default: false)
|
43
|
+
-h, --help Display help message
|
44
|
+
|
45
|
+
TUSD options:
|
46
|
+
-f, --data-dir DATA_DIR Directory to store uploaded and partial files (default: tus_data)
|
47
|
+
-b, --base-path BASE_PATH Url path used for handling uploads (default: /files/)
|
48
|
+
-m, --max-size MAX_SIZE How many bytes may be stored inside DATA_DIR (default: 1073741824)
|
49
|
+
```
|
50
|
+
|
51
|
+
## Contributing
|
52
|
+
|
53
|
+
1. Fork it
|
54
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
55
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
56
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
57
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/rubytusd
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
bin = Pathname.new(__FILE__).realpath
|
5
|
+
|
6
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', bin)
|
7
|
+
ENV['RACK_ENV'] ||= ENV['TUSD_ENV'] || 'development'
|
8
|
+
|
9
|
+
require 'bundler'
|
10
|
+
Bundler.setup
|
11
|
+
Bundler.require(:default)
|
12
|
+
|
13
|
+
require 'rubytus/command'
|
data/lib/rubytus.rb
ADDED
data/lib/rubytus/api.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'goliath'
|
2
|
+
require 'goliath/constants'
|
3
|
+
require 'rubytus/constants'
|
4
|
+
require 'rubytus/request'
|
5
|
+
require 'rubytus/helpers'
|
6
|
+
require 'rubytus/error'
|
7
|
+
require 'stringio'
|
8
|
+
|
9
|
+
module Rubytus
|
10
|
+
class API < Goliath::API
|
11
|
+
include Goliath::Constants
|
12
|
+
include Rubytus::Constants
|
13
|
+
include Rubytus::Helpers
|
14
|
+
|
15
|
+
def on_headers(env, headers)
|
16
|
+
env['api.options'] = @options
|
17
|
+
env['api.headers'] = COMMON_HEADERS.merge({ 'Date' => Time.now.httpdate })
|
18
|
+
prepare_headers(env, headers)
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_body(env, data)
|
22
|
+
if env['api.action'] == :patch
|
23
|
+
env['api.buffers'] << data
|
24
|
+
else
|
25
|
+
body = StringIO.new(data)
|
26
|
+
env[RACK_INPUT] = body
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def response(env)
|
31
|
+
status = STATUS_OK
|
32
|
+
headers = env['api.headers']
|
33
|
+
body = []
|
34
|
+
|
35
|
+
[status, headers, body]
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_setup
|
39
|
+
@options[:max_size] = validates_max_size(@options[:max_size])
|
40
|
+
@options[:base_path] = validates_base_path(@options[:base_path])
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_options
|
44
|
+
{
|
45
|
+
:base_path => ENV[ENV_BASE_PATH] || DEFAULT_BASE_PATH,
|
46
|
+
:max_size => ENV[ENV_MAX_SIZE] || DEFAULT_MAX_SIZE
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_parser(opts, options)
|
51
|
+
opts.separator ""
|
52
|
+
opts.separator "TUSD options:"
|
53
|
+
|
54
|
+
args = [
|
55
|
+
{
|
56
|
+
:name => :base_path,
|
57
|
+
:short => '-b',
|
58
|
+
:long => '--base-path BASE_PATH',
|
59
|
+
:desc => "Url path used for handling uploads (default: #{options[:base_path]})"
|
60
|
+
},
|
61
|
+
{
|
62
|
+
:name => :max_size,
|
63
|
+
:short => '-m',
|
64
|
+
:long => '--max-size MAX_SIZE',
|
65
|
+
:desc => "Maximum bytes may be stored inside storage (default: #{options[:max_size]})"
|
66
|
+
}
|
67
|
+
]
|
68
|
+
|
69
|
+
args.each do |arg|
|
70
|
+
opts.on(arg[:short], arg[:long], arg[:desc]) do |value|
|
71
|
+
options[arg[:name]] = value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# save into global options
|
76
|
+
@options = options
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'rubytus/api'
|
2
|
+
require 'rubytus/request'
|
3
|
+
require 'rubytus/error'
|
4
|
+
require 'rubytus/storage'
|
5
|
+
require 'rubytus/middlewares/tus_barrier'
|
6
|
+
require 'rubytus/middlewares/storage_barrier'
|
7
|
+
|
8
|
+
module Rubytus
|
9
|
+
class Command < API
|
10
|
+
include Constants
|
11
|
+
include StorageHelper
|
12
|
+
|
13
|
+
use Middlewares::TusBarrier
|
14
|
+
use Middlewares::StorageBarrier
|
15
|
+
|
16
|
+
def on_headers(env, headers)
|
17
|
+
super(env, headers)
|
18
|
+
|
19
|
+
request = Request.new(env)
|
20
|
+
|
21
|
+
begin
|
22
|
+
|
23
|
+
if env['api.action'] == :patch
|
24
|
+
uid = env['api.uid']
|
25
|
+
info = storage.read_info(uid)
|
26
|
+
|
27
|
+
validates_offset(request.offset, info.offset)
|
28
|
+
validates_length(request.content_length, info.remaining_length)
|
29
|
+
end
|
30
|
+
|
31
|
+
rescue PermissionError => e
|
32
|
+
error!(STATUS_INTERNAL_ERROR, e.message)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_close(env)
|
37
|
+
if env['api.action'] == :patch
|
38
|
+
storage.patch_file(env['api.uid'], env['api.buffers'], env['api.offset'])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def options_parser(opts, options)
|
43
|
+
options = init_options.merge(options)
|
44
|
+
default_parser(opts, options)
|
45
|
+
opts.on('-f', '--data-dir DATA_DIR', "Directory to store uploads, LOCAL storage only (default: #{options[:data_dir]})") do |value|
|
46
|
+
options[:data_dir] = value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def init_options
|
51
|
+
options = default_options
|
52
|
+
options[:data_dir] = ENV[ENV_DATA_DIR] || DEFAULT_DATA_DIR
|
53
|
+
options
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup
|
57
|
+
begin
|
58
|
+
default_setup
|
59
|
+
@options[:data_dir] = validates_data_dir(@options[:data_dir])
|
60
|
+
@options[:storage] = Storage.new(@options)
|
61
|
+
rescue PermissionError, ConfigurationError => e
|
62
|
+
puts '[ERROR] ' + e.message
|
63
|
+
exit(1)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def storage
|
69
|
+
@options[:storage]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Rubytus
|
2
|
+
module Constants
|
3
|
+
RESOURCE_UID_REGEX = /^([a-z0-9]{32})$/
|
4
|
+
BASE_PATH_REGEX = /^(\/[a-zA-Z0-9\-_]+\/)$/
|
5
|
+
|
6
|
+
RESUMABLE_CONTENT_TYPE = 'application/offset+octet-stream'
|
7
|
+
|
8
|
+
ENV_STORAGE = 'TUSD_STORAGE'
|
9
|
+
ENV_DATA_DIR = 'TUSD_DATA_DIR'
|
10
|
+
ENV_BASE_PATH = 'TUSD_BASE_PATH'
|
11
|
+
ENV_MAX_SIZE = 'TUSD_MAX_SIZE'
|
12
|
+
|
13
|
+
DEFAULT_STORAGE = 'local'
|
14
|
+
DEFAULT_DATA_DIR = 'tus_data'
|
15
|
+
DEFAULT_BASE_PATH = '/files/'
|
16
|
+
DEFAULT_MAX_SIZE = 1073741824
|
17
|
+
|
18
|
+
STATUS_OK = 200
|
19
|
+
STATUS_CREATED = 201
|
20
|
+
STATUS_BAD_REQUEST = 400
|
21
|
+
STATUS_FORBIDDEN = 403
|
22
|
+
STATUS_NOT_FOUND = 404
|
23
|
+
STATUS_NOT_ALLOWED = 405
|
24
|
+
STATUS_INTERNAL_ERROR = 500
|
25
|
+
|
26
|
+
COMMON_HEADERS = {
|
27
|
+
'Access-Control-Allow-Origin' => '*',
|
28
|
+
'Access-Control-Allow-Methods' => 'HEAD,GET,PUT,POST,PATCH,DELETE',
|
29
|
+
'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type, Accept, Content-Disposition, Final-Length, Offset',
|
30
|
+
'Access-Control-Expose-Headers' => 'Location, Range, Content-Disposition, Offset'
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rubytus/constants'
|
2
|
+
require 'rubytus/common'
|
3
|
+
require 'rubytus/error'
|
4
|
+
|
5
|
+
module Rubytus
|
6
|
+
module Helpers
|
7
|
+
include Rubytus::Constants
|
8
|
+
include Rubytus::Common
|
9
|
+
|
10
|
+
def prepare_headers(env, headers)
|
11
|
+
request = Rubytus::Request.new(env)
|
12
|
+
|
13
|
+
# CREATE
|
14
|
+
if request.collection? && request.post?
|
15
|
+
uid = generate_uid
|
16
|
+
|
17
|
+
env['api.action'] = :create
|
18
|
+
env['api.uid'] = uid
|
19
|
+
env['api.final_length'] = request.final_length
|
20
|
+
env['api.resource_url'] = request.resource_url(uid)
|
21
|
+
end
|
22
|
+
|
23
|
+
if request.resource?
|
24
|
+
# UID for this resource
|
25
|
+
env['api.uid'] = request.resource_uid
|
26
|
+
|
27
|
+
# HEAD
|
28
|
+
if request.head?
|
29
|
+
env['api.action'] = :head
|
30
|
+
end
|
31
|
+
|
32
|
+
# PATCH
|
33
|
+
if request.patch?
|
34
|
+
unless request.resumable_content_type?
|
35
|
+
error!(STATUS_BAD_REQUEST, "Content-Type must be '#{RESUMABLE_CONTENT_TYPE}'")
|
36
|
+
end
|
37
|
+
|
38
|
+
env['api.action'] = :patch
|
39
|
+
env['api.buffers'] = ''
|
40
|
+
env['api.offset'] = request.offset
|
41
|
+
end
|
42
|
+
|
43
|
+
# GET
|
44
|
+
if request.get?
|
45
|
+
env['api.action'] = :get
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def validates_offset(req_offset, info_offset)
|
51
|
+
if req_offset > info_offset
|
52
|
+
error!(STATUS_FORBIDDEN, "Offset: #{req_offset} exceeds current offset: #{info_offset}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def validates_length(req_length, remaining)
|
57
|
+
if req_length > remaining
|
58
|
+
error!(STATUS_FORBIDDEN, "Content-Length: #{req_length} exceeded remaining length: #{remaining}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def validates_base_path(base_path)
|
63
|
+
unless base_path =~ BASE_PATH_REGEX
|
64
|
+
raise ConfigurationError, "Invalid `base_path` configuration, it should be using format /uploads/, /user-data/, etc"
|
65
|
+
end
|
66
|
+
|
67
|
+
base_path
|
68
|
+
end
|
69
|
+
|
70
|
+
def validates_max_size(max_size)
|
71
|
+
if max_size.is_a? String
|
72
|
+
max_size = max_size.to_i
|
73
|
+
end
|
74
|
+
|
75
|
+
if max_size <= 0
|
76
|
+
raise ConfigurationError, "Invalid `max_size`, it should be > 0 bytes"
|
77
|
+
end
|
78
|
+
|
79
|
+
max_size
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|