From 8bbaee1d4a88ce1eb95002e5fc920159fdec3778 Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Mon, 4 Mar 2024 13:11:06 +0000 Subject: [PATCH] Initial commit --- .gitignore | 1 + cmd/gomvn/main.go | 80 +++++++++++++ config.example.yml | 5 + database/db.go | 31 +++++ .../migrations/20240304102707_init.down.sql | 3 + .../migrations/20240304102707_init.up.sql | 28 +++++ database/models.go | 33 ++++++ database/queries/user.sql | 17 +++ database/user.sql.go | 106 ++++++++++++++++++ go.mod | 17 +++ go.sum | 27 +++++ identifier.sqlite | Bin 0 -> 32768 bytes initdb.go | 38 +++++++ paths/paths.go | 47 ++++++++ paths/repo.go | 25 +++++ routes/index.go.html | 22 ++++ routes/router.go | 65 +++++++++++ routes/utils.go | 13 +++ sqlc.yaml | 13 +++ 19 files changed, 571 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/gomvn/main.go create mode 100644 config.example.yml create mode 100644 database/db.go create mode 100644 database/migrations/20240304102707_init.down.sql create mode 100644 database/migrations/20240304102707_init.up.sql create mode 100644 database/models.go create mode 100644 database/queries/user.sql create mode 100644 database/user.sql.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 identifier.sqlite create mode 100644 initdb.go create mode 100644 paths/paths.go create mode 100644 paths/repo.go create mode 100644 routes/index.go.html create mode 100644 routes/router.go create mode 100644 routes/utils.go create mode 100644 sqlc.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/cmd/gomvn/main.go b/cmd/gomvn/main.go new file mode 100644 index 0000000..844276d --- /dev/null +++ b/cmd/gomvn/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "errors" + "flag" + "github.com/1f349/gomvn" + "github.com/1f349/gomvn/routes" + exitReload "github.com/MrMelon54/exit-reload" + "gopkg.in/yaml.v3" + "log" + "net/http" + "os" + "path/filepath" + "time" +) + +type startupConfig struct { + Name string `yaml:"name"` + Listen string `yaml:"listen"` + Repository []string `yaml:"repository"` +} + +func main() { + configPath := flag.String("conf", "", "/path/to/config.yml : path to the config file") + flag.Parse() + + log.Println("[GoMVN] Starting...") + + if *configPath == "" { + log.Fatal("[GoMVN] Error: config flag is missing") + return + } + + openConf, err := os.Open(*configPath) + if err != nil { + if os.IsNotExist(err) { + log.Fatal("[Violet] Error: missing config file") + } else { + log.Fatal("[Violet] Error: open config file: ", err) + } + return + } + + var config startupConfig + err = yaml.NewDecoder(openConf).Decode(&config) + if err != nil { + log.Fatal("[GoMVN] Error: invalid config file: ", err) + } + + // working directory is the parent of the config file + wd := filepath.Dir(*configPath) + db, err := gomvn.InitDB(filepath.Join(wd, "gomvn.sqlite3.db")) + if err != nil { + log.Fatal("[GoMVN] Error: invalid database: ", err) + return + } + + srv := &http.Server{ + Addr: config.Listen, + Handler: routes.Router(db, config.Name, config.Repository), + ReadTimeout: time.Minute, + ReadHeaderTimeout: time.Minute, + WriteTimeout: time.Minute, + IdleTimeout: time.Minute, + MaxHeaderBytes: 5000, + } + go func() { + err = srv.ListenAndServe() + if err != nil && !errors.Is(http.ErrServerClosed, err) { + log.Println("Serve HTTP Error:", err) + } + }() + + exitReload.ExitReload("GoMVN", func() {}, func() { + err := srv.Close() + if err != nil { + log.Println(err) + } + }) +} diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..1fed99b --- /dev/null +++ b/config.example.yml @@ -0,0 +1,5 @@ +name: Maven Repository +listen: 0.0.0.0:8080 +repository: + - release + - snapshot diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..61f5bf4 --- /dev/null +++ b/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/database/migrations/20240304102707_init.down.sql b/database/migrations/20240304102707_init.down.sql new file mode 100644 index 0000000..4249837 --- /dev/null +++ b/database/migrations/20240304102707_init.down.sql @@ -0,0 +1,3 @@ +DROP TABLE artifacts; +DROP TABLE paths; +DROP TABLE users; diff --git a/database/migrations/20240304102707_init.up.sql b/database/migrations/20240304102707_init.up.sql new file mode 100644 index 0000000..141acc3 --- /dev/null +++ b/database/migrations/20240304102707_init.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE artifacts +( + mvn_group TEXT NOT NULL, + artifact TEXT NOT NULL, + version TEXT NOT NULL, + modified TEXT NOT NULL +); + +CREATE TABLE paths +( + user_id INTEGER UNIQUE, + path TEXT PRIMARY KEY, + deploy TINYINT, + created_at DATETIME, + updated_at DATETIME, + + FOREIGN KEY (user_id) REFERENCES users (id) +); + +CREATE TABLE users +( + id INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + admin TINYINT, + token_hash TEXT NOT NULL, + created_at DATETIME, + updated_at DATETIME +); diff --git a/database/models.go b/database/models.go new file mode 100644 index 0000000..eaee22c --- /dev/null +++ b/database/models.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package database + +import ( + "database/sql" +) + +type Artifact struct { + MvnGroup string `json:"mvn_group"` + Artifact string `json:"artifact"` + Version string `json:"version"` + Modified string `json:"modified"` +} + +type Path struct { + UserID sql.NullInt64 `json:"user_id"` + Path string `json:"path"` + Deploy sql.NullInt64 `json:"deploy"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` + Admin sql.NullInt64 `json:"admin"` + TokenHash string `json:"token_hash"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} diff --git a/database/queries/user.sql b/database/queries/user.sql new file mode 100644 index 0000000..6ba7357 --- /dev/null +++ b/database/queries/user.sql @@ -0,0 +1,17 @@ +-- name: CountUsers :one +SELECT count(*) +FROM users; + +-- name: IsAdmin :one +SELECT 1 +FROM users +WHERE admin = 1 + AND token_hash = ?; + +-- name: GetAllUsers :many +SELECT id, name, admin, created_at, updated_at +FROM users; + +-- name: CreateUser :execlastid +INSERT INTO users (name, admin, token_hash, created_at, updated_at) +VALUES (?, ?, ?, ?, ?); diff --git a/database/user.sql.go b/database/user.sql.go new file mode 100644 index 0000000..aafbc60 --- /dev/null +++ b/database/user.sql.go @@ -0,0 +1,106 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: user.sql + +package database + +import ( + "context" + "database/sql" +) + +const countUsers = `-- name: CountUsers :one +SELECT count(*) +FROM users +` + +func (q *Queries) CountUsers(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countUsers) + var count int64 + err := row.Scan(&count) + return count, err +} + +const createUser = `-- name: CreateUser :execlastid +INSERT INTO users (name, admin, token_hash, created_at, updated_at) +VALUES (?, ?, ?, ?, ?) +` + +type CreateUserParams struct { + Name string `json:"name"` + Admin sql.NullInt64 `json:"admin"` + TokenHash string `json:"token_hash"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (int64, error) { + result, err := q.db.ExecContext(ctx, createUser, + arg.Name, + arg.Admin, + arg.TokenHash, + arg.CreatedAt, + arg.UpdatedAt, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const getAllUsers = `-- name: GetAllUsers :many +SELECT id, name, admin, created_at, updated_at +FROM users +` + +type GetAllUsersRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + Admin sql.NullInt64 `json:"admin"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) { + rows, err := q.db.QueryContext(ctx, getAllUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllUsersRow + for rows.Next() { + var i GetAllUsersRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Admin, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const isAdmin = `-- name: IsAdmin :one +SELECT 1 +FROM users +WHERE admin = 1 + AND token_hash = ? +` + +func (q *Queries) IsAdmin(ctx context.Context, tokenHash string) (int64, error) { + row := q.db.QueryRowContext(ctx, isAdmin, tokenHash) + var column_1 int64 + err := row.Scan(&column_1) + return column_1, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3632f80 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/1f349/gomvn + +go 1.22 + +require ( + github.com/MrMelon54/exit-reload v0.0.1 + github.com/golang-migrate/migrate/v4 v4.17.0 + github.com/julienschmidt/httprouter v1.3.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + go.uber.org/atomic v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..85f2b45 --- /dev/null +++ b/go.sum @@ -0,0 +1,27 @@ +github.com/MrMelon54/exit-reload v0.0.1 h1:sxHa59tNEQMcikwuX2+93lw6Vi1+R7oCRF8a0C3alXc= +github.com/MrMelon54/exit-reload v0.0.1/go.mod h1:PLiSfmUzwdpTTQP3BBfUPhkqPwaIZjx0DuXBnM76Bug= +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/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/identifier.sqlite b/identifier.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..4a5c8e47f230d1069341238a0ee546612509ecf0 GIT binary patch literal 32768 zcmeI&OK#IZ7{GD6ZIUYLYv&!E9f=x&3Sx=WP^Ohd>b9;Uk(Fg?mso8cjhzaXg>!HZ zF2fmEZ~?~S^pVJb*joRQY|qp2_nYxzR46B zUCPQUcLjAdALaj)tr*XS-`6*7qqg?V*gV}>-F&j~YwhRy4^<=~fB*srAbrWysoM@>O!SB%+5O5jwcVLE4q$-+LhwOwU63vPrQ>o(e8T3 zwxe!xBpt7%2WCGy8|caRo^X!U(d~5d)*u|j!@SwEou0aNUZ0LGqoLmqlD;Sybv}uL zGzxvSS9`0%_3R^=51Wp|#fEybS*=>HEz^jHVf1Nc5=_&PF8ex2e^*~>xBaCY=DCI$ z2Wdair{y^qT{VNr2Ku?qJWT#XS=YzmKhnxOgwgnF^jUYA6ZoGPTXju+aO_I^(9tu| zm_1ihxbi@{(%F}1LI)`taoAKUD}Phb5&AFmrP5sfch_CrHQN~L&GK!_tXbRJ=9i}$ zGMJ?CMR1-b_nDfmPRf1He_nw^IZ`4wbN7<4pST}=F7+=JZTFP^c z_2zo^wrxGUa^w&|009ILKmY**5I_I{1Q0-=d;;wM%QvS)ga85vAb literal 0 HcmV?d00001 diff --git a/initdb.go b/initdb.go new file mode 100644 index 0000000..3146509 --- /dev/null +++ b/initdb.go @@ -0,0 +1,38 @@ +package gomvn + +import ( + "database/sql" + "embed" + "errors" + "github.com/1f349/gomvn/database" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed database/migrations/*.sql +var migrations embed.FS + +func InitDB(p string) (*database.Queries, error) { + migDrv, err := iofs.New(migrations, "database/migrations") + if err != nil { + return nil, err + } + dbOpen, err := sql.Open("sqlite3", p) + if err != nil { + return nil, err + } + dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{}) + if err != nil { + return nil, err + } + mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv) + if err != nil { + return nil, err + } + err = mig.Up() + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + return nil, err + } + return database.New(dbOpen), nil +} diff --git a/paths/paths.go b/paths/paths.go new file mode 100644 index 0000000..5c63f53 --- /dev/null +++ b/paths/paths.go @@ -0,0 +1,47 @@ +package paths + +import ( + "fmt" + "net/http" + "strings" +) + +type Paths struct { + Repository []string +} + +func (p Paths) NormalizePath(path string) string { + if path[0] == '/' { + path = path[1:] + } + if strings.Contains(path, "..") || strings.Contains(path, "~") { + return "" + } + if strings.Count(path, "/") <= 1 { + return path + } + for _, repo := range p.Repository { + if strings.HasPrefix(path, repo) { + return path + } + } + return "" +} + +func (p Paths) ParsePath(req *http.Request) (string, error) { + path := p.NormalizePath(req.URL.Path) + if strings.Count(path, "/") < 3 { + return "", fmt.Errorf("path should be repository/group/artifact") + } + return path, nil +} + +func (p Paths) ParsePathParts(req *http.Request) (string, string, string, error) { + path, err := p.ParsePath(req) + if err != nil { + return "", "", "", err + } + parts := strings.Split(path, "/") + last := len(parts) - 1 + return parts[0], strings.Join(parts[1:last-1], "/"), parts[last], nil +} diff --git a/paths/repo.go b/paths/repo.go new file mode 100644 index 0000000..ed5f35d --- /dev/null +++ b/paths/repo.go @@ -0,0 +1,25 @@ +package paths + +import ( + "os" + "path/filepath" + "strings" +) + +func GetRepositories(basePath string, repository []string) map[string][]*entity.Artifact { + result := map[string][]*database.Artifact{} + for _, repo := range repository { + result[repo] = []*database.Artifact{} + repoPath := filepath.Join(basePath, repo) + _ = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(path, ".pom") { + path = strings.Replace(path, "\\", "/", -1) + path = strings.Replace(path, repoPath+"/", "", 1) + artifact := entity.NewArtifact(path, info.ModTime()) + result[repo] = append(result[repo], artifact) + } + return nil + }) + } + return result +} diff --git a/routes/index.go.html b/routes/index.go.html new file mode 100644 index 0000000..496dbc6 --- /dev/null +++ b/routes/index.go.html @@ -0,0 +1,22 @@ + + + GoMVN - {{.Name}} + + + +

{{.Name}}

+{{range $k, $v := .Repositories}} + +{{end}} + + diff --git a/routes/router.go b/routes/router.go new file mode 100644 index 0000000..4bf6f88 --- /dev/null +++ b/routes/router.go @@ -0,0 +1,65 @@ +package routes + +import ( + _ "embed" + "github.com/1f349/gomvn/database" + "github.com/1f349/gomvn/paths" + "github.com/julienschmidt/httprouter" + "html/template" + "io" + "net/http" + "os" +) + +type routeCtx struct { + db *database.Queries + pathUtils paths.Paths + name string + basePath string + repository []string +} + +func Router(db *database.Queries, name, basePath string, repository []string) http.Handler { + pUtils := paths.Paths{Repository: repository} + base := routeCtx{db, pUtils, name, basePath, repository} + + r := httprouter.New() + r.PUT("/*", base.handlePut) + r.GET("/", base.handleIndex) + r.GET("/*", base.handleGet) + return r +} + +//go:embed index.go.html +var indexHtml string + +var indexTemplate = template.Must(template.New("index").Parse(indexHtml)) + +func (r *routeCtx) handleIndex(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { + _ = indexTemplate.Execute(rw, map[string]any{ + "Name": r.name, + "Repositories": paths.GetRepositories(r.basePath, r.repository), + }) +} + +func (r *routeCtx) handlePut(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { + p, err := r.pathUtils.ParsePath(req) + if err != nil { + http.Error(rw, "404 Not Found", http.StatusNotFound) + return + } + create, err := os.Create(p) + if err != nil { + http.Error(rw, "500 Failed to open file", http.StatusInternalServerError) + return + } + _, err = io.Copy(create, req.Body) + if err != nil { + http.Error(rw, "500 Failed to write file", http.StatusInternalServerError) + return + } +} + +func (r *routeCtx) handleGet(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { + +} diff --git a/routes/utils.go b/routes/utils.go new file mode 100644 index 0000000..ca7dfec --- /dev/null +++ b/routes/utils.go @@ -0,0 +1,13 @@ +package routes + +import ( + "github.com/julienschmidt/httprouter" + "strconv" +) + +func getQueryUserId(params httprouter.Params) (int64, bool) { + if val, err := strconv.ParseInt(params.ByName("id"), 10, 64); err == nil { + return val, true + } + return 0, false +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..5f83137 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,13 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "database/queries" + schema: "database/migrations" + gen: + go: + package: "database" + out: "database" + emit_json_tags: true + overrides: + - column: "builds.meta" + go_type: "*github.com/mrmelon54/mc-upload-api/database/types.BuildMeta" \ No newline at end of file