fixes and functions
This commit is contained in:
+10
-17
@@ -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
@@ -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(¬e.updated_at),
|
||||
can_delete_files,
|
||||
can_upload_files,
|
||||
upload_max_size_bytes,
|
||||
global_color,
|
||||
note_color,
|
||||
authorship_mode: resource_editor_settings.authorship_mode,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user