artifactize_core/remote/
session.rs1use std::{
2 collections::{BTreeSet, HashMap},
3 io::Write,
4 path::Path,
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicBool, Ordering},
8 },
9 time::{Duration, Instant},
10};
11
12use tokio::sync::OnceCell;
13
14use super::Record;
15use crate::{
16 auth::remote::{self, Failure, Principal, Remote, Share},
17 store::{Execution, Receipts, Request},
18};
19
20pub(crate) const REFRESH: Duration = Duration::from_secs(1);
23
24pub struct Session {
29 remote: Remote,
30 principal: OnceCell<Option<Principal>>,
31 offline: AtomicBool,
32 looked_up: Mutex<HashMap<crate::types::ReuseKey, Instant>>,
33}
34
35fn warn(message: &str) {
36 let _ = writeln!(std::io::stderr().lock(), "{message}");
37}
38
39impl Session {
40 pub fn open(state: Option<&Path>, repo: Option<&Path>) -> Result<Option<Arc<Self>>, String> {
42 Ok(remote::remote(state, repo)?.map(|remote| {
43 Arc::new(Self {
44 remote,
45 principal: OnceCell::new(),
46 offline: AtomicBool::new(false),
47 looked_up: Mutex::default(),
48 })
49 }))
50 }
51
52 fn failed<T: Default>(&self, failure: Failure) -> Result<T, String> {
53 if !failure.unavailable {
54 return Err(failure.message);
55 }
56 if !self.offline.swap(true, Ordering::SeqCst) {
57 warn(&format!(
58 "{}; continuing without the remote review store.",
59 failure.message.trim_end_matches('.')
60 ));
61 }
62 Ok(T::default())
63 }
64
65 async fn principal(&self) -> Result<Option<&Principal>, String> {
67 if self.offline.load(Ordering::SeqCst) {
68 return Ok(None);
69 }
70 let principal = self
71 .principal
72 .get_or_try_init(|| async {
73 match self.remote.whoami().await {
74 Ok(principal) => Ok(Some(principal)),
75 Err(failure) => self.failed(failure),
76 }
77 })
78 .await?;
79 Ok(principal.as_ref())
80 }
81
82 pub async fn lookup(&self, keys: &[crate::types::ReuseKey]) -> Result<Vec<Execution>, String> {
85 if keys.is_empty() {
86 return Ok(Vec::new());
87 }
88 let Some(principal) = self.principal().await? else {
89 return Ok(Vec::new());
90 };
91 if !has_scope(principal, &crate::auth::remote::Scope::Read) {
92 return Ok(Vec::new());
93 }
94 let entries = match self.remote.lookup(keys).await {
95 Ok(entries) => entries,
96 Err(failure) => return self.failed(failure),
97 };
98 let requested: BTreeSet<_> = keys.iter().map(crate::types::ReuseKey::as_str).collect();
99 let mut executions = Vec::new();
100 for entry in entries {
101 let mirrored = entry.and_then(|record| {
103 if !requested.contains(record.key.as_str()) {
104 return Err("the store returned an unrequested key".into());
105 }
106 record.mirror(self.remote.url.as_str())
107 });
108 match mirrored {
109 Ok(execution) => executions.push(execution),
110 Err(error) => warn(&format!(
111 "Ignoring an invalid remote review record: {error}"
112 )),
113 }
114 }
115 Ok(executions)
116 }
117
118 pub async fn refresh(
122 &self,
123 receipts: &Receipts,
124 keys: Vec<crate::types::ReuseKey>,
125 ) -> Result<(), String> {
126 let now = Instant::now();
127 let keys: Vec<_> = {
128 let mut looked_up = self.looked_up.lock().expect("lookup times");
129 keys.into_iter()
130 .filter(|key| {
131 let due = looked_up
132 .get(key)
133 .is_none_or(|at| now.duration_since(*at) >= REFRESH);
134 if due {
135 looked_up.insert(key.clone(), now);
136 }
137 due
138 })
139 .collect()
140 };
141 if keys.is_empty() || self.offline.load(Ordering::SeqCst) {
142 return Ok(());
143 }
144 for execution in self.lookup(&keys).await? {
145 receipts.mirror_execution(&execution).await?;
146 }
147 Ok(())
148 }
149
150 pub async fn publish_request(
153 &self,
154 receipts: &Receipts,
155 request: &Request,
156 ) -> Result<(), String> {
157 let (Some(_), Some(execution_id)) = (&request.key, &request.execution_id) else {
158 return Ok(());
159 };
160 match receipts.published_execution(execution_id).await? {
161 Some(execution) => self.publish(&execution).await,
162 None => Ok(()),
163 }
164 }
165
166 async fn publish(&self, execution: &Execution) -> Result<(), String> {
169 let Some(principal) = self.principal().await? else {
170 return Ok(());
171 };
172 if !has_scope(principal, &crate::auth::remote::Scope::Publish) {
173 return Ok(());
174 }
175 let record = match record(execution, principal, self.remote.share) {
176 Ok(record) => record,
177 Err(reason) => {
178 warn(&format!("This result stays local: {reason}."));
179 return Ok(());
180 }
181 };
182 match self.remote.publish(&record).await {
183 Ok(_) => Ok(()),
184 Err(failure) => self.failed(failure),
185 }
186 }
187}
188
189pub(super) fn has_scope(principal: &Principal, scope: &crate::auth::remote::Scope) -> bool {
190 principal.scopes.contains(scope)
191}
192
193pub(super) fn record(
195 execution: &Execution,
196 principal: &Principal,
197 share: Share,
198) -> Result<Record, String> {
199 if execution.profile.kind() == crate::config::ProfileKind::Human
200 && !has_scope(principal, &crate::auth::remote::Scope::Human)
201 {
202 return Err(format!(
203 "remote token {} lacks the human scope",
204 principal.principal
205 ));
206 }
207 Record::new(execution, share == Share::Full)
208}