Files
sharepoint-browser/sharepoint_browser.py

1265 lines
57 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import time
import threading
import hashlib
import json
import sys
import requests
import msal
import wx
import wx.lib.newevent
import webbrowser
# --- STIHÅNDTERING (Til EXE-brug) ---
if getattr(sys, 'frozen', False):
# RESOURCE_DIR er mapper indeni EXE-filen (ikoner, billeder)
RESOURCE_DIR = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
# CONFIG_DIR er mappen hvor selve .exe filen ligger (settings.json)
CONFIG_DIR = os.path.dirname(sys.executable)
else:
RESOURCE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_DIR = RESOURCE_DIR
SETTINGS_FILE = os.path.join(CONFIG_DIR, 'settings.json')
def load_settings():
default_settings = {
"client_id": "DIN_CLIENT_ID_HER",
"tenant_id": "DIN_TENANT_ID_HER",
"temp_dir": "C:\\Temp_SP",
"language": "da" # da eller en
}
if not os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(default_settings, f, indent=4)
return default_settings
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
try:
return json.load(f)
except:
return default_settings
def save_settings(new_settings):
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(new_settings, f, indent=4)
settings = load_settings()
CLIENT_ID = settings.get("client_id")
TENANT_ID = settings.get("tenant_id")
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["Files.ReadWrite.All", "Sites.Read.All", "User.Read"]
TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP")
CURRENT_LANG = settings.get("language", "da")
# --- TRANSLATIONS ---
STRINGS = {
"da": {
"title": "SharePoint Explorer",
"btn_back": "← Tilbage",
"btn_home": "🏠 Hjem",
"btn_save_changes": "💾 Gem ændringer i SharePoint",
"btn_login": "Log ind",
"btn_logged_in": "Logget ind",
"btn_refresh": "🔄 Opdater",
"btn_upload_file": "📤 Upload Fil",
"btn_upload_folder": "📁 Upload Mappe",
"btn_new_folder": " Ny Mappe",
"col_name": "Navn",
"col_type": "Type",
"col_size": "Størrelse",
"col_modified": "Sidst ændret",
"type_folder": "Mappe",
"type_file": "Fil",
"type_drive": "Bibliotek",
"type_site": "Site",
"status_ready": "Klar",
"status_logging_in": "Logger ind...",
"status_fetching_sites": "Henter sites...",
"status_loading": "Indlæser...",
"status_loading_content": "Indlæser indhold...",
"status_fetching_drives": "Henter biblioteker...",
"msg_confirm_delete_single": "Er du sikker på, at du vil slette '{name}' permanent fra SharePoint?",
"msg_confirm_delete_multi": "Er du sikker på, at du vil slette {num} markerede emner permanent fra SharePoint?\n\n{names}",
"msg_delete_title": "Bekræft sletning",
"msg_error": "Fejl",
"msg_success": "Succes!",
"msg_edit_file": "Rediger fil",
"msg_delete": "Slet",
"msg_rename": "Omdøb",
"msg_rename_prompt": "Indtast det nye navn for '{name}':",
"msg_rename_title": "Omdøb emne",
"msg_open_browser": "Åbn i browser",
"msg_download": "Download",
"msg_downloading_to": "Downloader '{name}' til '{path}'...",
"msg_download_done": "'{name}' downloadet færdig.",
"msg_upload_here": "Upload fil her",
"msg_upload_folder_here": "Upload mappe her",
"msg_new_folder_here": "Opret ny mappe her",
"msg_uploading": "Uploader '{name}'...",
"msg_creating_folder": "Opretter mappen '{name}'...",
"msg_folder_done": "Mappe '{name}' færdig.",
"msg_new_folder_prompt": "Indtast navnet på den nye mappe:",
"msg_new_folder_title": "Ny Mappe",
"msg_drop_info": "Du kan kun uploade filer, når du er inde i et bibliotek eller en mappe.",
"msg_drop_title": "Vælg lokation",
"msg_select_file": "Vælg fil til upload",
"msg_select_folder": "Vælg mappe til upload",
"msg_edit_warning": "Du er i gang med at redigere en fil. Luk din editor og gem ændringerne før du lukker programmet.",
"msg_login_failed": "Login fejlede.",
"msg_upload_success": "'{name}' uploadet succesfuldt.",
"msg_upload_failed": "Upload af '{name}' fejlede med kode {code}",
"msg_delete_failed": "Kunne ikke slette '{name}'. Stopper...",
"msg_deleted_status": "Slettet {count} af {total} emner.",
"msg_fetching_file": "Henter '{name}'...",
"msg_opening_file": "Åbner '{name}'...",
"msg_waiting_for_file": "Venter på '{name}'...",
"msg_editing_file": "Redigerer '{name}' - Luk for at gemme.",
"msg_file_unchanged": "Ingen ændringer fundet. Springer upload over.",
"msg_updating_changes": "Uploader ændringer...",
"msg_checking_in": "Tjekker '{name}' ind...",
"msg_update_success": "Succes! '{name}' er opdateret.",
"msg_update_failed_code": "Upload fejlede: {code}",
"msg_unknown_error": "Ukendt fejl",
"type_unknown": "Ukendt",
"status_login_needed": "Session udløbet. Log ind igen."
},
"en": {
"title": "SharePoint Explorer",
"btn_back": "← Back",
"btn_home": "🏠 Home",
"btn_save_changes": "💾 Save changes to SharePoint",
"btn_login": "Login",
"btn_logged_in": "Logged in",
"btn_refresh": "🔄 Refresh",
"btn_upload_file": "📤 Upload File",
"btn_upload_folder": "📁 Upload Folder",
"btn_new_folder": " New Folder",
"col_name": "Name",
"col_type": "Type",
"col_size": "Size",
"col_modified": "Last Modified",
"type_folder": "Folder",
"type_file": "File",
"type_drive": "Library",
"type_site": "Site",
"status_ready": "Ready",
"status_logging_in": "Logging in...",
"status_fetching_sites": "Fetching sites...",
"status_loading": "Loading...",
"status_loading_content": "Loading content...",
"status_fetching_drives": "Fetching libraries...",
"msg_confirm_delete_single": "Are you sure you want to permanently delete '{name}' from SharePoint?",
"msg_confirm_delete_multi": "Are you sure you want to permanently delete {num} selected items from SharePoint?\n\n{names}",
"msg_delete_title": "Confirm Delete",
"msg_error": "Error",
"msg_success": "Success!",
"msg_edit_file": "Edit file",
"msg_delete": "Delete",
"msg_rename": "Rename",
"msg_rename_prompt": "Enter new name for '{name}':",
"msg_rename_title": "Rename item",
"msg_open_browser": "Open in browser",
"msg_download": "Download",
"msg_downloading_to": "Downloading '{name}' to '{path}'...",
"msg_download_done": "'{name}' download finished.",
"msg_upload_here": "Upload file here",
"msg_upload_folder_here": "Upload folder here",
"msg_new_folder_here": "Create new folder here",
"msg_uploading": "Uploading '{name}'...",
"msg_creating_folder": "Creating folder '{name}'...",
"msg_folder_done": "Folder '{name}' finished.",
"msg_new_folder_prompt": "Enter the name of the new folder:",
"msg_new_folder_title": "New Folder",
"msg_drop_info": "You can only upload files when you are inside a library or a folder.",
"msg_drop_title": "Select Location",
"msg_select_file": "Select file to upload",
"msg_select_folder": "Select folder to upload",
"msg_edit_warning": "An editing task is active. Please close your editor and save changes before closing the app.",
"msg_login_failed": "Login failed.",
"msg_upload_success": "'{name}' uploaded successfully.",
"msg_upload_failed": "Upload of '{name}' failed with status {code}",
"msg_delete_failed": "Could not delete '{name}'. Stopping...",
"msg_deleted_status": "Deleted {count} of {total} items.",
"msg_fetching_file": "Fetching '{name}'...",
"msg_opening_file": "Opening '{name}'...",
"msg_waiting_for_file": "Waiting for '{name}'...",
"msg_editing_file": "Editing '{name}' - Close window to save.",
"msg_file_unchanged": "No changes found. Skipping upload.",
"msg_updating_changes": "Uploading changes...",
"msg_checking_in": "Checking in '{name}'...",
"msg_update_success": "Success! '{name}' has been updated.",
"msg_update_failed_code": "Upload failed: {code}",
"msg_unknown_error": "Unknown error",
"type_unknown": "Unknown",
"status_login_needed": "Session expired. Please login again."
}
}
if not os.path.exists(TEMP_DIR):
os.makedirs(TEMP_DIR)
class UploadDropTarget(wx.FileDropTarget):
def __init__(self, window, app):
wx.FileDropTarget.__init__(self)
self.window = window
self.app = app
def OnDropFiles(self, x, y, filenames):
if not self.app.current_drive_id:
wx.MessageBox(self.app.get_txt("msg_drop_info"),
self.app.get_txt("msg_drop_title"), wx.OK | wx.ICON_INFORMATION)
return False
for path in filenames:
if os.path.isfile(path):
threading.Thread(target=self.app._upload_file_bg,
args=(path, self.app.current_drive_id, self.app.current_folder_id),
daemon=True).start()
elif os.path.isdir(path):
threading.Thread(target=self.app._upload_folder_bg,
args=(path, self.app.current_drive_id, self.app.current_folder_id),
daemon=True).start()
return True
def get_file_hash(path):
if not os.path.exists(path):
return None
sha256_hash = hashlib.sha256()
with open(path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def format_size(bytes_num):
if bytes_num is None:
return ""
if bytes_num < 1024:
return f"{bytes_num} B"
elif bytes_num < 1024**2:
return f"{bytes_num/1024:.1f} KB"
elif bytes_num < 1024**3:
return f"{bytes_num/(1024**2):.1f} MB"
else:
return f"{bytes_num/(1024**3):.1f} GB"
class SharePointApp(wx.Frame):
def __init__(self):
self.lang = CURRENT_LANG
super().__init__(None, title=self.get_txt("title"), size=(1000, 750))
# State
self.access_token = None
self.headers = {}
self.history = [] # Stack af (mode, id, path_segment)
self.current_path = ["SharePoint"]
self.current_site_id = None
self.current_drive_id = None
self.current_folder_id = "root"
self.current_items = [] # Gemmer graf-objekterne for rækkerne
self.tree_item_data = {} # Mappenoder -> {type, id, name, drive_id, path}
self.tree_root = None
self.is_navigating_back = False
self.is_editing = False # Låse-state ved filredigering
# System Ikoner (ArtProvider - mest basale for kompatibilitet)
self.image_list = wx.ImageList(16, 16)
self.idx_site = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_GO_HOME, wx.ART_OTHER, (16, 16))) # Site (Hus ikon)
self.idx_drive = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_HARDDISK, wx.ART_OTHER, (16, 16))) # Drive
self.idx_folder = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, (16, 16))) # Folder
self.idx_file = self.image_list.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, (16, 16))) # File
# Threading/Sync til filredigering
self.edit_wait_event = threading.Event()
# MSAL Cache
self.msal_app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
self.InitUI()
self.Centre()
self.Show()
self.Bind(wx.EVT_CLOSE, self.on_close_window)
# SÆT VINDUESIKON (øverste venstre hjørne)
icon_path = os.path.join(RESOURCE_DIR, "icon.ico")
if os.path.exists(icon_path):
self.SetIcon(wx.Icon(icon_path, wx.BITMAP_TYPE_ICO))
def get_txt(self, key, **kwargs):
text = STRINGS[self.lang].get(key, key)
if kwargs:
try:
return text.format(**kwargs)
except:
pass
return text
def InitUI(self):
panel = wx.Panel(self)
main_sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(main_sizer)
main_sizer.Add(panel, 1, wx.EXPAND)
vbox = wx.BoxSizer(wx.VERTICAL)
# 0. INFO BAR (Beskeder der ikke blokerer)
self.info_bar = wx.InfoBar(panel)
vbox.Add(self.info_bar, 0, wx.EXPAND)
# Timer til at skjule info bar
self.info_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, lambda e: self.info_bar.Dismiss(), self.info_timer)
# 1. TOP NAVIGATION BAR
nav_panel = wx.Panel(panel)
nav_hbox = wx.BoxSizer(wx.HORIZONTAL)
self.back_btn = wx.Button(nav_panel, label=self.get_txt("btn_back"), size=(100, 30))
self.back_btn.Disable()
self.back_btn.Bind(wx.EVT_BUTTON, self.go_back)
nav_hbox.Add(self.back_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10)
self.home_btn = wx.Button(nav_panel, label=self.get_txt("btn_home"), size=(100, 30))
self.home_btn.Disable()
self.home_btn.Bind(wx.EVT_BUTTON, self.load_sites)
nav_hbox.Add(self.home_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
self.refresh_btn = wx.Button(nav_panel, label=self.get_txt("btn_refresh"), size=(100, 30))
self.refresh_btn.Disable()
self.refresh_btn.Bind(wx.EVT_BUTTON, self.on_refresh)
nav_hbox.Add(self.refresh_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
# NY KNAP: Gem ændringer (Vises kun ved redigering)
self.done_btn = wx.Button(nav_panel, label=self.get_txt("btn_save_changes"), size=(200, 30))
self.done_btn.SetBackgroundColour(wx.Colour(255, 69, 0)) # OrangeRed
self.done_btn.SetForegroundColour(wx.WHITE)
self.done_btn.Hide()
self.done_btn.Bind(wx.EVT_BUTTON, self.on_done_editing_clicked)
nav_hbox.Add(self.done_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10)
# NYE KNAPPER: Upload og Ny Mappe (Vises kun når man er inde i et drev/mappe)
self.upload_btn = wx.Button(nav_panel, label=self.get_txt("btn_upload_file"), size=(120, 30))
self.upload_btn.Hide()
self.upload_btn.Bind(wx.EVT_BUTTON, self.on_upload_clicked)
nav_hbox.Add(self.upload_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
self.upload_folder_btn = wx.Button(nav_panel, label=self.get_txt("btn_upload_folder"), size=(120, 30))
self.upload_folder_btn.Hide()
self.upload_folder_btn.Bind(wx.EVT_BUTTON, self.on_upload_folder_clicked)
nav_hbox.Add(self.upload_folder_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
self.new_folder_btn = wx.Button(nav_panel, label=self.get_txt("btn_new_folder"), size=(100, 30))
self.new_folder_btn.Hide()
self.new_folder_btn.Bind(wx.EVT_BUTTON, self.on_new_folder_clicked)
nav_hbox.Add(self.new_folder_btn, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5)
nav_hbox.AddStretchSpacer(1)
self.login_btn = wx.Button(nav_panel, label=self.get_txt("btn_login"), size=(120, 30))
self.login_btn.SetBackgroundColour(wx.Colour(40, 167, 69)) # Grøn
self.login_btn.SetForegroundColour(wx.WHITE)
self.login_btn.Bind(wx.EVT_BUTTON, self.login)
nav_hbox.Add(self.login_btn, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10)
# SPROG VÆLGER
self.lang_choice = wx.Choice(nav_panel, choices=["Dansk", "English"])
self.lang_choice.SetSelection(0 if self.lang == "da" else 1)
self.lang_choice.Bind(wx.EVT_CHOICE, self.on_language_changed)
nav_hbox.Add(self.lang_choice, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10)
nav_panel.SetSizer(nav_hbox)
vbox.Add(nav_panel, 0, wx.EXPAND | wx.ALL, 5)
# 2. PATH BREADCRUMBS (Adresselinje-stil)
self.path_panel = wx.Panel(panel, style=wx.BORDER_SIMPLE)
self.path_panel.SetBackgroundColour(wx.WHITE)
self.path_panel.SetMinSize((-1, 38)) # Sikrer synbarhed
self.path_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.path_panel.SetSizer(self.path_sizer)
vbox.Add(self.path_panel, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
# 3. SPLITTER FOR TREE AND LIST
self.splitter = wx.SplitterWindow(panel, style=wx.SP_LIVE_UPDATE | wx.SP_3DSASH)
# Left side: Tree
self.tree_ctrl = wx.TreeCtrl(self.splitter, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.BORDER_SUNKEN)
self.tree_ctrl.AssignImageList(self.image_list)
self.tree_ctrl.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.on_tree_expanding)
self.tree_ctrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_selected)
self.tree_ctrl.Bind(wx.EVT_TREE_ITEM_MENU, self.on_tree_right_click)
# Right side: File Area - ListCtrl
self.list_ctrl = wx.ListCtrl(self.splitter, style=wx.LC_REPORT | wx.BORDER_SUNKEN)
self.list_ctrl.AssignImageList(self.image_list, wx.IMAGE_LIST_SMALL)
self.list_ctrl.InsertColumn(0, self.get_txt("col_name"), width=450)
self.list_ctrl.InsertColumn(1, self.get_txt("col_type"), width=120)
self.list_ctrl.InsertColumn(2, self.get_txt("col_size"), width=80)
self.list_ctrl.InsertColumn(3, self.get_txt("col_modified"), width=180)
self.list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_item_activated)
self.list_ctrl.Bind(wx.EVT_CONTEXT_MENU, self.on_right_click)
# AKTIVER DRAG & DROP
self.list_ctrl.SetDropTarget(UploadDropTarget(self.list_ctrl, self))
self.splitter.SplitVertically(self.tree_ctrl, self.list_ctrl, 250)
self.splitter.SetMinimumPaneSize(100)
vbox.Add(self.splitter, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
# 4. STATUS BAR
self.status_bar = self.CreateStatusBar()
self.status_bar.SetStatusText(self.get_txt("status_ready"))
panel.SetSizer(vbox)
self.Layout()
def on_right_click(self, event):
selected_indices = []
idx = self.list_ctrl.GetFirstSelected()
while idx != -1:
selected_indices.append(idx)
idx = self.list_ctrl.GetNextSelected(idx)
menu = wx.Menu()
if selected_indices:
# Menu for de valgte emner
selected_items = [self.current_items[i] for i in selected_indices]
if len(selected_indices) == 1:
item = selected_items[0]
if item.get("web_url"):
browser_item = menu.Append(wx.ID_ANY, self.get_txt("msg_open_browser"))
self.Bind(wx.EVT_MENU, lambda e: webbrowser.open(item["web_url"]), browser_item)
download_item = menu.Append(wx.ID_ANY, self.get_txt("msg_download"))
self.Bind(wx.EVT_MENU, lambda e: self.on_download_clicked(item), download_item)
menu.AppendSeparator()
if item['type'] == "FILE":
edit_item = menu.Append(wx.ID_ANY, self.get_txt("msg_edit_file"))
self.Bind(wx.EVT_MENU, lambda e: threading.Thread(target=self.process_file, args=(item['id'], item['name']), daemon=True).start(), edit_item)
if item['type'] in ["FILE", "FOLDER"]:
rename_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_rename')} '{item['name']}'")
self.Bind(wx.EVT_MENU, lambda e: self.on_rename_clicked(item), rename_item)
delete_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_delete')} '{item['name']}'")
self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_item)
else:
# Flere emner valgt
delete_items = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_delete')} {len(selected_indices)} " + ("emner" if self.lang == "da" else "items"))
self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_items)
else:
# Menu for selve mappen (hvis man trykker på det tomme felt)
if self.current_drive_id:
upload_item = menu.Append(wx.ID_ANY, self.get_txt("msg_upload_here"))
self.Bind(wx.EVT_MENU, self.on_upload_clicked, upload_item)
upload_dir_item = menu.Append(wx.ID_ANY, self.get_txt("msg_upload_folder_here"))
self.Bind(wx.EVT_MENU, self.on_upload_folder_clicked, upload_dir_item)
new_folder_item = menu.Append(wx.ID_ANY, self.get_txt("msg_new_folder_here"))
self.Bind(wx.EVT_MENU, self.on_new_folder_clicked, new_folder_item)
# Tilføj altid opdater punkt til sidst
if menu.GetMenuItemCount() > 0:
menu.AppendSeparator()
refresh_item = menu.Append(wx.ID_ANY, self.get_txt("btn_refresh"))
self.Bind(wx.EVT_MENU, self.on_refresh, refresh_item)
self.PopupMenu(menu)
menu.Destroy()
def on_tree_right_click(self, event):
if self.is_editing: return
item = event.GetItem()
if not item.IsOk() or item == self.tree_root: return
self.tree_ctrl.SelectItem(item)
menu = wx.Menu()
refresh_item = menu.Append(wx.ID_ANY, self.get_txt("btn_refresh"))
self.Bind(wx.EVT_MENU, self.on_refresh, refresh_item)
self.PopupMenu(menu)
menu.Destroy()
# --- FILHÅNDTERING (Upload, Slet, Ny Mappe) ---
def on_delete_items_clicked(self, items):
if not items: return
names = ", ".join([f"'{i['name']}'" for i in items[:3]])
if len(items) > 3:
names += f" og {len(items)-3} andre..." if self.lang == "da" else f" and {len(items)-3} others..."
msg = self.get_txt("msg_confirm_delete_multi", num=len(items), names=names)
res = wx.MessageBox(msg, self.get_txt("msg_delete_title"), wx.YES_NO | wx.ICON_WARNING)
if res == wx.YES:
threading.Thread(target=self._delete_multiple_bg, args=(items,), daemon=True).start()
def _delete_multiple_bg(self, items):
if not self.ensure_valid_token(): return
self.lock_ui(True)
count = 0
total = len(items)
for item in items:
status_text = "Sletter" if self.lang == "da" else "Deleting"
self.set_status(f"{status_text} {count+1}/{total}: '{item['name']}'...")
url = f"https://graph.microsoft.com/v1.0/drives/{item['drive_id']}/items/{item['id']}"
res = requests.delete(url, headers=self.headers)
if res.status_code in [204, 200]:
count += 1
else:
self.set_status(self.get_txt("msg_delete_failed", name=item['name']))
wx.CallAfter(wx.MessageBox, f"Error deleting '{item['name']}': {res.status_code}", self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR)
break
self._refresh_current_view()
self.lock_ui(False)
self.set_status(self.get_txt("msg_deleted_status", count=count, total=total))
def on_upload_clicked(self, event):
if not self.current_drive_id: return
with wx.FileDialog(self, self.get_txt("msg_select_file"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fd:
if fd.ShowModal() == wx.ID_OK:
path = fd.GetPath()
threading.Thread(target=self._upload_file_bg, args=(path, self.current_drive_id, self.current_folder_id), daemon=True).start()
def _upload_file_bg(self, local_path, drive_id, parent_id):
if not self.ensure_valid_token(): return
filename = os.path.basename(local_path)
self.set_status(self.get_txt("msg_uploading", name=filename))
# Simpel upload (virker op til 4MB)
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{parent_id}:/{filename}:/content"
try:
with open(local_path, 'rb') as f:
res = requests.put(url, headers=self.headers, data=f)
if res.status_code in [200, 201]:
self.set_status(self.get_txt("msg_upload_success", name=filename))
self._refresh_current_view()
else:
self.set_status(self.get_txt("msg_upload_failed", name=filename, code=res.status_code))
wx.CallAfter(wx.MessageBox, self.get_txt("msg_upload_failed", name=filename, code=res.status_code), self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR)
except Exception as e:
self.set_status(f"Upload error: {e}")
def on_upload_folder_clicked(self, event):
if not self.current_drive_id: return
with wx.DirDialog(self, self.get_txt("msg_select_folder"), style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) as dd:
if dd.ShowModal() == wx.ID_OK:
path = dd.GetPath()
threading.Thread(target=self._upload_folder_bg, args=(path, self.current_drive_id, self.current_folder_id), daemon=True).start()
def _upload_folder_bg(self, local_dir, drive_id, parent_id):
if not self.ensure_valid_token(): return
dirname = os.path.basename(local_dir)
self.set_status(self.get_txt("msg_creating_folder", name=dirname))
# 1. Opret mappen på SharePoint
folder_id = self._create_folder_sync(dirname, drive_id, parent_id)
if not folder_id:
return
# 2. Upload filer i mappen
for item in os.listdir(local_dir):
full_path = os.path.join(local_dir, item)
if os.path.isfile(full_path):
self._upload_file_bg_sync(full_path, drive_id, folder_id)
elif os.path.isdir(full_path):
self._upload_folder_bg(full_path, drive_id, folder_id) # Rekursivt
self.set_status(self.get_txt("msg_folder_done", name=dirname))
self._refresh_current_view()
def _create_folder_sync(self, name, drive_id, parent_id):
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{parent_id}/children"
body = {"name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "rename"}
res = requests.post(url, headers=self.headers, json=body)
if res.status_code in [200, 201]:
return res.json().get('id')
return None
def _upload_file_bg_sync(self, local_path, drive_id, parent_id):
# Hjælper til sync upload brugt af mappe-upload
filename = os.path.basename(local_path)
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{parent_id}:/{filename}:/content"
with open(local_path, 'rb') as f:
requests.put(url, headers=self.headers, data=f)
def on_new_folder_clicked(self, event):
if not self.current_drive_id: return
dlg = wx.TextEntryDialog(self, self.get_txt("msg_new_folder_prompt"), self.get_txt("msg_new_folder_title"))
if dlg.ShowModal() == wx.ID_OK:
name = dlg.GetValue()
if name:
threading.Thread(target=self._create_folder_bg, args=(name, self.current_drive_id, self.current_folder_id), daemon=True).start()
dlg.Destroy()
def _create_folder_bg(self, name, drive_id, parent_id):
if not self.ensure_valid_token(): return
self.set_status(self.get_txt("msg_creating_folder", name=name))
folder_id = self._create_folder_sync(name, drive_id, parent_id)
if folder_id:
self.set_status(self.get_txt("msg_success"))
self._refresh_current_view()
else:
self.set_status(self.get_txt("msg_error"))
def on_rename_clicked(self, item):
dlg = wx.TextEntryDialog(self, self.get_txt("msg_rename_prompt", name=item['name']), self.get_txt("msg_rename_title"), item['name'])
if dlg.ShowModal() == wx.ID_OK:
new_name = dlg.GetValue()
if new_name and new_name != item['name']:
threading.Thread(target=self._rename_item_bg, args=(item, new_name), daemon=True).start()
dlg.Destroy()
def _rename_item_bg(self, item, new_name):
if not self.ensure_valid_token(): return
self.set_status(f"{self.get_txt('msg_rename')}...")
url = f"https://graph.microsoft.com/v1.0/drives/{item['drive_id']}/items/{item['id']}"
body = {"name": new_name}
res = requests.patch(url, headers=self.headers, json=body)
if res.status_code in [200, 201]:
self.set_status(self.get_txt("msg_success"))
self._refresh_current_view()
else:
self.set_status(self.get_txt("msg_error"))
wx.CallAfter(wx.MessageBox, f"Rename failed: {res.status_code}", self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR)
def on_download_clicked(self, item):
if not self.ensure_valid_token(): return
if item['type'] == "FILE":
with wx.FileDialog(self, self.get_txt("msg_select_file"), defaultFile=item['name'], style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fd:
if fd.ShowModal() == wx.ID_OK:
path = fd.GetPath()
threading.Thread(target=self._download_file_bg_task, args=(item, path), daemon=True).start()
else:
# Mappe eller Drev
with wx.DirDialog(self, self.get_txt("msg_select_folder"), style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) as dd:
if dd.ShowModal() == wx.ID_OK:
parent_path = dd.GetPath()
dest_path = os.path.join(parent_path, item['name'])
threading.Thread(target=self._download_folder_bg_task, args=(item, dest_path), daemon=True).start()
def _download_file_bg_task(self, item, dest_path):
if not self.ensure_valid_token(): return
self.set_status(self.get_txt("msg_downloading_to", name=item['name'], path=dest_path))
if self._download_file_sync_call(item['drive_id'], item['id'], dest_path):
self.set_status(self.get_txt("msg_download_done", name=item['name']))
else:
self.set_status(self.get_txt("msg_error"))
def _download_file_sync_call(self, drive_id, item_id, dest_path):
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}/content"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
with open(dest_path, 'wb') as f:
f.write(res.content)
return True
return False
def _download_folder_bg_task(self, item, dest_path):
if not self.ensure_valid_token(): return
self.set_status(self.get_txt("msg_downloading_to", name=item['name'], path=dest_path))
self._download_folder_recursive_sync(item['drive_id'], item['id'], dest_path)
self.set_status(self.get_txt("msg_download_done", name=item['name']))
def _download_folder_recursive_sync(self, drive_id, folder_id, local_dir):
if not os.path.exists(local_dir):
os.makedirs(local_dir)
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
items = res.json().get('value', [])
for item in items:
item_path = os.path.join(local_dir, item['name'])
if 'folder' in item:
self._download_folder_recursive_sync(drive_id, item['id'], item_path)
else:
self._download_file_sync_call(drive_id, item['id'], item_path)
def set_status(self, text):
wx.CallAfter(self.status_bar.SetStatusText, text)
def show_info(self, text, type=wx.ICON_INFORMATION, auto_hide=True):
def _do():
self.info_bar.Dismiss()
self.info_bar.ShowMessage(text, type)
if auto_hide:
self.info_timer.Start(4000, oneShot=True)
else:
self.info_timer.Stop()
wx.CallAfter(_do)
def on_done_editing_clicked(self, event):
print("[DEBUG] 'Jeg er færdig'-knap klikket.")
self.set_status("Knap trykket - Uploader nu...")
self.edit_wait_event.set()
self.info_bar.Dismiss()
def on_language_changed(self, event):
selection = self.lang_choice.GetSelection()
self.lang = "da" if selection == 0 else "en"
# Gem til settings
settings["language"] = self.lang
save_settings(settings)
# Opdater UI tekster med det samme
self.SetTitle(self.get_txt("title"))
self.back_btn.SetLabel(self.get_txt("btn_back"))
self.home_btn.SetLabel(self.get_txt("btn_home"))
self.done_btn.SetLabel(self.get_txt("btn_save_changes"))
self.upload_btn.SetLabel(self.get_txt("btn_upload_file"))
self.upload_folder_btn.SetLabel(self.get_txt("btn_upload_folder"))
self.new_folder_btn.SetLabel(self.get_txt("btn_new_folder"))
self.refresh_btn.SetLabel(self.get_txt("btn_refresh"))
if self.access_token:
self.login_btn.SetLabel(self.get_txt("btn_logged_in"))
else:
self.login_btn.SetLabel(self.get_txt("btn_login"))
# Opdater kolonner i ListCtrl
self.list_ctrl.SetColumnWidth(0, 450) # Refresh widths
item = self.list_ctrl.GetColumn(0)
self.list_ctrl.SetColumn(0, wx.ListItem()) # Reset column header logic could be complex in wx,
# but the simplest is to just re-insert columns or set text
# Re-set headers (Fix: explicitly set image to -1 to avoid icons in headers)
cols = [self.get_txt("col_name"), self.get_txt("col_type"), self.get_txt("col_size"), self.get_txt("col_modified")]
for i, text in enumerate(cols):
info = self.list_ctrl.GetColumn(i)
info.SetText(text)
info.SetImage(-1)
self.list_ctrl.SetColumn(i, info)
self.set_status(self.get_txt("status_ready"))
self._refresh_current_view() # Gendanner list-item tekster (Mappe/Fil)
def on_close_window(self, event):
if self.is_editing:
self.show_info(self.get_txt("msg_edit_warning"), wx.ICON_WARNING, auto_hide=False)
return
event.Skip()
def lock_ui(self, lock=True):
def _do():
self.tree_ctrl.Enable(not lock)
self.list_ctrl.Enable(not lock)
self.back_btn.Enable(not lock if len(self.history) > 1 else False)
self.home_btn.Enable(not lock)
self.refresh_btn.Enable(not lock)
self.login_btn.Enable(not lock)
wx.CallAfter(_do)
def on_refresh(self, event=None):
if self.is_editing: return
selected = self.tree_ctrl.GetSelection()
if not selected.IsOk() or selected == self.tree_root:
self.load_sites()
return
data = self.tree_item_data.get(selected)
if data:
self.set_status(f"Opdaterer '{data['name']}'...")
# Opdater Listekontrol (højre side)
threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start()
# Opdater Træstruktur (venstre side) - Ryd og indlæs igen hvis det er en container
if data['type'] in ["SITE", "DRIVE", "FOLDER"]:
data["loaded"] = False # Tillad genindlæsning
if self.tree_ctrl.IsExpanded(selected):
# Genindlæs træets børn asynkront
threading.Thread(target=self._fetch_tree_children_bg, args=(selected, data), daemon=True).start()
def _refresh_current_view(self):
sel = self.tree_ctrl.GetSelection()
if sel.IsOk():
data = self.tree_item_data.get(sel)
if data:
# Kør i nuværende baggrundstråd hvis muligt, ellers ny
self._fetch_list_contents_bg(data)
def clear_main(self):
self.list_ctrl.DeleteAllItems()
self.current_items = []
self.update_path_display()
def update_path_display(self):
if not self:
return
try:
self.path_sizer.Clear(True)
# Find alle noder fra rod til nuværende selektion
nodes = []
curr = self.tree_ctrl.GetSelection()
while curr.IsOk() and curr != self.tree_root:
nodes.insert(0, curr)
curr = self.tree_ctrl.GetItemParent(curr)
# Start ikon/label
self._add_path_segment("📍 " + self.get_txt("title"), "ROOT")
for node in nodes:
arrow = wx.StaticText(self.path_panel, label=" > ")
arrow.SetForegroundColour(wx.Colour(150, 150, 150))
self.path_sizer.Add(arrow, 0, wx.ALIGN_CENTER_VERTICAL)
name = self.tree_ctrl.GetItemText(node)
self._add_path_segment(name, node)
self.path_panel.Layout()
self.path_panel.Refresh()
self.Layout() # Tving rammen til at opdatere, så stien kommer frem
except RuntimeError:
# Sker oftest ved lukning hvor objekter er slettet
pass
def _add_path_segment(self, label, node):
btn = wx.Button(self.path_panel, label=label, style=wx.BU_EXACTFIT | wx.BORDER_NONE)
btn.SetBackgroundColour(wx.WHITE)
btn.SetFont(wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
if node == "ROOT":
btn.Bind(wx.EVT_BUTTON, self.load_sites)
elif node:
btn.Bind(wx.EVT_BUTTON, lambda e: self.tree_ctrl.SelectItem(node))
self.path_sizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
def ensure_valid_token(self):
"""Sikrer at vi har et gyldigt token. Returnerer True hvis OK."""
try:
accounts = self.msal_app.get_accounts()
if not accounts:
self.set_status(self.get_txt("status_login_needed"))
return False
result = self.msal_app.acquire_token_silent(SCOPES, account=accounts[0])
if result and "access_token" in result:
self.access_token = result["access_token"]
self.headers = {'Authorization': f'Bearer {self.access_token}'}
return True
except Exception as e:
print(f"Token refresh error: {e}")
self.set_status(self.get_txt("status_login_needed"))
return False
def login(self, event):
self.set_status(self.get_txt("status_logging_in"))
accounts = self.msal_app.get_accounts()
result = None
if accounts:
result = self.msal_app.acquire_token_silent(SCOPES, account=accounts[0])
if not result or "access_token" not in result:
result = self.msal_app.acquire_token_interactive(scopes=SCOPES)
if "access_token" in result:
self.access_token = result["access_token"]
self.headers = {'Authorization': f'Bearer {self.access_token}'}
self.login_btn.Disable()
# self.login_btn.Hide() # Valgfrit: Skjul login knap helt når vi er inde
self.login_btn.SetLabel(self.get_txt("btn_logged_in"))
self.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå
self.home_btn.Enable()
self.refresh_btn.Enable()
self.load_sites()
else:
self.set_status(self.get_txt("status_login_failed"))
wx.CallAfter(wx.MessageBox, result.get("error_description", self.get_txt("msg_unknown_error")), self.get_txt("msg_error"), wx.OK | wx.ICON_ERROR)
def load_sites(self, event=None):
self.set_status(self.get_txt("status_fetching_sites"))
self.tree_ctrl.DeleteAllItems()
self.list_ctrl.DeleteAllItems()
self.current_items = []
self.tree_item_data = {}
self._pending_tree_selection_id = None
self.tree_root = self.tree_ctrl.AddRoot("HiddenRoot")
self.current_path = ["SharePoint"]
self.history = []
self.update_path_display()
threading.Thread(target=self._fetch_sites_bg, daemon=True).start()
def _fetch_sites_bg(self):
if not self.ensure_valid_token(): return
url = "https://graph.microsoft.com/v1.0/sites?search=*"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
sites = res.json().get('value', [])
sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower())
wx.CallAfter(self._populate_sites_tree, sites)
else:
self.set_status(self.get_txt("msg_unknown_error"))
def _populate_sites_tree(self, sites):
self.set_status(f"{len(sites)} sites.")
for site in sites:
name = site.get('displayName', site.get('name'))
node = self.tree_ctrl.AppendItem(self.tree_root, name, image=self.idx_site)
self.tree_item_data[node] = {
"type": "SITE", "id": site['id'], "name": name,
"drive_id": None, "path": ["SharePoint", name], "loaded": False
}
self.tree_ctrl.SetItemHasChildren(node, True)
# Select the first site or just show in list (defaulting to showing root sites in list)
self.list_ctrl.DeleteAllItems()
self.current_items = []
for i, site in enumerate(sites):
name = site.get('displayName', site.get('name'))
self.list_ctrl.InsertItem(i, name, self.idx_site)
self.list_ctrl.SetItem(i, 1, self.get_txt("type_site"))
self.list_ctrl.SetItem(i, 2, "") # Størrelse
self.list_ctrl.SetItem(i, 3, "") # Sidst ændret
self.current_items.append({
"type": "SITE", "id": site['id'], "name": name,
"size": None, "modified": "", "web_url": site.get('webUrl')
})
def on_tree_expanding(self, event):
item = event.GetItem()
data = self.tree_item_data.get(item)
if not data or data.get("loaded"):
return
loading_node = self.tree_ctrl.AppendItem(item, self.get_txt("status_loading"))
threading.Thread(target=self._fetch_tree_children_bg, args=(item, data), daemon=True).start()
def _fetch_tree_children_bg(self, parent_node, data):
if not self.ensure_valid_token(): return
if data['type'] == "SITE":
url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
drives = res.json().get('value', [])
drives.sort(key=lambda x: x.get('name', '').lower())
wx.CallAfter(self._populate_tree_drives, parent_node, drives, data)
elif data['type'] == "DRIVE":
url = f"https://graph.microsoft.com/v1.0/drives/{data['id']}/root/children"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
items = res.json().get('value', [])
folders = [x for x in items if 'folder' in x]
folders.sort(key=lambda x: x['name'].lower())
wx.CallAfter(self._populate_tree_folders, parent_node, folders, data)
elif data['type'] == "FOLDER":
url = f"https://graph.microsoft.com/v1.0/drives/{data['drive_id']}/items/{data['id']}/children"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
items = res.json().get('value', [])
folders = [x for x in items if 'folder' in x]
folders.sort(key=lambda x: x['name'].lower())
wx.CallAfter(self._populate_tree_folders, parent_node, folders, data)
def _populate_tree_drives(self, parent_node, drives, parent_data):
self.tree_ctrl.DeleteChildren(parent_node)
parent_data["loaded"] = True
target_node = None
for drive in drives:
name = drive.get('name', self.get_txt("type_unknown"))
drive_id = drive['id']
node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_drive)
self.tree_item_data[node] = {
"type": "DRIVE", "id": drive_id, "name": name,
"drive_id": drive_id, "path": parent_data["path"] + [name], "loaded": False
}
self.tree_ctrl.SetItemHasChildren(node, True)
if drive_id == getattr(self, '_pending_tree_selection_id', None):
target_node = node
if target_node:
self._pending_tree_selection_id = None
self.tree_ctrl.SelectItem(target_node)
def _populate_tree_folders(self, parent_node, folders, parent_data):
self.tree_ctrl.DeleteChildren(parent_node)
parent_data["loaded"] = True
target_node = None
for folder in folders:
name = folder['name']
folder_id = folder['id']
node = self.tree_ctrl.AppendItem(parent_node, name, image=self.idx_folder)
self.tree_item_data[node] = {
"type": "FOLDER", "id": folder_id, "name": name,
"drive_id": parent_data["drive_id"], "path": parent_data["path"] + [name], "loaded": False
}
self.tree_ctrl.SetItemHasChildren(node, True)
if folder_id == getattr(self, '_pending_tree_selection_id', None):
target_node = node
if target_node:
self._pending_tree_selection_id = None
self.tree_ctrl.SelectItem(target_node)
def on_tree_selected(self, event):
if not self or self.is_editing: return
item = event.GetItem()
data = self.tree_item_data.get(item)
if not data:
return
try:
self.current_path = data["path"]
self.update_path_display()
if not self.is_navigating_back:
self.history.append(item)
# Check if button still exists
if self.back_btn:
self.back_btn.Enable(len(self.history) > 1)
self.list_ctrl.DeleteAllItems()
self.current_items = []
self.set_status(self.get_txt("status_loading_content"))
threading.Thread(target=self._fetch_list_contents_bg, args=(data,), daemon=True).start()
except RuntimeError:
pass
def _fetch_list_contents_bg(self, data):
if not self.ensure_valid_token(): return
items_data = []
if data['type'] == "SITE":
url = f"https://graph.microsoft.com/v1.0/sites/{data['id']}/drives"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
drives = res.json().get('value', [])
drives.sort(key=lambda x: x.get('name', '').lower())
for drive in drives:
items_data.append({
"type": "DRIVE", "id": drive['id'], "name": drive.get('name', ''),
"drive_id": drive['id'], "modified": "", "size": None,
"web_url": drive.get('webUrl')
})
elif data['type'] in ["DRIVE", "FOLDER"]:
drive_id = data['drive_id']
if data['type'] == "DRIVE":
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/root/children"
else:
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{data['id']}/children"
res = requests.get(url, headers=self.headers)
if res.status_code == 200:
items = res.json().get('value', [])
items.sort(key=lambda x: (not 'folder' in x, x['name'].lower()))
for item in items:
is_folder = 'folder' in item
modified = item.get('lastModifiedDateTime', '').replace('T', ' ').split('.')[0]
items_data.append({
"type": "FOLDER" if is_folder else "FILE",
"id": item['id'], "name": item['name'],
"drive_id": drive_id, "modified": modified,
"size": item.get('size') if not is_folder else None,
"web_url": item.get('webUrl')
})
wx.CallAfter(self._populate_list_ctrl, items_data, data)
def _populate_list_ctrl(self, items_data, parent_data):
if not self: return
try:
self.list_ctrl.DeleteAllItems()
self.current_items = []
for i, item in enumerate(items_data):
img_idx = self.idx_file
if item['type'] == "FOLDER": img_idx = self.idx_folder
elif item['type'] == "DRIVE": img_idx = self.idx_drive
elif item['type'] == "SITE": img_idx = self.idx_site
self.list_ctrl.InsertItem(i, item['name'], img_idx)
type_str = self.get_txt("type_folder") if item['type'] == "FOLDER" else self.get_txt("type_file") if item['type'] == "FILE" else self.get_txt("type_drive")
self.list_ctrl.SetItem(i, 1, type_str)
size_str = format_size(item['size']) if item['size'] is not None else ""
self.list_ctrl.SetItem(i, 2, size_str)
self.list_ctrl.SetItem(i, 3, item['modified'])
self.current_items.append(item)
self.set_status(self.get_txt("status_ready"))
if parent_data['type'] == "SITE":
self.current_site_id = parent_data['id']
elif parent_data['type'] == "DRIVE":
self.current_drive_id = parent_data['id']
self.current_folder_id = "root"
elif parent_data['type'] == "FOLDER":
self.current_drive_id = parent_data['drive_id']
self.current_folder_id = parent_data['id']
# Opdater knap-synlighed
can_upload = self.current_drive_id is not None
wx.CallAfter(lambda: self._safe_update_buttons(can_upload))
except RuntimeError:
pass
def _safe_update_buttons(self, can_upload):
try:
if not self: return
self.upload_btn.Show(can_upload)
self.upload_folder_btn.Show(can_upload)
self.new_folder_btn.Show(can_upload)
self.Layout()
except RuntimeError:
pass
def on_item_activated(self, event):
if self.is_editing: return
item_idx = event.GetIndex()
item = self.current_items[item_idx]
if item['type'] in ["SITE", "DRIVE", "FOLDER"]:
self._sync_tree_selection(item['id'])
elif item['type'] == "FILE":
self.current_drive_id = item['drive_id']
threading.Thread(target=self.process_file, args=(item['id'], item['name']), daemon=True).start()
def _sync_tree_selection(self, target_id):
selected = self.tree_ctrl.GetSelection()
if not selected.IsOk():
selected = self.tree_root
if selected.IsOk():
data = self.tree_item_data.get(selected)
if data and not data.get("loaded"):
self._pending_tree_selection_id = target_id
self.tree_ctrl.Expand(selected)
return
child, cookie = self.tree_ctrl.GetFirstChild(selected)
while child.IsOk():
cdata = self.tree_item_data.get(child)
if cdata and cdata['id'] == target_id:
self.tree_ctrl.SelectItem(child)
return
child, cookie = self.tree_ctrl.GetNextChild(selected, cookie)
def go_back(self, event=None):
if self.is_editing: return
if len(self.history) > 1:
self.history.pop() # Remove current
prev_item = self.history[-1] # Peak at previous
self.is_navigating_back = True
self.tree_ctrl.SelectItem(prev_item)
self.is_navigating_back = False
def process_file(self, item_id, file_name):
if not self.ensure_valid_token(): return
self.is_editing = True
self.lock_ui(True)
try:
# 1. Lokation info
site_id = self.current_site_id
drive_id = self.current_drive_id
base_url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}/items/{item_id}"
# Unik undermappe baseret på ID, men brug originalt filnavn indeni
item_hash = hashlib.md5(item_id.encode()).hexdigest()[:8]
working_dir = os.path.join(TEMP_DIR, item_hash)
if not os.path.exists(working_dir):
os.makedirs(working_dir)
local_path = os.path.join(working_dir, file_name)
# 2. Download
self.set_status(self.get_txt("msg_fetching_file", name=file_name))
res = requests.get(f"{base_url}/content", headers=self.headers)
if res.status_code != 200:
raise Exception(f"{self.get_txt('msg_unknown_error')}: {res.status_code}")
with open(local_path, 'wb') as f:
f.write(res.content)
# Beregn udgangspunkt hash
original_hash = get_file_hash(local_path)
# Checkout
requests.post(f"{base_url}/checkout", headers=self.headers)
# 3. Åbn & Overvåg
self.set_status(self.get_txt("msg_opening_file", name=file_name))
os.startfile(local_path)
locked = False
self.set_status(self.get_txt("msg_waiting_for_file", name=file_name))
for _ in range(10):
time.sleep(1)
try:
os.rename(local_path, local_path)
except OSError:
locked = True
break
if locked:
self.set_status(self.get_txt("msg_editing_file", name=file_name))
while True:
time.sleep(2)
try:
os.rename(local_path, local_path)
break
except OSError:
pass
else:
self.set_status(self.get_txt("msg_waiting_for_file", name=file_name))
self.edit_wait_event.clear()
wx.CallAfter(self.done_btn.Show)
wx.CallAfter(self.Layout)
self.edit_wait_event.wait()
wx.CallAfter(self.done_btn.Hide)
wx.CallAfter(self.Layout)
# 4. Tjek om noget er ændret
new_hash = get_file_hash(local_path)
if original_hash == new_hash:
self.set_status(self.get_txt("msg_file_unchanged"))
else:
# 5. Upload (kun hvis ændret)
self.set_status(self.get_txt("msg_updating_changes"))
with open(local_path, 'rb') as f:
upload_res = requests.put(f"{base_url}/content", headers=self.headers, data=f)
if upload_res.status_code not in [200, 201]:
raise Exception(f"{self.get_txt('msg_update_failed_code', code=upload_res.status_code)}")
# 6. Checkin (Uanset om ændret eller ej, for at frigive lås)
self.set_status(self.get_txt("msg_checking_in", name=file_name))
requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "SP Explorer Edit"})
# Oprydning: Slet fil og derefter mappe
try:
os.remove(local_path)
os.rmdir(working_dir)
except:
pass
self.set_status(self.get_txt("msg_update_success", name=file_name))
self.show_info(self.get_txt("msg_update_success", name=file_name), wx.ICON_INFORMATION)
self._refresh_current_view()
except Exception as e:
self.set_status(f"{self.get_txt('msg_error')}: {str(e)}")
self.show_info(f"{self.get_txt('msg_error')}: {e}", wx.ICON_ERROR)
finally:
self.is_editing = False
self.lock_ui(False)
if __name__ == "__main__":
app = wx.App()
SharePointApp()
app.MainLoop()