Add thumbnail support and validation
All checks were successful
Update Plugin Index / update-index (push) Successful in 7s

This commit is contained in:
2026-04-17 02:47:56 +10:00
parent 3ebdc6b561
commit edea39e558
2 changed files with 101 additions and 3 deletions

View File

@@ -10,12 +10,93 @@
* 4. Plugin ID matches filename
* 5. Author matches PR creator (for new plugins)
* 6. Can only remove own plugins
* 7. Thumbnail requirements: png/gif/jpg, max 512x512, max 2MB
*/
import { readFile, access } from 'fs/promises';
import { join } from 'path';
import https from 'https';
import http from 'http';
const REQUIRED_FIELDS = ['id', 'name', 'version', 'description', 'author', 'repository'];
const MAX_THUMBNAIL_SIZE = 2 * 1024 * 1024; // 2MB
const MAX_THUMBNAIL_DIMENSIONS = 512;
const ALLOWED_IMAGE_FORMATS = ['png', 'gif', 'jpg', 'jpeg'];
async function validateThumbnail(url) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
protocol.get(url, (res) => {
if (res.statusCode !== 200) {
return reject(new Error(`Thumbnail URL returned ${res.statusCode}`));
}
const chunks = [];
let size = 0;
res.on('data', (chunk) => {
chunks.push(chunk);
size += chunk.length;
if (size > MAX_THUMBNAIL_SIZE) {
res.destroy();
reject(new Error(`Thumbnail exceeds 2MB limit (${(size / 1024 / 1024).toFixed(2)}MB)`));
}
});
res.on('end', () => {
const buffer = Buffer.concat(chunks);
// Check image format by magic bytes
const isPNG = buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47;
const isJPG = buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF;
const isGIF = buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46;
if (!isPNG && !isJPG && !isGIF) {
return reject(new Error('Thumbnail must be PNG, JPG, or GIF format'));
}
// Check dimensions (simplified check - reads PNG/JPG headers)
let width, height;
if (isPNG) {
width = buffer.readUInt32BE(16);
height = buffer.readUInt32BE(20);
} else if (isJPG) {
// Simplified JPEG dimension reading
let offset = 2;
while (offset < buffer.length) {
if (buffer[offset] !== 0xFF) break;
offset++;
const marker = buffer[offset];
offset++;
if (marker === 0xC0 || marker === 0xC2) {
height = buffer.readUInt16BE(offset + 3);
width = buffer.readUInt16BE(offset + 5);
break;
}
const segmentLength = buffer.readUInt16BE(offset);
offset += segmentLength;
}
} else if (isGIF) {
width = buffer.readUInt16LE(6);
height = buffer.readUInt16LE(8);
}
if (width > MAX_THUMBNAIL_DIMENSIONS || height > MAX_THUMBNAIL_DIMENSIONS) {
return reject(new Error(`Thumbnail dimensions ${width}x${height} exceed ${MAX_THUMBNAIL_DIMENSIONS}x${MAX_THUMBNAIL_DIMENSIONS}`));
}
resolve({ width, height, size, format: isPNG ? 'PNG' : isJPG ? 'JPG' : 'GIF' });
});
res.on('error', reject);
}).on('error', reject);
});
}
const prAuthor = process.argv[2];
const changedFiles = process.argv[3]?.split('\n').filter(f => f.trim()) || [];
@@ -23,7 +104,24 @@ const changedFiles = process.argv[3]?.split('\n').filter(f => f.trim()) || [];
if (!prAuthor) {
console.error('❌ Error: PR author not provided');
process.exit(1);
}
}// Check thumbnail if provided (optional)
if (plugin.thumbnail) {
try {
new URL(plugin.thumbnail);
// Validate thumbnail meets requirements
try {
const thumbInfo = await validateThumbnail(plugin.thumbnail);
console.log(` ✓ Thumbnail: ${thumbInfo.format} ${thumbInfo.width}x${thumbInfo.height} (${(thumbInfo.size / 1024).toFixed(1)}KB)`);
} catch (thumbErr) {
errors.push(`${file}: Thumbnail validation failed: ${thumbErr.message}`);
}
} catch {
errors.push(`${file}: Invalid thumbnail URL: ${plugin.thumbnail}`);
}
}
console.log(`\n🔍 Validating PR from: ${prAuthor}`);
console.log(`📝 Changed files: ${changedFiles.length}`);
@@ -115,8 +213,7 @@ for (const file of changedFiles) {
} else if (isRemoved) {
// For removed files, we can't validate ownership easily in CI
// Just track that it was removed
removed.push(pluginId);
// Just track that it was removed console.log(' 7. Thumbnail (optional): PNG/JPG/GIF, max 512x512, max 2MB'); removed.push(pluginId);
console.log(`🗑️ Removed: ${pluginId}`);
}
}

View File

@@ -5,6 +5,7 @@
"description": "An example plugin demonstrating the plugin system capabilities",
"author": "litruv",
"repository": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example.git",
"thumbnail": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example/raw/branch/main/thumbnail.png",
"downloadUrl": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example/archive/main.zip",
"homepage": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example",
"tags": ["example", "demo"],