mirror of
https://github.com/kopia/kopia.git
synced 2026-10-27 00:28:47 -04:00
added 'kopia server' which runs simple daemon with simple HTTP API server, which will be used to build a web UI
This commit is contained in:
1 parent
e9c11ea106
commit
b105cca96c
7 files changed
+209
-1
No files matched your search
@@ -0,0 +1,27 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/kopia/kopia/internal/server"
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
serverCommand = app.Command("server", "Start Kopia server")
|
||||
serverCommandListenAddress = serverCommand.Flag("--listen", "Listen address").Default("127.0.0.1:51515").String()
|
||||
)
|
||||
|
||||
func init() {
|
||||
serverCommand.Action(repositoryAction(runServer))
|
||||
}
|
||||
|
||||
func runServer(ctx context.Context, rep *repo.Repository) error {
|
||||
srv := server.New(rep)
|
||||
url := "http://" + *serverCommandListenAddress
|
||||
log.Info().Msgf("starting server on %v", url)
|
||||
http.Handle("/api/", srv.APIHandlers())
|
||||
return http.ListenAndServe(*serverCommandListenAddress, nil)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type apiError struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
func malformedRequestError() *apiError {
|
||||
return &apiError{400, "malformed request"}
|
||||
}
|
||||
|
||||
func internalServerError(err error) *apiError {
|
||||
return &apiError{500, fmt.Sprintf("internal server error: %v", err)}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/snapshot"
|
||||
)
|
||||
|
||||
type snapshotListEntry struct {
|
||||
ID string `json:"id"`
|
||||
Source snapshot.SourceInfo `json:"source"`
|
||||
Description string `json:"description"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
IncompleteReason string `json:"incomplete,omitempty"`
|
||||
Summary *fs.DirectorySummary `json:"summary"`
|
||||
RootEntry string `json:"rootID"`
|
||||
RetentionReasons []string `json:"retention"`
|
||||
}
|
||||
|
||||
type snapshotListResponse struct {
|
||||
Snapshots []*snapshotListEntry `json:"snapshots"`
|
||||
}
|
||||
|
||||
func (s *Server) handleSourceSnapshotList(r *http.Request) (interface{}, *apiError) {
|
||||
mgr := snapshot.NewManager(s.rep)
|
||||
pmgr := snapshot.NewPolicyManager(s.rep)
|
||||
|
||||
manifestIDs := mgr.ListSnapshotManifests(nil)
|
||||
manifests, err := mgr.LoadSnapshots(manifestIDs)
|
||||
if err != nil {
|
||||
return nil, internalServerError(err)
|
||||
}
|
||||
|
||||
resp := &snapshotListResponse{}
|
||||
|
||||
groups := snapshot.GroupBySource(manifests)
|
||||
|
||||
for _, grp := range groups {
|
||||
pol, err := pmgr.GetEffectivePolicy(grp[0].Source)
|
||||
if err == nil {
|
||||
pol.RetentionPolicy.ComputeRetentionReasons(grp)
|
||||
}
|
||||
for _, m := range grp {
|
||||
resp.Snapshots = append(resp.Snapshots, convertSnapshotManifest(m))
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func convertSnapshotManifest(m *snapshot.Manifest) *snapshotListEntry {
|
||||
return &snapshotListEntry{
|
||||
ID: m.ID,
|
||||
Source: m.Source,
|
||||
Description: m.Description,
|
||||
StartTime: m.StartTime,
|
||||
EndTime: m.EndTime,
|
||||
IncompleteReason: m.IncompleteReason,
|
||||
RootEntry: m.RootObjectID().String(),
|
||||
Summary: m.RootEntry.DirSummary,
|
||||
RetentionReasons: m.RetentionReasons,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/kopia/kopia/snapshot"
|
||||
)
|
||||
|
||||
type sourcesListResponse struct {
|
||||
Sources []snapshot.SourceInfo `json:"sources"`
|
||||
}
|
||||
|
||||
func (s *Server) handleSourcesList(r *http.Request) (interface{}, *apiError) {
|
||||
mgr := snapshot.NewManager(s.rep)
|
||||
|
||||
resp := &sourcesListResponse{
|
||||
Sources: mgr.ListSources(),
|
||||
}
|
||||
|
||||
sort.Slice(resp.Sources, func(i, j int) bool {
|
||||
return resp.Sources[i].String() < resp.Sources[j].String()
|
||||
})
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/kopia/kopia/block"
|
||||
)
|
||||
|
||||
type statusResponse struct {
|
||||
ConfigFile string `json:"configFile"`
|
||||
CacheDir string `json:"cacheDir"`
|
||||
BlockFormatting block.FormattingOptions `json:"blockFormatting"`
|
||||
Storage string `json:"storage"`
|
||||
}
|
||||
|
||||
func (s *Server) handleStatus(r *http.Request) (interface{}, *apiError) {
|
||||
bf := s.rep.Blocks.Format
|
||||
bf.HMACSecret = nil
|
||||
bf.MasterKey = nil
|
||||
|
||||
return &statusResponse{
|
||||
ConfigFile: s.rep.ConfigFile,
|
||||
CacheDir: s.rep.CacheDirectory,
|
||||
BlockFormatting: bf,
|
||||
Storage: s.rep.Storage.ConnectionInfo().Type,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
|
||||
"github.com/bmizerany/pat"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
rep *repo.Repository
|
||||
}
|
||||
|
||||
func (s *Server) APIHandlers() http.Handler {
|
||||
p := pat.New()
|
||||
p.Get("/api/v1/status", s.handleAPI(s.handleStatus))
|
||||
p.Get("/api/v1/sources", s.handleAPI(s.handleSourcesList))
|
||||
p.Get("/api/v1/snapshots", s.handleAPI(s.handleSourceSnapshotList))
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *Server) handleAPI(f func(r *http.Request) (interface{}, *apiError)) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
v, err := f(r)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", " ")
|
||||
if err == nil {
|
||||
e.Encode(v)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, err.message, err.code)
|
||||
})
|
||||
}
|
||||
|
||||
func New(rep *repo.Repository) *Server {
|
||||
return &Server{rep: rep}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (r *RetentionPolicy) getRetentionReasons(i int, s *Manifest, cutoff cutoffT
|
||||
return nil
|
||||
}
|
||||
|
||||
var keepReasons []string
|
||||
keepReasons := []string{}
|
||||
var zeroTime time.Time
|
||||
|
||||
yyyy, wk := s.StartTime.ISOWeek()
|
||||
|
||||
Reference in new issue
Block a user