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