Merge pull request #2090 from gravitational/alexey/http-files

http files/separate remote location from file name in SCP requests
This commit is contained in:
Alexey Kontsevoy 2018-07-18 19:59:33 -04:00 committed by GitHub
commit a9589bf38f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 149 additions and 228 deletions

2
e

@ -1 +1 @@
Subproject commit d7cfa9e1a68e73c204dd90f1ba133ed27d60a8cc
Subproject commit 282e1b46d2527e893a3563bab4adde9564d31395

View file

@ -42,6 +42,8 @@ const (
type HTTPTransferRequest struct {
// RemoteLocation is a destination location of the file
RemoteLocation string
// FileName is a file name
FileName string
// HTTPRequest is HTTP request
HTTPRequest *http.Request
// HTTPRequest is HTTP request
@ -69,9 +71,12 @@ func CreateHTTPUpload(req HTTPTransferRequest) (Command, error) {
return nil, trace.BadParameter("missing parameter HTTPRequest")
}
dir, filename, err := req.parseRemoteLocation()
if err != nil {
return nil, trace.Wrap(err)
if req.FileName == "" {
return nil, trace.BadParameter("missing file name")
}
if req.RemoteLocation == "" {
return nil, trace.BadParameter("missing remote location")
}
contentLength := req.HTTPRequest.Header.Get("Content-Length")
@ -82,20 +87,21 @@ func CreateHTTPUpload(req HTTPTransferRequest) (Command, error) {
fs := &httpFileSystem{
reader: req.HTTPRequest.Body,
fileName: filename,
fileName: req.FileName,
fileSize: fileSize,
}
flags := Flags{
Target: []string{dir},
// scp treats it as a list of files to upload
Target: []string{req.FileName},
}
cfg := Config{
User: req.User,
Flags: flags,
FileSystem: fs,
User: req.User,
ProgressWriter: req.Progress,
RemoteLocation: dir,
RemoteLocation: req.RemoteLocation,
AuditLog: req.AuditLog,
}

View file

@ -47,8 +47,6 @@ func (s *SCPSuite) SetUpSuite(c *C) {
func (s *SCPSuite) TestHTTPSendFile(c *C) {
outdir := c.MkDir()
newFileRemoteLocation := filepath.Join(outdir, "new-file")
expectedBytes := []byte("hello")
buf := bytes.NewReader(expectedBytes)
req, err := http.NewRequest("POST", "/", buf)
@ -59,14 +57,15 @@ func (s *SCPSuite) TestHTTPSendFile(c *C) {
stdOut := bytes.NewBufferString("")
cmd, err := CreateHTTPUpload(
HTTPTransferRequest{
RemoteLocation: newFileRemoteLocation,
FileName: "filename",
RemoteLocation: outdir,
HTTPRequest: req,
Progress: stdOut,
User: "test-user",
})
c.Assert(err, IsNil)
s.runSCP(c, cmd, "scp", "-v", "-t", outdir)
bytesReceived, err := ioutil.ReadFile(newFileRemoteLocation)
bytesReceived, err := ioutil.ReadFile(filepath.Join(outdir, "filename"))
c.Assert(err, IsNil)
c.Assert(string(bytesReceived), Equals, string(expectedBytes))
}

View file

@ -182,8 +182,8 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
h.GET("/webapi/sites/:site/namespaces/:namespace/sessions/:sid/stream", h.siteSessionStreamGet) // get recorded session's bytes (from events)
// scp file transfer
h.GET("/webapi/sites/:site/namespaces/:namespace/nodes/:server/:login/scp/*filepath", h.WithClusterAuth(h.transferFile))
h.POST("/webapi/sites/:site/namespaces/:namespace/nodes/:server/:login/scp/*filepath", h.WithClusterAuth(h.transferFile))
h.GET("/webapi/sites/:site/namespaces/:namespace/nodes/:server/:login/scp", h.WithClusterAuth(h.transferFile))
h.POST("/webapi/sites/:site/namespaces/:namespace/nodes/:server/:login/scp", h.WithClusterAuth(h.transferFile))
// OIDC related callback handlers
h.GET("/webapi/oidc/login/web", httplib.MakeHandler(h.oidcLoginWeb))

View file

@ -19,7 +19,6 @@ package web
import (
"context"
"net/http"
"strings"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/client"
@ -41,16 +40,20 @@ type fileTransferRequest struct {
namespace string
// Cluster is the name of the remote cluster to connect to.
cluster string
// Cluster is the name of the remote cluster to connect to.
remoteFilePath string
// remoteLocation is file remote location
remoteLocation string
// filename is a file name
filename string
}
func (h *Handler) transferFile(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) {
query := r.URL.Query()
req := fileTransferRequest{
login: p.ByName("login"),
namespace: p.ByName("namespace"),
server: p.ByName("server"),
remoteFilePath: p.ByName("filepath"),
remoteLocation: query.Get("location"),
filename: query.Get("filename"),
}
clt, err := ctx.GetUserClient(site)
@ -86,13 +89,8 @@ type fileTransfer struct {
}
func (f *fileTransfer) download(req fileTransferRequest, w http.ResponseWriter) error {
remoteDest, err := f.resolveRemoteDest(req.remoteFilePath)
if err != nil {
return trace.Wrap(err)
}
cmd, err := scp.CreateHTTPDownload(scp.HTTPTransferRequest{
RemoteLocation: remoteDest,
RemoteLocation: req.remoteLocation,
HTTPResponse: w,
User: f.ctx.GetUser(),
})
@ -114,13 +112,9 @@ func (f *fileTransfer) download(req fileTransferRequest, w http.ResponseWriter)
}
func (f *fileTransfer) upload(req fileTransferRequest, httpReq *http.Request) error {
remoteDest, err := f.resolveRemoteDest(req.remoteFilePath)
if err != nil {
return trace.Wrap(err)
}
cmd, err := scp.CreateHTTPUpload(scp.HTTPTransferRequest{
RemoteLocation: remoteDest,
RemoteLocation: req.remoteLocation,
FileName: req.filename,
HTTPRequest: httpReq,
User: f.ctx.GetUser(),
})
@ -179,17 +173,3 @@ func (f *fileTransfer) createClient(req fileTransferRequest) (*client.TeleportCl
return tc, nil
}
func (f *fileTransfer) resolveRemoteDest(filepath string) (string, error) {
if strings.HasPrefix(filepath, "/absolute/") {
remoteDest := strings.TrimPrefix(filepath, "/absolute/")
return "/" + remoteDest, nil
}
if strings.HasPrefix(filepath, "/relative/") {
remoteDest := strings.TrimPrefix(filepath, "/relative/")
return "./" + remoteDest, nil
}
return "", trace.BadParameter("invalid remote file path: %q", filepath)
}

View file

@ -1,56 +0,0 @@
/*
Copyright 2018 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package web
import (
"gopkg.in/check.v1"
)
type FilesSuite struct {
}
var _ = check.Suite(&FilesSuite{})
func (s *FilesSuite) TestRemoteDestination(c *check.C) {
validCases := map[string]string{
"/relative/file.txt": "./file.txt",
"/relative/dir/file.txt": "./dir/file.txt",
"/relative/../file.txt": "./../file.txt",
"/relative/.": "./.",
"/absolute/file.txt": "/file.txt",
"/absolute/dir/file.txt": "/dir/file.txt",
"/absolute/./dir/file.txt": "/./dir/file.txt",
}
invalidCases := []string{
"wrong_prefix/file.txt",
"",
}
ft := fileTransfer{}
for url, expected := range validCases {
comment := check.Commentf(url)
path, err := ft.resolveRemoteDest(url)
c.Assert(err, check.IsNil, comment)
c.Assert(path, check.Equals, expected)
}
for _, url := range invalidCases {
_, err := ft.resolveRemoteDest(url)
c.Assert(err, check.NotNil)
}
}

View file

@ -1221,7 +1221,7 @@ webpackJsonp([0],[
},
api: {
scp: '/v1/webapi/sites/:siteId/nodes/:serverId/:login/scp',
scp: '/v1/webapi/sites/:siteId/nodes/:serverId/:login/scp?location=:location&filename=:filename',
ssoOidc: '/v1/webapi/oidc/login/web?redirect_url=:redirect&connector_id=:providerName',
ssoSaml: '/v1/webapi/saml/sso?redirect_url=:redirect&connector_id=:providerName',
renewTokenPath: '/v1/webapi/sessions/renew',
@ -1278,9 +1278,11 @@ webpackJsonp([0],[
getScpUrl: function getScpUrl(_ref3) {
var siteId = _ref3.siteId,
serverId = _ref3.serverId,
login = _ref3.login;
login = _ref3.login,
location = _ref3.location,
filename = _ref3.filename;
return (0, _patternUtils.formatPattern)(cfg.api.scp, { siteId: siteId, serverId: serverId, login: login });
return (0, _patternUtils.formatPattern)(cfg.api.scp, { siteId: siteId, serverId: serverId, login: login, location: location, filename: filename });
},
getFetchSessionsUrl: function getFetchSessionsUrl(siteId) {
return (0, _patternUtils.formatPattern)(cfg.api.siteEventSessionFilterPath, { siteId: siteId });
@ -13457,7 +13459,7 @@ webpackJsonp([0],[
return new FileTransferStore();
};
FileTransferStore.prototype.makeUrl = function makeUrl(fileName) {
FileTransferStore.prototype.makeUrl = function makeUrl(location, filename) {
var siteId = this.siteId,
serverId = this.serverId,
login = this.login;
@ -13466,15 +13468,11 @@ webpackJsonp([0],[
var url = _config2.default.api.getScpUrl({
siteId: siteId,
serverId: serverId,
login: login
login: login,
location: location,
filename: filename
});
if (fileName.indexOf('/') === 0) {
url = url + '/absolute' + fileName;
} else {
url = url + '/relative/' + fileName;
}
return url;
};
@ -13484,11 +13482,12 @@ webpackJsonp([0],[
};
FileTransferStore.prototype.addFile = function addFile(_ref2) {
var name = _ref2.name,
var location = _ref2.location,
name = _ref2.name,
blob = _ref2.blob,
isUpload = _ref2.isUpload;
var url = this.makeUrl(name);
var url = this.makeUrl(location, name);
var file = new File({
url: url,
name: name,
@ -15934,10 +15933,10 @@ webpackJsonp([0],[
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _Component.call.apply(_Component, [this].concat(args))), _this), _this.onDownload = function (fileName) {
_this.transfer(fileName, false, []);
}, _this.onUpload = function (remoteLocation, blob) {
_this.transfer(remoteLocation, true, blob);
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _Component.call.apply(_Component, [this].concat(args))), _this), _this.onDownload = function (location) {
_this.transfer(location, location, false);
}, _this.onUpload = function (location, filename, blob) {
_this.transfer(location, filename, true, blob);
}, _this.onKeyDown = function (e) {
// escape
if (e.keyCode !== 27) {
@ -15960,8 +15959,11 @@ webpackJsonp([0],[
}, _temp), _possibleConstructorReturn(_this, _ret);
}
FileTransferDialog.prototype.transfer = function transfer(name, isUpload, blob) {
FileTransferDialog.prototype.transfer = function transfer(location, name, isUpload) {
var blob = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
this.props.onTransfer({
location: location,
name: name,
isUpload: isUpload,
blob: blob
@ -16094,13 +16096,13 @@ webpackJsonp([0],[
_react2.default.createElement(
'h4',
null,
'DOWNLOAD A FILE'
'SCP DOWNLOAD'
)
),
_react2.default.createElement(
_items.Text,
{ className: 'm-b-xs' },
'Full path of file'
'File path'
),
_react2.default.createElement(
'div',
@ -16210,13 +16212,6 @@ webpackJsonp([0],[
limitations under the License.
*/
var defaultState = function defaultState() {
return {
files: [],
remoteLocation: "./"
};
};
var FileUploadSelector = exports.FileUploadSelector = function (_React$Component) {
_inherits(FileUploadSelector, _React$Component);
@ -16229,7 +16224,10 @@ webpackJsonp([0],[
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = defaultState(), _this.onFileSelected = function (e) {
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = {
files: [],
remoteLocation: "~/"
}, _this.onFileSelected = function (e) {
_this.addFiles([], e.target.files);
_this.inputRef.focus();
}, _this.onFilePathChanged = function (e) {
@ -16240,17 +16238,17 @@ webpackJsonp([0],[
var _this$state = _this.state,
files = _this$state.files,
remoteLocation = _this$state.remoteLocation;
// if multiple files selected, ensure that we are uploading to a directory
if (remoteLocation && remoteLocation[remoteLocation.length - 1] !== '/') {
if (files.length > 1 && remoteLocation[remoteLocation.length - 1] !== '/') {
remoteLocation = remoteLocation + '/';
}
for (var i = 0; i < files.length; i++) {
var name = remoteLocation + files[i].name;
_this.props.onUpload(name, files[i]);
_this.props.onUpload(remoteLocation, files[i].name, files[i]);
}
_this.setState(defaultState());
_this.setState({ files: [] });
_this.setFocus();
}, _this.onOpenFilePicker = function () {
// reset all selected files
@ -16335,17 +16333,50 @@ webpackJsonp([0],[
_react2.default.createElement(
'h4',
null,
'UPLOAD FILES'
'SCP UPLOAD'
)
),
_react2.default.createElement(
_items.Text,
{ className: 'm-b-xs' },
'Enter the location to upload files'
),
_react2.default.createElement(
'div',
{ className: 'grv-file-transfer-upload' },
_react2.default.createElement(
'div',
{ className: 'grv-file-transfer-upload-selected-files',
ref: function ref(e) {
return _this2.refDropzone = e;
},
onDragOver: function onDragOver(e) {
return e.preventDefault();
},
onDrop: this.onDrop
},
!hasFiles && _react2.default.createElement(
'div',
null,
_react2.default.createElement(
'a',
{ onClick: this.onOpenFilePicker },
'Select files'
),
' to upload or drag & drop them here'
),
hasFiles && _react2.default.createElement(
'div',
null,
_react2.default.createElement(
'a',
{ onClick: this.onOpenFilePicker },
' ',
files.length,
' files selected '
)
)
),
_react2.default.createElement(
_items.Text,
{ className: 'm-b-xs m-t' },
'Upload destination'
),
_react2.default.createElement(
'div',
{ style: { display: "flex" } },
@ -16369,39 +16400,6 @@ webpackJsonp([0],[
'Upload'
)
),
_react2.default.createElement(
'div',
{ className: 'grv-file-transfer-upload-selected-files m-t',
ref: function ref(e) {
return _this2.refDropzone = e;
},
onDragOver: function onDragOver(e) {
return e.preventDefault();
},
onDrop: this.onDrop
},
!hasFiles && _react2.default.createElement(
'div',
null,
_react2.default.createElement(
'a',
{ onClick: this.onOpenFilePicker },
'Select files'
),
' or place them here'
),
hasFiles && _react2.default.createElement(
'div',
null,
_react2.default.createElement(
'a',
{ onClick: this.onOpenFilePicker },
' ',
files.length,
' files selected '
)
)
),
_react2.default.createElement('input', { ref: function ref(e) {
return _this2.fileSelectorRef = e;
}, type: 'file',
@ -16727,7 +16725,7 @@ webpackJsonp([0],[
};
Transfer.prototype.handleError = function handleError(xhr) {
var errText = getErrorText(xhr);
var errText = getErrorText(xhr.response);
this.emit('error', new Error(errText));
};

View file

@ -1,4 +1,4 @@
!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,a){for(var s,u,l=0,c=[];l<o.length;l++)u=o[l],i[u]&&c.push.apply(c,i[u]),i[u]=0;for(s in a)Object.prototype.hasOwnProperty.call(a,s)&&(t[s]=a[s]);for(n&&n(o,a);c.length;)c.shift().call(null,e);if(a[0])return r[0]=0,e(0)};var r={},i={2:0};return e.e=function(t,n){if(0===i[t])return n.call(null,e);if(void 0!==i[t])i[t].push(n);else{i[t]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+{0:"67fd237ce9bf718943ac",1:"4b5db9e5abedc137bd2c"}[t]+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="/web/app",e(0)}([function(t,e,n){t.exports=n(594)},,function(t,e,n){"use strict";t.exports=n(3)},function(t,e,n){"use strict";var r=n(4),i=n(5),o=n(17),a=n(20),s=n(21),u=n(26),l=n(9),c=n(27),d=n(29),h=n(30),f=(n(11),l.createElement),p=l.createFactory,m=l.cloneElement,_=r,y={Children:{map:i.map,forEach:i.forEach,count:i.count,toArray:i.toArray,only:h},Component:o,PureComponent:a,createElement:f,cloneElement:m,isValidElement:l.isValidElement,PropTypes:c,createClass:s.createClass,createFactory:p,createMixin:function(t){return t},DOM:u,version:d,__spread:_};t.exports=y},function(t,e){/*
!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,a){for(var s,u,l=0,c=[];l<o.length;l++)u=o[l],i[u]&&c.push.apply(c,i[u]),i[u]=0;for(s in a)Object.prototype.hasOwnProperty.call(a,s)&&(t[s]=a[s]);for(n&&n(o,a);c.length;)c.shift().call(null,e);if(a[0])return r[0]=0,e(0)};var r={},i={2:0};return e.e=function(t,n){if(0===i[t])return n.call(null,e);if(void 0!==i[t])i[t].push(n);else{i[t]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+{0:"069703038f8a18384abf",1:"1844ddbfbcbde9fd8bae"}[t]+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="/web/app",e(0)}([function(t,e,n){t.exports=n(594)},,function(t,e,n){"use strict";t.exports=n(3)},function(t,e,n){"use strict";var r=n(4),i=n(5),o=n(17),a=n(20),s=n(21),u=n(26),l=n(9),c=n(27),d=n(29),h=n(30),f=(n(11),l.createElement),p=l.createFactory,m=l.cloneElement,_=r,y={Children:{map:i.map,forEach:i.forEach,count:i.count,toArray:i.toArray,only:h},Component:o,PureComponent:a,createElement:f,cloneElement:m,isValidElement:l.isValidElement,PropTypes:c,createClass:s.createClass,createFactory:p,createMixin:function(t){return t},DOM:u,version:d,__spread:_};t.exports=y},function(t,e){/*
object-assign
(c) Sindre Sorhus
@license MIT

4
web/dist/index.html vendored
View file

@ -8,8 +8,8 @@
<meta name="grv_bearer_token" content="{{ .Session }}" />
<title>Teleport by Gravitational</title>
<script src="/web/config.js"></script>
<link rel="shortcut icon" href="/web/app/favicon.ico"><link href="/web/app/vendor.8146b6f9c407124c24814e13e020f220.css" rel="stylesheet"></head>
<link rel="shortcut icon" href="/web/app/favicon.ico"><link href="/web/app/vendor.74f7a92a8aff0163c57f9be83b093efc.css" rel="stylesheet"></head>
<body class="grv">
<div id="app"></div>
<script type="text/javascript" src="/web/app/vendor.30afb39d16e3faf0f73e.js"></script><script type="text/javascript" src="/web/app/styles.30afb39d16e3faf0f73e.js"></script><script type="text/javascript" src="/web/app/app.30afb39d16e3faf0f73e.js"></script></body>
<script type="text/javascript" src="/web/app/vendor.a61d92a44f57c4eb872e.js"></script><script type="text/javascript" src="/web/app/styles.a61d92a44f57c4eb872e.js"></script><script type="text/javascript" src="/web/app/app.a61d92a44f57c4eb872e.js"></script></body>
</html>

View file

@ -53,10 +53,10 @@ export class FileDownloadSelector extends React.Component {
return (
<div className="grv-file-transfer-header m-b">
<Text className="m-b">
<h4>DOWNLOAD A FILE</h4>
<h4>SCP DOWNLOAD</h4>
</Text>
<Text className="m-b-xs">
Full path of file
File path
</Text>
<div className="grv-file-transfer-download">
<input onChange={this.onChangePath}

View file

@ -27,20 +27,21 @@ export class FileTransferDialog extends Component {
onClose: PropTypes.func.isRequired
}
transfer(name, isUpload, blob) {
transfer(location, name, isUpload, blob=[]) {
this.props.onTransfer({
location,
name,
isUpload,
blob
})
}
onDownload = fileName => {
this.transfer(fileName, false, [])
onDownload = location => {
this.transfer(location, location, false)
}
onUpload = (remoteLocation, blob) => {
this.transfer(remoteLocation, true, blob);
onUpload = (location, filename, blob) => {
this.transfer(location, filename, true, blob);
}
onKeyDown = e => {

View file

@ -17,10 +17,6 @@ limitations under the License.
import React, { PropTypes } from 'react';
import { Text } from './items';
const defaultState = () => ({
files: [],
remoteLocation: "./"
})
export class FileUploadSelector extends React.Component {
@ -28,7 +24,10 @@ export class FileUploadSelector extends React.Component {
onUpload: PropTypes.func.isRequired,
}
state = defaultState()
state = {
files: [],
remoteLocation: "~/"
}
componentWillUnmount() {
document.removeEventListener('drop', this.onDocumentDrop);
@ -67,16 +66,16 @@ export class FileUploadSelector extends React.Component {
onUpload = () => {
let { files, remoteLocation } = this.state;
if (remoteLocation && remoteLocation[remoteLocation.length - 1] !== '/') {
// if multiple files selected, ensure that we are uploading to a directory
if (files.length > 1 && remoteLocation[remoteLocation.length - 1] !== '/') {
remoteLocation = remoteLocation + '/';
}
for (var i = 0; i < files.length; i++) {
const name = remoteLocation + files[i].name;
this.props.onUpload(name, files[i]);
this.props.onUpload(remoteLocation, files[i].name, files[i]);
}
this.setState(defaultState())
this.setState({ files: [] });
this.setFocus();
}
@ -129,12 +128,28 @@ export class FileUploadSelector extends React.Component {
return (
<div className="grv-file-transfer-header m-b">
<Text className="m-b">
<h4>UPLOAD FILES</h4>
</Text>
<Text className="m-b-xs">
Enter the location to upload files
<h4>SCP UPLOAD</h4>
</Text>
<div className="grv-file-transfer-upload">
<div className="grv-file-transfer-upload-selected-files"
ref={ e => this.refDropzone = e }
onDragOver={e => e.preventDefault()}
onDrop={this.onDrop}
>
{!hasFiles &&
<div>
<a onClick={this.onOpenFilePicker}>Select files</a> to upload or drag & drop them here
</div>
}
{hasFiles &&
<div>
<a onClick={this.onOpenFilePicker}> {files.length} files selected </a>
</div>
}
</div>
<Text className="m-b-xs m-t" >
Upload destination
</Text>
<div style={{ display: "flex" }}>
<input className="grv-file-transfer-input m-r-sm"
ref={e => this.inputRef = e}
@ -152,22 +167,6 @@ export class FileUploadSelector extends React.Component {
Upload
</button>
</div>
<div className="grv-file-transfer-upload-selected-files m-t"
ref={ e => this.refDropzone = e }
onDragOver={e => e.preventDefault()}
onDrop={this.onDrop}
>
{!hasFiles &&
<div>
<a onClick={this.onOpenFilePicker}>Select files</a> or place them here
</div>
}
{hasFiles &&
<div>
<a onClick={this.onOpenFilePicker}> {files.length} files selected </a>
</div>
}
</div>
<input ref={e => this.fileSelectorRef = e} type="file"
multiple
style={{ display: "none" }}

View file

@ -53,7 +53,7 @@ const cfg = {
},
api: {
scp: '/v1/webapi/sites/:siteId/nodes/:serverId/:login/scp',
scp: '/v1/webapi/sites/:siteId/nodes/:serverId/:login/scp?location=:location&filename=:filename',
ssoOidc: '/v1/webapi/oidc/login/web?redirect_url=:redirect&connector_id=:providerName',
ssoSaml: '/v1/webapi/saml/sso?redirect_url=:redirect&connector_id=:providerName',
renewTokenPath:'/v1/webapi/sessions/renew',
@ -102,8 +102,8 @@ const cfg = {
return formatPattern(cfg.api.sessionEventsPath, {sid, siteId});
},
getScpUrl({ siteId, serverId, login }) {
return formatPattern(cfg.api.scp, {siteId, serverId, login});
getScpUrl({ siteId, serverId, login, location, filename }) {
return formatPattern(cfg.api.scp, {siteId, serverId, login, location, filename});
},
getFetchSessionsUrl(siteId){

View file

@ -71,7 +71,7 @@ export class FileTransferStore extends Record({
return new FileTransferStore();
}
makeUrl(fileName) {
makeUrl(location, filename) {
const {
siteId,
serverId,
@ -80,15 +80,11 @@ export class FileTransferStore extends Record({
let url = cfg.api.getScpUrl({
siteId,
serverId,
login
login,
location,
filename
});
if (fileName.indexOf('/') === 0) {
url = `${url}/absolute${fileName}`
} else {
url = `${url}/relative/${fileName}`
}
return url;
}
@ -97,8 +93,8 @@ export class FileTransferStore extends Record({
return this.set('files', files);
}
addFile({ name, blob, isUpload }) {
const url = this.makeUrl(name);
addFile({ location, name, blob, isUpload }) {
const url = this.makeUrl(location, name);
const file = new File({
url,
name,

View file

@ -57,7 +57,7 @@ class Transfer extends EventEmitter {
}
handleError(xhr) {
const errText = getErrorText(xhr);
const errText = getErrorText(xhr.response);
this.emit('error', new Error(errText));
}

View file

@ -97,8 +97,6 @@
background-color: #151d20;
padding: 30px 15px;
color: $grv-color-file-text;
text-transform: uppercase;
a{
color: $grv-color-file-text;
text-decoration: underline;