diff --git a/servlet/config.go b/servlet/config.go index 55b4654..3800770 100644 --- a/servlet/config.go +++ b/servlet/config.go @@ -58,40 +58,40 @@ func CreateBlankConfig() *Config { return &conf } -func LoadConfig(pathlike string) *VinegarHttpServlet { - contents, exists := vinegarUtil.GetDiskContent(pathlike) - if exists { - conf := Config{} - err := json.Unmarshal(*contents, &conf) - if err != nil { - panic(err) - return nil - } - servlet := NewServlet(conf.ListeningAddress) - for _, v := range conf.Routes { - constructor, err := getConstructorFunction(v.ConfigType) - if err != nil { - panic(err) - return nil - } - //we don't have to invoke servlet.AddRoute because the static routes already do that for us - constructor(servlet, v.UrlPattern, v.FileLocation, v.UseBuiltinCache) - - } - return servlet - } else { +func LoadConfig(pathlike string) (*VinegarHttpServlet, error) { + contents, err := vinegarUtil.GetDiskContent(pathlike) + if err != nil { CreateBlankConfig() - panic("Could not find config file at" + pathlike) - return nil + return nil, err } + + conf := Config{} + err = json.Unmarshal(*contents, &conf) + if err != nil { + return nil, err + } + servlet := NewServlet(conf.ListeningAddress) + for _, v := range conf.Routes { + constructor, err := getConstructorFunction(v.ConfigType) + if err != nil { + + return nil, err + } + //we don't have to invoke servlet.AddRoute because the static routes already do that for us + constructor(servlet, v.UrlPattern, v.FileLocation, v.UseBuiltinCache) + + } + return servlet, nil + } -func (e ConfigEntry) toRoute(serv *VinegarHttpServlet) { +func (e ConfigEntry) toRoute(serv *VinegarHttpServlet) error { constructor, err := getConstructorFunction(e.ConfigType) if err != nil { - panic(err) + return err } constructor(serv, e.UrlPattern, e.FileLocation, e.UseBuiltinCache) + return nil } func getConstructorFunction(t ConfigType) (RouteConstructor, error) { @@ -106,13 +106,14 @@ func getConstructorFunction(t ConfigType) (RouteConstructor, error) { return nil, errors.New(fmt.Sprintf("could not determine constructor for invalid ConfigType: %s", t)) } -func GenerateBlankConfig() { +func GenerateBlankConfig() error { fileName := "servlet.json.template.tmpl" conf := CreateBlankConfig() content, err := json.Marshal(&conf) if err != nil { - panic(err) + return err } fmt.Println("Generating a blank configuration file at ") ioutil.WriteFile(fileName, content, 0755) + return nil } diff --git a/servlet/dynamicRoute.go b/servlet/dynamicRoute.go index 6b5876a..2367127 100644 --- a/servlet/dynamicRoute.go +++ b/servlet/dynamicRoute.go @@ -2,6 +2,7 @@ package servlet import ( "fmt" + "log" "net/http" ) @@ -60,7 +61,7 @@ func (api *ApiRoute) RegisterHttpMethodHandler(method HttpMethod, handler Vinega func getHttpMethod(req *http.Request) HttpMethod { switch req.Method { - case "GET": + case http.MethodGet: return GET case "POST": return POST @@ -79,10 +80,10 @@ func getHttpMethod(req *http.Request) HttpMethod { func SendApiError(w http.ResponseWriter, httpCode int, messageCode int, message string) { respMessage := fmt.Sprintf("{\"code\":%d, \"message\":\"%s\"}", messageCode, message) - w.WriteHeader(httpCode) - + log.Printf("[ERROR]\t%s\n", respMessage) _, err := w.Write([]byte(respMessage)) if err != nil { - panic(err) + log.Println(err) } + w.WriteHeader(httpCode) } diff --git a/servlet/server.go b/servlet/server.go index f5a1293..868868a 100644 --- a/servlet/server.go +++ b/servlet/server.go @@ -7,7 +7,6 @@ import ( "geniuscartel.xyz/vinegar/vinegarUtil" "log" "net/http" - "os" "regexp" "strconv" ) @@ -83,10 +82,9 @@ func (s *VinegarHttpServlet) ServeHTTP(w http.ResponseWriter, req *http.Request) s.SendError(w, req, 404, "Couldn't find your content.", errors.New("failed to match route for ["+path+"]")) } -func (s *VinegarHttpServlet) Start() { +func (s *VinegarHttpServlet) Start() error { if len(s.Routes) < 1 { log.Fatal("No routes found for server. Nothing to listen and serve.") - os.Exit(1) } log.Printf("Listening on [%s]\n", s.Port) @@ -94,9 +92,9 @@ func (s *VinegarHttpServlet) Start() { err := http.ListenAndServe(s.Port, s) if err != nil { - panic(err) + return err } - + return nil } func (r *VinegarWebRoute) Announce() { @@ -105,28 +103,36 @@ func (r *VinegarWebRoute) Announce() { func (s *VinegarHttpServlet) SendError(w http.ResponseWriter, req *http.Request, code int, msg string, aErr error) { fmt.Printf("[%d][%s]. Rendering template for code %d with message: %s\n", code, req.URL.Path, code, msg) - fmt.Println(aErr) + out, _ := json.Marshal(aErr) + fmt.Println(string(out)) tmpl, exists := s.ErrorRoutes[code] if exists { - tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code)) - tmpl.TemplateManager.AddMixin("msg", msg) - w.WriteHeader(code) - _, err := w.Write([]byte(tmpl.TemplateManager.RenderTemplate(fmt.Sprintf("%d.html", code)))) + err := tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code)) if err != nil { - panic(err) + writeGeneric(w, code, msg) + return } - return - } else { - w.WriteHeader(code) - errorPayload := ErrorResponse{Code: code, Message: msg} - genericError, sErr := json.Marshal(errorPayload) - if sErr != nil { - panic(sErr) - } - _, err := w.Write(genericError) + err = tmpl.TemplateManager.AddMixin("msg", msg) if err != nil { - panic(err) + writeGeneric(w, code, msg) + return + } + w.WriteHeader(code) + bitties, err := tmpl.TemplateManager.RenderTemplate(fmt.Sprintf("%d.html", code)) + _, err = w.Write([]byte(bitties)) + if err != nil { + writeGeneric(w, code, msg) + return } return } + writeGeneric(w, code, msg) + return +} + +func writeGeneric(w http.ResponseWriter, code int, msg string) { + w.WriteHeader(code) + errorPayload := ErrorResponse{Code: code, Message: msg} + genericError, _ := json.Marshal(errorPayload) + w.Write(genericError) } diff --git a/servlet/server_test.go b/servlet/server_test.go new file mode 100644 index 0000000..3af5838 --- /dev/null +++ b/servlet/server_test.go @@ -0,0 +1,24 @@ +package servlet + +import ( + "testing" +) + +func TestNewServlet(t *testing.T) { + + port := ":8080" + + srv := NewServlet(port) + + if srv.Port != port { + t.Errorf("Expected port %s, got %s", port, srv.Port) + } + + if srv.Routes != nil { + t.Error("Expected Routes to be nil") + } + + if srv.ErrorRoutes == nil { + t.Error("Expected ErrorRoutes to be initialized") + } +} diff --git a/servlet/staticRoute.go b/servlet/staticRoute.go index 94ccf1c..7f4cdc2 100644 --- a/servlet/staticRoute.go +++ b/servlet/staticRoute.go @@ -3,6 +3,7 @@ package servlet import ( "errors" util "geniuscartel.xyz/vinegar/vinegarUtil" + "log" "net/http" "path" "path/filepath" @@ -45,7 +46,7 @@ type ( // // This function signature allows encapsulating the creation of different types of FileRoutes. It is used to define // constructor functions for each file type, like NewTextRoute or NewImageRoute. - RouteConstructor func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) *FileRoute + RouteConstructor func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) ) // NewTextRoute creates a new FileRoute for serving text files. @@ -66,10 +67,10 @@ type ( // // A FileRoute instance configured for serving text files, added to // the provided VinegarHttpServlet. -var NewTextRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) *FileRoute { +var NewTextRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { fileRoot := filepath.Clean(pathlike) if strings.Contains(fileRoot, "../") { - panic("Traversing the directory is not allowed, use an absolute filepath instead") + return nil, errors.New("Traversing the directory is not allowed, use an absolute filepath instead") } defaultPrune := strings.Replace(urlPattern, ".*", "", -1) route := FileRoute{srv: servlet, fileRoot: fileRoot, UseCache: useCache} @@ -79,13 +80,13 @@ var NewTextRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern servlet.AddRoute(route.VinegarRoute) - return &route + return &route, nil } -var NewImageRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) *FileRoute { +var NewImageRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { fileRoot := filepath.Clean(pathlike) if strings.Contains(fileRoot, "../") { - panic("Traversing the directory is not allowed, use an absolute filepath instead") + return nil, errors.New("Traversing the directory is not allowed, use an absolute filepath instead") } defaultPrune := strings.Replace(urlPattern, ".*", "", -1) route := FileRoute{srv: servlet, fileRoot: fileRoot, UseCache: useCache} @@ -93,17 +94,20 @@ var NewImageRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPatter route.VinegarRoute = rootRoute //i *kinda* don't like this pattern servlet.AddRoute(route.VinegarRoute) - return &route + return &route, nil } -var NewSingleFileRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) *FileRoute { +var NewSingleFileRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { route := FileRoute{ srv: servlet, fileRoot: pathlike, UseCache: useCache, } singleFileServletHandler := createSingleFileServletFunction(&route) - sfCache := util.NewSingleFileCache(pathlike) + sfCache, err := util.NewSingleFileCache(pathlike) + if err != nil { + return nil, err + } parentRoute := NewServletRoute(urlPattern, singleFileServletHandler) parentRoute.Handler = singleFileServletHandler @@ -113,7 +117,7 @@ var NewSingleFileRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlP servlet.AddRoute(route.VinegarRoute) - return &route + return &route, nil } // createSingleFileServletFunction creates a handler function for serving a single file. @@ -167,7 +171,7 @@ func createSingleFileServletFunction(route *FileRoute) VinegarHandlerFunction { w.Header().Add(ContentTypeHeaderKey, cache.MimeType) _, err := w.Write(content) if err != nil { - panic(err) + log.Println(err) } } @@ -189,20 +193,23 @@ func createCompressibleFileServletFunction(route *FileRoute, basePattern string, resourcePath := path.Join(pathRoot, filePath) if !exists { - content, fileExists := util.GetDiskContent(resourcePath) - if fileExists { - if route.UseCache { - route.VinegarRoute.Cache.Put(stub, resourcePath) - cachedContent, _ = route.VinegarRoute.Cache.Get(stub) - } else { - w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub)) - w.Write(*content) - return - } - } else { + content, err := util.GetDiskContent(resourcePath) + if err != nil { route.srv.SendError(w, req, 404, "Couldn't find your content.", errors.New("could not find valid file at ["+resourcePath+"]")) return } + + if route.UseCache { + err := route.VinegarRoute.Cache.Put(stub, resourcePath) + if err != nil { + route.srv.SendError(w, req, 500, "Internal Server Error", err) + } + cachedContent, _ = route.VinegarRoute.Cache.Get(stub) + } else { + w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub)) + w.Write(*content) + return + } } w.Header().Add(ContentTypeHeaderKey, cachedContent.MimeType) var err error = nil @@ -214,7 +221,7 @@ func createCompressibleFileServletFunction(route *FileRoute, basePattern string, _, err = w.Write(cachedContent.Content) } if err != nil { - panic(err) + log.Println(err) } } @@ -241,7 +248,7 @@ func createUncompressedFileServletFunction(route *FileRoute, basePattern string, w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub)) _, err := w.Write(entry.Content) if err != nil { - panic(err) + log.Println(err) } return diff --git a/servlet/templates.go b/servlet/templates.go index 77f9af1..68f38a2 100644 --- a/servlet/templates.go +++ b/servlet/templates.go @@ -31,17 +31,17 @@ func NewTemplateManager(templatePath string, componentPath string) *TemplateMana return &tm } -func (tm *TemplateManager) loadAssets() { +func (tm *TemplateManager) loadAssets() error { templateFiles, err := os.ReadDir(tm.templatePath) if err != nil { - panic(err) + return err } for _, templateFile := range templateFiles { if !templateFile.IsDir() { filePath := path.Join(tm.templatePath, templateFile.Name()) - dataContent, exists := util.GetDiskContent(filePath) - if !exists { - panic("Could not read file contents for " + filePath) + dataContent, err := util.GetDiskContent(filePath) + if err != nil { + return err } tmpl := template.New(templateFile.Name()) tmpl.Parse(string(*dataContent)) @@ -52,44 +52,43 @@ func (tm *TemplateManager) loadAssets() { componentFiles, err := os.ReadDir(tm.componentPath) if err != nil { - panic(err) + return err } for _, componentFile := range componentFiles { if !componentFile.IsDir() { filePath := path.Join(tm.componentPath, componentFile.Name()) - contentBytes, exists := util.GetDiskContent(filePath) - if !exists { - panic("could not find file for " + filePath) - return + contentBytes, err := util.GetDiskContent(filePath) + if err != nil { + return err } content := string(*contentBytes) tm.components[componentFile.Name()] = template.HTML(content) } } + return nil } -func (tm *TemplateManager) RenderTemplate(key string) string { +func (tm *TemplateManager) RenderTemplate(key string) (string, error) { tmpl, exists := tm.templates[key] if !exists { - panic("Could not load template for key: " + key) + return "", errors.New("Could not load template for key: " + key) } buff := strings.Builder{} err := tmpl.Execute(&buff, tm) if err != nil { - panic(err) + return "", err } - return buff.String() + return buff.String(), nil } -func (tm *TemplateManager) GetComponent(key string) template.HTML { +func (tm *TemplateManager) GetComponent(key string) (template.HTML, error) { content, exists := tm.components[key] if !exists { - panic("Could not load data for key: " + key) - return "" + return "", errors.New("Could not load data for key: " + key) } - return content + return content, nil } func (tm *TemplateManager) GetMixin(key string) template.HTML { @@ -107,50 +106,54 @@ func (tm *TemplateManager) AddMixin(key string, value string) error { } func (tm *TemplateManager) AddMixinFromFile(key string, pathlike string) error { - content, exists := util.GetDiskContent(pathlike) - if exists { - tm.AddMixin(key, string(*content)) - return nil + content, err := util.GetDiskContent(pathlike) + if err != nil { + return err } else { - return errors.New("mixin source file not found at " + pathlike) + err2 := tm.AddMixin(key, string(*content)) + if err2 != nil { + return err2 + } + return nil } } -func (tm *TemplateManager) RenderAllToFile(pathlike string) { +func (tm *TemplateManager) RenderAllToFile(pathlike string) error { for _, v := range tm.Templates { - path := path.Join(pathlike, v) + p := path.Join(pathlike, v) tmpl := tm.templates[v] buff := strings.Builder{} err := tmpl.Execute(&buff, tm) if err != nil { - panic(err) + return err } - err = ioutil.WriteFile(path, []byte(buff.String()), 0755) + err = ioutil.WriteFile(p, []byte(buff.String()), 0755) if err != nil { - panic(err) + return err } } + return nil } -func RenderTemplate(w http.ResponseWriter, pathlike string, data any) { +func RenderTemplate(w http.ResponseWriter, pathlike string, data any) error { templateHelper := template.New(pathlike) f, err := os.OpenFile(pathlike, os.O_RDONLY, 0777) if err != nil { - panic(err) + return err } defer f.Close() content, err := ioutil.ReadAll(f) if err != nil { - panic(err) + return err } templ, err := templateHelper.Parse(string(content)) err = templ.Execute(w, data) if err != nil { - panic(err) + return err } - + return nil } diff --git a/vinegarUtil/files.go b/vinegarUtil/files.go index 0769f7b..4b84f32 100644 --- a/vinegarUtil/files.go +++ b/vinegarUtil/files.go @@ -7,36 +7,36 @@ import ( "os" ) -func GetDiskContent(filePath string) (*[]byte, bool) { +func GetDiskContent(filePath string) (*[]byte, error) { _, ferr := os.Stat(filePath) if os.IsNotExist(ferr) { - return nil, false + return nil, ferr } else { f, err := os.OpenFile(filePath, os.O_RDONLY, 0755) if err != nil { - panic(err) + return nil, err } defer f.Close() content, err := ioutil.ReadAll(f) if err != nil { - panic(err) + return nil, err } - return &content, true + return &content, nil } } -func GZipBytes(uncompressed *[]byte) *[]byte { +func GZipBytes(uncompressed *[]byte) (*[]byte, error) { buff := bytes.Buffer{} gzip := gzip2.NewWriter(&buff) _, err := gzip.Write(*uncompressed) if err != nil { - panic(err) + return nil, err } err = gzip.Flush() if err != nil { - panic(err) + return nil, err } compressed := buff.Bytes() - return &compressed + return &compressed, nil } diff --git a/vinegarUtil/webLRU.go b/vinegarUtil/webLRU.go index 7b58a58..6c714a5 100644 --- a/vinegarUtil/webLRU.go +++ b/vinegarUtil/webLRU.go @@ -1,7 +1,6 @@ package vinegarUtil import ( - "errors" "fmt" "log" "strings" @@ -50,17 +49,23 @@ func NewLRU(size int64) *Lru { return &Lru{&cLru, size, 0} } -func NewSingleFileCache(pathlike string) *SingleFileCache { - entry := newLRUEntry(pathlike) +func NewSingleFileCache(pathlike string) (*SingleFileCache, error) { + entry, err := newLRUEntry(pathlike) + if err != nil { + return nil, err + } sfc := SingleFileCache{path: pathlike, entry: entry} - return &sfc + return &sfc, nil } func (l *Lru) Get(key string) (*LruEntry, bool) { entry, exists := (*l.entries)[key] if exists && entry.expires.Before(time.Now()) { fmt.Println("Cache miss due to expired content") - entry.Reload() + err := entry.Reload() + if err != nil { + return nil, false + } return entry, true } else { if exists { @@ -73,7 +78,10 @@ func (l *Lru) Get(key string) (*LruEntry, bool) { func (l *Lru) GetFresh(key string) (*LruEntry, bool) { entry, exists := l.Get(key) if exists { - entry.Reload() + err := entry.Reload() + if err != nil { + return nil, false + } } return l.Get(key) } @@ -84,11 +92,13 @@ func (l *Lru) Put(key string, pathlike string) error { var size int64 if exists { - content, fExists := GetDiskContent(pathlike) - if !fExists { - return errors.New("attempted to refresh cache with file that no longer exists on disk") + content, err := GetDiskContent(pathlike) + if err != nil { + return err } - zippedContent := *GZipBytes(content) + zippedBytes, err := GZipBytes(content) + zippedContent := *zippedBytes + size = int64(len(*content)) + int64(len(zippedContent)) l.ensureVacancy(size) @@ -99,8 +109,11 @@ func (l *Lru) Put(key string, pathlike string) error { entry.mostRecentAccess = time.Now() entry.totalSize = size } else { - entry = newLRUEntry(pathlike) - + nEntry, err := newLRUEntry(pathlike) + if err != nil { + return err + } + entry = nEntry } (*l.entries)[key] = entry @@ -143,9 +156,7 @@ func (l *Lru) getLeastRecentlyUsedKey() string { benchmark = entry } } - if leastUsedKey == "" { - panic("Invalid key for LRU: [" + leastUsedKey + "]") - } + return leastUsedKey } @@ -157,13 +168,23 @@ func (l *Lru) recalcSize() { l.currentSize = total } -func (le *LruEntry) Reload() { - content, _ := GetDiskContent(le.path) +func (le *LruEntry) Reload() error { + + content, err := GetDiskContent(le.path) + if err != nil { + return err + } + zippedBytes, err := GZipBytes(content) + if err != nil { + return err + } + le.Content = *content - le.CompressedContent = *GZipBytes(content) + le.CompressedContent = *zippedBytes le.created = time.Now() le.mostRecentAccess = le.created le.expires = le.created.Add(time.Minute * DefaultCacheTimeInMinutes) + return nil } func (sfc *SingleFileCache) Get(key string) (*LruEntry, bool) { @@ -171,14 +192,20 @@ func (sfc *SingleFileCache) Get(key string) (*LruEntry, bool) { } func (sfc *SingleFileCache) GetFresh(key string) (*LruEntry, bool) { - sfc.entry.Reload() + err := sfc.entry.Reload() + if err != nil { + return nil, false + } return sfc.entry, true } // Put THIS WILL DELETE ANYTHING YOU HAVE STORED IN THIS CACHE. func (sfc *SingleFileCache) Put(key string, pathlike string) error { //there's a bug in this. we don't return an error from newLRUEntry, so if the file disappears, the server will die - entry := newLRUEntry(pathlike) + entry, err := newLRUEntry(pathlike) + if err != nil { + return err + } sfc.entry = entry return nil } @@ -187,13 +214,16 @@ func (sfc *SingleFileCache) Remove(key string) { //i'm actually not sure what to do here. why would you ever remove from a single-file cache? } -func newLRUEntry(pathlike string) *LruEntry { - bits, exists := GetDiskContent(pathlike) - if !exists { - panic("Could not load single file for single file path: " + pathlike) +func newLRUEntry(pathlike string) (*LruEntry, error) { + bits, err := GetDiskContent(pathlike) + if err != nil { + return nil, err } - compressedBits := GZipBytes(bits) + compressedBits, err := GZipBytes(bits) + if err != nil { + return nil, err + } size := int64(len(*bits)) + int64(len(*compressedBits)) @@ -207,7 +237,7 @@ func newLRUEntry(pathlike string) *LruEntry { totalSize: size, MimeType: GuessMimetype(pathlike), } - return &entry + return &entry, nil } func GuessMimetype(filePath string) string {