Source: Router/files/controller.js

/**
 * Files Controller
 * 
 * Contains all the business logic for file operations:
 * - File uploads (private, public, images, PDFs, avatars, banners)
 * - File downloads with access control
 * - File listing and directory operations
 * - File deletion and cleanup
 * - Language and configuration file management
 * - Public file access with expiration
 * - Log file access for administrators
 * 
 * Handles various file types with different access levels and processing options.
 * Integrates with S3-compatible storage for scalable file management.
 */

// @ts-check
import './../../types.js';
import { busboyUpload } from '../../utils/uploadBusboy.js';
import { uploadLanguage } from '../../utils/uploadLanguage.js';
import { UUID2hex, query } from '@commtool/sql-query';
import { isAdmin, isVisible } from '../../utils/authChecks.js';
import cryptoRandomString from 'crypto-random-string';
import { requestUpdateLogger, errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import path from 'path';
import fs from 'fs';
import { s3, myMinioClient } from '../../utils/s3Client.js';

const bucket = process.env.bucket ? process.env.bucket : 'kpe20';

/**
 * Helper function to filter image file types
 */
const filterImage = (mimeType, extension) => {
    return ['image/jpeg', 'image/png', 'image/svg+xml', 'image/gif', 'image/tiff'].includes(mimeType) &&
        ['jpg', 'tiff', 'png', 'svg', 'gif', 'jpeg'].includes(extension);
};

/**
 * Helper function to filter image and PDF file types
 */
const filterImagePdf = (mimeType, extension) => {
    return ['image/jpeg', 'image/png', 'image/svg+xml', 'image/gif', 'image/tiff', 'application/pdf'].includes(mimeType) &&
        ['jpg', 'tiff', 'png', 'svg', 'gif', 'jpeg', 'pdf'].includes(extension);
};

/**
 * Helper function to filter JSON file types
 */
const filterJSON = (mimeType, extension) => {
    return ['application/json'].includes(mimeType) && ['json'].includes(extension);
};

/**
 * Middleware to check file expiration parameters
 */
export const checkExpire = async (req, res, next) => {
    try {
        if (['7', '30', '100', '365'].includes(req.params.expire)) {
            next();
            return;
        } else if (await isAdmin(req.session)) {
            if (req.params.expire === '0' || req.params.expire === 'dashboard') {
                next();
                return;
            } else {
                res.json({ success: false, message: 'you have to supply 7,30,100 or 365 for expiring links and 0 for none expiring links as the expire parameter' });
                return;
            }
        }
        res.status(401).json({ success: false, message: 'valid expire parameters are 7,30,100 or 365 for none admin users' });
        return;
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Helper function to generate key components for file paths
 */
const keyComponents = (key, UID) => {
    return `${UID}/${encodeURIComponent(key.replace(`${UID}/`, ''))}`;
};

/**
 * URL generator for private file uploads
 */
const privateUrlGen = ({ fields, filename, extension, params }) => {
    let prefix = fields.prefix;
    const UID = params.UID;
    if (prefix.charAt(prefix.length - 1) !== '/') {
        prefix += '/';
    }
    return `${UID}/${prefix}${filename.filename}`;
};

/**
 * URL generator for changelog file uploads
 */
const changelogUrlGen = ({ fields, filename, extension, params }) => {
    const api = params.api;
    return `config/${params.api}/changelog.yaml`;
};

/**
 * URL generator for banner file uploads
 */
const bannerUrlGen = ({ fields, filename, extension, params }) => {
    const UID = params.UID;
    return `${UID}/banner/banner-${Date.now().toString()}.${extension}`;
};

/**
 * URL generator for avatar file uploads
 */
const avatarUrlGen = ({ fields, filename, extension, params }) => {
    const UID = params.UID;
    return `${UID}/avatar/avatar.${extension}`;
};

/**
 * URL generator for public file uploads
 */
const publicUrlGen = ({ fields, filename, params }) => {
    const urlKey = cryptoRandomString({ length: 16, type: 'alphanumeric' });
    const extension = filename.filename.split('.')[filename.filename.split('.').length - 1];
    return 'public/' + params.expire + '/' + params.UID + '/' + urlKey + '.' + extension;
};

/**
 * URL generator for language file uploads
 */
const languageUrlGen = ({ fields, filename, extension, params }) => {
    return `${params.api}/languages/${filename.filename}`;
};

/**
 * Helper function to delete directories recursively
 */
const deleteDir = async (prefix, recursive = false) => {
    try {
        return new Promise((fulfill, reject) => {
            const objects = [];
            const stream = myMinioClient.extensions.listObjectsV2WithMetadata(bucket, prefix, recursive);
            stream.on('data', function (obj) {
                objects.push(obj.name);
            });
            stream.on('end', () => {
                if (objects.length > 0) {
                    myMinioClient.removeObjects(bucket, objects, function (err) {
                        if (err) {
                            reject(err);
                        } else {
                            fulfill({ success: true, message: 'OK', deleted: objects.length });
                        }
                    });
                } else {
                    fulfill({ success: true, message: 'OK', deleted: 0 });
                }
            });
        });
    } catch (e) {
        errorLoggerUpdate(e);
        throw e;
    }
};

/**
 * Middleware to check file visibility permissions
 */
export const checkFileVisible = async (req, res, next) => {
    try {
        if (['public', 'languages'].includes(req.params.UID)) {
            next();
            return;
        }
        const UID = UUID2hex(req.params.UID);
        if (isVisible(UID, UUID2hex(req.session.user))) {
            next();
        } else {
            // Check if we have a type with public file access
            const type = await query(`SELECT Type FROM ObjectBase WHERE UID=? AND Type IN(?)`, [UID, ['actionT', 'functionT']]);
            if (type.length === 1) {
                next();
            } else {
                res.status(410).json({ success: false, message: 'not accessible for this user' });
            }
        }
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload language or changelog files (POST /:type(language|changelog)/:api)
 */
export const uploadLanguageOrChangelog = async (req, res) => {
    try {
        let uploadedFileData;
        if (req.params.type === 'language') {
            uploadedFileData = await uploadLanguage(req, {
                s3,
                mimeFilter: filterJSON,
                uploadUrlGen: languageUrlGen,
                Bucket: bucket
            });
        } else {
            uploadedFileData = await busboyUpload(req, {
                s3,
                uploadUrlGen: changelogUrlGen,
                Bucket: bucket
            });
        }

        const urls = uploadedFileData.map(el => process.env.baseUrl + '/api/kpe20/files/' + keyComponents(el.Key, req.params.UID)) + '/languages';
        const fileNames = uploadedFileData.map(el => el.Key.split('/')[el.Key.split('/').length - 1]);
        
        if (uploadedFileData.length > 0) {
            res.json({ success: true, message: 'OK', fileNames: fileNames, urls: urls, Data: uploadedFileData });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload private files (POST /:UID)
 */
export const uploadPrivateFiles = async (req, res) => {
    try {
        req.query.UID = req.params.UID;
        let uploadedFileData;

        const uploadConfig = {
            s3,
            uploadUrlGen: privateUrlGen,
            Bucket: bucket,
            width: req.query.width ? parseInt(req.query.width) : null,
            viewportScale: req.query.scale ? req.query.scale : null
        };

        try {
            if (req.query.metadata) {
                uploadConfig.extraMetaData = JSON.parse(String(req.query.metadata));
            }
        } catch (e) {
            res.json({ success: false, message: 'extraMetaData: invalid json' });
            return;
        }

        uploadedFileData = await busboyUpload(req, uploadConfig);

        const urls = uploadedFileData.map(el => process.env.baseUrl + '/api/kpe20/files/' + keyComponents(el.Key, req.params.UID));
        const fileNames = uploadedFileData.map(el => el.Key.split('/')[el.Key.split('/').length - 1]);
        
        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                message: 'OK',
                files: req.files,
                name: uploadedFileData[0].originalname,
                url: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload image and PDF files (POST /imagePdf/:UID)
 */
export const uploadImagePdfFiles = async (req, res) => {
    try {
        req.query.UID = req.params.UID;
        const uploadedFileData = await busboyUpload(req, {
            mimeFilter: filterImagePdf,
            s3,
            uploadUrlGen: privateUrlGen,
            Bucket: bucket,
            width: req.query.width ? parseInt(req.query.width) : 1024,
            viewportScale: req.query.scale ? req.query.scale : '1.0'
        });

        const urls = uploadedFileData.map(el => process.env.baseUrl + '/api/kpe20/files/' + keyComponents(el.Key, req.params.UID));
        const fileNames = uploadedFileData.map(el => el.Key.split('/')[el.Key.split('/').length - 1]);
        
        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                message: 'OK',
                name: uploadedFileData[0].originalname,
                url: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload banner images (POST /banner/:UID)
 */
export const uploadBannerFiles = async (req, res) => {
    try {
        await deleteDir(`${req.params.UID}/banner/`);
        const uploadedFileData = await busboyUpload(req, {
            s3,
            mimeFilter: filterImage,
            uploadUrlGen: bannerUrlGen,
            Bucket: bucket,
            width: 50
        });

        const urls = uploadedFileData.map(el => process.env.baseUrl + '/api/kpe20/files/' + keyComponents(el.Key, req.params.UID));
        const fileNames = uploadedFileData.map(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,
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData,
                prefix: req.params.UID + '/banner'
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload avatar images (POST /avatar/:UID)
 */
export const uploadAvatarFiles = async (req, res) => {
    try {
        const uploadedFileData = await busboyUpload(req, {
            s3,
            mimeFilter: filterImage,
            uploadUrlGen: avatarUrlGen,
            Bucket: bucket,
            width: 300
        });

        const urls = uploadedFileData.map(el => process.env.baseUrl + '/api/kpe20/files/' + keyComponents(el.Key, req.params.UID));
        const fileNames = uploadedFileData.map(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: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData,
                prefix: req.params.UID + '/avatar'
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload public rail files for apps (POST /rail/:app/)
 */
export const uploadPublicRailFiles = async (req, res) => {
    try {
        req.query.UID = req.params.UID;
        const uploadedFileData = await busboyUpload(req, {
            s3,
            uploadUrlGen: ({ fields, filename, extension, params }) => {
                return `public/${req.params.app}/${fields.prefix}/${filename.filename}`;
            },
            Bucket: bucket,
            original: false,
            width: req.query.width ? parseInt(req.query.width) : 400,
            viewportScale: req.query.scale ? req.query.scale : '1.0'
        });

        const urls = uploadedFileData.filter(el => el.Key).map(el => process.env.baseUrl + '/api/' + el.Key);
        const fileNames = uploadedFileData.map(el => {
            const parts = el.Key.split('/');
            return parts[parts.length - 1];
        });

        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                files: uploadedFileData,
                name: uploadedFileData[0].originalname.filename,
                url: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload private rail files (POST /rail/:UID)
 */
export const uploadPrivateRailFiles = async (req, res) => {
    try {
        req.query.UID = req.params.UID;
        const uploadedFileData = await busboyUpload(req, {
            s3,
            uploadUrlGen: ({ fields, filename, extension, params }) => {
                return `${req.params.UID}/${fields.prefix}/${filename.filename}`;
            },
            Bucket: bucket,
            original: false,
            width: req.query.width ? parseInt(req.query.width) : 400,
            viewportScale: req.query.scale ? req.query.scale : '1.0'
        });

        const urls = uploadedFileData.filter(el => el.Key).map(el => process.env.baseUrl + '/api/' + el.Key);
        const fileNames = uploadedFileData.map(el => {
            const parts = el.Key.split('/');
            return parts[parts.length - 1];
        });

        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                files: uploadedFileData,
                name: uploadedFileData[0].originalname.filename,
                url: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload public files with expiration (POST /public/:expire/:UID)
 */
export const uploadPublicFiles = async (req, res) => {
    try {
        req.query.UID = req.params.UID;
        const uploadedFileData = await busboyUpload(req, {
            s3,
            mimeFilter: filterImagePdf,
            uploadUrlGen: publicUrlGen,
            Bucket: bucket,
            width: req.query.width ? parseInt(req.query.width) : 2048,
            viewportScale: req.query.scale ? req.query.scale : '1.0'
        });

        const urls = uploadedFileData.filter(el => el.Key).map(el => process.env.baseUrl + '/api/' + el.Key);
        const fileNames = uploadedFileData.map(el => {
            const parts = el.Key.split('/');
            return parts[parts.length - 1];
        });

        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                files: uploadedFileData,
                name: uploadedFileData[0].originalname.filename,
                url: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Upload editor files (POST /editor/:expire/:UID)
 */
export const uploadEditorFiles = async (req, res) => {
    try {
        req.query.UID = req.params.UID;
        const uploadedFileData = await busboyUpload(req, {
            s3,
            uploadUrlGen: publicUrlGen,
            Bucket: bucket,
            width: 2048
        });

        const urls = uploadedFileData.map(el => process.env.baseUrl + '/api/' + el.Key);
        const fileNames = uploadedFileData.map(el => el.Key.split('/')[el.Key.split('/').length - 1]);

        if (uploadedFileData.length > 0) {
            res.json({
                success: true,
                name: uploadedFileData[0].originalname,
                message: 'OK',
                url: uploadedFileData[1] ? urls[1] : urls[0],
                fileNames: fileNames,
                urls: urls,
                Data: uploadedFileData
            });
        } else {
            res.json({ success: false });
        }
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get log files (GET /:api/logs)
 */
export const getLogFiles = async (req, res) => {
    try {
        const logFilePath = path.join(process.cwd(), `/logs/${req.params.api}/requests.log`);
        
        fs.readFile(logFilePath, 'utf8', (err, data) => {
            if (err) {
                return res.status(500).send('Error reading log file');
            }
            
            res.setHeader('Content-Type', 'text/plain');
            res.send(data);
        });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get public language/config files (GET /:api/prefix1:(languages|config)/:filename/)
 */
export const getPublicLanguageFiles = async (req, res) => {
    try {
        myMinioClient.getObject(bucket, req.params.api + '/' + req.params.prefix1 + '/' + req.params.filename,
            function (err, data) {
                if (err) {
                    console.error(`S3 error for ${req.params.api}/${req.params.prefix1}/${req.params.filename}:`, err.code);
                    return res.status(410).json({ success: false, message: 'not available', code: err.code });
                } else {
                    try {
                        const contentType = data.headers['x-amz-meta-mimetype'] || 'application/octet-stream';
                        res.set('Content-Type', contentType);
                        
                        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 (streamSetupErr) {
                        console.error('Error setting up stream:', streamSetupErr);
                        if (!res.headersSent) {
                            res.status(500).json({ success: false, message: 'Error preparing file' });
                        }
                    }
                }
            });
    } catch (e) {
        errorLoggerRead(e);
        if (!res.headersSent) {
            res.status(500).json({ success: false, message: 'Internal server error' });
        }
    }
};

/**
 * Get public rail files (GET /rail/:app/:side/:filename)
 */
export const getPublicRailFiles = async (req, res) => {
    try {
        myMinioClient.getObject(bucket, 'public/' + req.params.app + '/' + req.params.side + '/' + decodeURIComponent(req.params.filename),
            function (err, data) {
                if (err) {
                    console.error(`S3 error for public/${req.params.app}/${req.params.side}/${req.params.filename}:`, err.code);
                    return res.status(410).json({ success: false, message: 'not available', code: err.code });
                } else {
                    try {
                        const mime = data.headers['content-type'] ? data.headers['content-type'] : data.headers['x-amz-meta-mimetype'];
                        res.set('Content-Type', mime);
                        
                        const transformHtml = (mime && mime === 'text/html' || req.params.filename.match(/.*html?$/i)) && req.query.replaceRoot;
                        
                        data.on('error', function (err) {
                            console.error('Stream error:', err);
                            if (!res.headersSent) {
                                res.status(500).json({ success: false, message: 'Stream error' });
                            }
                        });
                        
                        data.on('data', function (chunk) {
                            if (transformHtml) {
                                const text = chunk.toString('utf8');
                                const replacedText = text.replace(/\%root\%/ig, `${process.env.baseUrl}/api/kpe20/files/rail/${req.params.app}/${req.params.side}`);
                                res.write(Buffer.from(replacedText));
                            } else {
                                res.write(chunk);
                            }
                        });
                        
                        data.on('end', function () {
                            res.end();
                        });
                    } catch (streamErr) {
                        console.error('Error setting up stream:', streamErr);
                        if (!res.headersSent) {
                            res.status(500).json({ success: false, message: 'Error preparing file' });
                        }
                    }
                }
            });
    } catch (e) {
        errorLoggerRead(e);
        if (!res.headersSent) {
            res.status(500).json({ success: false, message: 'Server error' });
        }
    }
};

/**
 * Get private rail files (GET /rail/:UID/:side/:filename)
 */
export const getPrivateRailFiles = async (req, res) => {
    try {
        myMinioClient.getObject(bucket, req.params.UID + '/' + req.params.side + '/' + decodeURIComponent(req.params.filename),
            function (err, data) {
                if (err) {
                    console.error(`S3 error for ${req.params.UID}/${req.params.side}/${req.params.filename}:`, err.code);
                    return res.status(410).json({ success: false, message: 'not available', code: err.code });
                } else {
                    try {
                        const contentType = data.headers['content-type'] ? data.headers['content-type'] : data.headers['x-amz-meta-mimetype'];
                        res.set('Content-Type', contentType);
                        
                        const transformHtml = contentType && contentType === 'text/html' && req.query.replaceRoot;
                        
                        data.on('error', function (err) {
                            console.error('Stream error:', err);
                            if (!res.headersSent) {
                                res.status(500).json({ success: false, message: 'Stream error' });
                            }
                        });
                        
                        data.on('data', function (chunk) {
                            if (transformHtml) {
                                const text = chunk.toString('utf8');
                                const replacedText = text.replace('%root%', `https://kpe20.${process.env.API_BASE}/api/kpe20/files/rail/${req.params.UID}/${req.params.side}`);
                                res.write(Buffer.from(replacedText));
                            } else {
                                res.write(chunk);
                            }
                        });
                        
                        data.on('end', function () {
                            res.end();
                        });
                    } catch (streamErr) {
                        console.error('Error setting up stream:', streamErr);
                        if (!res.headersSent) {
                            res.status(500).json({ success: false, message: 'Error preparing file' });
                        }
                    }
                }
            });
    } catch (e) {
        errorLoggerRead(e);
        if (!res.headersSent) {
            res.status(500).json({ success: false, message: 'Server error' });
        }
    }
};

/**
 * Get files with prefix (GET /:UID/:prefix/:filename)
 */
export const getFileWithPrefix = async (req, res) => {
    try {
        const name = req.params.UID + '/' + decodeURIComponent(req.params.prefix) + '/' + req.params.filename;
        myMinioClient.getObject(bucket, name,
            function (err, data) {
                if (err) {
                    res.status(410).send({ success: false, message: 'not available' });
                } else {
                    res.set('Content-Type', data.headers['content-type'] ? data.headers['content-type'] : data.headers['x-amz-meta-mimetype']);
                    data.pipe(res);
                }
            });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get single file (GET /:UID/:filename)
 */
export const getSingleFile = async (req, res) => {
    try {
        myMinioClient.getObject(bucket, req.params.UID + '/' + decodeURIComponent(req.params.filename),
            function (err, data) {
                if (err) {
                    console.error(`S3 error for ${req.params.UID}/${req.params.filename}:`, err.code);
                    return res.status(410).json({ success: false, message: 'not available', code: err.code });
                } else {
                    try {
                        const contentType = data.headers['content-type'] ? data.headers['content-type'] : data.headers['x-amz-meta-mimetype'];
                        res.set('Content-Type', contentType);
                        
                        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 (streamErr) {
                        console.error('Error setting up stream:', streamErr);
                        if (!res.headersSent) {
                            res.status(500).json({ success: false, message: 'Error preparing file' });
                        }
                    }
                }
            });
    } catch (e) {
        errorLoggerRead(e);
        if (!res.headersSent) {
            res.status(500).json({ success: false, message: 'Server error' });
        }
    }
};

/**
 * Get public files with expiration (GET /public/:expire/:UID/:filename)
 */
export const getPublicFiles = async (req, res) => {
    try {
        myMinioClient.getObject(bucket, 'public/' + req.params.expire + '/' + req.params.UID + '/' + req.params.filename,
            function (err, data) {
                if (err) {
                    console.error(`S3 error for public/${req.params.expire}/${req.params.UID}/${req.params.filename}:`, err.code);
                    return res.status(410).json({ success: false, message: 'not available', code: err.code });
                } else {
                    try {
                        const mime = data.headers['content-type'] ? data.headers['content-type'] : data.headers['x-amz-meta-mimetype'];
                        res.set('Content-Type', mime);
                        
                        if (mime && mime.match(/^image\//)) {
                            res.set(`Content-Disposition: inline; filename="${data.headers.filename}"`);
                        } else {
                            res.set(`Content-Disposition: attachment; filename="${data.headers.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 (streamErr) {
                        console.error('Error setting up stream:', streamErr);
                        if (!res.headersSent) {
                            res.status(500).json({ success: false, message: 'Error preparing file' });
                        }
                    }
                }
            });
    } catch (e) {
        errorLoggerRead(e);
        if (!res.headersSent) {
            res.status(500).json({ success: false, message: 'Server error' });
        }
    }
};

/**
 * List files in directory (GET /:UID)
 */
export const listFiles = async (req, res) => {
    try {
        const UID = req.params.UID;
        if (['public'].includes(UID)) {
            res.status(401).json({ success: false, message: 'only admins are authorized to list the content of the public folder' });
            return;
        }

        const prefix = UID + (req.query.prefix !== undefined ? '/' + req.query.prefix : '');
        const recursive = req.query.recursive ? req.query.recursive === 'true' : true;
        const stream = myMinioClient.extensions.listObjectsV2WithMetadata(bucket, prefix, recursive);
        const objectList = [];

        stream.on('data', function (obj) {
            const filePath = obj.name.replace(req.params.UID + '/', '');
            obj.url = process.env.baseUrl + '/api/kpe20/files/' + req.params.UID + '/' + encodeURIComponent(filePath);
            objectList.push(obj);
        });

        stream.on('end', () => {
            res.json({ success: true, files: objectList });
        });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete public files (DELETE /public/:expire/:prefix/:filename)
 */
export const deletePublicFiles = async (req, res) => {
    try {
        myMinioClient.removeObject(bucket, ['public/' + req.params.expire + '/' + req.params.prefix + '/' + req.params.filename], function (err) {
            if (err) {
                res.json({ success: false, message: 'Unable to remove object', error: err });
                return;
            }
            res.json({ success: true, message: 'OK' });
        });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete public rail files (DELETE /rail/:app/:prefix/:filename)
 */
export const deletePublicRailFiles = async (req, res) => {
    try {
        myMinioClient.removeObjects(bucket, [`public/${req.params.app}/${req.params.prefix}/${decodeURIComponent(req.params.filename)}`], function (err) {
            if (err) {
                return res.json({ message: 'Unable to remove objects', error: err });
            }
            res.json({ success: true, message: 'OK' });
        });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete private rail files (DELETE /rail/:UID/:prefix/:filename)
 */
export const deletePrivateRailFiles = async (req, res) => {
    try {
        myMinioClient.removeObjects(bucket, [`${req.params.UID}${req.params.prefix}/${decodeURIComponent(req.params.filename)}`], function (err) {
            if (err) {
                console.error('Unable to remove objects', err);
                return res.json({ message: 'Unable to remove objects', error: err });
            }
            res.json({ success: true, message: 'OK' });
        });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete multiple files (DELETE /:UID)
 */
export const deleteMultipleFiles = async (req, res) => {
    try {
        myMinioClient.removeObjects(bucket, req.body.files.map(f => (req.params.UID + '/' + f)), function (err) {
            if (err) {
                return res.json({ message: 'Unable to remove objects', error: err });
            }
            res.json({ message: 'OK' });
        });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete directory (DELETE /dir/:UID/:prefix)
 */
export const deleteDirectory = async (req, res) => {
    try {
        const result = await deleteDir(`${req.params.UID}/${req.params.prefix}`, true);

        res.json(result);
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};