using Inversion; using Astralis; using Invercargill; using Invercargill.DataStructures; namespace Spry { public class FileStaticResource : Object, StaticResource { private string _name; private string _content_type; private string _e_tag; private ImmutableBuffer _encodings; private Dictionary _encoding_mapping; public string file_path { get; private set; } public uint64 header_length { get; private set; } public FileStaticResource(string path) throws Error { this.file_path = path; var stream = new DataInputStream(File.new_for_path(path).read()); read_header(stream); stream.close(); } public string get_best_encoding (Set supported) { return _encodings.first_or_default(e => supported.has(e.type))?.type ?? "identity"; } public HttpResult to_result(string encoding, StatusCode status = StatusCode.OK) throws Error { return new StreamingResult(this, _encoding_mapping[encoding], status); } public string name { get { return _name; } } public string content_type { get { return _content_type; } } public string get_etag_for (string encoding) { return @"\"$encoding-$_e_tag\""; } private void read_header(DataInputStream stream) throws Error { var magic = new uint8[8]; stream.read(magic); if(!Wrap.array(magic).equals(Iterate.these('s', 'p', 'r', 'y', '-', 's', 'r', 0))) { throw new IOError.INVALID_DATA("Incorrect magic number on Spry Static Resource file."); } _name = read_string_field(stream); _content_type = read_string_field(stream); var hash_len = (size_t)64; var hash_composition = new ByteComposition(); while(hash_composition.read_in (stream.read_bytes(hash_len), ref hash_len)); _e_tag = hash_composition.to_base64(); var encodings = new Buffer(stream.read_byte()); for(var i = 0; i < encodings.length; i++) { encodings[i] = new EncodingHeader(stream); } _encodings = encodings .order_by(e => e.size) .to_immutable_buffer(); _encoding_mapping = encodings .to_dictionary(e => e.type); header_length = stream.tell(); if(!_encoding_mapping.has("identity")) { throw new IOError.INVALID_DATA("Spry Static Resource file does not contain required \'identity\' encoding."); } } private static string read_string_field(DataInputStream stream) throws Error { var len = (size_t)stream.read_byte(); var composition = new ByteComposition(); while(composition.read_in (stream.read_bytes(len), ref len)); return composition.to_raw_string(); } private class EncodingHeader { public string type; public uint64 offset; public uint64 size; public EncodingHeader(DataInputStream stream) throws Error { type = read_string_field(stream); offset = stream.read_uint64(); size = stream.read_uint64(); } } private class StreamingResult : HttpResult { private FileStaticResource resource; private EncodingHeader encoding; public StreamingResult(FileStaticResource resource, EncodingHeader encoding, StatusCode status) { base(status); this.resource = resource; this.encoding = encoding; set_header("Content-Type", resource.content_type); set_header("Content-Encoding", encoding.type); set_header("Content-Length", encoding.size.to_string()); set_header("ETag", resource.get_etag_for(encoding.type)); } public async override void send_body(Astralis.AsyncOutput output) throws Error { var stream = yield File.new_for_path(resource.file_path).read_async(); var data_len = encoding.size; stream.seek((int64)resource.header_length, SeekType.SET); while(data_len > 0) { var to_read = (size_t)uint64.min(data_len, 1024 * 1024 * 4); var bytes = yield stream.read_bytes_async(to_read); data_len -= bytes.length; yield output.write(Wrap.bytes(bytes)); } } } } }