my_ftp
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import hmac
import json
import struct
import hashlib
import socketserver
from core.log import log
class MyServer(socketserver.BaseRequestHandler):
def pro_recv(self):
num = self.request.recv(4)
num = struct.unpack('i', num)[0]
str_dic = self.request.recv(num).decode('utf-8')
dic = json.loads(str_dic)
return dic
def pro_send(self, dic, pro=True):
bytes_dic = json.dumps(dic).encode('utf-8')
if pro:
len_bytes = struct.pack('i', len(bytes_dic))
self.request.send(len_bytes)
self.request.send(bytes_dic)
def get_md5(self, username=0, password=0):
md5 = hashlib.md5(username.encode('utf-8'))
md5.update(password.encode('utf-8'))
return md5.hexdigest()
@staticmethod
def bytes2human(num):
symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
prefix = {}
for i, s in enumerate(symbols):
prefix[s] = 1 << (i + 1) * 10
for s in reversed(symbols):
if num >= prefix[s]:
value = float(num) / prefix[s]
return '%.1f%s' % (value, s)
return '%sB' % num
def makencrypt(secret_key):
def outter(func):
def inner(self, *args, **kwargs):
func_ret = func(self, *args, **kwargs)
rand = os.urandom(32)
self.request.send(rand)
h_mac = hmac.new(secret_key.encode('utf-8'), rand)
res = h_mac.digest()
ret = self.request.recv(1024)
if ret == res:
log.logger.info('验证是合法的客户端')
return func_ret
else:
log.logger.info('验证不是合法的客户端')
self.request.close()
return False
return inner
return outter
def checkquota(func):
def wrapper(self, *args, **kwargs):
ret = func(self, *args, **kwargs)
usedsize = self.getusedsize(args[1]['userhome'])
quotasize = args[1]['userquota']
human_used = self.bytes2human(usedsize)
human_total = self.bytes2human(quotasize)
dic = {'used':usedsize,'total':quotasize}
self.pro_send(dic)
if usedsize > quotasize:
log.logger.warning(f'已经超过用户配额:使用{human_used},总共{human_total}')
else:
log.logger.info(f'使用{human_used},总共{human_total}')
return ret
return wrapper
@staticmethod
def getusedsize(path, usedsize=0):
l = [path]
while l:
userhome = l.pop()
lst = os.listdir(userhome)
for name in lst:
son_path = os.path.join(userhome, name)
if os.path.isfile(son_path):
usedsize += os.path.getsize(son_path)
else:
l.append(son_path)
return usedsize
@staticmethod
def sortdic(dic):
for k, v in dic.items():
v.update({'username': k})
return v
@staticmethod
def getfiles(path):
dirlist = list()
for file in os.listdir(path):
if file.startswith('_'):
pass
if file.startswith('.'):
pass
elif os.path.isfile(os.path.join(path, file)):
dirlist.append(('f', file))
elif os.path.isdir(os.path.join(path, file)):
dirlist.append(('d', file))
else:
pass
return dirlist
@makencrypt(secret_key='alexsb')
def login(self, userdic, userfile='userinfo.json', userhome='home'):
log.logger.info(f"用户{userdic['username']}开始登陆")
with open(userfile, encoding='utf-8') as f:
localdic = json.load(f)
if localdic.get(userdic['username']):
if localdic[userdic['username']]['password'] == self.get_md5(
userdic['username'], userdic['password']):
log.logger.info(f"用户{userdic['username']}登陆成功")
msg = f"用户{userdic['username']}登陆成功"
userdic['message'] = msg
userdic['actflag'] = True
self.pro_send(userdic)
return {userdic['username']: localdic[userdic['username']]}
else:
log.logger.warning(f"用户{userdic['username']}登陆的用户名或密码错误,无法登陆")
msg = f"用户{userdic['username']}登陆的用户名或密码错误,无法登陆。"
userdic['message'] = msg
userdic['actflag'] = False
self.pro_send(userdic)
return False
else:
log.logger.warning(f"用户{userdic['username']}不存在,无法登陆")
msg = f"用户{userdic['username']}不存在,无法登陆。"
userdic['message'] = msg
userdic['actflag'] = False
self.pro_send(userdic)
return False
@makencrypt(secret_key='alexsb')
def region(self, userdic, userfile='userinfo.json', userhome='home'):
__default_userquota = 10485760 # 10M
log.logger.info(f"用户{userdic['username']}开始注册")
userhome = os.path.join(userhome, userdic['username'])
if not os.path.exists(userhome):
os.makedirs(userhome)
# check local userinfo
with open(userfile, mode='r', encoding=('utf-8')) as f:
if os.path.getsize(userfile) == 0:
localdic = dict()
else:
localdic = json.load(f)
# check new userinfo
if localdic.get(userdic['username']):
log.logger.warning(f"用户{userdic['username']}已存在,无法注册")
msg = f"用户{userdic['username']}已存在,无法注册。"
userdic['message'] = msg,
userdic['actflag'] = False,
self.pro_send(userdic)
return False
else:
with open(userfile, mode='w', encoding='utf-8') as f:
log.logger.info(f"用户{userdic['username']}注册成功")
msg = f"用户{userdic['username']}注册成功。"
new_userinfo = {
userdic['username']: {
'password': self.get_md5(userdic['username'], userdic['password']),
'userquota': __default_userquota,
'userhome': userhome,
'userdir': userhome,
}}
new_sendinfo = dict()
new_sendinfo['actflag'] = True
new_sendinfo['message'] = msg
localdic.update(new_userinfo)
json.dump(localdic,f,sort_keys=True,indent=2,separators=(',',':'),
ensure_ascii=False)
self.pro_send(new_sendinfo)
return new_userinfo
@checkquota
def upload(self, fileinfo=dict(), userdic=dict()):
filepath = os.path.join(userdic['userdir'], fileinfo['filename'])
break_point = False
if os.path.exists(filepath):
break_point = True
received_size = os.path.getsize(filepath)
if received_size == fileinfo['filesize']:
self.pro_send({'continue':'hasfile'})
return True
else:
self.pro_send({'continue':'send','received':received_size})
else:
self.pro_send({'continue': False})
with open(filepath, 'ab') as f:
md5 = hashlib.md5()
filesize = fileinfo['filesize']
if break_point: filesize -= received_size
while filesize:
content = self.request.recv(2048)
if content == b'': break
md5.update(content)
f.write(content)
filesize -= len(content)
#check md5num
self.pro_send({'md5num': md5.hexdigest()})
client_md5 = self.pro_recv()
if client_md5['md5num'] == md5.hexdigest():
log.logger.info(f"文件{fileinfo['filename']}上传并校验完成")
return True
else:
log.logger.warning(f"文件{fileinfo['filename']}上传完成但,校验失败")
return False
@checkquota
def download(self, fileinfo=dict(), userdic=dict()):
userdir = userdic['userdir']
listdir = list()
for file in os.listdir(userdir):
if file.startswith('_'):
pass
if file.startswith('.'):
pass
elif os.path.isfile(os.path.join(userdir, file)):
listdir.append(('f', file))
elif os.path.isdir(os.path.join(userdir, file)):
listdir.append(('d', file))
else:
pass
fileinfo['downfiles'] = listdir
self.pro_send(fileinfo)
fileinfo = self.pro_recv()
filepath = os.path.join(userdir, fileinfo['downfiles'])
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
fileinfo['filename'] = filename
fileinfo['filesize'] = filesize
self.pro_send(fileinfo)
send_stat = self.pro_recv()
break_point = False
if send_stat['continue'] == 'send':
break_point = True
received_size = send_stat['received']
filesize -= received_size
elif send_stat['continue'] == 'hasfile':
return True
with open(filepath, 'rb') as f:
if break_point: f.seek(received_size)
md5 = hashlib.md5()
while filesize > 2048:
content = f.read(2048)
self.request.send(content)
filesize -= 2048
md5.update(content)
else:
content = f.read()
self.request.send(content)
md5.update(content)
# check md5num
file_name = fileinfo['filename']
log.logger.info(f"文件{file_name}开始下载")
fileinfo = self.pro_recv()
if fileinfo['md5num'] == md5.hexdigest():
log.logger.info(f"文件{file_name}下载并校验完成")
self.pro_send({'md5check': True})
else:
log.logger.warning(f"文件{file_name}下载完成,但校验失败")
self.pro_send({'md5check': False})
def changedir(self, fileinfo=dict(), userdic=dict()):
fileinfo = fileinfo
userdir = userdic['userhome']
self.getfiles(userdir)
dic = {'dir':'/','files':self.getfiles(userdir)}
self.pro_send(dic)
nowdir = userdir
while True:
act = self.pro_recv()
if act['act'] == 'q':
log.logger.info(f"用户{userdic['username']}登出服务器")
break
elif act['act'] == 'ls':
dic['files'] = self.getfiles(nowdir)
self.pro_send(dic)
elif act['act'] == 'cd':
newdir = os.path.dirname(nowdir)
changedir = newdir.split(userdir)[1]
if changedir == '': changedir = '/'
nowdir = newdir
if nowdir == userdir:
dic = {'dir':'/','files':self.getfiles(userdir)}
self.pro_send(dic)
else:
dic = {'dir':changedir,'files': self.getfiles(nowdir)}
self.pro_send(dic)
elif act['act'] == 'mkdir':
os.makedirs(os.path.join(nowdir,act['dir_name']))
dic['files'] = self.getfiles(nowdir)
self.pro_send(dic)
log.logger.info(f"用户{userdic['username']}创建了{act['dir_name']}文件夹")
elif act['act'] == 'rm':
rmobj = act['rm_obj']
path = os.path.join(nowdir,rmobj)
if os.path.isdir(path):
try:
os.rmdir(path)
except OSError:
pass
else:
os.remove(path)
dic['files'] = self.getfiles(nowdir)
self.pro_send(dic)
if rmobj not in dic['files']:
log.logger.info(f"用户{userdic['username']}删除了{rmobj}")
elif act['act'] == 'upload':
userdic['userdir'] = nowdir
fileinfo=self.pro_recv()
self.upload(fileinfo,userdic)
elif act['act'] == 'download':
userdic['userdir'] = nowdir
fileinfo = self.pro_recv()
self.download(fileinfo,userdic)
else:
newdir = os.path.join(nowdir, act['act'][1])
nowdir = newdir
changedir = newdir.split(userdir)[1]
dic = {'dir': changedir, 'files': self.getfiles(nowdir)}
self.pro_send(dic)
Client
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import hmac
import json
import time
import socket
import struct
import hashlib
class FtpClient(object):
def __init__(self, addr, port):
self.addr = addr
self.port = port
self.sock = socket.socket()
self.sock.connect((self.addr, self.port))
self.sock.settimeout(10)
welcom = self.sock.recv(1024).decode('utf-8')
print(welcom if welcom != '' else '服务器连接失败。')
def pro_recv(self):
num = self.sock.recv(4)
num = struct.unpack('i', num)[0]
str_dic = self.sock.recv(num).decode('utf-8')
dic = json.loads(str_dic)
return dic
def pro_send(self, dic, pro=True):
bytes_dic = json.dumps(dic).encode('utf-8')
if pro:
len_bytes = struct.pack('i', len(bytes_dic))
self.sock.send(len_bytes)
self.sock.send(bytes_dic)
def makencrypt(secret_key):
def outter(func):
def inner(self, *args, **kwargs):
func_ret = func(self, *args, **kwargs)
rand = self.sock.recv(32)
h_mac = hmac.new(secret_key.encode('utf-8'), rand)
res = h_mac.digest()
self.sock.send(res)
return func_ret
return inner
return outter
@staticmethod
def bytes2human(num):
symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
prefix = {}
for i, s in enumerate(symbols):
prefix[s] = 1 << (i + 1) * 10
for s in reversed(symbols):
if num >= prefix[s]:
value = float(num) / prefix[s]
return '%.1f%s' % (value, s)
return '%sB' % num
def getquota(func):
def wrapper(self,*args,**kwargs):
ret = func(self,*args,**kwargs)
userquota = self.pro_recv()
human_used = self.bytes2human(userquota['used'])
human_total = self.bytes2human(userquota['total'])
if userquota['used'] > userquota['total']:
print(f'用户已经超过了配额,使用了{human_used},总共{human_total},请尽快充值账号')
else:
print(f'用户已经使用了{human_used},总共{human_total}')
return ret
return wrapper
@staticmethod
def progress(percent, width=50):
'''进度打印功能'''
if percent >= 100:
percent = 100
show_str = ('[%%-%ds]' % width) % (int(width *
percent / 100) * "#") # 字符串拼接的嵌套使用
print('\r%s %d%%' % (show_str, percent), end='')
@makencrypt(secret_key='alexsb')
def login(self):
print('开始登陆:')
user = input('用户名>>').strip()
pswd = input('密码>>>>').strip()
self.pro_send(
{'username': user, 'password': pswd, 'action': 'login', })
actstat = self.pro_recv()
print(actstat['message'])
return actstat['actflag']
@makencrypt(secret_key='alexsb')
def region(self):
print('开始注册:')
user = input('用户名>>').strip()
pswd = input('密码>>>>').strip()
self.pro_send(
{'username': user, 'password': pswd, 'action': 'region', })
actstat = self.pro_recv()
print(actstat['message'])
return actstat['actflag']
def logout(self):
self.sock.close()
exit()
@getquota
def upload(self, recv_size=0):
file_name = input('输入你要上传的文件>>>').strip()
if os.path.isfile(file_name):
filename = os.path.basename(file_name)
filesize = os.path.getsize(file_name)
fileinfo = {
'action': 'upload',
'filename': filename,
'filesize': filesize,
}
self.pro_send(fileinfo)
send_stat = self.pro_recv()
break_point = False
if send_stat['continue'] == 'send':
break_point = True
received_size = send_stat['received']
filesize -= received_size
recv_size += received_size
elif send_stat['continue'] == 'hasfile':
return True
with open(file_name, 'rb') as f:
if break_point: f.seek(received_size)
md5 = hashlib.md5()
while filesize > 2048:
content = f.read(2048)
self.sock.send(content)
md5.update(content)
filesize -= len(content)
recv_size += len(content)
recv_per = int(100 * recv_size / fileinfo['filesize'])
self.progress(recv_per, width=35)
else:
content = f.read()
md5.update(content)
self.sock.send(content)
recv_size += len(content)
recv_per = int(100 * recv_size / fileinfo['filesize'])
self.progress(recv_per, width=35)
# check md5num
server_md5 = self.pro_recv()['md5num']
time.sleep(1)
self.pro_send({'md5num':md5.hexdigest()})
if server_md5 == md5.hexdigest():
print(f" 文件{fileinfo['filename']}上传并校验完成。")
return True
else:
print(f" 文件{fileinfo['filename']}上传完成但,校验失败。")
return False
@getquota
def download(self, dir='download', recvsize=0):
if not os.path.exists(dir):
os.makedirs(dir)
fileinfo = {
'action': 'download',
}
self.pro_send(fileinfo)
dic = self.pro_recv()
print('服务器文件:')
for index, file in enumerate(dic['downfiles'], 1):
print(f'{index} {file[0]} {file[1]}')
while True:
filename = input('输入你想下载的文件名>>>').strip()
if filename.isdigit():
if int(filename) in range(len(dic['downfiles']) + 1):
filename = dic['downfiles'][int(filename) - 1]
if filename[0] == 'd':
print('无法下载文件夹,请重新选择文件。')
continue
break
else:
print('请输入正确的序号.')
continue
else:
filename = filename
break
dic['downfiles'] = filename[1]
self.pro_send(dic)
dic = self.pro_recv()
filepath = os.path.join(dir, dic['filename'])
filesize = dic['filesize']
if os.path.exists(filepath):
received_size = os.path.getsize(filepath)
if received_size == filesize:
self.pro_send({'continue':'hasfile'})
return True
else:
self.pro_send({'continue': 'send', 'received': received_size})
filesize -= received_size
recvsize += received_size
else:
self.pro_send({'continue': False})
with open(filepath, 'ab') as f:
md5 = hashlib.md5()
while filesize:
content = self.sock.recv(2048)
f.write(content)
filesize -= len(content)
recvsize += len(content)
recv_per = int(100 * recvsize / dic['filesize']) # 接收的比例
self.progress(recv_per, width=35) # 调用进度条函数,进度条的宽度默认设置为30
md5.update(content)
# check md5num
fileinfo['md5num'] = md5.hexdigest()
self.pro_send(fileinfo)
ret = self.pro_recv()['md5check']
if ret:
print(f" 文件{dic['filename']}下载完成并校验结束")
return True
else:
print(f" 文件{dic['filename']}校验出错")
return False
def changedir(self):
fileinfo = {
'action': 'changedir',
}
self.pro_send(fileinfo)
server_dir = self.pro_recv()
def showfile(server_dir):
print('服务器目录'.center(20, '='))
if server_dir['files'] == []:
print('\033[1;31m 当前目录下没有文件 \033[0m!')
else:
for index, files in enumerate(server_dir['files'], 1):
print(f'索引:{index} 类型:{files[0]} 名称:{files[1]}')
print(f"\033[7;22m当前目录为{server_dir['dir']}\033[0m")
print(''.center(20,'-'))
showfile(server_dir)
while True:
act = input('你要干哈?(cd/mkdir/ls/rm/upload/download/q)>').strip()
if act.isdigit():
if int(act) not in range(len(server_dir['files']) + 1):
print('\033[1;31m 请输入正确的序号 \033[0m!')
showfile(server_dir)
continue
elif server_dir['files'][int(act) - 1][0] == 'f':
print('\033[1;31m 文件不能切换 \033[0m!')
showfile(server_dir)
continue
else:
self.pro_send({'act': server_dir['files'][int(act) - 1]})
server_dir = self.pro_recv()
showfile(server_dir)
elif act == 'cd':
if server_dir['dir'] == '/':
print('\033[1;31m 当前已经为根目录无法进行切换 \033[0m!')
showfile(server_dir)
else:
dic = {'act':'cd'}
self.pro_send(dic)
server_dir = self.pro_recv()
showfile(server_dir)
elif act == 'mkdir':
dic = {'act':'mkdir'}
dir_name = input('你要创建的文件夹名称?').strip()
dic['dir_name'] = dir_name
if server_dir['files'] == []:
server_files = []
else:
server_files = [obj[1] for obj in server_dir['files']]
if dir_name in server_files:
print('\033[1;31m 文件夹创建失败,存在同名文件夹或文件 \033[0m!')
else:
self.pro_send(dic)
print(f'文件夹{dir_name}创建完成')
server_dir=self.pro_recv()
elif act == 'rm':
dic = {'act':'rm'}
rmobj = input('请输入你要删除的文件或空文件夹?').strip()
if server_dir['files'] == []:
server_files = []
print(f"{rmobj}不存在无法删除")
self.pro_send({'act':'q'})
return False
else:
server_files = [obj[1] for obj in server_dir['files']]
if rmobj not in server_files:
print(f"{rmobj}不存在无法删除")
self.pro_send({'act':'q'})
return False
dic['rm_obj'] = rmobj
self.pro_send(dic)
server_dir = self.pro_recv()
if server_dir['files'] == []:
server_files = []
else:
server_files = [obj[1] for obj in server_dir['files']]
if rmobj not in server_files:
print(f'\033[1;31m{rmobj}删除完成\033[0m!')
else:
print(f'\033[1;31m{rmobj}无法删除,不是空文件夹\033[0m!')
elif act == 'upload':
dic = {'act':'upload'}
self.pro_send(dic)
self.upload()
elif act == 'download':
dic = {'act':'download'}
self.pro_send(dic)
self.download()
elif act == 'ls':
dic = {'act':'ls'}
self.pro_send(dic)
time.sleep(1)
server_dir = self.pro_recv()
showfile(server_dir)
elif act == 'q':
self.pro_send({'act': 'q'})
break
else:
print('\033[1;31m请输入文件夹序号\033[0m!')
continue
def run(self):
print('1.登陆\n2.注册\n3.退出')
act = input('>>>').strip()
act_dic = {
'1': 'login', '登陆': 'login', 'login': 'login',
'2': 'region', '注册': 'region', 'region': 'region',
'3': 'logout', '退出': 'logout',
}
while True:
if act.isdigit():
if act in act_dic.keys():
if hasattr(self, act_dic[act]):
ret = getattr(self, act_dic[act])()
break
else:
continue
else:
continue
if ret:
try:
menudic = self.pro_recv()
except Exception as e:
self.sock.close()
exit('远程终端出错:%s' % e)
print(menudic['welcom'])
del menudic['welcom']
# for index, menu in enumerate(menudic, 1):
# print(index, menudic[menu])
# act = input('>>>').strip()
act = 'changedir'
act_dic = {
'1': 'upload', '上传': 'upload', 'U': 'upload', 'u': 'upload', 'upload': 'upload',
'2': 'download', '下载': 'download', 'D': 'download', 'd': 'download', 'download': 'download',
'3': 'changedir', '切换': 'changedir', '切换目录': 'changedir', 'c': 'changedir','changedir':'changedir',
}
if hasattr(self, act_dic[act]):
ret = getattr(self, act_dic[act])()
if ret:
print(act_dic[act]+'-done.')
self.sock.close()
if __name__ == '__main__':
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.join(BASE_DIR,'SocketServer'))
# print(sys.path)
from conf import settings
client = FtpClient('127.0.0.1', settings.PORT)
client.run()