Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: return file hash from uploaded object #978

Merged
merged 14 commits into from
Oct 6, 2024
5 changes: 5 additions & 0 deletions .changeset/nice-crabs-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"uploadthing": minor
---

feat: return object hash in onUploadComplete
2 changes: 1 addition & 1 deletion examples/minimal-appdir/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
5 changes: 4 additions & 1 deletion examples/minimal-appdir/src/server/uploadthing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ export const uploadRouter = {
},
blob: {
maxFileSize: "8GB",
maxFileCount: 10,
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
},
})
.middleware(({ req, files }) => {
// Check some condition based on the incoming requrest
console.log("Request", req);
req;
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
//^?
// if (!req.headers.get("x-some-header")) {
// throw new Error("x-some-header is required");
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -57,6 +58,8 @@ export const uploadRouter = {
file.customId;
// ^?
console.log("upload completed", file);

return { foo: "bar" };
markflorkowski marked this conversation as resolved.
Show resolved Hide resolved
}),
} satisfies FileRouter;

Expand Down
2 changes: 2 additions & 0 deletions packages/uploadthing/src/internal/shared-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@ export class FileUploadDataWithCustomId extends FileUploadData.extend<FileUpload
* - a key
* - a direct URL for the file
* - an app-specific URL for the file (useful for scoping eg. for optimization allowed origins)
* - the hash (md5-hex) of the uploaded file's contents
*/
export class UploadedFileData extends FileUploadDataWithCustomId.extend<UploadedFileData>(
"UploadedFileData",
)({
key: S.String,
url: S.String,
appUrl: S.String,
fileHash: S.String,
}) {}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/uploadthing/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,13 @@ export type UTEvents = {
out: ReadonlyArray<NewPresignedUrl>;
};
};

/**
* Result from the PUT request to the UploadThing Ingest server
*/
export type UploadPutResult<TServerOutput = unknown> = {
url: string;
appUrl: string;
fileHash: string;
serverData: TServerOutput;
};
9 changes: 3 additions & 6 deletions packages/uploadthing/src/internal/upload.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
NewPresignedUrl,
UploadFilesOptions,
} from "../types";
import type { UploadPutResult } from "./types";
import { createUTReporter } from "./ut-reporter";

const uploadWithProgress = (
Expand Down Expand Up @@ -92,12 +93,7 @@ export const uploadFile = <
}),
),
),
Micro.map(
unsafeCoerce<
unknown,
{ url: string; appUrl: string; serverData: TServerOutput }
>,
),
Micro.map(unsafeCoerce<unknown, UploadPutResult<TServerOutput>>),
Micro.map((uploadResponse) => ({
name: file.name,
size: file.size,
Expand All @@ -108,6 +104,7 @@ export const uploadFile = <
appUrl: uploadResponse.appUrl,
customId: presigned.customId,
type: file.type,
fileHash: uploadResponse.fileHash,
})),
);

Expand Down
4 changes: 2 additions & 2 deletions packages/uploadthing/src/internal/upload.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { unsafeCoerce } from "effect/Function";
import { UploadThingError } from "@uploadthing/shared";

import type { FileEsque } from "../sdk/types";
import type { UploadPutResult } from "./types";

export const uploadWithoutProgress = (
file: FileEsque,
Expand All @@ -32,8 +33,7 @@ export const uploadWithoutProgress = (
}),
),
HttpClientResponse.json,

Effect.andThen(unsafeCoerce<unknown, { url: string; appUrl: string }>),
Effect.andThen(unsafeCoerce<unknown, UploadPutResult>),
);

yield* Effect.logDebug(`File ${file.name} uploaded successfully`).pipe(
Expand Down
7 changes: 4 additions & 3 deletions packages/uploadthing/src/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,17 @@ const uploadFile = (
Effect.gen(function* () {
const { file, presigned } = input;

const { url, appUrl } = yield* uploadWithoutProgress(file, presigned);
const response = yield* uploadWithoutProgress(file, presigned);

return {
key: presigned.key,
url: url,
appUrl: appUrl,
url: response.url,
appUrl: response.appUrl,
lastModified: file.lastModified ?? Date.now(),
name: file.name,
size: file.size,
type: file.type,
customId: file.customId ?? null,
fileHash: response.fileHash,
};
}).pipe(Effect.withLogSpan("uploadFile"));
4 changes: 4 additions & 0 deletions packages/uploadthing/test/__test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from "crypto";
import * as S from "@effect/schema/Schema";
import type { StrictRequest } from "msw";
import { http, HttpResponse } from "msw";
Expand Down Expand Up @@ -119,6 +120,9 @@ export const it = itBase.extend({
url: `${UTFS_IO_URL}/f/${params.key}`,
appUrl: `${UTFS_IO_URL}/a/${appId}/${params.key}`,
serverData: null,
hash: createHash("md5")
.update(new Uint8Array(await request.arrayBuffer()))
.digest("hex"),
});
},
),
Expand Down
3 changes: 3 additions & 0 deletions packages/uploadthing/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
const { uploadFiles, close } = await setupUTServer();
const file = new File(["foo"], "foo.txt", { type: "text/plain" });

await expect(uploadFiles("foo", { files: [file] })).resolves.toEqual([

Check failure on line 124 in packages/uploadthing/test/client.test.ts

View workflow job for this annotation

GitHub Actions / build

test/client.test.ts > uploadFiles > uploads file using presigned PUT

AssertionError: expected [ { name: 'foo.txt', size: 3, …(8) } ] to deeply equal [ { name: 'foo.txt', size: 3, …(8) } ] - Expected + Received Array [ Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/app-1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "foo.txt", "serverData": null, "size": 3, "type": "text/plain", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, ] ❯ test/client.test.ts:124:5
{
name: "foo.txt",
size: 3,
Expand All @@ -132,6 +132,7 @@
key: expect.stringMatching(/.+/),
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern()),
hash: expect.any(String),
},
]);

Expand Down Expand Up @@ -159,7 +160,7 @@
const { uploadFiles, close } = await setupUTServer();

const file = new File(["foo"], "foo.txt", { type: "text/plain" });
await expect(

Check failure on line 163 in packages/uploadthing/test/client.test.ts

View workflow job for this annotation

GitHub Actions / build

test/client.test.ts > uploadFiles > sends custom headers if set (static object)

AssertionError: expected [ { name: 'foo.txt', size: 3, …(8) } ] to deeply equal [ { name: 'foo.txt', size: 3, …(8) } ] - Expected + Received Array [ Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/app-1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "foo.txt", "serverData": null, "size": 3, "type": "text/plain", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, ] ❯ test/client.test.ts:163:5
uploadFiles("foo", {
files: [file],
headers: {
Expand All @@ -177,6 +178,7 @@
key: expect.stringMatching(/.+/),
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern()),
hash: expect.any(String),
},
]);

Expand All @@ -193,7 +195,7 @@
const { uploadFiles, close } = await setupUTServer();

const file = new File(["foo"], "foo.txt", { type: "text/plain" });
await expect(

Check failure on line 198 in packages/uploadthing/test/client.test.ts

View workflow job for this annotation

GitHub Actions / build

test/client.test.ts > uploadFiles > sends custom headers if set (async function)

AssertionError: expected [ { name: 'foo.txt', size: 3, …(8) } ] to deeply equal [ { name: 'foo.txt', size: 3, …(8) } ] - Expected + Received Array [ Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/app-1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "foo.txt", "serverData": null, "size": 3, "type": "text/plain", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, ] ❯ test/client.test.ts:198:5
uploadFiles("foo", {
files: [file],
headers: async () => ({
Expand All @@ -211,6 +213,7 @@
key: expect.stringMatching(/.+/),
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern()),
hash: expect.any(String),
},
]);

Expand Down
4 changes: 4 additions & 0 deletions packages/uploadthing/test/request-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ describe(".onUploadComplete()", () => {
size: 48,
type: "image/png",
customId: null,
hash: "some-md5-hash",
}),
});
const signature = await Effect.runPromise(
Expand Down Expand Up @@ -366,6 +367,7 @@ describe(".onUploadComplete()", () => {
type: "image/png",
url: "https://utfs.io/f/some-random-key.png",
appUrl: `https://utfs.io/a/${testToken.decoded.appId}/some-random-key.png`,
hash: "some-md5-hash",
},
metadata: {},
});
Expand All @@ -383,6 +385,7 @@ describe(".onUploadComplete()", () => {
size: 48,
type: "image/png",
customId: null,
hash: "some-md5-hash",
}),
});

Expand Down Expand Up @@ -415,6 +418,7 @@ describe(".onUploadComplete()", () => {
size: 48,
type: "image/png",
customId: null,
hash: "some-md5-hash",
}),
});
const signature = await Effect.runPromise(
Expand Down
10 changes: 10 additions & 0 deletions packages/uploadthing/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
);

const key = result.data?.key;
expect(result).toEqual({

Check failure on line 47 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > uploadFiles > uploads successfully

AssertionError: expected { data: { …(9) }, error: null } to deeply equal { data: { …(9) }, error: null } - Expected + Received Object { "data": Object { "appUrl": "https://utfs.io/a/app-1/u7rKiJYp268seyXN4iERSnf5E7mJgIFqsQM62cNpjouxyi3X", "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": 1727386237904, "name": "foo.txt", "size": 3, "type": "text/plain", "url": "https://utfs.io/f/u7rKiJYp268seyXN4iERSnf5E7mJgIFqsQM62cNpjouxyi3X", }, "error": null, } ❯ test/sdk.test.ts:47:20
data: {
key: expect.stringMatching(/.+/),
name: "foo.txt",
Expand All @@ -54,6 +54,7 @@
appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key}`,
customId: null,
type: "text/plain",
hash: expect.any(String),
},
error: null,
});
Expand Down Expand Up @@ -126,7 +127,7 @@
);

const key = result.data?.key;
expect(result).toEqual({

Check failure on line 130 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > uploadFilesFromUrl > downloads, then uploads successfully

AssertionError: expected { data: { …(9) }, error: null } to deeply equal { data: { …(9) }, error: null } - Expected + Received Object { "data": Object { "appUrl": "https://utfs.io/a/app-1/u7rKiJYp268skhH2rhvuZaFbQOrV90MKDzPohfTXwgpCvYWq", "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "foo.txt", "size": 26, "type": "text/plain", "url": "https://utfs.io/f/u7rKiJYp268skhH2rhvuZaFbQOrV90MKDzPohfTXwgpCvYWq", }, "error": null, } ❯ test/sdk.test.ts:130:20
data: {
key: expect.stringMatching(/.+/),
name: "foo.txt",
Expand All @@ -136,6 +137,7 @@
appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key}`,
customId: null,
type: "text/plain",
hash: expect.any(String),
},
error: null,
});
Expand Down Expand Up @@ -224,7 +226,7 @@
]);
const key1 = result[0].data?.key;
const key2 = result[2].data?.key;
expect(result).toStrictEqual([

Check failure on line 229 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > uploadFilesFromUrl > preserves order if some download fails

AssertionError: expected [ …(3) ] to strictly equal [ …(3) ] - Expected + Received Array [ Object { "data": Object { "appUrl": "https://utfs.io/a/app-1/u7rKiJYp268sVvNsG1jXiCAWDk13YnIKosJbx5Pg6e8Qtcmz", "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "foo.txt", "size": 26, "type": "text/plain", "url": "https://utfs.io/f/u7rKiJYp268sVvNsG1jXiCAWDk13YnIKosJbx5Pg6e8Qtcmz", }, "error": null, }, Object { "data": null, "error": Object { "code": "BAD_REQUEST", "data": undefined, "message": "Please use uploadFiles() for data URLs. uploadFilesFromUrl() is intended for use with remote URLs only.", }, }, Object { "data": Object { "appUrl": "https://utfs.io/a/app-1/u7rKiJYp268smtmrGEg75k3SVGs6EpbMDPafuF28WerKyAwx", "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "bar.txt", "size": 26, "type": "text/plain", "url": "https://utfs.io/f/u7rKiJYp268smtmrGEg75k3SVGs6EpbMDPafuF28WerKyAwx", }, "error": null, }, ] ❯ test/sdk.test.ts:229:20
{
data: {
customId: null,
Expand All @@ -235,6 +237,7 @@
type: "text/plain",
url: `${UTFS_IO_URL}/f/${key1}`,
appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key1}`,
hash: expect.any(String),
},
error: null,
},
Expand All @@ -257,6 +260,7 @@
type: "text/plain",
url: `${UTFS_IO_URL}/f/${key2}`,
appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key2}`,
hash: expect.any(String),
},
error: null,
},
Expand Down Expand Up @@ -463,7 +467,7 @@
const file = new File(["foo"], "foo.txt", { type: "text/plain" });
const result = await utapi.uploadFiles(file);
const key = result.data!.key;
expect(result).toEqual({

Check failure on line 470 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > smoke test with live api > should upload a file

AssertionError: expected { data: { …(9) }, error: null } to deeply equal { Object (data, error) } - Expected + Received Object { "data": Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/fr0hfwpst1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": 1727386240110, "name": "foo.txt", "size": 3, "type": "text/plain", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, "error": null, } ❯ test/sdk.test.ts:470:22
data: {
customId: null,
key: expect.stringMatching(/.+/),
Expand All @@ -473,6 +477,7 @@
type: "text/plain",
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern(appId)),
hash: expect.any(String),
},
error: null,
});
Expand All @@ -498,7 +503,7 @@
acl: "private",
});
const key = result.data!.key;
expect(result).toEqual({

Check failure on line 506 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > smoke test with live api > should upload a private file

AssertionError: expected { data: { …(9) }, error: null } to deeply equal { Object (data, error) } - Expected + Received Object { "data": Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/fr0hfwpst1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": 1727386241975, "name": "foo.txt", "size": 3, "type": "text/plain", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, "error": null, } ❯ test/sdk.test.ts:506:22
data: {
customId: null,
key: expect.stringMatching(/.+/),
Expand All @@ -508,6 +513,7 @@
type: "text/plain",
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern(appId)),
hash: expect.any(String),
},
error: null,
});
Expand All @@ -528,7 +534,7 @@
"https://uploadthing.com/favicon.ico",
);
const key = result.data!.key;
expect(result).toEqual({

Check failure on line 537 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > smoke test with live api > should upload a file from a url

AssertionError: expected { data: { …(9) }, error: null } to deeply equal { Object (data, error) } - Expected + Received Object { "data": Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/fr0hfwpst1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "favicon.ico", "size": Any<Number>, "type": "image/vnd.microsoft.icon", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, "error": null, } ❯ test/sdk.test.ts:537:22
data: {
customId: null,
key: expect.stringMatching(/.+/),
Expand All @@ -538,6 +544,7 @@
type: "image/vnd.microsoft.icon",
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern(appId)),
hash: expect.any(String),
},
error: null,
});
Expand All @@ -552,7 +559,7 @@
const file = new UTFile(["foo"], "bar.txt");
const result = await utapi.uploadFiles(file);
const fileKey = result.data!.key;
expect(result).toEqual({

Check failure on line 562 in packages/uploadthing/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / build

test/sdk.test.ts > smoke test with live api > should rename a file with fileKey

AssertionError: expected { data: { …(9) }, error: null } to deeply equal { Object (data, error) } - Expected + Received Object { "data": Object { "appUrl": StringMatching /^https:\/\/utfs.io\/a\/fr0hfwpst1\/.+$/, "customId": null, - "hash": Any<String>, + "fileHash": undefined, "key": StringMatching /.+/, "lastModified": Any<Number>, "name": "bar.txt", "size": 3, "type": "text/plain", "url": StringMatching /^https:\/\/utfs.io\/f\/.+$/, }, "error": null, } ❯ test/sdk.test.ts:562:22
data: {
customId: null,
key: expect.stringMatching(/.+/),
Expand All @@ -563,6 +570,7 @@
type: "text/plain",
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern(appId)),
hash: expect.any(String),
},
error: null,
});
Expand Down Expand Up @@ -605,6 +613,7 @@
type: "text/plain",
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern(appId)),
hash: expect.any(String),
},
error: null,
});
Expand Down Expand Up @@ -645,6 +654,7 @@
type: "text/plain",
url: expect.stringMatching(fileUrlPattern),
appUrl: expect.stringMatching(appUrlPattern(appId)),
hash: expect.any(String),
},
error: null,
});
Expand Down
Loading