22import sys , os
33from PyQt6 .QtWidgets import QApplication , QWidget , QVBoxLayout , QHBoxLayout , QPushButton , QLabel , QListWidget , QFileDialog , QProgressBar , QTextEdit , QMessageBox , QSizePolicy
44from PyQt6 .QtCore import QThread , pyqtSignal , Qt
5+ from PyQt6 .QtGui import QTextCursor
56from auth import list_accounts , acquire_token_interactive , remove_account
67from uploader import upload_items
78from Cocoa import NSOpenPanel
@@ -50,11 +51,13 @@ def progress_cb(uploaded_bytes, total_bytes, speed_bytes_per_sec=None, eta_secon
5051 should_stop = lambda : self ._stop
5152 )
5253 self .finished .emit (True )
53- except Exception as e :
54- self .log .emit ("ERROR: " + str (e ))
54+ except Exception :
55+ import traceback
56+ self .log .emit ("ERROR:\n " + traceback .format_exc ())
5557 self .finished .emit (False )
5658
5759class MainWindow (QWidget ):
60+ MAX_LOG_LINES = 1000
5861 def __init__ (self ):
5962 super ().__init__ ()
6063 self .setWindowTitle ("OneDrive Uploader" )
@@ -112,6 +115,35 @@ def __init__(self):
112115 # Connect the unified choose button
113116 self .btn_choose .clicked .connect (self .choose_files_and_folders )
114117
118+ def _append_log (self , message : str ):
119+ """Append formatted log efficiently and trim to last MAX_LOG_LINES lines."""
120+ # format any raw byte values first
121+ formatted = self ._format_log_message (message )
122+ cursor = self .log .textCursor ()
123+ cursor .movePosition (QTextCursor .MoveOperation .End )
124+ cursor .insertText (formatted + "\n " )
125+ self .log .setTextCursor (cursor )
126+ self .log .ensureCursorVisible ()
127+ # trim lines if too many
128+ doc = self .log .document ()
129+ if doc .blockCount () > self .MAX_LOG_LINES :
130+ # remove earliest extra lines
131+ extra = doc .blockCount () - self .MAX_LOG_LINES
132+ b = doc .begin ()
133+ rm = 0
134+ cur = self .log .textCursor ()
135+ cur .beginEditBlock ()
136+ while extra > 0 and b .isValid ():
137+ nxt = b .next ()
138+ cur .setPosition (b .position ())
139+ cur .movePosition (QTextCursor .MoveOperation .EndOfBlock , QTextCursor .MoveMode .KeepAnchor )
140+ cur .movePosition (QTextCursor .MoveOperation .Right , QTextCursor .MoveMode .KeepAnchor ) # include newline
141+ cur .removeSelectedText ()
142+ rm += 1
143+ extra -= 1
144+ b = nxt
145+ cur .endEditBlock ()
146+
115147 def refresh_accounts (self ):
116148 self .acct_list .clear ()
117149 accts = list_accounts ()
@@ -203,7 +235,7 @@ def format_size(bytes_value):
203235 return f"{ mb :.1f} MB"
204236 gb = mb / 1024
205237 return f"{ gb :.2f} GB"
206- self .log . append (f"Total upload size: { format_size (total_bytes )} " )
238+ self ._append_log (f"Total upload size: { format_size (total_bytes )} " )
207239
208240 def update_folder_label (self ):
209241 full_list = getattr (self , '_full_list' , [])
@@ -255,7 +287,7 @@ def add_account(self):
255287 try :
256288 token , acc = acquire_token_interactive ()
257289 QMessageBox .information (self , "Signed In" , f"Signed in as { acc .get ('username' )} " )
258- self .log . append (f"Signed in: { acc .get ('username' )} " )
290+ self ._append_log (f"Signed in: { acc .get ('username' )} " )
259291 self .refresh_accounts ()
260292 except Exception as e :
261293 QMessageBox .critical (self , "Error" , str (e ))
@@ -270,7 +302,7 @@ def remove_selected_account(self):
270302 hid = text .split ("[" )[- 1 ].split ("]" )[0 ]
271303 ok = remove_account (hid )
272304 if ok :
273- self .log . append (f"Removed account { hid } " )
305+ self ._append_log (f"Removed account { hid } " )
274306 self .refresh_accounts ()
275307 else :
276308 QMessageBox .warning (self , "Remove" , "Failed to remove account" )
@@ -283,6 +315,10 @@ def start_upload(self):
283315 if row < 0 :
284316 QMessageBox .warning (self , "No account" , "Add and select an account" )
285317 return
318+ # Thread lifecycle management: prevent multiple uploads
319+ if self .worker and self .worker .isRunning ():
320+ QMessageBox .warning (self , "Busy" , "An upload task is still running." )
321+ return
286322 text = self .acct_list .currentItem ().text ()
287323 hid = text .split ("[" )[- 1 ].split ("]" )[0 ]
288324
@@ -292,16 +328,16 @@ def start_upload(self):
292328 # 传 selected_paths 和 base_dir 给上传线程
293329 self .worker = UploadWorker (self .selected_paths , self .base_dir , account_home_id = hid )
294330 self .worker .progress .connect (self .on_progress )
295- self .worker .log .connect (lambda s : self .log . append ( self . _format_log_message ( s )) )
331+ self .worker .log .connect (self ._append_log )
296332 self .worker .finished .connect (lambda ok : self .on_finished (ok ))
297333 self .worker .start ()
298- self .log . append ("Upload started" )
334+ self ._append_log ("Upload started" )
299335
300336 def stop_upload (self ):
301337 if self .worker and self .worker .isRunning ():
302338 # signal worker to stop gracefully
303339 self .worker .request_stop ()
304- self .log . append ("Stopping... waiting for current chunk to finish" )
340+ self ._append_log ("Stopping... waiting for current chunk to finish" )
305341 self .btn_start .setEnabled (True )
306342 self .btn_stop .setEnabled (False )
307343
@@ -311,7 +347,8 @@ def on_progress(self, uploaded, _ignored_total, speed=0, eta=0):
311347 total = getattr (self , "total_bytes" , _ignored_total ) or 0
312348 if total > 0 :
313349 pct = int (uploaded * 100 / total )
314- self .progress .setValue (pct )
350+ if pct != self .progress .value ():
351+ self .progress .setValue (pct )
315352
316353 # --- 单位换算(动态单位显示) ---
317354 def format_size (bytes_value ):
@@ -342,7 +379,7 @@ def format_time(seconds):
342379 uploaded_str = format_size (uploaded )
343380 total_str = format_size (total )
344381 mbps = (speed or 0 ) / (1024 * 1024 )
345- eta_str = format_time (eta )
382+ eta_str = format_time (eta ) if eta and eta > 0 else " \u221e "
346383
347384 # --- 界面更新(进度条和状态标签) ---
348385 self .lbl_status .setText (
@@ -355,8 +392,14 @@ def format_time(seconds):
355392 def on_finished (self , ok ):
356393 self .btn_start .setEnabled (True )
357394 self .btn_stop .setEnabled (False )
358- self .log . append ("Upload finished" if ok else "Upload ended with errors" )
395+ self ._append_log ("Upload finished" if ok else "Upload ended with errors" )
359396 self .refresh_accounts ()
397+ if self .worker :
398+ try :
399+ self .worker .deleteLater ()
400+ except Exception :
401+ pass
402+ self .worker = None
360403 def _format_log_message (self , message : str ) -> str :
361404 """Convert raw byte values in uploader logs to human-readable units."""
362405 import re
0 commit comments