原文链接:http://www.itnose.net/detail/6642756.html
作业:开发一个支持多用户在线的FTP程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server上随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
附加功能:支持文件的断点续传(仅下载)
程序
1、最最重要的readme:
### 作者介绍:
* author:lzl
### 博客地址:
* http://www.cnblogs.com/lianzhilei/p/5813986.html
### 功能实现
作业:开发一个支持多用户在线的FTP程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server上随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
附加功能:支持文件的断点续传
### 目录结构:
FTP
│
├── ftpclient #客户端程序
│ ├── __init__.py
│ └── ftpclient.py #客户端主程序
└── ftpserver #服务端程序
├── README.txt
├── ftpserver.py #服务端入口程序
├── conf #配置文件目录
│ ├── __init__.py
│ └── setting.py
├── modules #程序核心目录
│ ├── __init__.py
│ ├── auth_user.py #用户认证模块
│ └── sokect_server.py #sokectserver模块
├── database #用户数据库
│ ├── alex.db
│ ├── lzl.db
│ └── eric.db
├── home #用户宿主目录
│ ├── alex
│ ├── lzl
│ └── eric
└── log
├── __init__.py
└── log #待扩展....
### 功能实现
1、conf目录下settings.py模块记录可操作用户信息,根据用户信息生成用户字典和宿主目录,已经生成的不再新建
2、每个用户的宿主目录磁盘空间配额默认为10M,可在settings.py模块里进行修改
3、程序运行在windows系统上,程序要求全部实现,下面是具体命令操作
4、切换目录:cd .. 返回上一级目录 cd dirname 进入dirname
用户登录后默认进入宿主目录,只可在宿主目录下随意切换
5、创建目录:mkdir dirname
在当前目录下创建目录,如果目录存在则报错,不存在创建
6、查看当前路径: pwd
7、查看当前路径下的文件名和目录名: dir
8、下载文件(可续传):get filename
①、服务端当前目录存在此文件,客户端不存在此文件,直接下载
②、服务端当前目录存在此文件,客户端存在此文件名,之前下载中断,文件可续传,进行续传
③、服务端当前目录存在此文件,客户端存在此文件名,大小与服务端一致,不下载
9、上传文件:put filename
判断宿主目录磁盘空间是否够用,可以,上传文件;否则,报错
### 状态码
400 用户认证失败
401 命令不正确
402 文件不存在
403 创建文件已经存在
404 磁盘空间不够
405 不续传
200 用户认证成功
201 命令可以执行
202 磁盘空间够用
203 文件具有一致性
205 续传
000 系统交互码
2、程序目录结构:
3、ftp客户端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#-Author-Lian
import socket
import os,sys
import hashlib
class Myclient():
'''ftp客户端'''
def __init__(self,ip_port):
self.ip_port = ip_port
def connect(self):
'''连接服务器'''
self.client = socket.socket()
self.client.connect(self.ip_port)
def start(self):
'''程序开始'''
self.connect()
while True:
username = input("输入用户名:").strip()
password = input("输入密码:").strip()
login_info = ("%s:%s" %(username, password))
self.client.sendall(login_info.encode()) #发送用户密码信息
status_code = self.client.recv(1024).decode() #返回状态码
if status_code == "400":
print("[%s]用户密码认证错误"%status_code)
continue
else:print("[%s]用户密码认证成功"%status_code)
self.interactive()
def interactive(self):
'''开始交互'''
while True:
command = input("->>").strip()
if not command:continue
#if command == "exit":break
command_str = command.split()[0]
if hasattr(self,command_str): # 执行命令
func = getattr(self,command_str)
func(command)
else:print("[%s]命令不存在"%401)
def get(self,command):
'''下载文件'''
self.client.sendall(command.encode()) #发送要执行的命令
status_code = self.client.recv(1024).decode()
if status_code == "201": #命令可执行
filename = command.split()[1]
# 文件名存在,判断是否续传
if os.path.isfile(filename):
revice_size = os.stat(filename).st_size #文件已接收大小
self.client.sendall("403".encode())
response = self.client.recv(1024)
self.client.sendall(str(revice_size).encode()) #发送已接收文件大小
status_code = self.client.recv(1024).decode()
# 文件大小不一致,续传
if status_code == "205":
print("继续上次上传位置进行续传")
self.client.sendall("000".encode())
# 文件大小一致,不续传,不下载
elif status_code == "405":
print("文件已经存在,大小一致")
return
# 文件不存在
else:
self.client.sendall("402".encode())
revice_size = 0
file_size = self.client.recv(1024).decode() #文件大小
file_size = int(file_size)
self.client.sendall("000".encode())
with open(filename,"ab") as file: #开始接收
#file_size 为文件总大小
file_size +=revice_size
m = hashlib.md5()
while revice_size < file_size:
minus_size = file_size - revice_size
if minus_size > 1024:
size = 1024
else:
size = minus_size
data = self.client.recv(size)
revice_size += len(data)
file.write(data)
m.update(data)
self.__progress(revice_size,file_size,"下载中") #进度条
new_file_md5 = m.hexdigest() #生成新文件的md5值
server_file_md5 = self.client.recv(1024).decode()
if new_file_md5 == server_file_md5: #md5值一致
print("\n文件具有一致性")
else:print("[%s] Error!"%(status_code))
def put(self,command):
'''上传文件'''
if len(command.split()) > 1:
filename = command.split()[1]
#file_path = self.current_path + r"\%s"%filename
if os.path.isfile(filename): #文件是否存在
self.client.sendall(command.encode()) #发送要执行的命令
response = self.client.recv(1024) #收到ack确认
file_size = os.stat(filename).st_size # 文件大小
self.client.sendall(str(file_size).encode()) # 发送文件大小
status_code = self.client.recv(1024).decode() # 等待响应,返回状态码
if status_code == "202":
with open(filename,"rb") as file:
m = hashlib.md5()
for line in file:
m.update(line)
send_size = file.tell()
self.client.sendall(line)
self.__progress(send_size, file_size, "上传中") # 进度条
self.client.sendall(m.hexdigest().encode()) #发送文件md5值
status_code = self.client.recv(1024).decode() #返回状态码
if status_code == "203":
print("\n文件具有一致性")
else:print("[%s] Error!"%(status_code))
else:
print("[402] Error")
else: print("[401] Error")
def dir(self,command):
'''查看当前目录下的文件'''
self.__universal_method_data(command)
pass
def pwd(self,command):
'''查看当前用户路径'''
self.__universal_method_data(command)
pass
def mkdir(self,command):
'''创建目录'''
self.__universal_method_none(command)
pass
def cd(self,command):
'''切换目录'''
self.__universal_method_none(command)
pass
def __progress(self, trans_size, file_size,mode):
'''
显示进度条
trans_size: 已经传输的数据大小
file_size: 文件的总大小
mode: 模式
'''
bar_length = 100 #进度条长度
percent = float(trans_size) / float(file_size)
hashes = '=' * int(percent * bar_length) #进度条显示的数量长度百分比
spaces = ' ' * (bar_length - len(hashes)) #定义空格的数量=总长度-显示长度
sys.stdout.write(
"\r%s:%.2fM/%.2fM %d%% [%s]"%(mode,trans_size/1048576,file_size/1048576,percent*100,hashes+spaces))
sys.stdout.flush()
def __universal_method_none(self,command):
'''通用方法,无data输出'''
self.client.sendall(command.encode()) # 发送要执行的命令
status_code = self.client.recv(1024).decode()
if status_code == "201": # 命令可执行
self.client.sendall("000".encode()) # 系统交互
else:
print("[%s] Error!" % (status_code))
def __universal_method_data(self,command):
'''通用方法,有data输出'''
self.client.sendall(command.encode()) #发送要执行的命令
status_code = self.client.recv(1024).decode()
if status_code == "201": #命令可执行
self.client.sendall("000".encode()) #系统交互
data = self.client.recv(1024).decode()
print(data)
else:print("[%s] Error!" % (status_code))
if __name__ == "__main__":
ip_port =("127.0.0.1",9999) #服务端ip、端口
client = Myclient(ip_port) #创建客户端实例
client.start() #开始连接
4、ftp服务端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#-Author-Lian
import os,hashlib
import json
from conf import settings
from modules import auth_user
from modules import sokect_server
def create_db():
'''创建用户数据库文件'''
user_database={}
encryption = auth_user.User_operation()
limitsize = settings.LIMIT_SIZE
for k,v in settings.USERS_PWD.items():
username = k
password = encryption.hash(v)
user_db_path = settings.DATABASE + r"\%s.db"%username
user_home_path = settings.HOME_PATH + r"\%s"%username
user_database["username"] = username
user_database["password"] = password
user_database["limitsize"] = limitsize
user_database["homepath"] = user_home_path
if not os.path.isfile(user_db_path):
with open(user_db_path,"w") as file:
file.write(json.dumps(user_database))
def create_dir():
'''创建用户属主目录'''
for username in settings.USERS_PWD:
user_home_path = settings.HOME_PATH + r"\%s" %username
if not os.path.isdir(user_home_path):
os.popen("mkdir %s" %user_home_path)
if __name__ == "__main__":
'''初始化系统数据并启动程序'''
create_db() #创建数据库
create_dir() #创建属主目录
#启动ftp服务
server = sokect_server.socketserver.ThreadingTCPServer(settings.IP_PORT, sokect_server.Myserver)
server.serve_forever()
5、conf配置文件
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#-Author-Lian
import os,sys
#程序主目录文件
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#添加环境变量
sys.path.insert(0,BASE_DIR)
#数据库目录
DATABASE = os.path.join(BASE_DIR,"database")
#用户属主目录
HOME_PATH = os.path.join(BASE_DIR,"home")
#用户字典
USERS_PWD = {"alex":"123456","lzl":"8888","eric":"6666"}
#磁盘配额 10M
LIMIT_SIZE = 10240000
#ftp服务端口
IP_PORT = ("0.0.0.0",9999)
6、database用户数据库(系统初始化自动生成)
{"username": "alex", "password": "e10adc3949ba59abbe56e057f20f883e", "limitsize": 10240000, "homepath": "C:\\Users\\L\\PycharmProjects\\s14\\homework\\Day8\\FTP\\ftpserver\\home\\alex"}
{"username": "eric", "password": "e9510081ac30ffa83f10b68cde1cac07", "limitsize": 10240000, "homepath": "C:\\Users\\L\\PycharmProjects\\s14\\homework\\Day8\\FTP\\ftpserver\\home\\eric"}
{"username": "alex", "password": "e10adc3949ba59abbe56e057f20f883e", "limitsize": 10240000, "homepath": "C:\\Users\\L\\PycharmProjects\\s14\\homework\\Day8\\FTP\\ftpserver\\home\\alex"}
7、modules模块dd
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#-Author-Lian
import json
import sys,os
import hashlib
from conf import settings
class User_operation():
'''对登录信息进行认证,登录成功返回用户名,失败返回None'''
def authentication(self,login_info):
list = login_info.split(":") #对信息进行分割
login_name = list[0]
login_passwd = self.hash(list[1])
DB_FILE = settings.DATABASE + r"\%s.db"%login_name
if os.path.isfile(DB_FILE):
user_database = self.cat_database(DB_FILE) #用户数据库信息
if login_name == user_database["username"]:
if login_passwd == user_database["password"]:
return user_database
def cat_database(self,DB_FILE):
#获取数据库信息
with open(DB_FILE,"r") as file:
data = json.loads(file.read())
return data
def hash(self,passwd):
'''对密码进行md5加密'''
m = hashlib.md5()
m.update(passwd.encode("utf-8"))
return m.hexdigest()
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#-Author-Lian
import socketserver
import sys,os
import hashlib
from os.path import join, getsize
from conf import settings
from modules import auth_user
class Myserver(socketserver.BaseRequestHandler):
'''ftp服务端'''
def handle(self):
try:
self.conn = self.request
while True:
login_info = self.conn.recv(1024).decode() # 接收客户端发的的账号密码信息
result = self.authenticat(login_info)
status_code = result[0]
self.conn.sendall(status_code.encode())
if status_code == "400":
continue
self.user_db = result[1] #当前登录用户信息
self.current_path = self.user_db["homepath"] #用户当前目录
self.home_path = self.user_db["homepath"] #用户宿主目录
while True:
command = self.conn.recv(1024).decode()
command_str = command.split()[0]
if hasattr(self,command_str):
func = getattr(self,command_str)
func(command)
else:self.conn.sendall("401".encode())
except ConnectionResetError as e:
self.conn.close()
print(e)
def authenticat(self,login_info):
'''认证用户'''
auth = auth_user.User_operation() # 创建认证实例
result = auth.authentication(login_info) # 认证用户
if result:return "200",result
else:return "400",result
def get(self,command):
'''下载文件'''
if len(command.split()) > 1:
filename = command.split()[1]
file_path = self.current_path + r"\%s"%filename
if os.path.isfile(file_path): #文件是否存在
self.conn.sendall("201".encode()) #命令可执行
file_size = os.stat(file_path).st_size # 文件总大小
status_code = self.conn.recv(1024).decode()
# 客户端存在此文件
if status_code == "403":
self.conn.sendall("000".encode()) #系统交互
has_send_size = self.conn.recv(1024).decode()
has_send_size = int(has_send_size)
# 客户端文件不完整可续传
if has_send_size < file_size:
self.conn.sendall("205".encode())
file_size -= has_send_size #续传文件大小
response = self.conn.recv(1024) # 等待响应
# 客户端文件完整不可续传、不提供下载
else:
self.conn.sendall("405".encode())
return
# 客户端不存在此文件
elif status_code == "402":
has_send_size = 0
with open(file_path,"rb") as file:
self.conn.sendall(str(file_size).encode()) #发送文件大小
response = self.conn.recv(1024) #等待响应
file.seek(has_send_size)
m = hashlib.md5()
for line in file:
m.update(line)
self.conn.sendall(line)
self.conn.sendall(m.hexdigest().encode()) #发送文件md5值
else:self.conn.sendall("402".encode())
else:self.conn.sendall("401".encode())
def put(self,command):
'''上传文件'''
filename = command.split()[1]
file_path = self.current_path + r"\%s" % filename
self.conn.sendall("000".encode()) #发送确认
file_size = self.conn.recv(1024).decode() # 文件大小
file_size = int(file_size)
limit_size = self.user_db["limitsize"] #磁盘额度
used_size = self.__getdirsize(self.home_path) #已用空间大小
if limit_size >= file_size+used_size:
self.conn.sendall("202".encode())
with open(file_path, "wb") as file: # 开始接收
revice_size = 0
m = hashlib.md5()
while revice_size < file_size:
minus_size = file_size - revice_size
if minus_size > 1024:
size = 1024
else:
size = minus_size
data = self.conn.recv(size)
revice_size += len(data)
file.write(data)
m.update(data)
new_file_md5 = m.hexdigest() # 生成新文件的md5值
server_file_md5 = self.conn.recv(1024).decode()
if new_file_md5 == server_file_md5: # md5值一致
self.conn.sendall("203".encode())
else:self.conn.sendall("404".encode())
def dir(self,command):
'''查看当前目录下的文件'''
if len(command.split()) == 1:
self.conn.sendall("201".encode())
response = self.conn.recv(1024)
send_data = os.popen("dir %s"%self.current_path)
self.conn.sendall(send_data.read().encode())
else:self.conn.sendall("401".encode())
def pwd(self,command):
'''查看当前用户路径'''
if len(command.split()) == 1:
self.conn.sendall("201".encode())
response = self.conn.recv(1024)
send_data = self.current_path
self.conn.sendall(send_data.encode())
else:self.conn.sendall("401".encode())
def mkdir(self,command):
'''创建目录'''
if len(command.split()) > 1:
dir_name = command.split()[1] #目录名
dir_path = self.current_path + r"\%s"%dir_name #目录路径
if not os.path.isdir(dir_path): #目录不存在
self.conn.sendall("201".encode())
response = self.conn.recv(1024)
os.popen("mkdir %s"%dir_path)
else:self.conn.sendall("403".encode())
else:self.conn.sendall("401".encode())
def cd(self,command):
'''切换目录'''
if len(command.split()) > 1:
dir_name = command.split()[1] #目录名
dir_path = self.current_path + r"\%s" %dir_name #目录路径
user_home_path = settings.HOME_PATH + r"\%s"%self.user_db["username"] #宿主目录
if dir_name == ".." and len(self.current_path)>len(user_home_path):
self.conn.sendall("201".encode())
response = self.conn.recv(1024)
self.current_path = os.path.dirname(self.current_path) #返回上一级目录
elif os.path.isdir(dir_path) :
self.conn.sendall("201".encode())
response = self.conn.recv(1024)
if dir_name != "." and dir_name != "..":
self.current_path += r"\%s"%dir_name #切换目录
else:self.conn.sendall("402".encode())
else:self.conn.sendall("401".encode())
def __getdirsize(self,home_path):
'''统计目录空间大小'''
size = 0
for root, dirs, files in os.walk(home_path):
size += sum([getsize(join(root, name)) for name in files])
return size
效果图:
下载:
续传: