1use super::gitlab::GitLabPackageClient;
16use crate::error::{Result, ToriiError};
17use chrono::{DateTime, Duration, Utc};
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Package {
22 pub id: String,
23 pub name: String,
24 pub version: String,
25 pub package_type: String,
27 pub status: String,
28 pub created_at: String,
29 pub web_url: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PackageFile {
34 pub id: String,
35 pub package_id: String,
36 pub file_name: String,
37 pub size_bytes: u64,
38 pub created_at: String,
39}
40
41#[derive(Debug, Clone, Default)]
42pub struct PackageListFilters {
43 pub package_type: Option<String>,
45 pub name_search: Option<String>,
47 pub per_page: usize,
49}
50
51#[allow(dead_code)]
52pub trait PackageClient: Send {
53 fn list(&self, owner: &str, repo: &str, filters: &PackageListFilters) -> Result<Vec<Package>>;
54 fn delete(&self, owner: &str, repo: &str, id: &str) -> Result<()>;
55 fn list_files(&self, owner: &str, repo: &str, id: &str) -> Result<Vec<PackageFile>>;
56}
57
58pub fn get_package_client(platform: &str) -> Result<Box<dyn PackageClient>> {
63 match platform.to_lowercase().as_str() {
64 "gitlab" => Ok(Box::new(GitLabPackageClient::new()?)),
65 "github" => Err(ToriiError::Unsupported("GitHub doesn't have a Generic Package Registry equivalent to GitLab's. \
66 Binary release assets on GitHub are managed through Releases: use `torii release` instead.".to_string())),
67 "gitea" => Err(ToriiError::Unsupported("Gitea/Codeberg has a Package Registry but its API isn't wired into torii yet. \
68 For binary assets, use Releases (see `torii release`).".to_string())),
69 "sourcehut" => Err(ToriiError::Unsupported("Sourcehut has no Package Registry concept. Binaries are distributed via the \
70 project's own homepage or builds.sr.ht's `triggers` (uploaded externally).".to_string())),
71 "radicle" => Err(ToriiError::Unsupported("Radicle is peer-to-peer and has no central package registry. \
72 Distribute binaries via the project's own channel or mirror to a registry host.".to_string())),
73 "bitbucket" => Err(ToriiError::Unsupported("Bitbucket Cloud has no Package Registry. Binary distribution happens via the \
74 Downloads tab (flat file list) or external hosting.".to_string())),
75 "azure" => Err(ToriiError::Unsupported("Azure Artifacts exists but lives at the organisation level (feeds), not per-repo. \
76 The mapping isn't 1:1 with torii's owner/repo abstraction. Wired in a future release \
77 once the org-feed-package addressing is designed; for now use the Azure DevOps UI \
78 (https://dev.azure.com/{org}/_packaging).".to_string())),
79 other => Err(ToriiError::Unsupported(format!("Unsupported platform: {}. Supported for `torii package`: gitlab", other))),
80 }
81}
82
83pub fn filter_older_than(packages: Vec<Package>, days: i64) -> Vec<Package> {
89 let cutoff = Utc::now() - Duration::days(days);
90 packages
91 .into_iter()
92 .filter(|p| match DateTime::parse_from_rfc3339(&p.created_at) {
93 Ok(dt) => dt.with_timezone(&Utc) < cutoff,
94 Err(_) => true,
95 })
96 .collect()
97}
98
99pub fn filter_by_version(packages: Vec<Package>, version: &str) -> Vec<Package> {
101 packages
102 .into_iter()
103 .filter(|p| p.version == version)
104 .collect()
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::platforms::gitlab::package::{parse_gitlab_package, parse_gitlab_package_file};
111
112 #[test]
113 fn parse_gitlab_package_basic() {
114 let json = serde_json::json!({
115 "id": 12345u64,
116 "name": "gitorii",
117 "version": "v0.7.9",
118 "package_type": "generic",
119 "status": "default",
120 "created_at": "2026-05-19T22:00:00Z",
121 "_links": { "web_path": "/paskidev/gitorii/-/packages/12345" }
122 });
123 let p = parse_gitlab_package(&json).unwrap();
124 assert_eq!(p.id, "12345");
125 assert_eq!(p.name, "gitorii");
126 assert_eq!(p.version, "v0.7.9");
127 assert_eq!(p.package_type, "generic");
128 }
129
130 #[test]
131 fn parse_gitlab_package_file_basic() {
132 let json = serde_json::json!({
133 "id": 99u64,
134 "file_name": "torii-linux-x86_64",
135 "size": 20221192u64,
136 "created_at": "2026-05-19T22:00:00Z"
137 });
138 let pf = parse_gitlab_package_file(&json, "12345").unwrap();
139 assert_eq!(pf.id, "99");
140 assert_eq!(pf.package_id, "12345");
141 assert_eq!(pf.file_name, "torii-linux-x86_64");
142 assert_eq!(pf.size_bytes, 20221192);
143 }
144
145 fn mk(v: &str, created: &str) -> Package {
146 Package {
147 id: "1".into(),
148 name: "gitorii".into(),
149 version: v.into(),
150 package_type: "generic".into(),
151 status: "default".into(),
152 created_at: created.into(),
153 web_url: String::new(),
154 }
155 }
156
157 #[test]
158 fn filter_older_than_keeps_old_drops_recent_keeps_unparseable() {
159 let now = Utc::now();
160 let recent = (now - Duration::days(2)).to_rfc3339();
161 let ancient = (now - Duration::days(100)).to_rfc3339();
162 let kept = filter_older_than(
163 vec![
164 mk("v0.7.0", &recent),
165 mk("v0.1.0", &ancient),
166 mk("v?.?.?", "not a date"),
167 ],
168 30,
169 );
170 assert_eq!(kept.len(), 2);
176 assert!(kept.iter().any(|p| p.version == "v0.1.0"));
177 assert!(kept.iter().any(|p| p.version == "v?.?.?"));
178 }
179}