/**
* 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 });
}
}
};