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



411
412
413
414
415
# File 'lib/tus/server.rb', line 411

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

#error!(status, message) ⇒ Object



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

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

#expiration_intervalObject



436
437
438
# File 'lib/tus/server.rb', line 436

def expiration_interval
  opts[:expiration_interval]
end

#expiration_timeObject



432
433
434
# File 'lib/tus/server.rb', line 432

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.



221
222
223
224
225
226
227
# File 'lib/tus/server.rb', line 221

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



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/tus/server.rb', line 390

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, Upload-Defer-Length, Upload-Concat"
    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, Upload-Defer-Length, Upload-Concat"
  end
end

#handle_range_request!(length) ⇒ Object

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



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/tus/server.rb', line 348

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

  response.headers["Content-Length"] = range.size.to_s

  range
end

#max_sizeObject



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

def max_size
  opts[:max_size]
end

#no_content!Object



406
407
408
409
# File 'lib/tus/server.rb', line 406

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

#redirect_downloadObject



377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/tus/server.rb', line 377

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



424
425
426
# File 'lib/tus/server.rb', line 424

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

#validate_content_length!(size, info) ⇒ Object



266
267
268
269
270
271
272
273
# File 'lib/tus/server.rb', line 266

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



229
230
231
# File 'lib/tus/server.rb', line 229

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, and at the same time calculates the sum of part lengths.



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/tus/server.rb', line 308

def validate_partial_uploads!(part_uids)
  length = 0

  part_uids.each do |part_uid|
    begin
      part_info = storage.read_info(part_uid)
    rescue Tus::NotFound
      error!(400, "Partial upload not found")
    end

    part_info = Tus::Info.new(part_info)

    error!(400, "Upload is not partial") unless part_info.partial?

    unless part_info.length == part_info.offset
      error!(400, "Partial upload is not finished")
    end

    length += part_info.length
  end

  if max_size && length > max_size
    error!(400, "The sum of partial upload lengths exceeds Tus-Max-Size")
  end

  length
end

#validate_tus_resumable!Object



233
234
235
236
237
238
239
240
# File 'lib/tus/server.rb', line 233

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



336
337
338
339
340
341
342
343
344
# File 'lib/tus/server.rb', line 336

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



293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/tus/server.rb', line 293

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



275
276
277
# File 'lib/tus/server.rb', line 275

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

#validate_upload_length!Object



242
243
244
245
246
247
248
249
250
251
252
# File 'lib/tus/server.rb', line 242

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



279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/tus/server.rb', line 279

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



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/tus/server.rb', line 254

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