Skip to content

Commit 4352848

Browse files
committed
fix(file-upload): Feedback + more simplifcation
1 parent 4b7b41e commit 4352848

File tree

3 files changed

+88
-60
lines changed

3 files changed

+88
-60
lines changed

src/features/account/service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import { fetchFile, uploadFile } from '../../files/utils';
88
export const useAvatarFetch = (url: string) => {
99
return useQuery({
1010
queryKey: ['account', url],
11-
queryFn: fetchFile(url, ['name']),
11+
queryFn: () => fetchFile(url, ['name']),
1212
enabled: !!url,
1313
});
1414
};
1515

1616
export const useAvatarUpload = () => {
1717
const getPresignedUrl = trpc.account.uploadAvatarPresignedUrl.useMutation();
1818
return useMutation({
19-
mutationFn: uploadFile(getPresignedUrl.mutateAsync),
19+
mutationFn: (file?: File) => uploadFile(getPresignedUrl.mutateAsync, file),
2020
});
2121
};

src/files/utils.ts

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,99 @@ import { UseMutateAsyncFunction } from '@tanstack/react-query';
22

33
import { UploadSignedUrlInput } from './schemas';
44

5-
export const fetchFile = (url: string, metadata?: string[]) => async () => {
5+
/**
6+
* Fetches a file from the specified URL and returns file information.
7+
* Designed to be used as a `queryFn` in a `useQuery`.
8+
*
9+
* @param url The URL from which the file should be fetched.
10+
* @param [metadata] The metadata of the file you want to retrieve.
11+
* @returns A Promise that resolves to an object containing information about the file.
12+
*
13+
* @example
14+
* // Usage with Tanstack Query's useQuery:
15+
* const fileQuery = useQuery({
16+
queryKey: ['fileKey', url],
17+
queryFn: () => fetchFile(url, ['name']),
18+
enabled: !!url,
19+
});
20+
*/
21+
export const fetchFile = async (url: string, metadata?: string[]) => {
622
const fileResponse = await fetch(url);
723
if (!fileResponse.ok) {
824
throw new Error('Could not fetch the file');
925
}
1026

1127
const lastModifiedDateHeader = fileResponse.headers.get('Last-Modified');
28+
const defaultFileData = {
29+
fileUrl: url,
30+
size: fileResponse.headers.get('Content-Length') ?? undefined,
31+
type: fileResponse.headers.get('Content-Type') ?? undefined,
32+
lastModifiedDate: lastModifiedDateHeader
33+
? new Date(lastModifiedDateHeader)
34+
: new Date(),
35+
};
1236

13-
return (metadata || []).reduce(
14-
(file, currentMetadata) => {
15-
return {
16-
...file,
17-
[currentMetadata]: fileResponse.headers.get(
18-
`x-amz-meta-${currentMetadata}`
19-
),
20-
};
21-
},
22-
{
23-
fileUrl: url,
24-
size: fileResponse.headers.get('Content-Length') ?? undefined,
25-
type: fileResponse.headers.get('Content-Type') ?? undefined,
26-
lastModifiedDate: lastModifiedDateHeader
27-
? new Date(lastModifiedDateHeader)
28-
: new Date(),
29-
}
30-
);
37+
if (!metadata) {
38+
return defaultFileData;
39+
}
40+
41+
return metadata.reduce((file, metadataKey) => {
42+
return {
43+
...file,
44+
[metadataKey]: fileResponse.headers.get(`x-amz-meta-${metadataKey}`),
45+
};
46+
}, defaultFileData);
3147
};
3248

33-
export const uploadFile =
34-
(
35-
getPresignedUrl: UseMutateAsyncFunction<
36-
{ signedUrl: string; futureFileUrl: string },
37-
unknown,
38-
UploadSignedUrlInput | void
39-
>
40-
) =>
41-
async (
42-
file?: File,
43-
{
44-
metadata,
45-
}: {
46-
metadata?: Record<string, string>;
47-
} = {}
48-
) => {
49-
if (!file) {
50-
return {
51-
fileUrl: undefined,
52-
};
53-
}
49+
/**
50+
* Asynchronously uploads a file to a server using a presigned URL.
51+
* Designed to be used as a `mutationFn` in a `useMutation`.
52+
*
53+
* @param getPresignedUrl
54+
* - An asyncMutation that is used to obtain the presigned URL and the future URL where the file will be accessible.
55+
*
56+
* @param file - The file object to upload.
57+
* @param metadata - Optional metadata for the file, which will be sent to the server when generating the presigned URL.
58+
*
59+
* @returns A promise that resolves to an object containing the URL of the uploaded file,
60+
* or undefined if no file was provided.
61+
*
62+
* @example
63+
* // Usage with Tanstack Query's useMutation:
64+
* const getPresignedUrl = trpc.routeToGetPresignedUrl.useMutation();
65+
const fileUpload = useMutation({
66+
mutationFn: (file?: File) => uploadFile(getPresignedUrl.mutateAsync, file),
67+
});
68+
*/
69+
export const uploadFile = async (
70+
getPresignedUrl: UseMutateAsyncFunction<
71+
{ signedUrl: string; futureFileUrl: string },
72+
unknown,
73+
UploadSignedUrlInput | void
74+
>,
75+
file?: File,
76+
metadata: Record<string, string> = {}
77+
) => {
78+
if (!file) {
79+
return {
80+
fileUrl: undefined,
81+
};
82+
}
5483

55-
console.log(file.name + ' uploaded');
56-
const { signedUrl, futureFileUrl } = await getPresignedUrl({
57-
metadata: {
58-
name: file.name,
59-
...metadata,
60-
},
61-
});
84+
const { signedUrl, futureFileUrl } = await getPresignedUrl({
85+
metadata: {
86+
name: file.name,
87+
...metadata,
88+
},
89+
});
6290

63-
await fetch(signedUrl, {
64-
method: 'PUT',
65-
headers: { 'Content-Type': file.type },
66-
body: file,
67-
});
91+
await fetch(signedUrl, {
92+
method: 'PUT',
93+
headers: { 'Content-Type': file.type },
94+
body: file,
95+
});
6896

69-
return {
70-
fileUrl: futureFileUrl,
71-
} as const;
72-
};
97+
return {
98+
fileUrl: futureFileUrl,
99+
} as const;
100+
};

src/server/config/s3.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
44
import { env } from '@/env.mjs';
55
import { UploadSignedUrlOutput } from '@/files/schemas';
66

7-
const SIGNED_URL_EXPIRATION_TIME_MS = 3600;
7+
const SIGNED_URL_EXPIRATION_TIME_SECONDS = 3600; // 1 hour
88

99
const S3 = new S3Client({
1010
region: 'auto',
@@ -36,7 +36,7 @@ export const getS3UploadSignedUrl = async (
3636
Key: options.key,
3737
Metadata: options.metadata,
3838
}),
39-
{ expiresIn: options.expiresIn ?? SIGNED_URL_EXPIRATION_TIME_MS }
39+
{ expiresIn: options.expiresIn ?? SIGNED_URL_EXPIRATION_TIME_SECONDS }
4040
);
4141

4242
return {

0 commit comments

Comments
 (0)