Commit 5823d999 authored by 广宏伟's avatar 广宏伟

Merged in sftp (pull request #103)

Sftp

* [Update] SFTP

* [Update] 修改

* [Update] 修改sftp

* [Update] 暂时这样

* [Update] 基本完成

* [Update] 修改sftp

* [Update] Sftp done
parent 24b420c2
...@@ -21,7 +21,6 @@ from .logger import create_logger ...@@ -21,7 +21,6 @@ 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,7 +43,6 @@ class Coco: ...@@ -44,7 +43,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
init_app(self)
@property @property
def sshd(self): def sshd(self):
...@@ -77,7 +75,7 @@ class Coco: ...@@ -77,7 +75,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()
......
...@@ -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
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
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'))
# 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, source, 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 source: The current parent of the targets.
: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
import logging
import stat
from flask import send_file
from .base import BaseVolume
logger = logging.getLogger(__name__)
class SFTPVolume(BaseVolume):
def __init__(self, sftp):
self.sftp = sftp
self.root_name = 'Home'
super().__init__()
self._stat_cache = {}
def close(self):
self.sftp.close()
def get_volume_id(self):
tran = self.sftp.get_channel().get_transport()
addr = tran.getpeername()
username = tran.get_username()
volume_id = '{}@{}:{}'.format(username, *addr)
return self._digest(volume_id)
def info(self, target):
"""
获取target的信息
:param target:
:return:
"""
path = self._path(target)
# print("Info target '{}' {}".format(target, path))
return self._info(path)
def _info(self, path, attr=None):
remote_path = self._remote_path(path)
# print('_Info: {} => {}'.format(path, remote_path))
if attr is None:
attr = self.sftp.lstat(remote_path)
if not hasattr(attr, 'filename'):
filename = self.root_name if self._is_root(path) else self._base_name(remote_path)
attr.filename = filename
parent_path = self._dir_name(path)
data = {
"name": attr.filename,
"hash": self._hash(path),
"phash": self._hash(parent_path),
"ts": 0,
"size": 'unknown',
"mime": "directory" if stat.S_ISDIR(attr.st_mode) else "file",
"locked": 0,
"hidden": 0,
"read": 1,
"write": 1,
}
if data["mime"] == 'directory':
data["dirs"] = 1
if self._is_root(path):
del data['phash']
data['name'] = self.root_name
data['locked'] = 1
data['volume_id'] = self.get_volume_id()
# print("_Get stat info end")
return data
def _list(self, path, name_only=False):
""" Returns current dir dirs/files
"""
remote_path = self._remote_path(path)
# print("_list {} => {}".format(path, remote_path))
if name_only:
return self.sftp.listdir(remote_path)
files = []
children_attrs = self.sftp.listdir_attr(remote_path)
for item in children_attrs:
item_path = self._join(path, item.filename)
info = self._info(item_path, attr=item)
files.append(info)
return files
def list(self, target, name_only=False):
""" Returns a list of files/directories in the target directory. """
path = self._path(target)
# print("List {}-{}".format(target, path))
return self._list(path)
def tree(self, target):
""" Get the sub directory of directory
"""
path = self._path(target)
# print("Tree {} {}".format(target, path))
infos = self.list(target)
tree = list(filter(lambda x: x['mime'] == 'directory', infos))
return tree
def parents(self, target, depth=0):
"""
获取目录的父目录, 如果deep为0,则直到根
"""
path = self._path(target).rstrip(self.path_sep)
return self._parents(path, depth=depth)
def _parents(self, path, depth=0):
path = self.path_sep + path.lstrip(self.path_sep)
max_depth = len(path.split(self.path_sep))
if depth == 0 or depth > max_depth:
depth = max_depth
parent_path = self._dir_name(path)
infos = self._list(parent_path)
_parents = list(filter(lambda x: x['mime'] == 'directory', infos))
if self._is_root(parent_path):
_parents.append(self._info(self.path_sep))
if depth == 1:
return _parents
parents = _parents + self._parents(parent_path, depth - 1)
return parents
def read_file_view(self, request, target, download=True):
remote_path = self._remote_path_h(target)
f = self.sftp.open(remote_path, 'r')
filename = self._base_name(remote_path)
response = send_file(f, mimetype='application/octet-stream',
as_attachment=True, attachment_filename=filename)
return response
def mkdir(self, names, parent, many=False):
""" Creates a new directory. """
parent_path = self._path(parent)
data = []
if not many:
names = [names]
for name in names:
path = self._join(parent_path, name)
remote_path = self._remote_path(path)
self.sftp.mkdir(remote_path)
data.append(self._info(path))
return data
def mkfile(self, name, parent):
""" Creates a new file. """
parent_path = self._path(parent)
remote_path = self._remote_path(parent_path)
with self.sftp.open(remote_path, mode='w'):
pass
return self._info(parent_path)
def rename(self, name, target):
""" Renames a file or directory. """
path = self._path(target)
remote_path = self._remote_path(path)
new_path = self._join(self._dir_name(path), name)
new_remote_path = self._remote_path(new_path)
self.sftp.rename(remote_path, new_remote_path)
return {
'added': [self._info(new_path)],
'removed': [target]
}
def paste(self, targets, source, dest, cut):
""" Moves/copies target files/directories from source to dest. """
return {"error": "Not support paste"}
def remove(self, target):
""" Delete a File or Directory object. """
path = self._path(target)
remote_path = self._remote_path(path)
try:
self.sftp.unlink(remote_path)
except OSError:
raise OSError("Delete {} failed".format(self._base_name(path)))
return target
def upload(self, files, parent):
""" For now, this uses a very naive way of storing files - the entire
file is read in to the File model's content field in one go.
This should be updated to use read_chunks to add the file one
chunk at a time.
"""
added = []
parent_path = self._path(parent)
item = files.get('upload[]')
path = self._join(parent_path, item.filename)
remote_path = self._remote_path(path)
infos = self._list(parent_path)
files_exist = [d['name'] for d in infos]
if item.filename in files_exist:
raise OSError("File {} exits".format(remote_path))
with self.sftp.open(remote_path, 'w') as rf:
for data in item:
rf.write(data)
added.append(self._info(path))
return {'added': added}
def size(self, target):
info = self.info(target)
return info.get('size') or 'Unknown'
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.
This diff is collapsed.
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.
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.
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