tempspace-cli 1.2.6__tar.gz → 1.2.7__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.
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/PKG-INFO +2 -2
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/cli/tempspace.py +149 -9
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/pyproject.toml +2 -2
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tempspace_cli.egg-info/PKG-INFO +2 -2
- tempspace_cli-1.2.7/tests/test_cli.py +123 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tests/test_main.py +47 -9
- tempspace_cli-1.2.6/tests/test_cli.py +0 -12
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/README.md +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/cli/__init__.py +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/setup.cfg +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tempspace_cli.egg-info/SOURCES.txt +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tempspace_cli.egg-info/dependency_links.txt +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tempspace_cli.egg-info/entry_points.txt +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tempspace_cli.egg-info/requires.txt +0 -0
- {tempspace_cli-1.2.6 → tempspace_cli-1.2.7}/tempspace_cli.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempspace-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.7
|
|
4
4
|
Summary: A command-line tool for uploading files to Tempspace.
|
|
5
5
|
Author-email: Tempspace <mcbplay1@gmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: requests==2.32.5
|
|
12
12
|
Requires-Dist: requests-toolbelt==1.0.0
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
import hashlib
|
|
6
6
|
import multiprocessing
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import math
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
from rich.panel import Panel
|
|
@@ -13,8 +13,12 @@ from rich import box
|
|
|
13
13
|
import qrcode
|
|
14
14
|
from rich.prompt import Prompt, Confirm
|
|
15
15
|
from rich.progress import Progress, BarColumn, TextColumn, TransferSpeedColumn, TimeRemainingColumn
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
from rich.live import Live
|
|
18
|
+
import shutil
|
|
19
|
+
import tempfile
|
|
20
|
+
from requests.adapters import HTTPAdapter
|
|
21
|
+
from urllib3.util.retry import Retry
|
|
18
22
|
|
|
19
23
|
# Default configuration
|
|
20
24
|
DEFAULT_SERVER_URL = "https://tempspace.needrp.net"
|
|
@@ -85,6 +89,113 @@ def calculate_file_hash(filepath: str) -> str:
|
|
|
85
89
|
return sha256_hash.hexdigest()
|
|
86
90
|
|
|
87
91
|
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_retry_session(retries=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504]):
|
|
95
|
+
"""Creates a requests Session with automatic retries."""
|
|
96
|
+
session = requests.Session()
|
|
97
|
+
retry = Retry(
|
|
98
|
+
total=retries,
|
|
99
|
+
read=retries,
|
|
100
|
+
connect=retries,
|
|
101
|
+
backoff_factor=backoff_factor,
|
|
102
|
+
status_forcelist=status_forcelist,
|
|
103
|
+
)
|
|
104
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
105
|
+
session.mount('http://', adapter)
|
|
106
|
+
session.mount('https://', adapter)
|
|
107
|
+
return session
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def zip_directory(path: str) -> str:
|
|
111
|
+
"""Zips a directory and returns the path to the temporary zip file."""
|
|
112
|
+
# Create a temporary directory to store the zip
|
|
113
|
+
temp_dir = tempfile.mkdtemp()
|
|
114
|
+
# Use the original folder name for the zip file
|
|
115
|
+
base_name = os.path.basename(os.path.normpath(path))
|
|
116
|
+
archive_base = os.path.join(temp_dir, base_name)
|
|
117
|
+
|
|
118
|
+
shutil.make_archive(archive_base, 'zip', path)
|
|
119
|
+
return archive_base + ".zip"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def download_file(console, url, password=None):
|
|
123
|
+
"""Downloads a file from Tempspace."""
|
|
124
|
+
session = get_retry_session()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Check if it's a valid Tempspace URL and extract ID logic if needed,
|
|
128
|
+
# but for now we'll assume the URL is the full download link or similar.
|
|
129
|
+
# Actually, the user provides the "Download Link" which usually leads to a landing page.
|
|
130
|
+
# The CLI needs to handle the actual file download.
|
|
131
|
+
# If the URL is http://site/DOWLOAD_ID, the direct download might be different.
|
|
132
|
+
# Let's assume the user passes the direct download link or we try to download from it.
|
|
133
|
+
# However, Tempspace likely has a landing page.
|
|
134
|
+
# If the user gives the landing page URL, we might need to scrape or use an API.
|
|
135
|
+
# Given I don't see the server code, I'll assume the user might provide a direct file URL
|
|
136
|
+
# or the tool should try to GET the URL.
|
|
137
|
+
# If the server has an API like /api/download/<id>?password=..., that would be best.
|
|
138
|
+
# Looking at upload_file response: `download_link = data.get('url')`.
|
|
139
|
+
# Code audit earlier showed `DEFAULT_SERVER_URL`.
|
|
140
|
+
# Use regex to extract ID from URL if possible, or just try GET.
|
|
141
|
+
|
|
142
|
+
# Simple approach: GET the URL. If it initiates a download (headers), good.
|
|
143
|
+
|
|
144
|
+
session.head(url, allow_redirects=True)
|
|
145
|
+
# Check headers for content-disposition or just proceed
|
|
146
|
+
|
|
147
|
+
if password:
|
|
148
|
+
# If password is needed, headers or query param might be required.
|
|
149
|
+
# Since I don't know the exact server auth mechanism for downloads,
|
|
150
|
+
# I will try passing it as a query param 'password' or header 'X-Password'.
|
|
151
|
+
# Better: ask the user to enter it if the server responds 401/403.
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# For a streaming download
|
|
155
|
+
response = session.get(url, stream=True, params={'password': password} if password else None)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
|
|
158
|
+
total_size = int(response.headers.get('content-length', 0))
|
|
159
|
+
disposition = response.headers.get('content-disposition')
|
|
160
|
+
# Try to get filename from Content-Disposition
|
|
161
|
+
filename = None
|
|
162
|
+
if disposition:
|
|
163
|
+
import re
|
|
164
|
+
# specific regex for filename="example.txt" or filename=example.txt
|
|
165
|
+
fname = re.findall(r'filename=["\']?([^"\';]+)["\']?', disposition)
|
|
166
|
+
if fname:
|
|
167
|
+
filename = fname[0]
|
|
168
|
+
|
|
169
|
+
# Fallback to URL if no filename found from headers
|
|
170
|
+
if not filename:
|
|
171
|
+
from urllib.parse import unquote
|
|
172
|
+
parsed_path = unquote(url.split("?")[0])
|
|
173
|
+
filename = parsed_path.split("/")[-1]
|
|
174
|
+
|
|
175
|
+
if not filename:
|
|
176
|
+
filename = "downloaded_file"
|
|
177
|
+
|
|
178
|
+
progress = Progress(
|
|
179
|
+
TextColumn("[bold blue]{task.description}", justify="right"),
|
|
180
|
+
BarColumn(bar_width=None),
|
|
181
|
+
"[progress.percentage]{task.percentage:>3.1f}%", "•",
|
|
182
|
+
TransferSpeedColumn(), "•",
|
|
183
|
+
TimeRemainingColumn(),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
with Live(Panel(progress, title="[cyan]Downloading[/cyan]", border_style="cyan", title_align="left")):
|
|
187
|
+
task_id = progress.add_task(filename, total=total_size)
|
|
188
|
+
with open(filename, 'wb') as f:
|
|
189
|
+
for chunk in response.iter_content(CHUNK_SIZE):
|
|
190
|
+
f.write(chunk)
|
|
191
|
+
progress.update(task_id, advance=len(chunk))
|
|
192
|
+
|
|
193
|
+
console.print(Panel(f"[bold green]Download successful![/] Saved to '{filename}'", border_style="green"))
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
console.print(Panel(f"[bold red]Download failed:[/] {e}", border_style="red"))
|
|
197
|
+
|
|
198
|
+
|
|
88
199
|
def upload_file(console, filepath, hours, password, one_time, qr, url):
|
|
89
200
|
"""Handles the upload of a single file.
|
|
90
201
|
|
|
@@ -125,9 +236,10 @@ def upload_file(console, filepath, hours, password, one_time, qr, url):
|
|
|
125
236
|
|
|
126
237
|
# --- Chunked Upload ---
|
|
127
238
|
response = None
|
|
239
|
+
session = get_retry_session()
|
|
128
240
|
try:
|
|
129
241
|
# 1. Initiate Upload
|
|
130
|
-
initiate_response =
|
|
242
|
+
initiate_response = session.post(f"{upload_url}/upload/initiate")
|
|
131
243
|
initiate_response.raise_for_status()
|
|
132
244
|
upload_id = initiate_response.json()['upload_id']
|
|
133
245
|
|
|
@@ -140,7 +252,7 @@ def upload_file(console, filepath, hours, password, one_time, qr, url):
|
|
|
140
252
|
TimeRemainingColumn(),
|
|
141
253
|
)
|
|
142
254
|
|
|
143
|
-
with Live(Panel(progress, title="[cyan]Uploading[/cyan]", border_style="cyan", title_align="left"))
|
|
255
|
+
with Live(Panel(progress, title="[cyan]Uploading[/cyan]", border_style="cyan", title_align="left")):
|
|
144
256
|
task_id = progress.add_task(filename, total=file_size)
|
|
145
257
|
with open(filepath, 'rb') as f:
|
|
146
258
|
chunk_number = 0
|
|
@@ -152,7 +264,7 @@ def upload_file(console, filepath, hours, password, one_time, qr, url):
|
|
|
152
264
|
}
|
|
153
265
|
files = {'file': (f'chunk_{chunk_number}', chunk, 'application/octet-stream')}
|
|
154
266
|
|
|
155
|
-
chunk_response =
|
|
267
|
+
chunk_response = session.post(
|
|
156
268
|
f"{upload_url}/upload/chunk",
|
|
157
269
|
data=chunk_data,
|
|
158
270
|
files=files
|
|
@@ -172,7 +284,7 @@ def upload_file(console, filepath, hours, password, one_time, qr, url):
|
|
|
172
284
|
if password:
|
|
173
285
|
finalize_data['password'] = password
|
|
174
286
|
|
|
175
|
-
response =
|
|
287
|
+
response = session.post(f"{upload_url}/upload/finalize", data=finalize_data)
|
|
176
288
|
response.raise_for_status()
|
|
177
289
|
|
|
178
290
|
except FileNotFoundError:
|
|
@@ -183,7 +295,7 @@ def upload_file(console, filepath, hours, password, one_time, qr, url):
|
|
|
183
295
|
if e.response:
|
|
184
296
|
try:
|
|
185
297
|
error_message = e.response.json().get('detail', e.response.text)
|
|
186
|
-
except:
|
|
298
|
+
except Exception:
|
|
187
299
|
error_message = e.response.text
|
|
188
300
|
console.print(Panel(f"[bold red]An error occurred:[/] {error_message}", title="[bold red]Error[/bold red]", border_style="red"))
|
|
189
301
|
return
|
|
@@ -244,6 +356,21 @@ def main():
|
|
|
244
356
|
"""
|
|
245
357
|
console = Console()
|
|
246
358
|
|
|
359
|
+
# --- Handle Download Command ---
|
|
360
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'download':
|
|
361
|
+
parser = argparse.ArgumentParser(description="Download a file from Tempspace.")
|
|
362
|
+
parser.add_argument("url", help="The URL of the file to download.")
|
|
363
|
+
parser.add_argument("-p", "--password", help="The password for the file.")
|
|
364
|
+
|
|
365
|
+
# We process only the download arguments
|
|
366
|
+
if len(sys.argv) == 2 and (sys.argv[1] == '-h' or sys.argv[1] == '--help'):
|
|
367
|
+
parser.print_help()
|
|
368
|
+
sys.exit(0)
|
|
369
|
+
|
|
370
|
+
args = parser.parse_args(sys.argv[2:])
|
|
371
|
+
download_file(console, args.url, args.password)
|
|
372
|
+
return
|
|
373
|
+
|
|
247
374
|
# --- Header ---
|
|
248
375
|
console.print(Panel("[bold cyan]Tempspace File Uploader[/bold cyan]", expand=False, border_style="blue"))
|
|
249
376
|
|
|
@@ -285,11 +412,24 @@ def main():
|
|
|
285
412
|
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"))
|
|
286
413
|
sys.exit(1)
|
|
287
414
|
|
|
415
|
+
# --- Process each file ---
|
|
288
416
|
# --- Process each file ---
|
|
289
417
|
for i, filepath in enumerate(filepaths):
|
|
418
|
+
actual_path = filepath
|
|
419
|
+
is_temp = False
|
|
420
|
+
|
|
421
|
+
if os.path.isdir(filepath):
|
|
422
|
+
console.print(f"[bold yellow]Zipping directory '{os.path.basename(filepath)}'...[/]")
|
|
423
|
+
actual_path = zip_directory(filepath)
|
|
424
|
+
is_temp = True
|
|
425
|
+
|
|
290
426
|
if len(filepaths) > 1:
|
|
291
|
-
console.print(f"\n[bold yellow]Uploading file {i+1} of {len(filepaths)}: {os.path.basename(
|
|
292
|
-
|
|
427
|
+
console.print(f"\n[bold yellow]Uploading file {i+1} of {len(filepaths)}: {os.path.basename(actual_path)}[/bold yellow]\n")
|
|
428
|
+
|
|
429
|
+
upload_file(console, actual_path, hours, args.password, args.one_time, args.qr, args.url)
|
|
430
|
+
|
|
431
|
+
if is_temp:
|
|
432
|
+
os.remove(actual_path)
|
|
293
433
|
|
|
294
434
|
|
|
295
435
|
if __name__ == "__main__":
|
|
@@ -7,14 +7,14 @@ packages = ["cli"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "tempspace-cli"
|
|
10
|
-
version = "1.2.
|
|
10
|
+
version = "1.2.7"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name="Tempspace", email="mcbplay1@gmail.com" },
|
|
13
13
|
]
|
|
14
14
|
description = "A command-line tool for uploading files to Tempspace."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
license = { text = "MIT License" }
|
|
17
|
-
requires-python = ">=3.
|
|
17
|
+
requires-python = ">=3.8"
|
|
18
18
|
classifiers = [
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Operating System :: OS Independent",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempspace-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.7
|
|
4
4
|
Summary: A command-line tool for uploading files to Tempspace.
|
|
5
5
|
Author-email: Tempspace <mcbplay1@gmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: requests==2.32.5
|
|
12
12
|
Requires-Dist: requests-toolbelt==1.0.0
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
from cli.tempspace import parse_time, format_size, calculate_file_hash, zip_directory, upload_file, download_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# --- Unit Tests ---
|
|
12
|
+
|
|
13
|
+
def test_parse_time():
|
|
14
|
+
assert parse_time("7d") == 168
|
|
15
|
+
assert parse_time("24h") == 24
|
|
16
|
+
assert parse_time("360") == 360
|
|
17
|
+
assert parse_time("1D") == 24
|
|
18
|
+
assert parse_time("2H") == 2
|
|
19
|
+
assert parse_time(" 7d ") == 168
|
|
20
|
+
assert parse_time("invalid") is None
|
|
21
|
+
assert parse_time("1w") is None
|
|
22
|
+
|
|
23
|
+
def test_format_size():
|
|
24
|
+
assert format_size(0) == "0B"
|
|
25
|
+
assert format_size(1023) == "1023.0 B"
|
|
26
|
+
assert format_size(100) == "100.0 B"
|
|
27
|
+
assert format_size(1024) == "1.0 KB"
|
|
28
|
+
assert format_size(1024 * 1024) == "1.0 MB"
|
|
29
|
+
assert format_size(1024 * 1024 * 1024 * 2.5) == "2.5 GB"
|
|
30
|
+
|
|
31
|
+
def test_calculate_file_hash():
|
|
32
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
33
|
+
f.write(b"hello world")
|
|
34
|
+
filepath = f.name
|
|
35
|
+
try:
|
|
36
|
+
expected_hash = hashlib.sha256(b"hello world").hexdigest()
|
|
37
|
+
assert calculate_file_hash(filepath) == expected_hash
|
|
38
|
+
finally:
|
|
39
|
+
os.remove(filepath)
|
|
40
|
+
|
|
41
|
+
def test_zip_directory():
|
|
42
|
+
# Create a dummy directory structure
|
|
43
|
+
temp_dir = tempfile.mkdtemp()
|
|
44
|
+
try:
|
|
45
|
+
os.makedirs(os.path.join(temp_dir, "subdir"))
|
|
46
|
+
with open(os.path.join(temp_dir, "file1.txt"), "w") as f:
|
|
47
|
+
f.write("content1")
|
|
48
|
+
|
|
49
|
+
# Call zip_directory
|
|
50
|
+
zip_path = zip_directory(temp_dir)
|
|
51
|
+
|
|
52
|
+
assert os.path.exists(zip_path)
|
|
53
|
+
assert zip_path.endswith(".zip")
|
|
54
|
+
assert os.path.basename(temp_dir) in os.path.basename(zip_path)
|
|
55
|
+
|
|
56
|
+
# Cleanup zip
|
|
57
|
+
os.remove(zip_path)
|
|
58
|
+
finally:
|
|
59
|
+
shutil.rmtree(temp_dir)
|
|
60
|
+
|
|
61
|
+
# --- Mock Tests ---
|
|
62
|
+
|
|
63
|
+
@patch('cli.tempspace.requests.Session')
|
|
64
|
+
def test_upload_file_success(mock_session_cls, capsys):
|
|
65
|
+
mock_session = mock_session_cls.return_value
|
|
66
|
+
|
|
67
|
+
# Mock Initiate
|
|
68
|
+
mock_session.post.side_effect = [
|
|
69
|
+
MagicMock(status_code=200, json=lambda: {'upload_id': '123'}), # Initiate
|
|
70
|
+
MagicMock(status_code=200), # Chunk 1
|
|
71
|
+
MagicMock(status_code=200, json=lambda: {'url': 'http://test.com/file', 'hash': 'abc', 'hash_verified': True}) # Finalize
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
75
|
+
f.write(b"test content")
|
|
76
|
+
filepath = f.name
|
|
77
|
+
|
|
78
|
+
mock_console = MagicMock()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
upload_file(mock_console, filepath, 24, None, False, False, "http://test-server")
|
|
82
|
+
|
|
83
|
+
# Assert calls
|
|
84
|
+
assert mock_session.post.call_count == 3
|
|
85
|
+
# Verify initiate call
|
|
86
|
+
mock_session.post.call_args_list[0][0][0].endswith("/upload/initiate")
|
|
87
|
+
# Verify finalize call
|
|
88
|
+
mock_session.post.call_args_list[2][0][0].endswith("/upload/finalize")
|
|
89
|
+
|
|
90
|
+
# Verify success message printed
|
|
91
|
+
# mock_console.print.assert_called() # Hard to check partial strings on rich objects without more complex matching
|
|
92
|
+
|
|
93
|
+
finally:
|
|
94
|
+
os.remove(filepath)
|
|
95
|
+
|
|
96
|
+
@patch('cli.tempspace.requests.Session')
|
|
97
|
+
def test_download_file_success(mock_session_cls):
|
|
98
|
+
mock_session = mock_session_cls.return_value
|
|
99
|
+
|
|
100
|
+
# Mock HEAD request (optional/handled in code?) Code does checks but uses same session
|
|
101
|
+
mock_session.head.return_value = MagicMock(status_code=200)
|
|
102
|
+
|
|
103
|
+
# Mock GET request
|
|
104
|
+
content = b"downloaded content"
|
|
105
|
+
mock_response = MagicMock(status_code=200)
|
|
106
|
+
mock_response.headers = {'content-length': str(len(content)), 'content-disposition': 'attachment; filename="server_file.txt"'}
|
|
107
|
+
mock_response.iter_content.return_value = [content]
|
|
108
|
+
mock_session.get.return_value = mock_response
|
|
109
|
+
|
|
110
|
+
mock_console = MagicMock()
|
|
111
|
+
|
|
112
|
+
# Run download
|
|
113
|
+
try:
|
|
114
|
+
download_file(mock_console, "http://test-server/file")
|
|
115
|
+
|
|
116
|
+
# Verify file saved
|
|
117
|
+
assert os.path.exists("server_file.txt")
|
|
118
|
+
with open("server_file.txt", "rb") as f:
|
|
119
|
+
assert f.read() == content
|
|
120
|
+
finally:
|
|
121
|
+
if os.path.exists("server_file.txt"):
|
|
122
|
+
os.remove("server_file.txt")
|
|
123
|
+
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from fastapi.testclient import TestClient
|
|
3
|
-
from main import
|
|
3
|
+
from main import (
|
|
4
|
+
app, UPLOAD_DIR, RateLimiter, RATE_LIMIT_UPLOADS, RATE_LIMIT_DOWNLOADS,
|
|
5
|
+
RATE_LIMIT_WINDOW, cleanup_expired_files, shutdown_event,
|
|
6
|
+
is_video, is_text, format_bytes
|
|
7
|
+
)
|
|
4
8
|
import os
|
|
5
9
|
import shutil
|
|
6
10
|
import asyncio
|
|
7
|
-
import
|
|
11
|
+
import hashlib
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
8
13
|
|
|
9
14
|
# Create a client for testing
|
|
10
15
|
client = TestClient(app)
|
|
11
16
|
|
|
12
|
-
# Import RateLimiter to reset it
|
|
13
|
-
from main import RateLimiter, RATE_LIMIT_UPLOADS, RATE_LIMIT_DOWNLOADS, RATE_LIMIT_WINDOW
|
|
14
17
|
|
|
15
18
|
@pytest.fixture(autouse=True)
|
|
16
19
|
def reset_rate_limiters(monkeypatch):
|
|
@@ -217,7 +220,7 @@ def test_duplicate_file_upload():
|
|
|
217
220
|
# Check that the file_id is the same for both uploads
|
|
218
221
|
assert data1["file_id"] == data2["file_id"]
|
|
219
222
|
|
|
220
|
-
|
|
223
|
+
|
|
221
224
|
|
|
222
225
|
def calculate_hash(content: bytes) -> str:
|
|
223
226
|
"""Helper function to calculate SHA256 hash."""
|
|
@@ -343,8 +346,6 @@ def test_debug_wipe_authorized():
|
|
|
343
346
|
assert get_response.status_code == 404
|
|
344
347
|
|
|
345
348
|
# Import necessary modules for the expiration test
|
|
346
|
-
from datetime import datetime, timedelta, timezone
|
|
347
|
-
from main import cleanup_expired_files, shutdown_event
|
|
348
349
|
|
|
349
350
|
@pytest.mark.asyncio
|
|
350
351
|
async def test_file_expiration_and_cleanup(monkeypatch):
|
|
@@ -425,7 +426,7 @@ async def test_rate_limiter_timezone_aware_expiration(monkeypatch):
|
|
|
425
426
|
Test that the RateLimiter correctly expires entries after its time window,
|
|
426
427
|
using timezone-aware datetime objects to prevent DST or timezone issues.
|
|
427
428
|
"""
|
|
428
|
-
|
|
429
|
+
|
|
429
430
|
|
|
430
431
|
# Use a local limiter instance for this test
|
|
431
432
|
limiter = RateLimiter(max_requests=1, window_seconds=10)
|
|
@@ -653,7 +654,7 @@ def test_get_scheme_and_host_forwarded():
|
|
|
653
654
|
assert data["url"].startswith("https://")
|
|
654
655
|
|
|
655
656
|
|
|
656
|
-
|
|
657
|
+
|
|
657
658
|
|
|
658
659
|
def test_format_bytes():
|
|
659
660
|
"""Test the format_bytes helper function."""
|
|
@@ -719,3 +720,40 @@ def test_delete_file_missing_client_id():
|
|
|
719
720
|
json={"some_other_key": "some_value"},
|
|
720
721
|
)
|
|
721
722
|
assert response.status_code == 400
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def test_upload_zip_file():
|
|
726
|
+
"""Test uploading a zip file to verify MIME type detection and handling."""
|
|
727
|
+
# Create a dummy zip content (header for a zip file)
|
|
728
|
+
zip_header = b"PK\x03\x04"
|
|
729
|
+
response = client.post(
|
|
730
|
+
"/upload",
|
|
731
|
+
files={"file": ("archive.zip", zip_header + b"some content", "application/zip")},
|
|
732
|
+
data={"hours": "1"},
|
|
733
|
+
)
|
|
734
|
+
assert response.status_code == 200
|
|
735
|
+
data = response.json()
|
|
736
|
+
assert data["success"] is True
|
|
737
|
+
assert data["filename"] == "archive.zip"
|
|
738
|
+
# Depending on filetype library, it might guess zip or octet-stream for this partial content.
|
|
739
|
+
# The server logic uses filetype.guess.
|
|
740
|
+
assert "url" in data
|
|
741
|
+
|
|
742
|
+
def test_download_content_disposition():
|
|
743
|
+
"""Test that the server sends the correct Content-Disposition header."""
|
|
744
|
+
filename = "disposition_test.txt"
|
|
745
|
+
# Use unique content to avoid deduplication returning an old filename
|
|
746
|
+
content = b"unique content for disposition test " + os.urandom(8)
|
|
747
|
+
upload_response = client.post(
|
|
748
|
+
"/upload",
|
|
749
|
+
files={"file": (filename, content, "text/plain")},
|
|
750
|
+
data={"hours": "1"},
|
|
751
|
+
)
|
|
752
|
+
path = upload_response.json()["url"].split("/", 3)[-1]
|
|
753
|
+
|
|
754
|
+
response = client.get(path)
|
|
755
|
+
assert response.status_code == 200
|
|
756
|
+
assert "content-disposition" in response.headers
|
|
757
|
+
# Starlette/FastAPI FileResponse might use filename*=UTF-8''... for safety
|
|
758
|
+
cd = response.headers["content-disposition"]
|
|
759
|
+
assert filename in cd
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|