这里写自定义目录标题
说明
jupyter 基于tornado web框架。
github地址:https://github.com/1060905996/notebook-master
需求和思路
需求
在系统上集成jupyter供用户使用生成编码。会有多个用户使用jupyter,为避免不同用户误删除其他用户的文件,要求每个用户只能对自己的文件进行增删该查。
思路
为每个用户在配置路劲下创建目录,每个用户只能对自己目录下文件和数据进行增删该查。
jupyter开发环境搭建
项目基于notebook version(7, 0, 0, ‘.dev0’),notebook版本可以在notebook/_version.py下查看。最好在linux下开发,我在windows下无法debug(可能是环境没配置好)。
下载
下载地址: https://github.com/jupyter/notebook , 历史版本:https://github.com/jupyter/notebook/releases。
开发环境配置
参考文档:https://github.com/jupyter/notebook/blob/master/CONTRIBUTING.rst
在pycharm中引入项目,并配置python环境。
安装依赖
cd notebook
pip install .
npm run build
解决启动错误
将__main__.py 移动到和notebook同等级目录即可解决。
项目启动文件为notebook文件夹下__main__.py,直接在__main__.py中点击[run]启动会报错,需要修改部分文件夹名称。
需要需改的文件夹如下:
notebook/notebook
notebook/nbconvert
对应文件名可以修改为任意名称,并在notebookapp.py中修改对应加载类路径,重新启动main.py。文件夹修改如下:
配置文件
为了方便管理,将配置文件jupyter_notebook_config.py放在notebook目录下,并修改配置,
# 允许所有ip访问
c.NotebookApp.allow_origin = '*'
c.NotebookApp.ip='*'
# 设置验证参数token
c.NotebookApp.token = 'mytoken'
# 解决跨域问题
c.NotebookApp.tornado_settings = {
'headers': {
'Content-Security-Policy': "frame-ancestors self *",
}
}
# jupyter工作目录
c.NotebookApp.notebook_dir ='/opt/workspace/jupyter'
# 启动时不在浏览器打开
c.NotebookApp.open_browser = False
# 端口
c.NotebookApp.port = 8001
项目目录
│ │ ├─
│ │ │ ├─
│ │ │ │ ├─
├─ notebooke
│ ├─ auth
│ │ ├─ __mian__.py --jupyter notebook password
│ │ ├─ login.py -- 登录handler
│ │ │ ├─ get.method
│ │ │ ├─ pst.method
│ │ ├─ logout.py -- 登出handler
│ │ ├─ security.py -- 设置和校验密码
│ ├─ base
│ │ ├─ handlers.py -- 所有handler基础类类
│ │ │ ├─AuthenticatedHandler.class --用户校验
│ │ │ ├─IPythonHandler.class --系统设置
│ │ │ ├─APIHandler.class --服务接口
│ │ │ ├─Template404.class --模板不存在
│ │ │ ├─AuthenticatedFileHandler.class --静态文件
│ │ ├─
│ ├─services
│ │ │ ├─contents
│ │ │ │ ├─ fileio.py -- 文件管理基础类
│ │ │ │ ├─ filemanage.py -- 文件管理业务实现
│ │ │ │ ├─ handlers.py -- 文件管理接口部分
│ │ │ │ │ ├─ get.method --查询
│ │ │ │ │ ├─ put.method --保存
│ │ │ │ │ ├─ post.method --新增文件
│ │ │ │ │ ├─ delete.method --删除
│ │ │ │ │ ├─ patch.method --移动或重命名
│ ├─ session --使框架支持session
│ │ │ ├─ all.py
│ │ │ ├─ session
│ ├─ static --js,css
│ ├─ template --模板文件
│ ├─ __main__.py --启动文件
│ ├─ _version.py --版本
│ ├─ notebookapp.py --初始化项目
代码修改
如何区分用户
jupyter可以使用token和password登录,但两者都无法对不同用户进行区分。所有在登录中加入user_name参数,用于区分用户。请求示例:http://192.168.5.138:8001/login?token=mytoken&user_name=kevin
如何记住用户
可以使用cookie和session记录登录的用户,个人选择使用session。tornado默认使不支持session的,为此我们需要新增session模块。转自博客:https://www.jianshu.com/p/a14f24700a6b。
all.py
# -*- coding:utf-8 -*-
# 存储所有的用户信息
ALL_USER_DIC = {}
session.py
# -*- coding:utf-8 -*-
from .all import ALL_USER_DIC
class Session:
def __init__(self, handler):
self.handler = handler
self.random_index_str = None
def __get_random_str(self):
import hashlib, time
# 生成md5对象
md = hashlib.md5()
# 加入自定义参数来更新md5对象
md.update(bytes(str(time.time()) + ' | own-secret', encoding='utf-8'))
# 得到加盐后的十六进制随机字符串来作为用户的索引
return md.hexdigest()
def __setitem__(self, key, value):
# 当前session对象中没有对应的索引的时候
if not self.random_index_str:
# 根据处理器对象获得浏览器传来的cookie的值
random_index_str = self.handler.get_secure_cookie("__sson__", None)
# 浏览器传来的cookie的值为空的时候, 表示该用户是第一次访问本网站
if not random_index_str:
# 为当前的新用户在当前的session对象中生成索引
self.random_index_str = self.__get_random_str()
# 为当前新用户设置cookie
self.handler.set_secure_cookie('__sson__', self.random_index_str)
# 为当前用户生成保存其相关内容的字典对象
ALL_USER_DIC[self.random_index_str] = {}
# 当浏览器传来的cookie不为空的时候
else:
# 浏览器传来的cookie非法的时候
if self.random_index_str not in ALL_USER_DIC.keys():
# 为当前非法用户生产索引
self.random_index_str = self.__get_random_str()
# 仅仅为当前非法用户生成其保存相关内容的字典对象, 避免合法老用户的字典对象被清空
ALL_USER_DIC[self.random_index_str] = {}
# 不管当前session对象有没有对应的索引都应该为他设置起相关的信息保存(当然了, 到这一步的时候经过if条件语句的过滤, 剩下来的就是刚刚创建字典对象的新用户或者非法用户, 以及其他合法的老用户了)
ALL_USER_DIC[self.random_index_str][key] = value
# 将为以上的新用户或者非法用户设置cookie的操作放在这里本无可厚非. 但是将老用户的cookie也重新设置一遍, 其实是为老用户更新过期时间而做的
self.handler.set_secure_cookie('__sson__', self.random_index_str)
def __getitem__(self, key):
# 获取当前用户cookie中保存的索引值, 注意加密方式返回的cookie的值是bytes类型的
self.random_index_str = self.handler.get_secure_cookie('__sson__', None)
# 若索引值为空表示当前用户是新用户, 则直接返回空, 程序到此终止
if not self.random_index_str:
return None
# 索引不为空的时候
else:
self.random_index_str = str(self.random_index_str, encoding="utf-8")
# 在服务器端为保存该索引值表示当前用户是非法用户,则直接返回空
current_user = ALL_USER_DIC.get(self.random_index_str, None)
if not current_user:
return None
else:
# 直接返回合法用户指定的key的值, 没有则默认返回空
return current_user.get(key, None)
在登录时记录用户
def get(self):
user_name = self.get_argument("user_name")
self.session["user"] = user_name
不同用户不同目录
方案1:
在调试代码中发现,在获取路径时都会通过fileio.py中_get_os_path获取根目录,然后根据传入path参数拼接路径。可以在方法中获取user_name,根据path,user_name和配置文件拼接参数。
缺点:
1. 无法在_get_os_path中获取当前用户
2. 可以在_get_os_path新增user_name参数,但这样需要修改的代码过于多,而且修改后不止是否有其他问题。
方案1被否决。
本来都要放弃jupyter去研究polynote了,但突然灵光一闪想到方案2。
方案2:
如果改变用户根目录比较困难,可以限制用户只能对自己目录下文件操作,这样也一样满足需求。根据这个思路调试代码最终找到services/contents/handlers.py,文件中重要方法如下:
get 查询文件
put 保存文件
post 新增文件
delete 删除文件
patch 移动或重命名文件
可以看出tornado是遵守RESTful APi接口规范的。这些方法都有相同的参数[path],path为请求参数,值为操作的目录或文件。_get_os_path(path)返回值为文件在机器上真实路径。
为让每个用户只能操作自己目录,只需要在这些方法中增加校验。判断条件:如当前用户为admin,那么在path参数必须以admin开头,不满足条件即无法修改文件。示例如下:
```
@gen.coroutine
def patch(self, path=''):
"""PATCH renames a file or directory without re-uploading content."""
print("PATCH path = " + path)
if not path.startswith(self.user_root_dir):
raise HTTPError(500, "您没有修改 %s 的权限" % path)
```
在方法中分别添加校验,现阶段满足需求要求,并且未发现问题。
优化体验
- 用户在每次登录时看到用户列表,而不是进入自己的文件目录
- 如果用户首次登录,不会针对用户创建新目录
login.py
def get(self):
user_name = self.get_argument("user_name")
self.session["user"] = user_name
if not self.contents_manager.dir_exists(user_name):
# 判断用户路径是否存在,如果不存在创建路径
user_path = self.contents_manager._get_os_path(user_name)
os.makedirs(user_path)
if self.current_user:
# 设置登录后默认调转路由
next_url = self.get_argument('next', default=self.default_url + "/"+user_name)
self._redirect_safe(next_url)
else:
self._render()
- 嵌入系统后,jupyter图标占用较多高度,而且logout等按钮需要隐藏。
notebook/templates/page.html
<div id="header" role="navigation" aria-label="{% trans %}Top Menu{% endtrans %}">
<div id="header-container" class="container">
<!-- <div id="ipython_notebook" class="nav navbar-brand"><a href="{{default_url}}
{%- if logged_in and token -%}?token={{token}}{%- endif -%}" title='{% trans %}dashboard{% endtrans %}'>
{% block logo %}<img src='{{static_url("base/images/logo.png") }}' alt='Jupyter Notebook'/>{% endblock %}
</a></div>-->
<!-- {% block headercontainer %}
{% endblock %}-->
{% block header_buttons %}
<!-- {% block login_widget %}
<span id="login_widget">
{% if logged_in %}
<button id="logout" class="btn btn-sm navbar-btn">{% trans %}Logout{% endtrans %}</button>
{% elif login_available and not logged_in %}
<button id="login" class="btn btn-sm navbar-btn">{% trans %}Login{% endtrans %}</button>
{% endif %}
</span>
{% endblock %}-->
{% endblock header_buttons %}
</div>
<!-- <div class="header-bar"></div>-->
{% block header %}
{% endblock %}
</div>