kopia-ui: added ability to connect to kopia server and few other minor tweaks (#546)

* kopia-ui: added ability to connect to kopia server

* kopia-ui: update status page to show some data for repositories connected to API server

* kopia-ui: hide user@host selection dropdown for kopia server repositories
This commit is contained in:
Jarek Kowalski
2020-08-16 17:57:37 -07:00
committed by GitHub
parent 27ec5c70a9
commit 48f253173b
8 changed files with 160 additions and 54 deletions

View File

@@ -81,30 +81,47 @@ export class RepoStatus extends Component {
<>
<h3>Connected To Repository</h3>
<Form>
<Form.Group>
<Form.Label>Config File</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.configFile} />
</Form.Group>
<Form.Group>
<Form.Label>Cache Directory</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.cacheDir} />
</Form.Group>
{this.state.status.apiServerURL ? <>
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Server URL</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.apiServerURL} />
</Form.Group>
</Form.Row>
</> : <>
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Config File</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.configFile} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Cache Directory</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.cacheDir} />
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Provider</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.storage} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Hash Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.hash} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Encryption Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.encryption} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Splitter Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.splitter} />
</Form.Group>
</Form.Row>
</>}
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Provider</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.storage} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Hash Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.hash} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Encryption Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.encryption} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Splitter Algorithm</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.splitter} />
<Form.Label>Connected as:</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.username + "@" + this.state.status.host} />
</Form.Group>
</Form.Row>
<Button variant="danger" onClick={this.disconnect}>Disconnect</Button>

View File

@@ -0,0 +1,27 @@
import React, { Component } from 'react';
import Form from 'react-bootstrap/Form';
import { handleChange, OptionalField, RequiredField, validateRequiredFields } from './forms';
export class SetupKopiaServer extends Component {
constructor() {
super();
this.state = {};
this.handleChange = handleChange.bind(this);
}
validate() {
return validateRequiredFields(this, ["url"])
}
render() {
return <>
<Form.Row>
{RequiredField(this, "Server address", "url", { placeholder: "enter server URL (https://<host>:port)" })}
</Form.Row>
<Form.Row>
{OptionalField(this, "Trusted server certificate finterprint (SHA256)", "serverCertFingerprint", { placeholder: "enter trusted server certificate fingerprint printed at server startup" })}
</Form.Row>
</>;
}
}

View File

@@ -13,6 +13,7 @@ import { SetupAzure } from './SetupAzure';
import { SetupSFTP } from './SetupSFTP';
import { SetupToken } from './SetupToken';
import { SetupWebDAV } from './SetupWebDAV';
import { SetupKopiaServer } from './SetupKopiaServer';
const supportedProviders = [
{ provider: "filesystem", description: "Filesystem", component: SetupFilesystem },
@@ -23,6 +24,7 @@ const supportedProviders = [
{ provider: "sftp", description: "SFTP server", component: SetupSFTP },
{ provider: "webdav", description: "WebDAV server", component: SetupWebDAV },
{ provider: "_token", description: "(use token)", component: SetupToken },
{ provider: "_server", description: "(connect to Kopia server)", component: SetupKopiaServer },
];
export class SetupRepository extends Component {
@@ -126,18 +128,29 @@ export class SetupRepository extends Component {
}
let request = null;
if (this.state.provider === "_token") {
request = {
token: ed.state.token,
}
} else {
request = {
storage: {
type: this.state.provider,
config: ed.state,
},
password: this.state.password,
}
switch (this.state.provider) {
case "_token":
request = {
token: ed.state.token,
};
break;
case "_server":
request = {
apiServer: ed.state,
password: this.state.password,
};
break;
default:
request = {
storage: {
type: this.state.provider,
config: ed.state,
},
password: this.state.password,
};
break;
}
this.setState({ isLoading: true });

View File

@@ -28,6 +28,7 @@ export class SourcesTable extends Component {
error: null,
localSourceName: "",
multiUser: false,
selectedOwner: localSnapshots,
selectedDirectory: "",
};
@@ -59,6 +60,7 @@ export class SourcesTable extends Component {
axios.get('/api/v1/sources').then(result => {
this.setState({
localSourceName: result.data.localUsername + "@" + result.data.localHost,
multiUser: result.data.multiUser,
sources: result.data.sources,
isLoading: false,
});
@@ -323,7 +325,7 @@ export class SourcesTable extends Component {
}]
return <div className="padded">
<ButtonToolbar className="float-sm-right">
{this.state.multiUser && <ButtonToolbar className="float-sm-right">
&nbsp;
<ButtonGroup>
<Dropdown>
@@ -343,7 +345,7 @@ export class SourcesTable extends Component {
<ButtonGroup>
<Button variant="primary">Refresh</Button>
</ButtonGroup>
</ButtonToolbar>
</ButtonToolbar>}
<ButtonToolbar>
<InputGroup>
<FormControl

View File

@@ -55,12 +55,26 @@ func (s *Server) handleRepoStatus(ctx context.Context, r *http.Request, body []b
MaxPackSize: dr.Content.Format.MaxPackSize,
Splitter: dr.Objects.Format.Splitter,
Storage: dr.Blobs.ConnectionInfo().Type,
Username: dr.Username(),
Host: dr.Hostname(),
}, nil
}
return &serverapi.StatusResponse{
type remoteRepository interface {
APIServerURL() string
}
result := &serverapi.StatusResponse{
Connected: true,
}, nil
Username: s.rep.Username(),
Host: s.rep.Hostname(),
}
if rr, ok := s.rep.(remoteRepository); ok {
result.APIServerURL = rr.APIServerURL()
}
return result, nil
}
func maybeDecodeToken(req *serverapi.ConnectRepositoryRequest) *apiError {
@@ -139,12 +153,18 @@ func (s *Server) handleRepoConnect(ctx context.Context, r *http.Request, body []
return nil, requestError(serverapi.ErrorMalformedRequest, "unable to decode request: "+err.Error())
}
if err := maybeDecodeToken(&req); err != nil {
return nil, err
}
if req.APIServer != nil {
if err := s.connectAPIServerAndOpen(ctx, req.APIServer, req.Password); err != nil {
return nil, err
}
} else {
if err := maybeDecodeToken(&req); err != nil {
return nil, err
}
if err := s.connectAndOpen(ctx, req.Storage, req.Password); err != nil {
return nil, err
if err := s.connectAndOpen(ctx, req.Storage, req.Password); err != nil {
return nil, err
}
}
return s.handleRepoStatus(ctx, r, nil)
@@ -171,6 +191,14 @@ func (s *Server) handleRepoSupportedAlgorithms(ctx context.Context, r *http.Requ
return res, nil
}
func (s *Server) connectAPIServerAndOpen(ctx context.Context, si *repo.APIServerInfo, password string) *apiError {
if err := repo.ConnectAPIServer(ctx, s.options.ConfigFile, si, password, s.options.ConnectOptions); err != nil {
return repoErrorToAPIError(err)
}
return s.open(ctx, password)
}
func (s *Server) connectAndOpen(ctx context.Context, conn blob.ConnectionInfo, password string) *apiError {
st, err := blob.NewStorage(ctx, conn)
if err != nil {
@@ -182,6 +210,10 @@ func (s *Server) connectAndOpen(ctx context.Context, conn blob.ConnectionInfo, p
return repoErrorToAPIError(err)
}
return s.open(ctx, password)
}
func (s *Server) open(ctx context.Context, password string) *apiError {
rep, err := repo.Open(ctx, s.options.ConfigFile, password, nil)
if err != nil {
return repoErrorToAPIError(err)

View File

@@ -11,15 +11,19 @@
"github.com/kopia/kopia/internal/ctxutil"
"github.com/kopia/kopia/internal/serverapi"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/policy"
)
func (s *Server) handleSourcesList(ctx context.Context, r *http.Request, body []byte) (interface{}, *apiError) {
_, multiUser := s.rep.(*repo.DirectRepository)
resp := &serverapi.SourcesResponse{
Sources: []*serverapi.SourceStatus{},
LocalHost: s.rep.Hostname(),
LocalUsername: s.rep.Username(),
MultiUser: multiUser,
}
for _, v := range s.sourceManagers {

View File

@@ -15,14 +15,17 @@
// StatusResponse is the response of 'status' HTTP API command.
type StatusResponse struct {
Connected bool `json:"connected"`
ConfigFile string `json:"configFile,omitempty"`
CacheDir string `json:"cacheDir,omitempty"`
Hash string `json:"hash,omitempty"`
Encryption string `json:"encryption,omitempty"`
Splitter string `json:"splitter,omitempty"`
MaxPackSize int `json:"maxPackSize,omitempty"`
Storage string `json:"storage,omitempty"`
Connected bool `json:"connected"`
ConfigFile string `json:"configFile,omitempty"`
CacheDir string `json:"cacheDir,omitempty"`
Hash string `json:"hash,omitempty"`
Encryption string `json:"encryption,omitempty"`
Splitter string `json:"splitter,omitempty"`
MaxPackSize int `json:"maxPackSize,omitempty"`
Storage string `json:"storage,omitempty"`
Username string `json:"username,omitempty"`
Host string `json:"host,omitempty"`
APIServerURL string `json:"apiServerURL,omitempty"`
}
// SourcesResponse is the response of 'sources' HTTP API command.
@@ -30,6 +33,9 @@ type SourcesResponse struct {
LocalUsername string `json:"localUsername"`
LocalHost string `json:"localHost"`
// if set to true, current repository supports accessing data for other users.
MultiUser bool `json:"multiUser"`
Sources []*SourceStatus `json:"sources"`
}
@@ -101,9 +107,10 @@ type CreateRepositoryRequest struct {
// ConnectRepositoryRequest contains request to connect to a repository.
type ConnectRepositoryRequest struct {
Storage blob.ConnectionInfo `json:"storage"`
Password string `json:"password"`
Token string `json:"token"` // when set, overrides Storage and Password
Storage blob.ConnectionInfo `json:"storage"`
Password string `json:"password"`
Token string `json:"token"` // when set, overrides Storage and Password
APIServer *repo.APIServerInfo `json:"apiServer"`
}
// SupportedAlgorithmsResponse returns the list of supported algorithms for repository creation.

View File

@@ -37,6 +37,10 @@ type apiServerRepository struct {
hostname string
}
func (r *apiServerRepository) APIServerURL() string {
return r.cli.BaseURL
}
func (r *apiServerRepository) OpenObject(ctx context.Context, id object.ID) (object.Reader, error) {
return r.omgr.Open(ctx, id)
}