Immich bindings and CLI in OCaml
at main 164 lines 6.8 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6type t = { 7 client : Immich.t; 8 session : Session.t; 9 fs : Eio.Fs.dir_ty Eio.Path.t; 10 profile : string option; 11} 12 13(* JSON type for .well-known/immich response *) 14let well_known_jsont = 15 let api_obj = 16 Jsont.Object.map ~kind:"api" (fun endpoint -> endpoint) 17 |> Jsont.Object.mem "endpoint" Jsont.string ~enc:Fun.id 18 |> Jsont.Object.finish 19 in 20 Jsont.Object.map ~kind:"well-known" (fun api -> api) 21 |> Jsont.Object.mem "api" api_obj ~enc:Fun.id 22 |> Jsont.Object.finish 23 24(** [resolve_api_url ~session base_url] resolves the actual API URL. 25 26 If [base_url] already ends with [/api], returns it unchanged. 27 Otherwise, tries to fetch [<base_url>/.well-known/immich] to discover 28 the API endpoint. Falls back to [<base_url>/api] if discovery fails. *) 29let resolve_api_url ~session base_url = 30 (* Remove trailing slash if present *) 31 let base_url = 32 if String.ends_with ~suffix:"/" base_url then 33 String.sub base_url 0 (String.length base_url - 1) 34 else base_url 35 in 36 (* If already has /api suffix, use as-is *) 37 if String.ends_with ~suffix:"/api" base_url then 38 base_url 39 else begin 40 (* Try .well-known/immich discovery *) 41 let well_known_url = base_url ^ "/.well-known/immich" in 42 try 43 let response = Requests.get session well_known_url in 44 if Requests.Response.ok response then begin 45 let json = Requests.Response.json response in 46 let endpoint = Openapi.Runtime.Json.decode_json_exn well_known_jsont json in 47 (* Construct full API URL *) 48 if String.starts_with ~prefix:"/" endpoint then 49 base_url ^ endpoint 50 else 51 base_url ^ "/" ^ endpoint 52 end 53 else 54 (* Discovery failed, default to /api *) 55 base_url ^ "/api" 56 with _ -> 57 (* Any error, default to /api *) 58 base_url ^ "/api" 59 end 60 61let create_with_session ~sw ~env ?requests_config ?profile ~session () = 62 let fs = env#fs in 63 let server_url = Session.server_url session in 64 (* Create a Requests session, optionally from cmdliner config *) 65 let requests_session = match requests_config with 66 | Some config -> Requests.Cmd.create config env sw 67 | None -> Requests.create ~sw env 68 in 69 let requests_session = 70 match Session.auth session with 71 | Session.Jwt { access_token; _ } -> 72 Requests.set_auth requests_session (Requests.Auth.bearer ~token:access_token) 73 | Session.Api_key { key; _ } -> 74 Requests.set_default_header requests_session "x-api-key" key 75 in 76 let client = Immich.create ~session:requests_session ~sw env ~base_url:server_url in 77 { client; session; fs; profile } 78 79let login_api_key ~sw ~env ?requests_config ?profile ~server_url ~api_key ?key_name () = 80 let fs = env#fs in 81 (* Create session with API key header *) 82 let requests_session = match requests_config with 83 | Some config -> Requests.Cmd.create config env sw 84 | None -> Requests.create ~sw env 85 in 86 let requests_session = Requests.set_default_header requests_session "x-api-key" api_key in 87 (* Resolve the API URL from .well-known/immich if available *) 88 let server_url = resolve_api_url ~session:requests_session server_url in 89 let client = Immich.create ~session:requests_session ~sw env ~base_url:server_url in 90 (* Validate by calling the validate endpoint *) 91 let resp = Immich.ValidateAccessToken.validate_access_token client () in 92 if not (Immich.ValidateAccessToken.ResponseDto.auth_status resp) then 93 failwith "API key validation failed"; 94 (* Create and save session *) 95 let auth = Session.Api_key { key = api_key; name = key_name } in 96 let session = Session.create ~server_url ~auth () in 97 Session.save fs ?profile session; 98 (* Set as current profile if first login or explicitly requested *) 99 let profiles = Session.list_profiles fs in 100 let profile_name = Option.value ~default:Session.default_profile profile in 101 if profiles = [] || Option.is_some profile then 102 Session.set_current_profile fs profile_name; 103 { client; session; fs; profile } 104 105let login_password ~sw ~env ?requests_config ?profile ~server_url ~email ~password () = 106 let fs = env#fs in 107 (* Create session without auth first *) 108 let requests_session = match requests_config with 109 | Some config -> Requests.Cmd.create config env sw 110 | None -> Requests.create ~sw env 111 in 112 (* Resolve the API URL from .well-known/immich if available *) 113 let server_url = resolve_api_url ~session:requests_session server_url in 114 let client = Immich.create ~session:requests_session ~sw env ~base_url:server_url in 115 (* Login using the API *) 116 let body = Immich.LoginCredential.Dto.v ~email ~password () in 117 let resp = Immich.Login.login client ~body () in 118 let access_token = Immich.Login.ResponseDto.access_token resp in 119 let user_id = Immich.Login.ResponseDto.user_id resp in 120 (* Now create a new client with the auth token *) 121 let requests_session = match requests_config with 122 | Some config -> Requests.Cmd.create config env sw 123 | None -> Requests.create ~sw env 124 in 125 let requests_session = Requests.set_auth requests_session (Requests.Auth.bearer ~token:access_token) in 126 let client = Immich.create ~session:requests_session ~sw env ~base_url:server_url in 127 (* Create and save session *) 128 let auth = Session.Jwt { access_token; user_id; email } in 129 let session = Session.create ~server_url ~auth () in 130 Session.save fs ?profile session; 131 (* Set as current profile if first login or explicitly requested *) 132 let profiles = Session.list_profiles fs in 133 let profile_name = Option.value ~default:email profile in 134 if profiles = [] || Option.is_some profile then 135 Session.set_current_profile fs profile_name; 136 { client; session; fs; profile } 137 138let resume ~sw ~env ?requests_config ?profile ~session () = 139 (* Check if JWT is expired and refresh if needed *) 140 let session = 141 if Session.is_expired session then begin 142 match Session.auth session with 143 | Session.Api_key _ -> session (* API keys don't expire *) 144 | Session.Jwt _ -> 145 (* JWT expired - for now just fail, user needs to re-login *) 146 failwith "Session expired. Please login again." 147 end 148 else session 149 in 150 create_with_session ~sw ~env ?requests_config ?profile ~session () 151 152let logout t = 153 Session.clear t.fs ?profile:t.profile () 154 155let client t = t.client 156let session t = t.session 157let profile t = t.profile 158let fs t = t.fs 159 160let is_valid t = 161 try 162 let resp = Immich.ValidateAccessToken.validate_access_token t.client () in 163 Immich.ValidateAccessToken.ResponseDto.auth_status resp 164 with _ -> false