Skip to main content

gproxy_protocol/adapt/files/
copy.rs

1use crate::{
2    WireRequest,
3    adapt::{
4        JsonInvocation,
5        files::{UploadError, upload_multipart_json},
6    },
7    capability::{ResourceAccess, ResourceReference, Upstream},
8    codec::{self, CodecLimits},
9    connection::{HttpBody, MultipartPart},
10    transform::TransformError,
11    wire::DeclaredFields,
12};
13
14/// Downloads a source resource and sends an actual selected Files upload.
15/// This does not use ResourceAccess::publish or treat publication as file upload.
16/// Source is fully bounded/preflighted before the target side effect begins.
17#[allow(clippy::too_many_arguments)] // Separate source/destination capabilities and scoped targets.
18pub async fn copy_to_multipart<
19    A: ResourceAccess,
20    U: Upstream,
21    O: serde::de::DeserializeOwned + DeclaredFields,
22>(
23    access: &A,
24    source_scope: &A::Scope,
25    source: &ResourceReference,
26    upstream: &U,
27    target: &U::Target,
28    template: WireRequest<()>,
29    boundary: String,
30    mut fields: Vec<MultipartPart>,
31    filename: Option<String>,
32    limits: CodecLimits,
33) -> Result<JsonInvocation<O>, UploadError> {
34    super::path(&template.path)?;
35    let read = access
36        .read(source_scope, source)
37        .await
38        .map_err(TransformError::from)?;
39    let filename = filename
40        .or(read.metadata.filename)
41        .ok_or_else(|| TransformError::missing_metadata("file.filename"))?;
42    if filename.contains(['\r', '\n', '\0']) {
43        return Err(TransformError::shape("filename", "invalid header characters").into());
44    }
45    let mime = read
46        .metadata
47        .mime
48        .ok_or_else(|| TransformError::missing_metadata("file.mime"))?;
49    let mut read_limits = limits;
50    read_limits.max_body_bytes = read_limits
51        .max_body_bytes
52        .min(access.limits().read_bytes)
53        .min(limits.max_part_bytes);
54    read_limits.max_buffer_bytes = read_limits.max_buffer_bytes.min(read_limits.max_body_bytes);
55    let bytes = codec::read_http_body(read.body, read_limits)
56        .await
57        .map_err(|e| {
58            TransformError::new(
59                if e.kind() == codec::CodecErrorKind::Limit {
60                    crate::transform::TransformErrorKind::Limit
61                } else {
62                    crate::transform::TransformErrorKind::Host
63                },
64                "file.copy.read",
65                e.to_string(),
66            )
67        })?;
68    if read
69        .metadata
70        .length
71        .is_some_and(|length| length != bytes.len() as u64)
72    {
73        return Err(TransformError::invalid_result(
74            "file.copy.length",
75            "source body differs from declared resource length",
76        )
77        .into());
78    }
79    let escaped = filename.replace('\\', "\\\\").replace('"', "\\\"");
80    let mut headers = http::HeaderMap::new();
81    headers.insert(
82        http::header::CONTENT_DISPOSITION,
83        http::HeaderValue::from_str(&format!("form-data; name=\"file\"; filename=\"{escaped}\""))
84            .map_err(|e| TransformError::shape("filename", e.to_string()))?,
85    );
86    headers.insert(
87        http::header::CONTENT_TYPE,
88        http::HeaderValue::from_str(&mime)
89            .map_err(|e| TransformError::shape("mime", e.to_string()))?,
90    );
91    fields.push(MultipartPart {
92        headers,
93        body: HttpBody::Bytes(bytes),
94    });
95    upload_multipart_json(upstream, target, template, boundary, fields, limits).await
96}