mirror of
https://github.com/restic/rest-server.git
synced 2025-10-19 15:43:21 +00:00
simple backend
This commit is contained in:
parent
016bbf619a
commit
c19c63325c
10 changed files with 123 additions and 668 deletions
31
auth.go
31
auth.go
|
@ -1,31 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Authorize(r *http.Request, c *Context) error {
|
|
||||||
file := filepath.Join(c.path, ".htpasswd")
|
|
||||||
htpasswd, err := NewHtpasswdFromFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("internal server error")
|
|
||||||
}
|
|
||||||
|
|
||||||
username, password, ok := r.BasicAuth()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("malformed basic auth credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !htpasswd.Validate(username, password) {
|
|
||||||
return errors.New("unknown user")
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := RepositoryName(r.RequestURI)
|
|
||||||
if err != nil || repo != username {
|
|
||||||
return errors.New("wrong repository")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
35
context.go
35
context.go
|
@ -1,35 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/restic/restic/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Context specifies the root directory where all repositories are stored
|
|
||||||
type Context struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewContext(path string) Context {
|
|
||||||
path = filepath.Clean(path)
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
os.MkdirAll(path, backend.Modes.Dir)
|
|
||||||
}
|
|
||||||
return Context{path}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Repository(name string) (*Repository, error) {
|
|
||||||
name, err := ParseRepositoryName(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := NewRepository(filepath.Join(c.path, name))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo, nil
|
|
||||||
}
|
|
224
handlers.go
224
handlers.go
|
@ -1,224 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Handler func(w http.ResponseWriter, r *http.Request, c *Context)
|
|
||||||
|
|
||||||
func HeadConfig(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !repo.HasConfig() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetConfig(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config, err := repo.ReadConfig()
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostConfig(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errc := repo.WriteConfig(config)
|
|
||||||
if errc != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListBlob(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bt := BackendType(uri)
|
|
||||||
if string(bt) == "" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
blobs, err := repo.ListBlob(bt)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
json, err := json.Marshal(blobs)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
func HeadBlob(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bt := BackendType(uri)
|
|
||||||
if string(bt) == "" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := BlobID(uri)
|
|
||||||
if id.IsNull() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !repo.HasBlob(bt, id) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBlob(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bt := BackendType(uri)
|
|
||||||
if string(bt) == "" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := BlobID(uri)
|
|
||||||
if id.IsNull() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
blob, err := repo.ReadBlob(bt, id)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.ServeContent(w, r, "", time.Unix(0, 0), blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostBlob(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bt := BackendType(uri)
|
|
||||||
if string(bt) == "" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := BlobID(uri)
|
|
||||||
if id.IsNull() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
blob, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errw := repo.WriteBlob(bt, id, blob)
|
|
||||||
if errw != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteBlob(w http.ResponseWriter, r *http.Request, c *Context) {
|
|
||||||
uri := r.RequestURI
|
|
||||||
name, err := RepositoryName(uri)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repo, err := c.Repository(name)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bt := BackendType(uri)
|
|
||||||
if string(bt) == "" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := BlobID(uri)
|
|
||||||
if id.IsNull() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errd := repo.DeleteBlob(bt, id)
|
|
||||||
if errd != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
62
htpasswd.go
62
htpasswd.go
|
@ -1,62 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
// Copied from github.com/bitly/oauth2_proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/csv"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// lookup passwords in a htpasswd file
|
|
||||||
// The entries must have been created with -s for SHA encryption
|
|
||||||
|
|
||||||
type HtpasswdFile struct {
|
|
||||||
Users map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
|
|
||||||
r, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
return NewHtpasswd(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {
|
|
||||||
csv_reader := csv.NewReader(file)
|
|
||||||
csv_reader.Comma = ':'
|
|
||||||
csv_reader.Comment = '#'
|
|
||||||
csv_reader.TrimLeadingSpace = true
|
|
||||||
|
|
||||||
records, err := csv_reader.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h := &HtpasswdFile{Users: make(map[string]string)}
|
|
||||||
for _, record := range records {
|
|
||||||
h.Users[record[0]] = record[1]
|
|
||||||
}
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HtpasswdFile) Validate(user string, password string) bool {
|
|
||||||
realPassword, exists := h.Users[user]
|
|
||||||
if !exists {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if realPassword[:5] == "{SHA}" {
|
|
||||||
d := sha1.New()
|
|
||||||
d.Write([]byte(password))
|
|
||||||
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/restic/restic/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Repository is the place where backups are stored
|
|
||||||
type Repository struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRepository(path string) (*Repository, error) {
|
|
||||||
dirs := []string{
|
|
||||||
path,
|
|
||||||
filepath.Join(path, string(backend.Data)),
|
|
||||||
filepath.Join(path, string(backend.Snapshot)),
|
|
||||||
filepath.Join(path, string(backend.Index)),
|
|
||||||
filepath.Join(path, string(backend.Lock)),
|
|
||||||
filepath.Join(path, string(backend.Key)),
|
|
||||||
}
|
|
||||||
for _, d := range dirs {
|
|
||||||
_, err := os.Stat(d)
|
|
||||||
if err != nil {
|
|
||||||
err := os.MkdirAll(d, backend.Modes.Dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Repository{path}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) HasConfig() bool {
|
|
||||||
file := filepath.Join(r.path, string(backend.Config))
|
|
||||||
if _, err := os.Stat(file); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) ReadConfig() ([]byte, error) {
|
|
||||||
file := filepath.Join(r.path, string(backend.Config))
|
|
||||||
return ioutil.ReadFile(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) WriteConfig(data []byte) error {
|
|
||||||
file := filepath.Join(r.path, string(backend.Config))
|
|
||||||
return ioutil.WriteFile(file, data, backend.Modes.File)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) ListBlob(t backend.Type) ([]string, error) {
|
|
||||||
var blobs []string
|
|
||||||
dir := filepath.Join(r.path, string(t))
|
|
||||||
files, err := ioutil.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return blobs, err
|
|
||||||
}
|
|
||||||
blobs = make([]string, len(files))
|
|
||||||
for i, f := range files {
|
|
||||||
blobs[i] = f.Name()
|
|
||||||
}
|
|
||||||
return blobs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) HasBlob(bt backend.Type, id backend.ID) bool {
|
|
||||||
file := filepath.Join(r.path, string(bt), id.String())
|
|
||||||
if _, err := os.Stat(file); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) ReadBlob(bt backend.Type, id backend.ID) (io.ReadSeeker, error) {
|
|
||||||
file := filepath.Join(r.path, string(bt), id.String())
|
|
||||||
blob, err := ioutil.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bytes.NewReader(blob), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) WriteBlob(bt backend.Type, id backend.ID, data []byte) error {
|
|
||||||
file := filepath.Join(r.path, string(bt), id.String())
|
|
||||||
return ioutil.WriteFile(file, data, backend.Modes.File)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) DeleteBlob(bt backend.Type, id backend.ID) error {
|
|
||||||
file := filepath.Join(r.path, string(bt), id.String())
|
|
||||||
return os.Remove(file)
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/restic/restic/backend"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRepositoryConfig(t *testing.T) {
|
|
||||||
path := "/tmp/repository"
|
|
||||||
repository, _ := NewRepository(path)
|
|
||||||
defer os.RemoveAll(path)
|
|
||||||
|
|
||||||
_, e1 := os.Stat(path)
|
|
||||||
require.NoError(t, e1, "repository not created")
|
|
||||||
|
|
||||||
require.False(t, repository.HasConfig())
|
|
||||||
|
|
||||||
_, e2 := repository.ReadConfig()
|
|
||||||
require.Error(t, e2, "reading config should fail")
|
|
||||||
|
|
||||||
e3 := repository.WriteConfig([]byte("test"))
|
|
||||||
require.NoError(t, e3, "writing config should succeed")
|
|
||||||
|
|
||||||
require.True(t, repository.HasConfig())
|
|
||||||
|
|
||||||
config, _ := repository.ReadConfig()
|
|
||||||
require.Equal(t, config, []byte("test"), "reading config should succeed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepositoryBlob(t *testing.T) {
|
|
||||||
path := "/tmp/repository"
|
|
||||||
repository, _ := NewRepository(path)
|
|
||||||
//defer os.RemoveAll(path)
|
|
||||||
|
|
||||||
_, e1 := os.Stat(path)
|
|
||||||
require.NoError(t, e1, "repository not created")
|
|
||||||
|
|
||||||
require.False(t, repository.HasBlob(backend.Data, BlobID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
|
|
||||||
|
|
||||||
_, e2 := repository.ReadBlob(backend.Data, BlobID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
|
|
||||||
require.Error(t, e2, "reading blob should fail")
|
|
||||||
|
|
||||||
e3 := repository.WriteBlob(backend.Data, BlobID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), []byte("test"))
|
|
||||||
require.NoError(t, e3, "saving blob should succeed")
|
|
||||||
|
|
||||||
require.True(t, repository.HasBlob(backend.Data, BlobID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
|
|
||||||
|
|
||||||
blob, _ := repository.ReadBlob(backend.Data, BlobID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
|
|
||||||
bytes, e4 := ioutil.ReadAll(blob)
|
|
||||||
require.NoError(t, e4, e4.Error())
|
|
||||||
require.Equal(t, bytes, []byte("test"), "reading blob should succeed")
|
|
||||||
}
|
|
79
router.go
79
router.go
|
@ -1,79 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/restic/restic/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouter(context Context) *Router {
|
|
||||||
return &Router{context}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
m := r.Method
|
|
||||||
u := r.RequestURI
|
|
||||||
|
|
||||||
log.Printf("%s %s", m, u)
|
|
||||||
|
|
||||||
if err := Authorize(r, &router.Context); err == nil {
|
|
||||||
if handler := RestAPI(m, u); handler != nil {
|
|
||||||
handler(w, r, &router.Context)
|
|
||||||
} else {
|
|
||||||
http.Error(w, "not found", 404)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, err.Error(), 403)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Rest API returns a Handler when a match occur or nil.
|
|
||||||
func RestAPI(m string, u string) Handler {
|
|
||||||
s := strings.Split(u, "/")
|
|
||||||
|
|
||||||
// Check for valid repository name
|
|
||||||
_, err := RepositoryName(u)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route config requests
|
|
||||||
bt := BackendType(u)
|
|
||||||
if len(s) == 3 && bt == backend.Config {
|
|
||||||
switch m {
|
|
||||||
case "HEAD":
|
|
||||||
return HeadConfig
|
|
||||||
case "GET":
|
|
||||||
return GetConfig
|
|
||||||
case "POST":
|
|
||||||
return PostConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route blob requests
|
|
||||||
id := BlobID(u)
|
|
||||||
if len(s) == 4 && string(bt) != "" && bt != backend.Config {
|
|
||||||
if s[3] == "" && m == "GET" {
|
|
||||||
return ListBlob
|
|
||||||
} else if !id.IsNull() {
|
|
||||||
switch m {
|
|
||||||
case "HEAD":
|
|
||||||
return HeadBlob
|
|
||||||
case "GET":
|
|
||||||
return GetBlob
|
|
||||||
case "POST":
|
|
||||||
return PostBlob
|
|
||||||
case "DELETE":
|
|
||||||
return DeleteBlob
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -119,26 +119,26 @@ func TestRestAPI(t *testing.T) {
|
||||||
route{"GET", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"GET", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"POST", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"POST", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"DELETE", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"DELETE", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/snapshot/"},
|
route{"GET", "/repo/snapshots/"},
|
||||||
route{"HEAD", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"HEAD", "/repo/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"GET", "/repo/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"POST", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"POST", "/repo/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"DELETE", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"DELETE", "/repo/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/index/"},
|
route{"GET", "/repo/index/"},
|
||||||
route{"HEAD", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"HEAD", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"GET", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"POST", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"POST", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"DELETE", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"DELETE", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/snapshot/"},
|
route{"GET", "/repo/locks/"},
|
||||||
route{"HEAD", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"HEAD", "/repo/locks/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"GET", "/repo/locks/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"POST", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"POST", "/repo/locks/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"DELETE", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"DELETE", "/repo/locks/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/key/"},
|
route{"GET", "/repo/keys/"},
|
||||||
route{"HEAD", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"HEAD", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"GET", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"POST", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"POST", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
route{"DELETE", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
route{"DELETE", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, route := range validEndpoints {
|
for _, route := range validEndpoints {
|
||||||
|
|
110
server.go
110
server.go
|
@ -1,10 +1,17 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,12 +20,111 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Parse command-line args
|
||||||
var path = flag.String("path", "/tmp/restic", "specifies the path of the data directory")
|
var path = flag.String("path", "/tmp/restic", "specifies the path of the data directory")
|
||||||
var tls = flag.Bool("tls", false, "turns on tls support")
|
var tls = flag.Bool("tls", false, "turns on tls support")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
context := NewContext(*path)
|
// Create all the necessary subdirectories
|
||||||
router := Router{context}
|
dirs := []string{
|
||||||
|
backend.Paths.Data,
|
||||||
|
backend.Paths.Snapshots,
|
||||||
|
backend.Paths.Index,
|
||||||
|
backend.Paths.Locks,
|
||||||
|
backend.Paths.Keys,
|
||||||
|
}
|
||||||
|
for _, d := range dirs {
|
||||||
|
os.MkdirAll(filepath.Join(*path, d), backend.Modes.Dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := http.NewServeMux()
|
||||||
|
|
||||||
|
// Check if a configuration exists.
|
||||||
|
router.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uri := r.RequestURI
|
||||||
|
method := r.Method
|
||||||
|
log.Printf("%s %s", method, uri)
|
||||||
|
|
||||||
|
file := filepath.Join(*path, "config")
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
|
||||||
|
// Check if the config exists
|
||||||
|
if method == "HEAD" && err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the config
|
||||||
|
if method == "GET" && err == nil {
|
||||||
|
bytes, _ := ioutil.ReadFile(file)
|
||||||
|
w.Write(bytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the config
|
||||||
|
if method == "POST" && err != nil {
|
||||||
|
bytes, _ := ioutil.ReadAll(r.Body)
|
||||||
|
ioutil.WriteFile(file, bytes, 0600)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "404 not found", 404)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
router.HandleFunc("/"+dir+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uri := r.RequestURI
|
||||||
|
method := r.Method
|
||||||
|
log.Printf("%s %s", method, uri)
|
||||||
|
|
||||||
|
vars := strings.Split(r.RequestURI, "/")
|
||||||
|
dir := vars[1]
|
||||||
|
name := vars[2]
|
||||||
|
path := filepath.Join(*path, dir, name)
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
|
||||||
|
// List the blobs of a given dir.
|
||||||
|
if method == "GET" && name == "" && err == nil {
|
||||||
|
files, _ := ioutil.ReadDir(path)
|
||||||
|
names := make([]string, len(files))
|
||||||
|
for i, f := range files {
|
||||||
|
names[i] = f.Name()
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(names)
|
||||||
|
w.Write(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the blob esists
|
||||||
|
if method == "HEAD" && name != "" && err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a blob of a given dir.
|
||||||
|
if method == "GET" && name != "" && err == nil {
|
||||||
|
file, _ := os.Open(path)
|
||||||
|
defer file.Close()
|
||||||
|
http.ServeContent(w, r, "", time.Unix(0, 0), file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save a blob
|
||||||
|
if method == "POST" && name != "" && err != nil {
|
||||||
|
bytes, _ := ioutil.ReadAll(r.Body)
|
||||||
|
ioutil.WriteFile(path, bytes, 0600)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a blob
|
||||||
|
if method == "DELETE" && name != "" && err == nil {
|
||||||
|
os.Remove(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, "404 not found", 404)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the server
|
||||||
if !*tls {
|
if !*tls {
|
||||||
log.Printf("start server on port %s", HTTP)
|
log.Printf("start server on port %s", HTTP)
|
||||||
http.ListenAndServe(HTTP, router)
|
http.ListenAndServe(HTTP, router)
|
||||||
|
|
68
variables.go
68
variables.go
|
@ -1,68 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/restic/restic/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Returns the repository name for a given path
|
|
||||||
func RepositoryName(u string) (string, error) {
|
|
||||||
s := strings.Split(u, "/")
|
|
||||||
if len(s) <= 1 {
|
|
||||||
return "", errors.New("path does not contain repository name")
|
|
||||||
}
|
|
||||||
return ParseRepositoryName(s[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseRepositoryName(n string) (string, error) {
|
|
||||||
if len(n) < 1 {
|
|
||||||
return "", errors.New("repository name should contain at least 1 character")
|
|
||||||
}
|
|
||||||
match, err := regexp.MatchString("^[a-zA-Z0-9_-]*$", n)
|
|
||||||
if !match || err != nil {
|
|
||||||
return "", errors.New("repository name should not contains special characters")
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the backend type for a given path
|
|
||||||
func BackendType(u string) backend.Type {
|
|
||||||
s := strings.Split(u, "/")
|
|
||||||
var bt backend.Type
|
|
||||||
if len(s) > 2 {
|
|
||||||
bt = parseBackendType(s[2])
|
|
||||||
}
|
|
||||||
return bt
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBackendType(u string) backend.Type {
|
|
||||||
switch u {
|
|
||||||
case string(backend.Config):
|
|
||||||
return backend.Config
|
|
||||||
case string(backend.Data):
|
|
||||||
return backend.Data
|
|
||||||
case string(backend.Snapshot):
|
|
||||||
return backend.Snapshot
|
|
||||||
case string(backend.Key):
|
|
||||||
return backend.Key
|
|
||||||
case string(backend.Index):
|
|
||||||
return backend.Index
|
|
||||||
case string(backend.Lock):
|
|
||||||
return backend.Lock
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the blob ID for a given path
|
|
||||||
func BlobID(u string) backend.ID {
|
|
||||||
s := strings.Split(u, "/")
|
|
||||||
var id backend.ID
|
|
||||||
if len(s) > 3 {
|
|
||||||
id, _ = backend.ParseID(s[3])
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue