diff --git a/server/handlers/console/buckets/objects.go b/server/handlers/console/buckets/objects.go index 729d115..7d8ac59 100644 --- a/server/handlers/console/buckets/objects.go +++ b/server/handlers/console/buckets/objects.go @@ -10,6 +10,7 @@ import ( "github.com/mojatter/s2" "github.com/mojatter/s2/internal/numconv" "github.com/mojatter/s2/server" + "github.com/mojatter/s2/server/handlers/console" "github.com/mojatter/s2/server/middleware" ) @@ -63,8 +64,14 @@ func handleObjects(s *server.Server, w http.ResponseWriter, r *http.Request) { parentPrefix = "" } + // Non-HTMX requests get redirected to the index page where the + // sidebar navigation triggers the htmx load. + if r.Header.Get("HX-Request") != "true" { + http.Redirect(w, r, "/", http.StatusFound) + return + } + data := struct { - Buckets []string BucketName string Objects []s2.Object Prefixes []string @@ -82,27 +89,8 @@ func handleObjects(s *server.Server, w http.ResponseWriter, r *http.Request) { HasParent: prefix != "" && prefix != "/", } - // Use a partial template for HTMX requests - if r.Header.Get("HX-Request") == "true" { - var buf bytes.Buffer - if err := s.Template.ExecuteTemplate(&buf, "buckets/objects.html", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - _, _ = buf.WriteTo(w) - return - } - - // Fallback to full page if accessed directly - bucketNames, err := s.Buckets.Names() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data.Buckets = bucketNames - var buf bytes.Buffer - if err := s.Template.ExecuteTemplate(&buf, "index.html", data); err != nil { + if err := s.Template.ExecuteTemplate(&buf, "buckets/objects.html", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -220,6 +208,6 @@ func init() { server.RegisterConsoleHandleFunc("POST /buckets/{name}/folders", middleware.BasicAuth(handleCreateFolder)) server.RegisterConsoleHandleFunc("POST /buckets/{name}/upload", middleware.BasicAuth(handleUploadFile)) server.RegisterConsoleHandleFunc("DELETE /buckets/{name}/objects", middleware.BasicAuth(handleDeleteObject)) - server.RegisterTemplate("buckets/objects.html") + console.RegisterTemplateWithScripts("buckets/objects.html") } diff --git a/server/handlers/console/buckets/objects/view.go b/server/handlers/console/buckets/objects/view.go index a7659a5..c8b874e 100644 --- a/server/handlers/console/buckets/objects/view.go +++ b/server/handlers/console/buckets/objects/view.go @@ -1,6 +1,7 @@ package objects import ( + "bytes" "encoding/json" "fmt" "io" @@ -106,7 +107,78 @@ func handleMeta(s *server.Server, w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(resp) } +func handlePreview(s *server.Server, w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + bucketName := r.PathValue("name") + objectName := r.PathValue("object") + + strg, err := s.Buckets.Get(ctx, bucketName) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + obj, err := strg.Get(ctx, objectName) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + ext := strings.ToLower(path.Ext(objectName)) + viewURL := fmt.Sprintf("/buckets/%s/view/%s", bucketName, objectName) + previewType := server.PreviewType(ext) + + var textContent string + if previewType == "text" { + rc, err := obj.Open() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = rc.Close() }() + + b, err := io.ReadAll(rc) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + textContent = string(b) + } + + data := struct { + Filename string + ViewURL string + ContentType string + Size uint64 + LastModified string + Metadata map[string]string + PreviewType string + TextContent string + }{ + Filename: path.Base(objectName), + ViewURL: viewURL, + ContentType: contentTypeByExt(ext), + Size: obj.Length(), + LastModified: obj.LastModified().Format("2006-01-02 15:04:05"), + PreviewType: previewType, + TextContent: textContent, + } + if md := obj.Metadata(); len(md) > 0 { + data.Metadata = md + } + + var buf bytes.Buffer + if err := s.Template.ExecuteTemplate(&buf, "buckets/preview.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = buf.WriteTo(w) +} + func init() { server.RegisterConsoleHandleFunc("GET /buckets/{name}/view/{object...}", middleware.BasicAuth(handleView)) server.RegisterConsoleHandleFunc("GET /buckets/{name}/meta/{object...}", middleware.BasicAuth(handleMeta)) + server.RegisterConsoleHandleFunc("GET /buckets/{name}/preview/{object...}", middleware.BasicAuth(handlePreview)) } diff --git a/server/handlers/console/buckets/objects_test.go b/server/handlers/console/buckets/objects_test.go index 86aeb25..34e0978 100644 --- a/server/handlers/console/buckets/objects_test.go +++ b/server/handlers/console/buckets/objects_test.go @@ -101,7 +101,7 @@ func (s *ObjectsTestSuite) TestHandleObjects() { s.Equal(http.StatusNotFound, w.Code) }) - s.Run("full page without HX-Request", func() { + s.Run("full page without HX-Request redirects to index", func() { s.createBucket("full") req := httptest.NewRequest("GET", "/buckets/full", nil) @@ -109,8 +109,8 @@ func (s *ObjectsTestSuite) TestHandleObjects() { w := httptest.NewRecorder() handleObjects(s.server, w, req) - s.Equal(http.StatusOK, w.Code) - s.Contains(w.Body.String(), "") + s.Equal(http.StatusFound, w.Code) + s.Equal("/", w.Header().Get("Location")) }) } diff --git a/server/handlers/console/console_test.go b/server/handlers/console/console_test.go index 7aef0de..ff18793 100644 --- a/server/handlers/console/console_test.go +++ b/server/handlers/console/console_test.go @@ -73,7 +73,9 @@ func (s *IndexTestSuite) TestHandleCreateBucket() { w := httptest.NewRecorder() handleCreateBucket(s.server, w, req) - s.Equal(http.StatusFound, w.Code) + s.Equal(http.StatusOK, w.Code) + s.Contains(w.Body.String(), "new-bucket") + exists, err := s.server.Buckets.Exists("new-bucket") s.Require().NoError(err) s.True(exists) @@ -102,7 +104,8 @@ func (s *IndexTestSuite) TestHandleDeleteBucket() { handleDeleteBucket(s.server, w, req) s.Equal(http.StatusOK, w.Code) - s.Equal("/", w.Header().Get("HX-Redirect")) + s.Equal("/", w.Header().Get("HX-Push-Url")) + s.Contains(w.Body.String(), "bucket-list") exists, err := s.server.Buckets.Exists("to-delete") s.Require().NoError(err) diff --git a/server/handlers/console/index.go b/server/handlers/console/index.go index c22a196..b69c1e0 100644 --- a/server/handlers/console/index.go +++ b/server/handlers/console/index.go @@ -42,8 +42,7 @@ func handleCreateBucket(s *server.Server, w http.ResponseWriter, r *http.Request return } - // For HTMX, just redirect or re-render (usually redirect to index is fine) - http.Redirect(w, r, "/", http.StatusFound) + renderBucketList(s, w) } func handleDeleteBucket(s *server.Server, w http.ResponseWriter, r *http.Request) { @@ -57,11 +56,52 @@ func handleDeleteBucket(s *server.Server, w http.ResponseWriter, r *http.Request return } - // For HTMX DELETE request, redirect to index - w.Header().Set("HX-Redirect", "/") - w.WriteHeader(http.StatusOK) + names, err := s.Buckets.Names() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := struct{ Buckets []string }{Buckets: names} + + var buf bytes.Buffer + if err := s.Template.ExecuteTemplate(&buf, "buckets/list.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // OOB swap to reset main content to empty state + buf.WriteString(`