From 9a4c0b177652be23b6e10ef42fc62d9c45b755e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:44:20 +0100 Subject: [PATCH] Add download support to CLI and client Implemented file and folder download functionality in the CLI and GofileClient, including new CLI arguments for downloading by URL or content ID and specifying output directory. --- README.md | 34 +++++++- src/gofilepy/cli.py | 172 +++++++++++++++++++++++++++++++++++--- src/gofilepy/client.py | 82 ++++++++++++++++++ test_download.py | 18 ++++ test.py => test_upload.py | 2 +- 5 files changed, 295 insertions(+), 13 deletions(-) create mode 100644 test_download.py rename test.py => test_upload.py (79%) diff --git a/README.md b/README.md index 0c884f1..5bb67c6 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ It supports the free API tiers, streaming uploads (low memory usage for large fi ## Features - **Streaming Uploads**: Upload 100GB+ files without loading them into RAM. +- **Download Support**: Download files from Gofile URLs or content IDs. - **Folder Management**: Upload to specific folders or create new ones automatically. - **Script Ready**: JSON output mode for easy parsing in pipelines. - **Free Tier Support**: Handles Guest accounts and Standard tokens. -- **Progress Bar**: Visual feedback for long uploads. +- **Progress Bar**: Visual feedback for long uploads and downloads. ## Installation @@ -68,6 +69,24 @@ Upload multiple files. The first file creates a folder, and the rest are uploade gofilepy -s part1.rar part2.rar part3.rar ``` +### Download Files +Download files from a Gofile URL or content ID. + +Download from URL: +```bash +gofilepy -d https://gofile.io/d/GxHNKL +``` + +Download from content ID: +```bash +gofilepy -d GxHNKL +``` + +Download to specific directory +```bash +gofilepy -d GxHNKL -o ./downloads +``` + ### Scripting Mode (JSON Output) Use `--json` to suppress human-readable text and output a JSON array. @@ -87,6 +106,8 @@ gofilepy -vv big_file.iso You can use `gofilepy` in your own Python scripts. +### Upload Files + ```python from gofilepy import GofileClient @@ -97,6 +118,17 @@ print(file.name) print(file.page_link) # View and download file at this link ``` +### Download Files + +```python +from gofilepy import GofileClient + +client = GofileClient() +contents = client.get_contents("GxHNKL") +print("Folder contents:") +print(contents) +``` + ## Development For contributors and developers: diff --git a/src/gofilepy/cli.py b/src/gofilepy/cli.py index 3f557eb..6950837 100644 --- a/src/gofilepy/cli.py +++ b/src/gofilepy/cli.py @@ -24,7 +24,21 @@ def parse_arguments() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Gofile.io CLI Uploader (HTTPX Edition)", ) - parser.add_argument("files", nargs="+", help="Files to upload") + parser.add_argument("files", nargs="*", help="Files to upload") + parser.add_argument( + "-d", + "--download", + type=str, + metavar="URL", + help="Download files from a Gofile URL (folder or content ID).", + ) + parser.add_argument( + "-o", + "--output", + type=str, + default=".", + help="Output directory for downloads (default: current directory).", + ) parser.add_argument( "-s", "--to-single-folder", @@ -84,12 +98,12 @@ def _progress_callback_factory(progress_bar: Optional[tqdm]) -> Callable[[int], return update -def _create_progress_bar(filename: str, total: int, quiet: bool) -> Optional[tqdm]: +def _create_progress_bar(filename: str, total: int, quiet: bool, mode: str = "Uploading") -> Optional[tqdm]: """Create a tqdm progress bar unless JSON mode is requested.""" if quiet: return None - return tqdm(total=total, unit="B", unit_scale=True, desc=f"Uploading {filename}") + return tqdm(total=total, unit="B", unit_scale=True, desc=f"{mode} {filename}") def _handle_upload_success( @@ -177,7 +191,129 @@ def upload_files(args: argparse.Namespace, client: GofileClient) -> List[Dict[st return results -def output_results(results: List[Dict[str, object]], json_mode: bool) -> None: +def extract_content_id(url_or_id: str) -> str: + """Extract the content ID from a Gofile URL or return the ID as-is.""" + + # Handle URLs like https://gofile.io/d/nC5ulQ or direct IDs + if "gofile.io/d/" in url_or_id: + return url_or_id.split("gofile.io/d/")[-1].split("?")[0].split("/")[0] + elif "gofile.io" in url_or_id: + # Handle other URL patterns + parts = url_or_id.rstrip("/").split("/") + return parts[-1].split("?")[0] + return url_or_id + + +def download_files(args: argparse.Namespace, client: GofileClient) -> List[Dict[str, object]]: + """Download files from a Gofile URL or content ID.""" + + results: List[Dict[str, object]] = [] + content_id = extract_content_id(args.download) + + try: + # Fetch content information + logger.info("Fetching content information for: %s", content_id) + response = client.get_contents(content_id) + + # The response is already the "data" object from get_contents + data = response if isinstance(response, dict) else {} + if not isinstance(data, dict): + raise GofileError("Invalid response structure from API") + + content_type = data.get("type") + + if content_type == "file": + # Single file download + file_name = str(data.get("name", "downloaded_file")) + download_link = str(data.get("link", "")) + + if not download_link: + raise GofileError("No download link found in response") + + output_path = os.path.join(args.output, file_name) + file_size = int(data.get("size", 0)) + + progress_bar = _create_progress_bar(file_name, file_size, args.json, mode="Downloading") + progress_callback = _progress_callback_factory(progress_bar) + + try: + client.download_file(download_link, output_path, progress_callback) + results.append({ + "file": file_name, + "status": "success", + "path": output_path, + "size": file_size, + }) + except (GofileError, httpx.HTTPError, OSError) as error: + logger.error("Download failed for %s: %s", file_name, error) + results.append(_handle_upload_error(file_name, error)) + finally: + if progress_bar: + progress_bar.close() + + elif content_type == "folder": + # Multiple files in folder + children = data.get("children", {}) + if not isinstance(children, dict): + raise GofileError("Invalid children structure in folder response") + + logger.info("Found %s file(s) in folder", len(children)) + + for child_id, child_data in children.items(): + if not isinstance(child_data, dict): + continue + + child_type = child_data.get("type") + if child_type != "file": + logger.debug("Skipping non-file item: %s", child_id) + continue + + file_name = str(child_data.get("name", f"file_{child_id}")) + download_link = str(child_data.get("link", "")) + + if not download_link: + logger.warning("No download link for %s, skipping", file_name) + continue + + output_path = os.path.join(args.output, file_name) + file_size = int(child_data.get("size", 0)) + + progress_bar = _create_progress_bar(file_name, file_size, args.json, mode="Downloading") + progress_callback = _progress_callback_factory(progress_bar) + + try: + client.download_file(download_link, output_path, progress_callback) + results.append({ + "file": file_name, + "status": "success", + "path": output_path, + "size": file_size, + }) + except (GofileError, httpx.HTTPError, OSError) as error: + logger.error("Download failed for %s: %s", file_name, error) + results.append(_handle_upload_error(file_name, error)) + finally: + if progress_bar: + progress_bar.close() + else: + raise GofileError(f"Unknown content type: {content_type}") + + except (GofileError, httpx.HTTPError) as error: + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Failed to download from %s", content_id) + else: + logger.error("Failed to download from %s: %s", content_id, error) + results.append({ + "content_id": content_id, + "status": "error", + "message": str(error), + "errorType": error.__class__.__name__, + }) + + return results + + +def output_results(results: List[Dict[str, object]], json_mode: bool, is_download: bool = False) -> None: """Display results in either JSON or human readable form.""" if json_mode: @@ -187,9 +323,12 @@ def output_results(results: List[Dict[str, object]], json_mode: bool) -> None: print("\n--- Summary ---") for result in results: if result["status"] == "success": - print(f"✅ {result['file']} -> {result['downloadPage']}") + if is_download: + print(f"✅ {result['file']} -> {result.get('path')}") + else: + print(f"✅ {result['file']} -> {result.get('downloadPage')}") else: - print(f"❌ {result['file']} -> {result.get('message')}") + print(f"❌ {result.get('file', result.get('content_id', 'unknown'))} -> {result.get('message')}") successes = sum(1 for res in results if res["status"] == "success") failures = len(results) - successes logger.info("Summary: %s succeeded, %s failed", successes, failures) @@ -203,11 +342,22 @@ def main() -> None: configure_logging(args.verbose) token = os.environ.get("GOFILE_TOKEN") - _log_token_state(token, args.json) - - client = GofileClient(token=token) - results = upload_files(args, client) - output_results(results, args.json) + + # Check if we're in download mode or upload mode + if args.download: + _log_token_state(token, args.json) + client = GofileClient(token=token) + results = download_files(args, client) + output_results(results, args.json, is_download=True) + elif args.files: + _log_token_state(token, args.json) + client = GofileClient(token=token) + results = upload_files(args, client) + output_results(results, args.json, is_download=False) + else: + logger.error("No files specified for upload and no download URL provided.") + logger.error("Use -d/--download to download or provide files to upload.") + raise SystemExit(1) if __name__ == "__main__": diff --git a/src/gofilepy/client.py b/src/gofilepy/client.py index 2f392a2..d8697ed 100644 --- a/src/gofilepy/client.py +++ b/src/gofilepy/client.py @@ -229,3 +229,85 @@ class GofileClient: result = self.upload(file_path, folder_id, callback) return result.to_dict() + + def create_guest_account(self) -> Dict[str, Any]: + """Create a guest account and return the token.""" + + logger.debug("Creating guest account") + url = f"{self.API_ROOT}/accounts" + response = self._request("POST", url, context={"action": "create_guest"}) + + if "token" in response: + self.token = str(response["token"]) + self.client.headers.update({"Authorization": f"Bearer {self.token}"}) + logger.debug("Guest account created with token: %s***", self.token[:4]) + + return response + + def get_contents(self, content_id: str) -> Dict[str, Any]: + """Fetch information about a content ID (folder or file).""" + + # If we don't have a token, create a guest account first + if not self.token: + logger.debug("No token available, creating guest account") + self.create_guest_account() + + logger.debug("Fetching contents for: %s", content_id) + # Add query parameters and website token header as shown in the API + url = f"{self.API_ROOT}/contents/{content_id}" + params = { + "contentFilter": "", + "page": "1", + "pageSize": "1000", + "sortField": "name", + "sortDirection": "1" + } + headers = { + "x-website-token": "4fd6sg89d7s6" # to avoid error-notPremium + } + return self._request("GET", url, params=params, headers=headers, context={"content_id": content_id}) + + def download_file( + self, + download_url: str, + output_path: str, + callback: Optional[Callable[[int], None]] = None, + ) -> None: + """Download a file from the provided direct link.""" + + logger.info("Starting download: %s -> %s", download_url, output_path) + + cookies = {} + if self.token: + cookies["accountToken"] = self.token + logger.debug("Using accountToken cookie for download") + + try: + with self.client.stream("GET", download_url, cookies=cookies, timeout=None) as response: + response.raise_for_status() + + total_size = int(response.headers.get("content-length", 0)) + logger.debug("File size: %s bytes", total_size) + + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + + with open(output_path, "wb") as f: + for chunk in response.iter_bytes(chunk_size=8192): + if chunk: + f.write(chunk) + if callback: + callback(len(chunk)) + + logger.info("Download complete: %s", output_path) + except httpx.HTTPError as exc: + logger.error("Download failed for %s: %s", download_url, exc) + raise GofileNetworkError( + f"Failed to download from {download_url}", + context={"url": download_url, "output": output_path} + ) from exc + except OSError as exc: + logger.error("Failed to write file %s: %s", output_path, exc) + raise GofileError( + f"Failed to write file to {output_path}", + context={"output": output_path} + ) from exc diff --git a/test_download.py b/test_download.py new file mode 100644 index 0000000..0431057 --- /dev/null +++ b/test_download.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Test script for download functionality.""" + +from gofilepy import GofileClient + +# Test downloading from the folder URL +client = GofileClient() + +# Get folder contents +contents = client.get_contents("GxHNKL") +print("Folder contents:") +print(contents) + +# You can also download programmatically like this: +# client.download_file( +# download_url="https://store-eu-par-6.gofile.io/download/web/folder-id/file.py", +# output_path="./downloaded_test.py" +# ) diff --git a/test.py b/test_upload.py similarity index 79% rename from test.py rename to test_upload.py index be4f14e..b6e00bd 100644 --- a/test.py +++ b/test_upload.py @@ -2,6 +2,6 @@ from gofilepy import GofileClient client = GofileClient() # client = GofileClient(token="YOUR_TOKEN_HERE") # Optional token for private uploads -file = client.upload(file=open("./test.py", "rb")) +file = client.upload(file=open("./test_upload.py", "rb")) print(file.name) print(file.page_link) # View and download file at this link \ No newline at end of file