实现支持多用户在线的FTP程序(C/S),一个C程序是由


1. 需求

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

 

2. 开发环境

  Python 3.7.3

3. 软件开发

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

 

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

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

5. 用户配置信息

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

6. 所有功能测试 

(1) 登陆

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

 

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

查看方法:

  • 命令 + –-help

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

 

(3) ls: 查看

① 支持功能:

  • 查看当前目录下的文件

    • ls

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

    • ls /目录1/目录2

  • 查看帮助信息

    • ls /?

② 运行效果:

1 [egon@localhost ~]# ls 2 驱动器 Z 中的卷是 固态硬盘 3 卷的序列号是 AA26-64F0 4 5 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon 的目录 6 7 2019-10-26 22:34 <DIR> . 8 2019-10-26 22:34 <DIR> .. 9 2019-10-19 20:03 1,081,540 123.docx 10 2019-10-27 13:45 <DIR> 我是egon的目录 11 1 个文件 1,081,540 字节 12 3 个目录 56,465,575,936 可用字节 1) 查看当前目录下的文件

 

 

1 [egon@localhost ~]# ls /我是egon的目录 2 驱动器 Z 中的卷是 固态硬盘 3 卷的序列号是 AA26-64F0 4 5 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录 的目录 6 7 2019-10-27 13:45 <DIR> . 8 2019-10-27 13:45 <DIR> .. 9 2019-10-27 13:45 <DIR> 我是江傻子的目录 10 2019-10-27 12:34 0 江傻子 11 1 个文件 0 字节 12 3 个目录 56,465,575,936 可用字节 2) 指定目录下的文件(只能查看到自己家目录的范围)

 

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

 

 

(4) cd: 切换目录

① 支持功能:

  • 相对路径切换

    • cd /目录1

      • cd /目录2

  • 切换到上一层目录

    • cd ..

  • 绝对路径切换

    • cd /目录1/目录2

  • 在当前目录下切当前目录

    • cd .

② 运行效果:

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

 

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

 

 

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

 

1 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# cd . 2 切换目录成功 3 [egon@localhost /home/egon/我是egon的目录/我是江傻子的目录]# ls 4 驱动器 Z 中的卷是 固态硬盘 5 卷的序列号是 AA26-64F0 6 7 Z:\pycharm\开发FTP程序之路\第2次FTP_修改后的内容\第二次实现方式\server\home\egon\我是egon的目录\我是江傻子的目录 的目录 8 9 2019-10-27 13:45 <DIR> . 10 2019-10-27 13:45 <DIR> .. 11 2019-10-27 13:45 0 我是江大傻.txt 12 1 个文件 0 字节 13 2 个目录 56,465,559,552 可用字节 4) 在当前目录下切当前目录

 

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

① 支持功能:

  • 相对路径创建:

    • mkdir /目录

  • 生成多层递归目录:

    • mkdir /目录1/目录2

② 运行效果:

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

 

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

 

(6) rmdir: 删除空目录

① 支持功能:

  • 删除空目录:

    • rmdir /目录1/空目录2

② 运行效果:

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

 

(7) remove: 删除文件

① 支持功能:

  • 删除文件

    • remove /目录1/文件

② 运行效果:

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

 

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

① 支持功能:

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

    • upload 文件

  • 通过cd切换目录上传文件到该目录下

    • cd /目录1/目录2

      • upload 文件

② 运行效果:

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

 

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

 

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

① 支持功能:

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

    • resume_upload 文件名

  • 通过cd切换目录, 到该目录下指定服务端的某个目录下继续上传:

    • cd /目录1/目录2

      • resume_upload 文件名

② 运行效果:

1 ------您的files文件夹下所含有的文件------ 2 1: .download 3 2: 03_函数调用的三种形式.mp4 4 3: 服务器管理综合报告.docx 5 6 [egon@localhost ~]# upload 03_函数调用的三种形式.mp4 7 你可以上传文件, 在您上传之前, 您的目前空间:97.10MB! 8 9 upload running... 10 [############ ] 25.43% 先断开传输:

 

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

 

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

 

(10) download: 下载文件

① 支持功能:

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

    • download 文件

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

    • download /目录1/文件

② 运行效果:

1 [egon@localhost ~]# download 服务器管理综合报告.docx 2 3 download run... 4 [##################################################] 100.00% 5 download succeed! 1) 从服务端当前目录下下载文件

 

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

 

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

① 支持功能:

  • 用户登陆的时候显示为下载完的文件

    • 用户根据序号选择要继续续传的文件

  • 用户可以多次循环选择

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

② 运行效果:

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

 

(12) 为用户磁盘配额

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

 

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

(14) 记录了日志功能

① 终端打印

② 保存文件之中 

 

7. 不足之处

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

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

8.代码展示

① client

1) conf

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

2) core

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

3) files

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

 

 

②server

1) conf

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

2) core

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

3) home

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

4) lib

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

5) log

 

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相关内容

    暂无相关文章

评论关闭