Add support for downloading all document libraries on a site

- New get_all_drives() fetches all drives from a SharePoint site
- main() loops over all drives when DOCUMENT_LIBRARY is empty, placing
  each library in its own subfolder under LOCAL_PATH
- MSAL force_refresh now catches TypeError/ValueError for compatibility
  with older MSAL versions that don't support the parameter
- GUI: "Download alle biblioteker" checkbox disables Library Navn and
  Mapper fields; load_settings restores checkbox state from config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Tranberg
2026-05-04 13:41:41 +02:00
parent d15b9afc03
commit 24665c5797
2 changed files with 81 additions and 20 deletions
+25 -4
View File
@@ -175,14 +175,18 @@ def verify_integrity(local_path, remote_hash, config):
def get_headers(app, force_refresh=False): def get_headers(app, force_refresh=False):
scopes = ["https://graph.microsoft.com/.default"] scopes = ["https://graph.microsoft.com/.default"]
# If force_refresh is True, we don't rely on the cache
result = None result = None
if not force_refresh: if not force_refresh:
result = app.acquire_token_for_client(scopes=scopes) result = app.acquire_token_for_client(scopes=scopes)
if force_refresh or not result or "access_token" not in result: if force_refresh or not result or "access_token" not in result:
logger.info("Refreshing Access Token...") logger.info("Refreshing Access Token...")
try:
result = app.acquire_token_for_client(scopes=scopes, force_refresh=True) result = app.acquire_token_for_client(scopes=scopes, force_refresh=True)
except (TypeError, ValueError):
# Ældre MSAL-versioner understøtter ikke force_refresh på acquire_token_for_client.
# MSAL håndterer udløbne tokens automatisk, så et nyt kald er tilstrækkeligt.
result = app.acquire_token_for_client(scopes=scopes)
if "access_token" in result: if "access_token" in result:
return {'Authorization': f'Bearer {result["access_token"]}'} return {'Authorization': f'Bearer {result["access_token"]}'}
@@ -216,6 +220,11 @@ def get_drive_id(app, site_id, drive_name):
logger.error(f"Drive '{drive_name}' not found. Available drives on this site: {available_names}") logger.error(f"Drive '{drive_name}' not found. Available drives on this site: {available_names}")
raise Exception(f"Drive {drive_name} not found. Check the log for available drive names.") raise Exception(f"Drive {drive_name} not found. Check the log for available drive names.")
def get_all_drives(app, site_id):
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives"
response = safe_graph_get(app, url)
return response.json().get('value', [])
# --- Punkt 2: Resume / Chunked Download logic --- # --- Punkt 2: Resume / Chunked Download logic ---
def get_fresh_download_url(app, drive_id, item_id): def get_fresh_download_url(app, drive_id, item_id):
"""Fetches a fresh download URL for a specific item ID with retries and robust error handling.""" """Fetches a fresh download URL for a specific item ID with retries and robust error handling."""
@@ -418,16 +427,28 @@ def main(config=None, stop_event=None):
logger.info("Initializing SharePoint Production Sync Tool...") logger.info("Initializing SharePoint Production Sync Tool...")
app = create_msal_app(tenant_id, client_id, client_secret) app = create_msal_app(tenant_id, client_id, client_secret)
site_id = get_site_id(app, site_url) site_id = get_site_id(app, site_url)
drive_id = get_drive_id(app, site_id, drive_name)
if not drive_name:
drives = get_all_drives(app, site_id)
logger.info(f"Downloading all {len(drives)} document libraries: {[d['name'] for d in drives]}")
use_subfolder = True
folders = [""] # Download fra rod af hvert bibliotek
else:
drives = [{'id': get_drive_id(app, site_id, drive_name), 'name': drive_name}]
use_subfolder = False
report = [] report = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS, thread_name_prefix="DL") as executor: with ThreadPoolExecutor(max_workers=MAX_WORKERS, thread_name_prefix="DL") as executor:
futures = {} futures = {}
for drive in drives:
if stop_event and stop_event.is_set():
break
drive_local_base = os.path.join(local_base, drive['name']) if use_subfolder else local_base
for folder in folders: for folder in folders:
if stop_event and stop_event.is_set(): if stop_event and stop_event.is_set():
break break
logger.info(f"Scanning: {folder or 'Root'}") logger.info(f"[{drive['name']}] Scanning: {folder or 'Root'}")
process_item_list(app, drive_id, folder, os.path.join(local_base, folder), report, executor, futures, config, stop_event) process_item_list(app, drive['id'], folder, os.path.join(drive_local_base, folder), report, executor, futures, config, stop_event)
logger.info(f"Scan complete. Processing {len(futures)} tasks...") logger.info(f"Scan complete. Processing {len(futures)} tasks...")
for future in as_completed(futures): for future in as_completed(futures):
+48 -8
View File
@@ -47,19 +47,15 @@ class SharepointApp(ctk.CTk):
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10)) self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
self.entries = {} self.entries = {}
fields = [
# Felter før DOCUMENT_LIBRARY
fields_before = [
("TENANT_ID", "Tenant ID"), ("TENANT_ID", "Tenant ID"),
("CLIENT_ID", "Client ID"), ("CLIENT_ID", "Client ID"),
("CLIENT_SECRET", "Client Secret"), ("CLIENT_SECRET", "Client Secret"),
("SITE_URL", "Site URL"), ("SITE_URL", "Site URL"),
("DOCUMENT_LIBRARY", "Library Navn"),
("FOLDERS_TO_DOWNLOAD", "Mapper (komma-sep)"),
("LOCAL_PATH", "Lokal Sti"),
("ENABLE_HASH_VALIDATION", "Valider Hash (True/False)"),
("HASH_THRESHOLD_MB", "Hash Grænse (MB)")
] ]
for i, (key, label) in enumerate(fields_before):
for i, (key, label) in enumerate(fields):
lbl = ctk.CTkLabel(self.sidebar_frame, text=label) lbl = ctk.CTkLabel(self.sidebar_frame, text=label)
lbl.grid(row=i*2+1, column=0, padx=20, pady=(5, 0), sticky="w") lbl.grid(row=i*2+1, column=0, padx=20, pady=(5, 0), sticky="w")
entry = ctk.CTkEntry(self.sidebar_frame, width=280) entry = ctk.CTkEntry(self.sidebar_frame, width=280)
@@ -67,6 +63,36 @@ class SharepointApp(ctk.CTk):
entry.grid(row=i*2+2, column=0, padx=20, pady=(0, 5)) entry.grid(row=i*2+2, column=0, padx=20, pady=(0, 5))
self.entries[key] = entry self.entries[key] = entry
# DOCUMENT_LIBRARY med "Alle biblioteker"-afkrydsningsfelt
lbl_lib = ctk.CTkLabel(self.sidebar_frame, text="Library Navn")
lbl_lib.grid(row=9, column=0, padx=20, pady=(5, 0), sticky="w")
self.library_entry = ctk.CTkEntry(self.sidebar_frame, width=280)
self.library_entry.grid(row=10, column=0, padx=20, pady=(0, 2))
self.entries["DOCUMENT_LIBRARY"] = self.library_entry
self.all_libraries_var = ctk.BooleanVar(value=False)
self.all_libraries_cb = ctk.CTkCheckBox(
self.sidebar_frame, text="Download alle biblioteker",
variable=self.all_libraries_var,
command=self.toggle_library_entry
)
self.all_libraries_cb.grid(row=11, column=0, padx=20, pady=(0, 5), sticky="w")
# Felter efter DOCUMENT_LIBRARY
fields_after = [
("FOLDERS_TO_DOWNLOAD", "Mapper (komma-sep)"),
("LOCAL_PATH", "Lokal Sti"),
("ENABLE_HASH_VALIDATION", "Valider Hash (True/False)"),
("HASH_THRESHOLD_MB", "Hash Grænse (MB)")
]
for i, (key, label) in enumerate(fields_after):
base_row = 12 + i * 2
lbl = ctk.CTkLabel(self.sidebar_frame, text=label)
lbl.grid(row=base_row, column=0, padx=20, pady=(5, 0), sticky="w")
entry = ctk.CTkEntry(self.sidebar_frame, width=280)
entry.grid(row=base_row+1, column=0, padx=20, pady=(0, 5))
self.entries[key] = entry
self.browse_button = ctk.CTkButton(self.sidebar_frame, text="Vælg Mappe", command=self.browse_folder, height=32) self.browse_button = ctk.CTkButton(self.sidebar_frame, text="Vælg Mappe", command=self.browse_folder, height=32)
self.browse_button.grid(row=20, column=0, padx=20, pady=10) self.browse_button.grid(row=20, column=0, padx=20, pady=10)
@@ -104,6 +130,16 @@ class SharepointApp(ctk.CTk):
handler.setFormatter(logging.Formatter('%(asctime)s: %(message)s', datefmt='%H:%M:%S')) handler.setFormatter(logging.Formatter('%(asctime)s: %(message)s', datefmt='%H:%M:%S'))
download_sharepoint.logger.addHandler(handler) download_sharepoint.logger.addHandler(handler)
def toggle_library_entry(self):
if self.all_libraries_var.get():
self.library_entry.configure(state="normal")
self.library_entry.delete(0, "end")
self.library_entry.configure(state="disabled")
self.entries["FOLDERS_TO_DOWNLOAD"].configure(state="disabled")
else:
self.library_entry.configure(state="normal")
self.entries["FOLDERS_TO_DOWNLOAD"].configure(state="normal")
def browse_folder(self): def browse_folder(self):
path = filedialog.askdirectory() path = filedialog.askdirectory()
if path: if path:
@@ -116,6 +152,10 @@ class SharepointApp(ctk.CTk):
for key, entry in self.entries.items(): for key, entry in self.entries.items():
val = config.get(key, "") val = config.get(key, "")
entry.insert(0, val) entry.insert(0, val)
if not config.get("DOCUMENT_LIBRARY", ""):
self.all_libraries_cb.select()
self.library_entry.configure(state="disabled")
self.entries["FOLDERS_TO_DOWNLOAD"].configure(state="disabled")
def save_settings(self): def save_settings(self):
config_lines = [f'{k} = "{v.get()}"' for k, v in self.entries.items()] config_lines = [f'{k} = "{v.get()}"' for k, v in self.entries.items()]