11 Commits

Author SHA1 Message Date
024646acf4 WIP: Deb upload, todos, and errata 2025-09-14 11:57:46 -05:00
ab8b21d01c Fix deserialization with some attributes
The "repository" field doesn't have a type right now, so I'm going to
skip deserializing it. The returned JSON has a value for this field, but
Serde will ignore it.

The `Packages.pkg_type` field is called "type" in the JSON string (and
Go code, as mentioned in the comment). Similarly, the PackageType enum
I have to represent this field in Rust has capital starting letters
where the JSON uses all lowercase. Both problems are solved with a Serde
rename attribute.
2025-09-12 13:00:11 -05:00
c6dd63143e Fix: Typo, missing 'a' in Package.created_at 2025-09-12 12:59:56 -05:00
94ec8cfd71 Add and connect "list-packages" subcommand
And with that I have the API call being made!... except it doesn't work
quite right.
2025-09-12 11:53:09 -05:00
55b992b5c4 Impl the list_packages API function 2025-09-12 09:29:33 -05:00
7b02063bb7 Add new structs for describing packages 2025-09-12 09:28:16 -05:00
59f67e1d4e Bump version to 4.0-dev 2025-09-12 09:24:29 -05:00
d982f42ae7 Update dependency versions for Debian 13 "Trixie"
I'm deciding that I'll only support the latest stable release of
Debian. Somehow I doubt anyone is using this tool, and those who do are
unlikely to need an even longer support window than Debian's stable
release period.

This change bumps the dependencies to match those available in Debian
13. Some upgrades would have already happened, while others are blocked
by the SemVer rules.

For example, Clap 4.0 to 4.5 would happen automatically, but TOML 0.5 to
0.8 would not.
2025-09-11 14:12:12 -05:00
641efc3bf7 Update automation workflow with new CLI args
All checks were successful
/ Compile and upload a release build (release) Successful in 1m10s
2025-07-22 09:41:54 -05:00
144fba5373 Bump crate version to v3.0.0
Some checks failed
/ Compile and upload a release build (release) Failing after 49s
2025-07-21 16:37:37 -05:00
7f35b808e5 Lint and format 2025-07-21 16:37:14 -05:00
9 changed files with 240 additions and 13 deletions

View File

@@ -22,7 +22,9 @@ jobs:
- name: Upload the program (using itself!)
run: >
target/release/gt-tool-${{ github.ref_name }}-$(arch)
-u ${{ vars.DEST_GITEA }} -r ${{ vars.DEST_REPO }}
-u ${{ vars.DEST_GITEA }}
-o ${{ vars.DEST_OWNER }}
-r ${{ vars.DEST_REPO }}
upload-release
"${{ github.ref_name }}"
target/release/gt-tool-${{ github.ref_name }}-$(arch)

View File

@@ -1,16 +1,16 @@
[package]
name = "gt-tool"
version = "3.0.0-alpha.1"
version = "4.0.0-dev"
edition = "2024"
[dependencies]
clap = { version = "4.0.7", features = ["derive", "env"] }
colored = "2.0.0"
itertools = "0.10.0"
reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] }
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
toml = "0.5"
clap = { version = "4.5.23", features = ["derive", "env"] }
colored = "2.2.0"
itertools = "0.13.0"
reqwest = { version = "0.12.15", features = ["json", "stream", "multipart"] }
serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
toml = "0.8.19"
# Packages available in Debian (Sid)
# clap = "4.5.23"

View File

@@ -126,3 +126,11 @@ Similar to how unspecified project settings will fall back to those in the "`[al
| token | Gitea auth token, exactly the same as `$RELEASE_KEY_GITEA` |
Additional keys are quietly ignored. The config loading is done by querying a HashMap, so anything not touched doesn't get inspected. The only requirements are that the file is valid TOML, and that these keys are all strings.h
# Errata
- Gitea's package *uploading* API endpoints are not documented with Swagger.
* https://github.com/go-gitea/gitea/issues/30597
* It seems this is intentional because of which side defines the protocol (the consumer, not Gitea).
* This is an open issue (as of 12-Sep-2025) that doesn't have much attention.
* HTTP-POST (`curl`) is sufficient for upload. Use that either directly or as reference to make custom code.

View File

@@ -1,4 +1,42 @@
pub fn list_packages() {}
use std::{fs, path::Path};
use crate::structs::package::{Package, PackageType};
/// Gets all packages of an owner
///
/// https://github.com/go-gitea/gitea/blob/main/routers/api/v1/packages/package.go#L22
///
/// Matches the route `GET /packages/{owner}`
pub async fn list_packages(
client: &reqwest::Client,
gitea_url: &str,
owner: &str,
pkg_type: Option<PackageType>,
) -> crate::Result<Vec<Package>> {
let request_url = format!("{gitea_url}/api/v1/packages/{owner}");
// add the query parameter only when a package type filter has been set.
let request_url = if let Some(pkg_type) = pkg_type {
let pkg_type = pkg_type.to_string();
format!("{request_url}?type={pkg_type}")
} else {
request_url
};
let req = client.get(request_url).send().await;
let response = req.map_err(crate::Error::WrappedReqwestErr)?;
if response.status().is_success() {
let release_list = response
.json::<Vec<Package>>()
.await
.map_err(crate::Error::WrappedReqwestErr)?;
return Ok(release_list);
} else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?;
return Err(crate::Error::ApiErrorMessage(mesg));
}
panic!("Reached the end of `api::list_packages()` without matching a return pathway.");
}
pub fn get_packages() {}
pub fn delete_package() {}
pub fn list_package_files() {}
@@ -6,3 +44,73 @@ pub fn get_latest_package_version() {}
pub fn link_package() {}
pub fn unlink_package() {}
pub fn search_packages() {}
/// Upload a Debian package and link it to it's source repository
fn upload_debian(
client: &reqwest::Client,
gitea_url: &str,
repo: &str,
file: &Path,
owner: &str,
distribution: &str,
component: &str,
) -> crate::Result<()> {
let request_url = format!("{gitea_url}/api/packages/{owner}/debian/pool/{distribution}/{component}/upload");
match file.try_exists() {
Ok(true) => (),
Ok(false) => return Err(crate::Error::NoSuchFile),
Err(e) => {
eprintln!("Uh oh! The file-exists check couldn't be done: {e}");
panic!(
"TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
);
}
};
let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap())
.file_name("deb-pkg")
.mime_str("text/plain")?;
Ok(())
}
/// Uploads a package file to the Gitea registry.
///
/// This route is not documented in Swagger. The reason seems to be that some
/// package managers have their own upload protocol (e.g. `docker push`). Gitea
/// implements it but does not define it, so it doesn't include the API docs.
pub fn upload_package(
client: &reqwest::Client,
gitea_url: &str,
file: String, // TODO: Use a path buffer of some flavor.
) {
let request_url = format!("{gitea_url}/{}", route_for_upload(PackageType::Debian));
}
fn route_for_upload(kind: PackageType) -> &'static str {
match kind {
PackageType::Alpine => "https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository}",
PackageType::Arch => "api/packages/{owner}/arch/{repository}",
PackageType::Cargo => todo!(),
PackageType::Chef => todo!(),
PackageType::Composer => todo!(),
PackageType::Conan => todo!(),
PackageType::Conda => todo!(),
PackageType::Container => todo!(),
PackageType::Cran => todo!(),
PackageType::Debian => "https://gitea.example.com/api/packages/{owner}/debian/pool/{distribution}/{component}/upload",
PackageType::Generic => todo!(),
PackageType::Go => todo!(),
PackageType::Helm => todo!(),
PackageType::Maven => todo!(),
PackageType::Npm => todo!(),
PackageType::Nuget => todo!(),
PackageType::Pub => todo!(),
PackageType::PyPi => todo!(),
PackageType::Rpm => todo!(),
PackageType::RubyGems => todo!(),
PackageType::Swift => todo!(),
PackageType::Vagrant => todo!(),
}
}

View File

@@ -44,4 +44,5 @@ pub enum Commands {
#[arg()]
files: Vec<String>,
},
ListPackages,
}

View File

@@ -31,16 +31,18 @@ async fn main() -> Result<(), gt_tool::Error> {
.or(config.gitea_url)
.ok_or(gt_tool::Error::MissingGiteaUrl)?;
let owner = args.owner
let owner = args
.owner
.or(config.owner)
.ok_or(gt_tool::Error::MissingRepoOwner)?;
let repo = args.repo
let repo = args
.repo
.or(config.repo)
.or_else(infer_repo)
.ok_or(gt_tool::Error::MissingRepoName)?;
let repo_fqrn = String::from(format!("{owner}/{repo}"));
let repo_fqrn = format!("{owner}/{repo}");
let mut headers = reqwest::header::HeaderMap::new();
headers.append(ACCEPT, header::HeaderValue::from_static("application/json"));
@@ -132,6 +134,16 @@ async fn main() -> Result<(), gt_tool::Error> {
return Err(gt_tool::Error::NoSuchRelease);
}
}
gt_tool::cli::Commands::ListPackages => {
let packages = gt_tool::api::packages::list_packages(&client, &gitea_url, &owner, None).await?;
itertools::Itertools::intersperse(
packages
.iter()
.rev()
.map(|pkg| pkg.colorized()),
String::from("")
).for_each(|package| println!("{package}"));
}
}
Ok(())

View File

@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
pub mod package;
pub mod release;
pub mod repo;

94
src/structs/package.rs Normal file
View File

@@ -0,0 +1,94 @@
use colored::Colorize;
use serde::{Deserialize, Serialize};
use crate::structs::release::Author;
/// Represents the package as described by JSON
///
/// https://github.com/go-gitea/gitea/blob/main/modules/structs/package.go
#[derive(Debug, Deserialize, Serialize)]
pub struct Package {
created_at: String, // TODO: Datetime struct
creator: Author,
html_url: String,
id: u64,
name: String,
owner: Author,
#[serde(skip)]
repository: (), // TODO: Create a `struct Repository`
#[serde(rename = "type")] // field is "type" in JSON & Go, but that's a
pkg_type: PackageType, // keyword in Rust so we need to serde-rename it.
version: String,
}
impl Package {
pub fn colorized(&self) -> String {
let name = "Name:".green().bold();
let version = &self.version;
let pkg_type = "Type:".green();
format!(
"{name} {} {version}
{pkg_type} {}",
self.name,
self.version,
)
}
}
/// A marker for the kind of package being handled.
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PackageType {
Alpine,
Arch,
Cargo,
Chef,
Composer,
Conan,
Conda,
Container,
Cran,
Debian,
Generic,
Go,
Helm,
Maven,
Npm,
Nuget,
Pub,
PyPi,
Rpm,
RubyGems,
Swift,
Vagrant,
}
impl ToString for PackageType {
fn to_string(&self) -> String {
match self {
PackageType::Alpine => "alpine".into(),
PackageType::Arch => "arch".into(),
PackageType::Cargo => "cargo".into(),
PackageType::Chef => "chef".into(),
PackageType::Composer => "composer".into(),
PackageType::Conan => "conan".into(),
PackageType::Conda => "conda".into(),
PackageType::Container => "container".into(),
PackageType::Cran => "cran".into(),
PackageType::Debian => "debian".into(),
PackageType::Generic => "generic".into(),
PackageType::Go => "go".into(),
PackageType::Helm => "helm".into(),
PackageType::Maven => "maven".into(),
PackageType::Npm => "npm".into(),
PackageType::Nuget => "nuget".into(),
PackageType::Pub => "pub".into(),
PackageType::PyPi => "pypi".into(),
PackageType::Rpm => "rpm".into(),
PackageType::RubyGems => "rubygems".into(),
PackageType::Swift => "swift".into(),
PackageType::Vagrant => "vagrant".into(),
}
}
}

View File

@@ -50,6 +50,7 @@ impl Release {
}
}
// TODO: Rename to "User" to match Gitea's code
#[derive(Debug, Deserialize, Serialize)]
pub struct Author {
id: usize,