mirror of
https://github.com/kopia/kopia.git
synced 2026-04-04 14:23:21 -04:00
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:
@@ -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>
|
||||
|
||||
27
htmlui/src/SetupKopiaServer.js
Normal file
27
htmlui/src/SetupKopiaServer.js
Normal 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>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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">
|
||||
|
||||
<ButtonGroup>
|
||||
<Dropdown>
|
||||
@@ -343,7 +345,7 @@ export class SourcesTable extends Component {
|
||||
<ButtonGroup>
|
||||
<Button variant="primary">Refresh</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbar>
|
||||
</ButtonToolbar>}
|
||||
<ButtonToolbar>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user