Compare commits

...

5 Commits

View File

@@ -11,8 +11,31 @@ import wx.lib.newevent
import webbrowser import webbrowser
import re import re
import ctypes import ctypes
import base64
import logging
from ctypes import wintypes from ctypes import wintypes
# --- LOGGING & KONSTANTER ---
def setup_logging(enabled=True):
level = logging.INFO if enabled else logging.CRITICAL
# Fjern eksisterende handlers hvis vi kalder den igen
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("SP_Browser")
# Initial setup (INFO) - vil blive opdateret efter settings er indlæst
setup_logging(True)
CHUNK_SIZE = 1 * 1024 * 1024 # 1MB
ENABLE_HASH_VALIDATION = True
HASH_THRESHOLD_MB = 250
try:
import quickxorhash as qxh_lib
except ImportError:
qxh_lib = None
# --- STIHÅNDTERING (Til EXE-brug) --- # --- STIHÅNDTERING (Til EXE-brug) ---
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
# RESOURCE_DIR er mapper indeni EXE-filen (ikoner, billeder) # RESOURCE_DIR er mapper indeni EXE-filen (ikoner, billeder)
@@ -33,6 +56,7 @@ def load_settings():
"language": "da", # da eller en "language": "da", # da eller en
"favorites": [], # Liste over {id, name, type, drive_id, site_id, path} "favorites": [], # Liste over {id, name, type, drive_id, site_id, path}
"fav_visible": True, "fav_visible": True,
"logging_enabled": True,
"license_key": "" "license_key": ""
} }
if not os.path.exists(SETTINGS_FILE): if not os.path.exists(SETTINGS_FILE):
@@ -57,6 +81,8 @@ AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["Files.ReadWrite.All", "Sites.Read.All", "User.Read"] SCOPES = ["Files.ReadWrite.All", "Sites.Read.All", "User.Read"]
TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP") TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP")
CURRENT_LANG = settings.get("language", "da") CURRENT_LANG = settings.get("language", "da")
# Opdater logging baseret på gemte indstillinger
setup_logging(settings.get("logging_enabled", True))
# --- TRANSLATIONS --- # --- TRANSLATIONS ---
STRINGS = { STRINGS = {
@@ -150,7 +176,9 @@ STRINGS = {
"msg_fav_exists": "'{name}' er allerede i favoritter.", "msg_fav_exists": "'{name}' er allerede i favoritter.",
"settings_license_group": "Licens / Aktivering", "settings_license_group": "Licens / Aktivering",
"settings_license_key": "Licensnøgle:", "settings_license_key": "Licensnøgle:",
"settings_license_status": "Status: Ikke aktiveret" "settings_license_status": "Status: Ikke aktiveret",
"settings_logging_group": "System / Diverse",
"settings_logging": "Aktiver log-output (anbefales til fejlfinding)"
}, },
"en": { "en": {
"title": "SharePoint Explorer", "title": "SharePoint Explorer",
@@ -242,7 +270,9 @@ STRINGS = {
"msg_fav_exists": "'{name}' is already in favorites.", "msg_fav_exists": "'{name}' is already in favorites.",
"settings_license_group": "License / Activation", "settings_license_group": "License / Activation",
"settings_license_key": "License Key:", "settings_license_key": "License Key:",
"settings_license_status": "Status: Not activated" "settings_license_status": "Status: Not activated",
"settings_logging_group": "System / Miscellaneous",
"settings_logging": "Enable log output (recommended for troubleshooting)"
} }
} }
@@ -290,14 +320,68 @@ class UploadDropTarget(wx.FileDropTarget):
daemon=True).start() daemon=True).start()
return True return True
def get_file_hash(path): CHUNK_SIZE = 1 * 1024 * 1024 # 1MB chunks
if not os.path.exists(path): ENABLE_HASH_VALIDATION = True
return None HASH_THRESHOLD_MB = 250 # Grænse for hvornår vi gider tjekke hash (pga. hastighed i Python)
sha256_hash = hashlib.sha256()
with open(path, "rb") as f: # --- HJÆLPEFUNKTIONER ---
for byte_block in iter(lambda: f.read(4096), b""): def get_long_path(path):
sha256_hash.update(byte_block) if os.name == 'nt' and not path.startswith('\\\\?\\'):
return sha256_hash.hexdigest() return '\\\\?\\' + os.path.abspath(path)
return path
def quickxorhash(file_path):
"""Compute Microsoft QuickXorHash for a file. Returns base64-encoded string."""
lp = get_long_path(file_path)
if not os.path.exists(lp): return None
if qxh_lib:
hasher = qxh_lib.quickxorhash()
with open(lp, 'rb') as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk: break
hasher.update(chunk)
return base64.b64encode(hasher.digest()).decode('ascii')
# Fallback til manuel Python implementering
h = 0
length = 0
mask = (1 << 160) - 1
with open(lp, 'rb') as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk: break
for b in chunk:
shift = (length * 11) % 160
shifted = b << shift
wrapped = (shifted & mask) | (shifted >> 160)
h ^= wrapped
length += 1
h ^= (length << (160 - 64))
result = h.to_bytes(20, byteorder='little')
return base64.b64encode(result).decode('ascii')
def verify_integrity(local_path, remote_hash):
"""Verifies file integrity based on global settings."""
if not remote_hash or not ENABLE_HASH_VALIDATION:
return True
lp = get_long_path(local_path)
if not os.path.exists(lp): return False
file_size = os.path.getsize(lp)
threshold_bytes = HASH_THRESHOLD_MB * 1024 * 1024
if file_size > threshold_bytes:
logger.info(f"Skipping hash check (size > {HASH_THRESHOLD_MB}MB): {os.path.basename(local_path)}")
return True
local_hash = quickxorhash(local_path)
if local_hash != remote_hash:
logger.warning(f"Hash mismatch for {local_path}: local={local_hash}, remote={remote_hash}")
return False
return True
def format_size(bytes_num): def format_size(bytes_num):
if bytes_num is None: if bytes_num is None:
@@ -398,6 +482,16 @@ class SettingsDialog(wx.Dialog):
inner_vbox.Add(lang_sizer, 0, wx.EXPAND | wx.ALL, 10) inner_vbox.Add(lang_sizer, 0, wx.EXPAND | wx.ALL, 10)
# --- Group: Miscellaneous ---
misc_box = wx.StaticBox(panel, label=self.get_txt("settings_logging_group"))
misc_sizer = wx.StaticBoxSizer(misc_box, wx.VERTICAL)
self.logging_cb = wx.CheckBox(panel, label=self.get_txt("settings_logging"))
self.logging_cb.SetValue(self.settings.get("logging_enabled", True))
misc_sizer.Add(self.logging_cb, 0, wx.ALL, 10)
inner_vbox.Add(misc_sizer, 0, wx.EXPAND | wx.ALL, 10)
panel.SetSizer(inner_vbox) panel.SetSizer(inner_vbox)
inner_vbox.Fit(panel) inner_vbox.Fit(panel)
vbox.Add(panel, 1, wx.EXPAND | wx.ALL, 0) vbox.Add(panel, 1, wx.EXPAND | wx.ALL, 0)
@@ -427,6 +521,10 @@ class SettingsDialog(wx.Dialog):
self.settings["temp_dir"] = self.temp_dir_picker.GetPath() self.settings["temp_dir"] = self.temp_dir_picker.GetPath()
self.settings["language"] = "da" if self.lang_choice.GetSelection() == 0 else "en" self.settings["language"] = "da" if self.lang_choice.GetSelection() == 0 else "en"
self.settings["license_key"] = self.license_ctrl.GetValue().strip() self.settings["license_key"] = self.license_ctrl.GetValue().strip()
self.settings["logging_enabled"] = self.logging_cb.GetValue()
# Anvend logning med det samme
setup_logging(self.settings["logging_enabled"])
if not self.settings["client_id"] or not self.settings["tenant_id"]: if not self.settings["client_id"] or not self.settings["tenant_id"]:
wx.MessageBox("Client ID og Tenant ID skal udfyldes.", "Fejl", wx.OK | wx.ICON_ERROR) wx.MessageBox("Client ID og Tenant ID skal udfyldes.", "Fejl", wx.OK | wx.ICON_ERROR)
@@ -825,18 +923,24 @@ class SharePointApp(wx.Frame):
if len(selected_indices) == 1: if len(selected_indices) == 1:
item = selected_items[0] item = selected_items[0]
added_fav = False
if item['type'] in ["FOLDER", "DRIVE", "SITE"]: if item['type'] in ["FOLDER", "DRIVE", "SITE"]:
fav_item = menu.Append(wx.ID_ANY, self.get_txt("btn_add_fav")) fav_item = menu.Append(wx.ID_ANY, self.get_txt("btn_add_fav"))
fav_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_MENU, (16, 16))) fav_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e, i=item: self.add_favorite(i), fav_item) self.Bind(wx.EVT_MENU, lambda e, i=item: self.add_favorite(i), fav_item)
menu.AppendSeparator() added_fav = True
added_file_action = False
if item['type'] == "FILE": if item['type'] == "FILE":
if added_fav: menu.AppendSeparator()
edit_item = menu.Append(wx.ID_ANY, self.get_txt("msg_edit_file")) edit_item = menu.Append(wx.ID_ANY, self.get_txt("msg_edit_file"))
edit_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_EDIT, wx.ART_MENU, (16, 16))) edit_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_EDIT, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e, i=item: self.open_file(i), edit_item) self.Bind(wx.EVT_MENU, lambda e, i=item: self.open_file(i), edit_item)
added_file_action = True
added_folder_action = False
if item['type'] in ["FILE", "FOLDER"]: if item['type'] in ["FILE", "FOLDER"]:
if added_fav and not added_file_action: menu.AppendSeparator()
rename_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_rename')} '{item['name']}'") rename_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_rename')} '{item['name']}'")
rename_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REPORT_VIEW, wx.ART_MENU, (16, 16))) rename_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_REPORT_VIEW, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e: self.on_rename_clicked(item), rename_item) self.Bind(wx.EVT_MENU, lambda e: self.on_rename_clicked(item), rename_item)
@@ -844,10 +948,12 @@ class SharePointApp(wx.Frame):
delete_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_delete')} '{item['name']}'") delete_item = menu.Append(wx.ID_ANY, f"{self.get_txt('msg_delete')} '{item['name']}'")
delete_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_MENU, (16, 16))) delete_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_item) self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_item)
added_folder_action = True
# Åbn i browser # Åbn i browser
if item.get('web_url'): if item.get('web_url'):
menu.AppendSeparator() if added_fav or added_file_action or added_folder_action:
menu.AppendSeparator()
web_item = menu.Append(wx.ID_ANY, self.get_txt("msg_open_browser")) web_item = menu.Append(wx.ID_ANY, self.get_txt("msg_open_browser"))
web_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_MENU, (16, 16))) web_item.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_MENU, (16, 16)))
self.Bind(wx.EVT_MENU, lambda e, url=item['web_url']: webbrowser.open(url), web_item) self.Bind(wx.EVT_MENU, lambda e, url=item['web_url']: webbrowser.open(url), web_item)
@@ -1178,6 +1284,9 @@ class SharePointApp(wx.Frame):
if not self.ensure_valid_token(): return if not self.ensure_valid_token(): return
self.set_status(self.get_txt("msg_downloading_to", name=item['name'], path=dest_path)) 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): if self._download_file_sync_call(item['drive_id'], item['id'], dest_path):
remote_hash = item.get('hash')
if remote_hash and not verify_integrity(dest_path, remote_hash):
self.show_info(f"Advarsel: Hash mismatch ved download af '{item['name']}'", wx.ICON_WARNING)
self.set_status(self.get_txt("msg_download_done", name=item['name'])) self.set_status(self.get_txt("msg_download_done", name=item['name']))
else: else:
self.set_status(self.get_txt("msg_error")) self.set_status(self.get_txt("msg_error"))
@@ -1526,7 +1635,7 @@ class SharePointApp(wx.Frame):
self.headers = {'Authorization': f'Bearer {self.access_token}'} self.headers = {'Authorization': f'Bearer {self.access_token}'}
self.login_btn.Disable() self.login_btn.Disable()
# self.login_btn.Hide() # Valgfrit: Skjul login knap helt når vi er inde # 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.SetLabel(self.get_txt("btn_logged_in") if not getattr(self, "compact_mode", False) else "")
self.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå self.login_btn.SetBackgroundColour(wx.Colour(200, 200, 200)) # Grå
self.home_btn.Enable() self.home_btn.Enable()
self.refresh_btn.Enable() self.refresh_btn.Enable()
@@ -1622,6 +1731,94 @@ class SharePointApp(wx.Frame):
folders.sort(key=lambda x: x['name'].lower()) folders.sort(key=lambda x: x['name'].lower())
wx.CallAfter(self._populate_tree_folders, parent_node, folders, data) wx.CallAfter(self._populate_tree_folders, parent_node, folders, data)
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,
"web_url": site.get('webUrl')
}
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'),
"path": ["SharePoint", name]
})
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): def _populate_tree_drives(self, parent_node, drives, parent_data):
self.tree_ctrl.DeleteChildren(parent_node) self.tree_ctrl.DeleteChildren(parent_node)
parent_data["loaded"] = True parent_data["loaded"] = True
@@ -1673,14 +1870,18 @@ class SharePointApp(wx.Frame):
if not data: if not data:
return return
self._navigate_to_item_data(data, tree_item=item)
def _navigate_to_item_data(self, data, tree_item=None):
try: try:
self.current_path = data["path"] self.current_path = data["path"]
self.update_path_display() self.update_path_display()
if not self.is_navigating_back: if tree_item and not self.is_navigating_back:
self.history.append(item) # Undgå dubletter i historikken hvis vi allerede er der
if not self.history or self.history[-1] != tree_item:
self.history.append(tree_item)
# Check if button still exists
if self.back_btn: if self.back_btn:
self.back_btn.Enable(len(self.history) > 1) self.back_btn.Enable(len(self.history) > 1)
@@ -1702,10 +1903,12 @@ class SharePointApp(wx.Frame):
drives = res.json().get('value', []) drives = res.json().get('value', [])
drives.sort(key=lambda x: x.get('name', '').lower()) drives.sort(key=lambda x: x.get('name', '').lower())
for drive in drives: for drive in drives:
name = drive.get('name', '')
items_data.append({ items_data.append({
"type": "DRIVE", "id": drive['id'], "name": drive.get('name', ''), "type": "DRIVE", "id": drive['id'], "name": name,
"drive_id": drive['id'], "modified": "", "size": None, "drive_id": drive['id'], "modified": "", "size": None,
"web_url": drive.get('webUrl') "web_url": drive.get('webUrl'),
"path": data['path'] + [name]
}) })
elif data['type'] in ["DRIVE", "FOLDER"]: elif data['type'] in ["DRIVE", "FOLDER"]:
drive_id = data['drive_id'] drive_id = data['drive_id']
@@ -1726,7 +1929,9 @@ class SharePointApp(wx.Frame):
"id": item['id'], "name": item['name'], "id": item['id'], "name": item['name'],
"drive_id": drive_id, "modified": modified, "drive_id": drive_id, "modified": modified,
"size": item.get('size') if not is_folder else None, "size": item.get('size') if not is_folder else None,
"web_url": item.get('webUrl') "web_url": item.get('webUrl'),
"hash": item.get('file', {}).get('hashes', {}).get('quickXorHash') if not is_folder else None,
"path": data['path'] + [item['name']]
}) })
wx.CallAfter(self._populate_list_ctrl, items_data, data) wx.CallAfter(self._populate_list_ctrl, items_data, data)
@@ -1882,6 +2087,7 @@ class SharePointApp(wx.Frame):
# Opdater knap-synlighed # Opdater knap-synlighed
can_upload = self.current_drive_id is not None can_upload = self.current_drive_id is not None
wx.CallAfter(lambda: self._safe_update_buttons(can_upload)) wx.CallAfter(lambda: self._safe_update_buttons(can_upload))
self.set_status(self.get_txt("status_ready"))
except RuntimeError: except RuntimeError:
pass pass
@@ -1900,7 +2106,10 @@ class SharePointApp(wx.Frame):
item = self.current_items[item_idx] item = self.current_items[item_idx]
if item['type'] in ["SITE", "DRIVE", "FOLDER"]: if item['type'] in ["SITE", "DRIVE", "FOLDER"]:
self._sync_tree_selection(item['id']) # Prøv at finde og vælge den i træet (hvilket trigger on_tree_selected)
if not self._sync_tree_selection(item['id']):
# Hvis den allerede var valgt eller ikke findes i træet, tvinger vi navigationen
self._navigate_to_item_data(item)
elif item['type'] == "FILE": elif item['type'] == "FILE":
self.open_file(item) self.open_file(item)
@@ -1911,19 +2120,28 @@ class SharePointApp(wx.Frame):
if selected.IsOk(): if selected.IsOk():
data = self.tree_item_data.get(selected) data = self.tree_item_data.get(selected)
# Hvis vi allerede HAR valgt den rigtige node, så returner False (så on_item_activated tvinger refresh)
if data and data['id'] == target_id:
return False
if data and not data.get("loaded"): if data and not data.get("loaded"):
self._pending_tree_selection_id = target_id self._pending_tree_selection_id = target_id
self.tree_ctrl.Expand(selected) self.tree_ctrl.Expand(selected)
return return True # Vi har sat en handling i gang
child, cookie = self.tree_ctrl.GetFirstChild(selected) child, cookie = self.tree_ctrl.GetFirstChild(selected)
while child.IsOk(): while child.IsOk():
cdata = self.tree_item_data.get(child) cdata = self.tree_item_data.get(child)
if cdata and cdata['id'] == target_id: if cdata and cdata['id'] == target_id:
if self.tree_ctrl.GetSelection() == child:
return False # Allerede valgt
self.tree_ctrl.SelectItem(child) self.tree_ctrl.SelectItem(child)
return return True # Selection changed
child, cookie = self.tree_ctrl.GetNextChild(selected, cookie) child, cookie = self.tree_ctrl.GetNextChild(selected, cookie)
return False
def go_back(self, event=None): def go_back(self, event=None):
if len(self.history) > 1: if len(self.history) > 1:
self.history.pop() # Remove current self.history.pop() # Remove current
@@ -1945,7 +2163,7 @@ class SharePointApp(wx.Frame):
wx.MessageBox("Du kan kun have 10 filer åbne til redigering ad gangen.", "Maksimum grænse nået", wx.OK | wx.ICON_WARNING) wx.MessageBox("Du kan kun have 10 filer åbne til redigering ad gangen.", "Maksimum grænse nået", wx.OK | wx.ICON_WARNING)
return return
threading.Thread(target=self.process_file, args=(item_id, file_name, drive_id), daemon=True).start() threading.Thread(target=self.process_file, args=(item_id, file_name, drive_id, item.get('hash')), daemon=True).start()
def update_edit_ui(self): def update_edit_ui(self):
def _do(): def _do():
@@ -1974,7 +2192,7 @@ class SharePointApp(wx.Frame):
pass pass
wx.CallAfter(_do) wx.CallAfter(_do)
def process_file(self, item_id, file_name, drive_id): def process_file(self, item_id, file_name, drive_id, remote_hash=None):
if not self.ensure_valid_token(): return if not self.ensure_valid_token(): return
edit_event = threading.Event() edit_event = threading.Event()
@@ -1999,14 +2217,45 @@ class SharePointApp(wx.Frame):
if res.status_code != 200: if res.status_code != 200:
raise Exception(f"{self.get_txt('msg_unknown_error')}: {res.status_code}") raise Exception(f"{self.get_txt('msg_unknown_error')}: {res.status_code}")
with open(local_path, 'wb') as f: with open(get_long_path(local_path), 'wb') as f:
f.write(res.content) f.write(res.content)
# Beregn udgangspunkt hash # Verificer integritet og gem hash til senere sammenligning
original_hash = get_file_hash(local_path) original_hash = None
if remote_hash and ENABLE_HASH_VALIDATION:
file_size = os.path.getsize(get_long_path(local_path))
if file_size <= (HASH_THRESHOLD_MB * 1024 * 1024):
# Vi bruger fjern-hash direkte som vores 'original', hvis den er tilgængelig.
# Vi tjekker dog lige at downloaden rent faktisk matchede.
local_check = quickxorhash(local_path)
if local_check == remote_hash:
original_hash = remote_hash
logger.info(f"Download ok for {file_name}. Bruger XOR hash til ændrings-detektering.")
else:
logger.warning(f"Hash mismatch efter download af {file_name}!")
self.show_info(f"Advarsel: Filens integritet kunne ikke bekræftes (XorHash mismatch)", wx.ICON_WARNING)
original_hash = local_check
# Hvis vi ikke beregnede hash pga. størrelse eller manglende remote_hash, gør det nu for lokal detektering
if original_hash is None:
# Her bruger vi SHA256 af hastighedsårsager til lokal sammenligning (før/efter)
sha256 = hashlib.sha256()
with open(get_long_path(local_path), 'rb') as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk: break
sha256.update(chunk)
original_hash = "SHA256:" + sha256.hexdigest()
logger.info(f"Bruger lokal SHA256 til ændrings-detektering for {file_name}")
# Checkout # Checkout
requests.post(f"{base_url}/checkout", headers=self.headers) is_checked_out = False
checkout_res = requests.post(f"{base_url}/checkout", headers=self.headers)
if checkout_res.status_code in [200, 201, 204]:
is_checked_out = True
logger.info(f"Fil {file_name} udtjekket succesfuldt.")
else:
logger.warning(f"Kunne ikke udtjekke {file_name} (Status: {checkout_res.status_code}). Fortsætter dog...")
# 3. Åbn & Overvåg # 3. Åbn & Overvåg
self.set_status(self.get_txt("msg_opening_file", name=file_name)) self.set_status(self.get_txt("msg_opening_file", name=file_name))
@@ -2044,20 +2293,41 @@ class SharePointApp(wx.Frame):
self.update_edit_ui() self.update_edit_ui()
# 4. Tjek om noget er ændret # 4. Tjek om noget er ændret
new_hash = get_file_hash(local_path) if original_hash.startswith("SHA256:"):
sha256 = hashlib.sha256()
with open(get_long_path(local_path), 'rb') as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk: break
sha256.update(chunk)
new_hash = "SHA256:" + sha256.hexdigest()
else:
new_hash = quickxorhash(local_path)
if original_hash == new_hash: if original_hash == new_hash:
logger.info(f"Ingen ændringer fundet i {file_name}. (Hash: {new_hash[:16]}...) Springer upload over.")
self.set_status(self.get_txt("msg_file_unchanged")) self.set_status(self.get_txt("msg_file_unchanged"))
if is_checked_out:
logger.info(f"Annullerer udtjekning (discardCheckout) for {file_name}...")
res = requests.post(f"{base_url}/discardCheckout", headers=self.headers)
if res.status_code in [200, 204]:
is_checked_out = False
else: else:
# 5. Upload (kun hvis ændret) # 5. Upload (kun hvis ændret)
logger.info(f"Ændring fundet! Uploader {file_name}...")
self.set_status(self.get_txt("msg_updating_changes")) self.set_status(self.get_txt("msg_updating_changes"))
with open(local_path, 'rb') as f: with open(local_path, 'rb') as f:
upload_res = requests.put(f"{base_url}/content", headers=self.headers, data=f) upload_res = requests.put(f"{base_url}/content", headers=self.headers, data=f)
if upload_res.status_code not in [200, 201]: if upload_res.status_code not in [200, 201]:
raise Exception(f"{self.get_txt('msg_update_failed_code', code=upload_res.status_code)}") 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) # 6. Checkin (Kun hvis vi faktisk uploadede noget)
self.set_status(self.get_txt("msg_checking_in", name=file_name)) if is_checked_out:
requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "SP Explorer Edit"}) self.set_status(self.get_txt("msg_checking_in", name=file_name))
res = requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "SP Explorer Edit"})
if res.status_code in [200, 201, 204]:
is_checked_out = False
# Oprydning: Slet fil og derefter mappe # Oprydning: Slet fil og derefter mappe
try: try:
@@ -2074,6 +2344,11 @@ class SharePointApp(wx.Frame):
self.set_status(f"{self.get_txt('msg_error')}: {str(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) self.show_info(f"{self.get_txt('msg_error')}: {e}", wx.ICON_ERROR)
finally: finally:
if is_checked_out:
# Emergency cleanup hvis vi stadig har fat i filen (f.eks. ved crash eller afbrydelse)
logger.info(f"Rydder op: Kalder discardCheckout for {file_name}...")
requests.post(f"{base_url}/discardCheckout", headers=self.headers)
if item_id in self.active_edits: if item_id in self.active_edits:
del self.active_edits[item_id] del self.active_edits[item_id]
self.update_edit_ui() self.update_edit_ui()