From 4240ac6e052206b22a85d88e5e0b9568262f1256 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 07:10:32 +0000 Subject: [PATCH] feat: Add dynamic Seafile repo lookup by name and robust path handling - Updated `SeafileClient` to accept `repo_name` in configuration. - Implemented `get_repo_id_by_name` to dynamically resolve `repo_id` from the Seafile API, ensuring 'rw' permission. - Added logic in `get_share_link` to automatically strip the repository name from the file path if present, fixing compatibility with Rclone paths that include the repository root. - Updated `main.py` to initialize `SeafileClient` with `repo_name` and handle initialization failures gracefully. - Updated `config.example.yaml` to include `repo_name` field. --- config/config.example.yaml | 3 +- src/main.py | 16 ++++++--- src/seafile_client.py | 72 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 59c2408..f41bc89 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -7,7 +7,8 @@ log_level: "INFO" # INFO, DEBUG, WARNING, ERROR seafile: host: "https://box.nju.edu.cn" api_token: "YOUR_SEAFILE_API_TOKEN_HERE" - repo_id: "YOUR_REPO_UUID_HERE" + repo_name: "YOUR_REPO_NAME_HERE" # Optional if repo_id is provided + # repo_id: "YOUR_REPO_UUID_HERE" # Optional if repo_name is provided # Rclone Configuration rclone: diff --git a/src/main.py b/src/main.py index 0b9535d..8e3e1f8 100644 --- a/src/main.py +++ b/src/main.py @@ -74,11 +74,17 @@ def main(): target_path = Path(args.path) # Init Clients - seafile = SeafileClient( - config['seafile']['host'], - config['seafile']['api_token'], - config['seafile']['repo_id'] - ) + try: + seafile = SeafileClient( + config['seafile']['host'], + config['seafile']['api_token'], + repo_id=config['seafile'].get('repo_id'), + repo_name=config['seafile'].get('repo_name') + ) + except Exception as e: + logging.critical(f"Failed to initialize Seafile client: {e}") + sys.exit(1) + rclone = RcloneWrapper( config['rclone']['remote_name'], config['rclone']['bwlimit'] diff --git a/src/seafile_client.py b/src/seafile_client.py index 1c329ed..cdd0f4c 100644 --- a/src/seafile_client.py +++ b/src/seafile_client.py @@ -3,13 +3,81 @@ import logging class SeafileClient: - def __init__(self, host, token, repo_id): + def __init__(self, host, token, repo_id=None, repo_name=None): self.host = host self.headers = {"Authorization": f"Token {token}", "Accept": "application/json"} - self.repo_id = repo_id + self.repo_name = repo_name + self.repo_id = None + + if self.repo_name: + try: + found_id = self.get_repo_id_by_name(self.repo_name) + if found_id: + self.repo_id = found_id + logging.info(f"Resolved repo_name '{self.repo_name}' to repo_id '{self.repo_id}'") + else: + logging.warning(f"Could not find repo with name '{self.repo_name}' and permission 'rw'.") + except Exception as e: + logging.error(f"Error during repo lookup: {e}") + + # Fallback or primary use of repo_id + if not self.repo_id and repo_id: + self.repo_id = repo_id + if self.repo_name: + logging.info(f"Falling back to provided repo_id '{self.repo_id}'") + + if not self.repo_id: + raise ValueError("Could not determine repo_id. Please provide a valid repo_id or a reachable repo_name.") + + def get_repos(self): + """Fetches the list of repositories from the API.""" + url = urljoin(self.host, "/api/v2.1/repos/") + resp = requests.get(url, headers=self.headers) + if not resp.ok: + logging.error(f"Failed to list repos. Status: {resp.status_code}, Body: {resp.text}") + resp.raise_for_status() + return resp.json() + + def get_repo_id_by_name(self, target_name): + """ + Finds a repo ID by name from the API response. + Ensures permission is 'rw'. + """ + data = self.get_repos() + + # Handle if response is dict with 'repos' key or just a list + repos = [] + if isinstance(data, dict): + repos = data.get('repos', []) + elif isinstance(data, list): + repos = data + else: + logging.error(f"Unexpected API response format: {type(data)}") + return None + + for repo in repos: + # Check for name match and write permission + if repo.get('repo_name') == target_name and repo.get('permission') == 'rw': + return repo.get('repo_id') + + return None def get_share_link(self, remote_path): """Generates or retrieves a direct download link.""" + + # Adjust path if repo_name is known and path starts with it + # This handles the case where Rclone remote root includes the repo name + if self.repo_name: + prefix = f"/{self.repo_name}" + # Check for "/RepoName" (exact) or "/RepoName/..." + if remote_path == prefix or remote_path.startswith(f"{prefix}/"): + # Strip the prefix + original_path = remote_path + remote_path = remote_path[len(prefix):] + if not remote_path.startswith("/"): + remote_path = "/" + remote_path + logging.info(f"Adjusted path from '{original_path}' to '{remote_path}' based on repo_name") + url = urljoin(self.host, "/api/v2.1/share-links/") # Payload for creating a link