mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-15 02:43:17 +02:00
1 parent
db81507b7c
commit
843ad1986e
9 files changed
+259
-78
No files matched your search
+34
-4
@@ -84,6 +84,11 @@ class Browser(ft.Column):
|
||||
spacing=8,
|
||||
tight=True,
|
||||
)
|
||||
# Shown while uploading: which file, how far (Uploads' progress).
|
||||
self.UploadText = ft.Text("", size=12, color=Palette.Overlay0)
|
||||
self.UploadBar = ft.ProgressBar(value=0, color=Palette.Mauve, bgcolor=Palette.Surface0, bar_height=4)
|
||||
self.UploadRow = ft.Container(ft.Column([self.UploadText, self.UploadBar], spacing=4),
|
||||
padding=ft.Padding.symmetric(horizontal=8, vertical=6), visible=False)
|
||||
self.EntryList = ft.ListView(expand=True, spacing=2)
|
||||
# One shared popup host, opened programmatically (see _RequestMenu).
|
||||
# Its built-in triggers can't be switched off, so they're set to
|
||||
@@ -103,6 +108,7 @@ class Browser(ft.Column):
|
||||
# and the breadcrumbs get the rest (and wrap when it's tight).
|
||||
ft.Row([ft.Container(self.Breadcrumbs, padding=8, expand=True), ft.Container(self.ActionsToolbar, padding=8)]),
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
self.UploadRow,
|
||||
self.FileArea,
|
||||
]
|
||||
self._ShowList(Update=False)
|
||||
@@ -600,19 +606,43 @@ class Browser(ft.Column):
|
||||
return
|
||||
Saved, Failed = 0, []
|
||||
Server = self.TheApp.Server
|
||||
for LocalFile in Paths:
|
||||
if Server:
|
||||
self.TheApp.Notify(f"Uploading {LocalFile.name} ({HumanSize(LocalFile.stat().st_size)})...", "info")
|
||||
Sizes = [LocalFile.stat().st_size if LocalFile.is_file() else 0 for LocalFile in Paths]
|
||||
Total = sum(Sizes)
|
||||
Verb = "Uploading" if Server else "Copying in"
|
||||
|
||||
Shown = [-1, -1] # (file, percent) on screen: redraw only when it changes
|
||||
|
||||
def ShowProgress(Index: int, Fraction: float) -> None:
|
||||
Done = sum(Sizes[:Index]) + Sizes[Index] * Fraction
|
||||
Percent = int(Done / Total * 100) if Total else 100
|
||||
if Shown == [Index, Percent]:
|
||||
return
|
||||
Shown[:] = [Index, Percent]
|
||||
Of = f" ({Index + 1} of {len(Paths)})" if len(Paths) > 1 else ""
|
||||
self.UploadText.value = (f"{Verb} {Paths[Index].name}{Of}: {Percent}% "
|
||||
f"({HumanSize(int(Done))} of {HumanSize(Total)})")
|
||||
self.UploadBar.value = Done / Total if Total else 1
|
||||
self.UploadRow.update()
|
||||
|
||||
self.UploadRow.visible = True
|
||||
for Index, LocalFile in enumerate(Paths):
|
||||
ShowProgress(Index, 0)
|
||||
try:
|
||||
await asyncio.to_thread(self.TheApp.Source.Upload, self.Root, self.Path, LocalFile)
|
||||
# Large files go up in chunks and report each one; small ones in one go.
|
||||
await asyncio.to_thread(self.TheApp.Source.Upload, self.Root, self.Path, LocalFile,
|
||||
lambda Fraction, I=Index: ShowProgress(I, Fraction))
|
||||
Saved += 1
|
||||
except SourceError as Error:
|
||||
if Error.Status == 401:
|
||||
self.UploadRow.visible = False
|
||||
self.UploadRow.update()
|
||||
self.TheApp.HandleError(Error)
|
||||
return
|
||||
Failed.append(f"{LocalFile.name}: {Error}")
|
||||
except OSError as Error:
|
||||
Failed.append(f"{LocalFile.name}: {Error}")
|
||||
self.UploadRow.visible = False
|
||||
self.UploadRow.update()
|
||||
self.Refresh()
|
||||
if Failed:
|
||||
self.TheApp.Notify("Couldn't upload " + "; ".join(Failed), "error")
|
||||
|
||||
+30
-4
@@ -5,6 +5,7 @@ entries shaped like the server's /api/tree items, so the rest of the client
|
||||
never needs to know which one it's talking to."""
|
||||
|
||||
import fnmatch
|
||||
import io
|
||||
import shutil
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
@@ -412,7 +413,8 @@ class ServerSource(Source):
|
||||
Params = self._At(Root, DirectoryPath)
|
||||
if Size <= ChunkedUploadThresholdBytes:
|
||||
with LocalFile.open("rb") as Handle:
|
||||
Result = self._Json("POST", "/upload", params=Params, files=[("Files", (LocalFile.name, Handle))])
|
||||
Sending = _CountingReader(Handle, Size, OnProgress) if OnProgress else Handle
|
||||
Result = self._Json("POST", "/upload", params=Params, files=[("Files", (LocalFile.name, Sending))])
|
||||
if Result.get("Errors"):
|
||||
raise SourceError("; ".join(Result["Errors"]))
|
||||
return Result["Saved"][0]
|
||||
@@ -423,10 +425,13 @@ class ServerSource(Source):
|
||||
for ChunkIndex in range(TotalChunks):
|
||||
ChunkParams = {**Params, "UploadId": UploadId, "ChunkIndex": ChunkIndex,
|
||||
"TotalChunks": TotalChunks, "FileName": LocalFile.name}
|
||||
self._Request("POST", "/upload/chunk", params=ChunkParams,
|
||||
files=[("Chunk", ("chunk", Handle.read(UploadChunkSizeBytes)))])
|
||||
Chunk = Handle.read(UploadChunkSizeBytes)
|
||||
Sending = io.BytesIO(Chunk)
|
||||
if OnProgress:
|
||||
OnProgress((ChunkIndex + 1) / TotalChunks)
|
||||
Before = ChunkIndex * UploadChunkSizeBytes
|
||||
Sending = _CountingReader(Sending, len(Chunk),
|
||||
lambda Fraction, B=Before, L=len(Chunk): OnProgress((B + L * Fraction) / Size))
|
||||
self._Request("POST", "/upload/chunk", params=ChunkParams, files=[("Chunk", ("chunk", Sending))])
|
||||
Done = {**Params, "UploadId": UploadId, "TotalChunks": TotalChunks, "FileName": LocalFile.name}
|
||||
return self._Json("POST", "/upload/complete", params=Done)["Saved"]
|
||||
|
||||
@@ -474,5 +479,26 @@ class ServerSource(Source):
|
||||
return self._Request("POST", "/diagram", json=Body).content
|
||||
|
||||
|
||||
class _CountingReader:
|
||||
"""A file handle that reports how much of it has been read (sent): httpx
|
||||
reads an upload's file in pieces, so this is its progress."""
|
||||
|
||||
def __init__(self, Handle, Size: int, OnProgress: Callable[[float], None]):
|
||||
self._Handle = Handle
|
||||
self._Size = Size
|
||||
self._OnProgress = OnProgress
|
||||
self._Sent = 0
|
||||
|
||||
def read(self, Count: int = -1) -> bytes:
|
||||
Data = self._Handle.read(Count)
|
||||
self._Sent += len(Data)
|
||||
if self._Size:
|
||||
self._OnProgress(min(1.0, self._Sent / self._Size))
|
||||
return Data
|
||||
|
||||
def __getattr__(self, Name: str):
|
||||
return getattr(self._Handle, Name) # seek, tell, fileno: for the length
|
||||
|
||||
|
||||
def MakeSource(Config: AppConfig, Token: str = "") -> Source:
|
||||
return ServerSource(Config, Token) if Config.Mode == "Server" else LocalSource(Config)
|
||||
Reference in new issue
Block a user