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



356
357
358
359
360
# File 'lib/tus/server.rb', line 356

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

#error!(status, message) ⇒ Object



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

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

#expiration_intervalObject



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

def expiration_interval
  opts[:expiration_interval]
end

#expiration_timeObject



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

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.



186
187
188
189
190
191
192
# File 'lib/tus/server.rb', line 186

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



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/tus/server.rb', line 335

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.



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
# File 'lib/tus/server.rb', line 308

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



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

def max_size
  opts[:max_size]
end

#no_content!Object



351
352
353
354
# File 'lib/tus/server.rb', line 351

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

#storageObject



369
370
371
# File 'lib/tus/server.rb', line 369

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

#validate_content_length!(size, info) ⇒ Object



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

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



194
195
196
# File 'lib/tus/server.rb', line 194

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.



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

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



198
199
200
201
202
203
204
205
# File 'lib/tus/server.rb', line 198

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



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

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



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

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



240
241
242
# File 'lib/tus/server.rb', line 240

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

#validate_upload_length!Object



207
208
209
210
211
212
213
214
215
216
217
# File 'lib/tus/server.rb', line 207

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



244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/tus/server.rb', line 244

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



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

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