Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions docs/src/app/(docs)/concepts/regions-acl/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,61 @@ you can select the region you want to upload your files to. Once changed, all
or via [email](mailto:ut-support@ping.gg).
</Note>

### Dynamic region selection (Private Beta)

UploadThing allows you to seamlessly select the region based on the user's
location. This is useful for applications that are global and want to upload
files to the region closest to the user for lower latencies.

This feature is currently in private beta and to enable it, reach out to us on
[support@uploadthing.com](mailto:support@uploadthing.com) if you're interested
in enrolling.

Once enabled, you can upload to any region you have enabled on your app in the
dashboard:

![Dynamic region selection](./multiregion.png)

Then, in your middleware, tag the upload's metadata with the
`experimental_UTRegion` key:

```ts
export const uploadRouter = {
anyPrivate: fileRoute({
blob: { maxFileSize: "256MB", maxFileCount: 10, acl: "public-read" },
})
.input(z.object({}))
.middleware(async (opts) => {
/**
* Simple example using continent code to select region.
*
* Europe and Africa will be routed to Frankfurt,
* Asia to Mumbai (India), and the rest to Virginia (US-East).
*
* You can do this selection however you like.
*/
const region =
(
{
AF: "fra1",
AN: "sea1",
AS: "bom1",
EU: "fra1",
NA: "sea1",
OC: "sea1",
SA: "sea1",
} as const
)[opts.req.headers.get("x-vercel-ip-continent")?.toUpperCase()] ??
"sea1"; // Fallback

return { [experimental_UTRegion]: region }; // <-- Tag the upload with the selected region
})
.onUploadComplete(async (opts) => {
console.log("Upload complete", opts.file);
}),
} satisfies FileRouter;
```

## Access Controls

By default every file uploaded to UploadThing is accessible simply by it's URL
Expand Down
15 changes: 0 additions & 15 deletions packages/react/coverage.json

This file was deleted.

9 changes: 7 additions & 2 deletions packages/uploadthing/src/_internal/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,14 @@ export const ApiUrl = Config.string("apiUrl").pipe(
Config.map((url) => url.href.replace(/\/$/, "")),
);

export const IngestUrl = Effect.gen(function* () {
export const IngestUrl = Effect.fn(function* (
preferredRegion: string | undefined,
) {
const { regions, ingestHost } = yield* UTToken;
const region = regions[0]; // Currently only support 1 region per app

const region = preferredRegion
? (regions.find((r) => r === preferredRegion) ?? regions[0])
: regions[0];

return yield* Config.string("ingestUrl").pipe(
Config.withDefault(`https://${region}.${ingestHost}`),
Expand Down
24 changes: 14 additions & 10 deletions packages/uploadthing/src/_internal/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
UploadedFileData,
UploadThingHook,
} from "./shared-schemas";
import { UTFiles } from "./types";
import { UTFiles, UTRegion } from "./types";
import type { AnyFileRoute, UTEvents } from "./types";

export class AdapterArguments extends Context.Tag(
Expand Down Expand Up @@ -329,6 +329,7 @@ const handleCallbackRequest = (opts: {
Schema.Struct({
status: Schema.String,
file: UploadedFileData,
origin: Schema.String,
metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
}),
);
Expand Down Expand Up @@ -379,14 +380,12 @@ const handleCallbackRequest = (opts: {
"'onUploadComplete' callback finished. Sending response to UploadThing:",
).pipe(Effect.annotateLogs("callbackData", payload));

const baseUrl = yield* IngestUrl;

const httpClient = (yield* HttpClient.HttpClient).pipe(
HttpClient.filterStatusOk,
);

yield* HttpClientRequest.post(`/callback-result`).pipe(
HttpClientRequest.prependUrl(baseUrl),
HttpClientRequest.prependUrl(requestInput.origin),
HttpClientRequest.setHeaders({
"x-uploadthing-api-key": Redacted.value(apiKey),
"x-uploadthing-version": pkgJson.version,
Expand Down Expand Up @@ -467,7 +466,11 @@ const runRouteMiddleware = (opts: {
}),
);

return { metadata, filesWithCustomIds };
return {
metadata,
filesWithCustomIds,
preferredRegion: metadata[UTRegion],
};
}).pipe(Effect.withLogSpan("runRouteMiddleware"));

const handleUploadAction = (opts: {
Expand Down Expand Up @@ -501,10 +504,11 @@ const handleUploadAction = (opts: {
Effect.annotateLogs("input", parsedInput),
);

const { metadata, filesWithCustomIds } = yield* runRouteMiddleware({
json: { input: parsedInput, files: json.files },
uploadable,
});
const { metadata, filesWithCustomIds, preferredRegion } =
yield* runRouteMiddleware({
json: { input: parsedInput, files: json.files },
uploadable,
});

yield* Effect.logDebug("Parsing route config").pipe(
Effect.annotateLogs("routerConfig", uploadable.routerConfig),
Expand Down Expand Up @@ -564,7 +568,7 @@ const handleUploadAction = (opts: {

const routeOptions = uploadable.routeOptions;
const { apiKey, appId } = yield* UTToken;
const ingestUrl = yield* IngestUrl;
const ingestUrl = yield* IngestUrl(preferredRegion);
const isDev = yield* IsDevelopment;

yield* Effect.logDebug("Generating presigned URLs").pipe(
Expand Down
18 changes: 18 additions & 0 deletions packages/uploadthing/src/_internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ import type {
UploadedFileData,
} from "./shared-schemas";

export type UTRegionAlias =
| "bom1"
| "icn1"
| "syd1"
| "can1"
| "fra1"
| "zrh1"
| "dub1"
| "cle1"
| "sfo1"
| "sea1";

/**
* Marker used to select the region based on the incoming request
*/
export const UTRegion = Symbol("uploadthing-region-symbol");

/**
* Marker used to append a `customId` to the incoming file data in `.middleware()`
* @example
Expand All @@ -40,6 +57,7 @@ export type UnsetMarker = "unsetMarker" & {
};

export type ValidMiddlewareObject = {
[UTRegion]?: UTRegionAlias;
[UTFiles]?: Partial<FileUploadDataWithCustomId>[];
[key: string]: unknown;
};
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/effect-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import type { CreateBuilderOptions } from "./_internal/upload-builder";
import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerConfig } from "./types";

export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export type { FileRouter };

type AdapterArgs = {
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import type { CreateBuilderOptions } from "./_internal/upload-builder";
import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export type { FileRouter };

type AdapterArgs = {
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import type { CreateBuilderOptions } from "./_internal/upload-builder";
import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export type { FileRouter };

type AdapterArgs = {
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import type { CreateBuilderOptions } from "./_internal/upload-builder";
import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export type { FileRouter };

type AdapterArgs = {
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/next-legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import type { CreateBuilderOptions } from "./_internal/upload-builder";
import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export type { FileRouter };

type AdapterArgs = {
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export type { FileRouter };
export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";

type AdapterArgs = {
req: NextRequest;
Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/remix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export type { FileRouter };
export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";

type AdapterArgs = {
event: ActionFunctionArgs;
Expand Down
2 changes: 1 addition & 1 deletion packages/uploadthing/src/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const generatePresignedUrl = (
) =>
Effect.gen(function* () {
const { apiKey, appId } = yield* UTToken;
const baseUrl = yield* IngestUrl;
const baseUrl = yield* IngestUrl(undefined);

const key = yield* generateKey(file, appId);

Expand Down
9 changes: 8 additions & 1 deletion packages/uploadthing/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import type { CreateBuilderOptions } from "./_internal/upload-builder";
import { createBuilder } from "./_internal/upload-builder";
import type { FileRouter, RouteHandlerOptions } from "./types";

export { UTFiles } from "./_internal/types";
export {
UTFiles,
/**
* This is an experimental feature.
* You need to be feature flagged on our backend to use this
*/
UTRegion as experimental_UTRegion,
} from "./_internal/types";
export { UTApi } from "./sdk";
export { UTFile } from "./sdk/ut-file";
export { UploadThingError, type FileRouter, makeAdapterHandler, createBuilder };
Expand Down
4 changes: 2 additions & 2 deletions packages/uploadthing/test/node/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ describe("ingest url infers correctly", () => {
);
process.env.UPLOADTHING_INGEST_URL = "http://localhost:1234";

const url = yield* IngestUrl.pipe(
const url = yield* IngestUrl(undefined).pipe(
Effect.provide(Layer.setConfigProvider(configProvider(null))),
Effect.exit,
);
Expand All @@ -263,7 +263,7 @@ describe("ingest url infers correctly", () => {
ParsedToken.make(app1TokenData),
);

const url = yield* IngestUrl.pipe(
const url = yield* IngestUrl(undefined).pipe(
Effect.provide(Layer.setConfigProvider(configProvider(null))),
Effect.exit,
);
Expand Down
1 change: 1 addition & 0 deletions packages/uploadthing/test/node/request-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ describe(".onUploadComplete()", () => {
const payload = JSON.stringify({
status: "uploaded",
metadata: {},
origin: "https://example.com",
file: new UploadedFileData({
url: `${UTFS_URL}/f/some-random-key.png`,
appUrl: `${UTFS_URL}/a/${testToken.decoded.appId}/f/some-random-key.png`,
Expand Down
20 changes: 18 additions & 2 deletions playground/app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from "zod";
import {
createRouteHandler,
createUploadthing,
experimental_UTRegion,
FileRouter,
} from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
Expand All @@ -15,7 +16,7 @@ const fileRoute = createUploadthing();

export const uploadRouter = {
anyPrivate: fileRoute({
blob: { maxFileSize: "256MB", maxFileCount: 10, acl: "private" },
blob: { maxFileSize: "256MB", maxFileCount: 10, acl: "public-read" },
})
.input(z.object({}))
.middleware(async (opts) => {
Expand All @@ -26,7 +27,22 @@ export const uploadRouter = {

console.log("middleware ::", session.sub, opts.input);

return {};
/**
* Example on how to select region based on user's location
*/
const region = (
{
AF: "fra1",
AN: "sea1",
AS: "bom1",
EU: "fra1",
NA: "sea1",
OC: "sea1",
SA: "sea1",
} as const
)[opts.req.headers.get("x-vercel-ip-continent")?.toUpperCase() ?? "US"]!;

return { [experimental_UTRegion]: region };
})
.onUploadComplete(async (opts) => {
console.log("Upload complete", opts.file);
Expand Down