Skip to main content

kellnr_db/database/
mod.rs

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