use super::error::ApiResponseError;
use super::handlers_uploads::{presign_issuer_error, presign_time};
use super::{AppJson, AppState, NamespaceIdPath};
use axum::extract::State;
use axum::Json;
#[cfg(feature = "openapi")]
use loonfs_api::ApiError;
use loonfs_api::{
v0::{BeginDownloadRequest, BeginDownloadResponse, ObjectTransferAccess},
FEATURE_DOWNLOADS_DIRECT_GET,
};
use loonfs_objectstore::presign::PresignedGetRequest;
use std::time::Duration;
const DIRECT_GET_URL_TTL: Duration = Duration::from_secs(15 * 60);
#[cfg_attr(
feature = "openapi",
utoipa::path(
post,
path = "/v0/namespaces/{namespace}/filesystem/downloads",
tag = "filesystem",
summary = "Begin download",
description = "Authorizes one direct read of a file's content object and returns a short-lived presigned GET capability, the resolved revision, and the content reference the client checks the arriving bytes against. `Range` is outside the signature, so one grant serves ranged, resumed, and parallel reads. Deployments that cannot presign answer 501 `not_supported`; the proxied `GET /filesystem/content` route stays available and is capped by `download.max_content_bytes`.",
params(("namespace" = String, Path, description = "Namespace id")),
request_body = BeginDownloadRequest,
responses(
(status = 200, description = "Download authorized", body = BeginDownloadResponse),
(status = 400, description = "Invalid path or revision", body = ApiError),
(status = 401, description = "Unauthorized", body = ApiError),
(status = 404, description = "Namespace, path, or revision not found", body = ApiError),
(status = 410, description = "Namespace deleted", body = ApiError),
(status = 501, description = "Direct download is unsupported", body = ApiError)
)
)
)]
pub(super) async fn begin_download(
State(state): State<AppState>,
namespace: NamespaceIdPath,
AppJson(request): AppJson<BeginDownloadRequest>,
) -> Result<Json<BeginDownloadResponse>, ApiResponseError> {
let namespace_id = namespace.into_id()?;
let Some(issuer) = state.transfer_issuer.as_ref() else {
return Err(ApiResponseError::not_supported(
FEATURE_DOWNLOADS_DIRECT_GET,
"direct_get requires an object store that can presign object reads; \
this deployment's endpoint cannot, so every read is proxied and \
bounded by `download.max_content_bytes`",
));
};
let target = state
.reader
.direct_download_target(&namespace_id, request.path.as_str(), request.revision_no)
.await
.map_err(|error| ApiResponseError::runtime_for_namespace(&namespace_id, error))?;
let signed = issuer
.presign_get(
PresignedGetRequest {
object_key: &target.object_key,
expires_in: DIRECT_GET_URL_TTL,
},
presign_time(),
)
.map_err(presign_issuer_error)?;
Ok(Json(BeginDownloadResponse {
namespace_id,
absolute_path: target.absolute_path,
revision_no: target.revision_no,
content_ref: target.content_ref,
access: ObjectTransferAccess::PresignedUrl {
method: signed.method,
url: signed.url,
headers: signed.headers,
expires_at_ms: signed.expires_at_ms,
},
}))
}