文章目录
- 一、小实验FTP程序需求
- 二、项目文件架构
- 三、服务端
- 1、conf/settings.py
- 2、conf/accounts.cgf
- 3、conf/STATUS_CODE.py
- 4、启动文件 bin/ftp_server.py
- 5、core/main.py
- 6、core/server.py
- 四、客户端
- 1、conf/STATUS_CODE.py
- 2、bin/ftp_client.py
- 五、在终端操作示例
一、小实验FTP程序需求
(1)用户加密认证
(2)允许同时多用户登录
(3)每个用户有自己的家目录,且只能访问自己的家目录
(4)对用户进行磁盘配额,每个用户的可用空间不同
(5)允许用户在FTP server上随意切换目录
(6)允许用户查看当前目录下文件
(7)允许上传和下载文件,保证文件一致性
(8)文件传输过程中显示进度条
(9)附加功能:支持文件的断点续传
二、项目文件架构
三、服务端
1、conf/settings.py
import os
BASE_DIE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IP = "127.0.0.1"
PORT = 8000
ACCOUNT_PATH = os.path.join(BASE_DIE, "conf", "accounts.cfg")
2、conf/accounts.cgf
[DEFAULT]
[LuMX]
Password = 123
Quotation = 100
[root]
Password = root
Quotation = 100
3、conf/STATUS_CODE.py
STATUS_CODE = {
250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
251 : "Invalid cmd ",
252 : "Invalid auth data",
253 : "Wrong username or password",
254 : "Passed authentication",
255 : "Filename doesn't provided",
256 : "File doesn't exist on server",
257 : "ready to send file",
258 : "md5 verification",
800 : "the file exist,but not enough ,is continue? ",
801 : "the file exist !",
802 : " ready to receive datas",
900 : "md5 valdate success"
}
4、启动文件 bin/ftp_server.py
import os, sys
# 获取启动模块所在目录的父级目录路径,
PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 将该目录路径添加到导入模块时要搜索的目录列表
sys.path.append(PATH)
from core import main
if __name__ == "__main__":
main.ArgvHandler() # 启动主函数
5、core/main.py
import optparse
import socketserver
from conf.settings import *
from core import server
class ArgvHandler:
def __init__(self):
self.op = optparse.OptionParser()
options, args = self.op.parse_args()
self.verify_args(args)
def verify_args(self,args):
cmd = args[0]
if hasattr(self, cmd):
# 方便扩展,例如可以在终端执行 python ftp_server.py start
func = getattr(self,cmd)
func()
def start(self):
print("the server is working...")
s = socketserver.ThreadingTCPServer((IP, PORT), server.ServerHandler)
s.serve_forever()
6、core/server.py
import json,configparser
import os,hashlib
from conf.STATUS_CODE import *
from conf import settings
import socketserver
class ServerHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
data = self.request.recv(1024).strip()
if not data:
print("the connect is breaked")
break
data = json.loads(data.decode("utf-8"))
if data.get("action"): # 字典data存在key为action,且不为空,执行下列代码
func = getattr(self, data.get("action"))
func(**data)
else:
print("Invalid cmd")
def send_response(self, state_code):
response = {"status_code": state_code}
self.request.sendall(json.dumps(response).encode("utf-8"))
def auth(self,**data):
username = data["username"]
password = data["password"]
user = self.authenticate(username, password)
if user:
self.send_response(254)
else:
self.send_response(253)
def authenticate(self,user,pwd):
cfg = configparser.ConfigParser()
cfg.read(settings.ACCOUNT_PATH)
if user in cfg.sections():
if cfg[user]["Password"]==pwd:
self.user = user
self.mainPath = os.path.join(settings.BASE_DIR,"home",self.user)
print("passed authentication")
return user
def put(self,**data):
print("data",data)
file_name = data.get("file_name")
file_size = data.get("file_size")
target_path = data.get("target_path")
abs_path = os.path.join(self.mainPath,target_path,file_name)
has_received=0
if os.path.exists(abs_path):
file_has_size=os.stat(abs_path).st_size
if file_has_size < file_size:
# 断点续传
self.request.sendall("800".encode("utf-8"))
choice = self.request.recv(1024).decode("utf-8")
if choice=="Y":
self.request.sendall(str(file_has_size).encode("utf-8"))
has_received += file_has_size
f=open(abs_path,"ab")
else:
f=open(abs_path,"wb")
else:
# 文件存在,且完整
self.request.sendall("801".encode("utf-8"))
return
else:
# 文件不存在
self.request.sendall("802".encode("utf-8"))
f = open(abs_path,"wb")
md5_obj = hashlib.md5()
while has_received < file_size:
md5_data = self.request.recv(32)
f_data = self.request.recv(992)
if self.md5_check(md5_obj,md5_data,f_data):
self.send_response(900)
f.write(f_data)
has_received += len(f_data)
else:
print("md5 check error")
self.send_response(901)
break
f.close()
def md5_check(self,md5_obj,md5_data,f_data):
md5_obj.update(f_data)
if md5_obj.hexdigest().encode("utf-8") == md5_data:
return True
def ls(self, **data):
file_list = os.listdir(self.mainPath)
file_str="\n".join(file_list)
if not len(file_list):
file_str="<empty dir>"
self.request.sendall(file_str.encode("utf-8"))
def cd(self,**data):
dirname=data.get("dirname")
if dirname == "..":
self.mainPath=os.path.dirname(self.mainPath)
else:
self.mainPath=os.path.join(self.mainPath,dirname)
self.request.sendall(self.mainPath.encode("utf-8"))
def mkdir(self,**data):
dirname=data.get("dirname")
path=os.path.join(self.mainPath,dirname)
if not os.path.exists(path):
if "/" in dirname:
os.makedirs(path)
else:
os.mkdir(path)
self.request.sendall("create success".encode("utf-8"))
else:
self.request.sendall("dirname exist".encode("utf-8"))
四、客户端
1、conf/STATUS_CODE.py
STATUS_CODE = {
250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
251 : "Invalid cmd ",
252 : "Invalid auth data",
253 : "Wrong username or password",
254 : "Passed authentication",
255 : "Filename doesn't provided",
256 : "File doesn't exist on server",
257 : "ready to send file",
258 : "md5 verification",
800 : "the file exist,but not enough ,is continue? ",
801 : "the file exist !",
802 : " ready to receive datas",
900 : "md5 valdate success"
}
2、bin/ftp_client.py
import os, sys, hashlib
import optparse,socket,json
PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PATH)
from conf.STATUS_CODE import STATUS_CODE
class ClientHandler():
def __init__(self):
self.op = optparse.OptionParser()
self.op.add_option("-s", "--server", dest="server")
self.op.add_option("-P", "--port", dest="port")
self.op.add_option("-u", "--username", dest="username")
self.op.add_option("-p", "--password", dest="password")
self.options, self.args = self.op.parse_args()
self.verify_args(self.options)
self.make_connection()
self.mainPath=os.path.dirname(os.path.abspath(__file__))
def verify_args(self,options):
'''
验证端口号是否合法
'''
port = options.port
if 0 <= int(port) <= 65535:
return True
else:
exit("the port is not in 0-65535")
def make_connection(self):
'''
处理链接
'''
self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.sock.connect((self.options.server, int(self.options.port)))
def interactive(self):
'''
处理通讯
'''
print("begin to interactive...")
if self.authenticate():
while True:
cmd_info = input("[%s]" %self.current_dir).strip()
cmd_list = cmd_info.split()
if hasattr(self,cmd_list[0]):
func=getattr(self,cmd_list[0])
func(*cmd_list)
def put(self,*cmd_list):
action,local_path,target_path=cmd_list
local_path=os.path.join(self.mainPath,local_path)
file_name=os.path.basename(local_path)
file_size=os.stat(local_path).st_size
data = {
"action": "put",
"file_name": file_name,
"file_size": file_size,
"target_path": target_path
}
self.sock.send(json.dumps(data).encode("utf-8"))
is_exist=self.sock.recv(1024).decode("utf-8")
has_sent=0
if is_exist=="800":
# 文件不完整
choice=input("the file exist,but not enough,do continue?[Y/N]").strip()
if choice.upper()=="Y":
self.sock.sendall("Y".encode("utf-8"))
continue_position=self.sock.recv(1024).decode("utf-8")
has_sent += int(continue_position)
else:
self.sock.sendall("N".encode("utf-8"))
elif is_exist=="801":
# 文件完全存在
print("the file exist")
return
f = open(local_path,"rb")
f.seek(has_sent)
md5_obj = hashlib.md5()
while has_sent < file_size:
f_data = f.read(992)
if self.md5_check(md5_obj,f_data):
has_sent += len(f_data)
self.show_progress(has_sent,file_size)
else:
print("md5 check is error!")
break
f.close()
if has_sent == file_size:
print("\n",end="")
print("put success!")
def md5_check(self,md5_obj,f_data):
md5_obj.update(f_data)
md5_data = md5_obj.hexdigest().encode("utf-8")
data = md5_data + f_data
self.sock.sendall(data)
response = self.response()
if response["status_code"] == 900:
return True
def show_progress(self,has,total):
'''
显示进度条
'''
rate=int(float(has)/float(total)*10000)/10000
rate_num=int(rate*100)
sys.stdout.write("{:.2%} {}\r".format(rate,rate_num*"#"))
# \r 表示光标移动到行首
def ls(self,*cmd_list):
data={
"action": "ls"
}
self.sock.sendall(json.dumps(data).encode("utf-8"))
data=self.sock.recv(1024).decode("utf-8")
print(data)
def cd(self,*cmd_list):
data={
"action": "cd",
"dirname": cmd_list[1]
}
self.sock.sendall(json.dumps(data).encode("utf-8"))
data = self.sock.recv(1024).decode("utf-8")
print(os.path.basename(data))
self.current_dir=os.path.basename(data)
def mkdir(self,*cmd_list):
data={
"action": "mkdir",
"dirname": cmd_list[1]
}
self.sock.sendall(json.dumps(data).encode("utf-8"))
data = self.sock.recv(1024).decode("utf-8")
print(data)
def authenticate(self):
if self.options.username is None or self.options.password is None:
username = input("username:")
password = input("password:")
return self.get_auth_result(username,password)
return self.get_auth_result(self.options.username, self.options.password)
def response(self):
data = self.sock.recv(1024).decode("utf-8")
data = json.loads(data)
return data
def get_auth_result(self,user,pwd):
data = {
"action":"auth",
"username":user,
"password":pwd
}
self.sock.send(json.dumps(data).encode("utf-8"))
response=self.response()
print("response:",response["status_code"])
if response["status_code"]==254:
self.user = user
self.current_dir=user
print(STATUS_CODE[254])
return True
else:
print(STATUS_CODE[response["status_code"]])
ch = ClientHandler()
ch.interactive()
五、在终端操作示例
服务端
客户端