Compare commits
5 Commits
bff066d3fb
...
ad6055963d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6055963d | ||
|
|
e14012d2a5 | ||
|
|
62725f9be6 | ||
|
|
1a85f1d963 | ||
|
|
62639632dc |
@@ -11,8 +11,31 @@ import wx.lib.newevent
|
||||
import webbrowser
|
||||
import re
|
||||
import ctypes
|
||||
import base64
|
||||
import logging
|
||||
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) ---
|
||||
if getattr(sys, 'frozen', False):
|
||||
# RESOURCE_DIR er mapper indeni EXE-filen (ikoner, billeder)
|
||||
@@ -33,6 +56,7 @@ def load_settings():
|
||||
"language": "da", # da eller en
|
||||
"favorites": [], # Liste over {id, name, type, drive_id, site_id, path}
|
||||
"fav_visible": True,
|
||||
"logging_enabled": True,
|
||||
"license_key": ""
|
||||
}
|
||||
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"]
|
||||
TEMP_DIR = settings.get("temp_dir", "C:\\Temp_SP")
|
||||
CURRENT_LANG = settings.get("language", "da")
|
||||
# Opdater logging baseret på gemte indstillinger
|
||||
setup_logging(settings.get("logging_enabled", True))
|
||||
|
||||
# --- TRANSLATIONS ---
|
||||
STRINGS = {
|
||||
@@ -150,7 +176,9 @@ STRINGS = {
|
||||
"msg_fav_exists": "'{name}' er allerede i favoritter.",
|
||||
"settings_license_group": "Licens / Aktivering",
|
||||
"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": {
|
||||
"title": "SharePoint Explorer",
|
||||
@@ -242,7 +270,9 @@ STRINGS = {
|
||||
"msg_fav_exists": "'{name}' is already in favorites.",
|
||||
"settings_license_group": "License / Activation",
|
||||
"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()
|
||||
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()
|
||||
CHUNK_SIZE = 1 * 1024 * 1024 # 1MB chunks
|
||||
ENABLE_HASH_VALIDATION = True
|
||||
HASH_THRESHOLD_MB = 250 # Grænse for hvornår vi gider tjekke hash (pga. hastighed i Python)
|
||||
|
||||
# --- HJÆLPEFUNKTIONER ---
|
||||
def get_long_path(path):
|
||||
if os.name == 'nt' and not path.startswith('\\\\?\\'):
|
||||
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):
|
||||
if bytes_num is None:
|
||||
@@ -398,6 +482,16 @@ class SettingsDialog(wx.Dialog):
|
||||
|
||||
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)
|
||||
inner_vbox.Fit(panel)
|
||||
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["language"] = "da" if self.lang_choice.GetSelection() == 0 else "en"
|
||||
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"]:
|
||||
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:
|
||||
item = selected_items[0]
|
||||
|
||||
added_fav = False
|
||||
if item['type'] in ["FOLDER", "DRIVE", "SITE"]:
|
||||
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)))
|
||||
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 added_fav: menu.AppendSeparator()
|
||||
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)))
|
||||
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 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.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)
|
||||
@@ -844,9 +948,11 @@ class SharePointApp(wx.Frame):
|
||||
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)))
|
||||
self.Bind(wx.EVT_MENU, lambda e: self.on_delete_items_clicked(selected_items), delete_item)
|
||||
added_folder_action = True
|
||||
|
||||
# Åbn i browser
|
||||
if item.get('web_url'):
|
||||
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.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, wx.ART_MENU, (16, 16)))
|
||||
@@ -1178,6 +1284,9 @@ class SharePointApp(wx.Frame):
|
||||
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):
|
||||
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']))
|
||||
else:
|
||||
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.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.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.home_btn.Enable()
|
||||
self.refresh_btn.Enable()
|
||||
@@ -1622,6 +1731,94 @@ class SharePointApp(wx.Frame):
|
||||
folders.sort(key=lambda x: x['name'].lower())
|
||||
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):
|
||||
self.tree_ctrl.DeleteChildren(parent_node)
|
||||
parent_data["loaded"] = True
|
||||
@@ -1673,14 +1870,18 @@ class SharePointApp(wx.Frame):
|
||||
if not data:
|
||||
return
|
||||
|
||||
self._navigate_to_item_data(data, tree_item=item)
|
||||
|
||||
def _navigate_to_item_data(self, data, tree_item=None):
|
||||
try:
|
||||
self.current_path = data["path"]
|
||||
self.update_path_display()
|
||||
|
||||
if not self.is_navigating_back:
|
||||
self.history.append(item)
|
||||
if tree_item and not self.is_navigating_back:
|
||||
# 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:
|
||||
self.back_btn.Enable(len(self.history) > 1)
|
||||
|
||||
@@ -1702,10 +1903,12 @@ class SharePointApp(wx.Frame):
|
||||
drives = res.json().get('value', [])
|
||||
drives.sort(key=lambda x: x.get('name', '').lower())
|
||||
for drive in drives:
|
||||
name = drive.get('name', '')
|
||||
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,
|
||||
"web_url": drive.get('webUrl')
|
||||
"web_url": drive.get('webUrl'),
|
||||
"path": data['path'] + [name]
|
||||
})
|
||||
elif data['type'] in ["DRIVE", "FOLDER"]:
|
||||
drive_id = data['drive_id']
|
||||
@@ -1726,7 +1929,9 @@ class SharePointApp(wx.Frame):
|
||||
"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')
|
||||
"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)
|
||||
@@ -1882,6 +2087,7 @@ class SharePointApp(wx.Frame):
|
||||
# Opdater knap-synlighed
|
||||
can_upload = self.current_drive_id is not None
|
||||
wx.CallAfter(lambda: self._safe_update_buttons(can_upload))
|
||||
self.set_status(self.get_txt("status_ready"))
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@@ -1900,7 +2106,10 @@ class SharePointApp(wx.Frame):
|
||||
item = self.current_items[item_idx]
|
||||
|
||||
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":
|
||||
self.open_file(item)
|
||||
|
||||
@@ -1911,19 +2120,28 @@ class SharePointApp(wx.Frame):
|
||||
|
||||
if selected.IsOk():
|
||||
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"):
|
||||
self._pending_tree_selection_id = target_id
|
||||
self.tree_ctrl.Expand(selected)
|
||||
return
|
||||
return True # Vi har sat en handling i gang
|
||||
|
||||
child, cookie = self.tree_ctrl.GetFirstChild(selected)
|
||||
while child.IsOk():
|
||||
cdata = self.tree_item_data.get(child)
|
||||
if cdata and cdata['id'] == target_id:
|
||||
if self.tree_ctrl.GetSelection() == child:
|
||||
return False # Allerede valgt
|
||||
self.tree_ctrl.SelectItem(child)
|
||||
return
|
||||
return True # Selection changed
|
||||
child, cookie = self.tree_ctrl.GetNextChild(selected, cookie)
|
||||
|
||||
return False
|
||||
|
||||
def go_back(self, event=None):
|
||||
if len(self.history) > 1:
|
||||
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)
|
||||
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 _do():
|
||||
@@ -1974,7 +2192,7 @@ class SharePointApp(wx.Frame):
|
||||
pass
|
||||
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
|
||||
|
||||
edit_event = threading.Event()
|
||||
@@ -1999,14 +2217,45 @@ class SharePointApp(wx.Frame):
|
||||
if res.status_code != 200:
|
||||
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)
|
||||
|
||||
# Beregn udgangspunkt hash
|
||||
original_hash = get_file_hash(local_path)
|
||||
# Verificer integritet og gem hash til senere sammenligning
|
||||
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
|
||||
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
|
||||
self.set_status(self.get_txt("msg_opening_file", name=file_name))
|
||||
@@ -2044,20 +2293,41 @@ class SharePointApp(wx.Frame):
|
||||
self.update_edit_ui()
|
||||
|
||||
# 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:
|
||||
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"))
|
||||
|
||||
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:
|
||||
# 5. Upload (kun hvis ændret)
|
||||
logger.info(f"Ændring fundet! Uploader {file_name}...")
|
||||
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)
|
||||
# 6. Checkin (Kun hvis vi faktisk uploadede noget)
|
||||
if is_checked_out:
|
||||
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"})
|
||||
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
|
||||
try:
|
||||
@@ -2074,6 +2344,11 @@ class SharePointApp(wx.Frame):
|
||||
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:
|
||||
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:
|
||||
del self.active_edits[item_id]
|
||||
self.update_edit_ui()
|
||||
|
||||
Reference in New Issue
Block a user