diff --git a/ServerReference/simpleserver.py b/ServerReference/simpleserver.py
index b2c910f..a66ff4c 100644
--- a/ServerReference/simpleserver.py
+++ b/ServerReference/simpleserver.py
@@ -1,12 +1,14 @@
import http.server
import mimetypes
import os
-import urllib.parse
import pathlib
import random
import socketserver
import sys
+import threading
import types
+import urllib.parse
+import zipstream
# pip install voussoirkit
from voussoirkit import bytestring
@@ -16,86 +18,21 @@ from voussoirkit import ratelimiter
FILE_READ_CHUNK = bytestring.MIBIBYTE
RATELIMITER = ratelimiter.Ratelimiter(16 * bytestring.MIBIBYTE)
-# The paths which the user may access.
-# Attempting to access anything outside will 403.
-# These are convered to Path objects after that class definition.
-OKAY_PATHS = set(['files', 'favicon.ico'])
-
OPENDIR_TEMPLATE = '''
+
-
-{entries}
+
'''
-class Path(pathclass.Path):
- '''
- Add some server-specific abilities to the Pathclass
- '''
- def __init__(self, path):
- path = urllib.parse.unquote(path)
- path = path.strip('/')
- pathclass.Path.__init__(self, path)
-
- @property
- def allowed(self):
- return any(self in okay for okay in OKAY_PATHS)
-
- def anchor(self, display_name=None):
- self.correct_case()
- if display_name is None:
- display_name = self.basename
-
- if self.is_dir:
- # Folder emoji
- icon = '\U0001F4C1'
- else:
- # Diamond emoji, because there's not one for files.
- icon = '\U0001F48E'
-
- #print('anchor', path)
- if display_name.endswith('.placeholder'):
- a = '{icon} {display}'
- else:
- a = '{icon} {display}'
- a = a.format(
- full=self.url_path,
- icon=icon,
- display=display_name,
- )
- return a
-
- def table_row(self, display_name=None, shaded=False):
- form = '{anchor} | {size} |
'
- size = self.size
- if size is None:
- size = ''
- else:
- size = bytestring.bytestring(size)
-
- bg = 'ddd' if shaded else 'fff';
- row = form.format(
- bg=bg,
- anchor=self.anchor(display_name=display_name),
- size=size,
- )
- return row
-
- @property
- def url_path(self):
- url = self.relative_path
- url = url.replace(os.sep, '/')
- url = '/' + url
- url = urllib.parse.quote(url)
- return url
-
-OKAY_PATHS = set(Path(p) for p in OKAY_PATHS)
+ROOT_DIRECTORY = pathclass.Path(os.getcwd())
class RequestHandler(http.server.BaseHTTPRequestHandler):
def write(self, data):
@@ -108,45 +45,8 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
else:
self.wfile.write(data)
- def read_filebytes(self, path, range_min=None, range_max=None):
- #print(path)
-
- if path.is_file:
- if range_min is None:
- range_min = 0
-
- if range_max is None:
- range_max = path.size
-
- range_span = range_max - range_min
-
- #print('read span', range_min, range_max, range_span)
- f = open(path.absolute_path, 'rb')
- f.seek(range_min)
- sent_amount = 0
- while sent_amount < range_span:
- chunk = f.read(FILE_READ_CHUNK)
- if len(chunk) == 0:
- break
-
- yield chunk
- sent_amount += len(chunk)
-
- #print('I read', len(fr))
- f.close()
-
- elif path.is_dir:
- text = generate_opendir(path)
- text = text.encode('utf-8')
- yield text
-
- else:
- self.send_error(404)
- yield bytes()
-
def do_GET(self):
- #print(dir(self))
- path = Path(self.path)
+ path = url_to_path(self.path)
if self.send_path_validation_error(path):
return
@@ -196,22 +96,38 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
headers['Content-length'] = content_length
- mime = mimetypes.guess_type(path.absolute_path)[0]
- if mime is not None:
- #print(mime)
- headers['Content-type'] = mime
+ if path.is_file:
+ headers['Content-type'] = mimetypes.guess_type(path.absolute_path)[0]
+ response = read_filebytes(path, range_min=range_min, range_max=range_max)
+
+ elif path.is_dir:
+ headers['Content-type'] = 'text/html'
+ response = generate_opendir(path)
+ response = response.encode('utf-8')
+
+ elif self.path.endswith('.zip'):
+ path = url_to_path(self.path.rsplit('.zip', 1)[0])
+ headers['Content-type'] = 'application/octet-stream'
+ headers['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{path.basename}.zip'
+ response = zip_directory(path)
+ response = iter(response)
+ # response = (print(chunk) or chunk for chunk in response)
+
+ else:
+ status_code = 404
+ self.send_error(status_code)
+ response = bytes()
self.send_response(status_code)
for (key, value) in headers.items():
self.send_header(key, value)
- d = self.read_filebytes(path, range_min=range_min, range_max=range_max)
- #print('write')
self.end_headers()
- self.write(d)
+ self.write(response)
+ return
def do_HEAD(self):
- path = Path(self.path)
+ path = url_to_path(self.path)
if self.send_path_validation_error(path):
return
@@ -230,55 +146,90 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
self.end_headers()
def send_path_validation_error(self, path):
- if not path.allowed:
+ if not allowed(path):
self.send_error(403, 'Stop that!')
return True
return False
-class ThreadedServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
- '''
- Thanks root and twasbrillig http://stackoverflow.com/a/14089457
- '''
- pass
+# class ThreadedServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
+# '''
+# Thanks root and twasbrillig http://stackoverflow.com/a/14089457
+# '''
+# pass
+def allowed(path):
+ return path in ROOT_DIRECTORY
+
+def anchor(path, display_name=None):
+ path.correct_case()
+ if display_name is None:
+ display_name = path.basename
+
+ if path.is_dir:
+ # Folder emoji
+ icon = '\U0001F4C1'
+ else:
+ # Diamond emoji
+ #icon = '\U0001F48E'
+ icon = '\U0001F381'
+
+ #print('anchor', path)
+ if display_name.endswith('.placeholder'):
+ a = '{icon} {display}'
+ else:
+ a = '{icon} {display}'
+ a = a.format(
+ full=path_to_url(path),
+ icon=icon,
+ display=display_name,
+ )
+ return a
def generate_opendir(path):
#print('Listdir:', path)
- items = os.listdir(path.absolute_path)
- items = [os.path.join(path.absolute_path, f) for f in items]
- #print(items)
# This places directories above files, each ordered alphabetically
- items.sort(key=str.lower)
+ try:
+ items = path.listdir()
+ except FileNotFoundError:
+ items = []
+
directories = []
files = []
- for item in items:
- if os.path.isdir(item):
+
+ for item in sorted(items, key=lambda p: p.basename.lower()):
+ if item.is_dir:
directories.append(item)
else:
files.append(item)
items = directories + files
- items = [Path(f) for f in items]
- entries = []
+ table_rows = []
- if any(path.absolute_path == okay.absolute_path for okay in OKAY_PATHS):
+ shaded = False
+
+ if path.absolute_path == ROOT_DIRECTORY.absolute_path:
# This is different than a permission check, we're seeing if they're
# actually at the top, in which case they don't need an up button.
pass
else:
- entry = path.parent.table_row(display_name='up')
- entries.append(entry)
-
- shaded = True
- for item in items:
- entry = item.table_row(shaded=shaded)
- entries.append(entry)
+ entry = table_row(path.parent, display_name='up', shaded=shaded)
+ table_rows.append(entry)
shaded = not shaded
- entries = '\n'.join(entries)
- text = OPENDIR_TEMPLATE.format(entries=entries)
+ for item in items:
+ entry = table_row(item, shaded=shaded)
+ table_rows.append(entry)
+ shaded = not shaded
+
+ if len(items) > 0:
+ entry = table_row(path.replace_extension('.zip'), display_name='zip', shaded=shaded)
+ shaded = not shaded
+ table_rows.append(entry)
+
+ table_rows = '\n'.join(table_rows)
+ text = OPENDIR_TEMPLATE.format(table_rows=table_rows)
return text
def generate_random_filename(original_filename='', length=8):
@@ -288,10 +239,87 @@ def generate_random_filename(original_filename='', length=8):
identifier = '{:x}'.format(bits).rjust(length, '0')
return identifier
+def read_filebytes(path, range_min=None, range_max=None):
+ #print(path)
+ if range_min is None:
+ range_min = 0
+
+ if range_max is None:
+ range_max = path.size
+
+ range_span = range_max - range_min
+
+ #print('read span', range_min, range_max, range_span)
+ f = open(path.absolute_path, 'rb')
+ f.seek(range_min)
+ sent_amount = 0
+ while sent_amount < range_span:
+ chunk = f.read(FILE_READ_CHUNK)
+ if len(chunk) == 0:
+ break
+
+ yield chunk
+ sent_amount += len(chunk)
+
+ #print('I read', len(fr))
+ f.close()
+
+def table_row(path, display_name=None, shaded=False):
+ form = '{anchor} | {size} |
'
+ size = path.size
+ if size is None:
+ size = ''
+ else:
+ size = bytestring.bytestring(size)
+
+ bg = 'ddd' if shaded else 'fff';
+ row = form.format(
+ bg=bg,
+ anchor=anchor(path, display_name=display_name),
+ size=size,
+ )
+ return row
+
+def path_to_url(path):
+ url = path.relative_path[2:]
+ url = url.replace(os.sep, '/')
+ url = '/' + url
+ url = urllib.parse.quote(url)
+ return url
+
+def url_to_path(path):
+ path = urllib.parse.unquote(path)
+ path = path.strip('/')
+ return pathclass.Path(path)
+
+def zip_directory(path):
+ zipfile = zipstream.ZipFile(mode='w', compression=zipstream.ZIP_STORED)
+
+ for item in path.walk():
+ if item.is_dir:
+ continue
+ arcname = item.relative_to(path).lstrip('.' + os.sep)
+ zipfile.write(filename=item.absolute_path, arcname=arcname)
+
+ return zipfile
+
def main():
- server = ThreadedServer(('', int(sys.argv[1] or 32768)), RequestHandler)
- print('server starting')
- server.serve_forever()
+ try:
+ port = int(sys.argv[1])
+ except IndexError:
+ port = 40000
+ server = http.server.ThreadingHTTPServer(('', port), RequestHandler)
+ print(f'server starting on {port}')
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ print('goodbye.')
+ t = threading.Thread(target=server.shutdown)
+ t.daemon = True
+ t.start()
+ server.shutdown()
+ print('really goodbye.')
+ return 0
if __name__ == '__main__':
main()