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::collections::HashMap;
use std::path::Path as FsPath; use std::path::Path as FsPath;
use chrono::{Duration as ChronoDuration, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
@@ -13,6 +14,12 @@ use crate::schema::{document_assets, document_versions};
use crate::state::{AppState, PgPooledConnection}; use crate::state::{AppState, PgPooledConnection};
use crate::utils::{http::inline_content_disposition, time::to_iso}; 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)] #[derive(Serialize, Clone, ToSchema)]
pub struct DocumentAssetResponse { pub struct DocumentAssetResponse {
pub id: Uuid, pub id: Uuid,
@@ -22,10 +29,7 @@ pub struct DocumentAssetResponse {
pub metadata: Value, pub metadata: Value,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)] #[schema(nullable)]
pub url: Option<String>, pub download: Option<DownloadLink>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)]
pub expires_at: Option<i64>,
} }
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
@@ -38,10 +42,7 @@ pub struct DocumentAssetDetailResponse {
pub created_at: String, pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)] #[schema(nullable)]
pub url: Option<String>, pub download: Option<DownloadLink>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(nullable)]
pub expires_at: Option<i64>,
} }
#[derive(Serialize, Clone, ToSchema)] #[derive(Serialize, Clone, ToSchema)]
@@ -61,23 +62,35 @@ pub struct DocumentVersionDetailResponse {
pub version: DocumentVersionResponse, pub version: DocumentVersionResponse,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assets: Vec<DocumentAssetResponse>, pub assets: Vec<DocumentAssetResponse>,
pub download_path: String, pub download: DownloadLink,
} }
pub fn build_download_path( pub fn build_download_link(
state: &AppState, state: &AppState,
document: &Document, document: &Document,
version_id: Uuid, version_id: Uuid,
user_id: Uuid, user_id: Uuid,
) -> AppResult<String> { ) -> AppResult<DownloadLink> {
state state
.jwt .jwt
.generate_download_token(document.id, version_id, user_id, document.tenant_id) .generate_download_token(document.id, version_id, user_id, document.tenant_id)
.map(|token| format!("/api/download/{token}"))
.map_err(|err| { .map_err(|err| {
tracing::error!(error = ?err, "failed to generate download token"); tracing::error!(error = ?err, "failed to generate download token");
AppError::internal("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 { 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, asset_type: asset.asset_type,
mime_type: asset.mime_type, mime_type: asset.mime_type,
metadata: asset.metadata, metadata: asset.metadata,
url: None, download: None,
expires_at: None,
} }
} }
pub fn to_asset_detail_response( pub fn to_asset_detail_response(
asset: DocumentAsset, asset: DocumentAsset,
url: Option<String>, download: Option<DownloadLink>,
expires_at: Option<i64>,
) -> DocumentAssetDetailResponse { ) -> DocumentAssetDetailResponse {
DocumentAssetDetailResponse { DocumentAssetDetailResponse {
id: asset.id, id: asset.id,
@@ -113,8 +124,7 @@ pub fn to_asset_detail_response(
mime_type: asset.mime_type, mime_type: asset.mime_type,
metadata: asset.metadata, metadata: asset.metadata,
created_at: to_iso(asset.created_at), created_at: to_iso(asset.created_at),
url, download,
expires_at,
} }
} }
+80 -1
View File
@@ -17,7 +17,7 @@ use uuid::Uuid;
use crate::auth::{ensure_active_tenant_with_conn, jwt::DownloadSubject, TenantScopedConn}; use crate::auth::{ensure_active_tenant_with_conn, jwt::DownloadSubject, TenantScopedConn};
use crate::documents::asset::{ use crate::documents::asset::{
asset_disposition, DocumentAssetDetailResponse, DocumentAssetResponse, asset_disposition, DocumentAssetDetailResponse, DocumentAssetResponse,
DocumentVersionDetailResponse, DocumentVersionResponse, DocumentVersionDetailResponse, DocumentVersionResponse, DownloadLink,
}; };
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::error::ApiErrorResponse; use crate::error::ApiErrorResponse;
@@ -492,6 +492,81 @@ pub async fn get_document_asset(
ok_json(detail) ok_json(detail)
} }
#[utoipa::path(
post,
path = "/api/documents/{id}/download",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Download link for current version", body = DownloadLink)),
tag = "Documents"
)]
pub async fn refresh_document_download(
State(state): State<AppState>,
Path(document_id): Path<Uuid>,
TenantScopedConn {
mut conn,
tenant_id,
user_id,
..
}: TenantScopedConn,
) -> AppResult<JsonResponse<DownloadLink>> {
let service = DocumentsService::new(&state);
let link = service
.get_document_download_link(&mut conn, tenant_id, user_id, document_id)
.await?;
ok_json(link)
}
#[utoipa::path(
post,
path = "/api/documents/{id}/versions/{version_id}/download",
params(
("id" = Uuid, Path, description = "Document ID"),
("version_id" = Uuid, Path, description = "Version ID")
),
responses((status = 200, description = "Download link for version", body = DownloadLink)),
tag = "Documents"
)]
pub async fn refresh_document_version_download(
State(state): State<AppState>,
Path((document_id, version_id)): Path<(Uuid, Uuid)>,
TenantScopedConn {
mut conn,
tenant_id,
user_id,
..
}: TenantScopedConn,
) -> AppResult<JsonResponse<DownloadLink>> {
let service = DocumentsService::new(&state);
let link = service
.get_document_version_download_link(&mut conn, tenant_id, user_id, document_id, version_id)
.await?;
ok_json(link)
}
#[utoipa::path(
post,
path = "/api/assets/{asset_id}/download",
params(("asset_id" = Uuid, Path, description = "Asset ID")),
responses((status = 200, description = "Asset download link", body = DownloadLink)),
tag = "Assets"
)]
pub async fn refresh_asset_download(
State(state): State<AppState>,
Path(asset_id): Path<Uuid>,
TenantScopedConn {
mut conn,
tenant_id,
user_id,
..
}: TenantScopedConn,
) -> AppResult<JsonResponse<DownloadLink>> {
let service = DocumentsService::new(&state);
let link = service
.get_asset_download_link(&mut conn, tenant_id, user_id, asset_id)
.await?;
ok_json(link)
}
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/documents/{id}/versions", path = "/api/documents/{id}/versions",
@@ -1017,6 +1092,7 @@ pub async fn remove_tag(
crate::routes::documents::check_document, crate::routes::documents::check_document,
crate::routes::documents::upload_document, crate::routes::documents::upload_document,
crate::routes::documents::get_document, crate::routes::documents::get_document,
crate::routes::documents::refresh_document_download,
crate::routes::documents::update_document, crate::routes::documents::update_document,
crate::routes::documents::trash_document, crate::routes::documents::trash_document,
crate::routes::documents::delete_document, crate::routes::documents::delete_document,
@@ -1036,6 +1112,8 @@ pub async fn remove_tag(
crate::routes::documents::get_document_asset, crate::routes::documents::get_document_asset,
crate::routes::documents::list_document_versions, crate::routes::documents::list_document_versions,
crate::routes::documents::get_document_version, crate::routes::documents::get_document_version,
crate::routes::documents::refresh_document_version_download,
crate::routes::documents::refresh_asset_download,
), ),
components(schemas( components(schemas(
crate::services::documents::DocumentListQuery, crate::services::documents::DocumentListQuery,
@@ -1066,6 +1144,7 @@ pub async fn remove_tag(
crate::documents::asset::DocumentVersionDetailResponse, crate::documents::asset::DocumentVersionDetailResponse,
crate::documents::asset::DocumentAssetResponse, crate::documents::asset::DocumentAssetResponse,
crate::documents::asset::DocumentAssetDetailResponse, crate::documents::asset::DocumentAssetDetailResponse,
crate::documents::asset::DownloadLink,
crate::documents::correspondents::DocumentCorrespondentResponse, crate::documents::correspondents::DocumentCorrespondentResponse,
crate::error::ApiErrorResponse, crate::error::ApiErrorResponse,
)) ))
+25 -6
View File
@@ -130,6 +130,12 @@ pub fn create_router(state: AppState) -> Router<()> {
ApiCapability::DocumentsRead, ApiCapability::DocumentsRead,
])), ])),
) )
.route(
"/{id}/download",
post(documents::refresh_document_download).layer(RequireCapabilitiesLayer::all([
ApiCapability::DocumentsRead,
])),
)
.route( .route(
"/{id}/trash", "/{id}/trash",
post(documents::trash_document).layer(RequireCapabilitiesLayer::all([ post(documents::trash_document).layer(RequireCapabilitiesLayer::all([
@@ -178,6 +184,12 @@ pub fn create_router(state: AppState) -> Router<()> {
ApiCapability::DocumentsRead, ApiCapability::DocumentsRead,
])), ])),
) )
.route(
"/{id}/versions/{version_id}/download",
post(documents::refresh_document_version_download).layer(
RequireCapabilitiesLayer::all([ApiCapability::DocumentsRead]),
),
)
.route( .route(
"/{id}/restore", "/{id}/restore",
post(documents::restore_document).layer(RequireCapabilitiesLayer::all([ post(documents::restore_document).layer(RequireCapabilitiesLayer::all([
@@ -366,12 +378,19 @@ pub fn create_router(state: AppState) -> Router<()> {
); );
let protected_state = state.clone(); let protected_state = state.clone();
let assets_routes = Router::new().route( let assets_routes = Router::new()
"/{asset_id}", .route(
get(documents::get_document_asset).layer(RequireCapabilitiesLayer::all([ "/{asset_id}",
ApiCapability::DocumentsRead, get(documents::get_document_asset).layer(RequireCapabilitiesLayer::all([
])), ApiCapability::DocumentsRead,
); ])),
)
.route(
"/{asset_id}/download",
post(documents::refresh_asset_download).layer(RequireCapabilitiesLayer::all([
ApiCapability::DocumentsRead,
])),
);
let manage_tenants_layer = RequireCapabilitiesLayer::all([ApiCapability::TenantsWrite]); let manage_tenants_layer = RequireCapabilitiesLayer::all([ApiCapability::TenantsWrite]);
let tenants_routes = Router::new() let tenants_routes = Router::new()
+99 -28
View File
@@ -17,9 +17,10 @@ use uuid::Uuid;
use crate::documents::{ use crate::documents::{
asset::{ asset::{
build_download_path, derive_document_title, filename_with_retained_extension, build_download_link, derive_document_title, filename_with_retained_extension,
to_asset_detail_response, to_version_response, DocumentAssetDetailResponse, to_asset_detail_response, to_version_response, DocumentAssetDetailResponse,
DocumentAssetResponse, DocumentVersionDetailResponse, DocumentVersionResponse, DocumentAssetResponse, DocumentVersionDetailResponse, DocumentVersionResponse,
DownloadLink,
}, },
correspondents::{ correspondents::{
insert_document_correspondents, normalize_correspondent_ids, DocumentCorrespondentResponse, insert_document_correspondents, normalize_correspondent_ids, DocumentCorrespondentResponse,
@@ -359,7 +360,8 @@ impl<'a> DocumentsService<'a> {
.unwrap_or_else(|| (Vec::new(), Vec::new())); .unwrap_or_else(|| (Vec::new(), Vec::new()));
let assets = self.load_asset_responses(conn, tenant_id, current_version.id, user_id)?; let assets = self.load_asset_responses(conn, tenant_id, current_version.id, user_id)?;
let current_version_data = Some((to_version_response(current_version), assets)); let download = build_download_link(self.state, &doc, current_version.id, user_id)?;
let current_version_data = Some((to_version_response(current_version), assets, download));
let response = let response =
self.to_document_response(user_id, doc, tags, correspondents, current_version_data)?; self.to_document_response(user_id, doc, tags, correspondents, current_version_data)?;
@@ -662,10 +664,12 @@ impl<'a> DocumentsService<'a> {
let current_version = doc_to_version let current_version = doc_to_version
.get(&doc.id) .get(&doc.id)
.and_then(|version_id| version_map.remove(version_id)) .and_then(|version_id| version_map.remove(version_id))
.map(|version| { .map(|version| -> AppResult<_> {
let assets = assets_by_version.remove(&version.id).unwrap_or_default(); let assets = assets_by_version.remove(&version.id).unwrap_or_default();
(to_version_response(version), assets) let download = build_download_link(self.state, &doc, version.id, user_id)?;
}); Ok((to_version_response(version), assets, download))
})
.transpose()?;
self.to_document_response(user_id, doc, tags, correspondents, current_version) self.to_document_response(user_id, doc, tags, correspondents, current_version)
}) })
.collect() .collect()
@@ -818,13 +822,15 @@ impl<'a> DocumentsService<'a> {
.cloned() .cloned()
.unwrap_or_else(|| (Vec::new(), Vec::new())); .unwrap_or_else(|| (Vec::new(), Vec::new()));
let download = build_download_link(self.state, &document, version.id, user_id)?;
DocumentDetailResponse { DocumentDetailResponse {
document: self.to_document_response( document: self.to_document_response(
user_id, user_id,
document, document,
tags, tags,
correspondents, correspondents,
Some((to_version_response(version.clone()), Vec::new())), Some((to_version_response(version.clone()), Vec::new(), download)),
)?, )?,
} }
}; };
@@ -962,9 +968,69 @@ impl<'a> DocumentsService<'a> {
drop(conn); drop(conn);
let (url, expires_at) = self.asset_download_url(asset.id, tenant_id, user_id)?; let download = self.asset_download_link(asset.id, tenant_id, user_id)?;
Ok(to_asset_detail_response(asset, Some(url), Some(expires_at))) Ok(to_asset_detail_response(asset, Some(download)))
}
pub async fn get_document_download_link(
&self,
conn: &mut PgPooledConnection,
tenant_id: Uuid,
user_id: Uuid,
document_id: Uuid,
) -> AppResult<DownloadLink> {
let document = load_active_document(conn, tenant_id, document_id)?;
let version: DocumentVersion = document_versions::table
.find(document.current_version_id)
.filter(document_versions::tenant_id.eq(tenant_id))
.first(conn)?;
build_download_link(self.state, &document, version.id, user_id)
}
pub async fn get_document_version_download_link(
&self,
conn: &mut PgPooledConnection,
tenant_id: Uuid,
user_id: Uuid,
document_id: Uuid,
version_id: Uuid,
) -> AppResult<DownloadLink> {
let document = load_active_document(conn, tenant_id, document_id)?;
let version: Option<DocumentVersion> = document_versions::table
.find(version_id)
.filter(document_versions::document_id.eq(document_id))
.filter(document_versions::tenant_id.eq(tenant_id))
.first(conn)
.optional()?;
let Some(version) = version else {
return Err(AppError::not_found());
};
build_download_link(self.state, &document, version.id, user_id)
}
pub async fn get_asset_download_link(
&self,
conn: &mut PgPooledConnection,
tenant_id: Uuid,
user_id: Uuid,
asset_id: Uuid,
) -> AppResult<DownloadLink> {
let asset: Option<DocumentAsset> = document_assets::table
.find(asset_id)
.filter(document_assets::tenant_id.eq(tenant_id))
.first(conn)
.optional()?;
let Some(asset) = asset else {
return Err(AppError::not_found());
};
self.asset_download_link(asset.id, tenant_id, user_id)
} }
pub fn list_document_versions( pub fn list_document_versions(
@@ -1001,13 +1067,13 @@ impl<'a> DocumentsService<'a> {
.first(conn)?; .first(conn)?;
let assets = self.load_asset_responses(conn, tenant_id, version.id, user_id)?; let assets = self.load_asset_responses(conn, tenant_id, version.id, user_id)?;
let download_path = build_download_path(self.state, &document, version.id, user_id)?; let download = build_download_link(self.state, &document, version.id, user_id)?;
let version_core = to_version_response(version); let version_core = to_version_response(version);
Ok(DocumentVersionDetailResponse { Ok(DocumentVersionDetailResponse {
version: version_core, version: version_core,
assets, assets,
download_path, download,
}) })
} }
@@ -1157,6 +1223,7 @@ impl<'a> DocumentsService<'a> {
let version_id = current_version.id; let version_id = current_version.id;
let assets = self.load_asset_responses(conn, tenant_id, version_id, user_id)?; let assets = self.load_asset_responses(conn, tenant_id, version_id, user_id)?;
let version_response = to_version_response(current_version); let version_response = to_version_response(current_version);
let download = build_download_link(self.state, &document, version_id, user_id)?;
let (tags, correspondents) = tags_and_correspondents let (tags, correspondents) = tags_and_correspondents
.get(&document_id) .get(&document_id)
.cloned() .cloned()
@@ -1167,7 +1234,7 @@ impl<'a> DocumentsService<'a> {
document, document,
tags, tags,
correspondents, correspondents,
Some((version_response, assets)), Some((version_response, assets, download)),
)?; )?;
Ok(DocumentDetailResponse { Ok(DocumentDetailResponse {
@@ -1327,21 +1394,22 @@ impl<'a> DocumentsService<'a> {
fn to_document_response( fn to_document_response(
&self, &self,
user_id: Uuid, _user_id: Uuid,
doc: Document, doc: Document,
tags: Vec<Tag>, tags: Vec<Tag>,
correspondents: Vec<DocumentCorrespondentResponse>, correspondents: Vec<DocumentCorrespondentResponse>,
current_version: Option<(DocumentVersionResponse, Vec<DocumentAssetResponse>)>, current_version: Option<(
DocumentVersionResponse,
Vec<DocumentAssetResponse>,
DownloadLink,
)>,
) -> AppResult<DocumentResponse> { ) -> AppResult<DocumentResponse> {
let current_version = match current_version { let current_version = match current_version {
Some((version, assets)) => { Some((version, assets, download)) => Some(DocumentVersionDetailResponse {
let download_path = build_download_path(self.state, &doc, version.id, user_id)?; version,
Some(DocumentVersionDetailResponse { assets,
version, download,
assets, }),
download_path,
})
}
None => None, None => None,
}; };
@@ -1469,6 +1537,7 @@ impl<'a> DocumentsService<'a> {
.unwrap_or_else(|| (Vec::new(), Vec::new())); .unwrap_or_else(|| (Vec::new(), Vec::new()));
let assets = self.load_asset_responses(conn, tenant_id, version.id, user_id)?; let assets = self.load_asset_responses(conn, tenant_id, version.id, user_id)?;
let download = build_download_link(self.state, &document, version.id, user_id)?;
let version_response = to_version_response(version.clone()); let version_response = to_version_response(version.clone());
info!( info!(
@@ -1483,19 +1552,19 @@ impl<'a> DocumentsService<'a> {
document, document,
tags, tags,
correspondents_list, correspondents_list,
Some((version_response, assets)), Some((version_response, assets, download)),
)?, )?,
}; };
Ok(Some(detail)) Ok(Some(detail))
} }
fn asset_download_url( fn asset_download_link(
&self, &self,
asset_id: Uuid, asset_id: Uuid,
tenant_id: Uuid, tenant_id: Uuid,
user_id: Uuid, user_id: Uuid,
) -> AppResult<(String, i64)> { ) -> AppResult<DownloadLink> {
let token = self let token = self
.state .state
.jwt .jwt
@@ -1512,7 +1581,10 @@ impl<'a> DocumentsService<'a> {
.ok_or_else(|| AppError::internal("failed to compute download expiry"))? .ok_or_else(|| AppError::internal("failed to compute download expiry"))?
.timestamp_millis(); .timestamp_millis();
Ok((format!("/api/download/{token}"), expires_at)) Ok(DownloadLink {
url: format!("/api/download/{token}"),
expires_at,
})
} }
fn asset_response( fn asset_response(
@@ -1521,15 +1593,14 @@ impl<'a> DocumentsService<'a> {
tenant_id: Uuid, tenant_id: Uuid,
user_id: Uuid, user_id: Uuid,
) -> AppResult<DocumentAssetResponse> { ) -> AppResult<DocumentAssetResponse> {
let (url, expires_at) = self.asset_download_url(asset.id, tenant_id, user_id)?; let download = self.asset_download_link(asset.id, tenant_id, user_id)?;
Ok(DocumentAssetResponse { Ok(DocumentAssetResponse {
id: asset.id, id: asset.id,
asset_type: asset.asset_type, asset_type: asset.asset_type,
mime_type: asset.mime_type, mime_type: asset.mime_type,
metadata: asset.metadata, metadata: asset.metadata,
url: Some(url), download: Some(download),
expires_at: Some(expires_at),
}) })
} }
+21 -13
View File
@@ -46,11 +46,17 @@ struct DocumentVersionPayload {
id: Uuid, id: Uuid,
version_number: i32, version_number: i32,
size_bytes: i64, size_bytes: i64,
download_path: String, download: DownloadLinkPayload,
#[serde(default)] #[serde(default)]
assets: Vec<DocumentAssetInfo>, assets: Vec<DocumentAssetInfo>,
} }
#[derive(Deserialize)]
struct DownloadLinkPayload {
url: String,
expires_at: i64,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct DocumentVersionListItem { struct DocumentVersionListItem {
id: Uuid, id: Uuid,
@@ -67,8 +73,7 @@ struct DocumentAssetInfo {
#[derive(Deserialize)] #[derive(Deserialize)]
struct AssetProxyDetail { struct AssetProxyDetail {
id: Uuid, id: Uuid,
url: Option<String>, download: Option<DownloadLinkPayload>,
expires_at: Option<i64>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -193,7 +198,8 @@ async fn upload_and_list_document() -> Result<()> {
.current_version .current_version
.as_ref() .as_ref()
.expect("current version detail"); .expect("current version detail");
assert!(current_version.download_path.starts_with("/api/download/")); assert!(current_version.download.url.starts_with("/api/download/"));
assert!(current_version.download.expires_at > 0);
assert_eq!(current_version.version_number, 1); assert_eq!(current_version.version_number, 1);
assert_eq!(current_version.size_bytes, file_bytes.len() as i64); assert_eq!(current_version.size_bytes, file_bytes.len() as i64);
assert!(current_version.assets.is_empty()); assert!(current_version.assets.is_empty());
@@ -224,10 +230,11 @@ async fn upload_and_list_document() -> Result<()> {
.current_version .current_version
.as_ref() .as_ref()
.expect("list current version") .expect("list current version")
.download_path .download
.url
.starts_with("/api/download/")); .starts_with("/api/download/"));
let redirect = app.get(&current_version.download_path, None).await?; let redirect = app.get(&current_version.download.url, None).await?;
assert_eq!(redirect.status(), StatusCode::TEMPORARY_REDIRECT); assert_eq!(redirect.status(), StatusCode::TEMPORARY_REDIRECT);
let location = redirect let location = redirect
.headers() .headers()
@@ -343,12 +350,12 @@ async fn asset_detail_uses_proxy_urls_when_configured() -> Result<()> {
let body = body_to_vec(response.into_body()).await?; let body = body_to_vec(response.into_body()).await?;
let asset_detail: AssetProxyDetail = serde_json::from_slice(&body)?; let asset_detail: AssetProxyDetail = serde_json::from_slice(&body)?;
assert_eq!(asset_detail.id, asset_id); assert_eq!(asset_detail.id, asset_id);
let url = asset_detail let download = asset_detail
.url .download
.as_deref() .as_ref()
.ok_or_else(|| anyhow!("missing url"))?; .ok_or_else(|| anyhow!("missing download link"))?;
assert!(url.starts_with("/api/download/")); assert!(download.url.starts_with("/api/download/"));
assert!(asset_detail.expires_at.is_some()); assert!(download.expires_at > 0);
app.cleanup().await?; app.cleanup().await?;
Ok(()) Ok(())
@@ -2055,7 +2062,8 @@ async fn list_document_versions_and_fetch_detail() -> Result<()> {
let detail_body = body_to_vec(detail_resp.into_body()).await?; let detail_body = body_to_vec(detail_resp.into_body()).await?;
let version_detail: DocumentVersionPayload = serde_json::from_slice(&detail_body)?; let version_detail: DocumentVersionPayload = serde_json::from_slice(&detail_body)?;
assert_eq!(version_detail.id, version_id); assert_eq!(version_detail.id, version_id);
assert!(version_detail.download_path.starts_with("/api/download/")); assert!(version_detail.download.url.starts_with("/api/download/"));
assert!(version_detail.download.expires_at > 0);
assert!(version_detail.assets.is_empty()); assert!(version_detail.assets.is_empty());
app.cleanup().await?; app.cleanup().await?;
+9 -2
View File
@@ -15,7 +15,13 @@ struct DocumentInfo {
#[derive(Deserialize)] #[derive(Deserialize)]
struct DocumentVersion { struct DocumentVersion {
download_path: String, download: DownloadLink,
}
#[derive(Deserialize)]
struct DownloadLink {
url: String,
expires_at: i64,
} }
#[tokio::test] #[tokio::test]
@@ -46,7 +52,8 @@ async fn document_download_redirects_when_proxy_disabled() -> Result<()> {
.current_version .current_version
.as_ref() .as_ref()
.expect("missing version") .expect("missing version")
.download_path .download
.url
.clone(); .clone();
let redirect = app.get(&download_path, None).await?; let redirect = app.get(&download_path, None).await?;