Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 214 additions & 111 deletions crates/gitlawb-node/src/api/ipfs.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/gitlawb-node/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ mod tests {
rate_limiter: RateLimiter::new(100, Duration::from_secs(60)),
create_ip_rate_limiter: RateLimiter::new(1000, Duration::from_secs(3600)),
push_rate_limiter: RateLimiter::new(600, Duration::from_secs(3600)),
ipfs_rate_limiter: RateLimiter::new(600, Duration::from_secs(3600)),
push_limiter_trust: crate::rate_limit::TrustedProxy::None,
sync_trigger_rate_limiter: RateLimiter::new(60, Duration::from_secs(3600)),
peer_write_rate_limiter: RateLimiter::new(600, Duration::from_secs(3600)),
Expand Down
79 changes: 79 additions & 0 deletions crates/gitlawb-node/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,19 @@ const MIGRATIONS: &[Migration] = &[
"CREATE UNIQUE INDEX IF NOT EXISTS idx_ref_certs_repo_ref ON ref_certificates(repo_id, ref_name)",
],
},
Migration {
version: 11,
name: "pinned_cids_cid_index",
stmts: &[
// GET /ipfs/{cid} resolves an incoming CID -> git oid via pinned_cids.cid
// (#173); index it so the per-request lookup is not a table scan. This is
// a NEW versioned migration (not appended to the applied v1 bundle) so a
// node already past v1 actually gets the index. Non-unique on purpose: cid
// is a function of raw content, so a UNIQUE index could reject a legitimate
// record_pinned_cid insert, and colliding rows serve byte-identical content.
"CREATE INDEX IF NOT EXISTS idx_pinned_cids_cid ON pinned_cids(cid)",
],
},
];

// ── Repos ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -2166,6 +2179,27 @@ impl Db {
Ok(row.get::<i64, _>("cnt") > 0)
}

/// Every git oid a pinned CID maps to (`pinned_cids.cid` -> `sha256_hex`).
/// `GET /ipfs/{cid}` resolves the content-addressed CID a client sends back to
/// the object's git oid this way: a real pin CID digests the raw object
/// content, not the git oid, so the digest cannot be `git cat-file`d directly
/// (#173). The index is unique on the git oid but NON-unique on cid, so two
/// distinct oids can share one content-CID (a tree and a blob whose raw bytes
/// collide, or byte-identical content pinned under two oids). Returning every
/// candidate lets the handler try each rather than pick one arbitrarily and
/// false-404 when the chosen one is withheld or absent while another is
/// readable (#173). Empty when the CID was never pinned on this node.
pub async fn oids_for_cid(&self, cid: &str) -> Result<Vec<String>> {
let rows = sqlx::query("SELECT sha256_hex FROM pinned_cids WHERE cid = $1")
.bind(cid)
.fetch_all(&self.pool)
.await?;
Ok(rows
.into_iter()
.map(|r| r.get::<String, _>("sha256_hex"))
.collect())
}

pub async fn record_pinned_cid(&self, sha256_hex: &str, cid: &str) -> Result<()> {
sqlx::query(
"INSERT INTO pinned_cids (sha256_hex, cid, pinned_at)
Expand Down Expand Up @@ -5037,6 +5071,51 @@ mod ref_certificate_tests {
);
}

/// INV-7: upgrade-path test — an existing node already past v1 must still get
/// the `pinned_cids.cid` index. It ships as its OWN v11 migration (not appended
/// to the applied v1 bundle), so dropping the index + its `schema_migrations`
/// row and re-running migrations must recreate it, exercising the real code
/// path rather than hand-copying the SQL.
#[sqlx::test]
async fn v11_pinned_cids_cid_index_applies_on_upgrade(pool: PgPool) {
async fn index_exists(pool: &PgPool) -> bool {
sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM pg_indexes WHERE indexname = 'idx_pinned_cids_cid')",
)
.fetch_one(pool)
.await
.unwrap()
}

let db = Db::for_testing(pool.clone());
db.run_migrations().await.unwrap();
assert!(
index_exists(&pool).await,
"fresh migration chain creates the index"
);

// Simulate a node at v10 (pre-v11): drop the index and its migration record.
sqlx::query("DROP INDEX IF EXISTS idx_pinned_cids_cid")
.execute(&pool)
.await
.unwrap();
sqlx::query("DELETE FROM schema_migrations WHERE version = 11")
.execute(&pool)
.await
.unwrap();
assert!(
!index_exists(&pool).await,
"precondition: index and its migration record removed"
);

// Re-run migrations: v11 re-applies and recreates the index on the upgrade.
db.run_migrations().await.unwrap();
assert!(
index_exists(&pool).await,
"v11 must recreate idx_pinned_cids_cid on an upgrading node"
);
}

/// INV-7: upgrade-path test — seed a database at v9 with duplicate
/// ref_certificates, then let the real v10 migration fire via
/// run_migrations(). This exercises the migration code path rather than
Expand Down
Loading
Loading