Fix ignored media source module
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const FONT_LIBRARY_DIR_NAME = 'fonts';
|
||||
const FONT_MANIFEST_FILE = 'fonts.json';
|
||||
const FONT_STYLESHEET_FILE = 'fonts.css';
|
||||
const DEFAULT_FONT_FAMILY_FORMATS = [
|
||||
'Arial=Arial,Helvetica,sans-serif',
|
||||
'Comic Sans MS=Comic Sans MS,cursive,sans-serif',
|
||||
'Courier New=Courier New,Courier,monospace',
|
||||
'Georgia=Georgia,serif',
|
||||
'Helvetica=Helvetica,Arial,sans-serif',
|
||||
'Impact=Impact,Charcoal,sans-serif',
|
||||
'Lucida Sans Unicode=Lucida Sans Unicode,Lucida Grande,sans-serif',
|
||||
'Palatino Linotype=Palatino Linotype,Book Antiqua,Palatino,serif',
|
||||
'Tahoma=Tahoma,Geneva,sans-serif',
|
||||
'Times New Roman=Times New Roman,Times,serif',
|
||||
'Trebuchet MS=Trebuchet MS,Helvetica,sans-serif',
|
||||
'Verdana=Verdana,Geneva,sans-serif'
|
||||
];
|
||||
const FONT_FILE_EXTENSIONS = new Map([
|
||||
['.woff2', 'woff2'],
|
||||
['.woff', 'woff'],
|
||||
['.ttf', 'truetype'],
|
||||
['.otf', 'opentype']
|
||||
]);
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function resolveMediaRoot(mediaRootOrUploadDir) {
|
||||
const resolved = path.resolve(String(mediaRootOrUploadDir || '').trim());
|
||||
return path.basename(resolved) === 'uploads' ? path.dirname(resolved) : resolved;
|
||||
}
|
||||
|
||||
function getFontLibraryDir(mediaRootOrUploadDir) {
|
||||
return path.join(resolveMediaRoot(mediaRootOrUploadDir), FONT_LIBRARY_DIR_NAME);
|
||||
}
|
||||
|
||||
function getFontManifestPath(mediaRootOrUploadDir) {
|
||||
return path.join(getFontLibraryDir(mediaRootOrUploadDir), FONT_MANIFEST_FILE);
|
||||
}
|
||||
|
||||
function getFontStylesheetPath(mediaRootOrUploadDir) {
|
||||
return path.join(getFontLibraryDir(mediaRootOrUploadDir), FONT_STYLESHEET_FILE);
|
||||
}
|
||||
|
||||
function getFontStylesheetHref(mediaRootOrUploadDir) {
|
||||
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||
if (!fs.existsSync(stylesheetPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const stat = fs.statSync(stylesheetPath);
|
||||
return `/media/${FONT_LIBRARY_DIR_NAME}/${FONT_STYLESHEET_FILE}?v=${Number(stat.mtimeMs) || Date.now()}`;
|
||||
}
|
||||
|
||||
function normalizeFontEntry(entry) {
|
||||
const fileName = normalizeText(entry && entry.fileName);
|
||||
const family = normalizeText(entry && (entry.family || entry.name || entry.displayName));
|
||||
const extension = path.extname(fileName).toLowerCase();
|
||||
|
||||
return {
|
||||
id: normalizeText(entry && entry.id) || fileName,
|
||||
name: family || fileName.replace(/\.[^.]+$/, ''),
|
||||
family: family || fileName.replace(/\.[^.]+$/, ''),
|
||||
fileName: fileName,
|
||||
format: normalizeText(entry && entry.format) || FONT_FILE_EXTENSIONS.get(extension) || '',
|
||||
enabled: entry && entry.enabled === false ? false : true,
|
||||
createdAt: normalizeText(entry && entry.createdAt),
|
||||
modifiedAt: normalizeText(entry && entry.modifiedAt)
|
||||
};
|
||||
}
|
||||
|
||||
function sortFonts(left, right) {
|
||||
return String(left && left.name || '').localeCompare(String(right && right.name || ''), undefined, { sensitivity: 'base' })
|
||||
|| String(left && left.fileName || '').localeCompare(String(right && right.fileName || ''), undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
function readManifest(mediaRootOrUploadDir) {
|
||||
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8') || '[]');
|
||||
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed && parsed.fonts) ? parsed.fonts : [];
|
||||
return list.map(normalizeFontEntry).filter(function (entry) {
|
||||
return Boolean(entry.fileName) && Boolean(normalizeText(entry.family || entry.name));
|
||||
}).sort(sortFonts);
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildFontFaceRule(entry) {
|
||||
const family = normalizeText(entry.family || entry.name);
|
||||
const fileName = normalizeText(entry.fileName);
|
||||
if (!family || !fileName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const extension = path.extname(fileName).toLowerCase();
|
||||
const format = FONT_FILE_EXTENSIONS.get(extension) || normalizeText(entry.format) || 'woff2';
|
||||
const escapedFamily = family.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
return [
|
||||
'@font-face {',
|
||||
` font-family: '${escapedFamily}';`,
|
||||
` src: url('/media/${FONT_LIBRARY_DIR_NAME}/${encodeURIComponent(fileName)}') format('${format}');`,
|
||||
' font-style: normal;',
|
||||
' font-weight: 400;',
|
||||
' font-display: swap;',
|
||||
'}'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildFontStylesheet(fonts) {
|
||||
const rules = (Array.isArray(fonts) ? fonts : [])
|
||||
.filter(function (font) {
|
||||
return font && font.enabled !== false;
|
||||
})
|
||||
.map(buildFontFaceRule)
|
||||
.filter(Boolean);
|
||||
return rules.length ? `/* Managed fonts */\n\n${rules.join('\n\n')}\n` : '/* Managed fonts */\n';
|
||||
}
|
||||
|
||||
function buildFontFamilyFormats(fonts) {
|
||||
const formatEntries = Array.from(new Set(DEFAULT_FONT_FAMILY_FORMATS.concat((Array.isArray(fonts) ? fonts : [])
|
||||
.filter(function (font) {
|
||||
return font && font.enabled !== false && normalizeText(font.family || font.name);
|
||||
})
|
||||
.map(function (font) {
|
||||
const family = normalizeText(font.family || font.name);
|
||||
return `${family}=${family}`;
|
||||
}))))
|
||||
.sort(function (left, right) {
|
||||
const leftLabel = String(left || '').split('=')[0];
|
||||
const rightLabel = String(right || '').split('=')[0];
|
||||
return leftLabel.localeCompare(rightLabel, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
return formatEntries.join(';');
|
||||
}
|
||||
|
||||
async function ensureFontLibrary(mediaRootOrUploadDir) {
|
||||
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||
|
||||
await fs.promises.mkdir(fontDir, { recursive: true });
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
await fs.promises.writeFile(manifestPath, '[]\n', 'utf8');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(stylesheetPath)) {
|
||||
await fs.promises.writeFile(stylesheetPath, buildFontStylesheet([]), 'utf8');
|
||||
}
|
||||
|
||||
return {
|
||||
fonts: [],
|
||||
fontDir: fontDir,
|
||||
manifestPath: manifestPath,
|
||||
stylesheetPath: stylesheetPath,
|
||||
stylesheetHref: getFontStylesheetHref(mediaRootOrUploadDir),
|
||||
fontFamilyFormats: buildFontFamilyFormats([])
|
||||
};
|
||||
}
|
||||
|
||||
function loadFontLibrary(mediaRootOrUploadDir) {
|
||||
const fonts = readManifest(mediaRootOrUploadDir);
|
||||
return {
|
||||
fonts: fonts,
|
||||
fontDir: getFontLibraryDir(mediaRootOrUploadDir),
|
||||
manifestPath: getFontManifestPath(mediaRootOrUploadDir),
|
||||
stylesheetPath: getFontStylesheetPath(mediaRootOrUploadDir),
|
||||
stylesheetHref: getFontStylesheetHref(mediaRootOrUploadDir),
|
||||
fontFamilyFormats: buildFontFamilyFormats(fonts)
|
||||
};
|
||||
}
|
||||
|
||||
async function saveFontLibrary(mediaRootOrUploadDir, fonts) {
|
||||
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||
const normalizedFonts = (Array.isArray(fonts) ? fonts : []).map(normalizeFontEntry).filter(function (entry) {
|
||||
return Boolean(entry.fileName) && Boolean(normalizeText(entry.family || entry.name));
|
||||
}).sort(sortFonts);
|
||||
|
||||
await fs.promises.mkdir(fontDir, { recursive: true });
|
||||
await fs.promises.writeFile(manifestPath, JSON.stringify(normalizedFonts, null, 2) + '\n', 'utf8');
|
||||
await fs.promises.writeFile(stylesheetPath, buildFontStylesheet(normalizedFonts), 'utf8');
|
||||
return normalizedFonts;
|
||||
}
|
||||
|
||||
function isSupportedFontUpload(file) {
|
||||
const extension = path.extname(String(file && file.originalname || file && file.filename || '')).toLowerCase();
|
||||
return FONT_FILE_EXTENSIONS.has(extension);
|
||||
}
|
||||
|
||||
async function addFontFile(mediaRootOrUploadDir, uploadedFile, requestedFamily) {
|
||||
if (!uploadedFile || !uploadedFile.path || !isSupportedFontUpload(uploadedFile)) {
|
||||
throw new Error('Unsupported font upload.');
|
||||
}
|
||||
|
||||
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||
const fonts = readManifest(mediaRootOrUploadDir);
|
||||
const family = normalizeText(requestedFamily) || normalizeText(path.basename(uploadedFile.originalname || uploadedFile.filename || '', path.extname(uploadedFile.originalname || uploadedFile.filename || '')));
|
||||
if (!family) {
|
||||
throw new Error('Font family is required.');
|
||||
}
|
||||
|
||||
if (fonts.some(function (font) {
|
||||
return String(font.family || font.name || '').trim().toLowerCase() === family.toLowerCase();
|
||||
})) {
|
||||
throw new Error('A font with that family name already exists.');
|
||||
}
|
||||
|
||||
const extension = path.extname(uploadedFile.originalname || uploadedFile.filename || '').toLowerCase();
|
||||
const slug = family.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'font';
|
||||
const fileName = `${slug}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}${extension}`;
|
||||
const destinationPath = path.join(fontDir, fileName);
|
||||
|
||||
await fs.promises.mkdir(fontDir, { recursive: true });
|
||||
await fs.promises.rename(uploadedFile.path, destinationPath);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const nextFonts = fonts.concat({
|
||||
id: fileName,
|
||||
name: family,
|
||||
family: family,
|
||||
fileName: fileName,
|
||||
format: FONT_FILE_EXTENSIONS.get(extension) || 'woff2',
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
modifiedAt: now
|
||||
});
|
||||
|
||||
await saveFontLibrary(mediaRootOrUploadDir, nextFonts);
|
||||
return {
|
||||
font: normalizeFontEntry(nextFonts[nextFonts.length - 1]),
|
||||
fonts: loadFontLibrary(mediaRootOrUploadDir).fonts
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteFontFile(mediaRootOrUploadDir, fontId) {
|
||||
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||
const fonts = readManifest(mediaRootOrUploadDir);
|
||||
const normalizedId = normalizeText(fontId);
|
||||
const index = fonts.findIndex(function (font) {
|
||||
return String(font.id || font.fileName || '').trim() === normalizedId || String(font.fileName || '').trim() === normalizedId;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [removed] = fonts.splice(index, 1);
|
||||
if (removed && removed.fileName) {
|
||||
try {
|
||||
await fs.promises.unlink(path.join(fontDir, removed.fileName));
|
||||
} catch (_error) {
|
||||
// ignore missing file cleanup issues
|
||||
}
|
||||
}
|
||||
|
||||
await saveFontLibrary(mediaRootOrUploadDir, fonts);
|
||||
return normalizeFontEntry(removed);
|
||||
}
|
||||
|
||||
async function setFontEnabled(mediaRootOrUploadDir, fontId, enabled) {
|
||||
const fonts = readManifest(mediaRootOrUploadDir);
|
||||
const normalizedId = normalizeText(fontId);
|
||||
const index = fonts.findIndex(function (font) {
|
||||
return String(font.id || font.fileName || '').trim() === normalizedId || String(font.fileName || '').trim() === normalizedId;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextFonts = fonts.slice();
|
||||
nextFonts[index] = Object.assign({}, nextFonts[index], {
|
||||
enabled: Boolean(enabled),
|
||||
modifiedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
await saveFontLibrary(mediaRootOrUploadDir, nextFonts);
|
||||
return normalizeFontEntry(nextFonts[index]);
|
||||
}
|
||||
|
||||
function collectFontLibraryUploadPaths(mediaRootOrUploadDir) {
|
||||
const fonts = readManifest(mediaRootOrUploadDir);
|
||||
const uploadPaths = new Set();
|
||||
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||
const mediaRoot = resolveMediaRoot(mediaRootOrUploadDir);
|
||||
|
||||
if (manifestPath) {
|
||||
uploadPaths.add('/media/' + path.relative(mediaRoot, manifestPath).replace(/\\/g, '/'));
|
||||
}
|
||||
if (stylesheetPath) {
|
||||
uploadPaths.add('/media/' + path.relative(mediaRoot, stylesheetPath).replace(/\\/g, '/'));
|
||||
}
|
||||
|
||||
fonts.forEach(function (font) {
|
||||
if (font && font.fileName) {
|
||||
uploadPaths.add(`/media/${FONT_LIBRARY_DIR_NAME}/${font.fileName}`);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uploadPaths);
|
||||
}
|
||||
|
||||
async function collectFontLibraryDirectoryUploadPaths(mediaRootOrUploadDir) {
|
||||
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||
try {
|
||||
const entries = await fs.promises.readdir(fontDir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter(function (entry) {
|
||||
return entry && entry.isFile();
|
||||
})
|
||||
.map(function (entry) {
|
||||
return `/media/${FONT_LIBRARY_DIR_NAME}/${entry.name}`;
|
||||
});
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function collectFontLibrarySyncOperations(mediaRootOrUploadDir) {
|
||||
const fonts = readManifest(mediaRootOrUploadDir);
|
||||
const operations = [
|
||||
{ type: 'put', uploadPath: '/media/' + FONT_LIBRARY_DIR_NAME + '/' + FONT_MANIFEST_FILE },
|
||||
{ type: 'put', uploadPath: '/media/' + FONT_LIBRARY_DIR_NAME + '/' + FONT_STYLESHEET_FILE }
|
||||
];
|
||||
|
||||
fonts.forEach(function (font) {
|
||||
if (!font || !font.fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
operations.push({
|
||||
type: font.enabled === false ? 'delete' : 'put',
|
||||
uploadPath: `/media/${FONT_LIBRARY_DIR_NAME}/${font.fileName}`
|
||||
});
|
||||
});
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureFontLibrary: ensureFontLibrary,
|
||||
loadFontLibrary: loadFontLibrary,
|
||||
saveFontLibrary: saveFontLibrary,
|
||||
addFontFile: addFontFile,
|
||||
deleteFontFile: deleteFontFile,
|
||||
setFontEnabled: setFontEnabled,
|
||||
collectFontLibraryUploadPaths: collectFontLibraryUploadPaths,
|
||||
collectFontLibraryDirectoryUploadPaths: collectFontLibraryDirectoryUploadPaths,
|
||||
collectFontLibrarySyncOperations: collectFontLibrarySyncOperations,
|
||||
getFontStylesheetHref: getFontStylesheetHref,
|
||||
buildFontFamilyFormats: buildFontFamilyFormats,
|
||||
isSupportedFontUpload: isSupportedFontUpload,
|
||||
getFontLibraryDir: getFontLibraryDir,
|
||||
getFontManifestPath: getFontManifestPath,
|
||||
getFontStylesheetPath: getFontStylesheetPath
|
||||
};
|
||||
Reference in New Issue
Block a user