diff --git a/docs/next.config.js b/docs/next.config.js index 6abdfa51ed..efd4f4e84a 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -19,8 +19,8 @@ const nextConfig = { images: { remotePatterns: [ { - hostname: "utfs.io", - pathname: "/a/s40vlb3kca/*", + hostname: "s40vlb3kca.ufs.sh", + pathname: "/f/*", }, ], }, diff --git a/docs/src/app/(api)/api/og/blog/route.tsx b/docs/src/app/(api)/api/og/blog/route.tsx index e25446512b..d255844674 100644 --- a/docs/src/app/(api)/api/og/blog/route.tsx +++ b/docs/src/app/(api)/api/og/blog/route.tsx @@ -24,7 +24,7 @@ export const GET = async (req: Request) => { tw="bg-zinc-900 h-full w-full flex flex-col p-14" style={{ backgroundImage: - "url(https://utfs.io/f/656e69ef-2800-45fd-87ac-b9f88346348c-hi270o.png)", + "url(https://s40vlb3kca.ufs.sh/f/656e69ef-2800-45fd-87ac-b9f88346348c-hi270o.png)", backgroundPosition: "cover", }} > diff --git a/docs/src/app/(api)/api/og/docs/route.tsx b/docs/src/app/(api)/api/og/docs/route.tsx index 29a99cd0f8..0bcd021337 100644 --- a/docs/src/app/(api)/api/og/docs/route.tsx +++ b/docs/src/app/(api)/api/og/docs/route.tsx @@ -24,7 +24,7 @@ export const GET = async (req: Request) => { tw="bg-zinc-900 h-full w-full flex flex-col p-14" style={{ backgroundImage: - "url(https://utfs.io/f/656e69ef-2800-45fd-87ac-b9f88346348c-hi270o.png)", + "url(https://s40vlb3kca.ufs.sh/f/656e69ef-2800-45fd-87ac-b9f88346348c-hi270o.png)", backgroundPosition: "cover", }} > diff --git a/docs/src/app/(docs)/concepts/regions-acl/page.mdx b/docs/src/app/(docs)/concepts/regions-acl/page.mdx index 20b312f569..d24a79ff07 100644 --- a/docs/src/app/(docs)/concepts/regions-acl/page.mdx +++ b/docs/src/app/(docs)/concepts/regions-acl/page.mdx @@ -50,8 +50,9 @@ you can select the region you want to upload your files to. Once changed, all ## Access Controls By default every file uploaded to UploadThing is accessible simply by it's URL -(`utfs.io/f/FILE_KEY`). Although this hard-to-guess URL is fine for many -applications, some applications require a more secure way to store their files. +(`.ufs.sh/f/`). Although this hard-to-guess URL is fine for +many applications, some applications require a more secure way to store their +files. You can configure your app's access control list (ACL) to restrict access to your files. UploadThing currently supports two different ACLs: diff --git a/docs/src/app/(docs)/working-with-files/page.mdx b/docs/src/app/(docs)/working-with-files/page.mdx index 06f71d43e0..8f2c37d709 100644 --- a/docs/src/app/(docs)/working-with-files/page.mdx +++ b/docs/src/app/(docs)/working-with-files/page.mdx @@ -12,29 +12,28 @@ export const metadata = docsMetadata({ After your files have been uploaded, you will most likely want to do something with them. This page shows how to work with your uploaded files. -## Accessing Files +## Accessing Public Files - - Do not use the raw file URL from the storage provider, e.g. `https://bucket.s3.region.amazonaws.com/`. We reserve the right to move objects between different storage providers and/or buckets, so this URL is not guaranteed to remain valid. - +UploadThing serves all files from a CDN at the following URL pattern: -There are multiple ways to access your files. The most generic way is to -construct the URL from the `fileKey` you get back after the file has been -uploaded: +`https://.ufs.sh/f/` -`https://utfs.io/f/` +If you set a `customId` when uploading the file, you can also use +`https://.ufs.sh/f/` to access it. -This URL will always work for public files and is the default URL returned by -the API and from any SDK method. However, sometimes you may want a URL that's -scoped to your application, for example when doing image optimizations and want -to filter what URLs are allowed to be optimized on your server. For this, the -following URL can be used: + + Do not use the raw file URL from the storage provider, e.g. `https://bucket.s3.region.amazonaws.com/`. We reserve the right to move objects between different storage providers and/or buckets, so this URL is not guaranteed to remain valid. + -`https://utfs.io/a//` + + Previously, all files were served from `https://utfs.io/f/`. + This is still supported, but not recommended and may be deprecated in the future. + -By using this URL pattern, you have more granular control over what URLs are -allowed to be optimized. Below is an example of how to setup image optimization -allow filtering in Next.js: +Given that all files are served from a subdomain of your app, you have granular +control over what URLs are allowed to be processed. Below is an example of how +to setup image optimization allow filtering in Next.js that only allows +optimizing images from your app: ```js /** @type {import('next').NextConfig} */ @@ -43,18 +42,14 @@ export default { remotePatterns: [ { protocol: "https", - hostname: "utfs.io", - pathname: "/a//*", + hostname: ".ufs.sh", + pathname: "/f/*", }, ], }, }; ``` - - If you set a `customId` when uploading the file, you can also use `https://utfs.io/a//`. - - ## Accessing Private Files If your files are protected with diff --git a/examples/minimal-expo/.env.example b/examples/minimal-expo/.env.example index 87877a7ff0..ae45d19e66 100644 --- a/examples/minimal-expo/.env.example +++ b/examples/minimal-expo/.env.example @@ -1,2 +1,3 @@ # Go to https://uploadthing.com/dashboard to get your token -UPLOADTHING_TOKEN='...' \ No newline at end of file +UPLOADTHING_TOKEN='...' +EXPO_PUBLIC_UPLOADTHING_APP_ID='...' \ No newline at end of file diff --git a/examples/minimal-expo/app/f/[key].tsx b/examples/minimal-expo/app/f/[key].tsx index 0d68983450..1d45d4519b 100644 --- a/examples/minimal-expo/app/f/[key].tsx +++ b/examples/minimal-expo/app/f/[key].tsx @@ -10,7 +10,7 @@ import { isImage } from "~/lib/utils"; export default function FileScreen() { const searchParams = useGlobalSearchParams<{ key: string; name?: string }>(); const { key, name = "Untitled" } = searchParams; - const fileUrl = `https://utfs.io/f/${key}`; + const fileUrl = `https://${process.env.EXPO_PUBLIC_UPLOADTHING_APP_ID}.ufs.sh/f/${key}`; return ( <> diff --git a/packages/react/test/upload-button.browser.test.tsx b/packages/react/test/upload-button.browser.test.tsx index 884f57422d..8ed4cf7594 100644 --- a/packages/react/test/upload-button.browser.test.tsx +++ b/packages/react/test/upload-button.browser.test.tsx @@ -47,7 +47,7 @@ const worker = setupWorker( http.all<{ key: string }>( "https://fra1.ingest.uploadthing.com/:key", ({ params }) => { - return HttpResponse.json({ url: "https://utfs.io/f/" + params.key }); + return HttpResponse.json({ url: "https://app-1.ufs.sh/f/" + params.key }); }, ), ); diff --git a/packages/react/test/upload-dropzone.browser.test.tsx b/packages/react/test/upload-dropzone.browser.test.tsx index 9305061b82..78d6b06e7b 100644 --- a/packages/react/test/upload-dropzone.browser.test.tsx +++ b/packages/react/test/upload-dropzone.browser.test.tsx @@ -43,7 +43,7 @@ const worker = setupWorker( http.all<{ key: string }>( "https://fra1.ingest.uploadthing.com/:key", ({ params }) => { - return HttpResponse.json({ url: "https://utfs.io/f/" + params.key }); + return HttpResponse.json({ url: "https://app-1.ufs.sh/f/" + params.key }); }, ), ); diff --git a/packages/uploadthing/src/_internal/shared-schemas.ts b/packages/uploadthing/src/_internal/shared-schemas.ts index 25bafdb50b..2ec5ebab61 100644 --- a/packages/uploadthing/src/_internal/shared-schemas.ts +++ b/packages/uploadthing/src/_internal/shared-schemas.ts @@ -78,6 +78,11 @@ export class UploadedFileData extends FileUploadDataWithCustomId.extend = { url: string; + /** + * @deprecated + * This field is now an alias for `url`. + * This field will be removed in uploadthing v9. + */ appUrl: string; fileHash: string; serverData: TServerOutput; diff --git a/packages/uploadthing/test/__test-helpers.ts b/packages/uploadthing/test/__test-helpers.ts index d9d65d7f54..f018623b6b 100644 --- a/packages/uploadthing/test/__test-helpers.ts +++ b/packages/uploadthing/test/__test-helpers.ts @@ -31,18 +31,17 @@ export const API_URL = typeof process !== "undefined" && process.env.UPLOADTHING_API_URL ? process.env.UPLOADTHING_API_URL : "https://api.uploadthing.com"; -export const UTFS_IO_URL = +export const UFS_HOST = typeof process !== "undefined" && process.env.UPLOADTHING_API_URL - ? "https://staging.utfs.io" - : "https://utfs.io"; + ? "utf-staging.com" + : "ufs.sh"; export const INGEST_URL = typeof process !== "undefined" && process.env.UPLOADTHING_API_URL ? "https://fra1.ingest.ut-staging.com" : "https://fra1.ingest.uploadthing.com"; -export const fileUrlPattern = new RegExp(`^${UTFS_IO_URL}/f/.+$`); -export const appUrlPattern = (appId = testToken.decoded.appId) => - new RegExp(`^${UTFS_IO_URL}/a/${appId}/.+$`); +export const fileUrlPattern = (appId = testToken.decoded.appId) => + new RegExp(`^https://${appId}.${UFS_HOST}/f/.+$`); export const createApiUrl = (slug: string, action?: typeof ActionType.Type) => { const url = new URL("http://localhost:3000"); @@ -86,7 +85,7 @@ export const handlers = [ await callRequestSpy(request); return HttpResponse.text("Lorem ipsum doler sit amet"); }), - http.get(`${UTFS_IO_URL}/f/:key`, async ({ request }) => { + http.get(`https://(.+).${UFS_HOST}/f/:key`, async ({ request }) => { await callRequestSpy(request); return HttpResponse.text("Lorem ipsum doler sit amet"); }), @@ -99,8 +98,8 @@ export const handlers = [ await callRequestSpy(request); const appId = new URLSearchParams(request.url).get("x-ut-identifier"); return HttpResponse.json({ - url: `${UTFS_IO_URL}/f/${params.key}`, - appUrl: `${UTFS_IO_URL}/a/${appId}/${params.key}`, + url: `https://${appId}.${UFS_HOST}/f/${params.key}`, + appUrl: `https://${appId}.${UFS_HOST}/f/${params.key}`, serverData: null, fileHash: Array.from(new Uint8Array(await request.arrayBuffer())) .map((b) => b.toString(16).padStart(2, "0")) @@ -114,7 +113,7 @@ export const handlers = [ http.post(`${API_URL}/v6/requestFileAccess`, async ({ request }) => { await callRequestSpy(request); return HttpResponse.json({ - url: `${UTFS_IO_URL}/f/someFileKey?x-some-amz=query-param`, + url: `https://{APP_ID}.${UFS_HOST}/f/someFileKey?x-some-amz=query-param`, }); }), http.post(`${API_URL}/v6/updateACL`, async ({ request }) => { diff --git a/packages/uploadthing/test/client.browser.test.ts b/packages/uploadthing/test/client.browser.test.ts index ac52d3c8a2..9f14b3e6e3 100644 --- a/packages/uploadthing/test/client.browser.test.ts +++ b/packages/uploadthing/test/client.browser.test.ts @@ -17,7 +17,6 @@ import type { GenerateUploaderOptions, } from "../src/types"; import { - appUrlPattern, doNotExecute, fileUrlPattern, handlers, @@ -141,8 +140,8 @@ describe("uploadFiles", () => { serverData: null, lastModified: expect.any(Number), key: expect.stringMatching(/.+/), - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern()), + url: expect.stringMatching(fileUrlPattern()), + appUrl: expect.stringMatching(fileUrlPattern()), fileHash: expect.any(String), }, ]); @@ -185,8 +184,8 @@ describe("uploadFiles", () => { serverData: null, lastModified: expect.any(Number), key: expect.stringMatching(/.+/), - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern()), + url: expect.stringMatching(fileUrlPattern()), + appUrl: expect.stringMatching(fileUrlPattern()), fileHash: expect.any(String), }, ]); @@ -220,8 +219,8 @@ describe("uploadFiles", () => { serverData: null, lastModified: expect.any(Number), key: expect.stringMatching(/.+/), - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern()), + url: expect.stringMatching(fileUrlPattern()), + appUrl: expect.stringMatching(fileUrlPattern()), fileHash: expect.any(String), }, ]); @@ -268,7 +267,7 @@ describe("uploadFiles", () => { // customId: null, // serverData: null, // key: "abc-123.txt", - // url: "https://utfs.io/f/abc-123.txt", + // url: "https://app-1.ufs.sh/f/abc-123.txt", // }, // ]); // expect(onErrorMock).not.toHaveBeenCalled(); diff --git a/packages/uploadthing/test/request-handler.test.ts b/packages/uploadthing/test/request-handler.test.ts index c051633e2b..3309df6208 100644 --- a/packages/uploadthing/test/request-handler.test.ts +++ b/packages/uploadthing/test/request-handler.test.ts @@ -13,6 +13,7 @@ import { middlewareMock, requestSpy, testToken, + UFS_HOST, uploadCompleteMock, } from "./__test-helpers"; @@ -409,8 +410,8 @@ describe(".onUploadComplete()", () => { status: "uploaded", metadata: {}, file: new UploadedFileData({ - url: "https://utfs.io/f/some-random-key.png", - appUrl: `https://utfs.io/a/${testToken.decoded.appId}/some-random-key.png`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, name: "foo.png", key: "some-random-key.png", size: 48, @@ -446,8 +447,8 @@ describe(".onUploadComplete()", () => { name: "foo.png", size: 48, type: "image/png", - url: "https://utfs.io/f/some-random-key.png", - appUrl: `https://utfs.io/a/${testToken.decoded.appId}/some-random-key.png`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, fileHash: "some-md5-hash", }, metadata: {}, @@ -459,8 +460,8 @@ describe(".onUploadComplete()", () => { status: "uploaded", metadata: {}, file: new UploadedFileData({ - url: "https://utfs.io/f/some-random-key.png", - appUrl: `https://utfs.io/a/${testToken.decoded.appId}/some-random-key.png`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, name: "foo.png", key: "some-random-key.png", size: 48, @@ -492,8 +493,8 @@ describe(".onUploadComplete()", () => { status: "uploaded", metadata: {}, file: new UploadedFileData({ - url: "https://utfs.io/f/some-random-key.png", - appUrl: `https://utfs.io/a/${testToken.decoded.appId}/some-random-key.png`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/some-random-key.png`, name: "foo.png", key: "some-random-key.png", size: 48, diff --git a/packages/uploadthing/test/sdk.live.test.ts b/packages/uploadthing/test/sdk.live.test.ts index 1188ce0c0f..51cf0955e3 100644 --- a/packages/uploadthing/test/sdk.live.test.ts +++ b/packages/uploadthing/test/sdk.live.test.ts @@ -4,12 +4,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { UTApi, UTFile } from "../src/sdk"; import { UploadThingToken } from "../src/types"; -import { - appUrlPattern, - fileUrlPattern, - testToken, - UTFS_IO_URL, -} from "./__test-helpers"; +import { fileUrlPattern, testToken } from "./__test-helpers"; const shouldRun = typeof process.env.UPLOADTHING_TEST_TOKEN === "string" && @@ -69,8 +64,8 @@ describe.runIf(shouldRun)( name: "foo.txt", size: 3, type: "text/plain", - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern(appId)), + url: expect.stringMatching(fileUrlPattern(appId)), + appUrl: expect.stringMatching(fileUrlPattern(appId)), fileHash: expect.any(String), }, error: null, @@ -104,8 +99,8 @@ describe.runIf(shouldRun)( name: "foo.txt", size: 3, type: "text/plain", - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern(appId)), + url: expect.stringMatching(fileUrlPattern(appId)), + appUrl: expect.stringMatching(fileUrlPattern(appId)), fileHash: expect.any(String), }, error: null, @@ -134,8 +129,8 @@ describe.runIf(shouldRun)( name: "favicon.ico", size: expect.any(Number), type: "image/vnd.microsoft.icon", - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern(appId)), + url: expect.stringMatching(fileUrlPattern(appId)), + appUrl: expect.stringMatching(fileUrlPattern(appId)), fileHash: expect.any(String), }, error: null, @@ -158,8 +153,8 @@ describe.runIf(shouldRun)( name: "bar.txt", size: 3, type: "text/plain", - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern(appId)), + url: expect.stringMatching(fileUrlPattern(appId)), + appUrl: expect.stringMatching(fileUrlPattern(appId)), fileHash: expect.any(String), }, error: null, @@ -200,8 +195,8 @@ describe.runIf(shouldRun)( name: "bar.txt", size: 3, type: "text/plain", - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern(appId)), + url: expect.stringMatching(fileUrlPattern(appId)), + appUrl: expect.stringMatching(fileUrlPattern(appId)), fileHash: expect.any(String), }, error: null, @@ -241,8 +236,8 @@ describe.runIf(shouldRun)( name: "foo.txt", size: 3, type: "text/plain", - url: expect.stringMatching(fileUrlPattern), - appUrl: expect.stringMatching(appUrlPattern(appId)), + url: expect.stringMatching(fileUrlPattern(appId)), + appUrl: expect.stringMatching(fileUrlPattern(appId)), fileHash: expect.any(String), }, error: null, @@ -264,16 +259,13 @@ describe.runIf(shouldRun)( const { files } = await utapi.listFiles(); const someFile = files[0]!; - const response = await fetch(`${UTFS_IO_URL}/f/${someFile.key}`); - const size = Number(response.headers.get("Content-Length")); - const result = await utapi.deleteFiles(someFile.key); expect(result).toEqual({ deletedCount: 1, success: true, }); - localInfo.totalBytes -= size; + localInfo.totalBytes -= someFile.size; localInfo.filesUploaded--; }); diff --git a/packages/uploadthing/test/sdk.test.ts b/packages/uploadthing/test/sdk.test.ts index 005e958a4e..f270f6d143 100644 --- a/packages/uploadthing/test/sdk.test.ts +++ b/packages/uploadthing/test/sdk.test.ts @@ -17,7 +17,7 @@ import { INGEST_URL, requestSpy, testToken, - UTFS_IO_URL, + UFS_HOST, } from "./__test-helpers"; const msw = setupServer(...handlers); @@ -50,8 +50,8 @@ describe("uploadFiles", () => { name: "foo.txt", size: 3, lastModified: fooFile.lastModified, - url: `${UTFS_IO_URL}/f/${key}`, - appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key}`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key}`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key}`, customId: null, type: "text/plain", fileHash: expect.any(String), @@ -126,8 +126,8 @@ describe("uploadFilesFromUrl", () => { name: "foo.txt", size: 26, lastModified: expect.any(Number), - url: `${UTFS_IO_URL}/f/${key}`, - appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key}`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key}`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key}`, customId: null, type: "text/plain", fileHash: expect.any(String), @@ -260,8 +260,8 @@ describe("uploadFilesFromUrl", () => { name: "exists.txt", size: 26, type: "text/plain", - url: `${UTFS_IO_URL}/f/${key1}`, - appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key1}`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key1}`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key1}`, }, error: null, }, @@ -298,8 +298,8 @@ describe("uploadFilesFromUrl", () => { name: "foo.txt", size: 26, type: "text/plain", - url: `${UTFS_IO_URL}/f/${key1}`, - appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key1}`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key1}`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key1}`, fileHash: expect.any(String), }, error: null, @@ -321,8 +321,8 @@ describe("uploadFilesFromUrl", () => { name: "bar.txt", size: 26, type: "text/plain", - url: `${UTFS_IO_URL}/f/${key2}`, - appUrl: `${UTFS_IO_URL}/a/${testToken.decoded.appId}/${key2}`, + url: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key2}`, + appUrl: `https://${testToken.decoded.appId}.${UFS_HOST}/f/${key2}`, fileHash: expect.any(String), }, error: null, diff --git a/playground/middleware.ts b/playground/middleware.ts index d777adaa93..9354e4974c 100644 --- a/playground/middleware.ts +++ b/playground/middleware.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { MiddlewareConfig, NextResponse, @@ -8,10 +9,11 @@ import { getSession } from "./lib/data"; export default (async (req) => { if (req.nextUrl.pathname !== "/") { - const sesh = await getSession(); - if (!sesh) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } + // TOGGLE TO MAKE CALLBACK NOT REACH + // const sesh = await getSession(); + // if (!sesh) { + // return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + // } } return NextResponse.next();