prevent filling up the disk

This commit is contained in:
xenofem 2024-03-27 16:53:02 -04:00
parent 37695b8bbd
commit 71fe7fed24
5 changed files with 77 additions and 20 deletions

1
Cargo.lock generated
View file

@ -1897,6 +1897,7 @@ dependencies = [
"futures-core", "futures-core",
"inotify", "inotify",
"jsondb", "jsondb",
"libc",
"log", "log",
"mime", "mime",
"mnemonic", "mnemonic",

View file

@ -24,6 +24,7 @@ env_logger = "0.11.3"
futures-core = "0.3" futures-core = "0.3"
inotify = "0.10" inotify = "0.10"
jsondb = "0.4.0" jsondb = "0.4.0"
libc = "0.2"
log = "0.4" log = "0.4"
mime = "0.3.16" mime = "0.3.16"
mnemonic = "1.0.1" mnemonic = "1.0.1"

View file

@ -62,6 +62,11 @@ transbeam is configured with the following environment variables:
case-sensitive.) case-sensitive.)
- `TRANSBEAM_MAX_STORAGE_SIZE`: maximum total size of all files being - `TRANSBEAM_MAX_STORAGE_SIZE`: maximum total size of all files being
stored by transbeam (default: 64G) stored by transbeam (default: 64G)
- `TRANSBEAM_MIN_DISK_FREE`: minimum amount of free disk space on the
filesystem where transbeam uploads are stored (default: 8G).
transbeam will not accept uploads that would reduce free disk space
lower than this value, even if they would otherwise be within the
max upload size and max storage size.
- `TRANSBEAM_MNEMONIC_CODES`: generate memorable download codes using - `TRANSBEAM_MNEMONIC_CODES`: generate memorable download codes using
English words, rather than random alphanumeric strings (default: English words, rather than random alphanumeric strings (default:
true) true)

View file

@ -36,6 +36,7 @@ struct AppData {
struct Config { struct Config {
base_url: String, base_url: String,
max_storage_size: u64, max_storage_size: u64,
min_disk_free: u64,
max_upload_size: u64, max_upload_size: u64,
max_lifetime: u16, max_lifetime: u16,
upload_password: String, upload_password: String,
@ -290,8 +291,8 @@ async fn handle_upload(
req: HttpRequest, req: HttpRequest,
stream: web::Payload, stream: web::Payload,
data: web::Data<AppData>, data: web::Data<AppData>,
) -> impl Responder { ) -> actix_web::Result<impl Responder> {
if data.state.read().await.full(data.config.max_storage_size) { if data.full().await? {
return Ok(HttpResponse::BadRequest().finish()); return Ok(HttpResponse::BadRequest().finish());
} }
let ip_addr = get_ip_addr(&req, data.config.reverse_proxy); let ip_addr = get_ip_addr(&req, data.config.reverse_proxy);
@ -326,16 +327,15 @@ struct UploadLimits {
} }
#[get("/upload/limits.json")] #[get("/upload/limits.json")]
async fn upload_limits(data: web::Data<AppData>) -> impl Responder { async fn upload_limits(data: web::Data<AppData>) -> actix_web::Result<impl Responder> {
let file_store = data.state.read().await; let open = !data.full().await?;
let open = !file_store.full(data.config.max_storage_size); let available_size = data.available_size().await?;
let available_size = file_store.available_size(data.config.max_storage_size);
let max_size = std::cmp::min(available_size, data.config.max_upload_size); let max_size = std::cmp::min(available_size, data.config.max_upload_size);
web::Json(UploadLimits { Ok(web::Json(UploadLimits {
open, open,
max_size, max_size,
max_lifetime: data.config.max_lifetime, max_lifetime: data.config.max_lifetime,
}) }))
} }
fn env_or<T: FromStr>(var: &str, default: T) -> T fn env_or<T: FromStr>(var: &str, default: T) -> T
@ -388,6 +388,8 @@ async fn main() -> std::io::Result<()> {
env_or::<ByteSize>("TRANSBEAM_MAX_UPLOAD_SIZE", ByteSize(16 * bytesize::GB)).as_u64(); env_or::<ByteSize>("TRANSBEAM_MAX_UPLOAD_SIZE", ByteSize(16 * bytesize::GB)).as_u64();
let max_storage_size: u64 = let max_storage_size: u64 =
env_or::<ByteSize>("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64(); env_or::<ByteSize>("TRANSBEAM_MAX_STORAGE_SIZE", ByteSize(64 * bytesize::GB)).as_u64();
let min_disk_free: u64 =
env_or::<ByteSize>("TRANSBEAM_MIN_DISK_FREE", ByteSize(8 * bytesize::GB)).as_u64();
let upload_password: String = env_or_panic("TRANSBEAM_UPLOAD_PASSWORD"); let upload_password: String = env_or_panic("TRANSBEAM_UPLOAD_PASSWORD");
let cachebuster: String = env_or_else("TRANSBEAM_CACHEBUSTER", String::new); let cachebuster: String = env_or_else("TRANSBEAM_CACHEBUSTER", String::new);
let admin_password_hash: PasswordHashString = env_or_panic("TRANSBEAM_ADMIN_PASSWORD_HASH"); let admin_password_hash: PasswordHashString = env_or_panic("TRANSBEAM_ADMIN_PASSWORD_HASH");
@ -423,6 +425,7 @@ async fn main() -> std::io::Result<()> {
base_url, base_url,
max_upload_size, max_upload_size,
max_storage_size, max_storage_size,
min_disk_free,
max_lifetime, max_lifetime,
upload_password, upload_password,
storage_dir, storage_dir,

View file

@ -4,13 +4,14 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use libc::statvfs64;
use log::{debug, error, info}; use log::{debug, error, info};
use rand::{ use rand::{
distributions::{Alphanumeric, DistString}, distributions::{Alphanumeric, DistString},
thread_rng, Rng, thread_rng, Rng,
}; };
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::fs::File; use tokio::fs::{metadata, File};
const MAX_STORAGE_FILES: usize = 1024; const MAX_STORAGE_FILES: usize = 1024;
@ -113,14 +114,14 @@ impl crate::AppData {
key: String, key: String,
entry: StoredFileWithPassword, entry: StoredFileWithPassword,
) -> Result<(), FileAddError> { ) -> Result<(), FileAddError> {
let mut store = self.state.write().await; if self.full().await? {
if store.full(self.config.max_storage_size) {
return Err(FileAddError::Full); return Err(FileAddError::Full);
} }
let available_size = store.available_size(self.config.max_storage_size); let available_size = self.available_size().await?;
if entry.file.size > available_size { if entry.file.size > available_size {
return Err(FileAddError::TooBig(available_size)); return Err(FileAddError::TooBig(available_size));
} }
let mut store = self.state.write().await;
store.0.insert(key, entry); store.0.insert(key, entry);
Ok(()) Ok(())
} }
@ -150,18 +151,64 @@ impl crate::AppData {
} }
Ok(()) Ok(())
} }
}
impl StoredFiles { fn disk_free(&self) -> std::io::Result<u64> {
fn total_size(&self) -> u64 { let storage_path_c = std::ffi::CString::new(
self.0.iter().fold(0, |acc, (_, v)| acc + v.file.size) self.config
.storage_dir
.to_str()
.expect("storage path is not valid unicode"),
)
.expect("storage path contains a null byte");
unsafe {
let mut stat: statvfs64 = std::mem::zeroed();
loop {
if statvfs64(storage_path_c.as_ptr(), std::ptr::addr_of_mut!(stat)) == 0 {
break;
}
let os_error = std::io::Error::last_os_error();
if os_error.kind() != ErrorKind::Interrupted {
return Err(os_error);
}
}
Ok(stat.f_bsize.saturating_mul(stat.f_bavail))
}
} }
pub fn available_size(&self, max_storage_size: u64) -> u64 { async fn pending_data_size(&self) -> std::io::Result<u64> {
max_storage_size.saturating_sub(self.total_size()) let store = self.state.read().await;
let mut total_pending_size = 0u64;
for (key, value) in store.0.iter() {
let file_path = self.config.storage_dir.join(key);
let pending_size = value
.file
.size
.saturating_sub(metadata(&file_path).await?.len());
total_pending_size = total_pending_size.saturating_add(pending_size);
}
Ok(total_pending_size)
} }
pub fn full(&self, max_storage_size: u64) -> bool { async fn total_stored_size(&self) -> u64 {
self.available_size(max_storage_size) == 0 || self.0.len() >= MAX_STORAGE_FILES let store = self.state.read().await;
store.0.iter().fold(0, |acc, (_, v)| acc + v.file.size)
}
pub async fn available_size(&self) -> std::io::Result<u64> {
let available_policy_size = self
.config
.max_storage_size
.saturating_sub(self.total_stored_size().await);
let available_disk_size = self
.disk_free()?
.saturating_sub(self.config.min_disk_free)
.saturating_sub(self.pending_data_size().await?);
Ok(std::cmp::min(available_policy_size, available_disk_size))
}
pub async fn full(&self) -> std::io::Result<bool> {
Ok(self.available_size().await? == 0
|| self.state.read().await.0.len() >= MAX_STORAGE_FILES)
} }
} }