From 80cf7fbaaab57659dbb60c7fd0bccf6a53e615d8 Mon Sep 17 00:00:00 2001 From: Andrew Hoddinott Date: Sat, 30 May 2026 13:11:29 +0900 Subject: [PATCH] fix: browse by local alias if no global alias available In actions.tsx, object-actions.tsx and object-list.tsx, use bucket.id instead of bucketName. In hooks.ts, rename bucket to bucketId for clarity. In browse.go, when no global alias available, find a key with a local alias. --- backend/router/browse.go | 77 +++++++++++++------ src/pages/buckets/manage/browse/actions.tsx | 12 +-- src/pages/buckets/manage/browse/hooks.ts | 14 ++-- .../buckets/manage/browse/object-actions.tsx | 6 +- .../buckets/manage/browse/object-list.tsx | 4 +- 5 files changed, 71 insertions(+), 42 deletions(-) diff --git a/backend/router/browse.go b/backend/router/browse.go index 5df5acb..9e877d9 100644 --- a/backend/router/browse.go +++ b/backend/router/browse.go @@ -24,7 +24,7 @@ type Browse struct{} func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() - bucket := r.PathValue("bucket") + bucketId := r.PathValue("bucket") prefix := query.Get("prefix") continuationToken := query.Get("next") @@ -33,7 +33,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { limit = 100 } - client, err := getS3Client(bucket) + client, bucket, err := getS3Client(bucketId) if err != nil { utils.ResponseError(w, err) return @@ -73,7 +73,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { ObjectKey: &key, LastModified: object.LastModified, Size: object.Size, - Url: fmt.Sprintf("/browse/%s/%s", bucket, *object.Key), + Url: fmt.Sprintf("/browse/%s/%s", bucketId, *object.Key), }) } @@ -81,14 +81,14 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { } func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { - bucket := r.PathValue("bucket") + bucketId := r.PathValue("bucket") key := r.PathValue("key") queryParams := r.URL.Query() view := queryParams.Get("view") == "1" thumbnail := queryParams.Get("thumb") == "1" download := queryParams.Get("dl") == "1" - client, err := getS3Client(bucket) + client, bucket, err := getS3Client(bucketId) if err != nil { utils.ResponseError(w, err) return @@ -170,7 +170,7 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { } func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { - bucket := r.PathValue("bucket") + bucketId := r.PathValue("bucket") key := r.PathValue("key") isDirectory := strings.HasSuffix(key, "/") @@ -184,7 +184,7 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { defer file.Close() } - client, err := getS3Client(bucket) + client, bucket, err := getS3Client(bucketId) if err != nil { utils.ResponseError(w, err) return @@ -215,12 +215,12 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { } func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) { - bucket := r.PathValue("bucket") + bucketId := r.PathValue("bucket") key := r.PathValue("key") recursive := r.URL.Query().Get("recursive") == "true" isDirectory := strings.HasSuffix(key, "/") - client, err := getS3Client(bucket) + client, bucket, err := getS3Client(bucketId) if err != nil { utils.ResponseError(w, err) return @@ -284,15 +284,23 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) { utils.ResponseSuccess(w, res) } -func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { - cacheKey := fmt.Sprintf("key:%s", bucket) - cacheData := utils.Cache.Get(cacheKey) +type bucketAccess struct { + Credentials aws.CredentialsProvider + S3Bucket string +} - if cacheData != nil { - return cacheData.(aws.CredentialsProvider), nil +func getBucketAccess(bucketId string) (*bucketAccess, error) { + if bucketId == "" { + return nil, errors.New("bucket id is required") } - body, err := utils.Garage.Fetch("/v2/GetBucketInfo?globalAlias="+bucket, &utils.FetchOptions{}) + cacheKey := fmt.Sprintf("bucket-access:%s", bucketId) + cacheData := utils.Cache.Get(cacheKey) + if cacheData != nil { + return cacheData.(*bucketAccess), nil + } + + body, err := utils.Garage.Fetch("/v2/GetBucketInfo?id="+bucketId, &utils.FetchOptions{}) if err != nil { return nil, err } @@ -303,11 +311,24 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { } var key schema.KeyElement + bucket := "" + if len(bucketData.GlobalAliases) > 0 { + bucket = bucketData.GlobalAliases[0] + } + found := false for _, k := range bucketData.Keys { if !k.Permissions.Read || !k.Permissions.Write { continue } + // If no global alias, use local alias instead + if bucket == "" { + if len(k.BucketLocalAliases) == 0 { + continue + } + bucket = k.BucketLocalAliases[0] + } + found = true body, err := utils.Garage.Fetch(fmt.Sprintf("/v2/GetKeyInfo?id=%s&showSecretKey=true", k.AccessKeyID), &utils.FetchOptions{}) if err != nil { @@ -318,17 +339,25 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { } break } + if !found { + return nil, errors.New("no read-write key for bucket") + } - credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "") - utils.Cache.Set(cacheKey, credential, time.Hour) - - return credential, nil + if bucket == "" { + return nil, errors.New("bucket has no alias") + } + access := &bucketAccess{ + Credentials: credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, ""), + S3Bucket: bucket, + } + utils.Cache.Set(cacheKey, access, time.Hour) + return access, nil } -func getS3Client(bucket string) (*s3.Client, error) { - creds, err := getBucketCredentials(bucket) +func getS3Client(bucketId string) (*s3.Client, string, error) { + access, err := getBucketAccess(bucketId) if err != nil { - return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err) + return nil, "", fmt.Errorf("cannot get credentials for bucket %s: %w", bucketId, err) } // Determine endpoint and whether to disable HTTPS @@ -337,7 +366,7 @@ func getS3Client(bucket string) (*s3.Client, error) { // AWS config without BaseEndpoint awsConfig := aws.Config{ - Credentials: creds, + Credentials: access.Credentials, Region: utils.Garage.GetS3Region(), } @@ -353,5 +382,5 @@ func getS3Client(bucket string) (*s3.Client, error) { }) }) - return client, nil + return client, access.S3Bucket, nil } diff --git a/src/pages/buckets/manage/browse/actions.tsx b/src/pages/buckets/manage/browse/actions.tsx index fea7b84..c474270 100644 --- a/src/pages/buckets/manage/browse/actions.tsx +++ b/src/pages/buckets/manage/browse/actions.tsx @@ -18,13 +18,13 @@ type Props = { }; const Actions = ({ prefix }: Props) => { - const { bucketName } = useBucketContext(); + const { bucket } = useBucketContext(); const queryClient = useQueryClient(); - const putObject = usePutObject(bucketName, { + const putObject = usePutObject(bucket.id, { onSuccess: () => { toast.success("File uploaded!"); - queryClient.invalidateQueries({ queryKey: ["browse", bucketName] }); + queryClient.invalidateQueries({ queryKey: ["browse", bucket.id] }); }, onError: handleError, }); @@ -76,7 +76,7 @@ type CreateFolderActionProps = { const CreateFolderAction = ({ prefix }: CreateFolderActionProps) => { const { isOpen, onOpen, onClose } = useDisclosure(); - const { bucketName } = useBucketContext(); + const { bucket } = useBucketContext(); const queryClient = useQueryClient(); const form = useForm({ @@ -88,10 +88,10 @@ const CreateFolderAction = ({ prefix }: CreateFolderActionProps) => { if (isOpen) form.setFocus("name"); }, [isOpen]); - const createFolder = usePutObject(bucketName, { + const createFolder = usePutObject(bucket.id, { onSuccess: () => { toast.success("Folder created!"); - queryClient.invalidateQueries({ queryKey: ["browse", bucketName] }); + queryClient.invalidateQueries({ queryKey: ["browse", bucket.id] }); onClose(); form.reset(); }, diff --git a/src/pages/buckets/manage/browse/hooks.ts b/src/pages/buckets/manage/browse/hooks.ts index 203c049..6ff2699 100644 --- a/src/pages/buckets/manage/browse/hooks.ts +++ b/src/pages/buckets/manage/browse/hooks.ts @@ -11,18 +11,18 @@ import { } from "./types"; export const useBrowseObjects = ( - bucket: string, + bucketId: string, options?: UseBrowserObjectOptions ) => { return useQuery({ - queryKey: ["browse", bucket, options], + queryKey: ["browse", bucketId, options], queryFn: () => - api.get(`/browse/${bucket}`, { params: options }), + api.get(`/browse/${bucketId}`, { params: options }), }); }; export const usePutObject = ( - bucket: string, + bucketId: string, options?: UseMutationOptions ) => { return useMutation({ @@ -32,19 +32,19 @@ export const usePutObject = ( formData.append("file", body.file); } - return api.put(`/browse/${bucket}/${body.key}`, { body: formData }); + return api.put(`/browse/${bucketId}/${body.key}`, { body: formData }); }, ...options, }); }; export const useDeleteObject = ( - bucket: string, + bucketId: string, options?: UseMutationOptions ) => { return useMutation({ mutationFn: (data) => - api.delete(`/browse/${bucket}/${data.key}`, { + api.delete(`/browse/${bucketId}/${data.key}`, { params: { recursive: data.recursive }, }), ...options, diff --git a/src/pages/buckets/manage/browse/object-actions.tsx b/src/pages/buckets/manage/browse/object-actions.tsx index 97ed549..bef10f2 100644 --- a/src/pages/buckets/manage/browse/object-actions.tsx +++ b/src/pages/buckets/manage/browse/object-actions.tsx @@ -17,14 +17,14 @@ type Props = { }; const ObjectActions = ({ prefix = "", object, end }: Props) => { - const { bucketName } = useBucketContext(); + const { bucket } = useBucketContext(); const queryClient = useQueryClient(); const isDirectory = object.objectKey.endsWith("/"); - const deleteObject = useDeleteObject(bucketName, { + const deleteObject = useDeleteObject(bucket.id, { onSuccess: () => { toast.success("Object deleted!"); - queryClient.invalidateQueries({ queryKey: ["browse", bucketName] }); + queryClient.invalidateQueries({ queryKey: ["browse", bucket.id] }); }, onError: handleError, }); diff --git a/src/pages/buckets/manage/browse/object-list.tsx b/src/pages/buckets/manage/browse/object-list.tsx index 081fb80..6a96677 100644 --- a/src/pages/buckets/manage/browse/object-list.tsx +++ b/src/pages/buckets/manage/browse/object-list.tsx @@ -21,8 +21,8 @@ type Props = { }; const ObjectList = ({ prefix, onPrefixChange }: Props) => { - const { bucketName } = useBucketContext(); - const { data, error, isLoading } = useBrowseObjects(bucketName, { + const { bucket } = useBucketContext(); + const { data, error, isLoading } = useBrowseObjects(bucket.id, { prefix, limit: 1000, });