diff --git a/.changeset/nice-crabs-compete.md b/.changeset/nice-crabs-compete.md new file mode 100644 index 0000000000..0fb860cbab --- /dev/null +++ b/.changeset/nice-crabs-compete.md @@ -0,0 +1,5 @@ +--- +"uploadthing": minor +--- + +feat: return object hash in onUploadComplete diff --git a/examples/minimal-appdir/src/server/uploadthing.ts b/examples/minimal-appdir/src/server/uploadthing.ts index c6c43ba817..981ac16f8b 100644 --- a/examples/minimal-appdir/src/server/uploadthing.ts +++ b/examples/minimal-appdir/src/server/uploadthing.ts @@ -36,8 +36,6 @@ export const uploadRouter = { }) .middleware(({ req, files }) => { // Check some condition based on the incoming requrest - console.log("Request", req); - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/examples/minimal-pagedir/src/server/uploadthing.ts b/examples/minimal-pagedir/src/server/uploadthing.ts index 95580c5ec1..b635168a09 100644 --- a/examples/minimal-pagedir/src/server/uploadthing.ts +++ b/examples/minimal-pagedir/src/server/uploadthing.ts @@ -30,8 +30,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers["x-some-header"]) { // throw new Error("x-some-header is required"); // } diff --git a/examples/minimal-solidstart/src/server/uploadthing.ts b/examples/minimal-solidstart/src/server/uploadthing.ts index 1e0941de0c..5812840e77 100644 --- a/examples/minimal-solidstart/src/server/uploadthing.ts +++ b/examples/minimal-solidstart/src/server/uploadthing.ts @@ -30,8 +30,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming request - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/examples/minimal-sveltekit/src/lib/server/uploadthing.ts b/examples/minimal-sveltekit/src/lib/server/uploadthing.ts index 6b32c8872c..32641c373d 100644 --- a/examples/minimal-sveltekit/src/lib/server/uploadthing.ts +++ b/examples/minimal-sveltekit/src/lib/server/uploadthing.ts @@ -33,8 +33,6 @@ export const uploadRouter = { }) .middleware(({ req, files }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/examples/with-drizzle-appdir/src/server/uploadthing.ts b/examples/with-drizzle-appdir/src/server/uploadthing.ts index 3bfdd76d44..36801d6600 100644 --- a/examples/with-drizzle-appdir/src/server/uploadthing.ts +++ b/examples/with-drizzle-appdir/src/server/uploadthing.ts @@ -35,8 +35,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/examples/with-drizzle-pagesdir/src/server/uploadthing.ts b/examples/with-drizzle-pagesdir/src/server/uploadthing.ts index aabdd7740b..8d884a0ae0 100644 --- a/examples/with-drizzle-pagesdir/src/server/uploadthing.ts +++ b/examples/with-drizzle-pagesdir/src/server/uploadthing.ts @@ -33,8 +33,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/examples/with-react-image-crop/src/server/uploadthing.ts b/examples/with-react-image-crop/src/server/uploadthing.ts index f239c2822f..a063885f0e 100644 --- a/examples/with-react-image-crop/src/server/uploadthing.ts +++ b/examples/with-react-image-crop/src/server/uploadthing.ts @@ -27,8 +27,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/examples/with-tailwindcss/src/server/uploadthing.ts b/examples/with-tailwindcss/src/server/uploadthing.ts index 306321ca8c..b79ce4e4a9 100644 --- a/examples/with-tailwindcss/src/server/uploadthing.ts +++ b/examples/with-tailwindcss/src/server/uploadthing.ts @@ -30,8 +30,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/packages/nuxt/playground/server/uploadthing.ts b/packages/nuxt/playground/server/uploadthing.ts index 9d74caeb3c..cf7272dcc0 100644 --- a/packages/nuxt/playground/server/uploadthing.ts +++ b/packages/nuxt/playground/server/uploadthing.ts @@ -30,8 +30,6 @@ export const uploadRouter = { }) .middleware(({ req }) => { // Check some condition based on the incoming requrest - req; - //^? // if (!req.headers.get("x-some-header")) { // throw new Error("x-some-header is required"); // } diff --git a/packages/uploadthing/src/internal/shared-schemas.ts b/packages/uploadthing/src/internal/shared-schemas.ts index e8f215efad..cbac8755dd 100644 --- a/packages/uploadthing/src/internal/shared-schemas.ts +++ b/packages/uploadthing/src/internal/shared-schemas.ts @@ -71,6 +71,7 @@ export class FileUploadDataWithCustomId extends FileUploadData.extend( "UploadedFileData", @@ -78,6 +79,7 @@ export class UploadedFileData extends FileUploadDataWithCustomId.extend; }; }; + +/** + * Result from the PUT request to the UploadThing Ingest server + */ +export type UploadPutResult = { + url: string; + appUrl: string; + fileHash: string; + serverData: TServerOutput; +}; diff --git a/packages/uploadthing/src/internal/upload.browser.ts b/packages/uploadthing/src/internal/upload.browser.ts index bb999a8339..ad5a75aee3 100644 --- a/packages/uploadthing/src/internal/upload.browser.ts +++ b/packages/uploadthing/src/internal/upload.browser.ts @@ -11,6 +11,7 @@ import type { NewPresignedUrl, UploadFilesOptions, } from "../types"; +import type { UploadPutResult } from "./types"; import { createUTReporter } from "./ut-reporter"; const uploadWithProgress = ( @@ -92,12 +93,7 @@ export const uploadFile = < }), ), ), - Micro.map( - unsafeCoerce< - unknown, - { url: string; appUrl: string; serverData: TServerOutput } - >, - ), + Micro.map(unsafeCoerce>), Micro.map((uploadResponse) => ({ name: file.name, size: file.size, @@ -108,6 +104,7 @@ export const uploadFile = < appUrl: uploadResponse.appUrl, customId: presigned.customId, type: file.type, + fileHash: uploadResponse.fileHash, })), ); diff --git a/packages/uploadthing/src/internal/upload.server.ts b/packages/uploadthing/src/internal/upload.server.ts index 8d4f994466..aaa882107a 100644 --- a/packages/uploadthing/src/internal/upload.server.ts +++ b/packages/uploadthing/src/internal/upload.server.ts @@ -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, @@ -32,8 +33,7 @@ export const uploadWithoutProgress = ( }), ), HttpClientResponse.json, - - Effect.andThen(unsafeCoerce), + Effect.andThen(unsafeCoerce), ); yield* Effect.logDebug(`File ${file.name} uploaded successfully`).pipe( diff --git a/packages/uploadthing/src/sdk/utils.ts b/packages/uploadthing/src/sdk/utils.ts index a5c4e81902..55ceb9446e 100644 --- a/packages/uploadthing/src/sdk/utils.ts +++ b/packages/uploadthing/src/sdk/utils.ts @@ -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")); diff --git a/packages/uploadthing/test/__test-helpers.ts b/packages/uploadthing/test/__test-helpers.ts index 7e6f27a914..95dfb49d5a 100644 --- a/packages/uploadthing/test/__test-helpers.ts +++ b/packages/uploadthing/test/__test-helpers.ts @@ -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"; @@ -7,6 +8,7 @@ import { afterAll, beforeAll, it as itBase, vi } from "vitest"; import { UPLOADTHING_VERSION } from "../src/internal/config"; import { ParsedToken, UploadThingToken } from "../src/internal/shared-schemas"; import type { ActionType } from "../src/internal/shared-schemas"; +import type { UploadPutResult } from "../src/internal/types"; export const requestSpy = vi.fn<(url: string, req: RequestInit) => void>(); export const requestsToDomain = (domain: string) => @@ -115,10 +117,13 @@ export const it = itBase.extend({ async ({ request, params }) => { await callRequestSpy(request); const appId = new URLSearchParams(request.url).get("x-ut-identifier"); - return HttpResponse.json({ + return HttpResponse.json({ url: `${UTFS_IO_URL}/f/${params.key}`, appUrl: `${UTFS_IO_URL}/a/${appId}/${params.key}`, serverData: null, + fileHash: createHash("md5") + .update(new Uint8Array(await request.arrayBuffer())) + .digest("hex"), }); }, ), diff --git a/packages/uploadthing/test/client.test.ts b/packages/uploadthing/test/client.test.ts index 2d72bc8fb8..5e3e189192 100644 --- a/packages/uploadthing/test/client.test.ts +++ b/packages/uploadthing/test/client.test.ts @@ -132,6 +132,7 @@ describe("uploadFiles", () => { key: expect.stringMatching(/.+/), url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern()), + fileHash: expect.any(String), }, ]); @@ -177,6 +178,7 @@ describe("uploadFiles", () => { key: expect.stringMatching(/.+/), url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern()), + fileHash: expect.any(String), }, ]); @@ -211,6 +213,7 @@ describe("uploadFiles", () => { key: expect.stringMatching(/.+/), url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern()), + fileHash: expect.any(String), }, ]); diff --git a/packages/uploadthing/test/request-handler.test.ts b/packages/uploadthing/test/request-handler.test.ts index ca85767bca..c68bfef3d0 100644 --- a/packages/uploadthing/test/request-handler.test.ts +++ b/packages/uploadthing/test/request-handler.test.ts @@ -337,6 +337,7 @@ describe(".onUploadComplete()", () => { size: 48, type: "image/png", customId: null, + fileHash: "some-md5-hash", }), }); const signature = await Effect.runPromise( @@ -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`, + fileHash: "some-md5-hash", }, metadata: {}, }); @@ -383,6 +385,7 @@ describe(".onUploadComplete()", () => { size: 48, type: "image/png", customId: null, + fileHash: "some-md5-hash", }), }); @@ -415,6 +418,7 @@ describe(".onUploadComplete()", () => { size: 48, type: "image/png", customId: null, + fileHash: "some-md5-hash", }), }); const signature = await Effect.runPromise( diff --git a/packages/uploadthing/test/sdk.test.ts b/packages/uploadthing/test/sdk.test.ts index b74803d3e5..61edb8d555 100644 --- a/packages/uploadthing/test/sdk.test.ts +++ b/packages/uploadthing/test/sdk.test.ts @@ -54,6 +54,7 @@ describe("uploadFiles", () => { appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key}`, customId: null, type: "text/plain", + fileHash: expect.any(String), }, error: null, }); @@ -136,6 +137,7 @@ describe("uploadFilesFromUrl", () => { appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key}`, customId: null, type: "text/plain", + fileHash: expect.any(String), }, error: null, }); @@ -235,6 +237,7 @@ describe("uploadFilesFromUrl", () => { type: "text/plain", url: `${UTFS_IO_URL}/f/${key1}`, appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key1}`, + fileHash: expect.any(String), }, error: null, }, @@ -257,6 +260,7 @@ describe("uploadFilesFromUrl", () => { type: "text/plain", url: `${UTFS_IO_URL}/f/${key2}`, appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key2}`, + fileHash: expect.any(String), }, error: null, }, @@ -473,6 +477,7 @@ describe.runIf(shouldRun)( type: "text/plain", url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern(appId)), + fileHash: expect.any(String), }, error: null, }); @@ -508,6 +513,7 @@ describe.runIf(shouldRun)( type: "text/plain", url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern(appId)), + fileHash: expect.any(String), }, error: null, }); @@ -538,6 +544,7 @@ describe.runIf(shouldRun)( type: "image/vnd.microsoft.icon", url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern(appId)), + fileHash: expect.any(String), }, error: null, }); @@ -563,6 +570,7 @@ describe.runIf(shouldRun)( type: "text/plain", url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern(appId)), + fileHash: expect.any(String), }, error: null, }); @@ -605,6 +613,7 @@ describe.runIf(shouldRun)( type: "text/plain", url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern(appId)), + fileHash: expect.any(String), }, error: null, }); @@ -645,6 +654,7 @@ describe.runIf(shouldRun)( type: "text/plain", url: expect.stringMatching(fileUrlPattern), appUrl: expect.stringMatching(appUrlPattern(appId)), + fileHash: expect.any(String), }, error: null, });