use std::sync::Arc;
use chrono::{Duration, Utc};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use uuid::Uuid;
use super::error::{ServiceError, ServiceResult};
use crate::domain::entity::StoredFile;
use crate::infrastructure::persistence::StoredFileRepository;
use crate::infrastructure::persistence::BucketRepository;
const DEFAULT_CDN_EXPIRY_HOURS: i64 = 1;
const CDN_SECRET_ENV: &str = "CDN_SIGNING_SECRET";
const DEFAULT_CDN_SECRET: &str = "bucket-cdn-dev-secret-change-in-production";
#[deprecated(
note = "HMAC-signed CDN URLs are not S3-compatible. Use ObjectStorage::presigned_get for real SigV4 URLs."
)]
pub struct CdnService {
file_repo: Arc<StoredFileRepository>,
bucket_repo: Arc<BucketRepository>,
}
#[allow(deprecated)]
impl CdnService {
pub fn new(
file_repo: Arc<StoredFileRepository>,
bucket_repo: Arc<BucketRepository>,
) -> Self {
Self { file_repo, bucket_repo }
}
pub async fn get_or_generate_url(
&self,
file_id: Uuid,
expiry_hours: Option<i64>,
) -> ServiceResult<String> {
let file = self.file_repo
.find_by_id(&file_id.to_string())
.await
.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
.ok_or(ServiceError::NotFound)?;
let bucket = self.bucket_repo
.find_by_id(&file.bucket_id.to_string())
.await
.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
.ok_or(ServiceError::NotFound)?;
if !bucket.enable_cdn {
return Err(ServiceError::Validation(format!("CDN is not enabled for bucket {}", bucket.id)));
}
if let (Some(ref url), Some(ref expires)) = (&file.cdn_url, &file.cdn_url_expires_at) {
if *expires > Utc::now() {
return Ok(url.clone());
}
}
let hours = expiry_hours.unwrap_or(DEFAULT_CDN_EXPIRY_HOURS);
let expires_at = Utc::now() + Duration::hours(hours);
let cdn_url = self.generate_signed_url(&file, expires_at);
let _ = (&cdn_url, expires_at);
Ok(cdn_url)
}
pub async fn invalidate(&self, _file_id: Uuid) -> ServiceResult<()> {
Ok(())
}
pub async fn invalidate_bucket(&self, _bucket_id: Uuid) -> ServiceResult<u64> {
Ok(0)
}
fn generate_signed_url(
&self,
file: &StoredFile,
expires_at: chrono::DateTime<Utc>,
) -> String {
let timestamp = expires_at.timestamp();
let path = format!("/cdn/files/{}/{}", file.bucket_id, file.id);
let signature = Self::sign_url(&path, timestamp);
format!("{}?expires={}&sig={}", path, timestamp, signature)
}
fn sign_url(path: &str, expires_timestamp: i64) -> String {
let secret = std::env::var(CDN_SECRET_ENV)
.unwrap_or_else(|_| DEFAULT_CDN_SECRET.to_string());
let message = format!("{}:{}", path, expires_timestamp);
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.expect("HMAC accepts any key length");
mac.update(message.as_bytes());
let result = mac.finalize();
hex::encode(result.into_bytes())
}
}