文章类别:

   2019-06-20 11:49:12    2019-06-24 14:49:10   

django ztree
### ztree文档 http://www.treejs.cn/v3/api.php ### django配置 #### 创建虚拟环境 ```bash virtualenv venv27 source venv27/bin/activate ``` #### 安装django ```bash pip install django binaryornot ``` #### 创建项目 ```bash django-admin startproject myproject ``` #### 创建app ```bash cd myproject python manage.py startapp blog ``` #### 配置项目settings.py ```bash vim myproject/settings.py ``` ```ini # -*- coding:utf-8 -*- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ALLOWED_HOSTS = ['*'] INSTALLED_APPS = [ ... 'blog',#安装app ] MEDIA_URL = "/upload/" #配置上传目录 MEDIA_ROOT = os.path.join(BASE_DIR, 'upload') STATIC_URL = '/static/' #配置静态文件目录 STATIC_DIR = os.path.join(BASE_DIR, 'static') STATICFILES_DIRS = [STATIC_DIR, ] ``` #### 创建上传和静态文件目录 ```bash mkdir myproject/{upload,static} ``` #### 配置项目urls.py ```bash vim myproject/urls.py ``` ```python # -*- coding:utf-8 -*- from django.conf.urls import url,include from django.conf.urls.static import static from . import settings urlpatterns = [ url(r'^blog/', include('blog.urls', namespace='blog')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ``` #### 配置应用urls.py ```bash vim blog/urls.py ``` ```python # -*- coding:utf-8 -*- from django.conf.urls import url from blog import views,filetree_views urlpatterns = [ url(r'^demo/$', views.demo, name='demo'),#展示视图 url(r'^async_get_nodes/', filetree_views.async_get_nodes, name='async_get_nodes'), #异步加载节点 url(r'^rename_node/', filetree_views.rename_node, name='rename_node'), #重命名节点 url(r'^add_node/', filetree_views.add_node, name='add_node'), #添加节点 url(r'^remove_node/', filetree_views.remove_node, name='remove_node'), #移除节点 url(r'^move_node/', filetree_views.move_node, name='move_node'), #移动节点 url(r'^read_node/', filetree_views.read_node, name='read_node'), #读取节点 url(r'^save_node/', filetree_views.save_node, name='save_node'), #保存节点 url(r'^upload_file/', filetree_views.upload_file, name='upload_file'), #节点上传文件 url(r'^upload_folder/', filetree_views.upload_folder, name='upload_folder'), #节点上传文件夹 url(r'^download_node/', filetree_views.download_node, name='download_node'), #下载节点 ] ``` #### 配置项目views.py ```bash vim blog/views.py ``` ```python # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.shortcuts import render from django.conf import settings from datetime import datetime import random import base64 import os code_root_path=os.path.join(settings.BASE_DIR,'upload/pygame/code_folder_path') def demo(request): #获取加密的参数 code_folder_enc=request.get_signed_cookie("code_folder_enc",default='',salt='ynotes') if not code_folder_enc: #创建随机目录 datetime_str=datetime.now().strftime('%Y%m%d%H%M%S%f') random_str=random.random() code_folder="{0}__{1}".format(datetime_str,random_str) code_folder_enc=base64.b64encode(code_folder) code_folder_path=code_folder code_folder_abspath=os.path.join(code_root_path,code_folder_path) os.makedirs(code_folder_abspath) else: #解密参数值 code_folder=base64.b64decode(code_folder_enc) code_folder_path=code_folder response = render(request, 'blog/demo.html',{'code_folder_path': code_folder_path}) response.set_signed_cookie('code_folder_enc',code_folder_enc,salt='ynotes',max_age=864000) return response ``` #### 配置项目filetree_views.py ```bash vim blog/filetree_views.py ``` ```python # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.http import HttpResponse from django.shortcuts import render from django.conf import settings import os import json import shutil import base64 import zipfile import random from datetime import datetime from binaryornot.check import is_binary code_relative_path='upload/pygame/code_folder_path' code_root_path=os.path.join(settings.BASE_DIR,code_relative_path) #zip压缩目录 def zipdir(path, ziph): for root, dirs, files in os.walk(path): for file in files: ziph.write(os.path.join(root, file)) def async_get_nodes(request): """异步加载节点""" code_folder_path=request.POST.get('codeFolderPath','')#获取代码的临时目录 code_folder_abspath=os.path.join(code_root_path,code_folder_path)#拼接临时目录绝对路径 treenode_path=request.POST.get('treeNodePath','') #获取选中节点的相对路径 get_nodes_dir=os.path.join(code_folder_abspath,treenode_path) #拼接选中节点的绝对路径 nodes_list=[] #如果code_folder_path参数为空或者绝对路径不存在返回空列表json if not code_folder_path or not os.path.exists(code_folder_abspath): return HttpResponse(json.dumps([])) #循环拼接ztree的数据 for i in os.listdir(get_nodes_dir): node_dict={} get_file=os.path.join(get_nodes_dir,i) inode=os.stat(get_file).st_ino node_dict['id']=inode node_dict['name']=i #通过文件后缀设置不同的zTree图标 if os.path.isdir(get_file): node_dict['isParent']='true' else: node_dict['isParent']='false' nodes_list.append(node_dict) return HttpResponse(json.dumps(nodes_list,ensure_ascii=False)) def rename_node(request): """重命名节点""" code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') old_path_name=os.path.join(code_folder_abspath,treenode_path) new_name=request.POST.get('newName','') if not code_folder_abspath: return HttpResponse('{"result":"error","msg":"参数异常"}') if not new_name: return HttpResponse('{"result":"error","msg":"请输入文件名"}') new_path_name=os.path.join(os.path.dirname(os.path.join(code_folder_abspath,treenode_path)),new_name) try: if os.path.exists(new_path_name): return HttpResponse('{"result":"error","msg":"文件名已存在"}') os.rename(old_path_name,new_path_name) except: return HttpResponse('{"result":"error","msg":"重命名异常"}') return HttpResponse('{"result":"ok","msg":"重命名完成"}') def add_node(request): """添加节点""" code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') file_type=request.POST.get('fileType','file') new_name=request.POST.get('newName','') if not code_folder_abspath: return HttpResponse('{"result":"error","msg":"参数异常"}') if not new_name: return HttpResponse('{"result":"error","msg":"请输入文件名"}') new_path_name=os.path.join(code_folder_abspath,treenode_path,new_name) try: if os.path.exists(new_path_name): return HttpResponse('{"result":"error","msg":"文件名已存在"}') if file_type == 'file': os.mknod(new_path_name,0644) else: os.mkdir(new_path_name) except Exception as e: return HttpResponse('{"result":"error","msg":"新建节点异常"}') return HttpResponse('{"result":"ok","msg":"新建节点完成"}') def remove_node(request): """移除节点""" code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') name=request.POST.get('name','') if not code_folder_abspath: return HttpResponse('{"result":"error","msg":"参数异常"}') if not name: return HttpResponse('{"result":"error","msg":"节点名为空"}') path_name=os.path.join(code_folder_abspath,treenode_path,name) try: if not os.path.exists(path_name): return HttpResponse('{"result":"error","msg":"节点不存在"}') else: if os.path.isdir(path_name): shutil.rmtree(path_name) else: os.remove(path_name) except Exception as e: return HttpResponse('{"result":"error","msg":"删除节点异常"}') return HttpResponse('{"result":"ok","msg":"删除成功"}') def move_node(request): """移动节点""" code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) src_treenode_path=request.POST.get('srcTreeNodePath','') dst_treenode_path=request.POST.get('dstTreeNodePath','') src_path_name=os.path.join(code_folder_abspath,src_treenode_path) dst_path_name=os.path.join(code_folder_abspath,dst_treenode_path) if not code_folder_abspath: return HttpResponse('{"result":"error","msg":"参数异常"}') try: if not os.path.exists(src_path_name): return HttpResponse('{"result":"error","msg":"原节点不存在"}') if not os.path.exists(dst_path_name): return HttpResponse('{"result":"error","msg":"目的节点不存在"}') shutil.move(src_path_name,dst_path_name) except Exception as e: return HttpResponse('{"result":"error","msg":"移动节点异常"}') return HttpResponse('{"result":"ok","msg":"移动成功"}') def read_node(request): """读取节点""" code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') path_name=os.path.join(code_folder_abspath,treenode_path) if not code_folder_abspath: return HttpResponse('{"result":"error","msg":"参数异常"}') if not os.path.exists(path_name): return HttpResponse('{"result":"error","msg":"文件不存在"}') print(path_name+"是否是二进制文件",is_binary(path_name)) if not is_binary(path_name): with open(path_name) as file_object: content=file_object.read() data={"result":"ok","msg":"读取完成","data":base64.b64encode(content)} else: data={"result":"error","msg":"非文本文件不能读取"} return HttpResponse(json.dumps(data)) def save_node(request): """保存节点,对非二进制文件节点进行保存""" code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') encode_file_content=request.POST.get('encodedFileContent','') #获取base64编码的文本内容 path_name=os.path.join(code_folder_abspath,treenode_path) if not code_folder_abspath: return HttpResponse('{"result":"error","msg":"参数异常"}') if not os.path.exists(path_name): return HttpResponse('{"result":"error","msg":"文件不存在"}') if not is_binary(path_name): with open(path_name,'w') as file_object: if len(encode_file_content) > 0: file_object.write(base64.b64decode(encode_file_content)) else: file_object.write('') data={"result":"ok","msg":"保存完成"} else: data={"result":"error","msg":"非文本文件不能保存"} return HttpResponse(json.dumps(data)) def upload_file(request): """节点上传文件""" if request.method == 'POST': code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') upload_file=request.FILES.get('uploadFile','') #上传的文件 if code_folder_path and upload_file: upload_file_abspath=os.path.join(code_folder_abspath,treenode_path,upload_file.name) #拼接上传文件后的绝对路径 if os.path.exists(upload_file_abspath) and os.path.isdir(upload_file_abspath): dict_data={"result":"error","msg":"上传失败,存在同名的目录"} else: with open(upload_file_abspath,'w') as f: f.write(upload_file.read()) if os.path.exists(upload_file_abspath): inode=os.stat(upload_file_abspath).st_ino dict_data={"result":"ok","msg":"上传成功","id":inode} else: dict_data={"result":"error","msg":"上传文件异常"} return HttpResponse(json.dumps(dict_data)) return HttpResponse(json.dumps({"result":"error","msg":"请求参数错误"})) return HttpResponse(json.dumps({"result":"error","msg":"请求方法不支持"})) def upload_folder(request): """节点上传文件夹""" if request.method == 'POST': code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') upload_files=request.FILES.getlist('uploadFiles',[]) #获取上传文件列表 upload_file_paths=request.POST.getlist('uploadFilePaths',[]) #获取上传文件的相对路径列表 is_exist_upload_success=False dict_data={} #判断上传文件列表和上传文件的相对路径列表是否相等 if code_folder_path and upload_files and len(upload_files) == len(upload_file_paths): upload_files_dirpath=os.path.join(code_folder_abspath,treenode_path) upload_fail_files_dict={} for idx in range(0,len(upload_files)): #逐级判断是否存在同名的文件 dir_temp_path='' is_exist_same_file=False for d in os.path.dirname(upload_file_paths[idx]).split('/'): dir_temp_path=os.path.join(dir_temp_path,d) dir_temp_abspath=os.path.join(upload_files_dirpath,dir_temp_path) if os.path.exists(dir_temp_abspath) and not os.path.isdir(dir_temp_abspath): is_exist_same_file=True #需要创建的目录存在同名的文件的字典key是否存在 if dir_temp_path in upload_fail_files_dict: upload_fail_files_dict[dir_temp_path].append(upload_files[idx].name) else: upload_fail_files_dict[dir_temp_path]=[] upload_fail_files_dict[dir_temp_path].append(upload_files[idx].name) #如果存在同名的文件没必要往下判断 break #如果存在同名的文件则直接跳到下一个文件 if is_exist_same_file: continue #判断是否存在目录,不存在则创建 upload_file_dir_abspath=os.path.join(upload_files_dirpath,os.path.dirname(upload_file_paths[idx])) if not os.path.exists(upload_file_dir_abspath): os.makedirs(upload_file_dir_abspath) #把文件保存到相应的路径下 #判断上传的文件名是否存在同名的目录 upload_file_abspath=os.path.join(upload_file_dir_abspath,upload_files[idx].name) if os.path.exists(upload_file_abspath) and os.path.isdir(upload_file_abspath): if upload_file_abspath in upload_fail_files_dict: upload_fail_files_dict[upload_file_paths[idx]].append(upload_file_paths[idx]) else: upload_fail_files_dict[upload_file_paths[idx]]=[] upload_fail_files_dict[upload_file_paths[idx]].append(upload_file_paths[idx]) continue with open(upload_file_abspath,'w') as f: f.write(upload_files[idx].read()) is_exist_upload_success=True fail_msg="" if upload_fail_files_dict: for d in upload_fail_files_dict.keys(): for f in upload_fail_files_dict[d]: if f == d: fail_msg+="已存在同名的目录{0},导致文件{1}上传失败.\n".format(d,f) else: fail_msg+="已存在同名的文件{0},创建目录{0}失败,导致该目录下的文件{1}上传失败.\n".format(d,f) if fail_msg: if is_exist_upload_success: dict_data["msg"]="警告:\n"+fail_msg dict_data["result"]="error" else: dict_data["msg"]="警告:\n"+fail_msg dict_data["result"]="error" else: dict_data["msg"]="上传文件夹成功" dict_data["result"]="ok" ##获取父目录的id if is_exist_upload_success: inode=os.stat(os.path.join(upload_files_dirpath,upload_file_paths[0].split('/')[0])).st_ino dict_data["id"]=inode return HttpResponse(json.dumps(dict_data)) return HttpResponse(json.dumps({"result":"error","msg":"请求参数错误"})) return HttpResponse(json.dumps({"result":"error","msg":"请求方法不支持"})) def download_node(request): """下载节点""" if request.method == 'POST': code_folder_path=request.POST.get('codeFolderPath','') code_folder_abspath=os.path.join(code_root_path,code_folder_path) treenode_path=request.POST.get('treeNodePath','') if code_folder_path: download_file=os.path.join(code_folder_abspath,treenode_path) dict_data={} if os.path.exists(download_file): if os.path.isdir(download_file): #生成一个压缩包目录 datetime_str=datetime.now().strftime('%Y%m%d%H%M%S%f') random_str=random.random() zip_folder="{0}__{1}".format(datetime_str,random_str) zip_folder_abspath=os.path.join(code_root_path,'zip',zip_folder) os.makedirs(zip_folder_abspath) #生成压缩包 cwd=os.getcwd() try: zip_filename = "%s" % os.path.basename(download_file) if treenode_path else "All" workspace_dir=os.path.dirname(download_file) if treenode_path else code_root_path os.chdir(workspace_dir) zip_dirname=os.path.basename(download_file) if treenode_path else code_folder_path shutil.make_archive(os.path.join(zip_folder_abspath,zip_filename), 'zip',zip_dirname) os.chdir(cwd) except Exception as e: os.chdir(cwd) #返回zip的下载链接 url=os.path.join(code_relative_path,'zip',zip_folder,zip_filename+'.zip') filename=zip_filename dict_data={"result":"ok","msg":"文件压缩包下载链接获取成功","url":url,"filename":filename} else: url=os.path.join(code_relative_path,code_folder_path,treenode_path) filename=os.path.basename(treenode_path) dict_data={"result":"ok","msg":"文件下载链接获取成功","url":url,"filename":filename} return HttpResponse(json.dumps(dict_data)) return HttpResponse(json.dumps({"result":"error","msg":"文件不存在"})) return HttpResponse(json.dumps({"result":"error","msg":"请求参数错误"})) return HttpResponse(json.dumps({"result":"error","msg":"请求方法不支持"})) ``` #### 配置django模板 创建模板目录 ```bash mkdir blog/templates/blog -p ``` 创建基础模板 ```bash vim blog/templates/blog/base.html ``` ##### 配置base.html ```html <!DOCTYPE html> {% load staticfiles %} <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <!--<meta name="viewport" content="width=device-width, initial-scale=1.0">--> <meta name='viewport' content='width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0' /> <meta name='apple-mobile-web-app-capable' content='yes' /> <title>Ztree demo</title> {% block css %} <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"> {% endblock %} {% block javascript %} <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script> {% endblock %} </head> <body> {% block body %} {% endblock %} {% block extra_javascript %} {% endblock %} </body> </html> ``` ```bash vim blog/templates/blog/demo.html ``` ```html {% extends 'blog/base.html' %} {% load staticfiles %} {% block css %} {{ block.super }} <link rel="stylesheet" href="{% static 'css/ztree/zTreeStyle.css' %}" type="text/css"> <style> div#rMenu {position:absolute; visibility:hidden; top:0; text-align: left;padding: 1px;} div#rMenu ul li{ margin: 0px 0; padding: 0 5px; cursor: pointer; list-style: none outside none; font-weight:bold; background-color: #90CAF9; font-family:"Microsoft YaHei"; } //设置ztree显示样式 .highlight_red {color:#A60000;} .highlight_green {color:#A7F43D;} //li {list-style: circle;font-size: 12px;} li.title {list-style: none;} ul.list {margin-left: 17px;} div.content_wrap {width: 600px;height:380px;} div.content_wrap div.left{float: left;width: 250px;} div.content_wrap div.right{float: right;width: 340px;} div.zTreeDemoBackground {width:250px;height:362px;text-align:left;} ul.ztree {margin-top: 0px;border: 1px solid #617775;background: #f0f6e4;width:220px;height:360px;overflow-y:scroll;overflow-x:auto;} ul.ztree {margin-top: 0px;border: 1px solid #617775;background: #f0f6e4;overflow-y:scroll;overflow-x:auto;} ul.log {border: 1px solid #617775;background: #f0f6e4;width:300px;height:170px;overflow: hidden;} ul.log.small {height:45px;} ul.log li {color: #666666;list-style: none;padding-left: 10px;} ul.log li.dark {background-color: #E3E3E3;} /* ruler */ div.ruler {height:20px; width:220px; background-color:#f0f6e4;border: 1px solid #333; margin-bottom: 5px; cursor: pointer} div.ruler div.cursor {height:20px; width:30px; background-color:#3C6E31; color:white; text-align: right; padding-right: 5px; cursor: pointer} .ztree li span.button.add {margin-left:2px; margin-right: -1px; background-position:-144px 0; vertical-align:top; *vertical-align:middle} [hidden] { display: none !important; } //配置图片div的显示区域 #yourimg_div {width:800px;height:600px;} </style> {% endblock %} {% block javascript %} {{ block.super }} <script type="text/javascript" src="{% static 'js/ztree/jquery.ztree.core.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/ztree/jquery.ztree.exedit.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/ztree/custom-ztree.js' %}"></script> {% endblock %} {% block body %} <div class="container-fluid"> <div class="row"> <div class="col-md-2"> <label class="btn btn-success btn-sm"> 上传文件<input type="file" id="uploadFile" value="uploadFile" hidden> </label> <label class="btn btn-success btn-sm"> 上传目录<input type="file" id="uploadFolder" value="uploadFolder" webkitdirectory directory multiple hidden> </label> <label class="btn btn-success btn-sm" id="downloadFile"> 下载 </label> </div> <div class="col-md-2"> <div id="notice"> </div> </div> </div> <div class="row"> <div class="col-md-2" id="treeDemo-parent-col"> <div id="filetree" class="content_wrap" style="width:100%;"> <div class="zTreeDemoBackground left" style="width:100%;"> <ul id="treeDemo" class="ztree" style="width:100%;"></ul> </div> </div> </div> <div class="col-md-5"> <div id="yourcode_div"> </div> <div id="yourimg_div" style="display:none;width:800px;height:600px;" > </div> <button class="btn btn-primary" id="saveCode" type="button">保存</button> </div> </div> </div> {% csrf_token %} <input id="codeFolderPath" value="{{code_folder_path}}" type="hidden"> <div id="rMenu"> <ul style="padding-left:0px;"> <li id="m_add_file" onclick="addTreeNode('file');">增加文件</li> <li id="m_add_folder" onclick="addTreeNode('folder');">增加目录</li> <li id="m_rename" onclick="renameTreeNode();">重命名</li> <li id="m_del" onclick="removeTreeNode();">删除</li> </ul> </div> {% endblock %} {% block extra_javascript %} <script type="text/javascript"> $(document).ready(function(){ //ztree $.fn.zTree.init($("#treeDemo"), setting); zTree = $.fn.zTree.getZTreeObj("treeDemo"); rMenu = $("#rMenu"); $("#uploadFile").bind("change",uploadFile); $("#uploadFile").bind("click",beforeUploadFile); $("#uploadFolder").bind("change",uploadFolder); $("#uploadFolder").bind("click",beforeUploadFolder); $("#downloadFile").bind("click",downloadFile); $("#saveCode").bind("click",saveNode); }); </script> {% endblock %} ``` #### 创建ztree目录 ```bash mkdir myproject/static/{js,css}/ztree/ -p ``` #### 下载zTree ztree.core(核心模块):https://gitee.com/zTree/zTree_v3/blob/master/js/jquery.ztree.core.min.js ztree.exedit(exedit扩展模块):https://gitee.com/zTree/zTree_v3/blob/master/js/jquery.ztree.exedit.min.js ztree css:https://gitee.com/zTree/zTree_v3/blob/master/css/zTreeStyle/zTreeStyle.css ztree 图片:https://gitee.com/zTree/zTree_v3/tree/master/css/zTreeStyle/img js文件放到 `myproject/static/js/ztree`目录 css文件放到 `myproject/static/css/ztree`目录 img文件夹放到 `myproject/static/css/ztree`目录 #### 创建自定义js文件 ```bash vim myproject/static/js/ztree/custom-ztree.js ``` ```javascript var setting = { async: { enable: true, url: '/blog/async_get_nodes/', otherParam: getOtherParam, }, view: { selectedMulti: false, showLine: false, expandSpeed: "slow" }, edit: { drag:{ isCopy: false, isMove: true, prev: true, next: true, inner: true }, enable: true, editNameSelectAll: true, showRemoveBtn: false, showRenameBtn: false, }, data: { keep: { leaf: true, parent: true }, simpleData: { enable: true } }, callback: { beforeExpand: beforeExpand, beforeDrag: beforeDrag, beforeDrop: beforeDrop, beforeClick: beforeClick, beforeRename: beforeRename, onRightClick: OnRightClick, onDblClick: OnDblClick, onClick: OnClick } }; function showNotice(type,msg) { if(type=="ok"){ $('#notice').html('<span class="glyphicon glyphicon-ok-sign"></span> '+msg); $('#notice').css('color', 'green'); }else if(type=="error"){ $('#notice').html('<span class="glyphicon glyphicon-remove-sign"></span> '+msg); $('#notice').css('color', 'red'); }else if(type=="hidden"){ $('#notice').html(''); } } function getOtherParam(treeId, treeNode){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var treeNodePath=getTreeNodePath(treeNode); return {'codeFolderPath':codeFolderPath,'treeNodePath':treeNodePath,'csrfmiddlewaretoken':token} } function getTreeNodePath(treeNode){ if(treeNode == null){ return ''; } var ParentNode=treeNode.getParentNode(); if(ParentNode==null){ return treeNode.name; } return getTreeNodePath(ParentNode)+"/"+treeNode.name; } function beforeExpand(treeId, treeNode) { if (!treeNode.isAjaxing) { startTime = new Date(); treeNode.times = 1; ajaxGetNodes(treeNode, "refresh"); return true; } else { showNotice("ok","加载数据中,请稍后..."); return false; } } function ajaxGetNodes(treeNode, reloadType) { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); showNotice("ok","数据加载中..."); if (reloadType == "refresh") { zTree.updateNode(treeNode); } zTree.reAsyncChildNodes(treeNode, reloadType, true,function(){ showNotice("ok","数据加载完成"); }); } function beforeDrag(treeId, treeNodes) { for (var i=0,l=treeNodes.length; i<l; i++) { if (treeNodes[i].drag === false) { return false; } } return true; } function beforeDrop(treeId, treeNodes, targetNode, moveType) { ajaxDragNode(treeNodes[0],targetNode); setTimeout(function(){ zTree.reAsyncChildNodes(targetNode, "refresh", false); },100); return targetNode ? targetNode.drop !== false : true; } function ajaxDragNode(treeNode,targetNode){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var srcTreeNodePath=getTreeNodePath(treeNode); var dstTreeNodePath=getTreeNodePath(targetNode); $.post('/blog/move_node/', { codeFolderPath: codeFolderPath, srcTreeNodePath: srcTreeNodePath, dstTreeNodePath: dstTreeNodePath, csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); console.log(obj.msg); if(obj.result == "ok"){ showNotice("ok",'节点移动完成'); return true; } showNotice('error','节点移动失败'); return false; } }).fail(function(){ showNotice('error','节点移动异常,请联系管理员'); return false; }); } function OnDblClick(event, treeId, treeNode) { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.cancelSelectedNode(); showNotice("ok",'已选中根目录'); } function OnClick(event, treeId, treeNode) { var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); var SelectedTreeNode = nodes[0]; if(SelectedTreeNode.isParent){ showNotice("ok",'已选中目录'); } } function OnRightClick(event, treeId, treeNode) { if (!treeNode && event.target.tagName.toLowerCase() != "button" && $(event.target).parents("a").length == 0) { zTree.cancelSelectedNode(); showRMenu("root", event.clientX, event.clientY); } else if (treeNode && !treeNode.noR) { zTree.selectNode(treeNode); if(treeNode.isParent){ showRMenu("folder", event.clientX, event.clientY); }else{ showRMenu("file", event.clientX, event.clientY); } } } function showRMenu(type, x, y) { $("#rMenu ul").show(); if (type=="file") { $("#m_del").show(); $("#m_edit").show(); $("#m_rename").show(); $("#m_add_folder").hide(); $("#m_add_file").hide(); } else if (type=="folder") { $("#m_del").show(); $("#m_edit").hide(); $("#m_rename").show(); $("#m_add_file").show(); $("#m_add_folder").show(); }else{ $("#m_del").hide(); $("#m_edit").hide(); $("#m_rename").hide(); $("#m_add_file").show(); $("#m_add_folder").show(); } y += document.body.scrollTop; x += document.body.scrollLeft; rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"}); $("body").bind("mousedown", onBodyMouseDown); } function hideRMenu() { if (rMenu) rMenu.css({"visibility": "hidden"}); $("body").unbind("mousedown", onBodyMouseDown); } function onBodyMouseDown(event){ if (!(event.target.id == "rMenu" || $(event.target).parents("#rMenu").length>0)) { rMenu.css({"visibility" : "hidden"}); } } function addTreeNode(fileType) { hideRMenu(); var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); var SelectedTreeNode = nodes[0]; zTree.reAsyncChildNodes(SelectedTreeNode, "refresh", false,function(){ handlerAddTreeNode(fileType); }); } function handlerAddTreeNode(fileType){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); var SelectedTreeNode = nodes[0]; var isParent = false; var tempName = "NewFile"; if(fileType == "folder"){ isParent = true; tempName = "NewFolder"; } tempName=MakeUniqueNodeName(SelectedTreeNode,fileType); if (SelectedTreeNode) { NewTreeNode = zTree.addNodes(SelectedTreeNode, {name: tempName, isParent: isParent}); } else { NewTreeNode = zTree.addNodes(null, {name: tempName, isParent: isParent} ); } ajaxAddNode(SelectedTreeNode,fileType,NewTreeNode[0].name); zTree.editName(NewTreeNode[0]); } function filter(Node){ return Node.level == 0; } function filterChildNode(Node){ return true; } function nameExistNode(NodeList,Name){ for(i=0;i<NodeList.length;i++){ if(NodeList[i].name == Name){ return true; } } return false; } function MakeUniqueNodeName(TreeNode,fileType){ var tempName = "NewFile"; if(fileType == "folder"){ tempName = "NewFolder"; } var zTree = $.fn.zTree.getZTreeObj("treeDemo"); if(TreeNode){ var NodesList=zTree.getNodesByFilter(filterChildNode,false,TreeNode); }else{ var NodesList=zTree.getNodesByFilter(filter); } var counter=1; var NewTempName=tempName; while(nameExistNode(NodesList,NewTempName)){ NewTempName=tempName+counter; counter++; } return NewTempName; } function ajaxAddNode(treeNode,fileType,newName){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var treeNodePath=getTreeNodePath(treeNode); $.post('/blog/add_node/', { codeFolderPath: codeFolderPath, treeNodePath: treeNodePath, newName: newName ,fileType: fileType, csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); console.log(obj.msg); if(obj.result == "ok"){ showNotice("ok","新建节点完成"); return true; } showNotice("error","新建节点请求失败"); return false; } }).fail(function(){ showNotice("error","新建节点请求异常,请联系管理员"); return false; }); } function removeTreeNode() { hideRMenu(); var nodes = zTree.getSelectedNodes(); if (nodes && nodes.length>0) { if (nodes[0].children && nodes[0].children.length > 0) { var msg = "要删除的节点是父节点,如果删除将连同子节点一起删掉。\n\n请确认!"; if (confirm(msg)==true){ ajaxRemoveNode(nodes[0].getParentNode(),nodes[0].name); zTree.reAsyncChildNodes(nodes[0],"refresh",false); } } else { ajaxRemoveNode(nodes[0].getParentNode(),nodes[0].name); zTree.reAsyncChildNodes(nodes[0],"refresh",false); } } } function ajaxRemoveNode(treeNode,name){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var treeNodePath=getTreeNodePath(treeNode); $.post('/blog/remove_node/', { codeFolderPath: codeFolderPath, treeNodePath: treeNodePath, name: name , csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.reAsyncChildNodes(treeNode,"refresh",false); console.log(obj.msg); if(obj.result == "ok"){ showNotice("ok","删除节点完成"); return true; } showNotice("error","删除节点失败"); return false; } }).fail(function(){ showNotice("error","删除异常异常,请联系管理员"); return false; }); } function beforeRename(treeId, treeNode, newName) { if (newName.length == 0) { showNotice("error","节点名称不能为空"); var zTree = $.fn.zTree.getZTreeObj("treeDemo"); setTimeout(function(){zTree.editName(treeNode)}, 10); return false; } ajaxRenameNode(treeNode,newName); return true; } function ajaxRenameNode(treeNode,newName){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var treeNodePath=getTreeNodePath(treeNode); if(treeNode.name == newName ){ return true; } $.post('/blog/rename_node/', { codeFolderPath: codeFolderPath, treeNodePath: treeNodePath, newName: newName , csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); zTree.reAsyncChildNodes(treeNode.getParentNode(),"refresh",false); if(obj.result == "ok"){ showNotice("ok",'重命名节点完成'); return true; } showNotice('error','重命名节点失败'); return false; } }).fail(function(){ showNotice('error','重命名节点异常,请联系管理员'); return false; }); } function renameTreeNode() { hideRMenu(); var zTree = $.fn.zTree.getZTreeObj("treeDemo"), nodes = zTree.getSelectedNodes(), treeNode = nodes[0]; if (nodes.length == 0) { showNotice("error","请先选择一个节点"); return; } zTree.editName(treeNode); }; function utf8_to_b64(str) { return window.btoa(unescape(encodeURIComponent( str ))); } function b64_to_utf8(str) { return decodeURIComponent(escape(window.atob( str ))); } function isImage(filename) { var flag = false; var arr = ["jpg","png","gif"]; var index = filename.lastIndexOf("."); var ext = filename.substr(index+1); for(var i=0;i<arr.length;i++) { if(ext == arr[i]) { flag = true; break; } } return flag; } function beforeClick(treeId, treeNode,clickFlag) { if(!treeNode.isParent){ if(isImage(treeNode.name)){ var codeFolderPath=$("#codeFolderPath").val(); var treeNodePath=getTreeNodePath(treeNode); var codeRootPath='upload/pygame/code_folder_path'; var image_url='/'+codeRootPath+'/'+codeFolderPath+'/'+treeNodePath; var elem = document.createElement("img"); elem.setAttribute("src", image_url); elem.setAttribute("style", "margin:auto;"); elem.setAttribute("alt", "Flower"); document.getElementById("yourcode_div").style.display="none"; document.getElementById("yourimg_div").innerHTML = ""; document.getElementById("yourimg_div").style.background="url("+image_url+") center no-repeat"; document.getElementById("yourimg_div").style.display=""; showNotice("ok","图片加载完成"); }else{ document.getElementById("yourcode_div").style.display=""; document.getElementById("yourimg_div").style.display="none"; ajaxReadNode(treeNode); } } return true; } function ajaxReadNode(treeNode){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var treeNodePath=getTreeNodePath(treeNode); $.post('/blog/read_node/', { codeFolderPath: codeFolderPath, treeNodePath: treeNodePath, csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); if(obj.result == "ok"){ var base64_enc_content=obj.data; if(base64_enc_content == ""){ $('#yourcode_div').text(""); }else{ $('#yourcode_div').text(b64_to_utf8(base64_enc_content)); } showNotice("ok","读取节点完成"); return true; } $('#yourcode_div').text(""); showNotice("error","读取节点失败,二进制格式无法读取"); return false; } }).fail(function(){ showNotice("error","读取节点异常,请联系管理员"); return false; }); } function saveNode(){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); var SelectedTreeNode = nodes[0]; if(nodes[0].isParent){ return; } ajaxsaveNode(SelectedTreeNode); } function ajaxsaveNode(treeNode){ var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var treeNodePath=getTreeNodePath(treeNode); var yourcode=$('#yourcode_div').text(); var encodedFileContent=utf8_to_b64(yourcode); $.post('/blog/save_node/', { codeFolderPath: codeFolderPath, treeNodePath: treeNodePath,encodedFileContent:encodedFileContent, csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); if(obj.result == "ok"){ showNotice("ok","节点保存完成"); return true; } showNotice("error","节点保存失败"); return false; } }).fail(function(){ showNotice("error","节点保存异常,请联系管理员"); return false; }); } function uploadFile(){ console.log('uploadFile'); var FileMaxSize=10*1024*1024; var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); zTree.reAsyncChildNodes(nodes[0], "refresh", false); if(nodes.length != 0 && !nodes[0].isParent){ showNotice("error","请选择目录"); return; } var treeNodePath=getTreeNodePath(nodes[0]); var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var uploadFile=$('#uploadFile')[0].files[0]; if ( uploadFile.size > FileMaxSize){ showNotice('error','上传文件最大限制为'+(FileMaxSize/1024/1024).toFixed(2)+'M,'+uploadFile.name+':'+(uploadFile.size/1024/1024).toFixed(2)+'M'); return false; } var formData=new FormData(); formData.append("codeFolderPath",codeFolderPath); formData.append("treeNodePath",treeNodePath); formData.append("csrfmiddlewaretoken",token); formData.append("uploadFile",uploadFile); $.ajax({ url: '/blog/upload_file/', data: formData, type: 'POST', contentType: false, processData: false, success(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.result == "ok"){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); zTree.reAsyncChildNodes(nodes[0], "refresh", false,function(){ showNotice("ok",'文件上传完成'); var upload_node = zTree.getNodeByParam("id",obj.id,nodes[0]); zTree.selectNode(upload_node); ajaxReadNode(upload_node); }); }else{ showNotice('error',obj.msg); } document.getElementById("uploadFile").value = ""; }, error(err){ showNotice('error','文件上传失败'); console.log(err); } }); } function downloadFile(){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); var treeNodePath=getTreeNodePath(nodes[0]); var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); $.post('/blog/download_node/', { codeFolderPath: codeFolderPath, treeNodePath: treeNodePath, csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('result')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); if(obj.result == "ok"){ showNotice("ok","下载链接获取成功"); var a = document.createElement('a'); var url = obj.url; a.href = window.location.protocol+'//'+window.location.host+'/'+url; a.download = obj.filename; a.click(); return true; } showNotice("error","下载节点失败"); return false; } }).fail(function(){ showNotice("error","下载节点异常,请联系管理员"); return false; }); } function uploadFolder(){ var FolderMaxNumber=1000; var FolderMaxSize=50*1024*1024; var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); zTree.reAsyncChildNodes(nodes[0], "refresh", false); if(nodes.length != 0 && !nodes[0].isParent){ showNotice("error","请选择目录"); return; } var treeNodePath=getTreeNodePath(nodes[0]); var codeFolderPath=$("#codeFolderPath").val(); var token=$('input[name=csrfmiddlewaretoken]').val(); var uploadFiles=$('#uploadFolder')[0].files; if(uploadFiles.length>FolderMaxNumber){ showNotice('error','上传文件夹包含的文件数限制为'+FolderMaxNumber+',当前文件数:'+uploadFiles.length); return; } var uploadFilesTotalSize=0; for(var i=0;i<uploadFiles.length;i++){ uploadFilesTotalSize+=uploadFiles[i].size } if (uploadFilesTotalSize>FolderMaxSize){ showNotice('error','上传文件夹包含的文件总大小为'+(FolderMaxSize/1024/1024).toFixed(2)+'M,当前文件总大小:'+(uploadFilesTotalSize/1024/1024).toFixed(2)+'M'); return; } var formData=new FormData(); for(var i=0;i<uploadFiles.length;i++){ formData.append("uploadFiles",uploadFiles[i]); formData.append("uploadFilePaths",uploadFiles[i]['webkitRelativePath']); } formData.append("codeFolderPath",codeFolderPath); formData.append("treeNodePath",treeNodePath); formData.append("csrfmiddlewaretoken",token); $.ajax({ url: '/blog/upload_folder/', data: formData, type: 'POST', contentType: false, processData: false, success(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('id')){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var upload_node = zTree.getNodeByParam("id",obj.id,nodes[0]); if(obj.result == "ok"){ setTimeout(function(){ zTree.reAsyncChildNodes(upload_node, "refresh", false,function(){ zTree.selectNode(upload_node); showNotice("ok",obj.msg); }); },100); }else{ setTimeout(function(){ zTree.reAsyncChildNodes(upload_node, "refresh", false,function(){ zTree.selectNode(upload_node); showNotice('error',"警告,部分文件上传失败!"); setTimeout(function(){ alert(obj.msg); },300); }); },100); } }else{ showNotice('error',"警告,所有文件上传失败!"); setTimeout(function(){ alert(obj.msg); },300); } document.getElementById("uploadFolder").value = ""; }, error(err){ showNotice('error','警告,文件夹上传失败'); document.getElementById("uploadFolder").value = ""; } }); } function beforeUploadIsReady(){ var zTree = $.fn.zTree.getZTreeObj("treeDemo"); var nodes = zTree.getSelectedNodes(); if(nodes.length != 0 && !nodes[0].isParent){ showNotice("error","请选择目录上传,选择根目录请双击空白处"); return false; } return true; } function beforeUploadFile(){ if(!beforeUploadIsReady()){ event.preventDefault(); } } function beforeUploadFolder(){ if(!beforeUploadIsReady()){ event.preventDefault(); } } ``` ### demo ![avatar](https://image.ynotes.cn/ztree_demo.png) ### 实例 https://ynotes.cn/blog/pygame_v2/
阅读 70 评论 0 收藏 0
阅读 70
评论 0
收藏 0

   2019-06-13 13:40:49    2019-06-20 11:00:42   

docker channels xterm.js websocket
### 流程 #### `浏览器-->nginx-->静态页面-->ws请求-->nginx-->daphne(django-channels)` &nbsp; ### 环境 `centos 7` `nginx 1.12` `python 3.6` `daphne 2.3.0` `django 2.2.2` `docker 4.0.1` `channels 2.2.0` `channels-redis 2.4.0` &nbsp; ### 一、django-channels的websocket实现 #### 创建虚拟环境 ```bash pip install virtualenv cd /data/app/ virtualenv venv source venv/bin/activate ``` #### 安装相关软件包 ```bash pip install -u django channels asgi_redis channels_redis ``` #### 创建项目 ```bash django-admin startproject xtermWS ``` #### 创建app ```bash cd xtermWS python manage.py startapp dockerCmd ``` #### 项目结构如下 ```bash xtermWS ├── dockerCmd │   ├── admin.py │   ├── apps.py │   ├── consumers.py #类似view,需要手动创建 │   ├── __init__.py │   ├── models.py │   ├── routing.py #类似urls,需要手动创建 │   └── views.py ├── manage.py └── xtermWS ├── asgi.py #类似wsgi,需要手动创建 ├── __init__.py ├── routing.py #类似urls,需要手动创建 ├── settings.py ├── urls.py └── wsgi.py ``` #### 配置项目settings.py ```bash vim xtermWS/settings.py ``` ```bash ALLOWED_HOSTS = ['*'] INSTALLED_APPS = [ 'channels', # channels 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'dockerCmd.apps.DockercmdConfig', #新创建的dockerCmd ] ... TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], #项目模板路径 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] #asgi配置 ASGI_APPLICATION = 'xtermWS.routing.application' #日志配置 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'normal': { 'format': '%(levelname)s | %(asctime)s | app: %(module)s pid: %(process)d th: %(thread)d | %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'normal', } }, 'loggers': { 'django': { 'handlers': ['console'], 'level': 'INFO', 'propagate': True, }, }, } #channel_redis配置 CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [('127.0.0.1', 6379)], }, }, } ``` #### 创建xtermWS/routing.py ```bash vim xtermWS/routing.py ``` ```python from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import dockerCmd.routing application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter( dockerCmd.routing.websocket_urlpatterns ) ), }) ``` #### 创建dockerCmd/routing.py ```bash vim dockerCmd/routing.py ``` ```python from django.urls import re_path from dockerCmd import consumers websocket_urlpatterns = [ re_path(r'^ws/(?P<cid>[^/]+)$', consumers.CommandConsumer), ] ``` #### 创建dockerCmd/consumers.py ```bash vim dockerCmd/consumers.py ``` ```python from channels.generic.websocket import WebsocketConsumer import docker import threading import logging logger = logging.getLogger('django') class CommandConsumer(WebsocketConsumer): def connect(self): self.container_id = self.scope['url_route']['kwargs']['cid'] self.accept() self.client=docker.APIClient() #推送logs self.send(text_data=self.client.logs(self.container_id,stdout=True, stderr=True).decode('utf-8')) #self.send(text_data=self.client.attach(container_id,stderr=True,stdout=True,demux=True)) self.socket=self.client.attach_socket(self.container_id, params={'stdin': 1, 'stream': 1}) #开启线程获取stdout,stdin,logs数据stream数据 self.stop_thread=False self.t = threading.Thread(target=self.send_stream_log) self.t.start() def disconnect(self, close_code): #关闭线程 #关闭socket self.stop_thread=True self.socket._sock.send('stop\r\n'.encode('utf-8')) #socket关闭 self.socket.close() #容器关闭删除 self.client.stop(self.container_id) self.client.wait(self.container_id) self.client.remove_container(self.container_id) #client关闭 self.client.close() def receive(self, text_data): self.socket._sock.send(text_data.encode('utf-8')) logger.info('CommandConsumer:receive') def send_stream_log(self): for b in self.client.attach(self.container_id,stderr=True,stdout=True,stream=True,demux=True): logger.info(b) if self.stop_thread: break if b[0]: self.send(text_data=b[0].decode('utf-8')) if b[1]: self.send(text_data=b[1].decode('utf-8')) logger.info('退出线程') ``` #### 创建asgi.py ```bash vim xtermWS/asgi.py ``` ```python import os import django from channels.routing import get_default_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xtermWS.settings") django.setup() application = get_default_application() ``` &nbsp; #### 二、docker安装redis ```bash docker run -d -p 6379:6379 --name redis redis ``` &nbsp; ### 三、supervisor管理daphne #### 安装supervisor ```bash yum install -y supervisor ``` #### 创建配置文件 ```bash vim /etc/supervisord.d/daphne.ini ``` ```bash [program:daphne] directory=/data/app/xtermWS/ environment=PATH="/data/app/venv/bin" command=/data/app/venv/bin/daphne xtermWS.asgi:application -b 0.0.0.0 -p 9000 autostart=true autorestart=true stdout_logfile=/var/daphne/daphne_ws.log redirect_stderr=true ``` #### 创建日志目录 ```bash mkdir /var/daphne ``` #### 启动supervisord ```bash supervisord -c /etc/supervisord.conf ``` #### 启动/停止daphne ```bash supervisorctl start daphne supervisorctl stop daphne ``` &nbsp; ### 四、nginx安装配置 #### 安装nginx ```bash yum install nginx -y ``` #### 配置nginx ```bash vim /etc/nginx/nginx.conf ``` ```ini ... upstream channels-backend-prod { server 127.0.0.1:9000; } server{ listen 80; server_name domain; location /ws { proxy_pass http://channels-backend-prod; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; } } ... ``` #### 启动nginx ```bash systemctl start nginx ``` &nbsp; ### 五、xterm.js安装配置 #### 安装xterm.js ```bash npm install xterm ``` #### 拷贝到项目静态文件目录 ```bash cp -r node_modules/xterm xtermWS/xtermWS/static/ ``` &nbsp; ### 六、前端配置 ```html ... //引入xtrem的css和js文件 <link rel="stylesheet" href="{% static 'xterm/xterm.css' %}" /> <script src="{% static 'xterm/xterm.js' %}" ></script> <script src="{% static 'xterm/addons/attach/attach.js' %}" ></script> ... //显示终端窗口 <div id="terminal-container"> </div> ... <script type="text/javascript"> ... $(document).ready(function(){ // 声明一个websocket变量 var term_websocket; Terminal.applyAddon(attach); const term = new Terminal({ windowsMode:true, rows:10 }); //关闭之前的websocket if (typeof(term_websocket) != "undefined"){ term_websocket.close(); } const container = document.getElementById('terminal-container'); term.open(container); const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; const port = location.port ? `:${location.port}` : ''; const socketUrl = `${protocol}${location.hostname}${port}/ws/容器id`; term_websocket = new WebSocket(socketUrl); term_websocket.onopen = (ev) => { term.attach(term_websocket); }; term_websocket.onclose = function(ev){ console.log('Connection closed.'); }; }); ... </script> ```
阅读 65 评论 0 收藏 0
阅读 65
评论 0
收藏 0

   2019-03-30 00:53:42    2019-03-30 00:53:42   

python asyncio 协程 coroutine
### 介绍 终端使用的是asyncio,单线程支持并发操作。 聊天终端,暂时仅提供登陆/注销/发送在线消息/发送离线消息/帮助功能(未完待续) 登陆命令:auth userid password (userid为用户账号,用户数据存储在mysql中) 注销命令:logout 发送消息:msg userid message (支持在线和离线消息,离线消息,当对方不在线时,存储到 redis中,等用户上线之后再推送给该用户) 帮助命令:help/? ### 环境 系统:`CentOS7` 编程语言:`Python` 语言解释器:`Cpython-3.7.2` 数据库:`mysql-5.7` `redis-5.0.3` 缓存:`redis-5.0.3` ### 表结构 #### user表 ```sql CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8; ``` ### 服务器端代码 chat_server.py ```py #coding:utf-8 ###################### # 作者: SheYinsong # # 时间: 2019-03-30 # ###################### import asyncio import aiomysql import aioredis import sys import struct import ssl import time import logging SERVER_ADDRESS = ('0.0.0.0',10000) logging.basicConfig( level=logging.DEBUG, format='%(name)s: %(message)s', stream=sys.stderr, ) log=logging.getLogger('main') #LOOP loop=asyncio.get_event_loop() #TLS配置 CERTFILE='ssl/server.crt' KEYFILE='ssl/server.key' #在线用户userid到读写Stream映射 auth_userid_map_wstream={} auth_wstream_map_userid={} #mysql 配置 db_config={ 'host':'localhost', 'port':3306, 'user':'root', 'password':'xxxxxxxx', 'db':'chat', 'charset':'utf8' } #SQL 模板 QUERY_AUTH_SQL='select password from user where id=%s' QUERY_USERID_IS_EXISTS_SQL='select id from user where id=%s' QUERY_USERNAME_OF_USERID_SQL='select username from user where id=%s' #redis配置 #redis_host='localhost' #redis_port=6379 redis_unix_socket='/var/run/redis/redis.sock' #redis KEY模板 KV_EXISTS_USERID=u'kv_exists_userid:userid:%s' KV_USERID_GET_USERNAME=u'kv_userid_get_username:userid:%s' LIST_USERMESSAGES_OF_USERID=u'list_usermessages_of_userid:userid:%s' #支持的命令列表 cmd_list=['auth','msg','logout'] #命令帮助 cmd_help=""" =============================================== | | | 命令使用说明: | | | =============================================== |命令 参数 参数 (说明) | =============================================== |auth UID PASSWORD (登录系统) | |msg UID MESSAGE (给用户发送消息) | =============================================== |命令 (说明) | =============================================== |logout (退出) | =============================================== """ async def get_custom_time_string(t_format="%Y-%m-%d %X"): """获取指定格式的时间字符串 默认格式为"%Y-%m-%d %X" """ return time.strftime(t_format) async def get_db_pool(): """获取mysql连接池""" pool = await aiomysql.create_pool(**db_config) return pool async def get_redis_pool(): """获取redis连接池""" pool = await aioredis.create_pool( redis_unix_socket, minsize=5, maxsize=10) return pool async def db_query(sql): """mysql执行查询操作""" pool = await get_db_pool() results=() async with pool.acquire() as conn: async with conn.cursor() as cur: log.debug(f'查询数据库,SQL->[ {sql} ]') await cur.execute(sql) results = await cur.fetchall() pool.close() await pool.wait_closed() return results async def redis_query(cmd,*args): """redis执行查询操作""" pool=await get_redis_pool() with await pool as conn: results =await conn.execute(cmd,*args) pool.close() await pool.wait_closed() return results async def redis_dml(cmd,*args): """redis执行增删改操作""" pool=await get_redis_pool() with await pool as conn: await conn.execute(cmd,*args) pool.close() await pool.wait_closed() async def get_username_of_userid(userid): """获取userid对应的用户名""" if await redis_query('exists',KV_USERID_GET_USERNAME % userid): username=await redis_query('get',KV_USERID_GET_USERNAME % userid) return username.decode('utf-8') else: res=await db_query(QUERY_USERNAME_OF_USERID_SQL % userid) if res: username=res[0][0] await redis_dml('set',KV_USERID_GET_USERNAME % userid,username) return username return '' async def get_userid_of_wstream(wstream): """通过writestream获取userid""" try: userid=auth_wstream_map_userid[wstream] return userid except KeyError: return -1 async def get_wstream_of_userid(userid): """通过userid获取writestream""" try: write_stream=auth_userid_map_wstream[userid] return write_stream except KeyError: return '' async def write_data(writer,data): """发送数据""" b_data=bytes(data,encoding='utf-8') b_datasize=struct.pack('H',len(b_data)) try: writer.write(b_datasize+b_data) await writer.drain() return True except: await clean_wstream(writer) return False async def read_data(reader): """读取数据""" try: length=struct.unpack('H',await reader.read(2))[0] b_data=await reader.read(int(length)) return b_data.decode('utf-8') except Exception as e: log.debug(f'读取客户端数据异常:[{e}]') return '' async def clean_wstream(writer): """清理客户端登陆信息和关闭stream""" userid=await get_userid_of_wstream(writer) if userid != -1: try: del auth_userid_map_wstream[userid] except: pass try: del auth_wstream_map_userid[writer] except: pass writer.close() async def user_auth(*args): """用户认证""" userid=int(args[0]) pwd=args[1] sql=QUERY_AUTH_SQL % (userid) res=await db_query(sql) if res: if res[0][0] == pwd: return True return False async def userid_is_exists(userid): """"判断userid是否存在""" if await redis_query('exists',KV_EXISTS_USERID % userid): return True else: res=await db_query(QUERY_USERID_IS_EXISTS_SQL % userid) if res: await redis_dml('set',KV_EXISTS_USERID % userid,'') return True return False async def userid_is_online(userid): """判断用户是否在线""" try: writer=auth_userid_map_wstream[userid] return True except KeyError: return False async def offline_userid(operator_type,*args): """下线用户""" userid=int(args[0]) try: writer=auth_userid_map_wstream[userid] if operator_type == 0: msg='有用户登录您的账号,您已被挤下线!' elif operator_type == 1: msg='您已退出登录!' else: msg='您已退出系统!' await write_data(writer,msg) except Exception as e: log.debug(e) finally: try: del auth_userid_map_wstream[userid] except: log.debug('auth_userid_map_wstream 删除失败') try: del auth_wstream_map_userid[writer] except: log.debug('auth_wstream_map_userid 删除失败') async def send_user_msg(writer,*args): """发送用户消息""" to_userid=int(args[0]) from_userid=auth_wstream_map_userid[writer] content=' '.join(args[1:]) if not await userid_is_exists(to_userid): msg='系统不存在该userid!' await write_data(writer,msg) return from_username=await get_username_of_userid(from_userid) to_username=await get_username_of_userid(to_userid) custom_time=await get_custom_time_string() send_content=f'{custom_time}\n[ {from_username}|UID:{from_userid} ]:{content}' if to_userid == from_userid: send_content=f'{custom_time}\n[ {from_username}|UID:{from_userid} ]:{content}' await write_data(writer,send_content) return if await userid_is_online(to_userid): to_writer=auth_userid_map_wstream[to_userid] if await write_data(to_writer,send_content): await write_data(writer,send_content) return #用户不在线,发送离线消息 print("用户不在线,发送离线消息") await redis_dml('rpush',LIST_USERMESSAGES_OF_USERID % to_userid,send_content) await write_data(writer,f'[离线消息]\n{send_content}') async def user_is_auth(writer): """判断用户是否认证""" if writer in auth_wstream_map_userid.keys(): return True return False async def get_offline_msg_of_userid(userid): "获取userid对应的离线消息" return await redis_query('lrange',LIST_USERMESSAGES_OF_USERID % userid,0,-1) async def push_offline_msg(writer,*args): """推送离线消息""" userid=int(args[0]) user_offline_messages_list=await get_offline_msg_of_userid(userid) for user_offline_message in user_offline_messages_list: await write_data(writer,user_offline_message.decode('utf-8')) await redis_query('del',LIST_USERMESSAGES_OF_USERID % userid) async def update_auth(writer,*args): """更新auth_userid_map_wstream和auth_wstream_map_userid""" userid=int(args[0]) auth_userid_map_wstream[userid]=writer auth_wstream_map_userid[writer]=userid async def check_args_validity(writer,cmd,*args): if cmd == 'auth': if len(args) != 2: msg='提示:认证格式: [ auth userid pwd ]' await write_data(writer,msg) return False try: userid=int(args[0]) except: msg='userid为整数!' await write_data(writer,msg) return False elif cmd == 'msg': if len(args) < 2: msg='提示:消息格式: [ msg userid content ]' await write_data(writer,msg) return False try: to_userid=int(args[0]) except: msg='userid为整数!' await write_data(writer,msg) return False return True async def handler_client(reader,writer): """处理客户端连接""" address=writer.get_extra_info('peername') log=logging.getLogger('echo_{}_{}'.format(*address)) log.debug('收到新连接') args_ok=True cmd_ok=True while True: if not await user_is_auth(writer) and cmd_ok and args_ok: msg=u'请登陆!' await write_data(writer,msg) data=await read_data(reader) if not data: break cmd=data.split(' ')[0] args=data.split(' ')[1:] if cmd == '?' or cmd == 'help': await write_data(writer,f'{cmd_help}') cmd_ok=False continue elif cmd not in cmd_list: await write_data(writer,'输入的命令不支持,请使用 help 或 ? 查看帮助!') cmd_ok=False continue cmd_ok=True if not await user_is_auth(writer): if cmd != 'auth': continue if not await check_args_validity(writer,cmd,*args): args_ok=False continue args_ok=True if await user_auth(*args): await offline_userid(0,*args) await update_auth(writer,*args) msg='认证成功!' await write_data(writer,msg) await push_offline_msg(writer,*args) else: msg='认证失败!' await write_data(writer,msg) else: if not await check_args_validity(writer,cmd,*args): args_ok=False continue args_ok=True if cmd == 'msg': await send_user_msg(writer,*args) elif cmd == 'logout': userid=await get_userid_of_wstream(writer) await offline_userid(1,userid) elif cmd == 'auth': await write_data(writer,'您已登录!') context=ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context.load_cert_chain(certfile=CERTFILE,keyfile=KEYFILE) factory=asyncio.start_server(handler_client,*SERVER_ADDRESS,ssl=context) server=loop.run_until_complete(factory) log.debug('starting up on {} port {}'.format(*SERVER_ADDRESS)) try: loop.run_forever() except KeyboardInterrupt: pass finally: log.debug('关闭服务器.') server.close() loop.run_until_complete(server.wait_closed()) log.debug('关闭事件循环LOOP.') loop.close() ``` ### 客户端 chat_client.py ```py #coding:utf-8 ###################### # 作者: SheYinsong # # 时间: 2019-03-30 # ###################### import asyncio from aioconsole import ainput import concurrent.futures import sys import ssl import time import struct import logging #日志配置 logging.basicConfig( level=logging.DEBUG, format='%(name)s: %(message)s', stream=sys.stderr, ) log = logging.getLogger('main') #TLS配置 CERTFILE='ssl/server.crt' #chat服务器配置 SERVER_ADDRESS = ('chat.unotes.co',10000) #证书使用的chat.unotes.co域名,根据实际证书域名填写 loop = asyncio.get_event_loop() async def write_data(writer,data): """发送数据""" b_data=bytes(data,encoding='utf-8') b_datasize=struct.pack('H',len(b_data)) try: writer.write(b_datasize+b_data) await writer.drain() return True except: return False async def read_data(reader): """读取数据""" try: length=struct.unpack('H',await reader.read(2))[0] b_data=await reader.read(int(length)) return b_data.decode('utf-8') except Exception as e: log.debug(f'读取客户端数据异常:[{e}]') return '' async def read(reader,once=False): """循环读取数据""" while True: data=await read_data(reader) if data: if once: print(f'{data}\n',end='') break print(f'\x08\x08\x08{data}\n>>>',end='') async def chat_client(address): """聊天客户端""" log = logging.getLogger('chat_client') log.debug('connecting to {} port {}'.format(*address)) context=ssl.create_default_context(ssl.Purpose.SERVER_AUTH) context.load_verify_locations(CERTFILE) reader, writer = await asyncio.open_connection(*address,ssl=context) await read(reader,True) read_task=asyncio.create_task(read(reader)) while True: data=await ainput('>>>') if not data: data='?' await write_data(writer,data) try: loop.run_until_complete( chat_client(SERVER_ADDRESS) ) except: pass finally: log.debug('closing event loop') loop.close() ``` ### 运行服务器 ```bash $ python chat_server_asyncio_tls.py ``` ``` asyncio: Using selector: EpollSelector main: starting up on 0.0.0.0 port 10000 ``` ### 运行客户端1 #### 用户张三 ```bash python chat_client_asyncio_tls.py ``` ``` 请登陆! >>>auth 10001 123456 认证成功! >>>msg 10002 你好,我是张三 [离线消息] 2019-03-29 17:00:24 [ 张三|UID:10001 ]:你好,我是张三 >>> ``` ### 运行客户端2 #### 用户李四 ```bash python chat_client_asyncio_tls.py ``` ``` 请登陆! >>>auth 10002 123456 认证成功! 2019-03-29 17:00:24 [ 张三|UID:10001 ]:你好,我是张三 >>>msg 10001 你好,张三,我收到您的消息 2019-03-29 17:02:52 [ 李四|UID:10002 ]:你好,张三,我收到您的消息 >>> ``` #### 查看客户端1 ### 用户张三 ```bash $ python chat_client_asyncio_tls.py ``` ``` 请登陆! >>>auth 10001 123456 认证成功! >>>msg 10002 你好,我是张三 [离线消息] 2019-03-29 17:00:24 [ 张三|UID:10001 ]:你好,我是张三 2019-03-29 17:02:52 [ 李四|UID:10002 ]:你好,张三,我收到您的消息 ```
阅读 171 评论 0 收藏 0
阅读 171
评论 0
收藏 0


第 1 页 / 共 3 页
 
第 1 页 / 共 3 页