2019-08-21 10:52:51    2019-11-14 14:25:36   

pygame NoVNC xvfb
### 流程 #### `浏览器--->noVNC--->xvfb虚拟桌面--->pygame程序` ### xvfb安装 #### 编辑Dockerfile ```bash cat Dockerfile ``` ```bash FROM ubuntu:xenial LABEL maintainer "sheyinsong" EXPOSE 5900 6099 ENV \ DEBIAN_FRONTEND="nonintractive" \ X11VNC_PASSWORD="password" RUN sed -i -e s#security.ubuntu.com#mirrors.aliyun.com#g -e s#archive.ubuntu.com#mirrors.aliyun.com#g /etc/apt/sources.list RUN apt-get update -y RUN apt-get install -y software-properties-common RUN add-apt-repository ppa:jonathonf/python-3.6 RUN apt-get update -y RUN apt-get install -y xvfb x11vnc python3.6 libglib2.0-0 RUN apt-get install -y curl RUN /bin/cp /usr/bin/python3.6 /usr/bin/python RUN curl https://bootstrap.pypa.io/get-pip.py | python3.6 RUN pip install pygame ADD ./start.sh /opt/start.sh ENTRYPOINT [ "bash","/opt/start.sh" ] ``` #### 创建start.sh脚本 ```bash cat start.sh ``` ```bash #!/bin/bash screen_width=${1:-800} screen_height=${2:-600} screen_depth=${3:-16} run_file=${4:-"main.py"} nohup Xvfb :1 -screen 0 ${screen_width}x${screen_height}x${screen_depth} >/dev/null 2>&1 & sleep 0.1 nohup /usr/bin/x11vnc -display :1.0 -passwd ${X11VNC_PASSWORD:-password} >/dev/null 2>&1 & sleep 0.1 DISPLAY=:1.0 export DISPLAY cd /run_game PYTHONIOENCODING=utf-8 python $run_file ``` #### 创建pygame镜像 ```bash docker build -t sheyinsong/pygame:1.0 . ```   ### Novnc安装 #### 编辑Dockerfile ```bash cat Dockerfile ``` ```bash FROM python MAINTAINER sys "sheyinsong" RUN apt-get update -y RUN apt-get upgrade -y RUN apt-get install -y python-numpy RUN apt-get clean RUN pip install redis RUN pip install simplejson ADD ./noVNC/ /noVNC/ CMD ["python", "/noVNC/utils/websockify/run", "--web", "/noVNC", "0.0.0.0:10240", "--target-config", "/noVNC/token.list"] ``` #### 下载noVNC 下载地址:https://github.com/novnc/noVNC/releases/ #### 创建token.list(token映射到对应的socket) ```bash cat noVNC/token.list #noVNC下载好后,到根目录创建该文件 ``` ```bash a35fe7f7fe8217b4369a0af4244d1fca: 127.0.0.1:5000 03b264c595403666634ac75d828439bc: 127.0.0.1:5001 ... ``` #### 创建novnc镜像 ```bash docker build -t sheyinsong/noVNC:1.0 . ``` #### 运行novnc容器 ```bash docker run -d --network="host" --restart=always -it sheyinsong/novnc:1.0 ```   #### 创建生成NoVNC地址的脚本 ```bash cat pygame_url.py ``` ```python #coding:utf-8 import sys import os import random import subprocess import hashlib import time import argparse import json parser = argparse.ArgumentParser() parser.add_argument("pygame_dir",nargs = 1, help="Pygame progrom root directory") parser.add_argument("--screen-width", help="Xvfb screen width,default value 800", default=800, type=int) parser.add_argument("--screen-height", help="Xvfb screen heigth,default value 600", default=600, type=int) parser.add_argument("--screen-depth", help="Xvfb screen depth,default value 16", default=16, type=int) parser.add_argument("--run-file", help="run specify file", default='main.py') args = parser.parse_args() pygame_dir=args.pygame_dir[0] screen_width=str(args.screen_width) screen_height=str(args.screen_height) screen_depth=str(args.screen_depth) run_file=args.run_file pygame_image='sheyinsong/pygame:1.0' sample_url='http://192.168.50.252:10240/vnc_lite.html?path=?token=' #地址为noVNC运行的地址 dict_data={} if not os.path.isdir(pygame_dir): dict_data['result']='error' dict_data['msg']=pygame_dir+'程序目录不存在.' print(json.dumps(dict_data)) exit(1) pygame_main=os.path.join(pygame_dir,'main.py') if not os.path.exists(pygame_main): dict_data['result']='error' dict_data['msg']=pygame_dir+'程序目录不存在文件main.py.' print(json.dumps(dict_data)) exit(1) source = open(pygame_main, 'r').read() + '\n' try: code_obj=compile(source, pygame_main, 'exec') except Exception as e: dict_data['result']='error' dict_data['msg']=str(e) print(json.dumps(dict_data)) exit(1) def get_free_random_port(): command="netstat -ltnp |grep x11vnc|awk '{ print $4}'|awk -F: '{ print $NF}'" p = subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True) usage_port_list=[ int(port) for port in p.stdout.read().split('\n') if port != '' ] free_post_list=set(range(5000,6000))-set(usage_port_list) return random.choice(list(free_post_list)) def convert_png(): command="find {0} -name *.png|while read line;do; convert $line $line; done".format(pygame_dir) subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True) free_random_port=get_free_random_port() convert_png() docker_out = subprocess.Popen(['docker', 'run','-ti','--cpus','0.2', '-d', '-p', str(free_random_port)+':5900', '-v', pygame_dir+':/run_game', pygame_image, screen_width, screen_height, screen_depth, run_file], stdout=subprocess.PIPE,stderr=subprocess.STDOUT) stdout,stderr = docker_out.communicate() time.sleep(1) containter_id=stdout.strip() p1 = subprocess.Popen('docker logs --tail 100 '+containter_id,stdout=subprocess.PIPE,shell=True) p2 = subprocess.Popen('grep -A 1000 Traceback',stdin=p1.stdout,stdout=subprocess.PIPE,shell=True) docker_out=p2.stdout.read() if docker_out: dict_data['result']='error' dict_data['msg']=docker_out.replace('\r\n','\\n').replace('"','\'') print(json.dumps(dict_data)) else: token=hashlib.md5(str(free_random_port)).hexdigest() url=sample_url+token+'&password=password' dict_data['result']='ok' dict_data['msg']=docker_out.replace('\r\n','\\n').replace('"','\'') dict_data['url']=url dict_data['cid']=containter_id print(json.dumps(dict_data)) ```   #### 运行脚本 ```bash python /data/app/pygame/pygame_url.py /data/app/pygame/Puser/ --run-file main.py ``` 程序返回结果(url为访问pygame的地址) ```json {"msg": "", "url": "http://192.168.50.252:10240/vnc_lite.html?path=?token=fcd4c889d516a54d5371f00e3fdd70dc&password=password", "result": "ok", "cid": "23cac4bb78e93f3fb1c7232d5778ad99468cd86cc927cfd8e5161e485239b46b"} ``` `/data/app/pygame/Puser/` 为程序根目录 `main.py` 为运行pygame的入口程序 ### 页面访问pygame ![](https://image.ynotes.cn/pygame_demo.png)
阅读 173 评论 0 收藏 0
阅读 173
评论 0
收藏 0

   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/
阅读 248 评论 0 收藏 0
阅读 248
评论 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> ```
阅读 550 评论 0 收藏 0
阅读 550
评论 0
收藏 0

   2019-05-24 15:36:32    2019-11-14 14:29:32   

python pygame
### 流程: - 1.用户在线编辑或者上传代码 - 2.用户点击执行按钮 - 2.1 首先把页面的代码保存到执行目录 - 2.2 ajax调用代码运行API (注:获取访问pygame的链接) - 2.2.1 API收到请求做处理 - 2.2.1.1 运行pygame(docker)生成可访问链接 - 2.2.2 API返回链接 - 2.3 ajax页面获取到API调用结果 - 2.4 把链接加载到页面Iframe展示 &emsp; ### 上传代码和保存代码 #### 上传功能代码 视图view.py(注:只提取功能相关代码) ```python @login_required def pygame(request): upload_error='' code='' code_dir='' if request.method == 'POST': myfile=request.FILES.get('myfile', None) if myfile: myfile = request.FILES['myfile'] fs = FileSystemStorage() file_path=os.path.join('pygame',str(request.user.id)) filename = fs.save(file_path, myfile) temp_dir=os.path.join(os.path.dirname(fs.path(filename)),str(request.user.id),'tempDir') #判断临时目录是否存在存在则删除目录,创建目录 if os.path.exists(temp_dir): shutil.rmtree(temp_dir) os.mkdir(temp_dir) #判断文件是否为zip if zipfile.is_zipfile(fs.path(filename)): zip_ref = zipfile.ZipFile(fs.path(filename), 'r') zip_ref.extractall(temp_dir) is_exist_main_file=0 for fname in zip_ref.namelist(): if 'main.py' in fname: main_file=os.path.join(temp_dir,fname) is_exist_main_file=1 if is_exist_main_file: code=fs.open(main_file,'r').read() code_dir=os.path.dirname(main_file) else: upload_error='压缩包根目录下不存在main.py' zip_ref.close() fs.delete(filename) else: upload_error='文件不是压缩文件' return render(request, 'blog/pygame.html',{'upload_error': upload_error,'code':code,'code_dir':code_dir}) ``` &emsp; 模板blog/pygame.html(注:只提取功能相关代码) ```html <form method="post" class="form-inline" enctype="multipart/form-data"> {% csrf_token %} <div class="form-group"> <input type="file" id="myfile" name="myfile"> </div> <div class="form-group"> <button class="btn btn-primary" type="submit" >上传</button> </div> <label for="myfile">注意:请上传zip压缩包,且压缩包根目录包含main.py</label> </form> ``` &emsp; ### 保存功能代码 #### 视图view.py(注:只提取功能相关代码) ```python def pygame_save_code(request): code_dir=request.POST.get('codeDir','') code_content=request.POST.get('codeContent','') if not code_content: HttpResponse('{"error":"代码为空"}') #判断是否有codeDir,如果有则直接写入到当前目录,如果没有写入一个用户随机目录 if not code_dir: datetime_str=datetime.now().strftime('%Y_%m_%d_%H_%M_%S') create_dir=os.path.join(django_settings.MEDIA_ROOT,'pygame',str(request.user.id),datetime_str) os.mkdir(create_dir) code_dir=create_dir #把代码写入文件,返回写入的目录 file=open(os.path.join(code_dir,'main.py'),'w+') file.write(code_content) file.close() return HttpResponse({'{"NewCodeDir":"'+code_dir+'"}'}) ``` &emsp; #### 模板blog/pygame.html(注:只提取功能相关代码) ```html //代码编辑窗口 <textarea class="form-control line_number" id="yourcode" cols="80" rows="30"></textarea> //pygame展示窗口 <iframe id="pygame_iframe" src="" ></iframe> <button class="btn btn-success" id="pygame_run" type="button">运行</button>:write //处理代码运行逻辑 $('#pygame_run').click(function() { var code_content=$("#yourcode").val(); var codedir=$("#codeDir").val(); var token = $('input[name=csrfmiddlewaretoken]').val(); //调用保存编辑pygame代码API $.post('/blog/pygame_save_code/', { codeDir: codedir, codeContent: code_content, csrfmiddlewaretoken: token}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); var NewCodeDir=obj.NewCodeDir; $("#codeDir").val(NewCodeDir); setTimeout( function() { var codedir=$("#codeDir").val(); //调用执行pygame程序的API $.post('https://pygame.ynotes.cn:3443/pygame/', { codeDir: codedir}, function(returnedData){ var obj = jQuery.parseJSON(returnedData); if(obj.hasOwnProperty('url')){ $('#pygame_iframe').attr('src', obj.url); $('#pygame_text').text('运行成功'); $('#pygame_text').css('color', 'green'); } if(obj.hasOwnProperty('error')){ console.log(obj.error); $('#pygame_text').text(obj.error); $('#pygame_text').css('color', 'red'); } }).fail(function(){ console.log("error"); }); }, 100); }).fail(function(){ console.log("error"); }); }); ``` &emsp; ### 生成访问pygame链接 #### 视图view.py ```python def index(request): code_dir=request.POST.get('codeDir','nodir') command='python /data/app/pygame/pygame_url.py '+code_dir p = subprocess.Popen(command,stdout=subprocess.PIPE,shell=True) pygame_out=p.stdout.read() return HttpResponse(pygame_out) ``` &emsp; #### 生成pygame访问链接的python脚本 pygame_url.py ```python #coding:utf-8 import sys import os import random import subprocess import hashlib import time pygame_image='codemax/pygame:v3' sample_url='https://pygame.ynotes.cn:2443/vnc_lite.html?path=?token=' if len(sys.argv) < 2: print('{ "error":"请指定pygame程序目录"}') exit(1) pygame_dir=sys.argv[1] if not os.path.isdir(pygame_dir): print('{ "error":"'+pygame_dir+'程序目录不存在."}') exit(1) pygame_main=os.path.join(pygame_dir,'main.py') if not os.path.exists(pygame_main): print('{ "error":"'+pygame_dir+'程序目录不存在文件main.py."}') exit(1) source = open(pygame_main, 'r').read() + '\n' try: code_obj=compile(source, pygame_main, 'exec') except Exception as e: print('{"error":"'+str(e)+'"}') exit(1) def get_free_random_port(): command="netstat -ltnp |grep x11vnc|awk '{ print $4}'|awk -F: '{ print $NF}'" p = subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True) usage_port_list=[ int(port) for port in p.stdout.read().split('\n') if port != '' ] free_post_list=set(range(5000,6000))-set(usage_port_list) return random.choice(list(free_post_list)) free_random_port=get_free_random_port() docker_out = subprocess.Popen(['docker', 'run', '-d', '-it','--cpus=0.2','-p', str(free_random_port)+':5900', '-v', pygame_dir+':/run_game', pygame_image], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout,stderr = docker_out.communicate() time.sleep(1) containter_id=stdout.strip() p1 = subprocess.Popen('docker logs --tail 100 '+containter_id,stdout=subprocess.PIPE,shell=True) p2 = subprocess.Popen('grep -A 1000 Traceback',stdin=p1.stdout,stdout=subprocess.PIPE,shell=True) docker_out=p2.stdout.read() if docker_out: print('{"error":"'+docker_out.replace('\r\n','\\n').replace('"','\'')+'"}') else: token=hashlib.md5(str(free_random_port)).hexdigest() url=sample_url+token+'&password=password' print('{"url":"'+url+'"}') ``` &emsp; ### 构建pygame镜像(拥有运行pygame代码) #### DockerFile ```yaml FROM ubuntu:xenial LABEL maintainer "sheyinsong@unotes.co" EXPOSE 5900 6099 ENV \ DEBIAN_FRONTEND="nonintractive" \ X11VNC_PASSWORD="password" RUN apt-get update -y RUN apt-get install -y xvfb x11vnc python libglib2.0-0 ADD ./get-pip.py /opt/get-pip.py RUN python /opt/get-pip.py ADD ./pygame-1.9.6-cp27-cp27mu-manylinux1_x86_64.whl /opt/pygame-1.9.6-cp27-cp27mu-manylinux1_x86_64.whl RUN pip install /opt/pygame-1.9.6-cp27-cp27mu-manylinux1_x86_64.whl ADD ./start.sh /opt/start.sh ENTRYPOINT /opt/start.sh ``` #### 文件start.sh ```bash #!/bin/sh Xvfb :1 -screen 0 800x600x16 & sleep 0.1 /usr/bin/x11vnc -display :1.0 -passwd ${X11VNC_PASSWORD:-password} & sleep 0.1 DISPLAY=:1.0 export DISPLAY cd /run_game timeout 3600 python main.py ``` &emsp; ### 构建noVNC镜像(用于html访问pygaem(docker)中的GUI界面) #### DockerFile ```yaml FROM python MAINTAINER sys "sheyinsong@unotes.co" RUN apt-get update -y RUN apt-get upgrade -y RUN apt-get install -y python-numpy RUN apt-get clean RUN pip install redis RUN pip install simplejson ADD ./noVNC/ /noVNC/ CMD ["python", "/noVNC/utils/websockify/run", "--web", "/noVNC", "0.0.0.0:10240", "--target-config", "/noVNC/token.list"] ``` &emsp; #### noVNC下载链接 https://github.com/novnc/noVNC/releases
阅读 60 评论 0 收藏 0
阅读 60
评论 0
收藏 0

   2019-03-30 00:53:42    2019-07-23 09:52:13   

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 ]:你好,张三,我收到您的消息 ```
阅读 375 评论 0 收藏 0
阅读 375
评论 0
收藏 0

   2019-03-18 17:59:43    2019-11-14 14:34:03   

zookeeper
### 环境 三台Centos7 ```bash 主机名 IP n188 144.202.93.188 n19 144.202.80.19 n111 149.248.37.111 ```    ### 准备工作 #### 安装JDK1.8 https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html #### 配置JDK环境变量 ```bash $ tail -3 /etc/profile export JAVA_HOME=/usr/local/jdk1.8 export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar export PATH=$PATH:$JAVA_HOME/bin ``` #### 查看JDK ```bash $ java -version java version "1.8.0_201" Java(TM) SE Runtime Environment (build 1.8.0_201-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode) ``` #### 配置hosts ```bash $ tail -3 /etc/hosts 144.202.93.188 n188 144.202.80.19 n19 149.248.37.111 n111 ```    ### 安装zookeeper #### 下载zookeeper(三台配置一样) http://zookeeper.apache.org/releases.html#download #### 配置zookeeper(三台配置一样) ```bash $ grep -v "^#" zoo.cfg tickTime=2000 initLimit=10 syncLimit=5 dataDir=/opt/zookeeper/data clientPort=2181 autopurge.purgeInterval=1 server.1=n188:2888:3888 server.2=n19:2888:3888 server.3=n111:2888:3888 ``` #### 创建数据目录(三台机器一样) ```bash $ mkdir -p /opt/zookeeper/{logs,data} ``` #### 配置myid n188机器 ```bash $ echo 1 >/opt/zookeeper/data/myid ``` n19机器 ```bash $ echo 2 >/opt/zookeeper/data/myid ``` n111机器 ```bash $ echo 3 >/opt/zookeeper/data/myid ``` 启动zookeeper #### 查看三台集群状态 n188机器 ```bash $./zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/zookeeper/bin/../conf/zoo.cfg Mode: follower ``` n19机器 ```bash $./zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/zookeeper/bin/../conf/zoo.cfg Mode: leader ``` n111机器 ```bash $./zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/zookeeper/bin/../conf/zoo.cfg Mode: follower ```
阅读 38 评论 0 收藏 0
阅读 38
评论 0
收藏 0

   2019-03-12 14:51:06    2019-07-23 09:53:02   

对称加密 非对称加密 摘要算法
### 加密算法 说明:这里为了方便介绍,引入发送方A和接收方B       #### 非对称加密算法(被加密数据的安全性) `场景`:加密(RSA/DH/Elgamal/ECC)少量数据、数字签名(RSA/DSA/Elgamal) `算法`:DH RSA DSA ECC(椭圆曲线算法) Elgamal Merkle-Hellman(背包算法) Miller-Rabin(素数测试算法) 加密/解密场景 ```bash 发送方A 加密:DH/RSA/DSA公钥(接收方B)+对称加密的KEY-->密文KEY 接收方B 解密:DH/RSA/DSA私钥(接收方B)+密文KEY-->对称加密的KEY ``` 数字签名/验签场景 ```bash 发送方A 签名: 1.(SHA1/MD5+明文)->摘要1 2.摘要1+(RSA/Elgamal)私钥(发送方A)-->数字签名 接收方B 验签: 1. RSA公钥(发送方A)+数字签名-->摘要1(解密出来的) 2. SHA1/MD5+明文->摘要2(接收方B自己计算的) 3. 摘要1==摘要2 ?"成功":"失败" (对比摘要1和摘要2的值是否一致) ```    #### 对称加密算法 `场景`:适合加密大数据 `算法`:AES DES 3DES PBE RC5 Blowfish 加密/解密场景 ```bash 加密过程:明文+AES/DES+对称加密的KEY->密文 解密过程:密文+AES/DES+对称加密的KEY->明文 ``` AES加密算法的Counter模式 - ECB(电子密码本模式) - CTR(计数器模式) - GCM(CTR和GMAC结合)    #### AEAD加密方式(Authenticated Encryption with Associated Data (AEAD) `介绍`:一种同时具备保密性,完整性和可认证性的加密形式 AEAD - EtM方式:加密 then MAC - MtE方式:MAC then 加密 - E&M方式:加密 and MAC 真正的AEAD - GCM=(AES-CTR模式+GMAC结合) - ChaCha20-IETF-Poly1305 - XChaCha20-IETF-Poly1305    #### 摘要算法 `算法` MD5 SHA1 MAC GMAC ```bash 摘要操作:MD5/SHA1+文本->固定长度的HASH值 ```
阅读 186 评论 0 收藏 0
阅读 186
评论 0
收藏 0

   2019-02-27 22:59:30    2019-11-14 14:33:50   

openssl 数字签名 加密
`简单介绍:` `公钥加密,私钥解密(数据的安全性):发送方使用接收方的公钥加密的密文,仅有接收方的私钥才可解密,保证了数据的安全。(公私钥加解密的对象是对称加密算法的密码文件或钥匙,对称加密算法加密数据文件)` `私钥签名,公钥验签(数据的不可抵赖性/数据的完整性):接收方使用发送方的公钥验证数字签名(发送方通过自己的私钥对数据摘要信息做签名),验证成功,则数据是不可抵赖。接收方对数据内容做摘要信息同发送方的数字签名的摘要信息做对比,摘要信息一致,则数据是完整无篡改的。` ### openssl使用对称加密算法加密大文件 1、使用aes-128-cbc算法加密文件: ```bash $ openssl enc -e -aes-128-cbc -in abc.txt -out enc_abc.txt ``` 2、使用aes-128-cbc算法解密文件: ```bash $ openssl enc -d -aes-128-cbc -in enc_abc.txt -out abc.txt ``` 3、使用aes-128-cbc算法免交互输入密码加密文件: ```bash $ openssl enc -e -aes-128-cbc -in abc.txt -out enc_abc.txt -pass pass:123456 ``` 4、使用aes-128-cbc算法使用密码文件解密文件: ```bash $ echo 123456 > passwd $ openssl enc -d -aes-128-cbc -in enc_abc.txt -out abc.txt -pass file:123456 ``` ``` 对称加密的使用中,-pass本质是采用key和iv向量进行加密的,-pass会转换成key和iv ``` 5、使用aes-128-cbc算法指定IV和KEY进行加密 ```bash $ openssl enc -e -aes-128-cbc -K 12345 -iv f888 -in abc.txt -out enc_abc.txt -p ``` 6、使用aes-128-cbc算法指定IV和KEY进行解密 ```bash $ openssl enc -d -aes-128-cbc -K 12345 -iv f888 -in enc_abc.txt -out abc.txt ``` 7、不加盐值进行加密(默认会加入随机盐值,如果不加入盐值同时密码一样,则每次加密的密文都是一样的) ```bash $ openssl enc -e -aes-128-cbc -nosalt -in abc.txt -out enc_abc.txt -p ``` ``` key=E10ADC3949BA59ABBE56E057F20F883E iv =65B4AD270B3B98098D256AB32F5B8FBA ``` 8、不加盐值进行解密 ```bash $ openssl enc -d -aes-128-cbc -nosalt -in enc_abc.txt -out abc7.txt -p ``` ``` key=81DC9BDB52D04DC20036DBD8313ED055 iv =CC5776D16A1FB6E4AFA34B18395DA656 ```    ### openssl使用非对称加密算法加密小文件 生成私钥 ```bash $ openssl genrsa -out rsa.key 2048 ``` 通过私钥导出公钥 ```bash $ openssl rsa -in rsa.key -pubout -out pub.key ``` 公钥加密文件 ```bash $ openssl rsautl -encrypt -inkey pub.key -pubin -in abc.txt -out abc_rsa.txt ``` 私钥解密文件 ```bash $ openssl rsautl -decrypt -inkey rsa.key -in abc_rsa.txt -out abc_rsa_de.txt ```    ### openssl使用对称加密算法和非对称加密算法加密大文件 `这里以发送方A和接收方B为例` #### 发送方A A.1 生成随机密码文件 ```bash $ openssl rand -hex 64 -out key.bin ``` A.2 使用加密算法aes-256-cbc和密码文件加密大文件 ```bash $ openssl enc -aes-256-cbc -salt -in largefile.pdf -out largefile.pdf.enc -pass file:./bin.key ``` A.3 使用接收方B的公钥加密密码文件 ```bash $ openssl rsautl -encrypt -inkey publickey.pem -pubin -in key.bin -out key.bin.enc ``` A.4 发送方A发送加密文件 largefile.pdf.enc(对称加密文件)和key.bin.enc(非对称加密文件/公钥加密文件)以及加密算法-aes-256-cbc给接收方B ``` A----largefile.pdf.enc---->B A-------key.bin.enc------->B A---aes-256-cbc加密算法---->B ``` #### 接收方B B.1 使用接收方B的私钥解密密码文件 ```bash $ openssl rsautl -decrypt -inkey privatekey.pem -in key.bin.enc -out key.bin ``` B.2 使用解密算法aes-256-cbc以及密码文件解密文件 ```bash $ openssl enc -d -aes-256-cbc -in largefile.pdf.enc -out largefile.pdf -pass file:./bin.key ```    ### openssl使用非对称加密做数字签名(私钥签名,公钥验签) `注意:这里只是说明验签过程,没有对data.txt文件进行加密传送,加密请看上面` #### 发送方A 生成数字签名 ```bash $ openssl dgst -sha256 -sign private.key -out signature data.txt ``` A把文件和数字签名一起发送给B ``` A-----data.txt--->B A--signature----->B ```    #### 接收方B 验证数字签名 ```bash $ openssl dgst -sha256 -verify public.key -signature signature data.txt ``` ``` Verified OK 验签成功 Verification Failure 验签失败 ```
阅读 42 评论 0 收藏 0
阅读 42
评论 0
收藏 0

第 1 页 / 共 7 页
 
第 1 页 / 共 7 页