fixes and functions

This commit is contained in:
Mateusz Gruszczyński
2026-08-04 23:21:27 +02:00
parent 6fc408ddf7
commit 4b25085bb5
28 changed files with 306 additions and 330 deletions
+10 -17
View File
@@ -16,7 +16,7 @@ pub async fn upload_pad_file(
Path(slug): Path<String>,
mut multipart: Multipart,
) -> Result<Json<serde_json::Value>, ApiError> {
require_upload_permission(&state, &headers).await?;
let upload_max_size_bytes = require_upload_permission(&state, &headers).await?;
let mut password: Option<String> = None;
let mut access_token: Option<String> = None;
let mut file: Option<(String, Vec<u8>)> = None;
@@ -46,8 +46,8 @@ pub async fn upload_pad_file(
.bytes()
.await
.map_err(|_| ApiError::bad_request("Failed to read the file"))?;
if bytes.len() > state.upload_max_size_bytes {
return Err(ApiError::payload_too_large(state.upload_max_size_bytes));
if bytes.len() > upload_max_size_bytes {
return Err(ApiError::payload_too_large(upload_max_size_bytes));
}
file = Some((filename, bytes.to_vec()));
}
@@ -255,7 +255,7 @@ pub async fn upload_note_file(
Path((workspace_slug, note_slug)): Path<(String, String)>,
mut multipart: Multipart,
) -> Result<Json<serde_json::Value>, ApiError> {
require_upload_permission(&state, &headers).await?;
let upload_max_size_bytes = require_upload_permission(&state, &headers).await?;
let mut password: Option<String> = None;
let mut access_token: Option<String> = None;
let mut file: Option<(String, Vec<u8>)> = None;
@@ -285,8 +285,8 @@ pub async fn upload_note_file(
.bytes()
.await
.map_err(|_| ApiError::bad_request("Failed to read the file"))?;
if bytes.len() > state.upload_max_size_bytes {
return Err(ApiError::payload_too_large(state.upload_max_size_bytes));
if bytes.len() > upload_max_size_bytes {
return Err(ApiError::payload_too_large(upload_max_size_bytes));
}
file = Some((filename, bytes.to_vec()));
}
@@ -507,17 +507,10 @@ pub async fn delete_note_file(
async fn require_upload_permission(
state: &SharedState,
headers: &HeaderMap,
) -> Result<(), ApiError> {
let user = crate::auth::optional_user(state, headers)
.await
.map_err(|error| ApiError::forbidden(&error.message))?;
if user.is_some() {
Ok(())
} else {
Err(ApiError::forbidden(
"Log in with read-write access to upload files.",
))
}
) -> Result<usize, ApiError> {
upload_limit_for_request(state, headers)
.await?
.ok_or_else(|| ApiError::forbidden("File uploads are disabled for guests."))
}
pub async fn download_file(
+31 -2
View File
@@ -173,6 +173,32 @@ async fn has_write_permission(
}
}
async fn upload_limit_for_request(
state: &SharedState,
headers: &HeaderMap,
) -> Result<Option<usize>, ApiError> {
if session_user(state, headers).await?.is_some() {
return Ok(Some(state.upload_max_size_bytes));
}
Ok(state
.guest_upload_enabled
.then_some(state.guest_upload_max_size_bytes))
}
async fn resource_upload_limit(
state: &SharedState,
headers: &HeaderMap,
kind: &str,
slug: &str,
) -> Result<Option<usize>, ApiError> {
let Some(limit) = upload_limit_for_request(state, headers).await? else {
return Ok(None);
};
Ok(has_write_permission(state, headers, kind, slug)
.await?
.then_some(limit))
}
#[derive(Debug, Serialize)]
pub struct PublishResponse {
url: Option<String>,
@@ -380,6 +406,7 @@ pub struct NoteInfo {
updated_at: String,
can_delete_files: bool,
can_upload_files: bool,
upload_max_size_bytes: Option<usize>,
global_color: Option<String>,
note_color: Option<String>,
authorship_mode: String,
@@ -996,8 +1023,9 @@ pub async fn note_info(
has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
let can_manage_authorship = workspace_owner || note_owner || password_write_access;
let can_delete_files = can_manage_authorship;
let can_upload_files = session_user(&state, &headers).await?.is_some()
&& has_write_permission(&state, &headers, "workspace", &workspace_slug).await?;
let upload_max_size_bytes =
resource_upload_limit(&state, &headers, "workspace", &workspace_slug).await?;
let can_upload_files = upload_max_size_bytes.is_some();
let can_save_editor_settings = (personal_editor_settings || can_manage_authorship)
&& has_write_permission(&state, &headers, "workspace", &workspace_slug).await?;
@@ -1024,6 +1052,7 @@ pub async fn note_info(
updated_at: db::normalize_timestamp(&note.updated_at),
can_delete_files,
can_upload_files,
upload_max_size_bytes,
global_color,
note_color,
authorship_mode: resource_editor_settings.authorship_mode,
+4 -2
View File
@@ -43,6 +43,7 @@ pub struct PadInfo {
updated_at: String,
can_delete_files: bool,
can_upload_files: bool,
upload_max_size_bytes: Option<usize>,
global_color: Option<String>,
note_color: Option<String>,
authorship_mode: String,
@@ -135,8 +136,8 @@ pub async fn pad_info(
let guest_owner = pad_creator_is_requester(&headers, &pad);
let password_write_access = has_password_write_access(&state, &headers, "pad", &slug).await?;
let can_manage_authorship = account_owner || guest_owner || password_write_access;
let can_upload_files = session_user(&state, &headers).await?.is_some()
&& has_write_permission(&state, &headers, "pad", &slug).await?;
let upload_max_size_bytes = resource_upload_limit(&state, &headers, "pad", &slug).await?;
let can_upload_files = upload_max_size_bytes.is_some();
let can_save_editor_settings = (personal_editor_settings || can_manage_authorship)
&& has_write_permission(&state, &headers, "pad", &slug).await?;
if pad.is_private == 0
@@ -159,6 +160,7 @@ pub async fn pad_info(
updated_at: db::normalize_timestamp(&pad.updated_at),
can_delete_files: can_manage_authorship,
can_upload_files,
upload_max_size_bytes,
global_color,
note_color,
authorship_mode: resource_editor_settings.authorship_mode,
+2 -4
View File
@@ -46,7 +46,7 @@ impl<B> MakeSpan<B> for PathOnlyMakeSpan {
pub fn router(
state: SharedState,
static_dir: &str,
upload_max_size_bytes: usize,
upload_body_limit_bytes: usize,
asset_cache_max_age_seconds: u64,
) -> Router {
let asset_version = state.asset_version.clone();
@@ -229,9 +229,7 @@ pub fn router(
)
.fallback(not_found)
.method_not_allowed_fallback(method_not_allowed)
.layer(DefaultBodyLimit::max(
upload_max_size_bytes.saturating_add(1024 * 1024),
))
.layer(DefaultBodyLimit::max(upload_body_limit_bytes))
.layer(TraceLayer::new_for_http().make_span_with(PathOnlyMakeSpan))
.layer(middleware::from_fn(require_csrf_token))
.layer(middleware::from_fn(apply_response_header_policy))
+1 -1
View File
@@ -81,7 +81,7 @@ pub fn render_html(
}
pub fn theme_bootstrap() -> &'static str {
r#"<script>(()=>{const key="rustpad:theme";let theme="dark";try{const saved=localStorage.getItem(key);if(saved==="light"||saved==="dark")theme=saved}catch{}const root=document.documentElement;root.dataset.theme=theme;root.style.colorScheme=theme;const meta=document.querySelector('meta[name="color-scheme"]');if(meta)meta.content=theme})();</script>"#
r#"<script>(()=>{const key="rustpad:theme";let theme=matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";try{const saved=localStorage.getItem(key);if(saved==="light"||saved==="dark")theme=saved}catch{}const root=document.documentElement;root.dataset.theme=theme;root.style.colorScheme=theme;const meta=document.querySelector('meta[name="color-scheme"]');if(meta)meta.content=theme})();</script>"#
}
pub fn stylesheet_tag(asset_version: &str, name: &str) -> String {
+73 -8
View File
@@ -55,6 +55,8 @@ pub struct Config {
pub files_dir: String,
pub storage: crate::storage::StorageConfig,
pub upload_max_size_bytes: usize,
pub guest_upload_enabled: bool,
pub guest_upload_max_size_bytes: usize,
pub asset_version: String,
pub asset_cache_max_age_seconds: u64,
pub file_cache_max_age_seconds: u64,
@@ -77,7 +79,9 @@ impl Config {
let host = values.get("APP_HOST", "127.0.0.1").parse()?;
let port = values.get("APP_PORT", "3000").parse()?;
let database_max_connections = values.get("DATABASE_MAX_CONNECTIONS", "8").parse()?;
let upload_max_size_mb: usize = values.get("UPLOAD_MAX_SIZE_MB", "20").parse()?;
let upload_max_size_mb = values.positive_u64("UPLOAD_MAX_SIZE_MB", 20)?;
let guest_upload_enabled = values.bool("GUEST_UPLOAD_ENABLED", false)?;
let guest_upload_max_size_mb = values.positive_u64("GUEST_UPLOAD_MAX_SIZE_MB", 5)?;
let anonymous_access_token_ttl_days =
values.positive_i64("ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", 7)?;
let user_session_ttl_days = values.positive_i64("USER_SESSION_TTL_DAYS", 3)?;
@@ -107,10 +111,6 @@ impl Config {
_ => return Err("STORAGE_DRIVER must be local or s3".into()),
};
if upload_max_size_mb == 0 {
return Err("UPLOAD_MAX_SIZE_MB must be greater than 0".into());
}
let authorization_type = AuthorizationType::from_values(&values)?;
let ldap = match authorization_type {
AuthorizationType::Local => None,
@@ -167,9 +167,12 @@ impl Config {
static_dir: values.get("STATIC_DIR", "static"),
files_dir,
storage,
upload_max_size_bytes: upload_max_size_mb
.checked_mul(1024 * 1024)
.ok_or("UPLOAD_MAX_SIZE_MB is too large")?,
upload_max_size_bytes: megabytes_to_bytes("UPLOAD_MAX_SIZE_MB", upload_max_size_mb)?,
guest_upload_enabled,
guest_upload_max_size_bytes: megabytes_to_bytes(
"GUEST_UPLOAD_MAX_SIZE_MB",
guest_upload_max_size_mb,
)?,
asset_version: env!("CARGO_PKG_VERSION").to_owned(),
asset_cache_max_age_seconds: values
.nonnegative_u64("ASSET_CACHE_MAX_AGE_SECONDS", 600)?,
@@ -213,4 +216,66 @@ impl Config {
}
Ok(())
}
pub fn upload_body_limit_bytes(&self) -> usize {
multipart_body_limit_bytes(
self.upload_max_size_bytes,
self.guest_upload_enabled,
self.guest_upload_max_size_bytes,
)
}
}
fn multipart_body_limit_bytes(
user_limit_bytes: usize,
guest_upload_enabled: bool,
guest_limit_bytes: usize,
) -> usize {
let file_limit = if guest_upload_enabled {
user_limit_bytes.max(guest_limit_bytes)
} else {
user_limit_bytes
};
file_limit.saturating_add(1024 * 1024)
}
fn megabytes_to_bytes(
name: &str,
megabytes: u64,
) -> Result<usize, Box<dyn std::error::Error>> {
let bytes = megabytes
.checked_mul(1024 * 1024)
.ok_or_else(|| format!("{name} is too large"))?;
usize::try_from(bytes).map_err(|_| format!("{name} is too large").into())
}
#[cfg(test)]
mod tests {
use super::{megabytes_to_bytes, multipart_body_limit_bytes};
#[test]
fn converts_upload_megabytes_to_bytes() {
assert_eq!(megabytes_to_bytes("LIMIT", 5).unwrap(), 5 * 1024 * 1024);
}
#[test]
fn rejects_overflowing_upload_limit() {
assert!(megabytes_to_bytes("LIMIT", u64::MAX).is_err());
}
#[test]
fn multipart_limit_uses_user_limit_when_guest_uploads_are_disabled() {
assert_eq!(
multipart_body_limit_bytes(20 * 1024 * 1024, false, 50 * 1024 * 1024),
21 * 1024 * 1024
);
}
#[test]
fn multipart_limit_uses_larger_enabled_guest_limit() {
assert_eq!(
multipart_body_limit_bytes(20 * 1024 * 1024, true, 50 * 1024 * 1024),
51 * 1024 * 1024
);
}
}
+2
View File
@@ -19,6 +19,8 @@ const KNOWN_CONFIG_KEYS: &[&str] = &[
"FILES_PUBLIC_URL",
"STORAGE_DRIVER",
"UPLOAD_MAX_SIZE_MB",
"GUEST_UPLOAD_ENABLED",
"GUEST_UPLOAD_MAX_SIZE_MB",
"ASSET_CACHE_MAX_AGE_SECONDS",
"FILE_CACHE_MAX_AGE_SECONDS",
"REGISTRATION_ENABLED",
+5 -1
View File
@@ -64,6 +64,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
files_dir = %config.files_dir,
storage_driver = match &config.storage { storage::StorageConfig::Local { .. } => "local", storage::StorageConfig::S3 { .. } => "s3" },
upload_max_size_bytes = config.upload_max_size_bytes,
guest_upload_enabled = config.guest_upload_enabled,
guest_upload_max_size_bytes = config.guest_upload_max_size_bytes,
asset_cache_max_age_seconds = config.asset_cache_max_age_seconds,
file_cache_max_age_seconds = config.file_cache_max_age_seconds,
files_public_url = config.files_public_url.as_deref().unwrap_or("application origin"),
@@ -106,6 +108,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
config.asset_version.clone(),
storage,
config.upload_max_size_bytes,
config.guest_upload_enabled,
config.guest_upload_max_size_bytes,
config.file_cache_max_age_seconds,
config.files_public_url.clone(),
config.smtp.clone(),
@@ -166,7 +170,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = app::router(
state,
&config.static_dir,
config.upload_max_size_bytes,
config.upload_body_limit_bytes(),
config.asset_cache_max_age_seconds,
);
let address = SocketAddr::new(config.host, config.port);
+6
View File
@@ -99,6 +99,8 @@ pub struct AppState {
pub asset_version: String,
pub storage: crate::storage::Storage,
pub upload_max_size_bytes: usize,
pub guest_upload_enabled: bool,
pub guest_upload_max_size_bytes: usize,
pub file_cache_max_age_seconds: u64,
pub files_public_url: Option<String>,
pub smtp: Option<SmtpConfig>,
@@ -123,6 +125,8 @@ impl AppState {
asset_version: String,
storage: crate::storage::Storage,
upload_max_size_bytes: usize,
guest_upload_enabled: bool,
guest_upload_max_size_bytes: usize,
file_cache_max_age_seconds: u64,
files_public_url: Option<String>,
smtp: Option<SmtpConfig>,
@@ -140,6 +144,8 @@ impl AppState {
asset_version,
storage,
upload_max_size_bytes,
guest_upload_enabled,
guest_upload_max_size_bytes,
file_cache_max_age_seconds,
files_public_url,
smtp,