Compare commits

..

No commits in common. "8ab2690d179884785a1f21859ecbf245896870f9" and "13350f872a35ef015d323b0d6bdb59e08579a656" have entirely different histories.

10 changed files with 173 additions and 129 deletions

View File

@ -20,7 +20,7 @@ pipeline:
# http://plugins.drone.io/drone-plugins/drone-github-release/ # http://plugins.drone.io/drone-plugins/drone-github-release/
image: plugins/github-release image: plugins/github-release
files: target/release/reel-moby files: target/release/reel-moby
secrets: [github_api_key] secrets: [github_release_api_key]
when: when:
event: tag event: tag
tag: v* tag: v*

28
Cargo.lock generated
View File

@ -20,12 +20,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "anyhow"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -681,7 +675,6 @@ dependencies = [
name = "reel-moby" name = "reel-moby"
version = "1.2.1" version = "1.2.1"
dependencies = [ dependencies = [
"anyhow",
"chrono", "chrono",
"lazy_static", "lazy_static",
"regex", "regex",
@ -690,7 +683,6 @@ dependencies = [
"serde_json", "serde_json",
"structopt", "structopt",
"termion", "termion",
"thiserror",
"tui", "tui",
] ]
@ -929,26 +921,6 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "thiserror"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"

View File

@ -16,8 +16,6 @@ termion = "1.5"
regex = "1.5.4" regex = "1.5.4"
lazy_static = "1.4.0" lazy_static = "1.4.0"
structopt = "0.3.23" structopt = "0.3.23"
thiserror = "1.0.32"
anyhow = "1.0.59"
[profile.release] [profile.release]
lto = "yes" lto = "yes"

View File

@ -1,13 +1,12 @@
use std::path::PathBuf;
use structopt::StructOpt;
mod common; mod common;
mod repo; mod repo;
mod repository; mod repository;
mod ui; mod ui;
mod widget; mod widget;
use anyhow::Result;
use std::path::PathBuf;
use structopt::StructOpt;
/// helps you searching or updating tags of your used docker images /// helps you searching or updating tags of your used docker images
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
pub struct Opt { pub struct Opt {
@ -20,10 +19,8 @@ pub struct Opt {
repo: Option<String>, repo: Option<String>,
} }
fn main() -> Result<()> { fn main() {
//parse parameter //parse parameter
let opt = Opt::from_args(); let opt = Opt::from_args();
ui::create_ui(&opt)?; ui::create_ui(&opt);
Ok(())
} }

View File

@ -19,11 +19,11 @@ pub struct Images {
} }
impl Images { impl Images {
pub fn from_tag(images: &Self) -> super::Tag { pub fn convert(&self) -> super::Tag {
super::Tag { super::Tag {
name: images.tag_name.clone(), name: self.tag_name.clone(),
last_updated: Some(images.last_updated.clone()), last_updated: Some(self.last_updated.clone()),
details: images details: self
.images .images
.iter() .iter()
.map(|d| super::TagDetails { .map(|d| super::TagDetails {
@ -69,7 +69,7 @@ impl DockerHub {
} }
Ok(super::Repo { Ok(super::Repo {
tags: tags.results.iter().map(Images::from_tag).collect(), tags: tags.results.iter().map(|t| t.convert()).collect(),
next_page: tags.next_page, next_page: tags.next_page,
}) })
} }

72
src/repository/ghcr.rs Normal file
View File

@ -0,0 +1,72 @@
use serde::Deserialize;
use crate::repository::Error;
#[derive(Deserialize)]
struct Token {
token: String,
}
#[derive(Deserialize)]
pub struct Ghcr {
tags: Vec<String>,
}
impl Ghcr {
/// fetches tag information with a repository name in the form of organization/repository or library/repository in the case of official images from docker
pub fn create_repo(repo: &str) -> Result<super::Repo, Error> {
let request_token = format!("https://ghcr.io/token?scope=repository:{}:pull", repo);
let response = match reqwest::blocking::get(request_token) {
Err(e) => return Err(Error::Fetching(format!("reqwest error: {}", e))),
Ok(response) => response,
};
let token = match response.json::<Token>() {
Err(e) => return Err(Error::Converting(format!("invalid token json: {}", e))),
Ok(token) => token.token,
};
let request = format!("https://ghcr.io/v2/{}/tags/list?n=100", repo);
let client = reqwest::blocking::Client::new();
let response = match client
.get(request)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
.send()
{
Ok(result) => result,
Err(e) => return Err(Error::Fetching(format!("reqwest error: {}", e))),
};
//convert it to json
let tags = match response.json::<Self>() {
Ok(result) => result,
Err(e) => return Err(Error::Converting(format!("invalid json: {}", e))),
};
if tags.tags.is_empty() {
return Err(Error::NoTagsFound);
}
Ok(super::Repo {
tags: tags
.tags
.iter()
.map(|t| super::Tag {
name: t.clone(),
details: vec![],
last_updated: None,
})
.collect(),
next_page: None,
})
}
}
#[cfg(test)]
mod tests {
use super::Ghcr;
#[test]
fn test_ghcr() {
Ghcr::create_repo("ghcr.io/linuxserver/beets").unwrap();
}
}

View File

@ -1,24 +1,33 @@
mod dockerhub; mod dockerhub;
mod ghcr;
use std::fmt;
use chrono::DateTime; use chrono::DateTime;
use thiserror::Error;
use crate::common::display_duration_ext::DisplayDurationExt; use crate::common::display_duration_ext::DisplayDurationExt;
use crate::repo; use crate::repo;
#[derive(Debug, PartialEq, Error)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
/// couldn't fetch json with reqwest /// couldn't fetch json with reqwest
#[error("Fetching error: {0}")]
Fetching(String), Fetching(String),
/// a serde error /// a serde error
#[error("Converting error: {0}")]
Converting(String), Converting(String),
/// invalid repos show a valid json with 0 tags /// invalid repos show a valid json with 0 tags
#[error("Given Repo does not exists or has 0 tags.")]
NoTagsFound, NoTagsFound,
} }
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Fetching(s) => write!(f, "Fetching error: {}", s),
Error::Converting(s) => write!(f, "Converting error: {}", s),
Error::NoTagsFound => write!(f, "Given Repo has 0 tags. Is it valid?"),
}
}
}
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct TagDetails { pub struct TagDetails {
pub arch: Option<String>, pub arch: Option<String>,
@ -74,10 +83,10 @@ impl Repo {
Err(e) => return Err(Error::Converting(format!("{}", e))), Err(e) => return Err(Error::Converting(format!("{}", e))),
}; };
if registry.unwrap_or_default().is_empty() { if registry.unwrap_or_default() == "ghcr.io" {
dockerhub::DockerHub::create_repo(&repo) ghcr::Ghcr::create_repo(&repo)
} else { } else {
Err(Error::Converting("This registry is not supported".into())) dockerhub::DockerHub::create_repo(&repo)
} }
} }

View File

@ -1,18 +1,17 @@
use anyhow::Result; use std::{io, thread};
use crate::Opt;
use termion::event::Key; use termion::event::Key;
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
use tui::backend::TermionBackend; use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal; use tui::Terminal;
use std::{io, thread};
use crate::repository; use crate::repository;
use crate::widget::info; use crate::widget::info;
use crate::widget::repo_entry; use crate::widget::repo_entry;
use crate::widget::service_switcher; use crate::widget::service_switcher;
use crate::widget::tag_list; use crate::widget::tag_list;
use crate::Opt;
pub struct Ui { pub struct Ui {
state: State, state: State,
@ -54,14 +53,14 @@ impl std::iter::Iterator for State {
} }
impl Ui { impl Ui {
pub fn run(opt: &Opt, switcher: service_switcher::ServiceSwitcher) -> Result<()> { pub fn run(opt: &Opt) {
let repo_id = opt.repo.as_deref(); let repo_id = opt.repo.as_deref();
let mut ui = Ui { let mut ui = Ui {
state: State::SelectService, state: State::SelectService,
repo: repo_entry::RepoEntry::new(repo_id), repo: repo_entry::RepoEntry::new(repo_id),
tags: tag_list::TagList::with_status("Tags are empty"), tags: tag_list::TagList::with_status("Tags are empty"),
services: switcher, services: service_switcher::ServiceSwitcher::new(&opt.file).unwrap(),
details: crate::widget::details::Details::new(), details: crate::widget::details::Details::new(),
info: info::Info::new("Select image of edit Repository"), info: info::Info::new("Select image of edit Repository"),
}; };
@ -71,9 +70,9 @@ impl Ui {
} }
//setup tui //setup tui
let stdout = io::stdout().into_raw_mode()?; let stdout = io::stdout().into_raw_mode().unwrap();
let backend = TermionBackend::new(stdout); let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend).unwrap();
//setup input thread //setup input thread
let receiver = super::spawn_stdin_channel(); let receiver = super::spawn_stdin_channel();
@ -81,32 +80,34 @@ impl Ui {
//core interaction loop //core interaction loop
'core: loop { 'core: loop {
//draw //draw
terminal.draw(|rect| { terminal
let chunks = Layout::default() .draw(|rect| {
.direction(Direction::Vertical) let chunks = Layout::default()
.constraints( .direction(Direction::Vertical)
[ .constraints(
Constraint::Length(10), [
Constraint::Length(3), Constraint::Length(10),
Constraint::Min(7), Constraint::Length(3),
Constraint::Length(2), Constraint::Min(7),
] Constraint::Length(2),
.as_ref(), ]
) .as_ref(),
.split(rect.size()); )
.split(rect.size());
let (list, state) = ui.services.render(ui.state == State::SelectService); let (list, state) = ui.services.render(ui.state == State::SelectService);
rect.render_stateful_widget(list, chunks[0], state); rect.render_stateful_widget(list, chunks[0], state);
rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[1]); rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[1]);
let (list, state) = ui.tags.render(ui.state == State::SelectTag); let (list, state) = ui.tags.render(ui.state == State::SelectTag);
let more_chunks = Layout::default() let more_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Min(15), Constraint::Length(28)].as_ref()) .constraints([Constraint::Min(15), Constraint::Length(28)].as_ref())
.split(chunks[2]); .split(chunks[2]);
rect.render_stateful_widget(list, more_chunks[0], state); rect.render_stateful_widget(list, more_chunks[0], state);
rect.render_widget(ui.details.render(), more_chunks[1]); rect.render_widget(ui.details.render(), more_chunks[1]);
rect.render_widget(ui.info.render(), chunks[3]); rect.render_widget(ui.info.render(), chunks[3]);
})?; })
.unwrap();
//handle input //handle input
match receiver.try_recv() { match receiver.try_recv() {
@ -218,8 +219,6 @@ impl Ui {
thread::sleep(std::time::Duration::from_millis(32)); thread::sleep(std::time::Duration::from_millis(32));
} }
terminal.clear()?; terminal.clear().unwrap();
Ok(())
} }
} }

View File

@ -1,23 +1,20 @@
mod default; mod default;
mod no_yaml; mod no_yaml;
use anyhow::Result;
use termion::input::TermRead;
use crate::widget::service_switcher;
use crate::Opt;
use std::sync::mpsc; use std::sync::mpsc;
use std::{io, thread}; use std::{io, thread};
pub fn create_ui(opt: &Opt) -> Result<()> { use crate::Opt;
use termion::input::TermRead;
use crate::widget::service_switcher;
pub fn create_ui(opt: &Opt) {
let service_result = service_switcher::ServiceSwitcher::new(&opt.file); let service_result = service_switcher::ServiceSwitcher::new(&opt.file);
match service_result { match service_result {
None => no_yaml::NoYaml::run(opt), None => no_yaml::NoYaml::run(opt),
Some(switcher) => default::Ui::run(opt, switcher), Some(_) => default::Ui::run(opt),
}?; }
Ok(())
} }
/// create a thread for catching input and send them to core loop /// create a thread for catching input and send them to core loop

View File

@ -1,12 +1,11 @@
use anyhow::Result; use std::{io, thread};
use termion::event::Key; use termion::event::Key;
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
use tui::backend::TermionBackend; use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal; use tui::Terminal;
use std::{io, thread};
use crate::widget::details; use crate::widget::details;
use crate::widget::info; use crate::widget::info;
use crate::widget::repo_entry; use crate::widget::repo_entry;
@ -49,7 +48,7 @@ pub struct NoYaml {
} }
impl NoYaml { impl NoYaml {
pub fn run(opt: &Opt) -> Result<()> { pub fn run(opt: &Opt) {
let repo_id = opt.repo.as_deref(); let repo_id = opt.repo.as_deref();
let mut ui = NoYaml { let mut ui = NoYaml {
@ -66,9 +65,9 @@ impl NoYaml {
} }
//setup tui //setup tui
let stdout = io::stdout().into_raw_mode()?; let stdout = io::stdout().into_raw_mode().unwrap();
let backend = TermionBackend::new(stdout); let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend).unwrap();
//setup input thread //setup input thread
let receiver = super::spawn_stdin_channel(); let receiver = super::spawn_stdin_channel();
@ -76,29 +75,31 @@ impl NoYaml {
//core interaction loop //core interaction loop
'core: loop { 'core: loop {
//draw //draw
terminal.draw(|rect| { terminal
let chunks = Layout::default() .draw(|rect| {
.direction(Direction::Vertical) let chunks = Layout::default()
.constraints( .direction(Direction::Vertical)
[ .constraints(
Constraint::Length(3), [
Constraint::Min(7), Constraint::Length(3),
Constraint::Length(2), Constraint::Min(7),
] Constraint::Length(2),
.as_ref(), ]
) .as_ref(),
.split(rect.size()); )
.split(rect.size());
rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[0]); rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[0]);
let (list, state) = ui.tags.render(ui.state == State::SelectTag); let (list, state) = ui.tags.render(ui.state == State::SelectTag);
let more_chunks = Layout::default() let more_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Min(15), Constraint::Length(28)].as_ref()) .constraints([Constraint::Min(15), Constraint::Length(28)].as_ref())
.split(chunks[1]); .split(chunks[1]);
rect.render_stateful_widget(list, more_chunks[0], state); rect.render_stateful_widget(list, more_chunks[0], state);
rect.render_widget(ui.details.render(), more_chunks[1]); rect.render_widget(ui.details.render(), more_chunks[1]);
rect.render_widget(ui.info.render(), chunks[2]); rect.render_widget(ui.info.render(), chunks[2]);
})?; })
.unwrap();
//handle input //handle input
match receiver.try_recv() { match receiver.try_recv() {
@ -155,7 +156,6 @@ impl NoYaml {
thread::sleep(std::time::Duration::from_millis(32)); thread::sleep(std::time::Duration::from_millis(32));
} }
terminal.clear()?; terminal.clear().unwrap();
Ok(())
} }
} }