Merge 80cf7fbaaab57659dbb60c7fd0bccf6a53e615d8 into ee420fbf2946e9f79977615cee5e29192d7da478

This commit is contained in:
Andrew Hoddinott 2026-05-30 13:21:17 +09:00 committed by GitHub
commit 6d748c3c28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 71 additions and 42 deletions

View File

@ -24,7 +24,7 @@ type Browse struct{}
func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query() query := r.URL.Query()
bucket := r.PathValue("bucket") bucketId := r.PathValue("bucket")
prefix := query.Get("prefix") prefix := query.Get("prefix")
continuationToken := query.Get("next") continuationToken := query.Get("next")
@ -33,7 +33,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
limit = 100 limit = 100
} }
client, err := getS3Client(bucket) client, bucket, err := getS3Client(bucketId)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -73,7 +73,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
ObjectKey: &key, ObjectKey: &key,
LastModified: object.LastModified, LastModified: object.LastModified,
Size: object.Size, 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) { func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) {
bucket := r.PathValue("bucket") bucketId := r.PathValue("bucket")
key := r.PathValue("key") key := r.PathValue("key")
queryParams := r.URL.Query() queryParams := r.URL.Query()
view := queryParams.Get("view") == "1" view := queryParams.Get("view") == "1"
thumbnail := queryParams.Get("thumb") == "1" thumbnail := queryParams.Get("thumb") == "1"
download := queryParams.Get("dl") == "1" download := queryParams.Get("dl") == "1"
client, err := getS3Client(bucket) client, bucket, err := getS3Client(bucketId)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return 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) { func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
bucket := r.PathValue("bucket") bucketId := r.PathValue("bucket")
key := r.PathValue("key") key := r.PathValue("key")
isDirectory := strings.HasSuffix(key, "/") isDirectory := strings.HasSuffix(key, "/")
@ -184,7 +184,7 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
defer file.Close() defer file.Close()
} }
client, err := getS3Client(bucket) client, bucket, err := getS3Client(bucketId)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return 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) { func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
bucket := r.PathValue("bucket") bucketId := r.PathValue("bucket")
key := r.PathValue("key") key := r.PathValue("key")
recursive := r.URL.Query().Get("recursive") == "true" recursive := r.URL.Query().Get("recursive") == "true"
isDirectory := strings.HasSuffix(key, "/") isDirectory := strings.HasSuffix(key, "/")
client, err := getS3Client(bucket) client, bucket, err := getS3Client(bucketId)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -284,15 +284,23 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
utils.ResponseSuccess(w, res) utils.ResponseSuccess(w, res)
} }
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { type bucketAccess struct {
cacheKey := fmt.Sprintf("key:%s", bucket) Credentials aws.CredentialsProvider
cacheData := utils.Cache.Get(cacheKey) S3Bucket string
if cacheData != nil {
return cacheData.(aws.CredentialsProvider), nil
} }
body, err := utils.Garage.Fetch("/v2/GetBucketInfo?globalAlias="+bucket, &utils.FetchOptions{}) func getBucketAccess(bucketId string) (*bucketAccess, error) {
if bucketId == "" {
return nil, errors.New("bucket id is required")
}
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 { if err != nil {
return nil, err return nil, err
} }
@ -303,11 +311,24 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
} }
var key schema.KeyElement var key schema.KeyElement
bucket := ""
if len(bucketData.GlobalAliases) > 0 {
bucket = bucketData.GlobalAliases[0]
}
found := false
for _, k := range bucketData.Keys { for _, k := range bucketData.Keys {
if !k.Permissions.Read || !k.Permissions.Write { if !k.Permissions.Read || !k.Permissions.Write {
continue 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{}) body, err := utils.Garage.Fetch(fmt.Sprintf("/v2/GetKeyInfo?id=%s&showSecretKey=true", k.AccessKeyID), &utils.FetchOptions{})
if err != nil { if err != nil {
@ -318,17 +339,25 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
} }
break break
} }
if !found {
credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "") return nil, errors.New("no read-write key for bucket")
utils.Cache.Set(cacheKey, credential, time.Hour)
return credential, nil
} }
func getS3Client(bucket string) (*s3.Client, error) { if bucket == "" {
creds, err := getBucketCredentials(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(bucketId string) (*s3.Client, string, error) {
access, err := getBucketAccess(bucketId)
if err != nil { 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 // Determine endpoint and whether to disable HTTPS
@ -337,7 +366,7 @@ func getS3Client(bucket string) (*s3.Client, error) {
// AWS config without BaseEndpoint // AWS config without BaseEndpoint
awsConfig := aws.Config{ awsConfig := aws.Config{
Credentials: creds, Credentials: access.Credentials,
Region: utils.Garage.GetS3Region(), Region: utils.Garage.GetS3Region(),
} }
@ -353,5 +382,5 @@ func getS3Client(bucket string) (*s3.Client, error) {
}) })
}) })
return client, nil return client, access.S3Bucket, nil
} }

View File

@ -18,13 +18,13 @@ type Props = {
}; };
const Actions = ({ prefix }: Props) => { const Actions = ({ prefix }: Props) => {
const { bucketName } = useBucketContext(); const { bucket } = useBucketContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const putObject = usePutObject(bucketName, { const putObject = usePutObject(bucket.id, {
onSuccess: () => { onSuccess: () => {
toast.success("File uploaded!"); toast.success("File uploaded!");
queryClient.invalidateQueries({ queryKey: ["browse", bucketName] }); queryClient.invalidateQueries({ queryKey: ["browse", bucket.id] });
}, },
onError: handleError, onError: handleError,
}); });
@ -76,7 +76,7 @@ type CreateFolderActionProps = {
const CreateFolderAction = ({ prefix }: CreateFolderActionProps) => { const CreateFolderAction = ({ prefix }: CreateFolderActionProps) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { bucketName } = useBucketContext(); const { bucket } = useBucketContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const form = useForm<CreateFolderSchema>({ const form = useForm<CreateFolderSchema>({
@ -88,10 +88,10 @@ const CreateFolderAction = ({ prefix }: CreateFolderActionProps) => {
if (isOpen) form.setFocus("name"); if (isOpen) form.setFocus("name");
}, [isOpen]); }, [isOpen]);
const createFolder = usePutObject(bucketName, { const createFolder = usePutObject(bucket.id, {
onSuccess: () => { onSuccess: () => {
toast.success("Folder created!"); toast.success("Folder created!");
queryClient.invalidateQueries({ queryKey: ["browse", bucketName] }); queryClient.invalidateQueries({ queryKey: ["browse", bucket.id] });
onClose(); onClose();
form.reset(); form.reset();
}, },

View File

@ -11,18 +11,18 @@ import {
} from "./types"; } from "./types";
export const useBrowseObjects = ( export const useBrowseObjects = (
bucket: string, bucketId: string,
options?: UseBrowserObjectOptions options?: UseBrowserObjectOptions
) => { ) => {
return useQuery({ return useQuery({
queryKey: ["browse", bucket, options], queryKey: ["browse", bucketId, options],
queryFn: () => queryFn: () =>
api.get<GetObjectsResult>(`/browse/${bucket}`, { params: options }), api.get<GetObjectsResult>(`/browse/${bucketId}`, { params: options }),
}); });
}; };
export const usePutObject = ( export const usePutObject = (
bucket: string, bucketId: string,
options?: UseMutationOptions<any, Error, PutObjectPayload> options?: UseMutationOptions<any, Error, PutObjectPayload>
) => { ) => {
return useMutation({ return useMutation({
@ -32,19 +32,19 @@ export const usePutObject = (
formData.append("file", body.file); formData.append("file", body.file);
} }
return api.put(`/browse/${bucket}/${body.key}`, { body: formData }); return api.put(`/browse/${bucketId}/${body.key}`, { body: formData });
}, },
...options, ...options,
}); });
}; };
export const useDeleteObject = ( export const useDeleteObject = (
bucket: string, bucketId: string,
options?: UseMutationOptions<any, Error, { key: string; recursive?: boolean }> options?: UseMutationOptions<any, Error, { key: string; recursive?: boolean }>
) => { ) => {
return useMutation({ return useMutation({
mutationFn: (data) => mutationFn: (data) =>
api.delete(`/browse/${bucket}/${data.key}`, { api.delete(`/browse/${bucketId}/${data.key}`, {
params: { recursive: data.recursive }, params: { recursive: data.recursive },
}), }),
...options, ...options,

View File

@ -17,14 +17,14 @@ type Props = {
}; };
const ObjectActions = ({ prefix = "", object, end }: Props) => { const ObjectActions = ({ prefix = "", object, end }: Props) => {
const { bucketName } = useBucketContext(); const { bucket } = useBucketContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isDirectory = object.objectKey.endsWith("/"); const isDirectory = object.objectKey.endsWith("/");
const deleteObject = useDeleteObject(bucketName, { const deleteObject = useDeleteObject(bucket.id, {
onSuccess: () => { onSuccess: () => {
toast.success("Object deleted!"); toast.success("Object deleted!");
queryClient.invalidateQueries({ queryKey: ["browse", bucketName] }); queryClient.invalidateQueries({ queryKey: ["browse", bucket.id] });
}, },
onError: handleError, onError: handleError,
}); });

View File

@ -21,8 +21,8 @@ type Props = {
}; };
const ObjectList = ({ prefix, onPrefixChange }: Props) => { const ObjectList = ({ prefix, onPrefixChange }: Props) => {
const { bucketName } = useBucketContext(); const { bucket } = useBucketContext();
const { data, error, isLoading } = useBrowseObjects(bucketName, { const { data, error, isLoading } = useBrowseObjects(bucket.id, {
prefix, prefix,
limit: 1000, limit: 1000,
}); });