OtaClient

Struct OtaClient 

Source
pub struct OtaClient {
    client: Client,
    token: Option<SecretString>,
}
Expand description

HTTP client for downloading OTA updates from GitHub.

This client handles the complete OTA update workflow:

  • Fetching PR information from GitHub API
  • Finding associated workflow runs
  • Downloading build artifacts and stable releases
  • Extracting and deploying updates

§Security

The GitHub personal access token is optional and wrapped in SecretString from the secrecy crate to prevent accidental exposure in logs, debug output, or error messages. The token is automatically wiped from memory when dropped. Access to the token value requires explicit use of .expose_secret().

Token is required for:

  • PR build downloads
  • Default branch artifact downloads

Token is optional for:

  • Stable release downloads (public URLs)

Fields§

§client: Client§token: Option<SecretString>

Implementations§

Source§

impl OtaClient

Source

pub fn new(github_token: Option<SecretString>) -> Result<Self, OtaError>

Creates a new OTA client with optional GitHub authentication.

Initializes an HTTP client with TLS configured using webpki-roots certificates for secure communication with GitHub’s API.

§Arguments
  • github_token - Optional personal access token wrapped in SecretString for secure handling. Required for artifact downloads, optional for stable release downloads.
§Errors

Returns OtaError::TlsConfig if the HTTP client fails to initialize with the provided TLS configuration.

§Security

The token is stored securely and will never appear in debug output or logs. It is only exposed when making authenticated API requests.

Source

fn get_token(&self) -> Result<&SecretString, OtaError>

Returns a reference to the GitHub token if available.

§Errors

Returns OtaError::NoToken if no token is configured.

Source

pub fn download_pr_artifact<F>( &self, pr_number: u32, progress_callback: F, ) -> Result<PathBuf, OtaError>
where F: FnMut(OtaProgress),

Downloads the build artifact from a GitHub pull request.

This performs the complete download workflow:

  1. Verifies sufficient disk space (100MB required)
  2. Fetches PR metadata to get the commit SHA
  3. Finds the associated “Cargo” workflow run
  4. Locates artifacts matching “cadmus-kobo-pr*” pattern
  5. Downloads the artifact ZIP file to /tmp/cadmus-ota-{pr_number}.zip

GitHub authentication is required for this operation.

§Arguments
  • pr_number - The pull request number from ogkevin/cadmus repository
  • progress_callback - Function called with progress updates during download
§Returns

The path to the downloaded ZIP file on success.

§Errors
  • OtaError::InsufficientSpace - Less than 100MB available in /tmp
  • OtaError::NoToken - GitHub token not configured
  • OtaError::PrNotFound - PR number doesn’t exist in repository
  • OtaError::NoArtifacts - No matching build artifacts found for the PR
  • OtaError::Api - GitHub API request failed
  • OtaError::Request - Network communication failed
  • OtaError::Io - Failed to write downloaded file to disk
Source

pub fn download_default_branch_artifact<F>( &self, progress_callback: F, ) -> Result<PathBuf, OtaError>
where F: FnMut(OtaProgress),

Downloads the latest build artifact from the default branch.

This performs the complete download workflow for default branch builds:

  1. Verifies sufficient disk space (100MB required)
  2. Queries GitHub API for the latest successful cargo.yml workflow run on the default branch
  3. Locates artifacts matching “cadmus-kobo-{sha}” pattern (or “cadmus-kobo-test-{sha}” with test feature)
  4. Downloads the artifact ZIP file to /tmp/cadmus-ota-{sha}.zip

GitHub authentication is required for this operation.

§Arguments
  • progress_callback - Function called with progress updates during download
§Returns

The path to the downloaded ZIP file on success.

§Errors
  • OtaError::InsufficientSpace - Less than 100MB available in /tmp
  • OtaError::NoToken - GitHub token not configured
  • OtaError::NoDefaultBranchArtifacts - No matching build artifacts found
  • OtaError::Api - GitHub API request failed
  • OtaError::Request - Network communication failed
  • OtaError::Io - Failed to write downloaded file to disk
Source

pub fn download_stable_release_artifact<F>( &self, progress_callback: F, ) -> Result<PathBuf, OtaError>
where F: FnMut(OtaProgress),

Downloads the latest stable release artifact from GitHub releases.

This performs the complete download workflow for stable releases:

  1. Verifies sufficient disk space (100MB required)
  2. Fetches the latest release from GitHub API
  3. Locates the KoboRoot.tgz asset in the release
  4. Downloads the file to /tmp/cadmus-ota-stable-release.tgz

GitHub authentication is not required for this operation as release assets are downloaded from public URLs without Authorization headers.

§Arguments
  • progress_callback - Function called with progress updates during download
§Returns

The path to the downloaded KoboRoot.tgz file on success.

§Errors
  • OtaError::InsufficientSpace - Less than 100MB available in /tmp
  • OtaError::Api - GitHub API request failed
  • OtaError::Request - Network communication failed
  • OtaError::NoArtifacts - KoboRoot.tgz not found in latest release
  • OtaError::Io - Failed to write downloaded file to disk
Source

pub fn deploy(&self, kobo_root_path: PathBuf) -> Result<PathBuf, OtaError>

Deploys KoboRoot.tgz from the specified path directly without extraction.

Used when the artifact is already in the correct format (e.g., stable releases that are distributed as bare KoboRoot.tgz files).

§Arguments
  • kobo_root_path - Path to the KoboRoot.tgz file to deploy
§Returns

The path where the file was deployed, or an error if deployment fails.

§Errors
  • OtaError::Io - Failed to read or write files
Source

fn deploy_bytes(&self, data: &[u8]) -> Result<PathBuf, OtaError>

Deploys KoboRoot.tgz data to the appropriate location.

Writes the provided data to the deployment path determined by the build configuration:

  • Test builds: temp directory
  • Emulator builds: /tmp/.kobo/KoboRoot.tgz
  • Production builds: {INTERNAL_CARD_ROOT}/.kobo/KoboRoot.tgz
§Arguments
  • data - The KoboRoot.tgz file contents to deploy
§Returns

The deployment path where KoboRoot.tgz was written.

§Errors
  • OtaError::Io - Failed to create directories or write deployment file
Source

pub fn extract_and_deploy(&self, zip_path: PathBuf) -> Result<PathBuf, OtaError>

Extracts KoboRoot.tgz from the artifact and deploys it for installation.

Opens the downloaded ZIP archive, locates the KoboRoot.tgz file, extracts it, and writes it to /mnt/onboard/.kobo/KoboRoot.tgz where the Kobo device will automatically install it on next reboot.

§Arguments
  • zip_path - Path to the downloaded artifact ZIP file
§Returns

The deployment path where KoboRoot.tgz was written.

§Errors
  • OtaError::ZipError - Failed to open or read ZIP archive
  • OtaError::DeploymentError - KoboRoot.tgz not found in archive
  • OtaError::Io - Failed to write deployment file
Source

fn fetch_default_branch(&self) -> Result<String, OtaError>

Queries the GitHub API for the repository’s default branch name.

Source

fn find_artifact_in_run( &self, run_id: u64, name_prefix: &str, ) -> Result<Artifact, OtaError>

Fetches artifacts for a workflow run and finds one matching the given prefix.

Source

fn download_by_url_to_path<F>( &self, url: &str, total_size: u64, download_path: &PathBuf, progress_callback: &mut F, use_auth: bool, ) -> Result<(), OtaError>
where F: FnMut(OtaProgress),

Downloads a file from a URL with chunked transfer and progress reporting.

Uses HTTP Range headers to request the file in chunks for resilience against network interruptions.

§Arguments
  • url - The complete download URL
  • total_size - Total file size in bytes
  • download_path - Path where the file should be saved
  • progress_callback - Function called with progress updates
  • use_auth - Whether to include Authorization header in requests
§Returns

Success if the file is written to disk, error otherwise.

Source

fn download_artifact_to_path<F>( &self, artifact: &Artifact, download_path: &PathBuf, progress_callback: &mut F, ) -> Result<(), OtaError>
where F: FnMut(OtaProgress),

Downloads an artifact ZIP to the specified path with chunked transfer and progress reporting.

GitHub authentication is required for this operation.

Source

fn download_chunk_with_retries( &self, url: &str, start: u64, end: u64, use_auth: bool, ) -> Result<Vec<u8>, OtaError>

Downloads a specific byte range of a file with automatic retry logic.

Uses HTTP Range headers to request a specific chunk of the artifact. Implements exponential backoff retry strategy for failed downloads.

§Arguments
  • url - The download URL
  • start - Starting byte offset (inclusive)
  • end - Ending byte offset (inclusive)
§Returns

The downloaded chunk data as a byte vector.

§Errors

Returns an error if all retry attempts fail.

Source

fn download_chunk( &self, url: &str, start: u64, end: u64, use_auth: bool, ) -> Result<Vec<u8>, OtaError>

Downloads a specific byte range from a URL using HTTP Range header.

§Arguments
  • url - The download URL
  • start - Starting byte offset (inclusive)
  • end - Ending byte offset (inclusive)
  • use_auth - Whether to include Authorization header
§Returns

The downloaded chunk data as a byte vector.

§Errors

Returns an error if the download fails or times out.

Source

fn download_release_asset<F>( &self, asset: &ReleaseAsset, download_path: &PathBuf, progress_callback: &mut F, ) -> Result<(), OtaError>
where F: FnMut(OtaProgress),

Downloads a release asset to the specified path with chunked transfer and progress reporting.

GitHub authentication is not required for this operation as release assets are downloaded from public URLs.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Converts Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.
§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Converts Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
§

fn as_any(&self) -> &(dyn Any + 'static)

Converts &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Converts &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> DowncastSend for T
where T: Any + Send,

§

fn into_any_send(self: Box<T>) -> Box<dyn Any + Send>

Converts Box<Trait> (where Trait: DowncastSend) to Box<dyn Any + Send>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

§

fn into_any_sync(self: Box<T>) -> Box<dyn Any + Send + Sync>

Converts Box<Trait> (where Trait: DowncastSync) to Box<dyn Any + Send + Sync>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Converts Arc<Trait> (where Trait: DowncastSync) to Arc<Any>, which can then be downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more