eoka_protocol/
download.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
4pub struct DownloadStartArgs {
5 #[serde(default)]
6 pub operation_id: Option<String>,
7 pub click: Option<String>,
8 pub url: Option<String>,
9 #[schemars(
10 description = "Absolute output filename. Must not exist; its parent directory must exist."
11 )]
12 pub output: String,
13 pub expect_bytes: Option<u64>,
14 #[serde(default = "default_download_timeout")]
15 #[schemars(range(min = 1, max = 300000))]
16 pub start_timeout_ms: u64,
17}
18
19impl DownloadStartArgs {
20 pub fn validate(&self) -> Result<(), String> {
21 if self.click.is_some() == self.url.is_some() {
22 return Err("Exactly one of click or url is required".into());
23 }
24 if self
25 .click
26 .as_ref()
27 .is_some_and(|value| value.trim().is_empty())
28 {
29 return Err("Click target must not be empty".into());
30 }
31 if !std::path::Path::new(&self.output).is_absolute() {
32 return Err("Download output must be an absolute filename".into());
33 }
34 if self.start_timeout_ms == 0 || self.start_timeout_ms > 300_000 {
35 return Err("start_timeout_ms must be between 1 and 300000".into());
36 }
37 Ok(())
38 }
39}
40
41pub fn default_download_timeout() -> u64 {
42 30_000
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
46pub struct DownloadIdArgs {
47 pub id: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
51pub struct DownloadWaitArgs {
52 pub id: String,
53 #[serde(default = "default_download_timeout")]
54 #[schemars(range(min = 1, max = 86400000))]
55 pub timeout_ms: u64,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
59#[serde(rename_all = "snake_case")]
60pub enum DownloadState {
61 Pending,
62 Downloading,
63 Verifying,
64 Completed,
65 Canceled,
66 Failed,
67}
68
69impl DownloadState {
70 pub fn is_terminal(self) -> bool {
71 matches!(self, Self::Completed | Self::Canceled | Self::Failed)
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
76pub struct DownloadRecord {
77 #[serde(default)]
78 pub native_guid: Option<String>,
79 #[serde(default)]
80 pub native_complete: bool,
81 #[serde(default)]
82 pub sha256: Option<String>,
83 pub id: String,
84 pub tab_id: String,
85 pub state: DownloadState,
86 pub output: String,
87 pub published_path: Option<String>,
88 pub staging_path: Option<String>,
89 pub received_bytes: u64,
90 pub total_bytes: Option<u64>,
91 pub suggested_filename: Option<String>,
92 pub source_url: Option<String>,
93 pub created_at_ms: u64,
94 pub updated_at_ms: u64,
95 pub error: Option<crate::ErrorDetail>,
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn download_inputs_reject_ambiguous_triggers_and_relative_paths() {
104 let mut args = DownloadStartArgs {
105 operation_id: None,
106 click: Some("@e1".into()),
107 url: None,
108 output: "/tmp/file".into(),
109 expect_bytes: None,
110 start_timeout_ms: 30_000,
111 };
112 assert!(args.validate().is_ok());
113 args.url = Some("https://example.com/file".into());
114 assert!(args.validate().is_err());
115 args.url = None;
116 args.output = "relative".into();
117 assert!(args.validate().is_err());
118 }
119}