Skip to content

Commit 828b613

Browse files
Add optional mode argument to persist_uploaded_file
This change adds an optional `mode` argument to the `persist_uploaded_file` function, allowing users to specify the Unix file permissions in octal notation when saving uploaded files. - Updated `sqlpage.persist_uploaded_file` signature to include `mode`. - Implemented permission setting logic using `std::os::unix::fs::PermissionsExt` (on Unix platforms). - Default permission is set to "600" (octal `0o600`). - Added documentation for the new parameter in `examples/official-site/sqlpage/migrations/39_persist_uploaded_file.sql`, including an explanation of octal notation and a link to Wikipedia. - Added a unit test `test_set_file_mode` to verify the permission setting logic. Co-authored-by: lovasoa <552629+lovasoa@users.noreply.github.com>
1 parent 3eb5274 commit 828b613

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

examples/official-site/sqlpage/migrations/39_persist_uploaded_file.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,14 @@ VALUES (
6262
'Optional. Comma-separated list of allowed file extensions. By default: jpg,jpeg,png,gif,bmp,webp,pdf,txt,doc,docx,xls,xlsx,csv,mp3,mp4,wav,avi,mov.
6363
Changing this may be dangerous ! If you add "sql", "svg" or "html" to the list, an attacker could execute arbitrary SQL queries on your database, or impersonate other users.',
6464
'TEXT'
65+
),
66+
(
67+
'persist_uploaded_file',
68+
4,
69+
'mode',
70+
'Optional. Unix permissions to set on the file, in octal notation. By default, the file will be saved with "600" (read/write for the owner only).
71+
Octal notation works by using three digits from 0 to 7: the first for the owner, the second for the group, and the third for others.
72+
For example, "644" means read/write for the owner, and read-only for others.
73+
[Learn more about numeric notation for file-system permissions on Wikipedia](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation).',
74+
'TEXT'
6575
);

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ super::function_definition_macro::sqlpage_functions! {
4242
link(file: Cow<str>, parameters: Option<Cow<str>>, hash: Option<Cow<str>>);
4343

4444
path((&RequestInfo));
45-
persist_uploaded_file((&RequestInfo), field_name: Cow<str>, folder: Option<Cow<str>>, allowed_extensions: Option<Cow<str>>);
45+
persist_uploaded_file((&RequestInfo), field_name: Cow<str>, folder: Option<Cow<str>>, allowed_extensions: Option<Cow<str>>, mode: Option<Cow<str>>);
4646
protocol((&RequestInfo));
4747

4848
random_string(string_length: SqlPageFunctionParam<usize>);
@@ -420,6 +420,7 @@ async fn persist_uploaded_file<'a>(
420420
field_name: Cow<'a, str>,
421421
folder: Option<Cow<'a, str>>,
422422
allowed_extensions: Option<Cow<'a, str>>,
423+
mode: Option<Cow<'a, str>>,
423424
) -> anyhow::Result<Option<String>> {
424425
let folder = folder.unwrap_or(Cow::Borrowed("uploads"));
425426
let allowed_extensions_str =
@@ -456,6 +457,7 @@ async fn persist_uploaded_file<'a>(
456457
target_path.display()
457458
)
458459
})?;
460+
set_file_mode(&target_path, mode.as_deref()).await?;
459461
// remove the WEB_ROOT prefix from the path, but keep the leading slash
460462
let path = "/".to_string()
461463
+ target_path
@@ -475,6 +477,28 @@ async fn protocol(request: &RequestInfo) -> &str {
475477
&request.protocol
476478
}
477479

480+
async fn set_file_mode(path: &std::path::Path, mode: Option<&str>) -> anyhow::Result<()> {
481+
#[cfg(unix)]
482+
{
483+
use std::os::unix::fs::PermissionsExt;
484+
let mode = if let Some(mode) = mode {
485+
u32::from_str_radix(mode, 8)
486+
.with_context(|| format!("unable to parse file mode {mode:?} as an octal number"))?
487+
} else {
488+
0o600
489+
};
490+
tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
491+
.await
492+
.with_context(|| format!("unable to set permissions on {}", path.display()))?;
493+
}
494+
#[cfg(not(unix))]
495+
{
496+
let _ = path;
497+
let _ = mode;
498+
}
499+
Ok(())
500+
}
501+
478502
/// Returns a random string of the specified length.
479503
pub(crate) async fn random_string(len: usize) -> anyhow::Result<String> {
480504
// OsRng can block on Linux, so we run this on a blocking thread.
@@ -660,6 +684,38 @@ async fn test_hash_password() {
660684
assert!(s.starts_with("$argon2"));
661685
}
662686

687+
#[tokio::test]
688+
async fn test_set_file_mode() {
689+
let tmp_dir = std::env::temp_dir();
690+
let tmp_file = tmp_dir.join("test_set_file_mode.txt");
691+
tokio::fs::write(&tmp_file, b"test").await.unwrap();
692+
693+
#[cfg(unix)]
694+
{
695+
use std::os::unix::fs::PermissionsExt;
696+
697+
set_file_mode(&tmp_file, Some("644")).await.unwrap();
698+
let metadata = tokio::fs::metadata(&tmp_file).await.unwrap();
699+
assert_eq!(metadata.permissions().mode() & 0o777, 0o644);
700+
701+
set_file_mode(&tmp_file, Some("755")).await.unwrap();
702+
let metadata = tokio::fs::metadata(&tmp_file).await.unwrap();
703+
assert_eq!(metadata.permissions().mode() & 0o777, 0o755);
704+
705+
set_file_mode(&tmp_file, None).await.unwrap();
706+
let metadata = tokio::fs::metadata(&tmp_file).await.unwrap();
707+
assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
708+
}
709+
710+
#[cfg(not(unix))]
711+
{
712+
set_file_mode(&tmp_file, Some("644")).await.unwrap();
713+
set_file_mode(&tmp_file, None).await.unwrap();
714+
}
715+
716+
tokio::fs::remove_file(&tmp_file).await.unwrap();
717+
}
718+
663719
async fn uploaded_file_mime_type<'a>(
664720
request: &'a RequestInfo,
665721
upload_name: Cow<'a, str>,

0 commit comments

Comments
 (0)