You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							962 lines
						
					
					
						
							47 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							962 lines
						
					
					
						
							47 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
|     "fmt" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/dash" | |
| ) | |
| 
 | |
| templ S3Buckets(data dash.S3BucketsData) { | |
|     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> | |
|         <h1 class="h2"> | |
|             <i class="fas fa-cube me-2"></i>Object Store Buckets | |
|         </h1> | |
|         <div class="btn-toolbar mb-2 mb-md-0"> | |
|             <div class="btn-group me-2"> | |
|                 <button type="button" class="btn btn-sm btn-primary"  | |
|                         data-bs-toggle="modal"  | |
|                         data-bs-target="#createBucketModal"> | |
|                     <i class="fas fa-plus me-1"></i>Create Bucket | |
|                 </button> | |
| 
 | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <div id="s3-buckets-content"> | |
|         <!-- Summary Cards --> | |
|         <div class="row mb-4"> | |
|             <div class="col-xl-4 col-md-6 mb-4"> | |
|                 <div class="card border-left-primary shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-primary text-uppercase mb-1"> | |
|                                     Total Buckets | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     {fmt.Sprintf("%d", data.TotalBuckets)} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-cube fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-4 col-md-6 mb-4"> | |
|                 <div class="card border-left-success shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-success text-uppercase mb-1"> | |
|                                     Total Storage | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     {formatBytes(data.TotalSize)} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-hdd fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
| 
 | |
|             <div class="col-xl-4 col-md-6 mb-4"> | |
|                 <div class="card border-left-warning shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-warning text-uppercase mb-1"> | |
|                                     Last Updated | |
|                                 </div> | |
|                                 <div class="h6 mb-0 font-weight-bold text-gray-800"> | |
|                                     {data.LastUpdated.Format("15:04:05")} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-clock fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Buckets Table --> | |
|         <div class="row"> | |
|             <div class="col-12"> | |
|                 <div class="card shadow mb-4"> | |
|                     <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> | |
|                         <h6 class="m-0 font-weight-bold text-primary"> | |
|                             <i class="fas fa-cube me-2"></i>Object Store Buckets | |
|                         </h6> | |
|                         <div class="dropdown no-arrow"> | |
|                             <a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> | |
|                                 <i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i> | |
|                             </a> | |
|                             <div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"> | |
|                                 <div class="dropdown-header">Actions:</div> | |
|                                 <a class="dropdown-item" href="#" onclick="exportBucketList()"> | |
|                                     <i class="fas fa-download me-2"></i>Export List | |
|                                 </a> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         <div class="table-responsive"> | |
|                             <table class="table table-hover" width="100%" cellspacing="0" id="bucketsTable"> | |
|                                 <thead> | |
|                                     <tr> | |
|                                         <th>Name</th> | |
|                                         <th>Created</th> | |
|                                         <th>Objects</th> | |
|                                         <th>Size</th> | |
|                                         <th>Quota</th> | |
|                                         <th>Versioning</th> | |
|                                         <th>Object Lock</th> | |
|                                         <th>Actions</th> | |
|                                     </tr> | |
|                                 </thead> | |
|                                 <tbody> | |
|                                     for _, bucket := range data.Buckets { | |
|                                         <tr> | |
|                                             <td> | |
|                                                 <a href={templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))}  | |
|                                                    class="text-decoration-none"> | |
|                                                     <i class="fas fa-cube me-2"></i> | |
|                                                     {bucket.Name} | |
|                                                 </a> | |
|                                             </td> | |
|                                             <td>{bucket.CreatedAt.Format("2006-01-02 15:04")}</td> | |
|                                             <td>{fmt.Sprintf("%d", bucket.ObjectCount)}</td> | |
|                                             <td>{formatBytes(bucket.Size)}</td> | |
|                                             <td> | |
|                                                 if bucket.Quota > 0 { | |
|                                                     <div> | |
|                                                         <span class={fmt.Sprintf("badge bg-%s", getQuotaStatusColor(bucket.Size, bucket.Quota, bucket.QuotaEnabled))}> | |
|                                                             {formatBytes(bucket.Quota)} | |
|                                                         </span> | |
|                                                         if bucket.QuotaEnabled { | |
|                                                             <div class="small text-muted"> | |
|                                                                 {fmt.Sprintf("%.1f%% used", float64(bucket.Size)/float64(bucket.Quota)*100)} | |
|                                                             </div> | |
|                                                         } else { | |
|                                                             <div class="small text-muted">Disabled</div> | |
|                                                         } | |
|                                                     </div> | |
|                                                 } else { | |
|                                                     <span class="text-muted">No quota</span> | |
|                                                 } | |
|                                             </td> | |
|                                             <td> | |
|                                                 if bucket.VersioningEnabled { | |
|                                                     <span class="badge bg-success"> | |
|                                                         <i class="fas fa-check me-1"></i>Enabled | |
|                                                     </span> | |
|                                                 } else { | |
|                                                     <span class="badge bg-secondary"> | |
|                                                         <i class="fas fa-times me-1"></i>Disabled | |
|                                                     </span> | |
|                                                 } | |
|                                             </td> | |
|                                             <td> | |
|                                                 if bucket.ObjectLockEnabled { | |
|                                                     <div> | |
|                                                         <span class="badge bg-warning"> | |
|                                                             <i class="fas fa-lock me-1"></i>Enabled | |
|                                                         </span> | |
|                                                         <div class="small text-muted"> | |
|                                                             {bucket.ObjectLockMode} • {fmt.Sprintf("%d days", bucket.ObjectLockDuration)} | |
|                                                         </div> | |
|                                                     </div> | |
|                                                 } else { | |
|                                                     <span class="badge bg-secondary"> | |
|                                                         <i class="fas fa-unlock me-1"></i>Disabled | |
|                                                     </span> | |
|                                                 } | |
|                                             </td> | |
|                                             <td> | |
|                                                 <div class="btn-group btn-group-sm" role="group"> | |
|                                                     <a href={templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name))}  | |
|                                                        class="btn btn-outline-success btn-sm" | |
|                                                        title="Browse Files"> | |
|                                                         <i class="fas fa-folder-open"></i> | |
|                                                     </a> | |
|                                                     <button type="button"  | |
|                                                             class="btn btn-outline-primary btn-sm view-details-btn" | |
|                                                             data-bucket-name={bucket.Name} | |
|                                                             title="View Details"> | |
|                                                         <i class="fas fa-eye"></i> | |
|                                                     </button> | |
|                                                     <button type="button"  | |
|                                                             class="btn btn-outline-warning btn-sm quota-btn" | |
|                                                             data-bucket-name={bucket.Name} | |
|                                                             data-current-quota={fmt.Sprintf("%d", getQuotaInMB(bucket.Quota))} | |
|                                                             data-quota-enabled={fmt.Sprintf("%t", bucket.QuotaEnabled)} | |
|                                                             title="Manage Quota"> | |
|                                                         <i class="fas fa-tachometer-alt"></i> | |
|                                                     </button> | |
|                                                     <button type="button"  | |
|                                                             class="btn btn-outline-danger btn-sm delete-bucket-btn" | |
|                                                             data-bucket-name={bucket.Name} | |
|                                                             title="Delete Bucket"> | |
|                                                         <i class="fas fa-trash"></i> | |
|                                                     </button> | |
|                                                 </div> | |
|                                             </td> | |
|                                         </tr> | |
|                                     } | |
|                                     if len(data.Buckets) == 0 { | |
|                                         <tr> | |
|                                             <td colspan="8" class="text-center text-muted py-4"> | |
|                                                 <i class="fas fa-cube fa-3x mb-3 text-muted"></i> | |
|                                                 <div> | |
|                                                     					<h5>No Object Store buckets found</h5> | |
|                                                     <p>Create your first bucket to get started with S3 storage.</p> | |
|                                                     <button type="button" class="btn btn-primary"  | |
|                                                             data-bs-toggle="modal"  | |
|                                                             data-bs-target="#createBucketModal"> | |
|                                                         <i class="fas fa-plus me-1"></i>Create Bucket | |
|                                                     </button> | |
|                                                 </div> | |
|                                             </td> | |
|                                         </tr> | |
|                                     } | |
|                                 </tbody> | |
|                             </table> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Last Updated --> | |
|         <div class="row"> | |
|             <div class="col-12"> | |
|                 <small class="text-muted"> | |
|                     <i class="fas fa-clock me-1"></i> | |
|                     Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")} | |
|                 </small> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- Create Bucket Modal --> | |
|     <div class="modal fade" id="createBucketModal" tabindex="-1" aria-labelledby="createBucketModalLabel" aria-hidden="true"> | |
|         <div class="modal-dialog"> | |
|             <div class="modal-content"> | |
|                 <div class="modal-header"> | |
|                     <h5 class="modal-title" id="createBucketModalLabel"> | |
|                         <i class="fas fa-plus me-2"></i>Create New S3 Bucket | |
|                     </h5> | |
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                 </div> | |
|                 <form id="createBucketForm"> | |
|                     <div class="modal-body"> | |
|                         <div class="mb-3"> | |
|                             <label for="bucketName" class="form-label">Bucket Name</label> | |
|                             <input type="text" class="form-control" id="bucketName" name="name"  | |
|                                    placeholder="my-bucket-name" required | |
|                                    pattern="[a-z0-9.-]+"  | |
|                                    title="Bucket name must contain only lowercase letters, numbers, dots, and hyphens"> | |
|                             <div class="form-text"> | |
|                                 Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens. | |
|                             </div> | |
|                         </div> | |
|                          | |
|                         <div class="mb-3"> | |
|                             <div class="form-check"> | |
|                                 <input class="form-check-input" type="checkbox" id="enableQuota" name="quota_enabled"> | |
|                                 <label class="form-check-label" for="enableQuota"> | |
|                                     Enable Storage Quota | |
|                                 </label> | |
|                             </div> | |
|                         </div> | |
|                          | |
|                         <div class="mb-3" id="quotaSettings" style="display: none;"> | |
|                             <div class="row"> | |
|                                 <div class="col-md-8"> | |
|                                     <label for="quotaSize" class="form-label">Quota Size</label> | |
|                                     <input type="number" class="form-control" id="quotaSize" name="quota_size"  | |
|                                            placeholder="1024" min="1" step="1"> | |
|                                 </div> | |
|                                 <div class="col-md-4"> | |
|                                     <label for="quotaUnit" class="form-label">Unit</label> | |
|                                     <select class="form-select" id="quotaUnit" name="quota_unit"> | |
|                                         <option value="MB" selected>MB</option> | |
|                                         <option value="GB">GB</option> | |
|                                         <option value="TB">TB</option> | |
|                                     </select> | |
|                                 </div> | |
|                             </div> | |
|                             <div class="form-text"> | |
|                                 Set the maximum storage size for this bucket. | |
|                             </div> | |
|                         </div> | |
| 
 | |
|                         <div class="mb-3"> | |
|                             <div class="form-check"> | |
|                                 <input class="form-check-input" type="checkbox" id="enableVersioning" name="versioning_enabled"> | |
|                                 <label class="form-check-label" for="enableVersioning"> | |
|                                     Enable Object Versioning | |
|                                 </label> | |
|                             </div> | |
|                             <div class="form-text"> | |
|                                 Keep multiple versions of objects in this bucket. | |
|                             </div> | |
|                         </div> | |
| 
 | |
|                         <div class="mb-3"> | |
|                             <div class="form-check"> | |
|                                 <input class="form-check-input" type="checkbox" id="enableObjectLock" name="object_lock_enabled"> | |
|                                 <label class="form-check-label" for="enableObjectLock"> | |
|                                     Enable Object Lock | |
|                                 </label> | |
|                             </div> | |
|                             <div class="form-text"> | |
|                                 Prevent objects from being deleted or overwritten for a specified period. Automatically enables versioning. | |
|                             </div> | |
|                         </div> | |
| 
 | |
|                         <div class="mb-3" id="objectLockSettings" style="display: none;"> | |
|                             <div class="row"> | |
|                                 <div class="col-md-6"> | |
|                                     <label for="objectLockMode" class="form-label">Object Lock Mode</label> | |
|                                     <select class="form-select" id="objectLockMode" name="object_lock_mode"> | |
|                                         <option value="GOVERNANCE" selected>Governance</option> | |
|                                         <option value="COMPLIANCE">Compliance</option> | |
|                                     </select> | |
|                                     <div class="form-text"> | |
|                                         Governance allows override with special permissions, Compliance is immutable. | |
|                                     </div> | |
|                                 </div> | |
|                                 <div class="col-md-6"> | |
|                                     <div class="form-check mb-3"> | |
|                                         <input class="form-check-input" type="checkbox" id="setDefaultRetention" name="set_default_retention"> | |
|                                         <label class="form-check-label" for="setDefaultRetention"> | |
|                                             Set Default Retention | |
|                                         </label> | |
|                                         <div class="form-text"> | |
|                                             Apply default retention to all new objects in this bucket. | |
|                                         </div> | |
|                                     </div> | |
|                                     <div id="defaultRetentionSettings" style="display: none;"> | |
|                                         <label for="objectLockDuration" class="form-label">Default Retention (days)</label> | |
|                                         <input type="number" class="form-control" id="objectLockDuration" name="object_lock_duration"  | |
|                                                placeholder="30" min="1" max="36500" step="1"> | |
|                                         <div class="form-text"> | |
|                                             Default retention period for new objects (1-36500 days). | |
|                                         </div> | |
|                                     </div> | |
|                                 </div> | |
|                             </div> | |
|                         </div> | |
| 
 | |
|                     </div> | |
|                     <div class="modal-footer"> | |
|                         <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
|                         <button type="submit" class="btn btn-primary"> | |
|                             <i class="fas fa-plus me-1"></i>Create Bucket | |
|                         </button> | |
|                     </div> | |
|                 </form> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- Delete Confirmation Modal --> | |
|     <div class="modal fade" id="deleteBucketModal" tabindex="-1" aria-labelledby="deleteBucketModalLabel" aria-hidden="true"> | |
|         <div class="modal-dialog"> | |
|             <div class="modal-content"> | |
|                 <div class="modal-header"> | |
|                     <h5 class="modal-title" id="deleteBucketModalLabel"> | |
|                         <i class="fas fa-exclamation-triangle me-2 text-warning"></i>Delete Bucket | |
|                     </h5> | |
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                 </div> | |
|                 <div class="modal-body"> | |
|                     <p>Are you sure you want to delete the bucket <strong id="deleteBucketName"></strong>?</p> | |
|                     <div class="alert alert-warning"> | |
|                         <i class="fas fa-exclamation-triangle me-2"></i> | |
|                         <strong>Warning:</strong> This action cannot be undone. All objects in the bucket will be permanently deleted. | |
|                     </div> | |
|                 </div> | |
|                 <div class="modal-footer"> | |
|                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
|                     <button type="button" class="btn btn-danger" onclick="deleteBucket()"> | |
|                         <i class="fas fa-trash me-1"></i>Delete Bucket | |
|                     </button> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- Manage Quota Modal --> | |
|     <div class="modal fade" id="manageQuotaModal" tabindex="-1" aria-labelledby="manageQuotaModalLabel" aria-hidden="true"> | |
|         <div class="modal-dialog"> | |
|             <div class="modal-content"> | |
|                 <div class="modal-header"> | |
|                     <h5 class="modal-title" id="manageQuotaModalLabel"> | |
|                         <i class="fas fa-tachometer-alt me-2"></i>Manage Bucket Quota | |
|                     </h5> | |
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                 </div> | |
|                 <form id="quotaForm"> | |
|                     <div class="modal-body"> | |
|                         <div class="mb-3"> | |
|                             <label class="form-label">Bucket Name</label> | |
|                             <input type="text" class="form-control" id="quotaBucketName" readonly> | |
|                         </div> | |
|                          | |
|                         <div class="mb-3"> | |
|                             <div class="form-check"> | |
|                                 <input class="form-check-input" type="checkbox" id="quotaEnabled" name="quota_enabled"> | |
|                                 <label class="form-check-label" for="quotaEnabled"> | |
|                                     Enable Storage Quota | |
|                                 </label> | |
|                             </div> | |
|                         </div> | |
|                          | |
|                         <div class="mb-3" id="quotaSizeSettings"> | |
|                             <div class="row"> | |
|                                 <div class="col-md-8"> | |
|                                     <label for="quotaSizeMB" class="form-label">Quota Size</label> | |
|                                     <input type="number" class="form-control" id="quotaSizeMB" name="quota_size"  | |
|                                            placeholder="1024" min="0" step="1"> | |
|                                 </div> | |
|                                 <div class="col-md-4"> | |
|                                     <label for="quotaUnitMB" class="form-label">Unit</label> | |
|                                     <select class="form-select" id="quotaUnitMB" name="quota_unit"> | |
|                                         <option value="MB" selected>MB</option> | |
|                                         <option value="GB">GB</option> | |
|                                         <option value="TB">TB</option> | |
|                                     </select> | |
|                                 </div> | |
|                             </div> | |
|                             <div class="form-text"> | |
|                                 Set the maximum storage size for this bucket. Set to 0 to remove quota. | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                     <div class="modal-footer"> | |
|                         <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
|                         <button type="submit" class="btn btn-warning"> | |
|                             <i class="fas fa-save me-1"></i>Update Quota | |
|                         </button> | |
|                     </div> | |
|                 </form> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- Bucket Details Modal --> | |
|     <div class="modal fade" id="bucketDetailsModal" tabindex="-1" aria-labelledby="bucketDetailsModalLabel" aria-hidden="true"> | |
|         <div class="modal-dialog modal-lg"> | |
|             <div class="modal-content"> | |
|                 <div class="modal-header"> | |
|                     <h5 class="modal-title" id="bucketDetailsModalLabel"> | |
|                         <i class="fas fa-cube me-2"></i>Bucket Details | |
|                     </h5> | |
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                 </div> | |
|                 <div class="modal-body"> | |
|                     <div id="bucketDetailsContent"> | |
|                         <div class="text-center py-4"> | |
|                             <div class="spinner-border text-primary" role="status"> | |
|                                 <span class="visually-hidden">Loading...</span> | |
|                             </div> | |
|                             <div class="mt-2">Loading bucket details...</div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|                 <div class="modal-footer"> | |
|                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- JavaScript for bucket management --> | |
|     <script> | |
|     document.addEventListener('DOMContentLoaded', function() { | |
|         const quotaCheckbox = document.getElementById('enableQuota'); | |
|         const quotaSettings = document.getElementById('quotaSettings'); | |
|         const versioningCheckbox = document.getElementById('enableVersioning'); | |
|         const objectLockCheckbox = document.getElementById('enableObjectLock'); | |
|         const objectLockSettings = document.getElementById('objectLockSettings'); | |
|         const setDefaultRetentionCheckbox = document.getElementById('setDefaultRetention'); | |
|         const defaultRetentionSettings = document.getElementById('defaultRetentionSettings'); | |
|         const createBucketForm = document.getElementById('createBucketForm'); | |
| 
 | |
|         // Toggle quota settings | |
|         quotaCheckbox.addEventListener('change', function() { | |
|             quotaSettings.style.display = this.checked ? 'block' : 'none'; | |
|         }); | |
| 
 | |
|         // Toggle object lock settings and automatically enable versioning | |
|         objectLockCheckbox.addEventListener('change', function() { | |
|             objectLockSettings.style.display = this.checked ? 'block' : 'none'; | |
|             if (this.checked) { | |
|                 versioningCheckbox.checked = true; | |
|                 versioningCheckbox.disabled = true; | |
|             } else { | |
|                 versioningCheckbox.disabled = false; | |
|                 // Reset default retention settings when object lock is disabled | |
|                 setDefaultRetentionCheckbox.checked = false; | |
|                 defaultRetentionSettings.style.display = 'none'; | |
|             } | |
|         }); | |
| 
 | |
|         // Toggle default retention settings | |
|         setDefaultRetentionCheckbox.addEventListener('change', function() { | |
|             defaultRetentionSettings.style.display = this.checked ? 'block' : 'none'; | |
|         }); | |
| 
 | |
|         // Handle form submission | |
|         createBucketForm.addEventListener('submit', function(e) { | |
|             e.preventDefault(); | |
|              | |
|             const formData = new FormData(this); | |
|             const data = { | |
|                 name: formData.get('name'), | |
|                 region: formData.get('region') || '', | |
|                 quota_size: quotaCheckbox.checked ? parseInt(formData.get('quota_size')) || 0 : 0, | |
|                 quota_unit: formData.get('quota_unit') || 'MB', | |
|                 quota_enabled: quotaCheckbox.checked, | |
|                 versioning_enabled: versioningCheckbox.checked, | |
|                 object_lock_enabled: objectLockCheckbox.checked, | |
|                 object_lock_mode: formData.get('object_lock_mode') || 'GOVERNANCE', | |
|                 set_default_retention: setDefaultRetentionCheckbox.checked, | |
|                 object_lock_duration: setDefaultRetentionCheckbox.checked ? parseInt(formData.get('object_lock_duration')) || 30 : 0 | |
|             }; | |
| 
 | |
|             // Validate object lock settings | |
|             if (data.object_lock_enabled && data.set_default_retention && data.object_lock_duration <= 0) { | |
|                 alert('Please enter a valid retention duration for object lock.'); | |
|                 return; | |
|             } | |
| 
 | |
|             fetch('/api/s3/buckets', { | |
|                 method: 'POST', | |
|                 headers: { | |
|                     'Content-Type': 'application/json', | |
|                 }, | |
|                 body: JSON.stringify(data) | |
|             }) | |
|             .then(response => response.json()) | |
|             .then(data => { | |
|                 if (data.error) { | |
|                     alert('Error creating bucket: ' + data.error); | |
|                 } else { | |
|                     alert('Bucket created successfully!'); | |
|                     // Properly close the modal before reloading | |
|                     const createModal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal')); | |
|                     if (createModal) { | |
|                         createModal.hide(); | |
|                     } | |
|                     setTimeout(() => location.reload(), 500); | |
|                 } | |
|             }) | |
|             .catch(error => { | |
|                 console.error('Error:', error); | |
|                 alert('Error creating bucket: ' + error.message); | |
|             }); | |
|         }); | |
| 
 | |
|         // Handle delete bucket | |
|         let deleteModalInstance = null; | |
|         document.querySelectorAll('.delete-bucket-btn').forEach(button => { | |
|             button.addEventListener('click', function() { | |
|                 const bucketName = this.dataset.bucketName; | |
|                 document.getElementById('deleteBucketName').textContent = bucketName; | |
|                 window.currentBucketToDelete = bucketName; | |
|                  | |
|                 // Dispose of existing modal instance if it exists | |
|                 if (deleteModalInstance) { | |
|                     deleteModalInstance.dispose(); | |
|                 } | |
|                  | |
|                 // Create new modal instance | |
|                 deleteModalInstance = new bootstrap.Modal(document.getElementById('deleteBucketModal')); | |
|                 deleteModalInstance.show(); | |
|             }); | |
|         }); | |
| 
 | |
|         // Add event listener to properly dispose of delete modal when hidden | |
|         document.getElementById('deleteBucketModal').addEventListener('hidden.bs.modal', function() { | |
|             if (deleteModalInstance) { | |
|                 deleteModalInstance.dispose(); | |
|                 deleteModalInstance = null; | |
|             } | |
|             // Force remove any remaining backdrops | |
|             document.querySelectorAll('.modal-backdrop').forEach(backdrop => { | |
|                 backdrop.remove(); | |
|             }); | |
|             // Ensure body classes are removed | |
|             document.body.classList.remove('modal-open'); | |
|             document.body.style.removeProperty('padding-right'); | |
|         }); | |
| 
 | |
|         // Handle quota management | |
|         let quotaModalInstance = null; | |
|         document.querySelectorAll('.quota-btn').forEach(button => { | |
|             button.addEventListener('click', function() { | |
|                 const bucketName = this.dataset.bucketName; | |
|                 const currentQuota = parseInt(this.dataset.currentQuota); | |
|                 const quotaEnabled = this.dataset.quotaEnabled === 'true'; | |
|                  | |
|                 document.getElementById('quotaBucketName').value = bucketName; | |
|                 document.getElementById('quotaEnabled').checked = quotaEnabled; | |
|                 document.getElementById('quotaSizeMB').value = currentQuota; | |
|                  | |
|                 // Toggle quota size settings | |
|                 document.getElementById('quotaSizeSettings').style.display = quotaEnabled ? 'block' : 'none'; | |
|                  | |
|                 window.currentBucketToUpdate = bucketName; | |
|                  | |
|                 // Dispose of existing modal instance if it exists | |
|                 if (quotaModalInstance) { | |
|                     quotaModalInstance.dispose(); | |
|                 } | |
|                  | |
|                 // Create new modal instance | |
|                 quotaModalInstance = new bootstrap.Modal(document.getElementById('manageQuotaModal')); | |
|                 quotaModalInstance.show(); | |
|             }); | |
|         }); | |
| 
 | |
|         // Add event listener to properly dispose of quota modal when hidden | |
|         document.getElementById('manageQuotaModal').addEventListener('hidden.bs.modal', function() { | |
|             if (quotaModalInstance) { | |
|                 quotaModalInstance.dispose(); | |
|                 quotaModalInstance = null; | |
|             } | |
|             // Force remove any remaining backdrops | |
|             document.querySelectorAll('.modal-backdrop').forEach(backdrop => { | |
|                 backdrop.remove(); | |
|             }); | |
|             // Ensure body classes are removed | |
|             document.body.classList.remove('modal-open'); | |
|             document.body.style.removeProperty('padding-right'); | |
|         }); | |
| 
 | |
|         // Handle quota form submission | |
|         document.getElementById('quotaForm').addEventListener('submit', function(e) { | |
|             e.preventDefault(); | |
|              | |
|             const formData = new FormData(this); | |
|             const enabled = document.getElementById('quotaEnabled').checked; | |
|             const data = { | |
|                 quota_size: enabled ? parseInt(formData.get('quota_size')) || 0 : 0, | |
|                 quota_unit: formData.get('quota_unit') || 'MB', | |
|                 quota_enabled: enabled | |
|             }; | |
| 
 | |
|             fetch(`/api/s3/buckets/${window.currentBucketToUpdate}/quota`, { | |
|                 method: 'PUT', | |
|                 headers: { | |
|                     'Content-Type': 'application/json', | |
|                 }, | |
|                 body: JSON.stringify(data) | |
|             }) | |
|             .then(response => response.json()) | |
|             .then(data => { | |
|                 if (data.error) { | |
|                     alert('Error updating quota: ' + data.error); | |
|                 } else { | |
|                     alert('Quota updated successfully!'); | |
|                     // Properly close the modal before reloading | |
|                     if (quotaModalInstance) { | |
|                         quotaModalInstance.hide(); | |
|                     } | |
|                     setTimeout(() => location.reload(), 500); | |
|                 } | |
|             }) | |
|             .catch(error => { | |
|                 console.error('Error:', error); | |
|                 alert('Error updating quota: ' + error.message); | |
|             }); | |
|         }); | |
| 
 | |
|         // Handle quota enabled checkbox | |
|         document.getElementById('quotaEnabled').addEventListener('change', function() { | |
|             document.getElementById('quotaSizeSettings').style.display = this.checked ? 'block' : 'none'; | |
|         }); | |
| 
 | |
|         // Handle view details button | |
|         let detailsModalInstance = null; | |
|         document.querySelectorAll('.view-details-btn').forEach(button => { | |
|             button.addEventListener('click', function() { | |
|                 const bucketName = this.dataset.bucketName; | |
|                  | |
|                 // Update modal title | |
|                 document.getElementById('bucketDetailsModalLabel').innerHTML =  | |
|                     '<i class="fas fa-cube me-2"></i>Bucket Details - ' + bucketName; | |
|                  | |
|                 // Show loading spinner | |
|                 document.getElementById('bucketDetailsContent').innerHTML =  | |
|                     '<div class="text-center py-4">' + | |
|                     '<div class="spinner-border text-primary" role="status">' + | |
|                     '<span class="visually-hidden">Loading...</span>' + | |
|                     '</div>' + | |
|                     '<div class="mt-2">Loading bucket details...</div>' + | |
|                     '</div>'; | |
|                  | |
|                 // Dispose of existing modal instance if it exists | |
|                 if (detailsModalInstance) { | |
|                     detailsModalInstance.dispose(); | |
|                 } | |
|                  | |
|                 // Create new modal instance | |
|                 detailsModalInstance = new bootstrap.Modal(document.getElementById('bucketDetailsModal')); | |
|                 detailsModalInstance.show(); | |
|                  | |
|                 // Fetch bucket details | |
|                 fetch('/api/s3/buckets/' + bucketName) | |
|                     .then(response => response.json()) | |
|                     .then(data => { | |
|                         if (data.error) { | |
|                             document.getElementById('bucketDetailsContent').innerHTML =  | |
|                                 '<div class="alert alert-danger">' + | |
|                                 '<i class="fas fa-exclamation-triangle me-2"></i>' + | |
|                                 'Error loading bucket details: ' + data.error + | |
|                                 '</div>'; | |
|                         } else { | |
|                             displayBucketDetails(data); | |
|                         } | |
|                     }) | |
|                     .catch(error => { | |
|                         console.error('Error fetching bucket details:', error); | |
|                         document.getElementById('bucketDetailsContent').innerHTML =  | |
|                             '<div class="alert alert-danger">' + | |
|                             '<i class="fas fa-exclamation-triangle me-2"></i>' + | |
|                             'Error loading bucket details: ' + error.message + | |
|                             '</div>'; | |
|                     }); | |
|             }); | |
|         }); | |
| 
 | |
|         // Add event listener to properly dispose of details modal when hidden | |
|         document.getElementById('bucketDetailsModal').addEventListener('hidden.bs.modal', function() { | |
|             if (detailsModalInstance) { | |
|                 detailsModalInstance.dispose(); | |
|                 detailsModalInstance = null; | |
|             } | |
|             // Force remove any remaining backdrops | |
|             document.querySelectorAll('.modal-backdrop').forEach(backdrop => { | |
|                 backdrop.remove(); | |
|             }); | |
|             // Ensure body classes are removed | |
|             document.body.classList.remove('modal-open'); | |
|             document.body.style.removeProperty('padding-right'); | |
|         }); | |
|     }); | |
| 
 | |
|     function deleteBucket() { | |
|         const bucketName = window.currentBucketToDelete; | |
|         if (!bucketName) return; | |
| 
 | |
|         fetch(`/api/s3/buckets/${bucketName}`, { | |
|             method: 'DELETE' | |
|         }) | |
|         .then(response => response.json()) | |
|         .then(data => { | |
|             if (data.error) { | |
|                 alert('Error deleting bucket: ' + data.error); | |
|             } else { | |
|                 alert('Bucket deleted successfully!'); | |
|                 // Properly close the modal before reloading | |
|                 if (deleteModalInstance) { | |
|                     deleteModalInstance.hide(); | |
|                 } | |
|                 setTimeout(() => location.reload(), 500); | |
|             } | |
|         }) | |
|         .catch(error => { | |
|             console.error('Error:', error); | |
|             alert('Error deleting bucket: ' + error.message); | |
|         }); | |
|     } | |
| 
 | |
|     function displayBucketDetails(data) { | |
|         const bucket = data.bucket; | |
|         const objects = data.objects || []; | |
|          | |
|         // Helper function to format bytes | |
|         function formatBytes(bytes) { | |
|             if (bytes === 0) return '0 Bytes'; | |
|             const k = 1024; | |
|             const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
|             const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
|             return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
|         } | |
|          | |
|         // Helper function to format date | |
|         function formatDate(dateString) { | |
|             const date = new Date(dateString); | |
|             return date.toLocaleString(); | |
|         } | |
|          | |
|         // Generate objects table | |
|         let objectsTable = ''; | |
|         if (objects.length > 0) { | |
|             objectsTable = '<div class="table-responsive">' + | |
|                 '<table class="table table-sm table-striped">' + | |
|                 '<thead>' + | |
|                 '<tr>' + | |
|                 '<th>Object Key</th>' + | |
|                 '<th>Size</th>' + | |
|                 '<th>Last Modified</th>' + | |
|                 '<th>Storage Class</th>' + | |
|                 '</tr>' + | |
|                 '</thead>' + | |
|                 '<tbody>' + | |
|                 objects.map(obj =>  | |
|                     '<tr>' + | |
|                     '<td><i class="fas fa-file me-1"></i>' + obj.key + '</td>' + | |
|                     '<td>' + formatBytes(obj.size) + '</td>' + | |
|                     '<td>' + formatDate(obj.last_modified) + '</td>' + | |
|                     '<td><span class="badge bg-primary">' + obj.storage_class + '</span></td>' + | |
|                     '</tr>' | |
|                 ).join('') + | |
|                 '</tbody>' + | |
|                 '</table>' + | |
|                 '</div>'; | |
|         } else { | |
|             objectsTable = '<div class="text-center py-4 text-muted">' + | |
|                 '<i class="fas fa-file fa-3x mb-3"></i>' + | |
|                 '<div>No objects found in this bucket</div>' + | |
|                 '</div>'; | |
|         } | |
|          | |
|         const content = '<div class="row">' + | |
|             '<div class="col-md-6">' + | |
|             '<h6><i class="fas fa-info-circle me-2"></i>Bucket Information</h6>' + | |
|             '<table class="table table-sm">' + | |
|             '<tr>' + | |
|             '<td><strong>Name:</strong></td>' + | |
|             '<td>' + bucket.name + '</td>' + | |
|             '</tr>' + | |
|             '<tr>' + | |
|             '<td><strong>Created:</strong></td>' + | |
|             '<td>' + formatDate(bucket.created_at) + '</td>' + | |
|             '</tr>' + | |
|             '<tr>' + | |
|             '<td><strong>Last Modified:</strong></td>' + | |
|             '<td>' + formatDate(bucket.last_modified) + '</td>' + | |
|             '</tr>' + | |
|             '<tr>' + | |
|             '<td><strong>Total Size:</strong></td>' + | |
|             '<td>' + formatBytes(bucket.size) + '</td>' + | |
|             '</tr>' + | |
|             '<tr>' + | |
|             '<td><strong>Object Count:</strong></td>' + | |
|             '<td>' + bucket.object_count + '</td>' + | |
|             '</tr>' + | |
|             '</table>' + | |
|             '</div>' + | |
|             '<div class="col-md-6">' + | |
|             '<h6><i class="fas fa-cogs me-2"></i>Configuration</h6>' + | |
|             '<table class="table table-sm">' + | |
|             '<tr>' + | |
|             '<td><strong>Quota:</strong></td>' + | |
|             '<td>' + | |
|             (bucket.quota_enabled ?  | |
|                 '<span class="badge bg-success">' + formatBytes(bucket.quota) + '</span>' :  | |
|                 '<span class="badge bg-secondary">Disabled</span>' | |
|             ) + | |
|             '</td>' + | |
|             '</tr>' + | |
|             '<tr>' + | |
|             '<td><strong>Versioning:</strong></td>' + | |
|             '<td>' + | |
|             (bucket.versioning_enabled ?  | |
|                 '<span class="badge bg-success"><i class="fas fa-check me-1"></i>Enabled</span>' :  | |
|                 '<span class="badge bg-secondary"><i class="fas fa-times me-1"></i>Disabled</span>' | |
|             ) + | |
|             '</td>' + | |
|             '</tr>' + | |
|             '<tr>' + | |
|             '<td><strong>Object Lock:</strong></td>' + | |
|             '<td>' + | |
|             (bucket.object_lock_enabled ?  | |
|                 '<span class="badge bg-warning"><i class="fas fa-lock me-1"></i>Enabled</span>' + | |
|                 '<br><small class="text-muted">' + bucket.object_lock_mode + ' • ' + bucket.object_lock_duration + ' days</small>' :  | |
|                 '<span class="badge bg-secondary"><i class="fas fa-unlock me-1"></i>Disabled</span>' | |
|             ) + | |
|             '</td>' + | |
|             '</tr>' + | |
|             '</table>' + | |
|             '</div>' + | |
|             '</div>' + | |
|             '<hr>' + | |
|             '<div class="row">' + | |
|             '<div class="col-12">' + | |
|             '<h6><i class="fas fa-list me-2"></i>Objects (' + objects.length + ')</h6>' + | |
|             objectsTable + | |
|             '</div>' + | |
|             '</div>'; | |
|          | |
|         document.getElementById('bucketDetailsContent').innerHTML = content; | |
|     } | |
| 
 | |
|     function exportBucketList() { | |
|         // Simple CSV export | |
|         const buckets = Array.from(document.querySelectorAll('#bucketsTable tbody tr')).map(row => { | |
|             const cells = row.querySelectorAll('td'); | |
|             if (cells.length > 1) { | |
|                 return { | |
|                     name: cells[0].textContent.trim(), | |
|                     created: cells[1].textContent.trim(), | |
|                     objects: cells[2].textContent.trim(), | |
|                     size: cells[3].textContent.trim(), | |
|                     quota: cells[4].textContent.trim(), | |
|                     versioning: cells[5].textContent.trim(), | |
|                     objectLock: cells[6].textContent.trim() | |
|                 }; | |
|             } | |
|             return null; | |
|         }).filter(bucket => bucket !== null); | |
| 
 | |
|         const csvContent = "data:text/csv;charset=utf-8," +  | |
|             "Name,Created,Objects,Size,Quota,Versioning,Object Lock\n" + | |
|             buckets.map(b => '"' + b.name + '","' + b.created + '","' + b.objects + '","' + b.size + '","' + b.quota + '","' + b.versioning + '","' + b.objectLock + '"').join("\n"); | |
| 
 | |
|         const encodedUri = encodeURI(csvContent); | |
|         const link = document.createElement("a"); | |
|         link.setAttribute("href", encodedUri); | |
|         link.setAttribute("download", "buckets.csv"); | |
|         document.body.appendChild(link); | |
|         link.click(); | |
|         document.body.removeChild(link); | |
|     } | |
|     </script> | |
| } | |
| 
 | |
| // Helper functions for template | |
| func getQuotaStatusColor(used, quota int64, enabled bool) string { | |
|     if !enabled || quota <= 0 { | |
|         return "secondary" | |
|     } | |
|      | |
|     percentage := float64(used) / float64(quota) * 100 | |
|     if percentage >= 90 { | |
|         return "danger" | |
|     } else if percentage >= 75 { | |
|         return "warning" | |
|     } else { | |
|         return "success" | |
|     } | |
| } | |
| 
 | |
| func getQuotaInMB(quotaBytes int64) int64 { | |
|     if quotaBytes < 0 { | |
|         quotaBytes = -quotaBytes // Handle disabled quotas (negative values) | |
|     } | |
|     return quotaBytes / (1024 * 1024) | |
| }  |