From 8743b03488d6e3e401308b185f1e72d2da4cdaf9 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 16 Aug 2024 18:52:28 -0400 Subject: [PATCH 01/11] add rudimentary upload progress bars --- backend/endpoints/responses/rom.py | 4 +- backend/endpoints/rom.py | 63 ++++++++++++++----- frontend/src/App.vue | 3 +- .../__generated__/models/AddRomsResponse.ts | 4 +- .../src/components/common/Game/Card/Base.vue | 1 - .../common/Game/Dialog/UploadRom.vue | 16 ++--- .../src/components/common/Notification.vue | 1 - .../components/common/UploadInProgress.vue | 36 +++++++++++ frontend/src/layouts/NotificationStack.vue | 23 ------- frontend/src/services/api/rom.ts | 33 +++++++++- frontend/src/stores/upload.ts | 29 +++++++++ 11 files changed, 157 insertions(+), 56 deletions(-) create mode 100644 frontend/src/components/common/UploadInProgress.vue delete mode 100644 frontend/src/layouts/NotificationStack.vue create mode 100644 frontend/src/stores/upload.ts diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 2369d06a6..b1e0956ec 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -188,8 +188,8 @@ class UserNotesSchema(TypedDict): class AddRomsResponse(TypedDict): - uploaded_roms: list[str] - skipped_roms: list[str] + uploaded_files: list[str] + skipped_files: list[str] class CustomStreamingResponse(StreamingResponse): diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index e69deab9e..939fd7790 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -1,3 +1,4 @@ +import os from collections.abc import AsyncIterator from datetime import datetime from shutil import rmtree @@ -22,12 +23,21 @@ ) from exceptions.endpoint_exceptions import RomNotFoundInDatabaseException from exceptions.fs_exceptions import RomAlreadyExistsException -from fastapi import File, HTTPException, Query, Request, UploadFile, status +from fastapi import ( + BackgroundTasks, + File, + HTTPException, + Query, + Request, + UploadFile, + status, +) from fastapi.responses import FileResponse from handler.database import db_platform_handler, db_rom_handler from handler.filesystem import fs_resource_handler, fs_rom_handler from handler.filesystem.base_handler import CoverSize from handler.metadata import meta_igdb_handler, meta_moby_handler +from handler.socket_handler import socket_handler from logger.logger import log from stream_zip import NO_COMPRESSION_32, ZIP_AUTO, AsyncMemberFile, async_stream_zip from utils.router import APIRouter @@ -39,7 +49,8 @@ async def add_roms( request: Request, platform_id: int, - roms: list[UploadFile] = File(...), # noqa: B008 + background_tasks: BackgroundTasks, + files: list[UploadFile] = File(...), # noqa: B008 ) -> AddRomsResponse: """Upload roms endpoint (one or more at the same time) @@ -56,40 +67,58 @@ async def add_roms( """ platform_fs_slug = db_platform_handler.get_platform(platform_id).fs_slug + roms_path = fs_rom_handler.build_upload_file_path(platform_fs_slug) + log.info(f"Uploading roms to {platform_fs_slug}") - if roms is None: + if not files: log.error("No roms were uploaded") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="No roms were uploaded", ) - roms_path = fs_rom_handler.build_upload_file_path(platform_fs_slug) - - uploaded_roms = [] - skipped_roms = [] + uploaded_files = [] + skipped_files = [] - for rom in roms: - if fs_rom_handler.file_exists(roms_path, rom.filename): - log.warning(f" - Skipping {rom.filename} since the file already exists") - skipped_roms.append(rom.filename) + for file in files: + if fs_rom_handler.file_exists(roms_path, file.filename): + skipped_files.append(file.filename) continue - log.info(f" - Uploading {rom.filename}") - file_location = f"{roms_path}/{rom.filename}" + log.info(f" - Uploading {file.filename}") + file_location = f"{roms_path}/{file.filename}" + file_size = os.fstat(file.file.fileno()).st_size + uploaded_size = 0 async with await open_file(file_location, "wb+") as f: while True: - chunk = rom.file.read(8192) + chunk = file.file.read(65536) if not chunk: break await f.write(chunk) - uploaded_roms.append(rom.filename) + uploaded_size += len(chunk) + progress = (uploaded_size / file_size) * 100 + await socket_handler.socket_server.emit( + "upload:in_progress", + { + "name": file.filename, + "progress": progress, + }, + ) + + uploaded_files.append(file.filename) + + await socket_handler.socket_server.emit( + "upload:complete", + { + "name": file.filename, + }, + ) return { - "uploaded_roms": uploaded_roms, - "skipped_roms": skipped_roms, + "uploaded_files": uploaded_files, + "skipped_files": skipped_files, } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5e38ff7a3..001e2c1ff 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,6 @@ + + diff --git a/frontend/src/layouts/NotificationStack.vue b/frontend/src/layouts/NotificationStack.vue deleted file mode 100644 index 3a17d5a94..000000000 --- a/frontend/src/layouts/NotificationStack.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index ea13dfc2a..19f52d1ee 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -6,11 +6,31 @@ import type { import api from "@/services/api/index"; import socket from "@/services/socket"; import storeDownload from "@/stores/download"; +import storeUpload from "@/stores/upload"; import type { DetailedRom, SimpleRom } from "@/stores/roms"; import { getDownloadLink } from "@/utils"; export const romApi = api; +function clearFileFromUploads({ name }: { name: string }) { + const uploadStore = storeUpload(); + uploadStore.remove(name); + + // Disconnect socket when no more uploads are in progress + if (uploadStore.value.length === 0) socket.disconnect(); +} + +socket.on("upload:in_progress", ({ name, progress }) => { + const uploadStore = storeUpload(); + uploadStore.update({ + filename: name, + progress, + }); +}); + +// Listen for upload completion events +socket.on("upload:complete", clearFileFromUploads); + async function uploadRoms({ platformId, romsToUpload, @@ -18,8 +38,19 @@ async function uploadRoms({ platformId: number; romsToUpload: File[]; }): Promise<{ data: AddRomsResponse }> { + if (!socket.connected) socket.connect(); + const uploadStore = storeUpload(); + const formData = new FormData(); - romsToUpload.forEach((rom) => formData.append("roms", rom)); + romsToUpload.forEach((file) => { + formData.append("files", file); + uploadStore.add(file.name); + + // Clear upload state after 180 seconds in case error/timeout + setTimeout(() => { + clearFileFromUploads(file); + }, 180 * 1000); + }); return api.post("/roms", formData, { headers: { diff --git a/frontend/src/stores/upload.ts b/frontend/src/stores/upload.ts new file mode 100644 index 000000000..024147c0a --- /dev/null +++ b/frontend/src/stores/upload.ts @@ -0,0 +1,29 @@ +import { defineStore } from "pinia"; + +type UploadingRom = { + filename: string; + progress: number; +}; + +export default defineStore("upload", { + state: () => ({ + value: [] as UploadingRom[], + }), + + actions: { + add(filename: string) { + this.value = [...this.value, { filename, progress: 0 }]; + }, + remove(fname: string) { + this.value = this.value.filter(({ filename }) => fname !== filename); + }, + update({ filename, progress }: { filename: string; progress: number }) { + this.value = this.value.map((rom) => + rom.filename === filename ? { ...rom, progress } : rom, + ); + }, + clear() { + this.value = [] as UploadingRom[]; + }, + }, +}); From e9c09b46aab79fc8c8b19536d20b2b5d7b54ab73 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 16 Aug 2024 20:07:04 -0400 Subject: [PATCH 02/11] better upload progress state --- backend/endpoints/rom.py | 4 +- .../common/Game/Dialog/UploadRom.vue | 9 ++--- .../components/common/UploadInProgress.vue | 40 ++++++++++++++----- frontend/src/services/api/rom.ts | 26 +++++------- frontend/src/stores/upload.ts | 9 ++--- 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 939fd7790..af5a10cc1 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -102,7 +102,7 @@ async def add_roms( await socket_handler.socket_server.emit( "upload:in_progress", { - "name": file.filename, + "filename": file.filename, "progress": progress, }, ) @@ -112,7 +112,7 @@ async def add_roms( await socket_handler.socket_server.emit( "upload:complete", { - "name": file.filename, + "filename": file.filename, }, ) diff --git a/frontend/src/components/common/Game/Dialog/UploadRom.vue b/frontend/src/components/common/Game/Dialog/UploadRom.vue index b24d6fd0d..052711e8d 100644 --- a/frontend/src/components/common/Game/Dialog/UploadRom.vue +++ b/frontend/src/components/common/Game/Dialog/UploadRom.vue @@ -6,6 +6,7 @@ import romApi from "@/services/api/rom"; import socket from "@/services/socket"; import storeHeartbeat from "@/stores/heartbeat"; import { type Platform } from "@/stores/platforms"; +import storeUpload from "@/stores/upload"; import storeScanning from "@/stores/scanning"; import type { Events } from "@/types/emitter"; import { formatBytes } from "@/utils"; @@ -21,6 +22,7 @@ const scanningStore = storeScanning(); const selectedPlatform = ref(null); const supportedPlatforms = ref(); const heartbeat = storeHeartbeat(); +const uploadStore = storeUpload(); const HEADERS = [ { title: "Name", @@ -93,11 +95,6 @@ async function uploadRoms() { } const platformId = selectedPlatform.value.id; - // emitter?.emit("snackbarShow", { - // msg: `Uploading ${romsToUpload.value.length} roms to ${selectedPlatform.value.name}...`, - // icon: "mdi-loading mdi-spin", - // color: "romm-accent-1", - // }); await romApi .uploadRoms({ @@ -123,6 +120,8 @@ async function uploadRoms() { timeout: 2000, }); + uploadStore.clear(); + if (!socket.connected) socket.connect(); setTimeout(() => { socket.emit("scan", { diff --git a/frontend/src/components/common/UploadInProgress.vue b/frontend/src/components/common/UploadInProgress.vue index 13b62dc7d..32fea7ac1 100644 --- a/frontend/src/components/common/UploadInProgress.vue +++ b/frontend/src/components/common/UploadInProgress.vue @@ -16,21 +16,43 @@ watch(romsList, (newList) => { + + diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index 19f52d1ee..693b392f0 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -12,24 +12,21 @@ import { getDownloadLink } from "@/utils"; export const romApi = api; -function clearFileFromUploads({ name }: { name: string }) { - const uploadStore = storeUpload(); - uploadStore.remove(name); - - // Disconnect socket when no more uploads are in progress - if (uploadStore.value.length === 0) socket.disconnect(); -} - -socket.on("upload:in_progress", ({ name, progress }) => { +socket.on("upload:in_progress", ({ filename, progress }) => { const uploadStore = storeUpload(); uploadStore.update({ - filename: name, + filename, progress, }); }); -// Listen for upload completion events -socket.on("upload:complete", clearFileFromUploads); +socket.on("upload:complete", ({ filename }) => { + const uploadStore = storeUpload(); + uploadStore.update({ + filename, + progress: 100, + }); +}); async function uploadRoms({ platformId, @@ -45,11 +42,6 @@ async function uploadRoms({ romsToUpload.forEach((file) => { formData.append("files", file); uploadStore.add(file.name); - - // Clear upload state after 180 seconds in case error/timeout - setTimeout(() => { - clearFileFromUploads(file); - }, 180 * 1000); }); return api.post("/roms", formData, { diff --git a/frontend/src/stores/upload.ts b/frontend/src/stores/upload.ts index 024147c0a..5fff52fc8 100644 --- a/frontend/src/stores/upload.ts +++ b/frontend/src/stores/upload.ts @@ -3,23 +3,22 @@ import { defineStore } from "pinia"; type UploadingRom = { filename: string; progress: number; + finished?: boolean; }; export default defineStore("upload", { state: () => ({ value: [] as UploadingRom[], }), - actions: { add(filename: string) { this.value = [...this.value, { filename, progress: 0 }]; }, - remove(fname: string) { - this.value = this.value.filter(({ filename }) => fname !== filename); - }, update({ filename, progress }: { filename: string; progress: number }) { this.value = this.value.map((rom) => - rom.filename === filename ? { ...rom, progress } : rom, + rom.filename === filename + ? { ...rom, progress, finished: Math.ceil(progress) === 100 } + : rom, ); }, clear() { From 4fb19b3b59e8e7226e053af986eaafcf6affea98 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 16 Aug 2024 20:11:26 -0400 Subject: [PATCH 03/11] fix uploading dups --- .trunk/trunk.yaml | 10 +++++----- backend/endpoints/rom.py | 2 +- .../src/components/common/Game/Dialog/UploadRom.vue | 8 ++++---- frontend/src/components/common/UploadInProgress.vue | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 0f939ce01..ff49302e9 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -2,7 +2,7 @@ # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml version: 0.1 cli: - version: 1.22.2 + version: 1.22.3 # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) plugins: sources: @@ -19,24 +19,24 @@ runtimes: lint: enabled: - markdownlint@0.41.0 - - eslint@9.8.0 + - eslint@9.9.0 - actionlint@1.7.1 - bandit@1.7.9 - black@24.8.0 - - checkov@3.2.219 + - checkov@3.2.228 - git-diff-check - isort@5.13.2 - mypy@1.11.1 - osv-scanner@1.8.3 - oxipng@9.1.2 - prettier@3.3.3 - - ruff@0.5.7 + - ruff@0.6.0 - shellcheck@0.10.0 - shfmt@3.6.0 - svgo@3.3.2 - taplo@0.9.3 - trivy@0.54.1 - - trufflehog@3.81.7 + - trufflehog@3.81.9 - yamllint@1.35.1 ignore: - linters: [ALL] diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index af5a10cc1..b40e5f6f5 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -81,7 +81,7 @@ async def add_roms( skipped_files = [] for file in files: - if fs_rom_handler.file_exists(roms_path, file.filename): + if bool(os.path.exists(f"{roms_path}/{file.filename}")): skipped_files.append(file.filename) continue diff --git a/frontend/src/components/common/Game/Dialog/UploadRom.vue b/frontend/src/components/common/Game/Dialog/UploadRom.vue index 052711e8d..084962250 100644 --- a/frontend/src/components/common/Game/Dialog/UploadRom.vue +++ b/frontend/src/components/common/Game/Dialog/UploadRom.vue @@ -102,6 +102,8 @@ async function uploadRoms() { platformId: platformId, }) .then(({ data }) => { + uploadStore.clear(); + const { uploaded_files, skipped_files } = data; if (uploaded_files.length == 0) { @@ -109,7 +111,7 @@ async function uploadRoms() { msg: `All files skipped, nothing to upload.`, icon: "mdi-close-circle", color: "orange", - timeout: 2000, + timeout: 5000, }); } @@ -117,11 +119,9 @@ async function uploadRoms() { msg: `${uploaded_files.length} files uploaded successfully (and ${skipped_files.length} skipped). Starting scan...`, icon: "mdi-check-bold", color: "green", - timeout: 2000, + timeout: 3000, }); - uploadStore.clear(); - if (!socket.connected) socket.connect(); setTimeout(() => { socket.emit("scan", { diff --git a/frontend/src/components/common/UploadInProgress.vue b/frontend/src/components/common/UploadInProgress.vue index 32fea7ac1..7bc541f62 100644 --- a/frontend/src/components/common/UploadInProgress.vue +++ b/frontend/src/components/common/UploadInProgress.vue @@ -37,7 +37,7 @@ watch(romsList, (newList) => { :color="rom.finished ? `green` : `white`" class="mx-2" /> - {{ rom.filename }}... + {{ rom.filename }} Date: Fri, 16 Aug 2024 20:49:12 -0400 Subject: [PATCH 04/11] better progress up with speeds --- backend/endpoints/rom.py | 8 ++- .../components/common/UploadInProgress.vue | 32 +++++++---- frontend/src/services/api/rom.ts | 23 ++++---- frontend/src/stores/upload.ts | 53 ++++++++++++++++--- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index b40e5f6f5..59f4a9400 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -89,6 +89,7 @@ async def add_roms( file_location = f"{roms_path}/{file.filename}" file_size = os.fstat(file.file.fileno()).st_size uploaded_size = 0 + start_time = datetime.now().timestamp() async with await open_file(file_location, "wb+") as f: while True: @@ -98,12 +99,15 @@ async def add_roms( await f.write(chunk) uploaded_size += len(chunk) - progress = (uploaded_size / file_size) * 100 await socket_handler.socket_server.emit( "upload:in_progress", { "filename": file.filename, - "progress": progress, + "file_size": file_size, + "uploaded_size": uploaded_size, + "upload_speed": round( + uploaded_size / (datetime.now().timestamp() - start_time) + ), }, ) diff --git a/frontend/src/components/common/UploadInProgress.vue b/frontend/src/components/common/UploadInProgress.vue index 7bc541f62..d7f98da22 100644 --- a/frontend/src/components/common/UploadInProgress.vue +++ b/frontend/src/components/common/UploadInProgress.vue @@ -1,5 +1,6 @@ @@ -27,31 +28,38 @@ watch(romsList, (newList) => { color="tooltip" > - - - {{ rom.filename }} - + + Uploading {{ filenames.length }} files... + + + + +
+ • {{ filename }} +
-