sys/sysmod/http/
upload.rs1use super::{ActixError, WebResult};
6use crate::taskserver::Control;
7use actix_multipart::{Multipart, MultipartError};
8use actix_web::{HttpResponse, Responder, http::header::ContentType, web};
9use anyhow::{Context, Result, anyhow, ensure};
10use log::{error, info, trace, warn};
11use std::path::Path;
12use tokio::{fs::File, io::AsyncWriteExt, process::Command};
13use tokio_stream::StreamExt;
14
15const FILE_NAME_MAX_LEN: usize = 32;
16const TMP_FILE_NAME: &str = "upload.tmp~";
17const UPLOAD_FILE_LIMIT_MB: usize = 4 << 30;
19const UPLOAD_TOTAL_LIMIT_MB: usize = 32 << 30;
20
21static FS_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
25
26#[actix_web::get("/upload/")]
27async fn index_get() -> impl Responder {
28 info!("GET /upload/");
29 let body = include_str!(concat!(
30 env!("CARGO_MANIFEST_DIR"),
31 "/res/http/upload/index.html"
32 ));
33
34 HttpResponse::Ok()
35 .content_type(ContentType::html())
36 .body(body)
37}
38
39#[actix_web::post("/upload/")]
40async fn index_post(mut payload: Multipart, ctrl: web::Data<Control>) -> WebResult {
41 let res = index_post_main(&mut payload, ctrl).await;
42
43 loop {
45 match payload.try_next().await {
46 Ok(res) => {
47 if res.is_none() {
49 break;
50 }
51 }
52 Err(e) => {
53 warn!("Upload: Error while drain: {e}");
54 break;
55 }
56 }
57 }
58
59 res
60}
61
62async fn index_post_main(payload: &mut Multipart, ctrl: web::Data<Control>) -> WebResult {
64 info!("POST /upload/");
65
66 let (dir, _flimit, _tlimit) = {
67 let http = ctrl.sysmods().http.lock().await;
68 let config = &http.config;
69 (
70 config.upload_dir.clone(),
71 UPLOAD_FILE_LIMIT_MB,
72 UPLOAD_TOTAL_LIMIT_MB,
73 )
74 };
75 let dir = Path::new(&dir);
76 let tmppath = dir.join(TMP_FILE_NAME);
77
78 let fs_lock = match FS_LOCK.try_lock() {
81 Ok(lock) => lock,
82 Err(_) => return Err(ActixError::new("Upload is busy", 503)),
83 };
84
85 info!("Upload: create all: {}", dir.to_string_lossy());
87 std::fs::create_dir_all(dir).context("Failed to create upload dir")?;
88
89 if let Some(mut field) = conv_mperror(payload.try_next().await)? {
91 info!("Upload: multipart/form-data entry");
92
93 let cont_disp = field
95 .content_disposition()
96 .ok_or_else(|| ActixError::new("Content-Disposition required", 400))?;
97 let fname = cont_disp.get_filename();
98 let fname = check_file_name(fname)?;
99 info!("Upload: filename: {fname}");
100 let dstpath = dir.join(fname);
101
102 info!("Upload: create: {}", tmppath.to_string_lossy());
104 let mut tmpf = File::create(&tmppath)
105 .await
106 .context("Failed to create temp file")?;
107
108 let mut total = 0;
110 while let Some(chunk) = conv_mperror(field.try_next().await)? {
111 tmpf.write(&chunk).await.context("Write error")?;
112 total += chunk.len();
113 trace!("{total} B received");
114 if total > UPLOAD_FILE_LIMIT_MB << 20 {
115 return Err(ActixError::new("File size is too large", 413));
116 }
117 }
118 info!("{total} B received");
119
120 let dirsize = get_disk_usage(&dir.to_string_lossy()).await?;
121 info!("Upload: du: {dirsize}");
122
123 if dirsize <= UPLOAD_TOTAL_LIMIT_MB << 20 {
124 info!(
126 "Upload: rename from {} to {}",
127 tmppath.to_string_lossy(),
128 dstpath.to_string_lossy()
129 );
130 tokio::fs::rename(&tmppath, &dstpath)
131 .await
132 .context("Rename failed")?;
133 } else {
134 error!("Upload: total size over");
136 info!("Upload: remove {}", tmppath.to_string_lossy());
137 tokio::fs::remove_file(&tmppath)
138 .await
139 .context("Remove failed")?;
140 return Err(ActixError::new("Insufficient storage", 507));
141 }
142
143 } else {
145 return Err(ActixError::new("File data required", 400));
146 }
147
148 drop(fs_lock);
150
151 Ok(HttpResponse::Ok()
152 .content_type(ContentType::plaintext())
153 .body(""))
154}
155
156fn conv_mperror<T>(res: Result<T, MultipartError>) -> Result<T, anyhow::Error> {
159 match res {
160 Ok(x) => Ok(x),
161 Err(e) => Err(anyhow!(e.to_string())),
162 }
163}
164
165fn check_file_name(name: Option<&str>) -> Result<&str, ActixError> {
172 let name = name.unwrap_or("");
173 if name.is_empty() {
174 Err(ActixError::new("No file name", 400))
175 } else if name.len() > FILE_NAME_MAX_LEN {
176 Err(ActixError::new("File name too long", 400))
177 } else if name
178 .chars()
179 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
180 && !name.starts_with('.')
181 {
182 Ok(name)
183 } else {
184 Err(ActixError::new("Invalid file name", 400))
185 }
186}
187
188async fn get_disk_usage(dirpath: &str) -> Result<usize> {
189 let mut cmd = Command::new(format!("du -s -B 1 {dirpath}"));
190 let output = cmd.output().await?;
191 ensure!(output.status.success(), "du command failed");
192
193 let stdout = String::from_utf8_lossy(&output.stdout);
195 let token = stdout
196 .split_ascii_whitespace()
197 .next()
198 .context("du parse error")?;
199 let size = token.parse().context("du parse error")?;
200
201 Ok(size)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn check_file_name_ok() {
210 assert!(check_file_name(Some("ok.txt")).is_ok());
211 }
212
213 #[test]
214 fn check_file_name_empty() {
215 assert!(check_file_name(None).is_err());
216 assert!(check_file_name(Some("")).is_err());
217 }
218
219 #[test]
220 fn check_file_name_long() {
221 let long_name = "012345678901234567890123456789.txt";
222 assert!(check_file_name(Some(long_name)).is_err());
223 }
224
225 #[test]
226 fn check_file_name_invalid() {
227 assert!(check_file_name(Some("あ.txt")).is_err());
228 }
229
230 #[test]
231 fn check_file_name_tmpfile() {
232 assert!(check_file_name(Some(TMP_FILE_NAME)).is_err());
233 }
234}