Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions pages/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,15 @@ def table_handle_row_click(e: events.GenericEventArguments) -> None:
async def update_rows():
"""
Update the rows in the table.

Avoid clearing the existing table during temporary backend/API failures.
This can happen while large uploads are being stored and encrypted.
"""
rows = await jobs_get()

if not rows and table.rows:
return

Comment on lines +200 to +207
if not rows:
delete.set_enabled(False)
bulk_export.set_enabled(False)
Expand All @@ -213,10 +219,10 @@ async def update_rows():
)
poll_timer.interval = 5.0 if has_active else 30.0

poll_timer = ui.timer(30.0, update_rows, active=False)

async def initial_load():
poll_timer.activate()
await update_rows()
rows = await jobs_get()
table.rows = rows
table.selection = "multiple" if rows else "none"
Comment on lines +223 to +225

poll_timer = ui.timer(30.0, update_rows)
ui.timer(0.0, initial_load, once=True)
112 changes: 81 additions & 31 deletions utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
# limitations under the License.

import asyncio
import os
import re
import tempfile
import httpx
import pytz

Expand All @@ -34,9 +36,8 @@
)
from utils.helpers import storage_decrypt, customers_get

MultiPartParser.spool_max_size = 1024 * 1024 * 4096
settings = get_settings()

MultiPartParser.spool_max_size = 1024 * 1024 * settings.MULTIPART_SPOOL_MAX_SIZE_MB

def sanitize_filename(filename: str) -> str:
"""
Expand Down Expand Up @@ -856,20 +857,26 @@ def table_click(event) -> None:
)


async def post_file(filedata: bytes, filename: str) -> bool:
async def post_file(file_path: str, filename: str) -> bool:
"""
Post a file to the API.
Stream a file from disk to the API without loading it into memory.
"""

files_json = {"file": (filename, filedata)}

try:
async with httpx.AsyncClient(timeout=900) as client:
response = await client.post(
f"{settings.API_URL}/api/v1/transcriber",
files=files_json,
headers=get_auth_header(),
)
with open(file_path, "rb") as upload_file:
response = await client.post(
f"{settings.API_URL}/api/v1/transcriber",
files={
"file": (
filename,
upload_file,
"application/octet-stream",
)
},
headers=get_auth_header(),
)

response.raise_for_status()

if response.status_code != 200:
Expand Down Expand Up @@ -929,7 +936,7 @@ def table_upload(table) -> None:
ui.upload(
label="hidden",
on_multi_upload=lambda e: handle_upload_with_feedback(
e, dialog, table
e, dialog, table, status_label
),
auto_upload=True,
multiple=True,
Expand All @@ -949,7 +956,10 @@ def table_upload(table) -> None:
upload_column, status_column, dialog
),
)
upload.on("finish", lambda _: dialog.close())
upload.on(
"finish",
lambda _: status_label.set_text("Finishing upload, please wait..."),
)

def on_byte_progress(e):
uploaded = e.args.get("uploaded", 0)
Expand Down Expand Up @@ -1028,46 +1038,86 @@ def on_byte_progress(e):
dialog.open()


async def handle_upload_with_feedback(files, dialog, table):
async def handle_upload_with_feedback(files, dialog, table, status_label):
"""
Handle file uploads with user feedback and validation.

The NiceGUI upload progress only covers browser -> UI.
This function keeps the dialog open while the UI forwards the file to the backend.
"""

client = ui.context.client

dialog.close()

# Read file data while the client context is still active
file_items = []
for file in files.files:
total = len(files.files)

for index, file in enumerate(files.files, 1):
file_name = sanitize_filename(file.name)
file_data = await file.read()
file_items.append((file_name, file_data))

# Upload to backend in a background task so the UI stays responsive
if not client._deleted:
status_label.set_text(
f"Saving file {index} of {total} locally: {file_name}"
)

temp_file = tempfile.NamedTemporaryFile(
delete=False,
prefix="scribe-ui-upload-",
suffix=".upload",
)
temp_path = temp_file.name
temp_file.close()

try:
await file.save(temp_path)
file_items.append((file_name, temp_path))
except Exception:
if os.path.exists(temp_path):
os.remove(temp_path)
raise

async def _upload():
for file_name, file_data in file_items:
for index, (file_name, temp_path) in enumerate(file_items, 1):
try:
await post_file(file_data, file_name)

if not client._deleted:
with table:
ui.notify(
f"Successfully uploaded {file_name}",
type="positive",
timeout=3000,
with client:
status_label.set_text(
f"Storing and encrypting file {index} of {total}: {file_name}"
)

success = await post_file(temp_path, file_name)

if not client._deleted:
with client:
if success:
ui.notify(
f"Successfully uploaded {file_name}",
type="positive",
timeout=3000,
)
else:
ui.notify(
f"Error uploading {file_name}",
type="negative",
timeout=5000,
)
except Exception as e:
if not client._deleted:
with table:
with client:
ui.notify(
f"Error uploading {file_name}: {str(e)}",
type="negative",
timeout=5000,
)
finally:
if os.path.exists(temp_path):
os.remove(temp_path)

if not client._deleted:
table.update_rows(await jobs_get(), clear_selection=False)
rows = await jobs_get()
with client:
if rows or not table.rows:
table.update_rows(rows, clear_selection=False)
dialog.close()

asyncio.create_task(_upload())

Expand Down
2 changes: 2 additions & 0 deletions utils/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class Settings(BaseSettings):
OIDC_APP_REFRESH_ROUTE: str = ""
STORAGE_SECRET: str = "change_this_secret_to_another_very_secret_secret"

MULTIPART_SPOOL_MAX_SIZE_MB: int = 4096

LOGO_LANDING: str = "sunet_logo.png"
LOGO_LANDING_WIDTH: str = "250"
LOGO_TOPBAR: str = "sunet_small.png"
Expand Down
Loading