tusc 0.6.1
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/.editorconfig +26 -0
- data/.gitignore +12 -0
- data/.tool-versions +2 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +83 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +21 -0
- data/README.md +109 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/rackup +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/config.ru +8 -0
- data/lib/core_ext/object/blank.rb +144 -0
- data/lib/core_ext/string/truncate.rb +44 -0
- data/lib/http_service.rb +87 -0
- data/lib/tusc.rb +56 -0
- data/lib/tusc/creation_request.rb +45 -0
- data/lib/tusc/creation_response.rb +26 -0
- data/lib/tusc/offset_request.rb +42 -0
- data/lib/tusc/offset_response.rb +20 -0
- data/lib/tusc/responsorial.rb +15 -0
- data/lib/tusc/upload_request.rb +64 -0
- data/lib/tusc/upload_response.rb +37 -0
- data/lib/tusc/uploader.rb +141 -0
- data/lib/tusc/version.rb +3 -0
- data/tusc.gemspec +48 -0
- metadata +202 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "tusc"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/rackup
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rackup' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rack", "rackup")
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
data/config.ru
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# File activesupport/lib/active_support/core_ext/object/blank.rb, line 122
|
2
|
+
class Object
|
3
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
4
|
+
# For example, +false+, '', ' ', +nil+, [], and {} are all blank.
|
5
|
+
#
|
6
|
+
# This simplifies
|
7
|
+
#
|
8
|
+
# !address || address.empty?
|
9
|
+
#
|
10
|
+
# to
|
11
|
+
#
|
12
|
+
# address.blank?
|
13
|
+
#
|
14
|
+
# @return [true, false]
|
15
|
+
def blank?
|
16
|
+
respond_to?(:empty?) ? !!empty? : !self
|
17
|
+
end
|
18
|
+
|
19
|
+
# An object is present if it's not blank.
|
20
|
+
#
|
21
|
+
# @return [true, false]
|
22
|
+
def present?
|
23
|
+
!blank?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the receiver if it's present otherwise returns +nil+.
|
27
|
+
# <tt>object.presence</tt> is equivalent to
|
28
|
+
#
|
29
|
+
# object.present? ? object : nil
|
30
|
+
#
|
31
|
+
# For example, something like
|
32
|
+
#
|
33
|
+
# state = params[:state] if params[:state].present?
|
34
|
+
# country = params[:country] if params[:country].present?
|
35
|
+
# region = state || country || 'US'
|
36
|
+
#
|
37
|
+
# becomes
|
38
|
+
#
|
39
|
+
# region = params[:state].presence || params[:country].presence || 'US'
|
40
|
+
#
|
41
|
+
# @return [Object]
|
42
|
+
def presence
|
43
|
+
self if present?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class NilClass
|
48
|
+
# +nil+ is blank:
|
49
|
+
#
|
50
|
+
# nil.blank? # => true
|
51
|
+
#
|
52
|
+
# @return [true]
|
53
|
+
def blank?
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class FalseClass
|
59
|
+
# +false+ is blank:
|
60
|
+
#
|
61
|
+
# false.blank? # => true
|
62
|
+
#
|
63
|
+
# @return [true]
|
64
|
+
def blank?
|
65
|
+
true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class TrueClass
|
70
|
+
# +true+ is not blank:
|
71
|
+
#
|
72
|
+
# true.blank? # => false
|
73
|
+
#
|
74
|
+
# @return [false]
|
75
|
+
def blank?
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Array
|
81
|
+
# An array is blank if it's empty:
|
82
|
+
#
|
83
|
+
# [].blank? # => true
|
84
|
+
# [1,2,3].blank? # => false
|
85
|
+
#
|
86
|
+
# @return [true, false]
|
87
|
+
alias blank? empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
class Hash
|
91
|
+
# A hash is blank if it's empty:
|
92
|
+
#
|
93
|
+
# {}.blank? # => true
|
94
|
+
# { key: 'value' }.blank? # => false
|
95
|
+
#
|
96
|
+
# @return [true, false]
|
97
|
+
alias blank? empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
class String
|
101
|
+
BLANK_RE = /\A[[:space:]]*\z/.freeze unless defined? BLANK_RE # rails may be loaded by client
|
102
|
+
|
103
|
+
# A string is blank if it's empty or contains whitespaces only:
|
104
|
+
#
|
105
|
+
# ''.blank? # => true
|
106
|
+
# ' '.blank? # => true
|
107
|
+
# "\t\n\r".blank? # => true
|
108
|
+
# ' blah '.blank? # => false
|
109
|
+
#
|
110
|
+
# Unicode whitespace is supported:
|
111
|
+
#
|
112
|
+
# "\u00a0".blank? # => true
|
113
|
+
#
|
114
|
+
# @return [true, false]
|
115
|
+
def blank?
|
116
|
+
# The regexp that matches blank strings is expensive. For the case of empty
|
117
|
+
# strings we can speed up this method (~3.5x) with an empty? call. The
|
118
|
+
# penalty for the rest of strings is marginal.
|
119
|
+
empty? || BLANK_RE === self
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Numeric #:nodoc:
|
124
|
+
# No number is blank:
|
125
|
+
#
|
126
|
+
# 1.blank? # => false
|
127
|
+
# 0.blank? # => false
|
128
|
+
#
|
129
|
+
# @return [false]
|
130
|
+
def blank?
|
131
|
+
false
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class Time #:nodoc:
|
136
|
+
# No Time is blank:
|
137
|
+
#
|
138
|
+
# Time.now.blank? # => false
|
139
|
+
#
|
140
|
+
# @return [false]
|
141
|
+
def blank?
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class String
|
2
|
+
def truncate(maximum_length, omission: '…', mode: :right)
|
3
|
+
case mode
|
4
|
+
when :right, 'right'
|
5
|
+
truncate_right(maximum_length, omission: omission)
|
6
|
+
when :middle, 'middle'
|
7
|
+
truncate_middle(maximum_length, omission)
|
8
|
+
else
|
9
|
+
raise ArgumentError, "Unsupported mode (#{mode}), expected [:middle, :right]."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# File activesupport/lib/active_support/core_ext/string/filters.rb, line 66
|
14
|
+
def truncate_right(truncate_at, options = {})
|
15
|
+
return dup unless length > truncate_at
|
16
|
+
|
17
|
+
omission = options[:omission] || '…'
|
18
|
+
length_with_room_for_omission = truncate_at - omission.length
|
19
|
+
stop = if options[:separator]
|
20
|
+
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
|
21
|
+
else
|
22
|
+
length_with_room_for_omission
|
23
|
+
end
|
24
|
+
|
25
|
+
+"#{self[0, stop]}#{omission}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Truncates the middle, leaving portions from start & end
|
29
|
+
# see https://stackoverflow.com/a/62713671
|
30
|
+
def truncate_middle(maximum_length = 3, separator = '…')
|
31
|
+
return '' if maximum_length.zero?
|
32
|
+
return self if length <= maximum_length
|
33
|
+
|
34
|
+
middle_length = length - maximum_length + separator.length
|
35
|
+
edges_length = (length - middle_length) / 2.0
|
36
|
+
left_length = edges_length.ceil
|
37
|
+
right_length = edges_length.floor
|
38
|
+
|
39
|
+
left_string = left_length.zero? ? '' : self[0, left_length]
|
40
|
+
right_string = right_length.zero? ? '' : self[-right_length, right_length]
|
41
|
+
|
42
|
+
"#{left_string}#{separator}#{right_string}"
|
43
|
+
end
|
44
|
+
end
|
data/lib/http_service.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'net/http'
|
3
|
+
require_relative 'core_ext/string/truncate'
|
4
|
+
|
5
|
+
# Provides basic http calls (head, patch, post), with detailed logging
|
6
|
+
class TusClient::HttpService
|
7
|
+
def self.head(uri:, headers:, logger:)
|
8
|
+
request = Net::HTTP::Head.new(uri, headers)
|
9
|
+
_perform(http_request: request, logger: logger)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.patch(uri:, headers:, body:, logger:)
|
13
|
+
request = Net::HTTP::Patch.new(uri, headers)
|
14
|
+
request.body = body
|
15
|
+
_perform(http_request: request, logger: logger)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.post(uri:, headers:, body: nil, logger:)
|
19
|
+
request = Net::HTTP::Post.new(uri, headers)
|
20
|
+
request.body = body
|
21
|
+
_perform(http_request: request, logger: logger)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self._log_request(http_request, logger)
|
25
|
+
logger.info do
|
26
|
+
uri = http_request.uri
|
27
|
+
|
28
|
+
header_info = {}
|
29
|
+
http_request.each_header do |key, value|
|
30
|
+
header_info[key] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
request_info = { uri: uri.to_s, header: header_info }
|
34
|
+
|
35
|
+
request_body = http_request.body.to_s
|
36
|
+
request_info[:body_md5] = Digest::MD5.hexdigest(request_body) unless request_body.blank?
|
37
|
+
request_info[:body_size] = request_body.size unless request_body.blank?
|
38
|
+
|
39
|
+
formatted_body = case request_body.encoding
|
40
|
+
when Encoding::ASCII_8BIT
|
41
|
+
request_body.encoding.inspect
|
42
|
+
else
|
43
|
+
request_body.truncate_middle(60)
|
44
|
+
end
|
45
|
+
request_info[:body] = formatted_body
|
46
|
+
|
47
|
+
[ "TUS #{http_request.method}",
|
48
|
+
{ request: request_info },
|
49
|
+
TusClient.log_info,
|
50
|
+
]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self._log_response(http_method, http_response, logger)
|
55
|
+
header_info = {}
|
56
|
+
http_response.each_header do |key, value|
|
57
|
+
header_info[key] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
logger.info do
|
61
|
+
[ "TUS #{http_method}",
|
62
|
+
{ response: {
|
63
|
+
status: http_response.code,
|
64
|
+
header: header_info,
|
65
|
+
body: http_response.body.to_s.truncate_middle(60)
|
66
|
+
}},
|
67
|
+
TusClient.log_info,
|
68
|
+
]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self._perform(http_request:, logger:)
|
73
|
+
_log_request(http_request, logger)
|
74
|
+
|
75
|
+
uri = http_request.uri
|
76
|
+
http_response = Net::HTTP.start(
|
77
|
+
uri.host,
|
78
|
+
uri.port,
|
79
|
+
use_ssl: uri.scheme == 'https'
|
80
|
+
) do |http|
|
81
|
+
http.request http_request
|
82
|
+
end
|
83
|
+
|
84
|
+
_log_response(http_request.method, http_response, logger)
|
85
|
+
http_response
|
86
|
+
end
|
87
|
+
end
|
data/lib/tusc.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'tusc/version'
|
3
|
+
require_relative 'core_ext/object/blank'
|
4
|
+
require_relative 'tusc/creation_request'
|
5
|
+
require_relative 'tusc/uploader'
|
6
|
+
|
7
|
+
class Logger::LogDevice
|
8
|
+
# MonkeyPatch: to disable log header
|
9
|
+
def add_log_header(file); end
|
10
|
+
end
|
11
|
+
|
12
|
+
module TusClient
|
13
|
+
KILOBYTE = 1024
|
14
|
+
MEGABYTE = KILOBYTE * 1024
|
15
|
+
|
16
|
+
class Error < StandardError; end
|
17
|
+
|
18
|
+
def self.log_dir
|
19
|
+
log_dir = Pathname.new(File.expand_path('./log'))
|
20
|
+
Dir.mkdir(log_dir) unless Dir.exist?(log_dir)
|
21
|
+
log_dir
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.log_info
|
25
|
+
# find first entry in under the tusc dir
|
26
|
+
# source should be tus code, not support code
|
27
|
+
source = caller_locations.find { |entry| entry.to_s =~ %r{/tusc/} }.to_s
|
28
|
+
# method_name = (source =~ /`([^']*)'/ and Regexp.last_match(1)).to_s
|
29
|
+
{
|
30
|
+
source: source,
|
31
|
+
# method: method_name,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.log_level
|
36
|
+
@log_level ||= Logger::INFO
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.log_level=(value)
|
40
|
+
@logger = nil # invalidate cache
|
41
|
+
@log_level = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.logger
|
45
|
+
@logger ||= begin
|
46
|
+
# logger = Logger.new(STDOUT)
|
47
|
+
Logger.new(log_dir.join('tusc.log'), 1, 1 * MEGABYTE).tap do |logger|
|
48
|
+
logger.level = log_level
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.logger=(value)
|
54
|
+
@logger = value
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative '../http_service'
|
2
|
+
require_relative 'creation_response'
|
3
|
+
|
4
|
+
# Sends the creation request to the tus server
|
5
|
+
class TusClient::CreationRequest
|
6
|
+
attr_reader :body, :extra_headers, :file_size, :tus_creation_url
|
7
|
+
def initialize(tus_creation_url:, file_size:, extra_headers: {}, body: nil)
|
8
|
+
@tus_creation_url = tus_creation_url
|
9
|
+
@file_size = file_size
|
10
|
+
@extra_headers = extra_headers
|
11
|
+
@body = body
|
12
|
+
end
|
13
|
+
|
14
|
+
def headers
|
15
|
+
{
|
16
|
+
'Content-Length' => 0.to_s,
|
17
|
+
'Tus-Resumable' => supported_tus_resumable_versions.first,
|
18
|
+
'Upload-Length' => file_size.to_s
|
19
|
+
}.merge(extra_headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= TusClient.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sends the creation request to the tus server
|
27
|
+
# returns an upload_url (in CreationResponse)
|
28
|
+
def perform
|
29
|
+
response = TusClient::HttpService.post(
|
30
|
+
uri: tus_creation_uri,
|
31
|
+
headers: headers,
|
32
|
+
body: body,
|
33
|
+
logger: logger
|
34
|
+
)
|
35
|
+
TusClient::CreationResponse.new(response)
|
36
|
+
end
|
37
|
+
|
38
|
+
def supported_tus_resumable_versions
|
39
|
+
['1.0.0']
|
40
|
+
end
|
41
|
+
|
42
|
+
def tus_creation_uri
|
43
|
+
@tus_creation_uri ||= URI.parse(tus_creation_url)
|
44
|
+
end
|
45
|
+
end
|