All files / scripts/lib/media mediaDownloader.js

93.61% Statements 44/47
82.85% Branches 29/35
100% Functions 3/3
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 615x 5x 5x 5x   5x 5x 5x     11x       3x                           10x 10x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 8x 8x 6x 5x 5x 4x 4x 3x 3x   2x 2x       5x     5x  
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { getAxios } = require('../axiosLoader');
 
const MEDIA_ROOT = path.join(__dirname, '..', '..', 'public', 'projects_media');
const MAX = 2 * 1024 * 1024; // 2 MB
const DEBUG_FETCH = process.env.DEBUG_FETCH === '1' || process.env.DEBUG_FETCH === 'true';
 
function ensureDir(dir) {
  try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (e) {}
}
 
function md5(text) {
  try { return crypto.createHash('md5').update(String(text || '')).digest('hex'); } catch (e) { return null; }
}
 
/**
 * Downloads a remote image into `public/projects_media/<repoName>/` with a
 * deterministic filename (sanitized basename + md5(url) suffix).
 * Skips files already on disk, enforces a 2 MB cap, and rejects non-image Content-Types.
 *
 * @param {string} repoName - GitHub repo name; used as the media subfolder
 * @param {string} url - Absolute URL of the image to download
 * @param {object} [opts] - Reserved for future overrides (currently unused)
 * @returns {Promise<string|null>} Filename relative to the repo media dir, or null on failure
 */
async function downloadIfNeeded(repoName, url, opts = {}) {
  try {
    if (!url) return null;
    const axios = getAxios();
    const u = String(url).split('?')[0];
    let ext = path.extname(u).toLowerCase();
    if (!ext || ext.length > 6) ext = '.png';
    const safeBase = path.basename(u).replace(/[^a-z0-9._-]/gi, '-').replace(/^-+|-+$/g, '');
    const hash = crypto.createHash('md5').update(String(url)).digest('hex').slice(0, 8);
    const fn = `${safeBase}-${hash}${ext}`;
    const destDir = path.join(MEDIA_ROOT, repoName);
    ensureDir(destDir);
    const outPath = path.join(destDir, fn);
    if (fs.existsSync(outPath)) return fn; // already present
    Iif (!axios) return null;
    const res = await axios.get(url, { responseType: 'arraybuffer', maxRedirects: 5, timeout: 15000 });
    if (!res || !res.data) return null;
    const buf = Buffer.from(res.data);
    if (buf.length > MAX) return null;
    const ct = (res.headers && (res.headers['content-type'] || res.headers['Content-Type'])) || '';
    if (!/image\//i.test(ct) && !/\.(png|jpe?g|gif|svg)$/i.test(ext)) return null;
    fs.writeFileSync(outPath, buf);
    return fn;
  } catch (e) {
    Iif (DEBUG_FETCH) console.log('mediaDownloader.downloadIfNeeded failed', url, e && e.message);
    return null;
  }
}
 
const persistence = require('./persistence');
 
 
module.exports = { ensureDir, md5, downloadIfNeeded, persistMetaForNode: persistence.persistMetaForNode };