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