Files
sharepoint-browser/sharepoint_browser.py

325 lines
13 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:
# Gem det ID vi er i NU, før vi går dybere, så vi kan vende tilbage til det
self.history.append(("FOLDERS", self.current_folder_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":
# Nu bruger vi det præcise ID vi gemte før (prev_id er folder_id)
self.load_folder(prev_id)
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}")
# Lav en unik undermappe til filen for at bevare det originale navn uden konflikter
short_hash = hashlib.md5(item_id.encode()).hexdigest()[:8]
file_dir = os.path.join(TEMP_DIR, short_hash)
if not os.path.exists(file_dir):
os.makedirs(file_dir)
local_path = os.path.join(file_dir, file_name)
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()