aboutsummaryrefslogtreecommitdiffstats
path: root/src/file_upload.rs
blob: 5f9738cb06752d60416a17027b8ae23a6a548a35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use actix_web::{http::header, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use std::{
    io::Write,
    path::{Component, PathBuf},
};

use crate::errors::ContextualError;
use crate::listing::{self};

/// Create future to save file.
async fn save_file(
    field: actix_multipart::Field,
    file_path: PathBuf,
    overwrite_files: bool,
) -> Result<u64, ContextualError> {
    if !overwrite_files && file_path.exists() {
        return Err(ContextualError::DuplicateFileError);
    }

    let file = std::fs::File::create(&file_path).map_err(|e| {
        ContextualError::IoError(format!("Failed to create {}", file_path.display()), e)
    })?;

    let (_, written_len) = field
        .map_err(ContextualError::MultipartError)
        .try_fold((file, 0u64), |(mut file, written_len), bytes| async move {
            file.write_all(bytes.as_ref())
                .map_err(|e| ContextualError::IoError("Failed to write to file".to_string(), e))?;
            Ok((file, written_len + bytes.len() as u64))
        })
        .await?;

    Ok(written_len)
}

/// Create new future to handle file as multipart data.
async fn handle_multipart(
    field: actix_multipart::Field,
    file_path: PathBuf,
    overwrite_files: bool,
) -> Result<u64, ContextualError> {
    let filename = field
        .content_disposition()
        .and_then(|cd| cd.get_filename().map(String::from))
        .ok_or_else(|| {
            ContextualError::ParseError(
                "HTTP header".to_string(),
                "Failed to retrieve the name of the file to upload".to_string(),
            )
        })?;

    match std::fs::metadata(&file_path) {
        Err(_) => Err(ContextualError::InsufficientPermissionsError(
            file_path.display().to_string(),
        )),
        Ok(metadata) if !metadata.is_dir() => Err(ContextualError::InvalidPathError(format!(
            "cannot upload file to {}, since it's not a directory",
            &file_path.display()
        ))),
        Ok(metadata) if metadata.permissions().readonly() => Err(
            ContextualError::InsufficientPermissionsError(file_path.display().to_string()),
        ),
        Ok(_) => Ok(()),
    }?;

    save_file(field, file_path.join(filename), overwrite_files).await
}

/// Handle incoming request to upload file.
/// Target file path is expected as path parameter in URI and is interpreted as relative from
/// server root directory. Any path which will go outside of this directory is considered
/// invalid.
/// This method returns future.
pub async fn upload_file(
    req: HttpRequest,
    payload: actix_web::web::Payload,
) -> Result<HttpResponse, ContextualError> {
    let conf = req.app_data::<crate::MiniserveConfig>().unwrap();
    let return_path = if let Some(header) = req.headers().get(header::REFERER) {
        header.to_str().unwrap_or("/").to_owned()
    } else {
        "/".to_string()
    };

    let query_params = listing::extract_query_parameters(&req);
    let upload_path = query_params.path.as_ref().ok_or_else(|| {
        ContextualError::InvalidHttpRequestError("Missing query parameter 'path'".to_string())
    })?;
    let upload_path = upload_path
        .strip_prefix(Component::RootDir)
        .unwrap_or(upload_path);

    let app_root_dir = conf.path.canonicalize().map_err(|e| {
        ContextualError::IoError("Failed to resolve path served by miniserve".to_string(), e)
    })?;

    // If the target path is under the app root directory, save the file.
    let target_dir = match app_root_dir.join(upload_path).canonicalize() {
        Ok(path) if path.starts_with(&app_root_dir) => Ok(path),
        _ => Err(ContextualError::InvalidHttpRequestError(
            "Invalid value for 'path' parameter".to_string(),
        )),
    }?;

    actix_multipart::Multipart::new(req.headers(), payload)
        .map_err(ContextualError::MultipartError)
        .and_then(|field| handle_multipart(field, target_dir.clone(), conf.overwrite_files))
        .try_collect::<Vec<u64>>()
        .await?;

    Ok(HttpResponse::SeeOther()
        .append_header((header::LOCATION, return_path))
        .finish())
}