123456789module cli1011import json212import net.http13import net.urllib14import os15import src.fmt16import src.machine17import src.remote18import term.termios1920pub fn cmd_remote(raw []string) int {21 args := parse('remote', raw, Options{22 values: ['code']23 flags: ['json', 'forget']24 }) or { return usage_error(err.msg()) }25 if args.flag('forget') {26 if !machine.has_remote() {27 return refuse(args.json(), 'this machine has no remote to forget')28 }29 machine.forget_remote() or { return refuse(args.json(), err.msg()) }30 if args.json() {31 emit_json(json2.encode(RemoteJson{32 configured: false33 }))34 return 035 }36 ok("forgot this machine's remote")37 return 038 }39 if args.positional.len == 0 {40 if !machine.has_remote() {41 if args.json() {42 emit_json(json2.encode(RemoteJson{43 configured: false44 }))45 return 046 }47 return setup_wizard()48 }49 settings := machine.load_remote() or {50 51 if !args.json() && interactive() {52 warn(err.msg())53 return setup_wizard()54 }55 return refuse(args.json(), err.msg())56 }57 if args.json() {58 emit_json(json2.encode(RemoteJson{59 configured: true60 kind: settings.kind61 target: describe(settings)62 }))63 return 064 }65 ok('remote: ${describe(settings)}')66 if !interactive() {67 note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')68 return 069 }70 print(' replace it? [y/N] ')71 if os.get_line().trim_right('\r') != 'y' {72 return 073 }74 75 return setup_wizard()76 }77 78 return set_remote(args)79}8081fn set_remote(args Args) int {82 if args.positional_at(0) !in [machine.kind_folder, machine.kind_s3] {83 return set_kepr_remote(args)84 }85 kind := args.positional_at(0)86 match kind {87 machine.kind_folder {88 path := args.positional_at(1)89 if path == '' {90 return usage_error('remote folder needs a path: koh remote folder /Volumes/usb/koh')91 }92 settings := machine.RemoteSettings{93 kind: machine.kind_folder94 path: os.real_path(path)95 }96 return save_after_check(settings, args, '', '')97 }98 machine.kind_s3 {99 endpoint := args.positional_at(1)100 bucket := args.positional_at(2)101 region := args.positional_at(3)102 if endpoint == '' || bucket == '' || region == '' {103 return usage_error('remote s3 needs an endpoint, bucket and region: koh remote s3 https://s3.fr-par.scw.cloud my-bucket fr-par')104 }105 access_key, secret_key := s3_keys() or { return refuse(args.json(), err.msg()) }106 settings := machine.RemoteSettings{107 kind: machine.kind_s3108 endpoint: endpoint109 bucket: bucket110 region: region111 }112 return save_after_check(settings, args, access_key, secret_key)113 }114 else {115 return usage_error('remote takes "folder", "s3", or a Kepr host')116 }117 }118}119120fn set_kepr_remote(args Args) int {121 if args.positional.len != 1 {122 return usage_error('koh remote <host> accepts one Kepr host')123 }124 mut host := args.positional_at(0)125 if !host.starts_with('http://') && !host.starts_with('https://') {126 host = 'https://${host}'127 }128 host = host.trim_right('/')129 connect_url := '${host}/connect?machine=${machine.name()}'130 println('Open ${connect_url} and sign in')131 open_command := if os.user_os() == 'macos' { 'open' } else { 'xdg-open' }132 133 os.execute('${open_command} "${connect_url}" >/dev/null 2>&1 &')134 mut code := args.value('code')135 if code == '' {136 if !interactive() {137 return fail('setting up a Kepr remote needs a terminal, or use --code <code>')138 }139 print('paste the code: ')140 code = os.get_line().trim_space()141 }142 response := http.fetch(143 url: '${host}/api/connect'144 method: .post145 header: http.new_header(146 key: .content_type147 value: 'application/x-www-form-urlencoded'148 )149 data: 'code=${urllib.query_escape(code)}'150 enable_http2: false151 ) or { return refuse(args.json(), 'could not reach Kepr: ${err.msg()}') }152 if response.status_code == 404 {153 probe := http.fetch(url: connect_url, method: .get, enable_http2: false) or {154 return refuse(args.json(), 'that code is wrong or has expired — nothing was changed')155 }156 if probe.status_code == 404 {157 return refuse(args.json(), 'this Kepr doesn\'t hand out bucket access; use "koh remote s3"')158 }159 return refuse(args.json(), 'that code is wrong or has expired — nothing was changed')160 }161 if response.status_code < 200 || response.status_code >= 300 {162 return refuse(args.json(), 'Kepr refused the code — nothing was changed')163 }164 reply := json2.decode[KeprReply](response.body) or {165 return refuse(args.json(), 'Kepr returned an invalid response — nothing was changed')166 }167 settings := machine.RemoteSettings{168 kind: machine.kind_s3169 endpoint: reply.endpoint170 bucket: reply.bucket171 region: reply.region172 kepr: host173 }174 return save_after_check(settings, args, reply.access_key_id, reply.secret_access_key)175}176177178179fn save_after_check(settings machine.RemoteSettings, args Args, access_key string, secret_key string) int {180 mut backend := build_backend(settings, access_key, secret_key) or {181 return refuse(args.json(), err.msg())182 }183 remote.check(mut backend) or {184 return refuse(args.json(), 'the remote did not pass its test: ${err.msg()}')185 }186 machine.save_remote(settings) or { return refuse(args.json(), err.msg()) }187 if settings.kind == machine.kind_s3 {188 machine.save_credentials(machine.Credentials{189 access_key: access_key190 secret_key: secret_key191 }) or { return refuse(args.json(), err.msg()) }192 }193 if args.json() {194 emit_json(json2.encode(RemoteJson{195 configured: true196 kind: settings.kind197 target: describe(settings)198 }))199 return 0200 }201 ok('remote set to ${describe(settings)}')202 note('every repo on this machine now offers to it')203 return 0204}205206fn build_backend(settings machine.RemoteSettings, access_key string, secret_key string) !remote.Backend {207 if settings.kind == machine.kind_folder {208 return remote.new_folder(settings.path)!209 }210 return remote.new_s3(settings.endpoint, settings.bucket, settings.region, access_key,211 secret_key)!212}213214fn setup_wizard() int {215 if !interactive() {216 return fail('setting up a remote needs a terminal, or use: koh remote folder <path>, koh remote s3 <endpoint> <bucket> <region>')217 }218 println(' choose a remote:')219 println(' ${fmt.amber('1')} folder a directory, on a disk or a NAS')220 println(' ${fmt.amber('2')} s3 an S3-compatible bucket')221 print(' which? [1] ')222 if os.get_line().trim_space() == '2' {223 return wizard_s3()224 }225 print(' folder path: ')226 path := os.get_line().trim_space()227 if path == '' {228 return fail('no path given — nothing was saved')229 }230 settings := machine.RemoteSettings{231 kind: machine.kind_folder232 path: os.real_path(path)233 }234 return save_after_check(settings, Args{}, '', '')235}236237fn wizard_s3() int {238 print(' endpoint [https://s3.fr-par.scw.cloud]: ')239 endpoint := or_default(os.get_line().trim_space(), 'https://s3.fr-par.scw.cloud')240 print(' bucket: ')241 bucket := os.get_line().trim_space()242 print(' region [fr-par]: ')243 region := or_default(os.get_line().trim_space(), 'fr-par')244 if bucket == '' {245 return fail('no bucket given — nothing was saved')246 }247 access_key, secret_key := s3_keys() or { return fail(err.msg()) }248 settings := machine.RemoteSettings{249 kind: machine.kind_s3250 endpoint: endpoint251 bucket: bucket252 region: region253 }254 return save_after_check(settings, Args{}, access_key, secret_key)255}256257258259fn s3_keys() !(string, string) {260 access_key := os.getenv('KOH_ACCESS_KEY')261 secret_key := os.getenv('KOH_SECRET_KEY')262 if access_key != '' && secret_key != '' {263 return access_key, secret_key264 }265 if !interactive() {266 return error('S3 keys are missing — set KOH_ACCESS_KEY and KOH_SECRET_KEY, or run koh remote on a terminal')267 }268 print(' access key: ')269 access_key_typed := os.get_line().trim_space()270 271 272 mut secret_key_typed := ''273 mut saved := termios.Termios{}274 if os.is_atty(1) != 0 && os.getenv('TERM') != 'dumb' && termios.tcgetattr(0, mut saved) == 0 {275 276 277 278 secret_key_typed = os.input_password(' secret key: ') or {279 println('')280 if termios.set_state(0, saved) != 0 {281 warn('the terminal may no longer echo — run "stty sane"')282 }283 ''284 }285 } else {286 print(' secret key: ')287 secret_key_typed = os.get_line()288 }289 secret_key_typed = secret_key_typed.trim_space()290 if access_key_typed == '' || secret_key_typed == '' {291 return error('both keys are needed — nothing was saved')292 }293 return access_key_typed, secret_key_typed294}295296fn describe(settings machine.RemoteSettings) string {297 if settings.kind == machine.kind_folder {298 return 'folder ${settings.path}'299 }300 return 's3 ${settings.endpoint}/${settings.bucket} (${settings.region})'301}302303fn or_default(value string, fallback string) string {304 return if value == '' { fallback } else { value }305}306307struct RemoteJson {308 configured bool309 kind string310 target string311}312313struct KeprReply {314 endpoint string315 bucket string316 region string317 access_key_id string318 secret_access_key string319}