123456789module cli1011import json212import os13import src.fmt14import src.machine15import src.remote16import term.termios1718pub fn cmd_remote(raw []string) int {19 args := parse('remote', raw, Options{20 flags: ['json', 'forget']21 }) or { return usage_error(err.msg()) }22 if args.flag('forget') {23 if !machine.has_remote() {24 return refuse(args.json(), 'this machine has no remote to forget')25 }26 machine.forget_remote() or { return refuse(args.json(), err.msg()) }27 if args.json() {28 emit_json(json2.encode(RemoteJson{29 configured: false30 }))31 return 032 }33 ok("forgot this machine's remote")34 return 035 }36 if args.positional.len == 0 {37 if !machine.has_remote() {38 if args.json() {39 emit_json(json2.encode(RemoteJson{40 configured: false41 }))42 return 043 }44 return setup_wizard()45 }46 settings := machine.load_remote() or {47 48 if !args.json() && interactive() {49 warn(err.msg())50 return setup_wizard()51 }52 return refuse(args.json(), err.msg())53 }54 if args.json() {55 emit_json(json2.encode(RemoteJson{56 configured: true57 kind: settings.kind58 target: describe(settings)59 }))60 return 061 }62 ok('remote: ${describe(settings)}')63 if !interactive() {64 note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')65 return 066 }67 print(' replace it? [y/N] ')68 if os.get_line().trim_right('\r') != 'y' {69 return 070 }71 72 return setup_wizard()73 }74 75 return set_remote(args)76}7778fn set_remote(args Args) int {79 kind := args.positional_at(0)80 match kind {81 machine.kind_folder {82 path := args.positional_at(1)83 if path == '' {84 return usage_error('remote folder needs a path: koh remote folder /Volumes/usb/koh')85 }86 settings := machine.RemoteSettings{87 kind: machine.kind_folder88 path: os.real_path(path)89 }90 return save_after_check(settings, args, '', '')91 }92 machine.kind_s3 {93 endpoint := args.positional_at(1)94 bucket := args.positional_at(2)95 region := args.positional_at(3)96 if endpoint == '' || bucket == '' || region == '' {97 return usage_error('remote s3 needs an endpoint, bucket and region: koh remote s3 https://s3.fr-par.scw.cloud my-bucket fr-par')98 }99 access_key, secret_key := s3_keys() or { return refuse(args.json(), err.msg()) }100 settings := machine.RemoteSettings{101 kind: machine.kind_s3102 endpoint: endpoint103 bucket: bucket104 region: region105 }106 return save_after_check(settings, args, access_key, secret_key)107 }108 else {109 return usage_error('remote takes "folder" or "s3", not "${kind}"')110 }111 }112}113114115116fn save_after_check(settings machine.RemoteSettings, args Args, access_key string, secret_key string) int {117 mut backend := build_backend(settings, access_key, secret_key) or {118 return refuse(args.json(), err.msg())119 }120 remote.check(mut backend) or {121 return refuse(args.json(), 'the remote did not pass its test: ${err.msg()}')122 }123 machine.save_remote(settings) or { return refuse(args.json(), err.msg()) }124 if settings.kind == machine.kind_s3 {125 machine.save_credentials(machine.Credentials{126 access_key: access_key127 secret_key: secret_key128 }) or { return refuse(args.json(), err.msg()) }129 }130 if args.json() {131 emit_json(json2.encode(RemoteJson{132 configured: true133 kind: settings.kind134 target: describe(settings)135 }))136 return 0137 }138 ok('remote set to ${describe(settings)}')139 note('every repo on this machine now offers to it')140 return 0141}142143fn build_backend(settings machine.RemoteSettings, access_key string, secret_key string) !remote.Backend {144 if settings.kind == machine.kind_folder {145 return remote.new_folder(settings.path)!146 }147 return remote.new_s3(settings.endpoint, settings.bucket, settings.region, access_key,148 secret_key)!149}150151fn setup_wizard() int {152 if !interactive() {153 return fail('setting up a remote needs a terminal, or use: koh remote folder <path>, koh remote s3 <endpoint> <bucket> <region>')154 }155 println(' choose a remote:')156 println(' ${fmt.amber('1')} folder a directory, on a disk or a NAS')157 println(' ${fmt.amber('2')} s3 an S3-compatible bucket')158 print(' which? [1] ')159 if os.get_line().trim_space() == '2' {160 return wizard_s3()161 }162 print(' folder path: ')163 path := os.get_line().trim_space()164 if path == '' {165 return fail('no path given — nothing was saved')166 }167 settings := machine.RemoteSettings{168 kind: machine.kind_folder169 path: os.real_path(path)170 }171 return save_after_check(settings, Args{}, '', '')172}173174fn wizard_s3() int {175 print(' endpoint [https://s3.fr-par.scw.cloud]: ')176 endpoint := or_default(os.get_line().trim_space(), 'https://s3.fr-par.scw.cloud')177 print(' bucket: ')178 bucket := os.get_line().trim_space()179 print(' region [fr-par]: ')180 region := or_default(os.get_line().trim_space(), 'fr-par')181 if bucket == '' {182 return fail('no bucket given — nothing was saved')183 }184 access_key, secret_key := s3_keys() or { return fail(err.msg()) }185 settings := machine.RemoteSettings{186 kind: machine.kind_s3187 endpoint: endpoint188 bucket: bucket189 region: region190 }191 return save_after_check(settings, Args{}, access_key, secret_key)192}193194195196fn s3_keys() !(string, string) {197 access_key := os.getenv('KOH_ACCESS_KEY')198 secret_key := os.getenv('KOH_SECRET_KEY')199 if access_key != '' && secret_key != '' {200 return access_key, secret_key201 }202 if !interactive() {203 return error('S3 keys are missing — set KOH_ACCESS_KEY and KOH_SECRET_KEY, or run koh remote on a terminal')204 }205 print(' access key: ')206 access_key_typed := os.get_line().trim_space()207 208 209 mut secret_key_typed := ''210 mut saved := termios.Termios{}211 if os.is_atty(1) != 0 && os.getenv('TERM') != 'dumb' && termios.tcgetattr(0, mut saved) == 0 {212 213 214 215 secret_key_typed = os.input_password(' secret key: ') or {216 println('')217 if termios.set_state(0, saved) != 0 {218 warn('the terminal may no longer echo — run "stty sane"')219 }220 ''221 }222 } else {223 print(' secret key: ')224 secret_key_typed = os.get_line()225 }226 secret_key_typed = secret_key_typed.trim_space()227 if access_key_typed == '' || secret_key_typed == '' {228 return error('both keys are needed — nothing was saved')229 }230 return access_key_typed, secret_key_typed231}232233fn describe(settings machine.RemoteSettings) string {234 if settings.kind == machine.kind_folder {235 return 'folder ${settings.path}'236 }237 return 's3 ${settings.endpoint}/${settings.bucket} (${settings.region})'238}239240fn or_default(value string, fallback string) string {241 return if value == '' { fallback } else { value }242}243244struct RemoteJson {245 configured bool246 kind string247 target string248}