Immich bindings and CLI in OCaml
at main 355 lines 12 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6open Cmdliner 7 8let app_name = "immich" 9 10(* Styled output helpers *) 11let header_style = Fmt.(styled `Bold string) 12let label_style = Fmt.(styled `Faint string) 13let value_style = Fmt.(styled (`Fg `Cyan) string) 14let success_style = Fmt.(styled (`Fg `Green) string) 15let warning_style = Fmt.(styled (`Fg `Yellow) string) 16let error_style = Fmt.(styled (`Fg `Red) string) 17let profile_style = Fmt.(styled (`Fg `Magenta) string) 18let current_style = Fmt.(styled (`Fg `Green) (styled `Bold string)) 19 20(* Common Arguments *) 21 22let server_arg = 23 let doc = "Immich server URL." in 24 let env = Cmd.Env.info "IMMICH_SERVER" ~doc in 25 Arg.(required & pos 0 (some string) None & info [] ~env ~docv:"SERVER" ~doc) 26 27let server_opt = 28 let doc = "Immich server URL." in 29 let env = Cmd.Env.info "IMMICH_SERVER" ~doc in 30 Arg.(value & opt (some string) None & info ["server"; "s"] ~env ~docv:"URL" ~doc) 31 32let api_key_arg = 33 let doc = "API key (will prompt if not provided and not using password auth)." in 34 let env = Cmd.Env.info "IMMICH_API_KEY" ~doc in 35 Arg.(value & opt (some string) None & info ["api-key"; "k"] ~env ~docv:"KEY" ~doc) 36 37let email_arg = 38 let doc = "Email for password authentication." in 39 Arg.(value & opt (some string) None & info ["email"; "e"] ~docv:"EMAIL" ~doc) 40 41let password_arg = 42 let doc = "Password (will prompt if using email auth and not provided)." in 43 Arg.(value & opt (some string) None & info ["password"; "p"] ~docv:"PASSWORD" ~doc) 44 45let profile_arg = 46 let doc = "Profile name (default: current profile)." in 47 Arg.(value & opt (some string) None & info ["profile"; "P"] ~docv:"PROFILE" ~doc) 48 49let key_name_arg = 50 let doc = "Name for the API key (for display purposes)." in 51 Arg.(value & opt (some string) None & info ["name"; "n"] ~docv:"NAME" ~doc) 52 53(* Logging setup - takes requests_config to extract verbose_http *) 54 55let setup_logging_with_config style_renderer level (requests_config : Requests.Cmd.config) = 56 Fmt_tty.setup_std_outputs ?style_renderer (); 57 Logs.set_level level; 58 Logs.set_reporter (Logs_fmt.reporter ()); 59 Requests.Cmd.setup_log_sources ~verbose_http:requests_config.verbose_http.value level 60 61let setup_logging_simple style_renderer level = 62 Fmt_tty.setup_std_outputs ?style_renderer (); 63 Logs.set_level level; 64 Logs.set_reporter (Logs_fmt.reporter ()) 65 66let setup_logging = 67 Term.(const (fun style_renderer level -> (style_renderer, level)) 68 $ Fmt_cli.style_renderer () 69 $ Logs_cli.level ()) 70 71(* Requests config term *) 72 73let requests_config_term fs = 74 Requests.Cmd.config_term app_name fs 75 76(* Session helper *) 77 78let with_session ?profile f env = 79 let fs = env#fs in 80 match Session.load fs ?profile () with 81 | None -> 82 let profile_name = 83 match profile with 84 | Some p -> p 85 | None -> Session.get_current_profile fs 86 in 87 Fmt.epr "%a Not logged in (profile: %a). Use '%a' first.@." 88 error_style "Error:" 89 profile_style profile_name 90 Fmt.(styled `Bold string) "immich auth login"; 91 raise (Error.Exit_code 1) 92 | Some session -> f fs session 93 94let with_client ?requests_config ?profile f env = 95 with_session ?profile (fun fs session -> 96 Eio.Switch.run @@ fun sw -> 97 let client = Client.resume ~sw ~env ?requests_config ?profile ~session () in 98 f fs client 99 ) env 100 101(* Profile configuration for external programs *) 102 103module Profile_config = struct 104 type t = { 105 style_renderer : Fmt.style_renderer option; 106 log_level : Logs.level option; 107 requests_config : Requests.Cmd.config; 108 profile : string option; 109 } 110 111 let style_renderer t = t.style_renderer 112 let log_level t = t.log_level 113 let requests_config t = t.requests_config 114 let profile t = t.profile 115 116 let setup_logging t = 117 setup_logging_with_config t.style_renderer t.log_level t.requests_config 118end 119 120let profile_config_term fs = 121 let make (sr, ll) rc p = 122 Profile_config.{ style_renderer = sr; log_level = ll; requests_config = rc; profile = p } 123 in 124 Term.(const make $ setup_logging $ requests_config_term fs $ profile_arg) 125 126(* Login command *) 127 128let login_action ~requests_config ~server ~api_key ~email ~password ~profile ~key_name env = 129 match (api_key, email) with 130 | None, None -> 131 (* No auth method specified, prompt for API key *) 132 Fmt.pr "%a @?" label_style "API Key:"; 133 let api_key = read_line () in 134 Eio.Switch.run @@ fun sw -> 135 let client = Client.login_api_key ~sw ~env ~requests_config ?profile ~server_url:server ~api_key ?key_name () in 136 let profile_name = Option.value ~default:Session.default_profile profile in 137 Fmt.pr "%a Logged in to %a (profile: %a)@." 138 success_style "Success:" value_style server profile_style profile_name; 139 ignore client 140 | Some api_key, None -> 141 Eio.Switch.run @@ fun sw -> 142 let client = Client.login_api_key ~sw ~env ~requests_config ?profile ~server_url:server ~api_key ?key_name () in 143 let profile_name = Option.value ~default:Session.default_profile profile in 144 Fmt.pr "%a Logged in to %a (profile: %a)@." 145 success_style "Success:" value_style server profile_style profile_name; 146 ignore client 147 | None, Some email -> 148 let password = 149 match password with 150 | Some p -> p 151 | None -> 152 Fmt.pr "%a @?" label_style "Password:"; 153 read_line () 154 in 155 Eio.Switch.run @@ fun sw -> 156 let client = Client.login_password ~sw ~env ~requests_config ?profile ~server_url:server ~email ~password () in 157 let profile_name = Option.value ~default:email profile in 158 Fmt.pr "%a Logged in as %a (profile: %a)@." 159 success_style "Success:" value_style email profile_style profile_name; 160 ignore client 161 | Some _, Some _ -> 162 Fmt.epr "%a Cannot specify both --api-key and --email. Choose one authentication method.@." 163 error_style "Error:"; 164 raise (Error.Exit_code 1) 165 166let login_cmd env fs = 167 let doc = "Login to an Immich server." in 168 let info = Cmd.info "login" ~doc in 169 let login' (style_renderer, level) requests_config server api_key email password profile key_name = 170 setup_logging_with_config style_renderer level requests_config; 171 Error.wrap (fun () -> 172 login_action ~requests_config ~server ~api_key ~email ~password ~profile ~key_name env) 173 in 174 Cmd.v info 175 Term.(const login' $ setup_logging $ requests_config_term fs $ server_arg $ api_key_arg $ email_arg $ password_arg $ profile_arg $ key_name_arg) 176 177(* Logout command *) 178 179let logout_action ~profile env = 180 let fs = env#fs in 181 match Session.load fs ?profile () with 182 | None -> Fmt.pr "%a Not logged in.@." warning_style "Note:" 183 | Some session -> 184 Session.clear fs ?profile (); 185 let profile_name = 186 match profile with 187 | Some p -> p 188 | None -> Session.get_current_profile fs 189 in 190 Fmt.pr "%a Logged out from %a (profile: %a).@." 191 success_style "Success:" 192 value_style (Session.server_url session) 193 profile_style profile_name 194 195let logout_cmd env = 196 let doc = "Logout and clear saved session." in 197 let info = Cmd.info "logout" ~doc in 198 let logout' (style_renderer, level) profile = 199 setup_logging_simple style_renderer level; 200 logout_action ~profile env 201 in 202 Cmd.v info Term.(const logout' $ setup_logging $ profile_arg) 203 204(* Status command *) 205 206let status_action ~profile env = 207 let fs = env#fs in 208 let home = Sys.getenv "HOME" in 209 Fmt.pr "%a %a@." label_style "Config directory:" value_style (home ^ "/.config/immich"); 210 let current = Session.get_current_profile fs in 211 Fmt.pr "%a %a@." label_style "Current profile:" current_style current; 212 let profiles = Session.list_profiles fs in 213 if profiles <> [] then begin 214 Fmt.pr "%a %a@." label_style "Available profiles:" 215 Fmt.(list ~sep:(any ", ") profile_style) profiles 216 end; 217 Fmt.pr "@."; 218 let profile = Option.value ~default:current profile in 219 match Session.load fs ~profile () with 220 | None -> 221 Fmt.pr "%a %a: %a@." 222 header_style "Profile" 223 profile_style profile 224 warning_style "Not logged in" 225 | Some session -> 226 Fmt.pr "%a %a:@." header_style "Profile" profile_style profile; 227 Fmt.pr " %a@." Session.pp session; 228 if Session.is_expired session then 229 Fmt.pr " %a@." warning_style "(token expired, please login again)" 230 231let auth_status_cmd env = 232 let doc = "Show authentication status." in 233 let info = Cmd.info "status" ~doc in 234 let status' (style_renderer, level) profile = 235 setup_logging_simple style_renderer level; 236 status_action ~profile env 237 in 238 Cmd.v info Term.(const status' $ setup_logging $ profile_arg) 239 240(* Profile list command *) 241 242let profile_list_action env = 243 let fs = env#fs in 244 let current = Session.get_current_profile fs in 245 let profiles = Session.list_profiles fs in 246 if profiles = [] then 247 Fmt.pr "%a No profiles found. Use '%a' to create one.@." 248 warning_style "Note:" 249 Fmt.(styled `Bold string) "immich auth login" 250 else begin 251 Fmt.pr "%a@." header_style "Profiles:"; 252 List.iter 253 (fun p -> 254 let is_current = p = current in 255 match Session.load fs ~profile:p () with 256 | Some session -> 257 if is_current then 258 Fmt.pr " %a %a - %a@." 259 current_style p 260 success_style "(current)" 261 value_style (Session.server_url session) 262 else 263 Fmt.pr " %a - %a@." 264 profile_style p 265 value_style (Session.server_url session) 266 | None -> 267 if is_current then 268 Fmt.pr " %a %a@." current_style p success_style "(current)" 269 else 270 Fmt.pr " %a@." profile_style p) 271 profiles 272 end 273 274let profile_list_cmd env = 275 let doc = "List available profiles." in 276 let info = Cmd.info "list" ~doc in 277 let list' (style_renderer, level) () = 278 setup_logging_simple style_renderer level; 279 profile_list_action env 280 in 281 Cmd.v info Term.(const list' $ setup_logging $ const ()) 282 283(* Profile switch command *) 284 285let profile_name_arg = 286 let doc = "Profile name to switch to." in 287 Arg.(required & pos 0 (some string) None & info [] ~docv:"PROFILE" ~doc) 288 289let profile_switch_action ~profile env = 290 let fs = env#fs in 291 let profiles = Session.list_profiles fs in 292 if List.mem profile profiles then begin 293 Session.set_current_profile fs profile; 294 Fmt.pr "%a Switched to profile: %a@." 295 success_style "Success:" 296 profile_style profile 297 end 298 else begin 299 Fmt.epr "%a Profile '%a' not found.@." 300 error_style "Error:" 301 profile_style profile; 302 if profiles <> [] then 303 Fmt.epr "%a %a@." 304 label_style "Available profiles:" 305 Fmt.(list ~sep:(any ", ") profile_style) profiles; 306 raise (Error.Exit_code 1) 307 end 308 309let profile_switch_cmd env = 310 let doc = "Switch to a different profile." in 311 let info = Cmd.info "switch" ~doc in 312 let switch' (style_renderer, level) profile = 313 setup_logging_simple style_renderer level; 314 profile_switch_action ~profile env 315 in 316 Cmd.v info Term.(const switch' $ setup_logging $ profile_name_arg) 317 318(* Profile current command *) 319 320let profile_current_action env = 321 let fs = env#fs in 322 let current = Session.get_current_profile fs in 323 Fmt.pr "%a@." current_style current 324 325let profile_current_cmd env = 326 let doc = "Show current profile name." in 327 let info = Cmd.info "current" ~doc in 328 let current' (style_renderer, level) () = 329 setup_logging_simple style_renderer level; 330 profile_current_action env 331 in 332 Cmd.v info Term.(const current' $ setup_logging $ const ()) 333 334(* Profile command group *) 335 336let profile_cmd env = 337 let doc = "Profile management commands." in 338 let info = Cmd.info "profile" ~doc in 339 Cmd.group info 340 [ profile_list_cmd env 341 ; profile_switch_cmd env 342 ; profile_current_cmd env 343 ] 344 345(* Auth command group *) 346 347let auth_cmd env fs = 348 let doc = "Authentication commands." in 349 let info = Cmd.info "auth" ~doc in 350 Cmd.group info 351 [ login_cmd env fs 352 ; logout_cmd env 353 ; auth_status_cmd env 354 ; profile_cmd env 355 ]