@scality/data-browser-library 1.1.2 → 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/objects/ObjectList.js +6 -31
- package/dist/hooks/__tests__/usePresigningS3Client.test.js +44 -23
- 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/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/package.json +1 -1
|
@@ -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
|
});
|
|
@@ -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 };
|
|
@@ -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
|
]);
|