import os import time import threading import hashlib import json import sys import requests import msal import customtkinter as ctk import tkinter as tk from tkinter import messagebox # --- STIHÅNDTERING (Til EXE-brug) --- if getattr(sys, 'frozen', False): # Vi kører som en kompileret .exe base_dir = os.path.dirname(sys.executable) else: # Vi kører som normalt script base_dir = os.path.dirname(os.path.abspath(__file__)) SETTINGS_FILE = os.path.join(base_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" } if not os.path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, 'w') as f: json.dump(default_settings, f, indent=4) return default_settings with open(SETTINGS_FILE, 'r') as f: return json.load(f) 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") if not os.path.exists(TEMP_DIR): os.makedirs(TEMP_DIR) ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") class SharePointApp(ctk.CTk): def __init__(self): super().__init__() self.title("SharePoint Explorer") self.geometry("1000x750") self.access_token = None self.headers = {} # Navigation State 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" # UI Layout - 3 Rækker: Top (Nav), Midt (Filer), Bund (Info) self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) # 1. TOP NAVIGATION BAR self.nav_frame = ctk.CTkFrame(self, height=60, corner_radius=0) self.nav_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0) self.back_btn = ctk.CTkButton(self.nav_frame, text="← Tilbage", width=100, command=self.go_back, state="disabled") self.back_btn.pack(side="left", padx=10, pady=10) self.home_btn = ctk.CTkButton(self.nav_frame, text="🏠 Hjem", width=100, command=self.load_sites, state="disabled") self.home_btn.pack(side="left", padx=5, pady=10) self.login_btn = ctk.CTkButton(self.nav_frame, text="Log ind", width=120, fg_color="#28a745", hover_color="#218838", command=self.login) self.login_btn.pack(side="right", padx=10, pady=10) # 2. MAIN CONTENT (File Area) self.main_frame = ctk.CTkScrollableFrame(self, fg_color=("gray95", "gray10"), corner_radius=0) self.main_frame.grid(row=1, column=0, sticky="nsew", padx=0, pady=0) # 3. FOOTER (Path & Status) self.footer_frame = ctk.CTkFrame(self, height=40, corner_radius=0) self.footer_frame.grid(row=2, column=0, sticky="ew") self.path_label = ctk.CTkLabel(self.footer_frame, text="Sti: /", font=ctk.CTkFont(size=12, weight="bold")) self.path_label.pack(side="left", padx=15, pady=5) self.status_label = ctk.CTkLabel(self.footer_frame, text="Klar", text_color="gray") self.status_label.pack(side="right", padx=15, pady=5) def set_status(self, text): self.status_label.configure(text=text) self.update_idletasks() def update_path_display(self): path_str = " > ".join(self.current_path) self.path_label.configure(text=f"📍 {path_str}") def login(self): self.set_status("Logger ind...") app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY) accounts = app.get_accounts() result = None if accounts: result = app.acquire_token_silent(SCOPES, account=accounts[0]) if not result: result = 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.configure(state="disabled", text="Logget ind", fg_color="gray") self.home_btn.configure(state="normal") self.load_sites() else: self.set_status("Login fejlede.") messagebox.showerror("Login Error", result.get("error_description", "Unknown error")) def clear_main(self): for widget in self.main_frame.winfo_children(): widget.destroy() self.back_btn.configure(state="normal" if self.history else "disabled") self.update_path_display() def create_explorer_item(self, text, icon, command, is_file=False): # En knap der ligner et række-element i Windows Explorer frame = ctk.CTkFrame(self.main_frame, fg_color="transparent") frame.pack(fill="x", padx=10, pady=1) btn = ctk.CTkButton( frame, text=f" {icon} {text}", anchor="w", height=35, fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray80", "gray25"), font=ctk.CTkFont(size=13), command=command ) btn.pack(fill="x") return btn def load_sites(self): self.set_status("Henter sites...") self.clear_main() self.current_path = ["SharePoint"] self.history = [] self.update_path_display() 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', []) # Sorter sites alfabetisk sites.sort(key=lambda x: x.get('displayName', x.get('name', '')).lower()) for site in sites: name = site.get('displayName', site.get('name')) self.create_explorer_item(name, "🌐", lambda s=site['id'], n=name: self.select_site(s, n)) self.set_status(f"Fandt {len(sites)} sites.") else: self.set_status("Kunne ikke hente sites.") def select_site(self, site_id, name): self.history.append(("SITES", None, "SharePoint")) self.current_site_id = site_id self.current_path.append(name) self.load_drives(site_id) def load_drives(self, site_id): self.set_status("Henter dokumentbiblioteker...") self.clear_main() url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives" res = requests.get(url, headers=self.headers) if res.status_code == 200: drives = res.json().get('value', []) # Sorter biblioteker alfabetisk drives.sort(key=lambda x: x.get('name', '').lower()) for drive in drives: name = drive.get('name') self.create_explorer_item(name, "📚", lambda d=drive['id'], n=name: self.select_drive(d, n)) self.set_status("Vælg et bibliotek.") else: self.set_status("Fejl ved hentning af biblioteker.") def select_drive(self, drive_id, name): self.history.append(("DRIVES", self.current_site_id, self.current_path[-1])) self.current_drive_id = drive_id self.current_path.append(name) self.load_folder("root") def load_folder(self, folder_id): self.set_status("Indlæser filer...") self.clear_main() url = f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/root/children" if folder_id == "root" else \ f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/items/{folder_id}/children" res = requests.get(url, headers=self.headers) if res.status_code == 200: items = res.json().get('value', []) # Sorter: Mapper først, derefter filer. Begge alfabetisk. items.sort(key=lambda x: (not 'folder' in x, x['name'].lower())) self.current_folder_id = folder_id for item in items: name, item_id, is_folder = item['name'], item['id'], 'folder' in item icon = "📁" if is_folder else "📄" cmd = lambda i=item_id, f=is_folder, n=name: self.item_clicked(i, f, n) self.create_explorer_item(name, icon, cmd, not is_folder) self.set_status("Klar") else: self.set_status(f"Fejl: {res.status_code}") def item_clicked(self, item_id, is_folder, name): if is_folder: self.history.append(("FOLDERS", self.current_drive_id, self.current_path[-1])) self.current_path.append(name) self.load_folder(item_id) else: threading.Thread(target=self.process_file, args=(item_id, name), daemon=True).start() def go_back(self): if not self.history: return mode, prev_id, path_segment = self.history.pop() self.current_path.pop() if mode == "SITES": self.load_sites() elif mode == "DRIVES": self.load_drives(prev_id) elif mode == "FOLDERS": # Her skal vi gemme folder_id i historikken for at gå tilbage korrekt self.load_folder("root") # Forenklet: Går tilbage til rod def process_file(self, item_id, file_name): base_url = f"https://graph.microsoft.com/v1.0/drives/{self.current_drive_id}/items/{item_id}" try: # 1. Checkout self.set_status(f"Tjekker '{file_name}' ud...") checkout_res = requests.post(f"{base_url}/checkout", headers=self.headers) if checkout_res.status_code not in [200, 204]: print(f"Checkout info: {checkout_res.status_code} - {checkout_res.text}") # 2. Download self.set_status(f"Downloader '{file_name}'...") res = requests.get(f"{base_url}/content", headers=self.headers) if res.status_code != 200: raise Exception(f"Download fejlede: {res.status_code}") ext = os.path.splitext(file_name)[1] local_path = os.path.join(TEMP_DIR, f"{hashlib.md5(item_id.encode()).hexdigest()[:8]}{ext}") with open(local_path, 'wb') as f: f.write(res.content) # 3. Åbn & Overvåg self.set_status(f"Åbner '{file_name}'... Vent på programmet starter.") os.startfile(local_path) # Vent på at programmet rent faktisk låser filen locked = False self.set_status(f"Venter på at '{file_name}' åbnes i redigeringsprogram...") for _ in range(10): # Vent op til 10 sekunder på at filen bliver låst time.sleep(1) try: os.rename(local_path, local_path) except OSError: locked = True break if locked: self.set_status(f"Redigerer '{file_name}' - Luk programmet for at gemme.") # Nu venter vi på at den bliver låst OP igen while True: time.sleep(2) try: os.rename(local_path, local_path) break # Filen er ikke længere låst except OSError: pass else: self.set_status("Kunne ikke detektere fillås. Tryk OK når du er færdig.") messagebox.showinfo("Info", f"Vi kunne ikke detektere om '{file_name}' blev låst af dit program.\n\nTryk OK når du har gemt og LUKKET filen i dit redigeringsprogram.") # 4. Upload self.set_status(f"Uploader ændringer til SharePoint...") 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"Upload fejlede: {upload_res.status_code} - {upload_res.text}") # 5. Checkin self.set_status(f"Tjekker '{file_name}' ind...") checkin_res = requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "Opdateret via SP Explorer"}) if checkin_res.status_code not in [200, 204]: print(f"Checkin info: {checkin_res.status_code} - {checkin_res.text}") os.remove(local_path) self.set_status(f"Succes! '{file_name}' er opdateret på SharePoint.") messagebox.showinfo("Færdig", f"Filen '{file_name}' er gemt og tjekket ind korrekt.") except Exception as e: self.set_status(f"Fejl: {str(e)}") print(f"DETALJERET FEJL: {e}") messagebox.showerror("Fejl", f"Der skete en fejl:\n{e}") if __name__ == "__main__": app = SharePointApp() app.mainloop()