AbelFile
AbelFile只在linux机器上以python3运行。
代码与报告(包括README.md文件的渲染阅读)可以在我的gitdub上找到备份托管:https://github.com/yangminz/AbelFile
通过以下命令可以使用:
cd ~ git clone https://github.com/yangminz/AbelFile.git cd AbelFile sudo bash abelEnv.txt # terminal 1 cd AbelFile/server python3 run.py # terminal 2 cd AbelFile/client python3 run.py
简介
AbelFile是非常简单的类FTP的project与协议。在实现AbelFile时,因为我想增加对服务器的控制功能,在client上直接使用命令执行server的命令,因此不宜使用GUI。虽然用GUI亦无不可,然而终有损韵味。另外,AbelFile有批量执行命令的功能,所以使用GUI也不怎么好。另外,我写AbelFile的初衷是实现一个外观上颇像git或ftp的project,而它们最初都是直接在terminal上运行的。基于上述种种原因,总之没有GUI。
AbelFile的名称来自于伟大的挪威数学家Niels Henrik Abel,他与同时代的法国天才数学家Évariste Galois各自做出了划时代的贡献,为数学开创了抽象代数。为了纪念他,这份project的名称为AbelFile,虽然project和Abel与群论并没有任何关系。
组织
整个project的文件与相应的功能如下:
AbelFile │ abelbash.txt # 批量处理命令 │ abelEnv.txt # 安装环境命令 │ hello.py # 远程执行脚本样例 │ README.md # README文件,以本报告为准 │ report.html # 本报告 │ ├─client # 客户端 │ basic_ops.py # 基本命令,UI中与socket无关的命令,bash, help, exit │ client_ops.py # 通信命令,cd, ls, push, pull, server │ config.json # 配置文件,包括主机地址与控制、数据端口 │ errordict.py # 错误信息文件,处理用户UI的错误 │ help.txt # help文件,可以通过help命令在终端打印 │ protocol.py # 协议脚本,用来设置与读取协议内容 │ run.py # 执行脚本,用户UI输入并调用命令脚本中的函数 │ tools.py # 工具脚本,包括AES加密、正则匹配字符串、zip压缩、打印进度条 │ ├─HOSTclient # 客户端虚拟主机 │ │ bing.jpg │ │ file.txt │ │ │ ├─folder0 │ │ file.txt │ │ │ └─folder1 │ │ bing.jpg │ │ file.txt │ │ │ └─subfolder │ file.txt │ ├─HOSTserver # 服务器虚拟主机 │ placeholder.txt │ ├─img # README.md与report.html用到的图 │ server.png │ tdreading.png │ └─server # 服务器 │ config.json # 配置文件,包括主机地址与控制、数据端口 │ protocol.py # 协议脚本,用来设置与读取协议内容 │ run.py # 执行脚本,开辟线程,与client的通信命令文件中的函数 │ server_ops.py # 与用户输入相应的命令 │ tools.py # 工具脚本,包括AES加密、正则匹配字符串、zip压缩、打印进度条 │ └─database # 数据库 clients.db # 储存有用户名、密码的简单数据库,用于用户登录 create.py # 创造clients.db的脚本 sql.py # 查询用户名与密码
其中主要的代码./server和./client两个目录下,./server/database目录存放用户数据库,用作用户登录的确认。HOSTserver和HOSTclient两个目录作为模拟主机,上面是用来通信的文件。在AbelFile主目录下,abelbash.txt是批量运行的命令文件,abelEnv.txt是运行AbelFile所必需的环境,可以通过:
sudo bash abelEnv.txt
以完成安装。
协议
协议非常简单,用一个8位比特串在server和client之间传递控制命令。协议的使用在./server/protocol.py和./client/protocol.py中,这两个文件内容相同,里面只有一个协议类CTRLPTC,它的成员函数控制对协议的修改与读取。
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
操作符类型 | 覆写 | 终止 | 目录/文件 | ||||
0/1 | 0/1 | 0/1 | 0/1 | 0/1 | 0/1 | 0/1 | 0/1 |
四位操作符表示client请求server执行的操作类型;覆写表示是否要覆盖现有的文件/目录;终止是client希望server退出当前正在执行的命令,这一位专门是为了远程控制功能存在的;目录/文件类型表明当前传输的是文件还是目录:’00’表示为空,也就是目录/文件不存在,’01’表示传输对象是文件类型,’10’表示传输对象是目录类型。
AbelFile一共有两个端口:控制端口CTRL_PORT,数据端口DATA_PORT,DATA_PORT传输的数据基本都是经过AES加密的,client和server在各自的确认文件config.json中指定。上述的8位字符串只在CTRL_PORT中传输,附带的信息则在DATA_PORT中传输,例如地址、文件内容等。
运行
启动
先后通过执行python3脚本开始启动:
# terminal 1 cd AbelFile/server python3 run.py # terminal 2 cd AbelFile/client python3 run.py
启动时双方都从json文件中读取配置好的主机地址和控制、数据端口号。
多线程循环
server随后会进入一个线程的循环,这是为了方便多线程处理:
def main(): while True: ctrl_conn, ctrl_addr = ctrl_skt.accept() data_conn, data_addr = data_skt.accept() #while True: t = threading.Thread(target=process_thread, args=(ctrl_conn, ctrl_addr, data_conn, data_addr)) t.start()
多线程处理时,会将相关的变量作为多线程的局部变量threading.local()保存,这样每个线程都有自己的局部变量,就不会引起混乱,例如:
可以看到,在这里,abel先cd到/home/yangminz/AbelFile/server/database,而newton当前在/home/yangminz/AbelFile/server。newton输入cdup时,如果和abel的目录混淆,会切换到/home/yangminz/AbelFile/server,但实际上newton切换到了/home/yangminz/AbelFile,这是每个线程使用自己私有变量的特点,对于多用户的情况下也是非常必要的。
数据库确认
进入线程以后,server会等待client的连接。此时用户会被要求输入用户名和密码。我在数据库中设置了{yangminz, abel, newton, descartes, gauss, einstein, landau, kepler}这些用户名,他们的密码都是’password’。
通过CTRL_PORT和DATA_PORT将用户名和密码传给server以后,server会通过./server/database/sql.py根据json文件中指定的数据库名称进行数据库查询,确认用户是否存在。
UI循环
server和client确认用户存在以后,会初始化三个类:SERVER, BASIC, CLIENT,分别在./server/server_ops.py, ./client/basic_ops, ./client/client_ops.py中:
srv = SERVER(ctrl_conn, data_conn)
clt = CLIENT(ctrl_skt, data_skt) bsc = BASIC(clt)
这三个class就是UI所在。创建三个class以后,client, server双双进入UI循环:
while True: argstr = get_cmd(clt) argkey, argvar = argsplit(argstr) # run basic operation if argkey in bsc.ops.keys(): bsc.ops[argkey](argvar) # run client operation elif argkey in clt.ops.keys(): clt.ops[argkey](argvar) else: print('%sCMD ERROR! Type \'help\' to check!%s'%(clr.R, clr.E))
其中clr类是tools.py中的颜色控制,因为是linux的终端颜色,所以不知道能不能在windows上使用,我没有试过。颜色有:clr.S (sky blue), clr.Y (yellow), clr.G (grass green), clr.R (red), clr.B (bold), clr.U (underline)
while True: print('%s : server dir:%s\n%s : client dir:%s'%(clt_name,srv.SERVER_DIR,clt_name,srv.CLIENT_DIR)) # receiving request from client cmdcode = deAES(ctrl_conn.recv(32)) print(cmdcode, len(cmdcode)) time.sleep(1) if not cmdcode: break # split request cmd ptc = CTRLPTC(cmdcode) # call corresponding function print('%s : request'%clt_name,ptc.deop()) srv.ops[ptc.deop()](ptc)
这样按照UI格式,用户每输入一行命令,会经过正则表达式函数argsplit()分析。argsplit()在tools.py中,它总是分析这样的字符串:
>>> argstr = 'op var1 var2 var3' >>> argkey, argvar = argsplit(argstr) >>> argkey op >>> argvar ['var1', 'var2', 'var3']
在client中,会根据得到的命令操作符关键字argkey,在BASIC或CLIENT两个类保存的关键字字典中去调用成员函数:
clt.ops[argkey](argvar)
相应的成员函数会通过控制端口CTRL_PORT发送协议命令给server,通过协议类CTRLPTC的成员函数解析出操作符:
srv.ops[ptc.deop()](ptc)
以此调用SERVER类的成员函数作为响应。
SERVER, CLIENT同步目录
在初始化SERVER和CLIENT这两个类的时候,它们会同步目录。这是为了在ls, push, pull, del命令做准备,所以这是必须的。同时,为了方便用户查看目录,需要直接在屏幕上打印地址,因此client知道自己以及server的目录是很重要的。这一步在class的初始化中完成,并且在cd中也需要。
实现的方法就是通过socket互相发送目录:
class CLIENT(object): def __init__(self, ctrl_skt, data_skt): self.ctrl_skt = ctrl_skt # control transfer socket self.data_skt = data_skt # data transfer socket # 保存真实的脚本工作目录、client, server目录 self.WORK_DIR = os.path.dirname(os.path.realpath(__file__)) self.SERVER_DIR = None self.CLIENT_DIR = self.WORK_DIR # 控制端口协议 self.ptc = CTRLPTC('00000000') # 同步client, server目录 self.syn_dir() # UI操作符字典 self.ops = {'cd' : self.cd, 'ls' : self.ls, 'pull' : self.pull, 'push' : self.push, 'del':self.delete, 'server' : self.server, } def syn_dir(self): # 设置协议操作符为‘同步目录’ self.ptc.enop('syndir') # 发送协议与本地client目录 self.ctrl_skt.send(enAES(self.ptc.tostr())) # 用来启动server的syndir()成员函数 self.data_skt.send(enAES(self.CLIENT_DIR)) # 设置server目录 self.SERVER_DIR = deAES(self.data_skt.recv(1024))
这样就能在client端看到本地与server的AbelFile运行的虚拟目录了:
在这个project中,目录一致性是非常重要的,任何一个操作都要保持client和server的目录一致性。
UI功能
用户在client端输入的下面这些指令,会根据上面描述的流程工作。当然对于用户输入的错误的UI,会在错误处理脚本./client/errordict.py中做相应的处理。
AbeFile > help
因为没有涉及到socket通信,所以这是在./client/basic_ops.py中的基本命令。很简单,就是读取./client/help.txt并且打印到屏幕上:
AbeFile > cd [c/s] absolute/dir
cd的c/s两种模式都必须保证服务器与客户端目录的一致性。
cd我能想到的有两种实现方式:
- 对已经获得的字符串进行解析,例如分割’/‘来获取新的目录
- 直接利用python的os.chdir()与os.popen()来获取目录
这两种方式各有优劣。对于第一种而言,坏处是对于复杂的目录切换很需要精力去设计,因为这是一个文件目录树的遍历问题。例如对于这样无聊的命令:
AbelFile > cd s ../../Documents/././../Pictures/./
进行字符串解析与目录树的遍历是很繁琐的。
第二种方法通过下面这些代码就能很简单地通过系统命令完成了,并不需要额外设计目录树与遍历算法:
if os.path.isdir(dirname): os.chdir(dirname) tmp = os.popen('pwd').readlines()[0].rstrip('\n') # os.chdir(self.WORK_DIR) 不将目录恢复为AbelFile/client self.CLIENT_DIR = tmp self.syn_dir() else: error['cd:client dir']()
缺点是目前只在linux上使用,windows因为有些cmd命令的格式不太一样,我没有测试,所以不知道能不能行。但是这样一来,上面那个既无聊又复杂的目录切换就可以实现了:
另外注意,这里由于cd时调用os.chdir()而没有恢复os的目录,也就是说os的目录和self.CLIENT_DIR保存的目录相一致,所以就可以使用相对目录而不需要很繁琐地输入绝对目录了。
AbeFile > ls [c/s]
ls命令同样有上面那两种选择,我同样选择了用系统命令完成。但是这回是确定不能在windows上运行了,因为windwos上相应的命令是’dir’,而且返还的结果和linux完全不是同一个格式。因此AbelFile只能在linux上运行。
图中的示意有一个错误的命令,它是参数错误,所以有此显示。所有的错误都由./client/errordict.py进行管理。
这里有一个着色的困难点。我们知道,一般linux GUI的ls命令是会通过颜色区分文件与目录的,对于client本地的文件与目录通过python的os模块就可以很容易的判断了,但是对于socket发过来的server的文件与目录,没有办法直接判断。为此,只能现在server先判断一遍,附加目录/文件标识再通过socket发送给client:
tmp = [] for item in itemls: item = item.rstrip('\n') if(os.path.isdir(item)): tmp.append('d %s' % item) else: tmp.append('f %s' % item)
这个附加的信息可以在client通过解析UI的正则表达函数解析:
_type_, item = argsplit(itemstr) if _type_ == 'd': print('%s%s%s%s%s'%(clr.S,clr.B,item[0],clr.E,clr.E), ' ', end = '') else: print(item[0], ' ', end = '')
这样就可以正确地着色了。
AbeFile > pull [file.txt/dir]
在push, pull命令中,目录一致性的重要性就显现出来了。在这两个命令中,都没有做其他目录下文件的扩展,也就是说用户只能将self.SERVER_DIR是文件pull到self.CLIENT_DIR。如果希望pull其他目录下的文件,他将只能cd切换目录。
当用户做出pull请求以后,client会先检查文件覆盖问题。如果当前目录下已经有了用户希望pull的文件或目录,就不得不决定是要继续pull或是放弃。随后,server会检查用户请求pull的对象是否存在或者它的类型,如果不存在server会发送:
最后两位清零以示不存在。否则如果检查到pull请求的是文件,则发送:
以示文件类型,如果检查到是目录,则为:
做这个检测是因为文件和目录的传输方式不太相同。对于文件而言,就是普通的传输。对于目录而言,我想到有两种传输方式:
- 用os遍历目录下所有的文件,逐一做文件传输
- 将目录压缩为.zip文件,只传输.zip文件,再解压
.zip的传输方式是:server先将self.SERVER_DIR下的目录压缩为AbelFile/server下的.zip文件,将.zip文件作为文件传输到AbelFile/client,然后解压到self.CLIENT_DIR,server和client删除AbelFile/server和AbelFile/client下的.zip文件。
这样的传输方式虽然别扭,但是实现起来简单。需要注意的是工作目录的问题,如果将目录压缩到当前目录下的.zip文件,则会存在同名覆盖的风险,那么计算机上我们唯一知道不会存在这种.zip同名文件的目录就是AbelFile/server和AbelFile/client,因此压缩只能压缩到这两个目录下。
需要说明的是,这种方式比较适合文件数多,但总大小比较小的目录。如果采用目录遍历的方式挨个做文件传输,那么对于文件数量很多的情况显然传输起来很不方便,但是通过zip方式的话则只需要传输一个.zip文件就够了。可是如果压缩之后的大小很大,那么就又不如挨个传输来得好,因为一旦链接断开,用整个.zip文件传输的话将一无所有,而挨个传输还能得到已经传输好的文件。对此需要权衡,因为这是一个简单的project,所以我就用压缩的方式做了。
此外,传输过程采用AES加密。而且有一个百分比进度条、传输时间的小功能:
AbeFile > push [file.txt/dir]
和push基本一样,只是过程相反,不赘述。
AbeFile > del [c/s] delete.txt
del操作会判断一下是文件还是目录,据此调用不同的函数:
# delete file if os.path.isfile(target): os.remove(target) # delete sub-folder elif os.path.isdir(target): shutil.rmtree(target) # null target else: error['del:no file']()
注意它也只能删除当前目录下的文件和目录。对于删除对象不存在的情况,也有报错处理。
AbeFile > bash /yourdir/yourbash.txt
之所以要将UI命令分成CLIENT, BASIC两类,主要就是为了方便地实现bash命令。
def bash(self, argvar): if os.path.isfile(argvar[0]) is False: error['bash:no file']() # bash file exists return bashlist = open(argvar[0], 'r').readlines() bashlist = [line.strip('\n') for line in bashlist] # execute bash cmds for bashline in bashlist: hint = '%s%sAbelFile%s%s server%s:%s%s%s\n %sclient%s:%s%s%s > '%(clr.B,clr.Y,clr.E,clr.G,clr.E,clr.S,self.clt.SERVER_DIR,clr.E,clr.G,clr.E,clr.S,self.clt.CLIENT_DIR,clr.E) argkey, argvar = argsplit(bashline) if argkey == 'bash': error['bash:abort']() return elif argkey == '#': pass elif argkey in self.ops.keys(): self.ops[argkey](argvar) elif argkey in self.clt.ops.keys(): print(hint, bashline) self.clt.ops[argkey](argvar) else: print('%sCMD ERROR! Type \'help\' to check!%s'%(clr.R, clr.E)) time.sleep(0.1)
从上到下执行CLIENT中的函数是方便的,但是如果CLIENT类有bash成员函数来调用其它成员函数就没那么方便。这个bash命令实际上就是将基础UI照抄了一遍,但是对我们进行测试却很方便。
注意在这样的bash命令中,bash脚本是可以通过’#’符号添加注释的。并且,禁止bash bash,也就是bash脚本中不允许再执行一个bash,以免陷入无限循环。
AbeFile > server
server命令将控制server的系统命令,这实际上是一个非常危险的行为,但是为了好玩我还是做了一个简单的。它的功能是server执行用户输入的命令,并且将终端上显示的信息返还给client:
def server(self, arg_ptc): os.chdir(self.SERVER_DIR) while True: cmd = deAES(self.ctrl_skt.recv(1024)) print('server > %s' % cmd) # time.sleep(1) if not cmd: self.data_skt.send(enAES('quit()')) break if cmd == 'quit()': break data = str(os.popen(cmd).readlines()) self.data_skt.sendall(enAES(data))
上面的代码是server端的。下面执行server的ls命令,并且运行了一个python脚本作为示范。注意这只能运行当前self.SERVER_DIR下的命令,想要运行其他目录也需要quit()退出然后cd切换目录。
AbeFile > exit
exit退出用户程序。