pyservx 1.0.4__py3-none-any.whl → 1.1.0__py3-none-any.whl
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 pyservx might be problematic. Click here for more details.
- pyservx/file_operations.py +52 -0
- pyservx/html_generator.py +443 -0
- pyservx/request_handler.py +146 -0
- pyservx/server.py +160 -639
- pyservx-1.1.0.dist-info/METADATA +74 -0
- pyservx-1.1.0.dist-info/RECORD +11 -0
- {pyservx-1.0.4.dist-info → pyservx-1.1.0.dist-info}/entry_points.txt +0 -0
- {pyservx-1.0.4.dist-info → pyservx-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {pyservx-1.0.4.dist-info → pyservx-1.1.0.dist-info}/top_level.txt +0 -0
- pyservx-1.0.4.dist-info/METADATA +0 -72
- pyservx-1.0.4.dist-info/RECORD +0 -8
- {pyservx-1.0.4.dist-info → pyservx-1.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import zipfile
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
|
|
7
|
+
def format_size(size):
|
|
8
|
+
if size < 1024:
|
|
9
|
+
return f"{size} B"
|
|
10
|
+
elif size < 1024**2:
|
|
11
|
+
return f"{size / 1024:.2f} KB"
|
|
12
|
+
elif size < 1024**3:
|
|
13
|
+
return f"{size / (1024**2):.2f} MB"
|
|
14
|
+
else:
|
|
15
|
+
return f"{size / (1024**3):.2f} GB"
|
|
16
|
+
|
|
17
|
+
def zip_folder(folder_path):
|
|
18
|
+
memory_file = BytesIO()
|
|
19
|
+
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
20
|
+
for root, _, files in os.walk(folder_path):
|
|
21
|
+
for file in files:
|
|
22
|
+
abs_path = os.path.join(root, file)
|
|
23
|
+
rel_path = os.path.relpath(abs_path, folder_path)
|
|
24
|
+
zipf.write(abs_path, rel_path)
|
|
25
|
+
memory_file.seek(0)
|
|
26
|
+
return memory_file
|
|
27
|
+
|
|
28
|
+
def write_file_in_chunks(file_path, file_content, progress_callback=None):
|
|
29
|
+
total_size = len(file_content)
|
|
30
|
+
bytes_written = 0
|
|
31
|
+
chunk_size = 8192 # 8KB chunks
|
|
32
|
+
|
|
33
|
+
with open(file_path, 'wb') as f:
|
|
34
|
+
for i in range(0, total_size, chunk_size):
|
|
35
|
+
chunk = file_content[i:i + chunk_size]
|
|
36
|
+
f.write(chunk)
|
|
37
|
+
bytes_written += len(chunk)
|
|
38
|
+
if progress_callback:
|
|
39
|
+
progress_callback(bytes_written, total_size)
|
|
40
|
+
|
|
41
|
+
def read_file_in_chunks(file_path, chunk_size=8192, progress_callback=None):
|
|
42
|
+
file_size = os.path.getsize(file_path)
|
|
43
|
+
bytes_read = 0
|
|
44
|
+
with open(file_path, 'rb') as f:
|
|
45
|
+
while True:
|
|
46
|
+
chunk = f.read(chunk_size)
|
|
47
|
+
if not chunk:
|
|
48
|
+
break
|
|
49
|
+
bytes_read += len(chunk)
|
|
50
|
+
if progress_callback:
|
|
51
|
+
progress_callback(bytes_read, file_size)
|
|
52
|
+
yield chunk
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
import os
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import datetime
|
|
7
|
+
|
|
8
|
+
def format_size(size):
|
|
9
|
+
if size < 1024:
|
|
10
|
+
return f"{size} B"
|
|
11
|
+
elif size < 1024**2:
|
|
12
|
+
return f"{size / 1024:.2f} KB"
|
|
13
|
+
elif size < 1024**3:
|
|
14
|
+
return f"{size / (1024**2):.2f} MB"
|
|
15
|
+
else:
|
|
16
|
+
return f"{size / (1024**3):.2f} GB"
|
|
17
|
+
|
|
18
|
+
def list_directory_page(handler, path):
|
|
19
|
+
try:
|
|
20
|
+
entries = os.listdir(path)
|
|
21
|
+
except OSError:
|
|
22
|
+
handler.send_error(404, "Cannot list directory")
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
query_params = urllib.parse.parse_qs(urllib.parse.urlparse(handler.path).query)
|
|
26
|
+
search_query = query_params.get('q', [''])[0]
|
|
27
|
+
sort_by = query_params.get('sort', ['name'])[0]
|
|
28
|
+
sort_order = query_params.get('order', ['asc'])[0]
|
|
29
|
+
|
|
30
|
+
if search_query:
|
|
31
|
+
entries = [e for e in entries if search_query.lower() in e.lower()]
|
|
32
|
+
|
|
33
|
+
def sort_key(item):
|
|
34
|
+
item_path = os.path.join(path, item)
|
|
35
|
+
if os.path.isdir(item_path):
|
|
36
|
+
return (0, item.lower()) # Directories first
|
|
37
|
+
if sort_by == 'size':
|
|
38
|
+
return (1, os.path.getsize(item_path))
|
|
39
|
+
elif sort_by == 'date':
|
|
40
|
+
return (1, os.path.getmtime(item_path))
|
|
41
|
+
else:
|
|
42
|
+
return (1, item.lower())
|
|
43
|
+
|
|
44
|
+
entries.sort(key=sort_key, reverse=sort_order == 'desc')
|
|
45
|
+
|
|
46
|
+
displaypath = html.escape(urllib.parse.unquote(handler.path))
|
|
47
|
+
|
|
48
|
+
# Build list items for directories and files
|
|
49
|
+
list_rows = []
|
|
50
|
+
# Parent directory link if not root
|
|
51
|
+
if handler.path != '/':
|
|
52
|
+
parent = os.path.dirname(handler.path.rstrip('/'))
|
|
53
|
+
if not parent.endswith('/'):
|
|
54
|
+
parent += '/'
|
|
55
|
+
list_rows.append(f"""
|
|
56
|
+
<tr class="hover:bg-green-900/20">
|
|
57
|
+
<td class="py-2 px-4 border-b border-green-700/50"><a href="{html.escape(parent)}" class="text-neon block">.. (Parent Directory)</a></td>
|
|
58
|
+
<td class="py-2 px-4 border-b border-green-700/50 text-right">-</td>
|
|
59
|
+
<td class="py-2 px-4 border-b border-green-700/50 text-right">-</td>
|
|
60
|
+
</tr>
|
|
61
|
+
""")
|
|
62
|
+
|
|
63
|
+
for name in entries:
|
|
64
|
+
fullpath = os.path.join(path, name)
|
|
65
|
+
displayname = name + '/' if os.path.isdir(fullpath) else name
|
|
66
|
+
href = urllib.parse.quote(name)
|
|
67
|
+
if os.path.isdir(fullpath):
|
|
68
|
+
href += '/'
|
|
69
|
+
|
|
70
|
+
size = "-"
|
|
71
|
+
date_modified = "-"
|
|
72
|
+
if os.path.isfile(fullpath):
|
|
73
|
+
size = format_size(os.path.getsize(fullpath))
|
|
74
|
+
date_modified = datetime.datetime.fromtimestamp(os.path.getmtime(fullpath)).strftime('%Y-%m-%d %H:%M:%S')
|
|
75
|
+
|
|
76
|
+
# Add download folder zip link for directories
|
|
77
|
+
if os.path.isdir(fullpath):
|
|
78
|
+
list_rows.append(
|
|
79
|
+
f"""
|
|
80
|
+
<tr class="hover:bg-green-900/20">
|
|
81
|
+
<td class="py-2 px-4 border-b border-green-700/50">
|
|
82
|
+
<a href="{href}" class="text-neon block">{html.escape(displayname)}</a>
|
|
83
|
+
</td>
|
|
84
|
+
<td class="py-2 px-4 border-b border-green-700/50 text-right">{size}</td>
|
|
85
|
+
<td class="py-2 px-4 border-b border-green-700/50 text-right">{date_modified}</td>
|
|
86
|
+
</tr>
|
|
87
|
+
<tr class="hover:bg-green-900/20">
|
|
88
|
+
<td class="py-2 px-4 border-b border-green-700/50" colspan="3">
|
|
89
|
+
<a href="{href}download_folder" class="text-neon block">📦 Zip Download</a>
|
|
90
|
+
</td>
|
|
91
|
+
</tr>
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
list_rows.append(f"""
|
|
96
|
+
<tr class="hover:bg-green-900/20">
|
|
97
|
+
<td class="py-2 px-4 border-b border-green-700/50"><a href="{href}" class="text-neon block">{html.escape(displayname)}</a></td>
|
|
98
|
+
<td class="py-2 px-4 border-b border-green-700/50 text-right">{size}</td>
|
|
99
|
+
<td class="py-2 px-4 border-b border-green-700/50 text-right">{date_modified}</td>
|
|
100
|
+
</tr>
|
|
101
|
+
""")
|
|
102
|
+
|
|
103
|
+
list_html = '\n'.join(list_rows)
|
|
104
|
+
|
|
105
|
+
return f"""<!DOCTYPE html>
|
|
106
|
+
<html lang="en">
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="UTF-8" />
|
|
109
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
110
|
+
<title>PyServeX - Index of {displaypath}</title>
|
|
111
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
112
|
+
<style>
|
|
113
|
+
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
|
114
|
+
|
|
115
|
+
body {{
|
|
116
|
+
font-family: 'VT323', monospace;
|
|
117
|
+
background: #000000;
|
|
118
|
+
min-height: 100vh;
|
|
119
|
+
margin: 0;
|
|
120
|
+
overflow-x: hidden;
|
|
121
|
+
}}
|
|
122
|
+
|
|
123
|
+
.text-neon {{
|
|
124
|
+
color: #00ff00;
|
|
125
|
+
}}
|
|
126
|
+
|
|
127
|
+
.typewriter h1 {{
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
animation: typing 3s steps(40, end), blink-caret 0.5s step-end infinite;
|
|
131
|
+
margin: 0 auto;
|
|
132
|
+
text-align: center;
|
|
133
|
+
}}
|
|
134
|
+
|
|
135
|
+
@keyframes typing {{
|
|
136
|
+
from {{ width: 0; }}
|
|
137
|
+
to {{ width: 100%; }}
|
|
138
|
+
}}
|
|
139
|
+
|
|
140
|
+
@keyframes blink-caret {{
|
|
141
|
+
from, to {{ border-right: 2px solid #00ff00; }}
|
|
142
|
+
50% {{ border-right: 2px solid transparent; }}
|
|
143
|
+
}}
|
|
144
|
+
|
|
145
|
+
.glitch {{
|
|
146
|
+
position: relative;
|
|
147
|
+
animation: glitch 2s infinite;
|
|
148
|
+
}}
|
|
149
|
+
|
|
150
|
+
@keyframes glitch {{
|
|
151
|
+
0% {{ transform: translate(0); }}
|
|
152
|
+
10% {{ transform: translate(-2px, 2px); }}
|
|
153
|
+
20% {{ transform: translate(2px, -2px); }}
|
|
154
|
+
30% {{ transform: translate(-2px, 2px); }}
|
|
155
|
+
40% {{ transform: translate(0); }}
|
|
156
|
+
100% {{ transform: translate(0); }}
|
|
157
|
+
}}
|
|
158
|
+
|
|
159
|
+
.scanline {{
|
|
160
|
+
position: absolute;
|
|
161
|
+
top: 0;
|
|
162
|
+
left: 0;
|
|
163
|
+
width: 100%;
|
|
164
|
+
height: 100%;
|
|
165
|
+
background: linear-gradient(
|
|
166
|
+
to bottom,
|
|
167
|
+
rgba(255, 255, 255, 0),
|
|
168
|
+
rgba(255, 255, 255, 0.1) 50%,
|
|
169
|
+
rgba(255, 255, 255, 0)
|
|
170
|
+
);
|
|
171
|
+
animation: scan 4s linear infinite;
|
|
172
|
+
pointer-events: none;
|
|
173
|
+
}}
|
|
174
|
+
|
|
175
|
+
@keyframes scan {{
|
|
176
|
+
0% {{ transform: translateY(-100%); }}
|
|
177
|
+
100% {{ transform: translateY(100%); }}
|
|
178
|
+
}}
|
|
179
|
+
|
|
180
|
+
.particle {{
|
|
181
|
+
position: absolute;
|
|
182
|
+
width: 3px;
|
|
183
|
+
height: 3px;
|
|
184
|
+
background: #00ff00;
|
|
185
|
+
opacity: 0.5;
|
|
186
|
+
animation: flicker 3s infinite;
|
|
187
|
+
}}
|
|
188
|
+
|
|
189
|
+
@keyframes flicker {{
|
|
190
|
+
0% {{ opacity: 0.5; }}
|
|
191
|
+
50% {{ opacity: 0.1; }}
|
|
192
|
+
100% {{ opacity: 0.5; }}
|
|
193
|
+
}}
|
|
194
|
+
|
|
195
|
+
main {{
|
|
196
|
+
margin-top: 100px; /* Adjust based on header height */
|
|
197
|
+
padding: 2rem;
|
|
198
|
+
color: #00ff00;
|
|
199
|
+
text-align: left;
|
|
200
|
+
max-width: 900px;
|
|
201
|
+
margin-left: auto;
|
|
202
|
+
margin-right: auto;
|
|
203
|
+
}}
|
|
204
|
+
|
|
205
|
+
ul {{
|
|
206
|
+
list-style-type: none;
|
|
207
|
+
padding-left: 0;
|
|
208
|
+
}}
|
|
209
|
+
|
|
210
|
+
li {{
|
|
211
|
+
margin-bottom: 0.7rem;
|
|
212
|
+
font-size: 1.2rem;
|
|
213
|
+
}}
|
|
214
|
+
|
|
215
|
+
a {{
|
|
216
|
+
text-decoration: none;
|
|
217
|
+
}}
|
|
218
|
+
|
|
219
|
+
a:hover {{
|
|
220
|
+
text-decoration: underline;
|
|
221
|
+
}}
|
|
222
|
+
|
|
223
|
+
.upload-form, .search-form {{
|
|
224
|
+
margin-top: 1.5rem;
|
|
225
|
+
padding: 1rem;
|
|
226
|
+
border: 1px solid #00ff00;
|
|
227
|
+
border-radius: 5px;
|
|
228
|
+
}}
|
|
229
|
+
|
|
230
|
+
.upload-form label, .search-form label {{
|
|
231
|
+
display: block;
|
|
232
|
+
margin-bottom: 0.5rem;
|
|
233
|
+
}}
|
|
234
|
+
|
|
235
|
+
.upload-form input[type="file"], .search-form input[type="text"] {{
|
|
236
|
+
color: #00ff00;
|
|
237
|
+
background: #000000;
|
|
238
|
+
border: 1px solid #00ff00;
|
|
239
|
+
padding: 0.5rem;
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
.upload-form button, .search-form button {{
|
|
243
|
+
background: #00ff00;
|
|
244
|
+
color: #000000;
|
|
245
|
+
padding: 0.5rem 1rem;
|
|
246
|
+
border: none;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
font-family: 'VT323', monospace;
|
|
249
|
+
font-size: 1.2rem;
|
|
250
|
+
}}
|
|
251
|
+
|
|
252
|
+
.upload-form button:hover, .search-form button:hover {{
|
|
253
|
+
background: #00cc00;
|
|
254
|
+
}}
|
|
255
|
+
|
|
256
|
+
table {{
|
|
257
|
+
width: 100%;
|
|
258
|
+
border-collapse: collapse;
|
|
259
|
+
margin-top: 1.5rem;
|
|
260
|
+
}}
|
|
261
|
+
|
|
262
|
+
th, td {{
|
|
263
|
+
text-align: left;
|
|
264
|
+
padding: 0.75rem;
|
|
265
|
+
border-bottom: 1px solid rgba(0, 255, 0, 0.3);
|
|
266
|
+
}}
|
|
267
|
+
|
|
268
|
+
th {{
|
|
269
|
+
background-color: rgba(0, 255, 0, 0.1);
|
|
270
|
+
color: #00ff00;
|
|
271
|
+
font-weight: normal;
|
|
272
|
+
cursor: pointer;
|
|
273
|
+
}}
|
|
274
|
+
|
|
275
|
+
th:hover {{
|
|
276
|
+
background-color: rgba(0, 255, 0, 0.2);
|
|
277
|
+
}}
|
|
278
|
+
|
|
279
|
+
tr:nth-child(even) {{
|
|
280
|
+
background-color: rgba(0, 255, 0, 0.05);
|
|
281
|
+
}}
|
|
282
|
+
|
|
283
|
+
/* Progress bar and popup styles */
|
|
284
|
+
#progressBarContainer {{
|
|
285
|
+
display: none;
|
|
286
|
+
margin-top: 1rem;
|
|
287
|
+
background-color: #333;
|
|
288
|
+
border: 1px solid #00ff00;
|
|
289
|
+
padding: 0.5rem;
|
|
290
|
+
border-radius: 5px;
|
|
291
|
+
}}
|
|
292
|
+
|
|
293
|
+
#progressBar {{
|
|
294
|
+
width: 0%;
|
|
295
|
+
height: 20px;
|
|
296
|
+
background-color: #00ff00;
|
|
297
|
+
text-align: center;
|
|
298
|
+
line-height: 20px;
|
|
299
|
+
color: #000;
|
|
300
|
+
font-size: 0.8rem;
|
|
301
|
+
}}
|
|
302
|
+
|
|
303
|
+
#progressText {{
|
|
304
|
+
color: #00ff00;
|
|
305
|
+
margin-top: 0.5rem;
|
|
306
|
+
font-size: 0.9rem;
|
|
307
|
+
}}
|
|
308
|
+
|
|
309
|
+
#successPopup {{
|
|
310
|
+
display: none;
|
|
311
|
+
position: fixed;
|
|
312
|
+
top: 20px;
|
|
313
|
+
left: 50%;
|
|
314
|
+
transform: translateX(-50%);
|
|
315
|
+
background-color: #00ff00;
|
|
316
|
+
color: #000;
|
|
317
|
+
padding: 1rem 2rem;
|
|
318
|
+
border-radius: 5px;
|
|
319
|
+
font-size: 1.2rem;
|
|
320
|
+
z-index: 1000;
|
|
321
|
+
box-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
|
|
322
|
+
}}
|
|
323
|
+
</style>
|
|
324
|
+
</head>
|
|
325
|
+
<body>
|
|
326
|
+
<div class="scanline"></div>
|
|
327
|
+
<header>
|
|
328
|
+
<div class="text-center">
|
|
329
|
+
<h1 class="text-4xl md:text-6xl text-neon typewriter glitch">PyServeX</h1>
|
|
330
|
+
</div>
|
|
331
|
+
</header>
|
|
332
|
+
<main>
|
|
333
|
+
<h2 class="text-3xl mb-4 text-neon">Index of {displaypath}</h2>
|
|
334
|
+
<div class="search-form mb-6 p-4 border border-green-700/50 rounded-lg">
|
|
335
|
+
<form action="{html.escape(handler.path)}" method="GET" class="flex flex-col sm:flex-row items-center space-y-2 sm:space-y-0 sm:space-x-2">
|
|
336
|
+
<input type="text" name="q" placeholder="Search files..." value="{html.escape(search_query)}" class="flex-grow p-2 bg-black text-neon border border-green-700/50 rounded-md focus:outline-none focus:ring-1 focus:ring-green-500">
|
|
337
|
+
<button type="submit" class="bg-green-500 text-black py-2 px-4 rounded-md hover:bg-green-600 transition-colors duration-200">Search</button>
|
|
338
|
+
</form>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="overflow-x-auto">
|
|
341
|
+
<table class="min-w-full bg-black border border-green-700/50 rounded-lg">
|
|
342
|
+
<thead>
|
|
343
|
+
<tr>
|
|
344
|
+
<th class="py-3 px-4 border-b border-green-700/50 text-left text-neon">
|
|
345
|
+
<a href="?sort=name&order={{'desc' if sort_by == 'name' and sort_order == 'asc' else 'asc'}}" class="block">Name</a>
|
|
346
|
+
</th>
|
|
347
|
+
<th class="py-3 px-4 border-b border-green-700/50 text-right text-neon">
|
|
348
|
+
<a href="?sort=size&order={{'desc' if sort_by == 'size' and sort_order == 'asc' else 'asc'}}" class="block">Size</a>
|
|
349
|
+
</th>
|
|
350
|
+
<th class="py-3 px-4 border-b border-green-700/50 text-right text-neon">
|
|
351
|
+
<a href="?sort=date&order={{'desc' if sort_by == 'date' and sort_order == 'asc' else 'asc'}}" class="block">Date Modified</a>
|
|
352
|
+
</th>
|
|
353
|
+
</tr>
|
|
354
|
+
</thead>
|
|
355
|
+
<tbody>
|
|
356
|
+
{list_html}
|
|
357
|
+
</tbody>
|
|
358
|
+
</table>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="upload-form mt-6 p-4 border border-green-700/50 rounded-lg">
|
|
361
|
+
<form id="uploadForm" action="{html.escape(handler.path)}upload" method="POST" enctype="multipart/form-data" class="flex flex-col sm:flex-row items-center space-y-2 sm:space-y-0 sm:space-x-2">
|
|
362
|
+
<label for="file-upload" class="text-neon">Upload files:</label>
|
|
363
|
+
<input type="file" id="file-upload" name="file" multiple class="flex-grow p-2 bg-black text-neon border border-green-700/50 rounded-md focus:outline-none focus:ring-1 focus:ring-green-500" />
|
|
364
|
+
<button type="submit" class="bg-green-500 text-black py-2 px-4 rounded-md hover:bg-green-600 transition-colors duration-200">Upload</button>
|
|
365
|
+
</form>
|
|
366
|
+
<div id="progressBarContainer">
|
|
367
|
+
<div id="progressBar"></div>
|
|
368
|
+
<div id="progressText"></div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</main>
|
|
372
|
+
|
|
373
|
+
<div id="successPopup"></div>
|
|
374
|
+
|
|
375
|
+
<script>
|
|
376
|
+
// Generate random particles for hacker effect
|
|
377
|
+
function createParticles() {{
|
|
378
|
+
const numParticles = 30;
|
|
379
|
+
for (let i = 0; i < numParticles; i++) {{
|
|
380
|
+
const particle = document.createElement('div');
|
|
381
|
+
particle.classList.add('particle');
|
|
382
|
+
particle.style.left = `${{Math.random() * 100}}vw`;
|
|
383
|
+
particle.style.top = `${{Math.random() * 100}}vh`;
|
|
384
|
+
particle.style.animationDelay = `${{Math.random() * 3}}s`;
|
|
385
|
+
document.body.appendChild(particle);
|
|
386
|
+
}}
|
|
387
|
+
}}
|
|
388
|
+
|
|
389
|
+
// Handle file uploads with progress bar
|
|
390
|
+
document.getElementById('uploadForm').addEventListener('submit', function(e) {{
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
|
|
393
|
+
const formData = new FormData(this);
|
|
394
|
+
const xhr = new XMLHttpRequest();
|
|
395
|
+
|
|
396
|
+
const progressBarContainer = document.getElementById('progressBarContainer');
|
|
397
|
+
const progressBar = document.getElementById('progressBar');
|
|
398
|
+
const progressText = document.getElementById('progressText');
|
|
399
|
+
const successPopup = document.getElementById('successPopup');
|
|
400
|
+
|
|
401
|
+
progressBarContainer.style.display = 'block';
|
|
402
|
+
progressBar.style.width = '0%';
|
|
403
|
+
progressText.textContent = 'Uploading...';
|
|
404
|
+
|
|
405
|
+
xhr.upload.addEventListener('progress', function(event) {{
|
|
406
|
+
if (event.lengthComputable) {{
|
|
407
|
+
const percent = (event.loaded / event.total) * 100;
|
|
408
|
+
progressBar.style.width = percent.toFixed(2) + '%';
|
|
409
|
+
progressText.textContent = `Uploading: ${{percent.toFixed(2)}}%`;
|
|
410
|
+
}}
|
|
411
|
+
}});
|
|
412
|
+
|
|
413
|
+
xhr.addEventListener('load', function() {{
|
|
414
|
+
progressBarContainer.style.display = 'none';
|
|
415
|
+
if (xhr.status === 200) {{
|
|
416
|
+
const response = JSON.parse(xhr.responseText);
|
|
417
|
+
successPopup.textContent = response.message;
|
|
418
|
+
successPopup.style.display = 'block';
|
|
419
|
+
setTimeout(() => {{
|
|
420
|
+
successPopup.style.display = 'none';
|
|
421
|
+
window.location.reload(); // Reload page to show new files
|
|
422
|
+
}}, 2000);
|
|
423
|
+
}} else {{
|
|
424
|
+
progressText.textContent = 'Upload failed!';
|
|
425
|
+
alert('Upload failed: ' + xhr.statusText);
|
|
426
|
+
}}
|
|
427
|
+
}});
|
|
428
|
+
|
|
429
|
+
xhr.addEventListener('error', function() {{
|
|
430
|
+
progressBarContainer.style.display = 'none';
|
|
431
|
+
progressText.textContent = 'Upload failed!';
|
|
432
|
+
alert('Upload failed due to a network error.');
|
|
433
|
+
}});
|
|
434
|
+
|
|
435
|
+
xhr.open('POST', this.action);
|
|
436
|
+
xhr.send(formData);
|
|
437
|
+
}});
|
|
438
|
+
|
|
439
|
+
window.onload = createParticles;
|
|
440
|
+
</script>
|
|
441
|
+
</body>
|
|
442
|
+
</html>
|
|
443
|
+
"""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import http.server
|
|
4
|
+
import os
|
|
5
|
+
import posixpath
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import shutil
|
|
8
|
+
import logging
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from . import html_generator
|
|
12
|
+
from . import file_operations
|
|
13
|
+
|
|
14
|
+
class FileRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
15
|
+
def translate_path(self, path):
|
|
16
|
+
# Prevent path traversal attacks
|
|
17
|
+
path = posixpath.normpath(urllib.parse.unquote(path))
|
|
18
|
+
rel_path = path.lstrip('/')
|
|
19
|
+
abs_path = os.path.abspath(os.path.join(self.base_dir, rel_path))
|
|
20
|
+
if not abs_path.startswith(self.base_dir):
|
|
21
|
+
logging.warning(f"Path traversal attempt detected: {path}")
|
|
22
|
+
return self.base_dir # Prevent access outside the base directory
|
|
23
|
+
return abs_path
|
|
24
|
+
|
|
25
|
+
def do_GET(self):
|
|
26
|
+
if self.path.endswith('/download_folder'):
|
|
27
|
+
folder_path = self.translate_path(self.path.replace('/download_folder', ''))
|
|
28
|
+
if os.path.isdir(folder_path):
|
|
29
|
+
zip_file = file_operations.zip_folder(folder_path)
|
|
30
|
+
self.send_response(200)
|
|
31
|
+
self.send_header("Content-Type", "application/zip")
|
|
32
|
+
self.send_header("Content-Disposition", f"attachment; filename={os.path.basename(folder_path)}.zip")
|
|
33
|
+
self.end_headers()
|
|
34
|
+
shutil.copyfileobj(zip_file, self.wfile)
|
|
35
|
+
else:
|
|
36
|
+
self.send_error(404, "Folder not found")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if os.path.isdir(self.translate_path(self.path)):
|
|
40
|
+
self.list_directory(self.translate_path(self.path))
|
|
41
|
+
else:
|
|
42
|
+
# Handle file downloads with progress tracking
|
|
43
|
+
path = self.translate_path(self.path)
|
|
44
|
+
if os.path.isfile(path):
|
|
45
|
+
try:
|
|
46
|
+
file_size = os.path.getsize(path)
|
|
47
|
+
self.send_response(200)
|
|
48
|
+
self.send_header("Content-type", self.guess_type(path))
|
|
49
|
+
self.send_header("Content-Length", str(file_size))
|
|
50
|
+
self.end_headers()
|
|
51
|
+
|
|
52
|
+
start_time = time.time()
|
|
53
|
+
for chunk in file_operations.read_file_in_chunks(path):
|
|
54
|
+
self.wfile.write(chunk)
|
|
55
|
+
end_time = time.time()
|
|
56
|
+
duration = end_time - start_time
|
|
57
|
+
speed_bps = file_size / duration if duration > 0 else 0
|
|
58
|
+
logging.info(f"Downloaded {os.path.basename(path)} ({file_operations.format_size(file_size)}) in {duration:.2f}s at {file_operations.format_size(speed_bps)}/s")
|
|
59
|
+
|
|
60
|
+
except OSError:
|
|
61
|
+
self.send_error(404, "File not found")
|
|
62
|
+
else:
|
|
63
|
+
super().do_GET()
|
|
64
|
+
|
|
65
|
+
def do_POST(self):
|
|
66
|
+
if self.path.endswith('/upload'):
|
|
67
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
68
|
+
|
|
69
|
+
# Parse multipart form data
|
|
70
|
+
content_type = self.headers.get('Content-Type', '')
|
|
71
|
+
if not content_type.startswith('multipart/form-data'):
|
|
72
|
+
self.send_error(400, "Invalid content type")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
boundary = content_type.split('boundary=')[1].encode()
|
|
76
|
+
body = self.rfile.read(content_length)
|
|
77
|
+
|
|
78
|
+
# Simple parsing of multipart form data
|
|
79
|
+
parts = body.split(b'--' + boundary)
|
|
80
|
+
uploaded_files = []
|
|
81
|
+
for part in parts:
|
|
82
|
+
if b'filename="' in part:
|
|
83
|
+
# Extract filename
|
|
84
|
+
start = part.find(b'filename="') + 10
|
|
85
|
+
end = part.find(b'"', start)
|
|
86
|
+
filename = part[start:end].decode('utf-8')
|
|
87
|
+
# Sanitize filename
|
|
88
|
+
filename = os.path.basename(filename)
|
|
89
|
+
if not filename:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Extract file content
|
|
93
|
+
content_start = part.find(b'\r\n\r\n') + 4
|
|
94
|
+
content_end = part.rfind(b'\r\n--' + boundary)
|
|
95
|
+
if content_end == -1:
|
|
96
|
+
content_end = len(part) - 2
|
|
97
|
+
file_content = part[content_start:content_end]
|
|
98
|
+
|
|
99
|
+
# Save file to the target directory
|
|
100
|
+
target_dir = self.translate_path(self.path.replace('/upload', ''))
|
|
101
|
+
if not os.path.isdir(target_dir):
|
|
102
|
+
self.send_error(404, "Target directory not found")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
file_path = os.path.join(target_dir, filename)
|
|
106
|
+
try:
|
|
107
|
+
start_time = time.time()
|
|
108
|
+
file_operations.write_file_in_chunks(file_path, file_content)
|
|
109
|
+
end_time = time.time()
|
|
110
|
+
duration = end_time - start_time
|
|
111
|
+
file_size_bytes = len(file_content)
|
|
112
|
+
speed_bps = file_size_bytes / duration if duration > 0 else 0
|
|
113
|
+
|
|
114
|
+
logging.info(f"Uploaded {filename} ({file_operations.format_size(file_size_bytes)}) in {duration:.2f}s at {file_operations.format_size(speed_bps)}/s")
|
|
115
|
+
uploaded_files.append(filename)
|
|
116
|
+
except OSError:
|
|
117
|
+
self.send_error(500, "Error saving file")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if not uploaded_files:
|
|
121
|
+
self.send_error(400, "No file provided")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Log the upload and redirect URL
|
|
125
|
+
redirect_url = self.path.replace('/upload', '') or '/'
|
|
126
|
+
logging.info(f"Files uploaded: {', '.join(uploaded_files)} to {target_dir}")
|
|
127
|
+
logging.info(f"Redirecting to: {redirect_url}")
|
|
128
|
+
|
|
129
|
+
self.send_response(200)
|
|
130
|
+
self.send_header("Content-type", "application/json")
|
|
131
|
+
self.end_headers()
|
|
132
|
+
response_data = {"status": "success", "message": "Files uploaded successfully!"}
|
|
133
|
+
self.wfile.write(json.dumps(response_data).encode('utf-8'))
|
|
134
|
+
return
|
|
135
|
+
else:
|
|
136
|
+
self.send_error(405, "Method not allowed")
|
|
137
|
+
|
|
138
|
+
def list_directory(self, path):
|
|
139
|
+
html_content = html_generator.list_directory_page(self, path)
|
|
140
|
+
encoded = html_content.encode('utf-8', 'surrogateescape')
|
|
141
|
+
self.send_response(200)
|
|
142
|
+
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
143
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
144
|
+
self.end_headers()
|
|
145
|
+
self.wfile.write(encoded)
|
|
146
|
+
return
|