@scality/data-browser-library 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/__tests__/BucketOverview.test.js +94 -9
- package/dist/components/buckets/BucketList.js +2 -1
- package/dist/components/buckets/BucketOverview.js +98 -13
- package/dist/components/buckets/EmptyBucketButton.js +5 -2
- package/dist/components/buckets/PublicAccessBlock.d.ts +18 -0
- package/dist/components/buckets/PublicAccessBlock.js +113 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +2 -1
- package/dist/components/objects/ObjectList.js +6 -31
- package/dist/hooks/__tests__/usePresigningS3Client.test.js +44 -23
- package/dist/hooks/bucketConfiguration.d.ts +3 -1
- package/dist/hooks/bucketConfiguration.js +9 -2
- package/dist/hooks/factories/index.d.ts +1 -1
- package/dist/hooks/factories/useCreateS3MutationHook.d.ts +2 -1
- package/dist/hooks/factories/useCreateS3MutationHook.js +19 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +2 -2
- package/dist/hooks/presignedOperations.js +6 -5
- package/dist/hooks/useDownloadObject.d.ts +18 -0
- package/dist/hooks/useDownloadObject.js +59 -0
- package/dist/hooks/usePresigningS3Client.d.ts +15 -8
- package/dist/hooks/usePresigningS3Client.js +32 -4
- package/dist/test/msw/handlers/getPublicAccessBlock.d.ts +2 -0
- package/dist/test/msw/handlers/getPublicAccessBlock.js +50 -0
- package/dist/test/msw/handlers.js +4 -1
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
3
3
|
import { MemoryRouter } from "react-router";
|
|
4
|
-
import { useGetBucketAcl, useGetBucketCors, useGetBucketLocation, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketTagging, useGetBucketVersioning, useISVBucketStatus } from "../../hooks/index.js";
|
|
4
|
+
import { useGetBucketAcl, useGetBucketCors, useGetBucketLocation, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useISVBucketStatus, usePutPublicAccessBlock, useSetBucketAcl } from "../../hooks/index.js";
|
|
5
5
|
import { useFeatures } from "../../hooks/useFeatures.js";
|
|
6
|
-
import { applyBucketMocks, createTestWrapper } from "../../test/testUtils.js";
|
|
6
|
+
import { applyBucketMocks, createMockMutationResult, createTestWrapper } from "../../test/testUtils.js";
|
|
7
7
|
import { EnhancedS3Error, ErrorCategory } from "../../utils/errorHandling.js";
|
|
8
8
|
import { BucketOverview, useBucketOverviewContext } from "../buckets/BucketOverview.js";
|
|
9
9
|
import * as __rspack_external__contexts_DataBrowserUICustomizationContext_js_f267b01c from "../../contexts/DataBrowserUICustomizationContext.js";
|
|
@@ -16,6 +16,9 @@ const mockUseGetBucketCors = jest.mocked(useGetBucketCors);
|
|
|
16
16
|
const mockUseGetBucketObjectLockConfiguration = jest.mocked(useGetBucketObjectLockConfiguration);
|
|
17
17
|
const mockUseGetBucketPolicy = jest.mocked(useGetBucketPolicy);
|
|
18
18
|
const mockUseGetBucketTagging = jest.mocked(useGetBucketTagging);
|
|
19
|
+
const mockUseGetPublicAccessBlock = jest.mocked(useGetPublicAccessBlock);
|
|
20
|
+
const mockUsePutPublicAccessBlock = jest.mocked(usePutPublicAccessBlock);
|
|
21
|
+
const mockUseSetBucketAcl = jest.mocked(useSetBucketAcl);
|
|
19
22
|
const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
|
|
20
23
|
const mockUseFeatures = jest.mocked(useFeatures);
|
|
21
24
|
const mockUseDataBrowserUICustomization = (config = {})=>{
|
|
@@ -69,6 +72,20 @@ describe('BucketOverview', ()=>{
|
|
|
69
72
|
jest.clearAllMocks();
|
|
70
73
|
mockHookDefaults();
|
|
71
74
|
mockUseDataBrowserUICustomization({});
|
|
75
|
+
mockUseGetPublicAccessBlock.mockReturnValue({
|
|
76
|
+
data: {
|
|
77
|
+
PublicAccessBlockConfiguration: {
|
|
78
|
+
BlockPublicAcls: false,
|
|
79
|
+
IgnorePublicAcls: false,
|
|
80
|
+
BlockPublicPolicy: false,
|
|
81
|
+
RestrictPublicBuckets: false
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
status: 'success',
|
|
85
|
+
error: null
|
|
86
|
+
});
|
|
87
|
+
mockUsePutPublicAccessBlock.mockReturnValue(createMockMutationResult(jest.fn()));
|
|
88
|
+
mockUseSetBucketAcl.mockReturnValue(createMockMutationResult(jest.fn()));
|
|
72
89
|
});
|
|
73
90
|
it('renders bucket overview with all sections', ()=>{
|
|
74
91
|
renderBucketOverview();
|
|
@@ -100,7 +117,9 @@ describe('BucketOverview', ()=>{
|
|
|
100
117
|
status: 'success'
|
|
101
118
|
});
|
|
102
119
|
renderBucketOverview();
|
|
103
|
-
|
|
120
|
+
const versioningLabel = screen.getByText('Versioning');
|
|
121
|
+
const row = versioningLabel.closest('[class]').parentElement;
|
|
122
|
+
expect(within(row).getByText('Inactive')).toBeInTheDocument();
|
|
104
123
|
});
|
|
105
124
|
it('displays bucket location', ()=>{
|
|
106
125
|
mockUseGetBucketLocation.mockReturnValue({
|
|
@@ -156,7 +175,9 @@ describe('BucketOverview', ()=>{
|
|
|
156
175
|
status: 'success'
|
|
157
176
|
});
|
|
158
177
|
renderBucketOverview();
|
|
159
|
-
|
|
178
|
+
const retentionLabel = screen.getByText('Default Retention');
|
|
179
|
+
const row = retentionLabel.closest('[class]').parentElement;
|
|
180
|
+
expect(within(row).getByText('Inactive')).toBeInTheDocument();
|
|
160
181
|
});
|
|
161
182
|
it('shows default retention with days in Governance mode', ()=>{
|
|
162
183
|
mockUseGetBucketObjectLockConfiguration.mockReturnValue({
|
|
@@ -378,7 +399,9 @@ describe('BucketOverview', ()=>{
|
|
|
378
399
|
status: 'success'
|
|
379
400
|
});
|
|
380
401
|
renderBucketOverview();
|
|
381
|
-
|
|
402
|
+
const publicToggle = document.getElementById('publicToggle');
|
|
403
|
+
expect(publicToggle).toBeInTheDocument();
|
|
404
|
+
expect(publicToggle).toBeChecked();
|
|
382
405
|
});
|
|
383
406
|
it('shows non-public bucket when no public grants', ()=>{
|
|
384
407
|
mockUseGetBucketAcl.mockReturnValue({
|
|
@@ -398,8 +421,68 @@ describe('BucketOverview', ()=>{
|
|
|
398
421
|
status: 'success'
|
|
399
422
|
});
|
|
400
423
|
renderBucketOverview();
|
|
401
|
-
const
|
|
402
|
-
expect(
|
|
424
|
+
const publicToggle = document.getElementById('publicToggle');
|
|
425
|
+
expect(publicToggle).toBeInTheDocument();
|
|
426
|
+
expect(publicToggle).not.toBeChecked();
|
|
427
|
+
});
|
|
428
|
+
it('shows disabled toggle and warning when IgnorePublicAcls blocks public access', ()=>{
|
|
429
|
+
mockUseGetBucketAcl.mockReturnValue({
|
|
430
|
+
data: {
|
|
431
|
+
Owner: {
|
|
432
|
+
DisplayName: 'owner'
|
|
433
|
+
},
|
|
434
|
+
Grants: [
|
|
435
|
+
{
|
|
436
|
+
Grantee: {
|
|
437
|
+
URI: 'http://acs.amazonaws.com/groups/global/AllUsers'
|
|
438
|
+
},
|
|
439
|
+
Permission: 'READ'
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
status: 'success'
|
|
444
|
+
});
|
|
445
|
+
mockUseGetPublicAccessBlock.mockReturnValue({
|
|
446
|
+
data: {
|
|
447
|
+
PublicAccessBlockConfiguration: {
|
|
448
|
+
BlockPublicAcls: true,
|
|
449
|
+
IgnorePublicAcls: true,
|
|
450
|
+
BlockPublicPolicy: false,
|
|
451
|
+
RestrictPublicBuckets: false
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
status: 'success',
|
|
455
|
+
error: null
|
|
456
|
+
});
|
|
457
|
+
renderBucketOverview();
|
|
458
|
+
const publicToggle = document.getElementById('publicToggle');
|
|
459
|
+
expect(publicToggle).toBeInTheDocument();
|
|
460
|
+
expect(publicToggle).not.toBeChecked();
|
|
461
|
+
expect(publicToggle).toBeDisabled();
|
|
462
|
+
expect(screen.getByText('Public access blocked by advanced settings below.')).toBeInTheDocument();
|
|
463
|
+
});
|
|
464
|
+
it('shows PAB collapsible section when PAB is supported', ()=>{
|
|
465
|
+
renderBucketOverview();
|
|
466
|
+
expect(screen.getByText('Public Access Block (PAB)')).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
it('hides PAB section when API returns 501', ()=>{
|
|
469
|
+
mockUseGetPublicAccessBlock.mockReturnValue({
|
|
470
|
+
data: void 0,
|
|
471
|
+
status: 'error',
|
|
472
|
+
error: {
|
|
473
|
+
statusCode: 501
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
renderBucketOverview();
|
|
477
|
+
expect(screen.queryByText('Public Access Block (PAB)')).not.toBeInTheDocument();
|
|
478
|
+
});
|
|
479
|
+
it('expands PAB section and shows 4 toggles', ()=>{
|
|
480
|
+
renderBucketOverview();
|
|
481
|
+
fireEvent.click(screen.getByText('Public Access Block (PAB)'));
|
|
482
|
+
expect(screen.getByText('BlockPublicAcls')).toBeInTheDocument();
|
|
483
|
+
expect(screen.getByText('IgnorePublicAcls')).toBeInTheDocument();
|
|
484
|
+
expect(screen.getByText('BlockPublicPolicy')).toBeInTheDocument();
|
|
485
|
+
expect(screen.getByText('RestrictPublicBuckets')).toBeInTheDocument();
|
|
403
486
|
});
|
|
404
487
|
it('shows loading states while data is being fetched', ()=>{
|
|
405
488
|
mockUseGetBucketVersioning.mockReturnValue({
|
|
@@ -516,7 +599,9 @@ describe('BucketOverview', ()=>{
|
|
|
516
599
|
status: 'success'
|
|
517
600
|
});
|
|
518
601
|
renderBucketOverview();
|
|
519
|
-
|
|
602
|
+
const versioningLabel = screen.getByText('Versioning');
|
|
603
|
+
const row = versioningLabel.closest('[class]').parentElement;
|
|
604
|
+
expect(within(row).getByText('Inactive')).toBeInTheDocument();
|
|
520
605
|
});
|
|
521
606
|
it('handles CORS error state', ()=>{
|
|
522
607
|
mockUseGetBucketCors.mockReturnValue({
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ConstrainedText, Icon, Loader, Stack, spacing } from "@scality/core-ui";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ConstrainedText, Icon, Loader, Stack, Text, Toggle, Tooltip, spacing, useToast } from "@scality/core-ui";
|
|
3
3
|
import { Box, Button } from "@scality/core-ui/dist/next";
|
|
4
|
-
import { createContext, memo, useContext, useMemo } from "react";
|
|
4
|
+
import { Fragment as external_react_Fragment, createContext, memo, useCallback, useContext, useMemo } from "react";
|
|
5
5
|
import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
|
|
6
|
-
import { useGetBucketAcl, useGetBucketCors, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketVersioning, useISVBucketStatus } from "../../hooks/index.js";
|
|
6
|
+
import { useGetBucketAcl, useGetBucketCors, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketVersioning, useISVBucketStatus, useSetBucketAcl } from "../../hooks/index.js";
|
|
7
7
|
import { isNotFoundError } from "../../utils/errorHandling.js";
|
|
8
8
|
import { EditRetentionButton } from "../objects/ObjectLock/EditRetentionButton.js";
|
|
9
9
|
import { useDataBrowserConfig } from "../providers/DataBrowserProvider.js";
|
|
10
10
|
import { Body, Group, GroupContent, GroupName, GroupValues, Key, Row, Table, TableContainer, Value } from "../ui/Table.elements.js";
|
|
11
11
|
import { BucketConfigEditButton } from "./BucketConfigEditButton.js";
|
|
12
12
|
import { BucketLocation } from "./BucketLocation.js";
|
|
13
|
+
import { PublicAccessBlock, usePublicAccessBlockStatus } from "./PublicAccessBlock.js";
|
|
13
14
|
const DEFAULT_PUBLIC_ACL_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
|
|
14
15
|
const STATUS_PENDING = 'pending';
|
|
15
16
|
const STATUS_ERROR = 'error';
|
|
@@ -189,8 +190,26 @@ const createCorsField = (bucketName, corsData, corsStatus, corsError, onEditCors
|
|
|
189
190
|
})
|
|
190
191
|
};
|
|
191
192
|
};
|
|
192
|
-
const createPublicField = (bucketName, aclStatus, isPublic, publicField, renderPublic)=>{
|
|
193
|
-
|
|
193
|
+
const createPublicField = ({ bucketName, aclStatus, isPublic, pabStatus, onPublicToggle, publicField, renderPublic })=>{
|
|
194
|
+
let defaultValue;
|
|
195
|
+
if (aclStatus === STATUS_PENDING || pabStatus.isPending) defaultValue = /*#__PURE__*/ jsx(Loader, {});
|
|
196
|
+
else {
|
|
197
|
+
const isDisabled = !pabStatus.isNotImplemented && (pabStatus.isPublicIgnored || pabStatus.isSettingPublicBlocked && !isPublic);
|
|
198
|
+
const toggleElement = /*#__PURE__*/ jsx(Toggle, {
|
|
199
|
+
id: "publicToggle",
|
|
200
|
+
disabled: isDisabled,
|
|
201
|
+
toggle: isPublic,
|
|
202
|
+
label: isPublic ? 'Yes' : 'No',
|
|
203
|
+
onChange: ()=>onPublicToggle?.()
|
|
204
|
+
});
|
|
205
|
+
defaultValue = isDisabled ? /*#__PURE__*/ jsx(Tooltip, {
|
|
206
|
+
overlay: "Public access is blocked by Public Access Block settings",
|
|
207
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
208
|
+
display: "inline-block",
|
|
209
|
+
children: toggleElement
|
|
210
|
+
})
|
|
211
|
+
}) : toggleElement;
|
|
212
|
+
}
|
|
194
213
|
return {
|
|
195
214
|
id: 'public',
|
|
196
215
|
label: 'Public',
|
|
@@ -434,16 +453,54 @@ const PermissionsSection = /*#__PURE__*/ memo(({ onEditPolicy, onEditCors, owner
|
|
|
434
453
|
const { data: policyData, error: policyError, status: policyStatus } = useGetBucketPolicy({
|
|
435
454
|
Bucket: bucketName
|
|
436
455
|
});
|
|
437
|
-
const
|
|
456
|
+
const pabStatus = usePublicAccessBlockStatus(bucketName);
|
|
457
|
+
const { mutate: setBucketAcl } = useSetBucketAcl();
|
|
458
|
+
const { showToast } = useToast();
|
|
459
|
+
const hasPublicAcl = useMemo(()=>aclData?.Grants?.some((grant)=>grant.Grantee?.URI === (config.publicAclIndicator || DEFAULT_PUBLIC_ACL_URI)) ?? false, [
|
|
438
460
|
aclData?.Grants,
|
|
439
461
|
config.publicAclIndicator
|
|
440
462
|
]);
|
|
463
|
+
const isPublic = hasPublicAcl && !pabStatus.isPublicIgnored;
|
|
464
|
+
const handlePublicToggle = useCallback(()=>{
|
|
465
|
+
setBucketAcl({
|
|
466
|
+
Bucket: bucketName,
|
|
467
|
+
ACL: isPublic ? 'private' : 'public-read'
|
|
468
|
+
}, {
|
|
469
|
+
onSuccess: ()=>{
|
|
470
|
+
showToast({
|
|
471
|
+
open: true,
|
|
472
|
+
message: `Bucket is now ${isPublic ? 'private' : 'public'}`,
|
|
473
|
+
status: 'success'
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
onError: (error)=>{
|
|
477
|
+
showToast({
|
|
478
|
+
open: true,
|
|
479
|
+
message: error instanceof Error ? error.message : 'Failed to update bucket visibility',
|
|
480
|
+
status: 'error'
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}, [
|
|
485
|
+
bucketName,
|
|
486
|
+
isPublic,
|
|
487
|
+
setBucketAcl,
|
|
488
|
+
showToast
|
|
489
|
+
]);
|
|
441
490
|
const fields = useMemo(()=>{
|
|
442
491
|
const defaultFields = {
|
|
443
492
|
owner: createOwnerField(bucketName, aclData, aclStatus, ownerField, renderOwner),
|
|
444
493
|
acl: createAclField(bucketName, aclData, aclStatus, aclField, renderAcl),
|
|
445
494
|
cors: createCorsField(bucketName, corsData, corsStatus, corsError, onEditCors, corsField, renderCors, isISVManaged, isvApplication),
|
|
446
|
-
public: createPublicField(
|
|
495
|
+
public: createPublicField({
|
|
496
|
+
bucketName,
|
|
497
|
+
aclStatus,
|
|
498
|
+
isPublic,
|
|
499
|
+
pabStatus,
|
|
500
|
+
onPublicToggle: handlePublicToggle,
|
|
501
|
+
publicField,
|
|
502
|
+
renderPublic
|
|
503
|
+
}),
|
|
447
504
|
bucketPolicy: createBucketPolicyField(bucketName, policyData, policyStatus, policyError, onEditPolicy, bucketPolicyField, renderBucketPolicy, isISVManaged, isvApplication)
|
|
448
505
|
};
|
|
449
506
|
return mergeFieldsWithExtras(defaultFields, extraBucketOverviewPermissions, bucketName, [
|
|
@@ -478,14 +535,42 @@ const PermissionsSection = /*#__PURE__*/ memo(({ onEditPolicy, onEditCors, owner
|
|
|
478
535
|
policyStatus,
|
|
479
536
|
isPublic,
|
|
480
537
|
isISVManaged,
|
|
481
|
-
isvApplication
|
|
538
|
+
isvApplication,
|
|
539
|
+
pabStatus,
|
|
540
|
+
handlePublicToggle
|
|
482
541
|
]);
|
|
542
|
+
const showPab = !pabStatus.isNotImplemented && !pabStatus.isPending;
|
|
483
543
|
return /*#__PURE__*/ jsx(Section, {
|
|
484
544
|
title: "Permissions",
|
|
485
|
-
children: fields.map((field)=>/*#__PURE__*/
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
545
|
+
children: fields.map((field)=>/*#__PURE__*/ jsxs(external_react_Fragment, {
|
|
546
|
+
children: [
|
|
547
|
+
/*#__PURE__*/ jsx(Field, {
|
|
548
|
+
label: field.label,
|
|
549
|
+
actions: field.actions,
|
|
550
|
+
children: field.value
|
|
551
|
+
}),
|
|
552
|
+
'public' === field.id && showPab && /*#__PURE__*/ jsxs(Fragment, {
|
|
553
|
+
children: [
|
|
554
|
+
(pabStatus.isPublicIgnored || pabStatus.isSettingPublicBlocked && !isPublic) && /*#__PURE__*/ jsxs(Row, {
|
|
555
|
+
children: [
|
|
556
|
+
/*#__PURE__*/ jsx(Key, {}),
|
|
557
|
+
/*#__PURE__*/ jsx(Value, {
|
|
558
|
+
children: /*#__PURE__*/ jsx(Text, {
|
|
559
|
+
color: "statusWarning",
|
|
560
|
+
variant: "Small",
|
|
561
|
+
children: "Public access blocked by advanced settings below."
|
|
562
|
+
})
|
|
563
|
+
})
|
|
564
|
+
]
|
|
565
|
+
}),
|
|
566
|
+
/*#__PURE__*/ jsx(PublicAccessBlock, {
|
|
567
|
+
bucketName: bucketName,
|
|
568
|
+
data: pabStatus.data,
|
|
569
|
+
status: pabStatus.status
|
|
570
|
+
})
|
|
571
|
+
]
|
|
572
|
+
})
|
|
573
|
+
]
|
|
489
574
|
}, field.id))
|
|
490
575
|
});
|
|
491
576
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Banner, Icon, Loader, Modal, Stack, Text, Tooltip, Wrap } from "@scality/core-ui";
|
|
2
|
+
import { Banner, ConstrainedText, Icon, Loader, Modal, Stack, Text, Tooltip, Wrap } from "@scality/core-ui";
|
|
3
3
|
import { Input } from "@scality/core-ui/dist/components/inputv2/inputv2";
|
|
4
4
|
import { Box, Button } from "@scality/core-ui/dist/next";
|
|
5
5
|
import { useMemo, useState } from "react";
|
|
@@ -91,7 +91,10 @@ const EmptyBucketButton = ({ bucketName })=>{
|
|
|
91
91
|
}),
|
|
92
92
|
/*#__PURE__*/ jsx(Modal, {
|
|
93
93
|
isOpen: isEmptyModalOpen,
|
|
94
|
-
title:
|
|
94
|
+
title: /*#__PURE__*/ jsx(ConstrainedText, {
|
|
95
|
+
text: `Empty Bucket '${bucketName}'?`,
|
|
96
|
+
lineClamp: 1
|
|
97
|
+
}),
|
|
95
98
|
close: ()=>{
|
|
96
99
|
if (!isEmptying) {
|
|
97
100
|
setIsEmptyModalOpen(false);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GetPublicAccessBlockOutput } from '@aws-sdk/client-s3';
|
|
2
|
+
interface PublicAccessBlockProps {
|
|
3
|
+
bucketName: string;
|
|
4
|
+
data: GetPublicAccessBlockOutput | undefined;
|
|
5
|
+
status: 'pending' | 'success' | 'error';
|
|
6
|
+
}
|
|
7
|
+
export declare const PublicAccessBlock: import("react").MemoExoticComponent<({ bucketName, data, status }: PublicAccessBlockProps) => import("react/jsx-runtime").JSX.Element | null>;
|
|
8
|
+
export declare function usePublicAccessBlockStatus(bucketName: string): {
|
|
9
|
+
data: import("@aws-sdk/client-s3").GetPublicAccessBlockCommandOutput | undefined;
|
|
10
|
+
status: "error" | "success" | "pending";
|
|
11
|
+
isNotImplemented: boolean;
|
|
12
|
+
isPending: boolean;
|
|
13
|
+
isError: boolean;
|
|
14
|
+
isAnyBlockActive: boolean;
|
|
15
|
+
isPublicIgnored: boolean;
|
|
16
|
+
isSettingPublicBlocked: boolean;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon, Text, Toggle, Tooltip, spacing } from "@scality/core-ui";
|
|
3
|
+
import { Accordion } from "@scality/core-ui/dist/components/accordion/Accordion.component";
|
|
4
|
+
import { Box } from "@scality/core-ui/dist/next";
|
|
5
|
+
import { memo, useCallback, useMemo } from "react";
|
|
6
|
+
import { useGetPublicAccessBlock, usePutPublicAccessBlock } from "../../hooks/index.js";
|
|
7
|
+
import { Key, Row, Value } from "../ui/Table.elements.js";
|
|
8
|
+
const PAB_SETTINGS = [
|
|
9
|
+
{
|
|
10
|
+
key: 'BlockPublicAcls',
|
|
11
|
+
label: 'BlockPublicAcls',
|
|
12
|
+
tooltip: 'Rejects PUT requests that include public ACL grants. Existing public ACLs are not affected.'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: 'IgnorePublicAcls',
|
|
16
|
+
label: 'IgnorePublicAcls',
|
|
17
|
+
tooltip: 'Ignores all public ACLs on this bucket and its objects. The ACLs still exist but have no effect on access.'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: 'BlockPublicPolicy',
|
|
21
|
+
label: 'BlockPublicPolicy',
|
|
22
|
+
tooltip: 'Rejects bucket policy changes that would grant public access. Existing public policies are not affected.'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: 'RestrictPublicBuckets',
|
|
26
|
+
label: 'RestrictPublicBuckets',
|
|
27
|
+
tooltip: 'Restricts access to buckets with public policies to only AWS service principals and authorized users within the bucket owner\'s account. The policy still exists but public and cross-account access is denied.'
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
const PublicAccessBlock = /*#__PURE__*/ memo(({ bucketName, data, status })=>{
|
|
31
|
+
const { mutate: putPublicAccessBlock, isPending } = usePutPublicAccessBlock();
|
|
32
|
+
const config = data?.PublicAccessBlockConfiguration;
|
|
33
|
+
const handleToggle = useCallback((key)=>{
|
|
34
|
+
const currentConfig = config ?? {};
|
|
35
|
+
putPublicAccessBlock({
|
|
36
|
+
Bucket: bucketName,
|
|
37
|
+
PublicAccessBlockConfiguration: {
|
|
38
|
+
...currentConfig,
|
|
39
|
+
[key]: !currentConfig[key]
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}, [
|
|
43
|
+
bucketName,
|
|
44
|
+
config,
|
|
45
|
+
putPublicAccessBlock
|
|
46
|
+
]);
|
|
47
|
+
if ('pending' === status) return null;
|
|
48
|
+
return /*#__PURE__*/ jsx(Box, {
|
|
49
|
+
marginTop: spacing.r8,
|
|
50
|
+
marginBottom: spacing.r16,
|
|
51
|
+
children: /*#__PURE__*/ jsx(Accordion, {
|
|
52
|
+
id: "pab",
|
|
53
|
+
title: "Public Access Block (PAB)",
|
|
54
|
+
children: 'error' === status ? /*#__PURE__*/ jsx(Text, {
|
|
55
|
+
color: "statusWarning",
|
|
56
|
+
children: "Failed to load Public Access Block settings"
|
|
57
|
+
}) : PAB_SETTINGS.map(({ key, label, tooltip })=>/*#__PURE__*/ jsxs(Row, {
|
|
58
|
+
children: [
|
|
59
|
+
/*#__PURE__*/ jsx(Key, {
|
|
60
|
+
children: /*#__PURE__*/ jsxs(Box, {
|
|
61
|
+
display: "flex",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
gap: spacing.r4,
|
|
64
|
+
children: [
|
|
65
|
+
label,
|
|
66
|
+
/*#__PURE__*/ jsx(Tooltip, {
|
|
67
|
+
overlay: tooltip,
|
|
68
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
69
|
+
name: "Info",
|
|
70
|
+
size: "xs"
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
/*#__PURE__*/ jsx(Value, {
|
|
77
|
+
children: /*#__PURE__*/ jsx(Toggle, {
|
|
78
|
+
id: `pab-${key}`,
|
|
79
|
+
disabled: isPending,
|
|
80
|
+
toggle: config?.[key] ?? false,
|
|
81
|
+
label: config?.[key] ? 'Active' : 'Inactive',
|
|
82
|
+
onChange: ()=>handleToggle(key)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
}, key))
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
PublicAccessBlock.displayName = 'PublicAccessBlock';
|
|
91
|
+
function usePublicAccessBlockStatus(bucketName) {
|
|
92
|
+
const { data, status, error } = useGetPublicAccessBlock({
|
|
93
|
+
Bucket: bucketName
|
|
94
|
+
});
|
|
95
|
+
const isNotImplemented = error?.statusCode === 501;
|
|
96
|
+
const config = data?.PublicAccessBlockConfiguration;
|
|
97
|
+
const isAnyBlockActive = useMemo(()=>!!(config?.BlockPublicAcls || config?.IgnorePublicAcls || config?.BlockPublicPolicy || config?.RestrictPublicBuckets), [
|
|
98
|
+
config
|
|
99
|
+
]);
|
|
100
|
+
const isPublicIgnored = !!config?.IgnorePublicAcls;
|
|
101
|
+
const isSettingPublicBlocked = !!config?.BlockPublicAcls;
|
|
102
|
+
return {
|
|
103
|
+
data,
|
|
104
|
+
status,
|
|
105
|
+
isNotImplemented,
|
|
106
|
+
isPending: 'pending' === status,
|
|
107
|
+
isError: 'error' === status && !isNotImplemented,
|
|
108
|
+
isAnyBlockActive,
|
|
109
|
+
isPublicIgnored,
|
|
110
|
+
isSettingPublicBlocked
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export { PublicAccessBlock, usePublicAccessBlockStatus };
|
|
@@ -13,6 +13,7 @@ export { BucketVersioning } from './buckets/BucketVersioning';
|
|
|
13
13
|
export { DeleteBucketButton } from './buckets/DeleteBucketButton';
|
|
14
14
|
export { EmptyBucketButton } from './buckets/EmptyBucketButton';
|
|
15
15
|
export { BucketNotificationFormPage } from './buckets/notifications/BucketNotificationFormPage';
|
|
16
|
+
export { PublicAccessBlock, usePublicAccessBlockStatus } from './buckets/PublicAccessBlock';
|
|
16
17
|
export { DataBrowserUI } from './DataBrowserUI';
|
|
17
18
|
export { CreateFolderButton } from './objects/CreateFolderButton';
|
|
18
19
|
export { ObjectDetails } from './objects/ObjectDetails';
|
package/dist/components/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { BucketVersioning } from "./buckets/BucketVersioning.js";
|
|
|
13
13
|
import { DeleteBucketButton } from "./buckets/DeleteBucketButton.js";
|
|
14
14
|
import { EmptyBucketButton } from "./buckets/EmptyBucketButton.js";
|
|
15
15
|
import { BucketNotificationFormPage } from "./buckets/notifications/BucketNotificationFormPage.js";
|
|
16
|
+
import { PublicAccessBlock, usePublicAccessBlockStatus } from "./buckets/PublicAccessBlock.js";
|
|
16
17
|
import { DataBrowserUI } from "./DataBrowserUI.js";
|
|
17
18
|
import { CreateFolderButton } from "./objects/CreateFolderButton.js";
|
|
18
19
|
import { ObjectDetails } from "./objects/ObjectDetails/index.js";
|
|
@@ -25,4 +26,4 @@ import { DataBrowserProvider, useDataBrowserConfig, useDataBrowserContext, useIn
|
|
|
25
26
|
import { QueryProvider } from "./providers/QueryProvider.js";
|
|
26
27
|
import { MetadataSearch } from "./search/MetadataSearch.js";
|
|
27
28
|
import { ArrayFieldActions } from "./ui/ArrayFieldActions.js";
|
|
28
|
-
export { ArrayFieldActions, Breadcrumb, BucketAccessor, BucketCorsPage, BucketCreate, BucketLifecycleFormPage, BucketList, BucketNotificationFormPage, BucketOverview, BucketOverviewField, BucketOverviewSection, BucketPage, BucketPolicyPage, BucketReplicationFormPage, BucketVersioning, CreateFolderButton, DataBrowserBreadcrumb, DataBrowserProvider, DataBrowserUI, DefaultReplicationDestinationFields, DeleteBucketButton, EditRetentionButton, EmptyBucketButton, MetadataSearch, ObjectDetails, ObjectList, ObjectLockSettings, ObjectPage, QueryProvider, UploadButton, baseBucketCreateSchema, bucketErrorMessage, bucketNameValidationSchema, useBreadcrumbPaths, useBucketOverviewContext, useDataBrowserConfig, useDataBrowserContext, useDataBrowserNavigate, useInvalidateQueries };
|
|
29
|
+
export { ArrayFieldActions, Breadcrumb, BucketAccessor, BucketCorsPage, BucketCreate, BucketLifecycleFormPage, BucketList, BucketNotificationFormPage, BucketOverview, BucketOverviewField, BucketOverviewSection, BucketPage, BucketPolicyPage, BucketReplicationFormPage, BucketVersioning, CreateFolderButton, DataBrowserBreadcrumb, DataBrowserProvider, DataBrowserUI, DefaultReplicationDestinationFields, DeleteBucketButton, EditRetentionButton, EmptyBucketButton, MetadataSearch, ObjectDetails, ObjectList, ObjectLockSettings, ObjectPage, PublicAccessBlock, QueryProvider, UploadButton, baseBucketCreateSchema, bucketErrorMessage, bucketNameValidationSchema, useBreadcrumbPaths, useBucketOverviewContext, useDataBrowserConfig, useDataBrowserContext, useDataBrowserNavigate, useInvalidateQueries, usePublicAccessBlockStatus };
|
|
@@ -6,7 +6,7 @@ import { useLocation, useNavigate } from "react-router";
|
|
|
6
6
|
import styled_components from "styled-components";
|
|
7
7
|
import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
|
|
8
8
|
import { useListObjectVersions, useListObjects, useSearchObjects, useSearchObjectsVersions } from "../../hooks/index.js";
|
|
9
|
-
import {
|
|
9
|
+
import { useDownloadObject } from "../../hooks/useDownloadObject.js";
|
|
10
10
|
import { useBatchObjectLegalHold } from "../../hooks/useBatchObjectLegalHold.js";
|
|
11
11
|
import { useFeatures } from "../../hooks/useFeatures.js";
|
|
12
12
|
import { useQueryParams } from "../../utils/hooks.js";
|
|
@@ -62,25 +62,6 @@ const removePrefix = (path, prefix)=>{
|
|
|
62
62
|
return path;
|
|
63
63
|
};
|
|
64
64
|
const createLegalHoldKey = (key, versionId)=>`${key}:${versionId ?? 'null'}`;
|
|
65
|
-
const downloadFile = (url, filename, onCleanup)=>{
|
|
66
|
-
try {
|
|
67
|
-
new URL(url);
|
|
68
|
-
const link = document.createElement('a');
|
|
69
|
-
link.href = url;
|
|
70
|
-
link.download = filename;
|
|
71
|
-
link.style.display = 'none';
|
|
72
|
-
link.rel = 'noopener noreferrer';
|
|
73
|
-
document.body.appendChild(link);
|
|
74
|
-
link.click();
|
|
75
|
-
const timeoutId = setTimeout(()=>{
|
|
76
|
-
if (document.body.contains(link)) document.body.removeChild(link);
|
|
77
|
-
}, 100);
|
|
78
|
-
onCleanup?.(timeoutId);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.error('Invalid download URL:', url, error);
|
|
81
|
-
throw new Error('Failed to initiate download: Invalid URL');
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
65
|
const createNameColumn = (onPrefixChange, onDownload)=>({
|
|
85
66
|
Header: 'Name',
|
|
86
67
|
accessor: 'displayName',
|
|
@@ -302,7 +283,7 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
|
|
|
302
283
|
const navigate = useNavigate();
|
|
303
284
|
const [searchValue, setSearchValue] = useState(queryParams.get(SEARCH_QUERY_PARAM) || '');
|
|
304
285
|
const [selectedObjects, setSelectedObjects] = useState([]);
|
|
305
|
-
const { mutateAsync:
|
|
286
|
+
const { mutateAsync: downloadObject } = useDownloadObject();
|
|
306
287
|
const downloadingRef = useRef(new Set());
|
|
307
288
|
const downloadTimeoutsRef = useRef(new Map());
|
|
308
289
|
const { showToast } = useToast();
|
|
@@ -337,18 +318,13 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
|
|
|
337
318
|
try {
|
|
338
319
|
const rawFilename = object.displayName || object.Key.split('/').pop() || 'download';
|
|
339
320
|
const sanitized = rawFilename.replace(/["\r\n\t]/g, '');
|
|
340
|
-
|
|
341
|
-
const result = await getPresignedDownload({
|
|
321
|
+
await downloadObject({
|
|
342
322
|
Bucket: bucketName,
|
|
343
323
|
Key: object.Key,
|
|
344
|
-
ResponseContentDisposition: `attachment; filename="${sanitized}"; filename*=UTF-8''${encoded}`,
|
|
345
324
|
...versionId ? {
|
|
346
325
|
VersionId: versionId
|
|
347
|
-
} : {}
|
|
348
|
-
|
|
349
|
-
if (!result?.Url) throw new Error('Failed to generate presigned URL: No URL returned');
|
|
350
|
-
downloadFile(result.Url, sanitized, (timeoutId)=>{
|
|
351
|
-
downloadTimeoutsRef.current.set(`${downloadKey}_link`, timeoutId);
|
|
326
|
+
} : {},
|
|
327
|
+
filename: sanitized
|
|
352
328
|
});
|
|
353
329
|
} catch (error) {
|
|
354
330
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -361,13 +337,12 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
|
|
|
361
337
|
const timeoutId = setTimeout(()=>{
|
|
362
338
|
downloadingRef.current.delete(downloadKey);
|
|
363
339
|
downloadTimeoutsRef.current.delete(downloadKey);
|
|
364
|
-
downloadTimeoutsRef.current.delete(`${downloadKey}_link`);
|
|
365
340
|
}, 1000);
|
|
366
341
|
downloadTimeoutsRef.current.set(downloadKey, timeoutId);
|
|
367
342
|
}
|
|
368
343
|
}, [
|
|
369
344
|
bucketName,
|
|
370
|
-
|
|
345
|
+
downloadObject,
|
|
371
346
|
showToast
|
|
372
347
|
]);
|
|
373
348
|
const isSearchActive = isMetadataSearchEnabled && Boolean(metadataSearchQuery);
|
|
@@ -24,7 +24,7 @@ describe('usePresigningS3Client', ()=>{
|
|
|
24
24
|
}));
|
|
25
25
|
});
|
|
26
26
|
it('should create an S3 client with the direct endpoint when no proxy is configured', ()=>{
|
|
27
|
-
renderHook(()=>usePresigningS3Client(), {
|
|
27
|
+
const { result } = renderHook(()=>usePresigningS3Client(), {
|
|
28
28
|
wrapper: createTestWrapper(testConfig, testCredentials)
|
|
29
29
|
});
|
|
30
30
|
expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -32,34 +32,38 @@ describe('usePresigningS3Client', ()=>{
|
|
|
32
32
|
region: testConfig.region,
|
|
33
33
|
forcePathStyle: true
|
|
34
34
|
}));
|
|
35
|
+
expect(result.current.client).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
it('should return identity rewriteUrl when no proxy is configured', ()=>{
|
|
38
|
+
const { result } = renderHook(()=>usePresigningS3Client(), {
|
|
39
|
+
wrapper: createTestWrapper(testConfig, testCredentials)
|
|
40
|
+
});
|
|
41
|
+
const url = 'http://s3.example.com/bucket/key?X-Amz-Signature=abc';
|
|
42
|
+
expect(result.current.rewriteUrl(url)).toBe(url);
|
|
35
43
|
});
|
|
36
|
-
it('should create an S3 client with the
|
|
44
|
+
it('should create an S3 client with the target endpoint when proxy is enabled', ()=>{
|
|
37
45
|
renderHook(()=>usePresigningS3Client(), {
|
|
38
46
|
wrapper: createTestWrapper(proxyConfig, testCredentials)
|
|
39
47
|
});
|
|
40
48
|
expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
|
|
41
|
-
endpoint: '
|
|
49
|
+
endpoint: 'http://internal-s3.cluster.local',
|
|
42
50
|
region: testConfig.region,
|
|
43
51
|
forcePathStyle: true
|
|
44
52
|
}));
|
|
45
53
|
const constructorCall = MockedS3Client.mock.calls[0][0];
|
|
46
|
-
expect(constructorCall?.endpoint).not.toContain('
|
|
54
|
+
expect(constructorCall?.endpoint).not.toContain('proxy.example.com');
|
|
47
55
|
});
|
|
48
|
-
it('should
|
|
49
|
-
const
|
|
50
|
-
MockedS3Client.mockImplementation(()=>({
|
|
51
|
-
config: {},
|
|
52
|
-
middlewareStack: {
|
|
53
|
-
use: mockUse,
|
|
54
|
-
add: jest.fn()
|
|
55
|
-
}
|
|
56
|
-
}));
|
|
57
|
-
renderHook(()=>usePresigningS3Client(), {
|
|
56
|
+
it('should rewrite presigned URLs from target to proxy endpoint', ()=>{
|
|
57
|
+
const { result } = renderHook(()=>usePresigningS3Client(), {
|
|
58
58
|
wrapper: createTestWrapper(proxyConfig, testCredentials)
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
const targetUrl = 'http://internal-s3.cluster.local/my-bucket/my-key?X-Amz-Signature=abc123';
|
|
61
|
+
const rewritten = result.current.rewriteUrl(targetUrl);
|
|
62
|
+
expect(rewritten).toContain('https://proxy.example.com');
|
|
63
|
+
expect(rewritten).toContain('/s3/my-bucket/my-key');
|
|
64
|
+
expect(rewritten).toContain('X-Amz-Signature=abc123');
|
|
61
65
|
});
|
|
62
|
-
it('should
|
|
66
|
+
it('should rewrite URLs with origin-relative proxy endpoint', ()=>{
|
|
63
67
|
const originRelativeConfig = {
|
|
64
68
|
...testConfig,
|
|
65
69
|
proxy: {
|
|
@@ -68,13 +72,30 @@ describe('usePresigningS3Client', ()=>{
|
|
|
68
72
|
target: 'http://artesca-data-connector-s3api.zenko.svc.cluster.local'
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
|
-
const
|
|
72
|
-
renderHook(()=>usePresigningS3Client(), {
|
|
75
|
+
const { result } = renderHook(()=>usePresigningS3Client(), {
|
|
73
76
|
wrapper: createTestWrapper(originRelativeConfig, testCredentials)
|
|
74
77
|
});
|
|
75
78
|
expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
|
|
76
|
-
endpoint:
|
|
79
|
+
endpoint: 'http://artesca-data-connector-s3api.zenko.svc.cluster.local'
|
|
77
80
|
}));
|
|
81
|
+
const targetUrl = 'http://artesca-data-connector-s3api.zenko.svc.cluster.local/bucket/key?X-Amz-Signature=xyz';
|
|
82
|
+
const rewritten = result.current.rewriteUrl(targetUrl);
|
|
83
|
+
expect(rewritten).toContain('/zenko/s3/bucket/key');
|
|
84
|
+
expect(rewritten).toContain('X-Amz-Signature=xyz');
|
|
85
|
+
});
|
|
86
|
+
it('should not attach proxy middleware even when proxy is enabled', ()=>{
|
|
87
|
+
const mockUse = jest.fn();
|
|
88
|
+
MockedS3Client.mockImplementation(()=>({
|
|
89
|
+
config: {},
|
|
90
|
+
middlewareStack: {
|
|
91
|
+
use: mockUse,
|
|
92
|
+
add: jest.fn()
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
renderHook(()=>usePresigningS3Client(), {
|
|
96
|
+
wrapper: createTestWrapper(proxyConfig, testCredentials)
|
|
97
|
+
});
|
|
98
|
+
expect(mockUse).not.toHaveBeenCalled();
|
|
78
99
|
});
|
|
79
100
|
it('should use direct endpoint when proxy is disabled', ()=>{
|
|
80
101
|
const disabledProxyConfig = {
|
|
@@ -91,14 +112,14 @@ describe('usePresigningS3Client', ()=>{
|
|
|
91
112
|
endpoint: testConfig.endpoint
|
|
92
113
|
}));
|
|
93
114
|
});
|
|
94
|
-
it('should return the same
|
|
115
|
+
it('should return the same result on re-render when s3ConfigIdentifier is unchanged', ()=>{
|
|
95
116
|
const { result, rerender } = renderHook(()=>usePresigningS3Client(), {
|
|
96
117
|
wrapper: createTestWrapper(testConfig, testCredentials)
|
|
97
118
|
});
|
|
98
|
-
const
|
|
119
|
+
const first = result.current;
|
|
99
120
|
rerender();
|
|
100
|
-
const
|
|
101
|
-
expect(
|
|
121
|
+
const second = result.current;
|
|
122
|
+
expect(first).toBe(second);
|
|
102
123
|
expect(MockedS3Client).toHaveBeenCalledTimes(1);
|
|
103
124
|
});
|
|
104
125
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This file contains hooks for managing bucket configuration settings
|
|
5
5
|
* like ACL, policies, versioning, CORS, lifecycle, etc.
|
|
6
6
|
*/
|
|
7
|
-
import { type DeleteBucketCorsCommandInput, type DeleteBucketCorsCommandOutput, type DeleteBucketLifecycleCommandInput, type DeleteBucketLifecycleCommandOutput, type DeleteBucketPolicyCommandInput, type DeleteBucketPolicyCommandOutput, type DeleteBucketReplicationCommandInput, type DeleteBucketReplicationCommandOutput, type DeleteBucketTaggingCommandInput, type DeleteBucketTaggingCommandOutput, type GetBucketAclCommandInput, type GetBucketAclCommandOutput, type GetBucketCorsCommandInput, type GetBucketCorsCommandOutput, type GetBucketEncryptionCommandInput, type GetBucketEncryptionCommandOutput, type GetBucketLifecycleConfigurationCommandInput, type GetBucketLifecycleConfigurationCommandOutput, type GetBucketNotificationConfigurationCommandInput, type GetBucketNotificationConfigurationCommandOutput, type GetBucketPolicyCommandInput, type GetBucketPolicyCommandOutput, type GetBucketReplicationCommandInput, type GetBucketReplicationCommandOutput, type GetBucketTaggingCommandInput, type GetBucketTaggingCommandOutput, type GetBucketVersioningCommandInput, type GetBucketVersioningCommandOutput, type GetObjectLockConfigurationCommandInput, type GetObjectLockConfigurationCommandOutput, type GetPublicAccessBlockCommandInput, type GetPublicAccessBlockCommandOutput, type PutBucketAclCommandInput, type PutBucketAclCommandOutput, type PutBucketCorsCommandInput, type PutBucketCorsCommandOutput, type PutBucketEncryptionCommandInput, type PutBucketEncryptionCommandOutput, type PutBucketLifecycleConfigurationCommandInput, type PutBucketLifecycleConfigurationCommandOutput, type PutBucketNotificationConfigurationCommandInput, type PutBucketNotificationConfigurationCommandOutput, type PutBucketPolicyCommandInput, type PutBucketPolicyCommandOutput, type PutBucketReplicationCommandInput, type PutBucketReplicationCommandOutput, type PutBucketTaggingCommandInput, type PutBucketTaggingCommandOutput, type PutBucketVersioningCommandInput, type PutBucketVersioningCommandOutput, type PutObjectLockConfigurationCommandInput, type PutObjectLockConfigurationCommandOutput } from '@aws-sdk/client-s3';
|
|
7
|
+
import { type DeleteBucketCorsCommandInput, type DeleteBucketCorsCommandOutput, type DeleteBucketLifecycleCommandInput, type DeleteBucketLifecycleCommandOutput, type DeleteBucketPolicyCommandInput, type DeleteBucketPolicyCommandOutput, type DeleteBucketReplicationCommandInput, type DeleteBucketReplicationCommandOutput, type DeleteBucketTaggingCommandInput, type DeleteBucketTaggingCommandOutput, type DeletePublicAccessBlockCommandInput, type DeletePublicAccessBlockCommandOutput, type GetBucketAclCommandInput, type GetBucketAclCommandOutput, type GetBucketCorsCommandInput, type GetBucketCorsCommandOutput, type GetBucketEncryptionCommandInput, type GetBucketEncryptionCommandOutput, type GetBucketLifecycleConfigurationCommandInput, type GetBucketLifecycleConfigurationCommandOutput, type GetBucketNotificationConfigurationCommandInput, type GetBucketNotificationConfigurationCommandOutput, type GetBucketPolicyCommandInput, type GetBucketPolicyCommandOutput, type GetBucketReplicationCommandInput, type GetBucketReplicationCommandOutput, type GetBucketTaggingCommandInput, type GetBucketTaggingCommandOutput, type GetBucketVersioningCommandInput, type GetBucketVersioningCommandOutput, type GetObjectLockConfigurationCommandInput, type GetObjectLockConfigurationCommandOutput, type GetPublicAccessBlockCommandInput, type GetPublicAccessBlockCommandOutput, type PutBucketAclCommandInput, type PutBucketAclCommandOutput, type PutBucketCorsCommandInput, type PutBucketCorsCommandOutput, type PutBucketEncryptionCommandInput, type PutBucketEncryptionCommandOutput, type PutBucketLifecycleConfigurationCommandInput, type PutBucketLifecycleConfigurationCommandOutput, type PutBucketNotificationConfigurationCommandInput, type PutBucketNotificationConfigurationCommandOutput, type PutBucketPolicyCommandInput, type PutBucketPolicyCommandOutput, type PutBucketReplicationCommandInput, type PutBucketReplicationCommandOutput, type PutBucketTaggingCommandInput, type PutBucketTaggingCommandOutput, type PutBucketVersioningCommandInput, type PutBucketVersioningCommandOutput, type PutObjectLockConfigurationCommandInput, type PutObjectLockConfigurationCommandOutput, type PutPublicAccessBlockCommandInput, type PutPublicAccessBlockCommandOutput } from '@aws-sdk/client-s3';
|
|
8
8
|
/**
|
|
9
9
|
* Hook for retrieving S3 bucket access control list (ACL) settings
|
|
10
10
|
*
|
|
@@ -173,3 +173,5 @@ export declare const useDeleteBucketReplication: (options?: Omit<import("@tansta
|
|
|
173
173
|
* This configuration determines whether public access is blocked at the bucket level.
|
|
174
174
|
*/
|
|
175
175
|
export declare const useGetPublicAccessBlock: (params?: GetPublicAccessBlockCommandInput | undefined, options?: Omit<import("@tanstack/react-query").UseQueryOptions<GetPublicAccessBlockCommandOutput, import("..").EnhancedS3Error, GetPublicAccessBlockCommandOutput, readonly unknown[]>, "queryKey" | "queryFn"> | undefined) => import("@tanstack/react-query").UseQueryResult<GetPublicAccessBlockCommandOutput, import("..").EnhancedS3Error>;
|
|
176
|
+
export declare const usePutPublicAccessBlock: (options?: Omit<import("@tanstack/react-query").UseMutationOptions<PutPublicAccessBlockCommandOutput, import("..").EnhancedS3Error, PutPublicAccessBlockCommandInput, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<PutPublicAccessBlockCommandOutput, import("..").EnhancedS3Error, PutPublicAccessBlockCommandInput>;
|
|
177
|
+
export declare const useDeletePublicAccessBlock: (options?: Omit<import("@tanstack/react-query").UseMutationOptions<DeletePublicAccessBlockCommandOutput, import("..").EnhancedS3Error, DeletePublicAccessBlockCommandInput, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<DeletePublicAccessBlockCommandOutput, import("..").EnhancedS3Error, DeletePublicAccessBlockCommandInput>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeleteBucketCorsCommand, DeleteBucketLifecycleCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, GetBucketAclCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, PutBucketAclCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketLifecycleConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutObjectLockConfigurationCommand } from "@aws-sdk/client-s3";
|
|
1
|
+
import { DeleteBucketCorsCommand, DeleteBucketLifecycleCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeletePublicAccessBlockCommand, GetBucketAclCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, PutBucketAclCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketLifecycleConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand } from "@aws-sdk/client-s3";
|
|
2
2
|
import { useCreateS3MutationHook } from "./factories/useCreateS3MutationHook.js";
|
|
3
3
|
import { useCreateS3QueryHook } from "./factories/useCreateS3QueryHook.js";
|
|
4
4
|
const useGetBucketAcl = useCreateS3QueryHook(GetBucketAclCommand, 'GetBucketAcl');
|
|
@@ -65,4 +65,11 @@ const useDeleteBucketReplication = useCreateS3MutationHook(DeleteBucketReplicati
|
|
|
65
65
|
'GetBucketReplication'
|
|
66
66
|
]);
|
|
67
67
|
const useGetPublicAccessBlock = useCreateS3QueryHook(GetPublicAccessBlockCommand, 'GetPublicAccessBlock');
|
|
68
|
-
|
|
68
|
+
const usePutPublicAccessBlock = useCreateS3MutationHook(PutPublicAccessBlockCommand, 'PutPublicAccessBlock', [
|
|
69
|
+
'GetPublicAccessBlock',
|
|
70
|
+
'ListBuckets'
|
|
71
|
+
]);
|
|
72
|
+
const useDeletePublicAccessBlock = useCreateS3MutationHook(DeletePublicAccessBlockCommand, 'DeletePublicAccessBlock', [
|
|
73
|
+
'GetPublicAccessBlock'
|
|
74
|
+
]);
|
|
75
|
+
export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeletePublicAccessBlock, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, usePutPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning };
|
|
@@ -14,5 +14,5 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export { useCreateS3InfiniteQueryHook } from './useCreateS3InfiniteQueryHook';
|
|
16
16
|
export { useCreateS3LoginHook } from './useCreateS3LoginHook';
|
|
17
|
-
export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook, } from './useCreateS3MutationHook';
|
|
17
|
+
export { type UrlRewriter, useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook, } from './useCreateS3MutationHook';
|
|
18
18
|
export { useCreateS3QueryHook } from './useCreateS3QueryHook';
|
|
@@ -3,4 +3,5 @@ import { type UseMutationOptions, type UseMutationResult } from '@tanstack/react
|
|
|
3
3
|
import { type EnhancedS3Error } from '../../utils/errorHandling';
|
|
4
4
|
export declare function useCreateS3MutationHook<TInput extends object, TOutput>(Command: new (input: TInput) => any, operationName: string, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
|
|
5
5
|
export declare function useCreateS3FunctionMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput, unknown>, "mutationFn"> | undefined) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
|
|
6
|
-
export
|
|
6
|
+
export type UrlRewriter = (url: string) => string;
|
|
7
|
+
export declare function useCreatePresigningMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput, rewriteUrl: UrlRewriter) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
|
|
@@ -57,6 +57,24 @@ function useCreateS3FunctionMutationHook(operation, invalidationKeys) {
|
|
|
57
57
|
return createFunctionMutationHook(operation, useS3Client, invalidationKeys);
|
|
58
58
|
}
|
|
59
59
|
function useCreatePresigningMutationHook(operation, invalidationKeys) {
|
|
60
|
-
return
|
|
60
|
+
return (options)=>{
|
|
61
|
+
const { s3ConfigIdentifier } = useDataBrowserContext();
|
|
62
|
+
const { client, rewriteUrl } = usePresigningS3Client();
|
|
63
|
+
const queryClient = useQueryClient();
|
|
64
|
+
return useMutation({
|
|
65
|
+
mutationFn: async (params)=>operation(client, params, rewriteUrl),
|
|
66
|
+
onSuccess: (_data, _variables)=>{
|
|
67
|
+
if (invalidationKeys) invalidationKeys.forEach((key)=>{
|
|
68
|
+
queryClient.invalidateQueries({
|
|
69
|
+
queryKey: [
|
|
70
|
+
s3ConfigIdentifier,
|
|
71
|
+
key
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
...options
|
|
77
|
+
});
|
|
78
|
+
};
|
|
61
79
|
}
|
|
62
80
|
export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook };
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, } from './bucketConfiguration';
|
|
1
|
+
export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeletePublicAccessBlock, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, usePutPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, } from './bucketConfiguration';
|
|
2
2
|
export { useBuckets, useCreateBucket, useDeleteBucket, useGetBucketLocation, useValidateBucketAccess, } from './bucketOperations';
|
|
3
3
|
export type { LoginConfig, LoginMutationResult } from './loginOperations';
|
|
4
4
|
export { useLoginMutation } from './loginOperations';
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning } from "./bucketConfiguration.js";
|
|
1
|
+
import { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeletePublicAccessBlock, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, usePutPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning } from "./bucketConfiguration.js";
|
|
2
2
|
import { useBuckets, useCreateBucket, useDeleteBucket, useGetBucketLocation, useValidateBucketAccess } from "./bucketOperations.js";
|
|
3
3
|
import { useLoginMutation } from "./loginOperations.js";
|
|
4
4
|
import { useCopyObject, useCreateFolder, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useListMultipartUploads, useListObjectVersions, useListObjects, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, useRestoreObject, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useUploadObjects } from "./objectOperations.js";
|
|
@@ -17,4 +17,4 @@ import { useS3Client } from "./useS3Client.js";
|
|
|
17
17
|
import { useS3ConfigSwitch } from "./useS3ConfigSwitch.js";
|
|
18
18
|
import { useSupportedNotificationEvents } from "./useSupportedNotificationEvents.js";
|
|
19
19
|
import { useTableRowSelection } from "./useTableRowSelection.js";
|
|
20
|
-
export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
|
|
20
|
+
export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useDeletePublicAccessBlock, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, usePutPublicAccessBlock, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
|
|
@@ -3,7 +3,7 @@ import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
|
|
|
3
3
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
4
4
|
import { createS3OperationError } from "../utils/errorHandling.js";
|
|
5
5
|
import { useCreatePresigningMutationHook } from "./factories/index.js";
|
|
6
|
-
const generatePresignedDownloadUrl = async (client, config)=>{
|
|
6
|
+
const generatePresignedDownloadUrl = async (client, config, rewriteUrl)=>{
|
|
7
7
|
try {
|
|
8
8
|
const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
|
|
9
9
|
const command = new GetObjectCommand({
|
|
@@ -16,7 +16,7 @@ const generatePresignedDownloadUrl = async (client, config)=>{
|
|
|
16
16
|
});
|
|
17
17
|
const expiresAt = new Date(Date.now() + 1000 * expiresIn);
|
|
18
18
|
return {
|
|
19
|
-
Url: url,
|
|
19
|
+
Url: rewriteUrl(url),
|
|
20
20
|
ExpiresAt: expiresAt,
|
|
21
21
|
Bucket: Bucket,
|
|
22
22
|
Key: Key,
|
|
@@ -26,7 +26,7 @@ const generatePresignedDownloadUrl = async (client, config)=>{
|
|
|
26
26
|
throw createS3OperationError(error, 'GeneratePresignedDownload', config.Bucket, config.Key);
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
const generatePresignedUploadUrl = async (client, config)=>{
|
|
29
|
+
const generatePresignedUploadUrl = async (client, config, rewriteUrl)=>{
|
|
30
30
|
try {
|
|
31
31
|
const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
|
|
32
32
|
const command = new PutObjectCommand({
|
|
@@ -39,7 +39,7 @@ const generatePresignedUploadUrl = async (client, config)=>{
|
|
|
39
39
|
});
|
|
40
40
|
const expiresAt = new Date(Date.now() + 1000 * expiresIn);
|
|
41
41
|
return {
|
|
42
|
-
Url: url,
|
|
42
|
+
Url: rewriteUrl(url),
|
|
43
43
|
ExpiresAt: expiresAt,
|
|
44
44
|
Bucket: Bucket,
|
|
45
45
|
Key: Key
|
|
@@ -48,7 +48,7 @@ const generatePresignedUploadUrl = async (client, config)=>{
|
|
|
48
48
|
throw createS3OperationError(error, 'GeneratePresignedUpload', config.Bucket, config.Key);
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
-
const generatePresignedPost = async (client, config)=>{
|
|
51
|
+
const generatePresignedPost = async (client, config, rewriteUrl)=>{
|
|
52
52
|
try {
|
|
53
53
|
const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
|
|
54
54
|
const presignedPost = await createPresignedPost(client, {
|
|
@@ -60,6 +60,7 @@ const generatePresignedPost = async (client, config)=>{
|
|
|
60
60
|
const expiresAt = new Date(Date.now() + 1000 * expiresIn);
|
|
61
61
|
return {
|
|
62
62
|
...presignedPost,
|
|
63
|
+
url: rewriteUrl(presignedPost.url),
|
|
63
64
|
ExpiresAt: expiresAt
|
|
64
65
|
};
|
|
65
66
|
} catch (error) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type GetObjectCommandInput } from '@aws-sdk/client-s3';
|
|
2
|
+
import { type EnhancedS3Error } from '../utils/errorHandling';
|
|
3
|
+
type DownloadObjectInput = GetObjectCommandInput & {
|
|
4
|
+
filename: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Hook for downloading S3 objects via the S3 SDK client.
|
|
8
|
+
*
|
|
9
|
+
* Uses the normal S3 client (with proxy middleware) to fetch the object,
|
|
10
|
+
* avoiding presigned URL signature issues through reverse proxies.
|
|
11
|
+
*
|
|
12
|
+
* Download strategy:
|
|
13
|
+
* 1. If File System Access API is available (Chrome/Edge): stream
|
|
14
|
+
* directly to disk via showSaveFilePicker, no memory buffering
|
|
15
|
+
* 2. Otherwise: buffer via transformToByteArray and download via Blob URL
|
|
16
|
+
*/
|
|
17
|
+
export declare const useDownloadObject: () => import("@tanstack/react-query").UseMutationResult<void, EnhancedS3Error, DownloadObjectInput, unknown>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { useMutation } from "@tanstack/react-query";
|
|
3
|
+
import { createS3OperationError } from "../utils/errorHandling.js";
|
|
4
|
+
import { useS3Client } from "./useS3Client.js";
|
|
5
|
+
const supportsStreamDownload = ()=>'undefined' != typeof window && 'showSaveFilePicker' in window;
|
|
6
|
+
async function streamDownload(stream, filename) {
|
|
7
|
+
const handle = await window.showSaveFilePicker({
|
|
8
|
+
suggestedName: filename
|
|
9
|
+
});
|
|
10
|
+
const writable = await handle.createWritable();
|
|
11
|
+
await stream.pipeTo(writable);
|
|
12
|
+
}
|
|
13
|
+
function downloadViaBlob(bytes, filename, contentType) {
|
|
14
|
+
const blob = new Blob([
|
|
15
|
+
bytes
|
|
16
|
+
], {
|
|
17
|
+
type: contentType
|
|
18
|
+
});
|
|
19
|
+
const url = URL.createObjectURL(blob);
|
|
20
|
+
const link = document.createElement('a');
|
|
21
|
+
link.href = url;
|
|
22
|
+
link.download = filename;
|
|
23
|
+
link.style.display = 'none';
|
|
24
|
+
document.body.appendChild(link);
|
|
25
|
+
link.click();
|
|
26
|
+
setTimeout(()=>{
|
|
27
|
+
URL.revokeObjectURL(url);
|
|
28
|
+
if (document.body.contains(link)) document.body.removeChild(link);
|
|
29
|
+
}, 100);
|
|
30
|
+
}
|
|
31
|
+
const useDownloadObject = ()=>{
|
|
32
|
+
const s3Client = useS3Client();
|
|
33
|
+
return useMutation({
|
|
34
|
+
mutationFn: async ({ filename, ...params })=>{
|
|
35
|
+
try {
|
|
36
|
+
if (supportsStreamDownload()) {
|
|
37
|
+
const response = await s3Client.send(new GetObjectCommand(params));
|
|
38
|
+
if (!response.Body) throw new Error('Empty response body');
|
|
39
|
+
try {
|
|
40
|
+
const stream = response.Body.transformToWebStream();
|
|
41
|
+
await streamDownload(stream, filename);
|
|
42
|
+
return;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error?.name === 'AbortError') return;
|
|
45
|
+
console.warn('Stream download failed, falling back to blob:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const response = await s3Client.send(new GetObjectCommand(params));
|
|
49
|
+
if (!response.Body) throw new Error('Empty response body');
|
|
50
|
+
const bytes = await response.Body.transformToByteArray();
|
|
51
|
+
const contentType = response.ContentType || 'application/octet-stream';
|
|
52
|
+
downloadViaBlob(bytes, filename, contentType);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw createS3OperationError(error, 'DownloadObject', params.Bucket, params.Key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
export { useDownloadObject };
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
2
|
/**
|
|
3
|
-
* Hook
|
|
3
|
+
* Hook for presigned URL generation that works through reverse proxies.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* pointing to the proxy endpoint instead of the internal S3 service.
|
|
5
|
+
* Returns an S3 client pointed at the real S3 target (for correct SigV4
|
|
6
|
+
* signing) plus a `rewriteUrl` function that rewrites the generated URL
|
|
7
|
+
* to go through the proxy so browsers can reach it.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. getSignedUrl() signs against the S3 target → signature covers
|
|
11
|
+
* target host + clean path (no proxy prefix)
|
|
12
|
+
* 2. rewriteUrl() replaces host/protocol/port and prepends the proxy
|
|
13
|
+
* path prefix → URL is browser-accessible
|
|
14
|
+
* 3. Browser hits the proxy (NGINX), which strips the prefix and
|
|
15
|
+
* forwards to S3 with the target Host → matches the signature
|
|
12
16
|
*/
|
|
13
|
-
export declare const usePresigningS3Client: () =>
|
|
17
|
+
export declare const usePresigningS3Client: () => {
|
|
18
|
+
client: S3Client;
|
|
19
|
+
rewriteUrl: (url: string) => string;
|
|
20
|
+
};
|
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { useDataBrowserContext } from "../components/providers/DataBrowserProvider.js";
|
|
4
|
-
import { resolveProxyEndpoint } from "../utils/proxyMiddleware.js";
|
|
4
|
+
import { parseEndpoint, resolveProxyEndpoint } from "../utils/proxyMiddleware.js";
|
|
5
5
|
const usePresigningS3Client = ()=>{
|
|
6
6
|
const { s3ConfigIdentifier, getS3Config } = useDataBrowserContext();
|
|
7
7
|
if (!getS3Config) throw new Error('usePresigningS3Client: S3 config not available. Ensure DataBrowserProvider has getS3Config prop set.');
|
|
8
8
|
return useMemo(()=>{
|
|
9
9
|
const config = getS3Config();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
if (!config.proxy?.enabled) {
|
|
11
|
+
const client = new S3Client({
|
|
12
|
+
endpoint: config.endpoint,
|
|
13
|
+
credentials: config.credentials,
|
|
14
|
+
forcePathStyle: config.forcePathStyle ?? true,
|
|
15
|
+
region: config.region
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
client,
|
|
19
|
+
rewriteUrl: (url)=>url
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const target = parseEndpoint(config.proxy.target);
|
|
23
|
+
const targetEndpoint = `${target.protocol}//${target.hostname}${80 !== target.port && 443 !== target.port ? `:${target.port}` : ''}`;
|
|
24
|
+
const client = new S3Client({
|
|
25
|
+
endpoint: targetEndpoint,
|
|
13
26
|
credentials: config.credentials,
|
|
14
27
|
forcePathStyle: config.forcePathStyle ?? true,
|
|
15
28
|
region: config.region
|
|
16
29
|
});
|
|
30
|
+
const proxyEndpoint = resolveProxyEndpoint(config.proxy.endpoint);
|
|
31
|
+
const proxy = parseEndpoint(proxyEndpoint);
|
|
32
|
+
const proxyOrigin = `${proxy.protocol}//${proxy.hostname}${80 !== proxy.port && 443 !== proxy.port ? `:${proxy.port}` : ''}`;
|
|
33
|
+
const proxyPathPrefix = proxy.pathname || '';
|
|
34
|
+
const rewriteUrl = (url)=>{
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
const rewritten = new URL(proxyOrigin);
|
|
37
|
+
rewritten.pathname = proxyPathPrefix + parsed.pathname;
|
|
38
|
+
rewritten.search = parsed.search;
|
|
39
|
+
return rewritten.toString();
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
client,
|
|
43
|
+
rewriteUrl
|
|
44
|
+
};
|
|
17
45
|
}, [
|
|
18
46
|
s3ConfigIdentifier
|
|
19
47
|
]);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HttpResponse, http } from "msw";
|
|
2
|
+
import { createS3ErrorXml, getS3BaseUrl } from "../utils.js";
|
|
3
|
+
const getPublicAccessBlockHandler = http.get(`${getS3BaseUrl()}/:bucketName`, async ({ request, params })=>{
|
|
4
|
+
const { bucketName } = params;
|
|
5
|
+
const url = new URL(request.url);
|
|
6
|
+
if (!url.searchParams.has('publicAccessBlock')) return;
|
|
7
|
+
if ('string' == typeof bucketName) {
|
|
8
|
+
if (bucketName.includes('pab-not-implemented')) return HttpResponse.xml(createS3ErrorXml('NotImplemented', 'A header you provided implies functionality that is not implemented'), {
|
|
9
|
+
status: 501
|
|
10
|
+
});
|
|
11
|
+
if (bucketName.includes('pab-not-found')) return HttpResponse.xml(createS3ErrorXml('NoSuchPublicAccessBlockConfiguration', 'The public access block configuration was not found', bucketName), {
|
|
12
|
+
status: 404
|
|
13
|
+
});
|
|
14
|
+
if (bucketName.includes('pab-all-blocked')) return HttpResponse.xml(`<?xml version="1.0" encoding="UTF-8"?>
|
|
15
|
+
<PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
16
|
+
<BlockPublicAcls>TRUE</BlockPublicAcls>
|
|
17
|
+
<IgnorePublicAcls>TRUE</IgnorePublicAcls>
|
|
18
|
+
<BlockPublicPolicy>TRUE</BlockPublicPolicy>
|
|
19
|
+
<RestrictPublicBuckets>TRUE</RestrictPublicBuckets>
|
|
20
|
+
</PublicAccessBlockConfiguration>`, {
|
|
21
|
+
status: 200
|
|
22
|
+
});
|
|
23
|
+
if (bucketName.includes('pab-acl-blocked')) return HttpResponse.xml(`<?xml version="1.0" encoding="UTF-8"?>
|
|
24
|
+
<PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
25
|
+
<BlockPublicAcls>TRUE</BlockPublicAcls>
|
|
26
|
+
<IgnorePublicAcls>TRUE</IgnorePublicAcls>
|
|
27
|
+
<BlockPublicPolicy>FALSE</BlockPublicPolicy>
|
|
28
|
+
<RestrictPublicBuckets>FALSE</RestrictPublicBuckets>
|
|
29
|
+
</PublicAccessBlockConfiguration>`, {
|
|
30
|
+
status: 200
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return HttpResponse.xml(`<?xml version="1.0" encoding="UTF-8"?>
|
|
34
|
+
<PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
35
|
+
<BlockPublicAcls>FALSE</BlockPublicAcls>
|
|
36
|
+
<IgnorePublicAcls>FALSE</IgnorePublicAcls>
|
|
37
|
+
<BlockPublicPolicy>FALSE</BlockPublicPolicy>
|
|
38
|
+
<RestrictPublicBuckets>FALSE</RestrictPublicBuckets>
|
|
39
|
+
</PublicAccessBlockConfiguration>`, {
|
|
40
|
+
status: 200
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
const putPublicAccessBlockHandler = http.put(`${getS3BaseUrl()}/:bucketName`, async ({ request })=>{
|
|
44
|
+
const url = new URL(request.url);
|
|
45
|
+
if (!url.searchParams.has('publicAccessBlock')) return;
|
|
46
|
+
return new HttpResponse(null, {
|
|
47
|
+
status: 200
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
export { getPublicAccessBlockHandler, putPublicAccessBlockHandler };
|
|
@@ -3,6 +3,7 @@ import { deleteBucketHandler } from "./handlers/deleteBucket.js";
|
|
|
3
3
|
import { getBucketAclHandler } from "./handlers/getBucketAcl.js";
|
|
4
4
|
import { getBucketLocationHandler } from "./handlers/getBucketLocation.js";
|
|
5
5
|
import { getBucketPolicyHandler } from "./handlers/getBucketPolicy.js";
|
|
6
|
+
import { getPublicAccessBlockHandler, putPublicAccessBlockHandler } from "./handlers/getPublicAccessBlock.js";
|
|
6
7
|
import { headObjectHandler } from "./handlers/headObject.js";
|
|
7
8
|
import { listBucketsHandler } from "./handlers/listBuckets.js";
|
|
8
9
|
import { listObjectsHandler } from "./handlers/listObjects.js";
|
|
@@ -126,7 +127,9 @@ const s3Handlers = [
|
|
|
126
127
|
getObjectLegalHoldHandler,
|
|
127
128
|
putObjectHandler,
|
|
128
129
|
headObjectHandler,
|
|
129
|
-
putBucketAclHandler
|
|
130
|
+
putBucketAclHandler,
|
|
131
|
+
getPublicAccessBlockHandler,
|
|
132
|
+
putPublicAccessBlockHandler
|
|
130
133
|
];
|
|
131
134
|
const handlers = s3Handlers;
|
|
132
135
|
export { createBucketHandler, handlers as default, getBucketVersioningHandler, s3Handlers };
|