From f151f8055a194135f6b32827e808d74ae7f9c629 Mon Sep 17 00:00:00 2001 From: Andrej Mickov Date: Sat, 21 Feb 2026 11:55:14 +0100 Subject: [PATCH] Working simple PUT/GET API --- api/api.go | 95 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + main.go | 36 ++--------------- metadata/metadata.go | 5 ++- service/service.go | 13 +++--- storage/blob.go | 7 ++-- 7 files changed, 116 insertions(+), 43 deletions(-) diff --git a/api/api.go b/api/api.go index 778f64e..b82fddf 100644 --- a/api/api.go +++ b/api/api.go @@ -1 +1,96 @@ package api + +import ( + "fmt" + "fs/service" + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +type Handler struct { + router *chi.Mux + svc *service.ObjectService +} + +func NewHandler(svc *service.ObjectService) *Handler { + r := chi.NewRouter() + r.Use(middleware.Recoverer) + + h := &Handler{ + router: r, + svc: svc, + } + return h +} + +func (h *Handler) setupRoutes() { + h.router.Use(middleware.Logger) + h.router.Get("/", h.handleWelcome) + h.router.Get("/*", h.handleGetObject) + h.router.Put("/*", h.handlePutObject) +} + +func (h *Handler) handleWelcome(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("Welcome to the Object Storage API!")) + if err != nil { + return + } +} + +func (h *Handler) handleGetObject(w http.ResponseWriter, r *http.Request) { + urlParams := chi.URLParam(r, "*") + bucket := strings.Split(urlParams, "/")[0] + key := strings.Join(strings.Split(urlParams, "/")[1:], "/") + + stream, manifest, err := h.svc.GetObject(bucket, key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", manifest.ContentType) + w.Header().Set("Content-Length", strconv.FormatInt(manifest.Size, 10)) + w.Header().Set("ETag", manifest.ETag) + w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set("Last-Modified", time.Unix(manifest.CreatedAt, 0).UTC().Format(time.RFC1123)) + w.WriteHeader(http.StatusOK) + _, err = io.Copy(w, stream) + +} + +func (h *Handler) handlePutObject(w http.ResponseWriter, r *http.Request) { + urlParams := chi.URLParam(r, "*") + bucket := strings.Split(urlParams, "/")[0] + key := strings.Join(strings.Split(urlParams, "/")[1:], "/") + + contentType := r.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/octet-stream" + } + + manifest, err := h.svc.PutObject(bucket, key, contentType, r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("ETag", manifest.ETag) + w.Header().Set("Content-Length", "0") + + w.WriteHeader(http.StatusOK) +} + +func (h *Handler) Start(address string) error { + fmt.Printf("Starting API server on %s\n", address) + h.setupRoutes() + return http.ListenAndServe(address, h.router) +} diff --git a/go.mod b/go.mod index 81e9a66..020cd73 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module fs go 1.25.7 require ( + github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/reedsolomon v1.13.2 // indirect go.etcd.io/bbolt v1.4.3 // indirect diff --git a/go.sum b/go.sum index b54a6cc..d4bfca0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/reedsolomon v1.13.2 h1:9qtQy2tKEVpVB8Pfq87ZljHZb60/LbeTQ1OxV8EGzdE= diff --git a/main.go b/main.go index caf974d..74d37c7 100644 --- a/main.go +++ b/main.go @@ -2,20 +2,12 @@ package main import ( "fmt" + "fs/api" "fs/metadata" "fs/service" - "io" - "os" ) func main() { - fmt.Println("Hello, World!") - imageStream, err := os.Open("fer.jpg") - if err != nil { - fmt.Printf("Error opening image stream: %v\n", err) - return - } - defer imageStream.Close() metadataHandler, err := metadata.NewMetadataHandler("metadata.db") if err != nil { @@ -24,31 +16,9 @@ func main() { } objectService := service.NewObjectService(metadataHandler) - - manifest, err := objectService.PutObject("test-bucket-ferdzo/fer.jpg", "image/jpeg", imageStream) + handler := api.NewHandler(objectService) + err = handler.Start("localhost:3000") if err != nil { - fmt.Printf("Error ingesting stream: %v\n", err) return } - fmt.Printf("Manifest: %+v\n", manifest) - - objectData, manifest2, err := objectService.GetObject("test-bucket-ferdzo", "fer.jpg") - if err != nil { - fmt.Printf("Error retrieving object: %v\n", err) - return - } - fmt.Printf("Retrieved manifest: %+v\n", manifest2) - recoveredFile, err := os.Create("recovered_" + manifest2.Key) - if err != nil { - fmt.Printf("Error creating file: %v\n", err) - return - } - defer recoveredFile.Close() - - bytesWritten, err := io.Copy(recoveredFile, objectData) - if err != nil { - fmt.Printf("Error streaming to recovered file: %v\n", err) - return - } - fmt.Printf("Successfully streamed %d bytes to disk!\n", bytesWritten) } diff --git a/metadata/metadata.go b/metadata/metadata.go index 7e4ef57..fbf6116 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -44,7 +44,7 @@ func (h *MetadataHandler) PutManifest(manifest *models.ObjectManifest) error { func (h *MetadataHandler) GetManifest(bucket, key string) (*models.ObjectManifest, error) { var manifest *models.ObjectManifest - h.db.View(func(tx *bbolt.Tx) error { + err := h.db.View(func(tx *bbolt.Tx) error { metadataBucket := tx.Bucket([]byte(ManifestBucketName)) if metadataBucket == nil { return fmt.Errorf("bucket %s not found", ManifestBucketName) @@ -61,6 +61,9 @@ func (h *MetadataHandler) GetManifest(bucket, key string) (*models.ObjectManifes } return nil }) + if err != nil { + return nil, err + } return manifest, nil } diff --git a/service/service.go b/service/service.go index 1750430..1f1500d 100644 --- a/service/service.go +++ b/service/service.go @@ -6,7 +6,6 @@ import ( "fs/models" "fs/storage" "io" - "strings" "time" ) @@ -18,10 +17,7 @@ func NewObjectService(metadataHandler *metadata.MetadataHandler) *ObjectService return &ObjectService{metadataHandler: metadataHandler} } -func (s *ObjectService) PutObject(uri string, contentType string, input io.Reader) (*models.ObjectManifest, error) { - - bucket := strings.Split(uri, "/")[0] - key := strings.Join(strings.Split(uri, "/")[1:], "/") +func (s *ObjectService) PutObject(bucket, key, contentType string, input io.Reader) (*models.ObjectManifest, error) { chunks, size, etag, err := storage.IngestStream(input) if err != nil { @@ -54,7 +50,12 @@ func (s *ObjectService) GetObject(bucket, key string) (io.ReadCloser, *models.Ob pr, pw := io.Pipe() go func() { - defer pw.Close() + defer func(pw *io.PipeWriter) { + err := pw.Close() + if err != nil { + + } + }(pw) err := storage.AssembleStream(manifest.Chunks, pw) if err != nil { diff --git a/storage/blob.go b/storage/blob.go index e56e06f..268f762 100644 --- a/storage/blob.go +++ b/storage/blob.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "crypto/sha256" "encoding/hex" + "errors" "io" "os" "path/filepath" @@ -21,7 +22,7 @@ func IngestStream(stream io.Reader) ([]string, int64, string, error) { for { bytesRead, err := io.ReadFull(stream, buffer) - if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + if err != nil && err != io.EOF && !errors.Is(err, io.ErrUnexpectedEOF) { return nil, 0, "", err } @@ -40,7 +41,7 @@ func IngestStream(stream io.Reader) ([]string, int64, string, error) { } chunkIDs = append(chunkIDs, chunkID) } - if err == io.EOF || err == io.ErrUnexpectedEOF { + if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { break } if err != nil { @@ -48,7 +49,7 @@ func IngestStream(stream io.Reader) ([]string, int64, string, error) { } } - + etag := hex.EncodeToString(fullFileHasher.Sum(nil)) return chunkIDs, totalSize, etag, nil }