import
hashlib
import
os
import
http.server
import
socketserver
import
time
import
threading
import
sys
import
signal
from
pystray
import
Icon as icon, Menu as menu, MenuItem as item
from
PIL
import
Image
import
configparser
import
logging
import
traceback
config
=
configparser.ConfigParser()
if
not
config.read(
'server_config.ini'
):
logging.warning(
"未找到 server_config.ini 文件,将使用默认配置。"
)
config[
'Server'
]
=
{
'update_folder'
:
'root'
,
'port'
:
8020
,
'log_level'
:
'INFO'
}
with
open
(
'server_config.ini'
,
'w'
) as configfile:
config.write(configfile)
log_level_str
=
config.get(
'Server'
,
'log_level'
, fallback
=
'INFO'
)
log_level
=
getattr
(logging, log_level_str.upper(), logging.INFO)
logging.basicConfig(
level
=
log_level,
format
=
'%(asctime)s - %(levelname)s - %(message)s'
,
filename
=
'server.log'
,
filemode
=
'a'
)
def
extract_embedded_file(file_name):
if
getattr
(sys,
'frozen'
,
False
):
base_path
=
sys._MEIPASS
source_path
=
os.path.join(base_path, file_name)
target_path
=
os.path.join(os.path.dirname(sys.executable), file_name)
try
:
with
open
(source_path,
'rb'
) as src,
open
(target_path,
'wb'
) as dst:
dst.write(src.read())
logging.info(f
"成功释放文件 {file_name} 到 {target_path}"
)
except
Exception as e:
logging.error(f
"释放文件 {file_name} 时出错: {e}"
)
return
os.path.join(os.path.dirname(sys.executable), file_name)
class
FileListGenerator:
def
__init__(
self
, update_folder):
self
.update_folder
=
self
.get_full_update_folder(update_folder)
def
get_full_update_folder(
self
, update_folder):
if
getattr
(sys,
'frozen'
,
False
):
script_dir
=
os.path.dirname(sys.executable)
else
:
script_dir
=
os.path.dirname(os.path.abspath(__file__))
full_update_folder
=
os.path.join(script_dir, update_folder)
if
not
os.path.exists(full_update_folder):
os.makedirs(full_update_folder)
return
full_update_folder
@staticmethod
def
calculate_md5(file_path):
hash_md5
=
hashlib.md5()
try
:
with
open
(file_path,
"rb"
) as f:
for
chunk
in
iter
(
lambda
: f.read(
4096
), b""):
hash_md5.update(chunk)
return
hash_md5.hexdigest()
except
Exception as e:
logging.error(f
"计算文件 {file_path} 的 MD5 时出错: {e}"
)
return
None
def
generate_file_list(
self
):
file_list
=
[]
try
:
for
root, dirs, files
in
os.walk(
self
.update_folder):
for
file
in
files:
file_path
=
os.path.join(root,
file
)
md5
=
self
.calculate_md5(file_path)
if
md5:
relative_file
=
os.path.relpath(file_path,
self
.update_folder)
file_list.append(f
"{relative_file} || {md5}"
)
file_list_path
=
os.path.join(
self
.update_folder,
'file_list.sha'
)
file_list_dir
=
os.path.dirname(file_list_path)
if
not
os.path.exists(file_list_dir):
os.makedirs(file_list_dir)
with
open
(file_list_path,
'w'
) as f:
f.write(
'\n'
.join(file_list))
logging.info(
"文件列表生成成功"
)
except
Exception as e:
logging.error(f
"生成文件列表时出错: {e}"
)
class
CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def
log_message(
self
,
format
,
*
args):
logging.info(
"%s - - [%s] %s\n"
%
(
self
.address_string(),
self
.log_date_time_string(),
format
%
args))
class
ServerManager:
def
__init__(
self
, update_folder, port):
self
.update_folder
=
update_folder
self
.port
=
port
self
.file_list_generator
=
FileListGenerator(update_folder)
self
.httpd
=
None
self
.server_thread
=
None
self
.timer_thread
=
None
self
.tray_icon
=
None
self
.running
=
True
def
start_timer_task(
self
):
def
timer_task():
while
self
.running:
try
:
self
.file_list_generator.generate_file_list()
except
Exception as e:
logging.error(f
"定时任务出错: {e}\n{traceback.format_exc()}"
)
time.sleep(
180
)
self
.timer_thread
=
threading.Thread(target
=
timer_task)
self
.timer_thread.daemon
=
True
self
.timer_thread.start()
def
start_http_server(
self
):
Handler
=
CustomHTTPRequestHandler
update_folder
=
self
.file_list_generator.update_folder
def
serve():
try
:
abs_update_folder
=
os.path.abspath(update_folder)
if
not
os.path.exists(abs_update_folder):
logging.error(f
"更新文件夹 {abs_update_folder} 不存在。"
)
return
os.chdir(abs_update_folder)
self
.httpd
=
socketserver.TCPServer(("",
self
.port), Handler)
logging.info(f
"服务端已启动,监听端口 {self.port},可通过 http://localhost:{self.port} 访问。"
)
self
.httpd.serve_forever()
except
OSError as e:
if
e.errno
=
=
98
:
logging.error(f
"端口 {self.port} 已被占用,请更换端口。"
)
else
:
logging.error(f
"启动 HTTP 服务时出错: {e}\n{traceback.format_exc()}"
)
except
Exception as e:
logging.error(f
"启动 HTTP 服务时出错: {e}\n{traceback.format_exc()}"
)
self
.server_thread
=
threading.Thread(target
=
serve)
self
.server_thread.daemon
=
True
self
.server_thread.start()
def
recalculate(
self
, icon, item):
try
:
self
.file_list_generator.generate_file_list()
except
Exception as e:
logging.error(f
"重新计算文件列表时出错: {e}\n{traceback.format_exc()}"
)
def
exit_program(
self
, icon, item):
self
.running
=
False
try
:
if
icon:
icon.stop()
except
Exception as e:
logging.error(f
"停止系统托盘图标时出错: {e}\n{traceback.format_exc()}"
)
try
:
if
self
.httpd:
self
.httpd.shutdown()
self
.httpd.server_close()
except
Exception as e:
logging.error(f
"关闭 HTTP 服务器时出错: {e}\n{traceback.format_exc()}"
)
try
:
if
self
.server_thread:
self
.server_thread.join(timeout
=
5
)
if
self
.server_thread.is_alive():
logging.warning(
"HTTP 服务器线程未能在 5 秒内关闭。"
)
except
Exception as e:
logging.error(f
"等待 HTTP 服务器线程结束时出错: {e}\n{traceback.format_exc()}"
)
try
:
if
self
.timer_thread:
self
.timer_thread.join(timeout
=
5
)
if
self
.timer_thread.is_alive():
logging.warning(
"定时任务线程未能在 5 秒内关闭。"
)
except
Exception as e:
logging.error(f
"等待定时任务线程结束时出错: {e}\n{traceback.format_exc()}"
)
sys.exit(
0
)
def
start_tray_icon(
self
):
try
:
image_path
=
extract_embedded_file(
'feather.png'
)
image
=
Image.
open
(image_path)
except
FileNotFoundError:
logging.warning(
"未找到图标文件,使用默认图标。"
)
image
=
Image.new(
'RGB'
, (
16
,
16
))
self
.tray_icon
=
icon(
'name'
, image,
'服务端程序'
, menu
=
menu(
item(
'重新计算'
,
self
.recalculate),
item(
'退出程序'
,
self
.exit_program)
))
self
.tray_icon.run()
def
start(
self
):
self
.file_list_generator.generate_file_list()
self
.start_timer_task()
self
.start_http_server()
self
.start_tray_icon()
if
__name__
=
=
"__main__"
:
update_folder
=
config.get(
'Server'
,
'update_folder'
, fallback
=
'root'
)
port
=
config.getint(
'Server'
,
'port'
, fallback
=
8020
)
server_manager
=
ServerManager(update_folder, port)
def
signal_handler(sig, frame):
server_manager.exit_program(
None
,
None
)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
server_manager.start()