Adding tags command.

Refactoring main and app.go to cmd. Refactoring to adding logrus instead of log. Refactoring handlers to add a global logger.
This commit is contained in:
OLUWADAMILOLA OKUSANYA 2023-06-17 18:41:07 -04:00
parent 234b593c26
commit e3e81130ba
17 changed files with 317 additions and 144 deletions

36
app.go
View File

@ -1,36 +0,0 @@
package main
import (
"gosimplenpm/config"
"gosimplenpm/handler"
"gosimplenpm/middlewares"
"log"
"net/http"
"github.com/gorilla/mux"
)
type application struct {
logger *log.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)
// main handler
m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.conf)(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}", handler.DistTagDelete).Methods("DELETE")
m.HandleFunc("/-/package/{name}/dist-tags/{tag}", handler.DistTagPut).Methods("PUT")
m.HandleFunc("/-/package/{name}/dist-tags", handler.DistTagGet).Methods("GET")
m.NotFoundHandler = http.HandlerFunc(handler.NotFound)
return m
}

36
cmd/main.go Normal file
View File

@ -0,0 +1,36 @@
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)
}

36
cmd/npmregserver/app.go Normal file
View File

@ -0,0 +1,36 @@
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
}

8
go.mod
View File

@ -2,6 +2,12 @@ module gosimplenpm
go 1.20 go 1.20
require github.com/gorilla/mux v1.8.0 require (
github.com/gorilla/mux v1.8.0
github.com/sirupsen/logrus v1.9.3
golang.org/x/mod v0.11.0
)
require golang.org/x/sys v0.9.0 // indirect
// replace gosimplenpm/handler => ./handler // replace gosimplenpm/handler => ./handler

18
go.sum
View File

@ -1,2 +1,20 @@
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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/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=

View File

@ -1,11 +1,10 @@
package handler package handler
import ( import (
"fmt"
"net/http" "net/http"
) )
// This handler is executed when the router cannot match any route // This handler is executed when the router cannot match any route
func NotFound(w http.ResponseWriter, r *http.Request) { func NotFound(w http.ResponseWriter, r *http.Request) {
fmt.Printf("%s - %s - %s\n", r.Method, r.URL, r.Host) http.Error(w, "Invalid url", http.StatusBadRequest)
} }

View File

@ -1,9 +1,79 @@
package handler package handler
import ( import (
"encoding/json"
"fmt"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"net/http" "net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
) )
func DistTagDelete(w http.ResponseWriter, r *http.Request) { func DistTagDelete(lg *logrus.Logger) 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)
escapedName = mux.Vars(r)["tag"]
tag, _ := url.PathUnescape(escapedName)
lg.Printf("Tag => %s\n", tag)
if semver.IsValid(tag) {
http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest)
return
}
if tag == "latest" {
http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest)
return
}
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
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
}
var jsonFile serviceidos.IndexJson
err = storage.ReadIndexJson(fileToServe, &jsonFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
delete(jsonFile.DistTags, tag)
// Write index.json
err = storage.WriteIndexJson(fileToServe, &jsonFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
distTagsString, _ := json.Marshal(jsonFile.DistTags)
response := serviceidos.TagPutResponse{
Ok: true,
ID: escapedName,
DistTags: string(distTagsString),
}
jsonString, _ := json.Marshal(response)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonString)))
w.Write(jsonString)
}
} }

View File

@ -1,9 +1,47 @@
package handler package handler
import ( import (
"encoding/json"
"fmt"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"net/http" "net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
) )
func DistTagGet(w http.ResponseWriter, r *http.Request) { func DistTagGet(lg *logrus.Logger) 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)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
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
}
var jsonFile serviceidos.IndexJson
err = storage.ReadIndexJson(fileToServe, &jsonFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonString, _ := json.Marshal(jsonFile.DistTags)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonString)))
w.Write(jsonString)
}
} }

View File

@ -1,9 +1,85 @@
package handler package handler
import ( import (
"encoding/json"
"fmt"
"gosimplenpm/serviceidos"
"gosimplenpm/storage"
"io"
"net/http" "net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
) )
func DistTagPut(w http.ResponseWriter, r *http.Request) { func DistTagPut(lg *logrus.Logger) 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)
escapedName = mux.Vars(r)["tag"]
tag, _ := url.PathUnescape(escapedName)
lg.Printf("Tag => %s\n", tag)
if semver.IsValid(tag) {
http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest)
return
}
if tag == "latest" {
http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest)
return
}
body, _ := io.ReadAll(r.Body)
var version string
_ = json.Unmarshal(body, &version)
lg.Printf("Body => %s", version)
fileToServe, found, err := storage.GetIndexJsonFromStore(packageName)
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
}
var jsonFile serviceidos.IndexJson
err = storage.ReadIndexJson(fileToServe, &jsonFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonFile.DistTags[tag] = version
// Write index.json
err = storage.WriteIndexJson(fileToServe, &jsonFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
distTagsString, _ := json.Marshal(jsonFile.DistTags)
response := serviceidos.TagPutResponse{
Ok: true,
ID: escapedName,
DistTags: string(distTagsString),
}
jsonString, _ := json.Marshal(response)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonString)))
w.Write(jsonString)
}
} }

45
main.go
View File

@ -1,45 +0,0 @@
package main
import (
"gosimplenpm/config"
"log"
"net/http"
)
func main() {
var cfg config.Config
err := config.LoadConfiguration("userdata/config.json", &cfg)
if err != nil {
log.Fatalf("Config is not loaded: %+v\n", err)
}
app := &application{
conf: cfg,
}
log.Print("Starting server on port 4000")
err = http.ListenAndServe(":4000", app.Routes())
log.Fatal(err)
}
// func main() {
// router := mux.NewRouter()
// router.NewRoute().Path("/{name}").Methods("PUT")
// router.NewRoute().Path("/{name}").Methods("GET")
// rMatch := &mux.RouteMatch{}
// u := url.URL{Path: "/@ookusanya%2fsimplepackone"}
// req := http.Request{Method: "GET", URL: &u}
// x := router.Match(&req, rMatch)
// fmt.Println("Is Matched: ", x)
// reqt := http.Request{Method: "PUT", URL: &u}
// g := router.Match(&reqt, rMatch)
// fmt.Println("Is Matched: ", g)
// ut := url.URL{Path: "/@ookusanya%2fsimplepackone/-/simplepackone-1.0.0.tgz"}
// rt := http.Request{Method: "PUT", URL: &ut}
// gt := router.Match(&rt, rMatch)
// fmt.Println("Is Matched: ", gt)
// }

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
) )
func AuthMiddleware(cfg config.Config) Middleware { func AuthMiddleware(cfg config.Config) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// get headers // get headers
@ -26,30 +26,3 @@ func AuthMiddleware(cfg config.Config) Middleware {
} }
} }
} }
// func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
// return func(w http.ResponseWriter, r *http.Request) {
// if cfg == nil {
// log.Println("Config load error")
// http.Error(w, "Config load error", http.StatusInternalServerError)
// return
// }
// log.Println("Config was loaded")
// // get headers
// authHeader := r.Header.Get("Authorization")
// authFields := strings.Fields(authHeader)
// if len(authFields) != 2 || strings.ToLower(authFields[0]) != "bearer" {
// http.Error(w, "Authentication Error", http.StatusForbidden)
// return
// }
// token := authFields[1]
// if token != cfg.Token {
// http.Error(w, "Authentication Error", http.StatusForbidden)
// return
// }
// fmt.Println("Authorized")
// next(w, r)
// }
// }

View File

@ -1,15 +1,16 @@
package middlewares package middlewares
import ( import (
"fmt"
"log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"github.com/sirupsen/logrus"
) )
func LogMiddleware(next http.Handler) http.Handler { func LogMiddleware(lg *logrus.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s - %s - %s", r.Method, r.URL, r.Host) lg.Debugf("%s - %s - %s", r.Method, r.URL, r.Host)
hasBody := false hasBody := false
if r.Method == "PUT" { if r.Method == "PUT" {
@ -18,16 +19,11 @@ func LogMiddleware(next http.Handler) http.Handler {
requestDump, err := httputil.DumpRequest(r, hasBody) requestDump, err := httputil.DumpRequest(r, hasBody)
if err != nil { if err != nil {
fmt.Println(err) lg.Debugln(err)
} }
fmt.Println("RequestDump: ", string(requestDump)) lg.Debugln("RequestDump: ", string(requestDump))
// fmt.Println("Printing headers")
// for name, values := range r.Header {
// for _, value := range values {
// fmt.Printf("%s:%s\n", name, value)
// }
// }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
}
} }

View File

@ -1,7 +0,0 @@
package middlewares
import (
"net/http"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc

View File

@ -40,3 +40,15 @@ type IndexJson struct {
Access string `json:"access"` Access string `json:"access"`
Attachments map[string]IndexJsonAttachments `json:"_attachments"` Attachments map[string]IndexJsonAttachments `json:"_attachments"`
} }
type TagPutResponse struct {
Ok bool `json:"ok"`
ID string `json:"id"`
DistTags string `json:"dist-tags"`
}
type TagDeleteResponse struct {
Ok bool `json:"ok"`
ID string `json:"id"`
DistTags string `json:"dist-tags"`
}

View File

@ -0,0 +1 @@
{"_id":"@ookusanya/simplepackone","name":"@ookusanya/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@ookusanya/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","files":null,"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","_id":"@ookusanya/simplepackone@1.0.0","_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-x9cAEHDPM0dLhdyd8baWLTNYskr41QXLNv5HS5WNDg1CVzwcMaAU+N9lYVuOvLf/xPHLielWmX+wLXEtRWQvGw==","shasum":"545a7737b59b1a6f064339a4ebe056e8a9b1511d","tarball":"http://localhost:4000/@ookusanya/simplepackone/-/@ookusanya/simplepackone-1.0.0.tgz"},"dependencies":{"eslint":"^7.x"}},"1.2.0":{"name":"@ookusanya/simplepackone","version":"1.2.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","files":null,"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","_id":"@ookusanya/simplepackone@1.2.0","_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==","shasum":"1adafdbe8878372c9f4b554bf30d5bfb1b7896c5","tarball":"http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz"},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@ookusanya/simplepackone-1.2.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA","length":1089}}}