03 实现支持多用户在线的FTP程序(C/S)
1. 用户加密认证
2. 允许多用户登录
3. 每个用户都有自己的家目录,且只能访问自己的家目录
4. 对用户进行磁盘分配,每一个用户的可用空间可以自己设置
5. 允许用户在ftp server上随意切换目录
6. 允许用户查看自己家目录下的文件
7. 允许用户上传和下载,保证文件的一致性(md5)
8. 文件上传、下载过程中显示进度条
9. 支持多并发的功能
10. 使用队列queue模块,实现线程池
11. 允许用户配置最大的并发数,比如允许只有10并发用户
升级需求:10%
1. 文件支持断点续传
客户端: |-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 # 服务端启动程序
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
1 用户名: 用户密码:
2 alex 123
3 egon 123
4 ly 123
5 jzd 123
6 shx 123
1 username>>:egon
2 password>>:123
3 用户名密码正确, 认证成功!
-
命令 + –-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 .
-
-
指定目录下的文件(只能查看到自己家目录的范围):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 [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 可用字节
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 ~]#
(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 [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的目录]#
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的目录/我是江傻子的目录]#
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 可用字节
① 支持功能:
-
相对路径创建: mkdir /目录
-
生成多层递归目录: mkdir /目录1/目录2
② 运行效果:
1 [egon@localhost ~]# mkdir /a
2 创建目录成功!
1 [egon@localhost ~]# mkdir /a/b
2 创建目录成功!
① 支持功能:
-
删除空目录: rmdir /目录1/空目录2
② 运行效果:
1 [egon@localhost ~]# rmdir /a/b
2 删除目录成功!
① 支持功能:
-
删除文件: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的目录 的目录
① 支持功能:
-
上传到服务端当前路径: 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 [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 可用字节
① 支持功能:
-
继续上传文件到服务端当前路径: 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 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!
① 支持功能:
-
从服务端当前目录下下载文件
-
download 文件
-
-
从服务端绝对路径下下载文件
-
download /目录1/文件
-
② 运行效果:
1 [egon@localhost ~]# download 服务器管理综合报告.docx
2
3 download run...
4 [##################################################] 100.00%
5 download succeed!
1 [egon@localhost ~]# download /我是egon的目录/03_函数调用的三种形式.mp4
2
3 download run...
4 [##################################################] 100.00%
5 download succeed!
① 支持功能:
-
用户登陆的时候显示为下载完的文件、用户根据序号选择要继续续传的文件
-
用户可以多次循环选择
-
支持断点以后据续断点续传
② 运行效果:
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
(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!
-
-
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 }
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
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()
②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
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 }
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)
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
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
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()
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
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()