-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMiniFileServer.py
More file actions
executable file
·241 lines (190 loc) · 7.71 KB
/
MiniFileServer.py
File metadata and controls
executable file
·241 lines (190 loc) · 7.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env python3
import argparse
import os
import re
import sys
import socket
import ssl
import urllib
from pathlib import Path
# Python3 ONLY: will now crash with Python 2.x
try:
import http.server as SimpleHTTPServer
from http.server import HTTPServer, SimpleHTTPRequestHandler, test
except ImportError:
# Python 2
print('ERROR: Requires Python 3.6+')
sys.exit(1)
current_dir = os.getcwd()
package_dir = os.path.dirname(__file__)
pattern = re.compile('.png|.jpg|.jpeg|.js|.css|.ico|.gif|.svg|.woff|.ttf|.woff2|.eot', re.IGNORECASE)
SPA_MODE = False
def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16*1024):
'''Like shutil.copyfileobj, but only copy a range of the streams.
Both start and stop are inclusive.
'''
if start is not None: infile.seek(start)
while 1:
to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize)
buf = infile.read(to_read)
if not buf:
break
outfile.write(buf)
BYTE_RANGE_RE = re.compile(r'bytes=(\d+)-(\d+)?$')
def parse_byte_range(byte_range):
'''Returns the two numbers in 'bytes=123-456' or throws ValueError.
The last number or both numbers may be None.
'''
if byte_range.strip() == '':
return None, None
m = BYTE_RANGE_RE.match(byte_range)
if not m:
raise ValueError('Invalid byte range %s' % byte_range)
first, last = [x and int(x) for x in m.groups()]
if last and last < first:
raise ValueError('Invalid byte range %s' % byte_range)
return first, last
class RangeRequestHandler(SimpleHTTPRequestHandler):
"""Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler
The approach is to:
- Override send_head to look for 'Range' and respond appropriately.
- Override copyfile to only transmit a range when requested.
"""
def send_head(self):
if 'Range' not in self.headers:
self.range = None
return SimpleHTTPRequestHandler.send_head(self)
try:
self.range = parse_byte_range(self.headers['Range'])
except ValueError as e:
self.send_error(400, 'Invalid byte range')
return None
first, last = self.range
# Mirroring SimpleHTTPServer.py here
path = self.translate_path(self.path)
f = None
ctype = self.guess_type(path)
try:
f = open(path, 'rb')
except IOError:
self.send_error(404, 'File not found')
return None
fs = os.fstat(f.fileno())
file_len = fs[6]
if first >= file_len:
self.send_error(416, 'Requested Range Not Satisfiable')
return None
self.send_response(206)
self.send_header('Content-type', ctype)
if last is None or last >= file_len:
last = file_len - 1
response_length = last - first + 1
self.send_header('Content-Range', 'bytes %s-%s/%s' % (first, last, file_len))
self.send_header('Content-Length', str(response_length))
self.send_header('Last-Modified', self.date_time_string(fs.st_mtime))
self.end_headers()
return f
def copyfile(self, source, outputfile):
if not self.range:
return SimpleHTTPRequestHandler.copyfile(self, source, outputfile)
# SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let
# you stop the copying before the end of the file.
start, stop = self.range # set in send_head()
copy_byte_range(source, outputfile, start, stop)
def end_headers(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Accept-Ranges,Range,*")
self.send_header("Access-Control-Allow-Private-Network", "true")
self.send_header("Access-Control-Expose-Headers", "*")
self.send_header("Access-Control-Max-Age", "0")
self.send_header("Access-Control-Allow-Methods", "GET,OPTIONS,HEAD")
self.send_header('Accept-Ranges', 'bytes')
self.send_header('Mini-File-Server-Root', current_dir)
self.send_header("Cache-Control", "no-cache, max-age=0, must-revalidate")
SimpleHTTPRequestHandler.end_headers(self)
# experimenting with no-cache but must-revalidate: which means
# the browser MUST ping us to see if there is a new copy of every
# file, but if it has the latest file then it can use a cached version
# instead of transferring it again.
# Former settings no caching EVER: "no-cache, max-age=0, must-revalidate, no-store"
def do_OPTIONS(self):
self.send_response(200)
self.end_headers()
return None
def do_GET(self):
print('\nREQUEST', self.directory, ':::', self.path)
# If we are not in SPA_MODE the just serve up the files
if not SPA_MODE:
return super().do_GET()
# -------------------------------------------------------
# SPA_MODE: We are -both- a file server and an app server
# File server
if self.path.startswith('/_f_/'):
self.path = self.path[4:]
print(' FILES', self.directory, ':::', self.path)
return super().do_GET()
# Single Page App - path shenanigans
url_parts = urllib.parse.urlparse(self.path)
request_file_path = Path(url_parts.path.strip("/"))
ext = request_file_path.suffix
if not request_file_path.is_file() and not pattern.match(ext):
self.path = 'index.html'
self.directory = package_dir + '/static'
print(' SITE', self.directory, self.path)
return super().do_GET()
def find_free_port(port):
for i in range(port,port+49):
s = socket.socket()
try:
s.bind(('', i))
s.close()
return i
except:
pass
finally:
s.close()
def run_mini_file_server(maybe_port, cert, key):
port = find_free_port(maybe_port)
print("\n-----------------------------------------------------------------")
print("SimWrapper file server: port", port)
if cert and key: print("Using HTTPS with PEM cert/key")
print(current_dir)
print("-----------------------------------------------------------------\n")
httpd = HTTPServer(('', port), RangeRequestHandler)
if cert and key: httpd.socket = ssl.wrap_socket(
httpd.socket,
certfile=cert,
keyfile=key,
server_side=True
)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
def serve_entire_website(port):
print("\n-----------------------------------------------------------------")
print("Start SimWrapper website on PORT:", port)
# Build the full URL for this site, including the free port number
if port == 8050:
print("\nBrowse: http://localhost:" + str(port) + "/live")
print(" Or: " + "http://" + socket.gethostname() + ":" + str(port) + "/live")
else:
print("\nTry: http://localhost:" + str(port) + "/" + str(port))
print("And: " + "http://" + socket.gethostname() + ":" + str(port) + "/" + str(port))
print("-----------------------------------------------------------------")
print("Serving files and folders in:")
print(current_dir)
print("-----------------------------------------------------------------\n")
httpd = HTTPServer(('', port), RangeRequestHandler)
# SPA_MODE handles the case where we are serving
# website internals -and- file contents from one URL.
global SPA_MODE
SPA_MODE = True
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
if __name__ == '__main__':
run_mini_file_server(8000)