diff --git a/README.md b/README.md index e08729a..bab2ec1 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,21 @@ - bun - go + +## Getting started + +To run filething, run + +```BASH +go generate +go build -tags netgo -ldflags=-s +DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data ./filething +``` + +### Contributing + +To run filething in dev mode with a hot reloading Ui server and auto rebuilding backend server, run + +```BASH +DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data CompileDaemon --build="go build -tags netgo,dev -ldflags=-s" --command=./filething --exclude-dir=data/ --graceful-kill +``` diff --git a/main.go b/main.go index 5bb1a98..5da7ade 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ -//go:generate bun --cwd=./ui install -//go:generate bun --bun --cwd=./ui run generate +//go:generate sh -c "NODE_ENV=production bun --cwd=./ui install" +//go:generate sh -c "NODE_ENV=production bun --bun --cwd=./ui run generate" package main import ( @@ -22,7 +22,10 @@ import ( "github.com/uptrace/bun/driver/pgdriver" ) +var initUi func(e *echo.Echo) + func main() { + dbHost := os.Getenv("DB_HOST") dbName := os.Getenv("DB_NAME") dbUser := os.Getenv("DB_USER") @@ -77,7 +80,7 @@ func main() { api.POST("/files/upload*", routes.UploadFile) api.GET("/files/get/*", routes.GetFiles) - api.GET("/files/download/*", routes.GetFile) + api.GET("/files/download*", routes.GetFile) api.POST("/files/delete*", routes.DeleteFiles) } @@ -85,11 +88,13 @@ func main() { // this isnt explicitly required, but it provides a better experience than doing this same thing clientside e.Use(middleware.AuthCheckMiddleware) - e.GET("/*", echo.StaticDirectoryHandler(ui.DistDirFS, false)) + initUi(e) e.HTTPErrorHandler = customHTTPErrorHandler - e.Logger.Fatal(e.Start(":1323")) + if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed { + fmt.Println("Error starting HTTP server:", err) + } } func customHTTPErrorHandler(err error, c echo.Context) { diff --git a/middleware/route.go b/middleware/route.go index ffcfe6b..ec1483c 100644 --- a/middleware/route.go +++ b/middleware/route.go @@ -20,6 +20,12 @@ var authenticatedPages = []string{ func AuthCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { path := c.Request().URL.Path + + // bypass auth checks for static and dev resources + if strings.HasPrefix(path, "/_nuxt/") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") { + return next(c) + } + _, cookieErr := c.Cookie("sessionToken") authenticated := cookieErr == nil diff --git a/routes/files.go b/routes/files.go index 7961e50..64c98c5 100644 --- a/routes/files.go +++ b/routes/files.go @@ -1,10 +1,13 @@ package routes import ( + "archive/zip" + "bytes" "filething/models" "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "strings" @@ -78,11 +81,6 @@ func UploadFile(c echo.Context) error { return err } - if err != nil { - fmt.Println(err) - return err - } - filepath := filepath.Join(basePath, part.FileName()) if _, err = os.Stat(filepath); err == nil { @@ -152,14 +150,11 @@ func UploadFile(c echo.Context) error { func calculateStorageUsage(basePath string) (int64, error) { var totalSize int64 - - // Read the directory entries, err := os.ReadDir(basePath) if err != nil { return 0, err } - // Iterate over directory entries for _, entry := range entries { if entry.IsDir() { // Recursively calculate size of directories @@ -226,9 +221,122 @@ func GetFile(c echo.Context) error { user := c.Get("user").(*models.User) fullPath := strings.Trim(c.Param("*"), "/") - basePath := fmt.Sprintf("%s/%s/%s", os.Getenv("STORAGE_PATH"), user.ID, fullPath) - return c.File(basePath) + fileNamesParam := c.QueryParam("filenames") + var fileNames []string + if fileNamesParam != "" { + fileNames = strings.Split(fileNamesParam, ",") + } + + if fullPath == "" && len(fileNames) == 0 { + return c.JSON(http.StatusBadRequest, map[string]string{"message": "A file is required"}) + } + + basePath := fmt.Sprintf("%s/%s", os.Getenv("STORAGE_PATH"), user.ID) + if fullPath != "" { + basePath = filepath.Join(basePath, fullPath) + } + + fileInfo, err := os.Stat(basePath) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"message": "No file found!"}) + } + + var buf bytes.Buffer + if fileInfo.IsDir() { + c.Response().Header().Set(echo.HeaderContentType, "application/zip") + + if len(fileNames) != 0 { + err := zipFiles(&buf, filepath.Join(basePath, fullPath), fileNames) + if err != nil { + fmt.Println(err) + return err + } + + _, err = buf.WriteTo(c.Response().Writer) + return err + } + + err := zipFiles(&buf, basePath, []string{""}) + if err != nil { + fmt.Println(err) + return err + } + + _, err = buf.WriteTo(c.Response().Writer) + return err + } else { + return c.File(basePath) + } +} + +func zipFiles(buf *bytes.Buffer, basePath string, files []string) error { + zipWriter := zip.NewWriter(buf) + defer zipWriter.Close() + + for _, filePath := range files { + unescapedFilePath, err := url.PathUnescape(filePath) + if err != nil { + return err + } + err = processFile(zipWriter, basePath, unescapedFilePath) + if err != nil { + return err + } + } + + return nil +} + +func processFile(zipWriter *zip.Writer, basePath string, filePath string) error { + fullFilePath := filepath.Join(basePath, filePath) + + fileInfo, err := os.Stat(fullFilePath) + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return err + } + + header.Method = zip.Deflate + + header.Name = filepath.ToSlash(filePath) + + if fileInfo.IsDir() { + header.Name += "/" + } + + headerWriter, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + if fileInfo.IsDir() { + files, err := os.ReadDir(fullFilePath) + if err != nil { + return err + } + + for _, file := range files { + err := processFile(zipWriter, basePath, filepath.Join(filePath, file.Name())) + if err != nil { + return err + } + } + return nil + } + + file, err := os.Open(fullFilePath) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(headerWriter, file) + return err } type DeleteRequest struct { diff --git a/server.go b/server.go new file mode 100644 index 0000000..37c3bde --- /dev/null +++ b/server.go @@ -0,0 +1,16 @@ +//go:build !dev +// +build !dev + +package main + +import ( + "filething/ui" + + "github.com/labstack/echo/v4" +) + +func init() { + initUi = func(e *echo.Echo) { + e.GET("/*", echo.StaticDirectoryHandler(ui.DistDirFS, false)) + } +} diff --git a/server_dev.go b/server_dev.go new file mode 100644 index 0000000..41d5118 --- /dev/null +++ b/server_dev.go @@ -0,0 +1,69 @@ +//go:build dev +// +build dev + +package main + +import ( + "context" + "fmt" + "net/url" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/labstack/echo/v4" + echoMiddleware "github.com/labstack/echo/v4/middleware" +) + +func init() { + initUi = func(e *echo.Echo) { + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + go func() { + cmd := exec.Command("bun", "--cwd=ui", "run", "dev") + cmd.Stderr = os.Stderr + + // use a preocess group since otherwise the node processes spawned by bun wont die + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + err := cmd.Start() + if err != nil { + if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM); err != nil { + fmt.Println("Error sending SIGTERM to Nuxt dev server group:", err) + } + + fmt.Println("Error starting Nuxt dev server:", err) + return + } + + go func() { + <-shutdown + + if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM); err != nil { + fmt.Println("Error sending SIGTERM to Nuxt dev server group:", err) + } + }() + + if err := cmd.Wait(); err != nil { + fmt.Println("Error waiting for Nuxt dev server to exit:", err) + } + + fmt.Println("Nuxt dev server stopped") + + if err := e.Shutdown(context.Background()); err != nil { + fmt.Println("Error shutting down HTTP server:", err) + } + }() + + target := "localhost:3000" + e.Group("/*").Use(echoMiddleware.ProxyWithConfig(echoMiddleware.ProxyConfig{ + Balancer: echoMiddleware.NewRoundRobinBalancer([]*echoMiddleware.ProxyTarget{ + {URL: &url.URL{ + Scheme: "http", + Host: target, + }}, + }), + })) + } +} diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index 9d8d55b..40f1639 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -13,9 +13,12 @@ --highlight-low: 11 18 22; --highlight-med: 32 37 38; --highlight-high: 49 55 58; - --color-foam: 86 148 159; + --color-foam: 32 159 181; --color-love: 220 100 130; --color-pine: 40 105 131; + --color-accent: 136 57 239; + --color-accent-20: #dac9f1; + --nav-height: 48px; } @@ -31,9 +34,11 @@ --highlight-low: 244 237 232; --highlight-med: 223 218 217; --highlight-high: 206 202 205; - --color-foam: 156 207 216; + --color-foam: 145 215 227; --color-love: 235 111 146; --color-pine: 49 116 143; + --color-accent: 154 87 237; + --color-accent-20: #342c3f; color-scheme: dark; } diff --git a/ui/bun.lockb b/ui/bun.lockb index c5debe2..e3c7ef7 100755 Binary files a/ui/bun.lockb and b/ui/bun.lockb differ diff --git a/ui/components/Breadcrumbs.vue b/ui/components/Breadcrumbs.vue index af54883..5073635 100644 --- a/ui/components/Breadcrumbs.vue +++ b/ui/components/Breadcrumbs.vue @@ -25,7 +25,7 @@ const crumbs = computed(() => { - {{ crumb.name }} diff --git a/ui/components/Checkbox.vue b/ui/components/Checkbox.vue index 9df3f03..ed7fccd 100644 --- a/ui/components/Checkbox.vue +++ b/ui/components/Checkbox.vue @@ -1,6 +1,6 @@