From eb798be5502639e69aa7d76ddcb4c0ab9217c9b8 Mon Sep 17 00:00:00 2001 From: Andrej Mickov Date: Sun, 22 Feb 2026 13:01:59 +0100 Subject: [PATCH] Updated error handling to be S3 XML compatible. Implemented DeleteObject. --- api/api.go | 73 ++++++++++++++++++++++++++++++-------------- metadata/metadata.go | 54 ++++++++++++++++++++++++++------ models/models.go | 9 ++++++ service/service.go | 2 +- 4 files changed, 104 insertions(+), 34 deletions(-) diff --git a/api/api.go b/api/api.go index de4c548..ab3b772 100644 --- a/api/api.go +++ b/api/api.go @@ -1,7 +1,9 @@ package api import ( + "errors" "fmt" + "fs/metadata" "fs/service" "fs/utils" "io" @@ -46,6 +48,7 @@ func (h *Handler) setupRoutes() { h.router.Get("/{bucket}/*", h.handleGetObject) h.router.Put("/{bucket}/*", h.handlePutObject) h.router.Head("/{bucket}/*", h.handleHeadObject) + h.router.Delete("/{bucket}/*", h.handleDeleteObject) } func (h *Handler) handleWelcome(w http.ResponseWriter, r *http.Request) { @@ -59,22 +62,27 @@ func (h *Handler) handleWelcome(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleGetObject(w http.ResponseWriter, r *http.Request) { bucket := chi.URLParam(r, "bucket") key := chi.URLParam(r, "*") + if key == "" { - http.Error(w, "object key is required", http.StatusBadRequest) + writeS3Error(w, r, s3ErrInvalidObjectKey, r.URL.Path) return } + if r.URL.Query().Get("uploadId") != "" { + + } + stream, manifest, err := h.svc.GetObject(bucket, key) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeMappedS3Error(w, r, err) 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("ETag", `"`+manifest.ETag+`"`) + w.Header().Set("Last-Modified", time.Unix(manifest.CreatedAt, 0).UTC().Format(http.TimeFormat)) 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) @@ -84,7 +92,7 @@ func (h *Handler) handlePutObject(w http.ResponseWriter, r *http.Request) { bucket := chi.URLParam(r, "bucket") key := chi.URLParam(r, "*") if key == "" { - http.Error(w, "object key is required", http.StatusBadRequest) + writeS3Error(w, r, s3ErrInvalidObjectKey, r.URL.Path) return } @@ -97,11 +105,11 @@ func (h *Handler) handlePutObject(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeMappedS3Error(w, r, err) return } - w.Header().Set("ETag", manifest.ETag) + w.Header().Set("ETag", `"`+manifest.ETag+`"`) w.Header().Set("Content-Length", "0") w.WriteHeader(http.StatusOK) @@ -111,26 +119,26 @@ func (h *Handler) handleHeadObject(w http.ResponseWriter, r *http.Request) { bucket := chi.URLParam(r, "bucket") key := chi.URLParam(r, "*") if key == "" { - http.Error(w, "object key is required", http.StatusBadRequest) + writeS3Error(w, r, s3ErrInvalidObjectKey, r.URL.Path) return } manifest, err := h.svc.HeadObject(bucket, key) if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) + writeMappedS3Error(w, r, err) return } - w.Header().Set("ETag", manifest.ETag) + w.Header().Set("ETag", `"`+manifest.ETag+`"`) w.Header().Set("Content-Length", "0") - w.Header().Set("Last-Modified", time.Unix(manifest.CreatedAt, 0).UTC().Format(time.RFC1123)) + w.Header().Set("Last-Modified", time.Unix(manifest.CreatedAt, 0).UTC().Format(http.TimeFormat)) w.WriteHeader(http.StatusOK) } func (h *Handler) handlePutBucket(w http.ResponseWriter, r *http.Request) { bucket := chi.URLParam(r, "bucket") - if h.svc.CreateBucket(bucket) != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + if err := h.svc.CreateBucket(bucket); err != nil { + writeMappedS3Error(w, r, err) return } w.WriteHeader(http.StatusCreated) @@ -138,17 +146,37 @@ func (h *Handler) handlePutBucket(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleDeleteBucket(w http.ResponseWriter, r *http.Request) { bucket := chi.URLParam(r, "bucket") - if h.svc.DeleteBucket(bucket) != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + if err := h.svc.DeleteBucket(bucket); err != nil { + writeMappedS3Error(w, r, err) return } - w.WriteHeader(http.StatusAccepted) + w.WriteHeader(http.StatusNoContent) +} + +func (h *Handler) handleDeleteObject(w http.ResponseWriter, r *http.Request) { + bucket := chi.URLParam(r, "bucket") + key := chi.URLParam(r, "*") + if key == "" { + writeS3Error(w, r, s3ErrInvalidObjectKey, r.URL.Path) + return + } + + err := h.svc.DeleteObject(bucket, key) + if err != nil { + if errors.Is(err, metadata.ErrObjectNotFound) { + w.WriteHeader(http.StatusNoContent) + return + } + writeMappedS3Error(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) } func (h *Handler) handleHeadBucket(w http.ResponseWriter, r *http.Request) { bucket := chi.URLParam(r, "bucket") - if h.svc.HeadBucket(bucket) != nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + if err := h.svc.HeadBucket(bucket); err != nil { + writeMappedS3Error(w, r, err) return } w.WriteHeader(http.StatusOK) @@ -157,7 +185,7 @@ func (h *Handler) handleHeadBucket(w http.ResponseWriter, r *http.Request) { func (h *Handler) handleGetBuckets(w http.ResponseWriter, r *http.Request) { buckets, err := h.svc.ListBuckets() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeMappedS3Error(w, r, err) return } w.Header().Set("Content-Type", "application/xml") @@ -178,20 +206,20 @@ func (h *Handler) handleGetBucket(w http.ResponseWriter, r *http.Request) { h.handleListObjectsV2(w, r, bucket, prefix) return } - http.Error(w, "NotImplemented", http.StatusNotImplemented) + writeS3Error(w, r, s3ErrNotImplemented, r.URL.Path) } func (h *Handler) handleListObjectsV2(w http.ResponseWriter, r *http.Request, bucket, prefix string) { objects, err := h.svc.ListObjects(bucket, prefix) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeMappedS3Error(w, r, err) return } xmlResponse, err := utils.ConstructXMLResponseForObjectList(bucket, objects) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + writeMappedS3Error(w, r, err) return } @@ -200,7 +228,6 @@ func (h *Handler) handleListObjectsV2(w http.ResponseWriter, r *http.Request, bu w.WriteHeader(http.StatusOK) _, err = w.Write([]byte(xmlResponse)) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/metadata/metadata.go b/metadata/metadata.go index 7857962..07a2344 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -20,6 +20,14 @@ var systemIndex = []byte("__SYSTEM_BUCKETS__") var validBucketName = regexp.MustCompile(`^[a-z0-9.-]{3,63}$`) +var ( + ErrInvalidBucketName = errors.New("invalid bucket name") + ErrBucketAlreadyExists = errors.New("bucket already exists") + ErrBucketNotFound = errors.New("bucket not found") + ErrBucketNotEmpty = errors.New("bucket not empty") + ErrObjectNotFound = errors.New("object not found") +) + func NewMetadataHandler(dbPath string) (*MetadataHandler, error) { db, err := bbolt.Open(dbPath, 0600, nil) if err != nil { @@ -41,7 +49,7 @@ func NewMetadataHandler(dbPath string) (*MetadataHandler, error) { func (h *MetadataHandler) CreateBucket(bucketName string) error { if !validBucketName.MatchString(bucketName) { - return fmt.Errorf("invalid bucket name: %s", bucketName) + return fmt.Errorf("%w: %s", ErrInvalidBucketName, bucketName) } err := h.db.Update(func(tx *bbolt.Tx) error { @@ -50,7 +58,7 @@ func (h *MetadataHandler) CreateBucket(bucketName string) error { return err } if indexBucket.Get([]byte(bucketName)) != nil { - return fmt.Errorf("bucket %s already exists", bucketName) + return fmt.Errorf("%w: %s", ErrBucketAlreadyExists, bucketName) } _, err = tx.CreateBucketIfNotExists([]byte(bucketName)) @@ -73,7 +81,7 @@ func (h *MetadataHandler) CreateBucket(bucketName string) error { func (h *MetadataHandler) DeleteBucket(bucketName string) error { if !validBucketName.MatchString(bucketName) { - return fmt.Errorf("invalid bucket name: %s", bucketName) + return fmt.Errorf("%w: %s", ErrInvalidBucketName, bucketName) } err := h.db.Update(func(tx *bbolt.Tx) error { @@ -82,7 +90,14 @@ func (h *MetadataHandler) DeleteBucket(bucketName string) error { return err } if indexBucket.Get([]byte(bucketName)) == nil { - return fmt.Errorf("bucket %s not found", bucketName) + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucketName) + } + metadataBucket := tx.Bucket([]byte(bucketName)) + if metadataBucket == nil { + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucketName) + } + if k, _ := metadataBucket.Cursor().First(); k != nil { + return fmt.Errorf("%w: %s", ErrBucketNotEmpty, bucketName) } if err := tx.DeleteBucket([]byte(bucketName)); err != nil && !errors.Is(err, bbolt.ErrBucketNotFound) { return fmt.Errorf("error deleting metadata bucket %s: %w", bucketName, err) @@ -127,7 +142,7 @@ func (h *MetadataHandler) GetBucketManifest(bucketName string) (*models.BucketMa } data := systemIndexBucket.Get([]byte(bucketName)) if data == nil { - return fmt.Errorf("bucket manifest not found for bucket %s", bucketName) + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucketName) } err := json.Unmarshal(data, &manifest) if err != nil { @@ -157,7 +172,7 @@ func (h *MetadataHandler) PutManifest(manifest *models.ObjectManifest) error { } metadataBucket := tx.Bucket([]byte(bucket)) if metadataBucket == nil { - return fmt.Errorf("metadata bucket %s not found; create it first", bucket) + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucket) } return metadataBucket.Put([]byte(key), data) }) @@ -173,12 +188,12 @@ func (h *MetadataHandler) GetManifest(bucket, key string) (*models.ObjectManifes err := h.db.View(func(tx *bbolt.Tx) error { metadataBucket := tx.Bucket([]byte(bucket)) if metadataBucket == nil { - return fmt.Errorf("bucket %s not found", bucket) + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucket) } data := metadataBucket.Get([]byte(key)) if data == nil { - return fmt.Errorf("manifest not found for bucket %s and key %s", bucket, key) + return fmt.Errorf("%w: %s/%s", ErrObjectNotFound, bucket, key) } err := json.Unmarshal(data, &manifest) if err != nil { @@ -203,11 +218,11 @@ func (h *MetadataHandler) ListObjects(bucket, prefix string) ([]*models.ObjectMa return errors.New("system index not found") } if systemIndexBucket.Get([]byte(bucket)) == nil { - return fmt.Errorf("bucket %s not found", bucket) + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucket) } _bucket := tx.Bucket([]byte(bucket)) if _bucket == nil { - return fmt.Errorf("bucket %s not found", bucket) + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucket) } err := _bucket.ForEach(func(k, v []byte) error { if prefix != "" && !strings.HasPrefix(string(k), prefix) { @@ -231,3 +246,22 @@ func (h *MetadataHandler) ListObjects(bucket, prefix string) ([]*models.ObjectMa } return objects, nil } + +func (h *MetadataHandler) DeleteManifest(bucket, key string) error { + if _, err := h.GetManifest(bucket, key); err != nil { + return err + } + + err := h.db.Update(func(tx *bbolt.Tx) error { + metadataBucket := tx.Bucket([]byte(bucket)) + if metadataBucket == nil { + return fmt.Errorf("%w: %s", ErrBucketNotFound, bucket) + } + return metadataBucket.Delete([]byte(key)) + }) + if err != nil { + return err + } + return nil + +} diff --git a/models/models.go b/models/models.go index 0dcaae8..78847bb 100644 --- a/models/models.go +++ b/models/models.go @@ -24,6 +24,15 @@ type BucketManifest struct { PublicAccessBlock bool `json:"public_access_block"` } +type S3ErrorResponse struct { + XMLName xml.Name `xml:"Error"` + Code string `xml:"Code"` + Message string `xml:"Message"` + Resource string `xml:"Resource,omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` +} + type ListBucketResult struct { XMLName xml.Name `xml:"ListBucketResult"` Xmlns string `xml:"xmlns,attr"` diff --git a/service/service.go b/service/service.go index eab434f..574fc03 100644 --- a/service/service.go +++ b/service/service.go @@ -74,7 +74,7 @@ func (s *ObjectService) HeadObject(bucket, key string) (models.ObjectManifest, e } func (s *ObjectService) DeleteObject(bucket, key string) error { - return nil + return s.metadataHandler.DeleteManifest(bucket, key) } func (s *ObjectService) ListObjects(bucket, prefix string) ([]*models.ObjectManifest, error) {