1use tokio::fs::File;
4use tokio::io::AsyncWriteExt as _;
5
6use crate::auth::AuthContext;
7use crate::cli::{FilesCmd, Runtime};
8use crate::error::{CliError, Result};
9use crate::graph::drives::{Drive, canonical_json};
10use crate::graph::sites::Site;
11use crate::graph::{GraphClient, drives, search};
12use crate::output::terminal_width;
13use crate::reference::{ParsedRef, parse};
14
15struct Resolved {
17 site: Site,
18 drive: Drive,
19 parsed: ParsedRef,
20}
21
22async fn resolve(rt: &Runtime, reference: &str) -> Result<(GraphClient, Resolved)> {
24 let default_site_set = rt.cfg.default_site.is_some();
25 let parsed = parse(reference, default_site_set)?;
26
27 let auth = AuthContext::new(rt.cfg.clone(), rt.cache_path.clone());
28 let graph = GraphClient::new(auth);
29
30 let site = crate::graph::sites::resolve(
31 &graph,
32 &parsed.site,
33 &rt.cfg.site_aliases,
34 rt.cfg.default_site.as_deref(),
35 )
36 .await?;
37
38 let library = parsed.library.as_deref().ok_or_else(|| {
39 CliError::Input(format!(
40 "reference '{reference}' has no library; use 'Site:Library/path'"
41 ))
42 })?;
43
44 let drive = drives::find_drive_by_name(&graph, &site.id, library).await?;
45
46 Ok((
47 graph,
48 Resolved {
49 site,
50 drive,
51 parsed,
52 },
53 ))
54}
55
56pub async fn run(rt: &Runtime, cmd: FilesCmd) -> Result<()> {
57 match cmd {
58 FilesCmd::Ls {
59 reference,
60 recursive,
61 limit,
62 all,
63 page,
64 } => ls(rt, &reference, recursive, limit, all, page.as_deref()).await,
65
66 FilesCmd::Stat { reference } => stat(rt, &reference).await,
67
68 FilesCmd::Download {
69 reference,
70 output,
71 overwrite,
72 } => download(rt, &reference, output.as_deref(), overwrite).await,
73
74 FilesCmd::Find {
75 reference,
76 query,
77 name,
78 limit,
79 all,
80 page,
81 } => {
82 find(
83 rt,
84 &reference,
85 query.as_deref(),
86 name.as_deref(),
87 limit,
88 all,
89 page.as_deref(),
90 )
91 .await
92 }
93 }
94}
95
96async fn ls(
97 rt: &Runtime,
98 reference: &str,
99 recursive: bool,
100 limit: usize,
101 all: bool,
102 page: Option<&str>,
103) -> Result<()> {
104 let (graph, r) = resolve(rt, reference).await?;
105
106 if recursive {
107 let items = drives::list_children_recursive(&graph, &r.drive.id, &r.parsed.path).await?;
108
109 if rt.out.json {
110 let json_items: Vec<_> = items
111 .iter()
112 .map(|it| canonical_json(it, &r.site, &r.drive, false))
113 .collect();
114 rt.out.print_json(&serde_json::json!({
115 "total": json_items.len(),
116 "next": null,
117 "items": json_items,
118 }));
119 } else {
120 let name_w = terminal_width().saturating_sub(40).max(20);
121 rt.out.print_data(&format!(
122 "{:<name_w$} {:<6} {:>10} {:<16}",
123 "NAME", "KIND", "SIZE", "MODIFIED"
124 ));
125 for it in &items {
126 let kind = if it.folder.is_some() {
127 "folder"
128 } else {
129 "file"
130 };
131 let modified = it.modified.as_deref().unwrap_or("");
132 let modified_short = modified
133 .replace('T', " ")
134 .chars()
135 .take(16)
136 .collect::<String>();
137 rt.out.print_data(&format!(
138 "{:<name_w$} {:<6} {:>10} {:<16}",
139 it.name, kind, it.size, modified_short
140 ));
141 }
142 rt.out.print_message(&format!("({} item(s))", items.len()));
143 }
144 return Ok(());
145 }
146
147 let mut items: Vec<_> = Vec::new();
149 let mut next = page.map(str::to_owned);
150 loop {
151 let pageres =
152 drives::list_children(&graph, &r.drive.id, &r.parsed.path, next.as_deref()).await?;
153 for it in pageres.items {
154 items.push(it);
155 if !all && items.len() >= limit {
156 break;
157 }
158 }
159 if !all || pageres.next.is_none() {
160 next = if all { None } else { pageres.next };
161 break;
162 }
163 next = pageres.next;
164 }
165 let next_token = next;
166
167 if rt.out.json {
168 let json_items: Vec<_> = items
169 .iter()
170 .map(|it| canonical_json(it, &r.site, &r.drive, false))
171 .collect();
172 rt.out.print_json(&serde_json::json!({
173 "total": json_items.len(),
174 "next": next_token,
175 "items": json_items,
176 }));
177 } else {
178 let name_w = terminal_width().saturating_sub(40).max(20);
179 rt.out.print_data(&format!(
180 "{:<name_w$} {:<6} {:>10} {:<16}",
181 "NAME", "KIND", "SIZE", "MODIFIED"
182 ));
183 for it in &items {
184 let kind = if it.folder.is_some() {
185 "folder"
186 } else {
187 "file"
188 };
189 let modified = it.modified.as_deref().unwrap_or("");
190 let modified_short = modified
191 .replace('T', " ")
192 .chars()
193 .take(16)
194 .collect::<String>();
195 rt.out.print_data(&format!(
196 "{:<name_w$} {:<6} {:>10} {:<16}",
197 it.name, kind, it.size, modified_short
198 ));
199 }
200 rt.out.print_message(&format!("({} item(s))", items.len()));
201 }
202 Ok(())
203}
204
205async fn stat(rt: &Runtime, reference: &str) -> Result<()> {
206 let (graph, r) = resolve(rt, reference).await?;
207 let item = drives::get_item_with_download_url(&graph, &r.drive.id, &r.parsed.path).await?;
208 let v = canonical_json(&item, &r.site, &r.drive, true);
209 rt.out.print_json(&v);
212 Ok(())
213}
214
215async fn download(
216 rt: &Runtime,
217 reference: &str,
218 output: Option<&str>,
219 overwrite: bool,
220) -> Result<()> {
221 let (graph, r) = resolve(rt, reference).await?;
222
223 let derived;
225 let target: &str = match output {
226 Some(o) => o,
227 None => {
228 derived = r
229 .parsed
230 .path
231 .rsplit('/')
232 .find(|s| !s.is_empty())
233 .unwrap_or("download")
234 .to_string();
235 &derived
236 }
237 };
238
239 if target == "-" {
240 let mut stdout = tokio::io::stdout();
241 let bytes = crate::graph::download::download_to_writer(
242 &graph,
243 &r.drive.id,
244 &r.parsed.path,
245 &mut stdout,
246 )
247 .await?;
248 stdout.flush().await.ok();
249 rt.out
250 .print_message(&format!("{bytes} bytes written to stdout"));
251 return Ok(());
252 }
253
254 let path = std::path::Path::new(target);
255 if path.exists() && !overwrite {
256 return Err(CliError::Input(format!(
257 "'{target}' already exists; use --overwrite to replace it"
258 )));
259 }
260
261 if let Some(parent) = path.parent()
262 && !parent.as_os_str().is_empty()
263 {
264 tokio::fs::create_dir_all(parent).await.map_err(|e| {
265 CliError::Other(format!("create directory '{}': {e}", parent.display()))
266 })?;
267 }
268
269 let mut file = File::create(path)
270 .await
271 .map_err(|e| CliError::Other(format!("create '{target}': {e}")))?;
272
273 let n =
274 crate::graph::download::download_to_writer(&graph, &r.drive.id, &r.parsed.path, &mut file)
275 .await?;
276 rt.out
277 .print_message(&format!("Downloaded {n} byte(s) to {}", path.display()));
278 if rt.out.json {
279 rt.out.print_json(&serde_json::json!({
280 "path": path.display().to_string(),
281 "bytes": n,
282 }));
283 }
284 Ok(())
285}
286
287async fn find(
288 rt: &Runtime,
289 reference: &str,
290 query: Option<&str>,
291 name_glob: Option<&str>,
292 limit: usize,
293 all: bool,
294 page: Option<&str>,
295) -> Result<()> {
296 let (graph, r) = resolve(rt, reference).await?;
297 let q = query.unwrap_or("*");
299
300 let mut items = Vec::new();
301 let mut next: Option<String> = page.map(String::from);
302 loop {
303 let res = search::search(&graph, &r.drive.id, q, next.as_deref()).await?;
304 for it in res.items {
305 if let Some(g) = name_glob
306 && !search::glob_matches(g, &it.name)
307 {
308 continue;
309 }
310 items.push(it);
311 if !all && items.len() >= limit {
312 break;
313 }
314 }
315 if !all || res.next.is_none() {
316 next = if all { None } else { res.next };
317 break;
318 }
319 next = res.next;
320 }
321
322 let total = items.len();
323 if rt.out.json {
324 let json_items: Vec<_> = items
325 .iter()
326 .map(|it| canonical_json(it, &r.site, &r.drive, false))
327 .collect();
328 rt.out.print_json(&serde_json::json!({
329 "total": total,
330 "next": next,
331 "items": json_items,
332 }));
333 } else {
334 for it in &items {
335 rt.out.print_data(&it.name);
336 }
337 rt.out.print_message(&format!("({total} match(es))"));
338 }
339 Ok(())
340}