Adding command line options

This commit is contained in:
OLUWADAMILOLA OKUSANYA 2023-06-30 15:06:46 -04:00
parent e3e81130ba
commit cb52ddab05
16 changed files with 751 additions and 274 deletions

26
cmd/gosimplenpm/conf.go Normal file
View File

@ -0,0 +1,26 @@
package gosimplenpm
import (
"fmt"
"gosimplenpm/config"
"os"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Aliases: []string{"conf"},
Short: "Display the config file",
Run: func(_ *cobra.Command, _ []string) {
err := config.PrintConfigFile()
if err != nil {
fmt.Printf("Error printing config: %+v\n", err)
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(configCmd)
}

83
cmd/gosimplenpm/root.go Normal file
View File

@ -0,0 +1,83 @@
package gosimplenpm
import (
"fmt"
"gosimplenpm/config"
"gosimplenpm/handler"
"os"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "gosimplenpm",
Short: "Gosimplenpm is an implementation of npm registry server",
Long: `Gosimplenpm is an implemenation of npm registry server.
It creates the repository to host all your private npm repos. It can
serve the npm packages offline.
Documentation about the npm private registry:
https://docs.npmjs.com/packages-and-modules
`,
Run: func(_ *cobra.Command, _ []string) {
err := config.VerifyConfig()
if err != nil {
fmt.Printf("Error verifying config: %+v\n", err)
os.Exit(1)
}
var cfg config.Config
err = config.LoadOrCreateConfig(&cfg)
if err != nil {
fmt.Printf("Error loading config: %+v\n", err)
os.Exit(1)
}
lvl, err := logrus.ParseLevel(cfg.LogLevel)
if err != nil {
fmt.Printf("%+v", err)
os.Exit(1)
}
log := &logrus.Logger{
Out: os.Stdout,
Level: lvl,
Formatter: &logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2009-01-02 15:15:15",
},
}
app := &handler.Application{
Conf: cfg,
Logger: log,
}
fmt.Println("\n Server is starting....")
err = app.Start()
fmt.Println("Why!")
if err != nil {
fmt.Printf("Server start up error: %+v\n", err)
os.Exit(1)
}
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.CompletionOptions.HiddenDefaultCmd = true
// hide help command
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
// hide help flag
rootCmd.PersistentFlags().BoolP("help", "h", false, "This help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
// TODO: get some variables from flags
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for server")
_ = rootCmd.MarkFlagRequired("listen")
rootCmd.Flags().BoolVarP(&config.CanLog, "verbose", "v", config.CanLog, "Logging level for server")
rootCmd.Flags().StringVar(&config.NpmRepoDir, "repodir", config.NpmRepoDir, "Repo dir to house published packages")
}

View File

@ -1,36 +0,0 @@
package main
import (
"gosimplenpm/cmd/npmregserver"
"gosimplenpm/config"
"net/http"
"os"
"github.com/sirupsen/logrus"
)
func main() {
log := &logrus.Logger{
Out: os.Stdout,
Level: logrus.DebugLevel,
Formatter: &logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2009-01-02 15:15:15",
},
}
var cfg config.Config
err := config.LoadConfiguration("userdata/config.json", &cfg)
if err != nil {
log.Fatalf("Config is not loaded: %+v\n", err)
}
app := &npmregserver.Application{
Conf: cfg,
Logger: log,
}
log.Infoln("Starting server on port 4000")
err = http.ListenAndServe(":4000", app.Routes())
log.Fatal(err)
}

View File

@ -1,36 +0,0 @@
package npmregserver
import (
"gosimplenpm/config"
"gosimplenpm/handler"
"gosimplenpm/middlewares"
"net/http"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type Application struct {
Logger *logrus.Logger
Conf config.Config
}
func (app *Application) Routes() *mux.Router {
// Need to use UseEncodedPath as shown here https://github.com/gorilla/mux/blob/master/mux.go#L269
m := mux.NewRouter().StrictSlash(true).UseEncodedPath()
m.Use(middlewares.LogMiddleware(app.Logger))
// main handler
m.HandleFunc("/{name}", handler.Get).Methods("GET")
m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.Conf)(handler.Publish)).Methods("PUT")
// tar handlers
m.HandleFunc("/{name}/-/{tar}", handler.Tar).Methods("GET")
// tag handlers
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(handler.DistTagDelete(app.Logger))).Methods("DELETE")
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(handler.DistTagPut(app.Logger))).Methods("PUT")
m.HandleFunc("/-/package/{name}/dist-tags", handler.DistTagGet(app.Logger)).Methods("GET")
m.NotFoundHandler = http.HandlerFunc(handler.NotFound)
return m
}

View File

@ -1,36 +1,316 @@
package config
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"path"
"golang.org/x/crypto/bcrypt"
)
var (
// File to store the app configuration, like username, password, token, repo dir, logging level
ConfigFilePath string
// Repo dir
NpmRepoDir string
// HTTPListen
HTTPListen string
// Set Verbose Logging
CanLog bool
// Logging Level
LoggingLvl string
// Username
RegUser string
// Password
RegPwd string
)
type Config struct {
Token string `json:"token"`
Token string `json:"token"`
RepoDir string `json:"repoDir"`
IpAddress string `json:"ipAddress"`
LogLevel string `json:"logLevel"`
}
func LoadConfiguration(file string, config *Config) error {
filePath, err := filepath.Abs(file)
func checkIfCorrectIPPort(s string) bool {
host, port, _ := net.SplitHostPort(s)
if host == "" || port == "" {
return false
}
if net.ParseIP(host) != nil {
return true
}
_, err := net.ResolveIPAddr("ip", host)
return err == nil
}
func VerifyConfig() error {
if !checkIfCorrectIPPort(HTTPListen) {
return errors.New("ip address should be in the format of <ip>:<port>")
}
// Check if config file exists
dirname, err := os.UserHomeDir()
if err != nil {
fmt.Printf("File repo not found: +%v\n", err)
return err
}
configFile, err := os.Open(filePath)
// From https://stackoverflow/a/76287159
defer func() {
err = errors.Join(err, configFile.Close())
configDirPath, err := checkOrCreateConfigDir(dirname, true)
if err != nil {
return err
}
ConfigFilePath = path.Join(configDirPath, "config.json")
if NpmRepoDir == "" {
NpmRepoDir = path.Join(dirname, ".gosimplenpm", "registry")
err := checkOrCreateRepoDir(NpmRepoDir)
if err != nil {
fmt.Printf("File cannot be closed: +%v\n", err)
return err
}
} else if NpmRepoDir != "" {
err := checkOrCreateRepoDir(NpmRepoDir)
if err != nil {
return err
}
}()
if err != nil {
fmt.Printf("File cannot be opened: +%v\n", err)
return err
}
json.NewDecoder(configFile).Decode(config)
fmt.Println("Json loaded")
if CanLog {
LoggingLvl = "DEBUG"
fmt.Println("\n Enabled debug logging")
} else {
LoggingLvl = "INFO"
}
return nil
}
func checkOrCreateConfigDir(fp string, canCreate bool) (string, error) {
configDirPath := path.Join(fp, ".gosimplenpm", "config")
ok := isDir(configDirPath)
if !ok && canCreate {
err := os.MkdirAll(configDirPath, os.ModePerm)
if err != nil {
return "", err
}
}
if !ok && !canCreate {
return "", nil
}
return configDirPath, nil
}
func checkOrCreateRepoDir(repoDirPath string) error {
ok := isDir(repoDirPath)
if !ok {
err := os.MkdirAll(repoDirPath, os.ModePerm)
if err != nil {
return err
}
}
return nil
}
func createConfig(cfg *Config, recreate bool) error {
var scanner *bufio.Scanner
if recreate {
fmt.Println("\nNew config variables. Saving...")
} else {
fmt.Println("\nConfig file is not found. Creating...")
}
configFile, err := os.Create(ConfigFilePath)
if err != nil {
return err
}
defer configFile.Close()
cfg.IpAddress = HTTPListen
cfg.LogLevel = LoggingLvl
// Get username
if cfg.Token == "" {
fmt.Println("Enter your username: ")
scanner = bufio.NewScanner(os.Stdin)
scanner.Scan()
err = scanner.Err()
if err != nil {
return err
}
RegUser = scanner.Text()
fmt.Println("Enter your password: ")
scanner = bufio.NewScanner(os.Stdin)
scanner.Scan()
err = scanner.Err()
if err != nil {
return err
}
RegPwd = scanner.Text()
token, err := generateAuthToken()
if err != nil {
return err
}
cfg.Token = token
}
fmt.Printf("The npm authToken is %s.\n", cfg.Token)
err = json.NewEncoder(configFile).Encode(cfg)
if err != nil {
return err
}
return nil
}
func loadConfig(cfg *Config) error {
configFile, err := os.Open(ConfigFilePath)
if err != nil {
return err
}
defer configFile.Close()
err = json.NewDecoder(configFile).Decode(cfg)
if err != nil {
return err
}
return nil
}
func LoadOrCreateConfig(cfg *Config) error {
var err error
ok := isFile(ConfigFilePath)
// If file is not found
if !ok {
err = createConfig(cfg, false)
if err != nil {
return err
}
}
if ok {
// File is found
err = loadConfig(cfg)
if err != nil {
return err
}
if cfg.Token == "" || cfg.IpAddress != HTTPListen || cfg.LogLevel != LoggingLvl || cfg.RepoDir != NpmRepoDir {
// recreate the config file
err = createConfig(cfg, true)
if err != nil {
return err
}
}
}
return err
}
func isFile(fp string) bool {
info, err := os.Stat(fp)
if os.IsNotExist(err) || !info.Mode().IsRegular() {
return false
}
return true
}
func isDir(fp string) bool {
info, err := os.Stat(fp)
if os.IsNotExist(err) || !info.IsDir() {
return false
}
return true
}
// Hash password
func hashPassword(password string) (string, error) {
// Convert password string to byte slice
var passwordBytes = []byte(password)
// Hash password with Bcrypt's min cost
hashedPasswordBytes, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.MinCost)
return string(hashedPasswordBytes), err
}
// Check if two passwords match using Bcrypt's CompareHashAndPassword
// which return nil on success and an error on failure.
// func doPasswordsMatch(hashedPassword, currPassword string) bool {
// err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(currPassword))
// return err == nil
// }
func generateAuthToken() (string, error) {
hashed, err := hashPassword(RegPwd)
if err != nil {
return "", err
}
token := fmt.Sprintf("%s::%s", RegUser, hashed)
return token, nil
}
func PrintConfigFile() error {
// Check if config file exists
dirname, err := os.UserHomeDir()
if err != nil {
return err
}
configDirPath, err := checkOrCreateConfigDir(dirname, false)
if err != nil {
return err
}
if configDirPath == "" {
return errors.New("config dir is not found")
}
ConfigFilePath = path.Join(configDirPath, "config.json")
ok := isFile(ConfigFilePath)
if !ok {
return errors.New("config file is not found")
}
configFile, err := os.Open(ConfigFilePath)
if err != nil {
return err
}
defer configFile.Close()
b, err := io.ReadAll(configFile)
if err != nil {
return err
}
var result map[string]interface{}
err = json.Unmarshal([]byte(b), &result)
if err != nil {
return err
}
// Pretty-print the result
marshaled, err := json.MarshalIndent(result, "", " ")
if err != nil {
return err
}
fmt.Printf("Printing config located at %s: \n %s\n", ConfigFilePath, (marshaled))
return nil
}

10
go.mod
View File

@ -5,9 +5,13 @@ go 1.20
require (
github.com/gorilla/mux v1.8.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.10.0
golang.org/x/mod v0.11.0
)
require golang.org/x/sys v0.9.0 // indirect
// replace gosimplenpm/handler => ./handler
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.9.0 // indirect
)

13
go.sum
View File

@ -1,20 +1,31 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

48
handler/app.go Normal file
View File

@ -0,0 +1,48 @@
package handler
import (
"gosimplenpm/config"
"gosimplenpm/middlewares"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type Application struct {
Logger *logrus.Logger
Conf config.Config
Mux *mux.Router
}
func (app *Application) Routes() {
// Need to use UseEncodedPath as shown here https://github.com/gorilla/mux/blob/master/mux.go#L269
m := mux.NewRouter().StrictSlash(true).UseEncodedPath()
m.Use(middlewares.LogMiddleware(app.Logger))
// main handler
m.HandleFunc("/{name}", GetPackage(app.Logger, app.Conf)).Methods("GET")
m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.Conf)(Publish(app.Logger, app.Conf))).Methods("PUT")
// tar handlers
m.HandleFunc("/{name}/-/{tar}", PackageTarGet(app.Logger, app.Conf)).Methods("GET")
// tag handlers
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagDelete(app.Logger, app.Conf))).Methods("DELETE")
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagPut(app.Logger, app.Conf))).Methods("PUT")
m.HandleFunc("/-/package/{name}/dist-tags", DistTagGet(app.Logger, app.Conf)).Methods("GET")
m.NotFoundHandler = http.HandlerFunc(NotFound)
app.Mux = m
}
func (app *Application) Start() error {
app.Routes()
server := &http.Server{
Addr: app.Conf.IpAddress,
Handler: app.Mux,
ReadTimeout: 4 * time.Second,
WriteTimeout: 4 * time.Second,
}
return server.ListenAndServe()
}

View File

@ -6,27 +6,33 @@ import (
"net/url"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"gosimplenpm/config"
"gosimplenpm/storage"
)
func Get(w http.ResponseWriter, r *http.Request) {
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
fmt.Printf("Package name => %s\n", packageName)
func GetPackage(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "get-package",
}).Debugf("Package name => %s\n", packageName)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !found {
ret := fmt.Sprintf("Package not found: %s", packageName)
http.Error(w, ret, http.StatusNotFound)
return
}
// serve file
http.ServeFile(w, r, fileToServe)
}
if !found {
ret := fmt.Sprintf("Package not found: %s", packageName)
http.Error(w, ret, http.StatusNotFound)
return
}
// serve file
http.ServeFile(w, r, fileToServe)
}

View File

@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/config"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"net/http"
@ -11,111 +12,131 @@ import (
"strings"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type NPMClientPutRequest struct {
Request serviceidos.IndexJson
}
func Publish(w http.ResponseWriter, r *http.Request) {
// (1) Parse Json Body
// (2) Check if package exists in the folder.
// (a) if it does, ckeck if it is the same version. If it is, return error. Else modify index.json from (2)
// (b) If it does not, add the latest tag to the new index.json
func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// (1) Parse Json Body
// (2) Check if package exists in the folder.
// (a) if it does, ckeck if it is the same version. If it is, return error. Else modify index.json from (2)
// (b) If it does not, add the latest tag to the new index.json
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
fmt.Printf("Package name => %s\n", packageName)
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("Package name => %s\n", packageName)
var cr NPMClientPutRequest
// Parse json body
err := json.NewDecoder(r.Body).Decode(&cr.Request)
var cr NPMClientPutRequest
// Parse json body
err := json.NewDecoder(r.Body).Decode(&cr.Request)
if err != nil {
fmt.Printf("Error unmarshaling put request: %+v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Extract relevant data from index.json
index := 0
var tag string
var version string
var versionData serviceidos.IndexJsonVersions
// TODO: Fix this as the order is not guaranteed
for key, value := range cr.Request.DistTags {
if index == 0 {
tag = key
version = value
break
if err != nil {
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("Error unmarshaling put request: %+v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
index++
}
versionData = cr.Request.Versions[version]
fmt.Printf("For version(%s) with tag(%s), versionData => %+v\n", version, tag, versionData)
// Rewrite the tarball path
tarballFileName := strings.Split(versionData.Dist.Tarball, "/-/")[1]
fmt.Printf("TarballName => %s\n", tarballFileName)
// versionData.Dist.Tarball = fmt.Sprintf("file://%s", packageFilePath)
versionData.Dist.Tarball = fmt.Sprintf("http://%s/%s/-/%s", r.Host, url.PathEscape(packageName), url.PathEscape(tarballFileName))
fmt.Printf("versionData.Dist.Tarball => %s\n", versionData.Dist.Tarball)
registryPath, _ := storage.GetRegistryPath()
tarBallFile := strings.Split(tarballFileName, "/")[1]
packageFilePath := path.Join(registryPath, packageName, tarBallFile)
// Extract relevant data from index.json
index := 0
var tag string
var version string
var versionData serviceidos.IndexJsonVersions
// TODO: Fix this as the order is not guaranteed
for key, value := range cr.Request.DistTags {
if index == 0 {
tag = key
version = value
break
}
index++
}
versionData = cr.Request.Versions[version]
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("For version(%s) with tag(%s), versionData => %+v\n", version, tag, versionData)
// Try to get the index.json from the store
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Rewrite the tarball path
tarballFileName := strings.Split(versionData.Dist.Tarball, "/-/")[1]
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("TarballName => %s\n", tarballFileName)
// versionData.Dist.Tarball = fmt.Sprintf("file://%s", packageFilePath)
versionData.Dist.Tarball = fmt.Sprintf("http://%s/%s/-/%s", r.Host, url.PathEscape(packageName), url.PathEscape(tarballFileName))
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("versionData.Dist.Tarball => %s\n", versionData.Dist.Tarball)
var jsonFile serviceidos.IndexJson
if !found {
// new package
jsonFile = cr.Request
jsonFile.DistTags["latest"] = version
} else {
// old package
err = storage.ReadIndexJson(fileToServe, &jsonFile)
tarBallFile := strings.Split(tarballFileName, "/")[1]
packageFilePath := path.Join(cfg.RepoDir, packageName, tarBallFile)
// Try to get the index.json from the store
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Checking that you are not publishing over a pervious published version
if jsonFile.Versions[version].Version == version {
fmt.Printf("Version %s of package %s already exists!!\n", version, packageName)
http.Error(w, err.Error(), http.StatusBadRequest)
var jsonFile serviceidos.IndexJson
if !found {
// new package
jsonFile = cr.Request
jsonFile.DistTags["latest"] = version
} else {
// old package
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Checking that you are not publishing over a pervious published version
if jsonFile.Versions[version].Version == version {
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("Version %s of package %s already exists!!\n", version, packageName)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Rewrite attachments
jsonFile.DistTags[tag] = version
nAttachments := make(map[string]serviceidos.IndexJsonAttachments)
nAttachments[fmt.Sprintf("%s-%s.tgz", packageName, version)] = cr.Request.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)]
jsonFile.Attachments = nAttachments
// Merge in the new version data
jsonFile.Versions[version] = versionData
}
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugln("FiletoServe ==> ", fileToServe)
// Write index.json
err = storage.WriteIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Rewrite attachments
jsonFile.DistTags[tag] = version
nAttachments := make(map[string]serviceidos.IndexJsonAttachments)
nAttachments[fmt.Sprintf("%s-%s.tgz", packageName, version)] = cr.Request.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)]
jsonFile.Attachments = nAttachments
// Merge in the new version data
jsonFile.Versions[version] = versionData
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugln("Package path => ", packageFilePath)
// Write bundled package
packageData := jsonFile.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)].Data
err = storage.WritePackageToStore(packageFilePath, packageData, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
fmt.Println("FiletoServe ==> ", fileToServe)
// Write index.json
err = storage.WriteIndexJson(fileToServe, &jsonFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Println("Package path => ", packageFilePath)
// Write bundled package
packageData := jsonFile.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)].Data
err = storage.WritePackageToStore(packageFilePath, packageData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/config"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"net/http"
@ -14,15 +15,19 @@ import (
"golang.org/x/mod/semver"
)
func DistTagDelete(lg *logrus.Logger) http.HandlerFunc {
func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.Printf("Package name => %s\n", packageName)
lg.WithFields(logrus.Fields{
"function": "dist-tags-delete",
}).Debugf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tag"]
tag, _ := url.PathUnescape(escapedName)
lg.Printf("Tag => %s\n", tag)
lg.WithFields(logrus.Fields{
"function": "dist-tags-delete",
}).Debugf("Tag => %s\n", tag)
if semver.IsValid(tag) {
http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest)
@ -34,7 +39,7 @@ func DistTagDelete(lg *logrus.Logger) http.HandlerFunc {
return
}
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -47,7 +52,7 @@ func DistTagDelete(lg *logrus.Logger) http.HandlerFunc {
}
var jsonFile serviceidos.IndexJson
err = storage.ReadIndexJson(fileToServe, &jsonFile)
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -56,7 +61,7 @@ func DistTagDelete(lg *logrus.Logger) http.HandlerFunc {
delete(jsonFile.DistTags, tag)
// Write index.json
err = storage.WriteIndexJson(fileToServe, &jsonFile)
err = storage.WriteIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/config"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"net/http"
@ -13,13 +14,15 @@ import (
"github.com/sirupsen/logrus"
)
func DistTagGet(lg *logrus.Logger) http.HandlerFunc {
func DistTagGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.Debugf("Package name => %s\n", packageName)
lg.WithFields(logrus.Fields{
"function": "dist-tags-get",
}).Debugf("Package name => %s\n", packageName)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -32,7 +35,7 @@ func DistTagGet(lg *logrus.Logger) http.HandlerFunc {
}
var jsonFile serviceidos.IndexJson
err = storage.ReadIndexJson(fileToServe, &jsonFile)
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/config"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"io"
@ -15,15 +16,19 @@ import (
"golang.org/x/mod/semver"
)
func DistTagPut(lg *logrus.Logger) http.HandlerFunc {
func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.Printf("Package name => %s\n", packageName)
lg.WithFields(logrus.Fields{
"function": "dist-tags-put",
}).Debugf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tag"]
tag, _ := url.PathUnescape(escapedName)
lg.Printf("Tag => %s\n", tag)
lg.WithFields(logrus.Fields{
"function": "dist-tags-put",
}).Debugf("Tag => %s\n", tag)
if semver.IsValid(tag) {
http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest)
@ -38,9 +43,11 @@ func DistTagPut(lg *logrus.Logger) http.HandlerFunc {
body, _ := io.ReadAll(r.Body)
var version string
_ = json.Unmarshal(body, &version)
lg.Printf("Body => %s", version)
lg.WithFields(logrus.Fields{
"function": "dist-tags-put",
}).Debugf("Body => %s", version)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -53,7 +60,7 @@ func DistTagPut(lg *logrus.Logger) http.HandlerFunc {
}
var jsonFile serviceidos.IndexJson
err = storage.ReadIndexJson(fileToServe, &jsonFile)
err = storage.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -62,7 +69,7 @@ func DistTagPut(lg *logrus.Logger) http.HandlerFunc {
jsonFile.DistTags[tag] = version
// Write index.json
err = storage.WriteIndexJson(fileToServe, &jsonFile)
err = storage.WriteIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -2,7 +2,7 @@ package handler
import (
"bytes"
"fmt"
"gosimplenpm/config"
"gosimplenpm/storage"
"io"
"net/http"
@ -11,33 +11,40 @@ import (
"strings"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
func Tar(w http.ResponseWriter, r *http.Request) {
// Sample output of npm view
// Public
// dist
// .tarball: https://registry.npmjs.org/react/-/react-18.2.0.tgz
// LocalHost
// dist
// .tarball: http://localhost:4000/@ookusanya/package1/-/package1-0.2.0.tgz
func PackageTarGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Sample output of npm view
// Public
// dist
// .tarball: https://registry.npmjs.org/react/-/react-18.2.0.tgz
// LocalHost
// dist
// .tarball: http://localhost:4000/@ookusanya/package1/-/package1-0.2.0.tgz
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
fmt.Printf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tar"]
tarFileName, _ := url.PathUnescape(escapedName)
fmt.Printf("Tarfile name => %s\n", tarFileName)
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "get-tar",
}).Debugf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tar"]
tarFileName, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "get-tar",
}).Debugf("Tarfile name => %s\n", tarFileName)
versionName := strings.Split(strings.Split(tarFileName, "-")[1], ".tgz")[0]
fileAsString, err := storage.GetTarFromStore(packageName, versionName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
versionName := strings.Split(strings.Split(tarFileName, "-")[1], ".tgz")[0]
fileAsString, err := storage.GetTarFromStore(packageName, versionName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Sending the tar as a base64 string
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(fileAsString))))
io.Copy(w, bytes.NewReader([]byte(fileAsString)))
}
// Sending the tar as a base64 string
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(fileAsString))))
io.Copy(w, bytes.NewReader([]byte(fileAsString)))
}

35
main.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"gosimplenpm/cmd/gosimplenpm"
)
func main() {
gosimplenpm.Execute()
}
// func main() {
// log := &logrus.Logger{
// Out: os.Stdout,
// Level: logrus.DebugLevel,
// Formatter: &logrus.TextFormatter{
// FullTimestamp: true,
// TimestampFormat: "2009-01-02 15:15:15",
// },
// }
// var cfg config.Config
// err := config.LoadConfiguration("userdata/config.json", &cfg)
// if err != nil {
// log.Fatalf("Config is not loaded: %+v\n", err)
// }
// app := &gosimplenpm.Application{
// Conf: cfg,
// Logger: log,
// }
// log.Infoln("Starting server on port 4000")
// err = http.ListenAndServe(":4000", app.Routes())
// log.Fatal(err)
// }

View File

@ -14,26 +14,15 @@ import (
"gosimplenpm/serviceidos"
"path/filepath"
"github.com/sirupsen/logrus"
)
func GetRegistryPath() (string, error) {
registryPath, err := filepath.Abs("./examples")
if err != nil {
fmt.Printf("File repo not found: +%v\n", err)
return "", err
}
return registryPath, err
}
func GetIndexJsonFromStore(packageName string) (string, bool, error) {
func GetIndexJsonFromStore(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) {
fileToServe := ""
found := false
searchDir, err := GetRegistryPath()
if err != nil {
return searchDir, found, err
}
err = filepath.WalkDir(searchDir, func(fp string, info fs.DirEntry, e error) error {
err := filepath.WalkDir(registryPath, func(fp string, info fs.DirEntry, e error) error {
if strings.Contains(fp, path.Join(packageName, "index.json")) {
fileToServe = fp
found = true
@ -42,25 +31,23 @@ func GetIndexJsonFromStore(packageName string) (string, bool, error) {
})
if err != nil {
fmt.Printf("List files error: +%v\n", err)
log.WithFields(logrus.Fields{
"function": "get-index-json-from-store",
}).Debugf("List files error: +%v\n", err)
return fileToServe, found, err
}
if fileToServe == "" && !found {
fileToServe = path.Join(searchDir, packageName, "index.json")
fileToServe = path.Join(registryPath, packageName, "index.json")
}
return fileToServe, found, nil
}
func GetTarFromStore(packageName string, tarFileName string) (string, error) {
func GetTarFromStore(packageName string, tarFileName string, registryPath string, log *logrus.Logger) (string, error) {
fileToServe := ""
searchDir, err := GetRegistryPath()
if err != nil {
return searchDir, err
}
err = filepath.WalkDir(searchDir, func(fp string, info fs.DirEntry, e error) error {
err := filepath.WalkDir(registryPath, func(fp string, info fs.DirEntry, e error) error {
if strings.Contains(fp, path.Join(packageName, tarFileName)) {
fileToServe = fp
}
@ -68,7 +55,9 @@ func GetTarFromStore(packageName string, tarFileName string) (string, error) {
})
if err != nil {
fmt.Printf("List files error: +%v\n", err)
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Debugf("List files error: +%v\n", err)
return fileToServe, err
}
@ -78,29 +67,37 @@ func GetTarFromStore(packageName string, tarFileName string) (string, error) {
file, err := os.Open(fileToServe)
if err != nil {
fmt.Printf("Open error: %s\n", fileToServe)
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Debugf("Open error: %s\n", fileToServe)
return "", err
}
archive, err := gzip.NewReader(file)
if err != nil {
fmt.Printf("Archive Open error: %s\n", fileToServe)
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Debugf("Archive Open error: %s\n", fileToServe)
return "", err
}
tr := tar.NewReader(archive)
bs, err := io.ReadAll(tr)
if err != nil {
fmt.Printf("Archive Read error: %s\n", fileToServe)
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Debugf("Archive Read error: %s\n", fileToServe)
return "", err
}
return base64.StdEncoding.EncodeToString(bs), err
}
func ReadIndexJson(fPath string, res *serviceidos.IndexJson) error {
func ReadIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error {
jsonFile, err := os.Open(fPath)
if err != nil {
fmt.Printf("File Not found: %s\n", fPath)
log.WithFields(logrus.Fields{
"function": "read-index-json",
}).Debugf("File Not found: %s\n", fPath)
return err
}
@ -108,19 +105,23 @@ func ReadIndexJson(fPath string, res *serviceidos.IndexJson) error {
err = json.NewDecoder(jsonFile).Decode(res)
if err != nil {
fmt.Printf("Unmarshalerror: %+v\n", err)
log.WithFields(logrus.Fields{
"function": "read-index-json",
}).Debugf("Unmarshalerror: %+v\n", err)
return err
}
return nil
}
func WriteIndexJson(fPath string, res *serviceidos.IndexJson) error {
func WriteIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error {
// Need to create the directory first
parent := path.Dir(fPath)
err := os.MkdirAll(parent, os.ModePerm)
if err != nil {
fmt.Printf("Folder (%s) creation failed.\n", fPath)
log.WithFields(logrus.Fields{
"function": "write-index-json",
}).Debugf("Folder (%s) creation failed.\n", fPath)
return err
}
@ -128,7 +129,9 @@ func WriteIndexJson(fPath string, res *serviceidos.IndexJson) error {
jsonFile, err := os.Create(fPath)
if err != nil {
fmt.Printf("Creation error for path(%s): %+v\n ", fPath, err)
log.WithFields(logrus.Fields{
"function": "write-index-json",
}).Debugf("Creation error for path(%s): %+v\n ", fPath, err)
return err
}
@ -136,23 +139,29 @@ func WriteIndexJson(fPath string, res *serviceidos.IndexJson) error {
err = json.NewEncoder(jsonFile).Encode(res)
if err != nil {
fmt.Printf("Marshalerror: %+v\n", err)
log.WithFields(logrus.Fields{
"function": "write-index-json",
}).Debugf("Marshalerror: %+v\n", err)
return err
}
return nil
}
func WritePackageToStore(fPath string, data string) error {
func WritePackageToStore(fPath string, data string, log *logrus.Logger) error {
dec, err := base64.StdEncoding.DecodeString(data)
if err != nil {
fmt.Printf("Base64 Decode error: %+v\n", err)
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Debugf("Base64 Decode error: %+v\n", err)
return err
}
dataFile, err := os.Create(fPath)
if err != nil {
fmt.Printf("Creation error: %s\n", fPath)
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Debugf("Creation error: %s\n", fPath)
return err
}
@ -160,13 +169,17 @@ func WritePackageToStore(fPath string, data string) error {
_, err = dataFile.Write(dec)
if err != nil {
fmt.Printf("Write error: %s\n", fPath)
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Debugf("Write error: %s\n", fPath)
return err
}
err = dataFile.Sync()
if err != nil {
fmt.Printf("Sync error: %s\n", fPath)
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Debugf("Sync error: %s\n", fPath)
return err
}