Skip to main content

kellnr_db/database/
mod.rs

1mod operations;
2pub mod test_utils;
3
4use std::cmp::max;
5use std::collections::BTreeMap;
6use std::path::Path;
7
8use chrono::{DateTime, Utc};
9use kellnr_common::crate_data::{CrateData, CrateRegistryDep, CrateVersionData};
10use kellnr_common::crate_overview::CrateOverview;
11use kellnr_common::cratesio_prefetch_msg::{CratesioPrefetchMsg, UpdateData};
12use kellnr_common::index_metadata::{IndexDep, IndexMetadata};
13use kellnr_common::normalized_name::NormalizedName;
14use kellnr_common::original_name::OriginalName;
15use kellnr_common::prefetch::Prefetch;
16use kellnr_common::publish_metadata::PublishMetadata;
17use kellnr_common::version::Version;
18use kellnr_common::webhook::{Webhook, WebhookEvent, WebhookQueue};
19use kellnr_entity::prelude::*;
20use kellnr_entity::{
21    auth_token, crate_author, crate_author_to_crate, crate_category, crate_category_to_crate,
22    crate_group, crate_index, crate_keyword, crate_keyword_to_crate, crate_meta, crate_user,
23    cratesio_crate, cratesio_index, cratesio_meta, doc_queue, group, group_user, krate,
24    oauth2_identity, oauth2_state, owner, session, toolchain, toolchain_component,
25    toolchain_target, user, webhook, webhook_queue,
26};
27use kellnr_migration::iden::{
28    AuthTokenIden, CrateIden, CrateMetaIden, CratesIoIden, CratesIoMetaIden, GroupIden,
29};
30use kellnr_settings::constants::OAUTH2_STATE_MAX_AGE;
31use sea_orm::entity::prelude::Uuid;
32use sea_orm::prelude::async_trait::async_trait;
33use sea_orm::query::{QueryOrder, QuerySelect, TransactionTrait};
34use sea_orm::sea_query::{
35    Alias, Cond, Condition, Expr, Func, Iden, JoinType, LikeExpr, Order, Query, SelectStatement,
36    UnionType,
37};
38use sea_orm::{
39    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ExprTrait,
40    FromQueryResult, ModelTrait, QueryFilter, RelationTrait, Set, Statement,
41};
42use tracing::warn;
43
44use crate::error::DbError;
45use crate::password::{PwdVerification, hash_pwd, hash_token, verify_pwd};
46use crate::provider::{
47    ChannelInfo, CrateVisibility, DbResult, OAuth2StateData, PrefetchState, SessionInfo,
48    ToolchainComponentInfo, ToolchainTargetInfo, ToolchainWithTargets,
49};
50use crate::tables::init_database;
51use crate::{
52    AuthToken, ConString, CrateMeta, CrateSummary, DbProvider, DocQueueEntry, Group, User,
53};
54
55const DB_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
56
57/// Timestamp (in [`DB_DATE_FORMAT`]) before which an `OAuth2` state is expired.
58fn oauth2_state_expiry_cutoff() -> String {
59    let max_age =
60        chrono::Duration::from_std(OAUTH2_STATE_MAX_AGE).expect("constant fits chrono::Duration");
61    (Utc::now() - max_age).format(DB_DATE_FORMAT).to_string()
62}
63
64/// Escape the LIKE wildcards `%` and `_` (and the escape character itself) in a
65/// user-supplied search term so it is matched literally. Callers wrap the result
66/// in `%...%` and pair it with `ESCAPE '\'`. Without this, a search for `%`
67/// matches every row and turns the search endpoint into a cheap `DoS`.
68fn escape_like_pattern(input: &str) -> String {
69    input
70        .replace('\\', "\\\\")
71        .replace('%', "\\%")
72        .replace('_', "\\_")
73}
74
75/// Upper bound on the configured session age. Clamping to this keeps cutoff
76/// arithmetic well within `chrono::Duration`/`DateTime` range, avoiding
77/// overflow panics for absurd configuration values.
78const MAX_SESSION_AGE: std::time::Duration = std::time::Duration::from_hours(876_000);
79
80/// Resolve a configured session age into the internal expiry duration.
81///
82/// A value of zero means sessions never expire ([`None`]). Any positive value
83/// is clamped to [`MAX_SESSION_AGE`] so cutoff calculations cannot overflow.
84fn session_age_to_duration(d: std::time::Duration) -> Option<chrono::Duration> {
85    if d.is_zero() {
86        return None;
87    }
88    let clamped = d.min(MAX_SESSION_AGE);
89    // `clamped` is at most MAX_SESSION_AGE, which fits comfortably in a
90    // `chrono::Duration`, so the conversion cannot fail.
91    Some(chrono::Duration::from_std(clamped).expect("clamped session age fits chrono::Duration"))
92}
93
94#[derive(Debug, PartialEq, FromQueryResult)]
95struct CountResult {
96    count: Option<i64>,
97}
98
99pub struct Database {
100    db_con: DatabaseConnection,
101    /// Maximum lifetime of a session before it is treated as expired, or
102    /// `None` if sessions never expire (configured age of zero).
103    session_age: Option<chrono::Duration>,
104}
105
106impl Database {
107    pub async fn new(con: &ConString, max_con: u32) -> Result<Self, DbError> {
108        let db_con = init_database(con, max_con)
109            .await
110            .map_err(|e| DbError::InitializationError(e.to_string()))?;
111
112        if operations::no_user_exists(&db_con).await? {
113            operations::insert_admin_credentials(&db_con, con).await?;
114        }
115
116        Ok(Self {
117            db_con,
118            session_age: session_age_to_duration(con.session_age()),
119        })
120    }
121
122    /// Timestamp (in [`DB_DATE_FORMAT`]) before which a session is expired, or
123    /// `None` when sessions never expire. A session whose `created` value is
124    /// older than the returned cutoff should not validate.
125    fn session_expiry_cutoff(&self) -> Option<String> {
126        self.session_age
127            .map(|age| (Utc::now() - age).format(DB_DATE_FORMAT).to_string())
128    }
129
130    /// Replaces the legacy SHA-256 hash of `user` with an argon2 hash of `pwd`.
131    /// The update only applies while the stored hash is still the legacy one,
132    /// so a password changed concurrently is never overwritten.
133    async fn upgrade_legacy_pwd_hash(&self, user: &User, pwd: &str) -> DbResult<()> {
134        let hashed = hash_pwd(pwd).await;
135        user::Entity::update_many()
136            .col_expr(user::Column::Pwd, Expr::value(hashed))
137            .col_expr(user::Column::Salt, Expr::value(String::new()))
138            .filter(user::Column::Id.eq(i64::from(user.id)))
139            .filter(user::Column::Pwd.eq(user.pwd.as_str()))
140            .exec(&self.db_con)
141            .await?;
142        Ok(())
143    }
144
145    /// Looks up a user by name; returns the entity model or [`DbError::UserNotFound`].
146    async fn get_user_model(&self, name: &str) -> DbResult<user::Model> {
147        user::Entity::find()
148            .filter(user::Column::Name.eq(name))
149            .one(&self.db_con)
150            .await?
151            .ok_or_else(|| DbError::UserNotFound(name.to_owned()))
152    }
153
154    /// Looks up a krate by normalized name; returns the entity model or [`DbError::CrateNotFound`].
155    async fn get_krate_model(&self, crate_name: &NormalizedName) -> DbResult<krate::Model> {
156        krate::Entity::find()
157            .filter(krate::Column::Name.eq(crate_name))
158            .one(&self.db_con)
159            .await?
160            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))
161    }
162
163    /// Looks up a group by name; returns the entity model or [`DbError::GroupNotFound`].
164    async fn get_group_model(&self, name: &str) -> DbResult<group::Model> {
165        group::Entity::find()
166            .filter(group::Column::Name.eq(name))
167            .one(&self.db_con)
168            .await?
169            .ok_or_else(|| DbError::GroupNotFound(name.to_owned()))
170    }
171
172    /// Loads targets (with their components) for a toolchain and returns a `ToolchainWithTargets`.
173    async fn toolchain_with_targets(&self, tc: toolchain::Model) -> DbResult<ToolchainWithTargets> {
174        let targets = toolchain_target::Entity::find()
175            .filter(toolchain_target::Column::ToolchainFk.eq(tc.id))
176            .all(&self.db_con)
177            .await?;
178
179        let mut target_infos = Vec::with_capacity(targets.len());
180        for t in targets {
181            let components = toolchain_component::Entity::find()
182                .filter(toolchain_component::Column::ToolchainTargetFk.eq(t.id))
183                .all(&self.db_con)
184                .await?;
185            target_infos.push(ToolchainTargetInfo {
186                id: t.id,
187                target: t.target,
188                storage_path: t.storage_path,
189                hash: t.hash,
190                size: t.size,
191                status: t.status,
192                components: components
193                    .into_iter()
194                    .map(ToolchainComponentInfo::from)
195                    .collect(),
196            });
197        }
198
199        Ok(ToolchainWithTargets {
200            id: tc.id,
201            name: tc.name,
202            version: tc.version,
203            date: tc.date,
204            channel: tc.channel,
205            created: tc.created,
206            targets: target_infos,
207        })
208    }
209
210    /// Looks up a crate index by name and version; returns the entity model or [`DbError::CrateIndexNotFound`].
211    async fn get_crate_index_model(
212        &self,
213        crate_name: &NormalizedName,
214        version: &Version,
215    ) -> DbResult<crate_index::Model> {
216        crate_index::Entity::find()
217            .filter(crate_index::Column::Name.eq(crate_name))
218            .filter(crate_index::Column::Vers.eq(version))
219            .one(&self.db_con)
220            .await?
221            .ok_or_else(|| DbError::CrateIndexNotFound(crate_name.to_string(), version.to_string()))
222    }
223
224    /// Looks up a toolchain by name and version; returns `None` if not found.
225    async fn get_toolchain_by_name_version(
226        &self,
227        name: &str,
228        version: &str,
229    ) -> DbResult<Option<toolchain::Model>> {
230        toolchain::Entity::find()
231            .filter(toolchain::Column::Name.eq(name))
232            .filter(toolchain::Column::Version.eq(version))
233            .one(&self.db_con)
234            .await
235            .map_err(Into::into)
236    }
237
238    /// Looks up a `crate_group` by crate name and group name; returns `None` if not found.
239    async fn get_crate_group_by_name_and_group(
240        &self,
241        crate_name: &NormalizedName,
242        group: &str,
243    ) -> DbResult<Option<crate_group::Model>> {
244        crate_group::Entity::find()
245            .join(JoinType::InnerJoin, crate_group::Relation::Krate.def())
246            .join(JoinType::InnerJoin, crate_group::Relation::Group.def())
247            .filter(
248                Cond::all()
249                    .add(krate::Column::Name.eq(crate_name))
250                    .add(group::Column::Name.eq(group)),
251            )
252            .one(&self.db_con)
253            .await
254            .map_err(Into::into)
255    }
256
257    /// Looks up a `group_user` by group name and user name; returns `None` if not found.
258    async fn get_group_user_by_group_and_user(
259        &self,
260        group_name: &str,
261        user: &str,
262    ) -> DbResult<Option<group_user::Model>> {
263        group_user::Entity::find()
264            .join(JoinType::InnerJoin, group_user::Relation::Group.def())
265            .join(JoinType::InnerJoin, group_user::Relation::User.def())
266            .filter(
267                Cond::all()
268                    .add(group::Column::Name.eq(group_name))
269                    .add(user::Column::Name.eq(user)),
270            )
271            .one(&self.db_con)
272            .await
273            .map_err(Into::into)
274    }
275
276    /// Looks up a `crate_user` by crate name and user name; returns `None` if not found.
277    async fn get_crate_user_by_crate_and_user(
278        &self,
279        crate_name: &NormalizedName,
280        user: &str,
281    ) -> DbResult<Option<crate_user::Model>> {
282        crate_user::Entity::find()
283            .join(JoinType::InnerJoin, crate_user::Relation::Krate.def())
284            .join(JoinType::InnerJoin, crate_user::Relation::User.def())
285            .filter(
286                Cond::all()
287                    .add(krate::Column::Name.eq(crate_name))
288                    .add(user::Column::Name.eq(user)),
289            )
290            .one(&self.db_con)
291            .await
292            .map_err(Into::into)
293    }
294
295    /// Adds an optional `where` clause for case-insensitive matching of name and
296    /// description.
297    ///
298    /// Case folding is only reliable for ASCII. The pattern is lowercased by
299    /// Rust, which is Unicode-aware, while the columns are lowercased by the
300    /// database: `PostgreSQL` folds according to the locale, but `SQLite`
301    /// ships without ICU and folds ASCII only. A description containing
302    /// "ÄPFEL" therefore stays uppercase on `SQLite` and never matches the
303    /// lowercased pattern, for any spelling of the query. Crate names are
304    /// unaffected, since `OriginalName` restricts them to ASCII.
305    fn add_filter_by_name_and_description(
306        mut query: SelectStatement,
307        contains: Option<&str>,
308        name: Expr,
309        description: Expr,
310    ) -> SelectStatement {
311        if let Some(contains) = contains {
312            let pattern = format!("%{}%", escape_like_pattern(contains)).to_lowercase();
313            query.and_where(
314                Func::lower(name)
315                    .like(LikeExpr::new(&pattern).escape('\\'))
316                    .or(Func::lower(description).like(LikeExpr::new(&pattern).escape('\\'))),
317            );
318        }
319        query
320    }
321
322    /// Joins the given SELECT statement against the crate table, restricts it to
323    /// the latest version of every crate and applies the optional name filter.
324    fn select_from_crates(mut query: SelectStatement, contains: Option<&str>) -> SelectStatement {
325        query
326            .inner_join(
327                CrateIden::Table,
328                Expr::col((CrateMetaIden::Table, CrateMetaIden::CrateFk))
329                    .equals((CrateIden::Table, CrateIden::Id)),
330            )
331            .and_where(
332                Expr::col((CrateMetaIden::Table, CrateMetaIden::Version))
333                    .equals((CrateIden::Table, CrateIden::MaxVersion)),
334            );
335
336        Self::add_filter_by_name_and_description(
337            query,
338            contains,
339            Expr::col((CrateIden::Table, CrateIden::OriginalName)),
340            Expr::col((CrateIden::Table, CrateIden::Description)),
341        )
342    }
343
344    /// Joins the given SELECT statement against the crates.io cache table,
345    /// restricts it to the latest version of every crate and applies the
346    /// optional name filter.
347    fn select_from_cache(mut query: SelectStatement, contains: Option<&str>) -> SelectStatement {
348        query
349            .inner_join(
350                CratesIoIden::Table,
351                Expr::col((CratesIoMetaIden::Table, CratesIoMetaIden::CratesIoFk))
352                    .equals((CratesIoIden::Table, CratesIoIden::Id)),
353            )
354            .and_where(
355                Expr::col((CratesIoMetaIden::Table, CratesIoMetaIden::Version))
356                    .equals((CratesIoIden::Table, CratesIoIden::MaxVersion)),
357            );
358
359        Self::add_filter_by_name_and_description(
360            query,
361            contains,
362            Expr::col((CratesIoIden::Table, CratesIoIden::OriginalName)),
363            Expr::col((CratesIoIden::Table, CratesIoIden::Description)),
364        )
365    }
366
367    /// Condition on the crate table that hides the download-restricted crates
368    /// `visibility` does not grant access to. `None` if nothing is hidden.
369    fn crate_visibility_condition(visibility: &CrateVisibility) -> Option<Condition> {
370        let unrestricted = Expr::col((CrateIden::Table, CrateIden::RestrictedDownload)).eq(false);
371        match visibility {
372            CrateVisibility::All => None,
373            CrateVisibility::Unrestricted => Some(Cond::all().add(unrestricted)),
374            CrateVisibility::User(user) => {
375                let crate_id = || Expr::col((CrateIden::Table, CrateIden::Id));
376                Some(
377                    Cond::any()
378                        .add(unrestricted)
379                        .add(crate_id().in_subquery(Self::owned_crate_ids(user)))
380                        .add(crate_id().in_subquery(Self::crate_user_crate_ids(user)))
381                        .add(crate_id().in_subquery(Self::crate_group_crate_ids(user))),
382                )
383            }
384        }
385    }
386
387    fn user_id_by_name(user: &str) -> SelectStatement {
388        Query::select()
389            .column((user::Entity, user::Column::Id))
390            .from(user::Entity)
391            .and_where(Expr::col((user::Entity, user::Column::Name)).eq(user))
392            .to_owned()
393    }
394
395    fn owned_crate_ids(user: &str) -> SelectStatement {
396        Query::select()
397            .column((owner::Entity, owner::Column::CrateFk))
398            .from(owner::Entity)
399            .and_where(
400                Expr::col((owner::Entity, owner::Column::UserFk))
401                    .in_subquery(Self::user_id_by_name(user)),
402            )
403            .to_owned()
404    }
405
406    fn crate_user_crate_ids(user: &str) -> SelectStatement {
407        Query::select()
408            .column((crate_user::Entity, crate_user::Column::CrateFk))
409            .from(crate_user::Entity)
410            .and_where(
411                Expr::col((crate_user::Entity, crate_user::Column::UserFk))
412                    .in_subquery(Self::user_id_by_name(user)),
413            )
414            .to_owned()
415    }
416
417    fn crate_group_crate_ids(user: &str) -> SelectStatement {
418        Query::select()
419            .column((crate_group::Entity, crate_group::Column::CrateFk))
420            .from(crate_group::Entity)
421            .inner_join(
422                group_user::Entity,
423                Expr::col((group_user::Entity, group_user::Column::GroupFk))
424                    .equals((crate_group::Entity, crate_group::Column::GroupFk)),
425            )
426            .and_where(
427                Expr::col((group_user::Entity, group_user::Column::UserFk))
428                    .in_subquery(Self::user_id_by_name(user)),
429            )
430            .to_owned()
431    }
432
433    /// Shared implementation for crate overview listing: optional name filter, optional limit/offset, cache union.
434    async fn query_crates(
435        &self,
436        contains: Option<&str>,
437        limit_offset: Option<(u64, u64)>,
438        cache: bool,
439        visibility: &CrateVisibility,
440    ) -> DbResult<Vec<CrateOverview>> {
441        /// Creates a SELECT statement for a given set of fields.
442        fn select_fields(fields: &[(Expr, &str)]) -> SelectStatement {
443            let mut query = Query::select();
444            for (col, alias) in fields {
445                query.expr_as(col.clone(), Alias::new(*alias));
446            }
447            query
448        }
449
450        let mut query = select_fields(&[
451            (Expr::col(CrateIden::OriginalName), "name"),
452            (Expr::col(CrateIden::MaxVersion), "version"),
453            (Expr::col(CrateIden::LastUpdated), "date"),
454            (Expr::col(CrateIden::TotalDownloads), "total_downloads"),
455            (Expr::col(CrateIden::Description), "description"),
456            (Expr::col(CrateMetaIden::Documentation), "documentation"),
457            (Expr::cust("false"), "is_cache"),
458        ]);
459        query.from(CrateMetaIden::Table);
460        query = Self::select_from_crates(query, contains);
461        if let Some(condition) = Self::crate_visibility_condition(visibility) {
462            query.cond_where(condition);
463        }
464
465        // UNION with cached crates
466        if cache {
467            let mut query2 = select_fields(&[
468                (Expr::col(CratesIoIden::OriginalName), "name"),
469                (Expr::col(CratesIoIden::MaxVersion), "version"),
470                (Expr::col(CratesIoIden::LastModified), "date"),
471                (Expr::col(CratesIoIden::TotalDownloads), "total_downloads"),
472                (Expr::col(CratesIoIden::Description), "description"),
473                (Expr::col(CratesIoMetaIden::Documentation), "documentation"),
474                (Expr::cust("true"), "is_cache"),
475            ]);
476            query2.from(CratesIoMetaIden::Table);
477            query2 = Self::select_from_cache(query2, contains);
478
479            query.union(UnionType::All, query2);
480        }
481
482        // Ordering and optional limit/offset
483        query.order_by(Alias::new("name"), Order::Asc);
484        if let Some((limit, offset)) = limit_offset {
485            query.limit(limit).offset(offset);
486        }
487
488        let stmt = self.db_con.get_database_backend().build(&query);
489
490        CrateOverview::find_by_statement(stmt)
491            .all(&self.db_con)
492            .await
493            .map_err(DbError::from)
494    }
495
496    async fn count_crates(
497        &self,
498        contains: Option<&str>,
499        cache: bool,
500        visibility: &CrateVisibility,
501    ) -> DbResult<u64> {
502        let mut query = Query::select();
503        query.expr_as(
504            Expr::col((CrateIden::Table, CrateIden::Id)).count(),
505            Alias::new("count"),
506        );
507        query.from(CrateMetaIden::Table);
508        query = Self::select_from_crates(query, contains);
509        if let Some(condition) = Self::crate_visibility_condition(visibility) {
510            query.cond_where(condition);
511        }
512
513        let count = self
514            .count_by_statement(
515                self.db_con.get_database_backend().build(&query),
516                DbError::FailedToCountCrates,
517            )
518            .await?;
519
520        let cache_count = if cache {
521            let mut query2 = Query::select();
522            query2.expr_as(
523                Expr::col((CratesIoIden::Table, CratesIoIden::Id)).count(),
524                Alias::new("count"),
525            );
526            query2.from(CratesIoMetaIden::Table);
527            query2 = Self::select_from_cache(query2, contains);
528
529            self.count_by_statement(
530                self.db_con.get_database_backend().build(&query2),
531                DbError::FailedToCountCrates,
532            )
533            .await?
534        } else {
535            0
536        };
537
538        Ok(count + cache_count)
539    }
540
541    /// Executes a count query `SELECT COUNT(id_column) FROM table` and returns the count as u64.
542    async fn count<T>(&self, table: T, id_column: T, error: DbError) -> DbResult<u64>
543    where
544        T: Iden + Copy,
545    {
546        let statement = self.db_con.get_database_backend().build(
547            Query::select()
548                .expr_as(Expr::col((table, id_column)).count(), Alias::new("count"))
549                .from(table),
550        );
551        self.count_by_statement(statement, error).await
552    }
553
554    /// Runs a statement that selects a single `count` column and returns it as u64.
555    async fn count_by_statement(&self, statement: Statement, error: DbError) -> DbResult<u64> {
556        let Some(result) = CountResult::find_by_statement(statement)
557            .one(&self.db_con)
558            .await?
559        else {
560            return Err(error);
561        };
562        result
563            .count
564            .and_then(|v| u64::try_from(v).ok())
565            .ok_or(error)
566    }
567
568    async fn get_webhook_model(&self, id: &str) -> DbResult<webhook::Model> {
569        webhook::Entity::find()
570            .filter(webhook::Column::Id.eq(
571                TryInto::<Uuid>::try_into(id).map_err(|_| DbError::InvalidId(id.to_string()))?,
572            ))
573            .one(&self.db_con)
574            .await?
575            .ok_or(DbError::WebhookNotFound)
576    }
577
578    async fn get_webhook_queue_model(&self, id: &str) -> DbResult<webhook_queue::Model> {
579        webhook_queue::Entity::find()
580            .filter(webhook_queue::Column::Id.eq(
581                TryInto::<Uuid>::try_into(id).map_err(|_| DbError::InvalidId(id.to_string()))?,
582            ))
583            .one(&self.db_con)
584            .await?
585            .ok_or(DbError::WebhookNotFound)
586    }
587
588    async fn get_owner_by_crate_and_user(
589        &self,
590        crate_name: &str,
591        owner_name: &str,
592    ) -> DbResult<Option<owner::Model>> {
593        owner::Entity::find()
594            .join(JoinType::InnerJoin, owner::Relation::Krate.def())
595            .join(JoinType::InnerJoin, owner::Relation::User.def())
596            .filter(
597                Cond::all()
598                    .add(krate::Column::Name.eq(crate_name))
599                    .add(user::Column::Name.eq(owner_name)),
600            )
601            .one(&self.db_con)
602            .await
603            .map_err(Into::into)
604    }
605
606    async fn delete_or_not_found<M, A>(&self, model: Option<M>, err: DbError) -> DbResult<()>
607    where
608        M: ModelTrait + sea_orm::IntoActiveModel<A>,
609        A: ActiveModelTrait<Entity = M::Entity> + sea_orm::ActiveModelBehavior + Send,
610    {
611        model.ok_or(err)?.delete(&self.db_con).await?;
612        Ok(())
613    }
614}
615
616fn webhook_model_to_obj(w: webhook::Model) -> DbResult<Webhook> {
617    let event = w
618        .event
619        .as_str()
620        .try_into()
621        .map_err(|_| DbError::InvalidWebhookEvent(w.event))?;
622    Ok(Webhook {
623        id: Some(w.id.into()),
624        name: w.name,
625        event,
626        callback_url: w.callback_url,
627        secret: w.secret,
628    })
629}
630
631/// Generates an `add_*` method that links two entities via a join table.
632///
633/// # Parameters
634/// - `$method`: The method name to generate (e.g., `add_crate_user_impl`)
635/// - `$relation_entity`: The join table entity module (e.g., `crate_user`)
636/// - `$insert_entity`: The Sea ORM Entity type for insertion (e.g., `CrateUser`)
637/// - `$fk1`: The foreign key field for the first entity
638/// - `$fk2`: The foreign key field for the second entity
639macro_rules! impl_add_relation {
640    ($method:ident, $relation_entity:ident, $insert_entity:ident, $fk1:ident, $fk2:ident) => {
641        async fn $method(&self, fk1: i64, fk2: i64) -> DbResult<()> {
642            let model = $relation_entity::ActiveModel {
643                $fk1: Set(fk1),
644                $fk2: Set(fk2),
645                ..Default::default()
646            };
647            $insert_entity::insert(model).exec(&self.db_con).await?;
648            Ok(())
649        }
650    };
651}
652
653impl Database {
654    impl_add_relation!(
655        add_crate_user_impl,
656        crate_user,
657        CrateUser,
658        crate_fk,
659        user_fk
660    );
661    impl_add_relation!(
662        add_crate_group_impl,
663        crate_group,
664        CrateGroup,
665        crate_fk,
666        group_fk
667    );
668    impl_add_relation!(
669        add_group_user_impl,
670        group_user,
671        GroupUser,
672        group_fk,
673        user_fk
674    );
675    impl_add_relation!(add_owner_impl, owner, Owner, crate_fk, user_fk);
676}
677
678#[async_trait]
679impl DbProvider for Database {
680    async fn get_total_unique_cached_crates(&self) -> DbResult<u64> {
681        self.count(
682            CratesIoIden::Table,
683            CratesIoIden::Id,
684            DbError::FailedToCountCrates,
685        )
686        .await
687    }
688
689    async fn get_total_cached_crate_versions(&self) -> DbResult<u64> {
690        self.count(
691            CratesIoMetaIden::Table,
692            CratesIoMetaIden::Id,
693            DbError::FailedToCountCrateVersions,
694        )
695        .await
696    }
697
698    async fn get_total_cached_downloads(&self) -> DbResult<u64> {
699        #[derive(FromQueryResult)]
700        struct Model {
701            total_downloads: i64,
702        }
703
704        let total_downloads = cratesio_crate::Entity::find()
705            .select_only()
706            .column(cratesio_crate::Column::TotalDownloads)
707            .into_model::<Model>()
708            .all(&self.db_con)
709            .await?;
710
711        Ok(total_downloads
712            .iter()
713            .map(|m| m.total_downloads as u64)
714            .sum())
715    }
716
717    async fn authenticate_user(&self, name: &str, pwd: &str) -> DbResult<User> {
718        let user = self.get_user(name).await?;
719
720        match verify_pwd(pwd, &user.pwd, &user.salt).await {
721            PwdVerification::Valid => Ok(user),
722            PwdVerification::ValidLegacy => {
723                // Upgrade the legacy SHA-256 hash now that the plaintext is
724                // known. A failure must not block an otherwise valid login,
725                // the upgrade is retried on the next one.
726                if let Err(e) = self.upgrade_legacy_pwd_hash(&user, pwd).await {
727                    warn!("Failed to upgrade password hash of user {}: {e}", user.name);
728                }
729                Ok(user)
730            }
731            PwdVerification::Invalid => Err(DbError::PasswordMismatch),
732        }
733    }
734
735    async fn increase_download_counter(
736        &self,
737        crate_name: &NormalizedName,
738        crate_version: &Version,
739    ) -> DbResult<()> {
740        let txn = self.db_con.begin().await?;
741
742        krate::Entity::update_many()
743            .col_expr(
744                krate::Column::TotalDownloads,
745                Expr::col(krate::Column::TotalDownloads).add(1),
746            )
747            .filter(krate::Column::Name.eq(crate_name))
748            .exec(&txn)
749            .await?;
750
751        let crate_id = krate::Entity::find()
752            .filter(krate::Column::Name.eq(crate_name))
753            .one(&txn)
754            .await?
755            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
756            .id;
757
758        crate_meta::Entity::update_many()
759            .col_expr(
760                crate_meta::Column::Downloads,
761                Expr::col(crate_meta::Column::Downloads).add(1),
762            )
763            .filter(
764                Cond::all()
765                    .add(crate_meta::Column::Version.eq(crate_version))
766                    .add(crate_meta::Column::CrateFk.eq(crate_id)),
767            )
768            .exec(&txn)
769            .await?;
770
771        txn.commit().await?;
772        Ok(())
773    }
774
775    async fn increase_cached_download_counter(
776        &self,
777        crate_name: &NormalizedName,
778        crate_version: &Version,
779    ) -> DbResult<()> {
780        let txn = self.db_con.begin().await?;
781
782        cratesio_crate::Entity::update_many()
783            .col_expr(
784                cratesio_crate::Column::TotalDownloads,
785                Expr::col(cratesio_crate::Column::TotalDownloads).add(1),
786            )
787            .filter(cratesio_crate::Column::Name.eq(crate_name))
788            .exec(&txn)
789            .await?;
790
791        let crate_id = cratesio_crate::Entity::find()
792            .filter(cratesio_crate::Column::Name.eq(crate_name))
793            .one(&txn)
794            .await?
795            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
796            .id;
797
798        cratesio_meta::Entity::update_many()
799            .col_expr(
800                cratesio_meta::Column::Downloads,
801                Expr::col(cratesio_meta::Column::Downloads).add(1),
802            )
803            .filter(
804                Cond::all()
805                    .add(cratesio_meta::Column::Version.eq(crate_version))
806                    .add(cratesio_meta::Column::CratesIoFk.eq(crate_id)),
807            )
808            .exec(&txn)
809            .await?;
810
811        txn.commit().await?;
812        Ok(())
813    }
814
815    async fn increase_download_counter_by(
816        &self,
817        crate_name: &NormalizedName,
818        crate_version: &Version,
819        count: u64,
820    ) -> DbResult<()> {
821        let txn = self.db_con.begin().await?;
822
823        krate::Entity::update_many()
824            .col_expr(
825                krate::Column::TotalDownloads,
826                Expr::col(krate::Column::TotalDownloads).add(count as i64),
827            )
828            .filter(krate::Column::Name.eq(crate_name))
829            .exec(&txn)
830            .await?;
831
832        let crate_id = krate::Entity::find()
833            .filter(krate::Column::Name.eq(crate_name))
834            .one(&txn)
835            .await?
836            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
837            .id;
838
839        crate_meta::Entity::update_many()
840            .col_expr(
841                crate_meta::Column::Downloads,
842                Expr::col(crate_meta::Column::Downloads).add(count as i64),
843            )
844            .filter(
845                Cond::all()
846                    .add(crate_meta::Column::Version.eq(crate_version))
847                    .add(crate_meta::Column::CrateFk.eq(crate_id)),
848            )
849            .exec(&txn)
850            .await?;
851
852        txn.commit().await?;
853        Ok(())
854    }
855
856    async fn increase_cached_download_counter_by(
857        &self,
858        crate_name: &NormalizedName,
859        crate_version: &Version,
860        count: u64,
861    ) -> DbResult<()> {
862        let txn = self.db_con.begin().await?;
863
864        cratesio_crate::Entity::update_many()
865            .col_expr(
866                cratesio_crate::Column::TotalDownloads,
867                Expr::col(cratesio_crate::Column::TotalDownloads).add(count as i64),
868            )
869            .filter(cratesio_crate::Column::Name.eq(crate_name))
870            .exec(&txn)
871            .await?;
872
873        let crate_id = cratesio_crate::Entity::find()
874            .filter(cratesio_crate::Column::Name.eq(crate_name))
875            .one(&txn)
876            .await?
877            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
878            .id;
879
880        cratesio_meta::Entity::update_many()
881            .col_expr(
882                cratesio_meta::Column::Downloads,
883                Expr::col(cratesio_meta::Column::Downloads).add(count as i64),
884            )
885            .filter(
886                Cond::all()
887                    .add(cratesio_meta::Column::Version.eq(crate_version))
888                    .add(cratesio_meta::Column::CratesIoFk.eq(crate_id)),
889            )
890            .exec(&txn)
891            .await?;
892
893        txn.commit().await?;
894        Ok(())
895    }
896
897    async fn get_last_updated_crate(
898        &self,
899        visibility: &CrateVisibility,
900    ) -> DbResult<Option<(OriginalName, Version)>> {
901        let mut query = krate::Entity::find();
902        if let Some(condition) = Self::crate_visibility_condition(visibility) {
903            query = query.filter(condition);
904        }
905        let krate = query
906            .order_by_desc(krate::Column::LastUpdated)
907            .one(&self.db_con)
908            .await?;
909
910        if let Some(krate) = krate {
911            // SAFETY: Unchecked is ok, as only valid crate names are inserted into the database
912            let name = OriginalName::from_unchecked(krate.original_name);
913            // SAFETY: Unchecked is ok, as only valid versions are inserted into the database
914            let version = Version::from_unchecked_str(&krate.max_version);
915            Ok(Some((name, version)))
916        } else {
917            Ok(None)
918        }
919    }
920
921    async fn validate_session(&self, session_token: &str) -> DbResult<SessionInfo> {
922        // An expired session is treated like a missing one: the age filter
923        // makes it invisible here so callers get the same SessionNotFound.
924        // With expiry disabled (no cutoff) the filter is omitted.
925        let mut query = user::Entity::find()
926            .join(JoinType::InnerJoin, user::Relation::Session.def())
927            .filter(session::Column::Token.eq(hash_token(session_token)));
928        if let Some(cutoff) = self.session_expiry_cutoff() {
929            query = query.filter(session::Column::Created.gte(cutoff));
930        }
931
932        let u = query
933            .one(&self.db_con)
934            .await?
935            .ok_or(DbError::SessionNotFound)?;
936
937        Ok(SessionInfo {
938            name: u.name,
939            is_admin: u.is_admin,
940            is_read_only: u.is_read_only,
941        })
942    }
943
944    async fn delete_expired_sessions(&self) -> DbResult<u64> {
945        // Expiry disabled: nothing to remove.
946        let Some(cutoff) = self.session_expiry_cutoff() else {
947            return Ok(0);
948        };
949        let res = session::Entity::delete_many()
950            .filter(session::Column::Created.lt(cutoff))
951            .exec(&self.db_con)
952            .await?;
953        Ok(res.rows_affected)
954    }
955
956    async fn add_session_token(&self, name: &str, session_token: &str) -> DbResult<()> {
957        let user = self.get_user(name).await?;
958        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
959
960        let s = session::ActiveModel {
961            token: Set(hash_token(session_token)),
962            created: Set(created),
963            user_fk: Set(user.id as i64),
964            ..Default::default()
965        };
966
967        s.insert(&self.db_con).await?;
968        Ok(())
969    }
970
971    async fn delete_user_sessions(&self, user_name: &str) -> DbResult<u64> {
972        let user = self.get_user_model(user_name).await?;
973        let res = session::Entity::delete_many()
974            .filter(session::Column::UserFk.eq(user.id))
975            .exec(&self.db_con)
976            .await?;
977        Ok(res.rows_affected)
978    }
979
980    async fn delete_other_user_sessions(
981        &self,
982        user_name: &str,
983        keep_session_token: &str,
984    ) -> DbResult<u64> {
985        let user = self.get_user_model(user_name).await?;
986        let res = session::Entity::delete_many()
987            .filter(session::Column::UserFk.eq(user.id))
988            .filter(session::Column::Token.ne(hash_token(keep_session_token)))
989            .exec(&self.db_con)
990            .await?;
991        Ok(res.rows_affected)
992    }
993
994    async fn add_crate_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<()> {
995        let crate_fk = self.get_krate_model(crate_name).await?.id;
996        let user_fk = self.get_user_model(user).await?.id;
997        self.add_crate_user_impl(crate_fk, user_fk).await
998    }
999
1000    async fn add_crate_group(&self, crate_name: &NormalizedName, group: &str) -> DbResult<()> {
1001        let crate_fk = self.get_krate_model(crate_name).await?.id;
1002        let group_fk = self.get_group_model(group).await?.id;
1003        self.add_crate_group_impl(crate_fk, group_fk).await
1004    }
1005
1006    async fn add_group_user(&self, group_name: &str, user: &str) -> DbResult<()> {
1007        let group_fk = self.get_group_model(group_name).await?.id;
1008        let user_fk = self.get_user_model(user).await?.id;
1009        self.add_group_user_impl(group_fk, user_fk).await
1010    }
1011
1012    async fn add_owner(&self, crate_name: &NormalizedName, owner: &str) -> DbResult<()> {
1013        let crate_fk = self.get_krate_model(crate_name).await?.id;
1014        let user_fk = self.get_user_model(owner).await?.id;
1015        self.add_owner_impl(crate_fk, user_fk).await
1016    }
1017
1018    async fn is_download_restricted(&self, crate_name: &NormalizedName) -> DbResult<bool> {
1019        Ok(krate::Entity::find()
1020            .filter(krate::Column::Name.eq(crate_name))
1021            .one(&self.db_con)
1022            .await?
1023            .is_some_and(|model| model.restricted_download))
1024    }
1025
1026    async fn change_download_restricted(
1027        &self,
1028        crate_name: &NormalizedName,
1029        restricted: bool,
1030    ) -> DbResult<()> {
1031        let mut krate: krate::ActiveModel = self.get_krate_model(crate_name).await?.into();
1032        krate.restricted_download = Set(restricted);
1033        krate.update(&self.db_con).await?;
1034        Ok(())
1035    }
1036
1037    async fn is_crate_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<bool> {
1038        Ok(self
1039            .get_crate_user_by_crate_and_user(crate_name, user)
1040            .await?
1041            .is_some())
1042    }
1043
1044    async fn is_crate_group(&self, crate_name: &NormalizedName, group: &str) -> DbResult<bool> {
1045        Ok(self
1046            .get_crate_group_by_name_and_group(crate_name, group)
1047            .await?
1048            .is_some())
1049    }
1050
1051    async fn is_crate_group_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<bool> {
1052        let user = user::Entity::find()
1053            .join(JoinType::InnerJoin, user::Relation::GroupUser.def())
1054            .join(JoinType::InnerJoin, group_user::Relation::Group.def())
1055            .join(JoinType::InnerJoin, group::Relation::CrateGroup.def())
1056            .join(JoinType::InnerJoin, crate_group::Relation::Krate.def())
1057            .filter(
1058                Cond::all()
1059                    .add(krate::Column::Name.eq(crate_name))
1060                    .add(user::Column::Name.eq(user)),
1061            )
1062            .one(&self.db_con)
1063            .await?;
1064        Ok(user.is_some())
1065    }
1066
1067    async fn is_group_user(&self, group_name: &str, user: &str) -> DbResult<bool> {
1068        Ok(self
1069            .get_group_user_by_group_and_user(group_name, user)
1070            .await?
1071            .is_some())
1072    }
1073
1074    async fn is_owner(&self, crate_name: &NormalizedName, user: &str) -> DbResult<bool> {
1075        let owner = owner::Entity::find()
1076            .join(JoinType::InnerJoin, owner::Relation::Krate.def())
1077            .join(JoinType::InnerJoin, owner::Relation::User.def())
1078            .filter(
1079                Cond::all()
1080                    .add(krate::Column::Name.eq(crate_name))
1081                    .add(user::Column::Name.eq(user)),
1082            )
1083            .one(&self.db_con)
1084            .await?;
1085
1086        Ok(owner.is_some())
1087    }
1088
1089    async fn get_crate_id(&self, crate_name: &NormalizedName) -> DbResult<Option<i64>> {
1090        let id = krate::Entity::find()
1091            .filter(krate::Column::Name.eq(crate_name))
1092            .one(&self.db_con)
1093            .await?
1094            .map(|model| model.id);
1095
1096        Ok(id)
1097    }
1098
1099    async fn get_crate_owners(&self, crate_name: &NormalizedName) -> DbResult<Vec<User>> {
1100        let u = user::Entity::find()
1101            .join(JoinType::InnerJoin, user::Relation::Owner.def())
1102            .join(JoinType::InnerJoin, owner::Relation::Krate.def())
1103            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1104            .all(&self.db_con)
1105            .await?;
1106
1107        Ok(u.into_iter().map(User::from).collect())
1108    }
1109
1110    async fn get_crate_users(&self, crate_name: &NormalizedName) -> DbResult<Vec<User>> {
1111        let u = user::Entity::find()
1112            .join(JoinType::InnerJoin, user::Relation::CrateUser.def())
1113            .join(JoinType::InnerJoin, crate_user::Relation::Krate.def())
1114            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1115            .all(&self.db_con)
1116            .await?;
1117
1118        Ok(u.into_iter().map(User::from).collect())
1119    }
1120
1121    async fn get_crate_groups(&self, crate_name: &NormalizedName) -> DbResult<Vec<Group>> {
1122        let u = group::Entity::find()
1123            .join(JoinType::InnerJoin, group::Relation::CrateGroup.def())
1124            .join(JoinType::InnerJoin, crate_group::Relation::Krate.def())
1125            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1126            .all(&self.db_con)
1127            .await?;
1128
1129        Ok(u.into_iter().map(Group::from).collect())
1130    }
1131
1132    async fn get_group_users(&self, group_name: &str) -> DbResult<Vec<User>> {
1133        let u = user::Entity::find()
1134            .join(JoinType::InnerJoin, user::Relation::GroupUser.def())
1135            .join(JoinType::InnerJoin, group_user::Relation::Group.def())
1136            .filter(Expr::col((GroupIden::Table, group::Column::Name)).eq(group_name))
1137            .all(&self.db_con)
1138            .await?;
1139
1140        Ok(u.into_iter().map(User::from).collect())
1141    }
1142
1143    async fn get_crate_versions(&self, crate_name: &NormalizedName) -> DbResult<Vec<Version>> {
1144        let u = crate_meta::Entity::find()
1145            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1146            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1147            .all(&self.db_con)
1148            .await?;
1149
1150        Ok(u.into_iter()
1151            .map(|meta| Version::from_unchecked_str(&meta.version))
1152            .collect())
1153    }
1154
1155    async fn delete_session_token(&self, session_token: &str) -> DbResult<()> {
1156        if let Some(s) = session::Entity::find()
1157            .filter(session::Column::Token.eq(hash_token(session_token)))
1158            .one(&self.db_con)
1159            .await?
1160        {
1161            s.delete(&self.db_con).await?;
1162        }
1163
1164        Ok(())
1165    }
1166
1167    async fn delete_user(&self, user_name: &str) -> DbResult<()> {
1168        self.get_user_model(user_name)
1169            .await?
1170            .delete(&self.db_con)
1171            .await?;
1172        Ok(())
1173    }
1174
1175    async fn delete_group(&self, group_name: &str) -> DbResult<()> {
1176        self.get_group_model(group_name)
1177            .await?
1178            .delete(&self.db_con)
1179            .await?;
1180        Ok(())
1181    }
1182
1183    async fn change_pwd(&self, user_name: &str, new_pwd: &str) -> DbResult<()> {
1184        let hashed = hash_pwd(new_pwd).await;
1185
1186        let mut u: user::ActiveModel = self.get_user_model(user_name).await?.into();
1187        u.pwd = Set(hashed);
1188        // The argon2 PHC string carries its own salt.
1189        u.salt = Set(String::new());
1190
1191        u.update(&self.db_con).await?;
1192        Ok(())
1193    }
1194
1195    async fn change_read_only_state(&self, user_name: &str, state: bool) -> DbResult<()> {
1196        let mut u: user::ActiveModel = self.get_user_model(user_name).await?.into();
1197        u.is_read_only = Set(state);
1198
1199        u.update(&self.db_con).await?;
1200        Ok(())
1201    }
1202
1203    async fn change_admin_state(&self, user_name: &str, state: bool) -> DbResult<()> {
1204        let mut u: user::ActiveModel = self.get_user_model(user_name).await?.into();
1205        u.is_admin = Set(state);
1206
1207        u.update(&self.db_con).await?;
1208        Ok(())
1209    }
1210
1211    async fn crate_version_exists(&self, crate_id: i64, version: &str) -> DbResult<bool> {
1212        let cm = crate_meta::Entity::find()
1213            .filter(
1214                Cond::all()
1215                    .add(crate_meta::Column::CrateFk.eq(crate_id))
1216                    .add(crate_meta::Column::Version.eq(version)),
1217            )
1218            .one(&self.db_con)
1219            .await?;
1220
1221        Ok(cm.is_some())
1222    }
1223
1224    async fn get_max_version_from_id(&self, crate_id: i64) -> DbResult<Version> {
1225        operations::get_max_version_from_id(&self.db_con, crate_id).await
1226    }
1227
1228    async fn get_max_version_from_name(&self, crate_name: &NormalizedName) -> DbResult<Version> {
1229        let k = self.get_krate_model(crate_name).await?;
1230        let v = Version::try_from(&k.max_version)
1231            .map_err(|_| DbError::FailedToGetMaxVersionByName(crate_name.to_string()))?;
1232        Ok(v)
1233    }
1234
1235    async fn update_max_version(&self, crate_id: i64, version: &Version) -> DbResult<()> {
1236        let krate = krate::Entity::find_by_id(crate_id)
1237            .one(&self.db_con)
1238            .await?
1239            .ok_or(DbError::CrateNotFoundWithId(crate_id))?;
1240
1241        let mut k: krate::ActiveModel = krate.into();
1242        k.max_version = Set(version.to_string());
1243        k.update(&self.db_con).await?;
1244
1245        Ok(())
1246    }
1247
1248    async fn add_auth_token(&self, name: &str, token: &str, user: &str) -> DbResult<()> {
1249        let hashed_token = hash_token(token);
1250        let user = self.get_user_model(user).await?;
1251
1252        let at = auth_token::ActiveModel {
1253            name: Set(name.to_owned()),
1254            token: Set(hashed_token),
1255            user_fk: Set(user.id),
1256            ..Default::default()
1257        };
1258
1259        at.insert(&self.db_con).await?;
1260
1261        Ok(())
1262    }
1263
1264    async fn get_user_from_token(&self, token: &str) -> DbResult<User> {
1265        let token = hash_token(token);
1266
1267        let u = user::Entity::find()
1268            .join(JoinType::InnerJoin, user::Relation::AuthToken.def())
1269            .filter(Expr::col((AuthTokenIden::Table, AuthTokenIden::Token)).eq(token))
1270            .one(&self.db_con)
1271            .await?
1272            .ok_or(DbError::TokenNotFound)?;
1273
1274        Ok(User::from(u))
1275    }
1276
1277    async fn get_user(&self, name: &str) -> DbResult<User> {
1278        Ok(User::from(self.get_user_model(name).await?))
1279    }
1280
1281    async fn get_group(&self, name: &str) -> DbResult<Group> {
1282        Ok(Group::from(self.get_group_model(name).await?))
1283    }
1284
1285    async fn get_auth_tokens(&self, user_name: &str) -> DbResult<Vec<AuthToken>> {
1286        let at: Vec<auth_token::Model> = auth_token::Entity::find()
1287            .join(JoinType::InnerJoin, auth_token::Relation::User.def())
1288            .filter(user::Column::Name.eq(user_name))
1289            .all(&self.db_con)
1290            .await?;
1291
1292        Ok(at.into_iter().map(AuthToken::from).collect())
1293    }
1294
1295    async fn delete_auth_token(&self, id: i32) -> DbResult<()> {
1296        auth_token::Entity::delete_by_id(id as i64)
1297            .exec(&self.db_con)
1298            .await?;
1299        Ok(())
1300    }
1301
1302    async fn delete_owner(&self, crate_name: &str, owner: &str) -> DbResult<()> {
1303        let model = self.get_owner_by_crate_and_user(crate_name, owner).await?;
1304        self.delete_or_not_found(model, DbError::OwnerNotFound(owner.to_string()))
1305            .await
1306    }
1307
1308    async fn delete_crate_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<()> {
1309        let model = self
1310            .get_crate_user_by_crate_and_user(crate_name, user)
1311            .await?;
1312        self.delete_or_not_found(model, DbError::UserNotFound(user.to_string()))
1313            .await
1314    }
1315
1316    async fn delete_crate_group(&self, crate_name: &NormalizedName, group: &str) -> DbResult<()> {
1317        let model = self
1318            .get_crate_group_by_name_and_group(crate_name, group)
1319            .await?;
1320        self.delete_or_not_found(model, DbError::GroupNotFound(group.to_string()))
1321            .await
1322    }
1323
1324    async fn delete_group_user(&self, group_name: &str, user: &str) -> DbResult<()> {
1325        let model = self
1326            .get_group_user_by_group_and_user(group_name, user)
1327            .await?;
1328        self.delete_or_not_found(model, DbError::UserNotFound(user.to_string()))
1329            .await
1330    }
1331
1332    async fn add_user(
1333        &self,
1334        name: &str,
1335        pwd: &str,
1336        is_admin: bool,
1337        is_read_only: bool,
1338    ) -> DbResult<()> {
1339        let hashed_pwd = hash_pwd(pwd).await;
1340        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
1341
1342        let u = user::ActiveModel {
1343            name: Set(name.to_owned()),
1344            pwd: Set(hashed_pwd),
1345            salt: Set(String::new()),
1346            is_admin: Set(is_admin),
1347            is_read_only: Set(is_read_only),
1348            created: Set(created),
1349            ..Default::default()
1350        };
1351
1352        u.insert(&self.db_con).await?;
1353        Ok(())
1354    }
1355
1356    async fn add_group(&self, name: &str) -> DbResult<()> {
1357        let g = group::ActiveModel {
1358            name: Set(name.to_owned()),
1359            ..Default::default()
1360        };
1361
1362        g.insert(&self.db_con).await?;
1363        Ok(())
1364    }
1365
1366    async fn get_users(&self) -> DbResult<Vec<User>> {
1367        let users = user::Entity::find()
1368            .order_by_asc(user::Column::Name)
1369            .all(&self.db_con)
1370            .await?;
1371
1372        Ok(users.into_iter().map(User::from).collect())
1373    }
1374
1375    async fn get_groups(&self) -> DbResult<Vec<Group>> {
1376        let groups = group::Entity::find()
1377            .order_by_asc(group::Column::Name)
1378            .all(&self.db_con)
1379            .await?;
1380
1381        Ok(groups.into_iter().map(Group::from).collect())
1382    }
1383
1384    async fn get_total_unique_crates(&self) -> DbResult<u64> {
1385        self.count(
1386            CrateIden::Table,
1387            CrateIden::Id,
1388            DbError::FailedToCountCrates,
1389        )
1390        .await
1391    }
1392
1393    async fn get_total_crate_versions(&self) -> DbResult<u64> {
1394        self.count(
1395            CrateMetaIden::Table,
1396            CrateMetaIden::Id,
1397            DbError::FailedToCountCrateVersions,
1398        )
1399        .await
1400    }
1401
1402    async fn get_total_downloads(&self) -> DbResult<u64> {
1403        #[derive(FromQueryResult)]
1404        struct Model {
1405            total_downloads: i64,
1406        }
1407
1408        Ok(krate::Entity::find()
1409            .select_only()
1410            .column(krate::Column::TotalDownloads)
1411            .into_model::<Model>()
1412            .all(&self.db_con)
1413            .await?
1414            .into_iter()
1415            .map(|m| m.total_downloads as u64)
1416            .sum())
1417    }
1418
1419    async fn get_top_crates_downloads(
1420        &self,
1421        top: u64,
1422        visibility: &CrateVisibility,
1423    ) -> DbResult<Vec<(String, u64)>> {
1424        #[derive(Debug, PartialEq, FromQueryResult)]
1425        struct SelectResult {
1426            original_name: String,
1427            total_downloads: i64,
1428        }
1429
1430        let mut query = Query::select()
1431            .columns(vec![CrateIden::OriginalName, CrateIden::TotalDownloads])
1432            .from(CrateIden::Table)
1433            .order_by(CrateIden::TotalDownloads, Order::Desc)
1434            .limit(top)
1435            .to_owned();
1436        if let Some(condition) = Self::crate_visibility_condition(visibility) {
1437            query.cond_where(condition);
1438        }
1439        let stmt = self.db_con.get_database_backend().build(&query);
1440
1441        Ok(SelectResult::find_by_statement(stmt)
1442            .all(&self.db_con)
1443            .await?
1444            .into_iter()
1445            .map(|x| (x.original_name, x.total_downloads as u64))
1446            .collect())
1447    }
1448
1449    async fn get_crate_summaries(&self) -> DbResult<Vec<CrateSummary>> {
1450        let krates = krate::Entity::find()
1451            .order_by(krate::Column::Name, Order::Asc)
1452            .all(&self.db_con)
1453            .await?;
1454
1455        Ok(krates.into_iter().map(CrateSummary::from).collect())
1456    }
1457
1458    async fn add_doc_queue(
1459        &self,
1460        krate: &NormalizedName,
1461        version: &Version,
1462        path: &Path,
1463    ) -> DbResult<()> {
1464        let s = doc_queue::ActiveModel {
1465            krate: Set(krate.to_string()),
1466            version: Set(version.to_string()),
1467            // FIXME: Convert Path to String properly, handle errors
1468            path: Set(path.to_string_lossy().to_string()),
1469            ..Default::default()
1470        };
1471
1472        s.insert(&self.db_con).await?;
1473        Ok(())
1474    }
1475
1476    async fn delete_doc_queue(&self, id: i64) -> DbResult<()> {
1477        DocQueue::delete_by_id(id).exec(&self.db_con).await?;
1478        Ok(())
1479    }
1480
1481    async fn get_doc_queue(&self) -> DbResult<Vec<DocQueueEntry>> {
1482        let entities = DocQueue::find().all(&self.db_con).await?;
1483
1484        Ok(entities.into_iter().map(DocQueueEntry::from).collect())
1485    }
1486
1487    async fn delete_crate(&self, krate: &NormalizedName, version: &Version) -> DbResult<()> {
1488        let txn = self.db_con.begin().await?;
1489
1490        // Delete the entry from the "crate_meta" table
1491        let crate_meta_version = crate_meta::Entity::find()
1492            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1493            .filter(krate::Column::Name.eq(krate))
1494            .filter(crate_meta::Column::Version.eq(version))
1495            .one(&txn)
1496            .await?
1497            .ok_or_else(|| DbError::CrateMetaNotFound(krate.to_string(), version.to_string()))?;
1498        let crate_id = crate_meta_version.crate_fk;
1499        let current_max_version = operations::get_max_version_from_id(&txn, crate_id).await?;
1500        crate_meta_version.delete(&txn).await?;
1501
1502        // Delete the crate index entry from "crate_index" table
1503        let crate_index_version = crate_index::Entity::find()
1504            .join(JoinType::InnerJoin, crate_index::Relation::Krate.def())
1505            .filter(krate::Column::Name.eq(krate))
1506            .filter(crate_index::Column::Vers.eq(version))
1507            .one(&txn)
1508            .await?
1509            .ok_or_else(|| DbError::CrateIndexNotFound(krate.to_string(), version.to_string()))?;
1510        crate_index_version.delete(&txn).await?;
1511
1512        // If it was the last entry in the "crate_meta" table, delete the entry
1513        // in the "crate" table as well
1514        let crate_meta_rows = crate_meta::Entity::find()
1515            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1516            .filter(krate::Column::Name.eq(krate))
1517            .all(&txn)
1518            .await?;
1519
1520        if crate_meta_rows.is_empty() {
1521            krate::Entity::delete_many()
1522                .filter(krate::Column::Name.eq(krate))
1523                .exec(&txn)
1524                .await?;
1525        } else {
1526            let c = krate::Entity::find_by_id(crate_id)
1527                .one(&txn)
1528                .await?
1529                .ok_or(DbError::CrateNotFoundWithId(crate_id))?;
1530            let mut c: krate::ActiveModel = c.into();
1531
1532            // Update the max. version if the deleted version was the max. version.
1533            if version == &current_max_version {
1534                let new_max_version = crate_meta_rows
1535                    .into_iter()
1536                    .map(|cm| Version::from_unchecked_str(&cm.version))
1537                    .max()
1538                    .unwrap(); // Safe to unwrap, as crate_meta_rows is not empty
1539                c.max_version = Set(new_max_version.into_inner());
1540            }
1541            // Update the ETag value of the crate index.
1542            let etag = operations::compute_etag(&txn, krate, crate_id).await?;
1543            c.e_tag = Set(etag);
1544            c.update(&txn).await?;
1545        }
1546
1547        txn.commit().await?;
1548
1549        Ok(())
1550    }
1551
1552    async fn get_crate_meta_list(&self, crate_name: &NormalizedName) -> DbResult<Vec<CrateMeta>> {
1553        let crate_meta = crate_meta::Entity::find()
1554            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1555            .filter(krate::Column::Name.eq(crate_name))
1556            .all(&self.db_con)
1557            .await?;
1558
1559        let crate_meta = crate_meta
1560            .into_iter()
1561            .map(|cm| CrateMeta {
1562                name: crate_name.to_string(),
1563                id: cm.id,
1564                version: cm.version,
1565                created: cm.created,
1566                downloads: cm.downloads,
1567                crate_fk: cm.crate_fk,
1568            })
1569            .collect();
1570
1571        Ok(crate_meta)
1572    }
1573
1574    async fn update_last_updated(&self, id: i64, last_updated: &DateTime<Utc>) -> DbResult<()> {
1575        let krate = krate::Entity::find_by_id(id)
1576            .one(&self.db_con)
1577            .await?
1578            .ok_or(DbError::CrateNotFoundWithId(id))?;
1579
1580        let date = last_updated.format(DB_DATE_FORMAT).to_string();
1581
1582        let mut krate: krate::ActiveModel = krate.into();
1583        krate.last_updated = Set(date);
1584        krate.update(&self.db_con).await?;
1585
1586        Ok(())
1587    }
1588
1589    async fn search_in_crate_name_and_description(
1590        &self,
1591        contains: &str,
1592        limit: u64,
1593        offset: u64,
1594        cache: bool,
1595        visibility: &CrateVisibility,
1596    ) -> DbResult<Vec<CrateOverview>> {
1597        self.query_crates(Some(contains), Some((limit, offset)), cache, visibility)
1598            .await
1599    }
1600
1601    async fn count_by_crate_name_and_description(
1602        &self,
1603        contains: &str,
1604        cache: bool,
1605        visibility: &CrateVisibility,
1606    ) -> DbResult<u64> {
1607        self.count_crates(Some(contains), cache, visibility).await
1608    }
1609
1610    async fn get_crate_overview_list(
1611        &self,
1612        limit: u64,
1613        offset: u64,
1614        cache: bool,
1615        visibility: &CrateVisibility,
1616    ) -> DbResult<Vec<CrateOverview>> {
1617        self.query_crates(None, Some((limit, offset)), cache, visibility)
1618            .await
1619    }
1620
1621    async fn get_crate_data(&self, crate_name: &NormalizedName) -> DbResult<CrateData> {
1622        let krate = self.get_krate_model(crate_name).await?;
1623
1624        let owners: Vec<String> = krate
1625            .find_related(owner::Entity)
1626            .find_also_related(user::Entity)
1627            .all(&self.db_con)
1628            .await?
1629            .into_iter()
1630            .filter_map(|(_, v)| v.map(|v| v.name))
1631            .collect();
1632        let categories: Vec<String> = krate
1633            .find_related(crate_category_to_crate::Entity)
1634            .find_also_related(crate_category::Entity)
1635            .all(&self.db_con)
1636            .await?
1637            .into_iter()
1638            .filter_map(|(_, v)| v.map(|v| v.category))
1639            .collect();
1640        let keywords: Vec<String> = krate
1641            .find_related(crate_keyword_to_crate::Entity)
1642            .find_also_related(crate_keyword::Entity)
1643            .all(&self.db_con)
1644            .await?
1645            .into_iter()
1646            .filter_map(|(_, v)| v.map(|v| v.keyword))
1647            .collect();
1648        let authors: Vec<String> = krate
1649            .find_related(crate_author_to_crate::Entity)
1650            .find_also_related(crate_author::Entity)
1651            .all(&self.db_con)
1652            .await?
1653            .into_iter()
1654            .filter_map(|(_, v)| v.map(|v| v.author))
1655            .collect();
1656        let crate_metas = krate
1657            .find_related(crate_meta::Entity)
1658            .all(&self.db_con)
1659            .await?;
1660        let crate_indices = krate
1661            .find_related(crate_index::Entity)
1662            .all(&self.db_con)
1663            .await?;
1664
1665        let mut versions = Vec::new();
1666        for cm in crate_metas {
1667            let ci = crate_indices
1668                .iter()
1669                .find(|ci| ci.vers == cm.version)
1670                .ok_or_else(|| {
1671                    DbError::CrateIndexNotFound(krate.name.clone(), cm.version.clone())
1672                })?;
1673            let dependencies: Vec<CrateRegistryDep> = match ci.deps.clone() {
1674                Some(deps) => {
1675                    let ix = serde_json::from_value::<Vec<IndexDep>>(deps)
1676                        .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
1677
1678                    let mut ft = Vec::new();
1679
1680                    let dep_descs = operations::get_desc_for_deps(&self.db_con, ix.iter()).await?;
1681
1682                    for dep in &ix {
1683                        ft.push(CrateRegistryDep::from_index(
1684                            dep_descs.get(&dep.name).cloned(),
1685                            dep.clone(),
1686                        ));
1687                    }
1688
1689                    ft
1690                }
1691                None => Vec::default(),
1692            };
1693            let features: BTreeMap<String, Vec<String>> = match ci.features.clone() {
1694                Some(features) => serde_json::from_value::<BTreeMap<String, Vec<String>>>(features)
1695                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?,
1696                None => BTreeMap::default(),
1697            };
1698
1699            versions.push(CrateVersionData {
1700                version: cm.version,
1701                created: cm.created,
1702                downloads: cm.downloads,
1703                readme: cm.readme,
1704                license: cm.license,
1705                license_file: cm.license_file,
1706                documentation: cm.documentation,
1707                dependencies,
1708                checksum: ci.cksum.clone(),
1709                features,
1710                yanked: ci.yanked,
1711                links: ci.links.clone(),
1712                v: ci.v,
1713            });
1714        }
1715        versions.sort_by(|a, b| {
1716            Version::from_unchecked_str(&b.version).cmp(&Version::from_unchecked_str(&a.version))
1717        });
1718
1719        let crate_data = CrateData {
1720            name: krate.original_name,
1721            owners,
1722            max_version: krate.max_version,
1723            total_downloads: krate.total_downloads,
1724            last_updated: krate.last_updated,
1725            homepage: krate.homepage,
1726            description: krate.description,
1727            repository: krate.repository,
1728            categories,
1729            keywords,
1730            authors,
1731            versions,
1732        };
1733
1734        Ok(crate_data)
1735    }
1736
1737    async fn add_empty_crate(&self, name: &str, created: &DateTime<Utc>) -> DbResult<i64> {
1738        let created = created.format(DB_DATE_FORMAT).to_string();
1739        let normalized_name = NormalizedName::from(
1740            OriginalName::try_from(name)
1741                .map_err(|_| DbError::InvalidCrateName(name.to_string()))?,
1742        );
1743        operations::ensure_no_similar_crate(&self.db_con, &normalized_name).await?;
1744        let krate = krate::ActiveModel {
1745            id: ActiveValue::default(),
1746            name: Set(normalized_name.to_string()),
1747            original_name: Set(name.to_string()),
1748            max_version: Set("0.0.0".to_string()),
1749            last_updated: Set(created),
1750            total_downloads: Set(0),
1751            homepage: Set(None),
1752            description: Set(None),
1753            repository: Set(None),
1754            e_tag: Set(String::new()), // Set to empty string, as it can be computed, when the crate index is inserted
1755            restricted_download: Set(false),
1756        };
1757        Ok(krate.insert(&self.db_con).await?.id)
1758    }
1759
1760    async fn add_crate(
1761        &self,
1762        pub_metadata: &PublishMetadata,
1763        cksum: &str,
1764        created: &DateTime<Utc>,
1765        owner: &str,
1766    ) -> DbResult<i64> {
1767        let created = created.format(DB_DATE_FORMAT).to_string();
1768        let normalized_name = NormalizedName::from(
1769            OriginalName::try_from(&pub_metadata.name)
1770                .map_err(|_| DbError::InvalidCrateName(pub_metadata.name.clone()))?,
1771        );
1772
1773        // Look up the crate inside the transaction and by its normalized name, so
1774        // a concurrent first publish of the same crate cannot slip in between.
1775        let txn = self.db_con.begin().await?;
1776        let existing = krate::Entity::find()
1777            .filter(krate::Column::Name.eq(&normalized_name))
1778            .one(&txn)
1779            .await?;
1780
1781        let crate_id = if let Some(krate) = existing {
1782            let krate_id = krate.id;
1783            operations::ensure_owner_or_admin(&txn, owner, &normalized_name, krate_id).await?;
1784            let max_version = max(
1785                parse_db_version(&krate.max_version)?,
1786                parse_db_version(&pub_metadata.vers)?,
1787            );
1788
1789            let mut krate: krate::ActiveModel = krate.into();
1790            krate.last_updated = Set(created.clone());
1791            krate.max_version = Set(max_version.into_inner());
1792            krate.homepage = Set(pub_metadata.homepage.clone());
1793            krate.description = Set(pub_metadata.description.clone());
1794            krate.repository = Set(pub_metadata.repository.clone());
1795            krate.e_tag = Set(String::new()); // Set to empty string, as it can be computed, when the crate index is inserted
1796            krate.update(&txn).await?;
1797            krate_id
1798        } else {
1799            operations::ensure_no_similar_crate(&txn, &normalized_name).await?;
1800            let krate = krate::ActiveModel {
1801                id: ActiveValue::default(),
1802                name: Set(normalized_name.to_string()),
1803                original_name: Set(pub_metadata.name.clone()),
1804                max_version: Set(pub_metadata.vers.clone()),
1805                last_updated: Set(created.clone()),
1806                total_downloads: Set(0),
1807                homepage: Set(pub_metadata.homepage.clone()),
1808                description: Set(pub_metadata.description.clone()),
1809                repository: Set(pub_metadata.repository.clone()),
1810                e_tag: Set(String::new()), // Set to empty string, as it can be computed, when the crate index is inserted
1811                restricted_download: Set(false),
1812            };
1813            let krate = krate.insert(&txn).await?;
1814            krate.id
1815        };
1816
1817        operations::add_owner_if_not_exists(&txn, owner, crate_id).await?;
1818        operations::add_crate_metadata(&txn, pub_metadata, &created, crate_id).await?;
1819        operations::add_crate_index(&txn, pub_metadata, cksum, crate_id).await?;
1820        operations::update_etag(&txn, &pub_metadata.name, crate_id).await?;
1821        operations::update_crate_categories(&txn, pub_metadata, crate_id).await?;
1822        operations::update_crate_keywords(&txn, pub_metadata, crate_id).await?;
1823        operations::update_crate_authors(&txn, pub_metadata, crate_id).await?;
1824
1825        txn.commit().await?;
1826        Ok(crate_id)
1827    }
1828
1829    async fn update_docs_link(
1830        &self,
1831        crate_name: &NormalizedName,
1832        version: &Version,
1833        docs_link: &str,
1834    ) -> DbResult<()> {
1835        let (cm, _c) = crate_meta::Entity::find()
1836            .find_also_related(krate::Entity)
1837            .filter(
1838                Cond::all()
1839                    .add(krate::Column::Name.eq(crate_name))
1840                    .add(crate_meta::Column::Version.eq(version)),
1841            )
1842            .one(&self.db_con)
1843            .await?
1844            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?;
1845
1846        let mut cm: crate_meta::ActiveModel = cm.into();
1847        cm.documentation = Set(Some(docs_link.to_string()));
1848        cm.update(&self.db_con).await?;
1849        Ok(())
1850    }
1851
1852    async fn add_crate_metadata(
1853        &self,
1854        pub_metadata: &PublishMetadata,
1855        created: &str,
1856        crate_id: i64,
1857    ) -> DbResult<()> {
1858        operations::add_crate_metadata(&self.db_con, pub_metadata, created, crate_id).await
1859    }
1860
1861    async fn get_prefetch_data(&self, crate_name: &str) -> DbResult<Prefetch> {
1862        let mut krate = krate::Entity::find()
1863            .filter(krate::Column::Name.eq(crate_name))
1864            .find_with_related(crate_index::Entity)
1865            .all(&self.db_con)
1866            .await?
1867            .into_iter();
1868
1869        // Expecting only one crate with its related indices
1870        let (Some((krate, crate_indices)), None) = (krate.next(), krate.next()) else {
1871            return Err(DbError::CrateNotFound(crate_name.to_string()));
1872        };
1873
1874        let index_metadata =
1875            operations::crate_index_model_to_index_metadata(crate_name, crate_indices)?;
1876        let data = operations::index_metadata_to_bytes(&index_metadata)?;
1877
1878        Ok(Prefetch {
1879            data,
1880            etag: krate.e_tag,
1881            last_modified: krate.last_updated,
1882        })
1883    }
1884
1885    async fn is_cratesio_cache_up_to_date(
1886        &self,
1887        crate_name: &NormalizedName,
1888        etag: Option<String>,
1889        last_modified: Option<String>,
1890    ) -> DbResult<PrefetchState> {
1891        let Some(krate) = cratesio_crate::Entity::find()
1892            .filter(cratesio_crate::Column::Name.eq(crate_name))
1893            .one(&self.db_con)
1894            .await?
1895        else {
1896            return Ok(PrefetchState::NotFound);
1897        };
1898
1899        let needs_update = match (etag, last_modified) {
1900            (Some(etag), Some(last_modified)) => {
1901                krate.e_tag != etag || krate.last_modified != last_modified
1902            }
1903            (Some(etag), None) => krate.e_tag != etag,
1904            (None, Some(last_modified)) => krate.last_modified != last_modified,
1905            (None, None) => true,
1906        };
1907
1908        if !needs_update {
1909            Ok(PrefetchState::UpToDate {
1910                etag: krate.e_tag,
1911                last_modified: krate.last_modified,
1912            })
1913        } else {
1914            let crate_indices = krate
1915                .find_related(cratesio_index::Entity)
1916                .all(&self.db_con)
1917                .await?;
1918            let index_metadata =
1919                operations::cratesio_index_model_to_index_metadata(crate_name, crate_indices)?;
1920            let data = operations::index_metadata_to_bytes(&index_metadata)?;
1921
1922            Ok(PrefetchState::NeedsUpdate(Prefetch {
1923                data,
1924                etag: krate.e_tag.clone(),
1925                last_modified: krate.last_modified,
1926            }))
1927        }
1928    }
1929
1930    async fn add_cratesio_prefetch_data(
1931        &self,
1932        crate_name: &OriginalName,
1933        etag: &str,
1934        last_modified: &str,
1935        description: Option<String>,
1936        indices: &[IndexMetadata],
1937    ) -> DbResult<Prefetch> {
1938        let normalized_name = crate_name.to_normalized();
1939
1940        let max_version = indices
1941            .iter()
1942            .map(|i| Version::from_unchecked_str(&i.vers))
1943            .max()
1944            .ok_or_else(|| DbError::FailedToGetMaxVersionByName(crate_name.to_string()))?;
1945
1946        // Use a transaction to ensure the etag update and all index inserts are
1947        // atomic. Without this, a partial failure would leave the crate record
1948        // with the latest etag but incomplete index data, causing stale cache
1949        // entries that can never self-heal (the background updater would get 304
1950        // from crates.io because the etag matches).
1951        let txn = self.db_con.begin().await?;
1952
1953        let krate = cratesio_crate::Entity::find()
1954            .filter(cratesio_crate::Column::Name.eq(&normalized_name))
1955            .one(&txn)
1956            .await?;
1957
1958        let krate = if let Some(krate) = krate {
1959            let mut krate: cratesio_crate::ActiveModel = krate.into();
1960            krate.e_tag = Set(etag.to_string());
1961            krate.last_modified = Set(last_modified.to_string());
1962            krate.max_version = Set(max_version.into_inner());
1963            krate.update(&txn).await?
1964        } else {
1965            let krate = cratesio_crate::ActiveModel {
1966                id: ActiveValue::default(),
1967                name: Set(normalized_name.to_string()),
1968                original_name: Set(crate_name.to_string()),
1969                description: Set(description),
1970                e_tag: Set(etag.to_string()),
1971                last_modified: Set(last_modified.to_string()),
1972                total_downloads: Set(0),
1973                max_version: Set(max_version.to_string()),
1974            };
1975            krate.insert(&txn).await?
1976        };
1977
1978        let current_indices = cratesio_index::Entity::find()
1979            .filter(cratesio_index::Column::CratesIoFk.eq(krate.id))
1980            .all(&txn)
1981            .await?;
1982
1983        for index in indices {
1984            // Refresh the mutable parts of an already cached version. "yanked"
1985            // can flip upstream at any time. "rust_version" is refreshed too
1986            // because caches written before the column existed hold NULL for
1987            // every row, and only a refresh can heal them.
1988            if let Some(current_index) = current_indices.iter().find(|ci| index.vers == ci.vers) {
1989                let mut ci: cratesio_index::ActiveModel = current_index.to_owned().into();
1990                let mut changed = false;
1991
1992                if index.yanked != current_index.yanked {
1993                    ci.yanked = Set(index.yanked);
1994                    changed = true;
1995                }
1996
1997                if index.rust_version != current_index.rust_version {
1998                    ci.rust_version = Set(index.rust_version.clone());
1999                    changed = true;
2000                }
2001
2002                if changed {
2003                    ci.update(&txn).await?;
2004                }
2005            } else {
2006                let deps = if index.deps.is_empty() {
2007                    None
2008                } else {
2009                    let deps = serde_json::to_value(&index.deps)
2010                        .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
2011                    Some(deps)
2012                };
2013
2014                let features = serde_json::to_value(&index.features)
2015                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
2016
2017                let features2 = serde_json::to_value(&index.features2)
2018                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
2019
2020                let new_index = cratesio_index::ActiveModel {
2021                    id: ActiveValue::default(),
2022                    name: Set(index.name.clone()),
2023                    vers: Set(index.vers.clone()),
2024                    deps: Set(deps),
2025                    cksum: Set(index.cksum.clone()),
2026                    features: Set(Some(features)),
2027                    features2: Set(Some(features2)),
2028                    yanked: Set(index.yanked),
2029                    links: Set(index.links.clone()),
2030                    pubtime: Set(index.pubtime.map(|dt| dt.naive_utc())),
2031                    v: Set(index.v.unwrap_or(1) as i32),
2032                    crates_io_fk: Set(krate.id),
2033                    rust_version: Set(index.rust_version.clone()),
2034                };
2035
2036                new_index.insert(&txn).await?;
2037
2038                // Add the meta data for the crate version.
2039                let meta = cratesio_meta::ActiveModel {
2040                    id: ActiveValue::default(),
2041                    version: Set(index.vers.clone()),
2042                    downloads: Set(0),
2043                    crates_io_fk: Set(krate.id),
2044                    documentation: Set(Some(format!(
2045                        "https://docs.rs/{normalized_name}/{}",
2046                        index.vers,
2047                    ))),
2048                };
2049
2050                meta.insert(&txn).await?;
2051            }
2052        }
2053
2054        txn.commit().await?;
2055
2056        Ok(Prefetch {
2057            data: operations::index_metadata_to_bytes(indices)?,
2058            etag: etag.to_string(),
2059            last_modified: last_modified.to_string(),
2060        })
2061    }
2062
2063    async fn get_cratesio_index_update_list(&self) -> DbResult<Vec<CratesioPrefetchMsg>> {
2064        let crates = cratesio_crate::Entity::find().all(&self.db_con).await?;
2065        let msgs = crates
2066            .into_iter()
2067            .map(|krate| {
2068                CratesioPrefetchMsg::Update(UpdateData {
2069                    name: OriginalName::from_unchecked(krate.original_name),
2070                    etag: Some(krate.e_tag),
2071                    last_modified: Some(krate.last_modified),
2072                })
2073            })
2074            .collect();
2075        Ok(msgs)
2076    }
2077
2078    async fn get_cratesio_cksum(
2079        &self,
2080        crate_name: &NormalizedName,
2081        version: &Version,
2082    ) -> DbResult<Option<String>> {
2083        let index = cratesio_index::Entity::find()
2084            .inner_join(cratesio_crate::Entity)
2085            .filter(cratesio_crate::Column::Name.eq(crate_name))
2086            .filter(cratesio_index::Column::Vers.eq(version))
2087            .one(&self.db_con)
2088            .await?;
2089        Ok(index.map(|i| i.cksum))
2090    }
2091
2092    async fn unyank_crate(&self, crate_name: &NormalizedName, version: &Version) -> DbResult<()> {
2093        let mut ci: crate_index::ActiveModel = self
2094            .get_crate_index_model(crate_name, version)
2095            .await?
2096            .into();
2097        ci.yanked = Set(false);
2098        ci.save(&self.db_con).await?;
2099        Ok(())
2100    }
2101
2102    async fn yank_crate(&self, crate_name: &NormalizedName, version: &Version) -> DbResult<()> {
2103        let mut ci: crate_index::ActiveModel = self
2104            .get_crate_index_model(crate_name, version)
2105            .await?
2106            .into();
2107        ci.yanked = Set(true);
2108        ci.save(&self.db_con).await?;
2109        Ok(())
2110    }
2111
2112    async fn register_webhook(&self, webhook: Webhook) -> DbResult<String> {
2113        let w = webhook::ActiveModel {
2114            event: Set(Into::<&str>::into(webhook.event).to_string()),
2115            callback_url: Set(webhook.callback_url),
2116            name: Set(webhook.name),
2117            secret: Set(webhook.secret),
2118            ..Default::default()
2119        };
2120
2121        let w: webhook::Model = w.insert(&self.db_con).await?;
2122        Ok(w.id.to_string())
2123    }
2124
2125    async fn delete_webhook(&self, id: &str) -> DbResult<()> {
2126        self.get_webhook_model(id)
2127            .await?
2128            .delete(&self.db_con)
2129            .await?;
2130        Ok(())
2131    }
2132
2133    async fn get_webhook(&self, id: &str) -> DbResult<Webhook> {
2134        webhook_model_to_obj(self.get_webhook_model(id).await?)
2135    }
2136
2137    async fn get_all_webhooks(&self) -> DbResult<Vec<Webhook>> {
2138        Ok(webhook::Entity::find()
2139            .all(&self.db_con)
2140            .await?
2141            .into_iter()
2142            .filter_map(|w| webhook_model_to_obj(w).ok())
2143            .collect())
2144    }
2145
2146    async fn add_webhook_queue(
2147        &self,
2148        event: WebhookEvent,
2149        payload: serde_json::Value,
2150    ) -> DbResult<()> {
2151        let w = webhook::Entity::find()
2152            .filter(webhook::Column::Event.eq(Into::<&str>::into(event)))
2153            .all(&self.db_con)
2154            .await?;
2155
2156        if w.is_empty() {
2157            return Ok(());
2158        }
2159
2160        let now = Utc::now();
2161
2162        let entries = w.into_iter().map(|w| webhook_queue::ActiveModel {
2163            webhook_fk: Set(w.id),
2164            payload: Set(payload.clone()),
2165            next_attempt: Set(now.into()),
2166            last_attempt: Set(None),
2167            ..Default::default()
2168        });
2169
2170        webhook_queue::Entity::insert_many(entries)
2171            .exec(&self.db_con)
2172            .await?;
2173        Ok(())
2174    }
2175    async fn get_pending_webhook_queue_entries(
2176        &self,
2177        timestamp: DateTime<Utc>,
2178    ) -> DbResult<Vec<WebhookQueue>> {
2179        let w = webhook_queue::Entity::find()
2180            .find_with_related(webhook::Entity)
2181            .filter(webhook_queue::Column::NextAttempt.lte(timestamp))
2182            .all(&self.db_con)
2183            .await?;
2184
2185        Ok(w.into_iter()
2186            .filter_map(|w| {
2187                let webhook = w.1.first()?;
2188                Some(WebhookQueue {
2189                    id: w.0.id.to_string(),
2190                    callback_url: webhook.callback_url.clone(),
2191                    payload: w.0.payload,
2192                    last_attempt: w.0.last_attempt.map(Into::into),
2193                    next_attempt: w.0.next_attempt.into(),
2194                    secret: webhook.secret.clone(),
2195                })
2196            })
2197            .collect())
2198    }
2199
2200    async fn update_webhook_queue(
2201        &self,
2202        id: &str,
2203        last_attempt: DateTime<Utc>,
2204        next_attempt: DateTime<Utc>,
2205    ) -> DbResult<()> {
2206        let mut w: webhook_queue::ActiveModel = self.get_webhook_queue_model(id).await?.into();
2207        w.last_attempt = Set(Some(last_attempt.into()));
2208        w.next_attempt = Set(next_attempt.into());
2209        w.update(&self.db_con).await?;
2210        Ok(())
2211    }
2212
2213    async fn delete_webhook_queue(&self, id: &str) -> DbResult<()> {
2214        self.get_webhook_queue_model(id)
2215            .await?
2216            .delete(&self.db_con)
2217            .await?;
2218        Ok(())
2219    }
2220
2221    // OAuth2 identity methods
2222
2223    async fn get_user_by_oauth2_identity(
2224        &self,
2225        issuer: &str,
2226        subject: &str,
2227    ) -> DbResult<Option<User>> {
2228        let identity = oauth2_identity::Entity::find()
2229            .filter(oauth2_identity::Column::ProviderIssuer.eq(issuer))
2230            .filter(oauth2_identity::Column::Subject.eq(subject))
2231            .one(&self.db_con)
2232            .await?;
2233
2234        if let Some(identity) = identity {
2235            let u = user::Entity::find_by_id(identity.user_fk)
2236                .one(&self.db_con)
2237                .await?
2238                .ok_or_else(|| DbError::UserNotFound(format!("user_id={}", identity.user_fk)))?;
2239
2240            Ok(Some(User::from(u)))
2241        } else {
2242            Ok(None)
2243        }
2244    }
2245
2246    async fn has_oauth2_admin(&self) -> DbResult<bool> {
2247        let found = user::Entity::find()
2248            .filter(user::Column::IsAdmin.eq(true))
2249            .inner_join(oauth2_identity::Entity)
2250            .one(&self.db_con)
2251            .await?;
2252
2253        Ok(found.is_some())
2254    }
2255
2256    async fn create_oauth2_user(
2257        &self,
2258        username: &str,
2259        issuer: &str,
2260        subject: &str,
2261        email: Option<String>,
2262        is_admin: bool,
2263        is_read_only: bool,
2264    ) -> DbResult<User> {
2265        // OAuth2 users do not use password auth, but the column needs a
2266        // valid hash. Hash before opening the transaction to keep it short.
2267        let random_pwd = Uuid::new_v4().to_string();
2268        let hashed_pwd = hash_pwd(&random_pwd).await;
2269        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2270
2271        // Use a transaction to ensure atomicity
2272        let txn = self.db_con.begin().await?;
2273
2274        // Create the user
2275        let new_user = user::ActiveModel {
2276            name: Set(username.to_string()),
2277            pwd: Set(hashed_pwd),
2278            salt: Set(String::new()),
2279            is_admin: Set(is_admin),
2280            is_read_only: Set(is_read_only),
2281            created: Set(created.clone()),
2282            ..Default::default()
2283        };
2284
2285        let res = user::Entity::insert(new_user).exec(&txn).await?;
2286        let user_id = res.last_insert_id;
2287
2288        // Link the OAuth2 identity
2289        let identity = oauth2_identity::ActiveModel {
2290            user_fk: Set(user_id),
2291            provider_issuer: Set(issuer.to_string()),
2292            subject: Set(subject.to_string()),
2293            email: Set(email),
2294            created: Set(created.clone()),
2295            ..Default::default()
2296        };
2297
2298        oauth2_identity::Entity::insert(identity).exec(&txn).await?;
2299
2300        txn.commit().await?;
2301
2302        Ok(User {
2303            id: user_id as i32,
2304            name: username.to_string(),
2305            pwd: String::new(),  // Don't expose password hash
2306            salt: String::new(), // Don't expose salt
2307            is_admin,
2308            is_read_only,
2309            created,
2310        })
2311    }
2312
2313    async fn link_oauth2_identity(
2314        &self,
2315        user_id: i64,
2316        issuer: &str,
2317        subject: &str,
2318        email: Option<String>,
2319    ) -> DbResult<()> {
2320        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2321        let identity = oauth2_identity::ActiveModel {
2322            user_fk: Set(user_id),
2323            provider_issuer: Set(issuer.to_string()),
2324            subject: Set(subject.to_string()),
2325            email: Set(email),
2326            created: Set(created),
2327            ..Default::default()
2328        };
2329
2330        oauth2_identity::Entity::insert(identity)
2331            .exec(&self.db_con)
2332            .await?;
2333
2334        Ok(())
2335    }
2336
2337    // OAuth2 state methods (CSRF/PKCE during auth flow)
2338
2339    async fn store_oauth2_state(
2340        &self,
2341        state: &str,
2342        pkce_verifier: &str,
2343        nonce: &str,
2344    ) -> DbResult<()> {
2345        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2346        let s = oauth2_state::ActiveModel {
2347            state: Set(state.to_string()),
2348            pkce_verifier: Set(pkce_verifier.to_string()),
2349            nonce: Set(nonce.to_string()),
2350            created: Set(created),
2351            ..Default::default()
2352        };
2353
2354        oauth2_state::Entity::insert(s).exec(&self.db_con).await?;
2355        Ok(())
2356    }
2357
2358    async fn get_and_delete_oauth2_state(&self, state: &str) -> DbResult<Option<OAuth2StateData>> {
2359        let Some(s) = oauth2_state::Entity::find()
2360            .filter(oauth2_state::Column::State.eq(state))
2361            .one(&self.db_con)
2362            .await?
2363        else {
2364            return Ok(None);
2365        };
2366
2367        // The state is single use. Only the request whose delete actually
2368        // removed the row may continue, so two concurrent callbacks with the
2369        // same state cannot both succeed.
2370        let deleted = oauth2_state::Entity::delete_many()
2371            .filter(oauth2_state::Column::Id.eq(s.id))
2372            .exec(&self.db_con)
2373            .await?
2374            .rows_affected;
2375        if deleted == 0 || s.created < oauth2_state_expiry_cutoff() {
2376            return Ok(None);
2377        }
2378
2379        Ok(Some(OAuth2StateData {
2380            state: s.state,
2381            pkce_verifier: s.pkce_verifier,
2382            nonce: s.nonce,
2383        }))
2384    }
2385
2386    async fn cleanup_expired_oauth2_states(&self) -> DbResult<u64> {
2387        let result = oauth2_state::Entity::delete_many()
2388            .filter(oauth2_state::Column::Created.lt(oauth2_state_expiry_cutoff()))
2389            .exec(&self.db_con)
2390            .await?;
2391
2392        Ok(result.rows_affected)
2393    }
2394
2395    async fn is_username_available(&self, username: &str) -> DbResult<bool> {
2396        let existing = user::Entity::find()
2397            .filter(user::Column::Name.eq(username))
2398            .one(&self.db_con)
2399            .await?;
2400
2401        Ok(existing.is_none())
2402    }
2403
2404    // Toolchain distribution methods
2405
2406    async fn add_toolchain(
2407        &self,
2408        name: &str,
2409        version: &str,
2410        date: &str,
2411        channel: Option<String>,
2412    ) -> DbResult<i64> {
2413        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2414
2415        let model = toolchain::ActiveModel {
2416            id: ActiveValue::NotSet,
2417            name: Set(name.to_string()),
2418            version: Set(version.to_string()),
2419            date: Set(date.to_string()),
2420            channel: Set(channel),
2421            created: Set(created),
2422        };
2423
2424        let result = model.insert(&self.db_con).await?;
2425        Ok(result.id)
2426    }
2427
2428    async fn add_toolchain_target(
2429        &self,
2430        toolchain_id: i64,
2431        target: &str,
2432        storage_path: &str,
2433        hash: &str,
2434        size: i64,
2435    ) -> DbResult<i64> {
2436        let model = toolchain_target::ActiveModel {
2437            id: ActiveValue::NotSet,
2438            toolchain_fk: Set(toolchain_id),
2439            target: Set(target.to_string()),
2440            storage_path: Set(storage_path.to_string()),
2441            hash: Set(hash.to_string()),
2442            size: Set(size),
2443            status: Set("processing".to_string()),
2444        };
2445
2446        let result = model.insert(&self.db_con).await?;
2447        Ok(result.id)
2448    }
2449
2450    async fn set_target_status(&self, target_id: i64, status: &str) -> DbResult<()> {
2451        toolchain_target::Entity::update_many()
2452            .col_expr(toolchain_target::Column::Status, Expr::value(status))
2453            .filter(toolchain_target::Column::Id.eq(target_id))
2454            .exec(&self.db_con)
2455            .await?;
2456        Ok(())
2457    }
2458
2459    async fn add_toolchain_component(
2460        &self,
2461        target_id: i64,
2462        name: &str,
2463        storage_path: &str,
2464        hash: &str,
2465        size: i64,
2466    ) -> DbResult<()> {
2467        let model = toolchain_component::ActiveModel {
2468            id: ActiveValue::NotSet,
2469            toolchain_target_fk: Set(target_id),
2470            name: Set(name.to_string()),
2471            storage_path: Set(storage_path.to_string()),
2472            hash: Set(hash.to_string()),
2473            size: Set(size),
2474        };
2475
2476        model.insert(&self.db_con).await?;
2477        Ok(())
2478    }
2479
2480    async fn get_toolchain_by_channel(
2481        &self,
2482        channel: &str,
2483    ) -> DbResult<Option<ToolchainWithTargets>> {
2484        let tc = toolchain::Entity::find()
2485            .filter(toolchain::Column::Channel.eq(channel))
2486            .one(&self.db_con)
2487            .await?;
2488
2489        Ok(match tc {
2490            Some(tc) => Some(self.toolchain_with_targets(tc).await?),
2491            None => None,
2492        })
2493    }
2494
2495    async fn get_toolchain_by_version(
2496        &self,
2497        name: &str,
2498        version: &str,
2499    ) -> DbResult<Option<ToolchainWithTargets>> {
2500        Ok(
2501            match self.get_toolchain_by_name_version(name, version).await? {
2502                Some(tc) => Some(self.toolchain_with_targets(tc).await?),
2503                None => None,
2504            },
2505        )
2506    }
2507
2508    async fn list_toolchains(&self) -> DbResult<Vec<ToolchainWithTargets>> {
2509        let toolchains = toolchain::Entity::find()
2510            .order_by_desc(toolchain::Column::Created)
2511            .all(&self.db_con)
2512            .await?;
2513
2514        let mut result = Vec::with_capacity(toolchains.len());
2515
2516        for tc in toolchains {
2517            result.push(self.toolchain_with_targets(tc).await?);
2518        }
2519
2520        Ok(result)
2521    }
2522
2523    async fn delete_toolchain(&self, name: &str, version: &str) -> DbResult<()> {
2524        toolchain::Entity::delete_many()
2525            .filter(toolchain::Column::Name.eq(name))
2526            .filter(toolchain::Column::Version.eq(version))
2527            .exec(&self.db_con)
2528            .await?;
2529
2530        Ok(())
2531    }
2532
2533    async fn delete_toolchain_target(
2534        &self,
2535        name: &str,
2536        version: &str,
2537        target: &str,
2538    ) -> DbResult<()> {
2539        if let Some(tc) = self.get_toolchain_by_name_version(name, version).await? {
2540            toolchain_target::Entity::delete_many()
2541                .filter(toolchain_target::Column::ToolchainFk.eq(tc.id))
2542                .filter(toolchain_target::Column::Target.eq(target))
2543                .exec(&self.db_con)
2544                .await?;
2545        }
2546
2547        Ok(())
2548    }
2549
2550    async fn set_channel(&self, channel: &str, name: &str, version: &str) -> DbResult<()> {
2551        // First, clear the channel from any other toolchain that has it
2552        let toolchains_with_channel = toolchain::Entity::find()
2553            .filter(toolchain::Column::Channel.eq(channel))
2554            .all(&self.db_con)
2555            .await?;
2556
2557        for tc in toolchains_with_channel {
2558            let mut model: toolchain::ActiveModel = tc.into();
2559            model.channel = Set(None);
2560            model.update(&self.db_con).await?;
2561        }
2562
2563        // Then set the channel on the target toolchain
2564        if let Some(tc) = self.get_toolchain_by_name_version(name, version).await? {
2565            let mut model: toolchain::ActiveModel = tc.into();
2566            model.channel = Set(Some(channel.to_string()));
2567            model.update(&self.db_con).await?;
2568        }
2569
2570        Ok(())
2571    }
2572
2573    async fn get_channels(&self) -> DbResult<Vec<ChannelInfo>> {
2574        let toolchains = toolchain::Entity::find()
2575            .filter(toolchain::Column::Channel.is_not_null())
2576            .all(&self.db_con)
2577            .await?;
2578
2579        Ok(toolchains
2580            .into_iter()
2581            .filter_map(|tc| {
2582                tc.channel.map(|channel| ChannelInfo {
2583                    name: channel,
2584                    version: tc.version,
2585                    date: tc.date,
2586                })
2587            })
2588            .collect())
2589    }
2590}
2591
2592fn parse_db_version(value: &str) -> DbResult<Version> {
2593    Version::try_from(value).map_err(|_| DbError::InvalidVersion(value.to_owned()))
2594}
2595
2596#[cfg(test)]
2597mod like_escape_tests {
2598    use super::escape_like_pattern;
2599
2600    #[test]
2601    fn escapes_wildcards_and_backslash() {
2602        assert_eq!(escape_like_pattern("a%b_c"), "a\\%b\\_c");
2603        assert_eq!(escape_like_pattern("100%"), "100\\%");
2604        // Backslash is escaped first so it doesn't double-escape the wildcards.
2605        assert_eq!(escape_like_pattern("a\\b"), "a\\\\b");
2606    }
2607
2608    #[test]
2609    fn leaves_plain_text_untouched() {
2610        assert_eq!(escape_like_pattern("serde-json"), "serde-json");
2611    }
2612}
2613
2614#[cfg(test)]
2615mod session_age_tests {
2616    use super::{MAX_SESSION_AGE, session_age_to_duration};
2617
2618    #[test]
2619    fn zero_means_never_expire() {
2620        assert_eq!(session_age_to_duration(std::time::Duration::ZERO), None);
2621    }
2622
2623    #[test]
2624    fn positive_age_is_preserved() {
2625        assert_eq!(
2626            session_age_to_duration(std::time::Duration::from_hours(1)),
2627            Some(chrono::Duration::seconds(3600))
2628        );
2629    }
2630
2631    #[test]
2632    fn absurd_age_is_clamped_without_panicking() {
2633        // u64::MAX seconds would overflow chrono::Duration if not clamped.
2634        let resolved = session_age_to_duration(std::time::Duration::from_secs(u64::MAX))
2635            .expect("non-zero age yields Some");
2636        assert_eq!(
2637            resolved,
2638            chrono::Duration::from_std(MAX_SESSION_AGE).unwrap()
2639        );
2640    }
2641}