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:
+33
-12
@@ -175,15 +175,19 @@ 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...")
|
||||||
result = app.acquire_token_for_client(scopes=scopes, force_refresh=True)
|
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:
|
if "access_token" in result:
|
||||||
return {'Authorization': f'Bearer {result["access_token"]}'}
|
return {'Authorization': f'Bearer {result["access_token"]}'}
|
||||||
raise Exception(f"Auth failed: {result.get('error_description')}")
|
raise Exception(f"Auth failed: {result.get('error_description')}")
|
||||||
@@ -198,12 +202,12 @@ def get_drive_id(app, site_id, drive_name):
|
|||||||
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives"
|
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives"
|
||||||
response = safe_graph_get(app, url)
|
response = safe_graph_get(app, url)
|
||||||
drives = response.json().get('value', [])
|
drives = response.json().get('value', [])
|
||||||
|
|
||||||
# Prøv præcis match
|
# Prøv præcis match
|
||||||
for drive in drives:
|
for drive in drives:
|
||||||
if drive['name'] == drive_name:
|
if drive['name'] == drive_name:
|
||||||
return drive['id']
|
return drive['id']
|
||||||
|
|
||||||
# Prøv fallback til "Documents" hvis "Delte dokumenter" fejler (SharePoint standard)
|
# Prøv fallback til "Documents" hvis "Delte dokumenter" fejler (SharePoint standard)
|
||||||
if drive_name == "Delte dokumenter":
|
if drive_name == "Delte dokumenter":
|
||||||
for drive in drives:
|
for drive in drives:
|
||||||
@@ -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."""
|
||||||
@@ -414,20 +423,32 @@ def main(config=None, stop_event=None):
|
|||||||
local_base = config.get('LOCAL_PATH', '').replace('\\', os.sep)
|
local_base = config.get('LOCAL_PATH', '').replace('\\', os.sep)
|
||||||
|
|
||||||
folders = [f.strip() for f in folders_str.split(',') if f.strip()] or [""]
|
folders = [f.strip() for f in folders_str.split(',') if f.strip()] or [""]
|
||||||
|
|
||||||
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 folder in folders:
|
for drive in drives:
|
||||||
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'}")
|
drive_local_base = os.path.join(local_base, drive['name']) if use_subfolder else local_base
|
||||||
process_item_list(app, drive_id, folder, os.path.join(local_base, folder), report, executor, futures, config, stop_event)
|
for folder in folders:
|
||||||
|
if stop_event and stop_event.is_set():
|
||||||
|
break
|
||||||
|
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...")
|
logger.info(f"Scan complete. Processing {len(futures)} tasks...")
|
||||||
for future in as_completed(futures):
|
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.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()]
|
||||||
|
|||||||
Reference in New Issue
Block a user