detached 01250ef

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) {
2323
2424 func main() {
2525 a := app.NewApp()
26- fmt.Println(a)
27-
2826 // Initialize database
2927 memDb, err := app.InitializeDB(a)
3028 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 database26 // 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 @@
11 <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>
316 <table class="p-4 border-t-2">
417 <!-- <thead>
518 <tr>
@@ -49,4 +62,5 @@
4962 }}
5063 </tbody>
5164 </table>
65+ <div id="upload-image"></div>
5266 </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 @@
11 <!-- 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 @@
11 package handler
22
33 import (
4- "io"
4+ "fmt"
55 "net/http"
6- "os"
7- "strings"
86
97 "github.com/bnema/gordon/internal/app"
8+ "github.com/bnema/gordon/internal/gotemplate/render"
9+ "github.com/bnema/gordon/pkg/utils/store"
1010 "github.com/labstack/echo/v4"
1111 )
1212
1313 const MaxUploadSize = 1024 * 1024 * 1024 // 1GB
1414
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 {
1735 // Set upload size limit
1836 c.Request().Body = http.MaxBytesReader(c.Response(), c.Request().Body, MaxUploadSize)
1937 if err := c.Request().ParseMultipartForm(MaxUploadSize); err != nil {
2038 return c.String(http.StatusRequestEntityTooLarge, "File size exceeded")
2139 }
22-
2340 // Get the file from the form
24- file, _, err := c.Request().FormFile("file")
41+ file, header, err := c.Request().FormFile("file")
2542 if err != nil {
2643 return sendError(c, err)
2744 }
2845 defer file.Close()
2946
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)
4250
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)
4553 if err != nil {
4654 return sendError(c, err)
4755 }
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)
5858 return c.HTML(http.StatusOK, "Success")
5959 }
@@ -1,59 +1,59 @@
1 package handler1 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 // 1GB13 const MaxUploadSize = 1024 * 1024 * 1024 // 1GB
14 14
15-// UploadImageHandler handles the /upload-image route15+// 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 limit35 // 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 form40 // 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 type47+ // 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 file51+ // 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) {
7373 e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error {
7474 return handler.ImageManagerDeleteHandler(c, a)
7575 })
76-
7776 e.GET("/htmx/container-manager", func(c echo.Context) error {
7877 return handler.ContainerManagerHandler(c, a)
7978 })
8079 e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error {
8180 return handler.ContainerManagerStopHandler(c, a)
8281 })
83-
8482 e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error {
8583 return handler.ContainerManagerDeleteHandler(c, a)
8684 })
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+ })
8791 }
@@ -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 {
600600 margin-top: 1rem;
601601 }
602602
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+
603615 .flex {
604616 display: flex;
605617 }
@@ -709,6 +721,10 @@ video {
709721 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
710722 }
711723
724+.rounded {
725+ border-radius: 0.25rem;
726+}
727+
712728 .rounded-lg {
713729 border-radius: 0.5rem;
714730 }
@@ -734,6 +750,11 @@ video {
734750 border-color: rgb(209 213 219 / var(--tw-border-opacity));
735751 }
736752
753+.bg-blue-500 {
754+ --tw-bg-opacity: 1;
755+ background-color: rgb(59 130 246 / var(--tw-bg-opacity));
756+}
757+
737758 .bg-gordonmascot-hblue {
738759 --tw-bg-opacity: 1;
739760 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -790,6 +811,23 @@ video {
790811 padding-bottom: 0.5rem;
791812 }
792813
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+
793831 .text-2xl {
794832 font-size: 1.5rem;
795833 line-height: 2rem;
@@ -872,6 +910,11 @@ video {
872910 color: rgb(255 255 255 / var(--tw-text-opacity));
873911 }
874912
913+.text-black {
914+ --tw-text-opacity: 1;
915+ color: rgb(0 0 0 / var(--tw-text-opacity));
916+}
917+
875918 .underline {
876919 text-decoration-line: underline;
877920 }
@@ -1007,6 +1050,11 @@ video {
10071050 margin-right: 8px;
10081051 }
10091052
1053+.hover\:bg-blue-700:hover {
1054+ --tw-bg-opacity: 1;
1055+ background-color: rgb(29 78 216 / var(--tw-bg-opacity));
1056+}
1057+
10101058 @media (min-width: 640px) {
10111059 .sm\:h-24 {
10121060 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
22
33 import (
44 "context"
5- "encoding/json"
65 "fmt"
7- "io"
86 "os"
9- "strings"
107
118 "github.com/docker/docker/api/types"
129 )
@@ -35,7 +32,9 @@ func DeleteContainerImage(imageID string) error {
3532 return nil
3633 }
3734
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)
3938 // Check if the Docker client has been initialized
4039 CheckIfInitialized()
4140
@@ -46,38 +45,34 @@ func LoadImage(imagePath string) (string, error) {
4645 }
4746 defer imageFile.Close()
4847
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)
5150 if err != nil {
5251 return "", err
5352 }
5453
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+ }
5959
60+ // Search for the image we just loaded
6061 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
7567 }
7668 }
69+ if imageID != "" {
70+ break
71+ }
7772 }
7873
7974 if imageID == "" {
80- return "", fmt.Errorf("could not find image ID in response")
75+ return "", fmt.Errorf("could not find the loaded image")
8176 }
8277
8378 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 nil32 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 initialized38 // 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 client48+ // 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 "", err51 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 ID56+ 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 string61 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- break65+ 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, nil78 return imageID, nil
M pkg/utils/store/storeimages.go +36 -7
@@ -2,6 +2,8 @@ package store
22
33 import (
44 "fmt"
5+ "io"
6+ "mime/multipart"
57 "os"
68 "path/filepath"
79
@@ -22,20 +24,47 @@ func NewStorageConfig(config *app.AppConfig) *StorageConfig {
2224 }
2325 }
2426
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)
2638
27- imageId, err := docker.LoadImage(imagePath)
39+ // Create or open a file for appending.
40+ outFile, err := os.Create(saveInPath)
2841 if err != nil {
29- return "", fmt.Errorf("failed to load image: %v", err)
42+ return "", fmt.Errorf("failed to open file: %v", err)
3043 }
44+ defer outFile.Close()
3145
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+ }
3451
35- err = os.Rename(imagePath, newPath)
52+ // Import the image into Docker
53+ imageId, err := docker.ImportImageToEngine(saveInPath)
3654 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)
3856 }
3957
4058 return imageId, nil
4159 }
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 directory46+ // 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, nil58 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close