From 68902f86af8db5339ae6d3d41879ae5d406f594d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lie=20DELHAIE?= Date: Tue, 30 Sep 2025 20:21:33 +0200 Subject: [PATCH] first commit --- .gitignore | 1 + cmd/server/api/api.go | 59 +++++++++++++++++ cmd/server/api/middlewares.go | 15 +++++ cmd/server/api/responses.go | 121 ++++++++++++++++++++++++++++++++++ cmd/server/core/git/git.go | 73 ++++++++++++++++++++ cmd/server/main.go | 21 ++++++ go.mod | 26 ++++++++ go.sum | 66 +++++++++++++++++++ pkg/constants/constants.go | 6 ++ pkg/remote/obj/obj.go | 30 +++++++++ 10 files changed, 418 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/server/api/api.go create mode 100644 cmd/server/api/middlewares.go create mode 100644 cmd/server/api/responses.go create mode 100644 cmd/server/core/git/git.go create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/constants/constants.go create mode 100644 pkg/remote/obj/obj.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cad2309 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tmp \ No newline at end of file diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go new file mode 100644 index 0000000..f8984a5 --- /dev/null +++ b/cmd/server/api/api.go @@ -0,0 +1,59 @@ +package api + +import ( + "fmt" + "mirror-sync/pkg/constants" + "mirror-sync/pkg/remote/obj" + "net/http" + "runtime" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +type ( + HTTPServer struct { + Server *http.Server + } +) + +func NewServer(port int) *HTTPServer { + s := &HTTPServer{} + router := chi.NewRouter() + router.NotFound(func(writer http.ResponseWriter, request *http.Request) { + notFound("id not found", writer, request) + }) + router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) { + methodNotAllowed(writer, request) + }) + router.Use(middleware.Logger) + router.Use(recoverMiddleware) + router.Use(middleware.GetHead) + router.Use(middleware.Compress(5, "application/gzip")) + router.Use(middleware.Heartbeat("/heartbeat")) + router.Route("/api", func(routerAPI chi.Router) { + routerAPI.Route("/v1", func(r chi.Router) { + // Get information about the server + r.Get("/version", s.Information) + r.Route("/sync", func(r chi.Router) { + + }) + }) + }) + s.Server = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: router, + } + return s +} + +func (s *HTTPServer) Information(w http.ResponseWriter, r *http.Request) { + info := obj.SystemInformation{ + Version: constants.Version, + APIVersion: constants.ApiVersion, + GoVersion: runtime.Version(), + OSName: runtime.GOOS, + OSArchitecture: runtime.GOARCH, + } + ok(info, w, r) +} diff --git a/cmd/server/api/middlewares.go b/cmd/server/api/middlewares.go new file mode 100644 index 0000000..afcbc38 --- /dev/null +++ b/cmd/server/api/middlewares.go @@ -0,0 +1,15 @@ +package api + +import "net/http" + +func recoverMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + err := recover() + if err != nil { + internalServerError(w, r) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/server/api/responses.go b/cmd/server/api/responses.go new file mode 100644 index 0000000..6a4b8af --- /dev/null +++ b/cmd/server/api/responses.go @@ -0,0 +1,121 @@ +package api + +import ( + "encoding/json" + "log/slog" + "mirror-sync/pkg/remote/obj" + "net/http" + "time" +) + +func internalServerError(w http.ResponseWriter, r *http.Request) { + payload := obj.HTTPError{ + HTTPCore: obj.HTTPCore{ + Status: http.StatusInternalServerError, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Internal Server Error", + Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.", + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) + } +} + +func notFound(message string, w http.ResponseWriter, r *http.Request) { + payload := obj.HTTPError{ + HTTPCore: obj.HTTPCore{ + Status: http.StatusNotFound, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Not Found", + Message: message, + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) + } +} + +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + payload := obj.HTTPError{ + HTTPCore: obj.HTTPCore{ + Status: http.StatusMethodNotAllowed, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Method Not Allowed", + Message: "The server knows the request method, but the target resource doesn't support this method", + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) + } +} + +func unauthorized(w http.ResponseWriter, r *http.Request) { + payload := obj.HTTPError{ + HTTPCore: obj.HTTPCore{ + Status: http.StatusUnauthorized, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Unauthorized", + Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.", + } + + w.Header().Add("Content-Type", "application/json") + w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"") + w.WriteHeader(http.StatusUnauthorized) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) + } +} + +func ok(o interface{}, w http.ResponseWriter, r *http.Request) { + payload := obj.HTTPObject{ + HTTPCore: obj.HTTPCore{ + Status: http.StatusOK, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Data: o, + } + w.Header().Add("Content-Type", "application/json") + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) + } +} + +func badRequest(message string, w http.ResponseWriter, r *http.Request) { + payload := obj.HTTPError{ + HTTPCore: obj.HTTPCore{ + Status: http.StatusBadRequest, + Path: r.RequestURI, + Timestamp: time.Now(), + }, + Error: "Bad Request", + Message: message, + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) + } +} diff --git a/cmd/server/core/git/git.go b/cmd/server/core/git/git.go new file mode 100644 index 0000000..6a15dcf --- /dev/null +++ b/cmd/server/core/git/git.go @@ -0,0 +1,73 @@ +package git + +import ( + "fmt" + + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/config" + "github.com/go-git/go-git/v6/plumbing/transport" + "github.com/go-git/go-git/v6/plumbing/transport/http" + "github.com/go-git/go-git/v6/storage/memory" +) + +type ( + Repository struct { + src string + dst string + auth Authentication + } + + Authentication interface { + Value() transport.AuthMethod + } + + TokenAuthentication struct { + username string + token string + } + + NoAuthentication struct{} +) + +func Sync(r Repository) error { + repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ + URL: r.src, + }) + if err != nil { + return fmt.Errorf("failed to clone repository from source: %w", err) + } + + m, err := repo.CreateRemote(&config.RemoteConfig{ + Name: "mirror", + Mirror: true, + URLs: []string{ + r.dst, + }, + }) + if err != nil { + return fmt.Errorf("failed to create remote: %w", err) + } + + err = m.Push(&git.PushOptions{ + RemoteName: "mirror", + Auth: r.auth.Value(), + RefSpecs: []config.RefSpec{"+refs/*:refs/*"}, + Force: true, + }) + if err != nil { + return fmt.Errorf("failed to push to mirror server: %w", err) + } + + return nil +} + +func (a TokenAuthentication) Value() transport.AuthMethod { + return &http.BasicAuth{ + Username: a.username, + Password: a.token, + } +} + +func (NoAuthentication) Value() transport.AuthMethod { + return nil +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..04041c9 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "mirror-sync/cmd/server/api" + "mirror-sync/pkg/constants" + "os" + "runtime" +) + +func main() { + fmt.Printf("mirror-sync daemon -- v%s.%s.%s\n\n", constants.Version, runtime.GOOS, runtime.GOARCH) + + s := api.NewServer(8080) + + fmt.Println("daemon listening to :8080") + if err := s.Server.ListenAndServe(); err != nil { + fmt.Fprintln(os.Stderr, "failed to start server:", err.Error()) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f1ca3b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module mirror-sync + +go 1.25 + +require ( + github.com/go-chi/chi/v5 v5.2.3 + github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg/v2 v2.0.2 // indirect + github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sys v0.36.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b2b5a8e --- /dev/null +++ b/go.sum @@ -0,0 +1,66 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= +github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= +github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA= +github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM= +github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w= +github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU= +github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd h1:30HEd5KKVM7GgMJ1GSNuYxuZXEg8Pdlngp6T51faxoc= +github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd/go.mod h1:lz8PQr/p79XpFq5ODVBwRJu5LnOF8Et7j95ehqmCMJU= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 0000000..8ef2fa1 --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,6 @@ +package constants + +const ( + Version string = "0.0.1" + ApiVersion int = 1 +) diff --git a/pkg/remote/obj/obj.go b/pkg/remote/obj/obj.go new file mode 100644 index 0000000..fed64c0 --- /dev/null +++ b/pkg/remote/obj/obj.go @@ -0,0 +1,30 @@ +package obj + +import "time" + +type ( + HTTPCore struct { + Status int `json:"status"` + Timestamp time.Time `json:"timestamp"` + Path string `json:"path"` + } + + HTTPError struct { + HTTPCore + Error string `json:"error"` + Message string `json:"message"` + } + + HTTPObject struct { + HTTPCore + Data any `json:"data"` + } + + SystemInformation struct { + Version string `json:"version"` + APIVersion int `json:"api_version"` + GoVersion string `json:"go_version"` + OSName string `json:"os_name"` + OSArchitecture string `json:"os_architecture"` + } +)