web.v

3.1 KB · raw

1//! The web backend: read-only access to a public Koh mirror.2module remote34import net.http5import net.urllib67pub struct Web {8pub mut:9	base string10}1112pub fn new_web(base string) !Web {13	return Web{ base: kepr_host(base)! }14}1516// kepr_host normalises a Kepr address: https unless the scheme says otherwise, and scheme, host17// and port only. A path, a query, a fragment or credentials would send the code somewhere else.18pub fn kepr_host(value string) !string {19	for letter in value {20		if letter <= ` ` {21			return error('"${value}" is not a Kepr address — try kepr.uk')22		}23	}24	text := if value.starts_with('http://') || value.starts_with('https://') {25		value26	} else {27		'https://${value}'28	}29	parsed := urllib.parse(text) or { return error('"${value}" is not a Kepr address — try kepr.uk') }30	if parsed.scheme !in ['http', 'https'] || parsed.host == '' || parsed.path.trim_right('/') != '' || parsed.raw_query != '' || parsed.fragment != '' {31		return error('"${value}" is not a Kepr address — try kepr.uk')32	}33	if text.all_after('//').contains('@') {34		return error('"${value}" is not a Kepr address — try kepr.uk')35	}36	// `/api/connect` answers with the bucket's secret key, so plain http is only for a Kepr on37	// this machine: a test, or a local run.38	if parsed.scheme == 'http' && parsed.hostname().to_lower() !in ['127.0.0.1', 'localhost', '::1'] {39		return error('"${value}" needs https — plain http is only for a Kepr on this machine')40	}41	// A hostname is case-insensitive, so the stored form is the lower-case one.42	return '${parsed.scheme}://${parsed.host.to_lower()}'43}4445pub fn (mut w Web) get(key string) !Object {46	response := http.fetch(47		url:            w.url(key)48		method:         .get49		enable_http2:   false50		allow_redirect: false51	)!52	if response.status_code == 404 {53		return Object{}54	}55	if response.status_code < 200 || response.status_code >= 300 {56		return error('web GET ${key} failed: ${response.status_code}')57	}58	return Object{59		found: true60		body:  response.body.bytes()61		tag:   response.header.get_custom('etag', http.HeaderQueryConfig{}) or { '' }62	}63}6465pub fn (mut w Web) list(prefix string) ![]string {66	response := http.fetch(67		url:            w.url(prefix)68		method:         .get69		enable_http2:   false70		allow_redirect: false71	)!72	if response.status_code == 404 {73		return []string{}74	}75	if response.status_code < 200 || response.status_code >= 300 {76		return error('web GET ${prefix} failed: ${response.status_code}')77	}78	mut result := []string{}79	for id in response.body.split_into_lines() {80		if id.trim_space() != '' { result << '${prefix}/${id.trim_space()}' }81	}82	return result83}8485pub fn (mut w Web) put_if_absent(key string, body []u8) !bool {86	return error('${w.repo(key)} is read-only.')87}8889pub fn (mut w Web) put_if_match(key string, body []u8, tag string) !bool {90	return error('${w.repo(key)} is read-only.')91}9293pub fn (mut w Web) delete(key string) ! {94	return error('${w.repo(key)} is read-only.')95}9697fn (w &Web) url(key string) string {98	parts := key.split('/')99	return '${w.base}/${parts[0]}/koh/${parts[1..].join('/')}'100}101102fn (w &Web) repo(key string) string {103	return '${w.base}/${key.all_before('/')}'104}