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