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.
@@ -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__)
@@ -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")
@@ -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")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ # tus server for testing
2
+ # start via $ `rackup`
3
+ # see: https://github.com/janko/tus-ruby-server
4
+ require 'tus/server'
5
+
6
+ map '/files' do
7
+ run Tus::Server
8
+ end
@@ -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
@@ -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
@@ -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