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