From ac8ac1186ad3dee4dd461caa9cfeb9d5912a29cd Mon Sep 17 00:00:00 2001 From: rslangl Date: Mon, 27 Apr 2026 22:03:35 +0200 Subject: [PATCH 1/4] feat: write chart through post request --- internal/api/helpers.go | 8 +++++++ internal/api/impl.go | 40 ++++++++++++++++++++++++++++++---- internal/storage/backend/fs.go | 2 +- internal/storage/storage.go | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 internal/api/helpers.go diff --git a/internal/api/helpers.go b/internal/api/helpers.go new file mode 100644 index 0000000..8a94df0 --- /dev/null +++ b/internal/api/helpers.go @@ -0,0 +1,8 @@ +package api + +func NewError(code int, message string) Error { + return Error{ + Code: &code, + Message: &message, + } +} diff --git a/internal/api/impl.go b/internal/api/impl.go index f16fe38..9132fee 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "io" "net/http" //"path" @@ -75,8 +76,39 @@ func (Server) GetChartVersions(w http.ResponseWriter, r *http.Request, name stri _ = json.NewEncoder(w).Encode(res) } -func (Server) AddChart(w http.ResponseWriter, r *http.Request) { - // TODO: interface to storage backend for creating - res := fmt.Sprintf("%v", r) - _ = json.NewEncoder(w).Encode(res) +func (s Server) AddChart(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") // TODO: set globally, or default? + + // Set max header size (50MB) + r.Body = http.MaxBytesReader(w, r.Body, 50<<20) // TODO: set globally, or default? + + if err := r.ParseMultipartForm(10 << 20); err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Invalid form data")) + return + } + + file, fh, err := r.FormFile("chart") + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Missing chart")) + return + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Malformed file upload")) + return + } + + if err := s.storageHandler.Write(fh.Filename, data); err != nil { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(NewError(http.StatusInternalServerError, "Error occurred durinf file IO")) + return + } + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode("") } diff --git a/internal/storage/backend/fs.go b/internal/storage/backend/fs.go index 34c0d06..3bb8c1c 100644 --- a/internal/storage/backend/fs.go +++ b/internal/storage/backend/fs.go @@ -23,7 +23,7 @@ func NewFSBackend(path string) (*FileSystem, error) { } // Implementation of the `Writer` interface -func (f *FileSystem) Write(bytes []byte) error { // TODO: define type `artifact` or similar instead +func (f *FileSystem) Write(name string, bytes []byte) error { // TODO: define type `artifact` or similar instead // TODO: create path if not exists (requires more parameters) return nil } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 1156ef8..8628daf 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -9,7 +9,7 @@ import ( type Storage interface { Read(location string, version string) ([]byte, error) - Write(bytes []byte) error + Write(name string, bytes []byte) error } func New(config config.StorageConfig) (Storage, error) { From 5f7e69284e91c0643ae6ad7fce3ab248b90031fa Mon Sep 17 00:00:00 2001 From: rslangl Date: Tue, 28 Apr 2026 22:21:11 +0200 Subject: [PATCH 2/4] chore: construct intermittent intsances using generated types --- api/api.yaml | 2 ++ internal/api/api.gen.go | 1 + internal/api/impl.go | 55 ++++++++++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/api/api.yaml b/api/api.yaml index 18b9013..e754a77 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -109,6 +109,8 @@ paths: schema: type: object properties: + name: + type: string chart: type: string format: binary diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 2a18ae5..538a50e 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -29,6 +29,7 @@ type Error struct { type AddChartMultipartBody struct { // Chart The packaged chart file (.tgz) Chart *openapi_types.File `json:"chart,omitempty"` + Name *string `json:"name,omitempty"` } // AddChartMultipartRequestBody defines body for AddChart for multipart/form-data ContentType. diff --git a/internal/api/impl.go b/internal/api/impl.go index 9132fee..9efcf58 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -5,7 +5,7 @@ import ( "fmt" "io" "net/http" - //"path" + openapi_types "github.com/oapi-codegen/runtime/types" "artifact-store/internal/storage" "artifact-store/internal/storage/storage_error" @@ -82,33 +82,66 @@ func (s Server) AddChart(w http.ResponseWriter, r *http.Request) { // Set max header size (50MB) r.Body = http.MaxBytesReader(w, r.Body, 50<<20) // TODO: set globally, or default? - if err := r.ParseMultipartForm(10 << 20); err != nil { + // Limit parsed form size (32MB) + if err := r.ParseMultipartForm(32 << 20); err != nil { w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Invalid form data")) + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusBadRequest)), + Message: new(string("Invalid form data")), + }) return } + defer func() { + if r.MultipartForm != nil { + // Clean up temp files + r.MultipartForm.RemoveAll() + } + }() + + // Extract chart name from form data + name := r.FormValue("name") - file, fh, err := r.FormFile("chart") + // Extract chart bytes from form data + f, fh, err := r.FormFile("chart") if err != nil { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Missing chart")) return } - defer file.Close() + defer f.Close() - data, err := io.ReadAll(file) + data, err := io.ReadAll(f) if err != nil { w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Malformed file upload")) - return + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusBadRequest)), + Message: new(string("Malformed file contents")), + }) + } + + // Construct runtime type for compatibility with generated types + file := &openapi_types.File{} + file.InitFromBytes(data, fh.Filename) + + bytes, err := file.Bytes() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusBadRequest)), + Message: new(string("Malformed file upload")), + }) } - if err := s.storageHandler.Write(fh.Filename, data); err != nil { + if err := s.storageHandler.Write(name, bytes); err != nil { // TODO: return created version w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(NewError(http.StatusInternalServerError, "Error occurred durinf file IO")) + _ = json.NewEncoder(w).Encode(NewError(http.StatusInternalServerError, "Error occurred during file IO")) return } w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode("") + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusCreated)), + //Message: &msg, + Message: new(string(fmt.Sprintf("Chart '%v' created", name))), + }) } From d2e040d4343d8a09da5e1ecf2070538afccceda0 Mon Sep 17 00:00:00 2001 From: rslangl Date: Tue, 28 Apr 2026 22:23:20 +0200 Subject: [PATCH 3/4] chore: remove unused helper --- internal/api/helpers.go | 8 -------- internal/api/impl.go | 11 ++++++++--- 2 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 internal/api/helpers.go diff --git a/internal/api/helpers.go b/internal/api/helpers.go deleted file mode 100644 index 8a94df0..0000000 --- a/internal/api/helpers.go +++ /dev/null @@ -1,8 +0,0 @@ -package api - -func NewError(code int, message string) Error { - return Error{ - Code: &code, - Message: &message, - } -} diff --git a/internal/api/impl.go b/internal/api/impl.go index 9efcf58..8c8f6c3 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -105,7 +105,10 @@ func (s Server) AddChart(w http.ResponseWriter, r *http.Request) { f, fh, err := r.FormFile("chart") if err != nil { w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(NewError(http.StatusBadRequest, "Missing chart")) + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusBadRequest)), + Message: new(string("Missing chart")), + }) return } defer f.Close() @@ -134,14 +137,16 @@ func (s Server) AddChart(w http.ResponseWriter, r *http.Request) { if err := s.storageHandler.Write(name, bytes); err != nil { // TODO: return created version w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(NewError(http.StatusInternalServerError, "Error occurred during file IO")) + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusInternalServerError)), + Message: new(string("Error occurred during file IO")), + }) return } w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(Error{ Code: new(int(http.StatusCreated)), - //Message: &msg, Message: new(string(fmt.Sprintf("Chart '%v' created", name))), }) } From 9f121d0377d7820f202a73fc1804ce6564ad66b0 Mon Sep 17 00:00:00 2001 From: rslangl Date: Tue, 28 Apr 2026 22:50:26 +0200 Subject: [PATCH 4/4] chore: ensure filesystem backend writer creates file --- internal/api/impl.go | 2 ++ internal/storage/backend/fs.go | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/internal/api/impl.go b/internal/api/impl.go index 8c8f6c3..b7d243e 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -113,6 +113,8 @@ func (s Server) AddChart(w http.ResponseWriter, r *http.Request) { } defer f.Close() + // TODO: validate file type is .tgz + data, err := io.ReadAll(f) if err != nil { w.WriteHeader(http.StatusBadRequest) diff --git a/internal/storage/backend/fs.go b/internal/storage/backend/fs.go index 3bb8c1c..8644626 100644 --- a/internal/storage/backend/fs.go +++ b/internal/storage/backend/fs.go @@ -2,6 +2,7 @@ package backend import ( "io/fs" + "log" "os" "path" "path/filepath" @@ -10,6 +11,7 @@ import ( ) type FileSystem struct { + root string Path fs.FS } @@ -18,14 +20,32 @@ func NewFSBackend(path string) (*FileSystem, error) { return nil, err } return &FileSystem{ + root: path, Path: os.DirFS(path), }, nil } // Implementation of the `Writer` interface func (f *FileSystem) Write(name string, bytes []byte) error { // TODO: define type `artifact` or similar instead - // TODO: create path if not exists (requires more parameters) - return nil + path := filepath.Join(f.root, name) + + // TODO: to be implemented when we decide to add repositories + // if err := os.MkdirAll(filepath.Dir(), 0o744); err != nil { + // return err + // } + + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Fatal(err) + return err + } + defer file.Close() + + _, err = file.Write(bytes) + if err != nil { + log.Fatal(err) + } + return err } // Implementation of the `Reader` interface