Unverified Commit a7503944 authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #103 from jumpserver/dev

支持web sftp
parents 134dc0cc 62007ba0
#!/usr/bin/python
#
from coco.httpd import app, socket_io
from coco.logger import create_logger
create_logger()
if __name__ == '__main__':
socket_io.run(app, debug=False)
...@@ -19,9 +19,7 @@ from .sshd import SSHServer ...@@ -19,9 +19,7 @@ from .sshd import SSHServer
from .httpd import HttpServer from .httpd import HttpServer
from .logger import create_logger from .logger import create_logger
from .tasks import TaskHandler from .tasks import TaskHandler
from .utils import get_logger, ugettext as _, \ from .utils import get_logger, ugettext as _, ignore_error
ignore_error
from .service import init_app
from .ctx import app_service from .ctx import app_service
from .recorder import get_replay_recorder from .recorder import get_replay_recorder
from .session import Session from .session import Session
...@@ -44,8 +42,6 @@ class Coco: ...@@ -44,8 +42,6 @@ class Coco:
self.replay_recorder_class = None self.replay_recorder_class = None
self.command_recorder_class = None self.command_recorder_class = None
self._task_handler = None self._task_handler = None
self.config = config
init_app(self)
@property @property
def sshd(self): def sshd(self):
...@@ -66,7 +62,7 @@ class Coco: ...@@ -66,7 +62,7 @@ class Coco:
return self._task_handler return self._task_handler
def make_logger(self): def make_logger(self):
create_logger(self) create_logger()
@staticmethod @staticmethod
def load_extra_conf_from_server(): def load_extra_conf_from_server():
...@@ -78,7 +74,7 @@ class Coco: ...@@ -78,7 +74,7 @@ class Coco:
def bootstrap(self): def bootstrap(self):
self.make_logger() self.make_logger()
app_service.initial() # app_service.initial()
self.load_extra_conf_from_server() self.load_extra_conf_from_server()
self.keep_heartbeat() self.keep_heartbeat()
self.monitor_sessions() self.monitor_sessions()
...@@ -86,7 +82,7 @@ class Coco: ...@@ -86,7 +82,7 @@ class Coco:
@ignore_error @ignore_error
def heartbeat(self): def heartbeat(self):
_sessions = [s.to_json() for s in Session.sessions.values()] _sessions = [s.to_json() for s in Session.sessions.values() if s]
tasks = app_service.terminal_heartbeat(_sessions) tasks = app_service.terminal_heartbeat(_sessions)
if tasks: if tasks:
self.handle_task(tasks) self.handle_task(tasks)
...@@ -106,7 +102,10 @@ class Coco: ...@@ -106,7 +102,10 @@ class Coco:
def keep_heartbeat(self): def keep_heartbeat(self):
def func(): def func():
while not self.stop_evt.is_set(): while not self.stop_evt.is_set():
try:
self.heartbeat() self.heartbeat()
except Exception as e:
logger.error("Unexpected error occur: {}".format(e))
time.sleep(config["HEARTBEAT_INTERVAL"]) time.sleep(config["HEARTBEAT_INTERVAL"])
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.start() thread.start()
...@@ -152,6 +151,7 @@ class Coco: ...@@ -152,6 +151,7 @@ class Coco:
def func(): def func():
while not self.stop_evt.is_set(): while not self.stop_evt.is_set():
try:
sessions_copy = [s for s in Session.sessions.values()] sessions_copy = [s for s in Session.sessions.values()]
for s in sessions_copy: for s in sessions_copy:
# Session 没有正常关闭, # Session 没有正常关闭,
...@@ -163,6 +163,8 @@ class Coco: ...@@ -163,6 +163,8 @@ class Coco:
Session.remove_session(s) Session.remove_session(s)
else: else:
check_session_idle_too_long(s) check_session_idle_too_long(s)
except Exception as e:
logger.error("Unexpected error occur: {}".format(e))
time.sleep(interval) time.sleep(interval)
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.start() thread.start()
......
...@@ -311,3 +311,6 @@ try: ...@@ -311,3 +311,6 @@ try:
config.from_object(_conf) config.from_object(_conf)
except ImportError: except ImportError:
pass pass
if not config['NAME']:
config['NAME'] = default_config['NAME']
...@@ -19,5 +19,6 @@ def _find(name): ...@@ -19,5 +19,6 @@ def _find(name):
app_service = AppService(config) app_service = AppService(config)
app_service.initial()
current_app = LocalProxy(partial(_find, 'current_app')) current_app = LocalProxy(partial(_find, 'current_app'))
# app_service = LocalProxy(partial(_find, 'app_service')) # app_service = LocalProxy(partial(_find, 'app_service'))
# -*- coding: utf-8 -*-
#
from .app import HttpServer, app, socket_io
from . import view
# -*- coding: utf-8 -*-
#
from flask_socketio import SocketIO
from flask import Flask
from coco.utils import get_logger
from coco.config import config
from coco.httpd.ws import ProxyNamespace, ElfinderNamespace
logger = get_logger(__file__)
app = Flask(__name__, template_folder='templates',
static_folder='static')
app.config.update(config)
socket_io = SocketIO()
socket_io.on_namespace(ProxyNamespace('/ssh'))
socket_io.on_namespace(ElfinderNamespace('/elfinder'))
# init_kwargs = {'async_mode': 'threading'}
init_kwargs = {'async_mode': 'eventlet'}
socket_io.init_app(app, **init_kwargs),
socket_io.on_error_default(lambda x: logger.exception(x))
class HttpServer:
@staticmethod
def run():
host = config["BIND_HOST"]
port = config["HTTPD_PORT"]
print('Starting websocket server at {}:{}'.format(host, port))
socket_io.run(app, port=port, host=host, debug=False)
@staticmethod
def shutdown():
socket_io.stop()
pass
# -*- coding: utf-8 -*-
#
from functools import wraps
from flask import request, abort
from ..ctx import app_service
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
session_id = request.cookies.get('sessionid', '')
csrf_token = request.cookies.get('csrftoken', '')
x_forwarded_for = request.headers.get("X-Forwarded-For", '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
remote_ip = x_forwarded_for[0]
else:
remote_ip = request.remote_addr
request.real_ip = remote_ip
if session_id and csrf_token:
user = app_service.check_user_cookie(session_id, csrf_token)
request.current_user = user
if not hasattr(request, 'current_user') or not request.current_user:
return abort(403)
response = func(*args, **kwargs)
return response
return wrapper
# -*- coding: utf-8 -*-
#
import os
import uuid
from flask_socketio import SocketIO, Namespace, join_room
from flask import Flask, request
from ..models import Connection, WSProxy
from ..proxy import ProxyServer
from ..utils import get_logger
from ..ctx import app_service
from ..config import config
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__)
class BaseNamespace(Namespace):
current_user = None
def on_connect(self):
self.current_user = self.get_current_user()
logger.debug("{} connect websocket".format(self.current_user))
def get_current_user(self):
session_id = request.cookies.get('sessionid', '')
csrf_token = request.cookies.get('csrftoken', '')
user = None
if session_id and csrf_token:
user = app_service.check_user_cookie(session_id, csrf_token)
msg = "Get current user: session_id<{}> => {}".format(
session_id, user
)
logger.debug(msg)
request.current_user = user
return user
This diff is collapsed.
from .sftp import SFTPVolume
import base64
import os
import hashlib
import logging
logger = logging.getLogger(__name__)
class BaseVolume:
def __init__(self, *args, **kwargs):
self.base_path = '/'
self.path_sep = '/'
self.dir_mode = '0o755'
self.file_mode = '0o644'
#
# @classmethod
# def get_volume(cls, request):
# raise NotImplementedError
def close(self):
pass
def get_volume_id(self):
""" Returns the volume ID for the volume, which is used as a prefix
for client hashes.
"""
raise NotImplementedError
def _remote_path(self, path):
path = path.lstrip(self.path_sep)
return self._join(self.base_path, path)
def _path(self, _hash):
"""
通过_hash获取path
:param _hash:
:return:
"""
if _hash in ['', '/']:
return self.path_sep
volume_id, path = self._get_volume_id_and_path_from_hash(_hash)
if volume_id != self.get_volume_id():
return self.path_sep
return path
def _remote_path_h(self, _hash):
path = self._path(_hash)
return self._remote_path(path)
def _is_root(self, path):
return path == self.path_sep
def _hash(self, path):
"""
通过path生成hash
:param path:
:return:
"""
if not self._is_root(path):
path = path.rstrip(self.path_sep)
_hash = "{}_{}".format(
self.get_volume_id(),
self._encode(path)
)
return _hash
@staticmethod
def _digest(s):
m = hashlib.md5()
m.update(s.encode())
return str(m.hexdigest())
@classmethod
def _get_volume_id_and_path_from_hash(cls, _hash):
volume_id, _path = _hash.split('_', 1)
return volume_id, cls._decode(_path)
def _encode(self, path):
if not self._is_root(path):
path = path.lstrip('/')
if isinstance(path, str):
path = path.encode()
_hash = base64.b64encode(path).decode()
_hash = _hash.translate(str.maketrans('+/=', '-_.')).rstrip('.')
return _hash
@staticmethod
def _decode(_hash):
_hash = _hash.translate(str.maketrans('-_.', '+/='))
_hash += "=" * ((4 - len(_hash) % 4) % 4)
if isinstance(_hash, str):
_hash = _hash.encode()
_hash = base64.b64decode(_hash).decode()
return _hash
@staticmethod
def _base_name(path):
return os.path.basename(path)
def _dir_name(self, path):
if path in ['', '/']:
return self.path_sep
path = path.rstrip('/')
parent_path = os.path.dirname(path)
return parent_path
@staticmethod
def _join(*args):
return os.path.join(*args)
def read_file_view(self, request, target):
""" Django view function, used to display files in response to the
'file' command.
:param request: The original HTTP request.
:param target: The hash of the target file.
:returns: dict -- a dict describing the new directory.
"""
raise NotImplementedError
def info(self, target):
""" Returns a dict containing information about the target directory
or file. This data is used in response to 'open' commands to
populates the 'cwd' response var.
:param target: The hash of the directory for which we want info.
If this is '', return information about the root directory.
:returns: dict -- A dict describing the directory.
"""
raise NotImplementedError
def mkdir(self, name, parent):
""" Creates a directory.
:param name: The name of the new directory.
:param parent: The hash of the parent directory.
:returns: dict -- a dict describing the new directory.
"""
raise NotImplementedError
def mkfile(self, name, parent):
""" Creates a directory.
:param name: The name of the new file.
:param parent: The hash of the parent directory.
:returns: dict -- a dict describing the new file.
"""
raise NotImplementedError
def rename(self, name, target):
""" Renames a file or directory.
:param name: The new name of the file/directory.
:param target: The hash of the target file/directory.
:returns: dict -- a dict describing which objects were added and
removed.
"""
raise NotImplementedError
def list(self, target, name_only=False):
""" Lists the contents of a directory.
:param target: The hash of the target directory.
:param name_only: Only return the name
:returns: list -- a list containing the names of files/directories
in this directory.
"""
raise NotImplementedError
def tree(self, target):
""" Get the sub directory of directory
:param target: The hash of the target directory.
:return: list - a list of containing the names of sub directories
"""
raise NotImplementedError
def parents(self, target, deep=0):
""" Returns all parent folders and its sub directory on required deep
This command is invoked when a directory is reloaded in the client.
Data provided by 'parents' command should enable the correct drawing
of tree hierarchy directories.
:param target: The hash of the target directory.
:param deep: The deep to show
:return list - a list of containing parent and sub directory info
"""
raise NotImplementedError
def paste(self, targets, dest, cut):
""" Moves/copies target files/directories from source to dest.
If a file with the same name already exists in the dest directory
it should be overwritten (the client asks the user to confirm this
before sending the request).
:param targets: A list of hashes of files/dirs to move/copy.
:param dest: The new parent of the targets.
:param cut: Boolean. If true, move the targets. If false, copy the
targets.
:returns: dict -- a dict describing which targets were moved/copied.
"""
raise Exception("Not support paste")
def remove(self, targets):
""" Deletes the target files/directories.
The 'rm' command takes a list of targets - this function is called
for each target, so should only delete one file/directory.
:param targets: A list of hashes of files/dirs to delete.
:returns: string -- the hash of the file/dir that was deleted.
"""
raise NotImplementedError
def upload(self, files, parent):
""" Uploads one or more files in to the parent directory.
:param files: A list of uploaded file objects, as described here:
https://docs.djangoproject.com/en/dev/topics/http/file-uploads/
:param parent: The hash of the directory in which to create the
new files.
:returns: TODO
"""
raise NotImplementedError
def upload_as_chunk(self, files, chunk_name, parent):
"""
Upload a large file as chunk
:param files:
:param chunk_name:
:param cid:
:param parent:
:return:
"""
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['elfinder'], factory);
} else if (typeof exports !== 'undefined') {
module.exports = factory(require('elfinder'));
} else {
factory(root.elFinder);
}
}(this, function(elFinder) {
"use strict";
try {
if (! elFinder.prototype.commands.quicklook.plugins) {
elFinder.prototype.commands.quicklook.plugins = [];
}
elFinder.prototype.commands.quicklook.plugins.push(function(ql) {
var fm = ql.fm,
preview = ql.preview;
preview.on('update', function(e) {
var win = ql.window,
file = e.file, node, loading;
if (file.mime.indexOf('application/vnd.google-apps.') === 0) {
if (file.url == '1') {
preview.hide();
$('<div class="elfinder-quicklook-info-data"><button class="elfinder-info-button">'+fm.i18n('getLink')+'</button></div>').appendTo(ql.info.find('.elfinder-quicklook-info'))
.on('click', function() {
$(this).html('<span class="elfinder-info-spinner">');
fm.request({
data : {cmd : 'url', target : file.hash},
preventDefault : true
})
.always(function() {
preview.show();
$(this).html('');
})
.done(function(data) {
var rfile = fm.file(file.hash);
ql.value.url = rfile.url = data.url || '';
if (ql.value.url) {
preview.trigger($.Event('update', {file : ql.value}));
}
});
});
}
if (file.url !== '' && file.url != '1') {
e.stopImmediatePropagation();
preview.one('change', function() {
loading.remove();
node.off('load').remove();
});
loading = $('<div class="elfinder-quicklook-info-data">'+fm.i18n('nowLoading')+'<span class="elfinder-info-spinner"></div>').appendTo(ql.info.find('.elfinder-quicklook-info'));
node = $('<iframe class="elfinder-quicklook-preview-iframe"/>')
.css('background-color', 'transparent')
.appendTo(preview)
.on('load', function() {
ql.hideinfo();
loading.remove();
$(this).css('background-color', '#fff').show();
})
.attr('src', fm.url(file.hash));
}
}
});
});
} catch(e) {}
}));
!function(n,e){"function"==typeof define&&define.amd?define(["elfinder"],e):"undefined"!=typeof exports?module.exports=e(require("elfinder")):e(n.elFinder)}(this,function(n){"use strict";try{n.prototype.commands.quicklook.plugins||(n.prototype.commands.quicklook.plugins=[]),n.prototype.commands.quicklook.plugins.push(function(n){var e=n.fm,o=n.preview;o.on("update",function(i){var t,l,r=(n.window,i.file);0===r.mime.indexOf("application/vnd.google-apps.")&&("1"==r.url&&(o.hide(),$('<div class="elfinder-quicklook-info-data"><button class="elfinder-info-button">'+e.i18n("getLink")+"</button></div>").appendTo(n.info.find(".elfinder-quicklook-info")).on("click",function(){$(this).html('<span class="elfinder-info-spinner">'),e.request({data:{cmd:"url",target:r.hash},preventDefault:!0}).always(function(){o.show(),$(this).html("")}).done(function(i){var t=e.file(r.hash);n.value.url=t.url=i.url||"",n.value.url&&o.trigger($.Event("update",{file:n.value}))})})),""!==r.url&&"1"!=r.url&&(i.stopImmediatePropagation(),o.one("change",function(){l.remove(),t.off("load").remove()}),l=$('<div class="elfinder-quicklook-info-data">'+e.i18n("nowLoading")+'<span class="elfinder-info-spinner"></div>').appendTo(n.info.find(".elfinder-quicklook-info")),t=$('<iframe class="elfinder-quicklook-preview-iframe"/>').css("background-color","transparent").appendTo(o).on("load",function(){n.hideinfo(),l.remove(),$(this).css("background-color","#fff").show()}).attr("src",e.url(r.hash))))})})}catch(e){}});
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
(function(factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports !== 'undefined') {
module.exports = factory();
} else {
factory();
}
}(this, function() {
return void 0;
}));
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<h2>Tipy na obsluhu</h2>
<p>Obsluha na uživatelském rozhraní je podobná standardnímu správci souborů operačního systému. Drag and Drop však není možné používat s mobilními prohlížeči. </p>
<ul>
<li>Kliknutím pravým tlačítkem nebo dlouhým klepnutím zobrazíte kontextové menu.</li>
<li>Přetáhněte do stromu složek nebo do aktuálního pracovního prostoru a přetáhněte / kopírujte položky.</li>
<li>Výběr položky v pracovním prostoru můžete rozšířit pomocí kláves Shift nebo Alt (Možnost).</li>
<li>Přemístěte soubory a složky do cílové složky nebo do pracovního prostoru.</li>
<li>Dialog předávání může přijímat data schránky nebo seznamy adres URL a přitáhnout a odejít z jiných prohlížečů nebo správců souborů.</li>
<li>Zatažením spusťte stisknutím klávesy Alt (Možnost) přetáhněte do vnějšího prohlížeče. Tato funkce se převezme pomocí prohlížeče Google Chrome.</li>
</ul>
<h2>Operation Tips</h2>
<p>Operation on the UI is similar to operating system&#39;s standard file manager. However, Drag and Drop is not possible with mobile browsers. </p>
<ul>
<li>Right click or long tap to show the context menu.</li>
<li>Drag and drop into the folder tree or the current workspace to move/copy items.</li>
<li>Item selection in the workspace can be extended selection with Shift or Alt (Option) key.</li>
<li>Drag and Drop to the destination folder or workspace to upload files and folders.</li>
<li>The upload dialog can accept paste/drop clipboard data or URL lists and Drag and Drop from other browser or file managers etc.</li>
<li>Drag start with pressing Alt(Option) key to drag out to outside browser. It will became download operation with Google Chrome.</li>
</ul>
<h2>Consejos de operaci&oacute;n</h2>
<p>Operar en la Interfaz del Usuario es similar al administrador de archivos estandar del sistema operativo. Sin embargo, Arrastrar y soltar no es posible con los navegadores m&oacute;viles.</p>
<ul>
<li>Click derecho o un tap largo para mostrar el men&uacute; de contexto.</li>
<li>Arrastrar y soltar dentro del &aacute;rbol de carpetas o el espacio de trabajo actual para mover/copiar elementos.</li>
<li>La selecci&oacute;n de elementos en el espacio de trabajo puede ampliarse con la tecla Shift o Alt (Opci&oacute;n).</li>
<li>Arrastrar y soltar a la carpeta de destino o &aacute;rea de trabajo para cargar archivos y carpetas.</li>
<li>El cuadro de di&aacute;logo de carga puede aceptar pegar/soltar datos del portapapeles o listas de URL y arrastrar y soltar desde otro navegador o administrador de archivos, etc.</li>
<li>Iniciar a arrastrar presionando la tecla Alt (Opci&oacute;n) para arrastrar fuera del navegador. Se convertir&aacute; en una operaci&oacute;n de descarga con Google Chrome.</li>
</ul>
<h2>操作のヒント</h2>
<p>UIの操作は、オペレーティングシステムの標準ファイルマネージャにほぼ準拠しています。ただし、モバイルブラウザではドラッグ&ドロップはできません。</p>
<ul>
<li>右クリックまたはロングタップでコンテキストメニューを表示します。</li>
<li>アイテムを移動/コピーするには、フォルダツリーまたはワークスペースにドラッグ&ドロップします。</li>
<li>ワークスペース内のアイテムの選択は、ShiftキーまたはAltキー(Optionキー)で選択範囲を拡張できます。</li>
<li>コピー先のフォルダまたはワークスペースにドラッグアンドドロップして、ファイルとフォルダをアップロードします。</li>
<li>アップロードダイアログでは、クリップボードのデータやURLリストのペースト/ドロップ、他のブラウザやファイルマネージャからのドラッグ&ドロップなどを受け入れることができます。</li>
<li>Altキー(Optionキー)を押しながらドラッグすると、ブラウザの外にドラッグできます。Google Chromeでダウンロード操作になります。</li>
</ul>
<h2>사용 </h2>
<p>UI 조작은 운영체제의 표준 파일 관리자를 사용하는 방법과 비슷합니다. 하지만 모바일 브라우저에서는 드래그앤드롭을 사용할 없습니다. </p>
<ul>
<li>오른쪽 클릭하거나 길게 누르면 컨텍스트 메뉴가 나타납니다.</li>
<li>이동/복사하려면 폴더 트리 또는 원하는 폴더로 드래그앤드롭하십시오.</li>
<li>작업공간에서 항목을 선택하려면 Shift또는 Alt(Option) 키를 사용하여 선택 영역을 넓힐 있습니다.</li>
<li>업로드 대상 폴더 또는 작업 영역으로 파일및 폴더를 드래그앤드롭하여 업로드할 있습니다.</li>
<li>다른 브라우저 또는 파일관리자등에서 드래그앤드롭하거나, 클립보드를 통해 데이터또는 URL 복사/붙여넣어 업로드할 있습니다.</li>
<li>크롬브라우저의 경우, Alt(Option) 키를 누른 상태에서 브라우저 밖으로 드래그앤드롭하면 다운로드가 가능합니다.</li>
</ul>
<h2>Wskazówki Obsługi</h2>
<p>Działanie w interfejsie użytkownika jest podobne do standardowego menedżera plików systemu operacyjnego. Jednak Przeciąganie i Upuszczanie nie jest możliwe w przeglądarkach mobilnych. </p>
<ul>
<li>Kliknij prawym przyciskiem myszy lub dłużej, aby wyświetlić menu kontekstowe.</li>
<li>Przeciągnij i upuść w drzewie folderów lub bieżącym obszarze roboczym, aby przenieść/kopiować elementy.</li>
<li>Wybór elementu w obszarze roboczym można rozszerzyć wybór z klawiszem Shift lub Alt(Opcja).</li>
<li>Przeciągnij i Upuść do folderu docelowego lub obszaru roboczego, aby przesłać pliki i foldery.</li>
<li>W oknie dialogowym przesyłania można zaakceptować wklejanie/upuszczanie danych schowka lub listy adresów URL, i Przeciągnij i Upuść z innych przeglądarek lub menedżerów plików, itp.</li>
<li>Rozpocznij Przeciąganie naciskając Alt (Opcja), aby przeciągnąć na zewnątrz przeglądarki. Stanie się operacją pobierania z Google Chrome. </li>
</ul>
<h2>Советы по работе</h2>
<p>Работа с пользовательским интерфейсом похожа на стандартный файловый менеджер операционной системы. Однако перетаскивание в мобильных браузерах невозможно.</p>
<ul>
<li>Щелкните правой кнопкой мыши или используйте «длинный тап», чтобы отобразить контекстное меню.</li>
<li>Перетащите в дерево папок или текущую рабочую область для перемещения / копирования элементов.</li>
<li>Выбор элемента в рабочей области может быть расширен с помощью клавиши Shift или Alt (Option).</li>
<li>Перетащите в папку назначения или рабочую область для загрузки файлов и папок.</li>
<li>В диалоговом окне загрузки можно использовать вставку данных или списков URL-адресов из буфера обмена, а также перетаскивать из других браузеров или файловых менеджеров и т.д.</li>
<li>Начните перетаскивание, нажав Alt (Option), чтобы перетащить за пределы браузера. Это запустить процесс скачивания в Google Chrome.</li>
</ul>
<h2>Tipy na obsluhu</h2>
<p>Obsluha na používateľskom rozhraní je podobná štandardnému správcovi súborov operačného systému. Drag and Drop však nie je možné používať s mobilnými prehliadačmi. </p>
<ul>
<li>Kliknutím pravým tlačidlom alebo dlhým klepnutím zobrazíte kontextové menu.</li>
<li>Presuňte myšou do stromu priečinkov alebo do aktuálneho pracovného priestoru a presuňte / kopírujte položky.</li>
<li>Výber položky v pracovnom priestore môžete rozšíriť pomocou klávesov Shift alebo Alt (Možnosť).</li>
<li>Premiestnite súbory a priečinky do cieľovej zložky alebo do pracovného priestoru.</li>
<li>Dialog odovzdávania môže prijímať dáta schránky alebo zoznamy adries URL a pritiahnuť a odísť z iných prehliadačov alebo správcov súborov.</li>
<li>Potiahnutím spustite stlačením klávesu Alt (Možnosť) pretiahnite do vonkajšieho prehliadača. Táto funkcia sa prevezme pomocou prehliadača Google Chrome.</li>
</ul>
This diff is collapsed.
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Command SFTP File manager</title>
</head>
<body>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
<html>
<head><title>{{ filename }}</title></head>
<body>
{{ content }}
</body>
</html>
# -*- coding: utf-8 -*-
#
__volumes_cached = {}
def get_cached_volume(sid):
return __volumes_cached.get(sid)
def set_cache_volume(sid, volume):
__volumes_cached[sid] = volume
This diff is collapsed.
This diff is collapsed.
...@@ -5,11 +5,12 @@ ...@@ -5,11 +5,12 @@
import os import os
import logging import logging
from logging.config import dictConfig from logging.config import dictConfig
from .config import config as app_config
def create_logger(app): def create_logger():
level = app.config['LOG_LEVEL'] level = app_config['LOG_LEVEL']
log_dir = app.config.get('LOG_DIR') log_dir = app_config.get('LOG_DIR')
log_path = os.path.join(log_dir, 'coco.log') log_path = os.path.join(log_dir, 'coco.log')
main_setting = { main_setting = {
'handlers': ['console', 'file'], 'handlers': ['console', 'file'],
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ctx import stack from jms.service import AppService
from .config import config
def init_app(app): inited = False
stack['current_app'] = app
app_service = AppService(config)
if not inited:
app_service.initial()
inited = True
This diff is collapsed.
...@@ -8,12 +8,12 @@ import threading ...@@ -8,12 +8,12 @@ import threading
import paramiko import paramiko
from .utils import ssh_key_gen, get_logger from coco.utils import ssh_key_gen, get_logger
from .interface import SSHInterface from coco.interface import SSHInterface
from .interactive import InteractiveServer from coco.interactive import InteractiveServer
from .models import Connection from coco.models import Connection
from .sftp import SFTPServer from coco.sftp import SFTPServer
from .config import config from coco.config import config
logger = get_logger(__file__) logger = get_logger(__file__)
BACKLOG = 5 BACKLOG = 5
...@@ -110,7 +110,10 @@ class SSHServer: ...@@ -110,7 +110,10 @@ class SSHServer:
kind = client.request.kind kind = client.request.kind
if kind == 'session' and chan_type in supported: if kind == 'session' and chan_type in supported:
logger.info("Request type `{}:{}`, dispatch to interactive mode".format(kind, chan_type)) logger.info("Request type `{}:{}`, dispatch to interactive mode".format(kind, chan_type))
try:
InteractiveServer(client).interact() InteractiveServer(client).interact()
except Exception as e:
logger.error("Unexpected error occur: {}".format(e))
connection = Connection.get_connection(client.connection_id) connection = Connection.get_connection(client.connection_id)
connection.remove_client(client.id) connection.remove_client(client.id)
elif chan_type == 'subsystem': elif chan_type == 'subsystem':
...@@ -122,3 +125,8 @@ class SSHServer: ...@@ -122,3 +125,8 @@ class SSHServer:
def shutdown(self): def shutdown(self):
self.stop_evt.set() self.stop_evt.set()
if __name__ == '__main__':
ssh_server = SSHServer()
ssh_server.run()
...@@ -28,7 +28,6 @@ DAEMON = False ...@@ -28,7 +28,6 @@ DAEMON = False
PID_FILE = os.path.join(BASE_DIR, 'coco.pid') PID_FILE = os.path.join(BASE_DIR, 'coco.pid')
coco = Coco() coco = Coco()
coco.config.from_object(config)
def check_pid(pid): def check_pid(pid):
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment