Release v2.13.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m15s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
lzstealth committed 2026-09-11 20:46:37 +01:00
1 parent 0a03cdd0b7
commit 394d23bb4d
47 files changed
+2408 -559

No files matched your search

@@ -1,42 +0,0 @@
const TASK = {
key: 'unused-upload-sweep',
title: 'Unused upload sweep',
category: 'cleanup',
trigger: 'recurring scheduled task, daily',
purpose: 'remove uploaded media files that are no longer referenced.',
taskType: 'recurring-run',
intervalMs: 24 * 60 * 60 * 1000
};
function registerUnusedUploadSweepTask(options) {
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const uploadSyncService = options && options.uploadSyncService;
const collectUploadPathsFromDirectory = uploadSyncService && uploadSyncService.collectUploadPathsFromDirectory;
const removeUnusedUploadFiles = uploadSyncService && uploadSyncService.removeUnusedUploadFiles;
const pool = options && options.pool;
const mediaDir = String(options && options.mediaDir || '').trim();
if (!backgroundTaskQueue || typeof collectUploadPathsFromDirectory !== 'function' || typeof removeUnusedUploadFiles !== 'function' || !pool || !mediaDir) {
throw new Error('registerUnusedUploadSweepTask requires the unused upload sweep dependencies.');
}
backgroundTaskQueue.registerRecurringTask({
key: TASK.key,
title: TASK.title,
category: TASK.category,
intervalMs: TASK.intervalMs,
metadata: {
mediaDir: mediaDir
},
run: async function () {
const uploadPaths = await collectUploadPathsFromDirectory(mediaDir);
if (!uploadPaths.length) {
return;
}
await removeUnusedUploadFiles(pool, mediaDir, uploadPaths);
}
});
}
module.exports = { registerUnusedUploadSweepTask };
+178
View File
@@ -0,0 +1,178 @@
// Data access helpers for the shared media library.
const fs = require('fs');
const path = require('path');
const MEDIA_MIME_TYPES = {
'.gif': 'image/gif',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.mp4': 'video/mp4',
'.mpeg': 'video/mpeg',
'.webm': 'video/webm'
};
function normalizeMediaPath(value) {
const mediaPath = String(value || '').trim();
return mediaPath.startsWith('/media/') ? mediaPath : null;
}
async function registerMediaAsset(pool, file, mediaPath, userId, options) {
const normalizedPath = normalizeMediaPath(mediaPath);
if (!pool || !file || !normalizedPath) {
return null;
}
await pool.query(
`INSERT INTO c_media_assets
(media_path, original_name, media_type, mime_type, file_size, is_published, created_by, modified_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
${options && options.preserveOriginalName ? 'original_name = original_name,' : 'original_name = VALUES(original_name),' }
media_type = VALUES(media_type),
mime_type = VALUES(mime_type),
file_size = VALUES(file_size),
${options && options.preservePublication ? 'is_published = is_published,' : 'is_published = VALUES(is_published),' }
modified_by = VALUES(modified_by)`,
[
normalizedPath,
String(file.originalname || file.filename || 'media').slice(0, 255),
String(file.mediaType || '').trim() || 'unknown',
String(file.mimetype || '').trim() || 'application/octet-stream',
Number(file.size) || 0,
options && options.published === false ? 0 : 1,
userId || null,
userId || null
]
);
return normalizedPath;
}
async function registerMediaAssets(pool, files, getMediaPath, getMediaType, userId, options) {
const registered = [];
const list = Array.isArray(files) ? files : [];
for (let index = 0; index < list.length; index += 1) {
const file = list[index];
const mediaPath = typeof getMediaPath === 'function' ? getMediaPath(file) : null;
if (!mediaPath) {
continue;
}
const enrichedFile = Object.assign({}, file, {
mediaType: typeof getMediaType === 'function' ? getMediaType(file) : 'unknown'
});
registered.push(await registerMediaAsset(pool, enrichedFile, mediaPath, userId, options));
}
return registered.filter(Boolean);
}
async function fetchMediaAssets(pool) {
const [rows] = await pool.query(
`SELECT id, media_path, original_name, media_type, mime_type, file_size, created_at
FROM c_media_assets
WHERE is_published = 1
ORDER BY created_at DESC, id DESC`
);
return rows || [];
}
async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) {
const directory = String(uploadDir || '').trim();
if (!pool || !directory) {
return 0;
}
let entries;
try {
entries = await fs.promises.readdir(directory, { withFileTypes: true });
} catch (error) {
if (error && error.code === 'ENOENT') {
return 0;
}
throw error;
}
let registeredCount = 0;
for (const entry of entries) {
if (!entry || !entry.isFile()) {
continue;
}
const originalName = String(entry.name || '').trim();
const mimeType = MEDIA_MIME_TYPES[path.extname(originalName).toLowerCase()];
if (!mimeType) {
continue;
}
const filePath = path.join(directory, originalName);
const stats = await fs.promises.stat(filePath);
const result = await registerMediaAsset(pool, {
originalname: originalName,
mimetype: mimeType,
size: stats.size,
mediaType: mimeType.startsWith('video/') ? 'video' : 'image'
}, '/media/uploads/' + originalName, userId, { preserveOriginalName: true, preservePublication: true });
if (result) {
registeredCount += 1;
}
}
return registeredCount;
}
async function countMediaAssetReferences(pool, mediaPath) {
const normalizedPath = normalizeMediaPath(mediaPath);
if (!normalizedPath) {
return 0;
}
const [slideRows] = await pool.query(
`SELECT COUNT(*) AS ref_count
FROM c_slides
WHERE LOCATE(?, COALESCE(content_json, '')) > 0`,
[normalizedPath]
);
const [templateRows] = await pool.query(
'SELECT COUNT(*) AS ref_count FROM c_templates WHERE background_image_path = ?',
[normalizedPath]
);
return Number(slideRows[0] && slideRows[0].ref_count || 0) + Number(templateRows[0] && templateRows[0].ref_count || 0);
}
async function publishMediaAssets(pool, mediaPaths) {
const paths = Array.from(new Set((Array.isArray(mediaPaths) ? mediaPaths : []).map(normalizeMediaPath).filter(Boolean)));
for (let index = 0; index < paths.length; index += 1) {
await pool.query('UPDATE c_media_assets SET is_published = 1 WHERE media_path = ?', [paths[index]]);
}
}
async function removePendingMediaAssets(pool, uploadDir, mediaPaths) {
const paths = Array.from(new Set((Array.isArray(mediaPaths) ? mediaPaths : []).map(normalizeMediaPath).filter(Boolean)));
for (let index = 0; index < paths.length; index += 1) {
const mediaPath = paths[index];
const [rows] = await pool.query('SELECT id FROM c_media_assets WHERE media_path = ? AND is_published = 0 LIMIT 1', [mediaPath]);
const asset = rows && rows[0];
if (!asset || await countMediaAssetReferences(pool, mediaPath) > 0) {
continue;
}
await pool.query('DELETE FROM c_media_assets WHERE id = ?', [asset.id]);
const filePath = path.join(path.dirname(uploadDir), mediaPath.replace(/^\/media\//, ''));
await fs.promises.unlink(filePath).catch(function (error) {
if (error && error.code !== 'ENOENT') {
throw error;
}
});
}
}
module.exports = {
normalizeMediaPath,
registerMediaAsset,
registerMediaAssets,
fetchMediaAssets,
syncMediaAssetsFromDirectory,
countMediaAssetReferences,
publishMediaAssets,
removePendingMediaAssets
};
+35 -27
View File
@@ -253,10 +253,18 @@ function createUploadSyncService(options) {
function normalizeUploadReference(uploadPath) {
const value = String(uploadPath || '').trim();
if (!value || !value.startsWith('/media/')) {
if (!value) {
return null;
}
if (value.startsWith('/media/')) {
return value;
}
try {
const parsed = new URL(value);
return parsed.pathname.startsWith('/media/') ? parsed.pathname : null;
} catch (_error) {
return null;
}
return value;
}
function getUploadRelativePath(uploadPath) {
@@ -313,6 +321,13 @@ function createUploadSyncService(options) {
if (reference) {
refs.add(reference);
}
const embeddedReferences = current.match(/\/media\/[^"'\s<>)]+/g) || [];
embeddedReferences.forEach(function (embeddedReference) {
const normalizedReference = normalizeUploadReference(embeddedReference);
if (normalizedReference) {
refs.add(normalizedReference);
}
});
}
}
return refs;
@@ -364,32 +379,21 @@ function createUploadSyncService(options) {
return Number(slideRows[0].ref_count || 0) + Number(thumbnailRows[0].ref_count || 0) + Number(templateRows[0].ref_count || 0);
}
async function removeUnusedUploadFiles(pool, uploadDir, uploadPaths) {
const uniquePaths = Array.from(new Set((uploadPaths || []).map(normalizeUploadReference).filter(Boolean)));
for (let i = 0; i < uniquePaths.length; i += 1) {
const uploadPath = uniquePaths[i];
const referenceCount = await countUploadReferences(pool, uploadPath);
if (referenceCount > 0) {
continue;
}
const filePath = resolveUploadFilePath(uploadDir, uploadPath);
if (String(uploadPath || '').startsWith('/media/player-cache/')) {
continue;
}
try {
await fs.promises.unlink(filePath);
} catch (error) {
if (error && error.code !== 'ENOENT') {
console.warn('Unable to remove unused upload file:', filePath, error);
}
}
queuePlayerUploadSync({
type: 'delete',
uploadPath: uploadPath,
uploadDir: uploadDir
});
async function uploadFileExists(uploadDir, uploadPath) {
const filePath = resolveUploadFilePath(uploadDir, uploadPath);
if (!filePath) {
return false;
}
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch (_error) {
return false;
}
}
async function removeUnusedUploadFiles(pool, uploadDir, uploadPaths) {
return;
}
async function collectUploadPathsFromDirectory(uploadDir) {
@@ -981,6 +985,10 @@ function createUploadSyncService(options) {
continue;
}
if (isLocalLikeBaseUrl(taskPayload.playerInternalBaseUrl) && await uploadFileExists(operation.localUploadDir, removedUploadRef)) {
continue;
}
const deleted = await removeUploadFileFromPlayer(removedUploadRef, operation.localUploadDir, taskPayload.playerInternalBaseUrl, taskPayload.playerIdentifier);
if (!deleted) {
queuePlayerUploadSync({