欢迎来到Louis的博客

人生三从境界:昨夜西风凋碧树,独上高楼,望尽天涯路。 衣带渐宽终不悔,为伊消得人憔悴。 众里寻他千百度,蓦然回首,那人却在灯火阑珊处。
扩大
缩小

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




posted on 2018-09-10 15:56  Louiszj  阅读(237)  评论(0编辑  收藏  举报

导航