Google Docs API client for OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-gauth: add Local_store, share across gdocs/gsheets/gslides

Lift the per-tool [Store] modules into one [Gauth.Local_store]. All
three Google CLI tools ([gdocs], [gsheets], [gslides]) now point at a
single XDG config directory ([$XDG_CONFIG_HOME/google/{client,token}.json])
so a user runs [install] and [login] once and every tool that
recognizes the granted scopes can read the resulting token.

Before, each package had a near-identical 60-line [Store] (atomic
write, save/load client and token, [acquire] returning a refreshable
[Gauth.token]) keyed off its own per-tool app name -- three copies
with cosmetic differences (the [cli_name] in the
"Run [<tool> install] first" diagnostic). Each per-tool [Store] is
now a thin wrapper:

include Gauth.Local_store
let acquire http ~clock ~fs =
Gauth.Local_store.acquire http ~clock ~fs ~cli_name:"<tool>"

The shared [Local_store] keeps the atomic-write pattern from the
prior [ocaml-gdocs] / [ocaml-gsheets] / [ocaml-gslides] commits
(sibling [.tmp.<pid>] file with [`Exclusive 0o600], then [rename]
over the target -- no chmod-after-write race, crash-before-rename
leaves the original target untouched).

Side-effect: on first run after this lands, users must re-run
[install] and [login] -- their existing
[~/.config/{gdocs,gsheets,gslides}/] directories are no longer
consulted. Pre-1.0 packages, no migration script.

Move:

- [Gauth.Local_store] in [gauth/lib/gauth.{ml,mli}]: client record,
config_dir/client_path/token_path, save_client/load_client,
save_token/load_token, clear_token, persist, acquire (with
[~cli_name:string] for the diagnostic).
- [gauth] now depends on [nox-xdge] for XDG paths (and [unix] which
was already pulled in transitively).
- [Gdocs.Store], [Gsheets.Store], [Gslides.Store] reduce to ~3 lines
each ([include Gauth.Local_store] + specialized [acquire]).
- Per-tool [lib/dune] and [dune-project] drop their direct
[nox-xdge] / [unix] deps -- those come transitively through gauth.
- Two store tests updated: the "config dir contains '<tool>'"
assertions become "config dir is shared 'google'" since the path
is now [.../google/...] for every tool.

All 307 tests across the four packages pass: gauth (5), gdocs (61),
gsheets (156), gslides (85). [monopam lint] reports the same
pre-existing [oauth unused] / [base64 ptime unused] false positives,
nothing new.

Followups still in flight: the [nox-xdge] -> [nox-xdg] rename is
tracked separately so it can sweep every consumer in one commit
without conflating with this refactor.

+13 -122
-1
dune-project
··· 32 32 (logs (>= 0.7)) 33 33 (oauth (>= 0.1)) 34 34 (requests (>= 0.1)) 35 - (nox-xdge (>= 0.1)) 36 35 (alcotest :with-test) 37 36 (nox-crypto-rng :with-test) 38 37 (odoc :with-doc)
-1
gdocs.opam
··· 25 25 "logs" {>= "0.7"} 26 26 "oauth" {>= "0.1"} 27 27 "requests" {>= "0.1"} 28 - "nox-xdge" {>= "0.1"} 29 28 "alcotest" {with-test} 30 29 "nox-crypto-rng" {with-test} 31 30 "odoc" {with-doc}
+1 -12
lib/dune
··· 1 1 (library 2 2 (name gdocs) 3 3 (public_name gdocs) 4 - (libraries 5 - cmarkit 6 - eio 7 - fmt 8 - gauth 9 - nox-http 10 - nox-json 11 - logs 12 - requests 13 - unix 14 - uri 15 - nox-xdge)) 4 + (libraries cmarkit eio fmt gauth nox-http nox-json logs requests uri)) 16 5 17 6 (mdx 18 7 (files gdocs.mli)
+2 -60
lib/store.ml
··· 1 - (** Config-directory storage for gdocs: client credentials and token. 2 - 3 - Uses {!Xdge} for Eio-aware XDG Base Directory resolution, so the config 4 - directory honors [XDG_CONFIG_HOME] and platform conventions. *) 5 - 6 - let app_name = "gdocs" 7 - let context fs = Xdge.v (fs :> Eio.Fs.dir_ty Eio.Path.t) app_name 8 - let config_dir fs = Xdge.config_dir (context fs) 9 - let client_path fs = Eio.Path.(config_dir fs / "client.json") 10 - let token_path fs = Eio.Path.(config_dir fs / "token.json") 11 - 12 - type client = { client_id : string; client_secret : string } 13 - 14 - let client_jsont = 15 - let open Json.Codec in 16 - Object.map ~kind:"gdocs_client" (fun client_id client_secret -> 17 - { client_id; client_secret }) 18 - |> Object.member "client_id" string ~enc:(fun c -> c.client_id) 19 - |> Object.member "client_secret" string ~enc:(fun c -> c.client_secret) 20 - |> Object.skip_unknown |> Object.seal 21 - 22 - (* Atomic write: create a sibling tmp file with O_CREAT|O_EXCL at 0600, 23 - write the body, then atomically rename it over the target. The result 24 - is never observed at a wider mode, and a crash before [rename] leaves 25 - the target untouched (instead of half-written or zero-byte). *) 26 - let save_file path data = 27 - let dir, base = path in 28 - let tmp = (dir, Fmt.str "%s.tmp.%d" base (Unix.getpid ())) in 29 - (try Eio.Path.unlink tmp with Eio.Io _ -> ()); 30 - Eio.Path.save ~create:(`Exclusive 0o600) tmp data; 31 - Eio.Path.rename tmp path 32 - 33 - let load_file path = 34 - if Eio.Path.is_file path then Some (Eio.Path.load path) else None 35 - 36 - let save_client fs c = 37 - save_file (client_path fs) (Json.to_string client_jsont c) 38 - 39 - let load_client fs = 40 - match load_file (client_path fs) with 41 - | None -> None 42 - | Some body -> ( 43 - match Json.of_string client_jsont body with 44 - | Ok c -> Some c 45 - | Error _ -> None) 46 - 47 - let save_token fs data = save_file (token_path fs) data 48 - let load_token fs = load_file (token_path fs) 49 - 50 - let clear_token fs = 51 - let path = token_path fs in 52 - if Eio.Path.is_file path then try Eio.Path.unlink path with Eio.Io _ -> () 53 - 54 - let persist fs token = save_token fs (Gauth.to_json token) 1 + include Gauth.Local_store 55 2 56 3 let acquire http ~clock ~fs = 57 - match load_client fs with 58 - | None -> Error (`Msg "no client credentials. Run `gdocs install` first.") 59 - | Some { client_id; client_secret } -> ( 60 - match load_token fs with 61 - | None -> Error (`Msg "not logged in. Run `gdocs login` first.") 62 - | Some json -> Gauth.of_json http ~clock ~client_id ~client_secret json) 4 + Gauth.Local_store.acquire http ~clock ~fs ~cli_name:"gdocs"
+10 -48
lib/store.mli
··· 1 - (** On-disk storage of gdocs client credentials and user token. 2 - 3 - Files live under the XDG config directory, typically [$HOME/.config/gdocs/]: 4 - - [client.json] -- OAuth client ID/secret (shared across users of this 5 - install). 6 - - [token.json] -- per-user OAuth access/refresh token. *) 7 - 8 - val config_dir : _ Eio.Path.t -> Eio.Fs.dir_ty Eio.Path.t 9 - (** [config_dir fs] is the gdocs config directory as an Eio path. *) 10 - 11 - val client_path : _ Eio.Path.t -> Eio.Fs.dir_ty Eio.Path.t 12 - (** [client_path fs] is the path to the saved OAuth client file. *) 13 - 14 - val token_path : _ Eio.Path.t -> Eio.Fs.dir_ty Eio.Path.t 15 - (** [token_path fs] is the path to the saved user-token file. *) 16 - 17 - type client = { client_id : string; client_secret : string } 18 - (** OAuth client credentials, as produced by [gdocs install]. *) 19 - 20 - val save_client : _ Eio.Path.t -> client -> unit 21 - (** [save_client fs c] writes [c] to {!client_path}. The file is created with 22 - mode 0600; if it already exists the mode is tightened to 0600. The parent 23 - directory is created by {!Xdge} with the permissions {!Xdge.v} assigns (0755 24 - on Unix). *) 25 - 26 - val load_client : _ Eio.Path.t -> client option 27 - (** [load_client fs] reads the client file, or [None] if absent or malformed. *) 28 - 29 - val save_token : _ Eio.Path.t -> string -> unit 30 - (** [save_token fs body] writes the token JSON produced by {!Gauth.to_json}, 31 - with the same permission handling as {!save_client}. *) 1 + (** Per-tool wrapper over {!Gauth.Local_store}. 32 2 33 - val load_token : _ Eio.Path.t -> string option 34 - (** [load_token fs] reads the saved token JSON, or [None] if absent. *) 3 + All Google CLI tools in this stack share one config directory 4 + ([$XDG_CONFIG_HOME/google/]) so a user runs [install] and [login] once and 5 + every tool that recognizes the granted scopes can read the resulting token. 6 + This module is a re-export of {!Gauth.Local_store} with {!acquire} 7 + preconfigured to surface ["gdocs"] in the "run [install/login] first" 8 + diagnostic. *) 35 9 36 - val clear_token : _ Eio.Path.t -> unit 37 - (** [clear_token fs] removes the saved token file if it exists. Used by 38 - [gdocs install] when the OAuth client is reinstalled, since tokens issued 39 - against the old client cannot be refreshed against the new one. *) 10 + include module type of Gauth.Local_store 40 11 41 12 val acquire : 42 13 Requests.t -> 43 14 clock:_ Eio.Time.clock -> 44 15 fs:_ Eio.Path.t -> 45 16 (Gauth.token, [ `Msg of string ]) result 46 - (** [acquire http ~clock ~fs] loads the saved client and token and returns a 47 - refreshable {!Gauth.token}. Returns an error describing the missing 48 - prerequisite (client or token) if either is absent. *) 49 - 50 - val persist : _ Eio.Path.t -> Gauth.token -> unit 51 - (** [persist fs token] serializes [token]'s current state to disk via 52 - {!save_token}. Call this at the end of a command so that any refresh 53 - performed in-memory during the run survives to the next invocation -- 54 - otherwise the next run reloads the stale pre-refresh token from disk and 55 - refreshes unnecessarily (or fails once the stale access token expires past 56 - the refresh_token's own lifetime). *) 17 + (** [acquire http ~clock ~fs] is {!Gauth.Local_store.acquire} with 18 + [~cli_name:"gdocs"]. *)