···269269270270- `GET /repos`: get a list of repositories and their sync status. supports pagination and filtering:
271271 - `limit`: max results (default 100, max 1000)
272272- - `cursor`: opaque key for paginating.
273273- - `partition`: `all` (default), `pending` (backfill queue), or `resync` (retries)
272272+ - `cursor`: did key for paginating.
274273- `GET /repos/{did}`: get the sync status and metadata of a specific repository.
275274 also returns the handle, PDS URL and the atproto signing key (these won't be
276275 available before the repo has been backfilled once at least).
+18-79
src/api/repos.rs
···11-use crate::control::{Hydrant, RepoInfo, repo_state_to_info};
22-use crate::db::keys;
11+use std::str::FromStr;
22+33+use crate::control::{Hydrant, RepoInfo};
34use axum::{
45 Json, Router,
56 body::Body,
···910 routing::{delete, get, post, put},
1011};
1112use jacquard_common::types::did::Did;
1313+use miette::IntoDiagnostic;
1214use serde::Deserialize;
13151416pub fn router() -> Router<Hydrant> {
···2931pub struct GetReposParams {
3032 pub limit: Option<usize>,
3133 pub cursor: Option<String>,
3232- pub partition: Option<String>,
3334}
34353536pub async fn handle_get_repos(
···3839 headers: HeaderMap,
3940) -> Result<Response, (StatusCode, String)> {
4041 let limit = params.limit.unwrap_or(100).min(1000);
4141- let partition = params.partition.unwrap_or_else(|| "all".to_string());
4242+ let cursor = params
4343+ .cursor
4444+ .map(|c| Did::from_str(&c))
4545+ .transpose()
4646+ .map_err(bad_request)?;
42474348 let items = tokio::task::spawn_blocking(move || {
4444- let db = &hydrant.state.db;
4545-4646- let to_info = |k: &[u8], v: &[u8]| -> Result<RepoInfo, (StatusCode, String)> {
4747- let repo_state = crate::db::deser_repo_state(v).map_err(internal)?;
4848- let did = crate::db::types::TrimmedDid::try_from(k)
4949- .map_err(internal)?
5050- .to_did();
5151-5252- Ok(repo_state_to_info(did, repo_state))
5353- };
5454-5555- let results = match partition.as_str() {
5656- "all" | "resync" => {
5757- let is_all = partition == "all";
5858- let ks = if is_all { &db.repos } else { &db.resync };
5959-6060- let start_bound = if let Some(cursor) = params.cursor {
6161- let did = Did::new_owned(&cursor).map_err(bad_request)?;
6262- let did_key = keys::repo_key(&did);
6363- std::ops::Bound::Excluded(did_key)
6464- } else {
6565- std::ops::Bound::Unbounded
6666- };
6767-6868- let mut items = Vec::new();
6969- for item in ks
7070- .range((start_bound, std::ops::Bound::Unbounded))
7171- .take(limit)
7272- {
7373- let (k, v) = item.into_inner().map_err(internal)?;
7474-7575- let repo_state_bytes = if is_all {
7676- v
7777- } else {
7878- db.repos.get(&k).map_err(internal)?.ok_or_else(|| {
7979- internal(format!("repository state missing for {}", partition))
8080- })?
8181- };
8282-8383- items.push(to_info(&k, &repo_state_bytes)?);
8484- }
8585- Ok::<_, (StatusCode, String)>(items)
8686- }
8787- "pending" => {
8888- let start_bound = if let Some(cursor) = params.cursor {
8989- let id = cursor.parse::<u64>().map_err(bad_request)?;
9090- std::ops::Bound::Excluded(id.to_be_bytes().to_vec())
9191- } else {
9292- std::ops::Bound::Unbounded
9393- };
9494-9595- let mut items = Vec::new();
9696- for item in db
9797- .pending
9898- .range((start_bound, std::ops::Bound::Unbounded))
9999- .take(limit)
100100- {
101101- let (_, did_key) = item.into_inner().map_err(internal)?;
102102-103103- if let Ok(Some(v)) = db.repos.get(&did_key) {
104104- items.push(to_info(&did_key, &v)?);
105105- }
106106- }
107107- Ok(items)
108108- }
109109- _ => Err((StatusCode::BAD_REQUEST, "invalid partition".to_string())),
110110- }?;
111111-112112- Ok::<_, (StatusCode, String)>(results)
4949+ hydrant
5050+ .repos
5151+ .iter(cursor.as_ref())
5252+ .take(limit)
5353+ .collect::<miette::Result<Vec<_>>>()
11354 })
11455 .await
115115- .map_err(internal)??;
5656+ .into_diagnostic()
5757+ .flatten()
5858+ .map_err(internal)?;
1165911760 if prefers_json(&headers) {
11861 return Ok(Json(items).into_response());
···201144 .filter_map(|item| Did::new_owned(&item.did).ok())
202145 .collect();
203146204204- let queued = hydrant
205205- .repos
206206- .resync(dids)
207207- .await
208208- .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
147147+ let queued = hydrant.repos.resync(dids).await.map_err(internal)?;
209148210149 Ok(did_list_response(queued, &headers))
211150}
-1
src/control/mod.rs
···77pub use crawler::{CrawlerHandle, CrawlerSourceInfo};
88pub use filter::{FilterControl, FilterPatch, FilterSnapshot};
99pub use firehose::{FirehoseHandle, FirehoseSourceInfo};
1010-pub(crate) use repos::repo_state_to_info;
1110pub use repos::{ListedRecord, Record, RecordList, RepoHandle, RepoInfo, ReposControl};
12111312use std::collections::BTreeMap;
+89-3
src/control/repos.rs
···88use jacquard_common::types::string::{Did, Handle, Rkey};
99use jacquard_common::types::tid::Tid;
1010use jacquard_common::{CowStr, Data, IntoStatic};
1111-use miette::{IntoDiagnostic, Result};
1111+use miette::{Context, IntoDiagnostic, Result};
1212use rand::Rng;
1313use smol_str::ToSmolStr;
1414use url::Url;
15151616-use crate::db::types::DbRkey;
1616+use crate::db::types::{DbRkey, TrimmedDid};
1717use crate::db::{self, Db, keys, ser_repo_state};
1818use crate::state::AppState;
1919use crate::types::{GaugeState, RepoState, RepoStatus};
···6767pub struct ReposControl(pub(super) Arc<AppState>);
68686969impl ReposControl {
7070- /// gets a handle for a repository to allow acting upon it.
7070+ /// iterates through all repositories, returning their state.
7171+ pub fn iter(&self, cursor: Option<&Did<'_>>) -> impl Iterator<Item = Result<RepoInfo>> {
7272+ let start_bound = if let Some(cursor) = cursor {
7373+ let did_key = keys::repo_key(cursor);
7474+ std::ops::Bound::Excluded(did_key)
7575+ } else {
7676+ std::ops::Bound::Unbounded
7777+ };
7878+7979+ self.0
8080+ .db
8181+ .repos
8282+ .range((start_bound, std::ops::Bound::Unbounded))
8383+ .map(|g| {
8484+ let (k, v) = g.into_inner().into_diagnostic()?;
8585+ let repo_state = crate::db::deser_repo_state(&v)?;
8686+ let did = TrimmedDid::try_from(k.as_ref())?.to_did();
8787+ Ok(repo_state_to_info(did, repo_state))
8888+ })
8989+ }
9090+9191+ #[allow(dead_code)]
9292+ /// iterates through pending repositories, returning their state.
9393+ fn iter_pending(&self, cursor: Option<u64>) -> impl Iterator<Item = Result<(u64, RepoInfo)>> {
9494+ let start_bound = if let Some(cursor) = cursor {
9595+ std::ops::Bound::Excluded(cursor.to_be_bytes().to_vec())
9696+ } else {
9797+ std::ops::Bound::Unbounded
9898+ };
9999+100100+ let repos = self.0.db.repos.clone();
101101+ self.0
102102+ .db
103103+ .pending
104104+ .range((start_bound, std::ops::Bound::Unbounded))
105105+ .map(move |g| {
106106+ let (id_raw, did_key) = g.into_inner().into_diagnostic()?;
107107+ let id = u64::from_be_bytes(
108108+ id_raw
109109+ .as_ref()
110110+ .try_into()
111111+ .into_diagnostic()
112112+ .wrap_err("can't parse pending key")?,
113113+ );
114114+ let Some(bytes) = repos.get(&did_key).into_diagnostic()? else {
115115+ // stale pending that we forgot to delete? shouldn't happen though
116116+ tracing::warn!(id, did = ?did_key, "stale pending???");
117117+ return Ok(None);
118118+ };
119119+ let repo_state = crate::db::deser_repo_state(&bytes)?;
120120+ let did = TrimmedDid::try_from(did_key.as_ref())?.to_did();
121121+ Ok(Some((id, repo_state_to_info(did, repo_state))))
122122+ })
123123+ .map(|b| b.transpose())
124124+ .flatten()
125125+ }
126126+127127+ #[allow(dead_code)]
128128+ fn iter_resync(&self, cursor: Option<&Did<'_>>) -> impl Iterator<Item = Result<RepoInfo>> {
129129+ let start_bound = if let Some(cursor) = cursor {
130130+ let did_key = keys::repo_key(cursor);
131131+ std::ops::Bound::Excluded(did_key)
132132+ } else {
133133+ std::ops::Bound::Unbounded
134134+ };
135135+136136+ let repos = self.0.db.repos.clone();
137137+ self.0
138138+ .db
139139+ .resync
140140+ .range((start_bound, std::ops::Bound::Unbounded))
141141+ .map(move |g| {
142142+ let did_key = g.key().into_diagnostic()?;
143143+ let Some(bytes) = repos.get(&did_key).into_diagnostic()? else {
144144+ // stale pending that we forgot to delete? shouldn't happen though
145145+ tracing::warn!(did = ?did_key, "stale resync???");
146146+ return Ok(None);
147147+ };
148148+ let repo_state = crate::db::deser_repo_state(&bytes)?;
149149+ let did = TrimmedDid::try_from(did_key.as_ref())?.to_did();
150150+ Ok(Some(repo_state_to_info(did, repo_state)))
151151+ })
152152+ .map(|b| b.transpose())
153153+ .flatten()
154154+ }
155155+156156+ /// gets a handle for a repository to read from it.
71157 pub fn get<'i>(&self, did: &Did<'i>) -> Result<RepoHandle<'i>> {
72158 Ok(RepoHandle {
73159 state: self.0.clone(),