Browse Source
s3tables: complete s3tables package implementation
s3tables: complete s3tables package implementation
- namespace.go: namespace CRUD operations (310 lines) - table.go: table CRUD operations with Iceberg schema support (409 lines) - policy.go: resource policies and tagging operations (419 lines) - types.go: request/response types and error definitions (290 lines) - All handlers updated to use standalone utilities from utils.go - All files follow single responsibility principlepull/8147/head
4 changed files with 1428 additions and 0 deletions
-
310weed/s3api/s3tables/namespace.go
-
419weed/s3api/s3tables/policy.go
-
409weed/s3api/s3tables/table.go
-
290weed/s3api/s3tables/types.go
@ -0,0 +1,310 @@ |
|||
package s3tables |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
// handleCreateNamespace creates a new namespace in a table bucket
|
|||
func (h *S3TablesHandler) handleCreateNamespace(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req CreateNamespaceRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
if len(req.Namespace) == 0 { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "namespace is required") |
|||
return fmt.Errorf("namespace is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
// For simplicity, use the first namespace element as the directory name
|
|||
namespaceName := req.Namespace[0] |
|||
|
|||
// Validate namespace name
|
|||
if len(namespaceName) < 1 || len(namespaceName) > 255 { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "namespace name must be between 1 and 255 characters") |
|||
return fmt.Errorf("invalid namespace name length") |
|||
} |
|||
|
|||
// Check if table bucket exists
|
|||
bucketPath := getTableBucketPath(bucketName) |
|||
var bucketExists bool |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, bucketPath, ExtendedKeyMetadata) |
|||
bucketExists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if !bucketExists { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName)) |
|||
return fmt.Errorf("bucket not found") |
|||
} |
|||
|
|||
namespacePath := getNamespacePath(bucketName, namespaceName) |
|||
|
|||
// Check if namespace already exists
|
|||
exists := false |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, namespacePath, ExtendedKeyMetadata) |
|||
exists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if exists { |
|||
h.writeError(w, http.StatusConflict, ErrCodeNamespaceAlreadyExists, fmt.Sprintf("namespace %s already exists", namespaceName)) |
|||
return fmt.Errorf("namespace already exists") |
|||
} |
|||
|
|||
// Create the namespace
|
|||
now := time.Now() |
|||
metadata := &namespaceMetadata{ |
|||
Namespace: req.Namespace, |
|||
CreatedAt: now, |
|||
OwnerID: h.accountID, |
|||
} |
|||
|
|||
metadataBytes, _ := json.Marshal(metadata) |
|||
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
// Create namespace directory
|
|||
if err := h.createDirectory(client, namespacePath); err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Set metadata as extended attribute
|
|||
if err := h.setExtendedAttribute(client, namespacePath, ExtendedKeyMetadata, metadataBytes); err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to create namespace") |
|||
return err |
|||
} |
|||
|
|||
resp := &CreateNamespaceResponse{ |
|||
Namespace: req.Namespace, |
|||
TableBucketARN: req.TableBucketARN, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleGetNamespace gets details of a namespace
|
|||
func (h *S3TablesHandler) handleGetNamespace(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req GetNamespaceRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" || req.Namespace == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN and namespace are required") |
|||
return fmt.Errorf("tableBucketARN and namespace are required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
namespacePath := getNamespacePath(bucketName, req.Namespace) |
|||
|
|||
var metadata namespaceMetadata |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
data, err := h.getExtendedAttribute(client, namespacePath, ExtendedKeyMetadata) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
return json.Unmarshal(data, &metadata) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", req.Namespace)) |
|||
return err |
|||
} |
|||
|
|||
resp := &GetNamespaceResponse{ |
|||
Namespace: metadata.Namespace, |
|||
CreatedAt: metadata.CreatedAt, |
|||
OwnerAccountID: metadata.OwnerID, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleListNamespaces lists all namespaces in a table bucket
|
|||
func (h *S3TablesHandler) handleListNamespaces(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req ListNamespacesRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
maxNamespaces := req.MaxNamespaces |
|||
if maxNamespaces <= 0 { |
|||
maxNamespaces = 100 |
|||
} |
|||
|
|||
bucketPath := getTableBucketPath(bucketName) |
|||
var namespaces []NamespaceSummary |
|||
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
resp, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ |
|||
Directory: bucketPath, |
|||
Limit: uint32(maxNamespaces), |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
for { |
|||
entry, err := resp.Recv() |
|||
if err != nil { |
|||
break |
|||
} |
|||
|
|||
if entry.Entry == nil || !entry.Entry.IsDirectory { |
|||
continue |
|||
} |
|||
|
|||
// Skip hidden entries
|
|||
if strings.HasPrefix(entry.Entry.Name, ".") { |
|||
continue |
|||
} |
|||
|
|||
// Apply prefix filter
|
|||
if req.Prefix != "" && !strings.HasPrefix(entry.Entry.Name, req.Prefix) { |
|||
continue |
|||
} |
|||
|
|||
// Read metadata from extended attribute
|
|||
data, ok := entry.Entry.Extended[ExtendedKeyMetadata] |
|||
if !ok { |
|||
continue |
|||
} |
|||
|
|||
var metadata namespaceMetadata |
|||
if err := json.Unmarshal(data, &metadata); err != nil { |
|||
continue |
|||
} |
|||
|
|||
namespaces = append(namespaces, NamespaceSummary{ |
|||
Namespace: metadata.Namespace, |
|||
CreatedAt: metadata.CreatedAt, |
|||
}) |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
namespaces = []NamespaceSummary{} |
|||
} |
|||
|
|||
resp := &ListNamespacesResponse{ |
|||
Namespaces: namespaces, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleDeleteNamespace deletes a namespace from a table bucket
|
|||
func (h *S3TablesHandler) handleDeleteNamespace(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req DeleteNamespaceRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" || req.Namespace == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN and namespace are required") |
|||
return fmt.Errorf("tableBucketARN and namespace are required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
namespacePath := getNamespacePath(bucketName, req.Namespace) |
|||
|
|||
// Check if namespace exists and is empty
|
|||
hasChildren := false |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
resp, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ |
|||
Directory: namespacePath, |
|||
Limit: 10, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
for { |
|||
entry, err := resp.Recv() |
|||
if err != nil { |
|||
break |
|||
} |
|||
if entry.Entry != nil && !strings.HasPrefix(entry.Entry.Name, ".") { |
|||
hasChildren = true |
|||
break |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if hasChildren { |
|||
h.writeError(w, http.StatusConflict, ErrCodeNamespaceNotEmpty, "namespace is not empty") |
|||
return fmt.Errorf("namespace not empty") |
|||
} |
|||
|
|||
// Delete the namespace
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.deleteDirectory(client, namespacePath) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to delete namespace") |
|||
return err |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
@ -0,0 +1,419 @@ |
|||
package s3tables |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
// handlePutTableBucketPolicy puts a policy on a table bucket
|
|||
func (h *S3TablesHandler) handlePutTableBucketPolicy(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req PutTableBucketPolicyRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
if req.ResourcePolicy == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "resourcePolicy is required") |
|||
return fmt.Errorf("resourcePolicy is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
// Check if bucket exists
|
|||
bucketPath := getTableBucketPath(bucketName) |
|||
var bucketExists bool |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, bucketPath, ExtendedKeyMetadata) |
|||
bucketExists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if !bucketExists { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName)) |
|||
return fmt.Errorf("bucket not found") |
|||
} |
|||
|
|||
// Write policy
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.setExtendedAttribute(client, bucketPath, ExtendedKeyPolicy, []byte(req.ResourcePolicy)) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to put table bucket policy") |
|||
return err |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
|
|||
// handleGetTableBucketPolicy gets the policy of a table bucket
|
|||
func (h *S3TablesHandler) handleGetTableBucketPolicy(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req GetTableBucketPolicyRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
bucketPath := getTableBucketPath(bucketName) |
|||
var policy []byte |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
var readErr error |
|||
policy, readErr = h.getExtendedAttribute(client, bucketPath, ExtendedKeyPolicy) |
|||
return readErr |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchPolicy, "table bucket policy not found") |
|||
return err |
|||
} |
|||
|
|||
resp := &GetTableBucketPolicyResponse{ |
|||
ResourcePolicy: string(policy), |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleDeleteTableBucketPolicy deletes the policy of a table bucket
|
|||
func (h *S3TablesHandler) handleDeleteTableBucketPolicy(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req DeleteTableBucketPolicyRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
bucketPath := getTableBucketPath(bucketName) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.deleteExtendedAttribute(client, bucketPath, ExtendedKeyPolicy) |
|||
}) |
|||
|
|||
if err != nil { |
|||
// Ignore error if policy doesn't exist
|
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
|
|||
// handlePutTablePolicy puts a policy on a table
|
|||
func (h *S3TablesHandler) handlePutTablePolicy(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req PutTablePolicyRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" || req.Namespace == "" || req.Name == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, namespace, and name are required") |
|||
return fmt.Errorf("missing required parameters") |
|||
} |
|||
|
|||
if req.ResourcePolicy == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "resourcePolicy is required") |
|||
return fmt.Errorf("resourcePolicy is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
// Check if table exists
|
|||
tablePath := getTablePath(bucketName, req.Namespace, req.Name) |
|||
var tableExists bool |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, tablePath, ExtendedKeyMetadata) |
|||
tableExists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if !tableExists { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", req.Name)) |
|||
return fmt.Errorf("table not found") |
|||
} |
|||
|
|||
// Write policy
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.setExtendedAttribute(client, tablePath, ExtendedKeyPolicy, []byte(req.ResourcePolicy)) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to put table policy") |
|||
return err |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
|
|||
// handleGetTablePolicy gets the policy of a table
|
|||
func (h *S3TablesHandler) handleGetTablePolicy(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req GetTablePolicyRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" || req.Namespace == "" || req.Name == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, namespace, and name are required") |
|||
return fmt.Errorf("missing required parameters") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
tablePath := getTablePath(bucketName, req.Namespace, req.Name) |
|||
var policy []byte |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
var readErr error |
|||
policy, readErr = h.getExtendedAttribute(client, tablePath, ExtendedKeyPolicy) |
|||
return readErr |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchPolicy, "table policy not found") |
|||
return err |
|||
} |
|||
|
|||
resp := &GetTablePolicyResponse{ |
|||
ResourcePolicy: string(policy), |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleDeleteTablePolicy deletes the policy of a table
|
|||
func (h *S3TablesHandler) handleDeleteTablePolicy(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req DeleteTablePolicyRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" || req.Namespace == "" || req.Name == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, namespace, and name are required") |
|||
return fmt.Errorf("missing required parameters") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
tablePath := getTablePath(bucketName, req.Namespace, req.Name) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.deleteExtendedAttribute(client, tablePath, ExtendedKeyPolicy) |
|||
}) |
|||
|
|||
if err != nil { |
|||
// Ignore error if policy doesn't exist
|
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
|
|||
// handleTagResource adds tags to a resource
|
|||
func (h *S3TablesHandler) handleTagResource(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req TagResourceRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.ResourceARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "resourceArn is required") |
|||
return fmt.Errorf("resourceArn is required") |
|||
} |
|||
|
|||
if len(req.Tags) == 0 { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tags are required") |
|||
return fmt.Errorf("tags are required") |
|||
} |
|||
|
|||
// Parse resource ARN to determine if it's a bucket or table
|
|||
resourcePath, extendedKey, err := h.resolveResourcePath(req.ResourceARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
// Read existing tags and merge
|
|||
existingTags := make(map[string]string) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
data, err := h.getExtendedAttribute(client, resourcePath, extendedKey) |
|||
if err == nil { |
|||
json.Unmarshal(data, &existingTags) |
|||
} |
|||
return nil |
|||
}) |
|||
|
|||
// Merge new tags
|
|||
for k, v := range req.Tags { |
|||
existingTags[k] = v |
|||
} |
|||
|
|||
// Write merged tags
|
|||
tagsBytes, _ := json.Marshal(existingTags) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.setExtendedAttribute(client, resourcePath, extendedKey, tagsBytes) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to tag resource") |
|||
return err |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
|
|||
// handleListTagsForResource lists tags for a resource
|
|||
func (h *S3TablesHandler) handleListTagsForResource(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req ListTagsForResourceRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.ResourceARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "resourceArn is required") |
|||
return fmt.Errorf("resourceArn is required") |
|||
} |
|||
|
|||
resourcePath, extendedKey, err := h.resolveResourcePath(req.ResourceARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
tags := make(map[string]string) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
data, err := h.getExtendedAttribute(client, resourcePath, extendedKey) |
|||
if err != nil { |
|||
return nil // Return empty tags if not found
|
|||
} |
|||
return json.Unmarshal(data, &tags) |
|||
}) |
|||
|
|||
resp := &ListTagsForResourceResponse{ |
|||
Tags: tags, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleUntagResource removes tags from a resource
|
|||
func (h *S3TablesHandler) handleUntagResource(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req UntagResourceRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.ResourceARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "resourceArn is required") |
|||
return fmt.Errorf("resourceArn is required") |
|||
} |
|||
|
|||
if len(req.TagKeys) == 0 { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tagKeys are required") |
|||
return fmt.Errorf("tagKeys are required") |
|||
} |
|||
|
|||
resourcePath, extendedKey, err := h.resolveResourcePath(req.ResourceARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
// Read existing tags
|
|||
tags := make(map[string]string) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
data, err := h.getExtendedAttribute(client, resourcePath, extendedKey) |
|||
if err != nil { |
|||
return nil |
|||
} |
|||
return json.Unmarshal(data, &tags) |
|||
}) |
|||
|
|||
// Remove specified tags
|
|||
for _, key := range req.TagKeys { |
|||
delete(tags, key) |
|||
} |
|||
|
|||
// Write updated tags
|
|||
tagsBytes, _ := json.Marshal(tags) |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.setExtendedAttribute(client, resourcePath, extendedKey, tagsBytes) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to untag resource") |
|||
return err |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
|
|||
// resolveResourcePath determines the resource path and extended attribute key from a resource ARN
|
|||
func (h *S3TablesHandler) resolveResourcePath(resourceARN string) (path string, key string, err error) { |
|||
// Try parsing as table ARN first
|
|||
bucketName, namespace, tableName, err := parseTableFromARN(resourceARN) |
|||
if err == nil { |
|||
return getTablePath(bucketName, namespace, tableName), ExtendedKeyTags, nil |
|||
} |
|||
|
|||
// Try parsing as bucket ARN
|
|||
bucketName, err = parseBucketNameFromARN(resourceARN) |
|||
if err == nil { |
|||
return getTableBucketPath(bucketName), ExtendedKeyTags, nil |
|||
} |
|||
|
|||
return "", "", fmt.Errorf("invalid resource ARN: %s", resourceARN) |
|||
} |
|||
@ -0,0 +1,409 @@ |
|||
package s3tables |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
// handleCreateTable creates a new table in a namespace
|
|||
func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req CreateTableRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
if req.Namespace == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "namespace is required") |
|||
return fmt.Errorf("namespace is required") |
|||
} |
|||
|
|||
if req.Name == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "name is required") |
|||
return fmt.Errorf("name is required") |
|||
} |
|||
|
|||
if req.Format == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "format is required") |
|||
return fmt.Errorf("format is required") |
|||
} |
|||
|
|||
// Validate format
|
|||
if req.Format != "ICEBERG" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "only ICEBERG format is supported") |
|||
return fmt.Errorf("invalid format") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
// Validate table name
|
|||
if len(req.Name) < 1 || len(req.Name) > 255 { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "table name must be between 1 and 255 characters") |
|||
return fmt.Errorf("invalid table name length") |
|||
} |
|||
|
|||
// Check if namespace exists
|
|||
namespacePath := getNamespacePath(bucketName, req.Namespace) |
|||
var namespaceExists bool |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, namespacePath, ExtendedKeyMetadata) |
|||
namespaceExists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if !namespaceExists { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", req.Namespace)) |
|||
return fmt.Errorf("namespace not found") |
|||
} |
|||
|
|||
tablePath := getTablePath(bucketName, req.Namespace, req.Name) |
|||
|
|||
// Check if table already exists
|
|||
exists := false |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, tablePath, ExtendedKeyMetadata) |
|||
exists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if exists { |
|||
h.writeError(w, http.StatusConflict, ErrCodeTableAlreadyExists, fmt.Sprintf("table %s already exists", req.Name)) |
|||
return fmt.Errorf("table already exists") |
|||
} |
|||
|
|||
// Create the table
|
|||
now := time.Now() |
|||
versionToken := generateVersionToken() |
|||
|
|||
metadata := &tableMetadataInternal{ |
|||
Name: req.Name, |
|||
Namespace: req.Namespace, |
|||
Format: req.Format, |
|||
CreatedAt: now, |
|||
ModifiedAt: now, |
|||
OwnerID: h.accountID, |
|||
VersionToken: versionToken, |
|||
Schema: req.Metadata, |
|||
} |
|||
|
|||
metadataBytes, _ := json.Marshal(metadata) |
|||
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
// Create table directory
|
|||
if err := h.createDirectory(client, tablePath); err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Create data subdirectory for Iceberg files
|
|||
dataPath := tablePath + "/data" |
|||
if err := h.createDirectory(client, dataPath); err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Set metadata as extended attribute
|
|||
if err := h.setExtendedAttribute(client, tablePath, ExtendedKeyMetadata, metadataBytes); err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Set tags if provided
|
|||
if len(req.Tags) > 0 { |
|||
tagsBytes, _ := json.Marshal(req.Tags) |
|||
if err := h.setExtendedAttribute(client, tablePath, ExtendedKeyTags, tagsBytes); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to create table") |
|||
return err |
|||
} |
|||
|
|||
tableARN := h.generateTableARN(bucketName, req.Namespace, req.Name) |
|||
|
|||
resp := &CreateTableResponse{ |
|||
TableARN: tableARN, |
|||
VersionToken: versionToken, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleGetTable gets details of a table
|
|||
func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req GetTableRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
var bucketName, namespace, tableName string |
|||
var err error |
|||
|
|||
// Support getting by ARN or by bucket/namespace/name
|
|||
if req.TableARN != "" { |
|||
bucketName, namespace, tableName, err = parseTableFromARN(req.TableARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
} else if req.TableBucketARN != "" && req.Namespace != "" && req.Name != "" { |
|||
bucketName, err = parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
namespace = req.Namespace |
|||
tableName = req.Name |
|||
} else { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "either tableARN or (tableBucketARN, namespace, name) is required") |
|||
return fmt.Errorf("missing required parameters") |
|||
} |
|||
|
|||
tablePath := getTablePath(bucketName, namespace, tableName) |
|||
|
|||
var metadata tableMetadataInternal |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
data, err := h.getExtendedAttribute(client, tablePath, ExtendedKeyMetadata) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
return json.Unmarshal(data, &metadata) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName)) |
|||
return err |
|||
} |
|||
|
|||
tableARN := h.generateTableARN(bucketName, namespace, tableName) |
|||
|
|||
resp := &GetTableResponse{ |
|||
Name: metadata.Name, |
|||
TableARN: tableARN, |
|||
Namespace: []string{metadata.Namespace}, |
|||
Format: metadata.Format, |
|||
CreatedAt: metadata.CreatedAt, |
|||
ModifiedAt: metadata.ModifiedAt, |
|||
OwnerAccountID: metadata.OwnerID, |
|||
MetadataLocation: metadata.MetadataLocation, |
|||
VersionToken: metadata.VersionToken, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
// handleListTables lists all tables in a namespace or bucket
|
|||
func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req ListTablesRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required") |
|||
return fmt.Errorf("tableBucketARN is required") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
maxTables := req.MaxTables |
|||
if maxTables <= 0 { |
|||
maxTables = 100 |
|||
} |
|||
|
|||
var tables []TableSummary |
|||
|
|||
// If namespace is specified, list tables in that namespace only
|
|||
if req.Namespace != "" { |
|||
err = h.listTablesInNamespace(filerClient, bucketName, req.Namespace, req.Prefix, maxTables, &tables) |
|||
} else { |
|||
// List tables in all namespaces
|
|||
err = h.listTablesInAllNamespaces(filerClient, bucketName, req.Prefix, maxTables, &tables) |
|||
} |
|||
|
|||
if err != nil { |
|||
tables = []TableSummary{} |
|||
} |
|||
|
|||
resp := &ListTablesResponse{ |
|||
Tables: tables, |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, resp) |
|||
return nil |
|||
} |
|||
|
|||
func (h *S3TablesHandler) listTablesInNamespace(filerClient FilerClient, bucketName, namespace, prefix string, maxTables int, tables *[]TableSummary) error { |
|||
namespacePath := getNamespacePath(bucketName, namespace) |
|||
|
|||
return filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
resp, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ |
|||
Directory: namespacePath, |
|||
Limit: uint32(maxTables), |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
for { |
|||
entry, err := resp.Recv() |
|||
if err != nil { |
|||
break |
|||
} |
|||
|
|||
if entry.Entry == nil || !entry.Entry.IsDirectory { |
|||
continue |
|||
} |
|||
|
|||
// Skip hidden entries
|
|||
if strings.HasPrefix(entry.Entry.Name, ".") { |
|||
continue |
|||
} |
|||
|
|||
// Apply prefix filter
|
|||
if prefix != "" && !strings.HasPrefix(entry.Entry.Name, prefix) { |
|||
continue |
|||
} |
|||
|
|||
// Read table metadata from extended attribute
|
|||
data, ok := entry.Entry.Extended[ExtendedKeyMetadata] |
|||
if !ok { |
|||
continue |
|||
} |
|||
|
|||
var metadata tableMetadataInternal |
|||
if err := json.Unmarshal(data, &metadata); err != nil { |
|||
continue |
|||
} |
|||
|
|||
tableARN := h.generateTableARN(bucketName, namespace, entry.Entry.Name) |
|||
|
|||
*tables = append(*tables, TableSummary{ |
|||
Name: metadata.Name, |
|||
TableARN: tableARN, |
|||
Namespace: []string{namespace}, |
|||
CreatedAt: metadata.CreatedAt, |
|||
ModifiedAt: metadata.ModifiedAt, |
|||
}) |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func (h *S3TablesHandler) listTablesInAllNamespaces(filerClient FilerClient, bucketName, prefix string, maxTables int, tables *[]TableSummary) error { |
|||
bucketPath := getTableBucketPath(bucketName) |
|||
|
|||
return filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
// List all namespaces first
|
|||
resp, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ |
|||
Directory: bucketPath, |
|||
Limit: 1000, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
for { |
|||
entry, err := resp.Recv() |
|||
if err != nil { |
|||
break |
|||
} |
|||
|
|||
if entry.Entry == nil || !entry.Entry.IsDirectory { |
|||
continue |
|||
} |
|||
|
|||
// Skip hidden entries
|
|||
if strings.HasPrefix(entry.Entry.Name, ".") { |
|||
continue |
|||
} |
|||
|
|||
namespace := entry.Entry.Name |
|||
|
|||
// List tables in this namespace
|
|||
if err := h.listTablesInNamespace(filerClient, bucketName, namespace, prefix, maxTables-len(*tables), tables); err != nil { |
|||
continue |
|||
} |
|||
|
|||
if len(*tables) >= maxTables { |
|||
break |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
// handleDeleteTable deletes a table from a namespace
|
|||
func (h *S3TablesHandler) handleDeleteTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { |
|||
var req DeleteTableRequest |
|||
if err := h.readRequestBody(r, &req); err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
if req.TableBucketARN == "" || req.Namespace == "" || req.Name == "" { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, namespace, and name are required") |
|||
return fmt.Errorf("missing required parameters") |
|||
} |
|||
|
|||
bucketName, err := parseBucketNameFromARN(req.TableBucketARN) |
|||
if err != nil { |
|||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) |
|||
return err |
|||
} |
|||
|
|||
tablePath := getTablePath(bucketName, req.Namespace, req.Name) |
|||
|
|||
// Check if table exists
|
|||
var tableExists bool |
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := h.getExtendedAttribute(client, tablePath, ExtendedKeyMetadata) |
|||
tableExists = err == nil |
|||
return nil |
|||
}) |
|||
|
|||
if !tableExists { |
|||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", req.Name)) |
|||
return fmt.Errorf("table not found") |
|||
} |
|||
|
|||
// Delete the table
|
|||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { |
|||
return h.deleteDirectory(client, tablePath) |
|||
}) |
|||
|
|||
if err != nil { |
|||
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to delete table") |
|||
return err |
|||
} |
|||
|
|||
h.writeJSON(w, http.StatusOK, nil) |
|||
return nil |
|||
} |
|||
@ -0,0 +1,290 @@ |
|||
package s3tables |
|||
|
|||
import "time" |
|||
|
|||
// Table bucket types
|
|||
|
|||
type TableBucket struct { |
|||
ARN string `json:"arn"` |
|||
Name string `json:"name"` |
|||
OwnerID string `json:"ownerAccountId"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
} |
|||
|
|||
type CreateTableBucketRequest struct { |
|||
Name string `json:"name"` |
|||
Tags map[string]string `json:"tags,omitempty"` |
|||
} |
|||
|
|||
type CreateTableBucketResponse struct { |
|||
ARN string `json:"arn"` |
|||
} |
|||
|
|||
type GetTableBucketRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
} |
|||
|
|||
type GetTableBucketResponse struct { |
|||
ARN string `json:"arn"` |
|||
Name string `json:"name"` |
|||
OwnerAccountID string `json:"ownerAccountId"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
} |
|||
|
|||
type ListTableBucketsRequest struct { |
|||
Prefix string `json:"prefix,omitempty"` |
|||
ContinuationToken string `json:"continuationToken,omitempty"` |
|||
MaxBuckets int `json:"maxBuckets,omitempty"` |
|||
} |
|||
|
|||
type TableBucketSummary struct { |
|||
ARN string `json:"arn"` |
|||
Name string `json:"name"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
} |
|||
|
|||
type ListTableBucketsResponse struct { |
|||
TableBuckets []TableBucketSummary `json:"tableBuckets"` |
|||
ContinuationToken string `json:"continuationToken,omitempty"` |
|||
} |
|||
|
|||
type DeleteTableBucketRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
} |
|||
|
|||
// Table bucket policy types
|
|||
|
|||
type PutTableBucketPolicyRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
ResourcePolicy string `json:"resourcePolicy"` |
|||
} |
|||
|
|||
type GetTableBucketPolicyRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
} |
|||
|
|||
type GetTableBucketPolicyResponse struct { |
|||
ResourcePolicy string `json:"resourcePolicy"` |
|||
} |
|||
|
|||
type DeleteTableBucketPolicyRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
} |
|||
|
|||
// Namespace types
|
|||
|
|||
type Namespace struct { |
|||
Namespace []string `json:"namespace"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
OwnerAccountID string `json:"ownerAccountId"` |
|||
} |
|||
|
|||
type CreateNamespaceRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace []string `json:"namespace"` |
|||
} |
|||
|
|||
type CreateNamespaceResponse struct { |
|||
Namespace []string `json:"namespace"` |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
} |
|||
|
|||
type GetNamespaceRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
} |
|||
|
|||
type GetNamespaceResponse struct { |
|||
Namespace []string `json:"namespace"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
OwnerAccountID string `json:"ownerAccountId"` |
|||
} |
|||
|
|||
type ListNamespacesRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Prefix string `json:"prefix,omitempty"` |
|||
ContinuationToken string `json:"continuationToken,omitempty"` |
|||
MaxNamespaces int `json:"maxNamespaces,omitempty"` |
|||
} |
|||
|
|||
type NamespaceSummary struct { |
|||
Namespace []string `json:"namespace"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
} |
|||
|
|||
type ListNamespacesResponse struct { |
|||
Namespaces []NamespaceSummary `json:"namespaces"` |
|||
ContinuationToken string `json:"continuationToken,omitempty"` |
|||
} |
|||
|
|||
type DeleteNamespaceRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
} |
|||
|
|||
// Table types
|
|||
|
|||
type IcebergSchemaField struct { |
|||
Name string `json:"name"` |
|||
Type string `json:"type"` |
|||
Required bool `json:"required,omitempty"` |
|||
} |
|||
|
|||
type IcebergSchema struct { |
|||
Fields []IcebergSchemaField `json:"fields"` |
|||
} |
|||
|
|||
type IcebergMetadata struct { |
|||
Schema IcebergSchema `json:"schema"` |
|||
} |
|||
|
|||
type TableMetadata struct { |
|||
Iceberg *IcebergMetadata `json:"iceberg,omitempty"` |
|||
} |
|||
|
|||
type Table struct { |
|||
Name string `json:"name"` |
|||
TableARN string `json:"tableARN"` |
|||
Namespace []string `json:"namespace"` |
|||
Format string `json:"format"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
ModifiedAt time.Time `json:"modifiedAt"` |
|||
OwnerAccountID string `json:"ownerAccountId"` |
|||
MetadataLocation string `json:"metadataLocation,omitempty"` |
|||
Metadata *TableMetadata `json:"metadata,omitempty"` |
|||
} |
|||
|
|||
type CreateTableRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
Name string `json:"name"` |
|||
Format string `json:"format"` |
|||
Metadata *TableMetadata `json:"metadata,omitempty"` |
|||
Tags map[string]string `json:"tags,omitempty"` |
|||
} |
|||
|
|||
type CreateTableResponse struct { |
|||
TableARN string `json:"tableARN"` |
|||
VersionToken string `json:"versionToken"` |
|||
MetadataLocation string `json:"metadataLocation,omitempty"` |
|||
} |
|||
|
|||
type GetTableRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN,omitempty"` |
|||
Namespace string `json:"namespace,omitempty"` |
|||
Name string `json:"name,omitempty"` |
|||
TableARN string `json:"tableARN,omitempty"` |
|||
} |
|||
|
|||
type GetTableResponse struct { |
|||
Name string `json:"name"` |
|||
TableARN string `json:"tableARN"` |
|||
Namespace []string `json:"namespace"` |
|||
Format string `json:"format"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
ModifiedAt time.Time `json:"modifiedAt"` |
|||
OwnerAccountID string `json:"ownerAccountId"` |
|||
MetadataLocation string `json:"metadataLocation,omitempty"` |
|||
VersionToken string `json:"versionToken"` |
|||
} |
|||
|
|||
type ListTablesRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace,omitempty"` |
|||
Prefix string `json:"prefix,omitempty"` |
|||
ContinuationToken string `json:"continuationToken,omitempty"` |
|||
MaxTables int `json:"maxTables,omitempty"` |
|||
} |
|||
|
|||
type TableSummary struct { |
|||
Name string `json:"name"` |
|||
TableARN string `json:"tableARN"` |
|||
Namespace []string `json:"namespace"` |
|||
CreatedAt time.Time `json:"createdAt"` |
|||
ModifiedAt time.Time `json:"modifiedAt"` |
|||
MetadataLocation string `json:"metadataLocation,omitempty"` |
|||
} |
|||
|
|||
type ListTablesResponse struct { |
|||
Tables []TableSummary `json:"tables"` |
|||
ContinuationToken string `json:"continuationToken,omitempty"` |
|||
} |
|||
|
|||
type DeleteTableRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
Name string `json:"name"` |
|||
VersionToken string `json:"versionToken,omitempty"` |
|||
} |
|||
|
|||
// Table policy types
|
|||
|
|||
type PutTablePolicyRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
Name string `json:"name"` |
|||
ResourcePolicy string `json:"resourcePolicy"` |
|||
} |
|||
|
|||
type GetTablePolicyRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
type GetTablePolicyResponse struct { |
|||
ResourcePolicy string `json:"resourcePolicy"` |
|||
} |
|||
|
|||
type DeleteTablePolicyRequest struct { |
|||
TableBucketARN string `json:"tableBucketARN"` |
|||
Namespace string `json:"namespace"` |
|||
Name string `json:"name"` |
|||
} |
|||
|
|||
// Tagging types
|
|||
|
|||
type TagResourceRequest struct { |
|||
ResourceARN string `json:"resourceArn"` |
|||
Tags map[string]string `json:"tags"` |
|||
} |
|||
|
|||
type ListTagsForResourceRequest struct { |
|||
ResourceARN string `json:"resourceArn"` |
|||
} |
|||
|
|||
type ListTagsForResourceResponse struct { |
|||
Tags map[string]string `json:"tags"` |
|||
} |
|||
|
|||
type UntagResourceRequest struct { |
|||
ResourceARN string `json:"resourceArn"` |
|||
TagKeys []string `json:"tagKeys"` |
|||
} |
|||
|
|||
// Error types
|
|||
|
|||
type S3TablesError struct { |
|||
Type string `json:"__type"` |
|||
Message string `json:"message"` |
|||
} |
|||
|
|||
func (e *S3TablesError) Error() string { |
|||
return e.Message |
|||
} |
|||
|
|||
// Error codes
|
|||
const ( |
|||
ErrCodeBucketAlreadyExists = "BucketAlreadyExists" |
|||
ErrCodeBucketNotEmpty = "BucketNotEmpty" |
|||
ErrCodeNoSuchBucket = "NoSuchBucket" |
|||
ErrCodeNoSuchNamespace = "NoSuchNamespace" |
|||
ErrCodeNoSuchTable = "NoSuchTable" |
|||
ErrCodeNamespaceAlreadyExists = "NamespaceAlreadyExists" |
|||
ErrCodeNamespaceNotEmpty = "NamespaceNotEmpty" |
|||
ErrCodeTableAlreadyExists = "TableAlreadyExists" |
|||
ErrCodeAccessDenied = "AccessDenied" |
|||
ErrCodeInvalidRequest = "InvalidRequest" |
|||
ErrCodeInternalError = "InternalError" |
|||
ErrCodeNoSuchPolicy = "NoSuchPolicy" |
|||
) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue