Compare commits

...

11 Commits
cli ... master

Author SHA1 Message Date
7689001e74 Merge pull request '404 / 500 Thumbnails + Fix Regex Capture Groups' (#168) from staging into master
Reviewed-on: StrafesNET/maps-service#168
2025-06-07 04:02:26 +00:00
127402fa77 Merge pull request 'fix regex capture groups' (#167) from pr2 into staging
Reviewed-on: StrafesNET/maps-service#167
2025-06-07 03:58:16 +00:00
40f83a4e30 fix regex capture groups 2025-06-06 20:52:17 -07:00
07391a84cb Merge pull request 'thumbnail fix - will this WORK THIS TIME?' (#154) from thumbnail-fix-1 into staging
Reviewed-on: StrafesNET/maps-service#154
Reviewed-by: Quaternions <quaternions@noreply@itzana.me>
2025-06-06 02:51:35 +00:00
ic3w0lf
3f848a35c8 implement cache de-exister 2025-06-05 17:42:34 -06:00
e89abed3d5 Merge pull request 'Thumbnail Fixes + Bypass Submit Button' (#161) from staging into master
Reviewed-on: StrafesNET/maps-service#161
2025-06-05 01:34:35 +00:00
ic3w0lf
8d5bd9e523 Fix error & include error message in response headers 2025-06-03 20:52:43 -06:00
ic3w0lf
e1fc637619 Implement errorImageResponse 2025-06-03 20:42:37 -06:00
ic3w0lf
762ee874a0 thumbnail fix - will this WORK THIS TIME? 2025-06-03 20:03:09 -06:00
b792d33164 Merge pull request 'Update Rust Dependencies (Roblox Format Zstd Support)' (#142) from staging into master
Reviewed-on: StrafesNET/maps-service#142
2025-06-01 23:13:58 +00:00
929b5949f0 Merge pull request 'Snapshot "Working" Code' (#139) from staging into master
Reviewed-on: StrafesNET/maps-service#139
2025-04-27 21:21:05 +00:00
6 changed files with 97 additions and 51 deletions

View File

@@ -73,26 +73,26 @@ impl std::str::FromStr for ModeElement{
"BonusFinish"=>Ok(Self{zone:Zone::Finish,mode_id:ModeID::BONUS}),
"BonusAnticheat"=>Ok(Self{zone:Zone::Anticheat,mode_id:ModeID::BONUS}),
other=>{
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
if let Some(captures)=bonus_start_pattern.captures(other){
return Ok(Self{
zone:Zone::Start,
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
}
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$");
if let Some(captures)=bonus_finish_pattern.captures(other){
return Ok(Self{
zone:Zone::Finish,
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
}
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
if let Some(captures)=bonus_finish_pattern.captures(other){
return Ok(Self{
zone:Zone::Anticheat,
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
let everything_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$|^Bonus(\d+)Finish$|^BonusFinish(\d+)$|^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
if let Some(captures)=everything_pattern.captures(other){
if let Some(mode_id)=captures.get(1).or(captures.get(2)){
return Ok(Self{
zone:Zone::Start,
mode_id:ModeID(mode_id.as_str().parse().map_err(IDParseError::ParseInt)?),
});
}
if let Some(mode_id)=captures.get(3).or(captures.get(4)){
return Ok(Self{
zone:Zone::Finish,
mode_id:ModeID(mode_id.as_str().parse().map_err(IDParseError::ParseInt)?),
});
}
if let Some(mode_id)=captures.get(5).or(captures.get(6)){
return Ok(Self{
zone:Zone::Anticheat,
mode_id:ModeID(mode_id.as_str().parse().map_err(IDParseError::ParseInt)?),
});
}
}
Err(IDParseError::NoCaptures)
}

BIN
web/public/errors/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
web/public/errors/500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,35 @@
import path from 'path';
import { promises as fs } from 'fs';
import { NextResponse } from 'next/server';
export async function errorImageResponse(
statusCode: number = 500,
options?: { message?: string }
): Promise<NextResponse> {
const file = `${statusCode}.png`;
const filePath = path.join(process.cwd(), 'public/errors', file);
const headers: Record<string, string> = {
'Content-Type': 'image/png',
};
if (options?.message) {
headers['X-Error-Message'] = encodeURIComponent(options.message);
}
try {
const buffer = await fs.readFile(filePath);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: statusCode,
headers,
});
} catch {
const fallback = path.join(process.cwd(), 'public/errors', '500.png');
const buffer = await fs.readFile(fallback);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: 500,
headers,
});
}
}

View File

@@ -1,8 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';
import { errorImageResponse } from '@/app/lib/errorImageResponse';
const cache = new Map<number, { buffer: Buffer; expires: number }>();
const CACHE_TTL = 15 * 60 * 1000;
setInterval(() => {
const now = Date.now();
for (const [key, value] of cache.entries()) {
if (value.expires <= now) {
cache.delete(key);
}
}
}, 60 * 5 * 1000);
export async function GET(
request: NextRequest,
context: { params: Promise<{ assetId: number }> }
@@ -10,10 +20,9 @@ export async function GET(
const { assetId } = await context.params;
if (!assetId) {
return NextResponse.json(
{ error: 'Missing asset ID' },
{ status: 400 }
);
return errorImageResponse(400, {
message: "Missing asset ID",
})
}
let finalAssetId = assetId;
@@ -31,17 +40,17 @@ export async function GET(
} catch { }
const now = Date.now();
const cached = cache.get(finalAssetId);
const cached = cache.get(finalAssetId);
if (cached && cached.expires > now) {
return new NextResponse(cached.buffer, {
headers: {
'Content-Type': 'image/png',
'Content-Length': cached.buffer.length.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
}
if (cached && cached.expires > now) {
return new NextResponse(cached.buffer, {
headers: {
'Content-Type': 'image/png',
'Content-Length': cached.buffer.length.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
}
try {
const response = await fetch(
@@ -49,22 +58,21 @@ export async function GET(
);
if (!response.ok) {
throw new Error('Failed to fetch thumbnail JSON');
throw new Error(`Failed to fetch thumbnail JSON [${response.status}]`)
}
const data = await response.json();
const imageUrl = data.data[0]?.imageUrl;
if (!imageUrl) {
return NextResponse.json(
{ error: 'No image URL found in the response' },
{ status: 404 }
);
return errorImageResponse(404, {
message: "No image URL found in the response",
})
}
const imageResponse = await fetch(imageUrl);
if (!imageResponse.ok) {
throw new Error('Failed to fetch the image');
throw new Error(`Failed to fetch the image [${imageResponse.status}]`)
}
const arrayBuffer = await imageResponse.arrayBuffer();
@@ -79,10 +87,9 @@ export async function GET(
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
} catch {
return NextResponse.json(
{ error: 'Failed to fetch or process thumbnail' },
{ status: 500 }
);
} catch (err) {
return errorImageResponse(500, {
message: `Failed to fetch or process thumbnail: ${err}`,
})
}
}

View File

@@ -1,16 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server"
export async function GET(
request: NextRequest,
context: { params: Promise<{ mapId: string }> }
): Promise<NextResponse> {
// TODO: implement this, we need a cdn for in-game map thumbnails...
if (!process.env.API_HOST) {
throw new Error('env variable "API_HOST" is not set')
}
const { mapId } = await context.params;
const { mapId } = await context.params
const protocol = request.headers.get("x-forwarded-proto") || "https";
const host = request.headers.get("host");
const origin = `${protocol}://${host}`;
const apiHost = process.env.API_HOST.replace(/\/api\/?$/, "")
const redirectPath = `/thumbnails/asset/${mapId}`
const redirectUrl = `${apiHost}${redirectPath}`
return NextResponse.redirect(`${origin}/thumbnails/asset/${mapId}`);
return NextResponse.redirect(redirectUrl)
}