feat(storage): wire choice of storage
This commit is contained in:
@@ -143,10 +143,19 @@ pub struct DashboardStatsDto {
|
||||
// Storage Settings DTOs (Admin Panel)
|
||||
// ============================================================================
|
||||
|
||||
/// Current storage settings returned to admin UI (secrets masked)
|
||||
/// Current storage settings returned to admin UI (secrets masked).
|
||||
///
|
||||
/// Post-multi-entry (`docs/plan/storage-multi-entry.md`) this carries
|
||||
/// two shapes side-by-side: the legacy flat storage-config fields
|
||||
/// (for the pre-multi-entry admin UI, until slice 6 completes the
|
||||
/// form retirement), plus the new `entries` / `active_entry_name` /
|
||||
/// `migration_readonly` view the multi-entry UI drives its entries-list,
|
||||
/// migration-target-dropdown, and readonly-banner from.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StorageSettingsDto {
|
||||
/// Active backend type: "local" or "s3"
|
||||
/// Active backend type: "local" or "s3". Legacy flat-config field
|
||||
/// — mirrors `entries[i where is_active].backend` for the active
|
||||
/// entry when multi-entry is in use.
|
||||
pub backend: String,
|
||||
pub s3_endpoint_url: Option<String>,
|
||||
pub s3_bucket: Option<String>,
|
||||
@@ -163,6 +172,47 @@ pub struct StorageSettingsDto {
|
||||
pub total_blobs: u64,
|
||||
pub total_bytes_stored: u64,
|
||||
pub dedup_ratio: f64,
|
||||
// ── Multi-entry view (slice 6) ──
|
||||
/// All named storage entries declared in env. Empty when running
|
||||
/// in legacy single-backend mode (`OXICLOUD_STORAGE_ENTRIES`
|
||||
/// unset AND no legacy synthesis happened). Order matches
|
||||
/// `_ENTRIES`.
|
||||
pub entries: Vec<StorageEntrySummaryDto>,
|
||||
/// Name of the entry the LIVE backend is currently bound to.
|
||||
/// Populated as the boot-selected name (per
|
||||
/// `CoreServices.active_backend_name`). Empty string for the
|
||||
/// zero-entries legacy path (`"legacy"` sentinel).
|
||||
pub active_entry_name: String,
|
||||
/// Global read-only flag — when true, all write-adjacent
|
||||
/// AuthZ checks refuse. Set by the migration handler at run
|
||||
/// start; cleared by the boot-clear rule after operator
|
||||
/// restart. Frontend renders a banner on the storage tab when
|
||||
/// true.
|
||||
pub migration_readonly: bool,
|
||||
}
|
||||
|
||||
/// Per-entry summary emitted in `StorageSettingsDto.entries`. Never
|
||||
/// carries credentials — those live in env vars only. `is_active`
|
||||
/// marks which entry the LIVE backend uses right now (matches
|
||||
/// `active_entry_name` on the parent DTO).
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StorageEntrySummaryDto {
|
||||
pub name: String,
|
||||
/// Backend type — "local" / "s3" / "azure".
|
||||
pub backend: String,
|
||||
/// True for exactly one entry (the entry the LIVE backend is on).
|
||||
/// Frontend uses this to badge the active row and to exclude it
|
||||
/// from the migration-target dropdown.
|
||||
pub is_active: bool,
|
||||
/// True when the entry has a per-entry encryption key. UI shows
|
||||
/// a lock icon. Presence-only — the key bytes never leave the
|
||||
/// server.
|
||||
pub encryption_enabled: bool,
|
||||
/// Human-readable physical location hint, if the backend surfaces
|
||||
/// one (`root_dir` for Local, `bucket` for S3, `container` for
|
||||
/// Azure). Cosmetic — helps the admin distinguish two Local
|
||||
/// entries pointing at different disks.
|
||||
pub location_hint: Option<String>,
|
||||
}
|
||||
|
||||
/// Request body for saving storage settings from the admin panel
|
||||
@@ -280,12 +330,11 @@ pub struct StartMigrationDto {
|
||||
pub concurrency: Option<usize>,
|
||||
}
|
||||
|
||||
/// Request body (empty) for `POST /api/admin/storage/migration/verify`.
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct VerifyMigrationDto {
|
||||
/// Number of random blobs to sample-check (default: 100).
|
||||
pub sample_size: Option<usize>,
|
||||
}
|
||||
// VerifyMigrationDto retired in slice 7 of
|
||||
// docs/plan/storage-multi-entry.md — the corresponding endpoint's
|
||||
// sample-based check is superseded by
|
||||
// `blobs_consistency?storage=<name>`, a full walk that emits
|
||||
// structured findings per mismatch.
|
||||
|
||||
// ============================================================================
|
||||
// SMTP Settings DTOs (Admin Panel)
|
||||
|
||||
@@ -2,11 +2,16 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::application::dtos::settings_dto::{
|
||||
SaveStorageSettingsDto, StorageSettingsDto, StorageTestResultDto, TestStorageConnectionDto,
|
||||
SaveStorageSettingsDto, StorageEntrySummaryDto, StorageSettingsDto, StorageTestResultDto,
|
||||
TestStorageConnectionDto,
|
||||
};
|
||||
use crate::application::ports::blob_storage_ports::BlobStorageBackend;
|
||||
use crate::common::config::{S3StorageConfig, StorageBackendType, StorageConfig};
|
||||
use crate::common::config::{
|
||||
NamedStorageEntry, S3StorageConfig, StorageBackendType, StorageConfig,
|
||||
};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::repositories::settings_repository::SettingsRepository;
|
||||
use crate::infrastructure::repositories::pg::SettingsPgRepository;
|
||||
@@ -22,18 +27,39 @@ pub struct StorageSettingsService {
|
||||
settings_repo: Arc<SettingsPgRepository>,
|
||||
env_storage_config: StorageConfig,
|
||||
dedup_service: Arc<DedupService>,
|
||||
/// Multi-entry snapshot from `AppConfig.storage_entries`. Populated
|
||||
/// at DI time; immutable per-process (env can only change on
|
||||
/// restart, per `docs/plan/storage-multi-entry.md`). Empty when
|
||||
/// running in the pre-multi-entry legacy path.
|
||||
storage_entries: Vec<NamedStorageEntry>,
|
||||
/// Name of the entry the LIVE backend is bound to (matches
|
||||
/// `CoreServices.active_backend_name`). Empty string / "legacy"
|
||||
/// for the zero-entries path.
|
||||
active_entry_name: String,
|
||||
/// Shared readonly flag — read into the admin DTO so the UI can
|
||||
/// render a "server in migration read-only mode" banner. Same
|
||||
/// atomic as `AppState.migration_readonly`; changes made by the
|
||||
/// migration handler are visible without a DB round-trip.
|
||||
migration_readonly: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl StorageSettingsService {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
settings_repo: Arc<SettingsPgRepository>,
|
||||
env_storage_config: StorageConfig,
|
||||
dedup_service: Arc<DedupService>,
|
||||
storage_entries: Vec<NamedStorageEntry>,
|
||||
active_entry_name: String,
|
||||
migration_readonly: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
settings_repo,
|
||||
env_storage_config,
|
||||
dedup_service,
|
||||
storage_entries,
|
||||
active_entry_name,
|
||||
migration_readonly,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +288,27 @@ impl StorageSettingsService {
|
||||
crate::common::config::StorageBackendType::Azure => "azure",
|
||||
};
|
||||
|
||||
// Project the multi-entry view. `is_active` is name-compared
|
||||
// against the boot-selected `active_entry_name` (matches
|
||||
// exactly one entry when we're in multi-entry mode; matches
|
||||
// nothing when running the zero-entries legacy path, which
|
||||
// is expected — the frontend hides the entries table then).
|
||||
let entries: Vec<StorageEntrySummaryDto> = self
|
||||
.storage_entries
|
||||
.iter()
|
||||
.map(|e| StorageEntrySummaryDto {
|
||||
name: e.name.clone(),
|
||||
backend: match e.backend {
|
||||
StorageBackendType::Local => "local".to_string(),
|
||||
StorageBackendType::S3 => "s3".to_string(),
|
||||
StorageBackendType::Azure => "azure".to_string(),
|
||||
},
|
||||
is_active: e.name == self.active_entry_name,
|
||||
encryption_enabled: e.encryption_key_base64.is_some(),
|
||||
location_hint: entry_location_hint(e),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(StorageSettingsDto {
|
||||
backend: backend_str.to_string(),
|
||||
s3_endpoint_url: effective.s3.as_ref().and_then(|s| s.endpoint_url.clone()),
|
||||
@@ -275,6 +322,9 @@ impl StorageSettingsService {
|
||||
total_blobs: stats.total_blobs,
|
||||
total_bytes_stored: stats.total_bytes_stored,
|
||||
dedup_ratio: stats.dedup_ratio,
|
||||
entries,
|
||||
active_entry_name: self.active_entry_name.clone(),
|
||||
migration_readonly: self.migration_readonly.load(Ordering::Relaxed),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -667,3 +717,17 @@ async fn run_backend_roundtrip(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Cosmetic human-readable identifier for an entry — the physical
|
||||
/// location piece an admin uses to disambiguate two entries of the
|
||||
/// same backend type. Never carries credentials. `None` when the
|
||||
/// entry doesn't have a natural short label (S3 without a bucket,
|
||||
/// which shouldn't happen because the parser rejects that shape at
|
||||
/// boot).
|
||||
fn entry_location_hint(entry: &NamedStorageEntry) -> Option<String> {
|
||||
match entry.backend {
|
||||
StorageBackendType::Local => entry.root_dir.clone(),
|
||||
StorageBackendType::S3 => entry.s3.as_ref().map(|s3| s3.bucket.clone()),
|
||||
StorageBackendType::Azure => entry.azure.as_ref().map(|az| az.container.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user