1. 用户加密认证 2. 容许多用户登陆 3. 每一个用户都有本身的家目录,且只能访问本身的家目录 4. 对用户进行磁盘分配,每个用户的可用空间能够本身设置 5. 容许用户在ftp server上随意切换目录 6. 容许用户查看本身家目录下的文件 7. 容许用户上传和下载,保证文件的一致性(md5) 8. 文件上传、下载过程当中显示进度条 9. 支持多并发的功能 10. 使用队列queue模块,实现线程池 11. 容许用户配置最大的并发数,好比容许只有10并发用户 升级需求:10%
1. 文件支持断点续传
python
客户端: |-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 用户名密码正确, 认证成功!
git
命令 + –-helpshell
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 .
json
lswindows
指定目录下的文件(只能查看到本身家目录的范围)安全
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 ~]#
相对路径切换app
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
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文件夹下的路径进行上传下载, 不能动态指定
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()
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
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()