作业:开发一直支持多用户在线的FTP程序

要求:

1.用户加密认证

2.允许同时多用户登录

3.每个用户有自己家目录,且只能访问自己的家目录

4.对用户进行磁盘配额,每个用户的可用空间不同

5.允许用户在ftp server上随意切换目录

6.允许用户查看当前目录下文件

7.允许上传和下载,保证文件一致性

8.文件传输过程中显示进度条

9.附加功能:支持文件的断点续传

作业:开发一个支持多用户在线的FTP程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server上随意切换目录
允许用户查看当前目录下文件

执行命令:

允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
附加功能:支持文件断点续传

3004 未授权
3014 已授权

1001 命令粘包命令

class Action:
  def __init__(self):
    self.is_login = False
    self.prev_home = '/User/Data'
    self.name = None #alex
    #/User/Data/alex

xiaohu = Action() #/User/Data/xiaohu
ls /User/Data/xiaohu

jinxin = Action() #/user/data/jinxin
ls /user/data/jinxin

obj1 = Action()
obj2 = Action()
os.cd(xiaohu.home)
结果 = subprocess.getoutput('ls xiaohu.home')

 

EasyServerFTP目录结构

smoke@smoke-GS70-2PC-Stealth:~/文档/DocumentFile/PycharmProjects/pythonProject$ tree EasyServerFTP/
EasyServerFTP/
├── bin
│   ├── __init__.py
│   └── program.py
├── config
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── __pycache__
│   │   ├── __init__.cpython-35.pyc
│   │   └── settings.cpython-35.pyc
│   ├── settings.py
│   └── settings.pyc
├── home
│   ├── alex
│   ├── eric
│   └── wupeiqi
│       ├── hello
│       │   └── h
│       └── tyrion11.gif
├── log
│   └── __init__.py
└── src
    ├── __init__.py
    ├── __init__.pyc
    ├── __pycache__
    │   ├── __init__.cpython-35.pyc
    │   └── service.cpython-35.pyc
    └── service.py

12 directories, 15 files

bin目录

program.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

from src import service

if __name__ == "__main__":
    service.MultiServer()

config目录

settings.py

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

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


BIND_HOST = '127.0.0.1'
BIND_PORT = 9992

USER_HOME = os.path.join(BASE_DIR, 'home')

USER_ACCOUNT = {
    'alex': {
        'password': 'alex123',
        'quotation': 1000000,
        'expire': '2016-01-22'
    },
    'rain': {
        'password': 'rain123',
        'quotation': 2000000,
        'expire': '2016-01-22'
    },
}

home目录

log目录

src目录

service.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import subprocess
import re
import os
import json
import socketserver

from config import settings

ACTION_CODE = {
    '1000': 'cmd',
    '2000': 'post',
    '3000': 'get',
}

REQUEST_CODE = {
    '1001': 'cmd info',
    '1002': 'cmd ack',
    '2001': 'post info',
    '2002': 'ACK(可以开始上传)',
    '2003': '文件已经存在',
    '2004': '续传',
    '2005': '不续传',
    '3001': 'get info',
    '3002': 'get ack',
    '4001': "未授权",
    '4002': "授权成功",
    '4003': "授权失败"
}


class Server(object):
    request_queue_size = 5

    def __init__(self):
        self.socket = socket.socket()
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            self.server_bind(settings.BIND_HOST, settings.BIND_PORT)

            self.server_activate()
        except Exception as e:
            print(e)
            self.server_close()

    def server_bind(self, ip, port):
        self.socket.bind((ip, port,))

    def server_activate(self):
        self.socket.listen(self.request_queue_size)
        self.run()

    def server_close(self):
        self.socket.close()

    def run(self):
        # get request
        while True:
            conn, address = self.socket.accept()
            conn.sendall(bytes("欢迎登陆", 'utf-8'))
            obj = Action(conn)
            while True:
                client_bytes = conn.recv(1024)
                if not client_bytes:
                    break

                client_str = str(client_bytes, encoding='utf-8')
                if obj.has_login:

                    o = client_str.split('|', 1)
                    if len(o) > 0:
                        func = getattr(obj, o[0])
                        func(client_str)
                    else:
                        conn.sendall(bytes('输入格式错误', 'utf-8'))
                else:
                    obj.login(client_str)

            conn.close()


class MultiServerHandler(socketserver.BaseRequestHandler):
    def handle(self):
        conn = self.request
        conn.sendall(bytes("欢迎登陆", 'utf-8'))
        obj = Action(conn)
        #  self.conn = conn
        # self.has_login = False
        # self.username = None
        # self.home = None
        # self.current_dir = None
        while True:
            client_bytes = conn.recv(1024)
            if not client_bytes:
                break
            # cmd|ipconfig
            client_str = str(client_bytes, encoding='utf-8')
            if obj.has_login:
                o = client_str.split('|', 1)
                # o[0]: cmd
                if len(o) > 0:
                    func = getattr(obj, o[0])
                    func(client_str) # cmd|ipconfig
                else:
                    conn.sendall(bytes('输入格式错误', 'utf-8'))
            else:
                obj.login(client_str)

        conn.close()


class MultiServer(object):
    def __init__(self):
        server = socketserver.ThreadingTCPServer((settings.BIND_HOST, settings.BIND_PORT), MultiServerHandler)
        server.serve_forever()


class Action(object):
    def __init__(self, conn):
        self.conn = conn
        self.has_login = False
        self.username = None
        self.home = None
        self.current_dir = None

    def login(self, origin):
        self.conn.sendall(bytes("4001", 'utf-8'))
        while True:
            login_str = str(self.conn.recv(1024), encoding='utf-8')
            login_dict = json.loads(login_str)
            if login_dict['username'] == 'wupeiqi' and login_dict['pwd'] == '123':
                self.conn.sendall(bytes("4002", 'utf-8'))
                self.has_login = True
                self.username = 'wupeiqi'
                self.initialize()
                break
            else:
                self.conn.sendall(bytes("4003", 'utf-8'))

    def initialize(self):
        self.home = os.path.join(settings.USER_HOME, self.username)
        self.current_dir = os.path.join(settings.USER_HOME, self.username)

    def cmd(self, origin):

        func, command = origin.split('|', 1)
        command_list = re.split('\s*', command, 1)

        if command_list[0] == 'ls':
            if len(command_list) == 1:
                if self.current_dir:
                    command_list.append(self.current_dir)
                else:
                    command_list.append(self.home)
            else:
                if self.current_dir:
                    p = os.path.join(self.current_dir, command_list[1])
                else:
                    p = os.path.join(self.home, command_list[1])
                command_list[1] = p

        if command_list[0] == 'cd':
            if len(command_list) == 1:
                command_list.append(self.home)

            else:
                if self.current_dir:
                    p = os.path.join(self.current_dir, command_list[1])
                else:
                    p = os.path.join(self.home, command_list[1])
                self.current_dir = p
                command_list[1] = p
        command = ' '.join(command_list)
        try:
            result_bytes = subprocess.check_output(command, shell=True)
            # result_bytes # gbk字节
            result_bytes = bytes(str(result_bytes, encoding='gbk'), encoding='utf-8')
        except Exception as e:
            result_bytes = bytes('error cmd', encoding='utf-8')



        info_str = "info|%d" % len(result_bytes)
        self.conn.sendall(bytes(info_str, 'utf-8'))
        ack = self.conn.recv(1024)
        self.conn.sendall(result_bytes)

    def post(self, origin):
        func, file_byte_size, file_name, file_md5, target_path = origin.split('|', 4)
        target_abs_md5_path = os.path.join(self.home, target_path)
        has_received = 0
        file_byte_size = int(file_byte_size)

        if os.path.exists(target_abs_md5_path):
            self.conn.sendall(bytes('2003', 'utf-8'))
            is_continue = str(self.conn.recv(1024), 'utf-8')
            if is_continue == "2004":
                has_file_size = os.stat(target_abs_md5_path).st_size
                self.conn.sendall(bytes(str(has_file_size), 'utf-8'))
                has_received += has_file_size
                f = open(target_abs_md5_path, 'ab')

            else:
                f = open(target_abs_md5_path, 'wb')
        else:
            self.conn.sendall(bytes('2002', 'utf-8'))
            f = open(target_abs_md5_path, 'wb')

        while file_byte_size > has_received:
            data = self.conn.recv(1024)
            f.write(data)
            has_received += len(data)
        f.close()

    def get(self, origin):
        pass

    def exit(self, origin):
        pass

EasyClientFTP目录结构

smoke@smoke-GS70-2PC-Stealth:~/文档/DocumentFile/PycharmProjects/pythonProject$ tree EasyClientFTP/
EasyClientFTP/
├── bin
│   ├── __init__.py
│   └── program.py
├── config
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-35.pyc
│   │   └── settings.cpython-35.pyc
│   └── settings.py
├── lib
│   ├── commons.py
│   ├── __init__.py
│   └── __pycache__
│       ├── commons.cpython-35.pyc
│       └── __init__.cpython-35.pyc
├── log
│   └── __init__.py
└── src
    ├── __init__.py
    ├── __pycache__
    │   ├── __init__.cpython-35.pyc
    │   └── service.cpython-35.pyc
    └── service.py

8 directories, 15 files

bin目录

program.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

from src import service

if __name__ == '__main__':

    service.main()

config目录

settings.py

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

server = '127.0.0.1'
port = 9992

lib目录

commons.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import sys
import hashlib


def fetch_file_md5(file_path):
    obj = hashlib.md5()
    f = open(file_path, 'rb')
    while True:
        b = f.read(8096)
        if not b:
            break
        obj.update(b)
    f.close()
    return obj.hexdigest()


def bar(num=1, total=100):
    rate = float(num) / float(total)
    rate_num = int(rate * 100)
    temp = '\r%d %%' % (rate_num, )
    sys.stdout.write(temp)
    sys.stdout.flush()

log目录

src目录

service.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import os
import re
import json
from config import settings
from lib import commons


def login(conn):
    while True:
        username = input('请输入用户名(q退出):')
        pwd = input('请输入密码:')
        login_info = {'username': username, 'pwd': pwd}
        conn.sendall(bytes(json.dumps(login_info), 'utf-8'))
        if str(conn.recv(1024), encoding='utf-8') == "4002":
            print('授权成功...')
            break
        else:
            print('用户名或密码错误...')


def cmd(conn, inp):
    conn.sendall(bytes(inp, 'utf-8'))
    basic_info_bytes = conn.recv(1024)
    basic_info_str = str(basic_info_bytes, 'utf-8')
    if basic_info_str == '4001':
        login(conn)
    else:
        conn.sendall(bytes("ack", 'utf-8'))
        print(str(basic_info_bytes, 'utf-8'))
        result_length = int(str(basic_info_bytes, 'utf-8').split('|')[1])
        has_received = 0
        content_bytes = bytes()
        while has_received < result_length:
            fetch_bytes = conn.recv(1024)
            has_received += len(fetch_bytes)
            content_bytes += fetch_bytes
        cmd_result = str(content_bytes, 'utf-8')
        print(cmd_result)


def post(conn, inp):
    # inp  ===> post|D:/1.txt     2.txt
    method, file_paths = inp.split("|", 1)
    local_path, target_path = re.split('\s*', file_paths, 1)
    # 获取本地文件大小
    file_byte_size = os.stat(local_path).st_size
    # 获取本地文件名
    file_name = os.path.basename(local_path)
    # 获取本地文件md5值
    file_md5 = commons.fetch_file_md5(local_path)

    post_info = "post|%s|%s|%s|%s" % (file_byte_size, file_name, file_md5, target_path)

    conn.sendall(bytes(post_info, 'utf-8'))

    result_exist = str(conn.recv(1024), 'utf-8')

    if result_exist == '4001':
        login(conn)
        return

    has_sent = 0
    if result_exist == "2003":
        inp_continue = input('文件已经存在,是否续传?Y/N')
        if inp_continue.upper() == "Y":
            conn.sendall(bytes('2004', 'utf-8'))
            result_continue_pos = str(conn.recv(1024), 'utf-8')
            has_sent = int(result_continue_pos)
        else:
            conn.sendall(bytes('2005', 'utf-8'))

    file_obj = open(local_path, 'rb')
    file_obj.seek(has_sent)
    while file_byte_size > has_sent:
        data = file_obj.read(1024)
        conn.sendall(data)
        has_sent += len(data)
        commons.bar(has_sent, file_byte_size)
    file_obj.close()
    print('上传成功')


def get(conn, inp):
    pass


def help_info():
    print("""
            cmd|命令
            post|文件路径
            get|下载文件路径
            exit|退出
        """)


def execute(conn):
    choice_dict = {
        'cmd': cmd,
        'get': get,
        'post': post,
    }
    help_info()
    while True:
        # cmd|ls
        # post|本地路径 服务器上路径
        # get|服务器路径 本地路径
        inp = input("please input:")
        if inp == 'help':
            help_info()
            continue
        choice = inp.split('|')[0]
        if choice == 'exit':
            return
        if choice in choice_dict:
            func = choice_dict[choice]
            func(conn, inp)


def main():
    ip_port = (settings.server, settings.port)
    conn = socket.socket()
    conn.connect(ip_port)
    welcome_bytes = conn.recv(1024)
    print(str(welcome_bytes, encoding='utf-8'))

    execute(conn)

    conn.close()

运行EasyServerFTP

/home/smoke/文档/DocumentFile/PycharmProjects/EasyServerFTP/venv/bin/python /home/smoke/文档/DocumentFile/PycharmProjects/EasyServerFTP/bin/program.py

运行EasyClientFTP

/home/smoke/文档/DocumentFile/PycharmProjects/EasyClientFTP/venv/bin/python /home/smoke/文档/DocumentFile/PycharmProjects/EasyClientFTP/bin/program.py
欢迎登陆

            cmd|命令
            post|文件路径
            get|下载文件路径
            exit|退出
        
please input:cmd|ls
请输入用户名(q退出):wupeiqi
请输入密码:123
授权成功...
please input:cmd|ip addr show
info|867
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp4s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
    link/ether 44:8a:5b:f1:bd:c5 brd ff:ff:ff:ff:ff:ff
3: wlp5s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether d8:fc:93:0f:85:93 brd ff:ff:ff:ff:ff:ff
    inet 192.168.254.15/27 brd 192.168.254.31 scope global dynamic noprefixroute wlp5s0
       valid_lft 6872sec preferred_lft 6872sec
    inet6 fe80::8df3:c59b:ccd4:56b2/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

please input: