feat: wire remaining handlers — tag, quota, register asset, sidecar, processing
14 new endpoints: POST tags, GET quota, POST register, 6 sidecar, 7 processing. DTOs, AppState groups, LogSidecarWriter, full bootstrap wiring.
This commit is contained in:
@@ -4,9 +4,10 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
serde = { workspace = true }
|
application = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde = { workspace = true }
|
||||||
uuid = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
chrono = { workspace = true }
|
uuid = { workspace = true }
|
||||||
utoipa = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
utoipa = { workspace = true }
|
||||||
|
|||||||
@@ -58,3 +58,79 @@ pub struct GenerateShareLinkRequest {
|
|||||||
pub expires_in_hours: Option<u64>,
|
pub expires_in_hours: Option<u64>,
|
||||||
pub max_uses: Option<u32>,
|
pub max_uses: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Organization ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct TagAssetRequest {
|
||||||
|
pub tag_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Storage ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct CheckQuotaParams {
|
||||||
|
pub usage_type: Option<String>,
|
||||||
|
pub amount: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Catalog ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct RegisterAssetRequest {
|
||||||
|
pub volume_id: uuid::Uuid,
|
||||||
|
pub relative_path: String,
|
||||||
|
pub checksum: String,
|
||||||
|
pub asset_type: String,
|
||||||
|
pub mime_type: String,
|
||||||
|
pub file_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sidecar ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ResolveConflictRequest {
|
||||||
|
pub policy: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Processing ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct EnqueueJobRequest {
|
||||||
|
pub job_type: String,
|
||||||
|
pub priority: Option<u32>,
|
||||||
|
pub payload: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||||
|
pub target_asset_id: Option<uuid::Uuid>,
|
||||||
|
pub batch_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct CompleteJobRequest {
|
||||||
|
pub result: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct FailJobRequest {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ManagePluginRequest {
|
||||||
|
pub plugin_id: Option<uuid::Uuid>,
|
||||||
|
pub action: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub plugin_type: Option<String>,
|
||||||
|
pub config: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ConfigurePipelineRequest {
|
||||||
|
pub trigger_event: String,
|
||||||
|
pub steps: Vec<PipelineStepRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct PipelineStepRequest {
|
||||||
|
pub plugin_id: uuid::Uuid,
|
||||||
|
pub config: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -196,3 +196,157 @@ impl SharedResourceResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Tag ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct TagResponse {
|
||||||
|
pub tag_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub tag_source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagResponse {
|
||||||
|
pub fn from_domain(tag: &domain::entities::Tag) -> Self {
|
||||||
|
Self {
|
||||||
|
tag_id: *tag.tag_id.as_uuid(),
|
||||||
|
name: tag.name.clone(),
|
||||||
|
tag_source: format!("{:?}", tag.tag_source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Quota ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct QuotaCheckResponse {
|
||||||
|
pub allowed: bool,
|
||||||
|
pub current_usage: u64,
|
||||||
|
pub limit: u64,
|
||||||
|
pub is_unlimited: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuotaCheckResponse {
|
||||||
|
pub fn from_domain(result: &domain::storage::services::QuotaCheckResult) -> Self {
|
||||||
|
Self {
|
||||||
|
allowed: result.allowed,
|
||||||
|
current_usage: result.current_usage,
|
||||||
|
limit: result.limit,
|
||||||
|
is_unlimited: result.is_unlimited,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sidecar ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct SidecarExportResponse {
|
||||||
|
pub asset_id: Uuid,
|
||||||
|
pub status: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidecarExportResponse {
|
||||||
|
pub fn from_domain(record: &domain::entities::SidecarRecord) -> Self {
|
||||||
|
Self {
|
||||||
|
asset_id: *record.asset_id.as_uuid(),
|
||||||
|
status: format!("{:?}", record.sync_status),
|
||||||
|
path: record.sidecar_storage_path.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct DetectChangesResponse {
|
||||||
|
pub changed_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct SidecarImportResponse {
|
||||||
|
pub asset_id: Uuid,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Processing ---
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct JobResponse {
|
||||||
|
pub job_id: Uuid,
|
||||||
|
pub job_type: String,
|
||||||
|
pub status: String,
|
||||||
|
pub priority: u32,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobResponse {
|
||||||
|
pub fn from_domain(job: &domain::entities::Job) -> Self {
|
||||||
|
Self {
|
||||||
|
job_id: *job.job_id.as_uuid(),
|
||||||
|
job_type: format!("{:?}", job.job_type),
|
||||||
|
status: format!("{:?}", job.status),
|
||||||
|
priority: job.priority,
|
||||||
|
created_at: *job.created_at.as_datetime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct BatchProgressResponse {
|
||||||
|
pub batch_id: Uuid,
|
||||||
|
pub batch_type: String,
|
||||||
|
pub total: u32,
|
||||||
|
pub completed: u32,
|
||||||
|
pub failed: u32,
|
||||||
|
pub status: String,
|
||||||
|
pub jobs: Vec<JobResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BatchProgressResponse {
|
||||||
|
pub fn from_domain(progress: &application::processing::BatchProgress) -> Self {
|
||||||
|
Self {
|
||||||
|
batch_id: *progress.batch.batch_id.as_uuid(),
|
||||||
|
batch_type: progress.batch.batch_type.clone(),
|
||||||
|
total: progress.batch.total_jobs,
|
||||||
|
completed: progress.batch.completed_count,
|
||||||
|
failed: progress.batch.failed_count,
|
||||||
|
status: format!("{:?}", progress.batch.status),
|
||||||
|
jobs: progress.jobs.iter().map(JobResponse::from_domain).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct PluginResponse {
|
||||||
|
pub plugin_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub plugin_type: String,
|
||||||
|
pub is_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginResponse {
|
||||||
|
pub fn from_domain(plugin: &domain::entities::Plugin) -> Self {
|
||||||
|
Self {
|
||||||
|
plugin_id: *plugin.plugin_id.as_uuid(),
|
||||||
|
name: plugin.name.clone(),
|
||||||
|
plugin_type: format!("{:?}", plugin.plugin_type),
|
||||||
|
is_enabled: plugin.is_enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct PipelineResponse {
|
||||||
|
pub pipeline_id: Uuid,
|
||||||
|
pub trigger_event: String,
|
||||||
|
pub steps_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PipelineResponse {
|
||||||
|
pub fn from_domain(pipeline: &domain::entities::ProcessingPipeline) -> Self {
|
||||||
|
Self {
|
||||||
|
pipeline_id: *pipeline.pipeline_id.as_uuid(),
|
||||||
|
trigger_event: pipeline.trigger_event.clone(),
|
||||||
|
steps_count: pipeline.steps.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,33 +11,52 @@ use adapters_auth::{BcryptPasswordHasher, JwtTokenIssuer};
|
|||||||
|
|
||||||
use adapters_postgres::{
|
use adapters_postgres::{
|
||||||
PostgresAlbumRepository, PostgresAssetMetadataRepository, PostgresAssetRepository,
|
PostgresAlbumRepository, PostgresAssetMetadataRepository, PostgresAssetRepository,
|
||||||
PostgresIngestSessionRepository, PostgresLibraryPathRepository, PostgresQuotaRepository,
|
PostgresDuplicateRepository, PostgresIngestSessionRepository, PostgresJobBatchRepository,
|
||||||
PostgresShareRepository, PostgresStorageVolumeRepository, PostgresUsageLedgerRepository,
|
PostgresJobRepository, PostgresLibraryPathRepository, PostgresPipelineRepository,
|
||||||
PostgresUserRepository, PostgresVisibilityFilterRepository, connect, run_migrations,
|
PostgresPluginRepository, PostgresQuotaRepository, PostgresShareRepository,
|
||||||
|
PostgresSidecarRepository, PostgresStorageVolumeRepository, PostgresTagRepository,
|
||||||
|
PostgresUsageLedgerRepository, PostgresUserRepository, PostgresVisibilityFilterRepository,
|
||||||
|
connect, run_migrations,
|
||||||
};
|
};
|
||||||
|
|
||||||
use adapters_storage::LocalFileStorage;
|
use adapters_storage::LocalFileStorage;
|
||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
catalog::{GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, UpdateMetadataHandler},
|
catalog::{
|
||||||
|
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, RegisterAssetHandler,
|
||||||
|
UpdateMetadataHandler,
|
||||||
|
},
|
||||||
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
||||||
organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler},
|
organization::{
|
||||||
|
CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler, TagAssetHandler,
|
||||||
|
},
|
||||||
|
processing::{
|
||||||
|
CompleteJobHandler, ConfigurePipelineHandler, EnqueueJobHandler, FailJobHandler,
|
||||||
|
ManagePluginHandler, ReportBatchProgressHandler, StartJobHandler,
|
||||||
|
},
|
||||||
sharing::{
|
sharing::{
|
||||||
AccessSharedResourceHandler, GenerateShareLinkHandler, RevokeShareHandler,
|
AccessSharedResourceHandler, GenerateShareLinkHandler, RevokeShareHandler,
|
||||||
ShareResourceHandler,
|
ShareResourceHandler,
|
||||||
},
|
},
|
||||||
storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler},
|
sidecar::{
|
||||||
|
DetectExternalChangesHandler, ExportSidecarHandler, FullExportHandler, FullImportHandler,
|
||||||
|
ImportSidecarHandler, ResolveConflictHandler,
|
||||||
|
},
|
||||||
|
storage::{
|
||||||
|
CheckQuotaHandler, IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use presentation::{
|
use presentation::{
|
||||||
routes::app_router,
|
routes::app_router,
|
||||||
state::{
|
state::{
|
||||||
AppState, CatalogHandlers, IdentityHandlers, OrganizationHandlers, SharingHandlers,
|
AppState, CatalogHandlers, IdentityHandlers, OrganizationHandlers, ProcessingHandlers,
|
||||||
StorageHandlers,
|
SharingHandlers, SidecarHandlers, StorageHandlers,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::log_event_publisher::LogEventPublisher;
|
use crate::log_event_publisher::LogEventPublisher;
|
||||||
|
use crate::log_sidecar_writer::LogSidecarWriter;
|
||||||
|
|
||||||
pub async fn build_app(config: &Config) -> Result<Router> {
|
pub async fn build_app(config: &Config) -> Result<Router> {
|
||||||
let pool = connect(&config.database_url).await?;
|
let pool = connect(&config.database_url).await?;
|
||||||
@@ -65,7 +84,15 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
|||||||
let session_repo = Arc::new(PostgresIngestSessionRepository::new(pool.clone()));
|
let session_repo = Arc::new(PostgresIngestSessionRepository::new(pool.clone()));
|
||||||
let quota_repo = Arc::new(PostgresQuotaRepository::new(pool.clone()));
|
let quota_repo = Arc::new(PostgresQuotaRepository::new(pool.clone()));
|
||||||
let ledger_repo = Arc::new(PostgresUsageLedgerRepository::new(pool.clone()));
|
let ledger_repo = Arc::new(PostgresUsageLedgerRepository::new(pool.clone()));
|
||||||
|
let tag_repo = Arc::new(PostgresTagRepository::new(pool.clone()));
|
||||||
|
let duplicate_repo = Arc::new(PostgresDuplicateRepository::new(pool.clone()));
|
||||||
|
let sidecar_repo = Arc::new(PostgresSidecarRepository::new(pool.clone()));
|
||||||
|
let job_repo = Arc::new(PostgresJobRepository::new(pool.clone()));
|
||||||
|
let batch_repo = Arc::new(PostgresJobBatchRepository::new(pool.clone()));
|
||||||
|
let plugin_repo = Arc::new(PostgresPluginRepository::new(pool.clone()));
|
||||||
|
let pipeline_repo = Arc::new(PostgresPipelineRepository::new(pool.clone()));
|
||||||
let event_publisher: Arc<LogEventPublisher> = Arc::new(LogEventPublisher);
|
let event_publisher: Arc<LogEventPublisher> = Arc::new(LogEventPublisher);
|
||||||
|
let sidecar_writer: Arc<LogSidecarWriter> = Arc::new(LogSidecarWriter);
|
||||||
|
|
||||||
// File storage
|
// File storage
|
||||||
let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string());
|
let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string());
|
||||||
@@ -80,8 +107,8 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
|||||||
let ingest_asset_handler = Arc::new(IngestAssetHandler::new(
|
let ingest_asset_handler = Arc::new(IngestAssetHandler::new(
|
||||||
session_repo,
|
session_repo,
|
||||||
path_repo.clone(),
|
path_repo.clone(),
|
||||||
quota_repo,
|
quota_repo.clone(),
|
||||||
ledger_repo,
|
ledger_repo.clone(),
|
||||||
asset_repo.clone(),
|
asset_repo.clone(),
|
||||||
file_storage.clone(),
|
file_storage.clone(),
|
||||||
event_publisher.clone(),
|
event_publisher.clone(),
|
||||||
@@ -96,10 +123,78 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
|||||||
));
|
));
|
||||||
let update_metadata_handler = Arc::new(UpdateMetadataHandler::new(
|
let update_metadata_handler = Arc::new(UpdateMetadataHandler::new(
|
||||||
asset_repo.clone(),
|
asset_repo.clone(),
|
||||||
metadata_repo,
|
metadata_repo.clone(),
|
||||||
event_publisher.clone(),
|
event_publisher.clone(),
|
||||||
));
|
));
|
||||||
let read_asset_file_handler = Arc::new(ReadAssetFileHandler::new(asset_repo, file_storage));
|
let read_asset_file_handler =
|
||||||
|
Arc::new(ReadAssetFileHandler::new(asset_repo.clone(), file_storage));
|
||||||
|
|
||||||
|
// Register asset handler
|
||||||
|
let register_asset_handler = Arc::new(RegisterAssetHandler::new(
|
||||||
|
asset_repo.clone(),
|
||||||
|
duplicate_repo,
|
||||||
|
event_publisher.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Tag handler
|
||||||
|
let tag_asset_handler = Arc::new(TagAssetHandler::new(asset_repo.clone(), tag_repo));
|
||||||
|
|
||||||
|
// Check quota handler
|
||||||
|
let check_quota_handler = Arc::new(CheckQuotaHandler::new(quota_repo, ledger_repo));
|
||||||
|
|
||||||
|
// Sidecar handlers
|
||||||
|
let export_sidecar_handler = Arc::new(ExportSidecarHandler::new(
|
||||||
|
metadata_repo.clone(),
|
||||||
|
sidecar_repo.clone(),
|
||||||
|
sidecar_writer.clone(),
|
||||||
|
));
|
||||||
|
let detect_changes_handler = Arc::new(DetectExternalChangesHandler::new(
|
||||||
|
sidecar_repo.clone(),
|
||||||
|
sidecar_writer.clone(),
|
||||||
|
));
|
||||||
|
let import_sidecar_handler = Arc::new(ImportSidecarHandler::new(
|
||||||
|
sidecar_repo.clone(),
|
||||||
|
sidecar_writer.clone(),
|
||||||
|
metadata_repo.clone(),
|
||||||
|
));
|
||||||
|
let resolve_conflict_handler = Arc::new(ResolveConflictHandler::new(
|
||||||
|
sidecar_repo.clone(),
|
||||||
|
sidecar_writer.clone(),
|
||||||
|
metadata_repo.clone(),
|
||||||
|
));
|
||||||
|
let full_export_handler = Arc::new(FullExportHandler::new(
|
||||||
|
asset_repo.clone(),
|
||||||
|
metadata_repo.clone(),
|
||||||
|
sidecar_repo.clone(),
|
||||||
|
sidecar_writer.clone(),
|
||||||
|
));
|
||||||
|
let full_import_handler = Arc::new(FullImportHandler::new(
|
||||||
|
asset_repo,
|
||||||
|
metadata_repo,
|
||||||
|
sidecar_repo,
|
||||||
|
sidecar_writer,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Processing handlers
|
||||||
|
let enqueue_job_handler = Arc::new(EnqueueJobHandler::new(
|
||||||
|
job_repo.clone(),
|
||||||
|
event_publisher.clone(),
|
||||||
|
));
|
||||||
|
let start_job_handler = Arc::new(StartJobHandler::new(job_repo.clone()));
|
||||||
|
let complete_job_handler = Arc::new(CompleteJobHandler::new(
|
||||||
|
job_repo.clone(),
|
||||||
|
batch_repo.clone(),
|
||||||
|
event_publisher.clone(),
|
||||||
|
));
|
||||||
|
let fail_job_handler = Arc::new(FailJobHandler::new(
|
||||||
|
job_repo.clone(),
|
||||||
|
batch_repo.clone(),
|
||||||
|
event_publisher.clone(),
|
||||||
|
));
|
||||||
|
let batch_progress_handler = Arc::new(ReportBatchProgressHandler::new(batch_repo, job_repo));
|
||||||
|
let manage_plugin_handler = Arc::new(ManagePluginHandler::new(plugin_repo.clone()));
|
||||||
|
let configure_pipeline_handler =
|
||||||
|
Arc::new(ConfigurePipelineHandler::new(pipeline_repo, plugin_repo));
|
||||||
|
|
||||||
// Sharing repos & handlers
|
// Sharing repos & handlers
|
||||||
let share_repo = Arc::new(PostgresShareRepository::new(pool.clone()));
|
let share_repo = Arc::new(PostgresShareRepository::new(pool.clone()));
|
||||||
@@ -130,17 +225,39 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
|||||||
get_timeline: get_timeline_handler,
|
get_timeline: get_timeline_handler,
|
||||||
update_metadata: update_metadata_handler,
|
update_metadata: update_metadata_handler,
|
||||||
read_asset_file: read_asset_file_handler,
|
read_asset_file: read_asset_file_handler,
|
||||||
|
register_asset: register_asset_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
let organization = OrganizationHandlers {
|
let organization = OrganizationHandlers {
|
||||||
create_album: create_album_handler,
|
create_album: create_album_handler,
|
||||||
get_album: get_album_handler,
|
get_album: get_album_handler,
|
||||||
manage_album_entries: manage_album_entries_handler,
|
manage_album_entries: manage_album_entries_handler,
|
||||||
|
tag_asset: tag_asset_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
let storage_handlers = StorageHandlers {
|
let storage_handlers = StorageHandlers {
|
||||||
register_volume: register_volume_handler,
|
register_volume: register_volume_handler,
|
||||||
register_library_path: register_library_path_handler,
|
register_library_path: register_library_path_handler,
|
||||||
|
check_quota: check_quota_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sidecar = SidecarHandlers {
|
||||||
|
export: export_sidecar_handler,
|
||||||
|
detect_changes: detect_changes_handler,
|
||||||
|
import: import_sidecar_handler,
|
||||||
|
resolve: resolve_conflict_handler,
|
||||||
|
full_export: full_export_handler,
|
||||||
|
full_import: full_import_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
let processing = ProcessingHandlers {
|
||||||
|
enqueue_job: enqueue_job_handler,
|
||||||
|
start_job: start_job_handler,
|
||||||
|
complete_job: complete_job_handler,
|
||||||
|
fail_job: fail_job_handler,
|
||||||
|
batch_progress: batch_progress_handler,
|
||||||
|
manage_plugin: manage_plugin_handler,
|
||||||
|
configure_pipeline: configure_pipeline_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
let sharing = SharingHandlers {
|
let sharing = SharingHandlers {
|
||||||
@@ -156,6 +273,8 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
|||||||
organization,
|
organization,
|
||||||
storage: storage_handlers,
|
storage: storage_handlers,
|
||||||
sharing,
|
sharing,
|
||||||
|
sidecar,
|
||||||
|
processing,
|
||||||
token_issuer: issuer,
|
token_issuer: issuer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod factory;
|
pub mod factory;
|
||||||
pub mod log_event_publisher;
|
pub mod log_event_publisher;
|
||||||
|
pub mod log_sidecar_writer;
|
||||||
|
|||||||
21
crates/bootstrap/src/log_sidecar_writer.rs
Normal file
21
crates/bootstrap/src/log_sidecar_writer.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use domain::{errors::DomainError, ports::SidecarWriterPort, value_objects::StructuredData};
|
||||||
|
|
||||||
|
pub struct LogSidecarWriter;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SidecarWriterPort for LogSidecarWriter {
|
||||||
|
fn format_name(&self) -> &str {
|
||||||
|
"log_noop"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_sidecar(&self, _data: &StructuredData, path: &str) -> Result<(), DomainError> {
|
||||||
|
tracing::info!(path, "sidecar write (no-op)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_sidecar(&self, path: &str) -> Result<StructuredData, DomainError> {
|
||||||
|
tracing::info!(path, "sidecar read (no-op)");
|
||||||
|
Ok(StructuredData::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ use tracing::info;
|
|||||||
mod config;
|
mod config;
|
||||||
mod factory;
|
mod factory;
|
||||||
mod log_event_publisher;
|
mod log_event_publisher;
|
||||||
|
mod log_sidecar_writer;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ use crate::{
|
|||||||
extractors::{JwtClaims, UploadedAsset},
|
extractors::{JwtClaims, UploadedAsset},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
use api_types::responses::{AssetResponse, IngestResponse, TimelineResponse};
|
use api_types::{
|
||||||
|
requests::{RegisterAssetRequest, TagAssetRequest},
|
||||||
|
responses::{AssetResponse, IngestResponse, TagResponse, TimelineResponse},
|
||||||
|
};
|
||||||
use application::{
|
use application::{
|
||||||
catalog::{GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, UpdateMetadataCommand},
|
catalog::{
|
||||||
|
GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, RegisterAssetCommand,
|
||||||
|
UpdateMetadataCommand,
|
||||||
|
},
|
||||||
|
organization::TagAssetCommand,
|
||||||
storage::IngestAssetCommand,
|
storage::IngestAssetCommand,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -16,7 +23,11 @@ use axum::{
|
|||||||
http::{StatusCode, header},
|
http::{StatusCode, header},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use domain::value_objects::{MetadataValue, StructuredData, SystemId};
|
use domain::{
|
||||||
|
catalog::entities::AssetType,
|
||||||
|
errors::DomainError,
|
||||||
|
value_objects::{MetadataValue, StructuredData, SystemId},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct TimelineParams {
|
pub struct TimelineParams {
|
||||||
@@ -124,3 +135,51 @@ pub async fn serve_file(
|
|||||||
.body(Body::from(result.data))
|
.body(Body::from(result.data))
|
||||||
.map_err(|e| AppError::from(domain::errors::DomainError::Internal(e.to_string())))
|
.map_err(|e| AppError::from(domain::errors::DomainError::Internal(e.to_string())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn tag_asset(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
claims: JwtClaims,
|
||||||
|
Path((asset_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
Json(req): Json<TagAssetRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<TagResponse>), AppError> {
|
||||||
|
let cmd = TagAssetCommand {
|
||||||
|
asset_id: SystemId::from_uuid(asset_id),
|
||||||
|
tag_name: req.tag_name,
|
||||||
|
user_id: claims.user_id,
|
||||||
|
};
|
||||||
|
let (tag, _asset_tag) = state.organization.tag_asset.execute(cmd).await?;
|
||||||
|
Ok((StatusCode::CREATED, Json(TagResponse::from_domain(&tag))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_asset_type(s: &str) -> Result<AssetType, AppError> {
|
||||||
|
match s {
|
||||||
|
"image" => Ok(AssetType::Image),
|
||||||
|
"video" => Ok(AssetType::Video),
|
||||||
|
"live_photo" => Ok(AssetType::LivePhoto),
|
||||||
|
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||||
|
"Invalid asset type: {s}"
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register_asset(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
claims: JwtClaims,
|
||||||
|
Json(req): Json<RegisterAssetRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<AssetResponse>), AppError> {
|
||||||
|
let asset_type = parse_asset_type(&req.asset_type)?;
|
||||||
|
let cmd = RegisterAssetCommand {
|
||||||
|
volume_id: SystemId::from_uuid(req.volume_id),
|
||||||
|
relative_path: req.relative_path,
|
||||||
|
checksum: req.checksum,
|
||||||
|
asset_type,
|
||||||
|
mime_type: req.mime_type,
|
||||||
|
file_size: req.file_size,
|
||||||
|
owner_id: claims.user_id,
|
||||||
|
};
|
||||||
|
let (asset, _dup_group) = state.catalog.register_asset.execute(cmd).await?;
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(AssetResponse::from_domain(&asset, &StructuredData::new())),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ pub mod albums;
|
|||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod processing;
|
||||||
pub mod sharing;
|
pub mod sharing;
|
||||||
|
pub mod sidecar;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
|||||||
197
crates/presentation/src/handlers/processing.rs
Normal file
197
crates/presentation/src/handlers/processing.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
||||||
|
use api_types::{
|
||||||
|
requests::{
|
||||||
|
CompleteJobRequest, ConfigurePipelineRequest, EnqueueJobRequest, FailJobRequest,
|
||||||
|
ManagePluginRequest,
|
||||||
|
},
|
||||||
|
responses::{BatchProgressResponse, JobResponse, PipelineResponse, PluginResponse},
|
||||||
|
};
|
||||||
|
use application::processing::{
|
||||||
|
CompleteJobCommand, ConfigurePipelineCommand, EnqueueJobCommand, FailJobCommand,
|
||||||
|
ManagePluginCommand, PipelineStepConfig, PluginAction, ReportBatchProgressQuery,
|
||||||
|
StartJobCommand,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use domain::{
|
||||||
|
entities::{JobType, PluginType},
|
||||||
|
errors::DomainError,
|
||||||
|
value_objects::{MetadataValue, StructuredData, SystemId},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parse_job_type(s: &str) -> JobType {
|
||||||
|
match s {
|
||||||
|
"scan_directory" => JobType::ScanDirectory,
|
||||||
|
"extract_metadata" => JobType::ExtractMetadata,
|
||||||
|
"generate_derivative" => JobType::GenerateDerivative,
|
||||||
|
"sync_sidecar" => JobType::SyncSidecar,
|
||||||
|
"detect_duplicates" => JobType::DetectDuplicates,
|
||||||
|
other => JobType::Custom(other.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_plugin_type(s: &str) -> Result<PluginType, AppError> {
|
||||||
|
match s {
|
||||||
|
"media_processor" => Ok(PluginType::MediaProcessor),
|
||||||
|
"scheduled_task" => Ok(PluginType::ScheduledTask),
|
||||||
|
"sidecar_writer" => Ok(PluginType::SidecarWriter),
|
||||||
|
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||||
|
"Invalid plugin type: {s}"
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hashmap_to_structured(
|
||||||
|
map: &std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
) -> StructuredData {
|
||||||
|
let mut sd = StructuredData::new();
|
||||||
|
for (k, v) in map {
|
||||||
|
sd.insert(k.clone(), MetadataValue::from(v.clone()));
|
||||||
|
}
|
||||||
|
sd
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn enqueue_job(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Json(req): Json<EnqueueJobRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<JobResponse>), AppError> {
|
||||||
|
let payload = req
|
||||||
|
.payload
|
||||||
|
.as_ref()
|
||||||
|
.map(hashmap_to_structured)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let cmd = EnqueueJobCommand {
|
||||||
|
job_type: parse_job_type(&req.job_type),
|
||||||
|
priority: req.priority.unwrap_or(0),
|
||||||
|
payload,
|
||||||
|
target_asset_id: req.target_asset_id.map(SystemId::from_uuid),
|
||||||
|
batch_id: req.batch_id.map(SystemId::from_uuid),
|
||||||
|
};
|
||||||
|
let job = state.processing.enqueue_job.execute(cmd).await?;
|
||||||
|
Ok((StatusCode::CREATED, Json(JobResponse::from_domain(&job))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_job(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((job_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
) -> Result<Json<JobResponse>, AppError> {
|
||||||
|
let cmd = StartJobCommand {
|
||||||
|
job_id: SystemId::from_uuid(job_id),
|
||||||
|
};
|
||||||
|
let job = state.processing.start_job.execute(cmd).await?;
|
||||||
|
Ok(Json(JobResponse::from_domain(&job)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn complete_job(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((job_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
Json(req): Json<CompleteJobRequest>,
|
||||||
|
) -> Result<Json<JobResponse>, AppError> {
|
||||||
|
let cmd = CompleteJobCommand {
|
||||||
|
job_id: SystemId::from_uuid(job_id),
|
||||||
|
result: hashmap_to_structured(&req.result),
|
||||||
|
};
|
||||||
|
let job = state.processing.complete_job.execute(cmd).await?;
|
||||||
|
Ok(Json(JobResponse::from_domain(&job)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fail_job(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((job_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
Json(req): Json<FailJobRequest>,
|
||||||
|
) -> Result<Json<JobResponse>, AppError> {
|
||||||
|
let cmd = FailJobCommand {
|
||||||
|
job_id: SystemId::from_uuid(job_id),
|
||||||
|
error: req.error,
|
||||||
|
};
|
||||||
|
let job = state.processing.fail_job.execute(cmd).await?;
|
||||||
|
Ok(Json(JobResponse::from_domain(&job)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn batch_progress(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((batch_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
) -> Result<Json<BatchProgressResponse>, AppError> {
|
||||||
|
let query = ReportBatchProgressQuery {
|
||||||
|
batch_id: SystemId::from_uuid(batch_id),
|
||||||
|
};
|
||||||
|
let progress = state.processing.batch_progress.execute(query).await?;
|
||||||
|
Ok(Json(BatchProgressResponse::from_domain(&progress)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn manage_plugin(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Json(req): Json<ManagePluginRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<PluginResponse>), AppError> {
|
||||||
|
let action = match req.action.as_str() {
|
||||||
|
"create" => {
|
||||||
|
let name = req.name.ok_or_else(|| {
|
||||||
|
AppError::from(DomainError::Validation("name required for create".into()))
|
||||||
|
})?;
|
||||||
|
let pt = req.plugin_type.as_deref().unwrap_or("media_processor");
|
||||||
|
let plugin_type = parse_plugin_type(pt)?;
|
||||||
|
let config = req
|
||||||
|
.config
|
||||||
|
.as_ref()
|
||||||
|
.map(hashmap_to_structured)
|
||||||
|
.unwrap_or_default();
|
||||||
|
PluginAction::Create {
|
||||||
|
name,
|
||||||
|
plugin_type,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"enable" => PluginAction::Enable,
|
||||||
|
"disable" => PluginAction::Disable,
|
||||||
|
other => {
|
||||||
|
return Err(AppError::from(DomainError::Validation(format!(
|
||||||
|
"Invalid plugin action: {other}. Use create, enable, or disable"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmd = ManagePluginCommand {
|
||||||
|
plugin_id: req.plugin_id.map(SystemId::from_uuid),
|
||||||
|
action,
|
||||||
|
};
|
||||||
|
let plugin = state.processing.manage_plugin.execute(cmd).await?;
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(PluginResponse::from_domain(&plugin)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn configure_pipeline(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Json(req): Json<ConfigurePipelineRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<PipelineResponse>), AppError> {
|
||||||
|
let steps = req
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.map(|s| PipelineStepConfig {
|
||||||
|
plugin_id: SystemId::from_uuid(s.plugin_id),
|
||||||
|
config: hashmap_to_structured(&s.config),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let cmd = ConfigurePipelineCommand {
|
||||||
|
trigger_event: req.trigger_event,
|
||||||
|
steps,
|
||||||
|
};
|
||||||
|
let pipeline = state.processing.configure_pipeline.execute(cmd).await?;
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(PipelineResponse::from_domain(&pipeline)),
|
||||||
|
))
|
||||||
|
}
|
||||||
103
crates/presentation/src/handlers/sidecar.rs
Normal file
103
crates/presentation/src/handlers/sidecar.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
||||||
|
use api_types::responses::{DetectChangesResponse, SidecarExportResponse, SidecarImportResponse};
|
||||||
|
use application::sidecar::{
|
||||||
|
DetectExternalChangesCommand, ExportSidecarCommand, FullExportCommand, FullImportCommand,
|
||||||
|
ImportSidecarCommand, ResolveConflictCommand,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
};
|
||||||
|
use domain::{entities::ConflictPolicy, errors::DomainError, value_objects::SystemId};
|
||||||
|
|
||||||
|
fn parse_conflict_policy(s: &str) -> Result<ConflictPolicy, AppError> {
|
||||||
|
match s {
|
||||||
|
"db_wins" => Ok(ConflictPolicy::DbWins),
|
||||||
|
"file_wins" => Ok(ConflictPolicy::FileWins),
|
||||||
|
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||||
|
"Invalid conflict policy: {s}. Use db_wins or file_wins"
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn export_sidecar(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((asset_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
) -> Result<Json<SidecarExportResponse>, AppError> {
|
||||||
|
let cmd = ExportSidecarCommand {
|
||||||
|
asset_id: SystemId::from_uuid(asset_id),
|
||||||
|
};
|
||||||
|
let record = state.sidecar.export.execute(cmd).await?;
|
||||||
|
Ok(Json(SidecarExportResponse::from_domain(&record)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn detect_changes(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
) -> Result<Json<DetectChangesResponse>, AppError> {
|
||||||
|
let count = state
|
||||||
|
.sidecar
|
||||||
|
.detect_changes
|
||||||
|
.execute(DetectExternalChangesCommand)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(DetectChangesResponse {
|
||||||
|
changed_count: count,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_sidecar(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((asset_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
) -> Result<Json<SidecarImportResponse>, AppError> {
|
||||||
|
let cmd = ImportSidecarCommand {
|
||||||
|
asset_id: SystemId::from_uuid(asset_id),
|
||||||
|
};
|
||||||
|
let metadata = state.sidecar.import.execute(cmd).await?;
|
||||||
|
Ok(Json(SidecarImportResponse {
|
||||||
|
asset_id: *metadata.asset_id.as_uuid(),
|
||||||
|
status: "imported".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve_conflict(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
_claims: JwtClaims,
|
||||||
|
Path((asset_id,)): Path<(uuid::Uuid,)>,
|
||||||
|
Json(req): Json<api_types::requests::ResolveConflictRequest>,
|
||||||
|
) -> Result<Json<SidecarExportResponse>, AppError> {
|
||||||
|
let policy = parse_conflict_policy(&req.policy)?;
|
||||||
|
let cmd = ResolveConflictCommand {
|
||||||
|
asset_id: SystemId::from_uuid(asset_id),
|
||||||
|
policy,
|
||||||
|
};
|
||||||
|
let record = state.sidecar.resolve.execute(cmd).await?;
|
||||||
|
Ok(Json(SidecarExportResponse::from_domain(&record)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn full_export(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
claims: JwtClaims,
|
||||||
|
) -> Result<Json<DetectChangesResponse>, AppError> {
|
||||||
|
let cmd = FullExportCommand {
|
||||||
|
owner_id: claims.user_id,
|
||||||
|
};
|
||||||
|
let count = state.sidecar.full_export.execute(cmd).await?;
|
||||||
|
Ok(Json(DetectChangesResponse {
|
||||||
|
changed_count: count,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn full_import(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
claims: JwtClaims,
|
||||||
|
) -> Result<Json<DetectChangesResponse>, AppError> {
|
||||||
|
let cmd = FullImportCommand {
|
||||||
|
owner_id: claims.user_id,
|
||||||
|
};
|
||||||
|
let count = state.sidecar.full_import.execute(cmd).await?;
|
||||||
|
Ok(Json(DetectChangesResponse {
|
||||||
|
changed_count: count,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
||||||
use api_types::{
|
use api_types::{
|
||||||
requests::{RegisterLibraryPathRequest, RegisterVolumeRequest},
|
requests::{CheckQuotaParams, RegisterLibraryPathRequest, RegisterVolumeRequest},
|
||||||
responses::{LibraryPathResponse, VolumeResponse},
|
responses::{LibraryPathResponse, QuotaCheckResponse, VolumeResponse},
|
||||||
};
|
};
|
||||||
use application::storage::{RegisterLibraryPathCommand, RegisterVolumeCommand};
|
use application::storage::{CheckQuotaQuery, RegisterLibraryPathCommand, RegisterVolumeCommand};
|
||||||
use axum::{Json, extract::State, http::StatusCode};
|
use axum::{
|
||||||
use domain::value_objects::SystemId;
|
Json,
|
||||||
|
extract::{Query, State},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use domain::{entities::UsageType, errors::DomainError, value_objects::SystemId};
|
||||||
|
|
||||||
pub async fn register_volume(
|
pub async fn register_volume(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -41,3 +45,38 @@ pub async fn register_library_path(
|
|||||||
Json(LibraryPathResponse::from_domain(&path)),
|
Json(LibraryPathResponse::from_domain(&path)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_QUOTA_USAGE_TYPE: &str = "storage_bytes";
|
||||||
|
const DEFAULT_QUOTA_AMOUNT: u64 = 0;
|
||||||
|
|
||||||
|
fn parse_usage_type(s: &str) -> Result<UsageType, AppError> {
|
||||||
|
match s {
|
||||||
|
"storage_bytes" => Ok(UsageType::StorageBytes),
|
||||||
|
"process_jobs" => Ok(UsageType::ProcessJobs),
|
||||||
|
"api_calls" => Ok(UsageType::ApiCalls),
|
||||||
|
"indexing_size" => Ok(UsageType::IndexingSize),
|
||||||
|
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||||
|
"Invalid usage type: {s}"
|
||||||
|
)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_quota(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
claims: JwtClaims,
|
||||||
|
Query(params): Query<CheckQuotaParams>,
|
||||||
|
) -> Result<Json<QuotaCheckResponse>, AppError> {
|
||||||
|
let usage_type = parse_usage_type(
|
||||||
|
params
|
||||||
|
.usage_type
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(DEFAULT_QUOTA_USAGE_TYPE),
|
||||||
|
)?;
|
||||||
|
let query = CheckQuotaQuery {
|
||||||
|
user_id: claims.user_id,
|
||||||
|
usage_type,
|
||||||
|
requested_amount: params.amount.unwrap_or(DEFAULT_QUOTA_AMOUNT),
|
||||||
|
};
|
||||||
|
let result = state.storage.check_quota.execute(query).await?;
|
||||||
|
Ok(Json(QuotaCheckResponse::from_domain(&result)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
handlers::{albums, assets, auth, health, sharing, storage},
|
handlers::{albums, assets, auth, health, processing, sharing, sidecar, storage},
|
||||||
openapi::openapi_router,
|
openapi::openapi_router,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
@@ -24,10 +24,12 @@ pub fn api_v1_router() -> Router<AppState> {
|
|||||||
)
|
)
|
||||||
// assets
|
// assets
|
||||||
.route("/assets/ingest", post(assets::ingest))
|
.route("/assets/ingest", post(assets::ingest))
|
||||||
|
.route("/assets/register", post(assets::register_asset))
|
||||||
.route("/assets/timeline", get(assets::timeline))
|
.route("/assets/timeline", get(assets::timeline))
|
||||||
.route("/assets/{id}", get(assets::get_asset))
|
.route("/assets/{id}", get(assets::get_asset))
|
||||||
.route("/assets/{id}/metadata", put(assets::update_metadata))
|
.route("/assets/{id}/metadata", put(assets::update_metadata))
|
||||||
.route("/assets/{id}/file", get(assets::serve_file))
|
.route("/assets/{id}/file", get(assets::serve_file))
|
||||||
|
.route("/assets/{id}/tags", post(assets::tag_asset))
|
||||||
// sharing
|
// sharing
|
||||||
.route("/sharing", post(sharing::share_resource))
|
.route("/sharing", post(sharing::share_resource))
|
||||||
.route("/sharing/links", post(sharing::generate_link))
|
.route("/sharing/links", post(sharing::generate_link))
|
||||||
@@ -39,6 +41,25 @@ pub fn api_v1_router() -> Router<AppState> {
|
|||||||
"/storage/library-paths",
|
"/storage/library-paths",
|
||||||
post(storage::register_library_path),
|
post(storage::register_library_path),
|
||||||
)
|
)
|
||||||
|
.route("/storage/quota", get(storage::check_quota))
|
||||||
|
// sidecar
|
||||||
|
.route("/sidecar/export/{asset_id}", post(sidecar::export_sidecar))
|
||||||
|
.route("/sidecar/detect-changes", post(sidecar::detect_changes))
|
||||||
|
.route("/sidecar/import/{asset_id}", post(sidecar::import_sidecar))
|
||||||
|
.route(
|
||||||
|
"/sidecar/resolve/{asset_id}",
|
||||||
|
post(sidecar::resolve_conflict),
|
||||||
|
)
|
||||||
|
.route("/sidecar/full-export", post(sidecar::full_export))
|
||||||
|
.route("/sidecar/full-import", post(sidecar::full_import))
|
||||||
|
// processing
|
||||||
|
.route("/jobs", post(processing::enqueue_job))
|
||||||
|
.route("/jobs/{id}/start", post(processing::start_job))
|
||||||
|
.route("/jobs/{id}/complete", post(processing::complete_job))
|
||||||
|
.route("/jobs/{id}/fail", post(processing::fail_job))
|
||||||
|
.route("/jobs/batches/{id}", get(processing::batch_progress))
|
||||||
|
.route("/plugins", post(processing::manage_plugin))
|
||||||
|
.route("/pipelines", post(processing::configure_pipeline))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_router() -> Router<AppState> {
|
pub fn app_router() -> Router<AppState> {
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
catalog::{GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, UpdateMetadataHandler},
|
catalog::{
|
||||||
|
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, RegisterAssetHandler,
|
||||||
|
UpdateMetadataHandler,
|
||||||
|
},
|
||||||
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
||||||
organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler},
|
organization::{
|
||||||
|
CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler, TagAssetHandler,
|
||||||
|
},
|
||||||
|
processing::{
|
||||||
|
CompleteJobHandler, ConfigurePipelineHandler, EnqueueJobHandler, FailJobHandler,
|
||||||
|
ManagePluginHandler, ReportBatchProgressHandler, StartJobHandler,
|
||||||
|
},
|
||||||
sharing::{
|
sharing::{
|
||||||
AccessSharedResourceHandler, GenerateShareLinkHandler, RevokeShareHandler,
|
AccessSharedResourceHandler, GenerateShareLinkHandler, RevokeShareHandler,
|
||||||
ShareResourceHandler,
|
ShareResourceHandler,
|
||||||
},
|
},
|
||||||
storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler},
|
sidecar::{
|
||||||
|
DetectExternalChangesHandler, ExportSidecarHandler, FullExportHandler, FullImportHandler,
|
||||||
|
ImportSidecarHandler, ResolveConflictHandler,
|
||||||
|
},
|
||||||
|
storage::{
|
||||||
|
CheckQuotaHandler, IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use domain::ports::TokenIssuer;
|
use domain::ports::TokenIssuer;
|
||||||
|
|
||||||
@@ -26,6 +41,7 @@ pub struct CatalogHandlers {
|
|||||||
pub get_timeline: Arc<GetTimelineHandler>,
|
pub get_timeline: Arc<GetTimelineHandler>,
|
||||||
pub update_metadata: Arc<UpdateMetadataHandler>,
|
pub update_metadata: Arc<UpdateMetadataHandler>,
|
||||||
pub read_asset_file: Arc<ReadAssetFileHandler>,
|
pub read_asset_file: Arc<ReadAssetFileHandler>,
|
||||||
|
pub register_asset: Arc<RegisterAssetHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -33,12 +49,14 @@ pub struct OrganizationHandlers {
|
|||||||
pub create_album: Arc<CreateAlbumHandler>,
|
pub create_album: Arc<CreateAlbumHandler>,
|
||||||
pub get_album: Arc<GetAlbumHandler>,
|
pub get_album: Arc<GetAlbumHandler>,
|
||||||
pub manage_album_entries: Arc<ManageAlbumEntriesHandler>,
|
pub manage_album_entries: Arc<ManageAlbumEntriesHandler>,
|
||||||
|
pub tag_asset: Arc<TagAssetHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct StorageHandlers {
|
pub struct StorageHandlers {
|
||||||
pub register_volume: Arc<RegisterVolumeHandler>,
|
pub register_volume: Arc<RegisterVolumeHandler>,
|
||||||
pub register_library_path: Arc<RegisterLibraryPathHandler>,
|
pub register_library_path: Arc<RegisterLibraryPathHandler>,
|
||||||
|
pub check_quota: Arc<CheckQuotaHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -49,6 +67,27 @@ pub struct SharingHandlers {
|
|||||||
pub access: Arc<AccessSharedResourceHandler>,
|
pub access: Arc<AccessSharedResourceHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SidecarHandlers {
|
||||||
|
pub export: Arc<ExportSidecarHandler>,
|
||||||
|
pub detect_changes: Arc<DetectExternalChangesHandler>,
|
||||||
|
pub import: Arc<ImportSidecarHandler>,
|
||||||
|
pub resolve: Arc<ResolveConflictHandler>,
|
||||||
|
pub full_export: Arc<FullExportHandler>,
|
||||||
|
pub full_import: Arc<FullImportHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProcessingHandlers {
|
||||||
|
pub enqueue_job: Arc<EnqueueJobHandler>,
|
||||||
|
pub start_job: Arc<StartJobHandler>,
|
||||||
|
pub complete_job: Arc<CompleteJobHandler>,
|
||||||
|
pub fail_job: Arc<FailJobHandler>,
|
||||||
|
pub batch_progress: Arc<ReportBatchProgressHandler>,
|
||||||
|
pub manage_plugin: Arc<ManagePluginHandler>,
|
||||||
|
pub configure_pipeline: Arc<ConfigurePipelineHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub identity: IdentityHandlers,
|
pub identity: IdentityHandlers,
|
||||||
@@ -56,5 +95,7 @@ pub struct AppState {
|
|||||||
pub organization: OrganizationHandlers,
|
pub organization: OrganizationHandlers,
|
||||||
pub storage: StorageHandlers,
|
pub storage: StorageHandlers,
|
||||||
pub sharing: SharingHandlers,
|
pub sharing: SharingHandlers,
|
||||||
|
pub sidecar: SidecarHandlers,
|
||||||
|
pub processing: ProcessingHandlers,
|
||||||
pub token_issuer: Arc<dyn TokenIssuer>,
|
pub token_issuer: Arc<dyn TokenIssuer>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user