女神博客链接:https://www.cnblogs.com/Eva-J/articles/8244551.html
作业链接:https://www.cnblogs.com/Eva-J/articles/7642557.html
需求分析
1. 多用户同时登陆
2. 用户登陆,加密认证
3. 上传/下载文件,保证文件一致性
4. 传输过程中现实进度条
5. 不同用户家目录不同,且只能访问自己的家目录
6. 对用户进行磁盘配额、不同用户配额可不同
7. 用户登陆server后,可在家目录权限下切换子目录
8. 查看当前目录下文件,新建文件夹
9. 删除文件和空文件夹
10. 充分使用面向对象知识
11. 支持断点续传

代码目录结构,根据助教提的修改意见,将客户端和服务器的代码完全解耦,这样做是符合实际场景的。
client代码目录结构如下:

server代码目录结构如下:

client代码:
bin目录:
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 15:20 import os import sys BASE_DIR = os.path.dirname(os.path.dirname(__file__)) sys.path.append(BASE_DIR) from core.ftp_client import FtpClient as fc if __name__ == '__main__': fc().run()
conf目录:
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 10:30 import os import time BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # 服务器ip地址和端口 IP_PORT = ('127.0.0.1',8888) # 数据库路径 DB_PATH = os.path.join(BASE_DIR,'db') if not os.path.exists(DB_PATH):os.makedirs(DB_PATH) # 用户下载目录 DOWNLOAD = lambda name:os.path.join(DB_PATH,'%s_download') % (name) # 日志文件路径 LOG_PATH = os.path.join(BASE_DIR,'log') if not os.path.exists(LOG_PATH):os.makedirs(LOG_PATH) LOG_FILE = r'%s/%s-log' % (LOG_PATH,time.strftime('%Y-%m-%d', time.localtime(time.time())))
core目录:
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 10:33 import json from core.log import Log as log from core.client_common import Common as cn class ClinetAuth: '''客户端认证类''' def __init__(self,conn): self.conn = conn self.auth_dict = {} def __auth(self,operate): '''身份认证''' for i in range(3):# 3次登录 name = input('请输入用户名:').strip() password = input('请输入密码:').strip() # 为了安全起见,发送明文到服务器再加密,避免破解加密算法。 if 'login' == operate: self.auth_dict = {'operate':'login','name':name,'password':password} elif 'register' == operate: password2 = input('请再次输入密码:').strip() if password == password2: self.auth_dict = {'operate':'register','name':name,'password':password} else: log.debug('两次输入密码不一致!') continue cn.mySend(self.conn,self.auth_dict,True) # 将用户信息发送给服务器校验 self.auth_dict = cn.myRecv(self.conn,True) # 接收服务器校验完成的结果 log.readAndWrite(self.auth_dict['msg']) if self.auth_dict['flag']:break # 登录或注册成功了 return self.auth_dict def login(self): '''登录''' return self.__auth('login') def register(self): '''注册''' return self.__auth('register') def quit(self): log.debug('谢谢使用!') cn.mySend(self.conn,b'exit') self.conn.close() exit(-1) def main(self): '''主逻辑''' head = '*' * 20 + '\n欢迎来到FTP系统!\n' + '*' * 20 print('\033[35;1m%s\033[0m' % head) opt_list = [('登录','login'),('注册','register'),('退出','quit')] while True: for index,opt in enumerate(opt_list,1):print('\033[35;1m%s、%s\033[0m' % (index, opt[0])) # 打印操作列表信息 try: num = int(input( '请输入您要选择的操作序号:')) if hasattr(self,opt_list[num-1][1]):return getattr(self,opt_list[num-1][1])() # 反射 except ValueError as e: log.error('%s不是效数字!!' % e) except IndexError as e: log.error('%s\n请输入1-3的有效数字!!' % e)
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/3 14:35 import struct import json import sys import hashlib from core.log import Log as log class Common: '''公共类''' @staticmethod def mySend(conn, msgb, dic=False): '''发送数据时,解决粘包问题''' if dic: msgb = json.dumps(msgb).encode('utf-8') len_msg = len(msgb) pack_len = struct.pack('i', len_msg) conn.send(pack_len) conn.send(msgb) @staticmethod def myRecv(conn, dic=False): '''接收数据时,解决粘包问题''' pack_len = conn.recv(4) # struct机制,在发送数据前,加上固定长度4字节的头部 len_msg = struct.unpack('i', pack_len)[0] # 解包,得到元组。 msg_b = conn.recv(len_msg) if dic: msg_b = json.loads(msg_b.decode('utf-8')) return msg_b @classmethod def showMessage(cls, conn, opt_dict): cls.mySend(conn, opt_dict, True) # 将执行命令发送给服务器,服务执行相应函数 opt_dict = cls.myRecv(conn, True) # 接收服务操作完成后返回的字典 log.debug(opt_dict['msg']) # 打印执行信息 @staticmethod def processBar(num, total): '''打印进度条''' rate = num / total rate_num = int(rate * 100) bar = ('>' * rate_num, rate_num,) # 展示的进度条符号 r = '\r%s>%d%%\n' % bar if rate_num == 100 else '\r%s>%d%%' % bar sys.stdout.write(r) # 覆盖写入 return sys.stdout.flush # 实时刷新 @classmethod def startTransfer(cls, conn, dic, kind, file, mode, b_size=1024000): '''开始传输,提取上传下载公共代码''' md5 = hashlib.md5() # 发送数据时,添加MD5校验,就不用再单独打开一次文件做校验了 if dic['exist_size']: log.debug('文件上次已经%s了%s字节,开始断点续传!' % (kind, dic['exist_size'])) with open(file, mode) as f: if kind == '上传': f.seek(dic['exist_size']) # 将指针移动到指定位置开始读 while dic['filesize'] > 0: if kind == '上传': line = f.read(b_size) conn.send(line) # 发生粘包也没有关系,反正最后把文件传完就行 elif kind == '下载': line = conn.recv(b_size) # 发生粘包也没有关系,反正最后把文件传完就行 f.write(line) dic['exist_size'] += len(line) # 累计发送文件大小,传输进度条用 dic['filesize'] -= len(line) # 退出循环用 cls.processBar(dic['exist_size'], dic['total_size']) md5.update(line) clinet_md5 = md5.hexdigest() # 自己的MD5值 cls.mySend(conn, clinet_md5.encode('utf-8')) # 发送MD5值给服务器做校验 opt_dic = cls.myRecv(conn, True) # 接收校验结果,返回字典 log.readAndWrite(opt_dic['msg']) return opt_dic @classmethod def startGetFile(cls, conn, dic): '''接收文件,客户端从服务器下载文件''' return cls.startTransfer(conn, dic, kind='下载', file=dic['download_file'], mode='ab') @classmethod def startPutFile(cls, conn, dic): '''发送文件,从客户端发送文件到服务器''' return cls.startTransfer(conn, dic, kind='上传', file=dic['file_path'], mode='rb')
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 15:23 import socket import os import time from conf import settings as ss from core.log import Log as log from core.client_common import Common as cn from core.client_auth import ClinetAuth class FtpClient: '''FTP客户端类''' def __init__(self): self.sk = socket.socket() self.sk.connect(ss.IP_PORT) def putFile(self): '''客户端上传文件到服务器''' file_path = input('请输入要上传到服务器的文件路径:').strip() if not os.path.isfile(file_path):# 客户端上传文件,自己先判断文件是否存在 cn.mySend(self.sk,b'error') return log.error('%s文件不存在!' % file_path) total_size = os.path.getsize(file_path) # 获取文件大小 opt_dict = {'operate':'putFile', 'file_path':file_path, 'total_size':total_size, 'name':self.name} cn.mySend(self.sk,opt_dict,True) # 将执行命令发送给服务器,服务执行相应函数 opt_dict = cn.myRecv(self.sk,True)# 接收服务器回应信息 log.debug(opt_dict['msg']) if opt_dict['flag']:# 该用户在服务器的磁盘配额满足 log.debug('开始上传%s到服务器!' % opt_dict['file_path']) cn.startPutFile(self.sk,opt_dict) def getFile(self): '''客户端从服务器下载文件''' file_path = input('请输入要从服务器下载的文件路径:').strip() download_path = ss.DOWNLOAD(self.name) # 指定下载目录 if not os.path.exists(download_path):os.makedirs(download_path) download_file = os.path.join(download_path,os.path.basename(file_path)) # 下载到客户端本地的文件路径 exist_size = os.path.getsize(download_file) if os.path.exists(download_file) else 0 # 判断本地文件是否存在,做断点续传 opt_dict = {'operate':'getFile', 'file_path':file_path, 'download_file':download_file, 'exist_size':exist_size, 'name':self.name} cn.mySend(self.sk,opt_dict,True) # 将执行命令发送给服务器,服务执行相应函数 opt_dict = cn.myRecv(self.sk,True) # 接收服务器回应信息 if opt_dict['flag']: # 文件存在 log.debug('开始服务器中下载文件%s' % opt_dict['file_path'] ) cn.startGetFile(self.sk,opt_dict) else: log.debug(opt_dict['msg']) def viewDir(self): '''查看服务器当前目录''' opt_dict = {'operate':'viewDir', 'name':self.name} cn.showMessage(self.sk,opt_dict) def mkdir(self): '''创建目录''' dirname = input('请输入新建文件夹名称:') opt_dict = {'operate':'mkdir', 'name':self.name,'dirname':dirname} cn.showMessage(self.sk,opt_dict) def rmdir(self): '''删除空目录''' dirname = input('请输入要删除的空文件夹名称:') opt_dict = {'operate':'rmdir', 'name':self.name,'dirname':dirname} cn.showMessage(self.sk,opt_dict) def rmfile(self): '''删除文件''' filename = input('请输入要删除的文件名称:') opt_dict = {'operate':'rmfile', 'name':self.name,'filename':filename} cn.showMessage(self.sk,opt_dict) def changeDir(self): '''切换子目录''' dirname = input('请输入切换目录名称:') opt_dict = {'operate':'changeDir', 'name':self.name,'dirname':dirname} cn.showMessage(self.sk,opt_dict) def quit(self): log.debug('谢谢使用!') cn.mySend(self.sk,b'exit') self.sk.close() exit(-1) def clientView(self): '''客户端视图''' head = '*' * 20 + '\n欢迎使用FTP服务器!\n' + '*' * 20 opt_list = [('上传文件','putFile'),('下载文件','getFile'),('查看当前目录信息','viewDir'),('创建目录','mkdir'), ('删除空目录','rmdir'),('删除文件','rmfile'),('切换子目录','changeDir'),('退出','quit')] while True: print('\033[35;1m%s\033[0m' % head) for index,opt in enumerate(opt_list,1):print('\033[35;1m%s、%s\033[0m' % (index, opt[0])) # 打印操作列表信息 try: num = int(input( '请输入您要选择的操作序号:')) if hasattr(self,opt_list[num-1][1]):getattr(self,opt_list[num-1][1])() # 反射 except ValueError as e: log.error('%s不是效数字!!' % e) except IndexError as e: log.error('%s\n请输入1-12的有效数字!!' % e) print('3秒后自动跳转回主页面!') time.sleep(3) def run(self): '''身份认证''' opt_dict = ClinetAuth(self.sk).main() if opt_dict['flag']: self.name = opt_dict['name'] self.clientView() else: self.quit()
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/8/26 15:37 import logging import sys import time from logging import handlers from conf import settings as ss class Log(object): ''' https://cloud.tencent.com/developer/article/1354396 ''' now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) sh = logging.StreamHandler() # 既打印输入又写入文件 # rh = handlers.RotatingFileHandler(ss.log_file, maxBytes=1024,backupCount=5) # 按大小切换日志,保留5份 fh = handlers.TimedRotatingFileHandler(filename=ss.LOG_FILE, when='D', backupCount=5, interval=5,encoding='utf-8') # 按时间切割日志 logging.basicConfig(level=logging.WARNING, # 打印日志级别 handlers=[fh, sh], datefmt='%Y-%m-%d %H:%M:%S', format='%(asctime)s - %(name)s - %(levelname)s -%(module)s: %(message)s') # [%(lineno)d] 只显示当前文件的行号 @staticmethod def writeOnly(content): '''自定义函数,只写入日志文件''' with open(ss.LOG_FILE, mode='a', encoding='utf-8') as f: f.write(Log.now_time + '\t' + str(content) + '\n') @staticmethod def readOnly(content): '''自定义函数,只打印日志''' print('\033[36;1m%s\033[0m' % content) @classmethod def readAndWrite(cls,content): '''自定义函数,既打印信息又记录log文件''' cls.readOnly(content) cls.writeOnly('[INFO]\t' + content) @classmethod def debug(cls, content): # return logging.debug(content) return cls.readOnly(content) @classmethod def info(cls, content): # return logging.info(content) return cls.writeOnly('[INFO]\t' + content) # info信息直接写入log文件 @staticmethod def warning(content): return logging.warning(content) @staticmethod def error(content): # 获取调用函数的文件名和行数 head = '%s line%s error!\n' % (sys._getframe().f_back.f_code.co_filename, sys._getframe().f_back.f_lineno) return logging.error(head + content) @staticmethod def critical(content): head = '%s line%s critical!\n' % (sys._getframe().f_back.f_code.co_filename, sys._getframe().f_back.f_lineno) return logging.critical(head + content)
server代码:
bin目录:
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 15:21 import os import sys BASE_DIR = os.path.dirname(os.path.dirname(__file__)) sys.path.append(BASE_DIR) from core import ftp_server as fs if __name__ == '__main__': fs.runServer()
conf目录:
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 10:30 import os import time BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # 服务器ip地址和端口 IP_PORT = ('127.0.0.1',8888) # 默认用户磁盘配额大小为1G,化为字节l QUOTA = '1073741824' # 数据库路径 DB_PATH = os.path.join(BASE_DIR,'db') if not os.path.exists(DB_PATH):os.makedirs(DB_PATH) # 用户信息文件 USER_FILE = os.path.join(DB_PATH,'users_info') # 用户家目录 USER_HOME = lambda name:os.path.join(DB_PATH,'users_home',name) # 用户上传文件指定目录 UPLOAD = lambda name:os.path.join(USER_HOME(name),'upload') # 日志文件路径 LOG_PATH = os.path.join(BASE_DIR,'log') if not os.path.exists(LOG_PATH):os.makedirs(LOG_PATH) LOG_FILE = r'%s/%s-log' % (LOG_PATH,time.strftime('%Y-%m-%d', time.localtime(time.time())))
core目录:
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 10:33 import os import hashlib from conf import settings as ss from core.log import Log as log class ServerAuth: '''服务器认证类''' @staticmethod def writeInfo(file, content): '''写入信息''' with open(file, mode='a', encoding='utf-8') as f: f.write(content) @staticmethod def readInfo(file): '''读取信息''' if not os.path.exists(ss.USER_FILE): return with open(file, mode='r', encoding='utf-8') as f: for line in f: usr, pwd, quota = line.strip().split('|') yield usr, pwd, quota @staticmethod def changeMD5(content, name): '''MD5加密''' md5 = hashlib.md5(('MD5加盐,加上用户%s' % name).encode('utf-8')) md5.update(content.encode('utf-8')) return md5.hexdigest() @classmethod def login(cls, opt_dict): '''登录''' opt_dict['password'] = cls.changeMD5(opt_dict['password'], opt_dict['name']) # 将密码转换成密文 for n, p, q in cls.readInfo(ss.USER_FILE): if opt_dict['name'] == n and opt_dict['password'] == p: opt_dict['flag'] = True opt_dict['msg'] = '%s登录成功!' % opt_dict['name'] break else: opt_dict['flag'] = False opt_dict['msg'] = '%s登录失败!' % opt_dict['name'] log.readAndWrite(opt_dict['msg']) return opt_dict @classmethod def register(cls, opt_dict): '''注册''' opt_dict['password'] = cls.changeMD5(opt_dict['password'], opt_dict['name']) # 将密码转换成密文 for n, p, q in cls.readInfo(ss.USER_FILE): if opt_dict['name'] == n: opt_dict['flag'] = False opt_dict['msg'] = '%s用户已存在,请重新注册!' % opt_dict['name'] break else: content = opt_dict['name'] + '|' + opt_dict['password'] + '|' + ss.QUOTA + '\n' cls.writeInfo(ss.USER_FILE, content) opt_dict['flag'] = True opt_dict['msg'] = '%s注册成功!' % opt_dict['name'] log.readAndWrite(opt_dict['msg']) return opt_dict
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/3 14:35 import struct import json import os import sys import hashlib from core.server_auth import ServerAuth as sa from core.log import Log as log from conf import settings as ss class Common: '''公共类''' @staticmethod def mySend(conn, msgb, dic=False): '''发送数据时,解决粘包问题''' if dic: msgb = json.dumps(msgb).encode('utf-8') len_msg = len(msgb) pack_len = struct.pack('i', len_msg) conn.send(pack_len) conn.send(msgb) @staticmethod def myRecv(conn, dic=False): '''接收数据时,解决粘包问题''' pack_len = conn.recv(4) # struct机制,在发送数据前,加上固定长度4字节的头部 len_msg = struct.unpack('i', pack_len)[0] # 解包,得到元组。 msg_b = conn.recv(len_msg) if dic: msg_b = json.loads(msg_b.decode('utf-8')) return msg_b @staticmethod def processBar(num, total): '''打印进度条''' rate = num / total rate_num = int(rate * 100) bar = ('>' * rate_num, rate_num,) # 展示的进度条符号 r = '\r%s>%d%%\n' % bar if rate_num == 100 else '\r%s>%d%%' % bar sys.stdout.write(r) # 覆盖写入 return sys.stdout.flush # 实时刷新 @staticmethod def updateQuota(file, name, quota_new): '''更新磁盘配额''' with open(file, mode='r', encoding='utf-8') as f1, open(file + '.bak', mode='w', encoding='utf-8') as f2: for line in f1: if line.strip(): if name in line: usr, pwd, quota_old = line.split('|') line = usr + '|' + pwd + '|' + quota_new + '\n' f2.write(line) os.remove(file) os.rename(file + '.bak', file) @staticmethod def checkQuota(file, dic): '''检查磁盘配额''' for n, p, q in sa.readInfo(file): if dic['name'] == n: dic['msg'] = '用户%s当前磁盘配额剩余:%s字节\n上传文件大小为:%s字节' % (dic['name'], q, dic['filesize']) num = int(q) - int(dic['filesize']) dic['flag'] = False if num < 0 else True if not dic['flag']: dic['msg'] = '%s用户磁盘配额不足!\n' % dic['name'] + dic['msg'] dic['total'] = q dic['quota'] = str(num) return dic @classmethod def startTransfer(cls, conn, dic, kind, file, mode, b_size=1024000): '''开始传输,提取上传下载公共代码''' md5 = hashlib.md5() # 发送数据时,添加MD5校验,就不用再单独打开一次文件做校验了 if dic['exist_size']: log.debug('文件上次已经%s了%s字节,开始断点续传!' % (kind, dic['exist_size'])) with open(file, mode) as f: if kind == '下载': f.seek(dic['exist_size']) # 将指针移动到指定位置开始读 while dic['filesize'] > 0: if kind == '下载': line = f.read(b_size) conn.send(line) # 发生粘包也没有关系,反正最后把文件传完就行 elif kind == '上传': line = conn.recv(b_size) # 发生粘包也没有关系,反正最后把文件传完就行 f.write(line) dic['exist_size'] += len(line) # 累计发送文件大小,传输进度条用 dic['filesize'] -= len(line) # 退出循环用 cls.processBar(dic['exist_size'], dic['total_size']) md5.update(line) dic['server_md5'] = md5.hexdigest() # 自己发送数据的MD5值 dic['client_md5'] = cls.myRecv(conn).decode('utf-8') # 接收对方的MD5值 dic['msg'] = 'MD5校验OK,文件传输成功!' if dic['client_md5'] == dic['server_md5'] else 'MD5不一致,文件传输失败!' if not dic['msg'].find('成功') < 0 and kind == '上传': cls.updateQuota(ss.USER_FILE, dic['name'], dic['quota']) # 传输成功时更新磁盘配额 dic['msg'] = dic['msg'] + '\n文件上传位置:' + dic['upload_file'] + '\nMD5值为:' + dic['server_md5'] + '\n磁盘配额剩余:%s字节' % dic['quota'] elif not dic['msg'].find('成功') < 0 and kind == '下载': dic['msg'] = dic['msg'] + '\n文件下载位置:' + dic['download_file'] + '\nMD5值为:' + dic['server_md5'] log.readAndWrite(dic['msg']) cls.mySend(conn, dic, True) return dic @classmethod def startGetFile(cls, conn, dic): '''客户端从服务器下载文件''' return cls.startTransfer(conn, dic, kind='下载', file=dic['file_path'], mode='rb') @classmethod def startPutFile(cls, conn, dic): '''从客户端上传文件到服务器''' return cls.startTransfer(conn, dic, kind='上传', file=dic['upload_file'], mode='ab')
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/9/2 15:24 import socketserver import json import os from conf import settings as ss from core.log import Log as log from core.server_common import Common as cn from core.server_auth import ServerAuth as sa class FtpServer(socketserver.BaseRequestHandler): def login(self, opt_dict): '''登录''' opt_dict = sa.login(opt_dict) cn.mySend(self.request, opt_dict, True) if opt_dict['flag']: return self.userHome(opt_dict) def register(self, opt_dict): '''注册''' opt_dict = sa.register(opt_dict) cn.mySend(self.request, opt_dict, True) if opt_dict['flag']: return self.userHome(opt_dict) def userHome(self, opt_dict): '''用户家目录''' self.pwd_path = ss.USER_HOME(opt_dict['name']) if not os.path.exists(self.pwd_path): os.makedirs(self.pwd_path) os.chdir(self.pwd_path) def viewDir(self, opt_dict): '''查看当前目录''' opt_dict['msg'] = '服务器的%s目录信息如下:' % self.pwd_path for index, name in enumerate(os.listdir(self.pwd_path), 1): path = os.path.join(self.pwd_path, name) if os.path.isfile(path): opt_dict['msg'] += '\n文件%s:%s' % (index, name) elif os.path.isdir(path): opt_dict['msg'] += '\n目录%s:%s' % (index, name) return cn.mySend(self.request, opt_dict, True) def mkdir(self, opt_dict): '''创建目录''' if os.path.exists(os.path.abspath(opt_dict['dirname'])): opt_dict['msg'] = '%s目录已存在!' % opt_dict['dirname'] else: os.mkdir(opt_dict['dirname']) opt_dict['msg'] = '%s目录创建成功!' % os.path.abspath(opt_dict['dirname']) return cn.mySend(self.request, opt_dict, True) def rmdir(self, opt_dict): '''删除空目录''' try: os.rmdir(os.path.abspath(opt_dict['dirname'])) opt_dict['msg'] = '%s目录删除成功!' % opt_dict['dirname'] except OSError as e: opt_dict['msg'] = '目录不存在或目录不为空!\n%s' % e return cn.mySend(self.request, opt_dict, True) def rmfile(self, opt_dict): '''删除文件''' name = os.path.abspath(opt_dict['filename']) if os.path.isfile(name): os.remove(name) opt_dict['msg'] = '%s文件删除成功!' % name else: opt_dict['msg'] = '%s文件不存在!' % name return cn.mySend(self.request, opt_dict, True) def changeDir(self, opt_dict): '''切换子目录''' name = os.path.abspath(opt_dict['dirname']) if os.path.isdir(name): os.chdir(name) self.pwd_path = name opt_dict['msg'] = '已切换到%s' % name else: opt_dict['msg'] = '%s目录不存在!' % name return cn.mySend(self.request, opt_dict, True) def getFile(self, opt_dict): '''客户端从服务器下载文件''' if not os.path.exists(opt_dict['file_path']): # 判断服务器上是否存在该文件 opt_dict['flag'] = False opt_dict['msg'] = '%s文件不存在!' % opt_dict['file_path'] log.readAndWrite(opt_dict['msg']) else: opt_dict['flag'] = True # 文件存在 opt_dict['total_size'] = os.path.getsize(opt_dict['file_path']) # 获取文件总字节大小 opt_dict['filesize'] = opt_dict['total_size'] - opt_dict['exist_size'] # 如果文件存在,获取要传输的文件剩余大小 cn.mySend(self.request, opt_dict, True) # 将文件基本信息反馈给客户端 if opt_dict['flag']: cn.startGetFile(self.request, opt_dict) # 文件存在,开始发送文件给客户端 def putFile(self, opt_dict): '''客户端上传文件到服务器''' put_path = ss.UPLOAD(opt_dict['name']) if not os.path.exists(put_path): os.makedirs(put_path) opt_dict['upload_file'] = os.path.join(put_path, os.path.basename(opt_dict['file_path'])) opt_dict['exist_size'] = os.path.getsize(opt_dict['upload_file']) if os.path.exists(opt_dict['upload_file']) else 0 # 判断上传文件服务器上是否存在,做断点续传 opt_dict['filesize'] = opt_dict['total_size'] - opt_dict['exist_size'] # 如果文件存在,获取要传输的文件剩余大小 opt_dict = cn.checkQuota(ss.USER_FILE, opt_dict) # 校验用户在服务器的磁盘配额 log.readAndWrite(opt_dict['msg']) cn.mySend(self.request, opt_dict, True) # 将文件基本信息反馈给客户端 if opt_dict['flag']: cn.startPutFile(self.request, opt_dict) # 开始接收客户端上传的文件 def handle(self): '''方法重写,重写handle函数,启动socketserver时执行该函数,看一下socketserver源码''' while True: try: log.readAndWrite("客户端%s已链接!" % (self.client_address[0])) log.debug('等待客户端发送操作命令...') dic_str = cn.myRecv(self.request).decode('utf-8') # 接收客户端发送指令 if not dic_str.find('exit') < 0: log.readAndWrite("客户端%s已主动断开链接!" % (self.client_address[0])) break elif not dic_str.find('error') < 0: log.warning('客户端操作错误!') # 客户端报错时,服务器不进行任何操作! else: opt_dict = json.loads(dic_str) if hasattr(self, opt_dict['operate']): getattr(self, opt_dict['operate'])(opt_dict) except ConnectionResetError as e: log.error("%s客户端已断开%s" % (self.client_address, e)) break def runServer(): '''启动FTP服务器''' server = socketserver.ThreadingTCPServer(ss.IP_PORT, FtpServer) server.serve_forever()
# -*- coding: utf-8 -*- __author__ = 'caiqinxiong_cai' # 2019/8/26 15:37 import logging import sys import time from logging import handlers from conf import settings as ss class Log(object): ''' https://cloud.tencent.com/developer/article/1354396 ''' now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) sh = logging.StreamHandler() # 既打印输入又写入文件 # rh = handlers.RotatingFileHandler(ss.log_file, maxBytes=1024,backupCount=5) # 按大小切换日志,保留5份 fh = handlers.TimedRotatingFileHandler(filename=ss.LOG_FILE, when='D', backupCount=5, interval=5,encoding='utf-8') # 按时间切割日志 logging.basicConfig(level=logging.WARNING, # 打印日志级别 handlers=[fh, sh], datefmt='%Y-%m-%d %H:%M:%S', format='%(asctime)s - %(name)s - %(levelname)s -%(module)s: %(message)s') # [%(lineno)d] 只显示当前文件的行号 @staticmethod def writeOnly(content): '''自定义函数,只写入日志文件''' with open(ss.LOG_FILE, mode='a', encoding='utf-8') as f: f.write(Log.now_time + '\t' + str(content) + '\n') @staticmethod def readOnly(content): '''自定义函数,只打印日志''' print('\033[36;1m%s\033[0m' % content) @classmethod def readAndWrite(cls,content): '''自定义函数,既打印信息又记录log文件''' cls.readOnly(content) cls.writeOnly('[INFO]\t' + content) @classmethod def debug(cls, content): # return logging.debug(content) return cls.readOnly(content) @classmethod def info(cls, content): # return logging.info(content) return cls.writeOnly('[INFO]\t' + content) # info信息直接写入log文件 @staticmethod def warning(content): return logging.warning(content) @staticmethod def error(content): # 获取调用函数的文件名和行数 head = '%s line%s error!\n' % (sys._getframe().f_back.f_code.co_filename, sys._getframe().f_back.f_lineno) return logging.error(head + content) @staticmethod def critical(content): head = '%s line%s critical!\n' % (sys._getframe().f_back.f_code.co_filename, sys._getframe().f_back.f_lineno) return logging.critical(head + content)
doc目录:
# 程序运行环境 ### Python 3.x版本 # 程序开始脚本 ### 1、先执行server/bin目录下的start_server.py,启动服务器 ### 2、再执行cline/bin目录下的start_client.py,启动客户端 # 账号信息 ### 支持多个客户端账号同时操作 账号1:xiaoqiang 密码:123 账号2:caiqinxiong 密码:cai 账号3:lixiaoxin 密码:li # 主要功能 ### 1、上传文件(支持断点上传) ### 2、下载文件(支持断点下载) ### 3、查看当前目录信息 ### 4、创建目录 ### 5、删除空目录 ### 6、删除文件 ### 7、切换子目录 ### 8、注册新用户
总结:基本功能完成,支持断点续传,用户磁盘配额,用户登录注册在服务器端完成,将重复代码进行提取优化等。
浙公网安备 33010602011771号