agent_first_http/cli/cmd/
upload.rs1use std::path::PathBuf;
5
6use clap::Args as ClapArgs;
7use serde::Serialize;
8
9use crate::cli::output;
10use crate::sdk::Client;
11use crate::shared::error::{Error, ErrorCode};
12use crate::shared::ids::TabId;
13
14#[derive(ClapArgs, Debug)]
15pub struct Args {
16 #[arg(long = "endpoint-url", env = "AFHTTP_ENDPOINT_URL")]
18 pub endpoint: String,
19 #[arg(long = "token-secret", env = "AFHTTP_TOKEN_SECRET")]
22 pub token: Option<String>,
23 #[arg(long)]
25 pub tab: String,
26 #[arg(long)]
28 pub selector: String,
29 #[arg(long)]
31 pub file: PathBuf,
32}
33
34#[derive(Serialize)]
35struct UploadResult {
36 tab_id: String,
37 selector: String,
38 path: String,
39 size_bytes: u64,
40}
41
42pub async fn run(args: Args) -> Result<(), Error> {
43 let file_meta = tokio::fs::metadata(&args.file).await.map_err(|e| {
44 Error::new(
45 ErrorCode::IoError,
46 format!("upload: stat {}: {e}", args.file.display()),
47 )
48 })?;
49 let bytes = file_meta.len();
50
51 let abs_path = tokio::fs::canonicalize(&args.file).await.map_err(|e| {
52 Error::new(
53 ErrorCode::IoError,
54 format!("upload: canonicalize {}: {e}", args.file.display()),
55 )
56 })?;
57 let path_str = abs_path.to_string_lossy().to_string();
58
59 let mut client = Client::connect(&args.endpoint)?;
60 if let Some(t) = args.token.as_deref() {
61 client = client.with_token(t);
62 }
63
64 let conn = client.cdp_connection().await?;
65 let tab = TabId::new(args.tab.clone());
66 let session_id = crate::sdk::cdp::session::attach_to_target(&conn, tab.as_str()).await?;
67
68 conn.send("DOM.enable", &serde_json::json!({}), Some(&session_id))
69 .await?;
70 let document = conn
71 .send(
72 "DOM.getDocument",
73 &serde_json::json!({"depth": 1, "pierce": true}),
74 Some(&session_id),
75 )
76 .await?;
77 let root_id = document["root"]["nodeId"].as_i64().ok_or_else(|| {
78 Error::new(
79 ErrorCode::CdpError,
80 "upload: DOM.getDocument missing root nodeId",
81 )
82 })?;
83 let query = conn
84 .send(
85 "DOM.querySelector",
86 &serde_json::json!({"nodeId": root_id, "selector": args.selector}),
87 Some(&session_id),
88 )
89 .await?;
90 let node_id = query["nodeId"]
91 .as_i64()
92 .filter(|id| *id > 0)
93 .ok_or_else(|| {
94 Error::new(
95 ErrorCode::InvalidArgument,
96 format!(
97 "upload: selector {:?} did not match an element",
98 args.selector
99 ),
100 )
101 })?;
102 ensure_file_input(&conn, &session_id, node_id, &args.selector).await?;
103 conn.send(
104 "DOM.setFileInputFiles",
105 &serde_json::json!({
106 "files": [path_str],
107 "nodeId": node_id,
108 }),
109 Some(&session_id),
110 )
111 .await?;
112
113 let _ = crate::sdk::cdp::session::detach_from_target(&conn, &session_id).await;
114
115 output::emit(
116 "upload",
117 &UploadResult {
118 tab_id: args.tab,
119 selector: args.selector,
120 path: path_str,
121 size_bytes: bytes,
122 },
123 )
124}
125
126async fn ensure_file_input(
127 conn: &crate::sdk::cdp::ws_client::Connection,
128 session_id: &str,
129 node_id: i64,
130 selector: &str,
131) -> Result<(), Error> {
132 let described = conn
133 .send(
134 "DOM.describeNode",
135 &serde_json::json!({"nodeId": node_id}),
136 Some(session_id),
137 )
138 .await?;
139 let node = &described["node"];
140 let is_input = node["nodeName"]
141 .as_str()
142 .is_some_and(|name| name.eq_ignore_ascii_case("input"));
143 let attrs = node["attributes"].as_array().cloned().unwrap_or_default();
144 let mut is_file = false;
145 for pair in attrs.chunks(2) {
146 if pair.first().and_then(|v| v.as_str()) == Some("type")
147 && pair
148 .get(1)
149 .and_then(|v| v.as_str())
150 .is_some_and(|value| value.eq_ignore_ascii_case("file"))
151 {
152 is_file = true;
153 }
154 }
155 if is_input && is_file {
156 Ok(())
157 } else {
158 Err(Error::new(
159 ErrorCode::InvalidArgument,
160 format!("upload: selector {selector:?} must resolve to <input type=file>"),
161 ))
162 }
163}