Merge pull request #1303 from PicPeak/fix/1300-remove-fragmentation

fix: remove the image-fragmentation surface
This commit is contained in:
Paul Nothaft
2026-09-05 23:37:14 +02:00
committed by GitHub
20 changed files with 9 additions and 281 deletions
-1
View File
@@ -1597,7 +1597,6 @@ module.exports = (router) => {
body('use_canvas_rendering').optional().isBoolean(),
body('overlay_protection').optional().isBoolean(),
body('image_quality').optional().isInt({ min: 1, max: 100 }),
body('fragmentation_level').optional().isInt({ min: 1, max: 10 }),
body('password').optional().isString().custom((value) => {
if (value === undefined || value === null || value === '') {
return true;
-2
View File
@@ -23,7 +23,6 @@ router.get('/settings', adminAuth, requirePermission(['settings.view', 'image_se
'max_image_requests_per_hour',
'suspicious_activity_threshold',
'enable_canvas_rendering',
'default_fragmentation_level',
'security_monitoring_enabled',
'block_suspicious_ips',
'log_security_events_to_db',
@@ -68,7 +67,6 @@ router.put('/settings', adminAuth, requirePermission('image_security.manage'), a
'max_image_requests_per_hour',
'suspicious_activity_threshold',
'enable_canvas_rendering',
'default_fragmentation_level',
'security_monitoring_enabled',
'block_suspicious_ips',
'log_security_events_to_db',
-1
View File
@@ -1227,7 +1227,6 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, noStoreCache, asy
protection_level: req.event.protection_level || 'standard',
image_quality: req.event.image_quality || 85,
use_canvas_rendering: parseBooleanInput(req.event.use_canvas_rendering, false),
fragmentation_level: req.event.fragmentation_level || 3,
overlay_protection: parseBooleanInput(req.event.overlay_protection, true)
};
+1 -17
View File
@@ -117,8 +117,7 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, blockHiddenGallery
const protectionSettings = {
protectionLevel: eventProtectionLevel,
quality: req.event.image_quality || 85,
addFingerprint: req.event.add_fingerprint !== false,
fragmentImage: eventProtectionLevel === 'maximum'
addFingerprint: req.event.add_fingerprint !== false
};
// Resolve photo location through the storage backend (managed) or local
@@ -151,21 +150,6 @@ router.get('/:slug/photo/:photoId/view', verifyGalleryAccess, blockHiddenGallery
? await withLocalCopy(storageKey, runProcessing)
: await runProcessing(resolvePhotoFilePath(req.event, photo));
if (processedImage.type === 'fragmented') {
return res.json({
type: 'fragmented',
fragments: processedImage.fragments.map(f => ({
index: f.index,
row: f.row,
col: f.col,
data: f.buffer.toString('base64'),
position: f.position
})),
dimensions: processedImage.originalDimensions,
fragmentDimensions: processedImage.fragmentDimensions
});
}
finalImage = processedImage;
}
+1 -61
View File
@@ -120,8 +120,6 @@ router.get('/:slug/secure/:photoId/:token',
tokenLength: token?.length,
hasAuthHeader: Boolean(req.headers.authorization),
});
const { fragment } = req.query;
// Verify secure token
const tokenValidation = secureImageService.verifySecureToken(
token,
@@ -212,8 +210,7 @@ router.get('/:slug/secure/:photoId/:token',
const protectionSettings = {
protectionLevel: event.protection_level || 'standard',
quality: event.image_quality || 85,
addFingerprint: event.add_fingerprint !== false,
fragmentImage: event.use_canvas_rendering === true && fragment !== undefined
addFingerprint: event.add_fingerprint !== false
};
let processedImage;
@@ -232,11 +229,6 @@ router.get('/:slug/secure/:photoId/:token',
return res.status(404).json({ error: 'Photo file not found' });
}
// Handle fragmented images
if (processedImage.type === 'fragmented') {
return await handleFragmentedImage(req, res, processedImage, fragment);
}
// Log successful access
await secureImageService.logImageAccess(
photoId,
@@ -267,58 +259,6 @@ router.get('/:slug/secure/:photoId/:token',
}
);
/**
* Handle fragmented image delivery
*/
async function handleFragmentedImage(req, res, fragmentedImage, fragmentIndex) {
const { photoId } = req.params;
try {
if (fragmentIndex === undefined) {
// Return fragment metadata
res.json({
type: 'fragmented',
fragments: fragmentedImage.fragments.length,
dimensions: fragmentedImage.originalDimensions,
fragmentDimensions: fragmentedImage.fragmentDimensions
});
return;
}
const index = parseInt(fragmentIndex);
if (isNaN(index) || index < 0 || index >= fragmentedImage.fragments.length) {
return res.status(400).json({ error: 'Invalid fragment index' });
}
const fragment = fragmentedImage.fragments[index];
// Log fragment access
await secureImageService.logImageAccess(
photoId,
req.event.id,
req.clientInfo,
`fragment_${index}`
);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': fragment.buffer.length,
'X-Fragment-Index': index,
'X-Fragment-Position': JSON.stringify(fragment.position)
});
res.send(fragment.buffer);
} catch (error) {
logger.error('Error serving image fragment', {
error: error.message,
fragmentIndex,
photoId
});
res.status(500).json({ error: 'Failed to serve image fragment' });
}
}
/**
* Download protected image with watermark
*/
+3 -56
View File
@@ -175,8 +175,7 @@ class SecureImageService {
quality = 85,
maxWidth = 1920,
maxHeight = 1080,
addFingerprint = true,
fragmentImage = false
addFingerprint = true
} = options;
try {
@@ -187,7 +186,7 @@ class SecureImageService {
// For standard protection without fingerprinting, return original file
// This avoids unnecessary recompression when no protection features are needed
if (protectionLevel === 'standard' && !addFingerprint && !fragmentImage) {
if (protectionLevel === 'standard' && !addFingerprint) {
return await fs.readFile(imagePath);
}
@@ -268,14 +267,7 @@ class SecureImageService {
});
}
const buffer = await image.toBuffer();
// Fragment image if requested (for canvas reconstruction)
if (fragmentImage && protectionLevel === 'maximum') {
return await this.fragmentImageBuffer(buffer, metadata);
}
return buffer;
return await image.toBuffer();
} catch (error) {
logger.error('Error processing protected image:', error);
// Return original on error
@@ -283,51 +275,6 @@ class SecureImageService {
}
}
/**
* Fragment image into multiple pieces for canvas reconstruction
*/
async fragmentImageBuffer(buffer, metadata) {
const { width, height } = metadata;
const fragments = [];
// Create 3x3 grid of fragments
const cols = 3;
const rows = 3;
const fragmentWidth = Math.floor(width / cols);
const fragmentHeight = Math.floor(height / rows);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const left = col * fragmentWidth;
const top = row * fragmentHeight;
const fragment = await sharp(buffer)
.extract({
left,
top,
width: fragmentWidth,
height: fragmentHeight
})
.toBuffer();
fragments.push({
index: row * cols + col,
row,
col,
buffer: fragment,
position: { left, top, width: fragmentWidth, height: fragmentHeight }
});
}
}
return {
type: 'fragmented',
fragments,
originalDimensions: { width, height },
fragmentDimensions: { width: fragmentWidth, height: fragmentHeight, cols, rows }
};
}
/**
* Log image access for security monitoring
*/