Add a container images upload feature and auto import into the engine, creating corresponding handlers and routes, and enhancing image import and storage functionalities.
- commit
- 01250ef5f28c51ad46b53c7d2fd19a26fa157e63
- author
- bnema · · 2023-09-23 09:42 UTC
- parent
- 30bfe7c
- signature
- unsigned
8 files · +206 -68
M cmd/srv/main.go +0 -2
| @@ -23,8 +23,6 @@ func cleanup(a *app.App, memDb *sql.DB) { | |||
| 23 | 23 | ||
| 24 | 24 | func main() { | |
| 25 | 25 | a := app.NewApp() | |
| 26 | - | fmt.Println(a) | |
| 27 | - | ||
| 28 | 26 | // Initialize database | |
| 29 | 27 | memDb, err := app.InitializeDB(a) | |
| 30 | 28 | if err != nil { | |
| @@ -23,8 +23,6 @@ func cleanup(a *app.App, memDb *sql.DB) { | |||||
| 23 | 23 | ||||
| 24 | func main() { | 24 | func main() { | ||
| 25 | a := app.NewApp() | 25 | a := app.NewApp() | ||
| 26 | - | fmt.Println(a) | |||
| 27 | - | ||||
| 28 | // Initialize database | 26 | // Initialize database | ||
| 29 | memDb, err := app.InitializeDB(a) | 27 | memDb, err := app.InitializeDB(a) | ||
| 30 | if err != nil { | 28 | if err != nil { | ||
M internal/gotemplate/models/html/fragments/imagelist.gohtml +15 -1
| @@ -1,5 +1,18 @@ | |||
| 1 | 1 | <div class="flex flex-col p-4"> | |
| 2 | - | <h1 class="text-lg md:text-xl font-bold">Images</h1> | |
| 2 | + | <!-- Upload button aligned right of Images --> | |
| 3 | + | <div class="flex justify-between items-center mb-1"> | |
| 4 | + | <h1 class="text-lg md:text-xl font-bold">Images</h1> | |
| 5 | + | <button | |
| 6 | + | id="upload-button" | |
| 7 | + | hx-get="/htmx/upload-image" | |
| 8 | + | hx-target="#upload-image" | |
| 9 | + | hx-swap="outerHTML" | |
| 10 | + | class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded" | |
| 11 | + | > | |
| 12 | + | <span class="iconoir-upload text-xl align-middle"></span> | |
| 13 | + | Upload | |
| 14 | + | </button> | |
| 15 | + | </div> | |
| 3 | 16 | <table class="p-4 border-t-2"> | |
| 4 | 17 | <!-- <thead> | |
| 5 | 18 | <tr> | |
| @@ -49,4 +62,5 @@ | |||
| 49 | 62 | }} | |
| 50 | 63 | </tbody> | |
| 51 | 64 | </table> | |
| 65 | + | <div id="upload-image"></div> | |
| 52 | 66 | </div> | |
| @@ -1,5 +1,18 @@ | |||||
| 1 | <div class="flex flex-col p-4"> | 1 | <div class="flex flex-col p-4"> | ||
| 2 | - | <h1 class="text-lg md:text-xl font-bold">Images</h1> | 2 | + | <!-- Upload button aligned right of Images --> |
| 3 | + | <div class="flex justify-between items-center mb-1"> | |||
| 4 | + | <h1 class="text-lg md:text-xl font-bold">Images</h1> | |||
| 5 | + | <button | |||
| 6 | + | id="upload-button" | |||
| 7 | + | hx-get="/htmx/upload-image" | |||
| 8 | + | hx-target="#upload-image" | |||
| 9 | + | hx-swap="outerHTML" | |||
| 10 | + | class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded" | |||
| 11 | + | > | |||
| 12 | + | <span class="iconoir-upload text-xl align-middle"></span> | |||
| 13 | + | Upload | |||
| 14 | + | </button> | |||
| 15 | + | </div> | |||
| 3 | <table class="p-4 border-t-2"> | 16 | <table class="p-4 border-t-2"> | ||
| 4 | <!-- <thead> | 17 | <!-- <thead> | ||
| 5 | <tr> | 18 | <tr> | ||
| @@ -49,4 +62,5 @@ | |||||
| 49 | }} | 62 | }} | ||
| 50 | </tbody> | 63 | </tbody> | ||
| 51 | </table> | 64 | </table> | ||
| 65 | + | <div id="upload-image"></div> | |||
| 52 | </div> | 66 | </div> | ||
M internal/gotemplate/models/html/fragments/uploadimage.gohtml +50 -0
| @@ -1,1 +1,51 @@ | |||
| 1 | 1 | <!-- Form to upload a container image (.tar.gz format) --> | |
| 2 | + | <div | |
| 3 | + | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |
| 4 | + | hx-ext="response-targets" | |
| 5 | + | hx-boost="true" | |
| 6 | + | > | |
| 7 | + | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |
| 8 | + | <h2>Upload Image Containers</h2> | |
| 9 | + | <!-- Upload Form --> | |
| 10 | + | <form | |
| 11 | + | action="/upload" | |
| 12 | + | method="post" | |
| 13 | + | enctype="multipart/form-data" | |
| 14 | + | hx-post="/htmx/upload-image" | |
| 15 | + | hx-swap="innerHTML" | |
| 16 | + | hx-target="#image-manager" | |
| 17 | + | hx-target-*="#error-message-div" | |
| 18 | + | > | |
| 19 | + | <div | |
| 20 | + | class="upload-zone w-full h-44 bg-gray-300 rounded-md shadow mb-4" | |
| 21 | + | hx-drop="true" | |
| 22 | + | > | |
| 23 | + | Drag & Drop your file here or | |
| 24 | + | <input | |
| 25 | + | type="file" | |
| 26 | + | name="file" | |
| 27 | + | accept=".tar" | |
| 28 | + | class="bg-transparent outline-none" | |
| 29 | + | /> | |
| 30 | + | </div> | |
| 31 | + | <input | |
| 32 | + | type="submit" | |
| 33 | + | value="Upload" | |
| 34 | + | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
| 35 | + | /> | |
| 36 | + | </form> | |
| 37 | + | <div id="error-message-div" class="text-red-500"></div> | |
| 38 | + | </div> | |
| 39 | + | </div> | |
| 40 | + | <script> | |
| 41 | + | document.body.addEventListener("htmx:afterOnLoad", function (event) { | |
| 42 | + | if ( | |
| 43 | + | event.target.id === "image-manager" && | |
| 44 | + | event.detail.xhr.responseText === "Success" | |
| 45 | + | ) { | |
| 46 | + | setTimeout(function () { | |
| 47 | + | location.reload(); | |
| 48 | + | }, 3000); | |
| 49 | + | } | |
| 50 | + | }); | |
| 51 | + | </script> | |
| @@ -1,1 +1,51 @@ | |||||
| 1 | <!-- Form to upload a container image (.tar.gz format) --> | 1 | <!-- Form to upload a container image (.tar.gz format) --> | ||
| 2 | + | <div | |||
| 3 | + | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |||
| 4 | + | hx-ext="response-targets" | |||
| 5 | + | hx-boost="true" | |||
| 6 | + | > | |||
| 7 | + | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |||
| 8 | + | <h2>Upload Image Containers</h2> | |||
| 9 | + | <!-- Upload Form --> | |||
| 10 | + | <form | |||
| 11 | + | action="/upload" | |||
| 12 | + | method="post" | |||
| 13 | + | enctype="multipart/form-data" | |||
| 14 | + | hx-post="/htmx/upload-image" | |||
| 15 | + | hx-swap="innerHTML" | |||
| 16 | + | hx-target="#image-manager" | |||
| 17 | + | hx-target-*="#error-message-div" | |||
| 18 | + | > | |||
| 19 | + | <div | |||
| 20 | + | class="upload-zone w-full h-44 bg-gray-300 rounded-md shadow mb-4" | |||
| 21 | + | hx-drop="true" | |||
| 22 | + | > | |||
| 23 | + | Drag & Drop your file here or | |||
| 24 | + | <input | |||
| 25 | + | type="file" | |||
| 26 | + | name="file" | |||
| 27 | + | accept=".tar" | |||
| 28 | + | class="bg-transparent outline-none" | |||
| 29 | + | /> | |||
| 30 | + | </div> | |||
| 31 | + | <input | |||
| 32 | + | type="submit" | |||
| 33 | + | value="Upload" | |||
| 34 | + | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |||
| 35 | + | /> | |||
| 36 | + | </form> | |||
| 37 | + | <div id="error-message-div" class="text-red-500"></div> | |||
| 38 | + | </div> | |||
| 39 | + | </div> | |||
| 40 | + | <script> | |||
| 41 | + | document.body.addEventListener("htmx:afterOnLoad", function (event) { | |||
| 42 | + | if ( | |||
| 43 | + | event.target.id === "image-manager" && | |||
| 44 | + | event.detail.xhr.responseText === "Success" | |||
| 45 | + | ) { | |||
| 46 | + | setTimeout(function () { | |||
| 47 | + | location.reload(); | |||
| 48 | + | }, 3000); | |||
| 49 | + | } | |||
| 50 | + | }); | |||
| 51 | + | </script> | |||
M internal/httpserve/handler/uploadimage.go +31 -31
| @@ -1,59 +1,59 @@ | |||
| 1 | 1 | package handler | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "io" | |
| 4 | + | "fmt" | |
| 5 | 5 | "net/http" | |
| 6 | - | "os" | |
| 7 | - | "strings" | |
| 8 | 6 | ||
| 9 | 7 | "github.com/bnema/gordon/internal/app" | |
| 8 | + | "github.com/bnema/gordon/internal/gotemplate/render" | |
| 9 | + | "github.com/bnema/gordon/pkg/utils/store" | |
| 10 | 10 | "github.com/labstack/echo/v4" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | 13 | const MaxUploadSize = 1024 * 1024 * 1024 // 1GB | |
| 14 | 14 | ||
| 15 | - | // UploadImageHandler handles the /upload-image route | |
| 16 | - | func UploadImageHandler(c echo.Context, a *app.App) error { | |
| 15 | + | // UploadImageHandler handles the /upload-image route to show the form | |
| 16 | + | func UploadImageGETHandler(c echo.Context, a *app.App) error { | |
| 17 | + | data := map[string]interface{}{ | |
| 18 | + | "Title": "Upload Image", | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | rendererData, err := render.GetHTMLRenderer("html/fragments", "uploadimage.gohtml", a.TemplateFS, a) | |
| 22 | + | if err != nil { | |
| 23 | + | return sendError(c, err) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | renderedHTML, err := rendererData.Render(data, a) | |
| 27 | + | if err != nil { | |
| 28 | + | return err | |
| 29 | + | } | |
| 30 | + | return c.HTML(200, renderedHTML) | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // UploadImageHandler handles the /upload-image | |
| 34 | + | func UploadImagePOSTHandler(c echo.Context, a *app.App) error { | |
| 17 | 35 | // Set upload size limit | |
| 18 | 36 | c.Request().Body = http.MaxBytesReader(c.Response(), c.Request().Body, MaxUploadSize) | |
| 19 | 37 | if err := c.Request().ParseMultipartForm(MaxUploadSize); err != nil { | |
| 20 | 38 | return c.String(http.StatusRequestEntityTooLarge, "File size exceeded") | |
| 21 | 39 | } | |
| 22 | - | ||
| 23 | 40 | // Get the file from the form | |
| 24 | - | file, _, err := c.Request().FormFile("file") | |
| 41 | + | file, header, err := c.Request().FormFile("file") | |
| 25 | 42 | if err != nil { | |
| 26 | 43 | return sendError(c, err) | |
| 27 | 44 | } | |
| 28 | 45 | defer file.Close() | |
| 29 | 46 | ||
| 30 | - | // Check file type | |
| 31 | - | buffer := make([]byte, 512) | |
| 32 | - | if _, err := file.Read(buffer); err != nil { | |
| 33 | - | return sendError(c, err) | |
| 34 | - | } | |
| 35 | - | contentType := http.DetectContentType(buffer) | |
| 36 | - | if !strings.Contains(contentType, "application/gzip") { | |
| 37 | - | return c.String(http.StatusUnsupportedMediaType, "Invalid file type") | |
| 38 | - | } | |
| 39 | - | if _, err := file.Seek(0, io.SeekStart); err != nil { | |
| 40 | - | return sendError(c, err) | |
| 41 | - | } | |
| 47 | + | // Get the filename | |
| 48 | + | filename := header.Filename | |
| 49 | + | fmt.Println("Uploaded file:", filename) | |
| 42 | 50 | ||
| 43 | - | // Save to temporary file | |
| 44 | - | tmpFile, err := os.CreateTemp("/tmp", "upload-*.tar.gz") | |
| 51 | + | // Save the image to the storage directory | |
| 52 | + | imageId, err := store.SaveImageToStorage(&a.Config, filename, file) | |
| 45 | 53 | if err != nil { | |
| 46 | 54 | return sendError(c, err) | |
| 47 | 55 | } | |
| 48 | - | defer os.Remove(tmpFile.Name()) | |
| 49 | - | if _, err := io.Copy(tmpFile, file); err != nil { | |
| 50 | - | return sendError(c, err) | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | // Close and flush the temporary file to make sure all content is written | |
| 54 | - | if err := tmpFile.Close(); err != nil { | |
| 55 | - | return sendError(c, err) | |
| 56 | - | } | |
| 57 | - | ||
| 56 | + | // Just print for now but add to the database later | |
| 57 | + | fmt.Println(imageId) | |
| 58 | 58 | return c.HTML(http.StatusOK, "Success") | |
| 59 | 59 | } | |
| @@ -1,59 +1,59 @@ | |||||
| 1 | package handler | 1 | package handler | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "io" | 4 | + | "fmt" |
| 5 | "net/http" | 5 | "net/http" | ||
| 6 | - | "os" | |||
| 7 | - | "strings" | |||
| 8 | 6 | ||||
| 9 | "github.com/bnema/gordon/internal/app" | 7 | "github.com/bnema/gordon/internal/app" | ||
| 8 | + | "github.com/bnema/gordon/internal/gotemplate/render" | |||
| 9 | + | "github.com/bnema/gordon/pkg/utils/store" | |||
| 10 | "github.com/labstack/echo/v4" | 10 | "github.com/labstack/echo/v4" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | const MaxUploadSize = 1024 * 1024 * 1024 // 1GB | 13 | const MaxUploadSize = 1024 * 1024 * 1024 // 1GB | ||
| 14 | 14 | ||||
| 15 | - | // UploadImageHandler handles the /upload-image route | 15 | + | // UploadImageHandler handles the /upload-image route to show the form |
| 16 | - | func UploadImageHandler(c echo.Context, a *app.App) error { | 16 | + | func UploadImageGETHandler(c echo.Context, a *app.App) error { |
| 17 | + | data := map[string]interface{}{ | |||
| 18 | + | "Title": "Upload Image", | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | rendererData, err := render.GetHTMLRenderer("html/fragments", "uploadimage.gohtml", a.TemplateFS, a) | |||
| 22 | + | if err != nil { | |||
| 23 | + | return sendError(c, err) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | renderedHTML, err := rendererData.Render(data, a) | |||
| 27 | + | if err != nil { | |||
| 28 | + | return err | |||
| 29 | + | } | |||
| 30 | + | return c.HTML(200, renderedHTML) | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // UploadImageHandler handles the /upload-image | |||
| 34 | + | func UploadImagePOSTHandler(c echo.Context, a *app.App) error { | |||
| 17 | // Set upload size limit | 35 | // Set upload size limit | ||
| 18 | c.Request().Body = http.MaxBytesReader(c.Response(), c.Request().Body, MaxUploadSize) | 36 | c.Request().Body = http.MaxBytesReader(c.Response(), c.Request().Body, MaxUploadSize) | ||
| 19 | if err := c.Request().ParseMultipartForm(MaxUploadSize); err != nil { | 37 | if err := c.Request().ParseMultipartForm(MaxUploadSize); err != nil { | ||
| 20 | return c.String(http.StatusRequestEntityTooLarge, "File size exceeded") | 38 | return c.String(http.StatusRequestEntityTooLarge, "File size exceeded") | ||
| 21 | } | 39 | } | ||
| 22 | - | ||||
| 23 | // Get the file from the form | 40 | // Get the file from the form | ||
| 24 | - | file, _, err := c.Request().FormFile("file") | 41 | + | file, header, err := c.Request().FormFile("file") |
| 25 | if err != nil { | 42 | if err != nil { | ||
| 26 | return sendError(c, err) | 43 | return sendError(c, err) | ||
| 27 | } | 44 | } | ||
| 28 | defer file.Close() | 45 | defer file.Close() | ||
| 29 | 46 | ||||
| 30 | - | // Check file type | 47 | + | // Get the filename |
| 31 | - | buffer := make([]byte, 512) | 48 | + | filename := header.Filename |
| 32 | - | if _, err := file.Read(buffer); err != nil { | 49 | + | fmt.Println("Uploaded file:", filename) |
| 33 | - | return sendError(c, err) | |||
| 34 | - | } | |||
| 35 | - | contentType := http.DetectContentType(buffer) | |||
| 36 | - | if !strings.Contains(contentType, "application/gzip") { | |||
| 37 | - | return c.String(http.StatusUnsupportedMediaType, "Invalid file type") | |||
| 38 | - | } | |||
| 39 | - | if _, err := file.Seek(0, io.SeekStart); err != nil { | |||
| 40 | - | return sendError(c, err) | |||
| 41 | - | } | |||
| 42 | 50 | ||||
| 43 | - | // Save to temporary file | 51 | + | // Save the image to the storage directory |
| 44 | - | tmpFile, err := os.CreateTemp("/tmp", "upload-*.tar.gz") | 52 | + | imageId, err := store.SaveImageToStorage(&a.Config, filename, file) |
| 45 | if err != nil { | 53 | if err != nil { | ||
| 46 | return sendError(c, err) | 54 | return sendError(c, err) | ||
| 47 | } | 55 | } | ||
| 48 | - | defer os.Remove(tmpFile.Name()) | 56 | + | // Just print for now but add to the database later |
| 49 | - | if _, err := io.Copy(tmpFile, file); err != nil { | 57 | + | fmt.Println(imageId) |
| 50 | - | return sendError(c, err) | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | // Close and flush the temporary file to make sure all content is written | |||
| 54 | - | if err := tmpFile.Close(); err != nil { | |||
| 55 | - | return sendError(c, err) | |||
| 56 | - | } | |||
| 57 | - | ||||
| 58 | return c.HTML(http.StatusOK, "Success") | 58 | return c.HTML(http.StatusOK, "Success") | ||
| 59 | } | 59 | } | ||
M internal/httpserve/router.go +6 -2
| @@ -73,15 +73,19 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||
| 73 | 73 | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { | |
| 74 | 74 | return handler.ImageManagerDeleteHandler(c, a) | |
| 75 | 75 | }) | |
| 76 | - | ||
| 77 | 76 | e.GET("/htmx/container-manager", func(c echo.Context) error { | |
| 78 | 77 | return handler.ContainerManagerHandler(c, a) | |
| 79 | 78 | }) | |
| 80 | 79 | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { | |
| 81 | 80 | return handler.ContainerManagerStopHandler(c, a) | |
| 82 | 81 | }) | |
| 83 | - | ||
| 84 | 82 | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | |
| 85 | 83 | return handler.ContainerManagerDeleteHandler(c, a) | |
| 86 | 84 | }) | |
| 85 | + | e.GET("/htmx/upload-image", func(c echo.Context) error { | |
| 86 | + | return handler.UploadImageGETHandler(c, a) | |
| 87 | + | }) | |
| 88 | + | e.POST("/htmx/upload-image", func(c echo.Context) error { | |
| 89 | + | return handler.UploadImagePOSTHandler(c, a) | |
| 90 | + | }) | |
| 87 | 91 | } | |
| @@ -73,15 +73,19 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||||
| 73 | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { | 73 | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { | ||
| 74 | return handler.ImageManagerDeleteHandler(c, a) | 74 | return handler.ImageManagerDeleteHandler(c, a) | ||
| 75 | }) | 75 | }) | ||
| 76 | - | ||||
| 77 | e.GET("/htmx/container-manager", func(c echo.Context) error { | 76 | e.GET("/htmx/container-manager", func(c echo.Context) error { | ||
| 78 | return handler.ContainerManagerHandler(c, a) | 77 | return handler.ContainerManagerHandler(c, a) | ||
| 79 | }) | 78 | }) | ||
| 80 | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { | 79 | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { | ||
| 81 | return handler.ContainerManagerStopHandler(c, a) | 80 | return handler.ContainerManagerStopHandler(c, a) | ||
| 82 | }) | 81 | }) | ||
| 83 | - | ||||
| 84 | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | 82 | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | ||
| 85 | return handler.ContainerManagerDeleteHandler(c, a) | 83 | return handler.ContainerManagerDeleteHandler(c, a) | ||
| 86 | }) | 84 | }) | ||
| 85 | + | e.GET("/htmx/upload-image", func(c echo.Context) error { | |||
| 86 | + | return handler.UploadImageGETHandler(c, a) | |||
| 87 | + | }) | |||
| 88 | + | e.POST("/htmx/upload-image", func(c echo.Context) error { | |||
| 89 | + | return handler.UploadImagePOSTHandler(c, a) | |||
| 90 | + | }) | |||
| 87 | } | 91 | } | ||
M internal/webui/public/assets/css/tailwind.css +48 -0
| @@ -600,6 +600,18 @@ video { | |||
| 600 | 600 | margin-top: 1rem; | |
| 601 | 601 | } | |
| 602 | 602 | ||
| 603 | + | .mb-1 { | |
| 604 | + | margin-bottom: 0.25rem; | |
| 605 | + | } | |
| 606 | + | ||
| 607 | + | .mt-1 { | |
| 608 | + | margin-top: 0.25rem; | |
| 609 | + | } | |
| 610 | + | ||
| 611 | + | .mt-3 { | |
| 612 | + | margin-top: 0.75rem; | |
| 613 | + | } | |
| 614 | + | ||
| 603 | 615 | .flex { | |
| 604 | 616 | display: flex; | |
| 605 | 617 | } | |
| @@ -709,6 +721,10 @@ video { | |||
| 709 | 721 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | |
| 710 | 722 | } | |
| 711 | 723 | ||
| 724 | + | .rounded { | |
| 725 | + | border-radius: 0.25rem; | |
| 726 | + | } | |
| 727 | + | ||
| 712 | 728 | .rounded-lg { | |
| 713 | 729 | border-radius: 0.5rem; | |
| 714 | 730 | } | |
| @@ -734,6 +750,11 @@ video { | |||
| 734 | 750 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | |
| 735 | 751 | } | |
| 736 | 752 | ||
| 753 | + | .bg-blue-500 { | |
| 754 | + | --tw-bg-opacity: 1; | |
| 755 | + | background-color: rgb(59 130 246 / var(--tw-bg-opacity)); | |
| 756 | + | } | |
| 757 | + | ||
| 737 | 758 | .bg-gordonmascot-hblue { | |
| 738 | 759 | --tw-bg-opacity: 1; | |
| 739 | 760 | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | |
| @@ -790,6 +811,23 @@ video { | |||
| 790 | 811 | padding-bottom: 0.5rem; | |
| 791 | 812 | } | |
| 792 | 813 | ||
| 814 | + | .py-1 { | |
| 815 | + | padding-top: 0.25rem; | |
| 816 | + | padding-bottom: 0.25rem; | |
| 817 | + | } | |
| 818 | + | ||
| 819 | + | .pt-2 { | |
| 820 | + | padding-top: 0.5rem; | |
| 821 | + | } | |
| 822 | + | ||
| 823 | + | .align-baseline { | |
| 824 | + | vertical-align: baseline; | |
| 825 | + | } | |
| 826 | + | ||
| 827 | + | .align-middle { | |
| 828 | + | vertical-align: middle; | |
| 829 | + | } | |
| 830 | + | ||
| 793 | 831 | .text-2xl { | |
| 794 | 832 | font-size: 1.5rem; | |
| 795 | 833 | line-height: 2rem; | |
| @@ -872,6 +910,11 @@ video { | |||
| 872 | 910 | color: rgb(255 255 255 / var(--tw-text-opacity)); | |
| 873 | 911 | } | |
| 874 | 912 | ||
| 913 | + | .text-black { | |
| 914 | + | --tw-text-opacity: 1; | |
| 915 | + | color: rgb(0 0 0 / var(--tw-text-opacity)); | |
| 916 | + | } | |
| 917 | + | ||
| 875 | 918 | .underline { | |
| 876 | 919 | text-decoration-line: underline; | |
| 877 | 920 | } | |
| @@ -1007,6 +1050,11 @@ video { | |||
| 1007 | 1050 | margin-right: 8px; | |
| 1008 | 1051 | } | |
| 1009 | 1052 | ||
| 1053 | + | .hover\:bg-blue-700:hover { | |
| 1054 | + | --tw-bg-opacity: 1; | |
| 1055 | + | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); | |
| 1056 | + | } | |
| 1057 | + | ||
| 1010 | 1058 | @media (min-width: 640px) { | |
| 1011 | 1059 | .sm\:h-24 { | |
| 1012 | 1060 | height: 6rem; | |
| @@ -600,6 +600,18 @@ video { | |||||
| 600 | margin-top: 1rem; | 600 | margin-top: 1rem; | ||
| 601 | } | 601 | } | ||
| 602 | 602 | ||||
| 603 | + | .mb-1 { | |||
| 604 | + | margin-bottom: 0.25rem; | |||
| 605 | + | } | |||
| 606 | + | ||||
| 607 | + | .mt-1 { | |||
| 608 | + | margin-top: 0.25rem; | |||
| 609 | + | } | |||
| 610 | + | ||||
| 611 | + | .mt-3 { | |||
| 612 | + | margin-top: 0.75rem; | |||
| 613 | + | } | |||
| 614 | + | ||||
| 603 | .flex { | 615 | .flex { | ||
| 604 | display: flex; | 616 | display: flex; | ||
| 605 | } | 617 | } | ||
| @@ -709,6 +721,10 @@ video { | |||||
| 709 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | 721 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | ||
| 710 | } | 722 | } | ||
| 711 | 723 | ||||
| 724 | + | .rounded { | |||
| 725 | + | border-radius: 0.25rem; | |||
| 726 | + | } | |||
| 727 | + | ||||
| 712 | .rounded-lg { | 728 | .rounded-lg { | ||
| 713 | border-radius: 0.5rem; | 729 | border-radius: 0.5rem; | ||
| 714 | } | 730 | } | ||
| @@ -734,6 +750,11 @@ video { | |||||
| 734 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | 750 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||
| 735 | } | 751 | } | ||
| 736 | 752 | ||||
| 753 | + | .bg-blue-500 { | |||
| 754 | + | --tw-bg-opacity: 1; | |||
| 755 | + | background-color: rgb(59 130 246 / var(--tw-bg-opacity)); | |||
| 756 | + | } | |||
| 757 | + | ||||
| 737 | .bg-gordonmascot-hblue { | 758 | .bg-gordonmascot-hblue { | ||
| 738 | --tw-bg-opacity: 1; | 759 | --tw-bg-opacity: 1; | ||
| 739 | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | 760 | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | ||
| @@ -790,6 +811,23 @@ video { | |||||
| 790 | padding-bottom: 0.5rem; | 811 | padding-bottom: 0.5rem; | ||
| 791 | } | 812 | } | ||
| 792 | 813 | ||||
| 814 | + | .py-1 { | |||
| 815 | + | padding-top: 0.25rem; | |||
| 816 | + | padding-bottom: 0.25rem; | |||
| 817 | + | } | |||
| 818 | + | ||||
| 819 | + | .pt-2 { | |||
| 820 | + | padding-top: 0.5rem; | |||
| 821 | + | } | |||
| 822 | + | ||||
| 823 | + | .align-baseline { | |||
| 824 | + | vertical-align: baseline; | |||
| 825 | + | } | |||
| 826 | + | ||||
| 827 | + | .align-middle { | |||
| 828 | + | vertical-align: middle; | |||
| 829 | + | } | |||
| 830 | + | ||||
| 793 | .text-2xl { | 831 | .text-2xl { | ||
| 794 | font-size: 1.5rem; | 832 | font-size: 1.5rem; | ||
| 795 | line-height: 2rem; | 833 | line-height: 2rem; | ||
| @@ -872,6 +910,11 @@ video { | |||||
| 872 | color: rgb(255 255 255 / var(--tw-text-opacity)); | 910 | color: rgb(255 255 255 / var(--tw-text-opacity)); | ||
| 873 | } | 911 | } | ||
| 874 | 912 | ||||
| 913 | + | .text-black { | |||
| 914 | + | --tw-text-opacity: 1; | |||
| 915 | + | color: rgb(0 0 0 / var(--tw-text-opacity)); | |||
| 916 | + | } | |||
| 917 | + | ||||
| 875 | .underline { | 918 | .underline { | ||
| 876 | text-decoration-line: underline; | 919 | text-decoration-line: underline; | ||
| 877 | } | 920 | } | ||
| @@ -1007,6 +1050,11 @@ video { | |||||
| 1007 | margin-right: 8px; | 1050 | margin-right: 8px; | ||
| 1008 | } | 1051 | } | ||
| 1009 | 1052 | ||||
| 1053 | + | .hover\:bg-blue-700:hover { | |||
| 1054 | + | --tw-bg-opacity: 1; | |||
| 1055 | + | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); | |||
| 1056 | + | } | |||
| 1057 | + | ||||
| 1010 | @media (min-width: 640px) { | 1058 | @media (min-width: 640px) { | ||
| 1011 | .sm\:h-24 { | 1059 | .sm\:h-24 { | ||
| 1012 | height: 6rem; | 1060 | height: 6rem; | ||
M pkg/utils/docker/images.go +20 -25
| @@ -2,11 +2,8 @@ package docker | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "encoding/json" | |
| 6 | 5 | "fmt" | |
| 7 | - | "io" | |
| 8 | 6 | "os" | |
| 9 | - | "strings" | |
| 10 | 7 | ||
| 11 | 8 | "github.com/docker/docker/api/types" | |
| 12 | 9 | ) | |
| @@ -35,7 +32,9 @@ func DeleteContainerImage(imageID string) error { | |||
| 35 | 32 | return nil | |
| 36 | 33 | } | |
| 37 | 34 | ||
| 38 | - | func LoadImage(imagePath string) (string, error) { | |
| 35 | + | func ImportImageToEngine(imagePath string) (string, error) { | |
| 36 | + | fmt.Println("Importing image to Docker engine") | |
| 37 | + | fmt.Println(imagePath) | |
| 39 | 38 | // Check if the Docker client has been initialized | |
| 40 | 39 | CheckIfInitialized() | |
| 41 | 40 | ||
| @@ -46,38 +45,34 @@ func LoadImage(imagePath string) (string, error) { | |||
| 46 | 45 | } | |
| 47 | 46 | defer imageFile.Close() | |
| 48 | 47 | ||
| 49 | - | // Load the image using the Docker client | |
| 50 | - | resp, err := dockerCli.ImageLoad(context.Background(), imageFile, false) | |
| 48 | + | // Import the image into Docker | |
| 49 | + | _, err = dockerCli.ImageLoad(context.Background(), imageFile, false) | |
| 51 | 50 | if err != nil { | |
| 52 | 51 | return "", err | |
| 53 | 52 | } | |
| 54 | 53 | ||
| 55 | - | defer resp.Body.Close() | |
| 56 | - | ||
| 57 | - | // Read the JSON response to get the image ID | |
| 58 | - | decoder := json.NewDecoder(resp.Body) | |
| 54 | + | // List all images | |
| 55 | + | images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{}) | |
| 56 | + | if err != nil { | |
| 57 | + | return "", err | |
| 58 | + | } | |
| 59 | 59 | ||
| 60 | + | // Search for the image we just loaded | |
| 60 | 61 | var imageID string | |
| 61 | - | for { | |
| 62 | - | var jm map[string]interface{} | |
| 63 | - | if err := decoder.Decode(&jm); err == io.EOF { | |
| 64 | - | break | |
| 65 | - | } else if err != nil { | |
| 66 | - | return "", fmt.Errorf("could not decode response: %v", err) | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | if id, exists := jm["stream"]; exists { | |
| 70 | - | if strID, ok := id.(string); ok { | |
| 71 | - | // Extract image ID from the stream. | |
| 72 | - | // This is a naive example; you may need to adjust this part | |
| 73 | - | // to correctly parse your specific Docker version's output. | |
| 74 | - | imageID = strings.TrimSpace(strID) | |
| 62 | + | for _, image := range images { | |
| 63 | + | for _, tag := range image.RepoTags { | |
| 64 | + | if tag == "alpine:latest" { | |
| 65 | + | imageID = image.ID | |
| 66 | + | break | |
| 75 | 67 | } | |
| 76 | 68 | } | |
| 69 | + | if imageID != "" { | |
| 70 | + | break | |
| 71 | + | } | |
| 77 | 72 | } | |
| 78 | 73 | ||
| 79 | 74 | if imageID == "" { | |
| 80 | - | return "", fmt.Errorf("could not find image ID in response") | |
| 75 | + | return "", fmt.Errorf("could not find the loaded image") | |
| 81 | 76 | } | |
| 82 | 77 | ||
| 83 | 78 | return imageID, nil | |
| @@ -2,11 +2,8 @@ package docker | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "encoding/json" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | - | "io" | |||
| 8 | "os" | 6 | "os" | ||
| 9 | - | "strings" | |||
| 10 | 7 | ||||
| 11 | "github.com/docker/docker/api/types" | 8 | "github.com/docker/docker/api/types" | ||
| 12 | ) | 9 | ) | ||
| @@ -35,7 +32,9 @@ func DeleteContainerImage(imageID string) error { | |||||
| 35 | return nil | 32 | return nil | ||
| 36 | } | 33 | } | ||
| 37 | 34 | ||||
| 38 | - | func LoadImage(imagePath string) (string, error) { | 35 | + | func ImportImageToEngine(imagePath string) (string, error) { |
| 36 | + | fmt.Println("Importing image to Docker engine") | |||
| 37 | + | fmt.Println(imagePath) | |||
| 39 | // Check if the Docker client has been initialized | 38 | // Check if the Docker client has been initialized | ||
| 40 | CheckIfInitialized() | 39 | CheckIfInitialized() | ||
| 41 | 40 | ||||
| @@ -46,38 +45,34 @@ func LoadImage(imagePath string) (string, error) { | |||||
| 46 | } | 45 | } | ||
| 47 | defer imageFile.Close() | 46 | defer imageFile.Close() | ||
| 48 | 47 | ||||
| 49 | - | // Load the image using the Docker client | 48 | + | // Import the image into Docker |
| 50 | - | resp, err := dockerCli.ImageLoad(context.Background(), imageFile, false) | 49 | + | _, err = dockerCli.ImageLoad(context.Background(), imageFile, false) |
| 51 | if err != nil { | 50 | if err != nil { | ||
| 52 | return "", err | 51 | return "", err | ||
| 53 | } | 52 | } | ||
| 54 | 53 | ||||
| 55 | - | defer resp.Body.Close() | 54 | + | // List all images |
| 56 | - | 55 | + | images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{}) | |
| 57 | - | // Read the JSON response to get the image ID | 56 | + | if err != nil { |
| 58 | - | decoder := json.NewDecoder(resp.Body) | 57 | + | return "", err |
| 58 | + | } | |||
| 59 | 59 | ||||
| 60 | + | // Search for the image we just loaded | |||
| 60 | var imageID string | 61 | var imageID string | ||
| 61 | - | for { | 62 | + | for _, image := range images { |
| 62 | - | var jm map[string]interface{} | 63 | + | for _, tag := range image.RepoTags { |
| 63 | - | if err := decoder.Decode(&jm); err == io.EOF { | 64 | + | if tag == "alpine:latest" { |
| 64 | - | break | 65 | + | imageID = image.ID |
| 65 | - | } else if err != nil { | 66 | + | break |
| 66 | - | return "", fmt.Errorf("could not decode response: %v", err) | |||
| 67 | - | } | |||
| 68 | - | ||||
| 69 | - | if id, exists := jm["stream"]; exists { | |||
| 70 | - | if strID, ok := id.(string); ok { | |||
| 71 | - | // Extract image ID from the stream. | |||
| 72 | - | // This is a naive example; you may need to adjust this part | |||
| 73 | - | // to correctly parse your specific Docker version's output. | |||
| 74 | - | imageID = strings.TrimSpace(strID) | |||
| 75 | } | 67 | } | ||
| 76 | } | 68 | } | ||
| 69 | + | if imageID != "" { | |||
| 70 | + | break | |||
| 71 | + | } | |||
| 77 | } | 72 | } | ||
| 78 | 73 | ||||
| 79 | if imageID == "" { | 74 | if imageID == "" { | ||
| 80 | - | return "", fmt.Errorf("could not find image ID in response") | 75 | + | return "", fmt.Errorf("could not find the loaded image") |
| 81 | } | 76 | } | ||
| 82 | 77 | ||||
| 83 | return imageID, nil | 78 | return imageID, nil | ||
M pkg/utils/store/storeimages.go +36 -7
| @@ -2,6 +2,8 @@ package store | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "io" | |
| 6 | + | "mime/multipart" | |
| 5 | 7 | "os" | |
| 6 | 8 | "path/filepath" | |
| 7 | 9 | ||
| @@ -22,20 +24,47 @@ func NewStorageConfig(config *app.AppConfig) *StorageConfig { | |||
| 22 | 24 | } | |
| 23 | 25 | } | |
| 24 | 26 | ||
| 25 | - | func (sc *StorageConfig) SaveImageToStorage(imagePath string) (string, error) { | |
| 27 | + | func SaveImageToStorage(config *app.AppConfig, filename string, file multipart.File) (string, error) { | |
| 28 | + | // Check if the folder exist if not create it | |
| 29 | + | if _, err := os.Stat(config.General.StorageDir); os.IsNotExist(err) { | |
| 30 | + | err := os.MkdirAll(config.General.StorageDir, 0755) | |
| 31 | + | if err != nil { | |
| 32 | + | return "", fmt.Errorf("failed to create storage directory: %v", err) | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // Define the path where the image will be saved | |
| 37 | + | saveInPath := filepath.Join(config.General.StorageDir, filename) | |
| 26 | 38 | ||
| 27 | - | imageId, err := docker.LoadImage(imagePath) | |
| 39 | + | // Create or open a file for appending. | |
| 40 | + | outFile, err := os.Create(saveInPath) | |
| 28 | 41 | if err != nil { | |
| 29 | - | return "", fmt.Errorf("failed to load image: %v", err) | |
| 42 | + | return "", fmt.Errorf("failed to open file: %v", err) | |
| 30 | 43 | } | |
| 44 | + | defer outFile.Close() | |
| 31 | 45 | ||
| 32 | - | // Generate the new path for the image in the storage directory | |
| 33 | - | newPath := filepath.Join(sc.StorageDir, imageId+".tar") | |
| 46 | + | // Write the uploaded file's content to the outFile | |
| 47 | + | _, err = io.Copy(outFile, file) | |
| 48 | + | if err != nil { | |
| 49 | + | return "", fmt.Errorf("failed to write file: %v", err) | |
| 50 | + | } | |
| 34 | 51 | ||
| 35 | - | err = os.Rename(imagePath, newPath) | |
| 52 | + | // Import the image into Docker | |
| 53 | + | imageId, err := docker.ImportImageToEngine(saveInPath) | |
| 36 | 54 | if err != nil { | |
| 37 | - | return "", fmt.Errorf("failed to move image to storage directory: %v", err) | |
| 55 | + | return "", fmt.Errorf("failed to save image to storage directory: %v", err) | |
| 38 | 56 | } | |
| 39 | 57 | ||
| 40 | 58 | return imageId, nil | |
| 41 | 59 | } | |
| 60 | + | ||
| 61 | + | func (sc *StorageConfig) DeleteImageFromStorage(imageId string) error { | |
| 62 | + | // Delete the image from the storage directory | |
| 63 | + | imagePath := filepath.Join(sc.StorageDir, imageId+".tar") | |
| 64 | + | err := os.Remove(imagePath) | |
| 65 | + | if err != nil { | |
| 66 | + | return fmt.Errorf("failed to delete image from storage directory: %v", err) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | return nil | |
| 70 | + | } | |
| @@ -2,6 +2,8 @@ package store | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | + | "io" | |||
| 6 | + | "mime/multipart" | |||
| 5 | "os" | 7 | "os" | ||
| 6 | "path/filepath" | 8 | "path/filepath" | ||
| 7 | 9 | ||||
| @@ -22,20 +24,47 @@ func NewStorageConfig(config *app.AppConfig) *StorageConfig { | |||||
| 22 | } | 24 | } | ||
| 23 | } | 25 | } | ||
| 24 | 26 | ||||
| 25 | - | func (sc *StorageConfig) SaveImageToStorage(imagePath string) (string, error) { | 27 | + | func SaveImageToStorage(config *app.AppConfig, filename string, file multipart.File) (string, error) { |
| 28 | + | // Check if the folder exist if not create it | |||
| 29 | + | if _, err := os.Stat(config.General.StorageDir); os.IsNotExist(err) { | |||
| 30 | + | err := os.MkdirAll(config.General.StorageDir, 0755) | |||
| 31 | + | if err != nil { | |||
| 32 | + | return "", fmt.Errorf("failed to create storage directory: %v", err) | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // Define the path where the image will be saved | |||
| 37 | + | saveInPath := filepath.Join(config.General.StorageDir, filename) | |||
| 26 | 38 | ||||
| 27 | - | imageId, err := docker.LoadImage(imagePath) | 39 | + | // Create or open a file for appending. |
| 40 | + | outFile, err := os.Create(saveInPath) | |||
| 28 | if err != nil { | 41 | if err != nil { | ||
| 29 | - | return "", fmt.Errorf("failed to load image: %v", err) | 42 | + | return "", fmt.Errorf("failed to open file: %v", err) |
| 30 | } | 43 | } | ||
| 44 | + | defer outFile.Close() | |||
| 31 | 45 | ||||
| 32 | - | // Generate the new path for the image in the storage directory | 46 | + | // Write the uploaded file's content to the outFile |
| 33 | - | newPath := filepath.Join(sc.StorageDir, imageId+".tar") | 47 | + | _, err = io.Copy(outFile, file) |
| 48 | + | if err != nil { | |||
| 49 | + | return "", fmt.Errorf("failed to write file: %v", err) | |||
| 50 | + | } | |||
| 34 | 51 | ||||
| 35 | - | err = os.Rename(imagePath, newPath) | 52 | + | // Import the image into Docker |
| 53 | + | imageId, err := docker.ImportImageToEngine(saveInPath) | |||
| 36 | if err != nil { | 54 | if err != nil { | ||
| 37 | - | return "", fmt.Errorf("failed to move image to storage directory: %v", err) | 55 | + | return "", fmt.Errorf("failed to save image to storage directory: %v", err) |
| 38 | } | 56 | } | ||
| 39 | 57 | ||||
| 40 | return imageId, nil | 58 | return imageId, nil | ||
| 41 | } | 59 | } | ||
| 60 | + | ||||
| 61 | + | func (sc *StorageConfig) DeleteImageFromStorage(imageId string) error { | |||
| 62 | + | // Delete the image from the storage directory | |||
| 63 | + | imagePath := filepath.Join(sc.StorageDir, imageId+".tar") | |||
| 64 | + | err := os.Remove(imagePath) | |||
| 65 | + | if err != nil { | |||
| 66 | + | return fmt.Errorf("failed to delete image from storage directory: %v", err) | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | return nil | |||
| 70 | + | } | |||