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 f16fe38..b7d243e 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" + "io" "net/http" - //"path" + openapi_types "github.com/oapi-codegen/runtime/types" "artifact-store/internal/storage" "artifact-store/internal/storage/storage_error" @@ -75,8 +76,79 @@ 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? + + // Limit parsed form size (32MB) + if err := r.ParseMultipartForm(32 << 20); err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = 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") + + // Extract chart bytes from form data + f, fh, err := r.FormFile("chart") + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(Error{ + Code: new(int(http.StatusBadRequest)), + Message: new(string("Missing chart")), + }) + return + } + defer f.Close() + + // TODO: validate file type is .tgz + + data, err := io.ReadAll(f) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = 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(name, bytes); err != nil { // TODO: return created version + w.WriteHeader(http.StatusInternalServerError) + _ = 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: new(string(fmt.Sprintf("Chart '%v' created", name))), + }) } diff --git a/internal/storage/backend/fs.go b/internal/storage/backend/fs.go index 34c0d06..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(bytes []byte) error { // TODO: define type `artifact` or similar instead - // TODO: create path if not exists (requires more parameters) - return nil +func (f *FileSystem) Write(name string, bytes []byte) error { // TODO: define type `artifact` or similar instead + 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 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) {