tempspace-cli 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tempspace-cli might be problematic. Click here for more details.
- tempspace_cli-1.1.1/PKG-INFO +109 -0
- tempspace_cli-1.1.1/README.md +93 -0
- tempspace_cli-1.1.1/cli/__init__.py +1 -0
- tempspace_cli-1.1.1/cli/tempspace.py +186 -0
- tempspace_cli-1.1.1/pyproject.toml +31 -0
- tempspace_cli-1.1.1/setup.cfg +4 -0
- tempspace_cli-1.1.1/tempspace_cli.egg-info/PKG-INFO +109 -0
- tempspace_cli-1.1.1/tempspace_cli.egg-info/SOURCES.txt +12 -0
- tempspace_cli-1.1.1/tempspace_cli.egg-info/dependency_links.txt +1 -0
- tempspace_cli-1.1.1/tempspace_cli.egg-info/entry_points.txt +2 -0
- tempspace_cli-1.1.1/tempspace_cli.egg-info/requires.txt +5 -0
- tempspace_cli-1.1.1/tempspace_cli.egg-info/top_level.txt +1 -0
- tempspace_cli-1.1.1/tests/test_cli.py +12 -0
- tempspace_cli-1.1.1/tests/test_main.py +405 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tempspace-cli
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: A command-line tool for uploading files to Tempspace.
|
|
5
|
+
Author-email: Tempspace <mcbplay1@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests==2.31.0
|
|
12
|
+
Requires-Dist: tqdm==4.66.1
|
|
13
|
+
Requires-Dist: requests-toolbelt==1.0.0
|
|
14
|
+
Requires-Dist: rich==13.7.1
|
|
15
|
+
Requires-Dist: qrcode==8.2
|
|
16
|
+
|
|
17
|
+
# 🚀 Tempspace
|
|
18
|
+
|
|
19
|
+
Tempspace is a terminal-style file sharing service that allows you to upload files and share them via a link, with features like password protection, one-time downloads, and automatic expiration.
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
## ✨ Features
|
|
24
|
+
|
|
25
|
+
- **Multiple Upload Methods:** Use the web interface, cURL, or the official CLI tool.
|
|
26
|
+
- **Security:** Optional password protection, one-time downloads, and rate limiting.
|
|
27
|
+
- **Modern UI:** A terminal-inspired design with QR code generation and upload history.
|
|
28
|
+
- **File Management:** Support for large files, multiple expiry options, and automatic cleanup.
|
|
29
|
+
|
|
30
|
+
## 🚀 Getting Started
|
|
31
|
+
|
|
32
|
+
### Method 1: Using the Web Interface
|
|
33
|
+
|
|
34
|
+
The easiest way to use Tempspace is through the web interface.
|
|
35
|
+
|
|
36
|
+
1. **Open** the service URL (e.g., `http://localhost:8000`) in your browser.
|
|
37
|
+
2. **Drag and drop** a file or click to browse.
|
|
38
|
+
3. **Configure** the expiry time and an optional password.
|
|
39
|
+
4. **Upload** and share the generated link.
|
|
40
|
+
|
|
41
|
+
### Method 2: Using cURL
|
|
42
|
+
|
|
43
|
+
You can use `cURL` to upload files directly from your terminal.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Basic upload
|
|
47
|
+
curl -F "file=@/path/to/file.txt" -F "hours=6" https://tempspace.fly.dev/upload
|
|
48
|
+
|
|
49
|
+
# With a password
|
|
50
|
+
curl -F "file=@/path/to/secret.txt" -F "password=secret123" https://tempspace.fly.dev/upload
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Method 3: Using the Official CLI Tool
|
|
54
|
+
|
|
55
|
+
For a more integrated terminal experience, you can install the official CLI tool from PyPI.
|
|
56
|
+
|
|
57
|
+
#### Installation
|
|
58
|
+
```bash
|
|
59
|
+
pip install tempspace-cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Usage
|
|
63
|
+
```bash
|
|
64
|
+
# Basic upload
|
|
65
|
+
tempspace /path/to/document.pdf -t 6h
|
|
66
|
+
|
|
67
|
+
# With a password
|
|
68
|
+
tempspace /path/to/secret.txt -p "mypassword"
|
|
69
|
+
|
|
70
|
+
# Point to a self-hosted server
|
|
71
|
+
tempspace /path/to/file.txt --url http://localhost:8000
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 🔧 Self-Hosting
|
|
75
|
+
|
|
76
|
+
You can run your own instance of Tempspace.
|
|
77
|
+
|
|
78
|
+
### Installation
|
|
79
|
+
```bash
|
|
80
|
+
# Clone the repository
|
|
81
|
+
git clone https://github.com/your-repo/tempspace.git
|
|
82
|
+
cd tempspace
|
|
83
|
+
|
|
84
|
+
# Install dependencies
|
|
85
|
+
pip install -r requirements.txt
|
|
86
|
+
|
|
87
|
+
# Configure
|
|
88
|
+
cp .env.example .env
|
|
89
|
+
nano .env # Change the admin password
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Running the Server
|
|
93
|
+
```bash
|
|
94
|
+
python main.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server will be available at `http://localhost:8000`.
|
|
98
|
+
|
|
99
|
+
## 🛡️ Admin & Configuration
|
|
100
|
+
|
|
101
|
+
For advanced configuration, such as setting up a reverse proxy, managing admin endpoints, and more, please refer to the project's documentation or open an issue.
|
|
102
|
+
|
|
103
|
+
## 🤝 Contributing
|
|
104
|
+
|
|
105
|
+
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
106
|
+
|
|
107
|
+
## 📝 License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 🚀 Tempspace
|
|
2
|
+
|
|
3
|
+
Tempspace is a terminal-style file sharing service that allows you to upload files and share them via a link, with features like password protection, one-time downloads, and automatic expiration.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- **Multiple Upload Methods:** Use the web interface, cURL, or the official CLI tool.
|
|
10
|
+
- **Security:** Optional password protection, one-time downloads, and rate limiting.
|
|
11
|
+
- **Modern UI:** A terminal-inspired design with QR code generation and upload history.
|
|
12
|
+
- **File Management:** Support for large files, multiple expiry options, and automatic cleanup.
|
|
13
|
+
|
|
14
|
+
## 🚀 Getting Started
|
|
15
|
+
|
|
16
|
+
### Method 1: Using the Web Interface
|
|
17
|
+
|
|
18
|
+
The easiest way to use Tempspace is through the web interface.
|
|
19
|
+
|
|
20
|
+
1. **Open** the service URL (e.g., `http://localhost:8000`) in your browser.
|
|
21
|
+
2. **Drag and drop** a file or click to browse.
|
|
22
|
+
3. **Configure** the expiry time and an optional password.
|
|
23
|
+
4. **Upload** and share the generated link.
|
|
24
|
+
|
|
25
|
+
### Method 2: Using cURL
|
|
26
|
+
|
|
27
|
+
You can use `cURL` to upload files directly from your terminal.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Basic upload
|
|
31
|
+
curl -F "file=@/path/to/file.txt" -F "hours=6" https://tempspace.fly.dev/upload
|
|
32
|
+
|
|
33
|
+
# With a password
|
|
34
|
+
curl -F "file=@/path/to/secret.txt" -F "password=secret123" https://tempspace.fly.dev/upload
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Method 3: Using the Official CLI Tool
|
|
38
|
+
|
|
39
|
+
For a more integrated terminal experience, you can install the official CLI tool from PyPI.
|
|
40
|
+
|
|
41
|
+
#### Installation
|
|
42
|
+
```bash
|
|
43
|
+
pip install tempspace-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Usage
|
|
47
|
+
```bash
|
|
48
|
+
# Basic upload
|
|
49
|
+
tempspace /path/to/document.pdf -t 6h
|
|
50
|
+
|
|
51
|
+
# With a password
|
|
52
|
+
tempspace /path/to/secret.txt -p "mypassword"
|
|
53
|
+
|
|
54
|
+
# Point to a self-hosted server
|
|
55
|
+
tempspace /path/to/file.txt --url http://localhost:8000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🔧 Self-Hosting
|
|
59
|
+
|
|
60
|
+
You can run your own instance of Tempspace.
|
|
61
|
+
|
|
62
|
+
### Installation
|
|
63
|
+
```bash
|
|
64
|
+
# Clone the repository
|
|
65
|
+
git clone https://github.com/your-repo/tempspace.git
|
|
66
|
+
cd tempspace
|
|
67
|
+
|
|
68
|
+
# Install dependencies
|
|
69
|
+
pip install -r requirements.txt
|
|
70
|
+
|
|
71
|
+
# Configure
|
|
72
|
+
cp .env.example .env
|
|
73
|
+
nano .env # Change the admin password
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Running the Server
|
|
77
|
+
```bash
|
|
78
|
+
python main.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The server will be available at `http://localhost:8000`.
|
|
82
|
+
|
|
83
|
+
## 🛡️ Admin & Configuration
|
|
84
|
+
|
|
85
|
+
For advanced configuration, such as setting up a reverse proxy, managing admin endpoints, and more, please refer to the project's documentation or open an issue.
|
|
86
|
+
|
|
87
|
+
## 🤝 Contributing
|
|
88
|
+
|
|
89
|
+
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
90
|
+
|
|
91
|
+
## 📝 License
|
|
92
|
+
|
|
93
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import hashlib
|
|
6
|
+
import multiprocessing
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor
|
|
9
|
+
import math
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich import box
|
|
14
|
+
import qrcode
|
|
15
|
+
from rich.prompt import Prompt, Confirm
|
|
16
|
+
|
|
17
|
+
# Default configuration
|
|
18
|
+
DEFAULT_SERVER_URL = "https://tempspace.fly.dev/"
|
|
19
|
+
CHUNK_SIZE = 1024 * 1024 # 1MB
|
|
20
|
+
|
|
21
|
+
def parse_time(time_str: str) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Parse a time string (e.g., '7d', '24h', '360') into an integer number of hours.
|
|
24
|
+
Returns the number of hours as an integer, or None if parsing fails.
|
|
25
|
+
"""
|
|
26
|
+
time_str = time_str.lower().strip()
|
|
27
|
+
if time_str.endswith('d'):
|
|
28
|
+
try:
|
|
29
|
+
days = int(time_str[:-1])
|
|
30
|
+
return days * 24
|
|
31
|
+
except ValueError:
|
|
32
|
+
return None
|
|
33
|
+
elif time_str.endswith('h'):
|
|
34
|
+
try:
|
|
35
|
+
return int(time_str[:-1])
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
return int(time_str)
|
|
41
|
+
except ValueError:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def calculate_file_hash(filepath: str) -> str:
|
|
45
|
+
"""Calculate the SHA256 hash of a file in chunks."""
|
|
46
|
+
sha256_hash = hashlib.sha256()
|
|
47
|
+
try:
|
|
48
|
+
with open(filepath, "rb") as f:
|
|
49
|
+
for byte_block in iter(lambda: f.read(CHUNK_SIZE), b""):
|
|
50
|
+
sha256_hash.update(byte_block)
|
|
51
|
+
return sha256_hash.hexdigest()
|
|
52
|
+
except IOError as e:
|
|
53
|
+
print(f"Error reading file for hashing: {e}", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
def format_size(size_bytes: int) -> str:
|
|
57
|
+
"""Converts a size in bytes to a human-readable format."""
|
|
58
|
+
if size_bytes == 0:
|
|
59
|
+
return "0B"
|
|
60
|
+
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
|
61
|
+
i = int(math.floor(math.log(size_bytes, 1024)))
|
|
62
|
+
p = math.pow(1024, i)
|
|
63
|
+
s = round(size_bytes / p, 2)
|
|
64
|
+
return f"{s} {size_name[i]}"
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
"""Main function to handle argument parsing and file upload."""
|
|
68
|
+
console = Console()
|
|
69
|
+
|
|
70
|
+
# --- Header ---
|
|
71
|
+
console.print(Panel("[bold cyan]Tempspace File Uploader[/bold cyan]", expand=False, border_style="blue"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
parser = argparse.ArgumentParser(
|
|
75
|
+
description="Upload a file to Tempspace.",
|
|
76
|
+
formatter_class=argparse.RawTextHelpFormatter
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
parser.add_argument("filepath", nargs='?', default=None, help="The path to the file you want to upload.")
|
|
80
|
+
parser.add_argument("-t", "--time", type=str, default='24', help="Set the file's expiration time. Examples: '24h', '7d', '360' (hours).\nDefault: '24' (24 hours).")
|
|
81
|
+
parser.add_argument("-p", "--password", type=str, help="Protect the file with a password.")
|
|
82
|
+
parser.add_argument("--one-time", action="store_true", help="The file will be deleted after the first download.")
|
|
83
|
+
parser.add_argument("--url", type=str, default=os.environ.get("TEMPSPACE_URL", DEFAULT_SERVER_URL), help=f"The URL of the Tempspace server.\nCan also be set with the TEMPSPACE_URL environment variable.\nDefault: {DEFAULT_SERVER_URL}")
|
|
84
|
+
parser.add_argument("--qr", action="store_true", help="Display a QR code of the download link.")
|
|
85
|
+
parser.add_argument("--it", action="store_true", help="Enable interactive mode.")
|
|
86
|
+
|
|
87
|
+
args = parser.parse_args()
|
|
88
|
+
|
|
89
|
+
# --- Interactive Mode ---
|
|
90
|
+
if args.it:
|
|
91
|
+
args.filepath = Prompt.ask("Enter the path to the file you want to upload")
|
|
92
|
+
args.time = Prompt.ask("Set the file's expiration time (e.g., '24h', '7d')", default='24')
|
|
93
|
+
args.password = Prompt.ask("Protect the file with a password?", default=None, password=True)
|
|
94
|
+
args.one_time = Confirm.ask("Delete the file after the first download?", default=False)
|
|
95
|
+
args.qr = Confirm.ask("Display a QR code of the download link?", default=False)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- Validate Inputs ---
|
|
99
|
+
if not args.filepath or not os.path.isfile(args.filepath):
|
|
100
|
+
console.print(Panel(f"[bold red]Error:[/] File not found at '{args.filepath}'", title="[bold red]Error[/bold red]", border_style="red"))
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
hours = parse_time(args.time)
|
|
104
|
+
if hours is None:
|
|
105
|
+
console.print(Panel(f"[bold red]Error:[/] Invalid time format '{args.time}'. Use formats like '24h', '7d', or '360'.", title="[bold red]Error[/bold red]", border_style="red"))
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
# --- Display File Details ---
|
|
109
|
+
table = Table(title="File Details", show_header=False, box=box.ROUNDED, border_style="cyan")
|
|
110
|
+
table.add_column("Field", style="bold")
|
|
111
|
+
table.add_column("Value")
|
|
112
|
+
table.add_row("File Name", os.path.basename(args.filepath))
|
|
113
|
+
table.add_row("File Size", format_size(os.path.getsize(args.filepath)))
|
|
114
|
+
table.add_row("Expiration", f"{hours} hours")
|
|
115
|
+
table.add_row("Password", "[green]Yes[/green]" if args.password else "[red]No[/red]")
|
|
116
|
+
table.add_row("One-Time Download", "[green]Yes[/green]" if args.one_time else "[red]No[/red]")
|
|
117
|
+
console.print(table)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --- Hashing ---
|
|
121
|
+
console.print("[cyan]Calculating file hash...[/]")
|
|
122
|
+
client_hash = calculate_file_hash(args.filepath)
|
|
123
|
+
console.print(f" [cyan]- Hash:[/] {client_hash}")
|
|
124
|
+
|
|
125
|
+
# --- Prepare Upload ---
|
|
126
|
+
upload_url = f"{args.url.rstrip('/')}/upload"
|
|
127
|
+
filename = os.path.basename(args.filepath)
|
|
128
|
+
file_size = os.path.getsize(args.filepath)
|
|
129
|
+
|
|
130
|
+
fields = {
|
|
131
|
+
'hours': str(hours),
|
|
132
|
+
'one_time': str(args.one_time).lower(),
|
|
133
|
+
'client_hash': client_hash,
|
|
134
|
+
'file': (filename, open(args.filepath, 'rb'), 'application/octet-stream')
|
|
135
|
+
}
|
|
136
|
+
if args.password:
|
|
137
|
+
fields['password'] = args.password
|
|
138
|
+
|
|
139
|
+
encoder = MultipartEncoder(fields=fields)
|
|
140
|
+
response = None
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
bar_format = "{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [Speed: {rate_fmt}, ETA: {remaining}]"
|
|
144
|
+
with tqdm(total=encoder.len, unit='B', unit_scale=True, desc=f"Uploading {filename}", bar_format=bar_format) as pbar:
|
|
145
|
+
monitor = MultipartEncoderMonitor(encoder, lambda m: pbar.update(m.bytes_read - pbar.n))
|
|
146
|
+
response = requests.post(upload_url, data=monitor, headers={'Content-Type': monitor.content_type})
|
|
147
|
+
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
console.print(Panel(f"[bold red]Error:[/] The file '{args.filepath}' was not found.", title="[bold red]Error[/bold red]", border_style="red"))
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
except requests.exceptions.RequestException as e:
|
|
152
|
+
console.print(Panel(f"[bold red]An error occurred while connecting to the server:[/]\n{e}", title="[bold red]Error[/bold red]", border_style="red"))
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
console.print(Panel(f"[bold red]An unexpected error occurred:[/] {e}", title="[bold red]Error[/bold red]", border_style="red"))
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
finally:
|
|
158
|
+
# Ensure the file handle is closed
|
|
159
|
+
if 'file' in fields and not fields['file'][1].closed:
|
|
160
|
+
fields['file'][1].close()
|
|
161
|
+
|
|
162
|
+
# --- Handle Response ---
|
|
163
|
+
if response is not None:
|
|
164
|
+
if response.status_code == 200:
|
|
165
|
+
download_link = response.text.strip()
|
|
166
|
+
success_panel = Panel(f"[bold green]Upload successful![/bold green]\n\nDownload Link: {download_link}",
|
|
167
|
+
title="[bold cyan]Success[/bold cyan]", border_style="green")
|
|
168
|
+
console.print(success_panel)
|
|
169
|
+
|
|
170
|
+
if args.qr:
|
|
171
|
+
qr = qrcode.QRCode()
|
|
172
|
+
qr.add_data(download_link)
|
|
173
|
+
qr.make(fit=True)
|
|
174
|
+
qr.print_ascii()
|
|
175
|
+
else:
|
|
176
|
+
try:
|
|
177
|
+
error_details = response.json()
|
|
178
|
+
error_message = error_details.get('detail', 'No details provided.')
|
|
179
|
+
except requests.exceptions.JSONDecodeError:
|
|
180
|
+
error_message = response.text
|
|
181
|
+
console.print(Panel(f"[bold red]Error:[/] Upload failed with status code {response.status_code}\n[red]Server message:[/] {error_message}", title="[bold red]Error[/bold red]", border_style="red"))
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
multiprocessing.freeze_support()
|
|
186
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools]
|
|
6
|
+
packages = ["cli"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "tempspace-cli"
|
|
10
|
+
version = "1.1.1"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name="Tempspace", email="mcbplay1@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
description = "A command-line tool for uploading files to Tempspace."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
license = { text = "MIT License" }
|
|
17
|
+
requires-python = ">=3.7"
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"requests==2.31.0",
|
|
24
|
+
"tqdm==4.66.1",
|
|
25
|
+
"requests-toolbelt==1.0.0",
|
|
26
|
+
"rich==13.7.1",
|
|
27
|
+
"qrcode==8.2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
tempspace = "cli.tempspace:main"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tempspace-cli
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: A command-line tool for uploading files to Tempspace.
|
|
5
|
+
Author-email: Tempspace <mcbplay1@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests==2.31.0
|
|
12
|
+
Requires-Dist: tqdm==4.66.1
|
|
13
|
+
Requires-Dist: requests-toolbelt==1.0.0
|
|
14
|
+
Requires-Dist: rich==13.7.1
|
|
15
|
+
Requires-Dist: qrcode==8.2
|
|
16
|
+
|
|
17
|
+
# 🚀 Tempspace
|
|
18
|
+
|
|
19
|
+
Tempspace is a terminal-style file sharing service that allows you to upload files and share them via a link, with features like password protection, one-time downloads, and automatic expiration.
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
## ✨ Features
|
|
24
|
+
|
|
25
|
+
- **Multiple Upload Methods:** Use the web interface, cURL, or the official CLI tool.
|
|
26
|
+
- **Security:** Optional password protection, one-time downloads, and rate limiting.
|
|
27
|
+
- **Modern UI:** A terminal-inspired design with QR code generation and upload history.
|
|
28
|
+
- **File Management:** Support for large files, multiple expiry options, and automatic cleanup.
|
|
29
|
+
|
|
30
|
+
## 🚀 Getting Started
|
|
31
|
+
|
|
32
|
+
### Method 1: Using the Web Interface
|
|
33
|
+
|
|
34
|
+
The easiest way to use Tempspace is through the web interface.
|
|
35
|
+
|
|
36
|
+
1. **Open** the service URL (e.g., `http://localhost:8000`) in your browser.
|
|
37
|
+
2. **Drag and drop** a file or click to browse.
|
|
38
|
+
3. **Configure** the expiry time and an optional password.
|
|
39
|
+
4. **Upload** and share the generated link.
|
|
40
|
+
|
|
41
|
+
### Method 2: Using cURL
|
|
42
|
+
|
|
43
|
+
You can use `cURL` to upload files directly from your terminal.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Basic upload
|
|
47
|
+
curl -F "file=@/path/to/file.txt" -F "hours=6" https://tempspace.fly.dev/upload
|
|
48
|
+
|
|
49
|
+
# With a password
|
|
50
|
+
curl -F "file=@/path/to/secret.txt" -F "password=secret123" https://tempspace.fly.dev/upload
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Method 3: Using the Official CLI Tool
|
|
54
|
+
|
|
55
|
+
For a more integrated terminal experience, you can install the official CLI tool from PyPI.
|
|
56
|
+
|
|
57
|
+
#### Installation
|
|
58
|
+
```bash
|
|
59
|
+
pip install tempspace-cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Usage
|
|
63
|
+
```bash
|
|
64
|
+
# Basic upload
|
|
65
|
+
tempspace /path/to/document.pdf -t 6h
|
|
66
|
+
|
|
67
|
+
# With a password
|
|
68
|
+
tempspace /path/to/secret.txt -p "mypassword"
|
|
69
|
+
|
|
70
|
+
# Point to a self-hosted server
|
|
71
|
+
tempspace /path/to/file.txt --url http://localhost:8000
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 🔧 Self-Hosting
|
|
75
|
+
|
|
76
|
+
You can run your own instance of Tempspace.
|
|
77
|
+
|
|
78
|
+
### Installation
|
|
79
|
+
```bash
|
|
80
|
+
# Clone the repository
|
|
81
|
+
git clone https://github.com/your-repo/tempspace.git
|
|
82
|
+
cd tempspace
|
|
83
|
+
|
|
84
|
+
# Install dependencies
|
|
85
|
+
pip install -r requirements.txt
|
|
86
|
+
|
|
87
|
+
# Configure
|
|
88
|
+
cp .env.example .env
|
|
89
|
+
nano .env # Change the admin password
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Running the Server
|
|
93
|
+
```bash
|
|
94
|
+
python main.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server will be available at `http://localhost:8000`.
|
|
98
|
+
|
|
99
|
+
## 🛡️ Admin & Configuration
|
|
100
|
+
|
|
101
|
+
For advanced configuration, such as setting up a reverse proxy, managing admin endpoints, and more, please refer to the project's documentation or open an issue.
|
|
102
|
+
|
|
103
|
+
## 🤝 Contributing
|
|
104
|
+
|
|
105
|
+
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
106
|
+
|
|
107
|
+
## 📝 License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cli/__init__.py
|
|
4
|
+
cli/tempspace.py
|
|
5
|
+
tempspace_cli.egg-info/PKG-INFO
|
|
6
|
+
tempspace_cli.egg-info/SOURCES.txt
|
|
7
|
+
tempspace_cli.egg-info/dependency_links.txt
|
|
8
|
+
tempspace_cli.egg-info/entry_points.txt
|
|
9
|
+
tempspace_cli.egg-info/requires.txt
|
|
10
|
+
tempspace_cli.egg-info/top_level.txt
|
|
11
|
+
tests/test_cli.py
|
|
12
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cli
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from cli.tempspace import parse_time
|
|
3
|
+
|
|
4
|
+
def test_parse_time():
|
|
5
|
+
assert parse_time("7d") == 168
|
|
6
|
+
assert parse_time("24h") == 24
|
|
7
|
+
assert parse_time("360") == 360
|
|
8
|
+
assert parse_time("1D") == 24
|
|
9
|
+
assert parse_time("2H") == 2
|
|
10
|
+
assert parse_time(" 7d ") == 168
|
|
11
|
+
assert parse_time("invalid") is None
|
|
12
|
+
assert parse_time("1w") is None
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fastapi.testclient import TestClient
|
|
3
|
+
from main import app, UPLOAD_DIR
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
# Create a client for testing
|
|
10
|
+
client = TestClient(app)
|
|
11
|
+
|
|
12
|
+
# Import RateLimiter to reset it
|
|
13
|
+
from main import RateLimiter, RATE_LIMIT_UPLOADS, RATE_LIMIT_DOWNLOADS, RATE_LIMIT_WINDOW
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def reset_rate_limiters(monkeypatch):
|
|
17
|
+
"""Fixture to reset rate limiters before each test to prevent test interference."""
|
|
18
|
+
monkeypatch.setattr("main.upload_limiter", RateLimiter(RATE_LIMIT_UPLOADS, RATE_LIMIT_WINDOW))
|
|
19
|
+
monkeypatch.setattr("main.download_limiter", RateLimiter(RATE_LIMIT_DOWNLOADS, RATE_LIMIT_WINDOW))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
23
|
+
def setup_and_teardown():
|
|
24
|
+
"""Fixture to set up a clean upload directory for tests and tear it down afterward."""
|
|
25
|
+
# Ensure a clean state before tests run
|
|
26
|
+
if UPLOAD_DIR.exists():
|
|
27
|
+
shutil.rmtree(UPLOAD_DIR)
|
|
28
|
+
UPLOAD_DIR.mkdir()
|
|
29
|
+
|
|
30
|
+
# Yield control to the tests
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
# Teardown: clean up the uploads directory after all tests in the module are done
|
|
34
|
+
if UPLOAD_DIR.exists():
|
|
35
|
+
shutil.rmtree(UPLOAD_DIR)
|
|
36
|
+
|
|
37
|
+
def test_upload_file():
|
|
38
|
+
"""Test basic file upload."""
|
|
39
|
+
response = client.post(
|
|
40
|
+
"/upload",
|
|
41
|
+
files={"file": ("test_file.txt", b"hello world", "text/plain")},
|
|
42
|
+
data={"hours": "1"},
|
|
43
|
+
)
|
|
44
|
+
assert response.status_code == 200
|
|
45
|
+
data = response.json()
|
|
46
|
+
assert data["success"] is True
|
|
47
|
+
assert data["filename"] == "test_file.txt"
|
|
48
|
+
assert "url" in data
|
|
49
|
+
assert "file_id" in data
|
|
50
|
+
|
|
51
|
+
def test_upload_with_password():
|
|
52
|
+
"""Test file upload with a password."""
|
|
53
|
+
response = client.post(
|
|
54
|
+
"/upload",
|
|
55
|
+
files={"file": ("test_password.txt", b"secret content", "text/plain")},
|
|
56
|
+
data={"hours": "1", "password": "testpassword"},
|
|
57
|
+
)
|
|
58
|
+
assert response.status_code == 200
|
|
59
|
+
data = response.json()
|
|
60
|
+
assert data["success"] is True
|
|
61
|
+
assert data["password_protected"] is True
|
|
62
|
+
|
|
63
|
+
def test_download_file_no_password():
|
|
64
|
+
"""Test downloading a file that doesn't require a password."""
|
|
65
|
+
# First, upload a file
|
|
66
|
+
upload_response = client.post(
|
|
67
|
+
"/upload",
|
|
68
|
+
files={"file": ("download_test.txt", b"download content", "text/plain")},
|
|
69
|
+
data={"hours": "1"},
|
|
70
|
+
)
|
|
71
|
+
upload_data = upload_response.json()
|
|
72
|
+
file_url = upload_data["url"]
|
|
73
|
+
|
|
74
|
+
# Now, try to download it
|
|
75
|
+
# The URL contains the full host, so we need to extract the path
|
|
76
|
+
path = file_url.split("/", 3)[-1]
|
|
77
|
+
download_response = client.get(path)
|
|
78
|
+
|
|
79
|
+
assert download_response.status_code == 200
|
|
80
|
+
assert download_response.content == b"download content"
|
|
81
|
+
|
|
82
|
+
def test_download_file_with_correct_password():
|
|
83
|
+
"""Test downloading a password-protected file with the correct password."""
|
|
84
|
+
upload_response = client.post(
|
|
85
|
+
"/upload",
|
|
86
|
+
files={"file": ("protected_download.txt", b"protected", "text/plain")},
|
|
87
|
+
data={"hours": "1", "password": "supersecret"},
|
|
88
|
+
)
|
|
89
|
+
upload_data = upload_response.json()
|
|
90
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
91
|
+
|
|
92
|
+
download_response = client.get(f"{path}?password=supersecret")
|
|
93
|
+
assert download_response.status_code == 200
|
|
94
|
+
assert download_response.content == b"protected"
|
|
95
|
+
|
|
96
|
+
def test_download_file_with_incorrect_password():
|
|
97
|
+
"""Test downloading a password-protected file with an incorrect password."""
|
|
98
|
+
upload_response = client.post(
|
|
99
|
+
"/upload",
|
|
100
|
+
files={"file": ("protected_fail.txt", b"protected fail", "text/plain")},
|
|
101
|
+
data={"hours": "1", "password": "supersecret"},
|
|
102
|
+
)
|
|
103
|
+
upload_data = upload_response.json()
|
|
104
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
105
|
+
|
|
106
|
+
download_response = client.get(f"{path}?password=wrongpassword")
|
|
107
|
+
assert download_response.status_code == 403 # Forbidden
|
|
108
|
+
|
|
109
|
+
def test_one_time_download():
|
|
110
|
+
"""Test that a one-time download file is deleted after being accessed."""
|
|
111
|
+
upload_response = client.post(
|
|
112
|
+
"/upload",
|
|
113
|
+
files={"file": ("one_time.txt", b"one time content", "text/plain")},
|
|
114
|
+
data={"hours": "1", "one_time": "true"},
|
|
115
|
+
)
|
|
116
|
+
upload_data = upload_response.json()
|
|
117
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
118
|
+
|
|
119
|
+
# First download should succeed
|
|
120
|
+
first_download_response = client.get(path)
|
|
121
|
+
assert first_download_response.status_code == 200
|
|
122
|
+
|
|
123
|
+
# Let the async deletion task run
|
|
124
|
+
async def wait_for_deletion():
|
|
125
|
+
await asyncio.sleep(0.1)
|
|
126
|
+
|
|
127
|
+
asyncio.run(wait_for_deletion())
|
|
128
|
+
|
|
129
|
+
# Second download should fail
|
|
130
|
+
second_download_response = client.get(path)
|
|
131
|
+
assert second_download_response.status_code == 404 # Not Found
|
|
132
|
+
|
|
133
|
+
def test_delete_file():
|
|
134
|
+
"""Test deleting a file using the client_id."""
|
|
135
|
+
# Upload a file with a specific client_id
|
|
136
|
+
client_id = "test-client-123"
|
|
137
|
+
upload_response = client.post(
|
|
138
|
+
"/upload",
|
|
139
|
+
files={"file": ("to_be_deleted.txt", b"delete me", "text/plain")},
|
|
140
|
+
data={"hours": "1", "client_id": client_id},
|
|
141
|
+
)
|
|
142
|
+
upload_data = upload_response.json()
|
|
143
|
+
file_id = upload_data["file_id"]
|
|
144
|
+
|
|
145
|
+
# Now, delete the file with the correct client_id
|
|
146
|
+
delete_response = client.request(
|
|
147
|
+
"DELETE",
|
|
148
|
+
f"/delete/{file_id}",
|
|
149
|
+
json={"client_id": client_id},
|
|
150
|
+
)
|
|
151
|
+
assert delete_response.status_code == 200
|
|
152
|
+
assert delete_response.json()["success"] is True
|
|
153
|
+
|
|
154
|
+
# Verify the file is gone
|
|
155
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
156
|
+
get_response = client.get(path)
|
|
157
|
+
assert get_response.status_code == 404
|
|
158
|
+
|
|
159
|
+
def test_delete_file_unauthorized():
|
|
160
|
+
"""Test that deleting a file with the wrong client_id fails."""
|
|
161
|
+
# Upload a file with a specific client_id
|
|
162
|
+
owner_client_id = "owner-client"
|
|
163
|
+
upload_response = client.post(
|
|
164
|
+
"/upload",
|
|
165
|
+
files={"file": ("unauthorized_delete.txt", b"don't delete me", "text/plain")},
|
|
166
|
+
data={"hours": "1", "client_id": owner_client_id},
|
|
167
|
+
)
|
|
168
|
+
upload_data = upload_response.json()
|
|
169
|
+
file_id = upload_data["file_id"]
|
|
170
|
+
|
|
171
|
+
# Attempt to delete with a different client_id
|
|
172
|
+
attacker_client_id = "attacker-client"
|
|
173
|
+
delete_response = client.request(
|
|
174
|
+
"DELETE",
|
|
175
|
+
f"/delete/{file_id}",
|
|
176
|
+
json={"client_id": attacker_client_id},
|
|
177
|
+
)
|
|
178
|
+
assert delete_response.status_code == 403 # Forbidden
|
|
179
|
+
|
|
180
|
+
def test_duplicate_file_upload():
|
|
181
|
+
"""Test that uploading a file with the same content hash returns the original file's URL."""
|
|
182
|
+
# First upload
|
|
183
|
+
response1 = client.post(
|
|
184
|
+
"/upload",
|
|
185
|
+
files={"file": ("duplicate_content.txt", b"this content is the same", "text/plain")},
|
|
186
|
+
data={"hours": "1"},
|
|
187
|
+
)
|
|
188
|
+
assert response1.status_code == 200
|
|
189
|
+
data1 = response1.json()
|
|
190
|
+
assert data1["success"] is True
|
|
191
|
+
|
|
192
|
+
# Second upload with the same content
|
|
193
|
+
response2 = client.post(
|
|
194
|
+
"/upload",
|
|
195
|
+
files={"file": ("another_name.txt", b"this content is the same", "text/plain")},
|
|
196
|
+
data={"hours": "1"},
|
|
197
|
+
)
|
|
198
|
+
assert response2.status_code == 200
|
|
199
|
+
data2 = response2.json()
|
|
200
|
+
assert data2["success"] is True
|
|
201
|
+
|
|
202
|
+
# Check that the file_id is the same for both uploads
|
|
203
|
+
assert data1["file_id"] == data2["file_id"]
|
|
204
|
+
|
|
205
|
+
import hashlib
|
|
206
|
+
|
|
207
|
+
def calculate_hash(content: bytes) -> str:
|
|
208
|
+
"""Helper function to calculate SHA256 hash."""
|
|
209
|
+
return hashlib.sha256(content).hexdigest()
|
|
210
|
+
|
|
211
|
+
def test_upload_with_matching_hash():
|
|
212
|
+
"""Test that a file upload with a matching client_hash is successful."""
|
|
213
|
+
file_content = b"content for hash test"
|
|
214
|
+
client_hash = calculate_hash(file_content)
|
|
215
|
+
|
|
216
|
+
response = client.post(
|
|
217
|
+
"/upload",
|
|
218
|
+
files={"file": ("hash_match.txt", file_content, "text/plain")},
|
|
219
|
+
data={"hours": "1", "client_hash": client_hash},
|
|
220
|
+
)
|
|
221
|
+
assert response.status_code == 200
|
|
222
|
+
data = response.json()
|
|
223
|
+
assert data["success"] is True
|
|
224
|
+
assert data["hash_verified"] is True
|
|
225
|
+
|
|
226
|
+
def test_upload_with_mismatching_hash():
|
|
227
|
+
"""Test that a file upload with a mismatching client_hash is rejected."""
|
|
228
|
+
file_content = b"content for hash mismatch test"
|
|
229
|
+
# Provide a deliberately incorrect hash
|
|
230
|
+
incorrect_hash = "thisisnotthecorrecthash" * 2
|
|
231
|
+
|
|
232
|
+
response = client.post(
|
|
233
|
+
"/upload",
|
|
234
|
+
files={"file": ("hash_mismatch.txt", file_content, "text/plain")},
|
|
235
|
+
data={"hours": "1", "client_hash": incorrect_hash},
|
|
236
|
+
)
|
|
237
|
+
assert response.status_code == 400 # Bad Request
|
|
238
|
+
data = response.json()
|
|
239
|
+
assert "Hash verification failed" in data["detail"]
|
|
240
|
+
|
|
241
|
+
def test_upload_rate_limiting():
|
|
242
|
+
"""Test that the upload rate limit is enforced."""
|
|
243
|
+
# The default rate limit is 10 uploads per hour. We'll exceed this.
|
|
244
|
+
for i in range(10):
|
|
245
|
+
response = client.post(
|
|
246
|
+
"/upload",
|
|
247
|
+
files={"file": (f"rate_limit_test_{i}.txt", b"rate limit content", "text/plain")},
|
|
248
|
+
data={"hours": "1"},
|
|
249
|
+
)
|
|
250
|
+
assert response.status_code == 200
|
|
251
|
+
|
|
252
|
+
# The 11th request should be rate-limited
|
|
253
|
+
response = client.post(
|
|
254
|
+
"/upload",
|
|
255
|
+
files={"file": ("rate_limit_exceeded.txt", b"this should fail", "text/plain")},
|
|
256
|
+
data={"hours": "1"},
|
|
257
|
+
)
|
|
258
|
+
assert response.status_code == 429
|
|
259
|
+
|
|
260
|
+
def test_download_rate_limiting():
|
|
261
|
+
"""Test that the download rate limit is enforced."""
|
|
262
|
+
# First, upload a file to download
|
|
263
|
+
upload_response = client.post(
|
|
264
|
+
"/upload",
|
|
265
|
+
files={"file": ("download_rate_limit.txt", b"download me", "text/plain")},
|
|
266
|
+
data={"hours": "1"},
|
|
267
|
+
)
|
|
268
|
+
upload_data = upload_response.json()
|
|
269
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
270
|
+
|
|
271
|
+
# The default download limit is 100 per hour.
|
|
272
|
+
# We will hit the endpoint 100 times, expecting success.
|
|
273
|
+
for _ in range(100):
|
|
274
|
+
download_response = client.get(path)
|
|
275
|
+
assert download_response.status_code == 200
|
|
276
|
+
|
|
277
|
+
# The 101st request should be rate-limited
|
|
278
|
+
final_download_response = client.get(path)
|
|
279
|
+
assert final_download_response.status_code == 429
|
|
280
|
+
|
|
281
|
+
def test_debug_stats_unauthorized():
|
|
282
|
+
"""Test that the /debug/stats endpoint requires authentication."""
|
|
283
|
+
response = client.get("/debug/stats")
|
|
284
|
+
assert response.status_code == 401 # Unauthorized
|
|
285
|
+
|
|
286
|
+
def test_debug_stats_authorized():
|
|
287
|
+
"""Test that the /debug/stats endpoint returns data with correct authentication."""
|
|
288
|
+
# First, upload a file to have some stats to check
|
|
289
|
+
client.post(
|
|
290
|
+
"/upload",
|
|
291
|
+
files={"file": ("stats_test_file.txt", b"some data", "text/plain")},
|
|
292
|
+
data={"hours": "1"},
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
response = client.get("/debug/stats", auth=("admin", "testpassword"))
|
|
296
|
+
assert response.status_code == 200
|
|
297
|
+
# Check for some expected keys in the HTML response content
|
|
298
|
+
assert b"Total Files" in response.content
|
|
299
|
+
assert b"Total Size" in response.content
|
|
300
|
+
assert b"stats_test_file.txt" in response.content
|
|
301
|
+
|
|
302
|
+
def test_debug_wipe_unauthorized():
|
|
303
|
+
"""Test that the /debug/wipe endpoint requires authentication."""
|
|
304
|
+
response = client.post("/debug/wipe")
|
|
305
|
+
assert response.status_code == 401 # Unauthorized
|
|
306
|
+
|
|
307
|
+
def test_debug_wipe_authorized():
|
|
308
|
+
"""Test that the /debug/wipe endpoint successfully deletes all data."""
|
|
309
|
+
# First, upload a file to ensure there is data to wipe
|
|
310
|
+
upload_response = client.post(
|
|
311
|
+
"/upload",
|
|
312
|
+
files={"file": ("wipe_test_file.txt", b"wipe me", "text/plain")},
|
|
313
|
+
data={"hours": "1"},
|
|
314
|
+
)
|
|
315
|
+
assert upload_response.status_code == 200
|
|
316
|
+
upload_data = upload_response.json()
|
|
317
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
318
|
+
|
|
319
|
+
# Now, wipe the data
|
|
320
|
+
wipe_response = client.post("/debug/wipe", auth=("admin", "testpassword"))
|
|
321
|
+
assert wipe_response.status_code == 200
|
|
322
|
+
wipe_data = wipe_response.json()
|
|
323
|
+
assert wipe_data["success"] is True
|
|
324
|
+
assert wipe_data["wiped_files"] > 0
|
|
325
|
+
|
|
326
|
+
# Verify that the previously uploaded file is gone
|
|
327
|
+
get_response = client.get(path)
|
|
328
|
+
assert get_response.status_code == 404
|
|
329
|
+
|
|
330
|
+
# Import necessary modules for the expiration test
|
|
331
|
+
from datetime import datetime, timedelta, timezone
|
|
332
|
+
from main import cleanup_expired_files, shutdown_event
|
|
333
|
+
|
|
334
|
+
@pytest.mark.asyncio
|
|
335
|
+
async def test_file_expiration_and_cleanup(monkeypatch):
|
|
336
|
+
"""Test that an expired file is automatically cleaned up by the background task."""
|
|
337
|
+
# Prevent the main app's cleanup task from running automatically
|
|
338
|
+
async def do_nothing():
|
|
339
|
+
pass
|
|
340
|
+
monkeypatch.setattr("main.cleanup_expired_files", do_nothing)
|
|
341
|
+
|
|
342
|
+
# Set a very short cleanup interval so we can trigger it manually
|
|
343
|
+
monkeypatch.setattr("main.CLEANUP_INTERVAL", 0.01)
|
|
344
|
+
|
|
345
|
+
# Upload a file with a 1-hour expiry
|
|
346
|
+
upload_response = client.post(
|
|
347
|
+
"/upload",
|
|
348
|
+
files={"file": ("cleanup_test.txt", b"I will be cleaned up", "text/plain")},
|
|
349
|
+
data={"hours": "1"},
|
|
350
|
+
)
|
|
351
|
+
assert upload_response.status_code == 200
|
|
352
|
+
upload_data = upload_response.json()
|
|
353
|
+
path = upload_data["url"].split("/", 3)[-1]
|
|
354
|
+
file_id = upload_data["file_id"]
|
|
355
|
+
|
|
356
|
+
# At this point, the file should exist in the metadata
|
|
357
|
+
from main import metadata
|
|
358
|
+
assert file_id in metadata
|
|
359
|
+
|
|
360
|
+
# A mock datetime class that is always 2 hours in the future
|
|
361
|
+
class MockDateTime(datetime):
|
|
362
|
+
@classmethod
|
|
363
|
+
def now(cls, tz=None):
|
|
364
|
+
return datetime.now(timezone.utc) + timedelta(hours=2)
|
|
365
|
+
|
|
366
|
+
# Now, patch the datetime object to simulate time passing
|
|
367
|
+
monkeypatch.setattr('main.datetime', MockDateTime)
|
|
368
|
+
|
|
369
|
+
# Manually run the cleanup task once
|
|
370
|
+
# Reset the shutdown event to allow the task to run
|
|
371
|
+
shutdown_event.clear()
|
|
372
|
+
cleanup_task = asyncio.create_task(cleanup_expired_files())
|
|
373
|
+
await asyncio.sleep(0.02) # Give it a moment to run
|
|
374
|
+
shutdown_event.set() # Stop the task
|
|
375
|
+
await cleanup_task # Wait for it to finish gracefully
|
|
376
|
+
|
|
377
|
+
# After cleanup, the file should be gone from metadata and disk
|
|
378
|
+
assert file_id not in metadata
|
|
379
|
+
download_response = client.get(path)
|
|
380
|
+
assert download_response.status_code == 404
|
|
381
|
+
|
|
382
|
+
def test_upload_with_invalid_expiry():
|
|
383
|
+
"""Test that uploading with an invalid expiry time fails."""
|
|
384
|
+
response = client.post(
|
|
385
|
+
"/upload",
|
|
386
|
+
files={"file": ("invalid_expiry.txt", b"some content", "text/plain")},
|
|
387
|
+
data={"hours": "999"}, # 999 is not a valid expiry option
|
|
388
|
+
)
|
|
389
|
+
assert response.status_code == 400
|
|
390
|
+
assert "Invalid expiry time" in response.json()["detail"]
|
|
391
|
+
|
|
392
|
+
def test_upload_exceeding_max_size(monkeypatch):
|
|
393
|
+
"""Test that uploading a file larger than MAX_FILE_SIZE fails."""
|
|
394
|
+
# Set a very small max file size for the test
|
|
395
|
+
monkeypatch.setattr("main.MAX_FILE_SIZE", 10) # 10 bytes
|
|
396
|
+
|
|
397
|
+
# Attempt to upload a file larger than the new max size
|
|
398
|
+
file_content = b"this file is definitely larger than ten bytes"
|
|
399
|
+
response = client.post(
|
|
400
|
+
"/upload",
|
|
401
|
+
files={"file": ("too_large.txt", file_content, "text/plain")},
|
|
402
|
+
data={"hours": "1"},
|
|
403
|
+
)
|
|
404
|
+
assert response.status_code == 413 # Payload Too Large
|
|
405
|
+
assert "File too large" in response.json()["detail"]
|