tus-server 2.1.2 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0caef3d7cfbadc9b74678f99e687c0fe51bfbbab77acece73e9e1332aaa8661
4
- data.tar.gz: 8be7b8e4ef8c2c6a5bc58ca11417c88b743b54252da7a3481f3866a591ef5abf
3
+ metadata.gz: f9ba14174a689878aa1224bdb2e526d869b888dc85164ec3e9320c8a171bcd73
4
+ data.tar.gz: 61e227c29273bd00007a96522a16b5f470fc433741999b072bf1c3642e13289b
5
5
  SHA512:
6
- metadata.gz: 65e6d0496407297529e55063bbb27036d08697a986291dd08299e8994bbbeb6329d15dac4fa6a1137364e126379ae2fcd8c23df5500e0cb8c5e60074265070ae
7
- data.tar.gz: b3328632108adeaf10761b3df9ab6ad9e957fe48a4b4ab92cccfaa36221f0c99be142af08ced374cbf1ab460e0b6622f0d964be55636683438b9c473e3533263
6
+ metadata.gz: a4914ab70cda026beee4de2b830778e43ef32131ddb65f470086d5cfd036f9dd046fafb09deced577d64965d539bf08a24ec44b810a77052822efde595610b04
7
+ data.tar.gz: 3c3f0d9196654541f5ebee297130c3d216dc1948a8a5c29695dc3251d969af85a101b0e249a889ec140e8f837c1fba1269a977649926d537c02dd5cd249d4e14
@@ -1,3 +1,19 @@
1
+ ## 2.2.0 (2018-12-02)
2
+
3
+ * Add `before_create`, `after_create`, `after_finish`, and `after_terminate` hooks (@janko-m)
4
+
5
+ * Rename `Tus::Info#concatenation?` to `Tus::Info#final?` (@janko-m)
6
+
7
+ * Use `Storage#concurrency` for parallelized retrieval of partial uploads in `Upload-Concat` validation (@janko-m)
8
+
9
+ * Replace `:thread_count` with `:concurrency` in S3 storage (@janko-m)
10
+
11
+ * Validate that sum of partial uploads doesn't exceed `Tus-Max-Size` on concatenation (@janko-m)
12
+
13
+ * Drop MRI 2.2 support (@janko-m)
14
+
15
+ * Accept absolute URLs of partial uploads when creating a final upload (@janko-m)
16
+
1
17
  ## 2.1.2 (2018-10-21)
2
18
 
3
19
  * Make tus-ruby-server fully work with non-rewindable Rack input (@janko-m)
data/README.md CHANGED
@@ -18,7 +18,7 @@ gem "tus-server", "~> 2.0"
18
18
 
19
19
  ## Usage
20
20
 
21
- The gem provides a `Tus::Server` Rack app, which you can mount inside your
21
+ The gem provides a `Tus::Server` [Roda] app, which you can mount inside your
22
22
  main application. If you're using Rails, you can mount it in `config/routes.rb`:
23
23
 
24
24
  ```rb
@@ -46,7 +46,7 @@ endpoint:
46
46
  // using tus-js-client
47
47
  new tus.Upload(file, {
48
48
  endpoint: "/files",
49
- chunkSize: 5*1024*1024, // required unless using Goliath
49
+ chunkSize: 5*1024*1024, // required unless using Falcon
50
50
  // ...
51
51
  })
52
52
  ```
@@ -54,79 +54,73 @@ new tus.Upload(file, {
54
54
  By default uploaded files will be stored in the `data/` directory. After the
55
55
  upload is complete, you'll probably want to attach the uploaded file to a
56
56
  database record. [Shrine] is currently the only file attachment library that
57
- integrates well with tus-ruby-server, see [this walkthrough][shrine resumable
58
- walkthrough] that adds resumable uploads from scratch, and for a complete
59
- example you can check out the [demo app][shrine-tus-demo].
57
+ provides an integration with tus-ruby-server, see [this walkthrough][shrine
58
+ resumable walkthrough] that adds resumable uploads from scratch, and for a
59
+ complete example you can check out the [demo app][shrine-tus-demo].
60
60
 
61
- ### Goliath
61
+ ### Streaming web server
62
62
 
63
- Running the tus server alongside your main app using classic web servers like
63
+ Running the tus server alongside your main app using popular web servers like
64
64
  Puma or Unicorn is probably fine for most cases, however, it does come with a
65
65
  few gotchas. First, since these web servers don't accept partial requests
66
66
  (request where the request body hasn't been fully received), the tus client
67
67
  must be configured to split the upload into multiple requests. Second, since
68
68
  web workers are tied for the duration of the request, serving uploaded files
69
- through the tus server app could significantly impact request throughput, so
70
- you need to be careful to avoid that.
71
-
72
- There is an alternative. [Goliath] is an asychronous web server built on top of
73
- [EventMachine], which supports streaming requests and streaming responses.
74
-
75
- * Asynchronous streaming requests allows the tus server to begin saving
76
- uploaded data while it's still being received. If the request is interrupted,
77
- the tus server will attempt to save as much of the data that was received so
78
- far. This means it's not necessary for the tus client to split the upload
79
- into multiple smaller requests.
80
-
81
- * Asynchronous streaming responses allows the tus server to stream large files
82
- with very small impact to the request throughput.
83
-
84
- Since Goliath is web server, to run tus server on it we'll have to run it as a
85
- standalone web app. It's recommended that you use [goliath-rack_proxy] for
86
- running your tus server app:
69
+ through the tus server app could significantly impact request throughput; this
70
+ can be avoided by having your frontend server (Nginx) serve the files if using
71
+ `Filesystem` storage, or if you're using a cloud service like S3 having
72
+ download requests redirect to the service file URL.
73
+
74
+ That being said, there is a ruby web server that addresses these limitations
75
+ [Falcon]. Falcon is part of the [`async` ecosystem][async], and it utilizes
76
+ non-blocking IO to process requests and responses in a streaming fashion
77
+ without tying up your web workers. This has several benefits for
78
+ `tus-ruby-server`:
79
+
80
+ * since tus server is called to handle the request as soon as the request
81
+ headers are received, data from the request body will be uploaded to the
82
+ configured storage as it's coming in from the client
83
+
84
+ * your web workers don't get tied up waiting for the client data, because as
85
+ soon as the server needs to wait for more data from the client, Falcon's
86
+ reactor switches to processing another request
87
+
88
+ * if the upload request that's in progress gets interrupted, tus server will be
89
+ able save data that has been received so far, so it's not necessary for the
90
+ tus client to split the upload into multiple chunks
91
+
92
+ * when uploaded files are being downloaded from the tus server, the request
93
+ throughput won't be impacted by the speed in which the client retrieves the
94
+ response body, because Falcon's reactor will switch to another request if the
95
+ client buffer gets full
96
+
97
+ Falcon provides a Rack adapter and is compatible with Rails, so you can use it
98
+ even if you're mounting `tus-ruby-server` inside your main app, just add
99
+ `falcon` to the Gemfile and run `falcon serve`.
87
100
 
88
101
  ```rb
89
102
  # Gemfile
90
- gem "tus-server", "~> 2.0"
91
- gem "goliath-rack_proxy", "~> 1.0"
92
- ```
93
- ```rb
94
- # tus.rb
95
- require "tus/server"
96
- require "goliath/rack_proxy"
97
-
98
- # any additional Tus::Server configuration you want to put in here
99
-
100
- class GoliathTusServer < Goliath::RackProxy
101
- rack_app Tus::Server
102
- rewindable_input false # set to true if you're using checksums
103
- end
103
+ gem "falcon"
104
104
  ```
105
105
  ```sh
106
- $ ruby tus.rb --stdout # enable logging
106
+ $ falcon serve # reads from config.ru and starts the server
107
107
  ```
108
108
 
109
- This will run the tus server app on the root URL; if you want to run it on some
110
- path you can use `Rack::Builder`:
109
+ Alternatively, you can run `tus-ruby-server` as a standalone app, by creating
110
+ a separate "rackup" file just for it and pointing Falcon to that rackup file:
111
111
 
112
112
  ```rb
113
- class GoliathTusServer < Goliath::RackProxy
114
- rack_app Rack::Builder.new {
115
- map("/files") { run Tus::Server }
116
- }
117
- rewindable_input false # set to true if you're using checksums
118
- end
119
- ```
113
+ # tus.ru
114
+ require "tus/server"
120
115
 
121
- In this case you'll have to configure the tus client to point to the standalone
122
- Goliath app:
116
+ # ... configure tus-ruby-server ...
123
117
 
124
- ```js
125
- // using tus-js-client
126
- new tus.Upload(file, {
127
- endpoint: "http://localhost:9000/files",
128
- // ...
129
- })
118
+ map "/files" do
119
+ run Tus::Server
120
+ end
121
+ ```
122
+ ```sh
123
+ $ falcon serve --config tus.ru
130
124
  ```
131
125
 
132
126
  ## Storage
@@ -258,6 +252,13 @@ can for example change the `:endpoint` to use S3's accelerate host:
258
252
  Tus::Storage::S3.new(endpoint: "https://s3-accelerate.amazonaws.com", **options)
259
253
  ```
260
254
 
255
+ If you're using [concatenation], you can specify the concurrency in which S3
256
+ storage will copy partial uploads to the final upload (defaults to `10`):
257
+
258
+ ```rb
259
+ Tus::Storage::S3.new(concurrency: { concatenation: 20 }, **options)
260
+ ```
261
+
261
262
  ### Google Cloud Storage, Microsoft Azure Blob Storage
262
263
 
263
264
  While tus-ruby-server doesn't currently ship with integrations for Google Cloud
@@ -360,6 +361,84 @@ def delete_file(uid, info = {}) ... end
360
361
  def expire_files(expiration_date) ... end
361
362
  ```
362
363
 
364
+ ## Hooks
365
+
366
+ You can register code to be executed on the following events:
367
+
368
+ * `before_create` – before upload has been created
369
+ * `after_create` – before upload has been created
370
+ * `after_finish` – after the last chunk has been stored
371
+ * `after_terminate` – after the upload has been deleted
372
+
373
+ Each hook also receives two parameters: ID of the upload (`String`) and
374
+ additional information about the upload (`Tus::Info`).
375
+
376
+ ```rb
377
+ Tus::Server.after_finish do |uid, info|
378
+ uid #=> "c0b67b04a9eccb4b1202000de628964f"
379
+ info #=> #<Tus::Info>
380
+
381
+ info.length #=> 10 (Upload-Length)
382
+ info.offset #=> 0 (Upload-Offset)
383
+ info.metadata #=> {...} (Upload-Metadata)
384
+ info.expires #=> #<Time> (Upload-Expires)
385
+
386
+ info.partial? #=> false (Upload-Concat)
387
+ info.final? #=> false (Upload-Concat)
388
+
389
+ info.defer_length? #=> false (Upload-Defer-Length)
390
+ end
391
+ ```
392
+
393
+ Because each hook is evaluated inside the `Tus::Server` instance (which is also
394
+ a [Roda] instance), you can access request information and set response status
395
+ and headers:
396
+
397
+ ```rb
398
+ Tus::Server.after_terminate do |uid, info|
399
+ self #=> #<Tus::Server> (and #<Roda>)
400
+ request #=> #<Roda::Request>
401
+ response #=> #<Roda::Response>
402
+ end
403
+ ```
404
+
405
+ So, with hooks you could for example add authentication to `Tus::Server`:
406
+
407
+ ```rb
408
+ Tus::Server.before_create do |uid, info|
409
+ authenticated = Authentication.call(request.headers["Authorization"])
410
+
411
+ unless authenticated
412
+ response.status = 403 # Forbidden
413
+ response.write("Not authenticated")
414
+ request.halt # return response now
415
+ end
416
+ end
417
+ ```
418
+
419
+ If you want to add hooks on more types of events, you can use Roda's
420
+ [hooks][roda hooks] plugin to set `before` or `after` hooks for any request,
421
+ which are also evaluated in context of the `Roda` instance:
422
+
423
+ ```rb
424
+ Tus::Server.plugin :hooks
425
+ Tus::Server.before do
426
+ # called before each Roda request
427
+ end
428
+ Tus::Server.after do
429
+ # called after each Roda request
430
+ end
431
+ ```
432
+
433
+ Provided that you're mounting `Tus::Server` inside your main app, any Rack
434
+ middlewares that your main app uses will also be called for requests routed to
435
+ the `Tus::Server`. If you want to add Rack middlewares to `Tus::Server`, you
436
+ can do it by calling `Tus::Server.use`:
437
+
438
+ ```rb
439
+ Tus::Server.use SomeRackMiddleware
440
+ ```
441
+
363
442
  ## Maximum size
364
443
 
365
444
  By default the size of files the tus server will accept is unlimited, but you
@@ -427,6 +506,19 @@ The following checksum algorithms are supported for the `checksum` extension:
427
506
  * MD5
428
507
  * CRC32
429
508
 
509
+ ## Concatenation
510
+
511
+ When validating the `Upload-Concat` header for the final upload,
512
+ tus-ruby-server needs to first fetch info about all partial uploads in order to
513
+ check whether everything is in order. By default this retrieval is parallelized
514
+ with 10 threads, but if you're using S3 storage you can change the
515
+ `:concurrency`, and `Tus::Server` will automatically pick it up for its
516
+ validation:
517
+
518
+ ```rb
519
+ Tus::Storage::S3.new(concurrency: { concatenation: 20 }, **options)
520
+ ```
521
+
430
522
  ## Tests
431
523
 
432
524
  Run tests with
@@ -446,6 +538,8 @@ The tus-ruby-server was inspired by [rubytus] and [tusd].
446
538
 
447
539
  [MIT](/LICENSE.txt)
448
540
 
541
+ [Roda]: https://github.com/jeremyevans/roda
542
+ [roda hooks]: http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Hooks.html
449
543
  [tus resumable upload protocol]: http://tus.io/
450
544
  [tus-js-client]: https://github.com/tus/tus-js-client
451
545
  [creation]: http://tus.io/protocols/resumable-upload.html#creation
@@ -463,9 +557,6 @@ The tus-ruby-server was inspired by [rubytus] and [tusd].
463
557
  [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method
464
558
  [`Aws::S3::Client#create_multipart_upload`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#create_multipart_upload-instance_method
465
559
  [Range requests]: https://tools.ietf.org/html/rfc7233
466
- [Goliath]: https://github.com/postrank-labs/goliath
467
- [EventMachine]: https://github.com/eventmachine/eventmachine
468
- [goliath-rack_proxy]: https://github.com/janko-m/goliath-rack_proxy
469
560
  [Rack::Sendfile]: https://www.rubydoc.info/github/rack/rack/master/Rack/Sendfile
470
561
  [`Aws::S3::Object#get`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
471
562
  [shrine resumable walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Resumable-Uploads
@@ -473,3 +564,5 @@ The tus-ruby-server was inspired by [rubytus] and [tusd].
473
564
  [minio gcp]: https://minio.io/gcp.html
474
565
  [minio azure]: https://minio.io/azure.html
475
566
  [tusd]: https://github.com/tus/tusd
567
+ [Falcon]: https://github.com/socketry/falcon
568
+ [async]: https://github.com/socketry
@@ -51,12 +51,12 @@ module Tus
51
51
  Time.parse(@hash["Upload-Expires"])
52
52
  end
53
53
 
54
- def concatenation?
55
- @hash["Upload-Concat"].to_s.start_with?("final")
54
+ def partial?
55
+ @hash["Upload-Concat"] == "partial"
56
56
  end
57
57
 
58
- def defer_length?
59
- @hash["Upload-Defer-Length"] == "1"
58
+ def final?
59
+ @hash["Upload-Concat"].to_s.start_with?("final")
60
60
  end
61
61
 
62
62
  def partial_uploads
@@ -64,6 +64,10 @@ module Tus
64
64
  urls.map { |url| url.split("/").last }
65
65
  end
66
66
 
67
+ def defer_length?
68
+ @hash["Upload-Defer-Length"] == "1"
69
+ end
70
+
67
71
  def remaining_length
68
72
  length - offset
69
73
  end
@@ -1,4 +1,5 @@
1
1
  # frozen-string-literal: true
2
+
2
3
  require "roda"
3
4
 
4
5
  require "tus/storage/filesystem"
@@ -24,11 +25,13 @@ module Tus
24
25
  ]
25
26
  SUPPORTED_CHECKSUM_ALGORITHMS = %w[sha1 sha256 sha384 sha512 md5 crc32]
26
27
  RESUMABLE_CONTENT_TYPE = "application/offset+octet-stream"
28
+ HOOKS = %i[before_create after_create after_finish after_terminate]
27
29
 
28
30
  opts[:max_size] = nil
29
31
  opts[:expiration_time] = 7*24*60*60
30
32
  opts[:disposition] = "inline"
31
33
  opts[:redirect_download] = nil
34
+ opts[:hooks] = {}
32
35
 
33
36
  plugin :all_verbs
34
37
  plugin :default_headers, {"Content-Type" => ""}
@@ -36,6 +39,16 @@ module Tus
36
39
  plugin :request_headers
37
40
  plugin :not_allowed
38
41
 
42
+ HOOKS.each do |hook|
43
+ define_singleton_method(hook) do |&block|
44
+ opts[:hooks][hook] = block
45
+ end
46
+
47
+ define_method(hook) do |*args|
48
+ instance_exec(*args, &opts[:hooks][hook]) if opts[:hooks][hook]
49
+ end
50
+ end
51
+
39
52
  route do |r|
40
53
  if request.headers["X-HTTP-Method-Override"]
41
54
  request.env["REQUEST_METHOD"] = request.headers["X-HTTP-Method-Override"]
@@ -77,7 +90,9 @@ module Tus
77
90
  "Upload-Expires" => (Time.now + expiration_time).httpdate,
78
91
  )
79
92
 
80
- if info.concatenation?
93
+ before_create(uid, info)
94
+
95
+ if info.final?
81
96
  validate_partial_uploads!(info.partial_uploads)
82
97
 
83
98
  length = storage.concatenate(uid, info.partial_uploads, info.to_h)
@@ -87,8 +102,9 @@ module Tus
87
102
  storage.create_file(uid, info.to_h)
88
103
  end
89
104
 
90
- storage.update_info(uid, info.to_h)
105
+ after_create(uid, info)
91
106
 
107
+ storage.update_info(uid, info.to_h)
92
108
  response.headers.update(info.headers)
93
109
 
94
110
  file_url = "#{request.url.chomp("/")}/#{uid}"
@@ -154,6 +170,8 @@ module Tus
154
170
 
155
171
  if info.offset == info.length # last chunk
156
172
  storage.finalize_file(uid, info.to_h) if storage.respond_to?(:finalize_file)
173
+
174
+ after_finish(uid, info)
157
175
  end
158
176
 
159
177
  storage.update_info(uid, info.to_h)
@@ -198,6 +216,8 @@ module Tus
198
216
  r.delete do
199
217
  storage.delete_file(uid, info.to_h)
200
218
 
219
+ after_terminate(uid, info)
220
+
201
221
  no_content!
202
222
  end
203
223
  end
@@ -285,34 +305,55 @@ module Tus
285
305
  if upload_concat.start_with?("final")
286
306
  string = upload_concat.split(";").last
287
307
  string.split(" ").each do |url|
288
- error!(400, "Invalid Upload-Concat header") if url !~ %r{^#{request.script_name}/\w+$}
308
+ error!(400, "Invalid Upload-Concat header") if url !~ /#{request.script_name}\/\w+$/
289
309
  end
290
310
  end
291
311
  end
292
312
 
293
313
  # Validates that each partial upload exists and is marked as one.
294
314
  def validate_partial_uploads!(part_uids)
295
- queue = Queue.new
296
- part_uids.each { |part_uid| queue << part_uid }
315
+ input = Queue.new
316
+ part_uids.each { |part_uid| input << part_uid }
317
+ input.close
297
318
 
298
- threads = 10.times.map do
319
+ results = Queue.new
320
+
321
+ thread_count = storage.concurrency[:concatenation] if storage.respond_to?(:concurrency)
322
+ thread_count ||= 10
323
+
324
+ threads = thread_count.times.map do
299
325
  Thread.new do
300
- results = []
301
- loop do
302
- part_uid = queue.deq(true) rescue break
303
- part_info = storage.read_info(part_uid)
304
- results << part_info["Upload-Concat"]
326
+ begin
327
+ loop do
328
+ part_uid = input.pop or break
329
+ part_info = storage.read_info(part_uid)
330
+ results << Tus::Info.new(part_info)
331
+ end
332
+ nil
333
+ rescue => error
334
+ input.clear
335
+ error
305
336
  end
306
- results
307
337
  end
308
338
  end
309
339
 
310
- upload_concat_values = threads.flat_map(&:value)
311
- unless upload_concat_values.all? { |value| value == "partial" }
340
+ errors = threads.map(&:value).compact
341
+
342
+ if errors.any? { |error| error.is_a?(Tus::NotFound) }
343
+ error!(400, "One or more partial uploads were not found")
344
+ elsif errors.any?
345
+ fail errors.first
346
+ end
347
+
348
+ part_infos = Array.new(results.size) { results.pop } # convert Queue into an Array
349
+
350
+ unless part_infos.all?(&:partial?)
312
351
  error!(400, "One or more uploads were not partial")
313
352
  end
314
- rescue Tus::NotFound
315
- error!(400, "One or more partial uploads were not found")
353
+
354
+ if max_size && part_infos.map(&:length).inject(0, :+) > max_size
355
+ error!(400, "The sum of partial upload lengths exceed Tus-Max-Size")
356
+ end
316
357
  end
317
358
 
318
359
  def validate_upload_checksum!(input)
@@ -16,17 +16,24 @@ module Tus
16
16
  class S3
17
17
  MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB is the minimum part size for S3 multipart uploads
18
18
 
19
- attr_reader :client, :bucket, :prefix, :upload_options
19
+ attr_reader :client, :bucket, :prefix, :upload_options, :concurrency
20
20
 
21
21
  # Initializes an aws-sdk-s3 client with the given credentials.
22
- def initialize(bucket:, prefix: nil, upload_options: {}, thread_count: 10, **client_options)
22
+ def initialize(bucket:, prefix: nil, upload_options: {}, concurrency: {}, thread_count: nil, **client_options)
23
+ fail ArgumentError, "the :bucket option was nil" unless bucket
24
+
25
+ if thread_count
26
+ warn "[Tus-Ruby-Server] :thread_count is deprecated and will be removed in the next major version, use :concurrency instead, e.g `concurrency: { concatenation: 20 }`"
27
+ concurrency[:concatenation] = thread_count
28
+ end
29
+
23
30
  resource = Aws::S3::Resource.new(**client_options)
24
31
 
25
32
  @client = resource.client
26
- @bucket = resource.bucket(bucket) or fail(ArgumentError, "the :bucket option was nil")
33
+ @bucket = resource.bucket(bucket)
27
34
  @prefix = prefix
28
35
  @upload_options = upload_options
29
- @thread_count = thread_count
36
+ @concurrency = concurrency
30
37
  end
31
38
 
32
39
  # Initiates multipart upload for the given upload, and stores its
@@ -122,7 +129,8 @@ module Tus
122
129
  end
123
130
 
124
131
  begin
125
- info["multipart_parts"] << upload_part(chunk, uid, upload_id, part_offset += 1)
132
+ part = upload_part(chunk, uid, upload_id, part_offset += 1)
133
+ info["multipart_parts"] << part
126
134
  bytes_uploaded += chunk.bytesize
127
135
  rescue Seahorse::Client::NetworkingError => exception
128
136
  warn "ERROR: #{exception.inspect} occurred during upload"
@@ -256,12 +264,21 @@ module Tus
256
264
  # given objects into them. It uses a queue and a fixed-size thread pool
257
265
  # which consumes that queue.
258
266
  def copy_parts(objects, multipart_upload)
259
- parts = compute_parts(objects, multipart_upload)
260
- queue = parts.inject(Queue.new) { |queue, part| queue << part }
267
+ parts = compute_parts(objects, multipart_upload)
268
+ input = Queue.new
269
+ results = Queue.new
270
+
271
+ parts.each { |part| input << part }
272
+ input.close
273
+
274
+ thread_count = concurrency[:concatenation] || 10
275
+ threads = thread_count.times.map { copy_part_thread(input, results) }
261
276
 
262
- threads = @thread_count.times.map { copy_part_thread(queue) }
277
+ errors = threads.map(&:value).compact
278
+ fail errors.first if errors.any?
263
279
 
264
- threads.flat_map(&:value).sort_by { |part| part["part_number"] }
280
+ part_results = Array.new(results.size) { results.pop } # convert Queue into an Array
281
+ part_results.sort_by { |part| part.fetch("part_number") }
265
282
  end
266
283
 
267
284
  # Computes data required for copying objects into new multipart parts.
@@ -279,18 +296,18 @@ module Tus
279
296
 
280
297
  # Consumes the queue for new multipart part information and issues the
281
298
  # copy requests.
282
- def copy_part_thread(queue)
299
+ def copy_part_thread(input, results)
283
300
  Thread.new do
284
301
  begin
285
- results = []
286
302
  loop do
287
- part = queue.deq(true) rescue break
288
- results << copy_part(part)
303
+ part = input.pop or break
304
+ part_result = copy_part(part)
305
+ results << part_result
289
306
  end
290
- results
291
- rescue
292
- queue.clear
293
- raise
307
+ nil
308
+ rescue => error
309
+ input.clear # clear other work
310
+ error
294
311
  end
295
312
  end
296
313
  end
@@ -1,8 +1,8 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "tus-server"
3
- gem.version = "2.1.2"
3
+ gem.version = "2.2.0"
4
4
 
5
- gem.required_ruby_version = ">= 2.2"
5
+ gem.required_ruby_version = ">= 2.3"
6
6
 
7
7
  gem.summary = "Ruby server implementation of tus.io, the open protocol for resumable file uploads."
8
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tus-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.2
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-21 00:00:00.000000000 Z
11
+ date: 2018-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: roda
@@ -148,7 +148,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: '2.2'
151
+ version: '2.3'
152
152
  required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  requirements:
154
154
  - - ">="