python实现ftp服务器
服务端
import socket import hashlib import struct import os import setting class FtpClient(object): features = ['get', 'put', 'ls', 'cd', 'mkdir', 'rm'] # 客户端提供的命令提供功能 def __init__(self, server_ip, port, user, pwd): ''' :param server_ip: 服务器ip :param port: 服务器端口 :param user: 用户名 :param pwd: 密码 ''' self.server_ip = server_ip self.port = port self.user = user self.pwd = pwd self.sock = socket.socket() # 初始化时创建套接字 try: self.sock.connect((self.server_ip, self.port)) # 连接服务器 except ConnectionRefusedError: print('501', setting.CODE['501']) return self.pwd_hash() # 密码hash if not self.auth(): return # 验证失败 # self.run() # 登录成功继续操作 def pwd_hash(self): # 对用户输入密码进行hash md5 = hashlib.md5(setting.MD5_SALT.encode('utf-8')) md5.update(self.pwd.encode('utf-8')) self.pwd = md5.hexdigest() def auth(self): # 上传用户密码进行验证 account = ','.join([self.user, self.pwd]) self.sock.send(account.encode('utf-8')) auth_result = self.sock.recv(1024).decode() # 获取登录结果 if auth_result == '8000': print(auth_result, setting.CODE[auth_result]) self.run() else: print(auth_result, setting.CODE[auth_result]) self.sock.close() return False def run(self): while True: user_input = input('\n>>>').strip() # 用户输入命令 command = user_input.split() self.cmd = command[0] # 获取命令前缀 if self.cmd == 'exit': # 检测到exit则退出 self.sock.close() break else: if self.cmd in self.features: # 判断命令是否支持 self.command_s = user_input.encode('utf-8') func = getattr(self, self.cmd) # 使用反射获取命令对应方法 if len(command) == 1: if command[0] in ['get', 'put', 'cd', 'mkdir', 'rm']: print(setting.CODE['4000']) continue func() elif len(command) == 2: func(command[1]) else: print(setting.CODE['4000']) else: print(setting.CODE['4000']) def get(self, *args, **kwargs): self.send_cmd(self.command_s) filename = args[0] file_stat = self.sock.recv(4).decode('utf-8') # 接收文件是否找到状态码 if file_stat == '1000': tmp_file = filename + '.tmp' file_path = os.path.join(setting.DOWNLOAD_DIR, tmp_file) # 存放文件路径 real_path = os.path.join(setting.DOWNLOAD_DIR, filename) tmp_size = 0 if not os.path.exists(file_path): self.sock.sendall(b'2000') else: self.sock.sendall(b'2001') tmp_size = os.path.getsize(file_path) self.sock.sendall(self.struck_info(tmp_size)) file_size = struct.unpack('i', self.sock.recv(4))[0] # 接收文件大小 receive_size = 0 receive_size += tmp_size with open(file_path, 'ab') as f: while receive_size < file_size: if file_size - receive_size <= 1024: data = self.sock.recv(file_size - receive_size) else: data = self.sock.recv(1024) if not data: print('文件下载中断!') return receive_size += len(data) f.write(data) self.toolbar(receive_size, file_size) print() src_md5 = self.sock.recv(1024).decode('utf-8') dec_md5 = self.file_md5(file_path) if src_md5 == dec_md5: print('{}下载成功!'.format(filename)) os.rename(file_path, real_path) else: print('文件一致性校验失败,上传失败') os.remove(file_path) else: print(setting.CODE[file_stat]) def put(self, *args, **kwargs): ''' 上传文件,断点续传 :param args: :param kwargs: :return: ''' if args: # args为文件名 file_name = args[0] if os.path.exists(file_name): # 判断文件是否存在 self.send_cmd(self.command_s) file_size = os.path.getsize(file_name) self.sock.send(self.struck_info(file_size)) # 发送文件大小 ques = self.sock.recv(1024).decode('utf-8') # 获取是否需要断点续传 send_size = 0 # 发送的数据计数 with open(file_name, 'rb') as f: # 打开文件 if ques == 'all_file': # 发送整个文件 start_index = 0 else: start_index = int(ques) # 获取断点文件大小 f.seek(start_index) send_size += start_index for line in f: self.sock.sendall(line) # 发送数据 send_size += len(line) self.toolbar(send_size, file_size) # 进度条 print() src_md5 = self.file_md5(file_name) # 文件md5 self.sock.send(src_md5.encode('utf-8')) # 发送自己文件的md5 result = self.sock.recv(1024).decode('utf-8') # 获取md5比对结果, print(result) else: print('{} file not found!'.format(file_name)) def ls(self, *args, **kwargs): self.send_cmd(self.cmd.encode('utf-8')) ''' 接收打印服务器发送的当前目录信息 :param args: :param kwargs: :return: ''' info = self.sock.recv(4) info_size = struct.unpack('i', info)[0] # 接收数据长度 receive_size = 0 info = b'' while receive_size < info_size: # 循环接收 data_size = 1024 if info_size - receive_size < 1024: data_size = info_size - receive_size data = self.sock.recv(data_size) info += data receive_size += len(data) print(info.decode('utf-8')) # 打印目录结构 def cd(self, *args, **kwargs): ''' 切换目录 :param args: :param kwargs: :return: ''' self.send_cmd(self.command_s) print(setting.CODE[self.sock.recv(4).decode()]) # 打印切换结果 def mkdir(self, *args, **kwargs): ''' 创建目录 :param args: :param kwargs: :return: ''' self.send_cmd(self.command_s) status = self.sock.recv(1024).decode('utf-8') print(setting.CODE[status]) def rm(self, *args, **kwargs): ''' 删除文件或者目录 :param args: :param kwargs: :return: ''' self.send_cmd(self.command_s) status = self.sock.recv(1024).decode('utf-8') print(setting.CODE[status]) def struck_info(self, info_size): ''' 打包数据 :param info_size: :return: ''' return struct.pack('i', info_size) def send_cmd(self, cmd): ''' 发送命令长度包和命令信息包 :param cmd: :return: ''' self.sock.send(self.struck_info(len(cmd))) self.sock.send(cmd) def file_md5(self, file): ''' 获取文件md5 :param file: :return: ''' md5 = hashlib.md5() with open(file, 'rb') as f: for i in f: md5.update(i) return md5.hexdigest() def toolbar(self, current, total): ''' 打印进度条 :param current: :param total: :return: ''' val = current / total * 100 print('\r{}{:.2f}%'.format(int(val) // 2 * '>', val), end='') if __name__ == '__main__': user = input('请输入用户名:').strip() pwd = input('请输入密码').strip() cli = FtpClient('127.0.0.1', 8001, user, pwd)
客户端
import socket import hashlib import struct import os import setting class FtpClient(object): features = ['get', 'put', 'ls', 'cd', 'mkdir', 'rm'] # 客户端提供的命令提供功能 def __init__(self, server_ip, port, user, pwd): ''' :param server_ip: 服务器ip :param port: 服务器端口 :param user: 用户名 :param pwd: 密码 ''' self.server_ip = server_ip self.port = port self.user = user self.pwd = pwd self.sock = socket.socket() # 初始化时创建套接字 try: self.sock.connect((self.server_ip, self.port)) # 连接服务器 except ConnectionRefusedError: print('501', setting.CODE['501']) return self.pwd_hash() # 密码hash if not self.auth(): return # 验证失败 # self.run() # 登录成功继续操作 def pwd_hash(self): # 对用户输入密码进行hash md5 = hashlib.md5(setting.MD5_SALT.encode('utf-8')) md5.update(self.pwd.encode('utf-8')) self.pwd = md5.hexdigest() def auth(self): # 上传用户密码进行验证 account = ','.join([self.user, self.pwd]) self.sock.send(account.encode('utf-8')) auth_result = self.sock.recv(1024).decode() # 获取登录结果 if auth_result == '8000': print(auth_result, setting.CODE[auth_result]) self.run() else: print(auth_result, setting.CODE[auth_result]) self.sock.close() return False def run(self): while True: user_input = input('\n>>>').strip() # 用户输入命令 command = user_input.split() self.cmd = command[0] # 获取命令前缀 if self.cmd == 'exit': # 检测到exit则退出 self.sock.close() break else: if self.cmd in self.features: # 判断命令是否支持 self.command_s = user_input.encode('utf-8') func = getattr(self, self.cmd) # 使用反射获取命令对应方法 if len(command) == 1: if command[0] in ['get', 'put', 'cd', 'mkdir', 'rm']: print(setting.CODE['4000']) continue func() elif len(command) == 2: func(command[1]) else: print(setting.CODE['4000']) else: print(setting.CODE['4000']) def get(self, *args, **kwargs): self.send_cmd(self.command_s) filename = args[0] file_stat = self.sock.recv(4).decode('utf-8') # 接收文件是否找到状态码 if file_stat == '1000': tmp_file = filename + '.tmp' file_path = os.path.join(setting.DOWNLOAD_DIR, tmp_file) # 存放文件路径 real_path = os.path.join(setting.DOWNLOAD_DIR, filename) tmp_size = 0 if not os.path.exists(file_path): self.sock.sendall(b'2000') else: self.sock.sendall(b'2001') tmp_size = os.path.getsize(file_path) self.sock.sendall(self.struck_info(tmp_size)) file_size = struct.unpack('i', self.sock.recv(4))[0] # 接收文件大小 receive_size = 0 receive_size += tmp_size with open(file_path, 'ab') as f: while receive_size < file_size: if file_size - receive_size <= 1024: data = self.sock.recv(file_size - receive_size) else: data = self.sock.recv(1024) if not data: print('文件下载中断!') return receive_size += len(data) f.write(data) self.toolbar(receive_size, file_size) print() src_md5 = self.sock.recv(1024).decode('utf-8') dec_md5 = self.file_md5(file_path) if src_md5 == dec_md5: print('{}下载成功!'.format(filename)) os.rename(file_path, real_path) else: print('文件一致性校验失败,上传失败') os.remove(file_path) else: print(setting.CODE[file_stat]) def put(self, *args, **kwargs): ''' 上传文件,断点续传 :param args: :param kwargs: :return: ''' if args: # args为文件名 file_name = args[0] if os.path.exists(file_name): # 判断文件是否存在 self.send_cmd(self.command_s) file_size = os.path.getsize(file_name) self.sock.send(self.struck_info(file_size)) # 发送文件大小 ques = self.sock.recv(1024).decode('utf-8') # 获取是否需要断点续传 send_size = 0 # 发送的数据计数 with open(file_name, 'rb') as f: # 打开文件 if ques == 'all_file': # 发送整个文件 start_index = 0 else: start_index = int(ques) # 获取断点文件大小 f.seek(start_index) send_size += start_index for line in f: self.sock.sendall(line) # 发送数据 send_size += len(line) self.toolbar(send_size, file_size) # 进度条 print() src_md5 = self.file_md5(file_name) # 文件md5 self.sock.send(src_md5.encode('utf-8')) # 发送自己文件的md5 result = self.sock.recv(1024).decode('utf-8') # 获取md5比对结果, print(result) else: print('{} file not found!'.format(file_name)) def ls(self, *args, **kwargs): self.send_cmd(self.cmd.encode('utf-8')) ''' 接收打印服务器发送的当前目录信息 :param args: :param kwargs: :return: ''' info = self.sock.recv(4) info_size = struct.unpack('i', info)[0] # 接收数据长度 receive_size = 0 info = b'' while receive_size < info_size: # 循环接收 data_size = 1024 if info_size - receive_size < 1024: data_size = info_size - receive_size data = self.sock.recv(data_size) info += data receive_size += len(data) print(info.decode('utf-8')) # 打印目录结构 def cd(self, *args, **kwargs): ''' 切换目录 :param args: :param kwargs: :return: ''' self.send_cmd(self.command_s) print(setting.CODE[self.sock.recv(4).decode()]) # 打印切换结果 def mkdir(self, *args, **kwargs): ''' 创建目录 :param args: :param kwargs: :return: ''' self.send_cmd(self.command_s) status = self.sock.recv(1024).decode('utf-8') print(setting.CODE[status]) def rm(self, *args, **kwargs): ''' 删除文件或者目录 :param args: :param kwargs: :return: ''' self.send_cmd(self.command_s) status = self.sock.recv(1024).decode('utf-8') print(setting.CODE[status]) def struck_info(self, info_size): ''' 打包数据 :param info_size: :return: ''' return struct.pack('i', info_size) def send_cmd(self, cmd): ''' 发送命令长度包和命令信息包 :param cmd: :return: ''' self.sock.send(self.struck_info(len(cmd))) self.sock.send(cmd) def file_md5(self, file): ''' 获取文件md5 :param file: :return: ''' md5 = hashlib.md5() with open(file, 'rb') as f: for i in f: md5.update(i) return md5.hexdigest() def toolbar(self, current, total): ''' 打印进度条 :param current: :param total: :return: ''' val = current / total * 100 print('\r{}{:.2f}%'.format(int(val) // 2 * '>', val), end='') if __name__ == '__main__': user = input('请输入用户名:').strip() pwd = input('请输入密码').strip() cli = FtpClient('127.0.0.1', 8001, user, pwd)
服务端配置
SOCKET_INFO = { 'BindIP': '127.0.0.1', 'Port': 8001 } # TMP_SHARE_DIR = '/Users/zhangjin/2018/share_dir/' # TMP_USER_INFO = { # 'USERNAME': 'Louis', # 'PASSWORD': '1a2e43405eaf0fa52b5b12eadd2b5eaf', # 'HOMEPATH': '/Users/zhangjin/2018/share_dir/' # } TMP_USER_INFO = { 'Louis': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/Louis/'], 'zhangjin': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/zhangjin/'], 'zhangsan': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/zhangsan/'], 'lisi': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/lisi/'], 'wangwu': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/wangwu/'], } MD5_SALT = '@#%$GjajhdbkwJGTkl' CODE = { '8000': 'LOGIN_SUCCESS', '8001': 'LOGIN_FAILED', '8002': 'Already logged in, unable to log in repeatedly', '5000': 'NETWORK ERROR', '1000': 'FILE FOUND', '1001': 'FILE NOT FOUND OR TARGET IS A DIR', '1010': 'Directory successful changed', '1011': 'Directory not found, directory change failed', '1012': 'Already a top-level directory, directory change failed', '1013': 'Target must be a directory', '2000': 'TMP FILE NOT FOUND', '2001': 'TMP FILE FOUND', '3000': 'SUCCESSFULLY DELETE', '3001': 'DELETE FAILED', '3002': 'FILE OR DIR NOT FOUND', '4000': 'Invalid command', '6000': 'Create directory successful', '6001': 'Create directory failed, directory already exists' }
客户端配置
MD5_SALT = '@#%$GjajhdbkwJGTkl' CODE = { '8000': 'LOGIN_SUCCESS', '8001': 'LOGIN_FAILED', '8002': 'Already logged in, unable to log in repeatedly', '5000': 'NETWORK ERROR', '1000': 'FILE FOUND', '1001': 'FILE NOT FOUND OR TARGET IS A DIR', '1010': 'Directory successful changed', '1011': 'Directory not found, directory change failed', '1012': 'Already a top-level directory, directory change failed', '1013': 'Target must be a directory', '2000': 'TMP FILE NOT FOUND', '2001': 'TMP FILE FOUND', '3000': 'SUCCESSFULLY DELETE', '3001': 'DELETE FAILED', '3002': 'FILE OR DIR NOT FOUND', '4000': 'Invalid command', '6000': 'Create directory successful', '6001': 'Create directory failed, directory already exists' } DOWNLOAD_DIR = '/Users/zhangjin/2018/download/'
目录结构
readme
#注意,本程序在mac下开发,目前没有考虑windows系统下功能的兼容问题,如需测试请在类unix环境下运行。
功能需求:
1. 多用户同时登陆:socketserver (完成)
2. 用户登陆,加密认证:md5加密 (完成)
3. 上传/下载文件,保证文件一致性:md5加密 (完成)
4. 传输过程中现实进度条 (完成)
5. 不同用户家目录不同,且只能访问自己的家目录,上传下载时,必须在自己目录 (完成)
6. 对用户进行磁盘配额、不同用户配额可不同: 上传、下载之前做文件夹大小的判断。(未完成)
7. 用户登陆server后,可在家目录权限下切换子目录 (完成)
8. 查看当前目录下文件,新建文件夹 (完成)
9. 删除文件和空文件夹 (完成)
10. 充分使用面向对象知识+反射 (完成)
11. 支持断点续传 (完成)
目录结构
bin
start.py 程序启动目录
conf
setting.py *服务端配置信息及用户密码信息*
core
ftp_server.py 服务端主程序
db
ftp_client
ftp_client1.py 客户端程序1
ftp_client2.py 客户端程序2
setting.py 客户端配置信息
使用方法:
运行bin目录下的start.py 启动服务端
运行ftp_client目录下的ftp_client1.py 登录用户一进行操作
运行ftp_client目录下的ftp_client2.py 登录用户二进行操作
用户名密码:(用户名密码都在conf/setting.py中,密码全部都是123456)
zhangjin 123456
Louis 123456
功能说明:
登录:支持多用户登录,禁止登录状态的用户再次登录,第一次登录用户会自动创建家目录
上传下载:支持断点续传
get filename 下载文件
put filename 上传文件
切换目录: 只能在家目录中切换
cd dirname 切换目录
cd .. 返回上层目录
查看当前路径下的目录信息:
ls
删除文件或文件夹:支持删除非空文件夹
rm filename
rm dirname
#创建文件夹
mkdir dirname