python项目开发:ftp server开发

 

两年后再看之前写的代码才知道写的有多难看(虽然几乎是照着别人做的),因此特地重写了一下代码,新版代码地址:https://github.com/night-cruise/ftp_app

程序要求:

1.用户加密认证 (对用户名密码进行MD5验证)
2.允许同时多用户登陆 (使用socket server方法,为每个用户都创建一个信息文件
3.每个用户有自己的家目录,且只能访问自己的家目录(每个用户都建立一个单独的目录)
4.对用户进行磁盘配额,每个用户的可用空间不同(用户信息字典中添加磁盘配额这一参数)
5.允许用户在ftp server 上随意切换目录
6.允许用户查看当前目录下的文件
7.允许上传和下载文件,保证文件的一致性(MD5验证)
8.文件传输过程中显示进度条
9.附加功能:支持文件的断点续传

分析:
数据存储:为每个用户都建立一个文件存储用户名、密码(MD5)、磁盘配额大小,为每一个用户都建立一个家目录
业务逻辑功能:可切换目录、创建目录、可查看目录下的文件、可上传、下载文件(支持断点续传、显示进度条)

开发过程中遇到的问题:
客户端和服务端需要互相传递消息,这样编写一端的程序的时候需要知道另一端程序的结构,

于是要先建立好两端之间大致传递的消息,编写好一端后,再编写另一端

程序目录框架:

程序代码:
README:
程序框架:
|--ftp
  |--ftpclinet
    |--bin
      |--start #程序入口
    |--core
      |--ftpclient #程序主要逻辑
  |--ftpserver
    |--bin
      |--start #程序入口
    |--core
      |--main #程序主要逻辑
    |--conf
      |--settings #定义database、homes的路径
    |--homes
    |--database
    |--modules
      |--authendencate #登陆模块
      |--socket_server #socket通信模块
  |--README

## 状态码
    400 用户认证失败
    401 命令不正确
    402 文件不存在
    403 创建文件已经存在
    404 磁盘空间不够
    405 不续传

    200 用户认证成功
    201 命令可以执行
    202 磁盘空间够用
    203 文件具有一致性
    205 续传

    000 系统交互码
切换目录:cd  ..  返回上一级目录    cd dirname   进入dirname
         注:用户登录后默认进入家目录,只可在家目录下随意切换

创建目录:mkdir dirname

查看当前路径: pwd

查看当前路径下的文件名和目录名: dir

初始化程序是自动创建用户: {"东云博士":"111111","马孔多":"","江南":"333333","白早":"444444"}
View Code
ftpclient:
bin目录下的start:
# -*- coding:utf-8 -*-
#!/user/bin/env.python
#Author:Mr Wu

import os,sys
BASE_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_dir)
from core import ftpclient

if __name__ == '__main__':
    obj = ftpclient.ftp_client(("127.0.0.1",9999))
    obj.start()
View Code

       core目录下的ftpclient:

# -*- coding:utf-8 -*-
#!/user/bin/env.python
#Author:Mr Wu
import os,hashlib,socket,json,sys,time

class ftp_client(object):
    '''ftp client客户端'''
    def __init__(self,ip_port):
        self.ip_port = ip_port

    def connect(self):
        '''连接套接字地址'''
        self.client = socket.socket()
        self.client.connect(self.ip_port)

    def start(self):
        '''开始运行'''
        self.connect()
        while True:
            user_name = input("input your name>>>:")
            user_password = input("input your password>>>:")
            user_msg = "%s:%s"%(user_name,user_password)
            self.client.send(user_msg.encode("utf-8"))
            status_code = self.client.recv(1024).decode()
            if status_code == "400":
                print("[400]认证失败!")
                continue
            elif status_code == "200":
                print("[200]认证成功!")
                #self.user_name = user_name
                self.interaction()

    def interaction(self):
        '''开始交互'''
        while True:
            command = input("input command>>>:")
            if not command: continue
            get_command = command.split()[0]
            if hasattr(self,get_command):
                func = getattr(self,get_command)
                func(command)
            else:
                print("[401]命令错误!")

    def get(self,command):
        '''下载文件'''
        self.client.send(command.encode())
        status_code = self.client.recv(1024).decode()
        if status_code == "401":
            print("[401]命令错误")
        elif status_code == "201":
            filename = command.split()[1]
            if os.path.isfile(filename):
                self.client.send("403".encode())
                self.client.recv(1024)
                received_size = os.stat(filename).st_size
                self.client.send(str(received_size).encode())
                status_code = self.client.recv(1024).decode()
                if status_code == "205":
                    print("[205]可续传!")
                    self.client.send("000".encode())
                elif status_code == "405":
                    print("[405]文件完整,不可续传!")
                    return
            else:
                self.client.send("402".encode())
                received_size = 0
                print("[205]可下载!")
            m = hashlib.md5()
            f = open(filename,"wb")
            file_total_size = int(self.client.recv(1024).decode())
            self.client.send("000".encode())
            f.seek(received_size)
            while received_size < file_total_size:
                last_size = file_total_size - received_size
                if last_size < 1024:
                    size = last_size
                else:
                    size = 1024
                data = self.client.recv(size)
                m.update(data)
                f.write(data)
                self.__progress_bar(received_size,file_total_size,"下载中")
                received_size += len(data)
            f.close()
            received_md5 = self.client.recv(1024).decode()
            md5 = m.hexdigest()
            if received_md5 == md5:
                print("[203]文件具有一致性!")
            else:
                print("[403]文件不一致!")
    def put(self,command):
        '''上传文件'''
        self.client.send(command.encode("utf-8"))
        status_code = self.client.recv(1024).decode()
        if status_code == "201":
            filename = command.split()[1]
            if os.path.isfile(filename):
                file_total_size = os.stat(filename).st_size
                self.client.send(str(file_total_size).encode())
                status_code = self.client.recv(1024).decode()
                if status_code == "205":
                    self.client.send("000".encode())
                    status_code = self.client.recv(1024).decode()
                    if status_code == "202":
                        print("[205]可上传")
                    elif status_code == "404":
                        print("[404]目录空间不足~")
                        return
                elif status_code == "405":
                    print("[405]文件完整,不续传~")
                    return
            else:
                print("[402]文件不存在!")
                return

            self.client.send("000".encode())
            received_size = int(self.client.recv(1024).decode())
            m = hashlib.md5()
            f = open(filename,"rb")
            f.seek(received_size)
            for line in f:
                self.client.send(line)
                m.update(line)
                received_size += len(line)
                self.__progress_bar(received_size,file_total_size,"上传中")
            f.close()
            self.client.send(m.hexdigest().encode())
            status_code = self.client.recv(1024).decode()
            if status_code == "203":
                print("[203]文件具有一致性")
            else:
                print("[403]传输文件不一致!")
        elif status_code == "401":
            print("[401]命令错误!")
    def cd(self,command):
        '''切换目录'''
        self.client.send(command.encode("utf-8"))
        status_code = self.client.recv(1024).decode()
        if status_code == "201":
            print("[201]命令执行成功")
        elif status_code == "401":
            print("[401]命令错误")
    def dir(self,command):
        '''查看当前目录下的文件'''
        self.client.send(command.encode("utf-8"))
        status_code = self.client.recv(1024).decode()
        if status_code == "201":
            self.client.send("000".encode())
            data = self.client.recv(1024).decode()
            print(data)
        elif status_code == "401":
            print("[401]命令错误!")
    def mkdir(self,command):
        '''创建目录'''
        self.client.send(command.encode())
        status_code = self.client.recv(1024).decode()
        if status_code == "401":
            print("[401]命令错误")
        elif status_code == "403":
            print("[403]创建目录已存在!")
        elif status_code == "201":
            print("[201]创建目录成功!")
    def pwd(self,command):
        '''查看当前用户路径'''
        self.client.send(command.encode())
        status_code = self.client.recv(1024).decode()
        if status_code == "201":
            self.client.send("000".encode())
            data = self.client.recv(1024).decode()
            print(data)
        elif status_code == "401":
            print("[401]命令错误")

    def __progress_bar(self,received_size,file_total_size,mode):
        width = 50 #进度条长度
        percent = received_size/file_total_size #进度条百分比
        use_num = int(percent*width) #已使用的进度条长度
        space_num = int(width - use_num) #未使用的进度条长度
        percent = percent*100
        sys.stdout.write("%s[%s%s]%d%%\r"%(mode,use_num*"#",space_num*" ",percent))
        sys.stdout.flush()
        return
View Code

  ftpserver:

       bin目录下的start:

dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_dir)
from core import ftpserver

if __name__ == '__main__':
    ftpserver.My_ftp_server()
View Code

       core目录下的ftpserver:

# -*- coding:utf-8 -*-
#!/user/bin/env.python
#Author:Mr Wu
import sys,os,json,hashlib,socketserver

from conf import settings
from modules import socket_server


class My_ftp_server(object):
    '''ftp初始化'''
    def __init__(self):
        self.interaction()
    def interaction(self):
        self.create_user()
        self.create_dir()
        server = socketserver.ThreadingTCPServer(settings.Ip_port,socket_server.ftp_server)
        server.serve_forever()
    def create_user(self):
        '''创建用户信息文件'''
        for key in settings.users_dict:
            user_database = {}
            name = key
            password = settings.users_dict[name]
            password = self.hash(password)
            user_database["name"] = name
            user_database["password"] = password
            user_database["limit_size"] = settings.Disk_quota #默认磁盘配额
            user_database["home_dir"] = settings.homes_dir + r"\%s"%name #用户家目录
            user_db = settings.database_dir + r"\%s.db"%name
            if not os.path.isfile(user_db):
                f = open(user_db,"w")
                json.dump(user_database,f)
                f.close()

    def create_dir(self):
        '''创建用户家目录'''
        for key in settings.users_dict:
            name = key
            user_dir = settings.homes_dir + r"\%s"%name
            if not os.path.isdir(user_dir):
                os.popen("mkdir %s"%user_dir)

    def hash(self,password):
        '''MD5加密'''
        m = hashlib.md5()
        m.update(password.encode())
        return m.hexdigest()
View Code

       modules目录下的authendencate:

# -*- coding:utf-8 -*-
#!/user/bin/env.python
#Author:Mr Wu
import sys,os,json,hashlib

from conf import settings


class authendencate(object):
    '''用户登录模块'''
    def __init__(self,user_msg):
        self.user_msg = user_msg


    def auth_user(self):
        '''验证用户登陆'''
        msg_list = self.user_msg.split(":")
        user_name = msg_list[0]
        user_password = self.hash(msg_list[1])
        user_database = self.get_user_msg(user_name)
        if  user_database:
            if user_name == user_database["name"] and user_password == user_database["password"]:
                return user_database

    def get_user_msg(self,user_name):
        '''获取用户信息'''
        user_db = settings.database_dir + r"\%s.db"%user_name
        if os.path.isfile(user_db):
            f = open(user_db,"r")
            user_database = json.load(f)
            f.close()
            return user_database
    def hash(self,passwd):
        '''md5加密'''
        m = hashlib.md5()
        m.update(passwd.encode())
        return m.hexdigest()
View Code

       modules目录下的socket_server:

# -*- coding:utf-8 -*-
#!/user/bin/env.python
#Author:Mr Wu
import sys,os,socketserver,hashlib
from conf import settings
from modules import authendencate

class ftp_server(socketserver.BaseRequestHandler):
    '''ftp server端'''
    def handle(self):
        try:
            self.conn = self.request
            while True:
                user_msg = self.conn.recv(1024).decode()
                auth_result = self.auth(user_msg)
                status_code = auth_result[0]
                self.conn.send(status_code.encode("utf-8"))
                if status_code == "400": continue
                elif status_code == "200":
                    user_database = auth_result[1]
                    self.user_dir = user_database["home_dir"] #用户默认目录
                    self.home_dir = user_database["home_dir"] #用户家目录
                    self.limit_size = user_database["limit_size"] #用户磁盘配额
                    while True:
                        command = self.conn.recv(1024).decode()
                        get_command = command.split()[0]
                        if hasattr(self,get_command):
                            func = getattr(self,get_command)
                            func(command)
                        else:
                            self.conn.send("401".encode("utf-8"))
        except ConnectionResetError as e:
            print(e)

    def get(self,command):
        '''下载文件'''
        if len(command.split()) > 1:
            filename = command.split()[1]
            file_db = self.user_dir + r"\%s"%filename
            if os.path.isfile(file_db):
                file_total_size = os.stat(file_db).st_size
                self.conn.send("201".encode("utf-8"))
                status_code = self.conn.recv(1024).decode()
                if status_code == "402":
                    received_size = 0
                elif status_code == "403":
                    self.conn.send("000".encode())
                    received_size = int(self.conn.recv(1024).decode())
                    if received_size < file_total_size:
                        self.conn.send("205".encode())
                        self.conn.recv(1024)
                    else:
                        self.conn.send("405".encode())
                        return
                f = open(file_db,"rb")
                m = hashlib.md5()
                self.conn.send(str(file_total_size).encode())
                self.conn.recv(1024)
                f.seek(received_size)
                for line in f:
                    self.conn.send(line)
                    m.update(line)
                f.close()
                self.conn.send(m.hexdigest().encode())
            else:
                self.conn.send("402".encode("utf-8"))
        else:
            self.conn.send("401".encode("utf-8"))

    def put(self,command):
        '''上传文件'''
        if len(command) > 1:
            self.conn.send("201".encode())
            file_name = command.split()[1]
            file_db = self.user_dir + r"\%s"%file_name
            file_total_size = int(self.conn.recv(1024).decode())
            if os.path.isfile(file_db):
                received_size = os.stat(file_db).st_size
                if received_size < file_total_size:
                    self.conn.send("205".encode())
                else:
                    self.conn.send("405".encode())
                    return
            else:
                received_size = 0
                self.conn.send("205".encode())
            put_size = file_total_size - received_size
            status_code = self.__get_size(put_size)
            self.conn.recv(1024)
            if status_code == "202":
                self.conn.send("202".encode())
            else:
                self.conn.send("404".encode())
                return
            self.conn.recv(1024)
            self.conn.send(str(received_size).encode())
            m = hashlib.md5()
            f = open(file_db,"wb")
            f.seek(received_size)
            while received_size < file_total_size:
                last_size = file_total_size - received_size
                if last_size < 1024:
                    size = last_size
                else:
                    size = 1024
                data = self.conn.recv(1024)
                received_size += len(data)
                f.write(data)
                m.update(data)
            f.close()
            md5 = m.hexdigest()
            new_md5 = self.conn.recv(1024).decode()
            if md5 == new_md5:
                self.conn.send("203".encode())
        else:
            self.conn.send("401".encode())

    def cd(self,command):
        '''切换目录'''
        if len(command) > 1:
            dir_name = command.split()[1]
            dir_path = self.user_dir + r"\%s"%dir_name
            if dir_name == ".." :
                if len(self.user_dir) > len(self.home_dir):
                    self.conn.send("201".encode())
                    self.user_dir = os.path.dirname(self.user_dir)
                else:
                    self.conn.send("401".encode())
            elif os.path.isdir(dir_path):
                self.user_dir = dir_path
                self.conn.send("201".encode())

            else:
                self.conn.send("401".encode())
        else:
            self.conn.send("401".encode())
    def dir(self,command):
        '''查看当前目录下的文件'''
        if command == "dir":
            self.conn.send("201".encode())
            self.conn.recv(1024)
            data = os.popen("dir %s"%self.user_dir)
            self.conn.send(data.read().encode())
        else:
            print("401".encode())
    def mkdir(self,command):
        '''创建目录'''
        if len(command) > 1:
            filename = command.split()[1]
            dir_path = self.user_dir + r"\%s"%filename
            if not os.path.isdir(dir_path):
                self.conn.send("201".encode())
                os.popen("mkdir %s"%dir_path)
            else:
                self.conn.send("403".encode())
        else:
            self.conn.send("401".encode())
    def pwd(self,command):
        '''查看当前用户路径'''
        if command == "pwd":
            self.conn.send("201".encode())
            data = self.user_dir
            self.conn.recv(1024)
            self.conn.send(data.encode())
        else:
            self.conn.send("401".encode())

    def auth(self,user_msg):
        '''用户登陆'''
        obj = authendencate.authendencate(user_msg)
        auth_result = obj.auth_user()
        if not auth_result:
            return "400"
        else:
            return "200",auth_result

    def __get_size(self,file_size):
        '''计算目录空间大小'''
        dir_size = os.stat(self.home_dir).st_size + file_size
        if dir_size < self.limit_size:
            return "202"
        else:
            return "404"
View Code

       conf目录下的settings:

# -*- coding:utf-8 -*-
#!/user/bin/env.python
#Author:Mr Wu
import os,sys
BASE_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
'''用户信息的属主目录'''
database_dir = os.path.join(BASE_dir,"database")
'''用户目录的属主目录'''
homes_dir = os.path.join(BASE_dir,"homes")
'''用户信息字典'''
users_dict = {"东云博士":"111111","马孔多":"","江南":"333333","白早":"444444"}
'''默认磁盘配额'''
Disk_quota = 102400
'''ip地址和端口'''
Ip_port = ("0.0.0.0",9999)
View Code

程序运行示例:

断点续传:

 

 

 

 

 









posted @ 2018-11-23 20:47  元骑鲸  阅读(478)  评论(0编辑  收藏  举报