开发一个支持多用户在线的FTP程序
首先对于这样的任务要建立文件目录,不能将所有的代码放在一个文件夹下。文件目录,先两个大目录,客户端FTP_client,服务端FTP_server。
服务端FTP_server下面肯定还要文件夹bin(放启动文件),conf(放配置文件,方便后续修改),core(放主逻辑,主程序),home,logger(放日志,这个作业用不到)。
下面按顺序介绍,先是bin文件下,有一个ftp_server.py,代码如下:
1 import os,sys 2 3 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 将当前搜索路径返回FTP_server这级 4 sys.path.append(BASE_DIR) 5 from core import main 6 7 if __name__ == '__main__': 8 9 main.ArgvHandler()
启动文件,顾名思义,相当于其他文件夹下的其他文件都是复制到这个目录下执行的,所以有关涉及路径操作时注意。
下面是core文件下的两个.py文件:
1 import optparse 2 import socketserver 3 from conf import settings 4 from core import server 5 ''' 6 core文件下面放的是主逻辑 7 ''' 8 9 10 class ArgvHandler(object): 11 12 def __init__(self): # 接收参数,解析命令 13 self.op=optparse.OptionParser() 14 15 # op.add_option("-s","--host",dest="host",help="server IP address") 16 # op.add_option("-P","--port",dest="port",help="server port") 17 18 options,args=self.op.parse_args() # options返回的类,里面有你add_option加入的属性,加入的其他放在args中 19 # print(options,args) 20 # print(options.host,options.port) 21 22 self.verify_argv(options,args) # 这个函数相当于是一个命令的分发,比如在命令行中输入Python ftp_server.py start 23 24 25 def verify_argv(self,options,args): # 命令行的分发,这里args[0],就是start,所以取得函数名后,对应执行 26 27 if hasattr(self,args[0]): 28 func=getattr(self,args[0]) 29 func() 30 31 else: 32 self.op.print_help() 33 34 35 def start(self): 36 print('server is working ....') 37 ser=socketserver.ThreadingTCPServer((settings.IP,settings.PORT),server.ServerHandler) # 建立TCP链接循环,而且是可并发的 38 ser.serve_forever()
1 import socketserver 2 import json 3 import configparser 4 import os 5 from conf import settings 6 import hashlib 7 import shutil 8 9 10 STATUS_CODE = { 11 250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}", 12 251 : "Invalid cmd ", 13 252 : "Invalid auth data", 14 253 : "Wrong username or password", 15 254 : "Passed authentication", 16 255 : "Filename doesn't provided", 17 256 : "File doesn't exist on server", 18 257 : "ready to send file", 19 258 : "md5 verification", 20 21 800 : "the file exist,but not enough ,is continue? ", 22 801 : "the file exist !", 23 802 : " ready to receive datas", 24 25 900 : "md5 valdate success" 26 27 } 28 ''' 29 写socketserver首先得写一个自己的类,里面要重写handle函数,里面在写通讯循环;然后外面写链接循环(main里面已经写了) 30 ''' 31 32 class ServerHandler(socketserver.BaseRequestHandler): 33 34 def handle(self): 35 36 while 1: # 这里不断循环,其实就是与一个客户端不断的通讯(交互) 37 38 data=self.request.recv(1024).strip() 39 print("data-----",data) 40 if len(data)==0:break 41 42 data=json.loads(data.decode("utf8")) 43 print("data:",data) 44 45 ''' 46 data = {'action':'auth', 47 'username':user, 48 'password':password} 49 ''' 50 51 if data.get("action"): # 这里又是一个命令行的分发,其实啰嗦的写可以用多个if,稍微好一点是用字典,这样写最好 52 if hasattr(self,'_%s'%data.get("action")): 53 func=getattr(self,'_%s'%data.get("action")) 54 func(**data) 55 56 else: 57 print("Invalid cmd!") 58 self.send_response(251) 59 60 else: 61 print("invalid cmd format") 62 self.send_response(250) 63 64 65 def send_response(self,status_code,data=None): 66 '''向客户端返回数据''' 67 response = {'status_code':status_code,'status_msg':STATUS_CODE[status_code]} 68 if data: 69 response.update(data) 70 self.request.send(json.dumps(response).encode()) 71 72 73 def _auth(self,**data): 74 75 if data.get("username") is None or data.get("password") is None: 76 self.send_response(252) 77 78 user=self.authenticate(data.get("username"),data.get("password")) 79 80 if user is None: 81 print("") 82 self.send_response(253) 83 84 else: 85 self.user=user 86 print("passed authentication",user) 87 self.send_response(254) 88 self.mainPath=os.path.join(settings.BASE_DIR,"home",self.user) 89 90 91 def authenticate(self,username,password): # 用户名与密码验证,相当于是跟数据库中的信息匹配,看有没有这个用户,看密码对不对 92 93 cfp=configparser.ConfigParser() 94 cfp.read(settings.ACCOUNT_PATH) 95 96 if username in cfp.sections(): 97 print(".....",cfp[username]["Password"]) 98 99 Password=cfp[username]["Password"] 100 if Password==password: 101 print("auth pass!") 102 return username 103 104 105 def _post(self,**data): # 文件上传功能实现 106 # 上传文件有几种可能:1,文件已经存在(就算存在,也分两种情况,1)存在的是完整的;2)存在的不完整(又分两种情况,是否需要续传)); 107 # 2,原来不存在 108 109 file_name=data.get("file_name") 110 file_size=data.get("file_size") 111 target_path=data.get("target_path") 112 print(file_name,file_size,target_path) 113 abs_path=os.path.join(self.mainPath,target_path,file_name) 114 print("abs_path",abs_path) 115 116 has_received=0 117 118 119 if os.path.exists(abs_path): 120 has_file_size=os.stat(abs_path).st_size 121 if has_file_size <file_size: 122 self.request.sendall(b"800") 123 124 is_continue=str(self.request.recv(1024),"utf8") 125 if is_continue=="Y": 126 self.request.sendall(bytes(str(has_file_size),"utf8")) 127 has_received+=has_file_size 128 f=open(abs_path,"ab") 129 130 else: 131 f=open(abs_path,"wb") 132 else: 133 self.request.sendall(b"801") 134 return #注意这个return必须有 135 136 else: 137 self.request.sendall(b"802") 138 f=open(abs_path,"wb") 139 140 141 while has_received<file_size: 142 try: 143 data=self.request.recv(1024) 144 if not data: 145 raise Exception 146 except Exception: 147 148 break 149 150 f.write(data) 151 has_received+=len(data) 152 153 f.close() 154 155 156 def _post_md5(self,**data): 157 158 file_name=data.get("file_name") 159 file_size=data.get("file_size") 160 target_path=data.get("target_path") 161 print(file_name,file_size,target_path) 162 abs_path=os.path.join(self.mainPath,target_path,file_name) 163 print("abs_path",abs_path) 164 165 has_received=0 166 167 168 if os.path.exists(abs_path): 169 has_file_size=os.stat(abs_path).st_size 170 if has_file_size <file_size: 171 self.request.sendall(b"800") 172 173 is_continue=str(self.request.recv(1024),"utf8") 174 if is_continue=="Y": 175 self.request.sendall(bytes(str(has_file_size),"utf8")) 176 has_received+=has_file_size 177 f=open(abs_path,"ab") 178 179 else: 180 f=open(abs_path,"wb") 181 else: 182 self.request.sendall(b"801") 183 return #注意这个return必须有 184 185 else: 186 self.request.sendall(b"802") 187 f=open(abs_path,"wb") 188 189 190 if data.get('md5'): 191 print("hhhhhhhhhhh") 192 md5_obj = hashlib.md5() 193 194 while has_received<file_size: 195 196 try: 197 data=self.request.recv(1024) 198 if not data: 199 raise Exception 200 except Exception: 201 202 break 203 204 f.write(data) 205 has_received+=len(data) 206 recv_file_md5=md5_obj.update(data) 207 print("mmmmmm") 208 209 else: 210 self.request.sendall(b"ok")#解决粘包 211 send_file_md5=self.request.recv(1024).decode("utf8") 212 print("send_file_md5",send_file_md5) 213 self.request.sendall("900".encode("utf8")) 214 215 else: 216 217 while has_received<file_size: 218 219 try: 220 data=self.request.recv(1024) 221 if not data: 222 raise Exception 223 except Exception: 224 225 break 226 f.write(data) 227 has_received+=len(data) 228 229 f.close() 230 231 232 233 def _ls(self,**data): 234 235 file_list=os.listdir(self.mainPath) 236 237 file_str='\n'.join(file_list) 238 if not file_list: 239 file_str="<empty directory>" 240 self.request.send(file_str.encode("utf8")) 241 242 243 def _cd(self,**data): 244 245 path=data.get("path") 246 247 if path=="..": 248 self.mainPath=os.path.dirname(self.mainPath) 249 else: 250 self.mainPath=os.path.join(self.mainPath,path) 251 252 self.request.send(self.mainPath.encode("utf8")) 253 254 255 256 257 def _mkdir(self,**data): 258 259 dirname = data.get("dirname") 260 tar_path=os.path.join(self.mainPath,dirname) 261 if not os.path.exists(tar_path): 262 if "/" in dirname: 263 os.makedirs(tar_path) #创建多级目录 264 else: 265 os.mkdir(tar_path) #创建单级目录 266 267 self.request.send(b"mkdir_success!") 268 269 else: 270 self.request.send(b"dir_exists!") 271 272 273 def _rmdir(self,**data): 274 dirname =data.get("dirname") 275 276 tar_path=os.path.join(self.mainPath,dirname) 277 if os.path.exists(tar_path): 278 if os.path.isfile(tar_path): 279 os.remove(tar_path) #删除文件 280 else: 281 shutil.rmtree(tar_path) #删除目录 282 283 self.request.send(b"rm_success!") 284 else: 285 self.request.send(b"the file or dir does not exist!") 286 287 288 def _pwd(self,**data): 289 self.request.send(self.mainPath.encode("utf8"))
下面是conf文件下的两个文件,一个.py,一个.cfg相当于数据库功能。
1 import os 2 3 BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 5 6 7 IP="127.0.0.1" 8 PORT=8088 9 10 11 ACCOUNT_PATH=os.path.join(BASE_DIR,"conf","accounts.cfg")
1 [DEFAULT] 2 3 [yuan] 4 Password = 123 5 Quotation = 100 6 7 [root] 8 Password = root 9 Quotation = 100
home文件夹下面还有两个文件夹,家目录root文件夹,与一个用户文件夹yuan。
yuan文件夹下面还有一个images文件夹。(调试时,就将用户yuan的一张图片上传到服务器的这个文件夹下)。上传前,它们都是空。
关于FTP的客户端,那就简单了,所有程序都在文件FTP_client下,里面有一个.py文件与一张要测试上传的图片。
1 import optparse 2 import socket 3 import json,os,sys,time,hashlib 4 5 STATUS_CODE = { 6 250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}", 7 251 : "Invalid cmd ", 8 252 : "Invalid auth data", 9 253 : "Wrong username or password", 10 254 : "Passed authentication", 11 12 800 : "the file exist,but not enough ,is continue? ", 13 801 : "the file exist !", 14 802 : " ready to receive datas", 15 16 900 : "md5 valdate success" 17 18 } 19 20 class ClientHanlder(object): 21 22 def __init__(self): 23 24 self.op=optparse.OptionParser() 25 self.op.add_option("-s","--server",dest="server") 26 self.op.add_option("-P","--port",dest="port") 27 self.op.add_option("-u","--username",dest="username") 28 self.op.add_option("-p","--password",dest="password") 29 self.options , self.args=self.op.parse_args() 30 self.verify_args() 31 self.make_connection() 32 self.mainPath=os.path.dirname(os.path.abspath(__file__)) 33 self.last=0 34 35 36 37 def verify_args(self,): 38 if self.options.server and self.options.port: 39 #print(options) 40 if int(self.options.port) >0 and int(self.options.port) <65535: 41 return True 42 else: 43 exit("Err:host port must in 0-65535") 44 45 def make_connection(self): 46 47 self.sock = socket.socket() 48 self.sock.connect((self.options.server,int(self.options.port))) 49 50 51 def interactive(self): 52 53 if self.authenticate(): 54 55 print("---start interactive with you ...") 56 while 1: 57 cmd_info=input("[%s]"%self.current_path).strip() 58 if len(cmd_info)==0:continue 59 cmd_list = cmd_info.split() 60 if hasattr(self,"_%s"%cmd_list[0]): 61 func=getattr(self,"_%s"%cmd_list[0]) 62 func(cmd_list) 63 64 else: 65 print("Invalid cmd") 66 67 68 69 70 def authenticate(self): 71 72 if self.options.username and self.options.password: 73 return self.get_auth_result(self.options.username,self.options.password) 74 else: 75 76 username=input("username: ") 77 password=input("password: ") 78 return self.get_auth_result(username,password) 79 80 def get_auth_result(self,username,password): 81 82 data = {'action':'auth', 83 'username':username, 84 'password':password} 85 86 self.sock.send(json.dumps(data).encode()) 87 response = self.get_response() 88 print("response",response) 89 if response.get('status_code') == 254: 90 print("Passed authentication!") 91 self.user = username 92 self.current_path = '/'+username 93 94 return True 95 else: 96 print(response.get("status_msg")) 97 98 def get_response(self): 99 100 data = self.sock.recv(1024) 101 data = json.loads(data.decode()) 102 return data 103 104 def progress_percent(self,has,total,): 105 106 107 rate = float(has) / float(total) 108 rate_num = int(rate * 100) 109 110 if self.last!=rate_num: 111 112 sys.stdout.write("%s%% %s\r"%(rate_num,"#"*rate_num)) 113 114 self.last=rate_num 115 116 117 def _post(self,cmd_list): 118 119 action,local_path,target_path=cmd_list 120 121 if "/" in local_path: 122 local_path=os.path.join(self.mainPath,local_path.split("/")) 123 local_path=os.path.join(self.mainPath,local_path) 124 125 file_name=os.path.basename(local_path) 126 file_size=os.stat(local_path).st_size 127 128 data_header = { 129 'action':'post', 130 'file_name': file_name, 131 'file_size': file_size, 132 'target_path':target_path 133 } 134 135 self.sock.send(json.dumps(data_header).encode()) 136 137 result_exist=str(self.sock.recv(1024),"utf8") 138 has_sent=0 139 140 if result_exist=="800": 141 choice=input("the file exist ,is_continue?").strip() 142 if choice.upper()=="Y": 143 self.sock.sendall(bytes("Y","utf8")) 144 result_continue_pos=str(self.sock.recv(1024),"utf8") 145 print(result_continue_pos) 146 has_sent=int(result_continue_pos) 147 148 else: 149 self.sock.sendall(bytes("N","utf8")) 150 151 elif result_exist=="801": 152 print(STATUS_CODE[801]) 153 return 154 155 156 file_obj=open(local_path,"rb") 157 file_obj.seek(has_sent) 158 start=time.time() 159 160 while has_sent<file_size: 161 162 data=file_obj.read(1024) 163 self.sock.sendall(data) 164 has_sent+=len(data) 165 166 self.progress_percent(has_sent,file_size) 167 168 file_obj.close() 169 end=time.time() 170 print("cost %s s"% (end-start)) 171 print("post success!") 172 173 def _post_md5(self,cmd_list): 174 175 176 if len(cmd_list)==3: 177 178 action,local_path,target_path=cmd_list 179 else: 180 action,local_path,target_path,is_md5=cmd_list 181 182 if "/" in local_path: 183 local_path=os.path.join(self.mainPath,local_path.split("/")) 184 local_path=os.path.join(self.mainPath,local_path) 185 186 file_name=os.path.basename(local_path) 187 file_size=os.stat(local_path).st_size 188 189 data_header = { 190 'action':'post_md5', 191 'file_name': file_name, 192 'file_size': file_size, 193 'target_path':target_path 194 } 195 196 if self.__md5_required(cmd_list): 197 data_header['md5'] = True 198 199 200 self.sock.send(json.dumps(data_header).encode()) 201 202 result_exist=str(self.sock.recv(1024),"utf8") 203 has_sent=0 204 205 if result_exist=="800": 206 choice=input("the file exist ,is_continue?").strip() 207 if choice.upper()=="Y": 208 self.sock.sendall(bytes("Y","utf8")) 209 result_continue_pos=str(self.sock.recv(1024),"utf8") 210 print(result_continue_pos) 211 has_sent=int(result_continue_pos) 212 213 else: 214 self.sock.sendall(bytes("N","utf8")) 215 216 elif result_exist=="801": 217 print(STATUS_CODE[801]) 218 return 219 220 221 file_obj=open(local_path,"rb") 222 file_obj.seek(has_sent) 223 start=time.time() 224 225 if self.__md5_required(cmd_list): 226 md5_obj = hashlib.md5() 227 228 while has_sent<file_size: 229 230 data=file_obj.read(1024) 231 self.sock.sendall(data) 232 has_sent+=len(data) 233 md5_obj.update(data) 234 self.progress_percent(has_sent,file_size) 235 236 else: 237 print("post success!") 238 md5_val = md5_obj.hexdigest() 239 self.sock.recv(1024)#解决粘包 240 self.sock.sendall(md5_val.encode("utf8")) 241 response=self.sock.recv(1024).decode("utf8") 242 print("response",response) 243 if response=="900": 244 print(STATUS_CODE[900]) 245 246 else: 247 while has_sent<file_size: 248 249 data=file_obj.read(1024) 250 self.sock.sendall(data) 251 has_sent+=len(data) 252 253 self.progress_percent(has_sent,file_size) 254 255 else: 256 file_obj.close() 257 end=time.time() 258 print("\ncost %s s"% (end-start)) 259 print("post success!") 260 261 262 263 def __md5_required(self,cmd_list): 264 '''检测命令是否需要进行MD5验证''' 265 if '--md5' in cmd_list: 266 return True 267 268 269 def _ls(self,cmd_list): 270 271 data_header = { 272 'action':'ls', 273 } 274 self.sock.send(json.dumps(data_header).encode()) 275 276 data = self.sock.recv(1024) 277 278 print(data.decode("utf8")) 279 280 281 def _cd(self,cmd_list): 282 283 data_header = { 284 285 'action':'cd', 286 287 'path':cmd_list[1] 288 } 289 290 self.sock.send(json.dumps(data_header).encode()) 291 292 data = self.sock.recv(1024) 293 print(data.decode("utf8")) 294 self.current_path='/'+os.path.basename(data.decode("utf8")) 295 296 def _mkdir(self,cmd_list): 297 298 data_header = { 299 'action':'mkdir', 300 'dirname':cmd_list[1] 301 } 302 303 self.sock.send(json.dumps(data_header).encode()) 304 data = self.sock.recv(1024) 305 print(data.decode("utf8")) 306 307 def _rmdir(self,cmd_list): 308 data_header = { 309 'action':'rm', 310 'target_path':cmd_list[1] 311 } 312 self.sock.send(json.dumps(data_header).encode()) 313 data = self.sock.recv(1024) 314 print(data.decode("utf8")) 315 316 317 def _pwd(self,cmd_list): 318 319 data_header = { 320 'action':'pwd', 321 } 322 323 self.sock.send(json.dumps(data_header).encode()) 324 data = self.sock.recv(1024) 325 print(data.decode("utf8")) 326 327 328 ch=ClientHanlder() 329 ch.interactive()
以上就是全部内容,但是注意,这个服务器打开,客户端上传下载都是用指令行完成的。
比如打开服务器,就先找到启动文件位置,然后python ftp_server.py start
客户端注册,就是Python ftp_client.py -s 127.0.0.1, -P 8088 -u 用户名 -p 密码。
上传就是 put 12.jpg(文件名) images(目标位置)
等等