Source: Router/languageFile/controller.js

/**
 * Language File Controller Layer
 * Handles HTTP request/response and delegates to the service layer.
 */

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
 */

import { s3 } from '../../utils/s3Client.js';
import { uploadLanguage } from '../../utils/uploadLanguage.js';
import { errorLoggerUpdate } from '../../utils/requestLogger.js';
import * as languageFileService from './service.js';

/**
 * List all available language files.
 * Handles GET /
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 * @returns {Promise<void>}
 */
export const listLanguageFilesController = async (req, res) => {
    try {
        const result = await languageFileService.listLanguageFiles();
        res.json({ success: true, result });
    } catch (e) {
        errorLoggerUpdate(e);
        const err = /** @type {Error} */ (e);
        res.status(500).json({ success: false, message: err.message });
    }
};

/**
 * List language files for a specific application.
 * Handles GET /:app
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 * @returns {Promise<void>}
 */
export const listLanguageFilesByAppController = async (req, res) => {
    try {
        const result = await languageFileService.listLanguageFilesByApp(req.params.app);
        res.json({ success: true, result });
    } catch (e) {
        errorLoggerUpdate(e);
        const err = /** @type {Error} */ (e);
        res.status(500).json({ success: false, message: err.message });
    }
};

/**
 * Upload a language file.
 * Handles POST /:api
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 * @returns {Promise<void>}
 */
export const uploadLanguageFileController = async (req, res) => {
    try {
        const uploadedFileData = await uploadLanguage(req, /** @type {any} */ ({
            s3,
            mimeFilter: languageFileService.filterJSON,
            uploadUrlGen: languageFileService.languageUrlGen,
            Bucket: languageFileService.bucket,
            width: 300
        }));

        // Public URL of each uploaded file, derived from its object key so the
        // organization layer (`<api>/languages/<UIDroot>/<file>`) gets a correct
        // URL too.
        const urls = /** @type {any[]} */ (uploadedFileData).map(
            /** @param {{Key: string}} el */
            el => `${process.env.baseUrl}/api/kpe20/languages/${String(el.Key).replace(/^[^/]+\/languages\//, '')}`
        );

        const fileNames = /** @type {any[]} */ (uploadedFileData).map(
            /** @param {{Key: string}} el */ el => el.Key.split('/')[el.Key.split('/').length - 1]
        );

        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                message: 'OK',
                files: uploadedFileData,
                name: uploadedFileData[0].originalname,
                url: urls[1] ? urls[1] : urls[0],
                fileNames,
                urls,
                Data: uploadedFileData,
                prefix: req.params.UIDroot ? `${req.params.UIDroot}/languages` : 'languages'
            });
        } else {
            res.json({ success: true, message: 'file already existed' });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        const err = /** @type {Error} */ (e);
        res.status(500).json({ success: false, message: err.message });
    }
};

/**
 * Stream a language file to the client.
 * Handles GET /:app/:filename and GET /:app/:UIDroot/:filename
 *
 * The organization layer is only readable for the organization of the session.
 * The portal routes in `http-server.js` run without `checkRoot`, so the check
 * has to happen here.
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 * @returns {Promise<void>}
 */
export const getLanguageFileController = async (req, res) => {
    const { app, filename } = req.params;
    const UIDroot = req.params.UIDroot ?? null;

    if (UIDroot && UIDroot !== req.session?.root) {
        res.status(403).json({ success: false, message: 'not accessible' });
        return;
    }

    try {
        const meta = await languageFileService.statLanguageFile(app, filename, UIDroot);

        if (!meta) {
            res.status(410).json({ success: false, message: 'not available', code: 'NoSuchKey' });
            return;
        }

        // Language files change rarely, but are requested on every app start.
        // Always revalidate (no-cache) so an edit shows up immediately, and
        // answer with 304 when the client already has the current revision.
        const etag = `"${meta.etag}"`;
        res.set('ETag', etag);
        res.set('Cache-Control', 'private, no-cache');
        res.set('Last-Modified', new Date(meta.lastModified).toUTCString());

        if (req.headers['if-none-match'] === etag) {
            res.status(304).end();
            return;
        }

        const data = await languageFileService.getLanguageFile(app, filename, UIDroot);

        res.set('Content-Type', 'application/octetstream');
        res.set('Content-Disposition', `attachment; filename="${filename}"`);

        data.on('error', (streamErr) => {
            console.error('Stream error:', streamErr);
            if (!res.headersSent) {
                res.status(500).json({ success: false, message: 'Stream error' });
            }
        });

        data.pipe(res);
    } catch (e) {
        const err = /** @type {any} */ (e);
        console.error(`S3 error for ${languageFileService.languageKey(app, filename, UIDroot)}:`, err.code);
        if (!res.headersSent) {
            const status = err.code === 'NoSuchKey' || err.code === 'NotFound' ? 410 : 500;
            res.status(status).json({ success: false, message: 'not available', code: err.code });
        }
    }
};

/**
 * Delete a language file from the object store.
 * Handles DELETE /:app/:filename and DELETE /:app/:UIDroot/:filename
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 * @returns {Promise<void>}
 */
export const deleteLanguageFileController = async (req, res) => {
    try {
        const result = await languageFileService.deleteLanguageFile(
            req.params.app,
            req.params.filename,
            req.params.UIDroot ?? null
        );

        res.json({ success: true, result });
    } catch (e) {
        console.error('Error deleting language file:', e);
        errorLoggerUpdate(e);
        const err = /** @type {Error} */ (e);
        if (!res.headersSent) {
            res.status(500).json({ success: false, message: 'Error deleting file', error: err.message });
        }
    }
};