comfyui-deploy/web/src/server/curdModel.ts
bennykok 8eb2ce3e10 Squashed commit of the following:
commit 33c0ad7d14a85f22c57f943dab58610c13d2ac07
Author: Nicholas Koben Kao <kobenkao@gmail.com>
Date:   Tue Jan 30 21:56:00 2024 -0800

    revert custom form change

commit d2905ad045ad7856156e3647a81d642999352de7
Merge: 654423d e3a1d24
Author: Nicholas Koben Kao <kobenkao@gmail.com>
Date:   Tue Jan 30 20:50:06 2024 -0800

    merge schema

commit 654423d597e019a5ebf1ab6568c9942fcb9181c5
Author: Nicholas Koben Kao <kobenkao@gmail.com>
Date:   Tue Jan 30 20:49:34 2024 -0800

    merge confl.ict

commit 641724c11346319674fbb329e8e29b362117c242
Author: Nicholas Koben Kao <kobenkao@gmail.com>
Date:   Tue Jan 30 20:47:34 2024 -0800

    model reload on create

commit eb4dfe8e3f39a0a98eab0fcf1affe7096c12f33b
Author: Nicholas Koben Kao <kobenkao@gmail.com>
Date:   Tue Jan 30 17:00:03 2024 -0800

    delete models

commit 0bea9583fada102396c4e08fe6da971c94d404df
Author: Nicholas Koben Kao <kobenkao@gmail.com>
Date:   Tue Jan 30 14:35:15 2024 -0800

    deploy volume uploader to have timeouts only be modal related
2024-01-31 14:29:36 +08:00

453 lines
13 KiB
TypeScript

"use server";
import { auth } from "@clerk/nextjs";
import {
modelEnumType,
modelTable,
ModelType,
userVolume,
UserVolumeType,
} from "@/db/schema";
import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db";
import type { z } from "zod";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { downloadUrlModelSchema } from "./addCivitaiModelSchema";
import { and, eq, isNull } from "drizzle-orm";
import { CivitaiModelResponse, getModelTypeDetails } from "@/types/civitai";
export async function getModel() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const models = await db
.select()
.from(modelTable)
.where(
orgId
? eq(modelTable.org_id, orgId)
// make sure org_id is null
: and(
eq(modelTable.user_id, userId),
isNull(modelTable.org_id),
),
);
return models;
}
export async function getModelById(id: string) {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const model = await db
.select()
.from(modelTable)
.where(
and(
orgId ? eq(modelTable.org_id, orgId) : and(
eq(modelTable.user_id, userId),
isNull(modelTable.org_id),
),
eq(modelTable.id, id),
),
);
return model[0];
}
export async function getModelVolumes() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const volume = await db
.select()
.from(userVolume)
.where(
and(
orgId
? eq(userVolume.org_id, orgId)
// make sure org_id is null
: and(
eq(userVolume.user_id, userId),
isNull(userVolume.org_id),
),
eq(userVolume.disabled, false),
),
);
return volume;
}
export async function retrieveModelVolumes() {
let volumes = await getModelVolumes();
if (volumes.length === 0) {
// create volume if not already created
volumes = await addModelVolume();
}
return volumes;
}
export async function addModelVolume() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const insertedVolume = await db
.insert(userVolume)
.values({
user_id: userId,
org_id: orgId,
volume_name: `models_${orgId ? orgId : userId}`, // if orgid is avalible use as part of the volume name
disabled: false,
})
.returning();
return insertedVolume;
}
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
// Helper function to make a HEAD request and follow redirects
async function fetchFinalUrl(
url: string,
): Promise<{ finalUrl: string; dispositionFilename?: string }> {
console.log("fetching");
const response = await fetch(url, { method: "HEAD", redirect: "follow" });
if (!response.ok) {
console.log("response not ok");
throw new Error(`Request failed with status ${response.status}`);
}
const contentDisposition = response.headers.get("content-disposition");
let filename;
if (contentDisposition) {
const matches = contentDisposition.match(
/filename\*?=['"]?(?:UTF-8'')?([^;'"\n]*)['"]?;?/i,
);
filename = matches && matches[1]
? decodeURIComponent(matches[1])
: undefined;
}
return { finalUrl: response.url, dispositionFilename: filename };
}
// The main function for validation
export const addModel = withServerPromise(
async (data: z.infer<typeof downloadUrlModelSchema>) => {
const { url } = data;
if (url.includes("civitai.com/models/")) {
// Make a HEAD request to check for 200 OK
const response = await fetch(url, { method: "HEAD" });
if (!response.ok) {
createModelErrorRecord(
url,
`civitai gave non-ok response`,
"civitai",
data.model_type,
);
}
addCivitaiModel(data);
} else {
const { finalUrl, dispositionFilename } = await fetchFinalUrl(url);
console.log("finished fetching");
console.log(finalUrl, dispositionFilename);
if (!dispositionFilename) {
console.log("no file name");
createModelErrorRecord(
url,
`Could not find a filename from resolved Url: ${finalUrl}`,
"download-url",
data.model_type,
);
return;
}
const validExtensions = [".ckpt", ".pt", ".bin", ".pth", ".safetensors"];
const extension = dispositionFilename.slice(
dispositionFilename.lastIndexOf("."),
);
if (!validExtensions.includes(extension)) {
console.log("invalid extension");
createModelErrorRecord(
url,
`file ext ${extension} is invalid. Valid extensions: ${validExtensions}`,
"download-url",
data.model_type,
);
}
addModelDownloadUrl(data, dispositionFilename);
}
},
);
export const addModelDownloadUrl = withServerPromise(
async (data: z.infer<typeof downloadUrlModelSchema>, filename: string) => {
console.log("adding model download");
const { userId, orgId } = auth();
if (!userId) return { error: "No user id" };
const volumes = await retrieveModelVolumes();
const a = await db
.insert(modelTable)
.values({
user_id: userId,
org_id: orgId,
upload_type: "download-url",
model_name: filename,
user_url: data.url,
user_volume_id: volumes[0].id,
model_type: data.model_type,
})
.returning();
const b = a[0];
console.log("download url about to upload");
await uploadModel(data, b, volumes[0]);
},
);
export const deleteModel = withServerPromise(
async (modelId: string) => {
const model = await db.query.modelTable.findFirst({
where: eq(modelTable.id, modelId),
});
// If the model does not exist, throw an error or return a message
if (!model) {
throw new Error("Model not found");
// Or return { error: "Model not found" }; if you prefer to handle it without throwing
}
const volumes = await retrieveModelVolumes();
if (
model.status === "success" && !!model.folder_path && !!model.model_name
) {
const result = await fetch(
`${process.env.MODAL_BUILDER_URL!}/delete-volume-model`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
volume_name: volumes[0].volume_name,
path: model.folder_path,
file_name: model.model_name,
}),
},
);
if (!result.ok) {
const error_log = await result.text();
throw new Error(`Error: ${result.statusText} ${error_log}`);
}
}
await db.delete(modelTable).where(eq(modelTable.id, modelId));
revalidatePath("/storage");
return { message: "Model Deleted" };
},
);
export const getCivitaiModelRes = async (civitaiUrl: string) => {
const { url, modelVersionId } = getUrl(civitaiUrl);
const civitaiModelRes = await fetch(url)
.then((x) => x.json())
.then((a) => {
return CivitaiModelResponse.parse(a);
});
return { civitaiModelRes, url, modelVersionId };
};
const createModelErrorRecord = async (
url: string,
errorMessage: string,
upload_type: "civitai" | "download-url",
model_type: modelEnumType,
) => {
const { userId, orgId } = auth();
if (!userId) return { error: "No user id" };
const volumes = await retrieveModelVolumes();
const a = await db
.insert(modelTable)
.values({
user_id: userId,
org_id: orgId,
user_volume_id: volumes[0].id,
upload_type: "civitai",
model_type,
civitai_url: upload_type === "civitai" ? url : undefined,
user_url: upload_type === "download-url" ? url : undefined,
error_log: errorMessage,
status: "failed",
})
.returning();
return a;
};
export const addCivitaiModel = withServerPromise(
async (data: z.infer<typeof downloadUrlModelSchema>) => {
const { userId, orgId } = auth();
if (!userId) return { error: "No user id" };
const { url, modelVersionId } = getUrl(data.url);
const civitaiModelRes = await fetch(url)
.then((x) => x.json())
.then((a) => {
return CivitaiModelResponse.parse(a);
});
if (civitaiModelRes?.modelVersions?.length === 0) {
return; // no versions to download
}
let selectedModelVersion;
let selectedModelVersionId: string | null = modelVersionId;
if (!selectedModelVersionId) {
selectedModelVersion = civitaiModelRes.modelVersions[0];
selectedModelVersionId = civitaiModelRes.modelVersions[0].id.toString();
} else {
selectedModelVersion = civitaiModelRes.modelVersions.find((version) =>
version.id.toString() === selectedModelVersionId
);
if (!selectedModelVersion) {
return; // version id is wrong
}
selectedModelVersionId = selectedModelVersion?.id.toString();
}
const volumes = await retrieveModelVolumes();
const model_type = getModelTypeDetails(civitaiModelRes.type);
if (!model_type) {
createModelErrorRecord(
url,
`Civitai model type ${civitaiModelRes.type} is not currently supported`,
"civitai",
data.model_type,
);
return;
}
const a = await db
.insert(modelTable)
.values({
user_id: userId,
org_id: orgId,
upload_type: "civitai",
model_name: selectedModelVersion.files[0].name,
civitai_id: civitaiModelRes.id.toString(),
civitai_version_id: selectedModelVersionId,
civitai_url: data.url,
civitai_download_url: selectedModelVersion.files[0].downloadUrl, // there is an issue when a model hoster might put multiple different types of files i.e. their training data.
civitai_model_response: civitaiModelRes,
user_volume_id: volumes[0].id,
model_type,
})
.returning();
const b = a[0];
await uploadModel(data, b, volumes[0]);
revalidatePath("/storage");
},
);
// export const redownloadCheckpoint = withServerPromise(
// async (data: CheckpointItemList) => {
// const { userId } = auth();
// if (!userId) return { error: "No user id" };
//
// const checkpointVolumes = await getCheckpointVolumes();
// let cVolume;
// if (checkpointVolumes.length === 0) {
// const volume = await addCheckpointVolume();
// cVolume = volume[0];
// } else {
// cVolume = checkpointVolumes[0];
// }
//
// console.log("data");
// console.log(data);
//
// const a = await db
// .update(checkpointTable)
// .set({
// // status: "started",
// // updated_at: new Date(),
// })
// .returning();
//
// const b = a[0];
//
// console.log("b");
// console.log(b);
//
// await uploadCheckpoint(data, b, cVolume);
// // redirect(`/checkpoints/${b.id}`);
// },
// );
async function uploadModel(
data: z.infer<typeof downloadUrlModelSchema>,
c: ModelType,
v: UserVolumeType,
) {
const headersList = headers();
const domain = headersList.get("x-forwarded-host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
if (domain === "") {
throw new Error("No domain");
}
// Call remote builder
const result = await fetch(
`${process.env.MODAL_BUILDER_URL!}/upload-volume`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
download_url: c.upload_type === "civitai"
? c.civitai_download_url
: c.user_url,
volume_name: v.volume_name,
volume_id: v.id,
model_id: c.id,
callback_url: `${protocol}://${domain}/api/volume-upload`,
upload_type: c.model_type,
}),
},
);
if (!result.ok) {
const error_log = await result.text();
await db
.update(modelTable)
.set({
status: "failed",
error_log: error_log,
})
.where(eq(modelTable.id, c.id));
throw new Error(`Error: ${result.statusText} ${error_log}`);
} else {
// setting the build machine id
const json = await result.json();
await db
.update(modelTable)
.set({
...data,
upload_machine_id: json.build_machine_instance_id,
})
.where(eq(modelTable.id, c.id));
}
}