refactoring

This commit is contained in:
Chapuis Bertil 2015-08-14 11:17:57 +02:00
parent 1ab2939041
commit 465ef4b493
18 changed files with 361 additions and 369 deletions

10
auth.go Normal file
View file

@ -0,0 +1,10 @@
package main
import (
"log"
"net/http"
)
func Authorize(r *http.Request) bool {
return true
}

View file

@ -1,37 +0,0 @@
package config
import (
"path/filepath"
"github.com/restic/restic/backend"
)
var root string
func Init(path string) {
root = path
}
func ConfigPath(repository string) string {
return filepath.Join(root, repository, string(backend.Config))
}
func DataPath(repository string) string {
return filepath.Join(root, repository, string(backend.Data))
}
func SnapshotPath(repository string) string {
return filepath.Join(root, repository, string(backend.Snapshot))
}
func IndexPath(repository string) string {
return filepath.Join(root, repository, string(backend.Index))
}
func LockPath(repository string) string {
return filepath.Join(root, repository, string(backend.Lock))
}
func KeyPath(repository string) string {
return filepath.Join(root, repository, string(backend.Key))
}

17
context.go Normal file
View file

@ -0,0 +1,17 @@
package main
import ()
// A Context specifies the root directory where all repositories are stored
type Context struct {
path string
}
func NewContext(path string) Context {
return Context{path}
}
// Creates the file structure of the Context
func (c *Context) Init() {
}

40
handlers.go Normal file
View file

@ -0,0 +1,40 @@
package main
import (
"fmt"
"net/http"
)
type Handler func(w http.ResponseWriter, r *http.Request, c *Context)
func HeadConfig(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "head config")
}
func GetConfig(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "get config")
}
func PostConfig(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "post config")
}
func ListBlob(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "list blob")
}
func HeadBlob(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "head blob")
}
func GetBlob(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "get blob")
}
func PostBlob(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "post blob")
}
func DeleteBlob(w http.ResponseWriter, r *http.Request, c *Context) {
fmt.Fprintln(w, "delete blob")
}

View file

@ -1,56 +0,0 @@
package handlers
import (
"io/ioutil"
"net/http"
"os"
"github.com/bchapuis/restic-server/config"
)
func HeadConfig(w http.ResponseWriter, r *http.Request) {
repo, err := ExtractRepository(r)
if err != nil {
http.Error(w, "403 invalid repository", 403)
return
}
file := config.ConfigPath(repo)
if _, err := os.Stat(file); err != nil {
http.Error(w, "404 repository not found", 404)
return
}
}
func GetConfig(w http.ResponseWriter, r *http.Request) {
repo, err := ExtractRepository(r)
if err != nil {
http.Error(w, "403 invalid repository", 403)
return
}
file := config.ConfigPath(repo)
if _, err := os.Stat(file); err == nil {
bytes, _ := ioutil.ReadFile(file)
w.Write(bytes)
return
} else {
http.Error(w, "404 repository not found", 404)
return
}
}
func PostConfig(w http.ResponseWriter, r *http.Request) {
repo, err := ExtractRepository(r)
if err != nil {
http.Error(w, "403 invalid repository", 403)
return
}
file := config.ConfigPath(repo)
if _, err := os.Stat(file); err == nil {
http.Error(w, "409 repository already initialized", 409)
return
} else {
bytes, _ := ioutil.ReadAll(r.Body)
ioutil.WriteFile(file, bytes, 0600)
return
}
}

View file

@ -1,54 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/bchapuis/restic-server/config"
)
func HeadData(w http.ResponseWriter, r *http.Request) {
repo, err := ExtractRepository(r)
if err != nil {
http.Error(w, "403 invalid repository", 403)
return
}
id, err := ExtractID(r)
if err != nil {
http.Error(w, "403 invalid ID", 403)
return
}
file := filepath.Join(config.DataPath(repo), id.String())
if _, err := os.Stat(file); err != nil {
http.Error(w, "404 repository not found", 404)
return
}
}
func GetData(w http.ResponseWriter, r *http.Request) {
repo, err := ExtractRepository(r)
if err != nil {
http.Error(w, "403 invalid repository", 403)
return
}
id, err := ExtractID(r)
if err != nil {
http.Error(w, "403 invalid ID", 403)
return
}
file := filepath.Join(config.DataPath(repo), id.String())
if _, err := os.Stat(file); err != nil {
http.Error(w, "404 repository not found", 404)
return
}
}
func PostData(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "data")
}
func DeleteData(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "data")
}

View file

@ -1,22 +0,0 @@
package handlers
import (
"fmt"
"net/http"
)
func HeadIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "data")
}
func GetIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "index")
}
func PostIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "index")
}
func DeleteIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "index")
}

View file

@ -1,22 +0,0 @@
package handlers
import (
"fmt"
"net/http"
)
func HeadKey(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "data")
}
func GetKey(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "key")
}
func PostKey(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "key")
}
func DeleteKey(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "key")
}

View file

@ -1,22 +0,0 @@
package handlers
import (
"fmt"
"net/http"
)
func HeadLock(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "data")
}
func GetLock(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "lock")
}
func PostLock(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "lock")
}
func DeleteLock(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "lock")
}

View file

@ -1,10 +0,0 @@
package handlers
import (
"log"
"net/http"
)
func RequestLogger(w http.ResponseWriter, r *http.Request) {
log.Printf("%v %v", r.Method, r.URL.String())
}

View file

@ -1,22 +0,0 @@
package handlers
import (
"fmt"
"net/http"
)
func HeadSnapshot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "data")
}
func GetSnapshot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "snapshot")
}
func PostSnapshot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "snapshot")
}
func DeleteSnapshot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "snapshot")
}

View file

@ -1,25 +0,0 @@
package handlers
import (
"errors"
"net/http"
"strings"
"github.com/restic/restic/backend"
)
func ExtractUser(r *http.Request) (string, string, error) {
return "username", "password", nil
}
func ExtractRepository(r *http.Request) (string, error) {
return "repository", nil
}
func ExtractID(r *http.Request) (backend.ID, error) {
path := strings.Split(r.URL.String(), "/")
if len(path) != 3 {
return backend.ID{}, errors.New("invalid request path")
}
return backend.ParseID(path[2])
}

13
repository.go Normal file
View file

@ -0,0 +1,13 @@
package main
import ()
// A Repository is the place where backups are stored
type Repository struct {
path string
}
// Creates the file structure of the Repository
func (r *Repository) Init() {
}

BIN
restic-server Executable file

Binary file not shown.

110
router.go Normal file
View file

@ -0,0 +1,110 @@
package main
import (
"errors"
"log"
"net/http"
"regexp"
"strings"
"github.com/restic/restic/backend"
)
// Route all the server requests
func Router(w http.ResponseWriter, r *http.Request) {
m := r.Method
u := r.RequestURI
log.Println("%s %s", m, u)
if Authorize(r) {
if handler := RestAPI(m, u); handler != nil {
handler(w, r, nil)
} else {
http.Error(w, "not found", 404)
}
} else {
http.Error(w, "unauthorized", 403)
}
}
// 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")
}
if len(s[1]) < 1 {
return "", errors.New("repository name should contain at least 1 character")
}
match, err := regexp.MatchString("^[a-zA-Z0-9_-]*$", s[1])
if !match || err != nil {
return "", errors.New("repository name should not contains special characters")
}
return s[1], 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, _ = backend.ParseType(s[2])
}
return bt
}
// 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
}
// 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 && !bt.IsNull() && 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
}

View file

@ -1,54 +0,0 @@
package router
import (
"net/http"
"strings"
)
type Route struct {
method string
pattern string
handler http.Handler
}
type Router struct {
filters []http.Handler
routes []Route
}
func NewRouter() Router {
filters := []http.Handler{}
routes := []Route{}
return Router{filters, routes}
}
func (router *Router) Filter(handler http.Handler) {
router.filters = append(router.filters, handler)
}
func (router *Router) FilterFunc(handlerFunc http.HandlerFunc) {
router.Filter(handlerFunc)
}
func (router *Router) Handle(method string, pattern string, handler http.Handler) {
router.routes = append(router.routes, Route{method, pattern, handler})
}
func (router *Router) HandleFunc(method string, pattern string, handlerFunc http.HandlerFunc) {
router.Handle(method, pattern, handlerFunc)
}
func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for i := 0; i < len(router.filters); i++ {
filter := router.filters[i]
filter.ServeHTTP(w, r)
}
for i := 0; i < len(router.routes); i++ {
route := router.routes[i]
if route.method == r.Method && strings.HasPrefix(r.URL.String(), route.pattern) {
route.handler.ServeHTTP(w, r)
return
}
}
http.NotFound(w, r)
}

168
router_test.go Normal file
View file

@ -0,0 +1,168 @@
package main
import (
"testing"
"github.com/restic/restic/backend"
)
func TestRepositoryName(t *testing.T) {
var name string
var err error
name, err = RepositoryName("")
if err == nil {
t.Error("empty string should produce an error")
}
name, err = RepositoryName("/")
if err == nil {
t.Error("empty repository name should produce an error")
}
name, err = RepositoryName("//")
if err == nil {
t.Error("empty repository name should produce an error")
}
name, err = RepositoryName("/$test")
if err == nil {
t.Error("special characters should produce an error")
}
name, err = RepositoryName("/test")
if name != "test" {
t.Errorf("repository name is %s but should be test", name)
}
name, err = RepositoryName("/test-1234")
if name != "test-1234" {
t.Errorf("repository name is %s but should be test-1234", name)
}
name, err = RepositoryName("/test_1234")
if name != "test_1234" {
t.Errorf("repository name is %s but should be test_1234", name)
}
}
func TestBackendType(t *testing.T) {
var bt backend.Type
bt = BackendType("/")
if !bt.IsNull() {
t.Error("backend type should be nil")
}
bt = BackendType("/test")
if !bt.IsNull() {
t.Error("backend type should be nil")
}
bt = BackendType("/test/config")
if bt != backend.Config {
t.Error("backend type should be config")
}
bt = BackendType("/test/config/")
if bt != backend.Config {
t.Error("backend type should be config")
}
bt = BackendType("/test/config/test")
if bt != backend.Config {
t.Error("backend type should be config")
}
}
func TestBlobID(t *testing.T) {
var id backend.ID
id = BlobID("/")
if !id.IsNull() {
t.Error("blob id should be nil")
}
id = BlobID("/test")
if !id.IsNull() {
t.Error("blob id should be nil")
}
id = BlobID("/test/data")
if !id.IsNull() {
t.Error("blob id should be nil")
}
id = BlobID("/test/data/")
if !id.IsNull() {
t.Error("blob id should be nil")
}
id = BlobID("/test/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
if id.IsNull() {
t.Error("blob id should not be nil")
}
}
func TestRestAPI(t *testing.T) {
type route struct {
method string
path string
}
validEndpoints := []route{
route{"HEAD", "/repo/config"},
route{"GET", "/repo/config"},
route{"POST", "/repo/config"},
route{"GET", "/repo/data/"},
route{"HEAD", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"POST", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"DELETE", "/repo/data/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/snapshot/"},
route{"HEAD", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"POST", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"DELETE", "/repo/snapshot/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/index/"},
route{"HEAD", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"POST", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"DELETE", "/repo/index/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/snapshot/"},
route{"HEAD", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"POST", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"DELETE", "/repo/lock/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/key/"},
route{"HEAD", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"POST", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"DELETE", "/repo/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
}
for _, route := range validEndpoints {
if RestAPI(route.method, route.path) == nil {
t.Errorf("request %s %s should return a handler", route.method, route.path)
}
}
invalidEndpoints := []route{
route{"GET", "/"},
route{"GET", "/repo"},
route{"GET", "/repo/config/"},
route{"GET", "/repo/config/aaaa"},
route{"GET", "/repo/data"},
route{"GET", "/repo/data/aaaaaaa"},
route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/"},
route{"GET", "/repo/keys/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/test"},
}
for _, route := range invalidEndpoints {
if RestAPI(route.method, route.path) != nil {
t.Errorf("request %s %s should return nil", route.method, route.path)
}
}
}

View file

@ -1,52 +1,10 @@
package main
import (
"io/ioutil"
"net/http"
"github.com/bchapuis/restic-server/config"
"github.com/bchapuis/restic-server/handlers"
"github.com/bchapuis/restic-server/router"
)
import ()
func main() {
path, _ := ioutil.TempDir("", "restic-repository-")
//path, _ := ioutil.TempDir("", "restic-repository-")
config.Init(path)
r := router.NewRouter()
r.FilterFunc(handlers.RequestLogger)
r.HandleFunc("HEAD", "/config", handlers.HeadConfig)
r.HandleFunc("GET", "/config", handlers.GetConfig)
r.HandleFunc("POST", "/config", handlers.PostConfig)
r.HandleFunc("HEAD", "/data", handlers.HeadData)
r.HandleFunc("GET", "/data", handlers.GetData)
r.HandleFunc("POST", "/data", handlers.PostData)
r.HandleFunc("DELETE", "/data", handlers.DeleteData)
r.HandleFunc("HEAD", "/snapshot", handlers.HeadSnapshot)
r.HandleFunc("GET", "/snapshot", handlers.GetSnapshot)
r.HandleFunc("POST", "/snapshot", handlers.PostSnapshot)
r.HandleFunc("DELETE", "/snapshot", handlers.DeleteSnapshot)
r.HandleFunc("HEAD", "/index", handlers.HeadIndex)
r.HandleFunc("GET", "/index", handlers.GetIndex)
r.HandleFunc("POST", "/index", handlers.PostIndex)
r.HandleFunc("DELETE", "/index", handlers.DeleteIndex)
r.HandleFunc("HEAD", "/lock", handlers.HeadLock)
r.HandleFunc("GET", "/lock", handlers.GetLock)
r.HandleFunc("POST", "/lock", handlers.PostLock)
r.HandleFunc("DELETE", "/lock", handlers.DeleteLock)
r.HandleFunc("HEAD", "/key", handlers.HeadKey)
r.HandleFunc("GET", "/key", handlers.GetKey)
r.HandleFunc("POST", "/key", handlers.PostKey)
r.HandleFunc("DELETE", "/key", handlers.DeleteKey)
http.ListenAndServe(":8000", r)
//http.ListenAndServe(":8000", r)
}