mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-28 16:07:08 +00:00
5d33382e30
* feat(backend,frontend): recursive delete of key prefixes in bulk actions
Bulk object selection previously only supported deleting individually
listed object keys — folders ("prefixes") could not be selected or
deleted, leaving no way to remove a directory and all of its contents.
Backend:
- Add S3Service.DeleteObjectsByPrefix, which recursively lists every
object under a prefix and batch-deletes them, returning the count.
- Extend the delete-multiple endpoint to accept a "prefixes" array
alongside "keys"; keys are batch-deleted and each prefix is deleted
recursively. Response now reports the total objects removed.
Frontend:
- Enable folder checkboxes and add a per-folder "Delete folder" action.
- Select-all now covers both files and folders.
- Route all bulk/folder deletes through a confirmation dialog that
spells out the file/folder counts and warns that folders are removed
recursively (previously bulk delete fired with no confirmation).
- api/hook send "prefixes"; optimistic update drops objects under any
deleted prefix.
* fix(backend): validate delete prefixes and count actual removals
Addresses maintainer review on the recursive prefix-delete endpoint:
- Reject blank/whitespace-only prefixes with a 400 instead of a 500, and
normalize each prefix to have a trailing "/" so "photos/2024" can no
longer also delete siblings like "photos/2024-old/..." on this
irreversible public endpoint.
- DeleteMultipleObjects now returns the number of objects actually
removed (requested keys minus failures) rather than assuming every
requested key was deleted; the handler sums real counts across the
keys and prefix paths. Draining the full RemoveObjects error channel
also fixes a potential sender-goroutine leak on early return.
- Add tests: blank-prefix -> 400, prefix trailing-slash normalization,
and S3Service.DeleteObjectsByPrefix (list-then-delete, empty prefix,
no-match, and list-error propagation).
* fix(frontend): align select-all with the active search filter
The header "select all" checkbox derived its checked state from the
filtered (searched) rows, but handleSelectAll operated on the full,
unfiltered object list — so with a search active it selected hidden
items and the checkbox state disagreed with the selection.
ObjectsTable now passes the keys of the currently visible (filtered)
rows to onSelectAll, and its checked state reflects whether every
visible row is selected. handleSelectAll toggles only those visible
rows, leaving any off-screen selection intact.
* fix(frontend): scope select-all to the visible page
After merging upstream's client-side deep-search pagination, the rendered
rows are pageObjects (one page slice) while select-all still operated on
filteredObjects (every match across hidden pages). Scope the header
checkbox's state and its select-all action to pageObjects so one click
never selects off-screen rows for a destructive bulk delete. In
normal/prefix browsing pageObjects === filteredObjects, so behavior there
is unchanged.
---------
Authored-by: Camilo Hollanda <775409+prem-prakash@users.noreply.github.com>
207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// DashboardMetrics represents aggregated metrics for the dashboard
|
|
type DashboardMetrics struct {
|
|
TotalSize int64 `json:"totalSize"`
|
|
ObjectCount int64 `json:"objectCount"`
|
|
BucketCount int `json:"bucketCount"`
|
|
UsageByBucket []BucketUsage `json:"usageByBucket"`
|
|
}
|
|
|
|
// BucketUsage represents storage usage for a single bucket
|
|
type BucketUsage struct {
|
|
BucketName string `json:"bucketName"`
|
|
Size int64 `json:"size"`
|
|
ObjectCount int64 `json:"objectCount"`
|
|
Percentage float64 `json:"percentage"`
|
|
}
|
|
|
|
// APIResponse is the standard response structure for all API endpoints
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error *APIError `json:"error,omitempty"`
|
|
}
|
|
|
|
// APIError represents an error in the API response
|
|
type APIError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// HealthResponse represents the health check response
|
|
type HealthResponse struct {
|
|
Status string `json:"status"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// BucketInfo represents information about a bucket
|
|
type BucketInfo struct {
|
|
Name string `json:"name"`
|
|
CreationDate time.Time `json:"creationDate"`
|
|
ObjectCount *int64 `json:"objectCount,omitempty"`
|
|
Size *int64 `json:"size,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
WebsiteAccess bool `json:"websiteAccess"`
|
|
WebsiteConfig *BucketWebsiteConfig `json:"websiteConfig,omitempty"`
|
|
Quotas *BucketQuotas `json:"quotas,omitempty"`
|
|
|
|
// EffectivePermissions is the caller's prefix-scoped permissions on this
|
|
// bucket, computed server-side. Omitted when access control is disabled.
|
|
EffectivePermissions []string `json:"effective_permissions,omitempty"`
|
|
}
|
|
|
|
// BucketListResponse represents a list of buckets
|
|
type BucketListResponse struct {
|
|
Buckets []BucketInfo `json:"buckets"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// ObjectInfo represents information about an object
|
|
type ObjectInfo struct {
|
|
Key string `json:"key"`
|
|
Size int64 `json:"size"`
|
|
LastModified time.Time `json:"last_modified"`
|
|
ETag string `json:"etag"`
|
|
ContentType string `json:"content_type,omitempty"`
|
|
StorageClass string `json:"storage_class,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// ObjectListResponse represents a list of objects in a bucket
|
|
type ObjectListResponse struct {
|
|
Bucket string `json:"bucket"`
|
|
Objects []ObjectInfo `json:"objects"`
|
|
Prefixes []string `json:"prefixes"`
|
|
Count int `json:"count"`
|
|
IsTruncated bool `json:"is_truncated"`
|
|
NextContinuationToken string `json:"next_continuation_token,omitempty"`
|
|
}
|
|
|
|
// ObjectUploadResponse represents the response after uploading an object
|
|
type ObjectUploadResponse struct {
|
|
Bucket string `json:"bucket"`
|
|
Key string `json:"key"`
|
|
ETag string `json:"etag"`
|
|
Size int64 `json:"size"`
|
|
ContentType string `json:"content_type"`
|
|
}
|
|
|
|
// ObjectUploadMultipleResponse represents the response after uploading multiple objects
|
|
type ObjectUploadMultipleResponse struct {
|
|
Bucket string `json:"bucket"`
|
|
TotalFiles int `json:"total_files"`
|
|
SuccessCount int `json:"success_count"`
|
|
FailureCount int `json:"failure_count"`
|
|
SuccessFiles []ObjectUploadResult `json:"success_files"`
|
|
FailedFiles []ObjectUploadFailedResult `json:"failed_files,omitempty"`
|
|
}
|
|
|
|
// ObjectUploadResult represents a successful upload result
|
|
type ObjectUploadResult struct {
|
|
Key string `json:"key"`
|
|
ETag string `json:"etag"`
|
|
Size int64 `json:"size"`
|
|
ContentType string `json:"content_type,omitempty"`
|
|
}
|
|
|
|
// ObjectUploadFailedResult represents a failed upload result
|
|
type ObjectUploadFailedResult struct {
|
|
Key string `json:"key"`
|
|
Error string `json:"error"`
|
|
ContentType string `json:"content_type,omitempty"`
|
|
}
|
|
|
|
// ObjectDeleteResponse represents the response after deleting an object
|
|
type ObjectDeleteResponse struct {
|
|
Bucket string `json:"bucket"`
|
|
Key string `json:"key"`
|
|
Deleted bool `json:"deleted"`
|
|
}
|
|
|
|
// UserInfo represents information about a Garage user (key pair)
|
|
type UserInfo struct {
|
|
AccessKeyID string `json:"accessKeyId"`
|
|
Name string `json:"name"`
|
|
SecretKey *string `json:"secretKey,omitempty"`
|
|
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
|
Status string `json:"status"` // "active" or "inactive"
|
|
BucketPermissions []BucketPermission `json:"permissions"` // Array of bucket permissions
|
|
Expiration *time.Time `json:"expiration,omitempty"`
|
|
Expired bool `json:"expired"`
|
|
}
|
|
|
|
// BucketPermission represents permissions for a specific bucket
|
|
type BucketPermission struct {
|
|
BucketID string `json:"bucketId"`
|
|
BucketName string `json:"bucketName"`
|
|
Read bool `json:"read"`
|
|
Write bool `json:"write"`
|
|
Owner bool `json:"owner"`
|
|
}
|
|
|
|
type PresignedURLResponse struct {
|
|
URL string `json:"url"`
|
|
ExpiresIn int64 `json:"expires_in"` // in seconds
|
|
Bucket string `json:"bucket"`
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
type ObjectDeleteMultipleResponse struct {
|
|
Bucket string `json:"bucket"`
|
|
Deleted int `json:"deleted"`
|
|
Keys []string `json:"keys"`
|
|
Prefixes []string `json:"prefixes,omitempty"`
|
|
}
|
|
|
|
// UserListResponse represents a list of users/keys
|
|
type UserListResponse struct {
|
|
Users []UserInfo `json:"users"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// Helper functions to create standard responses
|
|
|
|
// SuccessResponse creates a successful API response
|
|
func SuccessResponse(data interface{}) APIResponse {
|
|
return APIResponse{
|
|
Success: true,
|
|
Data: data,
|
|
Error: nil,
|
|
}
|
|
}
|
|
|
|
// ErrorResponse creates an error API response
|
|
func ErrorResponse(code, message string) APIResponse {
|
|
return APIResponse{
|
|
Success: false,
|
|
Data: nil,
|
|
Error: &APIError{
|
|
Code: code,
|
|
Message: message,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Common error codes
|
|
const (
|
|
ErrCodeBadRequest = "BAD_REQUEST"
|
|
ErrCodeUnauthorized = "UNAUTHORIZED"
|
|
ErrCodeForbidden = "FORBIDDEN"
|
|
ErrCodeNotFound = "NOT_FOUND"
|
|
ErrCodeConflict = "CONFLICT"
|
|
ErrCodeInternalError = "INTERNAL_ERROR"
|
|
ErrCodeBucketExists = "BUCKET_ALREADY_EXISTS"
|
|
ErrCodeBucketNotFound = "BUCKET_NOT_FOUND"
|
|
ErrCodeObjectNotFound = "OBJECT_NOT_FOUND"
|
|
ErrCodeInvalidBucketName = "INVALID_BUCKET_NAME"
|
|
ErrCodeInvalidObjectKey = "INVALID_OBJECT_KEY"
|
|
ErrCodeUploadFailed = "UPLOAD_FAILED"
|
|
ErrCodeDeleteFailed = "DELETE_FAILED"
|
|
ErrCodeListFailed = "LIST_FAILED"
|
|
ErrCodeUnsupported = "UNSUPPORTED"
|
|
)
|