Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ Use cached template when it is up-to-date #11

Merged
merged 12 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 107 additions & 33 deletions src/commands/new.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,98 @@
use cargo_metadata::camino::Utf8PathBuf;
#[cfg(feature = "fetch-template")]
use directories::ProjectDirs;
use log::{debug, error, info};
use log::{debug, info, warn};
use serde_json::Value;

use crate::errors::CliError;
use std::{
fs, io,
path::{Path, PathBuf},
};

#[derive(Debug, Clone)]
struct Template {
pub data: Vec<u8>,
pub sha: Option<String>,
}

#[cfg(feature = "fetch-template")]
async fn fetch_template() -> reqwest::Result<Vec<u8>> {
info!("Fetching template...");
async fn get_current_sha() -> Result<String, CliError> {
let client = reqwest::Client::new();
let response = client
.get("https://api.github.com/repos/vexide/vexide-template/commits/main?per-page=1")
.header("User-Agent", "vexide/cargo-v5")
.send()
.await;
let response = match response {
ion098 marked this conversation as resolved.
Show resolved Hide resolved
Ok(response) => response,
Err(err) => return Err(CliError::ReqwestError(err)),
};
let response_text = response.text().await.ok().unwrap_or("{}".to_string());
match &serde_json::from_str::<Value>(&response_text).unwrap_or_default()["sha"] {
Value::String(str) => Ok(str.clone()),
_ => unreachable!("Internal error: GitHub API broken"),
ion098 marked this conversation as resolved.
Show resolved Hide resolved
}
}

#[cfg(feature = "fetch-template")]
async fn fetch_template() -> Result<Template, CliError> {
debug!("Fetching template...");
let response =
reqwest::get("https://github.com/vexide/vexide-template/archive/refs/heads/main.tar.gz")
.await?;
.await;
let response = match response {
Ok(response) => response,
Err(err) => return Err(CliError::ReqwestError(err)),
};
let bytes = response.bytes().await?;

debug!("Successfully fetched template.");
Ok(bytes.to_vec())
let template = Template {
data: bytes.to_vec(),
sha: get_current_sha().await.ok(),
};
store_cached_template(template.clone());
Ok(template)
}

#[cfg(feature = "fetch-template")]
fn cached_template_path() -> Option<PathBuf> {
ProjectDirs::from("", "vexide", "cargo-v5").and_then(|dirs| {
dirs.cache_dir()
.canonicalize()
.ok()
.map(|path| path.with_file_name("vexide-template.tar.gz"))
})
fn get_cached_template() -> Option<Template> {
ion098 marked this conversation as resolved.
Show resolved Hide resolved
let sha = cached_template_dir()
.and_then(|path| fs::read_to_string(path.with_file_name("cache-id.txt")).ok());
cached_template_dir()
.map(|path| path.with_file_name("vexide-template.tar.gz"))
.and_then(|cache_file| fs::read(cache_file).ok())
.map(|data: Vec<u8>| Template { data, sha })
.inspect(|template| {log::debug!("Found cached template with sha: {:?}", template.sha)})
}

fn baked_in_template() -> Vec<u8> {
include_bytes!("./vexide-template.tar.gz").to_vec()
#[cfg(feature = "fetch-template")]
fn store_cached_template(template: Template) -> () {
cached_template_dir()
.map(|path| path.with_file_name("vexide-template.tar.gz"))
.map(|cache_file| fs::write(cache_file, &template.data));
cached_template_dir()
.map(|path| path.with_file_name("cache-id.txt"))
.map(|sha_file| {
if let Some(sha) = template.sha {
let _ = fs::write(sha_file, sha);
}
});

}

#[cfg(feature = "fetch-template")]
fn cached_template_dir() -> Option<PathBuf> {
ProjectDirs::from("", "vexide", "cargo-v5")
.and_then(|dirs| dirs.cache_dir().canonicalize().ok())
}

fn baked_in_template() -> Template {
Template {
data: include_bytes!("./vexide-template.tar.gz").to_vec(),
sha: None,
}
}

fn unpack_template(template: Vec<u8>, dir: &Utf8PathBuf) -> io::Result<()> {
Expand All @@ -55,7 +117,11 @@ fn unpack_template(template: Vec<u8>, dir: &Utf8PathBuf) -> io::Result<()> {
Ok(())
}

pub async fn new(path: Utf8PathBuf, name: Option<String>) -> Result<(), CliError> {
pub async fn new(
path: Utf8PathBuf,
name: Option<String>,
download_template: bool,
) -> Result<(), CliError> {
let dir = if let Some(name) = &name {
let dir = path.join(name);
std::fs::create_dir_all(&path).unwrap();
Expand All @@ -72,31 +138,39 @@ pub async fn new(path: Utf8PathBuf, name: Option<String>) -> Result<(), CliError
info!("Creating new project at {:?}", dir);

#[cfg(feature = "fetch-template")]
let template: Vec<u8> = match fetch_template().await {
Ok(bytes) => {
if let Some(cache_file) = cached_template_path() {
debug!("Storing fetched template in cache.");
fs::write(cache_file, &bytes).unwrap_or_else(|_| {
error!("Could not cache template.");
});
}
bytes
let template = get_cached_template();

ion098 marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "fetch-template")]
let template = match (
template.clone().and_then(|t| t.sha),
get_current_sha().await,
) {
_ if !download_template => template,
(Some(cached_sha), Ok(current_sha)) if cached_sha == current_sha => {
debug!("Cached template is current, skipping download.");
template
}
Err(_) => {
error!("Failed to fetch template, checking cached template.");
cached_template_path()
.and_then(|cache_file| fs::read(cache_file).ok())
.unwrap_or_else(|| {
error!("Failed to find cached template, using baked-in template.");
baked_in_template()
})
_ => {
debug!("Cached template is out of date.");
let fetched_template = fetch_template().await.ok();
fetched_template.or_else(|| {
warn!("Could not fetch template, falling back to cache.");
template
})
}
};

#[cfg(feature = "fetch-template")]
let template = template.unwrap_or_else(|| {
debug!("No template found in cache, using builtin template.");
baked_in_template()
});

#[cfg(not(feature = "fetch-template"))]
let template = baked_in_template();

debug!("Unpacking template...");
unpack_template(template, &dir)?;
unpack_template(template.data, &dir)?;
debug!("Successfully unpacked vexide-template!");

debug!("Renaming project to {}...", &name);
Expand Down
5 changes: 5 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub enum CliError {
#[diagnostic(code(cargo_v5::cdc2_nack))]
Nack(#[from] Cdc2Ack),

#[cfg(feature = "fetch-template")]
#[error(transparent)]
#[diagnostic(code(cargo_v5::bad_response))]
ReqwestError(#[from] reqwest::Error),

#[error(transparent)]
#[diagnostic(code(cargo_v5::image_error))]
ImageError(#[from] ImageError),
Expand Down
26 changes: 20 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use cargo_v5::{
connection::{open_connection, switch_radio_channel},
};
use chrono::Utc;
use clap::{Parser, Subcommand};
use clap::{Args, Parser, Subcommand};
use flexi_logger::{AdaptiveFormat, FileSpec, LogfileSelector, LoggerHandle};
use tokio::{runtime::Handle, select, task::block_in_place};
#[cfg(feature = "field-control")]
Expand Down Expand Up @@ -91,9 +91,15 @@ enum Command {
New {
/// The name of the project.
name: String,

#[clap(flatten)]
download_opts: DownloadOpts,
},
/// Creates a new vexide project in the current directory
Init,
Init {
#[clap(flatten)]
download_opts: DownloadOpts,
},
/// List files on flash.
#[clap(visible_alias = "ls")]
Dir,
Expand All @@ -118,6 +124,14 @@ enum Command {
FieldControl,
}

#[derive(Args, Debug)]
struct DownloadOpts {
/// Do not download the latest template online.
#[cfg_attr(feature = "fetch-template", arg(long, default_value = "false"))]
#[cfg_attr(not(feature = "fetch-template"), arg(skip = false))]
no_download_template: bool,
ion098 marked this conversation as resolved.
Show resolved Hide resolved
}

#[tokio::main]
async fn main() -> miette::Result<()> {
// Parse CLI arguments
Expand Down Expand Up @@ -250,11 +264,11 @@ async fn app(command: Command, path: Utf8PathBuf, logger: &mut LoggerHandle) ->

run_field_control_tui(&mut connection).await?;
}
Command::New { name } => {
new(path, Some(name)).await?;
Command::New { name , download_opts} => {
new(path, Some(name), !download_opts.no_download_template).await?;
}
Command::Init => {
new(path, None).await?;
Command::Init { download_opts } => {
new(path, None, !download_opts.no_download_template).await?;
}
}

Expand Down