mirror of
https://github.com/NohamR/gofilepy.git
synced 2026-01-09 07:48:17 +00:00
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:
34
README.md
34
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:
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
18
test_download.py
Normal 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"
|
||||
# )
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user