脱产班第五次大作业-FTP服务器

时间:2023-03-08 23:10:35
脱产班第五次大作业-FTP服务器

下载项目

my_ftp

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import hmac
import json
import struct
import hashlib
import socketserver
from core.log import log

class MyServer(socketserver.BaseRequestHandler):
    def pro_recv(self):
        num = self.request.recv(4)
        num = struct.unpack('i', num)[0]
        str_dic = self.request.recv(num).decode('utf-8')
        dic = json.loads(str_dic)
        return dic
    def pro_send(self, dic, pro=True):
        bytes_dic = json.dumps(dic).encode('utf-8')
        if pro:
            len_bytes = struct.pack('i', len(bytes_dic))
            self.request.send(len_bytes)
        self.request.send(bytes_dic)
    def get_md5(self, username=0, password=0):
        md5 = hashlib.md5(username.encode('utf-8'))
        md5.update(password.encode('utf-8'))
        return md5.hexdigest()
    @staticmethod
    def bytes2human(num):
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
        prefix = {}
        for i, s in enumerate(symbols):
            prefix[s] = 1 << (i + 1) * 10
        for s in reversed(symbols):
            if num >= prefix[s]:
                value = float(num) / prefix[s]
                return '%.1f%s' % (value, s)
        return '%sB' % num
    def makencrypt(secret_key):
        def outter(func):
            def inner(self, *args, **kwargs):
                func_ret = func(self, *args, **kwargs)
                rand = os.urandom(32)
                self.request.send(rand)
                h_mac = hmac.new(secret_key.encode('utf-8'), rand)
                res = h_mac.digest()
                ret = self.request.recv(1024)
                if ret == res:
                    log.logger.info('验证是合法的客户端')
                    return func_ret
                else:
                    log.logger.info('验证不是合法的客户端')
                    self.request.close()
                    return False
            return inner
        return outter
    def checkquota(func):
        def wrapper(self, *args, **kwargs):
            ret = func(self, *args, **kwargs)
            usedsize = self.getusedsize(args[1]['userhome'])
            quotasize = args[1]['userquota']
            human_used = self.bytes2human(usedsize)
            human_total = self.bytes2human(quotasize)
            dic = {'used':usedsize,'total':quotasize}
            self.pro_send(dic)
            if usedsize > quotasize:
                log.logger.warning(f'已经超过用户配额:使用{human_used},总共{human_total}')
            else:
                log.logger.info(f'使用{human_used},总共{human_total}')
            return ret
        return wrapper
    @staticmethod
    def getusedsize(path, usedsize=0):
        l = [path]
        while l:
            userhome = l.pop()
            lst = os.listdir(userhome)
            for name in lst:
                son_path = os.path.join(userhome, name)
                if os.path.isfile(son_path):
                    usedsize += os.path.getsize(son_path)
                else:
                    l.append(son_path)
        return usedsize
    @staticmethod
    def sortdic(dic):
        for k, v in dic.items():
            v.update({'username': k})
        return v
    @staticmethod
    def getfiles(path):
        dirlist = list()
        for file in os.listdir(path):
            if file.startswith('_'):
                pass
            if file.startswith('.'):
                pass
            elif os.path.isfile(os.path.join(path, file)):
                dirlist.append(('f', file))
            elif os.path.isdir(os.path.join(path, file)):
                dirlist.append(('d', file))
            else:
                pass
        return dirlist
    @makencrypt(secret_key='alexsb')
    def login(self, userdic, userfile='userinfo.json', userhome='home'):
        log.logger.info(f"用户{userdic['username']}开始登陆")

        with open(userfile, encoding='utf-8') as f:
            localdic = json.load(f)
            if localdic.get(userdic['username']):
                if localdic[userdic['username']]['password'] == self.get_md5(
                        userdic['username'], userdic['password']):
                    log.logger.info(f"用户{userdic['username']}登陆成功")
                    msg = f"用户{userdic['username']}登陆成功"
                    userdic['message'] = msg
                    userdic['actflag'] = True
                    self.pro_send(userdic)
                    return {userdic['username']: localdic[userdic['username']]}
                else:
                    log.logger.warning(f"用户{userdic['username']}登陆的用户名或密码错误,无法登陆")
                    msg = f"用户{userdic['username']}登陆的用户名或密码错误,无法登陆。"
                    userdic['message'] = msg
                    userdic['actflag'] = False
                    self.pro_send(userdic)
                    return False
            else:
                log.logger.warning(f"用户{userdic['username']}不存在,无法登陆")
                msg = f"用户{userdic['username']}不存在,无法登陆。"
                userdic['message'] = msg
                userdic['actflag'] = False
                self.pro_send(userdic)
                return False
    @makencrypt(secret_key='alexsb')
    def region(self, userdic, userfile='userinfo.json', userhome='home'):
        __default_userquota = 10485760  # 10M
        log.logger.info(f"用户{userdic['username']}开始注册")
        userhome = os.path.join(userhome, userdic['username'])
        if not os.path.exists(userhome):
            os.makedirs(userhome)
        # check local userinfo
        with open(userfile, mode='r', encoding=('utf-8')) as f:
            if os.path.getsize(userfile) == 0:
                localdic = dict()
            else:
                localdic = json.load(f)
        # check new userinfo
        if localdic.get(userdic['username']):
            log.logger.warning(f"用户{userdic['username']}已存在,无法注册")
            msg = f"用户{userdic['username']}已存在,无法注册。"
            userdic['message'] = msg,
            userdic['actflag'] = False,
            self.pro_send(userdic)
            return False
        else:
            with open(userfile, mode='w', encoding='utf-8') as f:
                log.logger.info(f"用户{userdic['username']}注册成功")
                msg = f"用户{userdic['username']}注册成功。"
                new_userinfo = {
                    userdic['username']: {
                        'password': self.get_md5(userdic['username'], userdic['password']),
                        'userquota': __default_userquota,
                        'userhome': userhome,
                        'userdir': userhome,
                    }}
                new_sendinfo = dict()
                new_sendinfo['actflag'] = True
                new_sendinfo['message'] = msg
                localdic.update(new_userinfo)
                json.dump(localdic,f,sort_keys=True,indent=2,separators=(',',':'),
                    ensure_ascii=False)
                self.pro_send(new_sendinfo)
                return new_userinfo
    @checkquota
    def upload(self, fileinfo=dict(), userdic=dict()):
        filepath = os.path.join(userdic['userdir'], fileinfo['filename'])
        break_point = False
        if os.path.exists(filepath):
            break_point = True
            received_size = os.path.getsize(filepath)
            if received_size == fileinfo['filesize']:
                self.pro_send({'continue':'hasfile'})
                return True
            else:
                self.pro_send({'continue':'send','received':received_size})
        else:
            self.pro_send({'continue': False})

        with open(filepath, 'ab') as f:
            md5 = hashlib.md5()
            filesize = fileinfo['filesize']
            if break_point: filesize -= received_size
            while filesize:
                content = self.request.recv(2048)
                if content == b'': break
                md5.update(content)
                f.write(content)
                filesize -= len(content)
        #check md5num
        self.pro_send({'md5num': md5.hexdigest()})
        client_md5 = self.pro_recv()
        if client_md5['md5num'] == md5.hexdigest():
            log.logger.info(f"文件{fileinfo['filename']}上传并校验完成")
            return True
        else:
            log.logger.warning(f"文件{fileinfo['filename']}上传完成但,校验失败")
            return False

    @checkquota
    def download(self, fileinfo=dict(), userdic=dict()):
        userdir = userdic['userdir']
        listdir = list()
        for file in os.listdir(userdir):
            if file.startswith('_'):
                pass
            if file.startswith('.'):
                pass
            elif os.path.isfile(os.path.join(userdir, file)):
                listdir.append(('f', file))
            elif os.path.isdir(os.path.join(userdir, file)):
                listdir.append(('d', file))
            else:
                pass
        fileinfo['downfiles'] = listdir
        self.pro_send(fileinfo)
        fileinfo = self.pro_recv()
        filepath = os.path.join(userdir, fileinfo['downfiles'])
        filename = os.path.basename(filepath)
        filesize = os.path.getsize(filepath)
        fileinfo['filename'] = filename
        fileinfo['filesize'] = filesize
        self.pro_send(fileinfo)

        send_stat = self.pro_recv()
        break_point = False
        if send_stat['continue'] == 'send':
            break_point = True
            received_size = send_stat['received']
            filesize -= received_size
        elif send_stat['continue'] == 'hasfile':
            return True

        with open(filepath, 'rb') as f:
            if break_point: f.seek(received_size)
            md5 = hashlib.md5()
            while filesize > 2048:
                content = f.read(2048)
                self.request.send(content)
                filesize -= 2048
                md5.update(content)
            else:
                content = f.read()
                self.request.send(content)
                md5.update(content)
        # check md5num
        file_name = fileinfo['filename']
        log.logger.info(f"文件{file_name}开始下载")
        fileinfo = self.pro_recv()
        if fileinfo['md5num'] == md5.hexdigest():
            log.logger.info(f"文件{file_name}下载并校验完成")
            self.pro_send({'md5check': True})
        else:
            log.logger.warning(f"文件{file_name}下载完成,但校验失败")
            self.pro_send({'md5check': False})

    def changedir(self, fileinfo=dict(), userdic=dict()):
        fileinfo = fileinfo
        userdir = userdic['userhome']
        self.getfiles(userdir)
        dic = {'dir':'/','files':self.getfiles(userdir)}
        self.pro_send(dic)
        nowdir = userdir
        while True:
            act = self.pro_recv()
            if act['act'] == 'q':
                log.logger.info(f"用户{userdic['username']}登出服务器")
                break
            elif act['act'] == 'ls':
                dic['files'] = self.getfiles(nowdir)
                self.pro_send(dic)
            elif act['act'] == 'cd':
                newdir = os.path.dirname(nowdir)
                changedir = newdir.split(userdir)[1]
                if changedir == '': changedir = '/'
                nowdir = newdir
                if nowdir == userdir:
                    dic = {'dir':'/','files':self.getfiles(userdir)}
                    self.pro_send(dic)
                else:
                    dic = {'dir':changedir,'files': self.getfiles(nowdir)}
                    self.pro_send(dic)
            elif act['act'] == 'mkdir':
                os.makedirs(os.path.join(nowdir,act['dir_name']))
                dic['files'] = self.getfiles(nowdir)
                self.pro_send(dic)
                log.logger.info(f"用户{userdic['username']}创建了{act['dir_name']}文件夹")
            elif act['act'] == 'rm':
                rmobj = act['rm_obj']
                path = os.path.join(nowdir,rmobj)
                if os.path.isdir(path):
                    try:
                        os.rmdir(path)
                    except OSError:
                        pass
                else:
                    os.remove(path)
                dic['files'] = self.getfiles(nowdir)
                self.pro_send(dic)
                if rmobj not in dic['files']:
                    log.logger.info(f"用户{userdic['username']}删除了{rmobj}")
            elif act['act'] == 'upload':
                userdic['userdir'] = nowdir
                fileinfo=self.pro_recv()
                self.upload(fileinfo,userdic)
            elif act['act'] == 'download':
                userdic['userdir'] = nowdir
                fileinfo = self.pro_recv()
                self.download(fileinfo,userdic)
            else:
                newdir = os.path.join(nowdir, act['act'][1])
                nowdir = newdir
                changedir = newdir.split(userdir)[1]
                dic = {'dir': changedir, 'files': self.getfiles(nowdir)}
                self.pro_send(dic)

Client

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import hmac
import json
import time
import socket
import struct
import hashlib

class FtpClient(object):
    def __init__(self, addr, port):
        self.addr = addr
        self.port = port
        self.sock = socket.socket()
        self.sock.connect((self.addr, self.port))
        self.sock.settimeout(10)

        welcom = self.sock.recv(1024).decode('utf-8')
        print(welcom if welcom != '' else '服务器连接失败。')

    def pro_recv(self):
        num = self.sock.recv(4)
        num = struct.unpack('i', num)[0]
        str_dic = self.sock.recv(num).decode('utf-8')
        dic = json.loads(str_dic)
        return dic

    def pro_send(self, dic, pro=True):
        bytes_dic = json.dumps(dic).encode('utf-8')
        if pro:
            len_bytes = struct.pack('i', len(bytes_dic))
            self.sock.send(len_bytes)
        self.sock.send(bytes_dic)

    def makencrypt(secret_key):
        def outter(func):
            def inner(self, *args, **kwargs):
                func_ret = func(self, *args, **kwargs)
                rand = self.sock.recv(32)
                h_mac = hmac.new(secret_key.encode('utf-8'), rand)
                res = h_mac.digest()
                self.sock.send(res)
                return func_ret
            return inner
        return outter

    @staticmethod
    def bytes2human(num):
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
        prefix = {}
        for i, s in enumerate(symbols):
            prefix[s] = 1 << (i + 1) * 10
        for s in reversed(symbols):
            if num >= prefix[s]:
                value = float(num) / prefix[s]
                return '%.1f%s' % (value, s)
        return '%sB' % num

    def getquota(func):
        def wrapper(self,*args,**kwargs):
            ret = func(self,*args,**kwargs)
            userquota = self.pro_recv()
            human_used = self.bytes2human(userquota['used'])
            human_total = self.bytes2human(userquota['total'])
            if userquota['used'] > userquota['total']:
                print(f'用户已经超过了配额,使用了{human_used},总共{human_total},请尽快充值账号')
            else:
                print(f'用户已经使用了{human_used},总共{human_total}')
            return ret
        return wrapper

    @staticmethod
    def progress(percent, width=50):
        '''进度打印功能'''
        if percent >= 100:
            percent = 100

        show_str = ('[%%-%ds]' % width) % (int(width *
                                               percent / 100) * "#")  # 字符串拼接的嵌套使用
        print('\r%s %d%%' % (show_str, percent), end='')

    @makencrypt(secret_key='alexsb')
    def login(self):
        print('开始登陆:')
        user = input('用户名>>').strip()
        pswd = input('密码>>>>').strip()
        self.pro_send(
            {'username': user, 'password': pswd, 'action': 'login', })
        actstat = self.pro_recv()
        print(actstat['message'])
        return actstat['actflag']

    @makencrypt(secret_key='alexsb')
    def region(self):
        print('开始注册:')
        user = input('用户名>>').strip()
        pswd = input('密码>>>>').strip()
        self.pro_send(
            {'username': user, 'password': pswd, 'action': 'region', })
        actstat = self.pro_recv()
        print(actstat['message'])
        return actstat['actflag']

    def logout(self):
        self.sock.close()
        exit()

    @getquota
    def upload(self, recv_size=0):
        file_name = input('输入你要上传的文件>>>').strip()
        if os.path.isfile(file_name):
            filename = os.path.basename(file_name)
            filesize = os.path.getsize(file_name)
            fileinfo = {
                'action': 'upload',
                'filename': filename,
                'filesize': filesize,
            }
            self.pro_send(fileinfo)

            send_stat = self.pro_recv()
            break_point = False
            if send_stat['continue'] == 'send':
                break_point = True
                received_size = send_stat['received']
                filesize -= received_size
                recv_size += received_size
            elif send_stat['continue'] == 'hasfile':
                return True

            with open(file_name, 'rb') as f:
                if break_point: f.seek(received_size)
                md5 = hashlib.md5()
                while filesize > 2048:
                    content = f.read(2048)
                    self.sock.send(content)
                    md5.update(content)
                    filesize -= len(content)
                    recv_size += len(content)
                    recv_per = int(100 * recv_size / fileinfo['filesize'])
                    self.progress(recv_per, width=35)
                else:
                    content = f.read()
                    md5.update(content)
                    self.sock.send(content)
                    recv_size += len(content)
                    recv_per = int(100 * recv_size / fileinfo['filesize'])
                    self.progress(recv_per, width=35)
            # check md5num
            server_md5 = self.pro_recv()['md5num']
            time.sleep(1)
            self.pro_send({'md5num':md5.hexdigest()})
            if server_md5 == md5.hexdigest():
                print(f"  文件{fileinfo['filename']}上传并校验完成。")
                return True
            else:
                print(f"  文件{fileinfo['filename']}上传完成但,校验失败。")
                return False
    @getquota
    def download(self, dir='download', recvsize=0):
        if not os.path.exists(dir):
            os.makedirs(dir)
        fileinfo = {
            'action': 'download',
        }
        self.pro_send(fileinfo)

        dic = self.pro_recv()
        print('服务器文件:')
        for index, file in enumerate(dic['downfiles'], 1):
            print(f'{index} {file[0]} {file[1]}')
        while True:
            filename = input('输入你想下载的文件名>>>').strip()
            if filename.isdigit():
                if int(filename) in range(len(dic['downfiles']) + 1):
                    filename = dic['downfiles'][int(filename) - 1]
                    if filename[0] == 'd':
                        print('无法下载文件夹,请重新选择文件。')
                        continue
                    break
                else:
                    print('请输入正确的序号.')
                    continue
            else:
                filename = filename
                break
        dic['downfiles'] = filename[1]
        self.pro_send(dic)
        dic = self.pro_recv()
        filepath = os.path.join(dir, dic['filename'])
        filesize = dic['filesize']

        if os.path.exists(filepath):
            received_size = os.path.getsize(filepath)
            if received_size == filesize:
                self.pro_send({'continue':'hasfile'})
                return True
            else:
                self.pro_send({'continue': 'send', 'received': received_size})
                filesize -= received_size
                recvsize += received_size
        else:
            self.pro_send({'continue': False})
        with open(filepath, 'ab') as f:
            md5 = hashlib.md5()
            while filesize:
                content = self.sock.recv(2048)
                f.write(content)
                filesize -= len(content)
                recvsize += len(content)
                recv_per = int(100 * recvsize / dic['filesize'])  # 接收的比例
                self.progress(recv_per, width=35)  # 调用进度条函数,进度条的宽度默认设置为30
                md5.update(content)
        # check md5num
        fileinfo['md5num'] = md5.hexdigest()
        self.pro_send(fileinfo)
        ret = self.pro_recv()['md5check']
        if ret:
            print(f" 文件{dic['filename']}下载完成并校验结束")
            return True
        else:
            print(f" 文件{dic['filename']}校验出错")
            return False

    def changedir(self):
        fileinfo = {
            'action': 'changedir',
        }
        self.pro_send(fileinfo)

        server_dir = self.pro_recv()
        def showfile(server_dir):
            print('服务器目录'.center(20, '='))
            if server_dir['files'] == []:
                print('\033[1;31m 当前目录下没有文件 \033[0m!')
            else:
                for index, files in enumerate(server_dir['files'], 1):
                    print(f'索引:{index} 类型:{files[0]} 名称:{files[1]}')
            print(f"\033[7;22m当前目录为{server_dir['dir']}\033[0m")

            print(''.center(20,'-'))
        showfile(server_dir)
        while True:
            act = input('你要干哈?(cd/mkdir/ls/rm/upload/download/q)>').strip()

            if act.isdigit():
                if int(act) not in range(len(server_dir['files']) + 1):
                    print('\033[1;31m 请输入正确的序号 \033[0m!')
                    showfile(server_dir)
                    continue
                elif server_dir['files'][int(act) - 1][0] == 'f':
                    print('\033[1;31m 文件不能切换 \033[0m!')
                    showfile(server_dir)
                    continue
                else:
                    self.pro_send({'act': server_dir['files'][int(act) - 1]})
                    server_dir = self.pro_recv()
                    showfile(server_dir)

            elif act == 'cd':
                if server_dir['dir'] == '/':
                    print('\033[1;31m 当前已经为根目录无法进行切换 \033[0m!')
                    showfile(server_dir)
                else:
                    dic = {'act':'cd'}
                    self.pro_send(dic)
                    server_dir = self.pro_recv()
                    showfile(server_dir)
            elif act == 'mkdir':
                dic = {'act':'mkdir'}
                dir_name = input('你要创建的文件夹名称?').strip()
                dic['dir_name'] = dir_name
                if server_dir['files'] == []:
                    server_files = []
                else:
                    server_files = [obj[1] for obj in server_dir['files']]

                if dir_name in server_files:
                    print('\033[1;31m 文件夹创建失败,存在同名文件夹或文件 \033[0m!')
                else:
                    self.pro_send(dic)
                    print(f'文件夹{dir_name}创建完成')
                    server_dir=self.pro_recv()

            elif act == 'rm':
                dic = {'act':'rm'}
                rmobj = input('请输入你要删除的文件或空文件夹?').strip()

                if server_dir['files'] == []:
                    server_files = []
                    print(f"{rmobj}不存在无法删除")
                    self.pro_send({'act':'q'})
                    return False
                else:
                    server_files = [obj[1] for obj in server_dir['files']]
                    if rmobj not in server_files:
                        print(f"{rmobj}不存在无法删除")
                        self.pro_send({'act':'q'})
                        return False

                dic['rm_obj'] = rmobj
                self.pro_send(dic)

                server_dir = self.pro_recv()
                if server_dir['files'] == []:
                    server_files = []
                else:
                    server_files = [obj[1] for obj in server_dir['files']]
                if rmobj not in server_files:
                    print(f'\033[1;31m{rmobj}删除完成\033[0m!')
                else:
                    print(f'\033[1;31m{rmobj}无法删除,不是空文件夹\033[0m!')

            elif act == 'upload':
                dic = {'act':'upload'}
                self.pro_send(dic)
                self.upload()
            elif act == 'download':
                dic = {'act':'download'}
                self.pro_send(dic)
                self.download()
            elif act == 'ls':
                dic = {'act':'ls'}
                self.pro_send(dic)
                time.sleep(1)
                server_dir = self.pro_recv()
                showfile(server_dir)
            elif act == 'q':
                self.pro_send({'act': 'q'})
                break
            else:
                print('\033[1;31m请输入文件夹序号\033[0m!')
                continue

    def run(self):
        print('1.登陆\n2.注册\n3.退出')
        act = input('>>>').strip()
        act_dic = {
            '1': 'login', '登陆': 'login', 'login': 'login',
            '2': 'region', '注册': 'region', 'region': 'region',
            '3': 'logout', '退出': 'logout',
        }
        while True:
            if act.isdigit():
                if act in act_dic.keys():
                    if hasattr(self, act_dic[act]):
                        ret = getattr(self, act_dic[act])()
                        break
                else:
                    continue
            else:
                continue

        if ret:
            try:
                menudic = self.pro_recv()
            except Exception as e:
                self.sock.close()
                exit('远程终端出错:%s' % e)
            print(menudic['welcom'])
            del menudic['welcom']
            # for index, menu in enumerate(menudic, 1):
            #     print(index, menudic[menu])

            # act = input('>>>').strip()
            act = 'changedir'
            act_dic = {
                '1': 'upload', '上传': 'upload', 'U': 'upload', 'u': 'upload', 'upload': 'upload',
                '2': 'download', '下载': 'download', 'D': 'download', 'd': 'download', 'download': 'download',
                '3': 'changedir', '切换': 'changedir', '切换目录': 'changedir', 'c': 'changedir','changedir':'changedir',
            }
            if hasattr(self, act_dic[act]):
                ret = getattr(self, act_dic[act])()
                if ret:
                    print(act_dic[act]+'-done.')
            self.sock.close()

if __name__ == '__main__':
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.append(os.path.join(BASE_DIR,'SocketServer'))
    # print(sys.path)
    from conf import settings
    client = FtpClient('127.0.0.1', settings.PORT)
    client.run()