12module remote34import net.http5import net.urllib67pub struct Web {8pub mut:9 base string10 11 on_body fn (received u64) = unsafe { nil }12}1314pub fn new_web(base string) !Web {15 return Web{ base: kepr_host(base)! }16}17181920pub fn kepr_host(value string) !string {21 for letter in value {22 if letter <= ` ` {23 return error('"${value}" is not a Kepr address — try kepr.uk')24 }25 }26 text := if value.starts_with('http://') || value.starts_with('https://') {27 value28 } else {29 'https://${value}'30 }31 parsed := urllib.parse(text) or { return error('"${value}" is not a Kepr address — try kepr.uk') }32 if parsed.scheme !in ['http', 'https'] || parsed.host == '' || parsed.path.trim_right('/') != '' || parsed.raw_query != '' || parsed.fragment != '' {33 return error('"${value}" is not a Kepr address — try kepr.uk')34 }35 if text.all_after('//').contains('@') {36 return error('"${value}" is not a Kepr address — try kepr.uk')37 }38 39 40 if parsed.scheme == 'http' && parsed.hostname().to_lower() !in ['127.0.0.1', 'localhost', '::1'] {41 return error('"${value}" needs https — plain http is only for a Kepr on this machine')42 }43 44 return '${parsed.scheme}://${parsed.host.to_lower()}'45}4647pub fn (mut w Web) get(key string) !Object {48 response := http.fetch(49 url: w.url(key)50 validate: true51 verify: tls_roots(w.url(key))!52 method: .get53 enable_http2: false54 allow_redirect: false55 )!56 if response.status_code == 404 {57 return Object{}58 }59 if response.status_code < 200 || response.status_code >= 300 {60 return error('web GET ${key} failed: ${response.status_code}')61 }62 return Object{63 found: true64 body: response.body.bytes()65 tag: response.header.get_custom('etag', http.HeaderQueryConfig{}) or { '' }66 }67}68697071pub fn (mut w Web) stream(key string, sink fn (piece []u8) !) !bool {72 on_body := w.on_body73 response := http.fetch(74 url: w.url(key)75 validate: true76 verify: tls_roots(w.url(key))!77 method: .get78 enable_http2: false79 allow_redirect: false80 81 stop_copying_limit: 64 * 102482 on_progress_body: fn [sink, on_body] (_ &http.Request, piece []u8, received u64, _ u64, status int) ! {83 84 85 if status < 200 || status >= 300 || received == 0 {86 return87 }88 sink(piece)!89 if on_body != unsafe { nil } && (received - u64(piece.len)) >> 20 != received >> 20 {90 on_body(received)91 }92 }93 )!94 if response.status_code == 404 {95 return false96 }97 if response.status_code < 200 || response.status_code >= 300 {98 return error('web GET ${key} failed: ${response.status_code}')99 }100 return true101}102103pub fn (mut w Web) list(prefix string) ![]string {104 response := http.fetch(105 url: w.url(prefix)106 validate: true107 verify: tls_roots(w.url(prefix))!108 method: .get109 enable_http2: false110 allow_redirect: false111 )!112 if response.status_code == 404 {113 return []string{}114 }115 if response.status_code < 200 || response.status_code >= 300 {116 return error('web GET ${prefix} failed: ${response.status_code}')117 }118 mut result := []string{}119 for id in response.body.split_into_lines() {120 if id.trim_space() != '' { result << '${prefix}/${id.trim_space()}' }121 }122 return result123}124125pub fn (mut w Web) put_if_absent(key string, body []u8) !bool {126 return error('${w.repo(key)} is read-only.')127}128129pub fn (mut w Web) put_if_match(key string, body []u8, tag string) !bool {130 return error('${w.repo(key)} is read-only.')131}132133pub fn (mut w Web) delete(key string) ! {134 return error('${w.repo(key)} is read-only.')135}136137fn (w &Web) url(key string) string {138 parts := key.split('/')139 return '${w.base}/${parts[0]}/koh/${parts[1..].join('/')}'140}141142fn (w &Web) repo(key string) string {143 return '${w.base}/${key.all_before('/')}'144}