web.v

4.6 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	// on_body hears how much of a response has arrived, each time it passes another MiB.11	on_body fn (received u64) = unsafe { nil }12}1314pub fn new_web(base string) !Web {15	return Web{ base: kepr_host(base)! }16}1718// kepr_host normalises a Kepr address: https unless the scheme says otherwise, and scheme, host19// and port only. A path, a query, a fragment or credentials would send the code somewhere else.20pub 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	// `/api/connect` answers with the bucket's secret key, so plain http is only for a Kepr on39	// this machine: a test, or a local run.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	// A hostname is case-insensitive, so the stored form is the lower-case one.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}6869// stream is get for a body too big to hold: each piece goes to `sink` as it arrives, and none of it70// is kept. False when the key does not exist.71pub 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		// Enough for the status line and headers, which are all that is read from the response.81		stop_copying_limit: 64 * 102482		on_progress_body:   fn [sink, on_body] (_ &http.Request, piece []u8, received u64, _ u64, status int) ! {83			// V hands over a read that holds only part of the headers as if it were body, with84			// nothing received yet; a real piece of body always counts itself.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}