03 实现支持多用户在线的FTP程序(C/S)

1. 需求

1. 用户加密认证
2. 允许多用户登录
3. 每个用户都有自己的家目录,且只能访问自己的家目录
4. 对用户进行磁盘分配,每一个用户的可用空间可以自己设置
5. 允许用户在ftp server上随意切换目录
6. 允许用户查看自己家目录下的文件
7. 允许用户上传和下载,保证文件的一致性(md5)
8. 文件上传、下载过程中显示进度条
9. 支持多并发的功能
10. 使用队列queue模块,实现线程池
11. 允许用户配置最大的并发数,比如允许只有10并发用户
升级需求:10%
1. 文件支持断点续传

2. 开发环境

  Python 3.7.3

3. 软件开发

客户端:
    |-conf
        |-setting.py        # 配置文件,存放服务端ip和port, 客户端下载文件的目录等
    |-core
        |-main.py            # FTP客户端功能
    |-files                    # 用户下载, 上传文件的存放目录
        |-.download             # 目录存放用户未下载完的文件的配置文件
    |-ftp_client.py             # 客户端启动程序
    
服务端:
    |-conf
        |-settings.py            # 配置文件,存放服务端ip和port, 用户目录及用户账户, 日志目录, 与用户确认交互的状态码, 日志配置文件等等
        |-accounts.ini            # 用户账户相关的信息
    |-core
        |-handler_request.py    # 专门处理服务端就与客户端的请求, 以及命令
        |-main.py                # FTP服务端专门与客户端建立连接
        |-management.py            # 管理FTP的的启动, 停止, 重启等
        |-mythreadpool.py        # 使用queue实现的简单版的线程池, 缺点: 线程不能重复利用
    |-home    
        |-egon                    # 用户家目录,每一个用户以用户名作为家目录
            |-.upload             # 目录存放用户未上传完的文件的配置文件信息
        |-....                    # 每个用户下都有: 用户家目录,每一个用户以用户名作为家目录
            |-.upload             # 每个用户下都有: 用户未上传完的文件的配置文件信息
    |-ftp_client.py                 # 服务端启动程序
目录结构 

4. 服务端与客户端的启动

11.打开cmd命令行终端22.python3+启动文件路径+startftpserver33.例子:4C:\Users\洋辣子>python3Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\ftp_server.pystartftpserver
服务端启动
1 1. 打开cmd命令行终端
2 2. python3 + 启动文件路径
3 3. 例子:
4     C:\Users\洋辣子> python3 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\ftp_client.py
客户端启动

5. 用户配置信息

1 用户名:                用户密码:
2 alex                    123
3 egon                    123
4 ly                    123
5 jzd                    123
6 shx                    123    
View Code

6. 所有功能测试 

(1) 登陆

1 username>>:egon
2 password>>:123
3 用户名密码正确, 认证成功!
View Code

(2) 查看所有命令所对应的帮助信息

查看方法:

  • 命令 + –-help

 1 [egon@localhost ~]# ls --help
 2 
 3             查看当前目录下的文件:
 4                 ls
 5             指定目录下的文件(只能查看到自己家目录的范围):
 6                 ls /我是egon的目录
 7             
 8 [egon@localhost ~]# cd --help
 9 
10             相对路径切换:
11                 cd /我是egon的目录
12                     cd /我是江傻子的目录
13             切换到上一层目录:
14                 cd ..
15             绝对路径切换:
16                 cd /我是egon的目录/我是江傻子的目录
17             在当前目录下切当前目录:
18                 cd .
View Code

(3) ls: 查看

① 支持功能:

  • 查看当前目录下的文件:ls

  • 指定目录下的文件(只能查看到自己家目录的范围):ls /目录1/目录2

  • 查看帮助信息:ls /?

② 运行效果:

 1 [egon@localhost ~]# ls
 2  驱动器 Z 中的卷是 固态硬盘
 3  卷的序列号是 AA26-64F0
 4 
 5  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon 的目录
 6 
 7 2019-10-26  22:34    <DIR>          .
 8 2019-10-26  22:34    <DIR>          ..
 9 2019-10-19  20:03         1,081,540 123.docx
10 2019-10-27  13:45    <DIR>          我是egon的目录
11                1 个文件      1,081,540 字节
12                3 个目录 56,465,575,936 可用字节
1) 查看当前目录下的文件
 1 [egon@localhost ~]# ls /我是egon的目录
 2  驱动器 Z 中的卷是 固态硬盘
 3  卷的序列号是 AA26-64F0
 4 
 5  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录
 6 
 7 2019-10-27  13:45    <DIR>          .
 8 2019-10-27  13:45    <DIR>          ..
 9 2019-10-27  13:45    <DIR>          我是江傻子的目录
10 2019-10-27  12:34                 0 江傻子
11                1 个文件              0 字节
12                3 个目录 56,465,575,936 可用字节
2) 指定目录下的文件(只能查看到自己家目录的范围)
 1 [egon@localhost ~]# ls /?
 2 显示目录中的文件和子目录列表。
 3 
 4 DIR [drive:][path][filename] [/A[[:]attributes]] [/B] [/C] [/D] [/L] [/N]
 5   [/O[[:]sortorder]] [/P] [/Q] [/R] [/S] [/T[[:]timefield]] [/W] [/X] [/4]
 6 
 7   [drive:][path][filename]
 8               指定要列出的驱动器、目录和/或文件。
 9 
10   /A          显示具有指定属性的文件。
11   属性         D  目录                R  只读文件
12                H  隐藏文件            A  准备存档的文件
13                S  系统文件            I  无内容索引文件
14                L  重新分析点          O  脱机文件
15                -  表示“否”的前缀
16   /B          使用空格式(没有标题信息或摘要)。
17   /C          在文件大小中显示千位数分隔符。这是默认值。用 /-C 来
18               禁用分隔符显示。
19   /D          跟宽式相同,但文件是按栏分类列出的。
20   /L          用小写。
21   /N          新的长列表格式,其中文件名在最右边。
22   /O          用分类顺序列出文件。
23   排列顺序     N  按名称(字母顺序)     S  按大小(从小到大)
24                E  按扩展名(字母顺序)   D  按日期/时间(从先到后)
25                G  组目录优先           -  反转顺序的前缀
26   /P          在每个信息屏幕后暂停。
27   /Q          显示文件所有者。
28   /R          显示文件的备用数据流。
29   /S          显示指定目录和所有子目录中的文件。
30   /T          控制显示或用来分类的时间字符域
31   时间段      C  创建时间
32               A  上次访问时间
33               W  上次写入的时间
34   /W          用宽列表格式。
35   /X          显示为非 8dot3 文件名产生的短名称。格式是 /N 的格式,
36               短名称插在长名称前面。如果没有短名称,在其位置则
37               显示空白。
38   /4          以四位数字显示年份
39 
40 可以在 DIRCMD 环境变量中预先设定开关。通过添加前缀 - (破折号)
41 来替代预先设定的开关。例如,/-W。
42 
43 [egon@localhost ~]# 
3) 查看帮助信息

(4) cd: 切换目录

① 支持功能:

  • 相对路径切换:cd /目录1 或者 cd /目录2

  • 切换到上一层目录:cd ..

  • 绝对路径切换:cd /目录1/目录2

  • 在当前目录下切当前目录:cd .

② 运行效果:

 1 [egon@localhost ~]# cd /我是egon的目录
 2 切换目录成功
 3 [egon@localhost /home/egon/我是egon的目录]# ls
 4  驱动器 Z 中的卷是 固态硬盘
 5  卷的序列号是 AA26-64F0
 6 
 7  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录
 8 
 9 2019-10-27  13:45    <DIR>          .
10 2019-10-27  13:45    <DIR>          ..
11 2019-10-27  13:45    <DIR>          我是江傻子的目录
12 2019-10-27  12:34                 0 江傻子
13                1 个文件              0 字节
14                3 个目录 56,465,563,648 可用字节
15 
16 [egon@localhost /home/egon/我是egon的目录]# cd /我是江傻子的目录
17 切换目录成功
18 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# ls
19  驱动器 Z 中的卷是 固态硬盘
20  卷的序列号是 AA26-64F0
21 
22  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录\我是江傻子的目录 的目录
23 
24 2019-10-27  13:45    <DIR>          .
25 2019-10-27  13:45    <DIR>          ..
26 2019-10-27  13:45                 0 我是江大傻.txt
27                1 个文件              0 字节
28                2 个目录 56,465,563,648 可用字节
29 
30 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# 
1) 相对路径切换
 1 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# cd ..
 2 切换目录成功
 3 [egon@localhost /home/egon/我是egon的目录]# ls
 4  驱动器 Z 中的卷是 固态硬盘
 5  卷的序列号是 AA26-64F0
 6 
 7  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录
 8 
 9 2019-10-27  13:45    <DIR>          .
10 2019-10-27  13:45    <DIR>          ..
11 2019-10-27  13:45    <DIR>          我是江傻子的目录
12 2019-10-27  12:34                 0 江傻子
13                1 个文件              0 字节
14                3 个目录 56,465,559,552 可用字节
15 
16 [egon@localhost /home/egon/我是egon的目录]# 
2) 切换到上一层目录
 1 [egon@localhost ~]# cd /我是egon的目录/我是江傻子的目录
 2 切换目录成功
 3 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# ls
 4  驱动器 Z 中的卷是 固态硬盘
 5  卷的序列号是 AA26-64F0
 6 
 7  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录\我是江傻子的目录 的目录
 8 
 9 2019-10-27  13:45    <DIR>          .
10 2019-10-27  13:45    <DIR>          ..
11 2019-10-27  13:45                 0 我是江大傻.txt
12                1 个文件              0 字节
13                2 个目录 56,465,559,552 可用字节
14 
15 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# 
3) 绝对路径切换
 1 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# cd .
 2 切换目录成功
 3 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# ls
 4  驱动器 Z 中的卷是 固态硬盘
 5  卷的序列号是 AA26-64F0
 6 
 7  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录\我是江傻子的目录 的目录
 8 
 9 2019-10-27  13:45    <DIR>          .
10 2019-10-27  13:45    <DIR>          ..
11 2019-10-27  13:45                 0 我是江大傻.txt
12                1 个文件              0 字节
13                2 个目录 56,465,559,552 可用字节
4) 在当前目录下切当前目录

(5) mkdir: 创建目录(支持递归创建目录)

① 支持功能:

  • 相对路径创建: mkdir /目录

  • 生成多层递归目录: mkdir /目录1/目录2

② 运行效果:

1 [egon@localhost ~]# mkdir /a
2 创建目录成功!
1) 相对路径创建:
1 [egon@localhost ~]# mkdir /a/b
2 创建目录成功!
2) 绝对路径创建:

(6) rmdir: 删除空目录

① 支持功能:

  • 删除空目录: rmdir /目录1/空目录2

② 运行效果:

1 [egon@localhost ~]# rmdir /a/b
2 删除目录成功!
1) 删除空目录

(7) remove: 删除文件

① 支持功能:

  • 删除文件:remove /目录1/文件

② 运行效果:

 1 [egon@localhost ~]# ls /我是egon的目录/江傻子
 2  驱动器 Z 中的卷是 固态硬盘
 3  卷的序列号是 AA26-64F0
 4 
 5  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录
 6 
 7 2019-10-27  12:34                 0 江傻子
 8                1 个文件              0 字节
 9                0 个目录 56,465,010,688 可用字节
10 
11 [egon@localhost ~]# remove /我是egon的目录/江傻子
12 删除文件成功!
13 
14 [egon@localhost ~]# ls /我是egon的目录/江傻子
15 找不到文件
16  驱动器 Z 中的卷是 固态硬盘
17  卷的序列号是 AA26-64F0
18 
19  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录
1) 删除文件

(8) upload: 上传文件到服务端

① 支持功能:

  • 上传到服务端当前路径: upload 文件

  • 通过cd切换目录上传文件到该目录下: cd /目录1/目录2 --> upload 文件

② 运行效果:

 1 [egon@localhost ~]# upload 服务器管理综合报告.docx
 2 你可以上传文件, 在您上传之前, 您的目前空间:68.97MB!
 3 
 4 upload running...
 5 [##################################################] 100.00%
 6 upload succeed!
 7 上传文件成功, 您上传完后的剩余空间:66.07MB!
 8 
 9 [egon@localhost ~]# ls
10  驱动器 Z 中的卷是 固态硬盘
11  卷的序列号是 AA26-64F0
12 
13  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon 的目录
14 
15 2019-10-31  16:37    <DIR>          .
16 2019-10-31  16:37    <DIR>          ..
17 2019-10-31  16:37    <DIR>          .upload
18 2019-10-31  16:09        32,535,704 03_函数调用的三种形式.mp4
19 2019-10-28  09:53    <DIR>          我是egon的目录
20 2019-10-31  16:37         3,039,102 服务器管理综合报告.docx
21                2 个文件     35,574,806 字节
22                4 个目录 56,393,715,712 可用字节
1) 上传到服务端当前路径:
 1 [egon@localhost ~]# cd /我是egon的目录
 2 切换目录成功
 3 
 4 [egon@localhost /我是的目录]# upload 服务器管理综合报告.docx
 5 你可以上传文件, 在您上传之前, 您的目前空间:66.07MB!
 6 
 7 upload running...
 8 [##################################################] 100.00%
 9 upload succeed!
10 上传文件成功, 您上传完后的剩余空间:63.17MB!
11 
12 
13 [egon@localhost /我是的目录]# ls
14  驱动器 Z 中的卷是 固态硬盘
15  卷的序列号是 AA26-64F0
16 
17  Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录
18 
19 2019-10-31  16:47    <DIR>          .
20 2019-10-31  16:47    <DIR>          ..
21 2019-10-27  13:45    <DIR>          我是江傻子的目录
22 2019-10-31  16:47         3,039,102 服务器管理综合报告.docx
23                1 个文件      3,039,102 字节
24                3 个目录 56,390,676,480 可用字节
通过cd切换目录上传文件到该目录下:

(9) resume_upload: 续传未上传完成的文件到服务端

① 支持功能:

  • 继续上传文件到服务端当前路径: resume_upload 文件名

  • 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传: cd /目录1/目录2 --> resume_upload 文件名

② 运行效果:

 1 ------您的files文件夹下所含有的文件------
 2 1: .download
 3 2: 03_函数调用的三种形式.mp4
 4 3: 服务器管理综合报告.docx
 5 
 6 [egon@localhost ~]# upload 03_函数调用的三种形式.mp4
 7 你可以上传文件, 在您上传之前, 您的目前空间:97.10MB!
 8 
 9 upload running...
10 [############                                      ] 25.43%
先断开传输:
 1 username>>:egon
 2 password>>:123
 3 用户名密码正确, 认证成功!
 4 您的还有为上传完的文件, 是否继续上传!
 5 
 6             数量: 1  文件路径: /03_函数调用的三种形式.mp4 文件名: 03_函数调用的三种形式.mp4
 7                 文件原大小: 32535704字节 未完成的文件大小: 8273050字节 上传的百分比: 25.43%
 8             
 9 
10 ------您的files文件夹下所含有的文件------
11 1: .download
12 2: 03_函数调用的三种形式.mp4
13 3: 服务器管理综合报告.docx
14 
15 
16 [egon@localhost ~]# resume_upload 03_函数调用的三种形式.mp4
17 您正在继续上传文件, 在您继传之前, 您的目前空间:89.21MB!
18 8273050
19 
20 upload running...
21 [##################################################] 100.00%
22 upload succeed!
23 上传文件成功, 您上传完后的剩余空间:66.07MB!
1) 继续上传文件到服务端当前路径:
 1 username>>:egon
 2 password>>:123
 3 用户名密码正确, 认证成功!
 4 您的还有为上传完的文件, 是否继续上传!
 5 
 6             数量: 1  文件路径: 的目录/03_函数调用的三种形式.mp4 文件名: 03_函数调用的三种形式.mp4
 7                 文件原大小: 32535704字节 未完成的文件大小: 12534221字节 上传的百分比: 38.52%
 8             
 9 
10 ------您的files文件夹下所含有的文件------
11 1: .download
12 2: 03_函数调用的三种形式.mp4
13 3: 服务器管理综合报告.docx
14 
15 [egon@localhost ~]# cd /我是egon的目录
16 切换目录成功
17 
18 ------您的files文件夹下所含有的文件------
19 1: .download
20 2: 03_函数调用的三种形式.mp4
21 3: 服务器管理综合报告.docx
22 
23 [egon@localhost /我是的目录]# resume_upload 03_函数调用的三种形式.mp4
24 您正在继续上传文件, 在您继传之前, 您的目前空间:66.07MB!
25 
26 upload running...
27 [##################################################] 100.00%
28 upload succeed!
29 上传文件成功, 您上传完后的剩余空间:47.00MB!
2) 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传:

(10) download: 下载文件

① 支持功能:

  • 从服务端当前目录下下载文件

    • download 文件

  • 从服务端绝对路径下下载文件

    • download /目录1/文件

② 运行效果:

1 [egon@localhost ~]# download 服务器管理综合报告.docx
2 
3 download run...
4 [##################################################] 100.00%
5 download succeed!
1) 从服务端当前目录下下载文件
1 [egon@localhost ~]# download /我是egon的目录/03_函数调用的三种形式.mp4
2 
3 download run...
4 [##################################################] 100.00%
5 download succeed!
2) 从服务端绝对路径下下载文件

(11) 在download基础之上: 继续从服务端续传下载文件

① 支持功能:

  • 用户登陆的时候显示为下载完的文件、用户根据序号选择要继续续传的文件

  • 用户可以多次循环选择

  • 支持断点以后据续断点续传

② 运行效果:

 1 username>>:egon
 2 password>>:123
 3 用户名密码正确, 认证成功!
 4 服务端检测您没有未上传完成的文件!
 5 检测到您本地还有未上传完成的文件
 6 --------------------------------------------------------------------未完成续传的数量: 2个---------------------------------------------------------------------
 7 序号: 1
 8 
 9             未完成的文件路径: Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\files\03_函数调用的三种形式.mp4.download    文件名: 03_函数调用的三种形式.mp4
10             文件原大小: 32535704字节      已完成的文件大小: 3511466字节     上传的百分比: 10.79%
11             
12 序号: 2
13 
14             未完成的文件路径: Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\files\服务器管理综合报告.docx.download    文件名: 服务器管理综合报告.docx
15             文件原大小: 3039102字节      已完成的文件大小: 712297字节     上传的百分比: 23.44%
16             
17 [退出: q/Q]请根据序号选择您是否继续下载没有完成的文件>>:1
18 开始续传......
19 
20 download run...
21 [##################################################] 100.00%
22 download succeed!
23 
24 续传完毕!
25 --------------------------------------------------------------------未完成续传的数量: 1个---------------------------------------------------------------------
26 序号: 1
27 
28             未完成的文件路径: Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\files\服务器管理综合报告.docx.download    文件名: 服务器管理综合报告.docx
29             文件原大小: 3039102字节      已完成的文件大小: 712297字节     上传的百分比: 23.44%
30             
31 [退出: q/Q]请根据序号选择您是否继续下载没有完成的文件>>:1
32 开始续传......
33 
34 download run...
35 [##################################################] 100.00%
36 download succeed!
37 
38 续传完毕!
39 
40 ------您的files文件夹下所含有的文件------
41 1: .download
42 2: 03_函数调用的三种形式.mp4
43 3: 服务器管理综合报告.docx
View Code

(12) 为用户磁盘配额

 1 [egon@localhost /我是的目录/我是江傻子的目录/目录1]# upload 03_函数调用的三种形式.mp4
 2 你可以上传文件, 在您上传之前, 您的目前空间:35.04MB!
 3 
 4 upload running...
 5 [##################################################] 100.00%
 6 upload succeed!
 7 上传文件成功, 您上传完后的剩余空间:4.02MB!
 8 
 9 
10 [egon@localhost /我是的目录/我是江傻子的目录/目录2]# upload 03_函数调用的三种形式.mp4
11 上传文件失败, 您的空间不足, 您的剩余空间:4.02MB!
View Code

(13) 使用队列queue模块,实现线程, 允许用户配置最大的并发数5个

(14) 记录了日志功能

① 终端打印

② 保存文件之中 

7. 不足之处

  • 没有实现多文件, 以及多文件夹打包上传

  • client用户暂时只能用files文件夹下的路径进行上传下载, 不能动态指定

8.代码展示

① client

1) conf

 1 import os
 2 
 3 BASE_DIR = os.path.normpath(os.path.join(__file__, '..', '..'))
 4 
 5 FILES_PATH = os.path.join(BASE_DIR, 'files')
 6 UNFINISHED_DOWNLOAD_FILES_PATH = os.path.join(FILES_PATH, '.download', 'unfinished.shv')
 7 
 8 HOST = '127.0.0.1'
 9 PORT = 8080
10 
11 
12 help_dic = {
13     'ls --help': """
14             查看当前目录下的文件:
15                 ls
16             指定目录下的文件(只能查看到自己家目录的范围):
17                 ls /目录1/目录2
18             查看ls的详细帮助:
19                 ls /?
20             """,
21     'cd --help': """
22             相对路径切换:
23                 cd /目录1
24                     cd /目录2
25             绝对路径切换:
26                 cd /目录1/目录2
27             切换到上一层目录:
28                 cd ..
29             在当前目录下切当前目录:
30                 cd .
31             """,
32     'mkdir --help': """
33             相对路径创建:
34                 mkdir /目录
35             生成多层递归目录:
36                 mkdir /目录1/目录2
37             """,
38     'rmdir --help': """
39             删除空目录:
40                 rmdir /目录1/空目录2
41             """,
42     'remove --help': """
43             删除文件:
44                 remove /目录1/文件
45             """,
46     'upload --help': """
47             上传到服务端当前路径:
48                 upload 文件名
49             通过cd切换目录上传文件到该目录下:
50                 cd /目录1/目录2
51                     upload 文件
52             """,
53     'resume_upload --help': """
54                 继续上传文件到服务端当前路径:
55                     resume_upload 文件名
56                 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传:
57                     cd /目录1/目录2
58                         resume_upload 文件名
59                 """,
60     None: """
61             查看相对应的帮助信息:
62                 1.  ls + --help         
63                 2.  cd --help
64                 3.  mkdir --help
65                 4.  rmdir --help
66                 5.  remove --help
67                 6.  upload --help
68                 7.  resume_upload --help
69             """,
70 }
settings.py

2) core

  1 import hashlib
  2 import json
  3 import os
  4 import shelve
  5 import socket
  6 import struct
  7 
  8 from conf import settings
  9 
 10 
 11 class FTPClient:
 12     """FTP客户端."""
 13     address_family = socket.AF_INET
 14     socket_type = socket.SOCK_STREAM
 15     max_packet_size = 8192
 16     encoding = 'utf-8'
 17     windows_encoding = 'gbk'
 18 
 19     struct_fmt = 'i'
 20     fixed_packet_size = 4
 21 
 22     def __init__(self, server_address, connect=True):
 23         self.server_address = server_address
 24         self.socket = socket.socket(self.address_family, self.socket_type)
 25 
 26         self.breakpoint_resume = shelve.open(settings.UNFINISHED_DOWNLOAD_FILES_PATH)
 27 
 28         self.username = None
 29         self.current_dir = '~'
 30         if connect:
 31             try:
 32                 self.client_connect()
 33             except Exception:
 34                 self.client_close()
 35                 raise
 36 
 37     def client_connect(self):
 38         """客户端连接服务端ip和port."""
 39         self.socket.connect(self.server_address)
 40 
 41     def client_close(self):
 42         """关闭连接通道."""
 43         self.socket.close()
 44 
 45     def interactive(self):
 46         """与服务端进行所有的交互."""
 47         if self.auth():
 48             self.unfinished_file_check()
 49             while True:
 50                 self.show_str()
 51                 msg = input('[%s@localhost %s]# ' % (self.username, self.current_dir)).strip()
 52                 if not msg:
 53                     continue
 54                 if not self.help_msg(msg):
 55                     continue
 56                 # 核验命令参数
 57                 cmd, path = self.verify_args(msg)
 58                 if hasattr(self, '_%s' % cmd):
 59                     func = getattr(self, '_%s' % cmd)
 60                     func(path)
 61                 else:
 62                     self.help_msg()
 63 
 64     @staticmethod
 65     def verify_args(msg):
 66         """
 67         效验参数.
 68         :param msg: ls       或 ls /路径       或 ls /路径1/路径2/
 69         :return:    (ls, []) 或 (ls, ['路径']) 或 (ls, ['路径1', '路径2'])
 70         """
 71         cmd_args = msg.split()
 72         cmd, path = cmd_args[0], cmd_args[1:]
 73         if path:
 74             path = ''.join(cmd_args[1:]).strip('//').split('/')
 75         # print('cmd, path:', cmd, path)
 76         return cmd, path
 77 
 78     def unfinished_file_check(self):
 79         if not list(self.breakpoint_resume.keys()):
 80             return
 81 
 82         print('检测到您本地还有未上传完成的文件')
 83         unfinished_path_list = []
 84         msg_list = []
 85         for unfinished_file_path in self.breakpoint_resume.keys():
 86             file_name = self.breakpoint_resume[unfinished_file_path]['file_name']
 87             file_size = self.breakpoint_resume[unfinished_file_path]['file_size']
 88             unfinished_file_size = os.path.getsize(unfinished_file_path)
 89             percent = unfinished_file_size / file_size * 100
 90             path = self.breakpoint_resume[unfinished_file_path]['path']
 91             dic = {'unfinished_file_size': unfinished_file_size, 'path': path}
 92             unfinished_path_list.append(dic)
 93             msg = """
 94             未完成的文件路径: %s    文件名: %s
 95             文件原大小: %s字节      已完成的文件大小: %s字节     上传的百分比: %.2f%%
 96             """ % (unfinished_file_path, file_name, file_size, unfinished_file_size, percent)
 97             msg_list.append(msg)
 98 
 99         while msg_list:
100             print("未完成续传的数量: %s个".center(150, '-') % len(msg_list))
101             for msg in msg_list:
102                 print('序号: %s' % (int(msg_list.index(msg) + 1)))
103                 print(msg)
104 
105             choice = input('[退出: q/Q]请根据序号选择您是否继续下载没有完成的文件>>:').strip()
106             if choice.lower() == 'q':
107                 break
108             if not choice.isdigit():
109                 continue
110             choice = int(choice)
111             if 0 < choice <= len(unfinished_path_list):  # len(unfinished_path_list)=3
112                 dic = unfinished_path_list[choice - 1]
113                 path, unfinished_file_size = dic['path'], dic['unfinished_file_size']
114 
115                 print('开始续传......')
116                 self.__resume_download(path, unfinished_file_size)
117                 print('\n续传完毕!')
118 
119                 unfinished_path_list.pop(choice-1)
120                 msg_list.pop(choice-1)
121             else:
122                 print('您的选择超出了范围!')
123 
124     def auth(self):
125         """
126         登陆.
127         100: '用户名密码正确, 认证成功!',
128         199: '用户名密码不正确, 认证失败!',
129         850: '您的还有为上传完的文件, 是否继续上传!',
130         851: '检测您不存在未上传完成的文件!',
131         """
132         count = 0
133         while count < 3:
134             username = input('username>>:').strip()
135             password = input('password>>:').strip()
136             if not all([username, password]):
137                 print('用户名密码不能为空.')
138                 count += 1
139                 continue
140             # 发报头
141             self.send_header(action_type='auth', username=username, password=password)
142             # 收报头
143             response_dic = self.receive_header()
144             status_code, status_msg = response_dic.get('status_code'), response_dic.get('status_msg')
145             # 100: '用户名密码正确, 认证成功!',
146             if status_code == 100:  # 100确认成功
147                 print(status_msg)
148                 self.username = username
149 
150                 # 850: '您的还有为上传完的文件, 是否继续上传!',
151                 # 851: '检测您不存在未上传完成的文件!',
152                 response_dic = self.receive_header()
153                 status_code, status_msg, msg_list, msg_dic = response_dic.get('status_code'), response_dic.get(
154                     'status_msg'), response_dic.get('msg_list'), response_dic.get('msg_dic')
155                 if msg_list:
156                     print(status_msg)
157                     for unfinished_msg in msg_list:
158                         print(unfinished_msg)
159                 else:
160                     print(status_msg)
161 
162                 return True
163             else:
164                 # 199: '用户名密码不正确, 认证失败!',
165                 print(status_msg)
166                 count += 1
167         else:
168             print('输入次数过多,强制退出!')
169             return False
170 
171     def _ls(self, path):
172         """
173         显示目录的文件列表.
174         :param path: [] 或 ['目录1', '目录2']
175         :return: None
176         """
177         # 发送报头
178         self.send_header(action_type='ls', path=path)
179         # 接收报头
180         response_dic = self.receive_header()
181         status_code, status_msg, cmd_size = response_dic.get('status_code'), response_dic.get(
182             'status_msg'), response_dic.get('cmd_size')
183         if status_code == 301 and cmd_size:
184             # print('status_msg:', status_msg)
185             # print('cmd_size:', cmd_size)
186             # 收消息
187             windows_cmd = self.socket.recv(cmd_size).decode(self.windows_encoding)
188             print(windows_cmd)
189         else:
190             print(status_msg)
191 
192     def _cd(self, path):
193         """
194         切换目录.
195         :param path: ['..'] 或 ['路径1', '目录2']
196         :return: None
197         """
198         # 发送报头
199         self.send_header(action_type='cd', path=path)
200         # 接收报头
201         response_dic = self.receive_header()
202         status_code, status_msg, current_dir = response_dic.get('status_code'), response_dic.get(
203             'status_msg'), response_dic.get('current_dir')
204         if status_code == 400:
205             self.current_dir = current_dir
206             print(status_msg)
207         else:
208             print(status_msg)
209 
210     def _mkdir(self, path):
211         """
212         新建目录.
213         :param path: ['目录1']
214                 或   [目录2', '目录3']
215         :return: None
216         """
217         # print(path)
218         # 发送报头
219         self.send_header(action_type='mkdir', path=path)
220         # 接收报头
221         response_dic = self.receive_header()
222         status_code, status_msg = response_dic.get('status_code'), response_dic.get(
223             'status_msg')
224         if status_code == 500:
225             print(status_msg)
226         else:
227             print(status_msg)
228 
229     def _rmdir(self, path):
230         """
231         删除空目录.
232         :param path: ['', '12312都1的发']
233         :return: None
234         """
235         # print(path)
236         # 发送报头
237         self.send_header(action_type='rmdir', path=path)
238         # 接收报头
239         response_dic = self.receive_header()
240         status_code, status_msg = response_dic.get('status_code'), response_dic.get(
241             'status_msg')
242         if status_code == 600:
243             print(status_msg)
244         else:
245             print(status_msg)
246 
247     def _remove(self, path):
248         """
249         删除文件.
250         :param path: ['目录1', '文件1']
251         :return:
252         """
253         # print(path)
254         # 发送报头
255         self.send_header(action_type='remove', path=path)
256         # 接收报头
257         response_dic = self.receive_header()
258         status_code, status_msg = response_dic.get('status_code'), response_dic.get(
259             'status_msg')
260         if status_code == 700:
261             print(status_msg)
262         else:
263             print(status_msg)
264 
265     def parser_path(self, action_type, path, **kwargs):
266         """
267         解析路径参数, 判断路径是文件名, 还是路径下的文件名.
268         :param action_type: 用户上传的功能类型
269         :param path: 用户路径例子: ['目录1', '文件1']  或 ['文件1']
270         :param kwargs:
271         :return: path列表长度合理的时候返回True, 不合理返回False
272         """
273         if len(path) > 1:
274             self.send_header(action_type=action_type, **kwargs, file_name=path[-1],
275                              path=path[:-1])
276         elif len(path) == 1:
277             self.send_header(action_type=action_type, **kwargs, file_name=path[-1],
278                              path=None)
279         else:
280             print('必须指定路径, 或者文件名!')
281             return False
282         return True
283 
284     def _resume_upload(self, path):
285         """
286         upload的断点续传功能.
287         860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
288         869: '您选择文件路径中没有要续传的文件, 请核对!',
289         """
290         self._upload(path, resume_upload=True)
291 
292     def _upload(self, path, resume_upload=False):
293         """
294         上传文件到服务端.
295         正常上传:
296             800: '你可以上传文件, 在您上传之前, 您的目前空间:%s!',
297             801: '上传文件成功, 您上传完后的剩余空间:%s!',
298             852: '您不能进行续传, 因为该文件是完整文件!',
299             894: '您不需要再本路径下上传文件, 该文件在您的当前路径下已经存在!',
300             895: '上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!',
301             896: '上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!',
302             897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
303             898: '上传文件失败, 上传命令不规范!',
304             899: '上传文件必须要有文件的md5值以及文件名!',
305         续传:
306             860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
307             869: '您选择文件路径中没有要续传的文件, 请核对!',
308         :param path: ['目录1', '文件1'] 或 ['文件1']
309         :return: None
310         """
311         # 判断用户文件路径是否是FILES_PATH路径下的文件
312         file_path = os.path.normpath(os.path.join(settings.FILES_PATH, *path))
313         if not os.path.isfile(file_path):
314             print('您要上传的文件不存在!')
315             return
316 
317         # 解析用户路径, 并提交upload的相关功能
318         file_size = os.path.getsize(file_path)
319         file_md5 = self.md5(file_path)
320 
321         if resume_upload:  # 断点续传时执行
322             action_type = 'resume_upload'
323         else:  # 正常长传时执行
324             action_type = 'upload'
325 
326         if not self.parser_path(action_type=action_type, file_md5=file_md5, file_size=file_size, path=path):
327             return
328 
329         # 接收服务端相应字典
330         # 正常: 800, 894, 897, 898, 899
331         # 800: '你可以上传文件, 在您上传之前, 您的目前空间:%s!',
332         # 894: '您不需要再本路径下上传文件, 该文件在您的当前路径下已经存在!',
333         # 898: '上传文件失败, 上传命令不规范!',
334         # 897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
335         # 899: '上传文件必须要有文件的md5值以及文件名!',
336         # 续传: 860, 869
337         # 860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
338         # 869: '您选择文件路径中没有要续传的文件, 请核对!',
339         response_dic = self.receive_header()
340         status_code, status_msg, residual_space_size, already_upload_size = response_dic.get(
341             'status_code'), response_dic.get(
342             'status_msg'), response_dic.get('residual_space_size'), response_dic.get('already_upload_size')
343 
344         # 判断状态码
345         # 800: '你可以上传文件, 在您上传之前, 您的目前空间:%s!',
346         # 860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
347         if status_code == 800 or status_code == 860:  # 800正常发送文件确认  860续传文件确认
348             print(status_msg % self.conversion_quota(residual_space_size))
349 
350             initial_size = 0
351             if resume_upload:  # 断点续传时执行: 目前文件总大小要减去上次没有上传完位置的大小
352                 total_size = file_size - already_upload_size
353             else:  # 正常上传时执行
354                 total_size = file_size
355             with open(file_path, 'rb') as f:
356                 if resume_upload:  # 断点续传时执行: 光标移动到上次没有上传完位置
357                     f.seek(already_upload_size)
358                 print('\nupload running...')
359                 for line in f:
360                     self.socket.sendall(line)
361                     initial_size += len(line)
362                     percent = initial_size / total_size
363                     self.progress_bar(percent)
364                 print('\nupload succeed!')
365 
366             # 第二次接收消息, 确认文件上传完毕
367             # 801, 895, 896
368             # 801: '上传文件成功, 您上传完后的剩余空间:%s!',
369             # 895: '上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!',
370             # 896: '上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!',
371             response_dic = self.receive_header()
372             status_code, status_msg, residual_space_size = response_dic.get('status_code'), response_dic.get(
373                 'status_msg'), response_dic.get('residual_space_size')
374             if residual_space_size:  # 801, 896
375                 print(status_msg % self.conversion_quota(residual_space_size))
376             else:  # 895
377                 print(status_msg)
378         else:
379             # 正常: 894, 897, 898, 899
380             # 894: '您不需要再本路径下上传文件, 该文件在您的当前路径下已经存在!',
381             # 897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
382             # 898: '上传文件失败, 上传命令不规范!',
383             # 899: '上传文件必须要有文件的md5值以及文件名!',
384             # 续传:
385             # 869: '您选择文件路径中没有要续传的文件, 请核对!',
386             if residual_space_size:  # 897
387                 print(status_msg % self.conversion_quota(residual_space_size))
388             else:  # 869, 894, 898, 899
389                 print(status_msg)
390 
391     def __resume_download(self, path, unfinished_file_size):
392         self._download(path, unfinished_file_size, resume_download=True)
393 
394     def _download(self, path, unfinished_file_size=None, resume_download=False):
395         """
396 
397         900: '准备开始下载文件!',
398         999: '下载文件失败, 您要下载的文件路径不规范!',
399         :param path:
400         :param resume_download:
401         :return:
402         """
403         if resume_download:
404             action_type = 'resume_download'
405         else:
406             action_type = 'download'
407         self.send_header(action_type=action_type, path=path, unfinished_file_size=unfinished_file_size)
408 
409         # 接收服务端消息
410         # self.send_header(status_code=900, file_name=file_name, file_size=file_size, file_md5=file_md5)
411         response_dic = self.receive_header()
412         status_code, status_msg, file_name, file_size, file_md5 = response_dic.get('status_code'), response_dic.get(
413             'status_msg'), response_dic.get('file_name'), response_dic.get('file_size'), response_dic.get('file_md5')
414 
415         # 判断状态码
416         # 900: '准备开始下载文件!',
417         # 950: '准备开始续传文件!',
418         # 998: '下载文件失败, 您要下载的文件路径不存在!',
419         # 999: '下载文件失败, 您要下载的文件路径不规范!',
420         if status_code == 900 or status_code == 950:
421 
422             file_path = os.path.join(settings.FILES_PATH, file_name)
423             if resume_download and file_path in self.breakpoint_resume.keys():
424                 unfinished_file_path = self.breakpoint_resume[file_path]['unfinished_file_path']
425             else:
426                 # 判断本次路径下是否有文件, 有文件则提示
427                 # file_path = os.path.join(settings.FILES_PATH, file_name)
428                 if os.path.isfile(file_path):
429                     print('本次路径下文件已经存在, 不需要继续下载!')
430                     return
431                 # 为没有下载完毕的文件名添加后缀
432                 unfinished_file_path = '%s.%s' % (file_path, 'download')
433 
434             # 为出现下载终端添加断点记录
435             self.breakpoint_resume[unfinished_file_path] = {'file_name': file_name, 'file_size': file_size,
436                                                             'path': path}
437 
438             # 开始进行下载
439             receive_size = 0
440             if resume_download:
441                 total_size = file_size - os.path.getsize(unfinished_file_path)
442                 mode = 'a'
443             else:
444                 total_size = file_size
445                 mode = 'w'
446             with open(unfinished_file_path, '%sb' % mode) as f:
447                 print('\ndownload run...')
448                 while receive_size < total_size:
449                     data_bytes = self.socket.recv(self.max_packet_size)
450                     f.write(data_bytes)
451                     receive_size += len(data_bytes)
452                     percent = receive_size / total_size
453                     self.progress_bar(percent)
454                 print('\ndownload succeed!')
455                 f.flush()
456 
457             # 正常下载成功把后缀去除, 文件改名, 删除断点记录
458             del self.breakpoint_resume[unfinished_file_path]
459             os.rename(unfinished_file_path, file_path)
460 
461             # 效验md5值询问用户是否保存
462             server_file_md5 = file_md5
463             current_file_md5 = self.md5(file_path)
464             if server_file_md5 != current_file_md5:
465                 print('您的文件不完成, 可能不能打开, 请重新下载!')
466         else:
467             # 998: '下载文件失败, 您要下载的文件路径不存在!',
468             # 999: '下载文件失败, 您要下载的文件路径不规范!',
469             print(status_msg)
470 
471     @staticmethod
472     def conversion_quota(residual_space_size):
473         """
474         换算服务端发送过来的字节为MB, 人性化的展现用户的空间剩余.
475         :param residual_space_size: 剩余空间字节数
476         :return: MB为单位的字节
477         """
478         residual_space_mb = residual_space_size / (1024 ** 2)
479         return '%.2fMB' % residual_space_mb
480 
481     def receive_header(self):
482         """
483         接收服务端发送过来的报头字典.
484         :return: {'status_code': 100, 'status_msg': '认证成功', 'cmd_size': 199}
485         """
486         header_bytes = self.socket.recv(self.fixed_packet_size)
487         header_dic_json_length = struct.unpack(self.struct_fmt, header_bytes)[0]
488         # 接收报头
489         header_dic_json = self.socket.recv(header_dic_json_length).decode(self.encoding)
490         header_dic = json.loads(header_dic_json)
491         return header_dic
492 
493     def send_header(self, *, action_type, **kwargs):
494         """
495         发送报头字典给客户端.
496         :param action_type: action_type='auth'
497         :param kwargs: {'username': 'egon', 'password': '123'}
498         :return: None
499         """
500         request_dic = kwargs
501         request_dic['action_type'] = action_type
502         request_dic.update(request_dic)
503 
504         request_dic_json_bytes = json.dumps(request_dic).encode(self.encoding)
505         request_dic_json_bytes_length = len(request_dic_json_bytes)
506         header_bytes = struct.pack(self.struct_fmt, request_dic_json_bytes_length)
507 
508         # 发送报头
509         self.socket.sendall(header_bytes)
510         # 发送json后bytes后的字典request_dic
511         self.socket.sendall(request_dic_json_bytes)
512 
513     @staticmethod
514     def md5(file_path):
515         """
516         md5加密哈希文件.
517         :param file_path: files下的文件路径
518         :return: 文件hash值
519         """
520         md5_obj = hashlib.md5()
521         with open(file_path, 'rb') as f:
522             for line in f:
523                 md5_obj.update(line)
524         return md5_obj.hexdigest()
525 
526     @staticmethod
527     def progress_bar(percent, width=50, symbol='#'):
528         """进度条功能."""
529         if percent > 1:
530             percent = 1
531         show_str = ('[%%-%ds]' % width) % (int(width * percent) * symbol)
532         print('\r%s %.2f%%' % (show_str, percent * 100), end='')
533 
534     @staticmethod
535     def show_str():
536         """显示客户端flies中的文件列表."""
537         print('\n------您的files文件夹下所含有的文件------')
538         for index, filename in enumerate(os.listdir(settings.FILES_PATH), 1):
539             print('%s: %s' % (index, filename))
540         print()
541 
542     @staticmethod
543     def help_msg(msgs=None):
544         """帮助信息."""
545         if msgs in settings.help_dic:
546             print(settings.help_dic[msgs])
547         else:
548             return True
main.py

3) files

  • 存放上传服务器的目录
 1     # encoding: utf-8
 2 
 3 import os
 4 import sys
 5 
 6 BASE_DIR = os.path.normpath(os.path.join(__file__, '..'))
 7 print(BASE_DIR)
 8 sys.path.append(BASE_DIR)
 9 
10 if __name__ == '__main__':
11     from core import main
12     client = main.FTPClient(('127.0.0.1', 8080))
13     client.interactive()
ftp_client.py

②server

1) conf

 1 [egon]
 2 password = 202cb962ac59075b964b07152d234b70
 3 quota = 100
 4 
 5 [alex]
 6 password = 202cb962ac59075b964b07152d234b70
 7 quota = 100
 8 
 9 [ly]
10 password = 202cb962ac59075b964b07152d234b70
11 quota = 200
12 
13 [jzd]
14 password = 202cb962ac59075b964b07152d234b70
15 quota = 300
16 
17 [shx]
18 password = 202cb962ac59075b964b07152d234b70
19 quota = 300
20 
21 
22 [xxx]
23 password = 202cb962ac59075b964b07152d234b70
24 quota = 300
accounts.ini
  1 import os
  2 
  3 
  4 def base_dir(*args):
  5     return os.path.normpath(os.path.join(__file__, '..', '..', *args))
  6 
  7 
  8 # 用户家目录存放路径
  9 USER_HOME_DIR = base_dir('home')
 10 
 11 # 用户账户信息文件路径
 12 ACCOUNTS_FILE = base_dir('conf', 'accounts.ini')
 13 
 14 # 本机测试的ip和port
 15 HOST = '127.0.0.1'
 16 PORT = 8080
 17 
 18 # 状态码: 负责提供交互成功及失败的提示信息反馈
 19 STATUS_CODE = {
 20     100: '用户名密码正确, 认证成功!',
 21     199: '用户名密码不正确, 认证失败!',
 22     200: '您的功能指定不能为空!',
 23     201: '没有该功能, 请查看帮助信息!',
 24     301: '本次返回结果包含命令大小.',
 25     400: '切换目录成功',
 26     498: '切换目录失败, 切换命令不规范',
 27     499: '切换目录失败, 目标地址不存在!',
 28     500: '创建目录成功!',
 29     598: '创建目录命令输入不规范!',
 30     599: '创建的目录已存在!',
 31     600: '删除目录成功!',
 32     699: '删除目录失败, 该目录不为空!',
 33     698: '删除目录失败, 不存在该目录!',
 34     697: '删除目录失败, 删除命令不规范!',
 35     700: '删除文件成功!',
 36     799: '删除文件失败, 不存在该文件!',
 37     800: '你可以上传文件, 在您上传之前, 您的目前空间:%s!',
 38     801: '上传文件成功, 您上传完后的剩余空间:%s!',
 39     850: '服务端检测您还有为上传完的文件, 是否继续上传!',
 40     851: '服务端检测您没有未上传完成的文件!',
 41     852: '您不能进行续传, 因为该文件是完整文件!',
 42     860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
 43     869: '您选择文件路径中没有要续传的文件, 请核对!',
 44     894: '您不需要再对本路径下上传文件, 该文件在您的当前路径下已经存在!',
 45     895: '上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!',
 46     896: '上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!',
 47     897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
 48     898: '上传文件失败, 上传命令不规范!',
 49     899: '上传文件必须要有文件的md5值以及文件名!',
 50     900: '准备开始下载文件!',
 51     950: '准备开始续传文件!',
 52     998: '下载文件失败, 您要下载的文件路径不存在!',
 53     999: '下载文件失败, 您要下载的文件路径不规范!',
 54 }
 55 
 56 # log日志路径
 57 ACCESS_LOG_PATH = base_dir('log', 'access.log')
 58 
 59 # 定义log日志输出格式
 60 standard_format = '%(asctime)s - %(threadName)s:%(thread)d - task_id:%(name)s - %(filename)s:%(lineno)d - ' \
 61                   '%(levelname)s - %(message)s'  # 其中name为getlogger指定的名字
 62 
 63 simple_format = '\n%(levelname)s - %(asctime)s - %(filename)s:%(lineno)d - %(message)s\n'
 64 
 65 
 66 # log配置字典
 67 LOGGING_DIC = {
 68     'version': 1,
 69     'disable_existing_loggers': False,
 70     'formatters': {
 71         'standard': {
 72             'format': standard_format
 73         },
 74         'simple': {
 75             'format': simple_format,
 76         },
 77     },
 78     'filters': {},
 79     'handlers': {
 80         # 打印到终端的日志
 81         'console': {
 82             'level': 'DEBUG',
 83             'class': 'logging.StreamHandler',  # 打印到屏幕
 84             'formatter': 'simple'
 85         },
 86         # 打印到文件的日志,收集info及以上的日志
 87         'access': {
 88             'level': 'DEBUG',
 89             'class': 'logging.handlers.RotatingFileHandler',  # 保存到文件
 90             'formatter': 'standard',
 91             'filename': ACCESS_LOG_PATH,  # 日志文件
 92             # 'maxBytes': 1024 * 1024 * 5,  # 日志大小 5M
 93             'maxBytes': 1024 * 1024 * 5,
 94             'backupCount': 10,
 95             'encoding': 'utf-8',  # 日志文件的编码,再也不用担心中文log乱码了
 96         },
 97     },
 98     'loggers': {
 99         # logging.getLogger(__name__)拿到的logger配置
100         '': {
101             'handlers': ['access', 'console'],  # 这里把上面定义的两个handler都加上,即log数据既写入文件又打印到屏幕
102             'level': 'DEBUG',
103             'propagate': False,  # 向上(更高level的logger)传递
104         },
105     },
106 }
settings.py

2) core

  1 import json
  2 import os
  3 import shelve
  4 import struct
  5 import subprocess
  6 
  7 from conf import settings
  8 from lib import common
  9 
 10 
 11 class HandlerRequest:
 12     """处理用户请求."""
 13     max_packet_size = 8192
 14     encoding = 'utf-8'
 15 
 16     struct_fmt = 'i'
 17     fixed_packet_size = 4
 18 
 19     logger = common.load_my_logging_cfg()
 20 
 21     def __init__(self, request, address):
 22         self.request = request
 23         self.address = address
 24 
 25         self.residual_space_size = None
 26 
 27         self.breakpoint_resume = None
 28 
 29         self.username = None
 30         self.user_obj = None
 31         self.user_current_dir = None
 32 
 33     def client_close(self):
 34         """关闭客户端连接."""
 35         self.request.close()
 36 
 37     def handle_request(self):
 38         """处理客户端请求."""
 39         count = 0
 40         while count < 3:  # 连接循环
 41             try:
 42                 if self.auth():
 43                     # 收消息
 44                     user_dic = self.receive_header()
 45                     action_type = user_dic.get('action_type')
 46                     if action_type:
 47                         if hasattr(self, '_%s' % action_type):
 48                             func = getattr(self, '_%s' % action_type)
 49                             func(user_dic)
 50                         else:
 51                             self.send_header(status_code=201)
 52                     # 发消息
 53                     else:
 54                         self.send_header(status_code=200)
 55                 else:
 56                     count += 1
 57                     self.send_header(status_code=199)
 58             except ConnectionResetError:
 59                 break
 60         # 关闭客户端连接
 61         self.logger.info('----连接断开---- ip:%s port:%s' % self.address)
 62         self.client_close()
 63 
 64     def unfinished_file_check(self):
 65         self.logger.info('#执行unfinished_file_check命令# ip:%s port:%s' % self.address)
 66 
 67         if not list(self.breakpoint_resume.keys()):
 68             self.send_header(status_code=851)
 69             return
 70 
 71         #  self.breakpoint_resume[file_path] =
 72         #  {'file_size': _file_size, 'unfinished_file_path': unfinished_file_path, 'file_name': _file_name}
 73         msg_list = []
 74 
 75         for index, abs_path in enumerate(self.breakpoint_resume.keys(), 1):\
 76 
 77             user_path = '/'.join(abs_path.split(self.username)[-1].split(os.sep))
 78             print('abs_path:', user_path)
 79             file_name = self.breakpoint_resume[abs_path]['file_name']
 80             src_file_size = self.breakpoint_resume[abs_path]['file_size']
 81             unfinished_file_size = os.path.getsize(self.breakpoint_resume[abs_path]['unfinished_file_path'])
 82             percent = unfinished_file_size / src_file_size * 100
 83 
 84             msg = """
 85             数量: %s  文件路径: %s 文件名: %s
 86                 文件原大小: %s字节 未完成的文件大小: %s字节 上传的百分比: %.2f%%
 87             """ % (index, user_path, file_name, src_file_size, unfinished_file_size, percent)
 88 
 89             msg_list.append(msg)
 90             # msg_dic['/03_函数调用的三种形式.mp4'] = 5772100
 91             # msg_dic[user_path] = unfinished_file_size
 92         # self.send_header(status_code=850, msg_list=msg_list, msg_dic=msg_dic)
 93         self.send_header(status_code=850, msg_list=msg_list)
 94 
 95     def auth(self):
 96         """用户登陆认证."""
 97         if self.user_current_dir:
 98             return True
 99 
100         # 涉及到交叉导入
101         from core import main
102         # 收消息
103         auth_dic = self.receive_header()
104 
105         user_name = auth_dic.get('username')
106         user_password = auth_dic.get('password')
107         md5_password = common.md5('password', password=user_password)
108 
109         # print(user_name, user_password,  md5_password)
110 
111         accounts = main.FTPServer.load_accounts()
112         if user_name in accounts.sections():
113             if md5_password == accounts[user_name]['password']:
114                 self.send_header(status_code=100)
115 
116                 self.username = user_name
117                 self.user_obj = accounts[user_name]
118                 self.user_obj['home'] = os.path.join(settings.USER_HOME_DIR, user_name)
119                 self.user_current_dir = self.user_obj['home']
120 
121                 # print('self.user_obj:', self.user_obj)
122                 # print("self.user_obj['home']:", self.user_obj['home'])
123 
124                 self.residual_space_size = common.conversion_quota(
125                     self.user_obj['quota']) - common.get_size(self.user_obj['home'])
126 
127                 breakpoint_resume_dir_path = os.path.join(self.user_obj['home'], '.upload')
128                 if not os.path.isdir(breakpoint_resume_dir_path):
129                     os.mkdir(breakpoint_resume_dir_path)
130                 self.breakpoint_resume = shelve.open(os.path.join(breakpoint_resume_dir_path, '.upload.shv'))
131                 self.unfinished_file_check()
132 
133                 self.logger.info('#认证成功# ip:%s port:%s' % self.address)
134                 return True
135         self.logger.info('#认证失败# ip:%s port:%s' % self.address)
136         return False
137 
138     def _ls(self, cmd_dic):
139         """
140         运行dir命令将结果发送到客户端.
141         :param cmd_dic: {'path': [], 'action_type': 'ls'}
142                     或  {'path': ['目录1', '目录2', '目录xxx'], 'action_type': 'ls'}
143                     或  {'path': ['?'], 'action_type': 'ls'}
144         :return: None
145         """
146         # print('_ls:', cmd_dic)
147         self.logger.info('#执行ls命令# ip:%s port:%s' % self.address)
148 
149         # 核验路径
150         dir_path = self.verify_path(cmd_dic)
151         if not dir_path:
152             dir_path = self.user_current_dir
153 
154         if cmd_dic.get('path') == ['?']:  # 为用户提供ls /?命令
155             dir_path = '/?'
156 
157         sub_obj = subprocess.Popen(
158             'dir %s' % dir_path,
159             shell=True,
160             stderr=subprocess.PIPE,
161             stdout=subprocess.PIPE
162         )
163         stderr_bytes, stdout_bytes = sub_obj.stderr.read(), sub_obj.stdout.read()
164         cmd_size = len(stderr_bytes) + len(stdout_bytes)
165 
166         # 发报头
167         self.send_header(status_code=301, cmd_size=cmd_size)
168         # 发消息
169         self.request.sendall(stderr_bytes)
170         self.request.sendall(stdout_bytes)
171 
172     def _cd(self, cmd_dic):
173         """
174         根据用户的目标目录, 改变用户的当前目录的值.
175         :param cmd_dic: {'action_type': 'cd', 'path': ['..']}
176                      或 {'action_type': 'cd', 'path': ['目录1', '目录2', '目录xxx'], }
177         :return: None
178                  Z:\\pycharm\\开发FTP程序之路\\第2次FTP_第四模块作业\\FUCK_FTP\\server\\home\\egon\\目录1
179         """
180         # print('_cd:', cmd_dic)
181         self.logger.info('#执行cd命令# ip:%s port:%s' % self.address)
182 
183         # 核验路径
184         dir_path = self.verify_path(cmd_dic)
185         if dir_path:
186             if os.path.isdir(dir_path):  # 判断用户切换的路径是否存在
187                 self.user_current_dir = dir_path
188                 if dir_path == self.user_obj['home']:
189                     current_dir = '~'
190                 else:
191                     join_dir = ''.join(dir_path.split('%s' % self.username)[1:])
192                     current_dir = '/'.join(join_dir.split('\\'))
193                 self.send_header(status_code=400, current_dir=current_dir)
194             else:
195                 self.send_header(status_code=499)
196         else:
197             self.send_header(status_code=498)
198 
199     def _mkdir(self, cmd_dic):
200         """
201         更具用户的目标目录, 且目录不存在, 创建目录标目录, 生成多层递归目录.
202         :param cmd_dic: {'action_type': 'mkdir', 'path': ['目录1']}
203                     或  {'action_type': 'mkdir', 'path': ['目录2', '目录3', '目录xxx']}
204         :return: None
205         """
206         # print('_mkdir:', cmd_dic)
207         self.logger.info('#执行mkdir命令# ip:%s port:%s' % self.address)
208 
209         dir_path = self.verify_path(cmd_dic)
210         if dir_path:
211             if not os.path.isdir(dir_path):  # 判断用户要创建的目录时否存在
212                 os.makedirs(dir_path)
213                 self.send_header(status_code=500)
214             else:
215                 self.send_header(status_code=599)
216         else:
217             self.send_header(status_code=598)
218 
219     def _rmdir(self, cmd_dic):
220         """
221         更具用户的目标目录, 删除不为空的目录.
222         :param cmd_dic: {'path': ['目录1', '目录xxx', '空目录'], 'action_type': 'rmdir'}
223         :return: None
224         """
225         # print('_rmdir:', cmd_dic)
226         self.logger.info('#执行rmdir命令# ip:%s port:%s' % self.address)
227 
228         dir_path = self.verify_path(cmd_dic)
229         if dir_path:
230             if os.path.isdir(dir_path):
231                 if os.listdir(dir_path):
232                     self.send_header(status_code=699)
233                 else:
234                     os.rmdir(dir_path)
235                     self.send_header(status_code=600)
236             else:
237                 self.send_header(status_code=698)
238         else:
239             self.send_header(status_code=697)
240 
241     def _remove(self, cmd_dic):
242         """
243         更具用户的目标文件, 删除该文件
244         :param cmd_dic: {'path': ['目录1', '目录xxx', '文件'], 'action_type': 'remove'}
245         :return:
246         """
247         # print('_remove:', cmd_dic)
248         self.logger.info('#执行remove命令# ip:%s port:%s' % self.address)
249         file_path = self.verify_path(cmd_dic)
250 
251         if file_path:
252             if os.path.isfile(file_path):
253                 # 判断用户删除的文件是否是要续传的文件, 如果是则先把把续传的记录删除
254                 if file_path in self.breakpoint_resume.keys:
255                     del self.breakpoint_resume[file_path]
256                 os.remove(file_path)
257                 self.send_header(status_code=700)
258             else:
259                 self.send_header(status_code=799)
260         else:
261             self.send_header(status_code=798)
262 
263     def _resume_upload(self, cmd_dic):
264         """
265         860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
266         869: '您选择文件路径中没有要续传的文件, 请核对!',
267         :param cmd_dic:
268         :return:
269         """
270         # print('def _resume_upload ===> cmd_args', cmd_dic)
271         self.logger.info('#执行resume_upload命令# ip:%s port:%s' % self.address)
272         self._upload(cmd_dic, resume_upload=True)
273 
274     def _upload(self, cmd_dic, resume_upload=False):
275         """客户端
276             800: '你可以上传文件, 在您上传之前, 您的目前空间:%s!',
277             801: '上传文件成功, 您上传完后的剩余空间:%s!',
278             850: '您的还有为上传完的文件, 是否继续上传!',
279             851: '检测您不存在未上传完成的文件!',
280             852: '您不能进行续传, 因为该文件是完整文件!',
281             860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
282             869: '您选择文件路径中没有要续传的文件, 请核对!',
283             894: '您不需要再对本路径下上传文件, 该文件在您的当前路径下已经存在!',
284             895: '上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!',
285             896: '上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!',
286             897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
287             898: '上传文件失败, 上传命令不规范!',
288             899: '上传文件必须要有文件的md5值以及文件名!',
289         """
290         # print('_upload:', cmd_dic)
291         if not resume_upload:
292             self.logger.info('#执行upload命令# ip:%s port:%s' % self.address)
293 
294         # 效验: 897, 898, 899
295         _path, _file_md5, _file_name, _file_size = cmd_dic.get('path'), cmd_dic.get('file_md5'), cmd_dic.get(
296             'file_name'), cmd_dic.get('file_size')
297         file_path = self.verify_upload_action(cmd_dic, _path=_path, _file_md5=_file_md5, _file_name=_file_name,
298 
299                                               _file_size=_file_size)
300 
301         if resume_upload:   # 断点续传时执行
302             if not file_path or file_path not in self.breakpoint_resume.keys():
303                 # 869: '您选择文件路径中没有要续传的文件, 请核对!',
304                 self.send_header(status_code=869)
305                 return
306 
307             # 找到之前未穿完的文件名
308             unfinished_file_path = self.breakpoint_resume[file_path]['unfinished_file_path']
309             already_upload_size = os.path.getsize(unfinished_file_path)
310 
311             # 效验成功通知续传信号
312             # 860: '您正在继续上传文件, 在您继传之前, 您的目前空间:%s!',
313             self.send_header(status_code=860, residual_space_size=self.residual_space_size,
314                              already_upload_size=already_upload_size)
315 
316             total_size = _file_size - already_upload_size
317             mode = 'a'
318         else:           # 正常上传执行
319             if not file_path:
320                 return
321 
322             # 判断用户上传的文件是否重复
323             if os.path.isfile(file_path):
324                 # 894: '您不需要再对本路径下上传文件, 该文件在您的当前路径下已经存在!',
325                 self.send_header(status_code=894)
326                 return
327             else:
328                 unfinished_file_path = '%s.%s' % (file_path, 'upload')
329 
330             # 效验成功通知上传信号: 800
331             # 800: '你可以上传文件, 在您上传之前, 您的目前空间:%s!',
332             self.send_header(status_code=800, residual_space_size=self.residual_space_size)
333 
334             total_size = _file_size
335             mode = 'w'
336 
337         # 记录断点的功能: 在服务端用户的路径, 记录文件大小, 加上后缀的路径, 文件名
338         # 或再次为未传完的文件记录断点
339         self.breakpoint_resume[file_path] = {'file_size': _file_size, 'unfinished_file_path': unfinished_file_path,
340                                              'file_name': _file_name}
341 
342         # 开始接收文件
343         receive_size = 0
344         with open(unfinished_file_path, '%sb' % mode) as f:
345             while receive_size < total_size:
346                 data_bytes = self.request.recv(self.max_packet_size)
347                 receive_size += len(data_bytes)
348                 f.write(data_bytes)
349         # 接收完毕, 把后缀改成用户上传的文件名
350         os.rename(unfinished_file_path, file_path)
351         # 删除记录断点的功能
352         del self.breakpoint_resume[file_path]
353 
354         # 801, 895, 896
355         # 效验用户端发送的md5于本次上传完毕的md5值
356         upload_file_md5 = common.md5(encryption_type='file', path=file_path)
357         if upload_file_md5 != _file_md5:
358             # print('def _upload ===> upload_file_md5:%s, _file_md5:%s' % (upload_file_md5, _file_md5))
359             # 895: '上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!',
360             self.send_header(status_code=895)
361             os.remove(file_path)
362             return
363 
364         # 安全性问题: 再次判断用户是否以假的文件大小来跳出服务端限制的配额
365         if receive_size > self.residual_space_size:
366             # 896: '上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!',
367             self.send_header(status_code=896, residual_space_size=self.residual_space_size)
368             os.remove(file_path)
369             return
370         else:
371             self.residual_space_size = self.residual_space_size - receive_size
372             # print('def _upload ===> receive_size:', receive_size)
373             # print('def _upload ===> os.path.getsize(file_path)', os.path.getsize('%s' % file_path))
374             # 801: '上传文件成功, 您上传完后的剩余空间:%s!',
375             self.send_header(status_code=801, residual_space_size=self.residual_space_size)
376 
377     def _resume_download(self, cmd_dic):
378         self._download(cmd_dic, resume_download=True)
379 
380     def _download(self, cmd_dic, resume_download=False):
381         self.logger.info('#执行download命令# ip:%s port:%s' % self.address)
382 
383         file_path = self.verify_path(cmd_dic)
384         if not file_path:
385             # 999: '下载文件失败, 您要下载的文件路径不规范!',
386             self.send_header(status_code=999)
387             return
388 
389         if not os.path.isfile(file_path):
390             # 998: '下载文件失败, 您要下载的文件路径不存在!',
391             self.send_header(status_code=998)
392             return
393 
394         # 通知可以开始下载
395         # 900: '准备开始下载文件!'.
396         file_name = file_path.split(os.sep)[-1]
397         file_size = os.path.getsize(file_path)
398         file_md5 = common.md5('file', file_path)
399         unfinished_file_size = cmd_dic.get('unfinished_file_size')
400         if resume_download:
401             # 950: '准备开始续传文件!',
402             self.send_header(status_code=950, file_name=file_name, file_size=file_size, file_md5=file_md5)
403         else:
404             # 900: '准备开始下载文件!'.
405             self.send_header(status_code=900, file_name=file_name, file_size=file_size, file_md5=file_md5)
406 
407         # 打开文件发送给客户端
408         with open(file_path, 'rb') as f:
409             if resume_download:
410                 f.seek(unfinished_file_size)
411             for line in f:
412                 self.request.sendall(line)
413 
414     def verify_upload_action(self, cmd_dic, *, _path, _file_name, _file_md5, _file_size):
415         """
416         核验上传功能.
417         897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
418         898: '上传文件失败, 上传命令不规范!',
419         899: '上传文件必须要有文件的md5值以及文件名!',
420         """
421         # _path=['03_函数调用的三种形式.mp4']
422         if _path is None:
423             if _file_name and _file_md5 and _file_size:
424                 if _file_size > self.residual_space_size:
425                     # print('def _upload ===> self.residual_space_size:', self.residual_space_size)
426 
427                     # 897: '上传文件失败, 您的空间不足, 您的剩余空间:%s!',
428                     self.send_header(status_code=897, residual_space_size=self.residual_space_size)
429                     return False
430                 else:
431                     # Z:\pycharm\开发FTP程序之路\第2次FTP_第四模块作业\FUCK_FTP\server\home\egon\03_函数调用的三种形式.mp4
432                     file_path = os.path.join(self.user_current_dir, _file_name)
433             else:
434                 # 899: '上传文件必须要有文件的md5值以及文件名!',
435                 self.send_header(status_code=899)
436                 return False
437         else:
438             path = self.verify_path(cmd_dic)
439 
440             if not path:
441                 # 898: '上传文件失败, 上传命令不规范!',
442                 self.send_header(status_code=898)
443                 return False
444             else:
445                 # Z:\pycharm\开发FTP程序之路\第2次FTP_第四模块作业\FUCK_FTP\server\home\egon\03_函数调用的三种形式.mp4
446                 file_path = os.path.join(path, _file_name)
447         return file_path
448 
449     def verify_path(self, cmd_dic):
450         """
451         核验客户端传过来的路径.
452         :param cmd_dic: {'action_type': 'ls', 'path': []}
453                     或  {'action_type': 'ls', 'path': ['目录1', '目录xxx']}
454                     或  {action_type': 'cd', 'path': ['目录2', '目录xxx']}
455         :return: None
456                  Z:\\pycharm\\开发FTP程序之路\\第2次FTP_第四模块作业\\FUCK_FTP\\server\\home\\egon\\目录1
457                  Z:\\pycharm\\开发FTP程序之路\\第2次FTP_第四模块作业\\FUCK_FTP\\server\\home\\egon\\目录1
458         """
459         # print(cmd_dic)
460         path = cmd_dic.get('path')
461         if path:
462             if isinstance(path, list):
463                 for element in path:
464                     if not isinstance(element, str):
465                         path = None
466                         return path
467                 abspath = os.path.normpath(os.path.join(self.user_current_dir, *path))
468                 # print('def verify_path() ===> abspath:', abspath)
469                 if abspath.startswith(self.user_obj['home']):
470                     path = abspath
471                 else:
472                     path = None  # 用户目录超出限制
473             else:
474                 path = None  # 不是列表类型例: '字符串'
475         else:
476             path = None  # []
477         # print('def verify_path() ====> path', path)
478         return path
479 
480     def receive_header(self):
481         """
482         接收客户端数据.
483         :return: {'action_type': 'cd', 'path': ['目录1', '目录xxx']}
484         """
485         header_bytes = self.request.recv(self.fixed_packet_size)
486         request_dic_json_length = struct.unpack(self.struct_fmt, header_bytes)[0]
487         # print('request_dic_json_length:', request_dic_json_length)
488         # 接收报头
489         request_dic_json = self.request.recv(request_dic_json_length).decode(self.encoding)
490         request_dic = json.loads(request_dic_json)
491 
492         # print('request_dic:', request_dic)
493 
494         if not request_dic:
495             return {}
496         # print("def receive_header():", request_dic)
497         return request_dic
498 
499     def send_header(self, *, status_code, **kwargs):
500         """
501         发送数据给客户端.
502         :param status_code: 400
503         :param kwargs: {'current_dir': '/home/egon/目录1/目录xxx'}
504         :return: None
505         """
506         # print(status_code)
507         # print(kwargs)
508         from core import main
509 
510         response_dic = kwargs
511         response_dic['status_code'] = status_code
512         response_dic['status_msg'] = main.FTPServer.STATUS_CODE[status_code]
513         response_dic.update(kwargs)
514 
515         response_dic_json_bytes = json.dumps(response_dic).encode(self.encoding)
516         response_dic_json_bytes_length = len(response_dic_json_bytes)
517         header_bytes = struct.pack(self.struct_fmt, response_dic_json_bytes_length)
518 
519         # print('header_bytes:', header_bytes)
520 
521         # 发送报头
522         self.request.sendall(header_bytes)
523         # 发送json后bytes后的字典response_dic
524         self.request.sendall(response_dic_json_bytes)
handler_request.py
 1 import configparser
 2 import socket
 3 
 4 from conf import settings
 5 from core import handler_request, mythreadpool
 6 from lib import common
 7 
 8 
 9 class FTPServer:
10     """FTP服务器."""
11     address_family = socket.AF_INET
12     socket_type = socket.SOCK_STREAM
13     allow_reuse_address = False
14     request_queue_size = 5
15 
16     max_pool_size = 5
17 
18     STATUS_CODE = settings.STATUS_CODE
19 
20     logger = common.load_my_logging_cfg()
21 
22     def __init__(self, management_instance, bind_address, bind_and_activate=True):
23         self.management_instance = management_instance
24 
25         self.pool = mythreadpool.MyThreadPool(self.max_pool_size)
26 
27         self.bind_address = bind_address
28         self.socket = socket.socket(self.address_family, self.socket_type)
29 
30         if bind_and_activate:
31             try:
32                 self.server_bind()
33                 self.server_activate()
34             except Exception:
35                 self.server_close()
36                 raise
37 
38     def server_bind(self):
39         """服务器绑定IP,端口."""
40         if self.allow_reuse_address:
41             self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42         self.socket.bind(self.bind_address)
43 
44     def server_activate(self):
45         """服务器激活."""
46         self.socket.listen(self.request_queue_size)
47 
48     def server_close(self):
49         """关闭服务socket对象."""
50         self.socket.close()
51 
52     def serve_forever(self):
53         """服务器永远运行."""
54         while True:  # 通信循环
55             request, address = self.socket.accept()
56 
57             self.logger.info('----连接----# ip:%s port:%s' % address)
58 
59             # 来一个连接, 实例化一个处理用户请求的对象
60             handler_response = handler_request.HandlerRequest(request, address)
61             # 来了一个连接取走一个线程
62             thread = self.pool.get_thread()
63             # 同时再添加一个线程
64             self.pool.put_thread()
65             t = thread(target=handler_response.handle_request)
66             t.start()
67 
68     @staticmethod
69     def load_accounts():
70         conf_obj = configparser.ConfigParser()
71         conf_obj.read(settings.ACCOUNTS_FILE)
72         return conf_obj
main.py
 1 import sys
 2 
 3 from conf import settings
 4 from core import main
 5 
 6 
 7 class ManagementTool(object):
 8     """管理服务器."""
 9     center_args1, center_args2 = 50, '-'
10 
11     def __init__(self):
12         self.script_argv = sys.argv
13         self.commands = None
14 
15         # print(self.script_argv)
16 
17         self.verify_argv()
18 
19     def verify_argv(self):
20         """
21         核查参数时否合理.
22             例:
23             ['启动文件路径', 'start', 'ftp', 'server']
24         """
25         if len(self.script_argv) != 4:
26             self.help_msg()
27 
28         action_type = self.script_argv[1]
29         self.commands = self.script_argv[2:]
30         if hasattr(self, action_type):
31             func = getattr(self, action_type)
32             func()
33         else:
34             self.help_msg()
35 
36     @staticmethod
37     def help_msg():
38         msg = """
39         ------严格要求输入以下命令:------
40             ① start   ftp server
41             ② stop    ftp server
42             ③ restart ftp server
43         """
44         exit(msg)
45 
46     def start(self):
47         """启动ftp服务."""
48         if self.execute():
49             print('FTP started successfully!')
50             # FTPServer中可能用到ManagementTool中功能
51             server = main.FTPServer(self, (settings.HOST, settings.PORT))
52             server.serve_forever()
53         else:
54             self.help_msg()
55 
56     def execute(self):
57         """解析命令."""
58         args1, args2 = self.commands
59         if args1 == 'ftp' and args2 == 'server':
60             return True
61         return False
management.py
 1 import os
 2 import queue
 3 from threading import Thread
 4 
 5 
 6 class MyThreadPool:
 7     def __init__(self, max_workers=None):
 8         if not max_workers:
 9             max_workers = os.cpu_count() * 5
10         if max_workers <= 0:
11             raise ValueError('max_workers 必须大于0')
12 
13         self.queue = queue.Queue(max_workers)
14         for count in range(max_workers):
15             self.put_thread()
16 
17     def put_thread(self):
18         self.queue.put(Thread)
19 
20     def get_thread(self):
21         return self.queue.get()
mythreadpool.py

3) home

  • 用户目录,以用户名作为文件名

4) lib

 1 import hashlib
 2 import logging.config
 3 import os
 4 
 5 from conf import settings
 6 
 7 
 8 def md5(encryption_type, path=None, password=None):
 9     """
10     md5加密.
11     :param encryption_type: 加密的类型, 支持file和password两种
12     :param path: 文件或目录路径
13     :param password: 明文密码
14     :return: 加密后的md5值
15     """
16     md5_obj = hashlib.md5()
17     if encryption_type == 'file':
18         if os.path.isfile(path):
19             with open(path, 'rb') as f:
20                 for line in f:
21                     md5_obj.update(line)
22             return md5_obj.hexdigest()
23         for filename in os.listdir(path):
24             current_path = os.path.join(path, filename)
25             if os.path.isdir(current_path):
26                 md5(encryption_type, path=current_path)
27             else:
28                 with open(current_path, 'rb') as f:
29                     for line in f:
30                         md5_obj.update(line)
31     elif encryption_type == 'password':
32         md5_obj.update(password.encode('utf-8'))
33     return md5_obj.hexdigest()
34 
35 
36 def load_my_logging_cfg():
37     """
38     加载日志字典.
39     :return: logger对象
40     """
41     logging.config.dictConfig(settings.LOGGING_DIC)
42     logger = logging.getLogger(__name__)
43     return logger
44 
45 
46 def get_size(path):
47     """
48     遍历用户path, 拿到path的路径大小, 该大小包含目录下的所有文件.
49     :param path: 路径
50     :return: 该路径下的所有文件的大小
51     """
52     initial_size = 0
53     if os.path.isfile(path):
54         return os.path.getsize(path)
55     for filename in os.listdir(path):
56         current_path = os.path.join(path, filename)
57         if os.path.isdir(current_path):
58             get_size(current_path)
59         else:
60             initial_size += os.path.getsize(current_path)
61     return initial_size
62 
63 
64 def conversion_quota(quota_mb: str):
65     """
66     换算用户磁盘配额, 把MB换算成bytes.
67     :param quota_mb:
68     :return: 满足isdigit返回quota_bytes, 不满足设置默认的配额大小
69     """
70     if quota_mb.isdigit():
71         quota_mb = int(quota_mb)
72         quota_bytes = quota_mb * 1024 ** 2
73         # print('def conversion_quota ===> quota_bytes:', quota_bytes)
74         return quota_bytes
75     else:
76         default_quota_bytes = 50 * 1024 ** 2
77         return default_quota_bytes
common.py

5) log

  •  access.log
 1 # encoding:utf-8
 2 
 3 import os
 4 import sys
 5 
 6 BASE_DIR = os.path.normpath(os.path.join(__file__, '..'))
 7 sys.path.append(BASE_DIR)
 8 
 9 if __name__ == '__main__':
10     from core import management
11     management = management.ManagementTool()
12     management.execute()
ftp_server.py

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2019-11-06 19:24  给你加马桶唱疏通  阅读(1441)  评论(0编辑  收藏  举报