Skip to main content

kellnr_db/database/
mod.rs

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