mirror of
https://github.com/NohamR/gofilepy.git
synced 2026-01-10 08:18:18 +00:00
Initial commit
This commit is contained in:
33
.github/workflows/build_release.yml
vendored
Normal file
33
.github/workflows/build_release.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
paths-ignore:
|
||||||
|
- "README.md"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.9"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install build
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: python -m build
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/
|
||||||
113
README.md
Normal file
113
README.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# GofilePy
|
||||||
|
|
||||||
|
A Python library and CLI tool for [Gofile.io](https://gofile.io).
|
||||||
|
It supports the free API tiers, streaming uploads (low memory usage for large files), and script-friendly JSON output.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 **Streaming Uploads**: Upload 100GB+ files without loading them into RAM.
|
||||||
|
- 📂 **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.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
1. Clone the repository.
|
||||||
|
2. Install via pip:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage (CLI)
|
||||||
|
|
||||||
|
### Basic Upload
|
||||||
|
Upload a single file. A new public folder will be created automatically if you don't provide one.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofilepy video.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload with Token
|
||||||
|
Export your token (Get it from your Gofile Profile) to access your account storage.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GOFILE_TOKEN="your_token_here"
|
||||||
|
gofilepy my_file.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload to a Specific Folder
|
||||||
|
If you have an existing folder ID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofilepy -f "folder-uuid-123" image.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group Upload (Single Folder)
|
||||||
|
Upload multiple files. The first file creates a folder, and the rest are uploaded into it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofilepy -s part1.rar part2.rar part3.rar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripting Mode (JSON Output)
|
||||||
|
Use `--json` to suppress human-readable text and output a JSON array.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofilepy --json file.txt
|
||||||
|
# Output: [{"file": "file.txt", "status": "success", "downloadPage": "...", ...}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verbose Mode
|
||||||
|
Debug connection issues or API responses.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofilepy -vv big_file.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage (Library)
|
||||||
|
|
||||||
|
You can use `gofilepy` in your own Python scripts.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from gofilepy import GofileClient
|
||||||
|
|
||||||
|
# Initialize (Token optional for guest upload)
|
||||||
|
client = GofileClient(token="your_token")
|
||||||
|
|
||||||
|
# Simple Callback for progress
|
||||||
|
def my_progress(bytes_read):
|
||||||
|
print(f"Read: {bytes_read} bytes")
|
||||||
|
|
||||||
|
# Upload (Streaming)
|
||||||
|
try:
|
||||||
|
response = client.upload_file(
|
||||||
|
file_path="/path/to/movie.mkv",
|
||||||
|
folder_id=None, # None = Create new folder
|
||||||
|
callback=my_progress
|
||||||
|
)
|
||||||
|
print(f"Uploaded! Download here: {response['downloadPage']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for Release
|
||||||
|
|
||||||
|
To build a `.whl` (Wheel) file and a source distribution:
|
||||||
|
|
||||||
|
1. Install build tools:
|
||||||
|
```bash
|
||||||
|
pip install build twine
|
||||||
|
```
|
||||||
|
2. Run build:
|
||||||
|
```bash
|
||||||
|
python -m build
|
||||||
|
```
|
||||||
|
3. Artifacts will be in `dist/`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT](LICENSE) License.
|
||||||
24
pyproject.toml
Normal file
24
pyproject.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "gofilepy"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A Python CLI and Library for gofile.io"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [{ name = "Garnajee", email = "62147746+garnajee@users.noreply.github.com" }]
|
||||||
|
license = { text = "MIT" }
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.7"
|
||||||
|
dependencies = [
|
||||||
|
"httpx", # The modern HTTP client
|
||||||
|
"tqdm" # Progress bar
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
gofilepy = "gofilepy.cli:main"
|
||||||
7
src/gofilepy/__init__.py
Normal file
7
src/gofilepy/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from .client import GofileClient
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__all__ = ["GofileClient"]
|
||||||
|
|
||||||
122
src/gofilepy/cli.py
Normal file
122
src/gofilepy/cli.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from tqdm import tqdm
|
||||||
|
from .client import GofileClient
|
||||||
|
|
||||||
|
# Configure Logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')
|
||||||
|
logger = logging.getLogger("gofilepy")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Gofile.io CLI Uploader (HTTPX Edition)")
|
||||||
|
|
||||||
|
parser.add_argument("files", nargs='+', help="Files to upload")
|
||||||
|
|
||||||
|
parser.add_argument("-s", "--to-single-folder", action="store_true",
|
||||||
|
help="Upload multiple files to the same folder.")
|
||||||
|
|
||||||
|
parser.add_argument("-f", "--folder-id", type=str, default=None,
|
||||||
|
help="ID of an existing Gofile folder.")
|
||||||
|
|
||||||
|
parser.add_argument("-vv", "--verbose", action="store_true",
|
||||||
|
help="Show detailed debug info.")
|
||||||
|
|
||||||
|
parser.add_argument("--json", action="store_true",
|
||||||
|
help="Output result as JSON for scripts.")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Log Level Handling
|
||||||
|
if args.verbose:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
# HTTPX can be verbose, enable if needed
|
||||||
|
# logging.getLogger("httpx").setLevel(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Token Logic
|
||||||
|
token = os.environ.get("GOFILE_TOKEN")
|
||||||
|
if not token and args.to_single_folder and not args.folder_id:
|
||||||
|
logger.debug("No token found. Proceeding as Guest.")
|
||||||
|
|
||||||
|
client = GofileClient(token=token)
|
||||||
|
|
||||||
|
target_folder_id = args.folder_id
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for file_path in args.files:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
res_err = {"file": file_path, "status": "error", "message": "File not found"}
|
||||||
|
results.append(res_err)
|
||||||
|
if not args.json:
|
||||||
|
logger.error(f"File not found: {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
# Init Progress Bar (Only if not JSON mode)
|
||||||
|
pbar = None
|
||||||
|
if not args.json:
|
||||||
|
pbar = tqdm(total=file_size, unit='B', unit_scale=True, desc=f"Uploading {filename}")
|
||||||
|
|
||||||
|
def progress_update(chunk_size):
|
||||||
|
if pbar:
|
||||||
|
pbar.update(chunk_size)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = client.upload_file(
|
||||||
|
file_path=file_path,
|
||||||
|
folder_id=target_folder_id,
|
||||||
|
callback=progress_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Auto-Folder Management for Guests ---
|
||||||
|
# If we are in single folder mode and it's the first upload
|
||||||
|
if args.to_single_folder and target_folder_id is None:
|
||||||
|
if 'parentFolder' in data:
|
||||||
|
target_folder_id = data['parentFolder']
|
||||||
|
logger.debug(f"Parent folder set to: {target_folder_id}")
|
||||||
|
|
||||||
|
# If guest, capture the guestToken to write to the same folder next time
|
||||||
|
if 'guestToken' in data and not client.token:
|
||||||
|
client.token = data['guestToken']
|
||||||
|
# Re-auth client with new token
|
||||||
|
client.client.headers.update({"Authorization": f"Bearer {client.token}"})
|
||||||
|
logger.debug(f"Guest token applied: {client.token}")
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"file": filename,
|
||||||
|
"status": "success",
|
||||||
|
"downloadPage": data.get("downloadPage"),
|
||||||
|
"directLink": data.get("directLink", "N/A"), # Sometimes available
|
||||||
|
"parentFolder": data.get("parentFolder")
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = str(e)
|
||||||
|
results.append({"file": filename, "status": "error", "message": err_msg})
|
||||||
|
if not args.json:
|
||||||
|
logger.error(f"Upload failed: {err_msg}")
|
||||||
|
finally:
|
||||||
|
if pbar:
|
||||||
|
pbar.close()
|
||||||
|
|
||||||
|
# Output
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(results, indent=2))
|
||||||
|
else:
|
||||||
|
print("\n--- Summary ---")
|
||||||
|
for res in results:
|
||||||
|
if res['status'] == 'success':
|
||||||
|
print(f"✅ {res['file']} -> {res['downloadPage']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ {res['file']} -> {res['message']}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
97
src/gofilepy/client.py
Normal file
97
src/gofilepy/client.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Optional, List, Dict, Callable
|
||||||
|
from .utils import ProgressFileReader
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class GofileClient:
|
||||||
|
API_ROOT = "https://api.gofile.io"
|
||||||
|
UPLOAD_SERVER_URL = "https://upload.gofile.io"
|
||||||
|
|
||||||
|
def __init__(self, token: Optional[str] = None):
|
||||||
|
self.token = token
|
||||||
|
# Increase timeout for large API operations, though uploads handle their own timeout
|
||||||
|
self.client = httpx.Client(timeout=30.0)
|
||||||
|
|
||||||
|
if self.token:
|
||||||
|
logger.debug(f"Initialized with token: {self.token[:4]}***")
|
||||||
|
self.client.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||||
|
|
||||||
|
def _handle_response(self, response: httpx.Response) -> Dict:
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except Exception:
|
||||||
|
logger.error(f"Failed to parse JSON: {response.text}")
|
||||||
|
response.raise_for_status()
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if data.get("status") != "ok":
|
||||||
|
logger.error(f"API Error: {data}")
|
||||||
|
raise Exception(f"Gofile API Error: {data.get('status')} - {data.get('data')}")
|
||||||
|
|
||||||
|
return data.get("data", {})
|
||||||
|
|
||||||
|
def get_server(self) -> str:
|
||||||
|
"""
|
||||||
|
Gofile suggests using specific servers (availables in their doc),
|
||||||
|
but 'upload.gofile.io' uses DNS geo-routing automatically.
|
||||||
|
We stick to the best practice default.
|
||||||
|
"""
|
||||||
|
return self.UPLOAD_SERVER_URL
|
||||||
|
|
||||||
|
def create_folder(self, parent_folder_id: str, folder_name: str) -> Dict:
|
||||||
|
logger.debug(f"Creating folder '{folder_name}' in '{parent_folder_id}'")
|
||||||
|
url = f"{self.API_ROOT}/contents/createFolder"
|
||||||
|
payload = {
|
||||||
|
"parentFolderId": parent_folder_id,
|
||||||
|
"folderName": folder_name
|
||||||
|
}
|
||||||
|
res = self.client.post(url, json=payload)
|
||||||
|
return self._handle_response(res)
|
||||||
|
|
||||||
|
def delete_content(self, content_ids: List[str]) -> Dict:
|
||||||
|
logger.debug(f"Deleting content IDs: {content_ids}")
|
||||||
|
url = f"{self.API_ROOT}/contents"
|
||||||
|
# HTTPX needs 'content' or 'json' for DELETE requests explicitly if body is required
|
||||||
|
res = self.client.request("DELETE", url, json={"contentsId": ",".join(content_ids)})
|
||||||
|
return self._handle_response(res)
|
||||||
|
|
||||||
|
def upload_file(self,
|
||||||
|
file_path: str,
|
||||||
|
folder_id: Optional[str] = None,
|
||||||
|
callback: Optional[Callable[[int], None]] = None) -> Dict:
|
||||||
|
|
||||||
|
server_url = f"{self.get_server()}/uploadfile"
|
||||||
|
file_name = os.path.basename(file_path)
|
||||||
|
|
||||||
|
# Prepare parameters
|
||||||
|
data = {}
|
||||||
|
if self.token:
|
||||||
|
data["token"] = self.token
|
||||||
|
if folder_id:
|
||||||
|
data["folderId"] = folder_id
|
||||||
|
|
||||||
|
# Use our custom ProgressFileReader
|
||||||
|
# If no callback is provided, we use a dummy lambda to avoid errors
|
||||||
|
progress_callback = callback if callback else lambda x: None
|
||||||
|
|
||||||
|
logger.info(f"Starting upload: {file_name} -> {server_url}")
|
||||||
|
|
||||||
|
# Open file using our wrapper
|
||||||
|
with ProgressFileReader(file_path, progress_callback) as f:
|
||||||
|
files = {'file': (file_name, f)}
|
||||||
|
|
||||||
|
# Use a longer timeout for the upload specifically (None = infinite)
|
||||||
|
# This is crucial for 2000GB files
|
||||||
|
res = self.client.post(
|
||||||
|
server_url,
|
||||||
|
data=data,
|
||||||
|
files=files,
|
||||||
|
timeout=None
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._handle_response(res)
|
||||||
28
src/gofilepy/utils.py
Normal file
28
src/gofilepy/utils.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import io
|
||||||
|
|
||||||
|
class ProgressFileReader(io.BufferedReader):
|
||||||
|
"""
|
||||||
|
Wraps a file object to trigger a callback when data is read.
|
||||||
|
This allows monitoring upload progress in httpx without loading the file into RAM.
|
||||||
|
"""
|
||||||
|
def __init__(self, filename: str, callback: typing.Callable[[int], None]):
|
||||||
|
self._f = open(filename, 'rb')
|
||||||
|
self._callback = callback
|
||||||
|
# Get file size for verification if needed, or just standard init
|
||||||
|
super().__init__(self._f)
|
||||||
|
|
||||||
|
def read(self, size: int = -1) -> bytes:
|
||||||
|
# Read the chunk from disk
|
||||||
|
chunk = self._f.read(size)
|
||||||
|
# Update the progress bar with the length of the chunk read
|
||||||
|
if chunk:
|
||||||
|
self._callback(len(chunk))
|
||||||
|
return chunk
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if hasattr(self, '_f'):
|
||||||
|
self._f.close()
|
||||||
|
|
||||||
Reference in New Issue
Block a user