pyservx 1.0.4__tar.gz → 1.1.0__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.
File without changes
File without changes
pyservx-1.1.0/PKG-INFO ADDED
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyservx
3
+ Version: 1.1.0
4
+ Summary: A simple Python server package for file sharing with a retro UI.
5
+ Author-email: Subz3r0x01 <subz3r0x01@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/pyservx
8
+ Project-URL: Repository, https://github.com/yourusername/pyservx
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: qrcode
15
+ Dynamic: license-file
16
+
17
+ # PyServeX
18
+
19
+ A simple HTTP server for file sharing with a retro-styled web interface.
20
+
21
+ ## Installation
22
+
23
+ Install using pip:
24
+
25
+ ```bash
26
+ pip install pyservx
27
+ ```
28
+
29
+ Or use pipx for an isolated environment (recommended for Linux):
30
+
31
+ ```bash
32
+ pipx install pyservx
33
+ ```
34
+
35
+ Ensure you have Python 3.6 or higher installed.
36
+
37
+ ## Usage
38
+
39
+ Run the server:
40
+
41
+ ```bash
42
+ python3 run.py
43
+ ```
44
+
45
+ - Follow the prompt to select a shared folder.
46
+ - The server will start at `http://localhost:8088` and other network IPs.
47
+ - Access the web interface to browse, download, or upload files.
48
+ - Use `Ctrl+C` to stop the server.
49
+
50
+ ## Features
51
+
52
+ - File and folder browsing with a retro "hacker" UI.
53
+ - Download entire folders as ZIP files.
54
+ - Upload files via the web interface.
55
+ - Accessible via localhost (`127.0.0.1`) and network IPs.
56
+ - **QR Code for Easy Access:** Scan a QR code in the terminal to quickly access the server from your mobile device on the same network.
57
+ - **Real-time Progress Bars:** Enjoy real-time progress updates for both uploads and downloads, including ETA, transfer speed, and file size.
58
+ - **Multiple File Uploads:** Upload multiple files simultaneously through the web interface.
59
+ - **No File Size Restriction:** Upload files of any size without limitations.
60
+ - **Enhanced Web Interface:**
61
+ - **Search Functionality:** Quickly find files with the new search bar.
62
+ - **File Sorting:** Sort files by name, size, or date for better organization.
63
+ - **Modern & Responsive UI:** A refreshed, responsive design for seamless use across all devices.
64
+ - **Automated `robots.txt`:** A `robots.txt` file is automatically generated to prevent search engines from indexing your file server, enhancing privacy.
65
+ - **Modular Codebase:** The `server.py` file has been refactored into smaller, more maintainable modules (`request_handler.py`, `html_generator.py`, `file_operations.py`).
66
+
67
+ ## Requirements
68
+
69
+ - Python 3.6+
70
+ - `qrcode` library (automatically installed with pip)
71
+
72
+ ## License
73
+
74
+ MIT License
@@ -0,0 +1,58 @@
1
+ # PyServeX
2
+
3
+ A simple HTTP server for file sharing with a retro-styled web interface.
4
+
5
+ ## Installation
6
+
7
+ Install using pip:
8
+
9
+ ```bash
10
+ pip install pyservx
11
+ ```
12
+
13
+ Or use pipx for an isolated environment (recommended for Linux):
14
+
15
+ ```bash
16
+ pipx install pyservx
17
+ ```
18
+
19
+ Ensure you have Python 3.6 or higher installed.
20
+
21
+ ## Usage
22
+
23
+ Run the server:
24
+
25
+ ```bash
26
+ python3 run.py
27
+ ```
28
+
29
+ - Follow the prompt to select a shared folder.
30
+ - The server will start at `http://localhost:8088` and other network IPs.
31
+ - Access the web interface to browse, download, or upload files.
32
+ - Use `Ctrl+C` to stop the server.
33
+
34
+ ## Features
35
+
36
+ - File and folder browsing with a retro "hacker" UI.
37
+ - Download entire folders as ZIP files.
38
+ - Upload files via the web interface.
39
+ - Accessible via localhost (`127.0.0.1`) and network IPs.
40
+ - **QR Code for Easy Access:** Scan a QR code in the terminal to quickly access the server from your mobile device on the same network.
41
+ - **Real-time Progress Bars:** Enjoy real-time progress updates for both uploads and downloads, including ETA, transfer speed, and file size.
42
+ - **Multiple File Uploads:** Upload multiple files simultaneously through the web interface.
43
+ - **No File Size Restriction:** Upload files of any size without limitations.
44
+ - **Enhanced Web Interface:**
45
+ - **Search Functionality:** Quickly find files with the new search bar.
46
+ - **File Sorting:** Sort files by name, size, or date for better organization.
47
+ - **Modern & Responsive UI:** A refreshed, responsive design for seamless use across all devices.
48
+ - **Automated `robots.txt`:** A `robots.txt` file is automatically generated to prevent search engines from indexing your file server, enhancing privacy.
49
+ - **Modular Codebase:** The `server.py` file has been refactored into smaller, more maintainable modules (`request_handler.py`, `html_generator.py`, `file_operations.py`).
50
+
51
+ ## Requirements
52
+
53
+ - Python 3.6+
54
+ - `qrcode` library (automatically installed with pip)
55
+
56
+ ## License
57
+
58
+ MIT License
@@ -1,26 +1,32 @@
1
- [project]
2
- name = "pyservx"
3
- version = "1.0.4" # Increment to avoid conflict with 1.0.3
4
- description = "A simple Python server package"
5
- readme = "README.md"
6
- requires-python = ">=3.6"
7
- license = { file = "LICENSE" }
8
- authors = [
9
- { name = "Your Name", email = "your.email@example.com" }
10
- ]
11
- classifiers = [
12
- "Programming Language :: Python :: 3",
13
- "License :: OSI Approved :: MIT License",
14
- "Operating System :: OS Independent",
15
- ]
16
-
17
- [build-system]
18
- requires = ["setuptools>=61.0", "wheel"]
19
- build-backend = "setuptools.build_meta"
20
-
21
- [project.urls]
22
- Homepage = "https://github.com/yourusername/pyservx"
23
- Repository = "https://github.com/yourusername/pyservx"
24
-
25
- [project.scripts]
1
+ [project]
2
+ name = "pyservx"
3
+ version = "1.1.0"
4
+ description = "A simple Python server package for file sharing with a retro UI."
5
+ readme = "README.md"
6
+ requires-python = ">=3.6"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Subz3r0x01", email = "subz3r0x01@protonmail.com" }
10
+ ]
11
+ dependencies = [
12
+ "qrcode"
13
+ ]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=61.0", "wheel"]
22
+ build-backend = "setuptools.build_meta"
23
+
24
+ [tool.setuptools.packages]
25
+ find = { include = ["pyservx*"] }
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/yourusername/pyservx"
29
+ Repository = "https://github.com/yourusername/pyservx"
30
+
31
+ [project.scripts]
26
32
  pyservx = "pyservx.server:main"
File without changes
@@ -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
+ """