女神博客链接: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()
start_client

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())))
settings.py

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)
client_auth.py
# -*- 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')
client_common.py
# -*- 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()
ftp_client.py
# -*- 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)
log.py

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()
start_server.py

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())))
settings.py

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
server_auth.py
# -*- 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')
server_common.py
# -*- 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()
ftp_server.py
# -*- 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)
log.py

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、注册新用户
READERME.md

总结:基本功能完成,支持断点续传,用户磁盘配额,用户登录注册在服务器端完成,将重复代码进行提取优化等。

posted on 2019-09-16 11:21  雨之夜&秋  阅读(184)  评论(0)    收藏  举报