Skip to content

Commit 08ecc90

Browse files
committed
feat(storage): add PUT method support for getUrl presigned upload URLs
1 parent 54bf753 commit 08ecc90

File tree

6 files changed

+103
-5
lines changed

6 files changed

+103
-5
lines changed

packages/storage/src/internals/apis/getUrl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const getUrl = (input: GetUrlInput) =>
2525
// Advanced options
2626
locationCredentialsProvider: input?.options?.locationCredentialsProvider,
2727
customEndpoint: input?.options?.customEndpoint,
28+
method: input?.options?.method,
2829
},
2930
// Type casting is necessary because `getPropertiesInternal` supports both Gen1 and Gen2 signatures, but here
3031
// given in input can only be Gen2 signature, the return can only ben Gen2 signature.

packages/storage/src/providers/s3/apis/getUrl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { getUrl as getUrlInternal } from './internal/getUrl';
1414

1515
/**
16-
* Get a temporary presigned URL to download the specified S3 object.
16+
* Get a temporary presigned URL to download or upload the specified S3 object.
1717
* The presigned URL expires when the associated role used to sign the request expires or
1818
* the option `expiresIn` is reached. The `expiresAt` property in the output object indicates when the URL MAY expire.
1919
*

packages/storage/src/providers/s3/apis/internal/getUrl.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { StorageAction } from '@aws-amplify/core/internals/utils';
66

77
import { GetUrlInput, GetUrlOutput, GetUrlWithPathOutput } from '../../types';
88
import { StorageValidationErrorCode } from '../../../../errors/types/validation';
9-
import { getPresignedGetObjectUrl } from '../../utils/client/s3data';
9+
import {
10+
getPresignedGetObjectUrl,
11+
getPresignedPutObjectUrl,
12+
} from '../../utils/client/s3data';
1013
import {
1114
resolveS3ConfigAndInput,
1215
validateBucketOwnerID,
@@ -39,8 +42,12 @@ export const getUrl = async (
3942

4043
const finalKey =
4144
inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey;
45+
const operation = getUrlOptions?.method ?? 'GET';
4246

43-
if (getUrlOptions?.validateObjectExistence) {
47+
if (
48+
getUrlOptions?.validateObjectExistence &&
49+
getUrlOptions?.method !== 'PUT'
50+
) {
4451
await getProperties(amplify, input, StorageAction.GetUrl);
4552
}
4653

@@ -63,7 +70,34 @@ export const getUrl = async (
6370
StorageValidationErrorCode.UrlExpirationMaxLimitExceed,
6471
);
6572

66-
// expiresAt is the minimum of credential expiration and url expiration
73+
if (operation === 'PUT') {
74+
return {
75+
url: await getPresignedPutObjectUrl(
76+
{
77+
...s3Config,
78+
credentials: resolvedCredential,
79+
expiration: urlExpirationInSec,
80+
},
81+
{
82+
Bucket: bucket,
83+
Key: finalKey,
84+
...(getUrlOptions?.contentType && {
85+
ContentType: getUrlOptions.contentType,
86+
}),
87+
...(getUrlOptions?.contentDisposition && {
88+
ContentDisposition:
89+
typeof getUrlOptions.contentDisposition === 'string'
90+
? getUrlOptions.contentDisposition
91+
: constructContentDisposition(getUrlOptions.contentDisposition),
92+
}),
93+
CacheControl: getUrlOptions?.cacheControl,
94+
ExpectedBucketOwner: getUrlOptions?.expectedBucketOwner,
95+
},
96+
),
97+
expiresAt: new Date(Date.now() + urlExpirationInSec * 1000),
98+
};
99+
}
100+
67101
return {
68102
url: await getPresignedGetObjectUrl(
69103
{

packages/storage/src/providers/s3/types/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ export type ListPaginateWithPathOptions = Omit<
158158
* Input options type for S3 getUrl API.
159159
*/
160160
export type GetUrlOptions = CommonOptions & {
161+
/**
162+
* The HTTP method for the presigned URL.
163+
* @default 'GET'
164+
*/
165+
method?: 'GET' | 'PUT';
161166
/**
162167
* Whether to head object to make sure the object existence before downloading.
163168
* @default false

packages/storage/src/providers/s3/utils/client/s3data/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export {
1313
ListObjectsV2Input,
1414
ListObjectsV2Output,
1515
} from './listObjectsV2';
16-
export { putObject, PutObjectInput, PutObjectOutput } from './putObject';
16+
export {
17+
putObject,
18+
PutObjectInput,
19+
PutObjectOutput,
20+
getPresignedPutObjectUrl,
21+
} from './putObject';
1722
export {
1823
createMultipartUpload,
1924
CreateMultipartUploadInput,

packages/storage/src/providers/s3/utils/client/s3data/putObject.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import {
55
Endpoint,
66
HttpRequest,
77
HttpResponse,
8+
PresignUrlOptions,
9+
UNSIGNED_PAYLOAD,
10+
UserAgentOptions,
811
parseMetadata,
12+
presignUrl,
913
} from '@aws-amplify/core/internals/aws-client-utils';
1014
import {
1115
AmplifyUrl,
1216
AmplifyUrlSearchParams,
17+
USER_AGENT_HEADER,
1318
} from '@aws-amplify/core/internals/utils';
1419
import { composeServiceApi } from '@aws-amplify/core/internals/aws-client-utils/composers';
1520

@@ -26,6 +31,7 @@ import { validateObjectUrl } from '../../validateObjectUrl';
2631

2732
import { defaultConfig, parseXmlError } from './base';
2833
import type { PutObjectCommandInput, PutObjectCommandOutput } from './types';
34+
import type { S3EndpointResolverOptions } from './base';
2935

3036
export type PutObjectInput = Pick<
3137
PutObjectCommandInput,
@@ -112,3 +118,50 @@ export const putObject = composeServiceApi(
112118
putObjectDeserializer,
113119
{ ...defaultConfig, responseType: 'text' },
114120
);
121+
122+
type S3PutObjectPresignedUrlConfig = Omit<
123+
UserAgentOptions & PresignUrlOptions & S3EndpointResolverOptions,
124+
'signingService' | 'signingRegion'
125+
> & {
126+
signingService?: string;
127+
signingRegion?: string;
128+
};
129+
130+
/**
131+
* Get a presigned URL for the `putObject` API.
132+
*
133+
* @internal
134+
*/
135+
export const getPresignedPutObjectUrl = async (
136+
config: S3PutObjectPresignedUrlConfig,
137+
input: Omit<PutObjectInput, 'Body'>,
138+
): Promise<URL> => {
139+
const endpoint = defaultConfig.endpointResolver(config, input);
140+
const { url, headers, method } = await putObjectSerializer(
141+
{ ...input, Body: undefined },
142+
endpoint,
143+
);
144+
145+
if (config.userAgentValue) {
146+
url.searchParams.append(
147+
config.userAgentHeader ?? USER_AGENT_HEADER,
148+
config.userAgentValue,
149+
);
150+
}
151+
152+
for (const [headerName, value] of Object.entries(headers).sort(
153+
([key1], [key2]) => key1.localeCompare(key2),
154+
)) {
155+
url.searchParams.append(headerName, value);
156+
}
157+
158+
return presignUrl(
159+
{ method, url, body: UNSIGNED_PAYLOAD },
160+
{
161+
signingService: defaultConfig.service,
162+
signingRegion: config.region,
163+
...defaultConfig,
164+
...config,
165+
},
166+
);
167+
};

0 commit comments

Comments
 (0)