要求:
1、用戶加密認證
2、允許同時多用戶登錄
3、每個用戶有自己的家目錄 ,且只能訪問自己的家目錄
4、對用戶進行磁盤配額,每個用戶的可用空間不同
5、允許用戶在ftp server上隨意切換目錄
6、允許用戶查看當前目錄下文件
7、允許上傳和下載文件,保證文件一致性
8、文件傳輸過程中顯示進度條
9、附加功能:支持文件的斷點續傳
README:
設計說明
1、client連接server端需要驗證賬號密碼,密碼使用MD5加密傳輸。
2、用戶信息保存在本地文件中,密碼MD5加密存儲。磁盤配額大小也保存在其中。
3、用戶連接上來後,可以執行命令如下
目錄變更:cd /cd dirname / cd . /cd ..
文件瀏覽:ls
文件刪除:rm filename
目錄增刪:mkdir dirname /rmdir dirname
查看當前目錄:pwd
查看當前目錄大小: du
上傳文件:put filename
下載文件:get filename
移動和重命名: mv filename/dirname filename/dirname
上傳斷點續傳: newput filename
下載斷點續傳: newget filename
4、涉及到目錄的操作,用戶登錄後,程序會給用戶一個“錨位”----以用戶名字命名的家目錄,使用戶無論怎麼操作,都只能在這個目錄底下。而在發給用戶的目錄信息時,隱去上層目錄信息。
5、用戶在創建時,磁盤配額大小默認是100M,在上傳文件時,程序會計算當前目錄大小加文件大小是否會超過配額上限。未超過,上傳;超過,返回磁盤大小不夠的信息。磁盤配額可通過用戶管理程序修改。
6、文件上傳和下載後都會進行MD5值比對,驗證文件是否一致。
7、服務端和客戶端都有顯示進度條功能,啓用該功能會降低文件傳輸速度,這是好看的代價。
8、文件斷點續傳,支持文件上傳和下載斷點續傳。斷點續傳上傳功能還會檢測用戶控件是否足夠。(斷點續傳命令使用前面new+put/get命名,包含put/get所有功能,由於邏輯增多,代碼複雜,特地保留原put/get,以備後用)。
暫且說到這,接下來是正式程序
試運行截圖
代碼如下:
1、服務端
server.conf
####用戶端配置文件#### [DEFAULT] logfile = ../log/server.log usermgr_log = ../log/usermgr.log upload_dir= ../user_files db_dir = ../db ####日誌文件位置#### [log] logfile = ../log/server.log usermgr_log = ../log/usermgr.log ####上傳文件存放位置#### [upload] upload_dir= ../user_files ####用戶信息存放位置#### [db] db_dir = ../db
main.py
#!/usr/bin/env python # -*- coding:utf-8 -*- import socketserver,os from usermanagement import useropr from server import MyTCPHandler info = ''' 1、啓動服務器 2、進入用戶管理 按q退出 ''' if __name__ == '__main__': while True: print(info) choice = input('>>>:') if choice == 'q': exit() elif choice == '1': ip, port = '0.0.0.0', 9999 server = socketserver.ThreadingTCPServer((ip, port), MyTCPHandler) server.serve_forever() elif choice == '2': useropr.interactive() else:continue
usermanagement
#!/usr/bin/env python # -*- coding:utf-8 -*- #filename:usermanagement.py import os,hashlib,time,pickle,shutil,configparser,logging ####讀取配置文件#### base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) config_file = os.path.join(base_dir, 'conf/server.conf') cf = configparser.ConfigParser() cf.read(config_file) ####設定日誌目錄#### if os.path.exists(cf.get('log','usermgr_log')): logfile = cf.get('log', 'usermgr_log') else: logfile = os.path.join(base_dir,'log/usermgr.log') ####設定用戶上傳文件目錄,這邊用於創建用戶家目錄使用#### if os.path.exists(cf.get('upload','upload_dir')): file_dir = cf.get('upload','upload_dir') else: file_dir = os.path.join(base_dir,'user_files') ####設定用戶信息存儲位置#### if os.path.exists(cf.get('db','db_dir')): db_path = cf.get('db','db_dir') else: db_path = os.path.join(base_dir,'db') def hashmd5(*args): ####用於加密密碼信息 m = hashlib.md5() m.update(str(*args).encode()) return m.hexdigest() class useropr(object): def __init__(self,user_name,passwd = '123456',phone_number=''): self.user_name = user_name self.id = time.strftime("%Y%m%d%H%M%S", time.localtime()) self.phone_number = phone_number self.passwd = passwd self.space_size = 104857600 ####初始分配100MB存儲空間 self.member_level = 1 ####會員等級,初始爲1,普通會員 @staticmethod ####使用靜態方法,可以直接用類命調用,如user.search_user(username),否則需要實例化一個對象後才能調用 def query_user(user_name): ####查詢用戶 db_filelist=os.listdir(db_path) #print(db_filelist) dict={} for filename in db_filelist: with open(os.path.join(db_path,filename),'rb') as f: content=pickle.load(f) #print(filename,content) ####開啓會打印出所有用戶信息 if content['username'] == user_name: #print(filename, content) dict={'filename':filename,'content':content} return dict def save_userinfo(self): ####保存用戶信息 query_result = self.query_user(self.user_name) ####檢查是否已存在同名用戶,如果沒有查詢結果應該爲None if query_result == None: user_info = { 'username':self.user_name, 'id':self.id, 'phonenumber':self.phone_number, 'passwd':hashmd5(self.passwd), 'spacesize':self.space_size, 'level':self.member_level } with open(os.path.join(db_path,self.id),'wb') as f: pickle.dump(user_info,f) print('用戶信息保存完畢') try: ####創建用戶家目錄 os.mkdir(os.path.join(file_dir, self.user_name)) print('用戶目錄創建成功!') except Exception as e: print('用戶目錄創建失敗,',e) else: print('用戶名重複,信息未保存') @staticmethod def change_info(user_name,**kwargs): ####修改信息 query_result = useropr.query_user(user_name) ####用於檢測用戶是否存在,不存在不處理 if query_result != None: userinfo_filename = query_result['filename'] user_info = query_result['content'] print('before update:',user_info) for key in kwargs: if key in ('username','id'): ####用戶名和ID不可更改 print(key,'項不可更改') elif key in ('passwd','phonenumber','spacesize','level'): ####允許修改的鍵值 if key == 'passwd': user_info[key] = hashmd5(kwargs[key]) ####加密密碼保存 else: user_info[key] = kwargs[key] with open(os.path.join(db_path, userinfo_filename), 'wb') as f: pickle.dump(user_info, f) print(key,'項用戶信息變更保存完畢') else: print('輸入信息錯誤,',key,'項不存在') print('after update:',user_info) else: print('用戶不存在') @staticmethod def delete_user(user_name): ####刪除用戶 query_result = useropr.query_user(user_name) ####用於檢測用戶是否存在,不存在不處理 if query_result != None: userinfo_filename = query_result['filename'] userfile_path=os.path.join(db_path, userinfo_filename) os.remove(userfile_path) query_result_again = useropr.query_user(user_name) if query_result_again == None: print('用戶DB文件刪除成功') try: shutil.rmtree(os.path.join(file_dir,user_name)) print('用戶家目錄刪除成功') except Exception as e: print('用戶家目錄刪除失敗:',e) else: print('用戶DB文件刪除失敗') else: print('用戶不存在或者已經被刪除') @staticmethod def query_alluser(): ####查詢所有用戶信息,用於調試使用 db_filelist=os.listdir(db_path) for filename in db_filelist: with open(os.path.join(db_path,filename),'rb') as f: content=pickle.load(f) print(filename,content) @staticmethod def interactive(): '''使用說明: 新增用戶請輸入類似: a=useropr(username,passwd) a.save_userinfo() 查詢用戶請輸入:useropr.query_user(username) 更改用戶信息請輸入:useropr.change_info(username,id=123,level=1,passwd=123,phonenumber=123),其中字典部分爲可選項 用戶刪除請輸入:useropr.delete_user(username) ''' info=''' 1、新增用戶 2、查詢用戶 3、修改用戶 4、刪除用戶 退出請按q ''' #useropr.query_alluser() ####查詢所有用戶信息,調試用 while True: print(info) choice = input('請輸入你的選擇:').strip() #print('operation choice: %s' % choice) if choice == 'q': exit() else: username = input('請輸入用戶名:').strip() #print('username: %s' % username) if username == '': print('用戶不能爲空') continue elif choice == '1': passwd = input('請輸入密碼:') new_user = useropr(username, passwd) new_user.save_userinfo() elif choice == '2': print(useropr.query_user(username)) elif choice == '3': update_item = input('請輸入要修改的項目,例如:level,passwd,phonenumber:') print('update item: %s' % update_item) update_value = input('請輸入要修改的項目新值:') useropr.change_info(username,**{update_item:update_value}) #### ‘**{}’ 不加**系統無法識別爲字典。不能直接使用update_item=update_value,update_item會直接被當成key值,而不是其中的變量。 elif choice == '4': useropr.delete_user(username) else: print('輸入錯誤') continue if __name__ == '__main__': useropr.interactive()
server.py
#!/usr/bin/env python # -*- coding:utf-8 -*- # filename:server.py import socketserver, json, os, sys, time, shutil, configparser, logging from usermanagement import useropr ####讀取配置文件#### base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) config_file = os.path.join(base_dir, 'conf/server.conf') cf = configparser.ConfigParser() cf.read(config_file) ####設定日誌目錄#### if os.path.exists(cf.get('log', 'logfile')): logfile = cf.get('log', 'logfile') else: logfile = os.path.join(base_dir, 'log/server.log') ####設定用戶上傳文件目錄#### if os.path.exists(cf.get('upload', 'upload_dir')): file_dir = cf.get('upload', 'upload_dir') else: file_dir = os.path.join(base_dir, 'user_files') ####設置日誌格式### logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S', filename=logfile, filemode='a+') def TimeStampToTime(timestamp): ####輸入timestamp格式化輸出時間,輸出格式如:2017-09-16 16:32:35 timeStruct = time.localtime(timestamp) return time.strftime('%Y-%m-%d %H:%M:%S', timeStruct) def ProcessBar(part, total): ####進度條模塊,運行會導致程序變慢 if total != 0: i = round(part * 100 / total) sys.stdout.write( '[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r') sys.stdout.flush() # if part == total: # print() class MyTCPHandler(socketserver.BaseRequestHandler): def put(self, *args): ####接收客戶端文件 # self.request.send(b'server have been ready to receive') ####發送ACK cmd_dict = args[0] filename = os.path.basename(cmd_dict['filename']) ####傳輸進來的文件名可能帶有路徑,將路徑去掉 filesize = cmd_dict['filesize'] filemd5 = cmd_dict['filemd5'] override = cmd_dict['override'] receive_size = 0 file_path = os.path.join(self.position, filename) if override != 'True' and os.path.exists(file_path): ####檢測文件是否已經存在 self.request.send(b'file have exits, do nothing!') else: if os.path.isfile(file_path): ####如果文件已經存在,先刪除,再計算磁盤空間大小 os.remove(file_path) current_size = self.du() ####調用du查看用戶磁盤空間大小,但是du命令的最後會發送一個結果信息給client,會和前面和後面的信息粘包,需要注意 self.request.recv(1024) ####接收客戶端ack信號,防止粘包,代號:P01 print(self.user_spacesize, current_size, filesize) if self.user_spacesize >= current_size + filesize: self.request.send(b'begin') ####發送開始傳輸信號 fk = open(file_path, 'wb') while filesize > receive_size: if filesize - receive_size > 1024: size = 1024 else: size = filesize - receive_size data = self.request.recv(size) fk.write(data) receive_size += len(data) # print(receive_size,len(data)) ####打印每次接收的數據 # ProcessBar(receive_size, filesize) ####服務端進度條,不需要可以註釋掉 fk.close() receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0] print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5) if receive_filemd5 == filemd5: self.request.send(b'file received successfully!') else: self.request.send(b'Error, file received have problems!') else: self.request.send( b'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % ( self.user_spacesize, current_size, self.user_spacesize - current_size, filesize)) def get(self, *args): ####發送給客戶端文件 # print('get receive the cmd',args[0]) filename = args[0]['filename'] print(filename) # self.request.send(b'server have been ready to send') ####發送ACK file_path = os.path.join(self.position, filename) if os.path.isfile(file_path): filesize = os.path.getsize(file_path) ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低 filemd5 = os.popen('md5sum %s' % file_path).read().split()[0] msg = { 'action': 'get', 'filename': filename, 'filesize': filesize, 'filemd5': filemd5, 'override': 'True' } print(msg) self.request.send(json.dumps(msg).encode('utf-8')) '''接下來發送文件給客戶端''' self.request.recv(1024) ####接收ACK信號,下一步發送文件 fk = open(file_path, 'rb') send_size = 0 for line in fk: send_size += len(line) self.request.send(line) # ProcessBar(send_size, filesize) ####服務端進度條,不需要可以註釋掉 else: print('文件傳輸完畢') fk.close() else: print(file_path, '文件未找到') self.request.send(json.dumps('Filenotfound').encode('utf-8')) def newput(self, *args): ####接收客戶端文件,具有斷點續傳功能 # self.request.send(b'server have been ready to receive') ####發送ACK cmd_dict = args[0] filename = os.path.basename(cmd_dict['filename']) ####傳輸進來的文件名可能帶有路徑,將路徑去掉 filesize = cmd_dict['filesize'] filemd5 = cmd_dict['filemd5'] override = cmd_dict['override'] receive_size = 0 file_path = os.path.join(self.position, filename) print(file_path,os.path.isdir(file_path)) if override != 'True' and os.path.exists(file_path): ####檢測文件是否已經存在 if os.path.isdir(file_path): self.request.send(b'file have exits, and is a directory, do nothing!') elif os.path.isfile(file_path): self.request.send(b'file have exits, do nothing!') resume_signal = self.request.recv(1024) ####接收客戶端發來的是否從文件斷點續傳的信號 if resume_signal == b'ready to resume from break point': ####執行斷點續傳功能 exits_file_size = os.path.getsize(file_path) current_size = self.du() time.sleep(0.5) ####防止粘包 print('用戶空間上限:%d, 當前已用空間:%d, 已存在文件大小:%d, 上傳文件大小:%d ' % (self.user_spacesize,current_size,exits_file_size,filesize)) if self.user_spacesize >= (current_size - exits_file_size + filesize): ####判斷剩餘空間是否足夠 if exits_file_size < filesize: receive_size = exits_file_size print('服務器上已存在的文件大小爲:',exits_file_size) msg = { 'state': True, 'position': exits_file_size, 'content': 'ready to receive file' } self.request.send(json.dumps(msg).encode('utf-8')) fk = open(file_path, 'ab+') while filesize > receive_size: if filesize - receive_size > 1024: size = 1024 else: size = filesize - receive_size data = self.request.recv(size) fk.write(data) receive_size += len(data) # print(receive_size,len(data)) ####打印每次接收的數據 # ProcessBar(receive_size, filesize) ####服務端進度條,不需要可以註釋掉 fk.close() receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0] print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5) if receive_filemd5 == filemd5: self.request.send(b'file received successfully!') else: self.request.send(b'Error, file received have problems!') else: ####如果上傳的文件小於當前服務器上的文件,則爲同名但不同文件,不上傳。實際還需要增加其他判斷條件,判斷是否爲同一文件。 msg = { 'state': False, 'position': '', 'content': 'Error, file mismatch, do nothing!' } self.request.send(json.dumps(msg).encode('utf-8')) else: ####如果續傳後的用戶空間大於上限,拒接續傳 msg = { 'state': False, 'position':'', 'content':'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, need_size:%d' % (self.user_spacesize, current_size, self.user_spacesize - current_size, filesize - exits_file_size) } self.request.send(json.dumps(msg).encode('utf-8')) else: pass else: if os.path.isfile(file_path): ####如果文件已經存在,先刪除,再計算磁盤空間大小 os.remove(file_path) current_size = self.du() ####調用du查看用戶磁盤空間大小,但是du命令的最後會發送一個結果信息給client,會和前面和後面的信息粘包,需要注意 self.request.recv(1024) ####接收客戶端ack信號,防止粘包,代號:P01 print(self.user_spacesize, current_size, filesize) if self.user_spacesize >= current_size + filesize: self.request.send(b'begin') ####發送開始傳輸信號 fk = open(file_path, 'wb') while filesize > receive_size: if filesize - receive_size > 1024: size = 1024 else: size = filesize - receive_size data = self.request.recv(size) fk.write(data) receive_size += len(data) # print(receive_size,len(data)) ####打印每次接收的數據 # ProcessBar(receive_size, filesize) ####服務端進度條,不需要可以註釋掉 fk.close() receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0] print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5) if receive_filemd5 == filemd5: self.request.send(b'file received successfully!') else: self.request.send(b'Error, file received have problems!') else: self.request.send( b'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % ( self.user_spacesize, current_size, self.user_spacesize - current_size, filesize)) def newget(self, *args): ####發送給客戶端文件,具有斷點續傳功能 # print('get receive the cmd',args[0]) filename = args[0]['filename'] remote_local_filesize = args[0]['filesize'] print(filename) # self.request.send(b'server have been ready to send') ####發送ACK file_path = os.path.join(self.position, filename) if os.path.isfile(file_path): filesize = os.path.getsize(file_path) ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低 filemd5 = os.popen('md5sum %s' % file_path).read().split()[0] msg = { 'action': 'newget', 'filename': filename, 'filesize': filesize, 'filemd5': filemd5, 'override': 'True' } print(msg) self.request.send(json.dumps(msg).encode('utf-8')) '''接下來發送文件給客戶端''' self.request.recv(1024) ####接收ACK信號,下一步發送文件 fk = open(file_path, 'rb') fk.seek(remote_local_filesize,0) send_size = remote_local_filesize for line in fk: send_size += len(line) self.request.send(line) # ProcessBar(send_size, filesize) ####服務端進度條,不需要可以註釋掉 else: print('文件傳輸完畢') fk.close() else: print(file_path, '文件未找到') self.request.send(json.dumps('Filenotfound').encode('utf-8')) def pwd(self, *args): current_position = self.position result = current_position.replace(file_dir, '') ####截斷目錄信息,使用戶只能看到自己的家目錄信息 self.request.send(json.dumps(result).encode('utf-8')) def ls(self, *args): ####列出當前目錄下的所有文件信息,類型,字節數,生成時間。 result = ['%-20s%-7s%-10s%-23s' % ('filename', 'type', 'bytes', 'creationtime')] ####信息標題 for f in os.listdir(self.position): type = 'unknown' f_abspath = os.path.join(self.position, f) ####給出文件的絕對路徑,不然程序會找不到文件 if os.path.isdir(f_abspath): type = 'd' elif os.path.isfile(f_abspath): type = 'f' result.append('%-20s%-7s%-10s%-23s' % ( f, type, os.path.getsize(f_abspath), TimeStampToTime(os.path.getctime(f_abspath)))) self.request.send(json.dumps(result).encode('utf-8')) def du(self, *args): '''統計純文件和目錄佔用空間大小,結果小於在OS上使用du -s查詢,因爲有一些(例如'.','..')隱藏文件未包含在內''' totalsize = 0 if os.path.isdir(self.position): dirsize, filesize = 0, 0 for root, dirs, files in os.walk(self.position): for d_item in dirs: ####計算目錄佔用空間,Linux中每個目錄佔用4096bytes,實際上也可以按這個值來相加 if d_item != '': dirsize += os.path.getsize(os.path.join(root, d_item)) for f_item in files: ####計算文件佔用空間 if f_item != '': filesize += os.path.getsize(os.path.join(root, f_item)) totalsize = dirsize + filesize result = 'current directory total sizes: %d' % totalsize else: result = 'Error,%s is not path ,or path does not exist!' % self.position self.request.send(json.dumps(result).encode('utf-8')) return totalsize def cd(self, *args): print(*args) user_homedir = os.path.join(file_dir, self.username) cmd_dict = args[0] error_tag = False '''判斷目錄信息''' if cmd_dict['dir'] == '': self.position = user_homedir elif cmd_dict['dir'] == '.' or cmd_dict['dir'] == '/' or '//' in cmd_dict['dir']: ####'.','/','//','///+'匹配 pass elif cmd_dict['dir'] == '..': if user_homedir != self.position and user_homedir in self.position: ####當前目錄不是家目錄,並且當前目錄是家目錄下的子目錄 self.position = os.path.dirname(self.position) elif '.' not in cmd_dict['dir'] and os.path.isdir( os.path.join(self.position, cmd_dict['dir'])): ####'.' not in cmd_dict['dir'] 防止../..輸入 self.position = os.path.join(self.position, cmd_dict['dir']) else: error_tag = True '''發送結果''' if error_tag: result = 'Error,%s is not path here, or path does not exist!' % cmd_dict['dir'] self.request.send(json.dumps(result).encode('utf-8')) else: self.pwd() def mv(self,*args): print(*args) try: objectname = args[0]['objectname'] dstname = args[0]['dstname'] abs_objectname = os.path.join(self.position,objectname) abs_dstname = os.path.join(self.position, dstname) print(abs_objectname,abs_dstname,os.path.isfile(abs_objectname),os.path.isdir(abs_objectname),os.path.isdir(abs_dstname)) result = '' if os.path.isfile(abs_objectname): if os.path.isdir(abs_dstname) or not os.path.exists(abs_dstname): shutil.move(abs_objectname, abs_dstname) print('moving success') result = 'moving success' elif os.path.isfile(abs_dstname): print('moving cancel, file has been exits') result = 'moving cancel, file has been exits' elif os.path.isdir(abs_objectname): if os.path.isdir(abs_dstname) or not os.path.exists(abs_dstname): shutil.move(abs_objectname, abs_dstname) print('moving success') result = 'moving success' elif os.path.isfile(abs_dstname): print('moving cancel, %s is file' % dstname) result = 'moving cancel, %s is file' % dstname else: print('nothing done') result = 'nothing done' self.request.send(json.dumps(result).encode('utf-8')) except Exception as e: print(e) result = 'moving fail,' + e self.request.send(json.dumps(result).encode('utf-8')) def mkdir(self, *args): ####創建目錄 try: dirname = args[0]['dirname'] if dirname.isalnum(): ####判斷文件是否只有數字和字母 if os.path.exists(os.path.join(self.position, dirname)): result = '%s have existed' % dirname else: os.mkdir(os.path.join(self.position, dirname)) result = '%s created succes' % dirname else: result = 'Illegal character %s, dirname can only by string and num here.' % dirname except TypeError: result = 'please input dirname' self.request.send(json.dumps(result).encode('utf-8')) def rm(self, *args): ####刪除文件 filename = args[0]['filename'] confirm = args[0]['confirm'] file_abspath = os.path.join(self.position, filename) if os.path.isfile(file_abspath): if confirm == True: os.remove(file_abspath) result = '%s have been delete.' % filename else: result = 'Not file deleted' elif os.path.isdir(file_abspath): result = '%s is a dir, plsese using rmdir' % filename else: result = 'File %s not exist!' % filename self.request.send(json.dumps(result).encode('utf-8')) def rmdir(self, *args): ###刪除目錄 dirname = args[0]['dirname'] confirm = args[0]['confirm'] file_abspath = os.path.join(self.position, dirname) if '.' in dirname or '/' in dirname: ####不能跨目錄刪除 result = 'should not rmdir %s this way' % dirname elif os.path.isdir(file_abspath): if confirm == True: shutil.rmtree(file_abspath) result = '%s have been delete.' % dirname else: result = 'Not file deleted' elif os.path.isfile(file_abspath): result = '%s is a file, not directory deleted' % dirname else: result = 'directory %s not exist!' % dirname self.request.send(json.dumps(result).encode('utf-8')) def auth(self): self.data = json.loads(self.request.recv(1024).decode('utf-8')) print(self.data) recv_username = self.data['username'] recv_passwd = self.data['passwd'] query_result = useropr.query_user(recv_username) print(query_result) if query_result == None: self.request.send(b'user does not exits') elif query_result['content']['passwd'] == recv_passwd: self.request.send(b'ok') return query_result ####返回查詢結果 elif query_result['content']['passwd'] != recv_passwd: self.request.send(b'password error') else: self.request.send(b'unknown error') def handle(self): ####處理類,調用以上方法 # self.position = file_dir # print(self.position) auth_tag = False while auth_tag != True: auth_result = self.auth() ####用戶認證,如果通過,返回用戶名,不通過爲None print('the authentication result is:', auth_result) if auth_result != None: self.username = auth_result['content']['username'] self.user_spacesize = auth_result['content']['spacesize'] auth_tag = True print(self.username, self.user_spacesize) user_homedir = os.path.join(file_dir, self.username) if os.path.isdir(user_homedir): self.position = user_homedir ####定錨,用戶家目錄 print(self.position) while True: print('當前連接:', self.client_address) self.data = self.request.recv(1024).strip() print(self.data) logging.info(self.client_address) if len(self.data) == 0: print('客戶端斷開連接') break ####檢查發送來的命令是否爲空 cmd_dict = json.loads(self.data.decode('utf-8')) action = cmd_dict['action'] logging.info(cmd_dict) if hasattr(self, action): func = getattr(self, action) func(cmd_dict) else: print('未支持指令:', action) logging.info('current directory:%s' % self.position) if __name__ == '__main__': ip, port = '0.0.0.0', 9999 server = socketserver.ThreadingTCPServer((ip, port), MyTCPHandler) server.serve_forever()
2、客戶端
client.conf
####用戶端配置文件#### [DEFAULT] logfile = ../log/client.log download_dir= ../temp ####日誌文件位置#### [log] logfile = ../log/client.log ####下載文件存放位置#### [download] download_dir= ../temp
main.py
#!/usr/bin/env python # -*- coding:utf-8 -*- import configparser,os from client import FtpClient if __name__ == '__main__': ftp = FtpClient() ftp.connect('127.0.0.1',9999) auth_tag=False while auth_tag != True: auth_tag=ftp.auth() ftp.interactive()
client.py
#!/usr/bin/env python # -*- coding:utf-8 -*- # filename:client.py import socket, json, os, sys, hashlib, getpass, logging, configparser,time ####讀取配置文件#### base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) config_file = os.path.join(base_dir, 'conf/client.conf') cf = configparser.ConfigParser() cf.read(config_file) ####設定日誌目錄#### if os.path.exists(cf.get('log', 'logfile')): logfile = cf.get('log', 'logfile') else: logfile = os.path.join(base_dir, 'log/client.log') ####設定下載目錄#### if os.path.exists(cf.get('download', 'download_dir')): download_dir = cf.get('download', 'download_dir') else: download_dir = os.path.join(base_dir, 'temp') ####設置日誌格式### logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S', filename=logfile, filemode='a+') def hashmd5(*args): ####用於加密密碼信息 m = hashlib.md5() m.update(str(*args).encode()) return m.hexdigest() def ProcessBar(part, total): ####進度條模塊 if total != 0: i = round(part * 100 / total) sys.stdout.write( '[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r') sys.stdout.flush() class FtpClient(object): def __init__(self): self.client = socket.socket() def connect(self, ip, port): self.client.connect((ip, port)) def exec_linux_cmd(self, dict): ####用於後面調用linux命令 logging.info(dict) ####將發送給服務端的命令保存到日誌中 self.client.send(json.dumps(dict).encode('utf-8')) server_response = json.loads(self.client.recv(4096).decode('utf-8')) if isinstance(server_response, list): for i in server_response: print(i) else: print(server_response) def help(self): info = ''' 僅支持如下命令: ls du pwd cd dirname/cd ./cd .. mkdir dirname rm filename rmdir dirname put filename get filename mv filename/dirname filename/dirname newput filename (後續增加的新功能,支持斷點續傳) newget filename (後續增加的新功能,支持斷點續傳) ''' print(info) def interactive(self): while True: self.pwd() ####打印當前目錄位置 cmd = input('>>>:').strip() if len(cmd) == 0: continue action = cmd.split()[0] if hasattr(self, action): func = getattr(self, action) func(cmd) else: self.help() def put(self, *args): ####上傳文件 cmd = args[0].split() override = cmd[-1] ####override:是否覆蓋參數,放在最後一位 if override != 'True': override = 'False' # print(cmd,override) if len(cmd) > 1: filename = cmd[1] if os.path.isfile(filename): filesize = os.path.getsize(filename) filemd5 = os.popen('md5sum %s' % filename).read().split()[ 0] ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低 msg = { 'action': 'put', 'filename': filename, 'filesize': filesize, 'filemd5': filemd5, 'override': override ####True ,or False } logging.info(msg) self.client.send(json.dumps(msg).encode('utf-8')) server_response = self.client.recv(1024) ####等待服務器確認信號,防止粘包 logging.info(server_response) if server_response == b'file have exits, do nothing!': override_tag = input('文件已存在,要覆蓋文件請輸入yes >>>:') if override_tag == 'yes': self.put('put %s True' % filename) else: print('文件未上傳') else: self.client.send(b'client have ready to send') ####發送確認信號,防止粘包,代號:P01 server_response = self.client.recv(1024).decode('utf-8') print(server_response) ####注意:用於打印服務器反饋信息,例如磁盤空間不足信息,不能取消 if server_response == 'begin': fk = open(filename, 'rb') send_size = 0 for line in fk: # print(len(line)) send_size += len(line) self.client.send(line) ProcessBar(send_size, filesize) else: print('\r\n', '文件傳輸完畢') fk.close() server_response = self.client.recv(1024).decode('utf-8') print(server_response) else: print('文件不存在') else: print('請輸入文件名') def get(self, *args): ####下載文件 cmd = args[0].split() # print(args[0],cmd) if len(cmd) > 1: filename = cmd[1] filepath = os.path.join(download_dir, filename) if os.path.isfile(filepath): ####判斷下載目錄是否已存在同名文件 override_tag = input('文件已存在,要覆蓋文件請輸入yes >>>:').strip() if override_tag == 'yes': msg = { 'action': 'get', 'filename': filename, 'filesize': 0, 'filemd5': '', 'override': 'True' } logging.info(msg) self.client.send(json.dumps(msg).encode('utf-8')) server_response = json.loads(self.client.recv(1024).decode('utf-8')) logging.info(server_response) if server_response == 'Filenotfound': print('File no found!') else: print(server_response) self.client.send(b'client have been ready to receive') ####發送信號,防止粘包 filesize = server_response['filesize'] filemd5 = server_response['filemd5'] receive_size = 0 fk = open(filepath, 'wb') while filesize > receive_size: if filesize - receive_size > 1024: size = 1024 else: size = filesize - receive_size data = self.client.recv(size) fk.write(data) receive_size += len(data) # print(receive_size, len(data)) ####打印數據流情況 ProcessBar(receive_size, filesize) ####打印進度條 fk.close() receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0] print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5) if receive_filemd5 == filemd5: print('文件接收完成!') else: print('Error,文件接收異常!') else: print('下載取消') else: print('請輸入文件名') def newput(self, *args): ####上傳文件,具有斷點續傳功能 cmd = args[0].split() override = cmd[-1] ####override:是否覆蓋參數,放在最後一位 if override != 'True': override = 'False' # print(cmd,override) if len(cmd) > 1: filename = cmd[1] if os.path.isfile(filename): filesize = os.path.getsize(filename) filemd5 = os.popen('md5sum %s' % filename).read().split()[ 0] ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低 msg = { 'action': 'newput', 'filename': filename, 'filesize': filesize, 'filemd5': filemd5, 'override': override ####True ,or False } logging.info(msg) self.client.send(json.dumps(msg).encode('utf-8')) server_response = self.client.recv(1024) ####等待服務器確認信號,防止粘包 logging.info(server_response) print(server_response) if server_response == b'file have exits, and is a directory, do nothing!': print('文件已存在且爲目錄,請先修改文件或目錄名字,然後再上傳') elif server_response == b'file have exits, do nothing!': override_tag = input('文件已存在,要覆蓋文件請輸入yes,要斷點續傳請輸入r >>>:').strip() if override_tag == 'yes': self.client.send(b'no need to do anything') ####服務端在等待是否續傳的信號,發送給服務端確認(功能號:s1) time.sleep(0.5) ####防止黏貼 self.put('put %s True' % filename) elif override_tag == 'r': self.client.send(b'ready to resume from break point') ####服務端在等待是否續傳的信號,發送給服務端確認(功能號:s1) self.client.recv(1024) ####這邊接收服務端發送過來的du信息,不顯示,直接丟棄 server_response = json.loads((self.client.recv(1024)).decode()) print(server_response) if server_response['state'] == True: exits_file_size = server_response['position'] fk = open(filename, 'rb') fk.seek(exits_file_size,0) send_size = exits_file_size for line in fk: # print(len(line)) send_size += len(line) self.client.send(line) ProcessBar(send_size, filesize) else: print('\r\n', '文件傳輸完畢') fk.close() server_response = self.client.recv(1024).decode('utf-8') print(server_response) else: print(server_response['content']) else: self.client.send(b'no need to do anything') ####服務端在等待是否續傳的信號,發送給服務端確認(功能號:s1) print('文件未上傳') else: self.client.send(b'client have ready to send') ####發送確認信號,防止粘包,代號:P01 server_response = self.client.recv(1024).decode('utf-8') print(server_response) ####注意:用於打印服務器反饋信息,例如磁盤空間不足信息,不能取消 if server_response == 'begin': fk = open(filename, 'rb') send_size = 0 for line in fk: # print(len(line)) send_size += len(line) self.client.send(line) ProcessBar(send_size, filesize) else: print('\r\n', '文件傳輸完畢') fk.close() server_response = self.client.recv(1024).decode('utf-8') print(server_response) else: print('文件不存在') else: print('請輸入文件名') def newget(self, *args): ####下載文件,具有斷點續傳功能 cmd = args[0].split() # print(args[0],cmd) if len(cmd) > 1: filename = cmd[1] filepath = os.path.join(download_dir, filename) transfer_tag = True ####傳輸控制信號,默認True爲下載 resume_tag = False ####斷點續傳信號 local_filesize = 0 ####本地文件大小,後面判斷是否有同名文件使用 if os.path.isfile(filepath): ####判斷下載目錄是否已存在同名文件 override_tag = input('文件已存在,要覆蓋文件請輸入yes,要斷點續傳請輸入r >>>:').strip() if override_tag == 'yes': pass elif override_tag == 'r': local_filesize = os.path.getsize(filepath) resume_tag = True else: print('下載取消') transfer_tag = False if transfer_tag == True: msg = { 'action': 'newget', 'filename': filename, 'filesize': local_filesize, 'filemd5': '', 'override': 'True' } logging.info(msg) self.client.send(json.dumps(msg).encode('utf-8')) server_response = json.loads(self.client.recv(1024).decode('utf-8')) logging.info(server_response) if server_response == 'Filenotfound': print('File no found!') else: print(server_response) self.client.send(b'client have been ready to receive') ####發送信號,防止粘包 filesize = server_response['filesize'] filemd5 = server_response['filemd5'] receive_size = local_filesize if resume_tag == True: fk = open(filepath, 'ab+') ####用於斷點續傳 else: fk = open(filepath, 'wb+') ####用於覆蓋或者新生成文件 while filesize > receive_size: if filesize - receive_size > 1024: size = 1024 else: size = filesize - receive_size data = self.client.recv(size) fk.write(data) receive_size += len(data) # print(receive_size, len(data)) ####打印數據流情況 ProcessBar(receive_size, filesize) ####打印進度條 fk.close() receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0] print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5) if receive_filemd5 == filemd5: print('文件接收完成!') else: print('Error,文件接收異常!') else: print('請輸入文件名') def pwd(self, *args): ####查看用戶目錄 msg = { 'action': 'pwd', } self.exec_linux_cmd(msg) def ls(self, *args): ####查看文件信息 msg = { 'action': 'ls', } self.exec_linux_cmd(msg) def du(self, *args): ####查看當前目錄大小 msg = { 'action': 'du', } self.exec_linux_cmd(msg) def cd(self, *args): ####切換目錄 try: ####如果是直接輸入cd,dirname='' dirname = args[0].split()[1] except IndexError: dirname = '' msg = { 'action': 'cd', 'dir': dirname } self.exec_linux_cmd(msg) def mkdir(self, *args): ####生成目錄 try: ####如果是直接輸入rm,跳出 dirname = args[0].split()[1] msg = { 'action': 'mkdir', 'dirname': dirname, } self.exec_linux_cmd(msg) except IndexError: print('Not dirname input, do nothing.') pass def rm(self, *args): ####刪除文件 try: ####如果是直接輸入rm,跳出 filename = args[0].split()[1] msg = { 'action': 'rm', 'filename': filename, 'confirm': True ####確認是否直接刪除標誌 } self.exec_linux_cmd(msg) except IndexError: print('Not filename input, do nothing.') pass def rmdir(self, *args): try: ####如果是直接輸入rm,跳出 dirname = args[0].split()[1] msg = { 'action': 'rmdir', 'dirname': dirname, 'confirm': True ####確認是否直接刪除標誌 } self.exec_linux_cmd(msg) except IndexError: print('Not dirname input, do nothing.') pass def mv(self,*args): ####實現功能:移動文件,移動目錄,文件重命名,目錄重命名 try: objectname = args[0].split()[1] dstname = args[0].split()[2] msg = { 'action': 'mv', 'objectname': objectname, 'dstname': dstname } print(msg) self.exec_linux_cmd(msg) except Exception as e: print(e) pass def auth(self): user_name = input('請輸入用戶名>>>:').strip() passwd = getpass.getpass('請輸入密碼>>>:').strip() ####在linux上輸入密碼不顯示 msg = { 'username': user_name, 'passwd': hashmd5(passwd) } self.client.send(json.dumps(msg).encode('utf-8')) server_response = self.client.recv(1024).decode('utf-8') if server_response == 'ok': print('認證通過!') return True else: print(server_response) return False if __name__ == '__main__': ftp = FtpClient() ftp.connect('127.0.0.1', 9999) auth_tag = False while auth_tag != True: auth_tag = ftp.auth() ftp.interactive()
注:配置文件中的中文註釋,可能會使程序在啓動時報出ASCII decode error,可以去掉。
另外服務端最好在Linux下啓動,我在windows下啓動日誌輸出模塊會報錯。