detached cccb252

refactor: improve code quality and fix linting issues

commit
cccb2521db27b3e00ad73ab12e3b177ba6cff0ac
author
Brice · · 2024-11-18 08:19 UTC
parent
9f77440
signature
unsigned
15 files · +316 -294
M .golangci.yml +0 -1
@@ -15,7 +15,6 @@ linters:
1515 - misspell
1616 - gosec
1717 - unconvert
18- - deadcode
1918 - gocyclo
2019
2120 linters-settings:
@@ -15,7 +15,6 @@ linters:
15 - misspell15 - misspell
16 - gosec16 - gosec
17 - unconvert17 - unconvert
18- - deadcode
19 - gocyclo18 - gocyclo
20 19
21 linters-settings:20 linters-settings:
M internal/cli/cmd/deploy.go +12 -27
@@ -86,8 +86,12 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
8686 return fmt.Errorf("conflict check failed: %w", err)
8787 }
8888
89- // Shorten the ContainerID to 12 characters
90- shortID := conflictResp.ContainerID[:12]
89+ var shortID string
90+ if len(conflictResp.ContainerID) >= 12 {
91+ shortID = conflictResp.ContainerID[:12]
92+ } else {
93+ shortID = conflictResp.ContainerID // Use full ID if less than 12 chars
94+ }
9195
9296 // If there's a conflict, handle it before proceeding
9397 if !conflictResp.Success && conflictResp.ContainerID != "" {
@@ -141,7 +145,9 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
141145 if deployResponse.ContainerID != "" {
142146 log.Warn("Container already exists")
143147 // Retry the deployment after handling the conflict
144- imageReader.Seek(0, 0) // Reset reader to beginning
148+ if _, err := imageReader.Seek(0, 0); err != nil {
149+ return fmt.Errorf("failed to reset image reader: %w", err)
150+ }
145151 resp, err = chunkedClient.SendFileAsChunks(ctx, "/deploy", headers, imageReader, size, imageName)
146152 if err != nil {
147153 return fmt.Errorf("deployment failed after container removal: %w", err)
@@ -207,7 +213,9 @@ func validateInputs(imageName, port, targetDomain string) error {
207213 return err
208214 }
209215
210- handler.EnsureImageTag(&imageName)
216+ if err := handler.EnsureImageTag(&imageName); err != nil {
217+ return fmt.Errorf("failed to ensure image tag: %w", err)
218+ }
211219
212220 if port == "" {
213221 return fmt.Errorf("port is required")
@@ -216,29 +224,6 @@ func validateInputs(imageName, port, targetDomain string) error {
216224 return handler.ValidateTargetDomain(targetDomain)
217225 }
218226
219-func handleDeployError(a *cli.App, err error, imageName, port, targetDomain string, reader io.Reader) error {
220- var deployErr *common.DeploymentError
221- if errors.As(err, &deployErr) {
222- var deployResponse common.DeployResponse
223- var conflictResp common.ConflictCheckResponse
224- if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil {
225- if deployErr.StatusCode == http.StatusConflict && deployResponse.ContainerID != "" {
226- if err := handler.HandleExistingContainer(a, &conflictResp); err != nil {
227- if strings.Contains(err.Error(), "cancelled by user") {
228- log.Warn("Deployment cancelled by user")
229- return nil
230- }
231- log.Error("Failed to handle existing container", "error", err)
232- return err
233- }
234- // Retry deployment
235- return deployImage(a, reader, imageName, port, targetDomain)
236- }
237- }
238- }
239- return err
240-}
241-
242227 func checkDeployConflict(a *cli.App, targetDomain string, port string) (*common.ConflictCheckResponse, error) {
243228
244229 // Create request to check-conflict endpoint with both domain and port parameters
@@ -86,8 +86,12 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
86 return fmt.Errorf("conflict check failed: %w", err)86 return fmt.Errorf("conflict check failed: %w", err)
87 }87 }
88 88
89- // Shorten the ContainerID to 12 characters89+ var shortID string
90- shortID := conflictResp.ContainerID[:12]90+ if len(conflictResp.ContainerID) >= 12 {
91+ shortID = conflictResp.ContainerID[:12]
92+ } else {
93+ shortID = conflictResp.ContainerID // Use full ID if less than 12 chars
94+ }
91 95
92 // If there's a conflict, handle it before proceeding96 // If there's a conflict, handle it before proceeding
93 if !conflictResp.Success && conflictResp.ContainerID != "" {97 if !conflictResp.Success && conflictResp.ContainerID != "" {
@@ -141,7 +145,9 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
141 if deployResponse.ContainerID != "" {145 if deployResponse.ContainerID != "" {
142 log.Warn("Container already exists")146 log.Warn("Container already exists")
143 // Retry the deployment after handling the conflict147 // Retry the deployment after handling the conflict
144- imageReader.Seek(0, 0) // Reset reader to beginning148+ if _, err := imageReader.Seek(0, 0); err != nil {
149+ return fmt.Errorf("failed to reset image reader: %w", err)
150+ }
145 resp, err = chunkedClient.SendFileAsChunks(ctx, "/deploy", headers, imageReader, size, imageName)151 resp, err = chunkedClient.SendFileAsChunks(ctx, "/deploy", headers, imageReader, size, imageName)
146 if err != nil {152 if err != nil {
147 return fmt.Errorf("deployment failed after container removal: %w", err)153 return fmt.Errorf("deployment failed after container removal: %w", err)
@@ -207,7 +213,9 @@ func validateInputs(imageName, port, targetDomain string) error {
207 return err213 return err
208 }214 }
209 215
210- handler.EnsureImageTag(&imageName)216+ if err := handler.EnsureImageTag(&imageName); err != nil {
217+ return fmt.Errorf("failed to ensure image tag: %w", err)
218+ }
211 219
212 if port == "" {220 if port == "" {
213 return fmt.Errorf("port is required")221 return fmt.Errorf("port is required")
@@ -216,29 +224,6 @@ func validateInputs(imageName, port, targetDomain string) error {
216 return handler.ValidateTargetDomain(targetDomain)224 return handler.ValidateTargetDomain(targetDomain)
217 }225 }
218 226
219-func handleDeployError(a *cli.App, err error, imageName, port, targetDomain string, reader io.Reader) error {
220- var deployErr *common.DeploymentError
221- if errors.As(err, &deployErr) {
222- var deployResponse common.DeployResponse
223- var conflictResp common.ConflictCheckResponse
224- if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil {
225- if deployErr.StatusCode == http.StatusConflict && deployResponse.ContainerID != "" {
226- if err := handler.HandleExistingContainer(a, &conflictResp); err != nil {
227- if strings.Contains(err.Error(), "cancelled by user") {
228- log.Warn("Deployment cancelled by user")
229- return nil
230- }
231- log.Error("Failed to handle existing container", "error", err)
232- return err
233- }
234- // Retry deployment
235- return deployImage(a, reader, imageName, port, targetDomain)
236- }
237- }
238- }
239- return err
240-}
241-
242 func checkDeployConflict(a *cli.App, targetDomain string, port string) (*common.ConflictCheckResponse, error) {227 func checkDeployConflict(a *cli.App, targetDomain string, port string) (*common.ConflictCheckResponse, error) {
243 228
244 // Create request to check-conflict endpoint with both domain and port parameters229 // Create request to check-conflict endpoint with both domain and port parameters
M internal/cli/handler/chunked_transfer.go +15 -22
@@ -38,14 +38,13 @@ type UploadProgress struct {
3838
3939 // ProgressDisplay manages the upload progress display
4040 type ProgressDisplay struct {
41- mu sync.Mutex
42- model *uploadModel
43- program *tea.Program
44- updateChan chan UploadProgress
45- done chan struct{}
46- interrupted bool
47- paused bool
48- pauseChan chan bool
41+ mu sync.Mutex
42+ model *uploadModel
43+ program *tea.Program
44+ updateChan chan UploadProgress
45+ done chan struct{}
46+ paused bool
47+ pauseChan chan bool
4948 }
5049
5150 type uploadModel struct {
@@ -80,7 +79,7 @@ func (pd *ProgressDisplay) Start() {
8079
8180 // Run the UI in a goroutine
8281 go func() {
83- if err := pd.program.Start(); err != nil {
82+ if _, err := pd.program.Run(); err != nil {
8483 fmt.Printf("Error running progress display: %v\n", err)
8584 }
8685 }()
@@ -199,14 +198,13 @@ func (m *uploadModel) View() string {
199198 type ChunkMetadata = common.ChunkMetadata
200199
201200 type ChunkedClient struct {
202- client *http.Client
203- baseURL string
204- token string
205- chunkSize int64
206- app *cli.App
207- progress *ProgressDisplay
208- cancelFunc context.CancelFunc
209- conflictAction chan common.ConflictAction
201+ client *http.Client
202+ baseURL string
203+ token string
204+ chunkSize int64
205+ app *cli.App
206+ progress *ProgressDisplay
207+ cancelFunc context.CancelFunc
210208 }
211209
212210 type transferStore struct {
@@ -562,11 +560,6 @@ func verifyChunkIntegrity(chunk []byte, metadata common.ChunkMetadata) error {
562560 return nil
563561 }
564562
565-// isAuthError checks if a response is an authentication error
566-func isAuthError(resp *Response) bool {
567- return resp != nil && resp.StatusCode == http.StatusUnauthorized
568-}
569-
570563 // cleanupClientTransfer cleans up a client transfer
571564 func cleanupClientTransfer(transferID string) {
572565 clientChunkStore.mu.Lock()
@@ -38,14 +38,13 @@ type UploadProgress struct {
38 38
39 // ProgressDisplay manages the upload progress display39 // ProgressDisplay manages the upload progress display
40 type ProgressDisplay struct {40 type ProgressDisplay struct {
41- mu sync.Mutex41+ mu sync.Mutex
42- model *uploadModel42+ model *uploadModel
43- program *tea.Program43+ program *tea.Program
44- updateChan chan UploadProgress44+ updateChan chan UploadProgress
45- done chan struct{}45+ done chan struct{}
46- interrupted bool46+ paused bool
47- paused bool47+ pauseChan chan bool
48- pauseChan chan bool
49 }48 }
50 49
51 type uploadModel struct {50 type uploadModel struct {
@@ -80,7 +79,7 @@ func (pd *ProgressDisplay) Start() {
80 79
81 // Run the UI in a goroutine80 // Run the UI in a goroutine
82 go func() {81 go func() {
83- if err := pd.program.Start(); err != nil {82+ if _, err := pd.program.Run(); err != nil {
84 fmt.Printf("Error running progress display: %v\n", err)83 fmt.Printf("Error running progress display: %v\n", err)
85 }84 }
86 }()85 }()
@@ -199,14 +198,13 @@ func (m *uploadModel) View() string {
199 type ChunkMetadata = common.ChunkMetadata198 type ChunkMetadata = common.ChunkMetadata
200 199
201 type ChunkedClient struct {200 type ChunkedClient struct {
202- client *http.Client201+ client *http.Client
203- baseURL string202+ baseURL string
204- token string203+ token string
205- chunkSize int64204+ chunkSize int64
206- app *cli.App205+ app *cli.App
207- progress *ProgressDisplay206+ progress *ProgressDisplay
208- cancelFunc context.CancelFunc207+ cancelFunc context.CancelFunc
209- conflictAction chan common.ConflictAction
210 }208 }
211 209
212 type transferStore struct {210 type transferStore struct {
@@ -562,11 +560,6 @@ func verifyChunkIntegrity(chunk []byte, metadata common.ChunkMetadata) error {
562 return nil560 return nil
563 }561 }
564 562
565-// isAuthError checks if a response is an authentication error
566-func isAuthError(resp *Response) bool {
567- return resp != nil && resp.StatusCode == http.StatusUnauthorized
568-}
569-
570 // cleanupClientTransfer cleans up a client transfer563 // cleanupClientTransfer cleans up a client transfer
571 func cleanupClientTransfer(transferID string) {564 func cleanupClientTransfer(transferID string) {
572 clientChunkStore.mu.Lock()565 clientChunkStore.mu.Lock()
M internal/cli/handler/http.go +1 -1
@@ -147,7 +147,7 @@ func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, t
147147 }
148148
149149 // Set the authorization header for all request types
150- req.Header.Set("Authorization", "Bearer "+token)
150+ setAuthRequestHeaders(req, token)
151151
152152 return req, nil
153153 }
@@ -147,7 +147,7 @@ func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, t
147 }147 }
148 148
149 // Set the authorization header for all request types149 // Set the authorization header for all request types
150- req.Header.Set("Authorization", "Bearer "+token)150+ setAuthRequestHeaders(req, token)
151 151
152 return req, nil152 return req, nil
153 }153 }
M internal/common/inputs.go +65 -4
@@ -1,11 +1,72 @@
11 package common
22
3-import "fmt"
3+import (
4+ "bufio"
5+ "fmt"
6+ "log"
7+ "os"
8+ "strings"
9+)
410
511 // ReadUserInput reads a string input from the user with a prompt.
12+// It handles multi-word inputs and properly checks for errors.
613 func ReadUserInput(prompt string) string {
7- fmt.Println(prompt)
8- var input string
9- fmt.Scanln(&input)
14+ fmt.Print(prompt + " ")
15+
16+ // Use bufio.NewReader for better input handling
17+ reader := bufio.NewReader(os.Stdin)
18+
19+ // ReadString reads until newline, handling spaces in input
20+ input, err := reader.ReadString('\n')
21+ if err != nil {
22+ log.Printf("Error reading input: %v", err)
23+ return ""
24+ }
25+
26+ // Trim spaces and newline characters
27+ return strings.TrimSpace(input)
28+}
29+
30+// ReadUserInputWithValidation reads user input and validates it using a provided function.
31+// It keeps prompting until valid input is received.
32+func ReadUserInputWithValidation(prompt string, validate func(string) bool) string {
33+ for {
34+ input := ReadUserInput(prompt)
35+ if validate(input) {
36+ return input
37+ }
38+ fmt.Println("Invalid input. Please try again.")
39+ }
40+}
41+
42+// ReadUserInputWithDefault reads user input with a default value if input is empty.
43+func ReadUserInputWithDefault(prompt string, defaultValue string) string {
44+ fmt.Printf("%s (default: %s) ", prompt, defaultValue)
45+
46+ reader := bufio.NewReader(os.Stdin)
47+ input, err := reader.ReadString('\n')
48+ if err != nil {
49+ log.Printf("Error reading input: %v", err)
50+ return defaultValue
51+ }
52+
53+ input = strings.TrimSpace(input)
54+ if input == "" {
55+ return defaultValue
56+ }
1057 return input
1158 }
59+
60+// ReadConfirmation reads a yes/no confirmation from the user.
61+func ReadConfirmation(prompt string) bool {
62+ input := ReadUserInputWithValidation(
63+ prompt+" (y/n)",
64+ func(s string) bool {
65+ s = strings.ToLower(s)
66+ return s == "y" || s == "n" || s == "yes" || s == "no"
67+ },
68+ )
69+
70+ input = strings.ToLower(input)
71+ return input == "y" || input == "yes"
72+}
@@ -1,11 +1,72 @@
1 package common1 package common
2 2
3-import "fmt"3+import (
4+ "bufio"
5+ "fmt"
6+ "log"
7+ "os"
8+ "strings"
9+)
4 10
5 // ReadUserInput reads a string input from the user with a prompt.11 // ReadUserInput reads a string input from the user with a prompt.
12+// It handles multi-word inputs and properly checks for errors.
6 func ReadUserInput(prompt string) string {13 func ReadUserInput(prompt string) string {
7- fmt.Println(prompt)14+ fmt.Print(prompt + " ")
8- var input string15+
9- fmt.Scanln(&input)16+ // Use bufio.NewReader for better input handling
17+ reader := bufio.NewReader(os.Stdin)
18+
19+ // ReadString reads until newline, handling spaces in input
20+ input, err := reader.ReadString('\n')
21+ if err != nil {
22+ log.Printf("Error reading input: %v", err)
23+ return ""
24+ }
25+
26+ // Trim spaces and newline characters
27+ return strings.TrimSpace(input)
28+}
29+
30+// ReadUserInputWithValidation reads user input and validates it using a provided function.
31+// It keeps prompting until valid input is received.
32+func ReadUserInputWithValidation(prompt string, validate func(string) bool) string {
33+ for {
34+ input := ReadUserInput(prompt)
35+ if validate(input) {
36+ return input
37+ }
38+ fmt.Println("Invalid input. Please try again.")
39+ }
40+}
41+
42+// ReadUserInputWithDefault reads user input with a default value if input is empty.
43+func ReadUserInputWithDefault(prompt string, defaultValue string) string {
44+ fmt.Printf("%s (default: %s) ", prompt, defaultValue)
45+
46+ reader := bufio.NewReader(os.Stdin)
47+ input, err := reader.ReadString('\n')
48+ if err != nil {
49+ log.Printf("Error reading input: %v", err)
50+ return defaultValue
51+ }
52+
53+ input = strings.TrimSpace(input)
54+ if input == "" {
55+ return defaultValue
56+ }
10 return input57 return input
11 }58 }
59+
60+// ReadConfirmation reads a yes/no confirmation from the user.
61+func ReadConfirmation(prompt string) bool {
62+ input := ReadUserInputWithValidation(
63+ prompt+" (y/n)",
64+ func(s string) bool {
65+ s = strings.ToLower(s)
66+ return s == "y" || s == "n" || s == "yes" || s == "no"
67+ },
68+ )
69+
70+ input = strings.ToLower(input)
71+ return input == "y" || input == "yes"
72+}
M internal/httpserve/handlers/endpoint_deploy.go +1 -1
@@ -30,7 +30,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
3030 })
3131 }
3232
33- _, err = saveAndImportDeployImage(c, a, payload)
33+ _, err = saveAndImportImage(c, a, payload)
3434 if err != nil {
3535 // Debug
3636 log.Error("Failed to save and import image", "error", err)
@@ -30,7 +30,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
30 })30 })
31 }31 }
32 32
33- _, err = saveAndImportDeployImage(c, a, payload)33+ _, err = saveAndImportImage(c, a, payload)
34 if err != nil {34 if err != nil {
35 // Debug35 // Debug
36 log.Error("Failed to save and import image", "error", err)36 log.Error("Failed to save and import image", "error", err)
M internal/httpserve/handlers/endpoint_helpers.go +29 -52
@@ -5,7 +5,6 @@ import (
55 "errors"
66 "fmt"
77 "net/http"
8- "os"
98 "regexp"
109 "sync"
1110 "time"
@@ -132,66 +131,59 @@ func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, err
132131 return payload, nil
133132 }
134133
135-// saveAndImportPushImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image
136-func saveAndImportPushImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) {
137- imageReader := c.Request().Body
138- defer imageReader.Close()
139-
140- imageFileName := sanitizeImageFileName(payload.ImageName)
141-
142- imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
143- if err != nil {
144- return "", fmt.Errorf("failed to save image: %v", err)
145- }
146-
147- imageID, err := docker.ImportImageToEngine(imagePath)
148- if err != nil {
149- store.RemoveFromStorage(imagePath)
150- return "", fmt.Errorf("failed to import image: %v", err)
134+// ssaveAndImportImage saves and imports an image from the request body
135+func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (string, error) {
136+ var imageName, imageID string
137+
138+ // Determine payload type and extract image name
139+ switch p := payload.(type) {
140+ case *common.PushPayload:
141+ imageName = p.ImageName
142+ defer func() { p.ImageID = imageID }()
143+ case *common.DeployPayload:
144+ imageName = p.ImageName
145+ defer func() { p.ImageID = imageID }()
146+ default:
147+ return "", fmt.Errorf("unsupported payload type: %T", payload)
151148 }
152149
153- if imageID == "" {
154- return "", errors.New("imported image ID is empty")
155- }
156-
157- err = store.RemoveFromStorage(imagePath)
158- if err != nil {
159- return "", fmt.Errorf("failed to remove temporary image file: %v", err)
160- }
161-
162- payload.ImageID = imageID
163- return imagePath, nil
164-}
165-
166-// saveAndImportDeployImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image
167-func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) {
168-
169150 imageReader := c.Request().Body
170151 defer imageReader.Close()
171152
172- imageFileName := sanitizeImageFileName(payload.ImageName)
153+ imageFileName := sanitizeImageFileName(imageName)
173154
174155 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
175156 if err != nil {
176157 return "", fmt.Errorf("failed to save image: %v", err)
177158 }
178159
179- imageID, err := docker.ImportImageToEngine(imagePath)
160+ imageID, err = docker.ImportImageToEngine(imagePath)
180161 if err != nil {
181- store.RemoveFromStorage(imagePath)
162+ if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil {
163+ log.Error("Failed to remove temporary image file after import failure",
164+ "error", removeErr,
165+ "path", imagePath)
166+ }
182167 return "", fmt.Errorf("failed to import image: %v", err)
183168 }
184169
185170 if imageID == "" {
171+ if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil {
172+ log.Error("Failed to remove temporary image file after empty ID",
173+ "error", removeErr,
174+ "path", imagePath)
175+ }
186176 return "", errors.New("imported image ID is empty")
187177 }
188178
189179 err = store.RemoveFromStorage(imagePath)
190180 if err != nil {
181+ log.Error("Failed to remove temporary image file after successful import",
182+ "error", err,
183+ "path", imagePath)
191184 return "", fmt.Errorf("failed to remove temporary image file: %v", err)
192185 }
193186
194- payload.ImageID = imageID
195187 return imagePath, nil
196188 }
197189
@@ -357,21 +349,6 @@ func extractContainerID(errorMessage string) string {
357349 return ""
358350 }
359351
360-// verifyFileContents checks if the file size matches the expected size
361-func verifyFileContents(file *os.File, expectedSize int64) error {
362- actualSize, err := file.Seek(0, 2) // Seek to end
363- if err != nil {
364- return fmt.Errorf("failed to verify file size: %w", err)
365- }
366-
367- if actualSize != expectedSize {
368- return fmt.Errorf("final file size mismatch: got %d, expected %d", actualSize, expectedSize)
369- }
370-
371- _, err = file.Seek(0, 0) // Reset to beginning
372- return err
373-}
374-
375352 // storeIDMapping stores a mapping between a short ID and a full ID
376353 func storeIDMapping(shortID, fullID string) {
377354 safelyInteractWithIDMap(Update, shortID, fullID)
@@ -5,7 +5,6 @@ import (
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7 "net/http"7 "net/http"
8- "os"
9 "regexp"8 "regexp"
10 "sync"9 "sync"
11 "time"10 "time"
@@ -132,66 +131,59 @@ func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, err
132 return payload, nil131 return payload, nil
133 }132 }
134 133
135-// saveAndImportPushImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image134+// ssaveAndImportImage saves and imports an image from the request body
136-func saveAndImportPushImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) {135+func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (string, error) {
137- imageReader := c.Request().Body136+ var imageName, imageID string
138- defer imageReader.Close()137+
139-138+ // Determine payload type and extract image name
140- imageFileName := sanitizeImageFileName(payload.ImageName)139+ switch p := payload.(type) {
141-140+ case *common.PushPayload:
142- imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)141+ imageName = p.ImageName
143- if err != nil {142+ defer func() { p.ImageID = imageID }()
144- return "", fmt.Errorf("failed to save image: %v", err)143+ case *common.DeployPayload:
145- }144+ imageName = p.ImageName
146-145+ defer func() { p.ImageID = imageID }()
147- imageID, err := docker.ImportImageToEngine(imagePath)146+ default:
148- if err != nil {147+ return "", fmt.Errorf("unsupported payload type: %T", payload)
149- store.RemoveFromStorage(imagePath)
150- return "", fmt.Errorf("failed to import image: %v", err)
151 }148 }
152 149
153- if imageID == "" {
154- return "", errors.New("imported image ID is empty")
155- }
156-
157- err = store.RemoveFromStorage(imagePath)
158- if err != nil {
159- return "", fmt.Errorf("failed to remove temporary image file: %v", err)
160- }
161-
162- payload.ImageID = imageID
163- return imagePath, nil
164-}
165-
166-// saveAndImportDeployImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image
167-func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) {
168-
169 imageReader := c.Request().Body150 imageReader := c.Request().Body
170 defer imageReader.Close()151 defer imageReader.Close()
171 152
172- imageFileName := sanitizeImageFileName(payload.ImageName)153+ imageFileName := sanitizeImageFileName(imageName)
173 154
174 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)155 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
175 if err != nil {156 if err != nil {
176 return "", fmt.Errorf("failed to save image: %v", err)157 return "", fmt.Errorf("failed to save image: %v", err)
177 }158 }
178 159
179- imageID, err := docker.ImportImageToEngine(imagePath)160+ imageID, err = docker.ImportImageToEngine(imagePath)
180 if err != nil {161 if err != nil {
181- store.RemoveFromStorage(imagePath)162+ if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil {
163+ log.Error("Failed to remove temporary image file after import failure",
164+ "error", removeErr,
165+ "path", imagePath)
166+ }
182 return "", fmt.Errorf("failed to import image: %v", err)167 return "", fmt.Errorf("failed to import image: %v", err)
183 }168 }
184 169
185 if imageID == "" {170 if imageID == "" {
171+ if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil {
172+ log.Error("Failed to remove temporary image file after empty ID",
173+ "error", removeErr,
174+ "path", imagePath)
175+ }
186 return "", errors.New("imported image ID is empty")176 return "", errors.New("imported image ID is empty")
187 }177 }
188 178
189 err = store.RemoveFromStorage(imagePath)179 err = store.RemoveFromStorage(imagePath)
190 if err != nil {180 if err != nil {
181+ log.Error("Failed to remove temporary image file after successful import",
182+ "error", err,
183+ "path", imagePath)
191 return "", fmt.Errorf("failed to remove temporary image file: %v", err)184 return "", fmt.Errorf("failed to remove temporary image file: %v", err)
192 }185 }
193 186
194- payload.ImageID = imageID
195 return imagePath, nil187 return imagePath, nil
196 }188 }
197 189
@@ -357,21 +349,6 @@ func extractContainerID(errorMessage string) string {
357 return ""349 return ""
358 }350 }
359 351
360-// verifyFileContents checks if the file size matches the expected size
361-func verifyFileContents(file *os.File, expectedSize int64) error {
362- actualSize, err := file.Seek(0, 2) // Seek to end
363- if err != nil {
364- return fmt.Errorf("failed to verify file size: %w", err)
365- }
366-
367- if actualSize != expectedSize {
368- return fmt.Errorf("final file size mismatch: got %d, expected %d", actualSize, expectedSize)
369- }
370-
371- _, err = file.Seek(0, 0) // Reset to beginning
372- return err
373-}
374-
375 // storeIDMapping stores a mapping between a short ID and a full ID352 // storeIDMapping stores a mapping between a short ID and a full ID
376 func storeIDMapping(shortID, fullID string) {353 func storeIDMapping(shortID, fullID string) {
377 safelyInteractWithIDMap(Update, shortID, fullID)354 safelyInteractWithIDMap(Update, shortID, fullID)
M internal/httpserve/handlers/endpoint_push.go +1 -45
@@ -2,7 +2,6 @@
22 package handlers
33
44 import (
5- "archive/tar"
65 "bytes"
76 "encoding/json"
87 "fmt"
@@ -35,7 +34,7 @@ func PostPush(c echo.Context, a *server.App) error {
3534 })
3635 }
3736
38- _, err = saveAndImportPushImage(c, a, payload)
37+ _, err = saveAndImportImage(c, a, payload)
3938 if err != nil {
4039 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
4140 Success: false,
@@ -176,26 +175,6 @@ func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error)
176175 return metadata, chunkStore.chunks[transferID], nil
177176 }
178177
179-func createTempFile() (*os.File, func(), error) {
180- tmpDir, err := os.MkdirTemp("", "docker-import-*")
181- if err != nil {
182- return nil, nil, err
183- }
184-
185- tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar"))
186- if err != nil {
187- os.RemoveAll(tmpDir)
188- return nil, nil, err
189- }
190-
191- cleanup := func() {
192- tmpFile.Close()
193- os.RemoveAll(tmpDir)
194- }
195-
196- return tmpFile, cleanup, nil
197-}
198-
199178 func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error {
200179 // First, seek to the beginning of the file
201180 if _, err := file.Seek(0, 0); err != nil {
@@ -235,29 +214,6 @@ func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) er
235214 return nil
236215 }
237216
238-func prepareFileForImport(file *os.File) error {
239- // Reset to beginning of file
240- if _, err := file.Seek(0, 0); err != nil {
241- return fmt.Errorf("failed to seek to beginning: %w", err)
242- }
243-
244- // Verify the tar format
245- tr := tar.NewReader(file)
246- header, err := tr.Next()
247- if err != nil {
248- return fmt.Errorf("invalid tar format: %w", err)
249- }
250-
251- // Check if this is a valid Docker image tar
252- if header.Name == "manifest.json" {
253- // Reset file pointer for subsequent reads
254- _, err = file.Seek(0, 0)
255- return err
256- }
257-
258- return fmt.Errorf("file does not appear to be a valid Docker image archive")
259-}
260-
261217 func sendErrorResponse(c echo.Context, message string, err error) error {
262218 log.Error(message, "error", err)
263219 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
@@ -2,7 +2,6 @@
2 package handlers2 package handlers
3 3
4 import (4 import (
5- "archive/tar"
6 "bytes"5 "bytes"
7 "encoding/json"6 "encoding/json"
8 "fmt"7 "fmt"
@@ -35,7 +34,7 @@ func PostPush(c echo.Context, a *server.App) error {
35 })34 })
36 }35 }
37 36
38- _, err = saveAndImportPushImage(c, a, payload)37+ _, err = saveAndImportImage(c, a, payload)
39 if err != nil {38 if err != nil {
40 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{39 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
41 Success: false,40 Success: false,
@@ -176,26 +175,6 @@ func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error)
176 return metadata, chunkStore.chunks[transferID], nil175 return metadata, chunkStore.chunks[transferID], nil
177 }176 }
178 177
179-func createTempFile() (*os.File, func(), error) {
180- tmpDir, err := os.MkdirTemp("", "docker-import-*")
181- if err != nil {
182- return nil, nil, err
183- }
184-
185- tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar"))
186- if err != nil {
187- os.RemoveAll(tmpDir)
188- return nil, nil, err
189- }
190-
191- cleanup := func() {
192- tmpFile.Close()
193- os.RemoveAll(tmpDir)
194- }
195-
196- return tmpFile, cleanup, nil
197-}
198-
199 func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error {178 func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error {
200 // First, seek to the beginning of the file179 // First, seek to the beginning of the file
201 if _, err := file.Seek(0, 0); err != nil {180 if _, err := file.Seek(0, 0); err != nil {
@@ -235,29 +214,6 @@ func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) er
235 return nil214 return nil
236 }215 }
237 216
238-func prepareFileForImport(file *os.File) error {
239- // Reset to beginning of file
240- if _, err := file.Seek(0, 0); err != nil {
241- return fmt.Errorf("failed to seek to beginning: %w", err)
242- }
243-
244- // Verify the tar format
245- tr := tar.NewReader(file)
246- header, err := tr.Next()
247- if err != nil {
248- return fmt.Errorf("invalid tar format: %w", err)
249- }
250-
251- // Check if this is a valid Docker image tar
252- if header.Name == "manifest.json" {
253- // Reset file pointer for subsequent reads
254- _, err = file.Seek(0, 0)
255- return err
256- }
257-
258- return fmt.Errorf("file does not appear to be a valid Docker image archive")
259-}
260-
261 func sendErrorResponse(c echo.Context, message string, err error) error {217 func sendErrorResponse(c echo.Context, message string, err error) error {
262 log.Error(message, "error", err)218 log.Error(message, "error", err)
263 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{219 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
M internal/httpserve/handlers/endpoint_remove.go +30 -30
@@ -11,34 +11,34 @@ import (
1111
1212 // PostContainerRemove handles the container remove request
1313 func PostContainerRemove(c echo.Context, a *server.App) error {
14- payload := new(common.RequestPayload)
15- if err := c.Bind(payload); err != nil {
16- return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
17- }
18-
19- if payload.Type != "remove" {
20- return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
21- }
22-
23- removePayload, ok := payload.Payload.(common.RemovePayload)
24- if !ok {
25- return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
26- }
27-
28- if removePayload.ContainerID == "" {
29- return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
30- }
31-
32- err := docker.RemoveContainer(removePayload.ContainerID)
33- if err != nil {
34- return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{
35- Success: false,
36- Message: err.Error(),
37- })
38- }
39-
40- return sendJSONResponse(c, http.StatusOK, RemoveResponse{
41- Success: true,
42- Message: "Container removed successfully",
43- })
14+ payload := new(common.RequestPayload)
15+ if err := c.Bind(payload); err != nil {
16+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
17+ }
18+
19+ if payload.Type != "remove" {
20+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
21+ }
22+
23+ removePayload, ok := payload.Payload.(common.RemovePayload)
24+ if !ok {
25+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
26+ }
27+
28+ if removePayload.ContainerID == "" {
29+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
30+ }
31+
32+ err := docker.RemoveContainer(removePayload.ContainerID)
33+ if err != nil {
34+ return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{
35+ Success: false,
36+ Message: err.Error(),
37+ })
38+ }
39+
40+ return sendJSONResponse(c, http.StatusOK, RemoveResponse{
41+ Success: true,
42+ Message: "Container removed successfully",
43+ })
4444 }
@@ -11,34 +11,34 @@ import (
11 11
12 // PostContainerRemove handles the container remove request12 // PostContainerRemove handles the container remove request
13 func PostContainerRemove(c echo.Context, a *server.App) error {13 func PostContainerRemove(c echo.Context, a *server.App) error {
14- payload := new(common.RequestPayload)14+ payload := new(common.RequestPayload)
15- if err := c.Bind(payload); err != nil {15+ if err := c.Bind(payload); err != nil {
16- return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())16+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
17- }17+ }
18-18+
19- if payload.Type != "remove" {19+ if payload.Type != "remove" {
20- return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")20+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
21- }21+ }
22-22+
23- removePayload, ok := payload.Payload.(common.RemovePayload)23+ removePayload, ok := payload.Payload.(common.RemovePayload)
24- if !ok {24+ if !ok {
25- return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")25+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
26- }26+ }
27-27+
28- if removePayload.ContainerID == "" {28+ if removePayload.ContainerID == "" {
29- return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")29+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
30- }30+ }
31-31+
32- err := docker.RemoveContainer(removePayload.ContainerID)32+ err := docker.RemoveContainer(removePayload.ContainerID)
33- if err != nil {33+ if err != nil {
34- return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{34+ return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{
35- Success: false,35+ Success: false,
36- Message: err.Error(),36+ Message: err.Error(),
37- })37+ })
38- }38+ }
39-39+
40- return sendJSONResponse(c, http.StatusOK, RemoveResponse{40+ return sendJSONResponse(c, http.StatusOK, RemoveResponse{
41- Success: true,41+ Success: true,
42- Message: "Container removed successfully",42+ Message: "Container removed successfully",
43- })43+ })
44 }44 }
M internal/server/sqlite_close.go +19 -12
@@ -1,23 +1,30 @@
11 package server
22
3+import "fmt"
4+
35 // CloseAndSaveDB closes the in-memory database and backup the database file if there is any modification.
46 func CloseDB(a *App) error {
57 if a.DB != nil {
6- saveDB(a)
7- a.DB.Close()
8+ // if err := saveDB(a); err != nil {
9+ // return fmt.Errorf("failed to save database: %w", err)
10+ // }
11+
12+ if err := a.DB.Close(); err != nil {
13+ return fmt.Errorf("failed to close database: %w", err)
14+ }
815 }
916 return nil
1017 }
1118
12-func saveDB(a *App) error {
19+// func saveDB(a *App) error {
1320
14- return nil
15-}
21+// return nil
22+// }
1623
17-// TODO
18-func backupDBFile(a *App) error {
19- // Create a backup of the database file at the same location as the database file.
20- // The backup file name is the database file name with a timestamp appended to it.
21- // If the backup file already exists, overwrite it.
22- return nil
23-}
24+// // TODO
25+// func backupDBFile(a *App) error {
26+// // Create a backup of the database file at the same location as the database file.
27+// // The backup file name is the database file name with a timestamp appended to it.
28+// // If the backup file already exists, overwrite it.
29+// return nil
30+// }
@@ -1,23 +1,30 @@
1 package server1 package server
2 2
3+import "fmt"
4+
3 // CloseAndSaveDB closes the in-memory database and backup the database file if there is any modification.5 // CloseAndSaveDB closes the in-memory database and backup the database file if there is any modification.
4 func CloseDB(a *App) error {6 func CloseDB(a *App) error {
5 if a.DB != nil {7 if a.DB != nil {
6- saveDB(a)8+ // if err := saveDB(a); err != nil {
7- a.DB.Close()9+ // return fmt.Errorf("failed to save database: %w", err)
10+ // }
11+
12+ if err := a.DB.Close(); err != nil {
13+ return fmt.Errorf("failed to close database: %w", err)
14+ }
8 }15 }
9 return nil16 return nil
10 }17 }
11 18
12-func saveDB(a *App) error {19+// func saveDB(a *App) error {
13 20
14- return nil21+// return nil
15-}22+// }
16 23
17-// TODO24+// // TODO
18-func backupDBFile(a *App) error {25+// func backupDBFile(a *App) error {
19- // Create a backup of the database file at the same location as the database file.26+// // Create a backup of the database file at the same location as the database file.
20- // The backup file name is the database file name with a timestamp appended to it.27+// // The backup file name is the database file name with a timestamp appended to it.
21- // If the backup file already exists, overwrite it.28+// // If the backup file already exists, overwrite it.
22- return nil29+// return nil
23-}30+// }
M internal/templating/cmdparams/from_inputs.go +0 -7
@@ -45,10 +45,3 @@ func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.Cont
4545
4646 return params, nil
4747 }
48-
49-func extractContainerPort(portMappings []docker.PortMapping) string {
50- if len(portMappings) > 0 {
51- return portMappings[0].ContainerPort
52- }
53- return ""
54-}
@@ -45,10 +45,3 @@ func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.Cont
45 45
46 return params, nil46 return params, nil
47 }47 }
48-
49-func extractContainerPort(portMappings []docker.PortMapping) string {
50- if len(portMappings) > 0 {
51- return portMappings[0].ContainerPort
52- }
53- return ""
54-}
M pkg/docker/containers.go +125 -60
@@ -109,8 +109,22 @@ func ListRunningContainers() ([]types.Container, error) {
109109
110110 // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully
111111 func StopContainer(containerID string) error {
112- StopContainerGracefully(containerID, 3*time.Second)
113- StopContainerRagefully(containerID)
112+ // Try graceful shutdown first
113+ stopped, err := StopContainerGracefully(containerID, 3*time.Second)
114+ if err != nil {
115+ log.Warn("Failed to stop container gracefully", "error", err)
116+ }
117+
118+ // If container wasn't stopped gracefully or there was an error, try forceful shutdown
119+ if !stopped {
120+ if err := StopContainerRagefully(containerID); err != nil {
121+ return fmt.Errorf("failed to stop container forcefully: %w", err)
122+ }
123+ log.Info("Container stopped forcefully", "containerID", containerID)
124+ } else {
125+ log.Info("Container stopped gracefully", "containerID", containerID)
126+ }
127+
114128 return nil
115129 }
116130
@@ -127,18 +141,16 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration)
127141 defer ticker.Stop()
128142
129143 for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ {
130- select {
131- case <-ticker.C:
132- // Check if the container is still running
133- container, err := dockerCli.ContainerInspect(context.Background(), containerID)
134- if err != nil {
135- return false, err
136- }
144+ <-ticker.C
145+ // Check if the container is still running
146+ container, err := dockerCli.ContainerInspect(context.Background(), containerID)
147+ if err != nil {
148+ return false, err
149+ }
137150
138- // If the container is not running, return true indicating it was stopped
139- if !container.State.Running {
140- return true, nil
141- }
151+ // If the container is not running, return true
152+ if !container.State.Running {
153+ return true, nil
142154 }
143155 }
144156
@@ -229,7 +241,10 @@ func StartContainer(containerID string) error {
229241
230242 // CreateContainer creates a container with the given parameters
231243 func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
232- CheckIfInitialized()
244+
245+ if err := CheckIfInitialized(); err != nil {
246+ return "", fmt.Errorf("failed to check if docker client is initialized: %w", err)
247+ }
233248
234249 log.Info("Creating container with params: %+v\n", cmdParams)
235250
@@ -241,7 +256,7 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
241256
242257 if !isNetworkCreated {
243258 fmt.Printf("Creating network: %s\n", cmdParams.Network)
244- _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, types.NetworkCreate{})
259+ _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{})
245260 if err != nil {
246261 return "", fmt.Errorf("network creation failed: %v", err)
247262 }
@@ -334,7 +349,7 @@ func WaitForContainerToBeRunning(containerID string, timeout time.Duration) erro
334349 }
335350
336351 func CheckIfNetworkExists(networkName string) (bool, error) {
337- networks, err := dockerCli.NetworkList(context.Background(), types.NetworkListOptions{})
352+ networks, err := dockerCli.NetworkList(context.Background(), network.ListOptions{})
338353 if err != nil {
339354 return false, err
340355 }
@@ -349,14 +364,13 @@ func CheckIfNetworkExists(networkName string) (bool, error) {
349364 }
350365
351366 // GetNetworkInfo returns information about a network
352-func GetNetworkInfo(networkName string) (types.NetworkResource, error) {
353- // Get network info using the Docker client
354- networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, types.NetworkInspectOptions{})
367+func GetNetworkInfo(networkName string) (*network.Inspect, error) {
368+ networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, network.InspectOptions{})
355369 if err != nil {
356- return types.NetworkResource{}, err
370+ return nil, err
357371 }
358372
359- return networkInfo, nil
373+ return &networkInfo, nil
360374 }
361375
362376 // GetContainerInfo returns information about a container
@@ -417,15 +431,16 @@ func UpdateContainerConfig(containerID string, newConfig *container.Config, newH
417431
418432 // GetContainerLogs returns the logs of a container
419433 func GetContainerLogs(containerID string) (string, error) {
420- // Get container logs using the Docker client
421434 containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true})
422435 if err != nil {
423436 return "", err
424437 }
438+ defer containerLogs.Close()
425439
426- // Read the logs
427440 buf := new(bytes.Buffer)
428- buf.ReadFrom(containerLogs)
441+ if _, err := buf.ReadFrom(containerLogs); err != nil {
442+ return "", fmt.Errorf("failed to read container logs: %w", err)
443+ }
429444 containerLogsString := buf.String()
430445
431446 return containerLogsString, nil
@@ -551,6 +566,15 @@ func GetContainerUptime(containerID string) (string, error) {
551566 return formatDuration(duration), nil
552567 }
553568
569+// ContainerPortInfo holds information about container ports
570+type ContainerPortInfo struct {
571+ Name string
572+ ExposedPorts map[nat.Port]struct{}
573+ PortBindings map[nat.Port][]nat.PortBinding
574+ PublishedPorts []types.Port
575+}
576+
577+// GetContainersUsingPort returns a list of container names that are using the specified port
554578 func GetContainersUsingPort(port string) ([]string, error) {
555579 cli, err := client.NewClientWithOpts(client.FromEnv)
556580 if err != nil {
@@ -561,59 +585,100 @@ func GetContainersUsingPort(port string) ([]string, error) {
561585 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
562586 defer cancel()
563587
564- // List all containers (including running and stopped)
565588 containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
589+ if err != nil {
590+ return nil, fmt.Errorf("failed to list containers: %w", err)
591+ }
592+
593+ return findContainersWithPort(ctx, cli, containers, port)
594+}
595+
596+// findContainersWithPort processes the container list and finds those using the specified port
597+func findContainersWithPort(ctx context.Context, cli *client.Client, containers []types.Container, port string) ([]string, error) {
566598 var containersUsingPort []string
599+ seen := make(map[string]bool)
567600
568601 for _, container := range containers {
569- // Inspect container to get detailed port information
570- inspect, err := cli.ContainerInspect(ctx, container.ID)
602+ containerInfo, err := getContainerPortInfo(ctx, cli, container)
571603 if err != nil {
572- continue // Skip containers we can't inspect
604+ log.Warn("Failed to inspect container", "container", container.ID, "error", err)
605+ continue
573606 }
574607
575- // Check exposed ports in container config
576- if inspect.Config != nil && inspect.Config.ExposedPorts != nil {
577- for exposedPort := range inspect.Config.ExposedPorts {
578- if strings.HasPrefix(string(exposedPort), port+"/") {
579- containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/"))
580- break
581- }
582- }
608+ if isContainerUsingPort(containerInfo, port) && !seen[containerInfo.Name] {
609+ seen[containerInfo.Name] = true
610+ containersUsingPort = append(containersUsingPort, containerInfo.Name)
583611 }
612+ }
584613
585- // Check port bindings in host config
586- if inspect.HostConfig != nil && inspect.HostConfig.PortBindings != nil {
587- for _, bindings := range inspect.HostConfig.PortBindings {
588- for _, binding := range bindings {
589- if binding.HostPort == port {
590- containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/"))
591- break
592- }
593- }
594- }
614+ return containersUsingPort, nil
615+}
616+
617+// getContainerPortInfo retrieves port-related information for a container
618+func getContainerPortInfo(ctx context.Context, cli *client.Client, container types.Container) (ContainerPortInfo, error) {
619+ inspect, err := cli.ContainerInspect(ctx, container.ID)
620+ if err != nil {
621+ return ContainerPortInfo{}, err
622+ }
623+
624+ return ContainerPortInfo{
625+ Name: strings.TrimPrefix(container.Names[0], "/"),
626+ ExposedPorts: inspect.Config.ExposedPorts,
627+ PortBindings: inspect.HostConfig.PortBindings,
628+ PublishedPorts: container.Ports,
629+ }, nil
630+}
631+
632+// isContainerUsingPort checks if a container is using the specified port
633+func isContainerUsingPort(info ContainerPortInfo, port string) bool {
634+ // Check exposed ports
635+ if hasExposedPort(info.ExposedPorts, port) {
636+ return true
637+ }
638+
639+ // Check port bindings
640+ if hasPortBinding(info.PortBindings, port) {
641+ return true
642+ }
643+
644+ // Check published ports
645+ if hasPublishedPort(info.PublishedPorts, port) {
646+ return true
647+ }
648+
649+ return false
650+}
651+
652+// hasExposedPort checks if the port is exposed in the container configuration
653+func hasExposedPort(exposedPorts nat.PortSet, port string) bool {
654+ for exposedPort := range exposedPorts {
655+ if strings.HasPrefix(string(exposedPort), port+"/") {
656+ return true
595657 }
658+ }
659+ return false
660+}
596661
597- // Check currently published ports
598- for _, portBinding := range container.Ports {
599- if fmt.Sprintf("%d", portBinding.PublicPort) == port {
600- containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/"))
601- break
662+// hasPortBinding checks if the port is bound in the host configuration
663+func hasPortBinding(portBindings nat.PortMap, port string) bool {
664+ for _, bindings := range portBindings {
665+ for _, binding := range bindings {
666+ if binding.HostPort == port {
667+ return true
602668 }
603669 }
604670 }
671+ return false
672+}
605673
606- // Remove duplicates
607- seen := make(map[string]bool)
608- unique := make([]string, 0, len(containersUsingPort))
609- for _, name := range containersUsingPort {
610- if !seen[name] {
611- seen[name] = true
612- unique = append(unique, name)
674+// hasPublishedPort checks if the port is currently published
675+func hasPublishedPort(ports []types.Port, port string) bool {
676+ for _, portBinding := range ports {
677+ if fmt.Sprintf("%d", portBinding.PublicPort) == port {
678+ return true
613679 }
614680 }
615-
616- return unique, nil
681+ return false
617682 }
618683
619684 func formatDuration(d time.Duration) string {
@@ -109,8 +109,22 @@ func ListRunningContainers() ([]types.Container, error) {
109 109
110 // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully110 // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully
111 func StopContainer(containerID string) error {111 func StopContainer(containerID string) error {
112- StopContainerGracefully(containerID, 3*time.Second)112+ // Try graceful shutdown first
113- StopContainerRagefully(containerID)113+ stopped, err := StopContainerGracefully(containerID, 3*time.Second)
114+ if err != nil {
115+ log.Warn("Failed to stop container gracefully", "error", err)
116+ }
117+
118+ // If container wasn't stopped gracefully or there was an error, try forceful shutdown
119+ if !stopped {
120+ if err := StopContainerRagefully(containerID); err != nil {
121+ return fmt.Errorf("failed to stop container forcefully: %w", err)
122+ }
123+ log.Info("Container stopped forcefully", "containerID", containerID)
124+ } else {
125+ log.Info("Container stopped gracefully", "containerID", containerID)
126+ }
127+
114 return nil128 return nil
115 }129 }
116 130
@@ -127,18 +141,16 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration)
127 defer ticker.Stop()141 defer ticker.Stop()
128 142
129 for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ {143 for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ {
130- select {144+ <-ticker.C
131- case <-ticker.C:145+ // Check if the container is still running
132- // Check if the container is still running146+ container, err := dockerCli.ContainerInspect(context.Background(), containerID)
133- container, err := dockerCli.ContainerInspect(context.Background(), containerID)147+ if err != nil {
134- if err != nil {148+ return false, err
135- return false, err149+ }
136- }
137 150
138- // If the container is not running, return true indicating it was stopped151+ // If the container is not running, return true
139- if !container.State.Running {152+ if !container.State.Running {
140- return true, nil153+ return true, nil
141- }
142 }154 }
143 }155 }
144 156
@@ -229,7 +241,10 @@ func StartContainer(containerID string) error {
229 241
230 // CreateContainer creates a container with the given parameters242 // CreateContainer creates a container with the given parameters
231 func CreateContainer(cmdParams ContainerCommandParams) (string, error) {243 func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
232- CheckIfInitialized()244+
245+ if err := CheckIfInitialized(); err != nil {
246+ return "", fmt.Errorf("failed to check if docker client is initialized: %w", err)
247+ }
233 248
234 log.Info("Creating container with params: %+v\n", cmdParams)249 log.Info("Creating container with params: %+v\n", cmdParams)
235 250
@@ -241,7 +256,7 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
241 256
242 if !isNetworkCreated {257 if !isNetworkCreated {
243 fmt.Printf("Creating network: %s\n", cmdParams.Network)258 fmt.Printf("Creating network: %s\n", cmdParams.Network)
244- _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, types.NetworkCreate{})259+ _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{})
245 if err != nil {260 if err != nil {
246 return "", fmt.Errorf("network creation failed: %v", err)261 return "", fmt.Errorf("network creation failed: %v", err)
247 }262 }
@@ -334,7 +349,7 @@ func WaitForContainerToBeRunning(containerID string, timeout time.Duration) erro
334 }349 }
335 350
336 func CheckIfNetworkExists(networkName string) (bool, error) {351 func CheckIfNetworkExists(networkName string) (bool, error) {
337- networks, err := dockerCli.NetworkList(context.Background(), types.NetworkListOptions{})352+ networks, err := dockerCli.NetworkList(context.Background(), network.ListOptions{})
338 if err != nil {353 if err != nil {
339 return false, err354 return false, err
340 }355 }
@@ -349,14 +364,13 @@ func CheckIfNetworkExists(networkName string) (bool, error) {
349 }364 }
350 365
351 // GetNetworkInfo returns information about a network366 // GetNetworkInfo returns information about a network
352-func GetNetworkInfo(networkName string) (types.NetworkResource, error) {367+func GetNetworkInfo(networkName string) (*network.Inspect, error) {
353- // Get network info using the Docker client368+ networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, network.InspectOptions{})
354- networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, types.NetworkInspectOptions{})
355 if err != nil {369 if err != nil {
356- return types.NetworkResource{}, err370+ return nil, err
357 }371 }
358 372
359- return networkInfo, nil373+ return &networkInfo, nil
360 }374 }
361 375
362 // GetContainerInfo returns information about a container376 // GetContainerInfo returns information about a container
@@ -417,15 +431,16 @@ func UpdateContainerConfig(containerID string, newConfig *container.Config, newH
417 431
418 // GetContainerLogs returns the logs of a container432 // GetContainerLogs returns the logs of a container
419 func GetContainerLogs(containerID string) (string, error) {433 func GetContainerLogs(containerID string) (string, error) {
420- // Get container logs using the Docker client
421 containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true})434 containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true})
422 if err != nil {435 if err != nil {
423 return "", err436 return "", err
424 }437 }
438+ defer containerLogs.Close()
425 439
426- // Read the logs
427 buf := new(bytes.Buffer)440 buf := new(bytes.Buffer)
428- buf.ReadFrom(containerLogs)441+ if _, err := buf.ReadFrom(containerLogs); err != nil {
442+ return "", fmt.Errorf("failed to read container logs: %w", err)
443+ }
429 containerLogsString := buf.String()444 containerLogsString := buf.String()
430 445
431 return containerLogsString, nil446 return containerLogsString, nil
@@ -551,6 +566,15 @@ func GetContainerUptime(containerID string) (string, error) {
551 return formatDuration(duration), nil566 return formatDuration(duration), nil
552 }567 }
553 568
569+// ContainerPortInfo holds information about container ports
570+type ContainerPortInfo struct {
571+ Name string
572+ ExposedPorts map[nat.Port]struct{}
573+ PortBindings map[nat.Port][]nat.PortBinding
574+ PublishedPorts []types.Port
575+}
576+
577+// GetContainersUsingPort returns a list of container names that are using the specified port
554 func GetContainersUsingPort(port string) ([]string, error) {578 func GetContainersUsingPort(port string) ([]string, error) {
555 cli, err := client.NewClientWithOpts(client.FromEnv)579 cli, err := client.NewClientWithOpts(client.FromEnv)
556 if err != nil {580 if err != nil {
@@ -561,59 +585,100 @@ func GetContainersUsingPort(port string) ([]string, error) {
561 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)585 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
562 defer cancel()586 defer cancel()
563 587
564- // List all containers (including running and stopped)
565 containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})588 containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
589+ if err != nil {
590+ return nil, fmt.Errorf("failed to list containers: %w", err)
591+ }
592+
593+ return findContainersWithPort(ctx, cli, containers, port)
594+}
595+
596+// findContainersWithPort processes the container list and finds those using the specified port
597+func findContainersWithPort(ctx context.Context, cli *client.Client, containers []types.Container, port string) ([]string, error) {
566 var containersUsingPort []string598 var containersUsingPort []string
599+ seen := make(map[string]bool)
567 600
568 for _, container := range containers {601 for _, container := range containers {
569- // Inspect container to get detailed port information602+ containerInfo, err := getContainerPortInfo(ctx, cli, container)
570- inspect, err := cli.ContainerInspect(ctx, container.ID)
571 if err != nil {603 if err != nil {
572- continue // Skip containers we can't inspect604+ log.Warn("Failed to inspect container", "container", container.ID, "error", err)
605+ continue
573 }606 }
574 607
575- // Check exposed ports in container config608+ if isContainerUsingPort(containerInfo, port) && !seen[containerInfo.Name] {
576- if inspect.Config != nil && inspect.Config.ExposedPorts != nil {609+ seen[containerInfo.Name] = true
577- for exposedPort := range inspect.Config.ExposedPorts {610+ containersUsingPort = append(containersUsingPort, containerInfo.Name)
578- if strings.HasPrefix(string(exposedPort), port+"/") {
579- containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/"))
580- break
581- }
582- }
583 }611 }
612+ }
584 613
585- // Check port bindings in host config614+ return containersUsingPort, nil
586- if inspect.HostConfig != nil && inspect.HostConfig.PortBindings != nil {615+}
587- for _, bindings := range inspect.HostConfig.PortBindings {616+
588- for _, binding := range bindings {617+// getContainerPortInfo retrieves port-related information for a container
589- if binding.HostPort == port {618+func getContainerPortInfo(ctx context.Context, cli *client.Client, container types.Container) (ContainerPortInfo, error) {
590- containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/"))619+ inspect, err := cli.ContainerInspect(ctx, container.ID)
591- break620+ if err != nil {
592- }621+ return ContainerPortInfo{}, err
593- }622+ }
594- }623+
624+ return ContainerPortInfo{
625+ Name: strings.TrimPrefix(container.Names[0], "/"),
626+ ExposedPorts: inspect.Config.ExposedPorts,
627+ PortBindings: inspect.HostConfig.PortBindings,
628+ PublishedPorts: container.Ports,
629+ }, nil
630+}
631+
632+// isContainerUsingPort checks if a container is using the specified port
633+func isContainerUsingPort(info ContainerPortInfo, port string) bool {
634+ // Check exposed ports
635+ if hasExposedPort(info.ExposedPorts, port) {
636+ return true
637+ }
638+
639+ // Check port bindings
640+ if hasPortBinding(info.PortBindings, port) {
641+ return true
642+ }
643+
644+ // Check published ports
645+ if hasPublishedPort(info.PublishedPorts, port) {
646+ return true
647+ }
648+
649+ return false
650+}
651+
652+// hasExposedPort checks if the port is exposed in the container configuration
653+func hasExposedPort(exposedPorts nat.PortSet, port string) bool {
654+ for exposedPort := range exposedPorts {
655+ if strings.HasPrefix(string(exposedPort), port+"/") {
656+ return true
595 }657 }
658+ }
659+ return false
660+}
596 661
597- // Check currently published ports662+// hasPortBinding checks if the port is bound in the host configuration
598- for _, portBinding := range container.Ports {663+func hasPortBinding(portBindings nat.PortMap, port string) bool {
599- if fmt.Sprintf("%d", portBinding.PublicPort) == port {664+ for _, bindings := range portBindings {
600- containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/"))665+ for _, binding := range bindings {
601- break666+ if binding.HostPort == port {
667+ return true
602 }668 }
603 }669 }
604 }670 }
671+ return false
672+}
605 673
606- // Remove duplicates674+// hasPublishedPort checks if the port is currently published
607- seen := make(map[string]bool)675+func hasPublishedPort(ports []types.Port, port string) bool {
608- unique := make([]string, 0, len(containersUsingPort))676+ for _, portBinding := range ports {
609- for _, name := range containersUsingPort {677+ if fmt.Sprintf("%d", portBinding.PublicPort) == port {
610- if !seen[name] {678+ return true
611- seen[name] = true
612- unique = append(unique, name)
613 }679 }
614 }680 }
615-681+ return false
616- return unique, nil
617 }682 }
618 683
619 func formatDuration(d time.Duration) string {684 func formatDuration(d time.Duration) string {
M pkg/docker/images.go +3 -1
@@ -18,7 +18,9 @@ import (
1818
1919 func ListContainerImages() ([]image.Summary, error) {
2020 // Check if the Docker client has been initialized
21- CheckIfInitialized()
21+ if err := CheckIfInitialized(); err != nil {
22+ return nil, fmt.Errorf("failed to initialize Docker client: %w", err)
23+ }
2224
2325 // List images using the Docker client
2426 images, err := dockerCli.ImageList(context.Background(), image.ListOptions{})
@@ -18,7 +18,9 @@ import (
18 18
19 func ListContainerImages() ([]image.Summary, error) {19 func ListContainerImages() ([]image.Summary, error) {
20 // Check if the Docker client has been initialized20 // Check if the Docker client has been initialized
21- CheckIfInitialized()21+ if err := CheckIfInitialized(); err != nil {
22+ return nil, fmt.Errorf("failed to initialize Docker client: %w", err)
23+ }
22 24
23 // List images using the Docker client25 // List images using the Docker client
24 images, err := dockerCli.ImageList(context.Background(), image.ListOptions{})26 images, err := dockerCli.ImageList(context.Background(), image.ListOptions{})
M pkg/parser/yaml.go +1 -1
@@ -47,7 +47,7 @@ func WriteYAMLFile(filename string, in interface{}) error {
4747 }
4848
4949 // Write the YAML data to the file
50- err = os.WriteFile(filename, data, 0644)
50+ err = os.WriteFile(filename, data, 0600)
5151 if err != nil {
5252 return fmt.Errorf("failed to write to %s: %w", filename, err)
5353 }
@@ -47,7 +47,7 @@ func WriteYAMLFile(filename string, in interface{}) error {
47 }47 }
48 48
49 // Write the YAML data to the file49 // Write the YAML data to the file
50- err = os.WriteFile(filename, data, 0644)50+ err = os.WriteFile(filename, data, 0600)
51 if err != nil {51 if err != nil {
52 return fmt.Errorf("failed to write to %s: %w", filename, err)52 return fmt.Errorf("failed to write to %s: %w", filename, err)
53 }53 }
M pkg/store/store_images.go +14 -30
@@ -5,36 +5,17 @@ import (
55 "io"
66 "os"
77 "path/filepath"
8- "sync"
98
109 "github.com/bnema/gordon/internal/common"
10+ "github.com/charmbracelet/log"
1111 "github.com/google/uuid"
1212 )
1313
14-var (
15- imagePath string
16- mu sync.Mutex
17-)
18-
19-// setImagePath sets the image path in a thread-safe manner
20-func setImagePath(path string) {
21- mu.Lock()
22- defer mu.Unlock()
23- imagePath = path
24-}
25-
26-// getImagePath gets the image path in a thread-safe manner
27-func getImagePath() string {
28- mu.Lock()
29- defer mu.Unlock()
30- return imagePath
31-}
32-
3314 func SaveImageToStorage(config *common.Config, originalFilename string, buf io.Reader) (string, error) {
3415 // Ensure the images directory exists
3516 imagesDir := filepath.Join(config.General.StorageDir, "images")
3617 if err := os.MkdirAll(imagesDir, 0755); err != nil {
37- return "", fmt.Errorf("failed to create images directory: %v", err)
18+ return "", fmt.Errorf("failed to create images directory: %w", err)
3819 }
3920
4021 // Generate a unique ID for the image
@@ -42,17 +23,17 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R
4223
4324 // Create a filename with the ID and original extension
4425 ext := filepath.Ext(originalFilename)
45- filename := fmt.Sprintf("%s%s", id, ext)
26+ filename := id + ext
4627
4728 // Full path for the new image
4829 imagePath := filepath.Join(imagesDir, filename)
4930
50- fmt.Printf("Saving image to: %s\n", imagePath)
31+ log.Info("saving image", "path", imagePath)
5132
5233 // Create the file
5334 outFile, err := os.Create(imagePath)
5435 if err != nil {
55- return "", fmt.Errorf("failed to create file: %v", err)
36+ return "", fmt.Errorf("failed to create file: %w", err)
5637 }
5738 defer outFile.Close()
5839
@@ -60,27 +41,30 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R
6041 written, err := io.Copy(outFile, buf)
6142 if err != nil {
6243 os.Remove(imagePath) // Clean up in case of error
63- return "", fmt.Errorf("failed to write file: %v", err)
44+ return "", fmt.Errorf("failed to write file: %w", err)
6445 }
6546
66- fmt.Printf("Wrote %d bytes to %s\n", written, imagePath)
47+ log.Info("file written successfully",
48+ "bytes", written,
49+ "path", imagePath,
50+ )
6751
6852 return imagePath, nil
6953 }
7054
7155 func RemoveFromStorage(imagePath string) error {
7256 if imagePath == "" {
73- return fmt.Errorf("no image path provided to remove")
57+ return fmt.Errorf("empty image path")
7458 }
7559
76- fmt.Printf("Attempting to remove file: %s\n", imagePath)
60+ log.Info("attempting to remove file", "path", imagePath)
7761
7862 err := os.Remove(imagePath)
7963 if err != nil {
80- return fmt.Errorf("failed to remove file: %v", err)
64+ return fmt.Errorf("failed to remove file: %w", err)
8165 }
8266
83- fmt.Printf("Successfully removed file: %s\n", imagePath)
67+ log.Info("file removed successfully", "path", imagePath)
8468
8569 return nil
8670 }
@@ -5,36 +5,17 @@ import (
5 "io"5 "io"
6 "os"6 "os"
7 "path/filepath"7 "path/filepath"
8- "sync"
9 8
10 "github.com/bnema/gordon/internal/common"9 "github.com/bnema/gordon/internal/common"
10+ "github.com/charmbracelet/log"
11 "github.com/google/uuid"11 "github.com/google/uuid"
12 )12 )
13 13
14-var (
15- imagePath string
16- mu sync.Mutex
17-)
18-
19-// setImagePath sets the image path in a thread-safe manner
20-func setImagePath(path string) {
21- mu.Lock()
22- defer mu.Unlock()
23- imagePath = path
24-}
25-
26-// getImagePath gets the image path in a thread-safe manner
27-func getImagePath() string {
28- mu.Lock()
29- defer mu.Unlock()
30- return imagePath
31-}
32-
33 func SaveImageToStorage(config *common.Config, originalFilename string, buf io.Reader) (string, error) {14 func SaveImageToStorage(config *common.Config, originalFilename string, buf io.Reader) (string, error) {
34 // Ensure the images directory exists15 // Ensure the images directory exists
35 imagesDir := filepath.Join(config.General.StorageDir, "images")16 imagesDir := filepath.Join(config.General.StorageDir, "images")
36 if err := os.MkdirAll(imagesDir, 0755); err != nil {17 if err := os.MkdirAll(imagesDir, 0755); err != nil {
37- return "", fmt.Errorf("failed to create images directory: %v", err)18+ return "", fmt.Errorf("failed to create images directory: %w", err)
38 }19 }
39 20
40 // Generate a unique ID for the image21 // Generate a unique ID for the image
@@ -42,17 +23,17 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R
42 23
43 // Create a filename with the ID and original extension24 // Create a filename with the ID and original extension
44 ext := filepath.Ext(originalFilename)25 ext := filepath.Ext(originalFilename)
45- filename := fmt.Sprintf("%s%s", id, ext)26+ filename := id + ext
46 27
47 // Full path for the new image28 // Full path for the new image
48 imagePath := filepath.Join(imagesDir, filename)29 imagePath := filepath.Join(imagesDir, filename)
49 30
50- fmt.Printf("Saving image to: %s\n", imagePath)31+ log.Info("saving image", "path", imagePath)
51 32
52 // Create the file33 // Create the file
53 outFile, err := os.Create(imagePath)34 outFile, err := os.Create(imagePath)
54 if err != nil {35 if err != nil {
55- return "", fmt.Errorf("failed to create file: %v", err)36+ return "", fmt.Errorf("failed to create file: %w", err)
56 }37 }
57 defer outFile.Close()38 defer outFile.Close()
58 39
@@ -60,27 +41,30 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R
60 written, err := io.Copy(outFile, buf)41 written, err := io.Copy(outFile, buf)
61 if err != nil {42 if err != nil {
62 os.Remove(imagePath) // Clean up in case of error43 os.Remove(imagePath) // Clean up in case of error
63- return "", fmt.Errorf("failed to write file: %v", err)44+ return "", fmt.Errorf("failed to write file: %w", err)
64 }45 }
65 46
66- fmt.Printf("Wrote %d bytes to %s\n", written, imagePath)47+ log.Info("file written successfully",
48+ "bytes", written,
49+ "path", imagePath,
50+ )
67 51
68 return imagePath, nil52 return imagePath, nil
69 }53 }
70 54
71 func RemoveFromStorage(imagePath string) error {55 func RemoveFromStorage(imagePath string) error {
72 if imagePath == "" {56 if imagePath == "" {
73- return fmt.Errorf("no image path provided to remove")57+ return fmt.Errorf("empty image path")
74 }58 }
75 59
76- fmt.Printf("Attempting to remove file: %s\n", imagePath)60+ log.Info("attempting to remove file", "path", imagePath)
77 61
78 err := os.Remove(imagePath)62 err := os.Remove(imagePath)
79 if err != nil {63 if err != nil {
80- return fmt.Errorf("failed to remove file: %v", err)64+ return fmt.Errorf("failed to remove file: %w", err)
81 }65 }
82 66
83- fmt.Printf("Successfully removed file: %s\n", imagePath)67+ log.Info("file removed successfully", "path", imagePath)
84 68
85 return nil69 return nil
86 }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