feat: add cache
Some checks failed
continuous-integration/drone/push Build is failing

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2024-09-14 20:45:49 +02:00
parent 1cc771be1e
commit 6c94f02428
11 changed files with 416 additions and 23 deletions

View File

@@ -20,6 +20,10 @@ toml = "0.8.19"
gitea-rs = { git = "https://git.front.kjuulh.io/kjuulh/gitea-rs", version = "1.22.1" }
url = "2.5.2"
octocrab = "0.39.0"
dirs = "5.0.1"
prost = "0.13.2"
prost-types = "0.13.2"
bytes = "1.7.1"
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
package gitnow.v1;
message Repositories {
repeated Repository repositories = 1;
}
message Repository {
string provider = 1;
string owner = 2;
string repo_name= 3;
string ssh_url = 4;
}

View File

@@ -0,0 +1,96 @@
use std::path::PathBuf;
use anyhow::Context;
use tokio::io::AsyncWriteExt;
use crate::{app::App, cache_codec::CacheCodecApp, config::Config, git_provider::Repository};
pub struct Cache {
app: &'static App,
}
impl Cache {
pub fn new(app: &'static App) -> Self {
Self { app }
}
pub async fn update(&self, repositories: &[Repository]) -> anyhow::Result<()> {
tracing::debug!(repository_len = repositories.len(), "storing repositories");
let location = self.app.config.get_cache_file_location()?;
tracing::trace!("found cache location: {}", location.display());
if let Some(parent) = location.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let cache_content = self
.app
.cache_codec()
.serialize_repositories(repositories)?;
let mut cache_file = tokio::fs::File::create(location)
.await
.context("failed to create cache file")?;
cache_file
.write_all(&cache_content)
.await
.context("failed to write cache content to file")?;
Ok(())
}
pub async fn get(&self) -> anyhow::Result<Option<Vec<Repository>>> {
tracing::debug!("fetching repositories");
let location = self.app.config.get_cache_file_location()?;
if !location.exists() {
tracing::debug!(
location = location.display().to_string(),
"cache doesn't exist"
);
return Ok(None);
}
let file = tokio::fs::read(location).await?;
if file.is_empty() {
tracing::debug!("cache file appears to be empty");
return Ok(None);
}
let repos = match self.app.cache_codec().deserialize_repositories(file) {
Ok(repos) => repos,
Err(e) => {
tracing::warn!(error = e.to_string(), "failed to deserialize repositories");
return Ok(None);
}
};
Ok(Some(repos))
}
}
pub trait CacheApp {
fn cache(&self) -> Cache;
}
impl CacheApp for &'static App {
fn cache(&self) -> Cache {
Cache::new(self)
}
}
pub trait CacheConfig {
fn get_cache_location(&self) -> anyhow::Result<PathBuf>;
fn get_cache_file_location(&self) -> anyhow::Result<PathBuf>;
}
impl CacheConfig for Config {
fn get_cache_location(&self) -> anyhow::Result<PathBuf> {
Ok(self.settings.cache.location.clone())
}
fn get_cache_file_location(&self) -> anyhow::Result<PathBuf> {
Ok(self.get_cache_location()?.join("cache.proto"))
}
}

View File

@@ -0,0 +1,61 @@
use std::io::Cursor;
use anyhow::Context;
use prost::Message;
use crate::{app::App, git_provider::Repository};
mod proto_codec {
include!("gen/gitnow.v1.rs");
}
pub struct CacheCodec {}
impl CacheCodec {
pub fn new() -> Self {
Self {}
}
pub fn serialize_repositories(&self, repositories: &[Repository]) -> anyhow::Result<Vec<u8>> {
let mut codec_repos = proto_codec::Repositories::default();
for repo in repositories.iter().cloned() {
codec_repos.repositories.push(proto_codec::Repository {
provider: repo.provider,
owner: repo.owner,
repo_name: repo.repo_name,
ssh_url: repo.ssh_url,
});
}
Ok(codec_repos.encode_to_vec())
}
pub fn deserialize_repositories(&self, content: Vec<u8>) -> anyhow::Result<Vec<Repository>> {
let codex_repos = proto_codec::Repositories::decode(&mut Cursor::new(content))
.context("failed to decode protobuf repositories")?;
let mut repos = Vec::new();
for codec_repo in codex_repos.repositories {
repos.push(Repository {
provider: codec_repo.provider,
owner: codec_repo.owner,
repo_name: codec_repo.repo_name,
ssh_url: codec_repo.ssh_url,
});
}
Ok(repos)
}
}
pub trait CacheCodecApp {
fn cache_codec(&self) -> CacheCodec;
}
impl CacheCodecApp for &'static App {
fn cache_codec(&self) -> CacheCodec {
CacheCodec::new()
}
}

View File

@@ -1,4 +1,4 @@
use crate::{ app::App, projects_list::ProjectsListApp};
use crate::{app::App, cache::CacheApp, projects_list::ProjectsListApp};
#[derive(Debug, Clone)]
pub struct RootCommand {
@@ -14,7 +14,16 @@ impl RootCommand {
pub async fn execute(&mut self) -> anyhow::Result<()> {
tracing::debug!("executing");
let repositories = self.app.projects_list().get_projects().await?;
let repositories = match self.app.cache().get().await? {
Some(repos) => repos,
None => {
let repositories = self.app.projects_list().get_projects().await?;
self.app.cache().update(&repositories).await?;
repositories
}
};
for repo in &repositories {
tracing::info!("repo: {}", repo.to_rel_path().display());

View File

@@ -15,12 +15,22 @@ pub struct Config {
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Settings {
#[serde(default)]
cache: Cache,
pub cache: Cache,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Cache {
location: Option<PathBuf>,
pub location: PathBuf,
}
impl Default for Cache {
fn default() -> Self {
let home = dirs::home_dir().unwrap_or_default();
Self {
location: home.join(".cache/gitnow"),
}
}
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
@@ -162,6 +172,9 @@ mod test {
#[test]
fn test_can_parse_config() -> anyhow::Result<()> {
let content = r#"
[settings.cache]
location = ".cache/gitnow"
[[providers.github]]
current_user = "kjuulh"
access_token = "some-token"
@@ -237,7 +250,7 @@ mod test {
},
settings: Settings {
cache: Cache {
location: Some(PathBuf::from("$HOME/.cache/gitnow/"))
location: PathBuf::from(".cache/gitnow/")
}
}
},
@@ -262,7 +275,7 @@ mod test {
gitea: vec![]
},
settings: Settings {
cache: Cache { location: None }
cache: Cache::default()
}
},
config

View File

@@ -0,0 +1,21 @@
// @generated
// This file is @generated by prost-build.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Repositories {
#[prost(message, repeated, tag="1")]
pub repositories: ::prost::alloc::vec::Vec<Repository>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Repository {
#[prost(string, tag="1")]
pub provider: ::prost::alloc::string::String,
#[prost(string, tag="2")]
pub owner: ::prost::alloc::string::String,
#[prost(string, tag="3")]
pub repo_name: ::prost::alloc::string::String,
#[prost(string, tag="4")]
pub ssh_url: ::prost::alloc::string::String,
}
// @@protoc_insertion_point(module)

View File

@@ -6,6 +6,8 @@ use commands::root::RootCommand;
use config::Config;
mod app;
mod cache;
mod cache_codec;
mod commands;
mod config;
mod git_provider;