Class: Tus::Server

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

Defined Under Namespace

Classes: Goliath

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



346
347
348
349
350
# File 'lib/tus/server.rb', line 346

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

#error!(status, message) ⇒ Object



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

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

#expiration_intervalObject



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

def expiration_interval
  opts[:expiration_interval]
end

#expiration_timeObject



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

def expiration_time
  opts[:expiration_time]
end

#get_input(info) ⇒ Object



180
181
182
183
184
185
186
# File 'lib/tus/server.rb', line 180

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



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/tus/server.rb', line 325

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

“Range” header handling logic copied from Rack::File



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/tus/server.rb', line 300

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

  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. Return error, and file size:
    response.headers["Content-Range"] = "bytes */#{length}"
    error!(416, "Byte range unsatisfiable")
  else
    # Partial content:
    range = ranges[0]
    response.status = 206
    response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{length}"
  end

  range
end

#max_sizeObject



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

def max_size
  opts[:max_size]
end

#no_content!Object



341
342
343
344
# File 'lib/tus/server.rb', line 341

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

#storageObject



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

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

#validate_content_length!(size, info) ⇒ Object



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

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



188
189
190
# File 'lib/tus/server.rb', line 188

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

#validate_partial_uploads!(part_uids) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/tus/server.rb', line 265

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!(404, "One or more partial uploads were not found")
end

#validate_tus_resumable!Object



192
193
194
195
196
197
198
199
# File 'lib/tus/server.rb', line 192

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



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

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



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

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



234
235
236
# File 'lib/tus/server.rb', line 234

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

#validate_upload_length!Object



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

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



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/tus/server.rb', line 238

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



213
214
215
216
217
218
219
220
221
222
223
# File 'lib/tus/server.rb', line 213

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