Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ FileUploader provides a shared provider layer plus three user interfaces:

`python -m fileuploader` is kept as an equivalent package entrypoint for the same CLI.

Created by **j0rd1s3rr4n0**: [jordiserrano.me](https://jordiserrano.me) · [github.com/j0rd1s3rr4n0](https://github.com/j0rd1s3rr4n0)
Created by **j0rd1s3rr4n0**: [jordiserrano.me](https://jordiserrano.me) | [github.com/j0rd1s3rr4n0](https://github.com/j0rd1s3rr4n0)

The current provider set includes GoFile, AnonFilesNew, JSONBin, AnonFiles, and BayFiles. Deprecated providers remain available for compatibility, but upstream APIs may be unreliable.
The current provider set includes GoFile plus a broad no-registration upload set: 0x0.st, MoonPush, Temp.sh, tmpfile.link, blipbin, EasySend, dropfile.dev, cupload.io, and qurl.sh. Deprecated providers remain available for compatibility, but upstream APIs may be unreliable.

## Features

Expand Down Expand Up @@ -63,6 +63,14 @@ Upload a file with GoFile and print JSON:
python -m fileuploader_cli upload --service gofile path/to/file.txt --json
```

Upload without registration:

```shell
python -m fileuploader_cli upload --service moonpush path/to/file.txt
python -m fileuploader_cli upload --service 0x0 path/to/file.txt
python -m fileuploader_cli upload --service tmpfilelink path/to/file.txt
```

Upload with AnonFilesNew using an API key:

```shell
Expand Down Expand Up @@ -105,13 +113,35 @@ See [docs/interfaces.md](docs/interfaces.md) for the interface design notes and
| Provider | Service name | Upload | Download | Info | API key | Status |
| --- | --- | --- | --- | --- | --- | --- |
| GoFile | `gofile` | Yes | Yes | No | No | Active |
| 0x0.st | `0x0` | Yes | No | No | No | Active |
| MoonPush | `moonpush` | Yes | No | Yes | No | Active |
| Temp.sh | `tempsh` | Yes | No | No | No | Active |
| tmpfile.link | `tmpfilelink` | Yes | No | No | No | Active |
| blipbin | `blipbin` | Yes | No | No | No | Active |
| EasySend | `easysend` | Yes | No | No | No | Active |
| dropfile.dev | `dropfiledev` | Yes | No | No | No | Active |
| cupload.io | `cupload` | Yes | No | No | No | Active |
| qurl.sh | `qurl` | Yes | No | No | No | Active |
| AnonFilesNew | `anonfilesnew` | Yes | No | Yes | Yes | Active |
| JSONBin | `jsonbin` | Yes | No | No | Yes | Active |
| AnonFiles | `anonfiles` | Yes | No | No | No | Deprecated |
| BayFiles | `bayfiles` | Yes | No | No | No | Deprecated |

## Provider-Specific Notes

### No-registration providers

Use these when you want a quick public link without creating an account or passing API keys:

```shell
python -m fileuploader_cli upload --service moonpush path/to/file.txt
python -m fileuploader_cli upload --service tempsh path/to/file.txt
python -m fileuploader_cli upload --service blipbin path/to/file.txt
python -m fileuploader_cli upload --service dropfiledev path/to/file.txt
```

The no-registration providers are best for temporary sharing and automation. Retention, size limits, and rate limits are controlled by each upstream service.

### GoFile

GoFile supports upload and download through the unified CLI. The legacy GoFile entrypoint also supports local encryption, local decryption, output formatting, and clipboard copying:
Expand Down
4 changes: 4 additions & 0 deletions docs/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ python -m fileuploader_cli services
python -m fileuploader_cli --no-banner services
python -m fileuploader_cli guided
python -m fileuploader_cli upload --service gofile path/to/file.txt --json
python -m fileuploader_cli upload --service moonpush path/to/file.txt
python -m fileuploader_cli upload --service tmpfilelink path/to/file.txt
python -m fileuploader_cli info --service anonfilesnew FILE_ID --api-key YOUR_API_KEY
```

Use `python -m fileuploader_cli guided` for a step-by-step flow that shows providers, asks for an action, and prompts only for the fields needed by that action.

Use `--no-banner` before the command when you want machine-friendly human output without the banner. JSON output never includes the banner.

For quick temporary sharing, prefer the no-registration providers such as `moonpush`, `0x0`, `tempsh`, `tmpfilelink`, `blipbin`, `easysend`, `dropfiledev`, `cupload`, and `qurl`.

Typer is used because it provides a modern command surface while building on Click internally. Click is therefore covered as the underlying command framework. The older argparse scripts remain available as legacy provider-specific entrypoints, but new automation should use `python -m fileuploader`.

## GUI
Expand Down
195 changes: 193 additions & 2 deletions fileuploader/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,33 @@ def _ensure_file(self, filepath):
raise ProviderError(self.info.name, f"File not found: {filepath}", code="file_not_found")
return path

def _post_file(self, url, filepath, **kwargs):
def _post_file(self, url, filepath, file_field="file", **kwargs):
path = self._ensure_file(filepath)
try:
with path.open("rb") as file_handle:
response = self.http.post(url, files={"file": (path.name, file_handle)}, **kwargs)
response = self.http.post(url, files={file_field: (path.name, file_handle)}, **kwargs)
except requests.RequestException as exc:
raise ProviderError(self.info.name, str(exc), code="network_error") from exc
return self._json_response(response)

def _post_file_text(self, url, filepath, file_field="file", **kwargs):
path = self._ensure_file(filepath)
try:
with path.open("rb") as file_handle:
response = self.http.post(url, files={file_field: (path.name, file_handle)}, **kwargs)
except requests.RequestException as exc:
raise ProviderError(self.info.name, str(exc), code="network_error") from exc
return self._text_response(response)

def _put_file_text(self, url, filepath, **kwargs):
path = self._ensure_file(filepath)
try:
with path.open("rb") as file_handle:
response = self.http.put(url, data=file_handle, **kwargs)
except requests.RequestException as exc:
raise ProviderError(self.info.name, str(exc), code="network_error") from exc
return self._text_response(response)

def _json_response(self, response):
try:
payload = response.json()
Expand All @@ -50,6 +68,14 @@ def _json_response(self, response):
raise ProviderError(self.info.name, message, code="http_error")
return payload

def _text_response(self, response):
text = getattr(response, "text", "").strip()
if getattr(response, "status_code", 200) >= 400:
raise ProviderError(self.info.name, text or "Provider returned an HTTP error", code="http_error")
if not text:
raise ProviderError(self.info.name, "Provider returned an empty response", code="empty_response")
return text


class GoFileProvider(BaseProvider):
info = ProviderInfo(
Expand Down Expand Up @@ -176,6 +202,171 @@ def upload(self, filepath, **options):
)


class ZeroXZeroProvider(BaseProvider):
info = ProviderInfo(
name="0x0",
display_name="0x0.st",
supports_info=False,
)

def upload(self, filepath, **options):
url = self._post_file_text("https://0x0.st", filepath)
return UploadResult(provider=self.info.name, status=True, url=url, raw={"response": url})


class MoonPushProvider(BaseProvider):
info = ProviderInfo(
name="moonpush",
display_name="MoonPush",
supports_info=True,
)

def upload(self, filepath, **options):
payload = self._post_file("https://www.moonpush.com/api/upload", filepath)
url = payload.get("shareUrl") or payload.get("share_url") or payload.get("url")
download_url = payload.get("downloadUrl") or payload.get("download_url")
file_id = payload.get("id") or payload.get("pipeId") or payload.get("fileId")
if not url and not download_url:
raise ProviderError(self.info.name, "Upload response did not include a URL", code="missing_url")
return UploadResult(
provider=self.info.name,
status=True,
url=url or download_url,
short_url=download_url if url and download_url != url else None,
file_id=file_id,
metadata=payload,
raw=payload,
)

def info_file(self, file_id, **options):
try:
response = self.http.get(f"https://www.moonpush.com/api/share/{file_id}")
except requests.RequestException as exc:
raise ProviderError(self.info.name, str(exc), code="network_error") from exc
return self._json_response(response)


class TempShProvider(BaseProvider):
info = ProviderInfo(
name="tempsh",
display_name="Temp.sh",
supports_info=False,
)

def upload(self, filepath, **options):
url = self._post_file_text("https://temp.sh/upload", filepath)
return UploadResult(provider=self.info.name, status=True, url=url, raw={"response": url})


class TmpFileLinkProvider(BaseProvider):
info = ProviderInfo(
name="tmpfilelink",
display_name="tmpfile.link",
supports_info=False,
)

def upload(self, filepath, **options):
payload = self._post_file("https://tmpfile.link/api/upload", filepath)
url = payload.get("downloadLink") or payload.get("download_link") or payload.get("url")
if not url:
raise ProviderError(self.info.name, "Upload response did not include a download link", code="missing_url")
return UploadResult(
provider=self.info.name,
status=True,
url=url,
short_url=payload.get("downloadLinkEncoded"),
file_id=payload.get("fileId") or payload.get("id"),
metadata=payload,
raw=payload,
)


class BlipbinProvider(BaseProvider):
info = ProviderInfo(
name="blipbin",
display_name="blipbin",
supports_info=False,
)

def upload(self, filepath, **options):
path = self._ensure_file(filepath)
try:
with path.open("rb") as file_handle:
response = self.http.post("https://blipbin.com/upload", files={"file": (path.name, file_handle)})
except requests.RequestException as exc:
raise ProviderError(self.info.name, str(exc), code="network_error") from exc
url = self._text_response(response)
token = getattr(response, "headers", {}).get("X-Token")
metadata = {"delete_token": token} if token else {}
return UploadResult(provider=self.info.name, status=True, url=url, metadata=metadata, raw={"response": url})


class EasySendProvider(BaseProvider):
info = ProviderInfo(
name="easysend",
display_name="EasySend",
supports_info=False,
)

def upload(self, filepath, **options):
payload = self._post_file("https://easysend.co/api/v1/upload", filepath, file_field="files[]")
url = payload.get("share_url") or payload.get("shareUrl")
short_code = payload.get("short_code") or payload.get("shortCode")
if not url and short_code:
url = f"https://easysend.co/{short_code}"
elif url and url.startswith("/"):
url = f"https://easysend.co{url}"
if not url:
raise ProviderError(self.info.name, "Upload response did not include a share URL", code="missing_url")
return UploadResult(
provider=self.info.name,
status=True,
url=url,
file_id=short_code,
metadata=payload,
raw=payload,
)


class DropFileDevProvider(BaseProvider):
info = ProviderInfo(
name="dropfiledev",
display_name="dropfile.dev",
supports_info=False,
)

def upload(self, filepath, **options):
path = self._ensure_file(filepath)
url = self._put_file_text(f"https://dropfile.dev/{path.name}", path)
return UploadResult(provider=self.info.name, status=True, url=url, raw={"response": url})


class CuploadProvider(BaseProvider):
info = ProviderInfo(
name="cupload",
display_name="cupload.io",
supports_info=False,
)

def upload(self, filepath, **options):
path = self._ensure_file(filepath)
url = self._put_file_text(f"https://cupload.io/{path.name}", path)
return UploadResult(provider=self.info.name, status=True, url=url, raw={"response": url})


class QurlProvider(BaseProvider):
info = ProviderInfo(
name="qurl",
display_name="qurl.sh",
supports_info=False,
)

def upload(self, filepath, **options):
path = self._ensure_file(filepath)
url = self._put_file_text(f"https://qurl.sh/{path.name}", path)
return UploadResult(provider=self.info.name, status=True, url=url, raw={"response": url})


class AnonFilesProvider(AnonFilesNewProvider):
info = ProviderInfo(
name="anonfiles",
Expand Down
18 changes: 18 additions & 0 deletions fileuploader/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
AnonFilesNewProvider,
AnonFilesProvider,
BayFilesProvider,
BlipbinProvider,
CuploadProvider,
DropFileDevProvider,
EasySendProvider,
GoFileProvider,
JsonBinProvider,
MoonPushProvider,
QurlProvider,
TempShProvider,
TmpFileLinkProvider,
ZeroXZeroProvider,
)


Expand Down Expand Up @@ -35,6 +44,15 @@ def create_default_registry():
GoFileProvider(),
AnonFilesNewProvider(),
JsonBinProvider(),
ZeroXZeroProvider(),
MoonPushProvider(),
TempShProvider(),
TmpFileLinkProvider(),
BlipbinProvider(),
EasySendProvider(),
DropFileDevProvider(),
CuploadProvider(),
QurlProvider(),
AnonFilesProvider(),
BayFilesProvider(),
]
Expand Down
Loading