Initial commit: SharePoint Explorer with modern UI and settings.json support

This commit is contained in:
Martin Tranberg
2026-03-30 15:18:48 +02:00
commit 741a7ee489
5 changed files with 481 additions and 0 deletions

284
sharepoint_browser.py Normal file
View File

@@ -0,0 +1,284 @@
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"Låser '{file_name}'...")
requests.post(f"{base_url}/checkout", headers=self.headers)
# 2. Download
self.set_status(f"Downloader '{file_name}'...")
res = requests.get(f"{base_url}/content", headers=self.headers)
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"Redigerer '{file_name}' - luk filen for at gemme...")
os.startfile(local_path)
time.sleep(3)
while True:
try:
os.rename(local_path, local_path)
break
except OSError:
time.sleep(2)
# 4. Upload & Checkin
self.set_status(f"Uploader ændringer...")
with open(local_path, 'rb') as f:
requests.put(f"{base_url}/content", headers=self.headers, data=f)
requests.post(f"{base_url}/checkin", headers=self.headers, json={"comment": "Opdateret via SP Explorer"})
os.remove(local_path)
self.set_status(f"'{file_name}' er gemt og tjekket ind.")
except Exception as e:
self.set_status(f"Fejl: {str(e)}")
messagebox.showerror("Fejl", f"Der skete en fejl under håndtering af filen:\n{e}")
if __name__ == "__main__":
app = SharePointApp()
app.mainloop()