uploadthing
Version:
Learn more: [docs.uploadthing.com](https://docs.uploadthing.com)
412 lines (406 loc) • 18.2 kB
JavaScript
import { version } from "../dist/package-Beb-iarE.js";
import { logDeprecationWarning } from "../dist/deprecations-pLmw6Ytd.js";
import "../dist/shared-schemas-BIFDoqPF.js";
import { ApiUrl, IngestUrl, UTFiles$1 as UTFiles, UTRegion$1 as UTRegion, UTToken, UfsAppIdLocation, UfsHost, createBuilder, extractRouterConfig, logHttpClientError, logHttpClientResponse, makeAdapterHandler, makeRuntime } from "../dist/upload-builder-e0_p9NOT.js";
import * as Arr from "effect/Array";
import { UploadThingError, UploadThingError as UploadThingError$1, generateKey, generateSignedURL, parseTimeToSeconds } from "@uploadthing/shared";
import { unsafeCoerce } from "effect/Function";
import * as Predicate from "effect/Predicate";
import * as Effect$3 from "effect/Effect";
import * as Effect$2 from "effect/Effect";
import * as Effect$1 from "effect/Effect";
import * as Effect from "effect/Effect";
import * as HttpClient$2 from "@effect/platform/HttpClient";
import * as HttpClient$1 from "@effect/platform/HttpClient";
import * as HttpClient from "@effect/platform/HttpClient";
import * as HttpClientRequest$2 from "@effect/platform/HttpClientRequest";
import * as HttpClientRequest$1 from "@effect/platform/HttpClientRequest";
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
import * as Redacted from "effect/Redacted";
import * as S from "effect/Schema";
import * as Cause from "effect/Cause";
import { lookup } from "@uploadthing/mime-types";
//#region src/sdk/ut-file.ts
/**
* Extension of the Blob class that simplifies setting the `name` and `customId` properties,
* similar to the built-in File class from Node > 20.
*/
var UTFile = class extends Blob {
name;
lastModified;
customId;
constructor(parts, name, options) {
const optionsWithDefaults = {
...options,
type: options?.type ?? (lookup(name) || "application/octet-stream"),
lastModified: options?.lastModified ?? Date.now()
};
super(parts, optionsWithDefaults);
this.name = name;
this.customId = optionsWithDefaults.customId;
this.lastModified = optionsWithDefaults.lastModified;
}
};
//#endregion
//#region src/_internal/upload-server.ts
const uploadWithoutProgress = (file, presigned) => Effect$3.gen(function* () {
const formData = new FormData();
formData.append("file", file);
const httpClient = (yield* HttpClient$2.HttpClient).pipe(HttpClient$2.filterStatusOk);
const json = yield* HttpClientRequest$2.put(presigned.url).pipe(HttpClientRequest$2.bodyFormData(formData), HttpClientRequest$2.setHeader("Range", "bytes=0-"), HttpClientRequest$2.setHeader("x-uploadthing-version", version), httpClient.execute, Effect$3.tapError(logHttpClientError("Failed to upload file")), Effect$3.mapError((e) => new UploadThingError$1({
code: "UPLOAD_FAILED",
message: "Failed to upload file",
cause: e
})), Effect$3.andThen((_) => _.json), Effect$3.andThen(unsafeCoerce), Effect$3.scoped);
yield* Effect$3.logDebug(`File ${file.name} uploaded successfully`).pipe(Effect$3.annotateLogs("json", json));
return {
...json,
get url() {
logDeprecationWarning("`file.url` is deprecated and will be removed in uploadthing v9. Use `file.ufsUrl` instead.");
return json.url;
},
get appUrl() {
logDeprecationWarning("`file.appUrl` is deprecated and will be removed in uploadthing v9. Use `file.ufsUrl` instead.");
return json.appUrl;
}
};
});
//#endregion
//#region src/sdk/utils.ts
function guardServerOnly() {
if (typeof window !== "undefined") throw new UploadThingError$1({
code: "INTERNAL_SERVER_ERROR",
message: "The `utapi` can only be used on the server."
});
}
const downloadFile = (_url) => Effect$2.gen(function* () {
let url = Predicate.isRecord(_url) ? _url.url : _url;
if (typeof url === "string") {
if (url.startsWith("data:")) return yield* Effect$2.fail({
code: "BAD_REQUEST",
message: "Please use uploadFiles() for data URLs. uploadFilesFromUrl() is intended for use with remote URLs only.",
data: void 0
});
}
url = new URL(url);
const { name = url.pathname.split("/").pop() ?? "unknown-filename", customId = void 0 } = Predicate.isRecord(_url) ? _url : {};
const httpClient = (yield* HttpClient$1.HttpClient).pipe(HttpClient$1.filterStatusOk);
const arrayBuffer = yield* HttpClientRequest$1.get(url).pipe(HttpClientRequest$1.modify({ headers: {} }), httpClient.execute, Effect$2.flatMap((_) => _.arrayBuffer), Effect$2.mapError((cause) => {
return {
code: "BAD_REQUEST",
message: `Failed to download requested file: ${cause.message}`,
data: cause.toJSON()
};
}), Effect$2.scoped);
return new UTFile([arrayBuffer], name, {
customId,
lastModified: Date.now()
});
}).pipe(Effect$2.withLogSpan("downloadFile"));
const generatePresignedUrl = (file, cd, acl) => Effect$2.gen(function* () {
const { apiKey, appId } = yield* UTToken;
const baseUrl = yield* IngestUrl(void 0);
const key = yield* generateKey(file, appId);
const url = yield* generateSignedURL(`${baseUrl}/${key}`, apiKey, { data: {
"x-ut-identifier": appId,
"x-ut-file-name": file.name,
"x-ut-file-size": file.size,
"x-ut-file-type": file.type,
"x-ut-custom-id": file.customId,
"x-ut-content-disposition": cd,
"x-ut-acl": acl
} });
return {
url,
key
};
}).pipe(Effect$2.withLogSpan("generatePresignedUrl"));
const uploadFile = (file, opts) => Effect$2.gen(function* () {
const presigned = yield* generatePresignedUrl(file, opts.contentDisposition ?? "inline", opts.acl).pipe(Effect$2.catchTag("UploadThingError", (e) => Effect$2.fail(UploadThingError$1.toObject(e))), Effect$2.catchTag("ConfigError", () => Effect$2.fail({
code: "INVALID_SERVER_CONFIG",
message: "Failed to generate presigned URL"
})));
const response = yield* uploadWithoutProgress(file, presigned).pipe(Effect$2.catchTag("UploadThingError", (e) => Effect$2.fail(UploadThingError$1.toObject(e))), Effect$2.catchTag("ResponseError", (e) => Effect$2.fail({
code: "UPLOAD_FAILED",
message: "Failed to upload file",
data: e.toJSON()
})));
return {
key: presigned.key,
url: response.url,
appUrl: response.appUrl,
ufsUrl: response.ufsUrl,
lastModified: file.lastModified ?? Date.now(),
name: file.name,
size: file.size,
type: file.type,
customId: file.customId ?? null,
fileHash: response.fileHash
};
}).pipe(Effect$2.withLogSpan("uploadFile"));
//#endregion
//#region src/sdk/index.ts
var UTApi = class {
fetch;
defaultKeyType;
runtime;
opts;
constructor(options) {
guardServerOnly();
this.opts = options ?? {};
this.fetch = this.opts.fetch ?? globalThis.fetch;
this.defaultKeyType = this.opts.defaultKeyType ?? "fileKey";
this.runtime = makeRuntime(this.fetch, this.opts);
}
requestUploadThing = (pathname, body, responseSchema) => Effect$1.gen(this, function* () {
const { apiKey } = yield* UTToken;
const baseUrl = yield* ApiUrl;
const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
return yield* HttpClientRequest.post(pathname).pipe(HttpClientRequest.prependUrl(baseUrl), HttpClientRequest.bodyUnsafeJson(body), HttpClientRequest.setHeaders({
"x-uploadthing-version": version,
"x-uploadthing-be-adapter": "server-sdk",
"x-uploadthing-api-key": Redacted.value(apiKey)
}), httpClient.execute, Effect$1.tapBoth({
onSuccess: logHttpClientResponse("UploadThing API Response"),
onFailure: logHttpClientError("Failed to request UploadThing API")
}), Effect$1.flatMap(HttpClientResponse.schemaBodyJson(responseSchema)), Effect$1.scoped);
}).pipe(Effect$1.catchTag("ConfigError", (e) => new UploadThingError$1({
code: "INVALID_SERVER_CONFIG",
message: "There was an error with the server configuration. More info can be found on this error's `cause` property",
cause: e
})), Effect$1.withLogSpan("utapi.#requestUploadThing"));
executeAsync = async (program, signal) => {
const exit = await program.pipe(Effect$1.withLogSpan("utapi.#executeAsync"), (e) => this.runtime.runPromiseExit(e, signal ? { signal } : void 0));
if (exit._tag === "Failure") throw Cause.squash(exit.cause);
return exit.value;
};
uploadFiles(files, opts) {
guardServerOnly();
const concurrency = opts?.concurrency ?? 1;
if (concurrency < 1 || concurrency > 25) throw new UploadThingError$1({
code: "BAD_REQUEST",
message: "concurrency must be a positive integer between 1 and 25"
});
const program = Effect$1.forEach(Arr.ensure(files), (file) => uploadFile(file, opts ?? {}).pipe(Effect$1.match({
onSuccess: (data) => ({
data,
error: null
}),
onFailure: (error) => ({
data: null,
error
})
})), { concurrency }).pipe(Effect$1.map((ups) => Array.isArray(files) ? ups : ups[0]), Effect$1.tap((res) => Effect$1.logDebug("Finished uploading").pipe(Effect$1.annotateLogs("uploadResult", res))), Effect$1.withLogSpan("uploadFiles"));
return this.executeAsync(program, opts?.signal);
}
uploadFilesFromUrl(urls, opts) {
guardServerOnly();
const concurrency = opts?.concurrency ?? 1;
if (concurrency < 1 || concurrency > 25) throw new UploadThingError$1({
code: "BAD_REQUEST",
message: "concurrency must be a positive integer between 1 and 25"
});
const program = Effect$1.forEach(Arr.ensure(urls), (url) => downloadFile(url).pipe(Effect$1.flatMap((file) => uploadFile(file, opts ?? {})), Effect$1.match({
onSuccess: (data) => ({
data,
error: null
}),
onFailure: (error) => ({
data: null,
error
})
})), { concurrency }).pipe(Effect$1.map((ups) => Array.isArray(urls) ? ups : ups[0]), Effect$1.tap((res) => Effect$1.logDebug("Finished uploading").pipe(Effect$1.annotateLogs("uploadResult", res))), Effect$1.withLogSpan("uploadFiles")).pipe(Effect$1.withLogSpan("uploadFilesFromUrl"));
return this.executeAsync(program, opts?.signal);
}
/**
* Request to delete files from UploadThing storage.
* @param {string | string[]} fileKeys
*
* @example
* await deleteFiles("2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg");
*
* @example
* await deleteFiles(["2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg","1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg"])
*
* @example
* await deleteFiles("myCustomIdentifier", { keyType: "customId" })
*/
deleteFiles = async (keys, opts) => {
guardServerOnly();
const { keyType = this.defaultKeyType } = opts ?? {};
class DeleteFileResponse extends S.Class("DeleteFileResponse")({
success: S.Boolean,
deletedCount: S.Number
}) {}
return await this.executeAsync(this.requestUploadThing("/v6/deleteFiles", keyType === "fileKey" ? { fileKeys: Arr.ensure(keys) } : { customIds: Arr.ensure(keys) }, DeleteFileResponse).pipe(Effect$1.withLogSpan("deleteFiles")));
};
/**
* Request file URLs from UploadThing storage.
* @param {string | string[]} fileKeys
*
* @example
* const data = await getFileUrls("2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg");
* console.log(data); // [{key: "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg", url: "https://uploadthing.com/f/2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg"}]
*
* @example
* const data = await getFileUrls(["2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg","1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg"])
* console.log(data) // [{key: "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg", url: "https://uploadthing.com/f/2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg" },{key: "1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg", url: "https://uploadthing.com/f/1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg"}]
*
* @deprecated - See https://docs.uploadthing.com/working-with-files#accessing-files for info how to access files
*/
getFileUrls = async (keys, opts) => {
guardServerOnly();
const { keyType = this.defaultKeyType } = opts ?? {};
class GetFileUrlResponse extends S.Class("GetFileUrlResponse")({ data: S.Array(S.Struct({
key: S.String,
url: S.String
})) }) {}
return await this.executeAsync(this.requestUploadThing("/v6/getFileUrl", keyType === "fileKey" ? { fileKeys: Arr.ensure(keys) } : { customIds: Arr.ensure(keys) }, GetFileUrlResponse).pipe(Effect$1.withLogSpan("getFileUrls")));
};
/**
* Request file list from UploadThing storage.
* @param {object} opts
* @param {number} opts.limit The maximum number of files to return
* @param {number} opts.offset The number of files to skip
*
* @example
* const data = await listFiles({ limit: 1 });
* console.log(data); // { key: "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg", id: "2e0fdb64-9957-4262-8e45-f372ba903ac8" }
*/
listFiles = async (opts) => {
guardServerOnly();
class ListFileResponse extends S.Class("ListFileResponse")({
hasMore: S.Boolean,
files: S.Array(S.Struct({
id: S.String,
customId: S.NullOr(S.String),
key: S.String,
name: S.String,
size: S.Number,
status: S.Literal("Deletion Pending", "Failed", "Uploaded", "Uploading"),
uploadedAt: S.Number
}))
}) {}
return await this.executeAsync(this.requestUploadThing("/v6/listFiles", { ...opts }, ListFileResponse).pipe(Effect$1.withLogSpan("listFiles")));
};
renameFiles = async (updates) => {
guardServerOnly();
class RenameFileResponse extends S.Class("RenameFileResponse")({ success: S.Boolean }) {}
return await this.executeAsync(this.requestUploadThing("/v6/renameFiles", { updates: Arr.ensure(updates) }, RenameFileResponse).pipe(Effect$1.withLogSpan("renameFiles")));
};
getUsageInfo = async () => {
guardServerOnly();
class GetUsageInfoResponse extends S.Class("GetUsageInfoResponse")({
totalBytes: S.Number,
appTotalBytes: S.Number,
filesUploaded: S.Number,
limitBytes: S.Number
}) {}
return await this.executeAsync(this.requestUploadThing("/v6/getUsageInfo", {}, GetUsageInfoResponse).pipe(Effect$1.withLogSpan("getUsageInfo")));
};
/**
* Generate a presigned url for a private file
* Unlike {@link getSignedURL}, this method does not make a fetch request to the UploadThing API
* and is the recommended way to generate a presigned url for a private file.
**/
generateSignedURL = async (key, opts) => {
guardServerOnly();
const expiresIn = parseTimeToSeconds(opts?.expiresIn ?? "5 minutes");
if (opts?.expiresIn && isNaN(expiresIn)) throw new UploadThingError$1({
code: "BAD_REQUEST",
message: "expiresIn must be a valid time string, for example '1d', '2 days', or a number of seconds."
});
if (expiresIn > 86400 * 7) throw new UploadThingError$1({
code: "BAD_REQUEST",
message: "expiresIn must be less than 7 days (604800 seconds)."
});
const program = Effect$1.gen(function* () {
const { apiKey, appId } = yield* UTToken;
const appIdLocation = yield* UfsAppIdLocation;
const ufsHost = yield* UfsHost;
const proto = ufsHost.includes("local") ? "http" : "https";
const urlBase = appIdLocation === "subdomain" ? `${proto}://${appId}.${ufsHost}/f/${key}` : `${proto}://${ufsHost}/a/${appId}/${key}`;
const ufsUrl = yield* generateSignedURL(urlBase, apiKey, { ttlInSeconds: expiresIn });
return { ufsUrl };
});
return await this.executeAsync(program.pipe(Effect$1.catchTag("ConfigError", (e) => new UploadThingError$1({
code: "INVALID_SERVER_CONFIG",
message: "There was an error with the server configuration. More info can be found on this error's `cause` property",
cause: e
})), Effect$1.withLogSpan("generateSignedURL")));
};
/**
* Request a presigned url for a private file(s)
* @remarks This method is no longer recommended as it makes a fetch
* request to the UploadThing API which incurs redundant latency. It
* will be deprecated in UploadThing v8 and removed in UploadThing v9.
*
* @see {@link generateSignedURL} for a more efficient way to generate a presigned url
**/
getSignedURL = async (key, opts) => {
guardServerOnly();
const expiresIn = opts?.expiresIn ? parseTimeToSeconds(opts.expiresIn) : void 0;
const { keyType = this.defaultKeyType } = opts ?? {};
if (opts?.expiresIn && isNaN(expiresIn)) throw new UploadThingError$1({
code: "BAD_REQUEST",
message: "expiresIn must be a valid time string, for example '1d', '2 days', or a number of seconds."
});
if (expiresIn && expiresIn > 86400 * 7) throw new UploadThingError$1({
code: "BAD_REQUEST",
message: "expiresIn must be less than 7 days (604800 seconds)."
});
class GetSignedUrlResponse extends S.Class("GetSignedUrlResponse")({
url: S.String,
ufsUrl: S.String
}) {}
return await this.executeAsync(this.requestUploadThing("/v6/requestFileAccess", keyType === "fileKey" ? {
fileKey: key,
expiresIn
} : {
customId: key,
expiresIn
}, GetSignedUrlResponse).pipe(Effect$1.withLogSpan("getSignedURL")));
};
/**
* Update the ACL of a file or set of files.
*
* @example
* // Make a single file public
* await utapi.updateACL("2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg", "public-read");
*
* // Make multiple files private
* await utapi.updateACL(
* [
* "2e0fdb64-9957-4262-8e45-f372ba903ac8_image.jpg",
* "1649353b-04ea-48a2-9db7-31de7f562c8d_image2.jpg",
* ],
* "private",
* );
*/
updateACL = async (keys, acl, opts) => {
guardServerOnly();
const { keyType = this.defaultKeyType } = opts ?? {};
const updates = Arr.ensure(keys).map((key) => {
return keyType === "fileKey" ? {
fileKey: key,
acl
} : {
customId: key,
acl
};
});
const responseSchema = S.Struct({ success: S.Boolean });
return await this.executeAsync(this.requestUploadThing("/v6/updateACL", { updates }, responseSchema).pipe(Effect$1.withLogSpan("updateACL")));
};
};
//#endregion
//#region src/server.ts
const createUploadthing = (opts) => createBuilder(opts);
const createRouteHandler = (opts) => {
return makeAdapterHandler((ev) => Effect.succeed({ req: "request" in ev ? ev.request : ev }), (ev) => Effect.succeed("request" in ev ? ev.request : ev), opts, "server");
};
const extractRouterConfig$1 = (router) => Effect.runSync(extractRouterConfig(router));
//#endregion
export { UTApi, UTFile, UTFiles, UploadThingError, createBuilder, createRouteHandler, createUploadthing, UTRegion as experimental_UTRegion, extractRouterConfig$1 as extractRouterConfig, makeAdapterHandler };
//# sourceMappingURL=index.js.map