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.
This commit is contained in:
√(noham)²
2025-12-13 15:44:20 +01:00
parent 66731e2fc1
commit 9a4c0b1776
5 changed files with 295 additions and 13 deletions

View File

@@ -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:

View File

@@ -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 <URL> to download or provide files to upload.")
raise SystemExit(1)
if __name__ == "__main__":

View File

@@ -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

18
test_download.py Normal file
View File

@@ -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"
# )

View File

@@ -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