123456789module cli1011import json12import 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(json.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(json.encode(RemoteJson{40 configured: false41 }))42 return 043 }44 return setup_wizard()45 }46 settings := machine.load_remote() or { return refuse(args.json(), err.msg()) }47 if args.json() {48 emit_json(json.encode(RemoteJson{49 configured: true50 kind: settings.kind51 target: describe(settings)52 }))53 return 054 }55 ok('remote: ${describe(settings)}')56 note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')57 return 058 }59 60 return set_remote(args)61}6263fn set_remote(args Args) int {64 kind := args.positional_at(0)65 match kind {66 machine.kind_folder {67 path := args.positional_at(1)68 if path == '' {69 return usage_error('remote folder needs a path: koh remote folder /Volumes/usb/koh')70 }71 settings := machine.RemoteSettings{72 kind: machine.kind_folder73 path: os.real_path(path)74 }75 return save_after_check(settings, args, '', '')76 }77 machine.kind_s3 {78 endpoint := args.positional_at(1)79 bucket := args.positional_at(2)80 region := args.positional_at(3)81 if endpoint == '' || bucket == '' || region == '' {82 return usage_error('remote s3 needs an endpoint, bucket and region: koh remote s3 https://s3.fr-par.scw.cloud my-bucket fr-par')83 }84 access_key, secret_key := s3_keys() or { return refuse(args.json(), err.msg()) }85 settings := machine.RemoteSettings{86 kind: machine.kind_s387 endpoint: endpoint88 bucket: bucket89 region: region90 }91 return save_after_check(settings, args, access_key, secret_key)92 }93 else {94 return usage_error('remote takes "folder" or "s3", not "${kind}"')95 }96 }97}9899100101fn save_after_check(settings machine.RemoteSettings, args Args, access_key string, secret_key string) int {102 mut backend := build_backend(settings, access_key, secret_key) or {103 return refuse(args.json(), err.msg())104 }105 remote.check(mut backend) or {106 return refuse(args.json(), 'the remote did not pass its test: ${err.msg()}')107 }108 machine.save_remote(settings) or { return refuse(args.json(), err.msg()) }109 if settings.kind == machine.kind_s3 {110 machine.save_credentials(machine.Credentials{111 access_key: access_key112 secret_key: secret_key113 }) or { return refuse(args.json(), err.msg()) }114 }115 if args.json() {116 emit_json(json.encode(RemoteJson{117 configured: true118 kind: settings.kind119 target: describe(settings)120 }))121 return 0122 }123 ok('remote set to ${describe(settings)}')124 note('every repo on this machine now offers to it')125 return 0126}127128fn build_backend(settings machine.RemoteSettings, access_key string, secret_key string) !remote.Backend {129 if settings.kind == machine.kind_folder {130 return remote.new_folder(settings.path)!131 }132 return remote.new_s3(settings.endpoint, settings.bucket, settings.region, access_key,133 secret_key)!134}135136fn setup_wizard() int {137 if !interactive() {138 return fail('setting up a remote needs a terminal, or use: koh remote folder <path>, koh remote s3 <endpoint> <bucket> <region>')139 }140 println(' choose a remote:')141 println(' ${fmt.amber('1')} folder a directory, on a disk or a NAS')142 println(' ${fmt.amber('2')} s3 an S3-compatible bucket')143 print(' which? [1] ')144 if os.get_line().trim_space() == '2' {145 return wizard_s3()146 }147 print(' folder path: ')148 path := os.get_line().trim_space()149 if path == '' {150 return fail('no path given — nothing was saved')151 }152 settings := machine.RemoteSettings{153 kind: machine.kind_folder154 path: os.real_path(path)155 }156 return save_after_check(settings, Args{}, '', '')157}158159fn wizard_s3() int {160 print(' endpoint [https://s3.fr-par.scw.cloud]: ')161 endpoint := or_default(os.get_line().trim_space(), 'https://s3.fr-par.scw.cloud')162 print(' bucket: ')163 bucket := os.get_line().trim_space()164 print(' region [fr-par]: ')165 region := or_default(os.get_line().trim_space(), 'fr-par')166 if bucket == '' {167 return fail('no bucket given — nothing was saved')168 }169 access_key, secret_key := s3_keys() or { return fail(err.msg()) }170 settings := machine.RemoteSettings{171 kind: machine.kind_s3172 endpoint: endpoint173 bucket: bucket174 region: region175 }176 return save_after_check(settings, Args{}, access_key, secret_key)177}178179180181fn s3_keys() !(string, string) {182 access_key := os.getenv('KOH_ACCESS_KEY')183 secret_key := os.getenv('KOH_SECRET_KEY')184 if access_key != '' && secret_key != '' {185 return access_key, secret_key186 }187 if !interactive() {188 return error('S3 keys are missing — set KOH_ACCESS_KEY and KOH_SECRET_KEY, or run koh remote on a terminal')189 }190 print(' access key: ')191 access_key_typed := os.get_line().trim_space()192 193 194 mut secret_key_typed := ''195 mut saved := termios.Termios{}196 if os.is_atty(1) != 0 && os.getenv('TERM') != 'dumb' && termios.tcgetattr(0, mut saved) == 0 {197 198 199 200 secret_key_typed = os.input_password(' secret key: ') or {201 println('')202 if termios.set_state(0, saved) != 0 {203 warn('the terminal may no longer echo — run "stty sane"')204 }205 ''206 }207 } else {208 print(' secret key: ')209 secret_key_typed = os.get_line()210 }211 secret_key_typed = secret_key_typed.trim_space()212 if access_key_typed == '' || secret_key_typed == '' {213 return error('both keys are needed — nothing was saved')214 }215 return access_key_typed, secret_key_typed216}217218fn describe(settings machine.RemoteSettings) string {219 if settings.kind == machine.kind_folder {220 return 'folder ${settings.path}'221 }222 return 's3 ${settings.endpoint}/${settings.bucket} (${settings.region})'223}224225fn or_default(value string, fallback string) string {226 return if value == '' { fallback } else { value }227}228229struct RemoteJson {230 configured bool231 kind string232 target string233}