diff --git a/cmd/gosimplenpm/conf.go b/cmd/gosimplenpm/conf.go new file mode 100644 index 0000000..8760932 --- /dev/null +++ b/cmd/gosimplenpm/conf.go @@ -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) +} diff --git a/cmd/gosimplenpm/root.go b/cmd/gosimplenpm/root.go new file mode 100644 index 0000000..190dd94 --- /dev/null +++ b/cmd/gosimplenpm/root.go @@ -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") +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index a320867..0000000 --- a/cmd/main.go +++ /dev/null @@ -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) -} diff --git a/cmd/npmregserver/app.go b/cmd/npmregserver/app.go deleted file mode 100644 index 501b6e7..0000000 --- a/cmd/npmregserver/app.go +++ /dev/null @@ -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 -} diff --git a/config/conf.go b/config/conf.go index c51e526..88c337b 100644 --- a/config/conf.go +++ b/config/conf.go @@ -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 :") + } + + // 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 } diff --git a/go.mod b/go.mod index 4366655..ea3aac0 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum index a5b6272..881c27f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handler/app.go b/handler/app.go new file mode 100644 index 0000000..2879a8e --- /dev/null +++ b/handler/app.go @@ -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() +} diff --git a/handler/get.go b/handler/get.go index 7e35660..649f38c 100644 --- a/handler/get.go +++ b/handler/get.go @@ -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) } diff --git a/handler/publish.go b/handler/publish.go index 54c6c0b..f1626a1 100644 --- a/handler/publish.go +++ b/handler/publish.go @@ -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 - } } diff --git a/handler/tagdelete.go b/handler/tagdelete.go index 41a09b9..b1c2574 100644 --- a/handler/tagdelete.go +++ b/handler/tagdelete.go @@ -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 diff --git a/handler/tagget.go b/handler/tagget.go index 5f5d38e..5dacdf1 100644 --- a/handler/tagget.go +++ b/handler/tagget.go @@ -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 diff --git a/handler/tagput.go b/handler/tagput.go index 07a4cea..0f3aed8 100644 --- a/handler/tagput.go +++ b/handler/tagput.go @@ -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 diff --git a/handler/tar.go b/handler/tar.go index 90e533f..d7c875b 100644 --- a/handler/tar.go +++ b/handler/tar.go @@ -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))) } diff --git a/main.go b/main.go new file mode 100644 index 0000000..446269b --- /dev/null +++ b/main.go @@ -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) +// } diff --git a/storage/fs.go b/storage/fs.go index 8641535..48bf365 100644 --- a/storage/fs.go +++ b/storage/fs.go @@ -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 }