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

1. 需求 1 . 用户加密认证 2 . 允许多用户登录 3 . 每个用户都有自己的家目录,且只能访问自己的家目录 4 . 对用户进行磁盘分配,每一个用户的可用空间可以自己设置 5 . 允许用户在ftp server上随意切换目录 6 . 允许用户查看自己家目录下的文件 7 . 允许用户上传和下载,保证文件的一致性(md5) 8 . 文件上传、下载过程中显示进度条 9 . 支持多并发的功能 10 . 使用队列queue模块,实现线程池 11 . 允许用户配置最大的并发数,比如允许只有10并发用户
升级需求: 10%
1. 文件支持断点续传 2. 开发环境 Python 3.7.3 3. 软件开发目录结构 目录结构 4. 服务端与客户端的启动 1 1 .打开cmd命令行终端 2 2.python3+启动文件路径+ startftpserver 3 3 .例子: 4 C:\Users\洋辣子>python3Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\ftp_server.pystartftpserver 服务端启动 1 1 . 打开cmd命令行终端 2 2. python3 + 启动文件路径 3 3 . 例子: 4 C:\Users\洋辣子> python3 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\ftp_client.py 客户端启动 5. 用户配置信息 1 用户名: 用户密码: 2 alex 123 3 egon 123 4 ly 123 5 jzd 123 6 shx 123 View Code 6. 所有功能测试 (1) 登陆 1 username>> :egon 2 password>>:123 3 用户名密码正确, 认证成功! View Code (2) 查看所有命令所对应的帮助信息 查看方法: 命令 + –-help 1 [egon@localhost ~] # ls –help 2 3 查看当前目录下的文件: 4 ls 5 指定目录下的文件(只能查看到自己家目录的范围): 6 ls / 我是egon的目录 7 8 [egon@localhost ~] # cd –help 9 10 相对路径切换: 11 cd / 我是egon的目录 12 cd / 我是江傻子的目录 13 切换到上一层目录: 14 cd .. 15 绝对路径切换: 16 cd /我是egon的目录/ 我是江傻子的目录 17 在当前目录下切当前目录: 18 cd . View Code (3) ls: 查看 ① 支持功能: 查看当前目录下的文件 ls 指定目录下的文件(只能查看到自己家目录的范围) ls /目录1/目录2 查看帮助信息 ls /? ② 运行效果: 1 [egon@localhost ~] # ls 2 驱动器 Z 中的卷是 固态硬盘 3 卷的序列号是 AA26- 64F0 4 5 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon 的目录 6 7 2019-10-26 22:34

. 8 2019-10-26 22:34 .. 9 2019-10-19 20:03 1,081,540 123 .docx 10 2019-10-27 13:45 我是egon的目录 11 1 个文件 1,081,540 字节 12 3 个目录 56,465,575,936 可用字节 1) 查看当前目录下的文件 1 [egon@localhost ~] # ls /我是egon的目录 2 驱动器 Z 中的卷是 固态硬盘 3 卷的序列号是 AA26- 64F0 4 5 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 6 7 2019-10-27 13:45 . 8 2019-10-27 13:45 .. 9 2019-10-27 13:45 我是江傻子的目录 10 2019-10-27 12:34 0 江傻子 11 1 个文件 0 字节 12 3 个目录 56,465,575,936 可用字节 2) 指定目录下的文件(只能查看到自己家目录的范围) 1 [egon@localhost ~] # ls /? 2 显示目录中的文件和子目录列表。 3 4 DIR [drive:][path][filename] [/A[[:]attributes]] [/B] [/C] [/D] [/L] [/ N] 5 [/O[[:]sortorder]] [/P] [/Q] [/R] [/S] [/T[[:]timefield]] [/W] [/X] [/4 ] 6 7 [drive:][path][filename] 8 指定要列出的驱动器、目录和/ 或文件。 9 10 / A 显示具有指定属性的文件。 11 属性 D 目录 R 只读文件 12 H 隐藏文件 A 准备存档的文件 13 S 系统文件 I 无内容索引文件 14 L 重新分析点 O 脱机文件 15 – 表示“否”的前缀 16 / B 使用空格式(没有标题信息或摘要)。 17 /C 在文件大小中显示千位数分隔符。这是默认值。用 /- C 来 18 禁用分隔符显示。 19 / D 跟宽式相同,但文件是按栏分类列出的。 20 / L 用小写。 21 / N 新的长列表格式,其中文件名在最右边。 22 / O 用分类顺序列出文件。 23 排列顺序 N 按名称(字母顺序) S 按大小(从小到大) 24 E 按扩展名(字母顺序) D 按日期/ 时间(从先到后) 25 G 组目录优先 – 反转顺序的前缀 26 / P 在每个信息屏幕后暂停。 27 / Q 显示文件所有者。 28 / R 显示文件的备用数据流。 29 / S 显示指定目录和所有子目录中的文件。 30 / T 控制显示或用来分类的时间字符域 31 时间段 C 创建时间 32 A 上次访问时间 33 W 上次写入的时间 34 / W 用宽列表格式。 35 /X 显示为非 8dot3 文件名产生的短名称。格式是 / N 的格式, 36 短名称插在长名称前面。如果没有短名称,在其位置则 37 显示空白。 38 /4 以四位数字显示年份 39 40 可以在 DIRCMD 环境变量中预先设定开关。通过添加前缀 – (破折号) 41 来替代预先设定的开关。例如,/- W。 42 43 [egon@localhost ~] # 3) 查看帮助信息 (4) cd: 切换目录 ① 支持功能: 相对路径切换 cd /目录1 cd /目录2 切换到上一层目录 cd .. 绝对路径切换 cd /目录1/目录2 在当前目录下切当前目录 cd . ② 运行效果: 1 [egon@localhost ~] # cd /我是egon的目录 2 切换目录成功 3 [egon@localhost /home/egon/我是egon的目录] # ls 4 驱动器 Z 中的卷是 固态硬盘 5 卷的序列号是 AA26- 64F0 6 7 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 8 9 2019-10-27 13:45 . 10 2019-10-27 13:45 .. 11 2019-10-27 13:45 我是江傻子的目录 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 . 25 2019-10-27 13:45 .. 26 2019-10-27 13:45 0 我是江大傻.txt 27 1 个文件 0 字节 28 2 个目录 56,465,563,648 可用字节 29 30 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录] # 1) 相对路径切换 1 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录] # cd .. 2 切换目录成功 3 [egon@localhost /home/egon/我是egon的目录] # ls 4 驱动器 Z 中的卷是 固态硬盘 5 卷的序列号是 AA26- 64F0 6 7 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 8 9 2019-10-27 13:45 . 10 2019-10-27 13:45 .. 11 2019-10-27 13:45 我是江傻子的目录 12 2019-10-27 12:34 0 江傻子 13 1 个文件 0 字节 14 3 个目录 56,465,559,552 可用字节 15 16 [egon@localhost /home/egon/我是egon的目录] # 2) 切换到上一层目录 1 [egon@localhost ~] # cd /我是egon的目录/我是江傻子的目录 2 切换目录成功 3 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录] # ls 4 驱动器 Z 中的卷是 固态硬盘 5 卷的序列号是 AA26- 64F0 6 7 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录\我是江傻子的目录 的目录 8 9 2019-10-27 13:45 . 10 2019-10-27 13:45 .. 11 2019-10-27 13:45 0 我是江大傻.txt 12 1 个文件 0 字节 13 2 个目录 56,465,559,552 可用字节 14 15 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录] # 3) 绝对路径切换 1 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录] # cd . 2 切换目录成功 3 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录] # ls 4 驱动器 Z 中的卷是 固态硬盘 5 卷的序列号是 AA26- 64F0 6 7 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录\我是江傻子的目录 的目录 8 9 2019-10-27 13:45 . 10 2019-10-27 13:45 .. 11 2019-10-27 13:45 0 我是江大傻.txt 12 1 个文件 0 字节 13 2 个目录 56,465,559,552 可用字节 4) 在当前目录下切当前目录 (5) mkdir: 创建目录(支持递归创建目录) ① 支持功能: 相对路径创建: mkdir /目录 生成多层递归目录: mkdir /目录1/目录2 ② 运行效果: 1 [egon@localhost ~] # mkdir /a 2 创建目录成功! 1) 相对路径创建: 1 [egon@localhost ~] # mkdir /a/b 2 创建目录成功! 2) 绝对路径创建: (6) rmdir: 删除空目录 ① 支持功能: 删除空目录: rmdir /目录1/空目录2 ② 运行效果: 1 [egon@localhost ~] # rmdir /a/b 2 删除目录成功! 1) 删除空目录 (7) remove: 删除文件 ① 支持功能: 删除文件 remove /目录1/文件 ② 运行效果: 1 [egon@localhost ~] # ls /我是egon的目录/江傻子 2 驱动器 Z 中的卷是 固态硬盘 3 卷的序列号是 AA26- 64F0 4 5 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 6 7 2019-10-27 12:34 0 江傻子 8 1 个文件 0 字节 9 0 个目录 56,465,010,688 可用字节 10 11 [egon@localhost ~] # remove /我是egon的目录/江傻子 12 删除文件成功! 13 14 [egon@localhost ~] # ls /我是egon的目录/江傻子 15 找不到文件 16 驱动器 Z 中的卷是 固态硬盘 17 卷的序列号是 AA26- 64F0 18 19 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 1) 删除文件 (8) upload: 上传文件到服务端 ① 支持功能: 上传到服务端当前路径: upload 文件 通过cd切换目录上传文件到该目录下 cd /目录1/目录2 upload 文件 ② 运行效果: 1 [egon@localhost ~] # upload 服务器管理综合报告.docx 2 你可以上传文件, 在您上传之前, 您的目前空间:68 .97MB! 3 4 upload running… 5 [ # #################################################] 100.00% 6 upload succeed! 7 上传文件成功, 您上传完后的剩余空间:66 .07MB! 8 9 [egon@localhost ~] # ls 10 驱动器 Z 中的卷是 固态硬盘 11 卷的序列号是 AA26- 64F0 12 13 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon 的目录 14 15 2019-10-31 16:37 . 16 2019-10-31 16:37 .. 17 2019-10-31 16:37 .upload 18 2019-10-31 16:09 32,535,704 03_函数调用的三种形式.mp4 19 2019-10-28 09:53 我是egon的目录 20 2019-10-31 16:37 3,039,102 服务器管理综合报告.docx 21 2 个文件 35,574,806 字节 22 4 个目录 56,393,715,712 可用字节 1) 上传到服务端当前路径: 1 [egon@localhost ~] # cd /我是egon的目录 2 切换目录成功 3 4 [egon@localhost /我是的目录] # upload 服务器管理综合报告.docx 5 你可以上传文件, 在您上传之前, 您的目前空间:66 .07MB! 6 7 upload running… 8 [ # #################################################] 100.00% 9 upload succeed! 10 上传文件成功, 您上传完后的剩余空间:63 .17MB! 11 12 13 [egon@localhost /我是的目录] # ls 14 驱动器 Z 中的卷是 固态硬盘 15 卷的序列号是 AA26- 64F0 16 17 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 18 19 2019-10-31 16:47 . 20 2019-10-31 16:47 .. 21 2019-10-27 13:45 我是江傻子的目录 22 2019-10-31 16:47 3,039,102 服务器管理综合报告.docx 23 1 个文件 3,039,102 字节 24 3 个目录 56,390,676,480 可用字节 通过cd切换目录上传文件到该目录下: (9) resume_upload: 续传未上传完成的文件到服务端 ① 支持功能: 继续上传文件到服务端当前路径: resume_upload 文件名 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传: cd /目录1/目录2 resume_upload 文件名 ② 运行效果: 1 ——您的files文件夹下所含有的文件—— 2 1 : .download 3 2 : 03_函数调用的三种形式.mp4 4 3 : 服务器管理综合报告.docx 5 6 [egon@localhost ~] # upload 03_函数调用的三种形式.mp4 7 你可以上传文件, 在您上传之前, 您的目前空间:97 .10MB! 8 9 upload running… 10 [ # ########### ] 25.43% 先断开传输: 1 username>> :egon 2 password>>:123 3 用户名密码正确, 认证成功! 4 您的还有为上传完的文件, 是否继续上传! 5 6 数量: 1 文件路径: / 03_函数调用的三种形式.mp4 文件名: 03_函数调用的三种形式.mp4 7 文件原大小: 32535704字节 未完成的文件大小: 8273050字节 上传的百分比: 25.43% 8 9 10 ——您的files文件夹下所含有的文件—— 11 1 : .download 12 2 : 03_函数调用的三种形式.mp4 13 3 : 服务器管理综合报告.docx 14 15 16 [egon@localhost ~] # resume_upload 03_函数调用的三种形式.mp4 17 您正在继续上传文件, 在您继传之前, 您的目前空间:89 .21MB! 18 8273050 19 20 upload running… 21 [ # #################################################] 100.00% 22 upload succeed! 23 上传文件成功, 您上传完后的剩余空间:66.07MB! 1) 继续上传文件到服务端当前路径: 1 username>> :egon 2 password>>:123 3 用户名密码正确, 认证成功! 4 您的还有为上传完的文件, 是否继续上传! 5 6 数量: 1 文件路径: 的目录/ 03_函数调用的三种形式.mp4 文件名: 03_函数调用的三种形式.mp4 7 文件原大小: 32535704字节 未完成的文件大小: 12534221字节 上传的百分比: 38.52% 8 9 10 ——您的files文件夹下所含有的文件—— 11 1 : .download 12 2 : 03_函数调用的三种形式.mp4 13 3 : 服务器管理综合报告.docx 14 15 [egon@localhost ~] # cd /我是egon的目录 16 切换目录成功 17 18 ——您的files文件夹下所含有的文件—— 19 1 : .download 20 2 : 03_函数调用的三种形式.mp4 21 3 : 服务器管理综合报告.docx 22 23 [egon@localhost /我是的目录] # resume_upload 03_函数调用的三种形式.mp4 24 您正在继续上传文件, 在您继传之前, 您的目前空间:66 .07MB! 25 26 upload running… 27 [ # #################################################] 100.00% 28 upload succeed! 29 上传文件成功, 您上传完后的剩余空间:47.00MB! 2) 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传: (10) download: 下载文件 ① 支持功能: 从服务端当前目录下下载文件 download 文件 从服务端绝对路径下下载文件 download /目录1/文件 ② 运行效果: 1 [egon@localhost ~] # download 服务器管理综合报告.docx 2 3 download run… 4 [ # #################################################] 100.00% 5 download succeed! 1) 从服务端当前目录下下载文件 1 [egon@localhost ~] # download /我是egon的目录/03_函数调用的三种形式.mp4 2 3 download run… 4 [ # #################################################] 100.00% 5 download succeed! 2) 从服务端绝对路径下下载文件 (11) 在download基础之上: 继续从服务端续传下载文件 ① 支持功能: 用户登陆的时候显示为下载完的文件 用户根据序号选择要继续续传的文件 用户可以多次循环选择 支持断点以后据续断点续传 ② 运行效果: 1 username>> :egon 2 password>>:123 3 用户名密码正确, 认证成功! 4 服务端检测您没有未上传完成的文件! 5 检测到您本地还有未上传完成的文件 6 ——————————————————————–未完成续传的数量: 2个——————————————————————— 7 序号: 1 8 9 未完成的文件路径: Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\files\03_函数调用的三种形式.mp4.download 文件名: 03_函数调用的三种形式.mp4 10 文件原大小: 32535704字节 已完成的文件大小: 3511466字节 上传的百分比: 10.79% 11 12 序号: 2 13 14 未完成的文件路径: Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\files\服务器管理综合报告.docx.download 文件名: 服务器管理综合报告.docx 15 文件原大小: 3039102字节 已完成的文件大小: 712297字节 上传的百分比: 23.44% 16 17 [退出: q/Q]请根据序号选择您是否继续下载没有完成的文件>>:1 18 开始续传…… 19 20 download run… 21 [ # #################################################] 100.00% 22 download succeed! 23 24 续传完毕! 25 ——————————————————————–未完成续传的数量: 1个——————————————————————— 26 序号: 1 27 28 未完成的文件路径: Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\client\files\服务器管理综合报告.docx.download 文件名: 服务器管理综合报告.docx 29 文件原大小: 3039102字节 已完成的文件大小: 712297字节 上传的百分比: 23.44% 30 31 [退出: q/Q]请根据序号选择您是否继续下载没有完成的文件>>:1 32 开始续传…… 33 34 download run… 35 [ # #################################################] 100.00% 36 download succeed! 37 38 续传完毕! 39 40 ——您的files文件夹下所含有的文件—— 41 1 : .download 42 2 : 03_函数调用的三种形式.mp4 43 3: 服务器管理综合报告.docx View Code (12) 为用户磁盘配额 1 [egon@localhost /我是的目录/我是江傻子的目录/目录1] # upload 03_函数调用的三种形式.mp4 2 你可以上传文件, 在您上传之前, 您的目前空间:35 .04MB! 3 4 upload running… 5 [ # #################################################] 100.00% 6 upload succeed! 7 上传文件成功, 您上传完后的剩余空间:4 .02MB! 8 9 10 [egon@localhost /我是的目录/我是江傻子的目录/目录2] # upload 03_函数调用的三种形式.mp4 11 上传文件失败, 您的空间不足, 您的剩余空间:4.02MB! View Code (13) 使用队列queue模块,实现线程, 允许用户配置最大的并发数5个 (14) 记录了日志功能 ① 终端打印 ② 保存文件之中 7. 不足之处 没有实现多文件, 以及多文件夹打包上传 client用户暂时只能用files文件夹下的路径进行上传下载, 不能动态指定 8.代码展示 ① client 1) conf 1 import os 2 3 BASE_DIR = os.path.normpath(os.path.join( __file__ , ‘ .. ‘ , ‘ .. ‘ )) 4 5 FILES_PATH = os.path.join(BASE_DIR, ‘ files ‘ ) 6 UNFINISHED_DOWNLOAD_FILES_PATH = os.path.join(FILES_PATH, ‘ .download ‘ , ‘ unfinished.shv ‘ ) 7 8 HOST = ‘ 127.0.0.1 ‘ 9 PORT = 8080 10 11 12 help_dic = { 13 ‘ ls –help ‘ : “”” 14 查看当前目录下的文件: 15 ls 16 指定目录下的文件(只能查看到自己家目录的范围): 17 ls /目录1/目录2 18 查看ls的详细帮助: 19 ls /? 20 “”” , 21 ‘ cd –help ‘ : “”” 22 相对路径切换: 23 cd /目录1 24 cd /目录2 25 绝对路径切换: 26 cd /目录1/目录2 27 切换到上一层目录: 28 cd .. 29 在当前目录下切当前目录: 30 cd . 31 “”” , 32 ‘ mkdir –help ‘ : “”” 33 相对路径创建: 34 mkdir /目录 35 生成多层递归目录: 36 mkdir /目录1/目录2 37 “”” , 38 ‘ rmdir –help ‘ : “”” 39 删除空目录: 40 rmdir /目录1/空目录2 41 “”” , 42 ‘ remove –help ‘ : “”” 43 删除文件: 44 remove /目录1/文件 45 “”” , 46 ‘ upload –help ‘ : “”” 47 上传到服务端当前路径: 48 upload 文件名 49 通过cd切换目录上传文件到该目录下: 50 cd /目录1/目录2 51 upload 文件 52 “”” , 53 ‘ resume_upload –help ‘ : “”” 54 继续上传文件到服务端当前路径: 55 resume_upload 文件名 56 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传: 57 cd /目录1/目录2 58 resume_upload 文件名 59 “”” , 60 None: “”” 61 查看相对应的帮助信息: 62 1. ls + –help 63 2. cd –help 64 3. mkdir –help 65 4. rmdir –help 66 5. remove –help 67 6. upload –help 68 7. resume_upload –help 69 “”” , 70 } settings.py 2) core 1 import hashlib 2 import json 3 import os 4 import shelve 5 import socket 6 import struct 7 8 from conf import settings 9 10 11 class FTPClient: 12 “”” FTP客户端. “”” 13 address_family = socket.AF_INET 14 socket_type = socket.SOCK_STREAM 15 max_packet_size = 8192 16 encoding = ‘ utf-8 ‘ 17 windows_encoding = ‘ gbk ‘ 18 19 struct_fmt = ‘ i ‘ 20 fixed_packet_size = 4 21 22 def __init__ (self, server_address, connect= True): 23 self.server_address = server_address 24 self.socket = socket.socket(self.address_family, self.socket_type) 25 26 self.breakpoint_resume = shelve.open(settings.UNFINISHED_DOWNLOAD_FILES_PATH) 27 28 self.username = None 29 self.current_dir = ‘ ~ ‘ 30 if connect: 31 try : 32 self.client_connect() 33 except Exception: 34 self.client_close() 35 raise 36 37 def client_connect(self): 38 “”” 客户端连接服务端ip和port. “”” 39 self.socket.connect(self.server_address) 40 41 def client_close(self): 42 “”” 关闭连接通道. “”” 43 self.socket.close() 44 45 def interactive(self): 46 “”” 与服务端进行所有的交互. “”” 47 if self.auth(): 48 self.unfinished_file_check() 49 while True: 50 self.show_str() 51 msg = input( ‘ [%s@localhost %s]# ‘ % (self.username, self.current_dir)).strip() 52 if not msg: 53 continue 54 if not self.help_msg(msg): 55 continue 56 # 核验命令参数 57 cmd, path = self.verify_args(msg) 58 if hasattr(self, ‘ _%s ‘ % cmd): 59 func = getattr(self, ‘ _%s ‘ % cmd) 60 func(path) 61 else : 62 self.help_msg() 63 64 @staticmethod 65 def verify_args(msg): 66 “”” 67 效验参数. 68 :param msg: ls 或 ls /路径 或 ls /路径1/路径2/ 69 :return: (ls, []) 或 (ls, [‘路径’]) 或 (ls, [‘路径1’, ‘路径2’]) 70 “”” 71 cmd_args = msg.split() 72 cmd, path = cmd_args[0], cmd_args[1 :] 73 if path: 74 path = ” .join(cmd_args[1:]).strip( ‘ // ‘ ).split( ‘ / ‘ ) 75 # print(‘cmd, path:’, cmd, path) 76 return cmd, path 77 78 def unfinished_file_check(self): 79 if not list(self.breakpoint_resume.keys()): 80 return 81 82 print ( ‘ 检测到您本地还有未上传完成的文件 ‘ ) 83 unfinished_path_list = [] 84 msg_list = [] 85 for unfinished_file_path in self.breakpoint_resume.keys(): 86 file_name = self.breakpoint_resume[unfinished_file_path][ ‘ file_name ‘ ] 87 file_size = self.breakpoint_resume[unfinished_file_path][ ‘ file_size ‘ ] 88 unfinished_file_size = os.path.getsize(unfinished_file_path) 89 percent = unfinished_file_size / file_size * 100 90 path = self.breakpoint_resume[unfinished_file_path][ ‘ path ‘ ] 91 dic = { ‘ unfinished_file_size ‘ : unfinished_file_size, ‘ path ‘ : path} 92 unfinished_path_list.append(dic) 93 msg = “”” 94 未完成的文件路径: %s 文件名: %s 95 文件原大小: %s字节 已完成的文件大小: %s字节 上传的百分比: %.2f%% 96 “”” % (unfinished_file_path, file_name, file_size, unfinished_file_size, percent) 97 msg_list.append(msg) 98 99 while msg_list: 100 print ( ” 未完成续传的数量: %s个 ” .center(150, ‘ – ‘ ) % len(msg_list)) 101 for msg in msg_list: 102 print ( ‘ 序号: %s ‘ % (int(msg_list.index(msg) + 1 ))) 103 print (msg) 104 105 choice = input( ‘ [退出: q/Q]请根据序号选择您是否继续下载没有完成的文件>>: ‘ ).strip() 106 if choice.lower() == ‘ q ‘ : 107 break 108 if not choice.isdigit(): 109 continue 110 choice = int(choice) 111 if 0 < choice <= len(unfinished_path_list): # len(unfinished_path_list)=3 112 dic = unfinished_path_list[choice – 1 ] 113 path, unfinished_file_size = dic[ ‘ path ‘ ], dic[ ‘ unfinished_file_size ‘ ] 114 115 print ( ‘ 开始续传…… ‘ ) 116 self. __resume_download (path, unfinished_file_size) 117 print ( ‘ \n续传完毕! ‘ ) 118 119 unfinished_path_list.pop(choice-1 ) 120 msg_list.pop(choice-1 ) 121 else : 122 print ( ‘ 您的选择超出了范围! ‘ ) 123 124 def auth(self): 125 “”” 126 登陆. 127 100: ‘用户名密码正确, 认证成功!’, 128 199: ‘用户名密码不正确, 认证失败!’, 129 850: ‘您的还有为上传完的文件, 是否继续上传!’, 130 851: ‘检测您不存在未上传完成的文件!’, 131 “”” 132 count = 0 133 while count < 3 : 134 username = input( ‘ username>>: ‘ ).strip() 135 password = input( ‘ password>>: ‘ ).strip() 136 if not all([username, password]): 137 print ( ‘ 用户名密码不能为空. ‘ ) 138 count += 1 139 continue 140 # 发报头 141 self.send_header(action_type= ‘ auth ‘ , username=username, password= password) 142 # 收报头 143 response_dic = self.receive_header() 144 status_code, status_msg = response_dic.get( ‘ status_code ‘ ), response_dic.get( ‘ status_msg ‘ ) 145 # 100: ‘用户名密码正确, 认证成功!’, 146 if status_code == 100: # 100确认成功 147 print (status_msg) 148 self.username = username 149 150 # 850: ‘您的还有为上传完的文件, 是否继续上传!’, 151 # 851: ‘检测您不存在未上传完成的文件!’, 152 response_dic = self.receive_header() 153 status_code, status_msg, msg_list, msg_dic = response_dic.get( ‘ status_code ‘ ), response_dic.get( 154 ‘ status_msg ‘ ), response_dic.get( ‘ msg_list ‘ ), response_dic.get( ‘ msg_dic ‘ ) 155 if msg_list: 156 print (status_msg) 157 for unfinished_msg in msg_list: 158 print (unfinished_msg) 159 else : 160 print (status_msg) 161 162 return True 163 else : 164 # 199: ‘用户名密码不正确, 认证失败!’, 165 print (status_msg) 166 count += 1 167 else : 168 print ( ‘ 输入次数过多,强制退出! ‘ ) 169 return False 170 171 def _ls(self, path): 172 “”” 173 显示目录的文件列表. 174 :param path: [] 或 [‘目录1’, ‘目录2’] 175 :return: None 176 “”” 177 # 发送报头 178 self.send_header(action_type= ‘ ls ‘ , path= path) 179 # 接收报头 180 response_dic = self.receive_header() 181 status_code, status_msg, cmd_size = response_dic.get( ‘ status_code ‘ ), response_dic.get( 182 ‘ status_msg ‘ ), response_dic.get( ‘ cmd_size ‘ ) 183 if status_code == 301 and cmd_size: 184 # print(‘status_msg:’, status_msg) 185 # print(‘cmd_size:’, cmd_size) 186 # 收消息 187 windows_cmd = self.socket.recv(cmd_size).decode(self.windows_encoding) 188 print (windows_cmd) 189 else : 190 print (status_msg) 191 192 def _cd(self, path): 193 “”” 194 切换目录. 195 :param path: [‘..’] 或 [‘路径1’, ‘目录2’] 196 :return: None 197 “”” 198 # 发送报头 199 self.send_header(action_type= ‘ cd ‘ , path= path) 200 # 接收报头 201 response_dic = self.receive_header() 202 status_code, status_msg, current_dir = response_dic.get( ‘ status_code ‘ ), response_dic.get( 203 ‘ status_msg ‘ ), response_dic.get( ‘ current_dir ‘ ) 204 if status_code == 400 : 205 self.current_dir = current_dir 206 print (status_msg) 207 else : 208 print (status_msg) 209 210 def _mkdir(self, path): 211 “”” 212 新建目录. 213 :param path: [‘目录1′] 214 或 [目录2’, ‘目录3’] 215 :return: None 216 “”” 217 # print(path) 218 # 发送报头 219 self.send_header(action_type= ‘ mkdir ‘ , path= path) 220 # 接收报头 221 response_dic = self.receive_header() 222 status_code, status_msg = response_dic.get( ‘ status_code ‘ ), response_dic.get( 223 ‘ status_msg ‘ ) 224 if status_code == 500 : 225 print (status_msg) 226 else : 227 print (status_msg) 228 229 def _rmdir(self, path): 230 “”” 231 删除空目录. 232 :param path: [”, ‘12312都1的发’] 233 :return: None 234 “”” 235 # print(path) 236 # 发送报头 237 self.send_header(action_type= ‘ rmdir ‘ , path= path) 238 # 接收报头 239 response_dic = self.receive_header() 240 status_code, status_msg = response_dic.get( ‘ status_code ‘ ), response_dic.get( 241 ‘ status_msg ‘ ) 242 if status_code == 600 : 243 print (status_msg) 244 else : 245 print (status_msg) 246 247 def _remove(self, path): 248 “”” 249 删除文件. 250 :param path: [‘目录1’, ‘文件1’] 251 :return: 252 “”” 253 # print(path) 254 # 发送报头 255 self.send_header(action_type= ‘ remove ‘ , path= path) 256 # 接收报头 257 response_dic = self.receive_header() 258 status_code, status_msg = response_dic.get( ‘ status_code ‘ ), response_dic.get( 259 ‘ status_msg ‘ ) 260 if status_code == 700 : 261 print (status_msg) 262 else : 263 print (status_msg) 264 265 def parser_path(self, action_type, path, ** kwargs): 266 “”” 267 解析路径参数, 判断路径是文件名, 还是路径下的文件名. 268 :param action_type: 用户上传的功能类型 269 :param path: 用户路径例子: [‘目录1’, ‘文件1’] 或 [‘文件1’] 270 :param kwargs: 271 :return: path列表长度合理的时候返回True, 不合理返回False 272 “”” 273 if len(path) > 1 : 274 self.send_header(action_type=action_type, **kwargs, file_name=path[-1 ], 275 path=path[:-1 ]) 276 elif len(path) == 1 : 277 self.send_header(action_type=action_type, **kwargs, file_name=path[-1 ], 278 path= None) 279 else : 280 print ( ‘ 必须指定路径, 或者文件名! ‘ ) 281 return False 282 return True 283 284 def _resume_upload(self, path): 285 “”” 286 upload的断点续传功能. 287 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 288 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 289 “”” 290 self._upload(path, resume_upload= True) 291 292 def _upload(self, path, resume_upload= False): 293 “”” 294 上传文件到服务端. 295 正常上传: 296 800: ‘你可以上传文件, 在您上传之前, 您的目前空间:%s!’, 297 801: ‘上传文件成功, 您上传完后的剩余空间:%s!’, 298 852: ‘您不能进行续传, 因为该文件是完整文件!’, 299 894: ‘您不需要再本路径下上传文件, 该文件在您的当前路径下已经存在!’, 300 895: ‘上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!’, 301 896: ‘上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!’, 302 897: ‘上传文件失败, 您的空间不足, 您的剩余空间:%s!’, 303 898: ‘上传文件失败, 上传命令不规范!’, 304 899: ‘上传文件必须要有文件的md5值以及文件名!’, 305 续传: 306 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 307 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 308 :param path: [‘目录1’, ‘文件1’] 或 [‘文件1’] 309 :return: None 310 “”” 311 # 判断用户文件路径是否是FILES_PATH路径下的文件 312 file_path = os.path.normpath(os.path.join(settings.FILES_PATH, * path)) 313 if not os.path.isfile(file_path): 314 print ( ‘ 您要上传的文件不存在! ‘ ) 315 return 316 317 # 解析用户路径, 并提交upload的相关功能 318 file_size = os.path.getsize(file_path) 319 file_md5 = self.md5(file_path) 320 321 if resume_upload: # 断点续传时执行 322 action_type = ‘ resume_upload ‘ 323 else : # 正常长传时执行 324 action_type = ‘ upload ‘ 325 326 if not self.parser_path(action_type=action_type, file_md5=file_md5, file_size=file_size, path= path): 327 return 328 329 # 接收服务端相应字典 330 # 正常: 800, 894, 897, 898, 899 331 # 800: ‘你可以上传文件, 在您上传之前, 您的目前空间:%s!’, 332 # 894: ‘您不需要再本路径下上传文件, 该文件在您的当前路径下已经存在!’, 333 # 898: ‘上传文件失败, 上传命令不规范!’, 334 # 897: ‘上传文件失败, 您的空间不足, 您的剩余空间:%s!’, 335 # 899: ‘上传文件必须要有文件的md5值以及文件名!’, 336 # 续传: 860, 869 337 # 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 338 # 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 339 response_dic = self.receive_header() 340 status_code, status_msg, residual_space_size, already_upload_size = response_dic.get( 341 ‘ status_code ‘ ), response_dic.get( 342 ‘ status_msg ‘ ), response_dic.get( ‘ residual_space_size ‘ ), response_dic.get( ‘ already_upload_size ‘ ) 343 344 # 判断状态码 345 # 800: ‘你可以上传文件, 在您上传之前, 您的目前空间:%s!’, 346 # 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 347 if status_code == 800 or status_code == 860: # 800正常发送文件确认 860续传文件确认 348 print (status_msg % self.conversion_quota(residual_space_size)) 349 350 initial_size = 0 351 if resume_upload: # 断点续传时执行: 目前文件总大小要减去上次没有上传完位置的大小 352 total_size = file_size – already_upload_size 353 else : # 正常上传时执行 354 total_size = file_size 355 with open(file_path, ‘ rb ‘ ) as f: 356 if resume_upload: # 断点续传时执行: 光标移动到上次没有上传完位置 357 f.seek(already_upload_size) 358 print ( ‘ \nupload running… ‘ ) 359 for line in f: 360 self.socket.sendall(line) 361 initial_size += len(line) 362 percent = initial_size / total_size 363 self.progress_bar(percent) 364 print ( ‘ \nupload succeed! ‘ ) 365 366 # 第二次接收消息, 确认文件上传完毕 367 # 801, 895, 896 368 # 801: ‘上传文件成功, 您上传完后的剩余空间:%s!’, 369 # 895: ‘上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!’, 370 # 896: ‘上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!’, 371 response_dic = self.receive_header() 372 status_code, status_msg, residual_space_size = response_dic.get( ‘ status_code ‘ ), response_dic.get( 373 ‘ status_msg ‘ ), response_dic.get( ‘ residual_space_size ‘ ) 374 if residual_space_size: # 801, 896 375 print (status_msg % self.conversion_quota(residual_space_size)) 376 else : # 895 377 print (status_msg) 378 else : 379 # 正常: 894, 897, 898, 899 380 # 894: ‘您不需要再本路径下上传文件, 该文件在您的当前路径下已经存在!’, 381 # 897: ‘上传文件失败, 您的空间不足, 您的剩余空间:%s!’, 382 # 898: ‘上传文件失败, 上传命令不规范!’, 383 # 899: ‘上传文件必须要有文件的md5值以及文件名!’, 384 # 续传: 385 # 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 386 if residual_space_size: # 897 387 print (status_msg % self.conversion_quota(residual_space_size)) 388 else : # 869, 894, 898, 899 389 print (status_msg) 390 391 def __resume_download (self, path, unfinished_file_size): 392 self._download(path, unfinished_file_size, resume_download= True) 393 394 def _download(self, path, unfinished_file_size=None, resume_download= False): 395 “”” 396 397 900: ‘准备开始下载文件!’, 398 999: ‘下载文件失败, 您要下载的文件路径不规范!’, 399 :param path: 400 :param resume_download: 401 :return: 402 “”” 403 if resume_download: 404 action_type = ‘ resume_download ‘ 405 else : 406 action_type = ‘ download ‘ 407 self.send_header(action_type=action_type, path=path, unfinished_file_size= unfinished_file_size) 408 409 # 接收服务端消息 410 # self.send_header(status_code=900, file_name=file_name, file_size=file_size, file_md5=file_md5) 411 response_dic = self.receive_header() 412 status_code, status_msg, file_name, file_size, file_md5 = response_dic.get( ‘ status_code ‘ ), response_dic.get( 413 ‘ status_msg ‘ ), response_dic.get( ‘ file_name ‘ ), response_dic.get( ‘ file_size ‘ ), response_dic.get( ‘ file_md5 ‘ ) 414 415 # 判断状态码 416 # 900: ‘准备开始下载文件!’, 417 # 950: ‘准备开始续传文件!’, 418 # 998: ‘下载文件失败, 您要下载的文件路径不存在!’, 419 # 999: ‘下载文件失败, 您要下载的文件路径不规范!’, 420 if status_code == 900 or status_code == 950 : 421 422 file_path = os.path.join(settings.FILES_PATH, file_name) 423 if resume_download and file_path in self.breakpoint_resume.keys(): 424 unfinished_file_path = self.breakpoint_resume[file_path][ ‘ unfinished_file_path ‘ ] 425 else : 426 # 判断本次路径下是否有文件, 有文件则提示 427 # file_path = os.path.join(settings.FILES_PATH, file_name) 428 if os.path.isfile(file_path): 429 print ( ‘ 本次路径下文件已经存在, 不需要继续下载! ‘ ) 430 return 431 # 为没有下载完毕的文件名添加后缀 432 unfinished_file_path = ‘ %s.%s ‘ % (file_path, ‘ download ‘ ) 433 434 # 为出现下载终端添加断点记录 435 self.breakpoint_resume[unfinished_file_path] = { ‘ file_name ‘ : file_name, ‘ file_size ‘ : file_size, 436 ‘ path ‘ : path} 437 438 # 开始进行下载 439 receive_size = 0 440 if resume_download: 441 total_size = file_size – os.path.getsize(unfinished_file_path) 442 mode = ‘ a ‘ 443 else : 444 total_size = file_size 445 mode = ‘ w ‘ 446 with open(unfinished_file_path, ‘ %sb ‘ % mode) as f: 447 print ( ‘ \ndownload run… ‘ ) 448 while receive_size < total_size: 449 data_bytes = self.socket.recv(self.max_packet_size) 450 f.write(data_bytes) 451 receive_size += len(data_bytes) 452 percent = receive_size / total_size 453 self.progress_bar(percent) 454 print ( ‘ \ndownload succeed! ‘ ) 455 f.flush() 456 457 # 正常下载成功把后缀去除, 文件改名, 删除断点记录 458 del self.breakpoint_resume[unfinished_file_path] 459 os.rename(unfinished_file_path, file_path) 460 461 # 效验md5值询问用户是否保存 462 server_file_md5 = file_md5 463 current_file_md5 = self.md5(file_path) 464 if server_file_md5 != current_file_md5: 465 print ( ‘ 您的文件不完成, 可能不能打开, 请重新下载! ‘ ) 466 else : 467 # 998: ‘下载文件失败, 您要下载的文件路径不存在!’, 468 # 999: ‘下载文件失败, 您要下载的文件路径不规范!’, 469 print (status_msg) 470 471 @staticmethod 472 def conversion_quota(residual_space_size): 473 “”” 474 换算服务端发送过来的字节为MB, 人性化的展现用户的空间剩余. 475 :param residual_space_size: 剩余空间字节数 476 :return: MB为单位的字节 477 “”” 478 residual_space_mb = residual_space_size / (1024 ** 2 ) 479 return ‘ %.2fMB ‘ % residual_space_mb 480 481 def receive_header(self): 482 “”” 483 接收服务端发送过来的报头字典. 484 :return: {‘status_code’: 100, ‘status_msg’: ‘认证成功’, ‘cmd_size’: 199} 485 “”” 486 header_bytes = self.socket.recv(self.fixed_packet_size) 487 header_dic_json_length = struct.unpack(self.struct_fmt, header_bytes)[0] 488 # 接收报头 489 header_dic_json = self.socket.recv(header_dic_json_length).decode(self.encoding) 490 header_dic = json.loads(header_dic_json) 491 return header_dic 492 493 def send_header(self, *, action_type, ** kwargs): 494 “”” 495 发送报头字典给客户端. 496 :param action_type: action_type=’auth’ 497 :param kwargs: {‘username’: ‘egon’, ‘password’: ‘123’} 498 :return: None 499 “”” 500 request_dic = kwargs 501 request_dic[ ‘ action_type ‘ ] = action_type 502 request_dic.update(request_dic) 503 504 request_dic_json_bytes = json.dumps(request_dic).encode(self.encoding) 505 request_dic_json_bytes_length = len(request_dic_json_bytes) 506 header_bytes = struct.pack(self.struct_fmt, request_dic_json_bytes_length) 507 508 # 发送报头 509 self.socket.sendall(header_bytes) 510 # 发送json后bytes后的字典request_dic 511 self.socket.sendall(request_dic_json_bytes) 512 513 @staticmethod 514 def md5(file_path): 515 “”” 516 md5加密哈希文件. 517 :param file_path: files下的文件路径 518 :return: 文件hash值 519 “”” 520 md5_obj = hashlib.md5() 521 with open(file_path, ‘ rb ‘ ) as f: 522 for line in f: 523 md5_obj.update(line) 524 return md5_obj.hexdigest() 525 526 @staticmethod 527 def progress_bar(percent, width=50, symbol= ‘ # ‘ ): 528 “”” 进度条功能. “”” 529 if percent > 1 : 530 percent = 1 531 show_str = ( ‘ [%%-%ds] ‘ % width) % (int(width * percent) * symbol) 532 print ( ‘ \r%s %.2f%% ‘ % (show_str, percent * 100), end= ” ) 533 534 @staticmethod 535 def show_str(): 536 “”” 显示客户端flies中的文件列表. “”” 537 print ( ‘ \n——您的files文件夹下所含有的文件—— ‘ ) 538 for index, filename in enumerate(os.listdir(settings.FILES_PATH), 1 ): 539 print ( ‘ %s: %s ‘ % (index, filename)) 540 print () 541 542 @staticmethod 543 def help_msg(msgs= None): 544 “”” 帮助信息. “”” 545 if msgs in settings.help_dic: 546 print (settings.help_dic[msgs]) 547 else : 548 return True main.py 3) files 存放上传服务器的目录 1 # encoding: utf-8 2 3 import os 4 import sys 5 6 BASE_DIR = os.path.normpath(os.path.join( __file__ , ‘ .. ‘ )) 7 print (BASE_DIR) 8 sys.path.append(BASE_DIR) 9 10 if __name__ == ‘ __main__ ‘ : 11 from core import main 12 client = main.FTPClient(( ‘ 127.0.0.1 ‘ , 8080 )) 13 client.interactive() ftp_client.py ②server 1) conf 1 [egon] 2 password = 202cb962ac59075b964b07152d234b70 3 quota = 100 4 5 [alex] 6 password = 202cb962ac59075b964b07152d234b70 7 quota = 100 8 9 [ly] 10 password = 202cb962ac59075b964b07152d234b70 11 quota = 200 12 13 [jzd] 14 password = 202cb962ac59075b964b07152d234b70 15 quota = 300 16 17 [shx] 18 password = 202cb962ac59075b964b07152d234b70 19 quota = 300 20 21 22 [xxx] 23 password = 202cb962ac59075b964b07152d234b70 24 quota = 300 accounts.ini 1 import os 2 3 4 def base_dir(* args): 5 return os.path.normpath(os.path.join( __file__ , ‘ .. ‘ , ‘ .. ‘ , * args)) 6 7 8 # 用户家目录存放路径 9 USER_HOME_DIR = base_dir( ‘ home ‘ ) 10 11 # 用户账户信息文件路径 12 ACCOUNTS_FILE = base_dir( ‘ conf ‘ , ‘ accounts.ini ‘ ) 13 14 # 本机测试的ip和port 15 HOST = ‘ 127.0.0.1 ‘ 16 PORT = 8080 17 18 # 状态码: 负责提供交互成功及失败的提示信息反馈 19 STATUS_CODE = { 20 100: ‘ 用户名密码正确, 认证成功! ‘ , 21 199: ‘ 用户名密码不正确, 认证失败! ‘ , 22 200: ‘ 您的功能指定不能为空! ‘ , 23 201: ‘ 没有该功能, 请查看帮助信息! ‘ , 24 301: ‘ 本次返回结果包含命令大小. ‘ , 25 400: ‘ 切换目录成功 ‘ , 26 498: ‘ 切换目录失败, 切换命令不规范 ‘ , 27 499: ‘ 切换目录失败, 目标地址不存在! ‘ , 28 500: ‘ 创建目录成功! ‘ , 29 598: ‘ 创建目录命令输入不规范! ‘ , 30 599: ‘ 创建的目录已存在! ‘ , 31 600: ‘ 删除目录成功! ‘ , 32 699: ‘ 删除目录失败, 该目录不为空! ‘ , 33 698: ‘ 删除目录失败, 不存在该目录! ‘ , 34 697: ‘ 删除目录失败, 删除命令不规范! ‘ , 35 700: ‘ 删除文件成功! ‘ , 36 799: ‘ 删除文件失败, 不存在该文件! ‘ , 37 800: ‘ 你可以上传文件, 在您上传之前, 您的目前空间:%s! ‘ , 38 801: ‘ 上传文件成功, 您上传完后的剩余空间:%s! ‘ , 39 850: ‘ 服务端检测您还有为上传完的文件, 是否继续上传! ‘ , 40 851: ‘ 服务端检测您没有未上传完成的文件! ‘ , 41 852: ‘ 您不能进行续传, 因为该文件是完整文件! ‘ , 42 860: ‘ 您正在继续上传文件, 在您继传之前, 您的目前空间:%s! ‘ , 43 869: ‘ 您选择文件路径中没有要续传的文件, 请核对! ‘ , 44 894: ‘ 您不需要再对本路径下上传文件, 该文件在您的当前路径下已经存在! ‘ , 45 895: ‘ 上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传! ‘ , 46 896: ‘ 上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s! ‘ , 47 897: ‘ 上传文件失败, 您的空间不足, 您的剩余空间:%s! ‘ , 48 898: ‘ 上传文件失败, 上传命令不规范! ‘ , 49 899: ‘ 上传文件必须要有文件的md5值以及文件名! ‘ , 50 900: ‘ 准备开始下载文件! ‘ , 51 950: ‘ 准备开始续传文件! ‘ , 52 998: ‘ 下载文件失败, 您要下载的文件路径不存在! ‘ , 53 999: ‘ 下载文件失败, 您要下载的文件路径不规范! ‘ , 54 } 55 56 # log日志路径 57 ACCESS_LOG_PATH = base_dir( ‘ log ‘ , ‘ access.log ‘ ) 58 59 # 定义log日志输出格式 60 standard_format = ‘ %(asctime)s – %(threadName)s:%(thread)d – task_id:%(name)s – %(filename)s:%(lineno)d – ‘ \ 61 ‘ %(levelname)s – %(message)s ‘ # 其中name为getlogger指定的名字 62 63 simple_format = ‘ \n%(levelname)s – %(asctime)s – %(filename)s:%(lineno)d – %(message)s\n ‘ 64 65 66 # log配置字典 67 LOGGING_DIC = { 68 ‘ version ‘ : 1 , 69 ‘ disable_existing_loggers ‘ : False, 70 ‘ formatters ‘ : { 71 ‘ standard ‘ : { 72 ‘ format ‘ : standard_format 73 }, 74 ‘ simple ‘ : { 75 ‘ format ‘ : simple_format, 76 }, 77 }, 78 ‘ filters ‘ : {}, 79 ‘ handlers ‘ : { 80 # 打印到终端的日志 81 ‘ console ‘ : { 82 ‘ level ‘ : ‘ DEBUG ‘ , 83 ‘ class ‘ : ‘ logging.StreamHandler ‘ , # 打印到屏幕 84 ‘ formatter ‘ : ‘ simple ‘ 85 }, 86 # 打印到文件的日志,收集info及以上的日志 87 ‘ access ‘ : { 88 ‘ level ‘ : ‘ DEBUG ‘ , 89 ‘ class ‘ : ‘ logging.handlers.RotatingFileHandler ‘ , # 保存到文件 90 ‘ formatter ‘ : ‘ standard ‘ , 91 ‘ filename ‘ : ACCESS_LOG_PATH, # 日志文件 92 # ‘maxBytes’: 1024 * 1024 * 5, # 日志大小 5M 93 ‘ maxBytes ‘ : 1024 * 1024 * 5 , 94 ‘ backupCount ‘ : 10 , 95 ‘ encoding ‘ : ‘ utf-8 ‘ , # 日志文件的编码,再也不用担心中文log乱码了 96 }, 97 }, 98 ‘ loggers ‘ : { 99 # logging.getLogger(__name__)拿到的logger配置 100 ” : { 101 ‘ handlers ‘ : [ ‘ access ‘ , ‘ console ‘ ], # 这里把上面定义的两个handler都加上,即log数据既写入文件又打印到屏幕 102 ‘ level ‘ : ‘ DEBUG ‘ , 103 ‘ propagate ‘ : False, # 向上(更高level的logger)传递 104 }, 105 }, 106 } settings.py 2) core 1 import json 2 import os 3 import shelve 4 import struct 5 import subprocess 6 7 from conf import settings 8 from lib import common 9 10 11 class HandlerRequest: 12 “”” 处理用户请求. “”” 13 max_packet_size = 8192 14 encoding = ‘ utf-8 ‘ 15 16 struct_fmt = ‘ i ‘ 17 fixed_packet_size = 4 18 19 logger = common.load_my_logging_cfg() 20 21 def __init__ (self, request, address): 22 self.request = request 23 self.address = address 24 25 self.residual_space_size = None 26 27 self.breakpoint_resume = None 28 29 self.username = None 30 self.user_obj = None 31 self.user_current_dir = None 32 33 def client_close(self): 34 “”” 关闭客户端连接. “”” 35 self.request.close() 36 37 def handle_request(self): 38 “”” 处理客户端请求. “”” 39 count = 0 40 while count < 3: # 连接循环 41 try : 42 if self.auth(): 43 # 收消息 44 user_dic = self.receive_header() 45 action_type = user_dic.get( ‘ action_type ‘ ) 46 if action_type: 47 if hasattr(self, ‘ _%s ‘ % action_type): 48 func = getattr(self, ‘ _%s ‘ % action_type) 49 func(user_dic) 50 else : 51 self.send_header(status_code=201 ) 52 # 发消息 53 else : 54 self.send_header(status_code=200 ) 55 else : 56 count += 1 57 self.send_header(status_code=199 ) 58 except ConnectionResetError: 59 break 60 # 关闭客户端连接 61 self.logger.info( ‘ —-连接断开—- ip:%s port:%s ‘ % self.address) 62 self.client_close() 63 64 def unfinished_file_check(self): 65 self.logger.info( ‘ #执行unfinished_file_check命令# ip:%s port:%s ‘ % self.address) 66 67 if not list(self.breakpoint_resume.keys()): 68 self.send_header(status_code=851 ) 69 return 70 71 # self.breakpoint_resume[file_path] = 72 # {‘file_size’: _file_size, ‘unfinished_file_path’: unfinished_file_path, ‘file_name’: _file_name} 73 msg_list = [] 74 75 for index, abs_path in enumerate(self.breakpoint_resume.keys(), 1 ):\ 76 77 user_path = ‘ / ‘ .join(abs_path.split(self.username)[-1 ].split(os.sep)) 78 print ( ‘ abs_path: ‘ , user_path) 79 file_name = self.breakpoint_resume[abs_path][ ‘ file_name ‘ ] 80 src_file_size = self.breakpoint_resume[abs_path][ ‘ file_size ‘ ] 81 unfinished_file_size = os.path.getsize(self.breakpoint_resume[abs_path][ ‘ unfinished_file_path ‘ ]) 82 percent = unfinished_file_size / src_file_size * 100 83 84 msg = “”” 85 数量: %s 文件路径: %s 文件名: %s 86 文件原大小: %s字节 未完成的文件大小: %s字节 上传的百分比: %.2f%% 87 “”” % (index, user_path, file_name, src_file_size, unfinished_file_size, percent) 88 89 msg_list.append(msg) 90 # msg_dic[‘/03_函数调用的三种形式.mp4’] = 5772100 91 # msg_dic[user_path] = unfinished_file_size 92 # self.send_header(status_code=850, msg_list=msg_list, msg_dic=msg_dic) 93 self.send_header(status_code=850, msg_list= msg_list) 94 95 def auth(self): 96 “”” 用户登陆认证. “”” 97 if self.user_current_dir: 98 return True 99 100 # 涉及到交叉导入 101 from core import main 102 # 收消息 103 auth_dic = self.receive_header() 104 105 user_name = auth_dic.get( ‘ username ‘ ) 106 user_password = auth_dic.get( ‘ password ‘ ) 107 md5_password = common.md5( ‘ password ‘ , password= user_password) 108 109 # print(user_name, user_password, md5_password) 110 111 accounts = main.FTPServer.load_accounts() 112 if user_name in accounts.sections(): 113 if md5_password == accounts[user_name][ ‘ password ‘ ]: 114 self.send_header(status_code=100 ) 115 116 self.username = user_name 117 self.user_obj = accounts[user_name] 118 self.user_obj[ ‘ home ‘ ] = os.path.join(settings.USER_HOME_DIR, user_name) 119 self.user_current_dir = self.user_obj[ ‘ home ‘ ] 120 121 # print(‘self.user_obj:’, self.user_obj) 122 # print(“self.user_obj[‘home’]:”, self.user_obj[‘home’]) 123 124 self.residual_space_size = common.conversion_quota( 125 self.user_obj[ ‘ quota ‘ ]) – common.get_size(self.user_obj[ ‘ home ‘ ]) 126 127 breakpoint_resume_dir_path = os.path.join(self.user_obj[ ‘ home ‘ ], ‘ .upload ‘ ) 128 if not os.path.isdir(breakpoint_resume_dir_path): 129 os.mkdir(breakpoint_resume_dir_path) 130 self.breakpoint_resume = shelve.open(os.path.join(breakpoint_resume_dir_path, ‘ .upload.shv ‘ )) 131 self.unfinished_file_check() 132 133 self.logger.info( ‘ #认证成功# ip:%s port:%s ‘ % self.address) 134 return True 135 self.logger.info( ‘ #认证失败# ip:%s port:%s ‘ % self.address) 136 return False 137 138 def _ls(self, cmd_dic): 139 “”” 140 运行dir命令将结果发送到客户端. 141 :param cmd_dic: {‘path’: [], ‘action_type’: ‘ls’} 142 或 {‘path’: [‘目录1’, ‘目录2’, ‘目录xxx’], ‘action_type’: ‘ls’} 143 或 {‘path’: [‘?’], ‘action_type’: ‘ls’} 144 :return: None 145 “”” 146 # print(‘_ls:’, cmd_dic) 147 self.logger.info( ‘ #执行ls命令# ip:%s port:%s ‘ % self.address) 148 149 # 核验路径 150 dir_path = self.verify_path(cmd_dic) 151 if not dir_path: 152 dir_path = self.user_current_dir 153 154 if cmd_dic.get( ‘ path ‘ ) == [ ‘ ? ‘ ]: # 为用户提供ls /?命令 155 dir_path = ‘ /? ‘ 156 157 sub_obj = subprocess.Popen( 158 ‘ dir %s ‘ % dir_path, 159 shell= True, 160 stderr= subprocess.PIPE, 161 stdout= subprocess.PIPE 162 ) 163 stderr_bytes, stdout_bytes = sub_obj.stderr.read(), sub_obj.stdout.read() 164 cmd_size = len(stderr_bytes) + len(stdout_bytes) 165 166 # 发报头 167 self.send_header(status_code=301, cmd_size= cmd_size) 168 # 发消息 169 self.request.sendall(stderr_bytes) 170 self.request.sendall(stdout_bytes) 171 172 def _cd(self, cmd_dic): 173 “”” 174 根据用户的目标目录, 改变用户的当前目录的值. 175 :param cmd_dic: {‘action_type’: ‘cd’, ‘path’: [‘..’]} 176 或 {‘action_type’: ‘cd’, ‘path’: [‘目录1’, ‘目录2’, ‘目录xxx’], } 177 :return: None 178 Z:\\pycharm\\开发FTP程序之路\\第2次FTP_第四模块作业\\FUCK_FTP\\server\\home\\egon\\目录1 179 “”” 180 # print(‘_cd:’, cmd_dic) 181 self.logger.info( ‘ #执行cd命令# ip:%s port:%s ‘ % self.address) 182 183 # 核验路径 184 dir_path = self.verify_path(cmd_dic) 185 if dir_path: 186 if os.path.isdir(dir_path): # 判断用户切换的路径是否存在 187 self.user_current_dir = dir_path 188 if dir_path == self.user_obj[ ‘ home ‘ ]: 189 current_dir = ‘ ~ ‘ 190 else : 191 join_dir = ” .join(dir_path.split( ‘ %s ‘ % self.username)[1 :]) 192 current_dir = ‘ / ‘ .join(join_dir.split( ‘ \\ ‘ )) 193 self.send_header(status_code=400, current_dir= current_dir) 194 else : 195 self.send_header(status_code=499 ) 196 else : 197 self.send_header(status_code=498 ) 198 199 def _mkdir(self, cmd_dic): 200 “”” 201 更具用户的目标目录, 且目录不存在, 创建目录标目录, 生成多层递归目录. 202 :param cmd_dic: {‘action_type’: ‘mkdir’, ‘path’: [‘目录1’]} 203 或 {‘action_type’: ‘mkdir’, ‘path’: [‘目录2’, ‘目录3’, ‘目录xxx’]} 204 :return: None 205 “”” 206 # print(‘_mkdir:’, cmd_dic) 207 self.logger.info( ‘ #执行mkdir命令# ip:%s port:%s ‘ % self.address) 208 209 dir_path = self.verify_path(cmd_dic) 210 if dir_path: 211 if not os.path.isdir(dir_path): # 判断用户要创建的目录时否存在 212 os.makedirs(dir_path) 213 self.send_header(status_code=500 ) 214 else : 215 self.send_header(status_code=599 ) 216 else : 217 self.send_header(status_code=598 ) 218 219 def _rmdir(self, cmd_dic): 220 “”” 221 更具用户的目标目录, 删除不为空的目录. 222 :param cmd_dic: {‘path’: [‘目录1’, ‘目录xxx’, ‘空目录’], ‘action_type’: ‘rmdir’} 223 :return: None 224 “”” 225 # print(‘_rmdir:’, cmd_dic) 226 self.logger.info( ‘ #执行rmdir命令# ip:%s port:%s ‘ % self.address) 227 228 dir_path = self.verify_path(cmd_dic) 229 if dir_path: 230 if os.path.isdir(dir_path): 231 if os.listdir(dir_path): 232 self.send_header(status_code=699 ) 233 else : 234 os.rmdir(dir_path) 235 self.send_header(status_code=600 ) 236 else : 237 self.send_header(status_code=698 ) 238 else : 239 self.send_header(status_code=697 ) 240 241 def _remove(self, cmd_dic): 242 “”” 243 更具用户的目标文件, 删除该文件 244 :param cmd_dic: {‘path’: [‘目录1’, ‘目录xxx’, ‘文件’], ‘action_type’: ‘remove’} 245 :return: 246 “”” 247 # print(‘_remove:’, cmd_dic) 248 self.logger.info( ‘ #执行remove命令# ip:%s port:%s ‘ % self.address) 249 file_path = self.verify_path(cmd_dic) 250 251 if file_path: 252 if os.path.isfile(file_path): 253 # 判断用户删除的文件是否是要续传的文件, 如果是则先把把续传的记录删除 254 if file_path in self.breakpoint_resume.keys: 255 del self.breakpoint_resume[file_path] 256 os.remove(file_path) 257 self.send_header(status_code=700 ) 258 else : 259 self.send_header(status_code=799 ) 260 else : 261 self.send_header(status_code=798 ) 262 263 def _resume_upload(self, cmd_dic): 264 “”” 265 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 266 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 267 :param cmd_dic: 268 :return: 269 “”” 270 # print(‘def _resume_upload ===> cmd_args’, cmd_dic) 271 self.logger.info( ‘ #执行resume_upload命令# ip:%s port:%s ‘ % self.address) 272 self._upload(cmd_dic, resume_upload= True) 273 274 def _upload(self, cmd_dic, resume_upload= False): 275 “”” 客户端 276 800: ‘你可以上传文件, 在您上传之前, 您的目前空间:%s!’, 277 801: ‘上传文件成功, 您上传完后的剩余空间:%s!’, 278 850: ‘您的还有为上传完的文件, 是否继续上传!’, 279 851: ‘检测您不存在未上传完成的文件!’, 280 852: ‘您不能进行续传, 因为该文件是完整文件!’, 281 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 282 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 283 894: ‘您不需要再对本路径下上传文件, 该文件在您的当前路径下已经存在!’, 284 895: ‘上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!’, 285 896: ‘上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!’, 286 897: ‘上传文件失败, 您的空间不足, 您的剩余空间:%s!’, 287 898: ‘上传文件失败, 上传命令不规范!’, 288 899: ‘上传文件必须要有文件的md5值以及文件名!’, 289 “”” 290 # print(‘_upload:’, cmd_dic) 291 if not resume_upload: 292 self.logger.info( ‘ #执行upload命令# ip:%s port:%s ‘ % self.address) 293 294 # 效验: 897, 898, 899 295 _path, _file_md5, _file_name, _file_size = cmd_dic.get( ‘ path ‘ ), cmd_dic.get( ‘ file_md5 ‘ ), cmd_dic.get( 296 ‘ file_name ‘ ), cmd_dic.get( ‘ file_size ‘ ) 297 file_path = self.verify_upload_action(cmd_dic, _path=_path, _file_md5=_file_md5, _file_name= _file_name, 298 299 _file_size= _file_size) 300 301 if resume_upload: # 断点续传时执行 302 if not file_path or file_path not in self.breakpoint_resume.keys(): 303 # 869: ‘您选择文件路径中没有要续传的文件, 请核对!’, 304 self.send_header(status_code=869 ) 305 return 306 307 # 找到之前未穿完的文件名 308 unfinished_file_path = self.breakpoint_resume[file_path][ ‘ unfinished_file_path ‘ ] 309 already_upload_size = os.path.getsize(unfinished_file_path) 310 311 # 效验成功通知续传信号 312 # 860: ‘您正在继续上传文件, 在您继传之前, 您的目前空间:%s!’, 313 self.send_header(status_code=860, residual_space_size= self.residual_space_size, 314 already_upload_size= already_upload_size) 315 316 total_size = _file_size – already_upload_size 317 mode = ‘ a ‘ 318 else : # 正常上传执行 319 if not file_path: 320 return 321 322 # 判断用户上传的文件是否重复 323 if os.path.isfile(file_path): 324 # 894: ‘您不需要再对本路径下上传文件, 该文件在您的当前路径下已经存在!’, 325 self.send_header(status_code=894 ) 326 return 327 else : 328 unfinished_file_path = ‘ %s.%s ‘ % (file_path, ‘ upload ‘ ) 329 330 # 效验成功通知上传信号: 800 331 # 800: ‘你可以上传文件, 在您上传之前, 您的目前空间:%s!’, 332 self.send_header(status_code=800, residual_space_size= self.residual_space_size) 333 334 total_size = _file_size 335 mode = ‘ w ‘ 336 337 # 记录断点的功能: 在服务端用户的路径, 记录文件大小, 加上后缀的路径, 文件名 338 # 或再次为未传完的文件记录断点 339 self.breakpoint_resume[file_path] = { ‘ file_size ‘ : _file_size, ‘ unfinished_file_path ‘ : unfinished_file_path, 340 ‘ file_name ‘ : _file_name} 341 342 # 开始接收文件 343 receive_size = 0 344 with open(unfinished_file_path, ‘ %sb ‘ % mode) as f: 345 while receive_size < total_size: 346 data_bytes = self.request.recv(self.max_packet_size) 347 receive_size += len(data_bytes) 348 f.write(data_bytes) 349 # 接收完毕, 把后缀改成用户上传的文件名 350 os.rename(unfinished_file_path, file_path) 351 # 删除记录断点的功能 352 del self.breakpoint_resume[file_path] 353 354 # 801, 895, 896 355 # 效验用户端发送的md5于本次上传完毕的md5值 356 upload_file_md5 = common.md5(encryption_type= ‘ file ‘ , path= file_path) 357 if upload_file_md5 != _file_md5: 358 # print(‘def _upload ===> upload_file_md5:%s, _file_md5:%s’ % (upload_file_md5, _file_md5)) 359 # 895: ‘上传文件失败, md5效验不一致, 部分文件内容在网络中丢失, 请重新上传!’, 360 self.send_header(status_code=895 ) 361 os.remove(file_path) 362 return 363 364 # 安全性问题: 再次判断用户是否以假的文件大小来跳出服务端限制的配额 365 if receive_size > self.residual_space_size: 366 # 896: ‘上传文件失败, 您的空间不足, 您的上传虚假文件大小, 您的剩余空间:%s!’, 367 self.send_header(status_code=896, residual_space_size= self.residual_space_size) 368 os.remove(file_path) 369 return 370 else : 371 self.residual_space_size = self.residual_space_size – receive_size 372 # print(‘def _upload ===> receive_size:’, receive_size) 373 # print(‘def _upload ===> os.path.getsize(file_path)’, os.path.getsize(‘%s’ % file_path)) 374 # 801: ‘上传文件成功, 您上传完后的剩余空间:%s!’, 375 self.send_header(status_code=801, residual_space_size= self.residual_space_size) 376 377 def _resume_download(self, cmd_dic): 378 self._download(cmd_dic, resume_download= True) 379 380 def _download(self, cmd_dic, resume_download= False): 381 self.logger.info( ‘ #执行download命令# ip:%s port:%s ‘ % self.address) 382 383 file_path = self.verify_path(cmd_dic) 384 if not file_path: 385 # 999: ‘下载文件失败, 您要下载的文件路径不规范!’, 386 self.send_header(status_code=999 ) 387 return 388 389 if not os.path.isfile(file_path): 390 # 998: ‘下载文件失败, 您要下载的文件路径不存在!’, 391 self.send_header(status_code=998 ) 392 return 393 394 # 通知可以开始下载 395 # 900: ‘准备开始下载文件!’. 396 file_name = file_path.split(os.sep)[-1 ] 397 file_size = os.path.getsize(file_path) 398 file_md5 = common.md5( ‘ file ‘ , file_path) 399 unfinished_file_size = cmd_dic.get( ‘ unfinished_file_size ‘ ) 400 if resume_download: 401 # 950: ‘准备开始续传文件!’, 402 self.send_header(status_code=950, file_name=file_name, file_size=file_size, file_md5= file_md5) 403 else : 404 # 900: ‘准备开始下载文件!’. 405 self.send_header(status_code=900, file_name=file_name, file_size=file_size, file_md5= file_md5) 406 407 # 打开文件发送给客户端 408 with open(file_path, ‘ rb ‘ ) as f: 409 if resume_download: 410 f.seek(unfinished_file_size) 411 for line in f: 412 self.request.sendall(line) 413 414 def verify_upload_action(self, cmd_dic, * , _path, _file_name, _file_md5, _file_size): 415 “”” 416 核验上传功能. 417 897: ‘上传文件失败, 您的空间不足, 您的剩余空间:%s!’, 418 898: ‘上传文件失败, 上传命令不规范!’, 419 899: ‘上传文件必须要有文件的md5值以及文件名!’, 420 “”” 421 # _path=[’03_函数调用的三种形式.mp4′] 422 if _path is None: 423 if _file_name and _file_md5 and _file_size: 424 if _file_size > self.residual_space_size: 425 # print(‘def _upload ===> self.residual_space_size:’, self.residual_space_size) 426 427 # 897: ‘上传文件失败, 您的空间不足, 您的剩余空间:%s!’, 428 self.send_header(status_code=897, residual_space_size= self.residual_space_size) 429 return False 430 else : 431 # Z:\pycharm\开发FTP程序之路\第2次FTP_第四模块作业\FUCK_FTP\server\home\egon\03_函数调用的三种形式.mp4 432 file_path = os.path.join(self.user_current_dir, _file_name) 433 else : 434 # 899: ‘上传文件必须要有文件的md5值以及文件名!’, 435 self.send_header(status_code=899 ) 436 return False 437 else : 438 path = self.verify_path(cmd_dic) 439 440 if not path: 441 # 898: ‘上传文件失败, 上传命令不规范!’, 442 self.send_header(status_code=898 ) 443 return False 444 else : 445 # Z:\pycharm\开发FTP程序之路\第2次FTP_第四模块作业\FUCK_FTP\server\home\egon\03_函数调用的三种形式.mp4 446 file_path = os.path.join(path, _file_name) 447 return file_path 448 449 def verify_path(self, cmd_dic): 450 “”” 451 核验客户端传过来的路径. 452 :param cmd_dic: {‘action_type’: ‘ls’, ‘path’: []} 453 或 {‘action_type’: ‘ls’, ‘path’: [‘目录1’, ‘目录xxx’]} 454 或 {action_type’: ‘cd’, ‘path’: [‘目录2’, ‘目录xxx’]} 455 :return: None 456 Z:\\pycharm\\开发FTP程序之路\\第2次FTP_第四模块作业\\FUCK_FTP\\server\\home\\egon\\目录1 457 Z:\\pycharm\\开发FTP程序之路\\第2次FTP_第四模块作业\\FUCK_FTP\\server\\home\\egon\\目录1 458 “”” 459 # print(cmd_dic) 460 path = cmd_dic.get( ‘ path ‘ ) 461 if path: 462 if isinstance(path, list): 463 for element in path: 464 if not isinstance(element, str): 465 path = None 466 return path 467 abspath = os.path.normpath(os.path.join(self.user_current_dir, * path)) 468 # print(‘def verify_path() ===> abspath:’, abspath) 469 if abspath.startswith(self.user_obj[ ‘ home ‘ ]): 470 path = abspath 471 else : 472 path = None # 用户目录超出限制 473 else : 474 path = None # 不是列表类型例: ‘字符串’ 475 else : 476 path = None # [] 477 # print(‘def verify_path() ====> path’, path) 478 return path 479 480 def receive_header(self): 481 “”” 482 接收客户端数据. 483 :return: {‘action_type’: ‘cd’, ‘path’: [‘目录1’, ‘目录xxx’]} 484 “”” 485 header_bytes = self.request.recv(self.fixed_packet_size) 486 request_dic_json_length = struct.unpack(self.struct_fmt, header_bytes)[0] 487 # print(‘request_dic_json_length:’, request_dic_json_length) 488 # 接收报头 489 request_dic_json = self.request.recv(request_dic_json_length).decode(self.encoding) 490 request_dic = json.loads(request_dic_json) 491 492 # print(‘request_dic:’, request_dic) 493 494 if not request_dic: 495 return {} 496 # print(“def receive_header():”, request_dic) 497 return request_dic 498 499 def send_header(self, *, status_code, ** kwargs): 500 “”” 501 发送数据给客户端. 502 :param status_code: 400 503 :param kwargs: {‘current_dir’: ‘/home/egon/目录1/目录xxx’} 504 :return: None 505 “”” 506 # print(status_code) 507 # print(kwargs) 508 from core import main 509 510 response_dic = kwargs 511 response_dic[ ‘ status_code ‘ ] = status_code 512 response_dic[ ‘ status_msg ‘ ] = main.FTPServer.STATUS_CODE[status_code] 513 response_dic.update(kwargs) 514 515 response_dic_json_bytes = json.dumps(response_dic).encode(self.encoding) 516 response_dic_json_bytes_length = len(response_dic_json_bytes) 517 header_bytes = struct.pack(self.struct_fmt, response_dic_json_bytes_length) 518 519 # print(‘header_bytes:’, header_bytes) 520 521 # 发送报头 522 self.request.sendall(header_bytes) 523 # 发送json后bytes后的字典response_dic 524 self.request.sendall(response_dic_json_bytes) handler_request.py 1 import configparser 2 import socket 3 4 from conf import settings 5 from core import handler_request, mythreadpool 6 from lib import common 7 8 9 class FTPServer: 10 “”” FTP服务器. “”” 11 address_family = socket.AF_INET 12 socket_type = socket.SOCK_STREAM 13 allow_reuse_address = False 14 request_queue_size = 5 15 16 max_pool_size = 5 17 18 STATUS_CODE = settings.STATUS_CODE 19 20 logger = common.load_my_logging_cfg() 21 22 def __init__ (self, management_instance, bind_address, bind_and_activate= True): 23 self.management_instance = management_instance 24 25 self.pool = mythreadpool.MyThreadPool(self.max_pool_size) 26 27 self.bind_address = bind_address 28 self.socket = socket.socket(self.address_family, self.socket_type) 29 30 if bind_and_activate: 31 try : 32 self.server_bind() 33 self.server_activate() 34 except Exception: 35 self.server_close() 36 raise 37 38 def server_bind(self): 39 “”” 服务器绑定IP,端口. “”” 40 if self.allow_reuse_address: 41 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) 42 self.socket.bind(self.bind_address) 43 44 def server_activate(self): 45 “”” 服务器激活. “”” 46 self.socket.listen(self.request_queue_size) 47 48 def server_close(self): 49 “”” 关闭服务socket对象. “”” 50 self.socket.close() 51 52 def serve_forever(self): 53 “”” 服务器永远运行. “”” 54 while True: # 通信循环 55 request, address = self.socket.accept() 56 57 self.logger.info( ‘ —-连接—-# ip:%s port:%s ‘ % address) 58 59 # 来一个连接, 实例化一个处理用户请求的对象 60 handler_response = handler_request.HandlerRequest(request, address) 61 # 来了一个连接取走一个线程 62 thread = self.pool.get_thread() 63 # 同时再添加一个线程 64 self.pool.put_thread() 65 t = thread(target= handler_response.handle_request) 66 t.start() 67 68 @staticmethod 69 def load_accounts(): 70 conf_obj = configparser.ConfigParser() 71 conf_obj.read(settings.ACCOUNTS_FILE) 72 return conf_obj main.py 1 import sys 2 3 from conf import settings 4 from core import main 5 6 7 class ManagementTool(object): 8 “”” 管理服务器. “”” 9 center_args1, center_args2 = 50, ‘ – ‘ 10 11 def __init__ (self): 12 self.script_argv = sys.argv 13 self.commands = None 14 15 # print(self.script_argv) 16 17 self.verify_argv() 18 19 def verify_argv(self): 20 “”” 21 核查参数时否合理. 22 例: 23 [‘启动文件路径’, ‘start’, ‘ftp’, ‘server’] 24 “”” 25 if len(self.script_argv) != 4 : 26 self.help_msg() 27 28 action_type = self.script_argv[1 ] 29 self.commands = self.script_argv[2 :] 30 if hasattr(self, action_type): 31 func = getattr(self, action_type) 32 func() 33 else : 34 self.help_msg() 35 36 @staticmethod 37 def help_msg(): 38 msg = “”” 39 ——严格要求输入以下命令:—— 40 ① start ftp server 41 ② stop ftp server 42 ③ restart ftp server 43 “”” 44 exit(msg) 45 46 def start(self): 47 “”” 启动ftp服务. “”” 48 if self.execute(): 49 print ( ‘ FTP started successfully! ‘ ) 50 # FTPServer中可能用到ManagementTool中功能 51 server = main.FTPServer(self, (settings.HOST, settings.PORT)) 52 server.serve_forever() 53 else : 54 self.help_msg() 55 56 def execute(self): 57 “”” 解析命令. “”” 58 args1, args2 = self.commands 59 if args1 == ‘ ftp ‘ and args2 == ‘ server ‘ : 60 return True 61 return False management.py 1 import os 2 import queue 3 from threading import Thread 4 5 6 class MyThreadPool: 7 def __init__ (self, max_workers= None): 8 if not max_workers: 9 max_workers = os.cpu_count() * 5 10 if max_workers <= 0: 11 raise ValueError( ‘ max_workers 必须大于0 ‘ ) 12 13 self.queue = queue.Queue(max_workers) 14 for count in range(max_workers): 15 self.put_thread() 16 17 def put_thread(self): 18 self.queue.put(Thread) 19 20 def get_thread(self): 21 return self.queue.get() mythreadpool.py 3) home 用户目录,以用户名作为文件名 4) lib 1 import hashlib 2 import logging.config 3 import os 4 5 from conf import settings 6 7 8 def md5(encryption_type, path=None, password= None): 9 “”” 10 md5加密. 11 :param encryption_type: 加密的类型, 支持file和password两种 12 :param path: 文件或目录路径 13 :param password: 明文密码 14 :return: 加密后的md5值 15 “”” 16 md5_obj = hashlib.md5() 17 if encryption_type == ‘ file ‘ : 18 if os.path.isfile(path): 19 with open(path, ‘ rb ‘ ) as f: 20 for line in f: 21 md5_obj.update(line) 22 return md5_obj.hexdigest() 23 for filename in os.listdir(path): 24 current_path = os.path.join(path, filename) 25 if os.path.isdir(current_path): 26 md5(encryption_type, path= current_path) 27 else : 28 with open(current_path, ‘ rb ‘ ) as f: 29 for line in f: 30 md5_obj.update(line) 31 elif encryption_type == ‘ password ‘ : 32 md5_obj.update(password.encode( ‘ utf-8 ‘ )) 33 return md5_obj.hexdigest() 34 35 36 def load_my_logging_cfg(): 37 “”” 38 加载日志字典. 39 :return: logger对象 40 “”” 41 logging.config.dictConfig(settings.LOGGING_DIC) 42 logger = logging.getLogger( __name__ ) 43 return logger 44 45 46 def get_size(path): 47 “”” 48 遍历用户path, 拿到path的路径大小, 该大小包含目录下的所有文件. 49 :param path: 路径 50 :return: 该路径下的所有文件的大小 51 “”” 52 initial_size = 0 53 if os.path.isfile(path): 54 return os.path.getsize(path) 55 for filename in os.listdir(path): 56 current_path = os.path.join(path, filename) 57 if os.path.isdir(current_path): 58 get_size(current_path) 59 else : 60 initial_size += os.path.getsize(current_path) 61 return initial_size 62 63 64 def conversion_quota(quota_mb: str): 65 “”” 66 换算用户磁盘配额, 把MB换算成bytes. 67 :param quota_mb: 68 :return: 满足isdigit返回quota_bytes, 不满足设置默认的配额大小 69 “”” 70 if quota_mb.isdigit(): 71 quota_mb = int(quota_mb) 72 quota_bytes = quota_mb * 1024 ** 2 73 # print(‘def conversion_quota ===> quota_bytes:’, quota_bytes) 74 return quota_bytes 75 else : 76 default_quota_bytes = 50 * 1024 ** 2 77 return default_quota_bytes common.py 5) log access.log 1 # encoding:utf-8 2 3 import os 4 import sys 5 6 BASE_DIR = os.path.normpath(os.path.join( __file__ , ‘ .. ‘ )) 7 sys.path.append(BASE_DIR) 8 9 if __name__ == ‘ __main__ ‘ : 10 from core import management 11 management = management.ManagementTool() 12 management.execute() ftp_server.pyhttps://www.cnblogs.com/yang1333/p/11807531.html

Python量化投资网携手4326手游为资深游戏玩家推荐:《《有杀气童话2》TestFlight秘密测试公告

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论