mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-26 04:04:14 +00:00
Creating & posting records works. Also messed up newlines but will fix later.
This commit is contained in:
@@ -14,9 +14,7 @@ use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::str::FromStr;
|
||||
use tracing::error;
|
||||
|
||||
const MAX_BLOB_SIZE: usize = 1_000_000;
|
||||
|
||||
pub async fn upload_blob(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -29,7 +27,6 @@ pub async fn upload_blob(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
@@ -42,7 +39,6 @@ pub async fn upload_blob(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
@@ -54,16 +50,13 @@ pub async fn upload_blob(
|
||||
}
|
||||
};
|
||||
let did = auth_user.did;
|
||||
|
||||
let mime_type = headers
|
||||
.get("content-type")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_string();
|
||||
|
||||
let size = body.len() as i64;
|
||||
let data = body.to_vec();
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let hash = hasher.finalize();
|
||||
@@ -80,13 +73,10 @@ pub async fn upload_blob(
|
||||
};
|
||||
let cid = Cid::new_v1(0x55, multihash);
|
||||
let cid_str = cid.to_string();
|
||||
|
||||
let storage_key = format!("blobs/{}", cid_str);
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let user_id = match user_query {
|
||||
Ok(Some(row)) => row.id,
|
||||
_ => {
|
||||
@@ -97,7 +87,6 @@ pub async fn upload_blob(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut tx = match state.db.begin().await {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
@@ -109,7 +98,6 @@ pub async fn upload_blob(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let insert = sqlx::query!(
|
||||
"INSERT INTO blobs (cid, mime_type, size_bytes, created_by_user, storage_key) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (cid) DO NOTHING RETURNING cid",
|
||||
cid_str,
|
||||
@@ -120,7 +108,6 @@ pub async fn upload_blob(
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await;
|
||||
|
||||
let was_inserted = match insert {
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => false,
|
||||
@@ -133,7 +120,6 @@ pub async fn upload_blob(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if was_inserted {
|
||||
if let Err(e) = state.blob_store.put_bytes(&storage_key, bytes::Bytes::from(data)).await {
|
||||
error!("Failed to upload blob to storage: {:?}", e);
|
||||
@@ -144,7 +130,6 @@ pub async fn upload_blob(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = tx.commit().await {
|
||||
error!("Failed to commit blob transaction: {:?}", e);
|
||||
if was_inserted {
|
||||
@@ -158,7 +143,6 @@ pub async fn upload_blob(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
"blob": {
|
||||
"ref": {
|
||||
@@ -170,26 +154,22 @@ pub async fn upload_blob(
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListMissingBlobsParams {
|
||||
pub limit: Option<i64>,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RecordBlob {
|
||||
pub cid: String,
|
||||
pub record_uri: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListMissingBlobsOutput {
|
||||
pub cursor: Option<String>,
|
||||
pub blobs: Vec<RecordBlob>,
|
||||
}
|
||||
|
||||
fn find_blobs(val: &serde_json::Value, blobs: &mut Vec<String>) {
|
||||
if let Some(obj) = val.as_object() {
|
||||
if let Some(type_val) = obj.get("$type") {
|
||||
@@ -212,7 +192,6 @@ fn find_blobs(val: &serde_json::Value, blobs: &mut Vec<String>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_missing_blobs(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -230,7 +209,6 @@ pub async fn list_missing_blobs(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
@@ -241,13 +219,10 @@ pub async fn list_missing_blobs(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let did = auth_user.did;
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let user_id = match user_query {
|
||||
Ok(Some(row)) => row.id,
|
||||
_ => {
|
||||
@@ -258,7 +233,6 @@ pub async fn list_missing_blobs(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let limit = params.limit.unwrap_or(500).clamp(1, 1000);
|
||||
let cursor_str = params.cursor.unwrap_or_default();
|
||||
let (cursor_collection, cursor_rkey) = if cursor_str.contains('|') {
|
||||
@@ -267,7 +241,6 @@ pub async fn list_missing_blobs(
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
};
|
||||
|
||||
let records_query = sqlx::query!(
|
||||
"SELECT collection, rkey, record_cid FROM records WHERE repo_id = $1 AND (collection, rkey) > ($2, $3) ORDER BY collection, rkey LIMIT $4",
|
||||
user_id,
|
||||
@@ -277,7 +250,6 @@ pub async fn list_missing_blobs(
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await;
|
||||
|
||||
let records = match records_query {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -289,40 +261,31 @@ pub async fn list_missing_blobs(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut missing_blobs = Vec::new();
|
||||
let mut last_cursor = None;
|
||||
|
||||
for row in &records {
|
||||
let collection = &row.collection;
|
||||
let rkey = &row.rkey;
|
||||
let record_cid_str = &row.record_cid;
|
||||
|
||||
last_cursor = Some(format!("{}|{}", collection, rkey));
|
||||
|
||||
let record_cid = match Cid::from_str(&record_cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let block_bytes = match state.block_store.get(&record_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let record_val: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block_bytes) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let mut blobs = Vec::new();
|
||||
find_blobs(&record_val, &mut blobs);
|
||||
|
||||
for blob_cid_str in blobs {
|
||||
let exists = sqlx::query!("SELECT 1 as one FROM blobs WHERE cid = $1 AND created_by_user = $2", blob_cid_str, user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
match exists {
|
||||
Ok(None) => {
|
||||
missing_blobs.push(RecordBlob {
|
||||
@@ -337,7 +300,6 @@ pub async fn list_missing_blobs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we fetched fewer records than limit, we are done, so cursor is None.
|
||||
// otherwise, cursor is the last one we saw.
|
||||
// ...right?
|
||||
@@ -346,7 +308,6 @@ pub async fn list_missing_blobs(
|
||||
} else {
|
||||
last_cursor
|
||||
};
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(ListMissingBlobsOutput {
|
||||
|
||||
+4
-30
@@ -11,10 +11,8 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
const DEFAULT_MAX_IMPORT_SIZE: usize = 100 * 1024 * 1024;
|
||||
const DEFAULT_MAX_BLOCKS: usize = 50000;
|
||||
|
||||
pub async fn import_repo(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -23,7 +21,6 @@ pub async fn import_repo(
|
||||
let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS")
|
||||
.map(|v| v != "false" && v != "0")
|
||||
.unwrap_or(true);
|
||||
|
||||
if !accepting_imports {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -34,12 +31,10 @@ pub async fn import_repo(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let max_size: usize = std::env::var("MAX_IMPORT_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAX_IMPORT_SIZE);
|
||||
|
||||
if body.len() > max_size {
|
||||
return (
|
||||
StatusCode::PAYLOAD_TOO_LARGE,
|
||||
@@ -50,21 +45,17 @@ pub async fn import_repo(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => return ApiError::AuthenticationRequired.into_response(),
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
};
|
||||
|
||||
let did = &auth_user.did;
|
||||
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, deactivated_at, takedown_ref FROM users WHERE did = $1",
|
||||
did
|
||||
@@ -89,7 +80,6 @@ pub async fn import_repo(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if user.deactivated_at.is_some() {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -100,7 +90,6 @@ pub async fn import_repo(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if user.takedown_ref.is_some() {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -111,9 +100,7 @@ pub async fn import_repo(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let user_id = user.id;
|
||||
|
||||
let (root, blocks) = match parse_car(&body).await {
|
||||
Ok((r, b)) => (r, b),
|
||||
Err(ImportError::InvalidRootCount) => {
|
||||
@@ -148,14 +135,12 @@ pub async fn import_repo(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Importing repo for user {}: {} blocks, root {}",
|
||||
did,
|
||||
blocks.len(),
|
||||
root
|
||||
);
|
||||
|
||||
let root_block = match blocks.get(&root) {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
@@ -169,7 +154,6 @@ pub async fn import_repo(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) {
|
||||
Ok(commit) => commit.did().to_string(),
|
||||
Err(e) => {
|
||||
@@ -183,7 +167,6 @@ pub async fn import_repo(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if commit_did != *did {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -197,15 +180,12 @@ pub async fn import_repo(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
|
||||
if !skip_verification {
|
||||
debug!("Verifying CAR file signature and structure for DID {}", did);
|
||||
let verifier = CarVerifier::new();
|
||||
|
||||
match verifier.verify_car(did, &root, &blocks).await {
|
||||
Ok(verified) => {
|
||||
debug!(
|
||||
@@ -285,12 +265,10 @@ pub async fn import_repo(
|
||||
} else {
|
||||
warn!("Skipping CAR signature verification for import (SKIP_IMPORT_VERIFICATION=true)");
|
||||
}
|
||||
|
||||
let max_blocks: usize = std::env::var("MAX_IMPORT_BLOCKS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAX_BLOCKS);
|
||||
|
||||
match apply_import(&state.db, user_id, root, blocks, max_blocks).await {
|
||||
Ok(records) => {
|
||||
info!(
|
||||
@@ -298,11 +276,9 @@ pub async fn import_repo(
|
||||
records.len(),
|
||||
did
|
||||
);
|
||||
|
||||
if let Err(e) = sequence_import_event(&state, did, &root.to_string()).await {
|
||||
warn!("Failed to sequence import event: {:?}", e);
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
Err(ImportError::SizeLimitExceeded) => (
|
||||
@@ -379,36 +355,34 @@ pub async fn import_repo(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn sequence_import_event(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
commit_cid: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let prev_cid: Option<String> = None;
|
||||
let prev_data_cid: Option<String> = None;
|
||||
let ops = serde_json::json!([]);
|
||||
let blobs: Vec<String> = vec![];
|
||||
let blocks_cids: Vec<String> = vec![];
|
||||
|
||||
let seq_row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)
|
||||
VALUES ($1, 'commit', $2, $3, $4, $5, $6)
|
||||
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, prev_data_cid, ops, blobs, blocks_cids)
|
||||
VALUES ($1, 'commit', $2, $3, $4, $5, $6, $7)
|
||||
RETURNING seq
|
||||
"#,
|
||||
did,
|
||||
commit_cid,
|
||||
prev_cid,
|
||||
prev_data_cid,
|
||||
ops,
|
||||
&blobs,
|
||||
&blocks_cids
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq_row.seq))
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+3
-19
@@ -7,36 +7,25 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DescribeRepoInput {
|
||||
pub repo: String,
|
||||
}
|
||||
|
||||
pub async fn describe_repo(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<DescribeRepoInput>,
|
||||
) -> Response {
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
let user_row = if input.repo.starts_with("did:") {
|
||||
sqlx::query!("SELECT id, handle, did FROM users WHERE did = $1", input.repo)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map(|opt| opt.map(|r| (r.id, r.handle, r.did)))
|
||||
} else {
|
||||
let suffix = format!(".{}", hostname);
|
||||
let short_handle = if input.repo.ends_with(&suffix) {
|
||||
input.repo.strip_suffix(&suffix).unwrap_or(&input.repo)
|
||||
} else {
|
||||
&input.repo
|
||||
};
|
||||
sqlx::query!("SELECT id, handle, did FROM users WHERE handle = $1", short_handle)
|
||||
sqlx::query!("SELECT id, handle, did FROM users WHERE handle = $1", input.repo)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map(|opt| opt.map(|r| (r.id, r.handle, r.did)))
|
||||
};
|
||||
|
||||
let (user_id, handle, did) = match user_row {
|
||||
Ok(Some((id, handle, did))) => (id, handle, did),
|
||||
_ => {
|
||||
@@ -47,25 +36,20 @@ pub async fn describe_repo(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let collections_query =
|
||||
sqlx::query!("SELECT DISTINCT collection FROM records WHERE repo_id = $1", user_id)
|
||||
.fetch_all(&state.db)
|
||||
.await;
|
||||
|
||||
let collections: Vec<String> = match collections_query {
|
||||
Ok(rows) => rows.iter().map(|r| r.collection.clone()).collect(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
let full_handle = format!("{}.{}", handle, hostname);
|
||||
let did_doc = json!({
|
||||
"id": did,
|
||||
"alsoKnownAs": [format!("at://{}", full_handle)]
|
||||
"alsoKnownAs": [format!("at://{}", handle)]
|
||||
});
|
||||
|
||||
Json(json!({
|
||||
"handle": full_handle,
|
||||
"handle": handle,
|
||||
"did": did,
|
||||
"didDoc": did_doc,
|
||||
"collections": collections,
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod blob;
|
||||
pub mod import;
|
||||
pub mod meta;
|
||||
pub mod record;
|
||||
|
||||
pub use blob::{list_missing_blobs, upload_blob};
|
||||
pub use import::import_repo;
|
||||
pub use meta::describe_repo;
|
||||
|
||||
@@ -9,18 +9,15 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use cid::Cid;
|
||||
use jacquard::types::string::Nsid;
|
||||
use jacquard::types::{integer::LimitedU32, string::{Nsid, Tid}};
|
||||
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
const MAX_BATCH_WRITES: usize = 200;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "$type")]
|
||||
pub enum WriteOp {
|
||||
@@ -39,7 +36,6 @@ pub enum WriteOp {
|
||||
#[serde(rename = "com.atproto.repo.applyWrites#delete")]
|
||||
Delete { collection: String, rkey: String },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApplyWritesInput {
|
||||
@@ -48,7 +44,6 @@ pub struct ApplyWritesInput {
|
||||
pub writes: Vec<WriteOp>,
|
||||
pub swap_commit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "$type")]
|
||||
pub enum WriteResult {
|
||||
@@ -59,19 +54,16 @@ pub enum WriteResult {
|
||||
#[serde(rename = "com.atproto.repo.applyWrites#deleteResult")]
|
||||
DeleteResult {},
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ApplyWritesOutput {
|
||||
pub commit: CommitInfo,
|
||||
pub results: Vec<WriteResult>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CommitInfo {
|
||||
pub cid: String,
|
||||
pub rev: String,
|
||||
}
|
||||
|
||||
pub async fn apply_writes(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -89,7 +81,6 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
@@ -100,9 +91,7 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let did = auth_user.did;
|
||||
|
||||
if input.repo != did {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -110,7 +99,6 @@ pub async fn apply_writes(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match has_verified_notification_channel(&state.db, &did).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
@@ -132,7 +120,6 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
if input.writes.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -140,7 +127,6 @@ pub async fn apply_writes(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if input.writes.len() > MAX_BATCH_WRITES {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -148,7 +134,6 @@ pub async fn apply_writes(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let user_id: uuid::Uuid = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
@@ -162,7 +147,6 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let root_cid_str: String =
|
||||
match sqlx::query_scalar!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
@@ -177,7 +161,6 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let current_root_cid = match Cid::from_str(&root_cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
@@ -188,7 +171,6 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (
|
||||
@@ -198,9 +180,7 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => {
|
||||
@@ -211,7 +191,6 @@ pub async fn apply_writes(
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
_ => {
|
||||
@@ -222,12 +201,11 @@ pub async fn apply_writes(
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
|
||||
let mut results: Vec<WriteResult> = Vec::new();
|
||||
let mut ops: Vec<RecordOp> = Vec::new();
|
||||
|
||||
let mut modified_keys: Vec<String> = Vec::new();
|
||||
for write in &input.writes {
|
||||
match write {
|
||||
WriteOp::Create {
|
||||
@@ -242,7 +220,7 @@ pub async fn apply_writes(
|
||||
}
|
||||
let rkey = rkey
|
||||
.clone()
|
||||
.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string());
|
||||
.unwrap_or_else(|| Tid::now(LimitedU32::MIN).to_string());
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, value).is_err() {
|
||||
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
|
||||
@@ -251,17 +229,16 @@ pub async fn apply_writes(
|
||||
Ok(c) => c,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to store record"}))).into_response(),
|
||||
};
|
||||
|
||||
let collection_nsid = match collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection", "message": "Invalid collection NSID"}))).into_response(),
|
||||
};
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
modified_keys.push(key.clone());
|
||||
mst = match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to add to MST"}))).into_response(),
|
||||
};
|
||||
|
||||
let uri = format!("at://{}/{}/{}", did, collection, rkey);
|
||||
results.push(WriteResult::CreateResult {
|
||||
uri,
|
||||
@@ -291,17 +268,17 @@ pub async fn apply_writes(
|
||||
Ok(c) => c,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to store record"}))).into_response(),
|
||||
};
|
||||
|
||||
let collection_nsid = match collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection", "message": "Invalid collection NSID"}))).into_response(),
|
||||
};
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
modified_keys.push(key.clone());
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
mst = match mst.update(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to update MST"}))).into_response(),
|
||||
};
|
||||
|
||||
let uri = format!("at://{}/{}/{}", did, collection, rkey);
|
||||
results.push(WriteResult::UpdateResult {
|
||||
uri,
|
||||
@@ -311,6 +288,7 @@ pub async fn apply_writes(
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
cid: record_cid,
|
||||
prev: prev_record_cid,
|
||||
});
|
||||
}
|
||||
WriteOp::Delete { collection, rkey } => {
|
||||
@@ -319,35 +297,50 @@ pub async fn apply_writes(
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection", "message": "Invalid collection NSID"}))).into_response(),
|
||||
};
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
modified_keys.push(key.clone());
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
mst = match mst.delete(&key).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to delete from MST"}))).into_response(),
|
||||
};
|
||||
|
||||
results.push(WriteResult::DeleteResult {});
|
||||
ops.push(RecordOp::Delete {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
prev: prev_record_cid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let new_mst_root = match mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to persist MST"}))).into_response(),
|
||||
};
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let mut relevant_blocks = std::collections::BTreeMap::new();
|
||||
for key in &modified_keys {
|
||||
if let Err(_) = mst.blocks_for_path(key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response();
|
||||
}
|
||||
if let Err(_) = original_mst.blocks_for_path(key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response();
|
||||
}
|
||||
}
|
||||
let mut written_cids = tracking_store.get_all_relevant_cids();
|
||||
for cid in relevant_blocks.keys() {
|
||||
if !written_cids.contains(cid) {
|
||||
written_cids.push(*cid);
|
||||
}
|
||||
}
|
||||
let written_cids_str = written_cids
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let commit_res = match commit_and_log(
|
||||
&state,
|
||||
&did,
|
||||
user_id,
|
||||
Some(current_root_cid),
|
||||
Some(commit.data),
|
||||
new_mst_root,
|
||||
ops,
|
||||
&written_cids_str,
|
||||
@@ -364,7 +357,6 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(ApplyWritesOutput {
|
||||
|
||||
@@ -16,7 +16,6 @@ use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteRecordInput {
|
||||
pub repo: String,
|
||||
@@ -27,7 +26,6 @@ pub struct DeleteRecordInput {
|
||||
#[serde(rename = "swapCommit")]
|
||||
pub swap_commit: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn delete_record(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -38,7 +36,6 @@ pub async fn delete_record(
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
};
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (
|
||||
@@ -48,9 +45,7 @@ pub async fn delete_record(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
@@ -59,7 +54,6 @@ pub async fn delete_record(
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(),
|
||||
};
|
||||
|
||||
let mst = Mst::load(
|
||||
Arc::new(tracking_store.clone()),
|
||||
commit.data,
|
||||
@@ -70,7 +64,6 @@ pub async fn delete_record(
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
|
||||
};
|
||||
let key = format!("{}/{}", collection_nsid, input.rkey);
|
||||
|
||||
if let Some(swap_record_str) = &input.swap_record {
|
||||
let expected_cid = Cid::from_str(swap_record_str).ok();
|
||||
let actual_cid = mst.get(&key).await.ok().flatten();
|
||||
@@ -78,11 +71,10 @@ pub async fn delete_record(
|
||||
return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Record has been modified or does not exist"}))).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
if mst.get(&key).await.ok().flatten().is_none() {
|
||||
let prev_record_cid = mst.get(&key).await.ok().flatten();
|
||||
if prev_record_cid.is_none() {
|
||||
return (StatusCode::OK, Json(json!({}))).into_response();
|
||||
}
|
||||
|
||||
let new_mst = match mst.delete(&key).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
@@ -90,7 +82,6 @@ pub async fn delete_record(
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to delete from MST: {:?}", e)}))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let new_mst_root = match new_mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@@ -98,14 +89,23 @@ pub async fn delete_record(
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to persist MST"}))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let op = RecordOp::Delete { collection: input.collection, rkey: input.rkey };
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let op = RecordOp::Delete { collection: input.collection, rkey: input.rkey, prev: prev_record_cid };
|
||||
let mut relevant_blocks = std::collections::BTreeMap::new();
|
||||
if let Err(_) = new_mst.blocks_for_path(&key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response();
|
||||
}
|
||||
if let Err(_) = mst.blocks_for_path(&key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response();
|
||||
}
|
||||
let mut written_cids = tracking_store.get_all_relevant_cids();
|
||||
for cid in relevant_blocks.keys() {
|
||||
if !written_cids.contains(cid) {
|
||||
written_cids.push(*cid);
|
||||
}
|
||||
}
|
||||
let written_cids_str = written_cids.iter().map(|c| c.to_string()).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), new_mst_root, vec![op], &written_cids_str).await {
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), Some(commit.data), new_mst_root, vec![op], &written_cids_str).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": e}))).into_response();
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod read;
|
||||
pub mod utils;
|
||||
pub mod validation;
|
||||
pub mod write;
|
||||
|
||||
pub use batch::apply_writes;
|
||||
pub use delete::{DeleteRecordInput, delete_record};
|
||||
pub use read::{GetRecordInput, ListRecordsInput, ListRecordsOutput, get_record, list_records};
|
||||
|
||||
@@ -12,7 +12,6 @@ use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetRecordInput {
|
||||
pub repo: String,
|
||||
@@ -20,13 +19,11 @@ pub struct GetRecordInput {
|
||||
pub rkey: String,
|
||||
pub cid: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_record(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<GetRecordInput>,
|
||||
) -> Response {
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
let user_id_opt = if input.repo.starts_with("did:") {
|
||||
sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo)
|
||||
.fetch_optional(&state.db)
|
||||
@@ -44,7 +41,6 @@ pub async fn get_record(
|
||||
.await
|
||||
.map(|opt| opt.map(|r| r.id))
|
||||
};
|
||||
|
||||
let user_id: uuid::Uuid = match user_id_opt {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
@@ -55,7 +51,6 @@ pub async fn get_record(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let record_row = sqlx::query!(
|
||||
"SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
|
||||
user_id,
|
||||
@@ -64,7 +59,6 @@ pub async fn get_record(
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let record_cid_str: String = match record_row {
|
||||
Ok(Some(row)) => row.record_cid,
|
||||
_ => {
|
||||
@@ -75,7 +69,6 @@ pub async fn get_record(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(expected_cid) = &input.cid {
|
||||
if &record_cid_str != expected_cid {
|
||||
return (
|
||||
@@ -85,7 +78,6 @@ pub async fn get_record(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let cid = match Cid::from_str(&record_cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
@@ -96,7 +88,6 @@ pub async fn get_record(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let block = match state.block_store.get(&cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => {
|
||||
@@ -107,7 +98,6 @@ pub async fn get_record(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let value: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@@ -119,7 +109,6 @@ pub async fn get_record(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
"uri": format!("at://{}/{}/{}", input.repo, input.collection, input.rkey),
|
||||
"cid": record_cid_str,
|
||||
@@ -127,7 +116,6 @@ pub async fn get_record(
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListRecordsInput {
|
||||
pub repo: String,
|
||||
@@ -140,19 +128,16 @@ pub struct ListRecordsInput {
|
||||
pub rkey_end: Option<String>,
|
||||
pub reverse: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListRecordsOutput {
|
||||
pub cursor: Option<String>,
|
||||
pub records: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub async fn list_records(
|
||||
State(state): State<AppState>,
|
||||
Query(input): Query<ListRecordsInput>,
|
||||
) -> Response {
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
let user_id_opt = if input.repo.starts_with("did:") {
|
||||
sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo)
|
||||
.fetch_optional(&state.db)
|
||||
@@ -170,7 +155,6 @@ pub async fn list_records(
|
||||
.await
|
||||
.map(|opt| opt.map(|r| r.id))
|
||||
};
|
||||
|
||||
let user_id: uuid::Uuid = match user_id_opt {
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
@@ -181,12 +165,10 @@ pub async fn list_records(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let limit = input.limit.unwrap_or(50).clamp(1, 100);
|
||||
let reverse = input.reverse.unwrap_or(false);
|
||||
let limit_i64 = limit as i64;
|
||||
let order = if reverse { "ASC" } else { "DESC" };
|
||||
|
||||
let rows_res: Result<Vec<(String, String)>, sqlx::Error> = if let Some(cursor) = &input.cursor {
|
||||
let comparator = if reverse { ">" } else { "<" };
|
||||
let query = format!(
|
||||
@@ -203,40 +185,32 @@ pub async fn list_records(
|
||||
} else {
|
||||
let mut conditions = vec!["repo_id = $1", "collection = $2"];
|
||||
let mut param_idx = 3;
|
||||
|
||||
if input.rkey_start.is_some() {
|
||||
conditions.push("rkey > $3");
|
||||
param_idx += 1;
|
||||
}
|
||||
|
||||
if input.rkey_end.is_some() {
|
||||
conditions.push(if param_idx == 3 { "rkey < $3" } else { "rkey < $4" });
|
||||
param_idx += 1;
|
||||
}
|
||||
|
||||
let limit_idx = param_idx;
|
||||
|
||||
let query = format!(
|
||||
"SELECT rkey, record_cid FROM records WHERE {} ORDER BY rkey {} LIMIT ${}",
|
||||
conditions.join(" AND "),
|
||||
order,
|
||||
limit_idx
|
||||
);
|
||||
|
||||
let mut query_builder = sqlx::query_as::<_, (String, String)>(&query)
|
||||
.bind(user_id)
|
||||
.bind(&input.collection);
|
||||
|
||||
if let Some(start) = &input.rkey_start {
|
||||
query_builder = query_builder.bind(start);
|
||||
}
|
||||
if let Some(end) = &input.rkey_end {
|
||||
query_builder = query_builder.bind(end);
|
||||
}
|
||||
|
||||
query_builder.bind(limit_i64).fetch_all(&state.db).await
|
||||
};
|
||||
|
||||
let rows = match rows_res {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -248,19 +222,15 @@ pub async fn list_records(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let last_rkey = rows.last().map(|(rkey, _)| rkey.clone());
|
||||
|
||||
let mut cid_to_rkey: HashMap<Cid, (String, String)> = HashMap::new();
|
||||
let mut cids: Vec<Cid> = Vec::with_capacity(rows.len());
|
||||
|
||||
for (rkey, cid_str) in &rows {
|
||||
if let Ok(cid) = Cid::from_str(cid_str) {
|
||||
cid_to_rkey.insert(cid, (rkey.clone(), cid_str.clone()));
|
||||
cids.push(cid);
|
||||
}
|
||||
}
|
||||
|
||||
let blocks = match state.block_store.get_many(&cids).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
@@ -272,7 +242,6 @@ pub async fn list_records(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut records = Vec::new();
|
||||
for (cid, block_opt) in cids.iter().zip(blocks.into_iter()) {
|
||||
if let Some(block) = block_opt {
|
||||
@@ -287,7 +256,6 @@ pub async fn list_records(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Json(ListRecordsOutput {
|
||||
cursor: last_rkey,
|
||||
records,
|
||||
|
||||
+249
-53
@@ -1,28 +1,88 @@
|
||||
use crate::state::AppState;
|
||||
use bytes::Bytes;
|
||||
use cid::Cid;
|
||||
use jacquard::types::{did::Did, integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::commit::Commit;
|
||||
use jacquard::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::storage::BlockStore;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
/*
|
||||
* Why am I making custom commit objects instead of jacquard's Commit::sign(), you ask?
|
||||
*
|
||||
* At time of writing, jacquard has a bug in how it creates unsigned bytes for signing.
|
||||
* Jacquard sets sig to empty bytes and serializes (6-field CBOR map)
|
||||
* Indigo/ATProto creates a struct *without* the sig field (5-field CBOR map)
|
||||
*
|
||||
* These produce different CBOR bytes, so signatures created with jacquard
|
||||
* don't verify with the relay's algorithm. The relay silently rejects commits
|
||||
* with invalid signatures.
|
||||
*
|
||||
* If you have it downloaded, see: reference-relay-indigo/atproto/repo/commit.go UnsignedBytes()
|
||||
*/
|
||||
#[derive(Serialize)]
|
||||
struct UnsignedCommit<'a> {
|
||||
data: Cid,
|
||||
did: &'a str,
|
||||
prev: Option<Cid>,
|
||||
rev: &'a str,
|
||||
version: i64,
|
||||
}
|
||||
fn create_signed_commit(
|
||||
did: &str,
|
||||
data: Cid,
|
||||
rev: &str,
|
||||
prev: Option<Cid>,
|
||||
signing_key: &SigningKey,
|
||||
) -> Result<(Vec<u8>, Bytes), String> {
|
||||
let unsigned = UnsignedCommit {
|
||||
data,
|
||||
did,
|
||||
prev,
|
||||
rev,
|
||||
version: 3,
|
||||
};
|
||||
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned)
|
||||
.map_err(|e| format!("Failed to serialize unsigned commit: {:?}", e))?;
|
||||
let sig: Signature = signing_key.sign(&unsigned_bytes);
|
||||
let sig_bytes = Bytes::copy_from_slice(&sig.to_bytes());
|
||||
#[derive(Serialize)]
|
||||
struct SignedCommit<'a> {
|
||||
data: Cid,
|
||||
did: &'a str,
|
||||
prev: Option<Cid>,
|
||||
rev: &'a str,
|
||||
#[serde(with = "serde_bytes")]
|
||||
sig: &'a [u8],
|
||||
version: i64,
|
||||
}
|
||||
let signed = SignedCommit {
|
||||
data,
|
||||
did,
|
||||
prev,
|
||||
rev,
|
||||
sig: &sig_bytes,
|
||||
version: 3,
|
||||
};
|
||||
let signed_bytes = serde_ipld_dagcbor::to_vec(&signed)
|
||||
.map_err(|e| format!("Failed to serialize signed commit: {:?}", e))?;
|
||||
Ok((signed_bytes, sig_bytes))
|
||||
}
|
||||
pub enum RecordOp {
|
||||
Create { collection: String, rkey: String, cid: Cid },
|
||||
Update { collection: String, rkey: String, cid: Cid },
|
||||
Delete { collection: String, rkey: String },
|
||||
Update { collection: String, rkey: String, cid: Cid, prev: Option<Cid> },
|
||||
Delete { collection: String, rkey: String, prev: Option<Cid> },
|
||||
}
|
||||
|
||||
pub struct CommitResult {
|
||||
pub commit_cid: Cid,
|
||||
pub rev: String,
|
||||
}
|
||||
|
||||
pub async fn commit_and_log(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
user_id: Uuid,
|
||||
current_root_cid: Option<Cid>,
|
||||
prev_data_cid: Option<Cid>,
|
||||
new_mst_root: Cid,
|
||||
ops: Vec<RecordOp>,
|
||||
blocks_cids: &[String],
|
||||
@@ -34,37 +94,29 @@ pub async fn commit_and_log(
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch signing key: {}", e))?;
|
||||
|
||||
let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| format!("Failed to decrypt signing key: {}", e))?;
|
||||
|
||||
let signing_key = SigningKey::from_slice(&key_bytes)
|
||||
.map_err(|e| format!("Invalid signing key: {}", e))?;
|
||||
|
||||
let did_obj = Did::new(did).map_err(|e| format!("Invalid DID: {}", e))?;
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let unsigned_commit = Commit::new_unsigned(did_obj, new_mst_root, rev.clone(), current_root_cid);
|
||||
|
||||
let signed_commit = unsigned_commit
|
||||
.sign(&signing_key)
|
||||
.map_err(|e| format!("Failed to sign commit: {:?}", e))?;
|
||||
|
||||
let new_commit_bytes = signed_commit.to_cbor().map_err(|e| format!("Failed to serialize commit: {:?}", e))?;
|
||||
|
||||
let rev_str = rev.to_string();
|
||||
let (new_commit_bytes, _sig) = create_signed_commit(
|
||||
did,
|
||||
new_mst_root,
|
||||
&rev_str,
|
||||
current_root_cid,
|
||||
&signing_key,
|
||||
)?;
|
||||
let new_root_cid = state.block_store.put(&new_commit_bytes).await
|
||||
.map_err(|e| format!("Failed to save commit block: {:?}", e))?;
|
||||
|
||||
let mut tx = state.db.begin().await
|
||||
.map_err(|e| format!("Failed to begin transaction: {}", e))?;
|
||||
|
||||
let lock_result = sqlx::query!(
|
||||
"SELECT repo_root_cid FROM repos WHERE user_id = $1 FOR UPDATE NOWAIT",
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await;
|
||||
|
||||
match lock_result {
|
||||
Err(e) => {
|
||||
if let Some(db_err) = e.as_database_error() {
|
||||
@@ -85,35 +137,28 @@ pub async fn commit_and_log(
|
||||
return Err("Repo not found".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2", new_root_cid.to_string(), user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repos): {}", e))?;
|
||||
|
||||
let rev_str = rev.to_string();
|
||||
|
||||
let mut upsert_collections: Vec<String> = Vec::new();
|
||||
let mut upsert_rkeys: Vec<String> = Vec::new();
|
||||
let mut upsert_cids: Vec<String> = Vec::new();
|
||||
|
||||
let mut delete_collections: Vec<String> = Vec::new();
|
||||
let mut delete_rkeys: Vec<String> = Vec::new();
|
||||
|
||||
for op in &ops {
|
||||
match op {
|
||||
RecordOp::Create { collection, rkey, cid } | RecordOp::Update { collection, rkey, cid } => {
|
||||
RecordOp::Create { collection, rkey, cid } | RecordOp::Update { collection, rkey, cid, .. } => {
|
||||
upsert_collections.push(collection.clone());
|
||||
upsert_rkeys.push(rkey.clone());
|
||||
upsert_cids.push(cid.to_string());
|
||||
}
|
||||
RecordOp::Delete { collection, rkey } => {
|
||||
RecordOp::Delete { collection, rkey, .. } => {
|
||||
delete_collections.push(collection.clone());
|
||||
delete_rkeys.push(rkey.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !upsert_collections.is_empty() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -133,7 +178,6 @@ pub async fn commit_and_log(
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (records batch upsert): {}", e))?;
|
||||
}
|
||||
|
||||
if !delete_collections.is_empty() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -149,7 +193,6 @@ pub async fn commit_and_log(
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (records batch delete): {}", e))?;
|
||||
}
|
||||
|
||||
let ops_json = ops.iter().map(|op| {
|
||||
match op {
|
||||
RecordOp::Create { collection, rkey, cid } => json!({
|
||||
@@ -157,26 +200,37 @@ pub async fn commit_and_log(
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": cid.to_string()
|
||||
}),
|
||||
RecordOp::Update { collection, rkey, cid } => json!({
|
||||
"action": "update",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": cid.to_string()
|
||||
}),
|
||||
RecordOp::Delete { collection, rkey } => json!({
|
||||
"action": "delete",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": null
|
||||
}),
|
||||
RecordOp::Update { collection, rkey, cid, prev } => {
|
||||
let mut obj = json!({
|
||||
"action": "update",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": cid.to_string()
|
||||
});
|
||||
if let Some(prev_cid) = prev {
|
||||
obj["prev"] = json!(prev_cid.to_string());
|
||||
}
|
||||
obj
|
||||
},
|
||||
RecordOp::Delete { collection, rkey, prev } => {
|
||||
let mut obj = json!({
|
||||
"action": "delete",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": null
|
||||
});
|
||||
if let Some(prev_cid) = prev {
|
||||
obj["prev"] = json!(prev_cid.to_string());
|
||||
}
|
||||
obj
|
||||
},
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let event_type = "commit";
|
||||
let prev_cid_str = current_root_cid.map(|c| c.to_string());
|
||||
|
||||
let prev_data_cid_str = prev_data_cid.map(|c| c.to_string());
|
||||
let seq_row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, prev_data_cid)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING seq
|
||||
"#,
|
||||
did,
|
||||
@@ -186,23 +240,165 @@ pub async fn commit_and_log(
|
||||
json!(ops_json),
|
||||
&[] as &[String],
|
||||
blocks_cids,
|
||||
prev_data_cid_str,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repo_seq): {}", e))?;
|
||||
|
||||
sqlx::query(
|
||||
&format!("NOTIFY repo_updates, '{}'", seq_row.seq)
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (notify): {}", e))?;
|
||||
|
||||
tx.commit().await
|
||||
.map_err(|e| format!("Failed to commit transaction: {}", e))?;
|
||||
|
||||
let _ = sequence_sync_event(state, did, &new_root_cid.to_string()).await;
|
||||
Ok(CommitResult {
|
||||
commit_cid: new_root_cid,
|
||||
rev: rev.to_string(),
|
||||
rev: rev_str,
|
||||
})
|
||||
}
|
||||
pub async fn create_record_internal(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
collection: &str,
|
||||
rkey: &str,
|
||||
record: &serde_json::Value,
|
||||
) -> Result<(String, Cid), String> {
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use jacquard_repo::mst::Mst;
|
||||
use std::sync::Arc;
|
||||
let user_id: Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB error: {}", e))?
|
||||
.ok_or_else(|| "User not found".to_string())?;
|
||||
let root_cid_str: String =
|
||||
sqlx::query_scalar!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB error: {}", e))?
|
||||
.ok_or_else(|| "Repo not found".to_string())?;
|
||||
let current_root_cid = Cid::from_str(&root_cid_str)
|
||||
.map_err(|_| "Invalid repo root CID".to_string())?;
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
let commit_bytes = tracking_store.get(¤t_root_cid).await
|
||||
.map_err(|e| format!("Failed to fetch commit: {:?}", e))?
|
||||
.ok_or_else(|| "Commit block not found".to_string())?;
|
||||
let commit = jacquard_repo::commit::Commit::from_cbor(&commit_bytes)
|
||||
.map_err(|e| format!("Failed to parse commit: {:?}", e))?;
|
||||
let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
let mut record_bytes = Vec::new();
|
||||
serde_ipld_dagcbor::to_writer(&mut record_bytes, record)
|
||||
.map_err(|e| format!("Failed to serialize record: {:?}", e))?;
|
||||
let record_cid = tracking_store.put(&record_bytes).await
|
||||
.map_err(|e| format!("Failed to save record block: {:?}", e))?;
|
||||
let key = format!("{}/{}", collection, rkey);
|
||||
let new_mst = mst.add(&key, record_cid).await
|
||||
.map_err(|e| format!("Failed to add to MST: {:?}", e))?;
|
||||
let new_mst_root = new_mst.persist().await
|
||||
.map_err(|e| format!("Failed to persist MST: {:?}", e))?;
|
||||
let op = RecordOp::Create {
|
||||
collection: collection.to_string(),
|
||||
rkey: rkey.to_string(),
|
||||
cid: record_cid,
|
||||
};
|
||||
let mut relevant_blocks = std::collections::BTreeMap::new();
|
||||
new_mst.blocks_for_path(&key, &mut relevant_blocks).await
|
||||
.map_err(|e| format!("Failed to get new MST blocks for path: {:?}", e))?;
|
||||
mst.blocks_for_path(&key, &mut relevant_blocks).await
|
||||
.map_err(|e| format!("Failed to get old MST blocks for path: {:?}", e))?;
|
||||
relevant_blocks.insert(record_cid, bytes::Bytes::from(record_bytes));
|
||||
let mut written_cids = tracking_store.get_all_relevant_cids();
|
||||
for cid in relevant_blocks.keys() {
|
||||
if !written_cids.contains(cid) {
|
||||
written_cids.push(*cid);
|
||||
}
|
||||
}
|
||||
let written_cids_str: Vec<String> = written_cids.iter().map(|c| c.to_string()).collect();
|
||||
let result = commit_and_log(
|
||||
state,
|
||||
did,
|
||||
user_id,
|
||||
Some(current_root_cid),
|
||||
Some(commit.data),
|
||||
new_mst_root,
|
||||
vec![op],
|
||||
&written_cids_str,
|
||||
).await?;
|
||||
let uri = format!("at://{}/{}/{}", did, collection, rkey);
|
||||
Ok((uri, result.commit_cid))
|
||||
}
|
||||
use std::str::FromStr;
|
||||
pub async fn sequence_identity_event(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
handle: Option<&str>,
|
||||
) -> Result<i64, String> {
|
||||
let seq_row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO repo_seq (did, event_type, handle)
|
||||
VALUES ($1, 'identity', $2)
|
||||
RETURNING seq
|
||||
"#,
|
||||
did,
|
||||
handle,
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repo_seq identity): {}", e))?;
|
||||
sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq_row.seq))
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (notify): {}", e))?;
|
||||
Ok(seq_row.seq)
|
||||
}
|
||||
pub async fn sequence_account_event(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
active: bool,
|
||||
status: Option<&str>,
|
||||
) -> Result<i64, String> {
|
||||
let seq_row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO repo_seq (did, event_type, active, status)
|
||||
VALUES ($1, 'account', $2, $3)
|
||||
RETURNING seq
|
||||
"#,
|
||||
did,
|
||||
active,
|
||||
status,
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repo_seq account): {}", e))?;
|
||||
sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq_row.seq))
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (notify): {}", e))?;
|
||||
Ok(seq_row.seq)
|
||||
}
|
||||
pub async fn sequence_sync_event(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
commit_cid: &str,
|
||||
) -> Result<i64, String> {
|
||||
let seq_row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO repo_seq (did, event_type, commit_cid)
|
||||
VALUES ($1, 'sync', $2)
|
||||
RETURNING seq
|
||||
"#,
|
||||
did,
|
||||
commit_cid,
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repo_seq sync): {}", e))?;
|
||||
sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq_row.seq))
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (notify): {}", e))?;
|
||||
Ok(seq_row.seq)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub fn validate_record(record: &serde_json::Value, collection: &str) -> Result<(), Response> {
|
||||
let validator = RecordValidator::new();
|
||||
match validator.validate(record, collection) {
|
||||
|
||||
@@ -8,9 +8,8 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use cid::Cid;
|
||||
use jacquard::types::string::Nsid;
|
||||
use jacquard::types::{integer::LimitedU32, string::{Nsid, Tid}};
|
||||
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -19,7 +18,6 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn has_verified_notification_channel(db: &PgPool, did: &str) -> Result<bool, sqlx::Error> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
@@ -35,7 +33,6 @@ pub async fn has_verified_notification_channel(db: &PgPool, did: &str) -> Result
|
||||
.bind(did)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
let email_confirmed: bool = r.get("email_confirmed");
|
||||
@@ -47,7 +44,6 @@ pub async fn has_verified_notification_channel(db: &PgPool, did: &str) -> Result
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn prepare_repo_write(
|
||||
state: &AppState,
|
||||
headers: &HeaderMap,
|
||||
@@ -62,7 +58,6 @@ pub async fn prepare_repo_write(
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
let auth_user = crate::auth::validate_bearer_token(&state.db, &token)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
@@ -72,7 +67,6 @@ pub async fn prepare_repo_write(
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if repo_did != auth_user.did {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -80,7 +74,6 @@ pub async fn prepare_repo_write(
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
match has_verified_notification_channel(&state.db, &auth_user.did).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
@@ -102,7 +95,6 @@ pub async fn prepare_repo_write(
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
@@ -117,7 +109,6 @@ pub async fn prepare_repo_write(
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
let root_cid_str: String =
|
||||
sqlx::query_scalar!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
@@ -133,7 +124,6 @@ pub async fn prepare_repo_write(
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
let current_root_cid = Cid::from_str(&root_cid_str).map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -141,10 +131,8 @@ pub async fn prepare_repo_write(
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
Ok((auth_user.did, user_id, current_root_cid))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CreateRecordInput {
|
||||
@@ -156,14 +144,12 @@ pub struct CreateRecordInput {
|
||||
#[serde(rename = "swapCommit")]
|
||||
pub swap_commit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateRecordOutput {
|
||||
pub uri: String,
|
||||
pub cid: String,
|
||||
}
|
||||
|
||||
pub async fn create_record(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -174,7 +160,6 @@ pub async fn create_record(
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
};
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (
|
||||
@@ -184,9 +169,7 @@ pub async fn create_record(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
@@ -195,26 +178,21 @@ pub async fn create_record(
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(),
|
||||
};
|
||||
|
||||
let mst = Mst::load(
|
||||
Arc::new(tracking_store.clone()),
|
||||
commit.data,
|
||||
None,
|
||||
);
|
||||
|
||||
let collection_nsid = match input.collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
|
||||
};
|
||||
|
||||
if input.validate.unwrap_or(true) {
|
||||
if let Err(err_response) = validate_record(&input.record, &input.collection) {
|
||||
return err_response;
|
||||
}
|
||||
}
|
||||
|
||||
let rkey = input.rkey.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string());
|
||||
|
||||
let rkey = input.rkey.unwrap_or_else(|| Tid::now(LimitedU32::MIN).to_string());
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record).is_err() {
|
||||
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
|
||||
@@ -223,7 +201,6 @@ pub async fn create_record(
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save record block"}))).into_response(),
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
let new_mst = match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
@@ -233,21 +210,30 @@ pub async fn create_record(
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to persist MST"}))).into_response(),
|
||||
};
|
||||
|
||||
let op = RecordOp::Create { collection: input.collection.clone(), rkey: rkey.clone(), cid: record_cid };
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let mut relevant_blocks = std::collections::BTreeMap::new();
|
||||
if let Err(_) = new_mst.blocks_for_path(&key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response();
|
||||
}
|
||||
if let Err(_) = mst.blocks_for_path(&key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response();
|
||||
}
|
||||
relevant_blocks.insert(record_cid, bytes::Bytes::from(record_bytes));
|
||||
let mut written_cids = tracking_store.get_all_relevant_cids();
|
||||
for cid in relevant_blocks.keys() {
|
||||
if !written_cids.contains(cid) {
|
||||
written_cids.push(*cid);
|
||||
}
|
||||
}
|
||||
let written_cids_str = written_cids.iter().map(|c| c.to_string()).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), new_mst_root, vec![op], &written_cids_str).await {
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), Some(commit.data), new_mst_root, vec![op], &written_cids_str).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": e}))).into_response();
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(CreateRecordOutput {
|
||||
uri: format!("at://{}/{}/{}", did, input.collection, rkey),
|
||||
cid: record_cid.to_string(),
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct PutRecordInput {
|
||||
@@ -261,14 +247,12 @@ pub struct PutRecordInput {
|
||||
#[serde(rename = "swapRecord")]
|
||||
pub swap_record: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PutRecordOutput {
|
||||
pub uri: String,
|
||||
pub cid: String,
|
||||
}
|
||||
|
||||
pub async fn put_record(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -279,15 +263,12 @@ pub async fn put_record(
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
};
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"}))).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
@@ -296,7 +277,6 @@ pub async fn put_record(
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(),
|
||||
};
|
||||
|
||||
let mst = Mst::load(
|
||||
Arc::new(tracking_store.clone()),
|
||||
commit.data,
|
||||
@@ -307,13 +287,11 @@ pub async fn put_record(
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
|
||||
};
|
||||
let key = format!("{}/{}", collection_nsid, input.rkey);
|
||||
|
||||
if input.validate.unwrap_or(true) {
|
||||
if let Err(err_response) = validate_record(&input.record, &input.collection) {
|
||||
return err_response;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(swap_record_str) = &input.swap_record {
|
||||
let expected_cid = Cid::from_str(swap_record_str).ok();
|
||||
let actual_cid = mst.get(&key).await.ok().flatten();
|
||||
@@ -321,9 +299,7 @@ pub async fn put_record(
|
||||
return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Record has been modified or does not exist"}))).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let existing_cid = mst.get(&key).await.ok().flatten();
|
||||
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record).is_err() {
|
||||
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
|
||||
@@ -332,7 +308,6 @@ pub async fn put_record(
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save record block"}))).into_response(),
|
||||
};
|
||||
|
||||
let new_mst = if existing_cid.is_some() {
|
||||
match mst.update(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
@@ -348,20 +323,29 @@ pub async fn put_record(
|
||||
Ok(c) => c,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to persist MST"}))).into_response(),
|
||||
};
|
||||
|
||||
let op = if existing_cid.is_some() {
|
||||
RecordOp::Update { collection: input.collection.clone(), rkey: input.rkey.clone(), cid: record_cid }
|
||||
RecordOp::Update { collection: input.collection.clone(), rkey: input.rkey.clone(), cid: record_cid, prev: existing_cid }
|
||||
} else {
|
||||
RecordOp::Create { collection: input.collection.clone(), rkey: input.rkey.clone(), cid: record_cid }
|
||||
};
|
||||
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let mut relevant_blocks = std::collections::BTreeMap::new();
|
||||
if let Err(_) = new_mst.blocks_for_path(&key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get new MST blocks for path"}))).into_response();
|
||||
}
|
||||
if let Err(_) = mst.blocks_for_path(&key, &mut relevant_blocks).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to get old MST blocks for path"}))).into_response();
|
||||
}
|
||||
relevant_blocks.insert(record_cid, bytes::Bytes::from(record_bytes));
|
||||
let mut written_cids = tracking_store.get_all_relevant_cids();
|
||||
for cid in relevant_blocks.keys() {
|
||||
if !written_cids.contains(cid) {
|
||||
written_cids.push(*cid);
|
||||
}
|
||||
}
|
||||
let written_cids_str = written_cids.iter().map(|c| c.to_string()).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), new_mst_root, vec![op], &written_cids_str).await {
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), Some(commit.data), new_mst_root, vec![op], &written_cids_str).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": e}))).into_response();
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(PutRecordOutput {
|
||||
uri: format!("at://{}/{}/{}", did, input.collection, input.rkey),
|
||||
cid: record_cid.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user