From 44290f46fe61ef5c7f684754048b29d4461fe16a Mon Sep 17 00:00:00 2001 From: E44 <129310925+programmer-44@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:22:59 +0100 Subject: [PATCH] chore(control + display): improve api urls, paths and file_names --- control/frontend/src/lib/ts/api_handler.ts | 5 ++- .../src/lib/ts/file_transfer_handler.ts | 45 +++++++++++++++---- control/frontend/src/lib/ts/utils.ts | 21 ++++++--- display/web/main.go | 15 +++++-- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/control/frontend/src/lib/ts/api_handler.ts b/control/frontend/src/lib/ts/api_handler.ts index 3e33aa7..c5c1027 100755 --- a/control/frontend/src/lib/ts/api_handler.ts +++ b/control/frontend/src/lib/ts/api_handler.ts @@ -8,6 +8,7 @@ import { type TreeElement } from './types'; import { dev } from '$app/environment'; +import { get_sanitized_file_url } from './utils'; export async function get_screenshot(ip: string): Promise { const options = { method: 'PATCH' }; @@ -18,7 +19,7 @@ export async function get_screenshot(ip: string): Promise { export async function open_file(ip: string, path_to_file: string): Promise { const options = { method: 'PATCH', headers: { 'content-type': 'application/octet-stream' } }; - await request_display(ip, `/file${path_to_file}`, options); + await request_display(ip, get_sanitized_file_url(path_to_file), options); } export async function send_keyboard_input(ip: string, key: string): Promise { @@ -165,7 +166,7 @@ export async function show_blackscreen(ip: string): Promise { export async function get_thumbnail_blob(ip: string, path_to_file: string): Promise { const raw_response = await request_display( ip, - `/file/preview${path_to_file}`, + get_sanitized_file_url(path_to_file, true), { method: 'GET' }, [415] ); diff --git a/control/frontend/src/lib/ts/file_transfer_handler.ts b/control/frontend/src/lib/ts/file_transfer_handler.ts index 5ad00d8..a5818e4 100644 --- a/control/frontend/src/lib/ts/file_transfer_handler.ts +++ b/control/frontend/src/lib/ts/file_transfer_handler.ts @@ -1,7 +1,11 @@ import { dev } from '$app/environment'; import { db } from './files_display.db'; import { get_display_by_id } from './stores/displays'; -import { remove_all_files_without_display, remove_file_from_display } from './stores/files'; +import { + get_current_folder_elements, + remove_all_files_without_display, + remove_file_from_display +} from './stores/files'; import { notifications } from './stores/notification'; import { generate_thumbnail } from './stores/thumbnails'; import { @@ -10,7 +14,7 @@ import { type FileTransferTask, type Inode } from './types'; -import { get_uuid, make_valid_name } from './utils'; +import { get_sanitized_file_url, get_uuid, make_valid_name } from './utils'; let is_processing: boolean = false; const tasks: FileTransferTask[] = []; @@ -22,8 +26,14 @@ export async function add_upload( ) { if (file_list.length === 0) return; + const used_file_names: string[] = await ( + await get_current_folder_elements(current_file_path, selected_display_ids) + ).map((e) => e.name); + for (const file of file_list) { - const file_name = make_valid_name(file.name); + const file_name = generate_valid_file_name(file.name, used_file_names); + used_file_names.push(file_name); + const db_file: Inode = { path: current_file_path, name: file_name, @@ -67,6 +77,28 @@ export async function add_upload( await start_task_processing(); } +function generate_valid_file_name(original_file_name: string, used_file_names: string[]): string { + const regex = /\s\((\d{1,3})\)$/; + + let name: string = make_valid_name(original_file_name); + while (used_file_names.includes(name)) { + const last_dot = name.lastIndexOf('.'); + let name_without_extension = last_dot > 0 ? name.slice(0, last_dot) : name; + const extension = last_dot > 0 ? name.slice(last_dot) : ''; + if (!regex.test(name_without_extension)) { + name_without_extension += ' (1)'; + } else { + const match = name_without_extension.match(regex); + const current_number: number = match ? Number(match[1]) : 0; + console.log("current", current_number) + name_without_extension = name_without_extension.replace(regex, ` (${current_number + 1})`); + } + name = name_without_extension + extension; + console.log(name) + } + return name; +} + async function start_task_processing() { if (!is_processing) { is_processing = tasks.length !== 0; @@ -94,9 +126,6 @@ async function start_task_loop() { } } -function file_url(ip: string, path: string, file_name: string) { - return `http://${ip}:1323/api/file${path}${encodeURIComponent(file_name)}`; -} async function upload(task: FileTransferTask): Promise { if (task.type !== 'upload' || !task.file) return; @@ -108,7 +137,7 @@ async function upload(task: FileTransferTask): Promise { return new Promise((resolve) => { const xhr = new XMLHttpRequest(); - xhr.open('POST', file_url(task.display_ip, task.path, task.file_name), true); + xhr.open('POST', `http://${task.display_ip}:1323/api${get_sanitized_file_url(task.path + task.file_name)}`, true); xhr.setRequestHeader('content-type', 'application/octet-stream'); xhr.upload.onprogress = (e) => { @@ -152,7 +181,7 @@ async function upload(task: FileTransferTask): Promise { if (!!inode_element && inode_element.thumbnail === null) { await generate_thumbnail(task.display_ip, task.path, inode_element); } - }, 0); + }, 10); }); } diff --git a/control/frontend/src/lib/ts/utils.ts b/control/frontend/src/lib/ts/utils.ts index 34ccf40..b7324eb 100644 --- a/control/frontend/src/lib/ts/utils.ts +++ b/control/frontend/src/lib/ts/utils.ts @@ -7,7 +7,7 @@ const supported_file_types: Record = supported_file_t >; const invalid_first_regex = /^[^a-zA-ZäöüßÄÖÜ0-9_]/u; -const invalid_rest_regex = /[^a-zA-ZäöüßÄÖÜ0-9 _\-()$${}.,+$€!?%&=/]/gu; +const invalid_rest_regex = /[^a-zA-ZäöüßÄÖÜ0-9 _\-()$${}.,+$€!=/]/gu; export function is_valid_name(input: string): boolean { return !invalid_rest_regex.test(input) && first_letter_is_valid(input); @@ -19,10 +19,10 @@ export function first_letter_is_valid(input: string): boolean { } export function make_valid_name(input: string): string { - const s = input.normalize("NFC"); + const s = input.normalize('NFC'); if (s.length === 0) return 'Datei'; - let out = s.replace(invalid_rest_regex, "_"); - out = out.replace(invalid_first_regex, "_"); + let out = s.replace(invalid_rest_regex, '_'); + out = out.replace(invalid_first_regex, '_'); return out; } @@ -44,7 +44,9 @@ export function get_file_type(file: Inode): SupportedFileType | null { } export function get_accepted_file_type_string(): string { - return Object.values(supported_file_types).map((e) => e.mime_type).join(','); + return Object.values(supported_file_types) + .map((e) => e.mime_type) + .join(','); } export function get_uuid(): string { @@ -102,3 +104,12 @@ export function display_status_to_info(status: DisplayStatus): string { return '???'; } } + +export function get_sanitized_file_url(file_path: string, is_preview = false) { + const pathSegments = file_path + .split('/') + .filter(Boolean) + .map((seg) => encodeURIComponent(seg)); + + return `/file/${is_preview ? 'preview/' : ''}${[...pathSegments].join('/')}`; +} diff --git a/display/web/main.go b/display/web/main.go index 24b9345..e6eefd6 100644 --- a/display/web/main.go +++ b/display/web/main.go @@ -10,6 +10,7 @@ import ( "log/slog" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -146,12 +147,20 @@ func qrRoute(c echo.Context) error { func extractFilePathMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) error { - pathParam := ctx.Param("path") - fullPath, exists, err := pkg.ResolveStorageFilePath(pathParam) + raw := ctx.Param("path") + + decoded, err := url.PathUnescape(raw) if err != nil { - slog.Warn("Failed to validate file path", "path", pathParam, "error", err) + slog.Warn("Invalid path encoding", "path", raw, "error", err) return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Description: "Invalid file path"}) } + + fullPath, exists, err := pkg.ResolveStorageFilePath(decoded) + if err != nil { + slog.Warn("Failed to validate file path", "path", decoded, "error", err) + return ctx.JSON(http.StatusBadRequest, shared.ErrorResponse{Description: "Invalid file path"}) + } + ctx.Set("fullPath", fullPath) ctx.Set("fileExists", exists) return next(ctx)