settings.v

4.8 KB · raw

1//! Machine-wide remote settings.2//!3//! Everything machine-specific lives in `~/.config/koh/`, never inside a repo:4//! one remote per machine, and the S3 keys in a file only the user can read.5//! The format is plain `key=value` lines so it can be read and edited by hand.67module machine89import os1011pub const remote_file = 'remote'12pub const credentials_file = 'credentials'1314pub const kind_folder = 'folder'15pub const kind_s3 = 's3'1617pub struct RemoteSettings {18pub:19	kind     string20	path     string // folder backend21	endpoint string // s3 backend22	bucket   string23	region   string24	kepr     string25}2627pub struct Credentials {28pub:29	access_key string30	secret_key string31}3233pub fn save_remote(settings RemoteSettings) ! {34	os.mkdir_all(config_dir(), os.MkdirParams{})!35	mut text := '# written by koh remote — the remote for every repo on this machine\n'36	text += 'kind=${settings.kind}\n'37	if settings.kind == kind_folder {38		text += 'path=${settings.path}\n'39	} else {40		text += 'endpoint=${settings.endpoint}\n'41		text += 'bucket=${settings.bucket}\n'42		text += 'region=${settings.region}\n'43		if settings.kepr != '' {44			text += 'kepr=${settings.kepr}\n'45		}46	}47	os.write_file(config_path(remote_file), text)!48}4950pub fn load_remote() !RemoteSettings {51	text := os.read_file(config_path(remote_file)) or {52		return error('this machine has no remote yet — run "koh remote"')53	}54	values := parse_config(text)55	settings := RemoteSettings{56		kind:     values['kind'] or { '' }57		path:     values['path'] or { '' }58		endpoint: values['endpoint'] or { '' }59		bucket:   values['bucket'] or { '' }60		region:   values['region'] or { '' }61		kepr:     values['kepr'] or { '' }62	}63	if settings.kind !in [kind_folder, kind_s3] {64		return error('the remote setting in ${config_path(remote_file)} is not readable — run "koh remote" again')65	}66	return settings67}6869pub fn has_remote() bool {70	return os.is_file(config_path(remote_file))71}7273pub fn forget_remote() ! {74	if os.exists(config_path(remote_file)) {75		os.rm(config_path(remote_file))!76	}77}7879// save_credentials writes the keys so that only the user can read them.80pub fn save_credentials(credentials Credentials) ! {81	os.mkdir_all(config_dir(), mode: 0o700)!82	// mkdir_all leaves an existing directory's mode alone.83	os.chmod(config_dir(), 0o700)!84	mut text := '# written by koh remote — keep this file private\n'85	text += 'access_key=${credentials.access_key}\n'86	text += 'secret_key=${credentials.secret_key}\n'87	path := config_path(credentials_file)88	// Created 0600 and renamed over the old file: the keys are never readable89	// by others, not even between a write and a chmod, and a looser old file90	// is replaced rather than rewritten in place.91	temporary := '${path}.tmp-${os.getpid()}'92	// A stale file (or link) by that name would keep its own mode, so it must93	// go; if it cannot, nothing is written.94	if os.exists(temporary) || os.is_link(temporary) {95		os.rm(temporary)!96	}97	mut file := os.open_file(temporary, 'w', 0o600) or {98		discard(temporary)99		return err100	}101	os.chmod(temporary, 0o600) or {102		file.close()103		discard(temporary)104		return err105	}106	file.write_string(text) or {107		file.close()108		discard(temporary)109		return err110	}111	file.close()112	// close() does not report a failed flush, so check what actually landed.113	if (os.read_file(temporary) or { '' }) != text {114		discard(temporary)115		return error('could not write ${path}')116	}117	// V's rename, like mv(1), would move the file INTO a directory there.118	if os.is_dir(path) {119		discard(temporary)120		return error('${path} is a directory')121	}122	os.rename(temporary, path) or {123		discard(temporary)124		return err125	}126}127128// discard removes the half-written temporary. Its own failure is dropped: the129// error that sent us here is the one worth reporting.130fn discard(temporary string) {131	os.rm(temporary) or {}132}133134pub fn load_credentials() !Credentials {135	path := config_path(credentials_file)136	text := os.read_file(path) or {137		return error('this machine has no S3 keys yet — run "koh remote"')138	}139	// Like ssh with a private key: a hand-written file others can read is140	// refused rather than used.141	if os.stat(path)!.mode & 0o077 != 0 {142		return error('${path} can be read by others — run "chmod 600 ${path}"')143	}144	values := parse_config(text)145	credentials := Credentials{146		access_key: values['access_key'] or { '' }147		secret_key: values['secret_key'] or { '' }148	}149	if credentials.access_key == '' || credentials.secret_key == '' {150		return error('the S3 keys in ${config_path(credentials_file)} are incomplete — run "koh remote" again')151	}152	return credentials153}154155fn parse_config(text string) map[string]string {156	mut values := map[string]string{}157	for line in text.split_into_lines() {158		trimmed := line.trim_space()159		if trimmed == '' || trimmed.starts_with('#') {160			continue161		}162		at := trimmed.index('=') or { continue }163		values[trimmed[..at].trim_space()] = trimmed[at + 1..].trim_space()164	}165	return values166}