···01use reqwest::header;
2use rsky_common::env::env_int;
3use rsky_repo::block_map::BlockMap;
···6use rsky_repo::repo::Repo;
7use rsky_repo::sync::consumer::{VerifyRepoInput, verify_diff};
8use rsky_repo::types::{RecordWriteDescript, VerifiedDiff};
9-use serde::Deserialize;
10-use std::num::NonZeroU64;
1112use super::*;
1314-struct ImportRepoInput {
15- car_with_root: CarWithRoot,
16-}
17-18-// #[rocket::async_trait]
19-// impl<'r> FromData<'r> for ImportRepoInput {
20-// type Error = ApiError;
21-22-// #[tracing::instrument(skip_all)]
23-// async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self, Self::Error> {
24-// let max_import_size = env_int("IMPORT_REPO_LIMIT").unwrap_or(100).megabytes();
25-// match req.headers().get_one(header::CONTENT_LENGTH.as_ref()) {
26-// None => {
27-// let error = ApiError::InvalidRequest("Missing content-length header".to_string());
28-// req.local_cache(|| Some(error.clone()));
29-// Outcome::Error((Status::BadRequest, error))
30-// }
31-// Some(res) => match res.parse::<NonZeroU64>() {
32-// Ok(content_length) => {
33-// if content_length.get().bytes() > max_import_size {
34-// let error = ApiError::InvalidRequest(format!(
35-// "Content-Length is greater than maximum of {max_import_size}"
36-// ));
37-// req.local_cache(|| Some(error.clone()));
38-// return Outcome::Error((Status::BadRequest, error));
39-// }
40-41-// let import_datastream = data.open(content_length.get().bytes());
42-// match read_stream_car_with_root(import_datastream).await {
43-// Ok(car_with_root) => Outcome::Success(ImportRepoInput { car_with_root }),
44-// Err(error) => {
45-// let error = ApiError::InvalidRequest(error.to_string());
46-// req.local_cache(|| Some(error.clone()));
47-// Outcome::Error((Status::BadRequest, error))
48-// }
49-// }
50-// }
51-// Err(_error) => {
52-// tracing::error!("{}", format!("Error parsing content-length\n{_error}"));
53-// let error =
54-// ApiError::InvalidRequest("Error parsing content-length".to_string());
55-// req.local_cache(|| Some(error.clone()));
56-// Outcome::Error((Status::BadRequest, error))
57-// }
58-// },
59-// }
60-// }
61-// }
6263-// TODO: lookup axum docs to impl deserialize
000000006465#[tracing::instrument(skip_all)]
66#[axum::debug_handler(state = AppState)]
000067pub async fn import_repo(
68 // auth: AccessFullImport,
69 auth: AuthenticatedUser,
70- Query(import_repo_input): Query<ImportRepoInput>,
71 State(actor_pools): State<HashMap<String, ActorStorage, RandomState>>,
072) -> Result<(), ApiError> {
73 // let requester = auth.access.credentials.unwrap().did.unwrap();
74 let requester = auth.did();
75 let mut actor_store = ActorStore::from_actor_pools(&requester, &actor_pools).await;
7600000000000000077 // Get current repo if it exists
78 let curr_root: Option<Cid> = actor_store.get_repo_root().await;
79 let curr_repo: Option<Repo> = match curr_root {
···82 };
8384 // Process imported car
85- let car_with_root = import_repo_input.car_with_root;
00000008687 // Get verified difference from current repo and imported repo
88 let mut imported_blocks: BlockMap = car_with_root.blocks;
···127128/// Converts list of RecordWriteDescripts into a list of PreparedWrites
129async fn prepare_import_repo_writes(
130- _did: String,
131 writes: Vec<RecordWriteDescript>,
132 blocks: &BlockMap,
133) -> Result<Vec<PreparedWrite>, ApiError> {
134 match stream::iter(writes)
135 .then(|write| {
136- let did = _did.clone();
137 async move {
138 Ok::<PreparedWrite, anyhow::Error>(match write {
139 RecordWriteDescript::Create(write) => {
···1+use axum::{body::Bytes, http::HeaderMap};
2use reqwest::header;
3use rsky_common::env::env_int;
4use rsky_repo::block_map::BlockMap;
···7use rsky_repo::repo::Repo;
8use rsky_repo::sync::consumer::{VerifyRepoInput, verify_diff};
9use rsky_repo::types::{RecordWriteDescript, VerifiedDiff};
10+use ubyte::ToByteUnit;
01112use super::*;
1314+async fn from_data(bytes: Bytes) -> Result<CarWithRoot, ApiError> {
15+ let max_import_size = env_int("IMPORT_REPO_LIMIT").unwrap_or(100).megabytes();
16+ if bytes.len() > max_import_size {
17+ return Err(ApiError::InvalidRequest(format!(
18+ "Content-Length is greater than maximum of {max_import_size}"
19+ )));
20+ }
000000000000000000000000000000000000000002122+ let mut cursor = std::io::Cursor::new(bytes);
23+ match read_stream_car_with_root(&mut cursor).await {
24+ Ok(car_with_root) => Ok(car_with_root),
25+ Err(error) => {
26+ tracing::error!("Error reading stream car with root\n{error}");
27+ Err(ApiError::InvalidRequest("Invalid CAR file".to_owned()))
28+ }
29+ }
30+}
3132#[tracing::instrument(skip_all)]
33#[axum::debug_handler(state = AppState)]
34+/// Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.
35+/// Request
36+/// mime application/vnd.ipld.car
37+/// Body - required
38pub async fn import_repo(
39 // auth: AccessFullImport,
40 auth: AuthenticatedUser,
41+ headers: HeaderMap,
42 State(actor_pools): State<HashMap<String, ActorStorage, RandomState>>,
43+ body: Bytes,
44) -> Result<(), ApiError> {
45 // let requester = auth.access.credentials.unwrap().did.unwrap();
46 let requester = auth.did();
47 let mut actor_store = ActorStore::from_actor_pools(&requester, &actor_pools).await;
4849+ // Check headers
50+ let content_length = headers
51+ .get(header::CONTENT_LENGTH)
52+ .expect("no content length provided")
53+ .to_str()
54+ .map_err(anyhow::Error::from)
55+ .and_then(|content_length| content_length.parse::<u64>().map_err(anyhow::Error::from))
56+ .expect("invalid content-length header");
57+ if content_length > env_int("IMPORT_REPO_LIMIT").unwrap_or(100).megabytes() {
58+ return Err(ApiError::InvalidRequest(format!(
59+ "Content-Length is greater than maximum of {}",
60+ env_int("IMPORT_REPO_LIMIT").unwrap_or(100).megabytes()
61+ )));
62+ };
63+64 // Get current repo if it exists
65 let curr_root: Option<Cid> = actor_store.get_repo_root().await;
66 let curr_repo: Option<Repo> = match curr_root {
···69 };
7071 // Process imported car
72+ // let car_with_root = import_repo_input.car_with_root;
73+ let car_with_root: CarWithRoot = match from_data(body).await {
74+ Ok(car) => car,
75+ Err(error) => {
76+ tracing::error!("Error importing repo\n{error:?}");
77+ return Err(ApiError::InvalidRequest("Invalid CAR file".to_owned()));
78+ }
79+ };
8081 // Get verified difference from current repo and imported repo
82 let mut imported_blocks: BlockMap = car_with_root.blocks;
···121122/// Converts list of RecordWriteDescripts into a list of PreparedWrites
123async fn prepare_import_repo_writes(
124+ did: String,
125 writes: Vec<RecordWriteDescript>,
126 blocks: &BlockMap,
127) -> Result<Vec<PreparedWrite>, ApiError> {
128 match stream::iter(writes)
129 .then(|write| {
130+ let did = did.clone();
131 async move {
132 Ok::<PreparedWrite, anyhow::Error>(match write {
133 RecordWriteDescript::Create(write) => {
+15-8
src/apis/com/atproto/repo/list_missing_blobs.rs
···1-//!
2use rsky_lexicon::com::atproto::repo::ListMissingBlobsOutput;
3use rsky_pds::actor_store::blob::ListMissingBlobsOpts;
45use super::*;
67-///
0000000008#[tracing::instrument(skip_all)]
9#[axum::debug_handler(state = AppState)]
10pub async fn list_missing_blobs(
···14) -> Result<Json<ListMissingBlobsOutput>, ApiError> {
15 let cursor = input.cursor;
16 let limit = input.limit;
17- let limit: Option<u16> = Some(limit.unwrap().into());
0018 // let did = auth.access.credentials.unwrap().did.unwrap();
19 let did = user.did();
20- let limit: u16 = limit.unwrap_or(500);
2122 let actor_store = ActorStore::from_actor_pools(&did, &actor_pools).await;
23···27 .await
28 {
29 Ok(blobs) => {
30- let cursor = match blobs.last() {
31- Some(last_blob) => Some(last_blob.cid.clone()),
32- None => None,
33- };
34 Ok(Json(ListMissingBlobsOutput { cursor, blobs }))
35 }
36 Err(error) => {
···1+//! Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.
2use rsky_lexicon::com::atproto::repo::ListMissingBlobsOutput;
3use rsky_pds::actor_store::blob::ListMissingBlobsOpts;
45use super::*;
67+/// Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.
8+/// Request
9+/// Query Parameters
10+/// limit integer
11+/// Possible values: >= 1 and <= 1000
12+/// Default value: 500
13+/// cursor string
14+/// Responses
15+/// cursor string
16+/// blobs object[]
17#[tracing::instrument(skip_all)]
18#[axum::debug_handler(state = AppState)]
19pub async fn list_missing_blobs(
···23) -> Result<Json<ListMissingBlobsOutput>, ApiError> {
24 let cursor = input.cursor;
25 let limit = input.limit;
26+ let default_limit: atrium_api::types::LimitedNonZeroU16<1000> =
27+ atrium_api::types::LimitedNonZeroU16::try_from(500).expect("default limit");
28+ let limit: u16 = limit.unwrap_or(default_limit).into();
29 // let did = auth.access.credentials.unwrap().did.unwrap();
30 let did = user.did();
03132 let actor_store = ActorStore::from_actor_pools(&did, &actor_pools).await;
33···37 .await
38 {
39 Ok(blobs) => {
40+ let cursor = blobs.last().map(|last_blob| last_blob.cid.clone());
00041 Ok(Json(ListMissingBlobsOutput { cursor, blobs }))
42 }
43 Err(error) => {
+5-3
src/apis/com/atproto/repo/put_record.rs
···86 };
8788 match current {
89- Some(current) if current.cid == write.cid().unwrap().to_string() => (None, write),
0090 _ => {
91 let commit = actor_store
92 .process_writes(vec![write.clone()], swap_commit_cid)
···97 };
9899 if let Some(commit) = commit {
100- sequencer
101 .write()
102 .await
103 .sequence_commit(did.clone(), commit.clone())
···115 }
116 Ok(PutRecordOutput {
117 uri: write.uri().to_string(),
118- cid: write.cid().unwrap().to_string(),
119 })
120 } else {
121 bail!("Could not find repo: `{repo}`")