Release 1.5.3
This commit is contained in:
+235
-2
@@ -1,3 +1,5 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { version: appVersion } = require('../../package.json');
|
||||
|
||||
function parseVersion(value) {
|
||||
@@ -260,6 +262,27 @@ function replaceUploadPrefixInValue(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function replaceLegacyMediaUploadPathInValue(value) {
|
||||
if (typeof value === 'string') {
|
||||
if (!value.startsWith('/media/') || value.startsWith('/media/uploads/')) {
|
||||
return value;
|
||||
}
|
||||
return '/media/uploads/' + path.basename(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(function (item) {
|
||||
return replaceLegacyMediaUploadPathInValue(item);
|
||||
});
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.keys(value).reduce(function (result, key) {
|
||||
result[key] = replaceLegacyMediaUploadPathInValue(value[key]);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseJsonValue(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
@@ -327,6 +350,187 @@ async function backfillLegacyMediaPaths(pool) {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveFileIfMissing(sourcePath, targetPath) {
|
||||
try {
|
||||
await fs.promises.access(targetPath, fs.constants.F_OK);
|
||||
return false;
|
||||
} catch (_error) {
|
||||
// target does not exist
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.access(sourcePath, fs.constants.F_OK);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
|
||||
try {
|
||||
await fs.promises.rename(sourcePath, targetPath);
|
||||
} catch (error) {
|
||||
if (error && error.code === 'EXDEV') {
|
||||
await fs.promises.copyFile(sourcePath, targetPath);
|
||||
await fs.promises.unlink(sourcePath);
|
||||
return true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function backfillLegacyMediaUploadsToSubfolder(pool, mediaDir) {
|
||||
const normalizedMediaDir = String(mediaDir || '').trim() ? path.resolve(String(mediaDir).trim()) : null;
|
||||
if (!normalizedMediaDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadsDir = path.join(normalizedMediaDir, 'uploads');
|
||||
await fs.promises.mkdir(uploadsDir, { recursive: true });
|
||||
|
||||
const [slides] = await pool.query(`
|
||||
SELECT id, media_path, content_json
|
||||
FROM slides
|
||||
WHERE media_path LIKE '/media/%'
|
||||
OR content_json LIKE '%/media/%'
|
||||
`);
|
||||
|
||||
for (const slide of slides || []) {
|
||||
let mediaPath = String(slide.media_path || '').trim() || null;
|
||||
let contentJson = slide.content_json;
|
||||
let changed = false;
|
||||
|
||||
if (mediaPath && mediaPath.startsWith('/media/') && !mediaPath.startsWith('/media/uploads/')) {
|
||||
const fileName = path.basename(mediaPath);
|
||||
await moveFileIfMissing(path.join(normalizedMediaDir, fileName), path.join(uploadsDir, fileName));
|
||||
mediaPath = '/media/uploads/' + fileName;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const parsedContent = parseJsonValue(contentJson);
|
||||
if (parsedContent && typeof parsedContent === 'object') {
|
||||
const updatedContent = replaceLegacyMediaUploadPathInValue(parsedContent);
|
||||
if (JSON.stringify(updatedContent) !== JSON.stringify(parsedContent)) {
|
||||
contentJson = JSON.stringify(updatedContent);
|
||||
changed = true;
|
||||
}
|
||||
} else if (typeof parsedContent === 'string' && parsedContent.indexOf('/media/') !== -1) {
|
||||
const updatedContent = replaceLegacyMediaUploadPathInValue(parsedContent);
|
||||
if (updatedContent !== parsedContent) {
|
||||
contentJson = updatedContent;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await pool.query('UPDATE slides SET media_path = ?, content_json = ? WHERE id = ?', [mediaPath, contentJson, slide.id]);
|
||||
}
|
||||
}
|
||||
|
||||
const [templates] = await pool.query(`
|
||||
SELECT id, background_image_path
|
||||
FROM slide_templates
|
||||
WHERE background_image_path LIKE '/media/%'
|
||||
`);
|
||||
|
||||
for (const template of templates || []) {
|
||||
const backgroundImagePath = String(template.background_image_path || '').trim();
|
||||
if (!backgroundImagePath.startsWith('/media/') || backgroundImagePath.startsWith('/media/uploads/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileName = path.basename(backgroundImagePath);
|
||||
await moveFileIfMissing(path.join(normalizedMediaDir, fileName), path.join(uploadsDir, fileName));
|
||||
await pool.query(
|
||||
'UPDATE slide_templates SET background_image_path = ? WHERE id = ?',
|
||||
['/media/uploads/' + fileName, template.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function backfillLooseMediaFilesToUploadsSubfolder(pool, mediaDir) {
|
||||
const normalizedMediaDir = String(mediaDir || '').trim() ? path.resolve(String(mediaDir).trim()) : null;
|
||||
if (!normalizedMediaDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadsDir = path.join(normalizedMediaDir, 'uploads');
|
||||
await fs.promises.mkdir(uploadsDir, { recursive: true });
|
||||
|
||||
const directoryEntries = await fs.promises.readdir(normalizedMediaDir, { withFileTypes: true });
|
||||
for (const entry of directoryEntries || []) {
|
||||
if (!entry || !entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileName = String(entry.name || '').trim();
|
||||
if (!fileName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourcePath = path.join(normalizedMediaDir, fileName);
|
||||
const targetPath = path.join(uploadsDir, fileName);
|
||||
await moveFileIfMissing(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
const [slides] = await pool.query(`
|
||||
SELECT id, media_path, content_json
|
||||
FROM slides
|
||||
WHERE media_path LIKE '/media/%'
|
||||
OR content_json LIKE '%/media/%'
|
||||
`);
|
||||
|
||||
for (const slide of slides || []) {
|
||||
let mediaPath = String(slide.media_path || '').trim() || null;
|
||||
let contentJson = slide.content_json;
|
||||
let changed = false;
|
||||
|
||||
if (mediaPath && mediaPath.startsWith('/media/') && !mediaPath.startsWith('/media/uploads/')) {
|
||||
mediaPath = '/media/uploads/' + path.basename(mediaPath);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const parsedContent = parseJsonValue(contentJson);
|
||||
if (parsedContent && typeof parsedContent === 'object') {
|
||||
const updatedContent = replaceLegacyMediaUploadPathInValue(parsedContent);
|
||||
if (JSON.stringify(updatedContent) !== JSON.stringify(parsedContent)) {
|
||||
contentJson = JSON.stringify(updatedContent);
|
||||
changed = true;
|
||||
}
|
||||
} else if (typeof parsedContent === 'string' && parsedContent.indexOf('/media/') !== -1) {
|
||||
const updatedContent = replaceLegacyMediaUploadPathInValue(parsedContent);
|
||||
if (updatedContent !== parsedContent) {
|
||||
contentJson = updatedContent;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await pool.query('UPDATE slides SET media_path = ?, content_json = ? WHERE id = ?', [mediaPath, contentJson, slide.id]);
|
||||
}
|
||||
}
|
||||
|
||||
const [templates] = await pool.query(`
|
||||
SELECT id, background_image_path
|
||||
FROM slide_templates
|
||||
WHERE background_image_path LIKE '/media/%'
|
||||
`);
|
||||
|
||||
for (const template of templates || []) {
|
||||
const backgroundImagePath = String(template.background_image_path || '').trim();
|
||||
if (!backgroundImagePath.startsWith('/media/') || backgroundImagePath.startsWith('/media/uploads/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileName = path.basename(backgroundImagePath);
|
||||
await pool.query(
|
||||
'UPDATE slide_templates SET background_image_path = ? WHERE id = ?',
|
||||
['/media/uploads/' + fileName, template.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureMigrationTable(pool) {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
@@ -410,6 +614,7 @@ const migrations = [
|
||||
|
||||
// v1.4.6: playlists gained fade_between_slides plus audit fields.
|
||||
await addColumnIfMissing(pool, 'playlists', 'fade_between_slides', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
await addColumnIfMissing(pool, 'playlists', 'skip_unavailable_rtmp', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
await addColumnIfMissing(pool, 'playlists', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
||||
await addAuditColumns(pool, 'playlists');
|
||||
|
||||
@@ -433,6 +638,7 @@ const migrations = [
|
||||
await addColumnIfMissing(pool, 'slides', 'content_json', 'JSON NULL');
|
||||
await addColumnIfMissing(pool, 'slides', 'media_path', 'VARCHAR(512) NULL');
|
||||
await addColumnIfMissing(pool, 'slides', 'media_type', 'VARCHAR(100) NULL');
|
||||
await addColumnIfMissing(pool, 'slides', 'thumbnail_path', 'VARCHAR(512) NULL');
|
||||
await addColumnIfMissing(pool, 'slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
||||
await addAuditColumns(pool, 'slides');
|
||||
|
||||
@@ -517,6 +723,15 @@ const migrations = [
|
||||
await addAuditColumns(pool, 'auth_sessions');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'playlist-skip-unavailable-rtmp',
|
||||
version: appVersion,
|
||||
comment: 'Add the playlist flag that skips RTMP slides when streams are unavailable.',
|
||||
order: 21,
|
||||
up: async function (pool) {
|
||||
await addColumnIfMissing(pool, 'playlists', 'skip_unavailable_rtmp', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'media-path-prefix-rename',
|
||||
version: appVersion,
|
||||
@@ -526,6 +741,24 @@ const migrations = [
|
||||
await backfillLegacyMediaPaths(pool);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'media-upload-subfolder-move',
|
||||
version: appVersion,
|
||||
comment: 'Move existing upload files and references from the media root into media/uploads.',
|
||||
order: 26,
|
||||
up: async function (pool, options) {
|
||||
await backfillLegacyMediaUploadsToSubfolder(pool, options && options.mediaDir);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'media-upload-subfolder-rescue',
|
||||
version: appVersion,
|
||||
comment: 'Rescan loose media files and promote them into media/uploads.',
|
||||
order: 27,
|
||||
up: async function (pool, options) {
|
||||
await backfillLooseMediaFilesToUploadsSubfolder(pool, options && options.mediaDir);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'background-tasks-table',
|
||||
version: appVersion,
|
||||
@@ -556,7 +789,7 @@ const migrations = [
|
||||
}
|
||||
];
|
||||
|
||||
async function runMigrations(pool) {
|
||||
async function runMigrations(pool, options) {
|
||||
const appliedRows = await getAppliedMigrationRows(pool);
|
||||
const appliedKeys = new Set((appliedRows || []).map(function (row) {
|
||||
return String(row.migration_key || '').trim();
|
||||
@@ -576,7 +809,7 @@ async function runMigrations(pool) {
|
||||
});
|
||||
|
||||
for (const migration of pendingMigrations) {
|
||||
await migration.up(pool);
|
||||
await migration.up(pool, options || {});
|
||||
await recordMigration(pool, migration);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user