Class: Tus::Server

Inherits:
Roda
  • Object
show all
Defined in:
lib/tus/server.rb

Constant Summary collapse

SUPPORTED_VERSIONS =
["1.0.0"]
SUPPORTED_EXTENSIONS =
[
  "creation", "creation-defer-length",
  "termination",
  "expiration",
  "concatenation",
  "checksum",
]
SUPPORTED_CHECKSUM_ALGORITHMS =
%w[sha1 sha256 sha384 sha512 md5 crc32]
RESUMABLE_CONTENT_TYPE =
"application/offset+octet-stream"
HOOKS =
i[before_create after_create after_finish after_terminate]

Instance Method Summary collapse

Instance Method Details

#created!(location) ⇒ Object



419
420
421
422
423
# File 'lib/tus/server.rb', line 419

def created!(location)
  response.status = 201
  response.headers["Location"] = location
  request.halt
end

#error!(status, message) ⇒ Object



425
426
427
428
429
430
# File 'lib/tus/server.rb', line 425

def error!(status, message)
  response.status = status
  response.write(message) unless request.head?
  response.headers["Content-Type"] = "text/plain"
  request.halt
end

#expiration_intervalObject



457
458
459
# File 'lib/tus/server.rb', line 457

def expiration_interval
  opts[:expiration_interval]
end

#expiration_timeObject



453
454
455
# File 'lib/tus/server.rb', line 453

def expiration_time
  opts[:expiration_time]
end

#get_input(info) ⇒ Object

Wraps the Rack input (request body) into a Tus::Input object, applying a size limit if one exists.



228
229
230
231
232
233
234
# File 'lib/tus/server.rb', line 228

def get_input(info)
  offset = info.offset
  total  = info.length || max_size
  limit  = total - offset if total

  Tus::Input.new(request.body, limit: limit)
end

#handle_cors!Object



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/tus/server.rb', line 398

def handle_cors!
  origin = request.headers["Origin"]

  return if origin.to_s == ""

  response.headers["Access-Control-Allow-Origin"] = origin

  if request.options?
    response.headers["Access-Control-Allow-Methods"] = "POST, GET, HEAD, PATCH, DELETE, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata"
    response.headers["Access-Control-Max-Age"]       = "86400"
  else
    response.headers["Access-Control-Expose-Headers"] = "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata"
  end
end

#handle_range_request!(length) ⇒ Object

Handles partial responses requested in the “Range” header. Implementation is mostly copied from Rack::File.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/tus/server.rb', line 371

def handle_range_request!(length)
  if Rack.release >= "2.0"
    ranges = Rack::Utils.get_byte_ranges(request.headers["Range"], length)
  else
    ranges = Rack::Utils.byte_ranges(request.env, length)
  end

  # we support ranged requests
  response.headers["Accept-Ranges"] = "bytes"

  if ranges.nil? || ranges.length > 1
    # no ranges, or multiple ranges (which we don't support)
    response.status = 200
    range = 0..length-1
  elsif ranges.empty?
    # unsatisfiable range
    response.headers["Content-Range"] = "bytes */#{length}"
    error!(416, "Byte range unsatisfiable")
  else
    range = ranges[0]
    response.status = 206
    response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{length}"
  end

  range
end

#max_sizeObject



449
450
451
# File 'lib/tus/server.rb', line 449

def max_size
  opts[:max_size]
end

#no_content!Object



414
415
416
417
# File 'lib/tus/server.rb', line 414

def no_content!
  response.status = 204
  request.halt
end

#redirect_downloadObject



432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/tus/server.rb', line 432

def redirect_download
  value = opts[:redirect_download]

  if opts[:download_url]
    value ||= opts[:download_url]
    warn "[TUS-RUBY-SERVER DEPRECATION] The :download_url option has been renamed to :redirect_download."
  end

  value = storage.method(:file_url) if value == true

  value
end

#storageObject



445
446
447
# File 'lib/tus/server.rb', line 445

def storage
  opts[:storage] || Tus::Storage::Filesystem.new("data")
end

#validate_content_length!(size, info) ⇒ Object



273
274
275
276
277
278
279
280
# File 'lib/tus/server.rb', line 273

def validate_content_length!(size, info)
  if info.length
    error!(403, "Cannot modify completed upload") if info.offset == info.length
    error!(413, "Size of this chunk surpasses Upload-Length") if info.offset + size > info.length
  elsif max_size
    error!(413, "Size of this chunk surpasses Tus-Max-Size") if info.offset + size > max_size
  end
end

#validate_content_type!Object



236
237
238
# File 'lib/tus/server.rb', line 236

def validate_content_type!
  error!(415, "Invalid Content-Type header") if request.content_type != RESUMABLE_CONTENT_TYPE
end

#validate_partial_uploads!(part_uids) ⇒ Object

Validates that each partial upload exists and is marked as one.



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/tus/server.rb', line 314

def validate_partial_uploads!(part_uids)
  input = Queue.new
  part_uids.each { |part_uid| input << part_uid }
  input.close

  results = Queue.new

  thread_count   = storage.concurrency[:concatenation] if storage.respond_to?(:concurrency)
  thread_count ||= 10

  threads = thread_count.times.map do
    Thread.new do
      begin
        loop do
          part_uid = input.pop or break
          part_info = storage.read_info(part_uid)
          results << Tus::Info.new(part_info)
        end
        nil
      rescue => error
        input.clear
        error
      end
    end
  end

  errors = threads.map(&:value).compact

  if errors.any? { |error| error.is_a?(Tus::NotFound) }
    error!(400, "One or more partial uploads were not found")
  elsif errors.any?
    fail errors.first
  end

  part_infos = Array.new(results.size) { results.pop } # convert Queue into an Array

  unless part_infos.all?(&:partial?)
    error!(400, "One or more uploads were not partial")
  end

  if max_size && part_infos.map(&:length).inject(0, :+) > max_size
    error!(400, "The sum of partial upload lengths exceed Tus-Max-Size")
  end
end

#validate_tus_resumable!Object



240
241
242
243
244
245
246
247
# File 'lib/tus/server.rb', line 240

def validate_tus_resumable!
  client_version = request.headers["Tus-Resumable"]

  unless SUPPORTED_VERSIONS.include?(client_version)
    response.headers["Tus-Version"] = SUPPORTED_VERSIONS.join(",")
    error!(412, "Unsupported version")
  end
end

#validate_upload_checksum!(input) ⇒ Object



359
360
361
362
363
364
365
366
367
# File 'lib/tus/server.rb', line 359

def validate_upload_checksum!(input)
  algorithm, checksum = request.headers["Upload-Checksum"].split(" ")

  error!(400, "Invalid Upload-Checksum header") if algorithm.nil? || checksum.nil?
  error!(400, "Invalid Upload-Checksum header") unless SUPPORTED_CHECKSUM_ALGORITHMS.include?(algorithm)

  generated_checksum = Tus::Checksum.generate(algorithm, input)
  error!(460, "Upload-Checksum value doesn't match generated checksum") if generated_checksum != checksum
end

#validate_upload_concat!Object



300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/tus/server.rb', line 300

def validate_upload_concat!
  upload_concat = request.headers["Upload-Concat"]

  error!(400, "Invalid Upload-Concat header") if upload_concat !~ /^(partial|final)/

  if upload_concat.start_with?("final")
    string = upload_concat.split(";").last
    string.split(" ").each do |url|
      error!(400, "Invalid Upload-Concat header") if url !~ /#{request.script_name}\/\w+$/
    end
  end
end

#validate_upload_finished!(info) ⇒ Object



282
283
284
# File 'lib/tus/server.rb', line 282

def validate_upload_finished!(info)
  error!(403, "Cannot download unfinished upload") unless info.length == info.offset
end

#validate_upload_length!Object



249
250
251
252
253
254
255
256
257
258
259
# File 'lib/tus/server.rb', line 249

def validate_upload_length!
  upload_length = request.headers["Upload-Length"]

  error!(400, "Missing Upload-Length header") if upload_length.to_s == ""
  error!(400, "Invalid Upload-Length header") if upload_length =~ /\D/
  error!(400, "Invalid Upload-Length header") if upload_length.to_i < 0

  if max_size && upload_length.to_i > max_size
    error!(413, "Upload-Length header too large")
  end
end

#validate_upload_metadata!Object



286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/tus/server.rb', line 286

def validate_upload_metadata!
   = request.headers["Upload-Metadata"]

  .split(",").each do |string|
    key, value = string.split(" ", 2)

    error!(400, "Invalid Upload-Metadata header") if key.nil?
    error!(400, "Invalid Upload-Metadata header") if key.ord > 127
    error!(400, "Invalid Upload-Metadata header") if key =~ /,| /

    error!(400, "Invalid Upload-Metadata header") if value =~ /[^a-zA-Z0-9+\/=]/
  end
end

#validate_upload_offset!(info) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/tus/server.rb', line 261

def validate_upload_offset!(info)
  upload_offset = request.headers["Upload-Offset"]

  error!(400, "Missing Upload-Offset header") if upload_offset.to_s == ""
  error!(400, "Invalid Upload-Offset header") if upload_offset =~ /\D/
  error!(400, "Invalid Upload-Offset header") if upload_offset.to_i < 0

  if upload_offset.to_i != info.offset
    error!(409, "Upload-Offset header doesn't match current offset")
  end
end