pyservx 1.0.3__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/server.py CHANGED
@@ -1,639 +1,160 @@
1
- #!/usr/bin/env python3
2
- # Improved Python HTTP Server Developed by Subz3r0x01
3
- # GitHub: https://github.com/SubZ3r0-0x01
4
-
5
- import os
6
- import posixpath
7
- import urllib.parse
8
- import http.server
9
- import socketserver
10
- import shutil
11
- import mimetypes
12
- from io import BytesIO
13
- import zipfile
14
- import threading
15
- import signal
16
- import sys
17
- import html
18
- import logging
19
- import socket
20
- import json
21
- import argparse
22
-
23
- # Configure logging for debugging
24
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
25
-
26
- PORT = 8088
27
- CONFIG_FILE = os.path.expanduser("~/.pyservx_config.json") # Store config in user's home directory
28
-
29
- def load_config():
30
- """Load shared folder path from config file if it exists."""
31
- if os.path.exists(CONFIG_FILE):
32
- try:
33
- with open(CONFIG_FILE, 'r') as f:
34
- config = json.load(f)
35
- return config.get("shared_folder")
36
- except json.JSONDecodeError:
37
- logging.warning("Invalid config file. Ignoring.")
38
- return None
39
-
40
- def save_config(folder_path):
41
- """Save shared folder path to config file."""
42
- try:
43
- os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
44
- with open(CONFIG_FILE, 'w') as f:
45
- json.dump({"shared_folder": folder_path}, f)
46
- except OSError as e:
47
- logging.error(f"Failed to save config: {e}")
48
-
49
- def get_shared_folder():
50
- """Prompt user for shared folder path or load from config."""
51
- saved_folder = load_config()
52
- if saved_folder and os.path.isdir(saved_folder):
53
- print(f"Using saved shared folder: {saved_folder}")
54
- return os.path.abspath(saved_folder)
55
-
56
- while True:
57
- folder_path = input("Enter the path to the shared folder: ").strip()
58
- if os.path.isdir(folder_path):
59
- break
60
- print("Invalid folder path. Please enter a valid directory.")
61
-
62
- while True:
63
- persist = input("Do you want this choice to be persistent? (y/n): ").strip().lower()
64
- if persist in ('y', 'n'):
65
- break
66
- print("Please enter 'y' or 'n'.")
67
-
68
- folder_path = os.path.abspath(folder_path)
69
- if persist == 'y':
70
- save_config(folder_path)
71
- print(f"Shared folder saved for future use: {folder_path}")
72
- else:
73
- print("Shared folder will be prompted again next time.")
74
-
75
- return folder_path
76
-
77
- def zip_folder(folder_path):
78
- memory_file = BytesIO()
79
- with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
80
- for root, _, files in os.walk(folder_path):
81
- for file in files:
82
- abs_path = os.path.join(root, file)
83
- rel_path = os.path.relpath(abs_path, folder_path)
84
- zipf.write(abs_path, rel_path)
85
- memory_file.seek(0)
86
- return memory_file
87
-
88
- class FileRequestHandler(http.server.SimpleHTTPRequestHandler):
89
- def translate_path(self, path):
90
- # Prevent path traversal attacks
91
- path = posixpath.normpath(urllib.parse.unquote(path))
92
- rel_path = path.lstrip('/')
93
- abs_path = os.path.abspath(os.path.join(self.base_dir, rel_path))
94
- if not abs_path.startswith(self.base_dir):
95
- logging.warning(f"Path traversal attempt detected: {path}")
96
- return self.base_dir # Prevent access outside the base directory
97
- return abs_path
98
-
99
- def do_GET(self):
100
- if self.path.endswith('/download_folder'):
101
- folder_path = self.translate_path(self.path.replace('/download_folder', ''))
102
- if os.path.isdir(folder_path):
103
- zip_file = zip_folder(folder_path)
104
- self.send_response(200)
105
- self.send_header("Content-Type", "application/zip")
106
- self.send_header("Content-Disposition", f"attachment; filename={os.path.basename(folder_path)}.zip")
107
- self.end_headers()
108
- shutil.copyfileobj(zip_file, self.wfile)
109
- else:
110
- self.send_error(404, "Folder not found")
111
- return
112
-
113
- if os.path.isdir(self.translate_path(self.path)):
114
- self.list_directory(self.translate_path(self.path))
115
- else:
116
- super().do_GET()
117
-
118
- def do_POST(self):
119
- if self.path.endswith('/upload'):
120
- content_length = int(self.headers.get('Content-Length', 0))
121
- # Limit file size to prevent abuse (e.g., 100MB)
122
- max_file_size = 100 * 1024 * 1024
123
- if content_length > max_file_size:
124
- self.send_error(413, "File too large")
125
- return
126
-
127
- # Parse multipart form data
128
- content_type = self.headers.get('Content-Type', '')
129
- if not content_type.startswith('multipart/form-data'):
130
- self.send_error(400, "Invalid content type")
131
- return
132
-
133
- boundary = content_type.split('boundary=')[1].encode()
134
- body = self.rfile.read(content_length)
135
-
136
- # Simple parsing of multipart form data
137
- parts = body.split(b'--' + boundary)
138
- for part in parts:
139
- if b'filename="' in part:
140
- # Extract filename
141
- start = part.find(b'filename="') + 10
142
- end = part.find(b'"', start)
143
- filename = part[start:end].decode('utf-8')
144
- # Sanitize filename
145
- filename = os.path.basename(filename)
146
- if not filename:
147
- continue
148
-
149
- # Extract file content
150
- content_start = part.find(b'\r\n\r\n') + 4
151
- content_end = part.rfind(b'\r\n--' + boundary)
152
- if content_end == -1:
153
- content_end = len(part) - 2
154
- file_content = part[content_start:content_end]
155
-
156
- # Save file to the target directory
157
- target_dir = self.translate_path(self.path.replace('/upload', ''))
158
- if not os.path.isdir(target_dir):
159
- self.send_error(404, "Target directory not found")
160
- return
161
-
162
- file_path = os.path.join(target_dir, filename)
163
- try:
164
- with open(file_path, 'wb') as f:
165
- f.write(file_content)
166
- except OSError:
167
- self.send_error(500, "Error saving file")
168
- return
169
-
170
- # Log the upload and redirect URL
171
- redirect_url = self.path.replace('/upload', '') or '/'
172
- logging.info(f"File uploaded: {filename} to {target_dir}")
173
- logging.info(f"Redirecting to: {redirect_url}")
174
-
175
- # Serve success page with redirect
176
- html_content = f'''<!DOCTYPE html>
177
- <html lang="en">
178
- <head>
179
- <meta charset="UTF-8" />
180
- <meta name="viewport" content="width=device-width, initial-scale=1" />
181
- <title>PyServeX - Upload Success</title>
182
- <script src="https://cdn.tailwindcss.com"></script>
183
- <style>
184
- @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
185
-
186
- body {{
187
- font-family: 'VT323', monospace;
188
- background: #000000;
189
- min-height: 100vh;
190
- margin: 0;
191
- overflow-x: hidden;
192
- }}
193
-
194
- .text-neon {{
195
- color: #00ff00;
196
- }}
197
-
198
- .typewriter h1 {{
199
- overflow: hidden;
200
- white-space: nowrap;
201
- animation: typing 3s steps(40, end), blink-caret 0.5s step-end infinite;
202
- margin: 0 auto;
203
- text-align: center;
204
- }}
205
-
206
- @keyframes typing {{
207
- from {{ width: 0; }}
208
- to {{ width: 100%; }}
209
- }}
210
-
211
- @keyframes blink-caret {{
212
- from, to {{ border-right: 2px solid #00ff00; }}
213
- 50% {{ border-right: 2px solid transparent; }}
214
- }}
215
-
216
- .glitch {{
217
- position: relative.
218
- animation: glitch 2s infinite;
219
- }}
220
-
221
- @keyframes glitch {{
222
- 0% {{ transform: translate(0); }}
223
- 10% {{ transform: translate(-2px, 2px); }}
224
- 20% {{ transform: translate(2px, -2px); }}
225
- 30% {{ transform: translate(-2px, 2px); }}
226
- 40% {{ transform: translate(0); }}
227
- 100% {{ transform: translate(0); }}
228
- }}
229
-
230
- .scanline {{
231
- position: absolute;
232
- top: 0;
233
- left: 0;
234
- width: 100%;
235
- height: 100%;
236
- background: linear-gradient(
237
- to bottom,
238
- rgba(255, 255, 255, 0),
239
- rgba(255, 255, 255, 0.1) 50%,
240
- rgba(255, 255, 255, 0)
241
- );
242
- animation: scan 4s linear infinite;
243
- pointer-events: none;
244
- }}
245
-
246
- @keyframes scan {{
247
- 0% {{ transform: translateY(-100%); }}
248
- 100% {{ transform: translateY(100%); }}
249
- }}
250
-
251
- .particle {{
252
- position: absolute;
253
- width: 3px;
254
- height: 3px;
255
- background: #00ff00;
256
- opacity: 0.5;
257
- animation: flicker 3s infinite;
258
- }}
259
-
260
- @keyframes flicker {{
261
- 0% {{ opacity: 0.5; }}
262
- 50% {{ opacity: 0.1; }}
263
- 100% {{ opacity: 0.5; }}
264
- }}
265
-
266
- main {{
267
- margin-top: 100px;
268
- padding: 2rem;
269
- color: #00ff00;
270
- text-align: center;
271
- max-width: 900px;
272
- margin-left: auto;
273
- margin-right: auto;
274
- }}
275
- </style>
276
- </head>
277
- <body>
278
- <div class="scanline"></div>
279
- <main>
280
- <h1 class="text-4xl md:text-6xl text-neon typewriter glitch">File Uploaded Successfully!</h1>
281
- <p class="text-neon text-2xl mt-4">Redirecting to directory in 3 seconds...</p>
282
- </main>
283
- <script>
284
- // Generate random particles for hacker effect
285
- function createParticles() {{
286
- const numParticles = 30;
287
- for (let i = 0; i < numParticles; i++) {{
288
- const particle = document.createElement('div');
289
- particle.classList.add('particle');
290
- particle.style.left = `${{Math.random() * 100}}vw`;
291
- particle.style.top = `${{Math.random() * 100}}vh`;
292
- particle.style.animationDelay = `${{Math.random() * 3}}s`;
293
- document.body.appendChild(particle);
294
- }}
295
- }}
296
-
297
- // Auto-redirect after 3 seconds
298
- setTimeout(() => {{
299
- window.location.href = "{html.escape(redirect_url)}";
300
- }}, 3000);
301
-
302
- window.onload = createParticles;
303
- </script>
304
- </body>
305
- </html>
306
- '''
307
- encoded = html_content.encode('utf-8', 'surrogateescape')
308
- self.send_response(200)
309
- self.send_header("Content-type", "text/html; charset=utf-8")
310
- self.send_header("Content-Length", str(len(encoded)))
311
- self.end_headers()
312
- self.wfile.write(encoded)
313
- return
314
- self.send_error(400, "No file provided")
315
- return
316
- else:
317
- self.send_error(405, "Method not allowed")
318
-
319
- def list_directory(self, path):
320
- try:
321
- entries = os.listdir(path)
322
- except OSError:
323
- self.send_error(404, "Cannot list directory")
324
- return None
325
-
326
- entries.sort(key=lambda a: a.lower())
327
- displaypath = html.escape(urllib.parse.unquote(self.path))
328
-
329
- # Build list items for directories and files
330
- list_items = []
331
- # Parent directory link if not root
332
- if self.path != '/':
333
- parent = os.path.dirname(self.path.rstrip('/'))
334
- if not parent.endswith('/'):
335
- parent += '/'
336
- list_items.append(f'<li><a href="{html.escape(parent)}" class="text-neon">.. (Parent Directory)</a></li>')
337
-
338
- for name in entries:
339
- fullpath = os.path.join(path, name)
340
- displayname = name + '/' if os.path.isdir(fullpath) else name
341
- href = urllib.parse.quote(name)
342
- if os.path.isdir(fullpath):
343
- href += '/'
344
- # Add download folder zip link for directories
345
- if os.path.isdir(fullpath):
346
- list_items.append(
347
- f'<li>'
348
- f'<a href="{href}" class="text-neon">{html.escape(displayname)}</a> '
349
- f' | <a href="{href}download_folder" class="text-neon">📦 Zip Download</a>'
350
- f'</li>'
351
- )
352
- else:
353
- list_items.append(f'<li><a href="{href}" class="text-neon">{html.escape(displayname)}</a></li>')
354
-
355
- list_html = '\n'.join(list_items)
356
-
357
- html_content = f'''<!DOCTYPE html>
358
- <html lang="en">
359
- <head>
360
- <meta charset="UTF-8" />
361
- <meta name="viewport" content="width=device-width, initial-scale=1" />
362
- <title>PyServeX - Index of {displaypath}</title>
363
- <script src="https://cdn.tailwindcss.com"></script>
364
- <style>
365
- @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
366
-
367
- body {{
368
- font-family: 'VT323', monospace;
369
- background: #000000;
370
- min-height: 100vh;
371
- margin: 0;
372
- overflow-x: hidden;
373
- }}
374
-
375
- header {{
376
- position: fixed;
377
- top: 0;
378
- left: 0;
379
- width: 100%;
380
- padding: 1rem 0;
381
- background: rgba(0, 0,0, 0.9);
382
- box-shadow: 0 2px 5px rgba(0, 255, 0, 0.2);
383
- z-index: 1000;
384
- }}
385
-
386
- .text-neon {{
387
- color: #00ff00;
388
- }}
389
-
390
- .typewriter h1 {{
391
- overflow: hidden;
392
- white-space: nowrap;
393
- animation: typing 3s steps(40, end), blink-caret 0.5s step-end infinite;
394
- margin: 0 auto;
395
- text-align: center;
396
- }}
397
-
398
- @keyframes typing {{
399
- from {{ width: 0; }}
400
- to {{ width: 100%; }}
401
- }}
402
-
403
- @keyframes blink-caret {{
404
- from, to {{ border-right: 2px solid #00ff00; }}
405
- 50% {{ border-right: 2px solid transparent; }}
406
- }}
407
-
408
- .glitch {{
409
- position: relative;
410
- animation: glitch 2s infinite;
411
- }}
412
-
413
- @keyframes glitch {{
414
- 0% {{ transform: translate(0); }}
415
- 10% {{ transform: translate(-2px, 2px); }}
416
- 20% {{ transform: translate(2px, -2px); }}
417
- 30% {{ transform: translate(-2px, 2px); }}
418
- 40% {{ transform: translate(0); }}
419
- 100% {{ transform: translate(0); }}
420
- }}
421
-
422
- .scanline {{
423
- position: absolute;
424
- top: 0;
425
- left: 0;
426
- width: 100%;
427
- height: 100%;
428
- background: linear-gradient(
429
- to bottom,
430
- rgba(255, 255, 255, 0),
431
- rgba(255, 255, 255, 0.1) 50%,
432
- rgba(255, 255, 255, 0)
433
- );
434
- animation: scan 4s linear infinite;
435
- pointer-events: none;
436
- }}
437
-
438
- @keyframes scan {{
439
- 0% {{ transform: translateY(-100%); }}
440
- 100% {{ transform: translateY(100%); }}
441
- }}
442
-
443
- .particle {{
444
- position: absolute;
445
- width: 3px;
446
- height: 3px;
447
- background: #00ff00;
448
- opacity: 0.5;
449
- animation: flicker 3s infinite;
450
- }}
451
-
452
- @keyframes flicker {{
453
- 0% {{ opacity: 0.5; }}
454
- 50% {{ opacity: 0.1; }}
455
- 100% {{ opacity: 0.5; }}
456
- }}
457
-
458
- main {{
459
- margin-top: 100px; /* Adjust based on header height */
460
- padding: 2rem;
461
- color: #00ff00;
462
- text-align: left;
463
- max-width: 900px;
464
- margin-left: auto;
465
- margin-right: auto;
466
- }}
467
-
468
- ul {{
469
- list-style-type: none;
470
- padding-left: 0;
471
- }}
472
-
473
- li {{
474
- margin-bottom: 0.7rem;
475
- font-size: 1.2rem;
476
- }}
477
-
478
- a {{
479
- text-decoration: none;
480
- }}
481
-
482
- a:hover {{
483
- text-decoration: underline;
484
- }}
485
-
486
- .upload-form {{
487
- margin-top: 1.5rem;
488
- padding: 1rem;
489
- border: 1px solid #00ff00;
490
- border-radius: 5px;
491
- }}
492
-
493
- .upload-form label {{
494
- display: block;
495
- margin-bottom: 0.5rem;
496
- }}
497
-
498
- .upload-form input[type="file"] {{
499
- color: #00ff00;
500
- background: #000000;
501
- border: 1px solid #00ff00;
502
- padding: 0.5rem;
503
- }}
504
-
505
- .upload-form button {{
506
- background: #00ff00;
507
- color: #000000;
508
- padding: 0.5rem 1rem;
509
- border: none;
510
- cursor: pointer;
511
- font-family: 'VT323', monospace;
512
- font-size: 1.2rem;
513
- }}
514
-
515
- .upload-form button:hover {{
516
- background: #00cc00;
517
- }}
518
- </style>
519
- </head>
520
- <body>
521
- <div class="scanline"></div>
522
- <header>
523
- <div class="text-center">
524
- <h1 class="text-4xl md:text-6xl text-neon typewriter glitch">PyServeX</h1>
525
- </div>
526
- </header>
527
- <main>
528
- <h2>Index of {displaypath}</h2>
529
- <ul>
530
- {list_html}
531
- </ul>
532
- <div class="upload-form">
533
- <form action="{html.escape(self.path)}upload" method="POST" enctype="multipart/form-data">
534
- <label for="file-upload" class="text-neon">Upload a file:</label>
535
- <input type="file" id="file-upload" name="file" />
536
- <button type="submit">Upload</button>
537
- </form>
538
- </div>
539
- </main>
540
-
541
- <script>
542
- // Generate random particles for hacker effect
543
- function createParticles() {{
544
- const numParticles = 30;
545
- for (let i = 0; i < numParticles; i++) {{
546
- const particle = document.createElement('div');
547
- particle.classList.add('particle');
548
- particle.style.left = `${{Math.random() * 100}}vw`;
549
- particle.style.top = `${{Math.random() * 100}}vh`;
550
- particle.style.animationDelay = `${{Math.random() * 3}}s`;
551
- document.body.appendChild(particle);
552
- }}
553
- }}
554
-
555
- window.onload = createParticles;
556
- </script>
557
- </body>
558
- </html>
559
- '''
560
-
561
- encoded = html_content.encode('utf-8', 'surrogateescape')
562
- self.send_response(200)
563
- self.send_header("Content-type", "text/html; charset=utf-8")
564
- self.send_header("Content-Length", str(len(encoded)))
565
- self.end_headers()
566
- self.wfile.write(encoded)
567
- return
568
-
569
- def get_ip_addresses():
570
- """Retrieve all non-loopback and loopback IPv4 addresses of the system."""
571
- ip_addresses = ["127.0.0.1"] # Explicitly include localhost
572
- try:
573
- # Get all network interfaces, filter for IPv4 (AF_INET)
574
- for interface in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
575
- ip = interface[4][0]
576
- # Filter out link-local (169.254.x.x) but keep 127.x.x.x
577
- if not ip.startswith("169.254.") and ip not in ip_addresses:
578
- ip_addresses.append(ip)
579
- return ip_addresses if ip_addresses else ["127.0.0.1", "No other IPv4 addresses found"]
580
- except socket.gaierror:
581
- return ["127.0.0.1", "Unable to resolve hostname"]
582
-
583
- def run(base_dir):
584
- """Run the HTTP server with the specified base directory."""
585
- class Handler(FileRequestHandler):
586
- def __init__(self, *args, **kwargs):
587
- self.base_dir = base_dir
588
- super().__init__(*args, **kwargs)
589
-
590
- # Print IP addresses before starting the server
591
- print("System IPv4 addresses (including localhost):")
592
- for ip in get_ip_addresses():
593
- print(f" http://{ip}:{PORT}")
594
-
595
- server = None
596
-
597
- try:
598
- server = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler)
599
- print(f"Serving at http://0.0.0.0:{PORT} (accessible from network and localhost)")
600
-
601
- def shutdown_handler(signum, frame):
602
- print("\nShutting down server...")
603
- if server:
604
- # Run shutdown in a separate thread to avoid blocking
605
- threading.Thread(target=server.shutdown, daemon=True).start()
606
- server.server_close()
607
- sys.exit(0)
608
-
609
- # Register signal handler for SIGINT (Ctrl+C)
610
- signal.signal(signal.SIGINT, shutdown_handler)
611
-
612
- # Start the server
613
- server.serve_forever()
614
-
615
- except KeyboardInterrupt:
616
- # Handle Ctrl+C explicitly to ensure clean shutdown
617
- if server:
618
- print("\nShutting down server...")
619
- server.shutdown()
620
- server.server_close()
621
- sys.exit(0)
622
- except Exception as e:
623
- print(f"Server error: {e}")
624
- if server:
625
- server.server_close()
626
- sys.exit(1)
627
-
628
- def main():
629
- """Main entry point for the command-line tool."""
630
- parser = argparse.ArgumentParser(description="PyServeX: A simple HTTP server for file sharing.")
631
- parser.add_argument('--version', action='version', version='PyServeX 1.0.1')
632
- args = parser.parse_args()
633
-
634
- # Get the shared folder
635
- base_dir = get_shared_folder()
636
- run(base_dir)
637
-
638
- if __name__ == "__main__":
639
- main()
1
+ #!/usr/bin/env python3
2
+ # Improved Python HTTP Server Developed by Subz3r0x01
3
+ # GitHub: https://github.com/SubZ3r0-0x01
4
+
5
+ import os
6
+ import socketserver
7
+ import threading
8
+ import signal
9
+ import sys
10
+ import logging
11
+ import socket
12
+ import json
13
+ import argparse
14
+ import qrcode
15
+ from . import request_handler
16
+
17
+ # Configure logging for debugging
18
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
19
+
20
+ PORT = 8088
21
+ CONFIG_FILE = os.path.expanduser("~/.pyservx_config.json") # Store config in user's home directory
22
+
23
+ def load_config():
24
+ """Load shared folder path from config file if it exists."""
25
+ if os.path.exists(CONFIG_FILE):
26
+ try:
27
+ with open(CONFIG_FILE, 'r') as f:
28
+ config = json.load(f)
29
+ return config.get("shared_folder")
30
+ except json.JSONDecodeError:
31
+ logging.warning("Invalid config file. Ignoring.")
32
+ return None
33
+
34
+ def save_config(folder_path):
35
+ """Save shared folder path to config file."""
36
+ try:
37
+ os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
38
+ with open(CONFIG_FILE, 'w') as f:
39
+ json.dump({"shared_folder": folder_path}, f)
40
+ except OSError as e:
41
+ logging.error(f"Failed to save config: {e}")
42
+
43
+ def get_shared_folder():
44
+ """Prompt user for shared folder path or load from config."""
45
+ saved_folder = load_config()
46
+ if saved_folder and os.path.isdir(saved_folder):
47
+ print(f"Using saved shared folder: {saved_folder}")
48
+ return os.path.abspath(saved_folder)
49
+
50
+ while True:
51
+ folder_path = input("Enter the path to the shared folder: ").strip()
52
+ if os.path.isdir(folder_path):
53
+ break
54
+ print("Invalid folder path. Please enter a valid directory.")
55
+
56
+ while True:
57
+ persist = input("Do you want this choice to be persistent? (y/n): ").strip().lower()
58
+ if persist in ('y', 'n'):
59
+ break
60
+ print("Please enter 'y' or 'n'.")
61
+
62
+ folder_path = os.path.abspath(folder_path)
63
+ if persist == 'y':
64
+ save_config(folder_path)
65
+ print(f"Shared folder saved for future use: {folder_path}")
66
+ else:
67
+ print("Shared folder will be prompted again next time.")
68
+
69
+ return folder_path
70
+
71
+ def get_ip_addresses():
72
+ """Retrieve all non-loopback and loopback IPv4 addresses of the system."""
73
+ ip_addresses = ["127.0.0.1"] # Explicitly include localhost
74
+ try:
75
+ # Get all network interfaces, filter for IPv4 (AF_INET)
76
+ for interface in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
77
+ ip = interface[4][0]
78
+ # Filter out link-local (169.254.x.x) but keep 127.x.x.x
79
+ if not ip.startswith("169.254.") and ip not in ip_addresses:
80
+ ip_addresses.append(ip)
81
+ return ip_addresses if ip_addresses else ["127.0.0.1", "No other IPv4 addresses found"]
82
+ except socket.gaierror:
83
+ return ["127.0.0.1", "Unable to resolve hostname"]
84
+
85
+ def run(base_dir, no_qr=False):
86
+ """Run the HTTP server with the specified base directory."""
87
+ class Handler(request_handler.FileRequestHandler):
88
+ def __init__(self, *args, **kwargs):
89
+ self.base_dir = base_dir
90
+ super().__init__(*args, **kwargs)
91
+
92
+ # Create robots.txt if it doesn't exist
93
+ robots_txt_path = os.path.join(base_dir, "robots.txt")
94
+ if not os.path.exists(robots_txt_path):
95
+ with open(robots_txt_path, "w") as f:
96
+ f.write("User-agent: *\nDisallow: /\n")
97
+
98
+ if not no_qr:
99
+ # Print IP addresses before starting the server
100
+ print("System IPv4 addresses (including localhost):")
101
+ for ip in get_ip_addresses():
102
+ print(f" http://{ip}:{PORT}")
103
+ qr = qrcode.QRCode(
104
+ version=1,
105
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
106
+ box_size=10,
107
+ border=4,
108
+ )
109
+ qr.add_data(f"http://{ip}:{PORT}")
110
+ qr.make(fit=True)
111
+ try:
112
+ qr.print_tty()
113
+ except OSError:
114
+ print("Not a TTY. Cannot print QR code.")
115
+
116
+ server = None
117
+
118
+ try:
119
+ server = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler)
120
+ print(f"Serving at http://0.0.0.0:{PORT} (accessible from network and localhost)")
121
+
122
+ def shutdown_handler(signum, frame):
123
+ print("\nShutting down server...")
124
+ if server:
125
+ # Run shutdown in a separate thread to avoid blocking
126
+ threading.Thread(target=server.shutdown, daemon=True).start()
127
+ server.server_close()
128
+ sys.exit(0)
129
+
130
+ # Register signal handler for SIGINT (Ctrl+C)
131
+ signal.signal(signal.SIGINT, shutdown_handler)
132
+
133
+ # Start the server
134
+ server.serve_forever()
135
+
136
+ except KeyboardInterrupt:
137
+ # Handle Ctrl+C explicitly to ensure clean shutdown
138
+ if server:
139
+ print("\nShutting down server...")
140
+ server.shutdown()
141
+ server.server_close()
142
+ sys.exit(0)
143
+ except Exception as e:
144
+ print(f"Server error: {e}")
145
+ if server:
146
+ server.server_close()
147
+ sys.exit(1)
148
+
149
+ def main():
150
+ """Main entry point for the command-line tool."""
151
+ parser = argparse.ArgumentParser(description="PyServeX: A simple HTTP server for file sharing.")
152
+ parser.add_argument('--version', action='version', version='PyServeX 1.0.1')
153
+ args = parser.parse_args()
154
+
155
+ # Get the shared folder
156
+ base_dir = get_shared_folder()
157
+ run(base_dir)
158
+
159
+ if __name__ == "__main__":
160
+ main()