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:
Jarek Kowalski committed 2018-06-10 17:24:42 -07:00
1 parent e9c11ea106
commit b105cca96c
7 files changed
+209 -1

No files matched your search

+27
View File
@@ -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)
}
+18
View File
@@ -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)}
}
+66
View File
@@ -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,
}
}
+26
View File
@@ -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
}
+27
View File
@@ -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
}
+44
View File
@@ -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}
}
+1 -1
View File
@@ -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()