diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f54110f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,201 @@
+
+
+
+
+
+ ImageServer
+
+
+
+
+
+
+
+
+
+ 加载更多
+
+
+
+
+
diff --git a/network.py b/network.py
new file mode 100644
index 0000000..fec2c76
--- /dev/null
+++ b/network.py
@@ -0,0 +1,251 @@
+import socket
+import threading
+import traceback
+from http import HTTPStatus
+
+def parse_address(address):
+ try:
+ ip = address.split(':')[0]
+ port = int(address.split(':')[1])
+ return ip, port
+ except:
+ print('Invalid address [{0}]').format(address)
+ print('exception information:')
+ print(traceback.format_exc())
+ raise ValueError
+
+class BasicTCPServer():
+ def __init__(self, address="127.0.0.1:12345", handler=None):
+ self.ip, self.port = parse_address(address)
+
+ self.handler = handler if handler is not None else lambda clientsocket, addr:None
+
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket.bind((self.ip, self.port))
+ self.socket.listen(5)
+ self.socket.settimeout(0.5)
+ self.terminate = False
+
+ def __del__(self):
+ self.stop()
+
+ def set_handler(self, handler):
+ self.handler = handler
+
+ def handle_message(self, clientsocket, addr):
+ self.handler(clientsocket, addr)
+ def loop(self):
+ try:
+ while not self.terminate:
+ try:
+ clientsocket,addr = self.socket.accept()
+ t = threading.Thread(
+ target=self.handle_message,
+ args=[clientsocket, addr],
+ name='Client[{0}]'.format(addr),
+ daemon=True
+ )
+ t.start()
+ except socket.timeout:
+ pass
+ except socket.timeout:
+ pass
+ except (Exception, KeyboardInterrupt):
+ self.socket.close()
+ print('Bye~')
+
+ def start(self, back=True):
+ if back:
+ t = threading.Thread(target=self.loop, name='SocketMainLoop', daemon=True)
+ t.start()
+ else:
+ self.loop()
+
+ def stop(self):
+ self.terminate = True
+ self.socket.close()
+
+class HTTPBasicHeader():
+ def __init__(self, words=None, content=None):
+ if content is None:
+ content = {}
+ self.words = words
+ self.content = content
+
+ def encode(self):
+ contents = [' '.join([str(w) for w in self.words])]
+ contents += ['{0}: {1}'.format(name, value) for name, value in self.content.items()]
+ header_message = '\r\n'.join(contents)
+ header_message = header_message.encode('utf-8')
+ return header_message
+
+ def decode(self, message):
+ if type(message) is bytes:
+ message = message.decode('utf-8')
+ contents = message.split('\r\n')
+ contents = [line.strip() for line in contents]
+ contents = [line for line in contents if line != '']
+ header_line = contents[0]
+ contents = contents[1:]
+ words = header_line.split(' ')
+ valid_contents = {}
+ invalid_lines = []
+ for line in contents:
+ delpos = line.find(':')
+ if delpos == -1:
+ invalid_lines.append(line)
+ else:
+ key = line[:delpos].strip()
+ value = line[delpos+1:].strip()
+ valid_contents[key] = value
+ if len(invalid_lines) > 0:
+ print('Warning: in-completed line found:')
+ print(invalid_lines)
+ return words, valid_contents
+
+class InvalidHTTPHeaderError(Exception):
+ def __init__(self, message=None):
+ pass
+
+class HTTPHeaderDictInterface():
+ def __init__(self, content):
+ self.content = content
+ def __getitem__(self, index):
+ return self.content[index]
+ def __setitem__(self, index, value):
+ self.content[index] = value
+ def __contains__(self, index):
+ return index in self.content
+ def __iter__(self, index):
+ for key in self.content:
+ yield key
+
+class HTTPRequestHeader(HTTPHeaderDictInterface):
+ methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']
+ def __init__(self, method=None, url=None, version='HTTP/1.1', content=None):
+ if content is None:
+ content = {}
+ self.method = method
+ self.url = url
+ self.version = version
+ self.content = content
+
+ def check_valid(self):
+ if self.method is None or self.url is None:
+ return False
+ elif self.method not in HTTPRequestHeader.methods:
+ return False
+ else:
+ return True
+
+ def encode(self):
+ if not self.check_valid():
+ raise InvalidHTTPHeaderError('Invalid header, method and url should at least be provided.')
+ words = [self.method, self.url, self.version]
+ content = self.content
+ message = HTTPBasicHeader(words=words, content=content).encode()
+ return message
+ def decode(self, message):
+ words, content = HTTPBasicHeader().decode(message)
+ if len(words) != 3:
+ raise InvalidHTTPHeaderError
+ self.method, self.url, self.version = words
+ self.content = content
+
+class HTTPResponseHeader(HTTPHeaderDictInterface):
+ def __init__(self, code=None, version='HTTP/1.1', content=None):
+ if content is None:
+ content = {}
+ self.code = code
+ self.version = version
+ self.content = content
+
+ def check_valid(self):
+ if self.code is None:
+ return False
+ else:
+ return True
+
+ def encode(self):
+ if not self.check_valid():
+ raise InvalidHTTPHeaderError('Invalid header, code is required for a http header.')
+ words = [self.version, self.code, HTTPStatus(self.code).phrase]
+ content = self.content
+ message = HTTPBasicHeader(words=words, content=content).encode()
+ return message
+ def decode(self, message):
+ words, content = HTTPBasicHeader().decode(message)
+ if len(words) != 3:
+ raise InvalidHTTPHeaderError
+ self.version, self.code, _ = words
+ self.content = content
+
+class SingleHTTPConnection():
+ def __init__(self, header, cached, connection):
+ self.header = header
+ self.connection = connection # connection is a basic socket connection.
+ self.cached = cached
+ self.length = 0
+ if 'Content-Length' in self.header:
+ self.length = self.header['Content-Length'] # the remeaning legth of the connection.
+
+ # To ensure all data is send, so we use sendall here.
+ def write(self, message):
+ self.connection.sendall(message)
+
+ # this function will read fixed length from the socket.
+ def read_fixed_size(self, size):
+ if size <= 0:
+ return b''
+ recvd = b''
+ while len(recvd) < size:
+ this_message = self.connection.recv(size - len(recvd))
+ if len(this_message) == 0:
+ break
+ recvd += this_message
+ return recvd
+
+ def read(self, size=None):
+ if size is None:
+ self.length = 0
+ return self.cached + self.read_fixed_size(self.length - self.cached)
+ if size > self.length:
+ size = self.length
+ message = b''
+ message = self.cached[:size]
+ if len(message) < size:
+ message += self.read_fixed_size(size - len(message))
+ self.length -= size
+ return message
+
+class HTTPBaseServer(BasicTCPServer):
+ def __init__(self, request_handler, bind_addr='127.0.0.1:80'):
+ if not callable(request_handler):
+ raise ValueError('You must provide an callable request handler.')
+ super(HTTPBaseServer, self).__init__(address=bind_addr)
+ self.request_handler = request_handler
+
+ def handle_message(self, sock, addr):
+ # print('processing connection from ', addr)
+ # recieve until header ends.
+ delemeter = b'\r\n\r\n'
+ header_text = b''
+ # print('New connection established from', addr)
+ while True:
+ # wait for the end of the
+ while header_text.find(delemeter) == -1:
+ this_mesage = sock.recv(8192)
+ if len(this_mesage) == 0:
+ # print('connection exited. addr:', addr)
+ return
+ header_text += this_mesage
+ delpos = header_text.find(delemeter)
+ content = header_text[delpos + len(delemeter):]
+ header_text = header_text[:delpos]
+ header = HTTPRequestHeader()
+ header.decode(header_text)
+ header_text = b''
+ wraped_connection = SingleHTTPConnection(header, content, sock)
+ self.request_handler(wraped_connection) # the request handler can only read limited data, once finish, send, and return, we will move on.
+ # print('request handler finished.')
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..a28b457
--- /dev/null
+++ b/server.py
@@ -0,0 +1,219 @@
+import os
+import io
+import json
+import time
+
+import threading
+import queue
+from http import HTTPStatus
+from urllib.parse import unquote
+from PIL import Image
+
+from network import HTTPBaseServer, HTTPResponseHeader
+
+app_dir = os.path.split(os.path.realpath(__file__))[0]
+index_path = os.path.join(app_dir, 'index.html')
+
+def loadfile(path):
+ with open(path, 'r', encoding='utf-8') as f:
+ return f.read()
+
+class HTTPImageServer():
+ def __init__(self, bind_addr, imgroot='.'):
+ self.server = HTTPBaseServer(request_handler=self.handle, bind_addr=bind_addr)
+ self.imgroot = imgroot
+ self.img_extension = ['png', 'jpg', 'jpeg', 'tiff', 'webp', 'bmp']
+ self.print_lock = threading.Lock()
+ self.logqueue = queue.Queue()
+ def start(self, back=True):
+ t = threading.Thread(target=self.logger, name='Logger thread', daemon=True)
+ t.start()
+ self.server.start(back=back)
+
+
+ def logger(self):
+ while True:
+ try:
+ msg = self.logqueue.get(timeout=1)
+ print(msg)
+ except queue.Empty:
+ pass
+
+ @staticmethod
+ def parse_url(url):
+ location = url.split('?')[0]
+ params_str = url[len(location)+1:]
+ location = unquote(location)
+ params = {}
+ splits = params_str.split('&')
+ for split in splits:
+ split = unquote(split)
+ eq_pos = split.find('=')
+ if eq_pos == -1:
+ params[split] = None
+ continue
+ else:
+ key = split[:eq_pos]
+ value = split[eq_pos+1:]
+ params[key] = value
+ return location, params
+
+ def log(self, msg):
+ self.logqueue.put(msg)
+
+ def response(self, connection, header, content):
+ msg = '[{time}] {method}: {url} - {stat}'.format(
+ time = time.strftime("%H:%M:%S", time.localtime()),
+ method = connection.header.method,
+ url = connection.header.url,
+ stat = '{0}({1})'.format(header.code, HTTPStatus(header.code).phrase)
+ )
+ self.log(msg)
+
+ header['Content-Length'] = len(content)
+ connection.write(header.encode() + b'\r\n\r\n')
+ connection.write(content)
+
+ def response_404(self, connection):
+ header = HTTPResponseHeader(404)
+ content = b'404 Not Found'
+ self.response(connection, header, content)
+ @staticmethod
+ def safe_path(path):
+ path = '/'.join(path.split('\\'))
+ path = path.split('/')
+ path = [p for p in path if p not in ['', '..', '.']]
+ path = '/'.join(path)
+ return path
+
+
+ def handle_index(self, params):
+ if 'path' not in params:
+ return HTTPResponseHeader(404), b'404 Not Found'
+ directory = params['path']
+ while '\\' in directory:
+ directory = directory.replace('\\', '/')
+ directory = self.safe_path(directory)
+ disk_directory = os.path.join(self.imgroot, directory)
+ filenames = []
+ try:
+ filenames = os.listdir(disk_directory)
+ filenames.sort()
+ except Exception:
+ pass
+ response = {"dirs": [], "imgs": []}
+ for filename in filenames:
+ full_path = os.path.join(disk_directory, filename)
+ request_path = '/{0}/{1}'.format(directory, filename)
+ request_path = '/' + request_path.strip('/\\')
+ if os.path.isdir(full_path):
+ response['dirs'].append(request_path)
+ else:
+ if filename.split('.')[-1] in self.img_extension:
+ response['imgs'].append(request_path)
+ response = json.dumps(response).encode('utf-8')
+ return HTTPResponseHeader(200), response
+
+ def handle_image(self, params):
+ invalid_request = False
+ if 'path' not in params:
+ invalid_request = True
+ filepath = params['path']
+ filepath = self.safe_path(filepath)
+ full_path = os.path.join(self.imgroot, filepath)
+ if filepath.split('.')[-1] not in self.img_extension:
+ invalid_request = True
+ elif not os.path.isfile(full_path):
+ invalid_request = True
+
+ # parse height and width limit.
+ max_h, max_w = None, None
+ try:
+ if 'height' in params:
+ max_h = int(params['height'])
+ elif 'width' in params:
+ max_w = int(params['width'])
+ except Exception:
+ invalid_request = True
+
+ if invalid_request:
+ return HTTPResponseHeader(404), b'404 Not Found'
+
+ header = HTTPResponseHeader(200)
+ content = b''
+ if max_h is not None or max_w is not None:
+ img = Image.open(full_path)
+ real_w, real_h = img.size
+ h_ratio = None
+ w_ratio = None
+ if max_h is not None:
+ h_ratio = max_h / real_h
+ h_ratio = h_ratio if h_ratio < 1 else 1
+ if max_w is not None:
+ w_ratio = max_w / real_w
+ w_ratio = w_ratio if w_ratio < 1 else 1
+ max_ratio = 0
+ if h_ratio is None:
+ max_ratio = w_ratio
+ elif w_ratio is None:
+ max_ratio = h_ratio
+ else:
+ max_ratio = h_ratio if h_ratio < w_ratio else w_ratio
+ new_h, new_w = (real_h * max_ratio, real_w * max_ratio)
+ img = img.resize((int(new_w), int(new_h)))
+ img_stream = io.BytesIO()
+ img = img.save(img_stream, format='webp')
+ content = img_stream.getvalue()
+ else:
+ with open(full_path, 'rb') as f:
+ content = f.read()
+ return header, content
+
+
+ """
+ request_type:
+ request index: http://domain.com/directory?path=relative/path/to/file
+ request image: http://domain.com/img?path=relative/path/to/file&height=100px&width=200px
+ """
+ def handle(self, connection):
+ method = connection.header.method
+ if method != 'GET':
+ self.response_404(connection)
+ return
+
+ url = connection.header.url
+ location, params = self.parse_url(url)
+ location = location.strip('/\\')
+ header, content = None, None
+ if location == 'directory':
+ header, content = self.handle_index(params)
+ elif location == 'img':
+ header, content = self.handle_image(params)
+ elif location in ['', 'index', 'index.html']:
+ header = HTTPResponseHeader(200)
+ content = loadfile(index_path).encode('utf-8')
+ else:
+ header = HTTPResponseHeader(404)
+ content = b'Please Do Not Try To Access Non-Image File!'
+ self.response(connection, header, content)
+
+if __name__ == '__main__':
+ import sys
+ args= sys.argv[1:]
+
+ port = 80
+ root = '.'
+ if len(args) > 0:
+ try:
+ port = int(args[0])
+ except Exception:
+ print('Port {0} not understood, use 80 instead'.format(args[0]), file=sys.stderr)
+ if len(args) > 1:
+ root = args[1]
+ if not os.path.isdir(root):
+ print('Path {0} is not a valid path, use current directory instead.'.format(root), file=sys.stderr)
+ root = '.'
+
+ print('Start HTTP server on port {0} and use web root as {1}'.format(port, root))
+ server = HTTPImageServer(bind_addr='0.0.0.0:{0}'.format(port), imgroot=root)
+ server.start(back=False)