bucket_management.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. package dash
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "strings"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  11. "github.com/seaweedfs/seaweedfs/weed/s3api"
  12. )
  13. // S3 Bucket management data structures for templates
  14. type S3BucketsData struct {
  15. Username string `json:"username"`
  16. Buckets []S3Bucket `json:"buckets"`
  17. TotalBuckets int `json:"total_buckets"`
  18. TotalSize int64 `json:"total_size"`
  19. LastUpdated time.Time `json:"last_updated"`
  20. }
  21. type CreateBucketRequest struct {
  22. Name string `json:"name" binding:"required"`
  23. Region string `json:"region"`
  24. QuotaSize int64 `json:"quota_size"` // Quota size in bytes
  25. QuotaUnit string `json:"quota_unit"` // Unit: MB, GB, TB
  26. QuotaEnabled bool `json:"quota_enabled"` // Whether quota is enabled
  27. VersioningEnabled bool `json:"versioning_enabled"` // Whether versioning is enabled
  28. ObjectLockEnabled bool `json:"object_lock_enabled"` // Whether object lock is enabled
  29. ObjectLockMode string `json:"object_lock_mode"` // Object lock mode: "GOVERNANCE" or "COMPLIANCE"
  30. SetDefaultRetention bool `json:"set_default_retention"` // Whether to set default retention
  31. ObjectLockDuration int32 `json:"object_lock_duration"` // Default retention duration in days
  32. }
  33. // S3 Bucket Management Handlers
  34. // ShowS3Buckets displays the Object Store buckets management page
  35. func (s *AdminServer) ShowS3Buckets(c *gin.Context) {
  36. username := c.GetString("username")
  37. buckets, err := s.GetS3Buckets()
  38. if err != nil {
  39. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get Object Store buckets: " + err.Error()})
  40. return
  41. }
  42. // Calculate totals
  43. var totalSize int64
  44. for _, bucket := range buckets {
  45. totalSize += bucket.Size
  46. }
  47. data := S3BucketsData{
  48. Username: username,
  49. Buckets: buckets,
  50. TotalBuckets: len(buckets),
  51. TotalSize: totalSize,
  52. LastUpdated: time.Now(),
  53. }
  54. c.JSON(http.StatusOK, data)
  55. }
  56. // ShowBucketDetails displays detailed information about a specific bucket
  57. func (s *AdminServer) ShowBucketDetails(c *gin.Context) {
  58. bucketName := c.Param("bucket")
  59. if bucketName == "" {
  60. c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"})
  61. return
  62. }
  63. details, err := s.GetBucketDetails(bucketName)
  64. if err != nil {
  65. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()})
  66. return
  67. }
  68. c.JSON(http.StatusOK, details)
  69. }
  70. // CreateBucket creates a new S3 bucket
  71. func (s *AdminServer) CreateBucket(c *gin.Context) {
  72. var req CreateBucketRequest
  73. if err := c.ShouldBindJSON(&req); err != nil {
  74. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
  75. return
  76. }
  77. // Validate bucket name (basic validation)
  78. if len(req.Name) < 3 || len(req.Name) > 63 {
  79. c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"})
  80. return
  81. }
  82. // Validate object lock settings
  83. if req.ObjectLockEnabled {
  84. // Object lock requires versioning to be enabled
  85. req.VersioningEnabled = true
  86. // Validate object lock mode
  87. if req.ObjectLockMode != "GOVERNANCE" && req.ObjectLockMode != "COMPLIANCE" {
  88. c.JSON(http.StatusBadRequest, gin.H{"error": "Object lock mode must be either GOVERNANCE or COMPLIANCE"})
  89. return
  90. }
  91. // Validate retention duration if default retention is enabled
  92. if req.SetDefaultRetention {
  93. if req.ObjectLockDuration <= 0 {
  94. c.JSON(http.StatusBadRequest, gin.H{"error": "Object lock duration must be greater than 0 days when default retention is enabled"})
  95. return
  96. }
  97. }
  98. }
  99. // Convert quota to bytes
  100. quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit)
  101. err := s.CreateS3BucketWithObjectLock(req.Name, quotaBytes, req.QuotaEnabled, req.VersioningEnabled, req.ObjectLockEnabled, req.ObjectLockMode, req.SetDefaultRetention, req.ObjectLockDuration)
  102. if err != nil {
  103. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()})
  104. return
  105. }
  106. c.JSON(http.StatusCreated, gin.H{
  107. "message": "Bucket created successfully",
  108. "bucket": req.Name,
  109. "quota_size": req.QuotaSize,
  110. "quota_unit": req.QuotaUnit,
  111. "quota_enabled": req.QuotaEnabled,
  112. "versioning_enabled": req.VersioningEnabled,
  113. "object_lock_enabled": req.ObjectLockEnabled,
  114. "object_lock_mode": req.ObjectLockMode,
  115. "object_lock_duration": req.ObjectLockDuration,
  116. })
  117. }
  118. // UpdateBucketQuota updates the quota settings for a bucket
  119. func (s *AdminServer) UpdateBucketQuota(c *gin.Context) {
  120. bucketName := c.Param("bucket")
  121. if bucketName == "" {
  122. c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"})
  123. return
  124. }
  125. var req struct {
  126. QuotaSize int64 `json:"quota_size"`
  127. QuotaUnit string `json:"quota_unit"`
  128. QuotaEnabled bool `json:"quota_enabled"`
  129. }
  130. if err := c.ShouldBindJSON(&req); err != nil {
  131. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
  132. return
  133. }
  134. // Convert quota to bytes
  135. quotaBytes := convertQuotaToBytes(req.QuotaSize, req.QuotaUnit)
  136. err := s.SetBucketQuota(bucketName, quotaBytes, req.QuotaEnabled)
  137. if err != nil {
  138. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bucket quota: " + err.Error()})
  139. return
  140. }
  141. c.JSON(http.StatusOK, gin.H{
  142. "message": "Bucket quota updated successfully",
  143. "bucket": bucketName,
  144. "quota_size": req.QuotaSize,
  145. "quota_unit": req.QuotaUnit,
  146. "quota_enabled": req.QuotaEnabled,
  147. })
  148. }
  149. // DeleteBucket deletes an S3 bucket
  150. func (s *AdminServer) DeleteBucket(c *gin.Context) {
  151. bucketName := c.Param("bucket")
  152. if bucketName == "" {
  153. c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"})
  154. return
  155. }
  156. err := s.DeleteS3Bucket(bucketName)
  157. if err != nil {
  158. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()})
  159. return
  160. }
  161. c.JSON(http.StatusOK, gin.H{
  162. "message": "Bucket deleted successfully",
  163. "bucket": bucketName,
  164. })
  165. }
  166. // ListBucketsAPI returns the list of buckets as JSON
  167. func (s *AdminServer) ListBucketsAPI(c *gin.Context) {
  168. buckets, err := s.GetS3Buckets()
  169. if err != nil {
  170. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get buckets: " + err.Error()})
  171. return
  172. }
  173. c.JSON(http.StatusOK, gin.H{
  174. "buckets": buckets,
  175. "total": len(buckets),
  176. })
  177. }
  178. // Helper function to convert quota size and unit to bytes
  179. func convertQuotaToBytes(size int64, unit string) int64 {
  180. if size <= 0 {
  181. return 0
  182. }
  183. switch strings.ToUpper(unit) {
  184. case "TB":
  185. return size * 1024 * 1024 * 1024 * 1024
  186. case "GB":
  187. return size * 1024 * 1024 * 1024
  188. case "MB":
  189. return size * 1024 * 1024
  190. default:
  191. // Default to MB if unit is not recognized
  192. return size * 1024 * 1024
  193. }
  194. }
  195. // Helper function to convert bytes to appropriate unit and size
  196. func convertBytesToQuota(bytes int64) (int64, string) {
  197. if bytes == 0 {
  198. return 0, "MB"
  199. }
  200. // Convert to TB if >= 1TB
  201. if bytes >= 1024*1024*1024*1024 && bytes%(1024*1024*1024*1024) == 0 {
  202. return bytes / (1024 * 1024 * 1024 * 1024), "TB"
  203. }
  204. // Convert to GB if >= 1GB
  205. if bytes >= 1024*1024*1024 && bytes%(1024*1024*1024) == 0 {
  206. return bytes / (1024 * 1024 * 1024), "GB"
  207. }
  208. // Convert to MB (default)
  209. return bytes / (1024 * 1024), "MB"
  210. }
  211. // SetBucketQuota sets the quota for a bucket
  212. func (s *AdminServer) SetBucketQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error {
  213. return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  214. // Get the current bucket entry
  215. lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
  216. Directory: "/buckets",
  217. Name: bucketName,
  218. })
  219. if err != nil {
  220. return fmt.Errorf("bucket not found: %w", err)
  221. }
  222. bucketEntry := lookupResp.Entry
  223. // Determine quota value (negative if disabled)
  224. var quota int64
  225. if quotaEnabled && quotaBytes > 0 {
  226. quota = quotaBytes
  227. } else if !quotaEnabled && quotaBytes > 0 {
  228. quota = -quotaBytes
  229. } else {
  230. quota = 0
  231. }
  232. // Update the quota
  233. bucketEntry.Quota = quota
  234. // Update the entry
  235. _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{
  236. Directory: "/buckets",
  237. Entry: bucketEntry,
  238. })
  239. if err != nil {
  240. return fmt.Errorf("failed to update bucket quota: %w", err)
  241. }
  242. return nil
  243. })
  244. }
  245. // CreateS3BucketWithQuota creates a new S3 bucket with quota settings
  246. func (s *AdminServer) CreateS3BucketWithQuota(bucketName string, quotaBytes int64, quotaEnabled bool) error {
  247. return s.CreateS3BucketWithObjectLock(bucketName, quotaBytes, quotaEnabled, false, false, "", false, 0)
  248. }
  249. // CreateS3BucketWithObjectLock creates a new S3 bucket with quota, versioning, and object lock settings
  250. func (s *AdminServer) CreateS3BucketWithObjectLock(bucketName string, quotaBytes int64, quotaEnabled, versioningEnabled, objectLockEnabled bool, objectLockMode string, setDefaultRetention bool, objectLockDuration int32) error {
  251. return s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  252. // First ensure /buckets directory exists
  253. _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
  254. Directory: "/",
  255. Entry: &filer_pb.Entry{
  256. Name: "buckets",
  257. IsDirectory: true,
  258. Attributes: &filer_pb.FuseAttributes{
  259. FileMode: uint32(0755 | os.ModeDir), // Directory mode
  260. Uid: uint32(1000),
  261. Gid: uint32(1000),
  262. Crtime: time.Now().Unix(),
  263. Mtime: time.Now().Unix(),
  264. TtlSec: 0,
  265. },
  266. },
  267. })
  268. // Ignore error if directory already exists
  269. if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "existing entry") {
  270. return fmt.Errorf("failed to create /buckets directory: %w", err)
  271. }
  272. // Check if bucket already exists
  273. _, err = client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
  274. Directory: "/buckets",
  275. Name: bucketName,
  276. })
  277. if err == nil {
  278. return fmt.Errorf("bucket %s already exists", bucketName)
  279. }
  280. // Determine quota value (negative if disabled)
  281. var quota int64
  282. if quotaEnabled && quotaBytes > 0 {
  283. quota = quotaBytes
  284. } else if !quotaEnabled && quotaBytes > 0 {
  285. quota = -quotaBytes
  286. } else {
  287. quota = 0
  288. }
  289. // Prepare bucket attributes with versioning and object lock metadata
  290. attributes := &filer_pb.FuseAttributes{
  291. FileMode: uint32(0755 | os.ModeDir), // Directory mode
  292. Uid: filer_pb.OS_UID,
  293. Gid: filer_pb.OS_GID,
  294. Crtime: time.Now().Unix(),
  295. Mtime: time.Now().Unix(),
  296. TtlSec: 0,
  297. }
  298. // Create extended attributes map for versioning
  299. extended := make(map[string][]byte)
  300. // Create bucket entry
  301. bucketEntry := &filer_pb.Entry{
  302. Name: bucketName,
  303. IsDirectory: true,
  304. Attributes: attributes,
  305. Extended: extended,
  306. Quota: quota,
  307. }
  308. // Handle versioning using shared utilities
  309. if err := s3api.StoreVersioningInExtended(bucketEntry, versioningEnabled); err != nil {
  310. return fmt.Errorf("failed to store versioning configuration: %w", err)
  311. }
  312. // Handle Object Lock configuration using shared utilities
  313. if objectLockEnabled {
  314. var duration int32 = 0
  315. if setDefaultRetention {
  316. // Validate Object Lock parameters only when setting default retention
  317. if err := s3api.ValidateObjectLockParameters(objectLockEnabled, objectLockMode, objectLockDuration); err != nil {
  318. return fmt.Errorf("invalid Object Lock parameters: %w", err)
  319. }
  320. duration = objectLockDuration
  321. }
  322. // Create Object Lock configuration using shared utility
  323. objectLockConfig := s3api.CreateObjectLockConfigurationFromParams(objectLockEnabled, objectLockMode, duration)
  324. // Store Object Lock configuration in extended attributes using shared utility
  325. if err := s3api.StoreObjectLockConfigurationInExtended(bucketEntry, objectLockConfig); err != nil {
  326. return fmt.Errorf("failed to store Object Lock configuration: %w", err)
  327. }
  328. }
  329. // Create bucket directory under /buckets
  330. _, err = client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
  331. Directory: "/buckets",
  332. Entry: bucketEntry,
  333. })
  334. if err != nil {
  335. return fmt.Errorf("failed to create bucket directory: %w", err)
  336. }
  337. return nil
  338. })
  339. }