diff --git a/lightcomics.py b/lightcomics.py index f71d62b..4daceb0 100755 --- a/lightcomics.py +++ b/lightcomics.py @@ -14,79 +14,68 @@ import logging import chardet import flask +import re +import socket +import threading from flask import request from PIL import Image from io import BytesIO from werkzeug.routing import BaseConverter from functools import wraps from io import StringIO -from urllib.parse import * import tkinter as tk from tkinter import filedialog from tkinter import messagebox -import threading from urllib.request import urlopen -import re -import socket - +from urllib.parse import * # 버전 __version__ = (1, 0, 3) - # 변수 설정 -allow_extensions_image = ['jpg', 'gif', 'png', 'tif', 'bmp', 'jpeg', 'tiff'] -allow_extensions_archive = ['zip', 'cbz'] -allow_extensions = allow_extensions_image + allow_extensions_archive - -ZIP_FILENAME_UTF8_FLAG = 0x800 - - +EXTENSIONS_ALLOW_IMAGE = ['jpg', 'gif', 'png', 'tif', 'bmp', 'jpeg', 'tiff'] +EXTENSIONS_ALLOW_ARCHIVE = ['zip', 'cbz'] +EXTENSIONS_ALLOW = EXTENSIONS_ALLOW_IMAGE + EXTENSIONS_ALLOW_ARCHIVE # 로거 설정 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) - - -# 설정파일 로그 -CONF_ROOT_PATH = "" -CONF_SERVER_PORT = 12370 -CONF_PASSWORD = "" -CONF_HOST = "0.0.0.0" - - # 운영체제 변수 IS_OS_WINDOWS = sys.platform == 'win32' IS_OS_MACOSX = sys.platform == 'darwin' IS_OS_LINUX = sys.platform == 'linux' +# 설정파일 로그 +DEFAULT_PORT = 12370 - +CONF_ROOT_PATH = "" +CONF_SERVER_PORT = DEFAULT_PORT +CONF_PASSWORD = "" +CONF_HOST = "0.0.0.0" if IS_OS_WINDOWS: CONF_ROOT_PATH = "c:/" - CONF_SERVER_PORT = 12370 + CONF_SERVER_PORT = DEFAULT_PORT CONF_PASSWORD = "" - + elif IS_OS_MACOSX: CONF_ROOT_PATH = "/" - CONF_SERVER_PORT = 12370 + CONF_SERVER_PORT = DEFAULT_PORT CONF_PASSWORD = "" - + elif IS_OS_LINUX: CONF = json.loads(open('./lightcomics.json', 'r').read()) CONF_ROOT_PATH = CONF['ROOT'] CONF_SERVER_PORT = CONF['PORT'] CONF_PASSWORD = CONF['PASSWORD'] - CONF_HOST= CONF['HOST']; + CONF_HOST = CONF['HOST'] if not os.path.exists(CONF_ROOT_PATH): - raise Exception("No Root Directory!!!!") - + raise Exception( + "루트 디렉토리를 찾을 수 없습니다. lightcomics.json 파일의 ROOT 경로를 확인해주세요.") else: - raise Exception("hmm?") - + raise Exception("hmm..?") # 앱 선언 @@ -94,23 +83,25 @@ # 권한 체크 -def check_auth(username, password): +def authention_validate(username, password): return username == 'LightComics' and password == CONF_PASSWORD + # 권한 오류 반환 def authenticate(): - return flask.Response( - 'You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'}) + return flask.Response('You have to login with proper credentials', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + # 권한 요구 -def requires_auth(f): +def requires_authenticate(f): @wraps(f) def decorated(*args, **kwargs): auth = flask.request.authorization - if not auth or not check_auth(auth.username, auth.password): + if not auth or not authention_validate(auth.username, auth.password): return authenticate() return f(*args, **kwargs) + return decorated @@ -119,6 +110,7 @@ class LightEncoder(json.JSONEncoder): def default(self, o): return o.__dict__ + # Identifier 모델 class BaseIdentifierModel(LightEncoder): def __init__(self): @@ -133,6 +125,7 @@ def __init__(self): self._width = -1 self._height = -1 + # 리스팅 모델 class BaseListingModel(LightEncoder): def __init__(self): @@ -145,7 +138,6 @@ def __init__(self): # 함수 def fix_str(str): name = str - try: name = name.encode('cp437').decode('cp949') except UnicodeEncodeError: @@ -155,58 +147,65 @@ def fix_str(str): return name + def get_image_size_from_bytes(head): """ 이미지 사이즈를 반환한다 """ try: im = Image.open(head) return im.size except: - return 0,0 + return 0, 0 + def is_hidden_or_trash(full_path): """ 숨김 파일 또는 __MACOSX 디렉토리인지 확인한다. """ - if full_path.startswith('DS_STORE'): + if 'DS_STORE' in full_path: return True - if '__MACOSX' in full_path: + elif '__MACOSX' in full_path: return True - return False + else: + return False + def get_extension(file_name): """ 확장자를 반환한다. (숨김파일 또는 MACOSX파일의 경우 확장자를 반환하지 않는다._) """ extension = os.path.splitext(file_name)[-1] if extension.startswith('.'): extension = extension[1:] - + if is_hidden_or_trash(extension): return '' return extension -def is_allow_extensions_image(file_name): + +def is_EXTENSIONS_ALLOW_IMAGE(file_name): """ 허용된 이미지 확장자인 경우 True를 반환한다 """ extension = get_extension(file_name) - if extension not in allow_extensions_image: + if extension not in EXTENSIONS_ALLOW_IMAGE: return False else: return True - -def is_allow_extensions_archive(file_name): + + +def is_EXTENSIONS_ALLOW_ARCHIVE(file_name): """ 허용된 압축파일 확장자인 경우 True를 반환한다 """ extension = get_extension(file_name) - if extension not in allow_extensions_archive: + if extension not in EXTENSIONS_ALLOW_ARCHIVE: return False else: return True + def get_imagemodel_in_dir(dir_path, mode): """ 디렉토리의(dir_path)의 이미지파일의 name, width, height를 모아서 반환한다.""" image_models = [] for name in os.listdir(dir_path): - - if is_allow_extensions_image(name): + + if is_EXTENSIONS_ALLOW_IMAGE(name): model = BaseImageModel() - model._name = os.path.join(dir_path, name).replace("\\","/") + model._name = os.path.join(dir_path, name).replace("\\", "/") if mode == "1": with open(model._name, mode='rb') as f: bytesIO = BytesIO() @@ -217,17 +216,18 @@ def get_imagemodel_in_dir(dir_path, mode): model._height = size[1] image_models.append(model) - + return image_models + def get_imagemodel_in_zip(zip_path, mode): """ 압축파일(zip_path)의 이미지파일의 name, width, height를 모아서 반환한다.""" image_models = [] - + with zipfile.ZipFile(zip_path) as zf: for name in zf.namelist(): - - if is_allow_extensions_image(name): + + if is_EXTENSIONS_ALLOW_IMAGE(name): model = BaseImageModel() model._name = name model._decode_name = fix_str(name) @@ -241,17 +241,18 @@ def get_imagemodel_in_zip(zip_path, mode): model._height = size[1] image_models.append(model) - + return image_models + # def get_imagemodel_in_rar(rar_path, mode): # """ 압축파일(rar_path)의 이미지파일의 name, width, height를 모아서 반환한다.""" # image_models = [] - + # with rarfile.RarFile(rar_path) as rf: # for name in rf.namelist(): - -# if is_allow_extensions_image(name): + +# if is_EXTENSIONS_ALLOW_IMAGE(name): # model = BaseImageModel() # model._name = name # if mode == "1": @@ -262,11 +263,12 @@ def get_imagemodel_in_zip(zip_path, mode): # size = get_image_size_from_bytes(bytesIO) # model._width = size[0] # model._height = size[1] - + # image_models.append(model) # return image_models + def get_image_data_in_dir(file_path): """ 이미지 파일(file_path)의 데이터를 반환한다. """ with open(file_path, mode='rb') as f: @@ -275,12 +277,13 @@ def get_image_data_in_dir(file_path): bytesIO.seek(0) return bytesIO + def get_image_data_in_zip(zip_path, file_path): """ 압축 파일(zip_path)에서 이미지 파일(file_path)의 데이터를 반환한다. """ with zipfile.ZipFile(zip_path) as zf: for name in zf.namelist(): if name == file_path: - if is_allow_extensions_image(name): + if is_EXTENSIONS_ALLOW_IMAGE(name): model = BaseImageModel() model._name = name @@ -290,27 +293,29 @@ def get_image_data_in_zip(zip_path, file_path): bytesIO.seek(0) return bytesIO + def get_listing_model(path): """ 리스팅 """ listing_model = BaseListingModel() for name in os.listdir(path): - full_path = os.path.join(path, name).replace("\\","/") - + full_path = os.path.join(path, name).replace("\\", "/") + if os.path.isdir(full_path): listing_model._directories.append(full_path) - - elif is_allow_extensions_archive(full_path): + + elif is_EXTENSIONS_ALLOW_ARCHIVE(full_path): listing_model._archives.append(full_path) - - elif is_allow_extensions_image(full_path): + + elif is_EXTENSIONS_ALLOW_IMAGE(full_path): listing_model._images.append(full_path) - + else: app.logger.info(name + " ignore") - + return listing_model + def get_unique_identifier(path): """ path에 해당하는 고유값을 생성하여 반환한다 """ path = remove_trail_slash(path) @@ -322,20 +327,23 @@ def get_unique_identifier(path): app.logger.info(uniqueue_identifier) return uniqueue_identifier + def get_real_path(base, abs_path): """ 실제 경로를 반환한다 """ abs_path = unquote(abs_path) if abs_path == "": return base - real_path = os.path.join(base, abs_path).replace("\\","/") + real_path = os.path.join(base, abs_path).replace("\\", "/") return real_path + def remove_trail_slash(s): """ 마지막 slash를 제거한다 """ if s.endswith('/'): s = s[:-1] return s + def getSizeOf(path): """ 해당 경로 파일 또는 디렉토리의 사이즈를 구하여 반환한다 """ total_size = os.path.getsize(path) @@ -344,7 +352,7 @@ def getSizeOf(path): return total_size for item in os.listdir(path): - itempath = os.path.join(folder, item).replace("\\","/") + itempath = os.path.join(folder, item).replace("\\", "/") if os.path.isfile(itempath): total_size += os.path.getsize(itempath) elif os.path.isdir(itempath): @@ -352,10 +360,9 @@ def getSizeOf(path): return total_size - # Flask 네트워크 맵핑 시작 @app.route('/') -@requires_auth +@requires_authenticate def root(): """ 리스팅 @@ -367,7 +374,7 @@ def root(): @app.route('//') -@requires_auth +@requires_authenticate def listing(req_path): """ 리스팅 @@ -375,10 +382,10 @@ def listing(req_path): """ app.logger.info("@app.route('//')") - basePath = get_real_path(CONF_ROOT_PATH, "") + basePath = get_real_path(CONF_ROOT_PATH, "") full_path = "%s" % unquote(req_path) full_real_path = get_real_path(basePath, full_path) - full_real_path = os.path.join(full_real_path, "").replace("\\","/") + full_real_path = os.path.join(full_real_path, "").replace("\\", "/") app.logger.info(full_real_path) model = get_listing_model(full_real_path) @@ -388,7 +395,7 @@ def listing(req_path): @app.route('/./') -@requires_auth +@requires_authenticate def load_image_model(archive, archive_ext): """ 압축파일 내부 이미지 정보 @@ -400,23 +407,25 @@ def load_image_model(archive, archive_ext): @app.route('//./') -@requires_auth +@requires_authenticate def load_image_model2(req_path, archive, archive_ext): """ 압축파일 내부 이미지 정보 localhost:12370/dir/sglee/sample.zip/ """ - app.logger.info("@app.route('//./')") + app.logger.info( + "@app.route('//./')" + ) - basePath = get_real_path(CONF_ROOT_PATH, "") + basePath = get_real_path(CONF_ROOT_PATH, "") full_path = "%s" % unquote(req_path) full_real_path = get_real_path(basePath, full_path) - full_real_path = os.path.join(full_real_path, "").replace("\\","/") + full_real_path = os.path.join(full_real_path, "").replace("\\", "/") app.logger.info(full_real_path) - archive_name = "%s" % unquote(archive) + "." + archive_ext - archive_path = os.path.join(full_real_path, archive_name).replace("\\","/") + archive_path = os.path.join(full_real_path, + archive_name).replace("\\", "/") app.logger.info(archive_path) @@ -426,15 +435,17 @@ def load_image_model2(req_path, archive, archive_ext): if archive_ext == 'zip' or archive_ext == 'cbz': models = get_imagemodel_in_zip(archive_path, mode) data = json.dumps(models, indent=4, cls=LightEncoder) - response = flask.Response(data, headers=None, mimetype='application/json') + response = flask.Response(data, + headers=None, + mimetype='application/json') return response - + # elif archive_ext == 'rar': # models = get_imagemodel_in_rar(archive_path, mode) # data = json.dumps(models, indent=4, cls=LightEncoder) # response = flask.Response(data, headers=None, mimetype='application/json') # return response - + return ('', 204) @@ -445,30 +456,35 @@ def load_image_data(archive, archive_ext, img_path): localhost:12370/sample.zip/img1.jpg localhost:12370/sample.zip/test/img1.jpg """ - app.logger.info("@app.route('/./')") - + app.logger.info( + "@app.route('/./')") + return load_image_data2("", archive, archive_ext, img_path) -@app.route('//./') +@app.route( + '//./') def load_image_data2(req_path, archive, archive_ext, img_path): """ 압축파일 내부 이미지 데이터 반환 localhost:12370/dir/sglee/sample.zip/img1.jpg localhost:12370/dir/sglee/sample.zip/test/img1.jpg """ - app.logger.info("@app.route('//./')") + app.logger.info( + "@app.route('//./')" + ) basePath = get_real_path(CONF_ROOT_PATH, "") full_path = "%s" % unquote(req_path) full_real_path = get_real_path(basePath, full_path) - full_real_path = os.path.join(full_real_path, "").replace("\\","/") - + full_real_path = os.path.join(full_real_path, "").replace("\\", "/") + app.logger.info(full_real_path) archive_name = "%s" % unquote(archive) + "." + archive_ext - archive_path = os.path.join(full_real_path, archive_name).replace("\\","/") - + archive_path = os.path.join(full_real_path, + archive_name).replace("\\", "/") + app.logger.info(archive_path) img_path = unquote(img_path) @@ -476,13 +492,15 @@ def load_image_data2(req_path, archive, archive_ext, img_path): if archive_ext == 'zip' or archive_ext == 'cbz': img = get_image_data_in_zip(archive_path, img_path) - return flask.send_file(img, attachment_filename=os.path.basename(img_path), as_attachment=True) + return flask.send_file(img, + attachment_filename=os.path.basename(img_path), + as_attachment=True) return ('', 204) @app.route('/id/') -@requires_auth +@requires_authenticate def get_identifier(req_path): """ 해당하는 경로의 파일 identifier를 반환한다. @@ -490,10 +508,10 @@ def get_identifier(req_path): """ app.logger.info("@app.route('/id/')") - basePath = get_real_path(CONF_ROOT_PATH, "") + basePath = get_real_path(CONF_ROOT_PATH, "") full_path = "%s" % unquote(req_path) full_real_path = get_real_path(basePath, full_path) - full_real_path = os.path.join(full_real_path, "").replace("\\","/") + full_real_path = os.path.join(full_real_path, "").replace("\\", "/") app.logger.info(full_real_path) model = BaseIdentifierModel() @@ -505,18 +523,18 @@ def get_identifier(req_path): return response - - # UI 구현 for Windows or Mac OSX + def onClickServerState(): global server_run global server_state_label global server_on_off_button global server_threading - + if server_run == True: - tk.messagebox.showinfo("알림", "서버 정지는 정상적으로 동작되지 않습니다.\n프로그램 종료후 재시작 해야 합니다.") + tk.messagebox.showinfo( + "알림", "서버 정지는 정상적으로 동작되지 않습니다.\n프로그램 종료후 재시작 해야 합니다.") return shutdown_server() server_state_label['text'] = "서버: 정지됨" @@ -527,42 +545,48 @@ def onClickServerState(): server_threading.start() server_state_label['text'] = "서버: 가동중" server_on_off_button['text'] = " 정지 " - + server_run = not server_run + def start_server(): app.logger.info("Server Start: " + str(CONF_SERVER_PORT)) app.run(host=local_ip.get(), port=CONF_SERVER_PORT) - + + def shutdown_server(): # TODO: 서버 어떻게 멈추냐.. 안되네 # func = request.environ.get('werkzeug.server.shutdown') - # if func is None: - # raise RuntimeError('Not running with the Werkzeug Server') + # if func is None: + # raise RuntimeError('Not running with the Werkzeug Server') # func() app.logger.info("Sever Stopped") # server_threading.join() - + def getPublicIp(): - data = str(urlopen('http://checkip.dyndns.com/').read()) - return re.compile(r'Address: (\d+\.\d+\.\d+\.\d+)').search(data).group(1) + data = str(urlopen('http://checkip.dyndns.com/').read()) + return re.compile(r'Address: (\d+\.\d+\.\d+\.\d+)').search(data).group(1) + def updateServerIP(): app.logger.info(getPublicIp()) local_ip.set(socket.gethostbyname(socket.gethostname())) public_ip.set(getPublicIp()) + def updateServerPort(): global CONF_SERVER_PORT CONF_SERVER_PORT = int(server_port.get()) app.logger.info(CONF_SERVER_PORT) + def updatePassword(): global CONF_PASSWORD CONF_PASSWORD = password_var.get() app.logger.info(CONF_PASSWORD) + def updateRootPath(): global CONF_ROOT_PATH folder_selected = filedialog.askdirectory() @@ -570,8 +594,9 @@ def updateRootPath(): root_path_var.set(CONF_ROOT_PATH) app.logger.info(CONF_ROOT_PATH) -def resource_path(relative_path): - try: + +def resource_path(relative_path): + try: base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") @@ -579,15 +604,25 @@ def resource_path(relative_path): # Set UI values for Windows +server_run = False if IS_OS_WINDOWS: - server_run = False server_threading = threading.Thread(target=start_server) - window = tk.Tk() - server_state_label = tk.Label(window, text="서버: 중지됨", width=15, anchor="w", padx=10, pady=5) - server_on_off_button = tk.Button(window, text=" 가동 ", command=onClickServerState, width=20) - change_root_path_button = tk.Button(window, text=" 변경 ", command=updateRootPath, width=20) + server_state_label = tk.Label(window, + text="서버: 중지됨", + width=15, + anchor="w", + padx=10, + pady=5) + server_on_off_button = tk.Button(window, + text=" 가동 ", + command=onClickServerState, + width=20) + change_root_path_button = tk.Button(window, + text=" 변경 ", + command=updateRootPath, + width=20) public_ip = tk.StringVar() local_ip = tk.StringVar() @@ -598,11 +633,21 @@ def resource_path(relative_path): root_path_var = tk.StringVar() root_path_var.set(CONF_ROOT_PATH) - local_ip_textbox = tk.Entry(window, width=20, textvariable=local_ip, state='readonly') - public_ip_textbox = tk.Entry(window, width=20, textvariable=public_ip, state='readonly') + local_ip_textbox = tk.Entry(window, + width=20, + textvariable=local_ip, + state='readonly') + public_ip_textbox = tk.Entry(window, + width=20, + textvariable=public_ip, + state='readonly') server_port_textbox = tk.Entry(window, width=20, textvariable=server_port) password_textbox = tk.Entry(window, width=20, textvariable=password_var) - root_path_textbox = tk.Entry(window, width=20, textvariable=root_path_var, state='readonly') + root_path_textbox = tk.Entry(window, + width=20, + textvariable=root_path_var, + state='readonly') + def applicationUI(): global window @@ -619,7 +664,7 @@ def applicationUI(): server_state_label.grid(row=1, column=0) server_on_off_button.grid(row=1, column=1) - + reuse_label = tk.Label(window, text="Local IP", width=15, anchor="w") reuse_label.grid(row=2, column=0) local_ip_textbox.grid(row=2, column=1) @@ -644,27 +689,22 @@ def applicationUI(): reuse_label.grid(row=7, column=0) change_root_path_button.grid(row=7, column=1) - updateServerIP() window.mainloop() - # 앱 시작 if __name__ == '__main__': if IS_OS_WINDOWS: applicationUI() - + elif IS_OS_MACOSX: print("not yet") - + elif IS_OS_LINUX: app.run(host=CONF_HOST, port=CONF_SERVER_PORT) - + else: print("hmm..?") - - -