mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-08 00:50:59 +01:00
fix(docker): send ROBOTS_NOINDEX as a header, make COMPRESSION_MODE work and fix the Docker guide example
This commit is contained in:
@@ -1,220 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
import { createReadStream, createWriteStream, readdirSync, statSync, unlinkSync } from 'fs';
|
||||
import { join, extname } from 'path';
|
||||
import { createBrotliCompress, createGzip, constants } from 'zlib';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
const DIST_DIR = './dist';
|
||||
|
||||
// File extensions to compress
|
||||
const EXTENSIONS_TO_COMPRESS = [
|
||||
'.wasm',
|
||||
'.whl',
|
||||
'.data',
|
||||
'.zip',
|
||||
'.json'
|
||||
];
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
import { promisify } from 'util';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DIST_DIR = path.resolve(__dirname, '../dist');
|
||||
const COMPRESSIBLE = /\.(html|js|mjs|css|json|svg|wasm|xml|txt|webmanifest)$/i;
|
||||
const MIN_SIZE = 1024;
|
||||
const MODES = { o: [], g: ['.gz'], b: ['.br'], all: ['.gz', '.br'] };
|
||||
|
||||
const mode = process.env.COMPRESSION_MODE || process.argv[2] || 'all';
|
||||
const brotli = promisify(zlib.brotliCompress);
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const ENCODERS = {
|
||||
'.gz': (data) => gzip(data, { level: 9 }),
|
||||
'.br': (data) =>
|
||||
brotli(data, {
|
||||
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 },
|
||||
}),
|
||||
};
|
||||
|
||||
const VALID_MODES = ['g', 'b', 'o', 'all'];
|
||||
if (!VALID_MODES.includes(mode)) {
|
||||
console.error(`❌ Invalid compression mode: ${mode}`);
|
||||
console.error(` Valid modes: g (gzip only), b (brotli only), o (original only), all (all formats)`);
|
||||
process.exit(1);
|
||||
const mode = process.env.COMPRESSION_MODE || 'o';
|
||||
if (!MODES[mode]) {
|
||||
console.error(
|
||||
`Invalid COMPRESSION_MODE "${mode}". Use o (none), g (.gz), b (.br) or all (.gz and .br).`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔧 Compression mode: ${mode}`);
|
||||
console.log(` ${getModeDescription(mode)}\n`);
|
||||
|
||||
function getModeDescription(mode) {
|
||||
const descriptions = {
|
||||
g: 'gzip ONLY (keeps .gz, deletes originals and .br)',
|
||||
b: 'brotli ONLY (keeps .br, deletes originals and .gz)',
|
||||
o: 'original ONLY (keeps originals, deletes .gz and .br)',
|
||||
all: 'ALL formats (keeps .gz, .br, and originals)'
|
||||
};
|
||||
return descriptions[mode];
|
||||
function listFiles(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return listFiles(full);
|
||||
return entry.isFile() ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function getAllFiles(dir, files = []) {
|
||||
const entries = readdirSync(dir);
|
||||
const queue = MODES[mode].length
|
||||
? listFiles(DIST_DIR).filter(
|
||||
(file) => COMPRESSIBLE.test(file) && fs.statSync(file).size >= MIN_SIZE
|
||||
)
|
||||
: [];
|
||||
let written = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
getAllFiles(fullPath, files);
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
}
|
||||
async function compress(file) {
|
||||
const { mtimeMs } = fs.statSync(file);
|
||||
const stale = MODES[mode].filter((ext) => {
|
||||
const copy = `${file}${ext}`;
|
||||
return !fs.existsSync(copy) || fs.statSync(copy).mtimeMs < mtimeMs;
|
||||
});
|
||||
if (!stale.length) return;
|
||||
const data = await fs.promises.readFile(file);
|
||||
for (const ext of stale) {
|
||||
const encoded = await ENCODERS[ext](data);
|
||||
if (encoded.length < data.length) {
|
||||
await fs.promises.writeFile(`${file}${ext}`, encoded);
|
||||
written++;
|
||||
} else {
|
||||
fs.rmSync(`${file}${ext}`, { force: true });
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress a file with Brotli (level 11)
|
||||
*/
|
||||
async function compressBrotli(inputPath, outputPath) {
|
||||
const brotli = createBrotliCompress({
|
||||
params: {
|
||||
[constants.BROTLI_PARAM_QUALITY]: 11,
|
||||
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC,
|
||||
},
|
||||
});
|
||||
|
||||
await pipeline(
|
||||
createReadStream(inputPath),
|
||||
brotli,
|
||||
createWriteStream(outputPath)
|
||||
);
|
||||
async function worker() {
|
||||
for (let file = queue.pop(); file; file = queue.pop()) {
|
||||
await compress(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress a file with gzip (level 9)
|
||||
*/
|
||||
async function compressGzip(inputPath, outputPath) {
|
||||
const gzip = createGzip({ level: 9 });
|
||||
await Promise.all(Array.from({ length: os.availableParallelism() }, worker));
|
||||
|
||||
await pipeline(
|
||||
createReadStream(inputPath),
|
||||
gzip,
|
||||
createWriteStream(outputPath)
|
||||
);
|
||||
}
|
||||
|
||||
function deleteCompressedFiles() {
|
||||
console.log('🗑️ Deleting compressed files...\n');
|
||||
const allFiles = getAllFiles(DIST_DIR);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of allFiles) {
|
||||
if (file.endsWith('.br') || file.endsWith('.gz')) {
|
||||
unlinkSync(file);
|
||||
deletedCount++;
|
||||
console.log(` Deleted: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Deleted ${deletedCount} compressed files\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete original files (keep only compressed versions)
|
||||
*/
|
||||
function deleteOriginalFiles(filePaths, extension) {
|
||||
console.log(`🗑️ Deleting original files (keeping only ${extension})...\n`);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of filePaths) {
|
||||
try {
|
||||
unlinkSync(file);
|
||||
deletedCount++;
|
||||
console.log(` Deleted: ${file}`);
|
||||
} catch (error) {
|
||||
console.error(` ⚠️ Failed to delete ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Deleted ${deletedCount} original files\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete specific compressed format
|
||||
*/
|
||||
function deleteCompressedFormat(extension) {
|
||||
console.log(`🗑️ Deleting ${extension} files...\n`);
|
||||
const allFiles = getAllFiles(DIST_DIR);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of allFiles) {
|
||||
if (file.endsWith(extension)) {
|
||||
unlinkSync(file);
|
||||
deletedCount++;
|
||||
console.log(` Deleted: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Deleted ${deletedCount} ${extension} files\n`);
|
||||
}
|
||||
|
||||
async function compressStaticAssets() {
|
||||
if (mode === 'o') {
|
||||
deleteCompressedFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 Scanning for static assets to compress...\n');
|
||||
|
||||
const allFiles = getAllFiles(DIST_DIR);
|
||||
const filesToCompress = allFiles.filter(file => {
|
||||
const ext = extname(file);
|
||||
const stat = statSync(file);
|
||||
|
||||
if (file.endsWith('.br') || file.endsWith('.gz')) {
|
||||
return false;
|
||||
}
|
||||
return EXTENSIONS_TO_COMPRESS.includes(ext) && stat.size >= MIN_SIZE;
|
||||
});
|
||||
|
||||
console.log(`📦 Found ${filesToCompress.length} files to compress\n`);
|
||||
|
||||
let processedCount = 0;
|
||||
const shouldCompressBrotli = mode === 'b' || mode === 'all';
|
||||
const shouldCompressGzip = mode === 'g' || mode === 'all';
|
||||
|
||||
for (const file of filesToCompress) {
|
||||
const stat = statSync(file);
|
||||
const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
try {
|
||||
let brotliStat, gzipStat;
|
||||
|
||||
if (shouldCompressBrotli) {
|
||||
await compressBrotli(file, `${file}.br`);
|
||||
brotliStat = statSync(`${file}.br`);
|
||||
}
|
||||
if (shouldCompressGzip) {
|
||||
await compressGzip(file, `${file}.gz`);
|
||||
gzipStat = statSync(`${file}.gz`);
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
console.log(`✅ ${file}`);
|
||||
console.log(` Original: ${sizeMB} MB`);
|
||||
|
||||
if (brotliStat) {
|
||||
const brotliSizeMB = (brotliStat.size / (1024 * 1024)).toFixed(2);
|
||||
console.log(` Brotli: ${brotliSizeMB} MB (${((brotliStat.size / stat.size) * 100).toFixed(1)}%)`);
|
||||
}
|
||||
|
||||
if (gzipStat) {
|
||||
const gzipSizeMB = (gzipStat.size / (1024 * 1024)).toFixed(2);
|
||||
console.log(` Gzip: ${gzipSizeMB} MB (${((gzipStat.size / stat.size) * 100).toFixed(1)}%)`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to compress ${file}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Compressed ${processedCount}/${filesToCompress.length} files successfully!\n`);
|
||||
|
||||
if (mode === 'g') {
|
||||
deleteCompressedFormat('.br');
|
||||
deleteOriginalFiles(filesToCompress, '.gz');
|
||||
} else if (mode === 'b') {
|
||||
deleteCompressedFormat('.gz');
|
||||
deleteOriginalFiles(filesToCompress, '.br');
|
||||
}
|
||||
}
|
||||
|
||||
compressStaticAssets().catch(err => {
|
||||
console.error('❌ Compression failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
console.log(`Compression mode ${mode}: wrote ${written} precompressed files.`);
|
||||
|
||||
@@ -181,7 +181,7 @@ OPTIONS:
|
||||
--dockerfile <path> Dockerfile to use (default: Dockerfile)
|
||||
--simple-mode Enable Simple Mode
|
||||
--base-url <path> Subdirectory base URL (e.g. /pdf/)
|
||||
--compression <mode> Compression: g, b, o, all (default: all)
|
||||
--compression <mode> Compression: g, b, o, all (default: o)
|
||||
--language <code> Default UI language (e.g. fr, de, es)
|
||||
--brand-name <name> Custom brand name
|
||||
--brand-logo <path> Logo path relative to public/
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DIST_DIR = path.resolve(__dirname, '../dist');
|
||||
|
||||
function listHtml(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return listHtml(full);
|
||||
return entry.name.endsWith('.html') ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
let refreshed = 0;
|
||||
for (const file of listHtml(DIST_DIR)) {
|
||||
const brotli = `${file}.br`;
|
||||
const gzip = `${file}.gz`;
|
||||
if (!fs.existsSync(brotli) && !fs.existsSync(gzip)) continue;
|
||||
const html = fs.readFileSync(file);
|
||||
if (fs.existsSync(brotli)) {
|
||||
fs.writeFileSync(
|
||||
brotli,
|
||||
zlib.brotliCompressSync(html, {
|
||||
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 },
|
||||
})
|
||||
);
|
||||
}
|
||||
if (fs.existsSync(gzip)) {
|
||||
fs.writeFileSync(gzip, zlib.gzipSync(html, { level: 9 }));
|
||||
}
|
||||
refreshed++;
|
||||
}
|
||||
console.log(`Recompressed ${refreshed} precompressed HTML files.`);
|
||||
Reference in New Issue
Block a user