Files
sharepoint-browser/sharepoint_browser.py

285 lines
11 KiB
Python

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()