class LocalFile

Represents a local static resource and constructs responses for it.

Definitions

def initialize(path)

Initialize metadata for a local file.

Signature

parameter path String

The resolved filesystem path.

Implementation

def initialize(path)
	@path = path
	@stat = File.stat(@path)
	@mtime_date = @stat.mtime.httpdate
	
	fingerprint = Digest::SHA1.hexdigest("#{@stat.size}:#{@stat.mtime.to_i}:#{@stat.mtime.nsec}")
	@etag = %Q{W/"#{fingerprint}"}
end

def mtime_date

Format the file's modification time for an HTTP header.

Signature

returns String

The HTTP-date modification time.

Implementation

def mtime_date
	@mtime_date
end

def bytesize

Measure the file's content length.

Signature

returns Integer

The file size in bytes.

Implementation

def bytesize
	@stat.size
end

def modified?(request)

Check whether the file has changed since the request validators.

Signature

parameter request Utopia::Request

The request.

returns Boolean

Whether the file is newer than the request validators.

Implementation

def modified?(request)
	if etags = request.headers["if-none-match"]
		return !etags.weak_match?(@etag)
	end
	
	if modified_since = request.headers["if-modified-since"]
		return @stat.mtime.to_i > modified_since.to_time.to_i
	end
	
	return true
end

def serve(request, response_headers)

Serve.

Signature

parameter request Utopia::Request

The request.

parameter response_headers Hash

The response headers.

returns Protocol::HTTP::Response

The response.

Implementation

def serve(request, response_headers)
	ranges = byte_ranges(request)
	size = bytesize
	
	# puts "Requesting ranges: #{ranges.inspect} (#{size})"
	
	if ranges == nil or ranges.size != 1
		# No ranges, or multiple ranges (which we don't support).
		# TODO: Support multiple byte-ranges, for now just send entire file:
		status = 200
		response_headers[CONTENT_LENGTH] = size.to_s
		range = nil
	else
		# Partial content:
		range = ranges[0]
		partial_size = range.size
		
		status = 206
		response_headers[CONTENT_LENGTH] = partial_size.to_s
		response_headers[CONTENT_RANGE] = "bytes #{range.min}-#{range.max}/#{size}"
	end
	
	if request.head?
		body = Protocol::HTTP::Body::Head.new(size)
	else
		body = Protocol::HTTP::Body::File.open(@path, range, size: size)
	end
	
	return Response[status, response_headers, body]
end

def byte_ranges(request)

Resolve satisfiable byte ranges from the parsed range header.

Signature

parameter request Utopia::Request

The request.

returns Array | Nil

The resulting values, or nil if the range is not applicable.

Implementation

def byte_ranges(request)
	return nil unless request.method == Protocol::HTTP::Methods::GET
	
	range = request.headers["range"]
	return nil unless range&.bytes?
	
	if validator = request.headers["if-range"]
		return nil unless if_range?(validator)
	end
	
	return range.resolve(bytesize)
end