UtopiaSourceUtopiaStaticLocalFile

class LocalFile

Represents a local file on disk which can be served directly, or passed upstream to sendfile.

Definitions

def initialize(root, path)

Initialize metadata for a file beneath a static root.

Signature

parameter root String

The root directory.

parameter path Utopia::Path | String

The path.

Implementation

def initialize(root, path)
	@root = root
	@path = path
	@etag = Digest::SHA1.hexdigest("#{File.size(full_path)}#{mtime_date}")
	
	@range = nil
end

def to_path

Fit in with Rack::Sendfile

Implementation

def to_path
	full_path
end

def full_path

Resolve this file beneath its configured root.

Signature

returns String

The full filesystem path.

Implementation

def full_path
	File.join(@root, @path.components)
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
	File.mtime(full_path).httpdate
end

def bytesize

Measure the file's content length.

Signature

returns Integer

The file size in bytes.

Implementation

def bytesize
	File.size(full_path)
end

def empty?

This reflects whether calling each would yield anything.

Implementation

def empty?
	bytesize == 0
end

def each

Enumerate the contained values.

Signature

returns Enumerator

An enumerator over the resulting values.

Implementation

def each
	File.open(full_path, "rb") do |file|
		file.seek(@range.begin)
		remaining = @range.end - @range.begin+1
		
		while remaining > 0
			break unless part = file.read([8192, remaining].min)
			
			remaining -= part.length
			
			yield part
		end
	end
end

def modified?(env)

Check whether the file has changed since the request validators.

Signature

parameter env Hash

The Rack environment.

returns Boolean

Whether the file is newer than the request validators.

Implementation

def modified?(env)
	if modified_since = env["HTTP_IF_MODIFIED_SINCE"]
		return false if File.mtime(full_path) <= Time.parse(modified_since)
	end
	
	if etags = env["HTTP_IF_NONE_MATCH"]
		etags = etags.split(/\s*,\s*/)
		return false if etags.include?(etag) || etags.include?("*")
	end
	
	return true
end

def serve(env, response_headers)

Serve the file, honoring a single byte range when requested.

Signature

parameter env Hash

The Rack environment.

parameter response_headers Hash

The response headers.

returns Array

The Rack response.

Implementation

def serve(env, response_headers)
	ranges = Rack::Utils.get_byte_ranges(env["HTTP_RANGE"], size)
	response = [200, response_headers, self]
	
	# 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:
		response[0] = 200
		response[1][CONTENT_LENGTH] = size.to_s
		@range = 0...size
	else
		# Partial content:
		@range = ranges[0]
		partial_size = @range.size
		
		response[0] = 206
		response[1][CONTENT_LENGTH] = partial_size.to_s
		response[1][CONTENT_RANGE] = "bytes #{@range.min}-#{@range.max}/#{size}"
	end
	
	return response
end