Files
Oxicloud/src/domain/repositories/user_repository.rs
T
Claude 8e55caa8a9 perf(round29): cache-serve borrow-probe, NC REPORT href buffer, auth per-req allocs, DB over-fetch
Seven behaviour-preserving allocation / copy / bandwidth cuts, each behind a
counting-allocator BEFORE/AFTER gate that exit(1)s unless AFTER allocates
strictly fewer than BEFORE (benches/ROUND29.md, examples/bench_round29_micro.rs).

- [B] Content-cache serve fast path (optimized_inner Tier 1 +
  get_file_range_preloaded — the video-scrub hot path): probe the cache with a
  borrow first and build the owned get_or_load args (quoted-etag / key / id
  Strings) only on a miss, instead of allocating them before every probe and
  discarding them on a hit. 6 -> 0 allocs per cache hit. Splits get_or_load into
  get + load_and_cache so the miss path is not re-probed and the hit/miss stat
  counters stay byte-identical. Also drops the unconditional content_hash/name
  clones that ran for the >=10 MB streaming tier that used neither.
- [A] NextCloud REPORT emit loops: per-row href String (and format! per folder
  row) -> one reused href_buf via nc_href_into / nc_collection_href_into with the
  URL-encoded user computed once per page. 1497 fewer allocs on a 500-row page.
- [C] read_full: a single-frame blob is returned zero-copy instead of a second
  whole-payload memcpy into a fresh BytesMut; multi-frame path unchanged.
- [D] login-lockout key: to_lowercase()+format! -> one pre-sized ASCII buffer
  (non-ASCII keeps str::to_lowercase). 3 -> 1 alloc/req, byte-identical key.
- [E] NC composite-username parse: owned clone/to_string -> &str borrow of the
  already-owned raw_username. 1 -> 0 alloc on the common no-marker path.
- [F] get_contacts_in_group: stop SELECTing the discarded multi-KB vcard column
  (the live method ROUND25 §Q2 missed; ContactDto has no vcard field).
- [G] count_admin_users: add count_users_by_role -> scalar COUNT(*) instead of
  hydrating every admin's full row (incl. up-to-512 KiB avatar + ui_preferences
  JSONB) only to .len() it, on a bootstrap-polled status endpoint.

All seven gates pass; cargo fmt --check and cargo clippy --all-features
--all-targets -D warnings clean. §F/§G additionally validated against a live
PostgreSQL 16 with the full migration set (query validity, result equivalence,
600000 -> 8 byte wire delta on the admin count).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhpDZxSQTAGnAqCHUdtG5N
2026-07-21 10:25:36 +00:00

152 lines
5.2 KiB
Rust

use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum UserRepositoryError {
#[error("User not found: {0}")]
NotFound(String),
#[error("User already exists: {0}")]
AlreadyExists(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Operation not allowed: {0}")]
OperationNotAllowed(String),
}
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
// Conversion from UserRepositoryError to DomainError
impl From<UserRepositoryError> for DomainError {
fn from(err: UserRepositoryError) -> Self {
match err {
UserRepositoryError::NotFound(msg) => DomainError::not_found("User", msg),
UserRepositoryError::AlreadyExists(msg) => DomainError::already_exists("User", msg),
UserRepositoryError::DatabaseError(msg) => DomainError::internal_error("Database", msg),
UserRepositoryError::ValidationError(msg) => DomainError::validation_error(msg),
UserRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
UserRepositoryError::OperationNotAllowed(msg) => {
DomainError::access_denied("User", msg)
}
}
}
}
pub trait UserRepository: Send + Sync + 'static {
/// Creates a new user
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User>;
/// Batch-loads a set of users by id, preserving no particular order
/// and silently skipping ids that don't match any row. Caller is
/// responsible for de-duplicating the input vec. Returns an empty
/// vec when given an empty input. Used by group-recipient expansion
/// in `RecipientNotificationService` to avoid N+1 queries.
async fn get_users_by_ids(&self, ids: Vec<Uuid>) -> UserRepositoryResult<Vec<User>>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
/// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
/// Updates an existing user
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
/// Updates only a user's storage usage
async fn update_storage_usage(
&self,
user_id: Uuid,
usage_bytes: i64,
) -> UserRepositoryResult<()>;
/// Updates the last login date
async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()>;
/// Lists users with pagination.
///
/// `include_external` controls whether external (grant-only) users
/// appear in the result. Default callers should pass `false` so
/// external users stay invisible to internal-user surfaces (system
/// address book autocomplete, sharee search, etc.). Only the admin
/// management UI should request `true`.
async fn list_users(
&self,
limit: i64,
offset: i64,
include_external: bool,
) -> UserRepositoryResult<Vec<User>>;
/// Searches users by username or email (SQL ILIKE) with a limit.
/// See [`list_users`] for the meaning of `include_external`.
async fn search_users(
&self,
query: &str,
limit: i64,
include_external: bool,
) -> UserRepositoryResult<Vec<User>>;
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: Uuid, active: bool)
-> UserRepositoryResult<()>;
/// Changes a user's password
async fn change_password(&self, user_id: Uuid, password_hash: &str)
-> UserRepositoryResult<()>;
/// Changes a user's role
async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()>;
/// Lists users by role (admin or user)
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Counts users with a given role via a scalar `COUNT(*)` — no row
/// hydration (benches/ROUND29.md §G).
async fn count_users_by_role(&self, role: &str) -> UserRepositoryResult<i64>;
/// Deletes a user
async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> UserRepositoryResult<User>;
/// Updates a user's storage quota
async fn update_storage_quota(
&self,
user_id: Uuid,
quota_bytes: i64,
) -> UserRepositoryResult<()>;
/// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64>;
/// Gets aggregated storage statistics
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
}
/// Aggregated storage statistics
#[derive(Debug, Clone)]
pub struct StorageStats {
pub total_users: i64,
pub active_users: i64,
pub total_quota_bytes: i64,
pub total_used_bytes: i64,
pub users_over_80_percent: i64,
pub users_over_quota: i64,
}