removed original name
This commit is contained in:
@@ -37,6 +37,6 @@ pub async fn delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.storage.delete(&id).await;
|
state.storage.delete(&id).await;
|
||||||
tracing::info!("deleted id={id} name={:?}", meta.original_name);
|
tracing::info!("deleted id={id} name={:?}", meta.download_name);
|
||||||
json(&serde_json::json!({ "ok": true }), StatusCode::OK)
|
json(&serde_json::json!({ "ok": true }), StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub async fn handle_download(
|
|||||||
.query()
|
.query()
|
||||||
.map(|q| q.split('&').any(|p| p == "e=1"))
|
.map(|q| q.split('&').any(|p| p == "e=1"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
return render_password_form(&key, &meta.original_name, error);
|
return render_password_form(&key, &meta.download_name, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path = state.storage.file_path(&key);
|
let file_path = state.storage.file_path(&key);
|
||||||
@@ -65,7 +65,7 @@ pub async fn handle_download(
|
|||||||
} else {
|
} else {
|
||||||
"inline"
|
"inline"
|
||||||
};
|
};
|
||||||
let filename = security::download_file_name(&key, &meta.original_name);
|
let filename = security::renamed_file_name(&key, &meta.mime_type);
|
||||||
|
|
||||||
let builder = Response::builder()
|
let builder = Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ pub async fn upload(
|
|||||||
let mut multipart =
|
let mut multipart =
|
||||||
multer::Multipart::with_constraints(body.into_data_stream(), boundary, constraints);
|
multer::Multipart::with_constraints(body.into_data_stream(), boundary, constraints);
|
||||||
|
|
||||||
let mut filename = String::new();
|
|
||||||
let mut mime = "application/octet-stream".to_string();
|
let mut mime = "application/octet-stream".to_string();
|
||||||
let mut size: u64 = 0;
|
let mut size: u64 = 0;
|
||||||
let mut has_file = false;
|
let mut has_file = false;
|
||||||
@@ -68,7 +67,6 @@ pub async fn upload(
|
|||||||
if field.file_name().is_some() {
|
if field.file_name().is_some() {
|
||||||
if !has_file {
|
if !has_file {
|
||||||
has_file = true;
|
has_file = true;
|
||||||
filename = field.file_name().unwrap_or("").to_string();
|
|
||||||
let ct = field
|
let ct = field
|
||||||
.content_type()
|
.content_type()
|
||||||
.map(|m| m.as_ref().to_string())
|
.map(|m| m.as_ref().to_string())
|
||||||
@@ -172,7 +170,8 @@ pub async fn upload(
|
|||||||
return json_error(StatusCode::BAD_REQUEST, "no file part in the upload");
|
return json_error(StatusCode::BAD_REQUEST, "no file part in the upload");
|
||||||
}
|
}
|
||||||
|
|
||||||
let original_name = security::sanitize_original_name(&filename);
|
// The original client filename is discarded. The file's identity is the
|
||||||
|
// server-generated id plus a mime-derived extension.
|
||||||
let password_hash = if !password.trim().is_empty() {
|
let password_hash = if !password.trim().is_empty() {
|
||||||
security::hash_password(&password)
|
security::hash_password(&password)
|
||||||
} else {
|
} else {
|
||||||
@@ -180,11 +179,11 @@ pub async fn upload(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let download_name = security::download_file_name(&id, &original_name);
|
let download_name = security::renamed_file_name(&id, &mime);
|
||||||
|
|
||||||
let meta = FileMeta {
|
let meta = FileMeta {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
original_name: original_name.clone(),
|
download_name: download_name.clone(),
|
||||||
size,
|
size,
|
||||||
mime_type: mime.clone(),
|
mime_type: mime.clone(),
|
||||||
download_key_hash: security::hash_secret(&id),
|
download_key_hash: security::hash_secret(&id),
|
||||||
@@ -206,14 +205,13 @@ pub async fn upload(
|
|||||||
None => format!("/{download_name}"),
|
None => format!("/{download_name}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("upload ok id={id} size={size} name={original_name:?}");
|
tracing::info!("upload ok id={id} size={size} name={download_name:?}");
|
||||||
|
|
||||||
json(
|
json(
|
||||||
&serde_json::json!({
|
&serde_json::json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"key": id,
|
"key": id,
|
||||||
"url": url,
|
"url": url,
|
||||||
"originalName": original_name,
|
|
||||||
"downloadName": download_name,
|
"downloadName": download_name,
|
||||||
"size": size,
|
"size": size,
|
||||||
"mimeType": mime,
|
"mimeType": mime,
|
||||||
|
|||||||
+39
-21
@@ -177,21 +177,6 @@ pub fn parse_auth_token(token: &str, secret: &str) -> Option<String> {
|
|||||||
Some(key.to_string())
|
Some(key.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sanitize_original_name(name: &str) -> String {
|
|
||||||
let base = name.replace('\\', "/");
|
|
||||||
let base = base.rsplit('/').next().unwrap_or("");
|
|
||||||
let cleaned: String = base
|
|
||||||
.chars()
|
|
||||||
.map(|c| if (c as u32) < 0x20 || c == '\u{7f}' { '_' } else { c })
|
|
||||||
.collect();
|
|
||||||
let cleaned = cleaned.trim();
|
|
||||||
if cleaned.is_empty() {
|
|
||||||
"file".to_string()
|
|
||||||
} else {
|
|
||||||
cleaned.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sanitize_mime(value: &str) -> String {
|
pub fn sanitize_mime(value: &str) -> String {
|
||||||
let cleaned: String = value
|
let cleaned: String = value
|
||||||
.chars()
|
.chars()
|
||||||
@@ -235,15 +220,48 @@ pub fn content_disposition(name: &str, disposition: &str) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_file_name(key: &str, original_name: &str) -> String {
|
/// Derive a short, safe file extension from a MIME type. Used to build the
|
||||||
match original_name.rfind('.') {
|
/// public download name from the server-generated id, so the client-provided
|
||||||
Some(dot) if dot > 0 && dot < original_name.len() - 1 => {
|
/// original filename is never relied upon (or preserved).
|
||||||
format!("{key}.{}", &original_name[dot + 1..])
|
pub fn extension_from_mime(mime_type: &str) -> &'static str {
|
||||||
}
|
let mime = mime_type.split(';').next().unwrap_or("").trim().to_ascii_lowercase();
|
||||||
_ => key.to_string(),
|
match mime.as_str() {
|
||||||
|
"image/jpeg" => "jpg",
|
||||||
|
"image/png" => "png",
|
||||||
|
"image/gif" => "gif",
|
||||||
|
"image/webp" => "webp",
|
||||||
|
"image/bmp" => "bmp",
|
||||||
|
"image/tiff" => "tiff",
|
||||||
|
"image/svg+xml" => "svg",
|
||||||
|
"video/mp4" => "mp4",
|
||||||
|
"video/webm" => "webm",
|
||||||
|
"video/quicktime" => "mov",
|
||||||
|
"video/x-msvideo" => "avi",
|
||||||
|
"audio/mpeg" => "mp3",
|
||||||
|
"audio/ogg" => "ogg",
|
||||||
|
"audio/wav" => "wav",
|
||||||
|
"audio/aac" => "aac",
|
||||||
|
"audio/flac" => "flac",
|
||||||
|
"application/pdf" => "pdf",
|
||||||
|
"application/zip" => "zip",
|
||||||
|
"application/gzip" => "gz",
|
||||||
|
"application/x-tar" => "tar",
|
||||||
|
"application/json" => "json",
|
||||||
|
"text/plain" => "txt",
|
||||||
|
"text/csv" => "csv",
|
||||||
|
"text/html" => "html",
|
||||||
|
_ => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the public/on-disk download name from the server-generated id plus a
|
||||||
|
/// mime-derived extension. This is the renamed identity of the file — the
|
||||||
|
/// original client filename is deliberately discarded.
|
||||||
|
pub fn renamed_file_name(key: &str, mime_type: &str) -> String {
|
||||||
|
let ext = extension_from_mime(mime_type);
|
||||||
|
if ext.is_empty() { key.to_string() } else { format!("{key}.{ext}") }
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a file should render inline in the browser rather than be forced as
|
/// Whether a file should render inline in the browser rather than be forced as
|
||||||
/// a download. Restricted to inert media types so the XSS protection is kept.
|
/// a download. Restricted to inert media types so the XSS protection is kept.
|
||||||
pub fn should_inline(mime_type: &str) -> bool {
|
pub fn should_inline(mime_type: &str) -> bool {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::security::is_valid_key;
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FileMeta {
|
pub struct FileMeta {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub original_name: String,
|
pub download_name: String,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub mime_type: String,
|
pub mime_type: String,
|
||||||
pub download_key_hash: String,
|
pub download_key_hash: String,
|
||||||
|
|||||||
@@ -53,8 +53,7 @@
|
|||||||
id: data.id,
|
id: data.id,
|
||||||
key: data.key,
|
key: data.key,
|
||||||
url: data.url,
|
url: data.url,
|
||||||
originalName: data.originalName,
|
downloadName: data.downloadName || '',
|
||||||
downloadName: data.downloadName || data.originalName,
|
|
||||||
mimeType: data.mimeType || '',
|
mimeType: data.mimeType || '',
|
||||||
size: data.size,
|
size: data.size,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
async function onDelete(item) {
|
async function onDelete(item) {
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
`Delete "${item.originalName || item.downloadName}" permanently? This cannot be undone.`
|
`Delete "${item.downloadName || 'file'}" permanently? This cannot be undone.`
|
||||||
);
|
);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
let copied = false;
|
let copied = false;
|
||||||
|
|
||||||
$: name = item.originalName || item.downloadName || 'file';
|
$: name = item.downloadName || 'file';
|
||||||
|
|
||||||
async function copy() {
|
async function copy() {
|
||||||
await copyText(downloadUrl(item));
|
await copyText(downloadUrl(item));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
let copied = false;
|
let copied = false;
|
||||||
|
|
||||||
$: url = result ? downloadUrl(result) : '';
|
$: url = result ? downloadUrl(result) : '';
|
||||||
$: name = result ? result.originalName || result.downloadName || 'file' : '';
|
$: name = result ? result.downloadName || 'file' : '';
|
||||||
|
|
||||||
async function copy() {
|
async function copy() {
|
||||||
await copyText(url);
|
await copyText(url);
|
||||||
|
|||||||
+19
-1
@@ -1,9 +1,27 @@
|
|||||||
|
// Generate a short, unguessable lowercase-alphanumeric name (client side),
|
||||||
|
// keeping the original extension so the backend can derive a cosmetic name.
|
||||||
|
function randomRename(name) {
|
||||||
|
const charset = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let id = '';
|
||||||
|
const bytes = crypto.getRandomValues(new Uint8Array(8));
|
||||||
|
for (let i = 0; i < 8; i += 1) id += charset[bytes[i] % charset.length];
|
||||||
|
const dot = name.lastIndexOf('.');
|
||||||
|
const ext = dot > 0 && dot < name.length - 1 ? name.slice(dot) : '';
|
||||||
|
return `${id}${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function uploadFile(file, password, onProgress) {
|
export function uploadFile(file, password, onProgress) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
|
|
||||||
|
// Rename the file client side before uploading: guard against the
|
||||||
|
// original name leaking and collisions. The backend re-asserts its own
|
||||||
|
// random id as the authoritative stored name regardless.
|
||||||
|
const renamed = new File([file], randomRename(file.name), { type: file.type });
|
||||||
|
|
||||||
if (password) form.append('password', password);
|
if (password) form.append('password', password);
|
||||||
form.append('file', file);
|
form.append('file', renamed);
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', (e) => {
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total);
|
if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function forcedDownloadUrl(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function displayName(item) {
|
export function displayName(item) {
|
||||||
return item.downloadName || item.originalName || 'file';
|
return item.downloadName || 'file';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileExtension(item) {
|
export function fileExtension(item) {
|
||||||
@@ -20,7 +20,7 @@ export function fileExtension(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function linkLabel(item) {
|
export function linkLabel(item) {
|
||||||
return item.originalName || item.downloadName || 'file';
|
return item.downloadName || 'file';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildLinkFormats(item) {
|
export function buildLinkFormats(item) {
|
||||||
|
|||||||
Reference in New Issue
Block a user