Skip to content

Commit 226c942

Browse files
committed
feat(s3): bucket versioning XML
1 parent 89a69b9 commit 226c942

4 files changed

Lines changed: 143 additions & 1 deletion

File tree

internal/api/s3/handlers.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ func handleCreateBucket(c *fiber.Ctx) error {
112112
if c.Request().URI().QueryArgs().Has("object-lock") {
113113
return handlePutBucketObjectLock(c)
114114
}
115+
if c.Request().URI().QueryArgs().Has("versioning") {
116+
return handlePutBucketVersioning(c)
117+
}
115118

116119
name := c.Params("bucket")
117120

@@ -571,6 +574,9 @@ func handleListObjectsV2(c *fiber.Ctx) error {
571574
if c.Request().URI().QueryArgs().Has("versions") {
572575
return handleListObjectVersions(c)
573576
}
577+
if c.Request().URI().QueryArgs().Has("versioning") {
578+
return handleGetBucketVersioning(c)
579+
}
574580

575581
name := c.Params("bucket")
576582

internal/api/s3/versioning.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package s3
2+
3+
import (
4+
"encoding/xml"
5+
6+
"github.com/anhostfr/hangar/internal/service/auth"
7+
"github.com/anhostfr/hangar/internal/service/bucket"
8+
"github.com/gofiber/fiber/v2"
9+
)
10+
11+
func handleGetBucketVersioning(c *fiber.Ctx) error {
12+
name := c.Params("bucket")
13+
if !hasPerm(c, auth.PermRead) || !keyAllowsBucket(c, name) {
14+
return writeError(c, fiber.StatusForbidden, "AccessDenied", "Access denied", "/"+name)
15+
}
16+
17+
info, err := bucket.GetBucket(name)
18+
if err != nil {
19+
return writeError(c, fiber.StatusNotFound, "NoSuchBucket", err.Error(), "/"+name)
20+
}
21+
22+
out := VersioningConfigurationXML{Xmlns: xmlNamespace}
23+
if info.VersioningEnabled {
24+
out.Status = "Enabled"
25+
}
26+
27+
return writeXML(c, fiber.StatusOK, out)
28+
}
29+
30+
func handlePutBucketVersioning(c *fiber.Ctx) error {
31+
name := c.Params("bucket")
32+
if !hasPerm(c, auth.PermWrite) || !keyAllowsBucket(c, name) {
33+
return writeError(c, fiber.StatusForbidden, "AccessDenied", "Access denied", "/"+name)
34+
}
35+
36+
if _, err := bucket.GetBucket(name); err != nil {
37+
return writeError(c, fiber.StatusNotFound, "NoSuchBucket", err.Error(), "/"+name)
38+
}
39+
40+
body := c.Body()
41+
if len(body) == 0 {
42+
return writeError(c, fiber.StatusBadRequest, "MalformedXML", "empty versioning body", "/"+name)
43+
}
44+
45+
var in VersioningConfigurationXML
46+
if err := xml.Unmarshal(body, &in); err != nil {
47+
return writeError(c, fiber.StatusBadRequest, "MalformedXML", err.Error(), "/"+name)
48+
}
49+
50+
var enabled bool
51+
switch in.Status {
52+
case "Enabled":
53+
enabled = true
54+
case "Suspended":
55+
enabled = false
56+
default:
57+
return writeError(c, fiber.StatusBadRequest, "IllegalVersioningConfigurationException", "invalid Status: must be Enabled or Suspended", "/"+name)
58+
}
59+
60+
if _, err := bucket.UpdateVersioning(name, enabled); err != nil {
61+
return writeError(c, fiber.StatusInternalServerError, "InternalError", err.Error(), "/"+name)
62+
}
63+
64+
return c.SendStatus(fiber.StatusOK)
65+
}

internal/api/s3/versioning_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package s3
2+
3+
import (
4+
"encoding/xml"
5+
"io"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/anhostfr/hangar/internal/service/bucket"
10+
)
11+
12+
func TestS3BucketVersioningXML(t *testing.T) {
13+
s := newS3TestServer(t)
14+
if _, err := bucket.CreateBucket(&bucket.CreateBucketRequest{Name: "vxb"}); err != nil {
15+
t.Fatalf("bucket: %v", err)
16+
}
17+
18+
resp := s.do(t, http.MethodGet, "/vxb", "versioning=", nil)
19+
body, _ := io.ReadAll(resp.Body)
20+
resp.Body.Close()
21+
if resp.StatusCode != http.StatusOK {
22+
t.Fatalf("get initial: %d", resp.StatusCode)
23+
}
24+
var got VersioningConfigurationXML
25+
if err := xml.Unmarshal(body, &got); err != nil {
26+
t.Fatalf("decode: %v", err)
27+
}
28+
if got.Status != "" {
29+
t.Fatalf("expected empty status before enabling, got %q", got.Status)
30+
}
31+
32+
putBody := []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status></VersioningConfiguration>`)
33+
resp = s.do(t, http.MethodPut, "/vxb", "versioning=", putBody)
34+
resp.Body.Close()
35+
if resp.StatusCode != http.StatusOK {
36+
t.Fatalf("put enable: %d", resp.StatusCode)
37+
}
38+
39+
info, err := bucket.GetBucket("vxb")
40+
if err != nil || !info.VersioningEnabled {
41+
t.Fatalf("expected versioning enabled in storage, info=%+v err=%v", info, err)
42+
}
43+
44+
resp = s.do(t, http.MethodGet, "/vxb", "versioning=", nil)
45+
body, _ = io.ReadAll(resp.Body)
46+
resp.Body.Close()
47+
if err := xml.Unmarshal(body, &got); err != nil {
48+
t.Fatalf("decode after enable: %v", err)
49+
}
50+
if got.Status != "Enabled" {
51+
t.Fatalf("status=%q want Enabled", got.Status)
52+
}
53+
54+
putBody = []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Suspended</Status></VersioningConfiguration>`)
55+
resp = s.do(t, http.MethodPut, "/vxb", "versioning=", putBody)
56+
resp.Body.Close()
57+
if resp.StatusCode != http.StatusOK {
58+
t.Fatalf("put suspend: %d", resp.StatusCode)
59+
}
60+
info, _ = bucket.GetBucket("vxb")
61+
if info.VersioningEnabled {
62+
t.Fatalf("expected versioning disabled after Suspended")
63+
}
64+
65+
putBody = []byte(`<VersioningConfiguration><Status>Bogus</Status></VersioningConfiguration>`)
66+
resp = s.do(t, http.MethodPut, "/vxb", "versioning=", putBody)
67+
resp.Body.Close()
68+
if resp.StatusCode != http.StatusBadRequest {
69+
t.Fatalf("expected 400 for bogus status, got %d", resp.StatusCode)
70+
}
71+
}

website/src/content/docs/s3-compatibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Configure SDKs with `UsePathStyle: true` (Go SDK v2), `s3={"addressing_style":"p
3535
| `GetBucketLocation` || Returns the server's configured `region` implicitly through SigV4 |
3636
| `GetBucketAcl` / `PutBucketAcl` || |
3737
| `GetBucketPolicy` / `PutBucketPolicy` || |
38-
| `GetBucketVersioning` / `PutBucketVersioning`| | Versioning is toggled via the native admin API |
38+
| `GetBucketVersioning` / `PutBucketVersioning`| | `VersioningConfiguration` XML with `Status: Enabled` / `Suspended` |
3939
| `GetBucketTagging` / `PutBucketTagging` || |
4040
| `GetBucketLogging` / `PutBucketLogging` || |
4141
| `GetBucketCors` / `PutBucketCors` / `DeleteBucketCors` || See [CORS](/operations/cors/) |

0 commit comments

Comments
 (0)