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"

Instance Method Summary collapse

Instance Method Details

#created!(location) ⇒ Object



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

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

#error!(status, message) ⇒ Object



365
366
367
368
369
370
# File 'lib/tus/server.rb', line 365

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

#expiration_intervalObject



384
385
386
# File 'lib/tus/server.rb', line 384

def expiration_interval
  opts[:expiration_interval]
end

#expiration_timeObject



380
381
382
# File 'lib/tus/server.rb', line 380

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.



189
190
191
192
193
194
195
# File 'lib/tus/server.rb', line 189

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



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/tus/server.rb', line 338

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.



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

def handle_range_request!(length)
  # we support ranged requests
  response.headers["Accept-Ranges"] = "bytes"

  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

  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



376
377
378
# File 'lib/tus/server.rb', line 376

def max_size
  opts[:max_size]
end

#no_content!Object



354
355
356
357
# File 'lib/tus/server.rb', line 354

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

#storageObject



372
373
374
# File 'lib/tus/server.rb', line 372

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

#validate_content_length!(size, info) ⇒ Object



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

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



197
198
199
# File 'lib/tus/server.rb', line 197

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.



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/tus/server.rb', line 275

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

  threads = 10.times.map do
    Thread.new do
      results = []
      loop do
        part_uid = queue.deq(true) rescue break
        part_info = storage.read_info(part_uid)
        results << part_info["Upload-Concat"]
      end
      results
    end
  end

  upload_concat_values = threads.flat_map(&:value)
  unless upload_concat_values.all? { |value| value == "partial" }
    error!(400, "One or more uploads were not partial")
  end
rescue Tus::NotFound
  error!(400, "One or more partial uploads were not found")
end

#validate_tus_resumable!Object



201
202
203
204
205
206
207
208
# File 'lib/tus/server.rb', line 201

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



299
300
301
302
303
304
305
306
307
# File 'lib/tus/server.rb', line 299

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



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

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 !~ %r{^#{request.script_name}/\w+$}
    end
  end
end

#validate_upload_finished!(info) ⇒ Object



243
244
245
# File 'lib/tus/server.rb', line 243

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

#validate_upload_length!Object



210
211
212
213
214
215
216
217
218
219
220
# File 'lib/tus/server.rb', line 210

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



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

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

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

    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



222
223
224
225
226
227
228
229
230
231
232
# File 'lib/tus/server.rb', line 222

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