mirror of
https://github.com/FAUSheppy/homelab_gamevault
synced 2025-12-06 06:51:36 +01:00
feat: fully working async install
This commit is contained in:
@@ -34,6 +34,7 @@ db = None # app data-backend (i.e. LocalFS or FTP)
|
|||||||
|
|
||||||
CONFIG_FILE = "gamevault_config.json"
|
CONFIG_FILE = "gamevault_config.json"
|
||||||
|
|
||||||
|
|
||||||
def close_input_window(input_window):
|
def close_input_window(input_window):
|
||||||
'''Close the config window and save the settings'''
|
'''Close the config window and save the settings'''
|
||||||
|
|
||||||
@@ -188,7 +189,7 @@ def load_main():
|
|||||||
app.title("Lan Vault: Overview")
|
app.title("Lan Vault: Overview")
|
||||||
|
|
||||||
if not infowidget_window:
|
if not infowidget_window:
|
||||||
infowidget_window = infowidget.ProgressBarApp(app)
|
infowidget_window = infowidget.ProgressBarApp(app, data_backend=db)
|
||||||
|
|
||||||
# navbar should not expand when window is resized
|
# navbar should not expand when window is resized
|
||||||
app.grid_rowconfigure(0, weight=0)
|
app.grid_rowconfigure(0, weight=0)
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ class HTTP(DataBackend):
|
|||||||
local_file = os.path.join(self.cache_dir, fullpath)
|
local_file = os.path.join(self.cache_dir, fullpath)
|
||||||
print("Local Target is", local_file)
|
print("Local Target is", local_file)
|
||||||
return local_file
|
return local_file
|
||||||
|
|
||||||
|
def local_delete_cache_file(self, path, cache_dir=None):
|
||||||
|
'''Delete a local cache file'''
|
||||||
|
|
||||||
|
print("WARNING: removing:", self.get_local_target(path))
|
||||||
|
os.remove(self.get_local_target(path))
|
||||||
|
|
||||||
def get(self, path, cache_dir=None, return_content=False, wait=False):
|
def get(self, path, cache_dir=None, return_content=False, wait=False):
|
||||||
|
|
||||||
@@ -146,6 +152,7 @@ class HTTP(DataBackend):
|
|||||||
|
|
||||||
if return_content or wait:
|
if return_content or wait:
|
||||||
|
|
||||||
|
print("Sync Requested")
|
||||||
# the content is needed for the UI now and not cached, it's needs to be downloaded synchroniously #
|
# the content is needed for the UI now and not cached, it's needs to be downloaded synchroniously #
|
||||||
# as there cannot be a meaningful UI-draw without it. #
|
# as there cannot be a meaningful UI-draw without it. #
|
||||||
# THIS IS THE OLD WAY
|
# THIS IS THE OLD WAY
|
||||||
@@ -156,13 +163,30 @@ class HTTP(DataBackend):
|
|||||||
# f.write(r.text)
|
# f.write(r.text)
|
||||||
|
|
||||||
# this is with streaming
|
# this is with streaming
|
||||||
chunk_size = 1024 * 1024 * 5 # 5MB
|
chunk_size = 1024 * 1024 * 50 # 50MB
|
||||||
r = requests.get(self._get_url(), params={"path": path, "as_string": True}, stream=True)
|
r = requests.get(self._get_url(), params={"path": path, "as_string": True}, stream=True)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
with open(local_file, "wb") as f:
|
if path.endswith(".reg") or path.endswith(".txt"):
|
||||||
|
TYPE = "w"
|
||||||
|
else:
|
||||||
|
TYPE = "wb"
|
||||||
|
|
||||||
|
with open(local_file, TYPE) as f:
|
||||||
|
count = 0
|
||||||
for chunk in r.iter_content(chunk_size=chunk_size, decode_unicode=True):
|
for chunk in r.iter_content(chunk_size=chunk_size, decode_unicode=True):
|
||||||
|
|
||||||
|
print(f"Doing chunk.. {chunk_size*count}")
|
||||||
if chunk:
|
if chunk:
|
||||||
f.write(chunk)
|
|
||||||
|
try:
|
||||||
|
f.write(chunk)
|
||||||
|
except TypeError as e:
|
||||||
|
print("Cannot write:", chunk, "..to ", path, " because it is the wrong type.", e)
|
||||||
|
raise e
|
||||||
|
f.flush()
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
if return_content:
|
if return_content:
|
||||||
print("Content for", path, ":", r.text)
|
print("Content for", path, ":", r.text)
|
||||||
|
|||||||
6
db.py
6
db.py
@@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker, scoped_session
|
|||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.basicConfig()
|
||||||
|
logging.getLogger('sqlalchemy').setLevel(logging.ERROR)
|
||||||
class Download(Base):
|
class Download(Base):
|
||||||
|
|
||||||
__tablename__ = 'files'
|
__tablename__ = 'files'
|
||||||
@@ -24,7 +27,8 @@ class Download(Base):
|
|||||||
class Database:
|
class Database:
|
||||||
|
|
||||||
def __init__(self, db_url="sqlite:///database.db"):
|
def __init__(self, db_url="sqlite:///database.db"):
|
||||||
self.engine = create_engine(db_url, echo=True)
|
|
||||||
|
self.engine = create_engine(db_url, echo=False)
|
||||||
self.session_factory = sessionmaker(bind=self.engine)
|
self.session_factory = sessionmaker(bind=self.engine)
|
||||||
self.Session = scoped_session(self.session_factory) # Thread-safe sessions
|
self.Session = scoped_session(self.session_factory) # Thread-safe sessions
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import statekeeper
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
class ProgressBarApp:
|
class ProgressBarApp:
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, data_backend):
|
||||||
|
|
||||||
|
self.data_backend = data_backend
|
||||||
|
self.parent = parent
|
||||||
self.root = tk.Toplevel(parent)
|
self.root = tk.Toplevel(parent)
|
||||||
self.root.title("Dynamic Progress Bars")
|
self.root.title("Dynamic Progress Bars")
|
||||||
|
|
||||||
@@ -32,44 +34,51 @@ class ProgressBarApp:
|
|||||||
self.progress_bars = [] # Store tuples of (progressbar, frame, duration, delete_button)
|
self.progress_bars = [] # Store tuples of (progressbar, frame, duration, delete_button)
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
threading.Thread(target=self.add_progress_bars, daemon=True).start()
|
self.root.after(0, self.start_tracking_progress_bars)
|
||||||
|
|
||||||
def add_progress_bars(self):
|
def start_tracking_progress_bars(self):
|
||||||
|
|
||||||
already_tracked = set()
|
self.already_tracked = set()
|
||||||
while self.running:
|
self.check_for_new_progress_bars()
|
||||||
|
|
||||||
downloads = set(statekeeper.get_download())
|
def check_for_new_progress_bars(self):
|
||||||
new = downloads - already_tracked
|
|
||||||
already_tracked |= set(downloads)
|
|
||||||
|
|
||||||
for element in new:
|
downloads = set(statekeeper.get_download())
|
||||||
|
new = downloads - self.already_tracked
|
||||||
|
self.already_tracked |= downloads
|
||||||
|
|
||||||
frame = tk.Frame(self.frame)
|
for element in new:
|
||||||
frame.pack(fill=tk.X, pady=2)
|
frame = tk.Frame(self.frame)
|
||||||
|
frame.pack(fill=tk.X, pady=2)
|
||||||
|
|
||||||
progress = ttk.Progressbar(frame, length=200, mode='determinate')
|
progress = ttk.Progressbar(frame, length=200, mode='determinate')
|
||||||
progress.pack(side=tk.LEFT, padx=5)
|
progress.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
delete_button = tk.Button(frame, text="Delete", command=lambda f=frame: self.delete_progress(f), state=tk.DISABLED)
|
delete_button = tk.Button(frame, text="Delete", command=lambda f=frame: self.delete_progress(f), state=tk.DISABLED)
|
||||||
delete_button.pack(side=tk.LEFT, padx=5)
|
delete_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
label = tk.Label(frame, text=os.path.basename(element.path))
|
label = tk.Label(frame, text=os.path.basename(element.path))
|
||||||
label.pack(side=tk.LEFT, padx=5)
|
label.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
self.progress_bars.insert(0, (progress, frame, delete_button)) # Insert at the top
|
self.progress_bars.insert(0, (progress, frame, delete_button)) # Insert at the top
|
||||||
frame.pack(fill=tk.X, pady=2, before=self.frame.winfo_children()[-1] if self.frame.winfo_children() else None)
|
frame.pack(fill=tk.X, pady=2, before=self.frame.winfo_children()[-1] if self.frame.winfo_children() else None)
|
||||||
|
|
||||||
print("Starting tracker for", element.path)
|
print("Starting tracker for", element.path)
|
||||||
threading.Thread(target=self.fill_progress, args=(progress, element.path, frame, delete_button), daemon=True).start()
|
threading.Thread(target=self.fill_progress, args=(progress, element.path, frame, delete_button), daemon=True).start()
|
||||||
|
|
||||||
time.sleep(2) # Wait before adding a new progress bar
|
# Schedule the next check in 2 seconds
|
||||||
|
if self.running:
|
||||||
|
self.root.after(2000, self.check_for_new_progress_bars)
|
||||||
|
|
||||||
def fill_progress(self, progress, path, frame, delete_button):
|
def fill_progress(self, progress, path, frame, delete_button):
|
||||||
|
|
||||||
fail_count = 0
|
fail_count = 0
|
||||||
|
same_size_count = 0
|
||||||
|
prev_precent = 0
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
|
print("Checking download progress..")
|
||||||
|
|
||||||
if not progress.winfo_exists(): # Check if progress bar still exists
|
if not progress.winfo_exists(): # Check if progress bar still exists
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -84,13 +93,29 @@ class ProgressBarApp:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
print("Percent filled:", percent_filled, path)
|
print("Percent filled:", percent_filled, path)
|
||||||
if not percent_filled or percent_filled >= 99.9:
|
if percent_filled >= 99.9:
|
||||||
self.root.after(0, progress.config, { "value" : 100 })
|
self.root.after(0, progress.config, { "value" : 100 })
|
||||||
|
print("Finished", path)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.root.after(0, progress.config, { "value" : percent_filled })
|
self.root.after(0, progress.config, { "value" : percent_filled })
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# check for stuck downloads #
|
||||||
|
print("same size count", same_size_count)
|
||||||
|
if prev_precent == percent_filled:
|
||||||
|
same_size_count += 1
|
||||||
|
else:
|
||||||
|
same_size_count = 0
|
||||||
|
if same_size_count > 5:
|
||||||
|
self.root.after(0, delete_button.config, {"state": tk.NORMAL, "text": "Failed - Delete file manually!"})
|
||||||
|
self.progress_bars.append((progress, frame, path, delete_button))
|
||||||
|
self.update_delete_all_button()
|
||||||
|
statekeeper.log_end_download(path)
|
||||||
|
self.data_backend.local_delete_cache_file(path)
|
||||||
|
break
|
||||||
|
prev_precent = percent_filled
|
||||||
|
|
||||||
# handle finished download #
|
# handle finished download #
|
||||||
self.root.after(0, delete_button.config, {"state": tk.NORMAL})
|
self.root.after(0, delete_button.config, {"state": tk.NORMAL})
|
||||||
self.progress_bars.append((progress, frame, path, delete_button))
|
self.progress_bars.append((progress, frame, path, delete_button))
|
||||||
@@ -98,7 +123,7 @@ class ProgressBarApp:
|
|||||||
|
|
||||||
def delete_progress(self, frame):
|
def delete_progress(self, frame):
|
||||||
frame.destroy()
|
frame.destroy()
|
||||||
self.progress_bars = [(p, f, d, b) for p, f, d, b in self.progress_bars if f != frame]
|
self.progress_bars = [(p, f, d) for p, f, d in self.progress_bars if f != frame]
|
||||||
self.update_delete_all_button()
|
self.update_delete_all_button()
|
||||||
|
|
||||||
def delete_all_finished(self):
|
def delete_all_finished(self):
|
||||||
|
|||||||
16
software.py
16
software.py
@@ -12,6 +12,7 @@ import threading
|
|||||||
import sys
|
import sys
|
||||||
import tkinter
|
import tkinter
|
||||||
import statekeeper
|
import statekeeper
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
class Software:
|
class Software:
|
||||||
|
|
||||||
@@ -82,8 +83,16 @@ class Software:
|
|||||||
'''Extract a cached, downloaded zip to the target location'''
|
'''Extract a cached, downloaded zip to the target location'''
|
||||||
|
|
||||||
software_path = os.path.join(target, self.title)
|
software_path = os.path.join(target, self.title)
|
||||||
|
|
||||||
if os.path.isdir(software_path):
|
if os.path.isdir(software_path):
|
||||||
return # TODO better skip
|
overwrite = messagebox.askyesno(
|
||||||
|
"Overwrite Existing Directory",
|
||||||
|
f"The directory '{software_path}' already exists.\nDo you want to overwrite it?"
|
||||||
|
)
|
||||||
|
if not overwrite:
|
||||||
|
print("Skipping install as instructed by user...")
|
||||||
|
return
|
||||||
|
|
||||||
os.makedirs(software_path, exist_ok=True)
|
os.makedirs(software_path, exist_ok=True)
|
||||||
|
|
||||||
with zipfile.ZipFile(cache_src, 'r') as zip_ref:
|
with zipfile.ZipFile(cache_src, 'r') as zip_ref:
|
||||||
@@ -99,6 +108,7 @@ class Software:
|
|||||||
#self.progress_bar_wrapper.set_text(
|
#self.progress_bar_wrapper.set_text(
|
||||||
# text="Extracting: {:.2f}%".format(count/len(total_count)*100))
|
# text="Extracting: {:.2f}%".format(count/len(total_count)*100))
|
||||||
except zipfile.error as e:
|
except zipfile.error as e:
|
||||||
|
print(e)
|
||||||
pass # TODO ???
|
pass # TODO ???
|
||||||
#zip_ref.extractall(software_path)
|
#zip_ref.extractall(software_path)
|
||||||
|
|
||||||
@@ -137,13 +147,17 @@ class Software:
|
|||||||
local_file = self.backend.get(remote_file, self.cache_dir, wait=True)
|
local_file = self.backend.get(remote_file, self.cache_dir, wait=True)
|
||||||
statekeeper.log_end_download(remote_file)
|
statekeeper.log_end_download(remote_file)
|
||||||
|
|
||||||
|
print("Deciding on installer...")
|
||||||
|
|
||||||
# execute or unpack #
|
# execute or unpack #
|
||||||
if local_file.endswith(".exe"):
|
if local_file.endswith(".exe"):
|
||||||
|
print("Target is an executable.. running as installer.")
|
||||||
if os.name != "nt" and not os.path.isabs(local_file):
|
if os.name != "nt" and not os.path.isabs(local_file):
|
||||||
# need abs path for wine #
|
# need abs path for wine #
|
||||||
local_file = os.path.join(os.getcwd(), local_file)
|
local_file = os.path.join(os.getcwd(), local_file)
|
||||||
localaction.run_exe(local_file)
|
localaction.run_exe(local_file)
|
||||||
elif local_file.endswith(".zip"):
|
elif local_file.endswith(".zip"):
|
||||||
|
print("Target is a zip.. unpacking first.")
|
||||||
self._extract_to_target(local_file, self.backend.install_dir)
|
self._extract_to_target(local_file, self.backend.install_dir)
|
||||||
|
|
||||||
# download & install registry files #
|
# download & install registry files #
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ def _download(url, path):
|
|||||||
def log_begin_download(path, local_path, url):
|
def log_begin_download(path, local_path, url):
|
||||||
|
|
||||||
session = db.session()
|
session = db.session()
|
||||||
print("Current path", path)
|
print("Download path", path)
|
||||||
path_exists = session.query(Download).filter(and_(Download.path==path, Download.finished==False)).first()
|
path_exists = session.query(Download).filter(and_(Download.path==path, Download.finished==False)).first()
|
||||||
if path_exists:
|
if path_exists and False: # TODO FIX THIS
|
||||||
print("DAFUG", path_exists)
|
print("DAFUG", path_exists)
|
||||||
print("WTF", path_exists.path)
|
print("WTF", path_exists.path)
|
||||||
raise AssertionError("ERROR: {} is already downloading.".format(path))
|
raise AssertionError("ERROR: {} is already downloading.".format(path))
|
||||||
@@ -59,6 +59,7 @@ def log_begin_download(path, local_path, url):
|
|||||||
|
|
||||||
def log_end_download(path):
|
def log_end_download(path):
|
||||||
|
|
||||||
|
print("Downlod end logged", path)
|
||||||
session = db.session()
|
session = db.session()
|
||||||
obj = session.query(Download).filter(Download.path==path).first()
|
obj = session.query(Download).filter(Download.path==path).first()
|
||||||
if not obj:
|
if not obj:
|
||||||
@@ -99,11 +100,16 @@ def get_percent_filled(path):
|
|||||||
|
|
||||||
session = db.session()
|
session = db.session()
|
||||||
obj = session.query(Download).filter(Download.path==path, Download.finished==False).first()
|
obj = session.query(Download).filter(Download.path==path, Download.finished==False).first()
|
||||||
|
if not obj:
|
||||||
|
return 100 # means its finished
|
||||||
size = _bytes_to_mb(os.stat(obj.local_path).st_size)
|
size = _bytes_to_mb(os.stat(obj.local_path).st_size)
|
||||||
total_size = get_download_size(obj.path)
|
total_size = get_download_size(obj.path)
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
if total_size == 0:
|
if total_size == 0:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
print("Current filled:", size / total_size * 100)
|
||||||
return size / total_size * 100
|
return size / total_size * 100
|
||||||
|
|
||||||
def get_download(path=None):
|
def get_download(path=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user