Refactor HACKY_resolvePicture away (#2606)

This commit is contained in:
Kalle
2025-11-08 13:54:02 +02:00
committed by GitHub
parent 62808206c1
commit 9296319d23
73 changed files with 880 additions and 584 deletions

View File

@@ -0,0 +1,171 @@
import "dotenv/config";
import fs from "node:fs";
import path from "node:path";
import { Readable } from "node:stream";
import { db } from "~/db/sql";
import { uploadStreamToS3 } from "~/features/img-upload/s3.server";
import { databaseTimestampNow } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const TOURNAMENT_LOGO_PATH = "public/static-assets/img/tournament-logos";
const ADMIN_ID = 1;
const LOGO_IDENTIFIER_TO_PATTERN: Record<string, string[]> = {
sf: ["sendouq"],
pp: ["paddling pool"],
itz: ["in the zone"],
pn: ["picnic"],
pg: ["proving grounds"],
tc: ["triton"],
sos: ["swim or sink"],
ftiu: ["from the ink up"],
cc: ["coral clash"],
lu: ["level up"],
a41: ["all 4 one"],
fb: ["fry basket"],
d: ["the depths"],
e: ["eclipse"],
hc: ["homecoming"],
bio: ["bad ideas"],
ai: ["tenoch"],
mm: ["megalodon monday"],
ho: ["heaven 2 ocean"],
kr: ["kraken royale"],
mr: ["menu royale"],
bc: ["barracuda co"],
ci: ["crimson ink"],
me: ["mesozoic mayhem"],
ros: ["rain or shine"],
sj: ["squid junction"],
ss: ["silly sausage"],
ul: ["united-lan"],
sc: ["soul cup"],
};
async function uploadLogoFile(
filePath: string,
identifier: string,
): Promise<string> {
logger.info(`Uploading ${identifier}.png to S3...`);
const fileBuffer = fs.readFileSync(filePath);
const stream = Readable.from(fileBuffer);
const fileName = `tournament-logo-${identifier}.png`;
const s3Url = await uploadStreamToS3(stream, fileName);
invariant(s3Url, `Failed to upload ${identifier}.png to S3`);
logger.info(`Uploaded ${identifier}.png to ${s3Url}`);
return fileName;
}
async function createImageRecord(url: string): Promise<number> {
const result = await db
.insertInto("UnvalidatedUserSubmittedImage")
.values({
url,
validatedAt: databaseTimestampNow(),
submitterUserId: ADMIN_ID,
})
.returning("id")
.executeTakeFirstOrThrow();
return result.id;
}
async function updateCalendarEvents(
imageId: number,
patterns: string[],
): Promise<number> {
const events = await db
.selectFrom("CalendarEvent")
.select(["id", "name"])
.where("avatarImgId", "is", null)
.execute();
let updateCount = 0;
for (const event of events) {
const normalizedEventName = event.name.toLowerCase();
const matches = patterns.some((pattern) =>
normalizedEventName.includes(pattern),
);
if (matches) {
await db
.updateTable("CalendarEvent")
.set({ avatarImgId: imageId })
.where("id", "=", event.id)
.execute();
logger.info(`Updated CalendarEvent ${event.id}: "${event.name}"`);
updateCount++;
}
}
return updateCount;
}
async function main() {
logger.info("Starting tournament logo migration to S3...");
const defaultLogoPath = path.join(TOURNAMENT_LOGO_PATH, "default.png");
if (fs.existsSync(defaultLogoPath)) {
logger.info("\n=== Uploading default tournament logo ===");
const defaultS3Url = await uploadLogoFile(defaultLogoPath, "default");
const defaultImageId = await createImageRecord(defaultS3Url);
logger.info(
`Created UnvalidatedUserSubmittedImage record for default logo with ID ${defaultImageId}\n`,
);
}
const logoFiles = fs
.readdirSync(TOURNAMENT_LOGO_PATH)
.filter((file) => file.endsWith(".png") && file !== "default.png");
logger.info(`Found ${logoFiles.length} logo files to migrate`);
let totalUpdated = 0;
for (const file of logoFiles) {
const identifier = file.replace(".png", "");
const patterns = LOGO_IDENTIFIER_TO_PATTERN[identifier];
if (!patterns) {
logger.warn(`No pattern mapping found for ${identifier}, skipping...`);
continue;
}
const filePath = path.join(TOURNAMENT_LOGO_PATH, file);
const s3Url = await uploadLogoFile(filePath, identifier);
const imageId = await createImageRecord(s3Url);
logger.info(
`Created UnvalidatedUserSubmittedImage record with ID ${imageId}`,
);
const updatedCount = await updateCalendarEvents(imageId, patterns);
totalUpdated += updatedCount;
logger.info(
`Updated ${updatedCount} CalendarEvent records for ${identifier}\n`,
);
}
logger.info("\n=== Migration Complete ===");
logger.info(`Total CalendarEvent records updated: ${totalUpdated}`);
}
main()
.then(() => {
logger.info("Script completed successfully");
process.exit(0);
})
.catch((error) => {
logger.error("Script failed:", error);
process.exit(1);
});

48
scripts/seed-art-urls.ts Normal file
View File

@@ -0,0 +1,48 @@
export const SEED_ART_URLS = [
"https://images.unsplash.com/photo-1611627474565-2367887415d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTA2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1625120742520-3f085b6894ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTI2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1656695607245-9686ce8e1a91?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTQ1&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1673011526786-c7bf154d2c6d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTY5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1643833994700-059713434c71?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTc2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1541425284102-3d2c49dcb2bc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTk5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1526946366170-7a81b443c4e6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjA4&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1551368003-4d96079d0a99?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjIz&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1595960684234-49d2a004e753?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjMw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1676275062470-4b628cf1ce01?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjM5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1602099081031-767e09dfdbad?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDYz&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1547532182-bf296f6be875?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDY5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1601549838695-57580707e367?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDc2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1595361315899-72a291112b7b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDgy&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1676275061266-a28f2f3f4552?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDg4&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/44/C3EWdWzT8imxs0fKeKoC_blackforrest.JPG?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDk1&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1628425242605-a0039d89e8b2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NTAx&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1542917118-105d7d34b9ca?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NTEw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1601549838695-57580707e367?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NTE3&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1660583490803-75c0307c805b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTQ2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1490042706304-06c664f6fd9a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTU4&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1676998652985-fd74c7b2a8d5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTY0&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1470390356535-d19bbf47bacb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTcx&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1503256207526-0d5d80fa2f47?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2Njcy&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1604495589307-973bd87d7fa3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2Njg2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1563476651637-3e5c5941d432?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NzEw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1551567819-eef106c515b6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NzE2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1553536590-d28c5d5dee92?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NzIx&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
];
export function getArtFilename(index: number): string {
return `art-${index}.jpg`;
}
export const SEED_TEAM_IMAGES = [
{ filename: "alliance-rogue.png", teamId: 1 },
{ filename: "default.png", teamId: null },
];
export const SEED_TOURNAMENT_IMAGES = [
{ filename: "picnic.png", tournamentId: 1 },
{ filename: "in-the-zone.png", tournamentId: 2 },
{ filename: "paddling-pool.png", tournamentId: 3 },
{ filename: "swim-or-sink.png", tournamentId: 4 },
{ filename: "the-depths.png", tournamentId: 5 },
{ filename: "luti.png", tournamentId: 6 },
];

244
scripts/seed-images.ts Normal file
View File

@@ -0,0 +1,244 @@
import "dotenv/config";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { Readable } from "node:stream";
import { S3 } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { logger } from "~/utils/logger";
import {
getArtFilename,
SEED_ART_URLS,
SEED_TEAM_IMAGES,
SEED_TOURNAMENT_IMAGES,
} from "./seed-art-urls";
async function checkMinioConnection(): Promise<boolean> {
try {
const {
STORAGE_END_POINT,
STORAGE_ACCESS_KEY,
STORAGE_SECRET,
STORAGE_REGION,
STORAGE_BUCKET,
} = process.env;
if (
!(
STORAGE_ACCESS_KEY &&
STORAGE_END_POINT &&
STORAGE_SECRET &&
STORAGE_REGION &&
STORAGE_BUCKET
)
) {
logger.warn("Storage configuration not found in environment");
return false;
}
const s3 = new S3({
endpoint: STORAGE_END_POINT,
forcePathStyle: false,
credentials: {
accessKeyId: STORAGE_ACCESS_KEY,
secretAccessKey: STORAGE_SECRET,
},
region: STORAGE_REGION,
});
await s3.headBucket({ Bucket: STORAGE_BUCKET });
return true;
} catch {
return false;
}
}
async function downloadImage(url: string): Promise<Buffer> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download image: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
async function readLocalImage(filename: string): Promise<Buffer> {
const imagePath = join(process.cwd(), "app", "db", "seed", "img", filename);
return await readFile(imagePath);
}
async function uploadToMinio(
imageBuffer: Buffer,
filename: string,
): Promise<string> {
const {
STORAGE_END_POINT,
STORAGE_ACCESS_KEY,
STORAGE_SECRET,
STORAGE_REGION,
STORAGE_BUCKET,
} = process.env;
const s3 = new S3({
endpoint: STORAGE_END_POINT,
forcePathStyle: false,
credentials: {
accessKeyId: STORAGE_ACCESS_KEY!,
secretAccessKey: STORAGE_SECRET!,
},
region: STORAGE_REGION,
});
const stream = Readable.from(imageBuffer);
const upload = new Upload({
client: s3,
params: {
Bucket: STORAGE_BUCKET!,
Key: filename,
Body: stream,
ACL: "public-read",
},
});
await upload.done();
return filename;
}
async function fileExistsInMinio(filename: string): Promise<boolean> {
try {
const {
STORAGE_END_POINT,
STORAGE_ACCESS_KEY,
STORAGE_SECRET,
STORAGE_REGION,
STORAGE_BUCKET,
} = process.env;
const s3 = new S3({
endpoint: STORAGE_END_POINT,
forcePathStyle: false,
credentials: {
accessKeyId: STORAGE_ACCESS_KEY!,
secretAccessKey: STORAGE_SECRET!,
},
region: STORAGE_REGION,
});
await s3.headObject({
Bucket: STORAGE_BUCKET!,
Key: filename,
});
return true;
} catch {
return false;
}
}
export async function seedImages(): Promise<void> {
const minioAvailable = await checkMinioConnection();
if (!minioAvailable) {
logger.warn(
"⚠️ Minio is not available. Skipping image seeding. Make sure Docker is running if you want to seed images.",
);
return;
}
logger.info(`📥 Processing ${SEED_ART_URLS.length} art images`);
let successCount = 0;
let failCount = 0;
let skippedCount = 0;
for (let i = 0; i < SEED_ART_URLS.length; i++) {
const url = SEED_ART_URLS[i];
const filename = getArtFilename(i);
const smallFilename = filename.replace(/\.(\w+)$/, "-small.$1");
try {
const regularExists = await fileExistsInMinio(filename);
const smallExists = await fileExistsInMinio(smallFilename);
if (regularExists && smallExists) {
skippedCount++;
logger.info(
` ↷ Files ${filename} and ${smallFilename} already exist in Minio`,
);
} else {
const imageBuffer = await downloadImage(url);
if (!regularExists) {
logger.info(` Uploading ${filename} to Minio...`);
await uploadToMinio(imageBuffer, filename);
}
if (!smallExists) {
logger.info(` Uploading ${smallFilename} to Minio...`);
await uploadToMinio(imageBuffer, smallFilename);
}
successCount++;
}
} catch (err) {
failCount++;
logger.error(
` ✗ Failed to process ${filename}: ${(err as Error).message}`,
);
}
}
logger.info(
`\n✅ Art image seeding complete: ${successCount} uploaded, ${skippedCount} already existed, ${failCount} failed`,
);
logger.info(
`\n📥 Processing ${SEED_TEAM_IMAGES.length} team images and ${SEED_TOURNAMENT_IMAGES.length} tournament images`,
);
const localImages = [...SEED_TEAM_IMAGES, ...SEED_TOURNAMENT_IMAGES];
let localSuccessCount = 0;
let localFailCount = 0;
let localSkippedCount = 0;
for (const { filename } of localImages) {
const smallFilename = filename.replace(/\.(\w+)$/, "-small.$1");
try {
const regularExists = await fileExistsInMinio(filename);
const smallExists = await fileExistsInMinio(smallFilename);
if (regularExists && smallExists) {
localSkippedCount++;
logger.info(
` ↷ Files ${filename} and ${smallFilename} already exist in Minio`,
);
} else {
const imageBuffer = await readLocalImage(filename);
if (!regularExists) {
logger.info(` Uploading ${filename} to Minio...`);
await uploadToMinio(imageBuffer, filename);
}
if (!smallExists) {
logger.info(` Uploading ${smallFilename} to Minio...`);
await uploadToMinio(imageBuffer, smallFilename);
}
localSuccessCount++;
}
} catch (err) {
localFailCount++;
logger.error(
` ✗ Failed to process ${filename}: ${(err as Error).message}`,
);
}
}
logger.info(
`\n✅ Local image seeding complete: ${localSuccessCount} uploaded, ${localSkippedCount} already existed, ${localFailCount} failed`,
);
}

View File

@@ -3,6 +3,7 @@ import fs from "node:fs";
import { seed } from "~/db/seed";
import { db } from "~/db/sql";
import { logger } from "~/utils/logger";
import { seedImages } from "./seed-images";
async function main() {
// Step 1: Create .env if it doesn't exist
@@ -33,6 +34,14 @@ async function main() {
process.exit(1);
}
}
// Step 3: Seed images to Minio
logger.info("🖼️ Seeding images to Minio...");
try {
await seedImages();
} catch (err) {
logger.error("Error seeding images:", (err as Error).message);
}
}
main();