add support for uploading archives

This commit is contained in:
2026-07-12 17:03:19 +02:00
parent ab1ea7b368
commit d424b24221
15 changed files with 168 additions and 45 deletions

View File

@@ -560,8 +560,37 @@ export default new class queue {
});
}
}
else if (mime.startsWith('application/') && cfg.mimes[mime] && !['swf', 'pdf'].includes(cfg.mimes[mime])) {
// Any application/* MIME registered in config that isn't swf or pdf is treated as an archive.
// Both the detection and the label come straight from cfg.mimes — no hardcoded list needed.
let customThumb = cfg.websrv && cfg.websrv.archive_thumb;
if (customThumb && customThumb.startsWith('/')) {
customThumb = path.join(path.resolve(), 'public', customThumb);
}
let usedCustom = false;
if (customThumb) {
try {
const stat = await fs.promises.stat(customThumb).catch(() => null);
if (stat && stat.size > 0) {
await this.spawn('magick', [customThumb, tmpFile]);
usedCustom = true;
}
} catch (_) {}
}
if (!usedCustom) {
const archiveLabel = (cfg.mimes[mime] || 'arc').toUpperCase();
await this.spawn('magick', [
'-size', thumbSpec, 'xc:#1a2e1a',
'-gravity', 'center',
'-fill', '#66bb6a',
'-pointsize', '48',
'-annotate', '0', archiveLabel,
tmpFile
]).catch(() => {});
}
}
// Determine if we should use a checkerboard background for transparency
const isTransparentMime = mime === 'image/png' || mime === 'image/webp' || mime === 'image/avif' || mime === 'image/gif';
if (isTransparentMime) {
// Build a grey/white checkerboard via explicit xc: squares (no pattern replacement tricks):

View File

@@ -844,7 +844,8 @@ export default {
is_repost: actitem.checksum ? actitem.checksum.includes('_bypass_') : false,
reposts: repostItems,
width: actitem.width || null,
height: actitem.height || null
height: actitem.height || null,
original_filename: actitem.original_filename || null
},
title: `${actitem.id} - ${cfg.websrv.domain}`,
pagination: {

View File

@@ -129,7 +129,7 @@ export default (router, tpl) => {
const item = data.item;
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
data.can_manage_item = !!(session && (session.admin || session.is_moderator || session.user === item.username));
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1);
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
data.user_has_favorited = !!(session && Array.isArray(item.favorites) && item.favorites.some(f => f.user === session.user));
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
@@ -138,6 +138,7 @@ export default (router, tpl) => {
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
data.item_username_lower = (item.username || '').toLowerCase();
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';

View File

@@ -348,7 +348,7 @@ export default (router, tpl) => {
data.can_manage_item = !!(session && (session.admin || session.is_moderator || session.user === item.username));
// Is the item's MIME type suitable for metadata extraction?
// YouTube items use oEmbed via /meta/fetch; all non-flash MIME types are eligible.
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1);
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
// Has the current user favorited this item?
data.user_has_favorited = !!(session && Array.isArray(item.favorites) && item.favorites.some(f => f.user === session.user));
// Hall columns for display
@@ -359,6 +359,7 @@ export default (router, tpl) => {
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
data.item_username_lower = (item.username || '').toLowerCase();
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';

View File

@@ -166,8 +166,12 @@ export default (router, tpl) => {
const anchorId = qs.anchor ? parseInt(qs.anchor, 10) : null;
const swfMimes = ['application/x-shockwave-flash', 'application/vnd.adobe.flash.movie'];
const archiveMimes = Object.entries(cfg.mimes)
.filter(([mime, ext]) => mime.startsWith('application/') && !['swf', 'pdf'].includes(ext))
.map(([mime]) => mime);
const excludeSwfSQL = !cfg.websrv.enable_swf ? db`AND items.mime != ALL(${swfMimes})` : db``;
const excludePdfSQL = db`AND items.mime != 'application/pdf'`;
const excludeArchiveSQL = db`AND items.mime != ALL(${archiveMimes})`;
const mimeParts = (mime || '').split(',').filter(m => ['video', 'audio', 'image'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`AND (${mimeParts.map(m => db`items.mime ilike ${m + '/%'}`).reduce((a, b) => db`${a} OR ${b}`)})`
@@ -246,7 +250,9 @@ export default (router, tpl) => {
WHERE items.id = ${anchorId}
AND items.active = true
AND ${db.unsafe(modeQuery)}
${excludeSwfSQL}
${excludePdfSQL}
${excludeArchiveSQL}
${!req.session && nsfp ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(nsfp)}))` : db``}
`;
// If the anchor item doesn't pass the rating filter, it's inaccessible to this user.
@@ -264,6 +270,7 @@ export default (router, tpl) => {
AND items.active = true
${excludeSwfSQL}
${excludePdfSQL}
${excludeArchiveSQL}
AND items.id != ${anchorId}
${excludeSQL}
${mimeSQL}
@@ -287,6 +294,7 @@ export default (router, tpl) => {
AND items.active = true
${excludeSwfSQL}
${excludePdfSQL}
${excludeArchiveSQL}
${cursorSQL}
${excludeSQL}
${mimeSQL}
@@ -341,6 +349,7 @@ export default (router, tpl) => {
fav_count: +row.fav_count || 0,
comment_count: +row.comment_count || 0,
is_swf: !!(row.mime === 'application/x-shockwave-flash' || row.mime === 'application/vnd.adobe.flash.movie'),
is_archive: !!(row.mime && archiveMimes.includes(row.mime)),
is_video: isVideo,
is_youtube: isYouTube,
is_audio: isAudio,

View File

@@ -163,7 +163,7 @@ export default (router, tpl) => {
const item = data.item;
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
data.can_manage_item = !!(session && (session.admin || session.is_moderator || session.user === item.username));
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1);
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
data.user_has_favorited = !!(session && Array.isArray(item.favorites) && item.favorites.some(f => f.user === session.user));
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
@@ -171,6 +171,7 @@ export default (router, tpl) => {
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
data.item_username_lower = (item.username || '').toLowerCase();
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';