pyutilscripts 0.8.0__tar.gz → 0.9.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.
- {pyutilscripts-0.8.0/pyutilscripts.egg-info → pyutilscripts-0.9.0}/PKG-INFO +1 -1
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyproject.toml +2 -1
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts/__init__.py +1 -1
- pyutilscripts-0.9.0/pyutilscripts/httpd.py +105 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0/pyutilscripts.egg-info}/PKG-INFO +1 -1
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts.egg-info/SOURCES.txt +1 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts.egg-info/entry_points.txt +1 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/LICENSE +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/README.md +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts/fcopy.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts/forward_tcp.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts/prunedirs.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts/toast.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts/utils/__init__.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts.egg-info/dependency_links.txt +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts.egg-info/requires.txt +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/pyutilscripts.egg-info/top_level.txt +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/setup.cfg +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/tests/test_action_parser.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/tests/test_fcopy.py +0 -0
- {pyutilscripts-0.8.0 → pyutilscripts-0.9.0}/tests/test_fcopy_cli.py +0 -0
|
@@ -46,4 +46,5 @@ Issues = "https://github.com/ZeroKwok/pyutilscripts/issues"
|
|
|
46
46
|
[project.scripts]
|
|
47
47
|
"fcopy" = "pyutilscripts.fcopy:main"
|
|
48
48
|
"prunedirs" = "pyutilscripts.prunedirs:main"
|
|
49
|
-
"forward.tcp" = "pyutilscripts.forward_tcp:main"
|
|
49
|
+
"forward.tcp" = "pyutilscripts.forward_tcp:main"
|
|
50
|
+
"httpd" = "pyutilscripts.httpd:main"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
9
|
+
from socketserver import ThreadingMixIn
|
|
10
|
+
|
|
11
|
+
class RateLimitHandler(SimpleHTTPRequestHandler):
|
|
12
|
+
# Default value
|
|
13
|
+
SPEED_LIMIT = -1
|
|
14
|
+
|
|
15
|
+
def copyfile(self, source, outputfile):
|
|
16
|
+
"""Override copyfile to implement rate limiting. If unlimited, use the original efficient method."""
|
|
17
|
+
# If unlimited, call parent class method (which uses shutil.copyfileobj internally and is more efficient)
|
|
18
|
+
if self.SPEED_LIMIT <= 0:
|
|
19
|
+
return super().copyfile(source, outputfile)
|
|
20
|
+
|
|
21
|
+
chunk_size = 1024 * 64 # 64KB chunk size
|
|
22
|
+
try:
|
|
23
|
+
start_time = time.time()
|
|
24
|
+
bytes_sent = 0
|
|
25
|
+
while True:
|
|
26
|
+
data = source.read(chunk_size)
|
|
27
|
+
if not data:
|
|
28
|
+
break
|
|
29
|
+
outputfile.write(data)
|
|
30
|
+
bytes_sent += len(data)
|
|
31
|
+
|
|
32
|
+
# Rate limiting calculation
|
|
33
|
+
elapsed = time.time() - start_time
|
|
34
|
+
expected_time = bytes_sent / self.SPEED_LIMIT
|
|
35
|
+
|
|
36
|
+
if elapsed < expected_time:
|
|
37
|
+
# 最小睡眠时间设为 0.5 秒, 因此理论最低速度约为 0.5 * 64kb = 128KB/s
|
|
38
|
+
time.sleep(min(expected_time - elapsed, 0.5))
|
|
39
|
+
except (ConnectionResetError, BrokenPipeError):
|
|
40
|
+
print(f"\n[!] Client {self.client_address[0]} disconnected")
|
|
41
|
+
|
|
42
|
+
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
43
|
+
daemon_threads = True
|
|
44
|
+
|
|
45
|
+
def parse_speed(speed_str):
|
|
46
|
+
"""Parse speed string; support negative for unlimited."""
|
|
47
|
+
s = speed_str.upper()
|
|
48
|
+
try:
|
|
49
|
+
# Handle unlimited cases
|
|
50
|
+
if s == '-1' or s == '0' or s == 'UNLIMITED':
|
|
51
|
+
return -1
|
|
52
|
+
|
|
53
|
+
if s.endswith('MB'):
|
|
54
|
+
return int(float(s[:-2]) * 1024 * 1024)
|
|
55
|
+
elif s.endswith('KB'):
|
|
56
|
+
return int(float(s[:-2]) * 1024)
|
|
57
|
+
else:
|
|
58
|
+
return int(s)
|
|
59
|
+
except ValueError:
|
|
60
|
+
raise argparse.ArgumentTypeError("Invalid speed format. Use '1MB', '500KB', '-1' (unlimited), or a byte value.")
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
parser = argparse.ArgumentParser(description="Multithreaded HTTP file server with rate limiting")
|
|
64
|
+
parser.add_argument('--port', '-p', type=int, default=8000, help='Port to listen on (default: 8000)')
|
|
65
|
+
parser.add_argument('--bind', '-b', default='0.0.0.0', help='Bind address (default: 0.0.0.0)')
|
|
66
|
+
parser.add_argument('--limit', '-l', default='-1', help="Rate limit (e.g., 1MB, 500KB). Use -1 for unlimited")
|
|
67
|
+
parser.add_argument('--dir', '-d', default='.', help='Root directory to serve (default: current directory)')
|
|
68
|
+
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
|
|
71
|
+
# 1. Verify directory exists
|
|
72
|
+
if not os.path.isdir(args.dir):
|
|
73
|
+
print(f"[ERROR] Directory does not exist: {args.dir}")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
# 2. Change to and record absolute path
|
|
77
|
+
abs_path = os.path.abspath(args.dir)
|
|
78
|
+
os.chdir(abs_path)
|
|
79
|
+
|
|
80
|
+
# 3. Parse rate limit
|
|
81
|
+
limit_bps = parse_speed(args.limit)
|
|
82
|
+
limit_display = f"{args.limit}/s ({limit_bps})" if limit_bps > 0 else "Unlimited"
|
|
83
|
+
|
|
84
|
+
# 4. Dynamic handler class
|
|
85
|
+
HandlerClass = type('CustomHandler', (RateLimitHandler,), {'SPEED_LIMIT': limit_bps})
|
|
86
|
+
|
|
87
|
+
server_address = (args.bind, args.port)
|
|
88
|
+
httpd = ThreadedHTTPServer(server_address, HandlerClass)
|
|
89
|
+
|
|
90
|
+
print(f"{'='*45}")
|
|
91
|
+
print(f"[*] HTTP server started")
|
|
92
|
+
print(f"[*] Listening on: http://{args.bind}:{args.port}")
|
|
93
|
+
print(f"[*] Serving directory: {abs_path}")
|
|
94
|
+
print(f"[*] Speed limit: {limit_display}")
|
|
95
|
+
print(f"{'='*45}")
|
|
96
|
+
print("[!] Press Ctrl+C to stop the server\n")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
httpd.serve_forever()
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
print("\n[!] Shutting down server...")
|
|
102
|
+
httpd.server_close()
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|