Initial commit: SharePoint Explorer with modern UI and settings.json support
This commit is contained in:
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Local settings (keep your secrets safe)
|
||||||
|
settings.json
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
C:\Temp_SP\
|
||||||
65
GEMINI.md
Normal file
65
GEMINI.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# SharePoint Browser
|
||||||
|
|
||||||
|
A modern Python-based file browser for Microsoft SharePoint, specifically designed to bypass the Windows `MAX_PATH` (260 character) limitation. It achieves this by interacting directly with the Microsoft Graph API using unique IDs and downloading files to short, temporary local paths for editing.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
- **Purpose:** Provide a seamless SharePoint browsing and editing experience regardless of folder depth.
|
||||||
|
- **Key Strategy:** Uses unique Microsoft Graph API IDs instead of traditional file paths to avoid path length issues.
|
||||||
|
- **Core Workflow:**
|
||||||
|
1. Authenticate via MSAL.
|
||||||
|
2. Browse SharePoint sites/folders dynamically.
|
||||||
|
3. **Checkout** a file on SharePoint.
|
||||||
|
4. **Download** to a short local path (e.g., `C:\Temp_SP\[ShortHash].[ext]`).
|
||||||
|
5. **Monitor** local file usage; detect when the editing application is closed.
|
||||||
|
6. **Upload** the modified file back to SharePoint.
|
||||||
|
7. **Checkin** and clean up local temporary files.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Language:** Python 3.x
|
||||||
|
- **GUI Framework:** [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) (Modern, responsive UI)
|
||||||
|
- **Authentication:** [MSAL (Microsoft Authentication Library)](https://github.com/AzureAD/microsoft-authentication-library-for-python)
|
||||||
|
- **API Interaction:** Microsoft Graph API via `requests`
|
||||||
|
- **File Monitoring:** Local file lock polling
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Ensure you have Python installed. You will also need to install the following dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install customtkinter msal requests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
|
||||||
|
Execute the main script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python sharepoint_browser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The application is pre-configured with a `CLIENT_ID` and `TENANT_ID` for Microsoft authentication. These are located at the top of `sharepoint_browser.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
CLIENT_ID = '281c91fc-4572-4614-b4d2-e3795d4b502d'
|
||||||
|
TENANT_ID = 'cce9d2ae-239b-4a22-86bb-fb904c85be79'
|
||||||
|
TEMP_DIR = "C:\\Temp_SP"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture & Conventions
|
||||||
|
|
||||||
|
- **Main Entry Point:** `sharepoint_browser.py` contains both the GUI logic and the Graph API integration.
|
||||||
|
- **Threading:** File operations (download, wait, upload) are executed in background threads (`threading.Thread`) to keep the GUI responsive.
|
||||||
|
- **File Detection:** Uses a polling mechanism with `os.rename(local_path, local_path)` to detect when the external editor (e.g., Word, Excel) has released the file lock.
|
||||||
|
- **Path Handling:** Generates unique local filenames using MD5 hashes of the SharePoint item IDs to ensure they remain short and avoid collisions.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `sharepoint_browser.py`: The complete application logic.
|
||||||
|
- `project_description.md`: Original project requirements and context (in Danish).
|
||||||
|
- `GEMINI.md`: This instructional context file.
|
||||||
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# SharePoint Explorer
|
||||||
|
|
||||||
|
En moderne Python-baseret fil-browser til Microsoft SharePoint, designet til at omgå Windows' `MAX_PATH` (260 karakterer) begrænsning.
|
||||||
|
|
||||||
|
## Funktioner
|
||||||
|
- **Søg og Browse:** Naviger dynamisk gennem SharePoint sites, dokumentbiblioteker og mapper.
|
||||||
|
- **Sikker Redigering:** Automatisk Check-out/Check-in håndtering via Microsoft Graph API.
|
||||||
|
- **Explorer Vibes:** Moderne brugerflade med sortering (mapper øverst) og brødkrummesti (breadcrumb).
|
||||||
|
- **Ingen Sti-begrænsning:** Arbejder med unikke ID'er og korte midlertidige stier for at undgå MAX_PATH fejl.
|
||||||
|
|
||||||
|
## Installation & Udvikling
|
||||||
|
|
||||||
|
### Forudsætninger
|
||||||
|
- Python 3.x
|
||||||
|
- Microsoft 365 licens (Business Standard eller højere anbefales)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
1. Installer afhængigheder:
|
||||||
|
```bash
|
||||||
|
pip install customtkinter msal requests
|
||||||
|
```
|
||||||
|
2. Konfigurer `settings.json` med din `client_id` og `tenant_id`.
|
||||||
|
|
||||||
|
### Kørsel
|
||||||
|
```bash
|
||||||
|
python sharepoint_browser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kompilering til EXE
|
||||||
|
For at pakke programmet til en enkelt selvstændig `.exe` fil:
|
||||||
|
```bash
|
||||||
|
pip install pyinstaller
|
||||||
|
python -m PyInstaller --noconsole --onefile --collect-all customtkinter --name "SharePoint Explorer" sharepoint_browser.py
|
||||||
|
```
|
||||||
|
Den færdige fil findes i mappen `dist/`. Husk at placere `settings.json` i samme mappe som `.exe` filen.
|
||||||
|
|
||||||
|
## Konfiguration (`settings.json`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_id": "DIN_CLIENT_ID",
|
||||||
|
"tenant_id": "DIN_TENANT_ID",
|
||||||
|
"temp_dir": "C:\\Temp_SP"
|
||||||
|
}
|
||||||
|
```
|
||||||
42
project_description.md
Normal file
42
project_description.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
Projektbeskrivelse: Python SharePoint File Browser (Modern GUI)
|
||||||
|
Kontekst og Formål:
|
||||||
|
Udvikl en fil-browser med en moderne grafisk brugerflade i Python. Formålet er at løse problemet med Windows', SharePoints og OneDrives begrænsning på stinavne (over 260 karakterer / MAX_PATH). Programmet skal omgå denne grænse ved at arbejde med filernes unikke ID'er og URL'er via Microsoft Graph API frem for at bruge traditionelle filstier.
|
||||||
|
|
||||||
|
Teknologier:
|
||||||
|
|
||||||
|
Sprog: Python
|
||||||
|
|
||||||
|
GUI-framework: CustomTkinter (eller PyQt6) for et moderne, responsivt design.
|
||||||
|
|
||||||
|
Integration: Microsoft Graph API (Autentificering via MSAL).
|
||||||
|
|
||||||
|
Filhåndtering: Windows OS procesovervågning og native fil-operationer.
|
||||||
|
|
||||||
|
Kernefunktioner og User Flow:
|
||||||
|
|
||||||
|
Autentificering: Sikker login via MSAL (Microsoft Authentication Library) til brugerens Microsoft 365 / SharePoint konto.
|
||||||
|
|
||||||
|
Navigation (UI): En moderne grænseflade, hvor brugeren kan browse SharePoint-sites, mapper og filer dynamisk – uanset hvor dyb mappestrukturen er.
|
||||||
|
|
||||||
|
Sikker stihåndtering & Check-out: Når en fil vælges til redigering:
|
||||||
|
|
||||||
|
Udfør et "Check-out" via Graph API, så filen låses for andre brugere på SharePoint.
|
||||||
|
|
||||||
|
Download filen til en midlertidig, meget kort lokal sti (f.eks. C:\Temp\[KortHash].[ext]) for at undgå MAX_PATH-begrænsningen i Windows.
|
||||||
|
|
||||||
|
Lokal Redigering & Overvågning:
|
||||||
|
|
||||||
|
Åbn den downloadede fil med standardprogrammet i Windows (f.eks. Word til .docx).
|
||||||
|
|
||||||
|
Programmet skal overvåge den specifikke proces eller fillås og registrere, præcis hvornår brugeren lukker programmet/filen igen.
|
||||||
|
|
||||||
|
Gen-upload & Check-in:
|
||||||
|
|
||||||
|
Når filen er lukket lokalt, uploades den ændrede fil automatisk tilbage til den oprindelige placering via Graph API.
|
||||||
|
|
||||||
|
Udfør et "Check-in" på SharePoint for at frigive filen til andre.
|
||||||
|
|
||||||
|
Slet den midlertidige lokale fil for at rydde op.
|
||||||
|
|
||||||
|
Krav til output:
|
||||||
|
Jeg ønsker det komplette, kørbare Python-script og ikke kun delektioner eller ændringer. Undlad for meget forklarende tekst, og fokuser på at levere en robust og fuldstændig kode.
|
||||||
284
sharepoint_browser.py
Normal file
284
sharepoint_browser.py
Normal 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()
|
||||||
Reference in New Issue
Block a user