Skip to main content

canvas_lms_api/
upload.rs

1// Two-step Canvas file upload.
2// Step 1: POST metadata to obtain upload_url + upload_params.
3// Step 2: POST multipart form to upload_url (no auth header).
4
5use crate::{error::Result, resources::file::File};
6use reqwest::{
7    multipart::{Form, Part},
8    Client,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Default, Serialize)]
14pub struct UploadRequest {
15    pub name: String,
16    pub size: u64,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub content_type: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub parent_folder_id: Option<u64>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub parent_folder_path: Option<String>,
23}
24
25#[derive(Debug, Deserialize)]
26struct UploadIntent {
27    upload_url: String,
28    upload_params: HashMap<String, String>,
29}
30
31/// Step 2: POST multipart form data to `upload_url` without an Authorization header.
32/// Strips the `while(1);` anti-CSRF prefix from the response body if present.
33pub(crate) async fn execute_upload(
34    client: &Client,
35    upload_url: &str,
36    upload_params: HashMap<String, String>,
37    filename: String,
38    content_type: String,
39    data: Vec<u8>,
40) -> Result<File> {
41    // Build multipart form: upload_params first, then the file field (must be last).
42    let mut form = Form::new();
43    for (key, value) in upload_params {
44        form = form.text(key, value);
45    }
46
47    // Build the file part, applying MIME type if valid.
48    let file_part = Part::bytes(data.clone()).file_name(filename.clone());
49    let file_part = file_part
50        .mime_str(&content_type)
51        .unwrap_or_else(|_| Part::bytes(data).file_name(filename));
52
53    form = form.part("file", file_part);
54
55    let resp = client.post(upload_url).multipart(form).send().await?;
56
57    let body = resp.text().await?;
58
59    // Canvas sometimes prefixes its JSON with `while(1);` to prevent CSRF via JSON hijacking.
60    let json_str = body.strip_prefix("while(1);").unwrap_or(&body);
61
62    let file: File = serde_json::from_str(json_str)?;
63    Ok(file)
64}
65
66/// Full two-step upload: POST metadata to Canvas to get upload intent, then upload the file.
67pub(crate) async fn initiate_and_upload(
68    requester: &crate::http::Requester,
69    endpoint: &str,
70    request: UploadRequest,
71    data: Vec<u8>,
72) -> Result<File> {
73    // Serialize UploadRequest as flat params (no bracket notation) for Canvas step 1.
74    let params = crate::params::flatten_params(&serde_json::to_value(&request).unwrap());
75
76    let intent: UploadIntent = requester.post(endpoint, &params).await?;
77
78    // Determine content_type for the multipart part.
79    let content_type = request
80        .content_type
81        .unwrap_or_else(|| "application/octet-stream".to_string());
82
83    execute_upload(
84        &requester.client,
85        &intent.upload_url,
86        intent.upload_params,
87        request.name,
88        content_type,
89        data,
90    )
91    .await
92}