Updated error handling to be S3 XML compatible. Implemented DeleteObject.

This commit is contained in:
2026-02-22 13:01:59 +01:00
parent b19c24d9b7
commit eb798be550
4 changed files with 104 additions and 34 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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) {