Class: Utopia::Static::LocalFile

Inherits:
Object
  • Object
show all
Defined in:
lib/utopia/static/local_file.rb

Overview

Represents a local static resource and constructs responses for it.

Constant Summary collapse

CONTENT_LENGTH =
"content-length".freeze
CONTENT_RANGE =
"content-range".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ LocalFile

Initialize metadata for a local file.



22
23
24
25
26
27
28
29
# File 'lib/utopia/static/local_file.rb', line 22

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

Instance Attribute Details

#etagObject (readonly)

Returns the value of attribute etag.



32
33
34
# File 'lib/utopia/static/local_file.rb', line 32

def etag
  @etag
end

#pathObject (readonly)

Returns the value of attribute path.



31
32
33
# File 'lib/utopia/static/local_file.rb', line 31

def path
  @path
end

Instance Method Details

#byte_ranges(request) ⇒ Object

Resolve satisfiable byte ranges from the parsed range header.



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/utopia/static/local_file.rb', line 102

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

#bytesizeObject

Measure the file's content length.



42
43
44
# File 'lib/utopia/static/local_file.rb', line 42

def bytesize
	@stat.size
end

#modified?(request) ⇒ Boolean

Check whether the file has changed since the request validators.

Returns:

  • (Boolean)


49
50
51
52
53
54
55
56
57
58
59
# File 'lib/utopia/static/local_file.rb', line 49

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

#mtime_dateObject

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



36
37
38
# File 'lib/utopia/static/local_file.rb', line 36

def mtime_date
	@mtime_date
end

#serve(request, response_headers) ⇒ Object

Serve.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/utopia/static/local_file.rb', line 68

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