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