DownloadLink

This commit is contained in:
2025-11-21 13:53:13 +01:00
parent bd55b784d7
commit 5871c7ae6e
6 changed files with 262 additions and 68 deletions
+28 -18
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::path::Path as FsPath;
use chrono::{Duration as ChronoDuration, Utc};
use diesel::prelude::*;
use serde::Serialize;
use serde_json::Value;
@@ -13,6 +14,12 @@ use crate::schema::{document_assets, document_versions};
use crate::state::{AppState, PgPooledConnection};
use crate::utils::{http::inline_content_disposition, time::to_iso};
#[derive(Serialize, Clone, ToSchema)]
pub struct DownloadLink {
pub url: String,
pub expires_at: i64,
}
#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentAssetResponse {
pub id: Uuid,
@@ -22,10 +29,7 @@ pub struct DocumentAssetResponse {
pub metadata: Value,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)]
pub expires_at: Option<i64>,
pub download: Option<DownloadLink>,
}
#[derive(Serialize, ToSchema)]
@@ -38,10 +42,7 @@ pub struct DocumentAssetDetailResponse {
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)]
pub expires_at: Option<i64>,
pub download: Option<DownloadLink>,
}
#[derive(Serialize, Clone, ToSchema)]
@@ -61,23 +62,35 @@ pub struct DocumentVersionDetailResponse {
pub version: DocumentVersionResponse,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assets: Vec<DocumentAssetResponse>,
pub download_path: String,
pub download: DownloadLink,
}
pub fn build_download_path(
pub fn build_download_link(
state: &AppState,
document: &Document,
version_id: Uuid,
user_id: Uuid,
) -> AppResult<String> {
) -> AppResult<DownloadLink> {
state
.jwt
.generate_download_token(document.id, version_id, user_id, document.tenant_id)
.map(|token| format!("/api/download/{token}"))
.map_err(|err| {
tracing::error!(error = ?err, "failed to generate download token");
AppError::internal("failed to generate download token")
})
.and_then(|token| {
let expires_at = Utc::now()
.checked_add_signed(ChronoDuration::minutes(
state.config.download_token_expiry_minutes,
))
.ok_or_else(|| AppError::internal("failed to compute download expiry"))?
.timestamp_millis();
Ok(DownloadLink {
url: format!("/api/download/{token}"),
expires_at,
})
})
}
pub fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse {
@@ -97,15 +110,13 @@ pub fn to_asset_summary(asset: DocumentAsset) -> DocumentAssetResponse {
asset_type: asset.asset_type,
mime_type: asset.mime_type,
metadata: asset.metadata,
url: None,
expires_at: None,
download: None,
}
}
pub fn to_asset_detail_response(
asset: DocumentAsset,
url: Option<String>,
expires_at: Option<i64>,
download: Option<DownloadLink>,
) -> DocumentAssetDetailResponse {
DocumentAssetDetailResponse {
id: asset.id,
@@ -113,8 +124,7 @@ pub fn to_asset_detail_response(
mime_type: asset.mime_type,
metadata: asset.metadata,
created_at: to_iso(asset.created_at),
url,
expires_at,
download,
}
}