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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +153 -60
- data/lib/tus/info.rb +8 -4
- data/lib/tus/server.rb +57 -16
- data/lib/tus/storage/s3.rb +34 -17
- data/tus-server.gemspec +2 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9ba14174a689878aa1224bdb2e526d869b888dc85164ec3e9320c8a171bcd73
|
4
|
+
data.tar.gz: 61e227c29273bd00007a96522a16b5f470fc433741999b072bf1c3642e13289b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4914ab70cda026beee4de2b830778e43ef32131ddb65f470086d5cfd036f9dd046fafb09deced577d64965d539bf08a24ec44b810a77052822efde595610b04
|
7
|
+
data.tar.gz: 3c3f0d9196654541f5ebee297130c3d216dc1948a8a5c29695dc3251d969af85a101b0e249a889ec140e8f837c1fba1269a977649926d537c02dd5cd249d4e14
|
data/CHANGELOG.md
CHANGED
@@ -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`
|
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
|
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
|
-
|
58
|
-
walkthrough] that adds resumable uploads from scratch, and for a
|
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
|
-
###
|
61
|
+
### Streaming web server
|
62
62
|
|
63
|
-
Running the tus server alongside your main app using
|
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
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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 "
|
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
|
-
$
|
106
|
+
$ falcon serve # reads from config.ru and starts the server
|
107
107
|
```
|
108
108
|
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
|
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
|
-
|
122
|
-
Goliath app:
|
116
|
+
# ... configure tus-ruby-server ...
|
123
117
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
data/lib/tus/info.rb
CHANGED
@@ -51,12 +51,12 @@ module Tus
|
|
51
51
|
Time.parse(@hash["Upload-Expires"])
|
52
52
|
end
|
53
53
|
|
54
|
-
def
|
55
|
-
@hash["Upload-Concat"]
|
54
|
+
def partial?
|
55
|
+
@hash["Upload-Concat"] == "partial"
|
56
56
|
end
|
57
57
|
|
58
|
-
def
|
59
|
-
@hash["Upload-
|
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
|
data/lib/tus/server.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 !~
|
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
|
-
|
296
|
-
part_uids.each { |part_uid|
|
315
|
+
input = Queue.new
|
316
|
+
part_uids.each { |part_uid| input << part_uid }
|
317
|
+
input.close
|
297
318
|
|
298
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|
-
|
311
|
-
|
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
|
-
|
315
|
-
|
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)
|
data/lib/tus/storage/s3.rb
CHANGED
@@ -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:
|
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)
|
33
|
+
@bucket = resource.bucket(bucket)
|
27
34
|
@prefix = prefix
|
28
35
|
@upload_options = upload_options
|
29
|
-
@
|
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
|
-
|
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
|
260
|
-
|
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
|
-
|
277
|
+
errors = threads.map(&:value).compact
|
278
|
+
fail errors.first if errors.any?
|
263
279
|
|
264
|
-
|
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(
|
299
|
+
def copy_part_thread(input, results)
|
283
300
|
Thread.new do
|
284
301
|
begin
|
285
|
-
results = []
|
286
302
|
loop do
|
287
|
-
part =
|
288
|
-
|
303
|
+
part = input.pop or break
|
304
|
+
part_result = copy_part(part)
|
305
|
+
results << part_result
|
289
306
|
end
|
290
|
-
|
291
|
-
rescue
|
292
|
-
|
293
|
-
|
307
|
+
nil
|
308
|
+
rescue => error
|
309
|
+
input.clear # clear other work
|
310
|
+
error
|
294
311
|
end
|
295
312
|
end
|
296
313
|
end
|
data/tus-server.gemspec
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
Gem::Specification.new do |gem|
|
2
2
|
gem.name = "tus-server"
|
3
|
-
gem.version = "2.
|
3
|
+
gem.version = "2.2.0"
|
4
4
|
|
5
|
-
gem.required_ruby_version = ">= 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.
|
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-
|
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.
|
151
|
+
version: '2.3'
|
152
152
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
153
|
requirements:
|
154
154
|
- - ">="
|