plugin_packager/
publish.rs1use anyhow::{anyhow, Context, Result};
14use serde::{Deserialize, Serialize};
15use std::path::Path;
16
17use crate::platform::ArtifactMetadata;
18
19#[derive(Clone)]
21pub struct ArtifactPublisher {
22 config: PublishConfig,
23}
24
25#[derive(Debug, Clone)]
27pub struct PublishConfig {
28 pub registry_url: String,
30 pub auth_token: String,
32 pub skip_verify: bool,
34 pub as_draft: bool,
36 pub sign: bool,
38 pub key_id: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ArtifactPublishResult {
45 pub plugin_id: String,
47 pub version: String,
49 pub download_url: String,
51 pub checksum: String,
53 pub status: String,
55 pub message: String,
57 pub registry_url: Option<String>,
59}
60
61#[derive(Debug, Clone)]
63pub struct LocalArtifact {
64 pub path: std::path::PathBuf,
66 pub metadata: ArtifactMetadata,
68 pub checksum: String,
70}
71
72impl ArtifactPublisher {
73 pub fn new(config: PublishConfig) -> Self {
75 Self { config }
76 }
77
78 pub fn validate(&self, artifact_path: &Path) -> Result<LocalArtifact> {
82 if !artifact_path.exists() {
83 return Err(anyhow!("Artifact not found: {}", artifact_path.display()));
84 }
85
86 let filename = artifact_path
87 .file_name()
88 .and_then(|n| n.to_str())
89 .context("Invalid artifact filename")?;
90
91 let metadata = ArtifactMetadata::parse(filename)?;
92
93 crate::verify_artifact(artifact_path, None)?;
94
95 let checksum = compute_artifact_checksum(artifact_path)?;
96
97 Ok(LocalArtifact {
98 path: artifact_path.to_path_buf(),
99 metadata,
100 checksum,
101 })
102 }
103
104 pub fn is_authenticated(&self) -> bool {
106 !self.config.auth_token.is_empty()
107 }
108
109 pub fn registry_url(&self) -> &str {
111 &self.config.registry_url
112 }
113}
114
115fn compute_artifact_checksum(path: &Path) -> Result<String> {
117 use sha2::{Digest, Sha256};
118 use std::fs::File;
119 use std::io::Read;
120
121 let mut file = File::open(path)?;
122 let mut hasher = Sha256::new();
123 let mut buf = [0u8; 8192];
124
125 loop {
126 let n = file.read(&mut buf)?;
127 if n == 0 {
128 break;
129 }
130 hasher.update(&buf[..n]);
131 }
132
133 Ok(hex::encode(hasher.finalize()))
134}
135
136#[allow(dead_code)]
138fn extract_plugin_metadata(artifact_path: &Path) -> Result<PluginMetadataExtract> {
139 use flate2::read::GzDecoder;
140 use std::fs::File;
141 use std::io::Read;
142
143 let file = File::open(artifact_path)?;
144 let decoder = GzDecoder::new(file);
145 let mut archive = tar::Archive::new(decoder);
146
147 for entry in archive.entries()? {
149 let mut entry = entry?;
150 let path = entry.path()?;
151
152 if path.file_name().and_then(|n| n.to_str()) == Some("plugin.toml") {
153 let mut content = String::new();
154 entry.read_to_string(&mut content)?;
155
156 return parse_plugin_toml(&content);
157 }
158 }
159
160 Err(anyhow!("plugin.toml not found in artifact"))
161}
162
163#[derive(Debug, Clone, Default)]
165#[allow(dead_code)]
166struct PluginMetadataExtract {
167 abi_version: String,
168 description: Option<String>,
169 author: Option<String>,
170 license: Option<String>,
171 repository: Option<String>,
172 keywords: Option<Vec<String>>,
173 categories: Option<Vec<String>>,
174 homepage: Option<String>,
175 documentation: Option<String>,
176 plugin_type: Option<String>,
177 maturity: Option<String>,
178 provides_services: Option<Vec<String>>,
179 requires_services: Option<Vec<String>>,
180}
181
182#[allow(dead_code)]
184fn parse_plugin_toml(content: &str) -> Result<PluginMetadataExtract> {
185 let value: toml::Value = toml::from_str(content)?;
186
187 let get_str = |key: &str| -> Option<String> {
188 value
189 .get("package")
190 .and_then(|p| p.get(key))
191 .or_else(|| value.get(key))
192 .and_then(|v| v.as_str())
193 .map(|s| s.to_string())
194 };
195
196 let get_vec = |key: &str| -> Option<Vec<String>> {
197 value
198 .get("package")
199 .and_then(|p| p.get(key))
200 .or_else(|| value.get(key))
201 .and_then(|v| v.as_array())
202 .map(|arr| {
203 arr.iter()
204 .filter_map(|v| v.as_str().map(|s| s.to_string()))
205 .collect()
206 })
207 };
208
209 Ok(PluginMetadataExtract {
210 abi_version: get_str("abi_version").unwrap_or_else(|| "1".to_string()),
211 description: get_str("description"),
212 author: get_str("author"),
213 license: get_str("license"),
214 repository: get_str("repository"),
215 keywords: get_vec("keywords"),
216 categories: get_vec("categories"),
217 homepage: get_str("homepage"),
218 documentation: get_str("documentation"),
219 plugin_type: get_str("plugin_type"),
220 maturity: get_str("maturity"),
221 provides_services: get_vec("provides_services"),
222 requires_services: get_vec("requires_services"),
223 })
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_publish_config() {
232 let config = PublishConfig {
233 registry_url: "https://registry.example.com".to_string(),
234 auth_token: "secret-token".to_string(),
235 skip_verify: false,
236 as_draft: false,
237 sign: false,
238 key_id: None,
239 };
240
241 assert_eq!(config.registry_url, "https://registry.example.com");
242 assert!(!config.skip_verify);
243 }
244
245 #[test]
246 fn test_artifact_publisher_creation() {
247 let config = PublishConfig {
248 registry_url: "https://registry.example.com".to_string(),
249 auth_token: "secret-token".to_string(),
250 skip_verify: false,
251 as_draft: false,
252 sign: false,
253 key_id: None,
254 };
255
256 let publisher = ArtifactPublisher::new(config);
257 assert!(publisher.is_authenticated());
258 }
259
260 #[test]
261 fn test_parse_plugin_toml() {
262 let content = r#"
263[package]
264name = "test-plugin"
265version = "1.0.0"
266abi_version = "2"
267description = "A test plugin"
268author = "Test Author"
269license = "MIT"
270keywords = ["test", "plugin"]
271"#;
272
273 let metadata = parse_plugin_toml(content).unwrap();
274 assert_eq!(metadata.abi_version, "2");
275 assert_eq!(metadata.description, Some("A test plugin".to_string()));
276 assert_eq!(metadata.author, Some("Test Author".to_string()));
277 assert_eq!(metadata.license, Some("MIT".to_string()));
278 assert_eq!(
279 metadata.keywords,
280 Some(vec!["test".to_string(), "plugin".to_string()])
281 );
282 }
283
284 #[test]
285 fn test_parse_plugin_toml_flat() {
286 let content = r#"
287name = "flat-plugin"
288version = "0.1.0"
289abi_version = "1"
290description = "Flat format plugin"
291"#;
292
293 let metadata = parse_plugin_toml(content).unwrap();
294 assert_eq!(metadata.abi_version, "1");
295 assert_eq!(metadata.description, Some("Flat format plugin".to_string()));
296 }
297}