sharepoint_cli/commands/
sites.rs1use crate::auth::AuthContext;
4use crate::cli::{Runtime, SitesCmd};
5use crate::config;
6use crate::error::{CliError, Result};
7use crate::graph::{Cursor, GraphClient, decode_cursor, encode_cursor, sites};
8
9pub async fn run(rt: &Runtime, cmd: SitesCmd) -> Result<()> {
10 match cmd {
11 SitesCmd::List {
12 query,
13 limit,
14 all,
15 page,
16 fields,
17 } => list(rt, query.as_deref(), limit, all, page.as_deref(), &fields).await,
18 SitesCmd::Use { site } => use_site(rt, &site).await,
19 }
20}
21
22async fn list(
23 rt: &Runtime,
24 query: Option<&str>,
25 limit: usize,
26 all: bool,
27 page: Option<&str>,
28 fields: &[String],
29) -> Result<()> {
30 let auth = AuthContext::new(rt.cfg.clone(), rt.cache_path.clone());
31 let graph = GraphClient::new(auth);
32
33 let (mut current_url, mut skip) = if let Some(token) = page {
35 let endpoint = graph.graph_endpoint().await;
36 let cursor = decode_cursor(&endpoint, token)?;
37 (cursor.next, cursor.skip)
38 } else {
39 (None, 0)
40 };
41
42 let mut items = Vec::new();
43 let mut source_label: &str;
44
45 let out_cursor: Option<Cursor> = 'outer: loop {
46 let res = sites::list(&graph, query, current_url.as_deref()).await?;
47 source_label = match res.source {
48 sites::SiteListSource::Followed => "followed",
49 sites::SiteListSource::Search => "search",
50 };
51
52 for (idx, s) in res.items.iter().enumerate() {
53 if idx < skip {
54 continue;
55 }
56 items.push(s.clone());
57 if !all && items.len() >= limit {
58 let consumed_in_page = idx + 1;
60 break 'outer Some(Cursor {
61 next: Some(res.fetched_url),
62 skip: consumed_in_page,
63 });
64 }
65 }
66 skip = 0;
67
68 if all {
69 if res.next_url.is_none() {
70 break None;
72 }
73 current_url = res.next_url;
74 } else {
75 break res.next_url.map(|url| Cursor {
77 next: Some(url),
78 skip: 0,
79 });
80 }
81 };
82
83 let next_token = out_cursor.as_ref().map(encode_cursor);
84 let total = items.len();
85 if rt.out.json {
86 let json_items: Vec<_> = items
87 .iter()
88 .map(|s| {
89 filter_fields(
90 serde_json::json!({
91 "id": s.id,
92 "name": s.display_name,
93 "url": s.web_url,
94 }),
95 fields,
96 )
97 })
98 .collect();
99 rt.out.print_json(&serde_json::json!({
100 "total": total,
101 "next": next_token,
102 "source": source_label,
103 "items": json_items,
104 }));
105 } else {
106 for s in &items {
107 rt.out
108 .print_data(&format!("{:40} {}", s.display_name, s.web_url));
109 }
110 rt.out
111 .print_message(&format!("({total} site(s), source={source_label})"));
112 }
113 Ok(())
114}
115
116async fn use_site(rt: &Runtime, value: &str) -> Result<()> {
117 if rt.cfg.read_only {
118 return Err(CliError::ReadOnly(
119 "sites use modifies the config file; not allowed in read-only mode".into(),
120 ));
121 }
122 let mut file = rt.config_file.clone();
123 let entry = file.profile.entry(rt.cfg.profile_name.clone()).or_default();
124 entry.default_site = Some(value.to_string());
125 config::save_file(&rt.config_path, &file)?;
126 rt.out.print_message(&format!(
127 "Set default_site for profile '{}' to '{}'",
128 rt.cfg.profile_name, value
129 ));
130 if rt.out.json {
131 rt.out.print_json(&serde_json::json!({
132 "profile": rt.cfg.profile_name,
133 "default_site": value,
134 }));
135 }
136 Ok(())
137}
138
139fn filter_fields(mut value: serde_json::Value, fields: &[String]) -> serde_json::Value {
141 if fields.is_empty() {
142 return value;
143 }
144 if let serde_json::Value::Object(ref mut map) = value {
145 map.retain(|k, _| fields.iter().any(|f| f == k));
146 }
147 value
148}