//! Machine-wide remote settings. //! //! Everything machine-specific lives in `~/.config/koh/`, never inside a repo: //! one remote per machine, and the S3 keys in a file only the user can read. //! The format is plain `key=value` lines so it can be read and edited by hand. module machine import os pub const remote_file = 'remote' pub const credentials_file = 'credentials' pub const kind_folder = 'folder' pub const kind_s3 = 's3' pub struct RemoteSettings { pub: kind string path string // folder backend endpoint string // s3 backend bucket string region string } pub struct Credentials { pub: access_key string secret_key string } pub fn save_remote(settings RemoteSettings) ! { os.mkdir_all(config_dir(), os.MkdirParams{})! mut text := '# written by koh remote — the remote for every repo on this machine\n' text += 'kind=${settings.kind}\n' if settings.kind == kind_folder { text += 'path=${settings.path}\n' } else { text += 'endpoint=${settings.endpoint}\n' text += 'bucket=${settings.bucket}\n' text += 'region=${settings.region}\n' } os.write_file(config_path(remote_file), text)! } pub fn load_remote() !RemoteSettings { text := os.read_file(config_path(remote_file)) or { return error('this machine has no remote yet — run "koh remote"') } values := parse_config(text) settings := RemoteSettings{ kind: values['kind'] or { '' } path: values['path'] or { '' } endpoint: values['endpoint'] or { '' } bucket: values['bucket'] or { '' } region: values['region'] or { '' } } if settings.kind !in [kind_folder, kind_s3] { return error('the remote setting in ${config_path(remote_file)} is not readable — run "koh remote" again') } return settings } pub fn has_remote() bool { return os.is_file(config_path(remote_file)) } pub fn forget_remote() ! { if os.exists(config_path(remote_file)) { os.rm(config_path(remote_file))! } } // save_credentials writes the keys so that only the user can read them. pub fn save_credentials(credentials Credentials) ! { os.mkdir_all(config_dir(), mode: 0o700)! // mkdir_all leaves an existing directory's mode alone. os.chmod(config_dir(), 0o700)! mut text := '# written by koh remote — keep this file private\n' text += 'access_key=${credentials.access_key}\n' text += 'secret_key=${credentials.secret_key}\n' path := config_path(credentials_file) // Created 0600 and renamed over the old file: the keys are never readable // by others, not even between a write and a chmod, and a looser old file // is replaced rather than rewritten in place. temporary := '${path}.tmp-${os.getpid()}' // A stale file (or link) by that name would keep its own mode, so it must // go; if it cannot, nothing is written. if os.exists(temporary) || os.is_link(temporary) { os.rm(temporary)! } mut file := os.open_file(temporary, 'w', 0o600) or { os.rm(temporary) or {} return err } os.chmod(temporary, 0o600) or { file.close() os.rm(temporary) or {} return err } file.write_string(text) or { file.close() os.rm(temporary) or {} return err } file.close() // close() does not report a failed flush, so check what actually landed. if (os.read_file(temporary) or { '' }) != text { os.rm(temporary) or {} return error('could not write ${path}') } // V's rename, like mv(1), would move the file INTO a directory there. if os.is_dir(path) { os.rm(temporary) or {} return error('${path} is a directory') } os.rename(temporary, path) or { os.rm(temporary) or {} return err } } pub fn load_credentials() !Credentials { path := config_path(credentials_file) text := os.read_file(path) or { return error('this machine has no S3 keys yet — run "koh remote"') } // Like ssh with a private key: a hand-written file others can read is // refused rather than used. if os.stat(path)!.mode & 0o077 != 0 { return error('${path} can be read by others — run "chmod 600 ${path}"') } values := parse_config(text) credentials := Credentials{ access_key: values['access_key'] or { '' } secret_key: values['secret_key'] or { '' } } if credentials.access_key == '' || credentials.secret_key == '' { return error('the S3 keys in ${config_path(credentials_file)} are incomplete — run "koh remote" again') } return credentials } fn parse_config(text string) map[string]string { mut values := map[string]string{} for line in text.split_into_lines() { trimmed := line.trim_space() if trimmed == '' || trimmed.starts_with('#') { continue } at := trimmed.index('=') or { continue } values[trimmed[..at].trim_space()] = trimmed[at + 1..].trim_space() } return values }