# frozen_string_literal: truerequire'time'require'zlib'require'stringio'require'digest/sha1'require_relative'status'require_relative'mime_types'moduleQeweneymoduleStaticFileCachingclass<<selfdeffile_stat_to_etag(stat)"#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"enddeffile_stat_to_last_modified(stat)stat.mtime.httpdateendendendmoduleResponseMethodsdefupgrade(protocol,custom_headers=nil)upgrade_headers={':status'=>Status::SWITCHING_PROTOCOLS,'Upgrade'=>protocol,'Connection'=>'upgrade'}upgrade_headers.merge!(custom_headers)ifcustom_headersrespond(nil,upgrade_headers)endWEBSOCKET_GUID='258EAFA5-E914-47DA-95CA-C5AB0DC85B11'defupgrade_to_websocket(custom_headers=nil)key="#{headers['sec-websocket-key']}#{WEBSOCKET_GUID}"upgrade_headers={'Sec-WebSocket-Accept'=>Digest::SHA1.base64digest(key)}upgrade_headers.merge!(custom_headers)ifcustom_headersupgrade('websocket',upgrade_headers)adapter.websocket_connection(self)enddefredirect(url,status=Status::FOUND)respond(nil,':status'=>status,'Location'=>url)enddefredirect_to_https(status=Status::MOVED_PERMANENTLY)secure_uri="https://#{host}#{uri}"redirect(secure_uri,status)enddefredirect_to_host(new_host,status=Status::FOUND)secure_uri="//#{new_host}#{uri}"redirect(secure_uri,status)enddefserve_file(path,opts={})full_path=file_full_path(path,opts)stat=File.stat(full_path)etag=StaticFileCaching.file_stat_to_etag(stat)last_modified=StaticFileCaching.file_stat_to_last_modified(stat)ifvalidate_static_file_cache(etag,last_modified)returnrespond(nil,{':status'=>Status::NOT_MODIFIED,'etag'=>etag})endmime_type=Qeweney::MimeTypes[File.extname(path)]opts[:stat]=stat(opts[:headers]||={})['Content-Type']||=mime_typeifmime_typerespond_with_static_file(full_path,etag,last_modified,opts)rescueErrno::ENOENTrespond(nil,':status'=>Status::NOT_FOUND)enddefrespond_with_static_file(path,etag,last_modified,opts)cache_headers={'etag'=>etag,'last-modified'=>last_modified,}File.open(path,'r')do|f|ifopts[:headers]opts[:headers].merge!(cache_headers)elseopts[:headers]=cache_headersend# accept_encoding should return encodings in client's order of preferenceaccept_encoding.eachdo|encoding|caseencodingwhen'deflate'returnserve_io_deflate(f,opts)when'gzip'returnserve_io_gzip(f,opts)endendserve_io(f,opts)endenddefvalidate_static_file_cache(etag,last_modified)if(none_match=headers['if-none-match'])returntrueifnone_match==etagendif(modified_since=headers['if-modified-since'])returntrueifmodified_since==last_modifiedendfalseenddeffile_full_path(path,opts)if(base_path=opts[:base_path])File.join(opts[:base_path],path)elsepathendenddefserve_io(io,opts)respond(io.read,opts[:headers]||{})enddefserve_io_deflate(io,opts)deflate=Zlib::Deflate.newheaders=opts[:headers].merge('content-encoding'=>'deflate','vary'=>'Accept-Encoding')respond(deflate.deflate(io.read,Zlib::FINISH),headers)enddefserve_io_gzip(io,opts)buf=StringIO.newz=Zlib::GzipWriter.new(buf)z<<io.readz.flushz.closeheaders=opts[:headers].merge('content-encoding'=>'gzip','vary'=>'Accept-Encoding')respond(buf.string,headers)enddefserve_rack(app)response=app.(Qeweney.rack_env_from_request(self))headers=(response[1]||{}).merge(':status'=>response[0])respond(response[2].join,headers)# TODO: send separate chunks for multi-part body# TODO: add support for streaming body# TODO: add support for websocketendendend