客户端
import os
import socket
import json
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from datetime import datetime
class FileClient:
def __init__(self, root, server_host='localhost', server_port=65432):
self.root = root
self.root.title("网络文件管理系统 - 客户端")
self.root.geometry("900x600")
self.root.minsize(800, 500)
# 设置中文字体
self.font = ('SimHei', 10)
# 服务器连接信息
self.server_host = server_host
self.server_port = server_port
self.client_socket = None
# 当前路径
self.current_path = "/"
# 创建界面
self.create_widgets()
self.connect_to_server()
def create_widgets(self):
"""创建用户界面"""
# 顶部导航栏
nav_frame = tk.Frame(self.root)
nav_frame.pack(fill=tk.X, padx=5, pady=5)
# 连接/断开按钮
self.connect_button = tk.Button(nav_frame, text="连接", font=self.font, command=self.toggle_connection)
self.connect_button.pack(side=tk.LEFT, padx=(0, 5))
# 返回上级目录按钮
self.back_button = tk.Button(nav_frame, text="返回上级", font=self.font, command=self.go_up, state=tk.DISABLED)
self.back_button.pack(side=tk.LEFT, padx=(0, 5))
# 刷新按钮
self.refresh_button = tk.Button(nav_frame, text="刷新", font=self.font, command=self.update_file_list, state=tk.DISABLED)
self.refresh_button.pack(side=tk.LEFT, padx=(0, 5))
# 地址栏
self.path_var = tk.StringVar()
self.path_var.set(self.current_path)
self.path_entry = tk.Entry(nav_frame, textvariable=self.path_var, font=self.font, width=70)
self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
self.path_entry.bind("<Return>", lambda event: self.navigate_to_path())
# 转到按钮
self.goto_button = tk.Button(nav_frame, text="转到", font=self.font, command=self.navigate_to_path, state=tk.DISABLED)
self.goto_button.pack(side=tk.LEFT)
# 中间文件列表区域
center_frame = tk.Frame(self.root)
center_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 文件列表视图
columns = ("name", "type", "size", "modified")
self.file_list = ttk.Treeview(center_frame, columns=columns, show="headings")
self.file_list.heading("name", text="名称")
self.file_list.heading("type", text="类型")
self.file_list.heading("size", text="大小")
self.file_list.heading("modified", text="修改日期")
self.file_list.column("name", width=300)
self.file_list.column("type", width=100)
self.file_list.column("size", width=100)
self.file_list.column("modified", width=200)
self.file_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 添加滚动条
scrollbar = ttk.Scrollbar(center_frame, orient="vertical", command=self.file_list.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.file_list.configure(yscroll=scrollbar.set)
# 绑定双击事件
self.file_list.bind("<Double-1>", self.on_double_click)
# 底部状态栏
status_frame = tk.Frame(self.root)
status_frame.pack(fill=tk.X, padx=5, pady=5)
self.status_var = tk.StringVar()
self.status_var.set("未连接到服务器")
self.status_label = tk.Label(status_frame, textvariable=self.status_var, font=self.font)
self.status_label.pack(side=tk.LEFT)
# 右键菜单
self.menu = tk.Menu(self.root, tearoff=0)
self.menu.add_command(label="打开", command=self.open_file, state=tk.DISABLED)
self.menu.add_command(label="复制", command=self.copy_file, state=tk.DISABLED)
self.menu.add_command(label="移动", command=self.move_file, state=tk.DISABLED)
self.menu.add_command(label="删除", command=self.delete_file, state=tk.DISABLED)
self.menu.add_separator()
self.menu.add_command(label="新建文件夹", command=self.create_folder, state=tk.DISABLED)
self.menu.add_command(label="重命名", command=self.rename_file, state=tk.DISABLED)
self.file_list.bind("<Button-3>", self.show_menu)
def connect_to_server(self):
"""连接到服务器"""
try:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client_socket.connect((self.server_host, self.server_port))
self.status_var.set(f"已连接到 {self.server_host}:{self.server_port}")
self.connect_button.config(text="断开", command=self.disconnect_from_server)
# 启用界面元素
self.back_button.config(state=tk.NORMAL)
self.refresh_button.config(state=tk.NORMAL)
self.goto_button.config(state=tk.NORMAL)
# 启用右键菜单项
for i in range(self.menu.index('end') + 1):
self.menu.entryconfig(i, state=tk.NORMAL)
# 更新文件列表
self.update_file_list()
except Exception as e:
messagebox.showerror("连接错误", f"无法连接到服务器: {e}")
self.client_socket = None
def disconnect_from_server(self):
"""断开与服务器的连接"""
if self.client_socket:
try:
self.client_socket.close()
except Exception as e:
print(f"关闭连接时出错: {e}")
self.client_socket = None
self.status_var.set("未连接到服务器")
self.connect_button.config(text="连接", command=self.connect_to_server)
# 禁用界面元素
self.back_button.config(state=tk.DISABLED)
self.refresh_button.config(state=tk.DISABLED)
self.goto_button.config(state=tk.DISABLED)
# 禁用右键菜单项
for i in range(self.menu.index('end') + 1):
self.menu.entryconfig(i, state=tk.DISABLED)
def toggle_connection(self):
"""切换连接状态"""
if self.client_socket:
self.disconnect_from_server()
else:
self.connect_to_server()
def send_request(self, command, path="", destination="", new_name=""):
"""向服务器发送请求并接收响应"""
if not self.client_socket:
messagebox.showerror("错误", "未连接到服务器")
return None
try:
# 构建请求
request = {
'command': command,
'path': path,
'destination': destination,
'new_name': new_name
}
# 发送请求
serialized = json.dumps(request).encode('utf-8')
self.client_socket.sendall(serialized)
# 接收响应
response_data = self.client_socket.recv(4096).decode('utf-8')
if not response_data:
messagebox.showerror("错误", "与服务器的连接已断开")
self.disconnect_from_server()
return None
response = json.loads(response_data)
return response
except Exception as e:
messagebox.showerror("通信错误", f"与服务器通信时出错: {e}")
self.disconnect_from_server()
return None
def update_file_list(self):
"""更新文件列表"""
if not self.client_socket:
return
# 清空文件列表
for item in self.file_list.get_children():
self.file_list.delete(item)
# 更新当前路径显示
self.path_var.set(self.current_path)
self.status_var.set(f"当前路径: {self.current_path}")
# 发送列表请求
response = self.send_request('list', self.current_path)
if not response or not response.get('success'):
messagebox.showerror("错误", response.get('message', "获取文件列表失败"))
return
# 填充文件列表
for item in response.get('items', []):
self.file_list.insert("", "end", values=(
item['name'],
item['type'],
item['size'],
item['modified']
))
def go_up(self):
"""导航到上级目录"""
if self.current_path == "/":
return
parent_path = os.path.dirname(self.current_path.rstrip("/"))
if parent_path == "":
parent_path = "/"
self.current_path = parent_path
self.update_file_list()
def navigate_to_path(self):
"""导航到地址栏中输入的路径"""
new_path = self.path_var.get()
response = self.send_request('open', new_path)
if not response or not response.get('success'):
messagebox.showerror("错误", response.get('message', "无效的目录路径"))
self.path_var.set(self.current_path)
return
if response.get('is_file', False):
messagebox.showinfo("信息", f"这是一个文件: {os.path.basename(new_path)}")
self.path_var.set(self.current_path)
else:
self.current_path = new_path
self.update_file_list()
def on_double_click(self, event):
"""双击打开文件夹或文件"""
selection = self.file_list.selection()
if not selection:
return
item = selection[0]
item_name = self.file_list.item(item, "values")[0]
item_path = os.path.join(self.current_path, item_name)
response = self.send_request('open', item_path)
if not response or not response.get('success'):
messagebox.showerror("错误", response.get('message', "无法打开"))
return
if response.get('is_file', False):
messagebox.showinfo("信息", f"文件: {item_name}\n大小: {response['size']}\n修改时间: {response['modified']}")
else:
self.current_path = item_path
self.update_file_list()
def show_menu(self, event):
"""显示右键菜单"""
selection = self.file_list.identify_row(event.y)
if selection:
self.file_list.selection_set(selection)
self.menu.post(event.x_root, event.y_root)
def get_selected_item_path(self):
"""获取选中项的完整路径"""
selection = self.file_list.selection()
if not selection:
return None
item = selection[0]
item_name = self.file_list.item(item, "values")[0]
return os.path.join(self.current_path, item_name)
def open_file(self):
"""打开选中的文件或文件夹"""
item_path = self.get_selected_item_path()
if not item_path:
return
response = self.send_request('open', item_path)
if not response or not response.get('success'):
messagebox.showerror("错误", response.get('message', "无法打开"))
return
if response.get('is_file', False):
messagebox.showinfo("信息", f"文件: {os.path.basename(item_path)}\n大小: {response['size']}\n修改时间: {response['modified']}")
else:
self.current_path = item_path
self.update_file_list()
def copy_file(self):
"""复制选中的文件或文件夹"""
source_path = self.get_selected_item_path()
if not source_path:
return
destination = filedialog.askdirectory(title="选择目标文件夹")
if not destination:
return
# 转换为服务器路径格式
server_destination = self._convert_to_server_path(destination)
response = self.send_request('copy', source_path, server_destination)
if response and response.get('success'):
messagebox.showinfo("成功", response.get('message', "复制完成"))
self.update_file_list()
else:
messagebox.showerror("错误", response.get('message', "复制失败"))
def move_file(self):
"""移动选中的文件或文件夹"""
source_path = self.get_selected_item_path()
if not source_path:
return
destination = filedialog.askdirectory(title="选择目标文件夹")
if not destination:
return
# 转换为服务器路径格式
server_destination = self._convert_to_server_path(destination)
response = self.send_request('move', source_path, server_destination)
if response and response.get('success'):
messagebox.showinfo("成功", response.get('message', "移动完成"))
self.update_file_list()
else:
messagebox.showerror("错误", response.get('message', "移动失败"))
def delete_file(self):
"""删除选中的文件或文件夹"""
item_path = self.get_selected_item_path()
if not item_path:
return
item_name = os.path.basename(item_path)
if messagebox.askyesno("确认删除", f"确定要删除 {item_name} 吗?\n此操作不可恢复!"):
response = self.send_request('delete', item_path)
if response and response.get('success'):
messagebox.showinfo("成功", response.get('message', "删除完成"))
self.update_file_list()
else:
messagebox.showerror("错误", response.get('message', "删除失败"))
def create_folder(self):
"""创建新文件夹"""
def create():
folder_name = entry.get()
if folder_name:
new_folder_path = os.path.join(self.current_path, folder_name)
response = self.send_request('create_folder', new_folder_path)
if response and response.get('success'):
messagebox.showinfo("成功", response.get('message', "文件夹创建完成"))
top.destroy()
self.update_file_list()
else:
messagebox.showerror("错误", response.get('message', "创建文件夹失败"))
else:
messagebox.showerror("错误", "文件夹名称不能为空")
# 创建对话框
top = tk.Toplevel(self.root)
top.title("新建文件夹")
top.geometry("300x120")
top.resizable(False, False)
top.transient(self.root)
top.grab_set()
frame = tk.Frame(top, padx=20, pady=20)
frame.pack(fill=tk.BOTH, expand=True)
label = tk.Label(frame, text="文件夹名称:", font=self.font)
label.pack(anchor=tk.W)
entry = tk.Entry(frame, font=self.font)
entry.pack(fill=tk.X, pady=(5, 15))
entry.focus_set()
button_frame = tk.Frame(frame)
button_frame.pack(fill=tk.X)
ok_button = tk.Button(button_frame, text="确定", font=self.font, command=create)
ok_button.pack(side=tk.RIGHT, padx=(5, 0))
cancel_button = tk.Button(button_frame, text="取消", font=self.font, command=top.destroy)
cancel_button.pack(side=tk.RIGHT)
def rename_file(self):
"""重命名选中的文件或文件夹"""
item_path = self.get_selected_item_path()
if not item_path:
return
old_name = os.path.basename(item_path)
def rename():
new_name = entry.get()
if new_name and new_name != old_name:
response = self.send_request('rename', item_path, new_name=new_name)
if response and response.get('success'):
messagebox.showinfo("成功", response.get('message', "重命名完成"))
top.destroy()
self.update_file_list()
else:
messagebox.showerror("错误", response.get('message', "重命名失败"))
else:
messagebox.showerror("错误", "名称无效或未更改")
# 创建对话框
top = tk.Toplevel(self.root)
top.title("重命名")
top.geometry("300x120")
top.resizable(False, False)
top.transient(self.root)
top.grab_set()
frame = tk.Frame(top, padx=20, pady=20)
frame.pack(fill=tk.BOTH, expand=True)
label = tk.Label(frame, text="新名称:", font=self.font)
label.pack(anchor=tk.W)
entry = tk.Entry(frame, font=self.font)
entry.pack(fill=tk.X, pady=(5, 15))
entry.insert(0, old_name)
entry.selection_range(0, tk.END)
entry.focus_set()
button_frame = tk.Frame(frame)
button_frame.pack(fill=tk.X)
ok_button = tk.Button(button_frame, text="确定", font=self.font, command=rename)
ok_button.pack(side=tk.RIGHT, padx=(5, 0))
cancel_button = tk.Button(button_frame, text="取消", font=self.font, command=top.destroy)
cancel_button.pack(side=tk.RIGHT)
def _convert_to_server_path(self, local_path):
"""将本地路径转换为服务器路径格式(简化处理)"""
# 注意:实际应用中可能需要更复杂的路径映射逻辑
# 这里简单地将路径分隔符统一为正斜杠
return local_path.replace("\\", "/")
if __name__ == "__main__":
root = tk.Tk()
app = FileClient(root)
root.mainloop()
服务端
import os
import socket
import json
import threading
import datetime # 添加缺失的导入
import shutil # 添加缺失的导入
import time # 添加缺失的导入
class FileServer:
def __init__(self, host='localhost', port=65432, buffer_size=4096):
self.host = host # 服务器主机
self.port = port # 服务器端口
self.buffer_size = buffer_size # 缓冲区大小
self.root_dir = os.path.abspath(".") # 服务器文件根目录
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
self.running = False
self.clients = []
def start(self):
"""启动服务器"""
self.running = True
print(f"服务器启动: {self.host}:{self.port}")
print(f"根目录: {self.root_dir}")
# 启动监听线程
accept_thread = threading.Thread(target=self._accept_clients)
accept_thread.daemon = True
accept_thread.start()
def stop(self):
"""停止服务器"""
self.running = False
if self.server_socket:
try:
# 创建一个临时连接以唤醒accept()
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
temp_socket.connect((self.host, self.port))
temp_socket.close()
except Exception as e:
print(f"关闭服务器时出错: {e}")
finally:
self.server_socket.close()
self.server_socket = None
print("服务器已停止")
def _accept_clients(self):
"""接受客户端连接"""
while self.running:
try:
client_socket, client_address = self.server_socket.accept()
print(f"客户端连接: {client_address}")
client_handler = threading.Thread(target=self._handle_client, args=(client_socket,))
client_handler.daemon = True
client_handler.start()
except Exception as e:
if self.running:
print(f"接受客户端连接时出错: {e}")
def _handle_client(self, client_socket):
"""处理客户端请求"""
try:
while True:
# 接收请求
request = self._receive_data(client_socket)
if not request:
break
response = self._process_request(request)
self._send_data(client_socket, response)
except Exception as e:
print(f"处理客户端请求时出错: {e}")
finally:
client_socket.close()
print("客户端断开连接")
def _receive_data(self, client_socket):
"""接收并解析客户端数据"""
try:
data = client_socket.recv(self.buffer_size).decode('utf-8')
if not data:
return None
return json.loads(data)
except Exception as e:
print(f"接收数据时出错: {e}")
return None
def _send_data(self, client_socket, data):
"""序列化并发送数据到客户端"""
try:
serialized = json.dumps(data).encode('utf-8')
client_socket.sendall(serialized)
except Exception as e:
print(f"发送数据时出错: {e}")
def _process_request(self, request):
"""处理客户端请求"""
command = request.get('command')
path = request.get('path', '')
destination = request.get('destination', '')
new_name = request.get('new_name', '')
# 规范化路径,防止路径遍历攻击
safe_path = self._sanitize_path(path)
safe_destination = self._sanitize_path(destination)
try:
if command == 'list':
return self._list_directory(safe_path)
elif command == 'open':
return self._open_path(safe_path)
elif command == 'copy':
return self._copy_file(safe_path, safe_destination)
elif command == 'move':
return self._move_file(safe_path, safe_destination)
elif command == 'delete':
return self._delete_file(safe_path)
elif command == 'create_folder':
return self._create_folder(safe_path)
elif command == 'rename':
return self._rename_file(safe_path, new_name)
else:
return {'success': False, 'message': f"未知命令: {command}"}
except Exception as e:
return {'success': False, 'message': str(e)}
def _sanitize_path(self, path):
"""规范化路径,防止路径遍历攻击"""
# 确保路径从服务器根目录开始
full_path = os.path.abspath(os.path.join(self.root_dir, path))
# 验证路径是否在服务器根目录内
if not full_path.startswith(self.root_dir):
raise PermissionError("访问被拒绝")
return full_path
def _list_directory(self, path):
"""列出目录内容"""
if not os.path.exists(path):
return {'success': False, 'message': "路径不存在"}
if not os.path.isdir(path):
return {'success': False, 'message': "不是目录"}
items = []
try:
for item in os.listdir(path):
item_path = os.path.join(path, item)
try:
stats = os.stat(item_path)
modified_time = datetime.datetime.fromtimestamp(stats.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
if os.path.isdir(item_path):
item_type = "文件夹"
item_size = ""
else:
item_type = "文件"
# 格式化文件大小
size = stats.st_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
item_size = f"{size:.2f} {unit}"
break
size /= 1024.0
items.append({
'name': item,
'type': item_type,
'size': item_size,
'modified': modified_time
})
except Exception as e:
print(f"无法获取 {item} 的信息: {e}")
# 排序:文件夹在前,文件在后
items.sort(key=lambda x: (0 if x['type'] == '文件夹' else 1, x['name'].lower()))
return {'success': True, 'items': items}
except Exception as e:
return {'success': False, 'message': f"无法读取目录: {e}"}
def _open_path(self, path):
"""打开文件或目录"""
if not os.path.exists(path):
return {'success': False, 'message': "路径不存在"}
if os.path.isdir(path):
return self._list_directory(path)
else:
# 对于文件,返回文件信息
try:
stats = os.stat(path)
modified_time = datetime.datetime.fromtimestamp(stats.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
size = stats.st_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
item_size = f"{size:.2f} {unit}"
break
size /= 1024.0
return {
'success': True,
'is_file': True,
'name': os.path.basename(path),
'size': item_size,
'modified': modified_time
}
except Exception as e:
return {'success': False, 'message': f"无法获取文件信息: {e}"}
def _copy_file(self, source, destination):
"""复制文件或目录"""
if not os.path.exists(source):
return {'success': False, 'message': "源路径不存在"}
try:
if os.path.isdir(source):
# 确保目标目录不存在
if os.path.exists(destination):
return {'success': False, 'message': "目标路径已存在"}
shutil.copytree(source, destination)
else:
# 如果目标是目录,使用源文件名
if os.path.isdir(destination):
destination = os.path.join(destination, os.path.basename(source))
shutil.copy2(source, destination)
return {'success': True, 'message': "复制完成"}
except Exception as e:
return {'success': False, 'message': f"复制失败: {e}"}
def _move_file(self, source, destination):
"""移动文件或目录"""
if not os.path.exists(source):
return {'success': False, 'message': "源路径不存在"}
try:
# 如果目标是目录,使用源文件名
if os.path.isdir(destination):
destination = os.path.join(destination, os.path.basename(source))
shutil.move(source, destination)
return {'success': True, 'message': "移动完成"}
except Exception as e:
return {'success': False, 'message': f"移动失败: {e}"}
def _delete_file(self, path):
"""删除文件或目录"""
if not os.path.exists(path):
return {'success': False, 'message': "路径不存在"}
try:
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
return {'success': True, 'message': "删除完成"}
except Exception as e:
return {'success': False, 'message': f"删除失败: {e}"}
def _create_folder(self, path):
"""创建新文件夹"""
if os.path.exists(path):
return {'success': False, 'message': "路径已存在"}
try:
os.makedirs(path)
return {'success': True, 'message': "文件夹创建完成"}
except Exception as e:
return {'success': False, 'message': f"创建文件夹失败: {e}"}
def _rename_file(self, path, new_name):
"""重命名文件或目录"""
if not os.path.exists(path):
return {'success': False, 'message': "路径不存在"}
directory = os.path.dirname(path)
new_path = os.path.join(directory, new_name)
if os.path.exists(new_path):
return {'success': False, 'message': "新名称已存在"}
try:
os.rename(path, new_path)
return {'success': True, 'message': "重命名完成"}
except Exception as e:
return {'success': False, 'message': f"重命名失败: {e}"}
if __name__ == "__main__":
server = FileServer()
try:
server.start()
# 保持主线程运行
while True:
time.sleep(1)
except KeyboardInterrupt:
server.stop() 服务器无法连接到客户端