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:
+25
-4
@@ -175,14 +175,18 @@ def verify_integrity(local_path, remote_hash, config):
|
||||
|
||||
def get_headers(app, force_refresh=False):
|
||||
scopes = ["https://graph.microsoft.com/.default"]
|
||||
# If force_refresh is True, we don't rely on the cache
|
||||
result = None
|
||||
if not force_refresh:
|
||||
result = app.acquire_token_for_client(scopes=scopes)
|
||||
|
||||
if force_refresh or not result or "access_token" not in result:
|
||||
logger.info("Refreshing Access Token...")
|
||||
try:
|
||||
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:
|
||||
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}")
|
||||
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 ---
|
||||
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."""
|
||||
@@ -418,16 +427,28 @@ def main(config=None, stop_event=None):
|
||||
logger.info("Initializing SharePoint Production Sync Tool...")
|
||||
app = create_msal_app(tenant_id, client_id, client_secret)
|
||||
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 = []
|
||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS, thread_name_prefix="DL") as executor:
|
||||
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:
|
||||
if stop_event and stop_event.is_set():
|
||||
break
|
||||
logger.info(f"Scanning: {folder or 'Root'}")
|
||||
process_item_list(app, drive_id, folder, os.path.join(local_base, folder), report, executor, futures, config, stop_event)
|
||||
logger.info(f"[{drive['name']}] Scanning: {folder or 'Root'}")
|
||||
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...")
|
||||
for future in as_completed(futures):
|
||||
|
||||
+48
-8
@@ -47,19 +47,15 @@ class SharepointApp(ctk.CTk):
|
||||
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
|
||||
|
||||
self.entries = {}
|
||||
fields = [
|
||||
|
||||
# Felter før DOCUMENT_LIBRARY
|
||||
fields_before = [
|
||||
("TENANT_ID", "Tenant ID"),
|
||||
("CLIENT_ID", "Client ID"),
|
||||
("CLIENT_SECRET", "Client Secret"),
|
||||
("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):
|
||||
for i, (key, label) in enumerate(fields_before):
|
||||
lbl = ctk.CTkLabel(self.sidebar_frame, text=label)
|
||||
lbl.grid(row=i*2+1, column=0, padx=20, pady=(5, 0), sticky="w")
|
||||
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))
|
||||
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.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'))
|
||||
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):
|
||||
path = filedialog.askdirectory()
|
||||
if path:
|
||||
@@ -116,6 +152,10 @@ class SharepointApp(ctk.CTk):
|
||||
for key, entry in self.entries.items():
|
||||
val = config.get(key, "")
|
||||
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):
|
||||
config_lines = [f'{k} = "{v.get()}"' for k, v in self.entries.items()]
|
||||
|
||||
Reference in New Issue
Block a user