refactor(control): improve file transfer backend data structures

This commit is contained in:
E44
2026-01-05 21:40:11 +01:00
parent 6f0b05ca67
commit 6f0cb7298a
4 changed files with 113 additions and 51 deletions
@@ -10,7 +10,8 @@
supported_file_type_icon, supported_file_type_icon,
type Inode, type Inode,
get_file_primary_key, get_file_primary_key,
type FileOnDisplay type FileOnDisplay,
type FileLoadingData
} from '$lib/ts/types'; } from '$lib/ts/types';
import { import {
@@ -47,7 +48,8 @@
| Observable<{ | Observable<{
is_loading: boolean; is_loading: boolean;
total_percentage: number; total_percentage: number;
display_data: { is_loading: boolean; percentage: number }[]; total_seconds_until_finish: number;
display_data: FileLoadingData[];
}> }>
| undefined = $state(); | undefined = $state();
$effect(() => { $effect(() => {
@@ -198,7 +200,8 @@
): Promise<{ ): Promise<{
is_loading: boolean; is_loading: boolean;
total_percentage: number; total_percentage: number;
display_data: { is_loading: boolean; percentage: number }[]; total_seconds_until_finish: number;
display_data: FileLoadingData[];
}> { }> {
const file_on_display_data: FileOnDisplay[] = await db.files_on_display const file_on_display_data: FileOnDisplay[] = await db.files_on_display
.where('file_primary_key') .where('file_primary_key')
@@ -209,30 +212,40 @@
return { return {
is_loading: true, is_loading: true,
total_percentage: 0, total_percentage: 0,
total_seconds_until_finish: -1,
display_data: [] display_data: []
}; };
} }
const display_data = []; const display_data = [];
let is_loading = false; let is_loading = false;
let percentage_sum = 0; let percentage_sum = 0;
let total_seconds_until_finish = 0;
for (const fod of file_on_display_data) { for (const fod of file_on_display_data) {
if (!is_loading) is_loading = fod.is_loading; if (!!fod.loading_data) {
percentage_sum += fod.percentage; if (!is_loading) is_loading = true;
display_data.push({ percentage_sum += fod.loading_data.percentage;
is_loading: fod.is_loading, total_seconds_until_finish += fod.loading_data.seconds_until_finish;
percentage: fod.percentage display_data.push(fod.loading_data);
}); } else {
percentage_sum += 100;
}
} }
let total_percentage = percentage_sum / display_data.length; let total_percentage = percentage_sum / display_data.length;
return { return {
is_loading, is_loading,
total_percentage, total_percentage,
total_seconds_until_finish,
display_data display_data
}; };
} }
</script> </script>
<div data-testid="inode" class="flex flex-row h-{$current_height.file} w-full {loading_finished ? 'scale-105' : ''} transition-[scale] duration-300"> <div
data-testid="inode"
class="flex flex-row h-{$current_height.file} w-full {loading_finished
? 'scale-105'
: ''} transition-[scale] duration-300"
>
{#if !not_interactable} {#if !not_interactable}
<div class="h-{$current_height.file} aspect-square max-w-15 flex"> <div class="h-{$current_height.file} aspect-square max-w-15 flex">
<Button <Button
@@ -13,7 +13,7 @@ import {
type FileTransferTask, type FileTransferTask,
type Inode type Inode
} from './types'; } from './types';
import { get_sanitized_file_url, get_uuid, make_valid_name } from './utils'; import { get_sanitized_file_url, make_valid_name } from './utils';
let is_processing: boolean = false; let is_processing: boolean = false;
const tasks: FileTransferTask[] = []; const tasks: FileTransferTask[] = [];
@@ -51,20 +51,27 @@ export async function add_upload(
display_id, display_id,
file_primary_key, file_primary_key,
date_created: new Date(), date_created: new Date(),
is_loading: true, loading_data: {
percentage: 0 type: 'upload',
percentage: 0,
bytes_per_second: 0,
seconds_until_finish: -1
}
}; };
await db.files_on_display.put(db_file_on_display); await db.files_on_display.put(db_file_on_display);
return { return {
id: get_uuid(), data: {
type: 'upload' as const, type: 'upload' as const,
display_id, file
display_ip, },
display: {
id: display_id,
ip: display_ip
},
path: current_file_path, path: current_file_path,
file_name: file_name, file_name: file_name,
file_primary_key, file_primary_key,
file,
bytes_total: file.size bytes_total: file.size
}; };
}); });
@@ -108,7 +115,7 @@ async function start_task_processing() {
async function start_task_loop() { async function start_task_loop() {
while (tasks.length > 0) { while (tasks.length > 0) {
const current_task = tasks[0]; const current_task = tasks[0];
switch (current_task.type) { switch (current_task.data.type) {
case 'upload': case 'upload':
await upload(current_task); await upload(current_task);
break; break;
@@ -123,15 +130,16 @@ async function start_task_loop() {
} }
async function upload(task: FileTransferTask): Promise<void> { async function upload(task: FileTransferTask): Promise<void> {
if (task.type !== 'upload' || !task.file) return; const task_data = task.data;
if (task_data.type !== 'upload' || !task_data.file) return;
await db.files_on_display.update([task.display_id, task.file_primary_key], { percentage: 1 }); const start_time = new Date();
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open( xhr.open(
'POST', 'POST',
`http://${task.display_ip}:1323/api${get_sanitized_file_url(task.path + task.file_name)}`, `http://${task.display.ip}:1323/api${get_sanitized_file_url(task.path + task.file_name)}`,
true true
); );
xhr.setRequestHeader('content-type', 'application/octet-stream'); xhr.setRequestHeader('content-type', 'application/octet-stream');
@@ -140,33 +148,39 @@ async function upload(task: FileTransferTask): Promise<void> {
const total = e.lengthComputable ? e.total : task.bytes_total; const total = e.lengthComputable ? e.total : task.bytes_total;
const current_percentage = total > 0 ? Math.round((e.loaded / total) * 100) : 1; const current_percentage = total > 0 ? Math.round((e.loaded / total) * 100) : 1;
const apply = async () => { const apply = async () => {
await db.files_on_display.update([task.display_id, task.file_primary_key], { const prognosed_data = get_prognosed_data(start_time, e.loaded, total);
percentage: current_percentage
await db.files_on_display.update([task.display.id, task.file_primary_key], {
loading_data: {
type: 'upload',
percentage: current_percentage,
bytes_per_second: prognosed_data.bytes_per_second,
seconds_until_finish: prognosed_data.seconds_until_finish
}
}); });
}; };
apply(); apply();
}; };
xhr.onerror = async (e: ProgressEvent) => { xhr.onerror = async (e: ProgressEvent) => {
await show_error('upload', e, task); await show_error(task, e);
resolve(); resolve();
}; };
xhr.onreadystatechange = async () => { xhr.onreadystatechange = async () => {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
if (xhr.status == 200) { if (xhr.status == 200) {
await db.files_on_display.update([task.display_id, task.file_primary_key], { await db.files_on_display.update([task.display.id, task.file_primary_key], {
percentage: 100, loading_data: null
is_loading: false }); // TODO: Stattdessen update machen und gucken, ob die Datei wirklich da ist
});
} else { } else {
await show_error('upload', `HTTP ${xhr.status}`, task); await show_error(task, `HTTP ${xhr.status}`);
} }
resolve(); resolve();
} }
}; };
xhr.send(task.file); xhr.send(task_data.file);
}).then(() => { }).then(() => {
// Generate Thumbnail if not done already // Generate Thumbnail if not done already
setTimeout(async () => { setTimeout(async () => {
@@ -174,23 +188,39 @@ async function upload(task: FileTransferTask): Promise<void> {
JSON.parse(task.file_primary_key) as [string, string, number, string] JSON.parse(task.file_primary_key) as [string, string, number, string]
); );
if (!!inode_element && inode_element.thumbnail === null) { if (!!inode_element && inode_element.thumbnail === null) {
await generate_thumbnail(task.display_ip, task.path, inode_element); await generate_thumbnail(task.display.ip, task.path, inode_element);
} }
}, 10); }, 10);
}); });
} }
async function show_error(
type: 'upload' | 'download' | 'sync', function get_prognosed_data(
error: ProgressEvent | string, start_time: Date,
task: FileTransferTask bytes_done: number,
) { bytes_total: number
): { bytes_per_second: number; seconds_until_finish: number } {
const diff_seconds = (Date.now() - start_time.getTime()) / 1000;
const bytes_per_second = bytes_done / diff_seconds;
const seconds_until_finish = bytes_total / bytes_per_second;
return { bytes_per_second, seconds_until_finish };
}
async function show_error(task: FileTransferTask, error: ProgressEvent | string) {
const task_data = task.data;
notifications.push( notifications.push(
'error', 'error',
`Fehler beim ${type === 'upload' ? 'Dateiupload' : type === 'download' ? 'Dateidownload' : 'Sychronisieren von Dateien'}`, `Fehler beim ${task_data.type === 'upload' ? 'Dateiupload' : task_data.type === 'download' ? 'Dateidownload' : 'Sychronisieren von Dateien'}`,
`Datei: "${task.file_name}", Display-IP: ${task.display_ip}\nFehler: ${error}` `Datei: "${task.file_name}", Display-IP: ${task.display.ip}\nFehler: ${error}`
); );
if (type === 'download') return; if (task_data.type === 'upload') {
await remove_file_from_display(task.display_id, task.file_primary_key); await remove_file_from_display(task.display.id, task.file_primary_key);
await remove_all_files_without_display(); await remove_all_files_without_display();
} else if (task_data.type === 'sync') {
for (const display_id of task_data.destination_displays.map((e) => e.id)) {
await remove_file_from_display(display_id, task.file_primary_key);
}
await remove_all_files_without_display();
}
} }
+1 -2
View File
@@ -232,8 +232,7 @@ export async function update_folder_elements_recursively(
const file_on_display: FileOnDisplay = { const file_on_display: FileOnDisplay = {
display_id: display.id, display_id: display.id,
file_primary_key: get_file_primary_key(new_element.folder_element), file_primary_key: get_file_primary_key(new_element.folder_element),
is_loading: false, loading_data: null,
percentage: 100,
date_created: new_element.date_created date_created: new_element.date_created
}; };
await db.files_on_display.put(file_on_display); await db.files_on_display.put(file_on_display);
+27 -7
View File
@@ -29,25 +29,45 @@ export const supported_file_type_icon: Record<string, typeof X> = {
}; };
export type FileTransferTask = { export type FileTransferTask = {
id: string; data: FileTransferTaskData;
type: 'upload' | 'download' | 'sync'; display: ShortDisplay;
display_id: string;
display_ip: string;
path: string; path: string;
file_name: string; file_name: string;
file_primary_key: string; file_primary_key: string;
file?: File; // only if type === 'upload'
bytes_total: number; // if type === 'sync' -> bytes_total = file_size * 2 (1x download + 1x upload) bytes_total: number; // if type === 'sync' -> bytes_total = file_size * 2 (1x download + 1x upload)
}; };
export type FileTransferTaskData = {
type: 'upload',
file: File,
} | {
type: 'download',
} | {
type: "sync",
destination_displays: ShortDisplay[],
}
export type ShortDisplay = {
id: string;
ip: string;
}
export type FileOnDisplay = { export type FileOnDisplay = {
display_id: string; display_id: string;
file_primary_key: string; // JSON.stringify([string, string, number, string]) file_primary_key: string; // JSON.stringify([string, string, number, string])
date_created: Date; date_created: Date;
is_loading: boolean; loading_data: FileLoadingData | null; // null if not loading
percentage: number;
}; };
export type FileLoadingData = {
type: 'upload' | 'download' | 'sync_download' | 'sync_upload';
percentage: number;
bytes_per_second: number;
seconds_until_finish: number;
}
export type Inode = { export type Inode = {
path: string; path: string;
name: string; name: string;