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