Skip to main content

bestool_canopy/
lib.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5mod backup;
6mod client;
7pub mod registration;
8
9/// Wire types generated at build time from canopy's OpenAPI document.
10///
11/// These are the canonical request and response types for canopy's API, and the
12/// ones to reach for first. The build script fetches the live spec and
13/// regenerates them, so they track canopy as it evolves and nothing here is
14/// hand-maintained or committed. Each type carries the schema's own description
15/// as rustdoc. (A failed fetch fails the build rather than silently using the
16/// committed snapshot, which is reserved for docs.rs and explicit offline
17/// builds — see the build script.)
18///
19/// Naming follows canopy's schema: request bodies are `…Args` (e.g.
20/// [`BackupCredentialsArgs`], [`ReportArgs`], [`BackupCapabilitiesArgs`]), and
21/// credentials come back as [`CredentialProcessOutput`].
22///
23/// The generated source is rewritten in two ways the raw JSON Schema can't
24/// express (see the build script): timestamp fields are [`jiff::Timestamp`]
25/// rather than strings, and credential secrets (`secret_access_key`,
26/// `session_token`, `repo_password`) are wrapped in [`Redacted`] so they never
27/// surface in `Debug` output or logs — read them through the inner value.
28///
29/// [`CanopyClient`] has one generated method per endpoint (also emitted from the
30/// spec into this module — e.g. `backup_credentials`, `restore_worklist`,
31/// `tags`), taking and returning these types; that's how you call canopy. The
32/// method name is the path (`/backup-credentials` → `backup_credentials`), verb-
33/// prefixed only where a path is served by several verbs. `backup_target`'s
34/// dormant-device case is read from its result via [`TargetOutcome::from_result`];
35/// any non-2xx surfaces as [`CanopyHttpError`]. The generic
36/// `get`/`request`/`request_json` escape hatch is behind the off-by-default
37/// `raw-requests` feature — reach for it only for something the generated methods
38/// don't cover.
39///
40/// [`BackupCredentialsArgs`]: schema::BackupCredentialsArgs
41/// [`ReportArgs`]: schema::ReportArgs
42/// [`BackupCapabilitiesArgs`]: schema::BackupCapabilitiesArgs
43/// [`CredentialProcessOutput`]: schema::CredentialProcessOutput
44pub mod schema {
45	include!(concat!(env!("OUT_DIR"), "/canopy_schema.rs"));
46}
47
48pub use backup::{ContainerCreds, TargetOutcome};
49pub use client::{
50	CERT_RENEW_AFTER, CanopyClient, CanopyHttpError, ClientBuilderFactory, DEFAULT_CANOPY_URL,
51	TAILSCALE_URL, device_identity, tailscale_client,
52};
53pub use reqwest;
54
55/// Wraps a sensitive value so its `Debug` output doesn't leak the contents.
56#[derive(Clone)]
57pub struct Redacted<T>(pub T);
58
59impl<T> fmt::Debug for Redacted<T> {
60	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61		f.write_str("<redacted>")
62	}
63}
64
65impl<T> std::ops::Deref for Redacted<T> {
66	type Target = T;
67	fn deref(&self) -> &T {
68		&self.0
69	}
70}
71
72impl<T: Serialize> Serialize for Redacted<T> {
73	fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
74		self.0.serialize(serializer)
75	}
76}
77
78impl<'de, T: Deserialize<'de>> Deserialize<'de> for Redacted<T> {
79	fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
80		T::deserialize(deserializer).map(Redacted)
81	}
82}