| 1 | import fs from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | import { Buffer } from 'node:buffer'; |
| 4 | |
| 5 | import express from 'express'; |
| 6 | import sanitize from 'sanitize-filename'; |
| 7 | |
| 8 | import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js'; |
| 9 | import { MEDIA_EXTENSIONS, MEDIA_REQUEST_TYPE } from '../constants.js'; |
| 10 | |
| 11 | /** |
| 12 | * Ensure the directory for the provided file path exists. |
| 13 | * If not, it will recursively create the directory. |
| 14 | * |
| 15 | * @param {string} filePath - The full path of the file for which the directory should be ensured. |
| 16 | */ |
| 17 | function ensureDirectoryExistence(filePath) { |
| 18 | const dirname = path.dirname(filePath); |
| 19 | if (fs.existsSync(dirname)) { |
| 20 | return true; |
| 21 | } |
| 22 | ensureDirectoryExistence(dirname); |
| 23 | fs.mkdirSync(dirname); |
| 24 | } |
| 25 | |
| 26 | export const router = express.Router(); |
| 27 | |
| 28 | /** |
| 29 | * Endpoint to handle image uploads. |
| 30 | * The image should be provided in the request body in base64 format. |
| 31 | * Optionally, a character name can be provided to save the image in a sub-folder. |
| 32 | * |
| 33 | * @route POST /api/images/upload |
| 34 | * @param {Object} request.body - The request payload. |
| 35 | * @param {string} request.body.image - The base64 encoded image data. |
| 36 | * @param {string} [request.body.ch_name] - Optional character name to determine the sub-directory. |
| 37 | * @returns {Object} response - The response object containing the path where the image was saved. |
| 38 | */ |
| 39 | router.post('/upload', async (request, response) => { |
| 40 | try { |
| 41 | if (!request.body) { |
| 42 | return response.status(400).send({ error: 'No data provided' }); |
| 43 | } |
| 44 | |
| 45 | const { image, format } = request.body; |
| 46 | |
| 47 | if (!image) { |
| 48 | return response.status(400).send({ error: 'No image data provided' }); |
| 49 | } |
| 50 | |
| 51 | const validFormat = MEDIA_EXTENSIONS.includes(format); |
| 52 | if (!validFormat) { |
| 53 | return response.status(400).send({ error: 'Invalid image format' }); |
| 54 | } |
| 55 | |
| 56 | // Constructing filename and path |
| 57 | let filename; |
| 58 | if (request.body.filename) { |
| 59 | filename = `${removeFileExtension(request.body.filename)}.${format}`; |
| 60 | } else { |
| 61 | filename = `${Date.now()}.${format}`; |
| 62 | } |
| 63 | |
| 64 | // if character is defined, save to a sub folder for that character |
| 65 | let pathToNewFile = path.join(request.user.directories.userImages, sanitize(filename)); |
| 66 | if (request.body.ch_name) { |
| 67 | pathToNewFile = path.join(request.user.directories.userImages, sanitize(request.body.ch_name), sanitize(filename)); |
| 68 | } |
| 69 | |
| 70 | ensureDirectoryExistence(pathToNewFile); |
| 71 | const imageBuffer = Buffer.from(image, 'base64'); |
| 72 | await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer)); |
| 73 | response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) }); |
| 74 | } catch (error) { |
| 75 | console.error(error); |
| 76 | response.status(500).send({ error: 'Failed to save the image' }); |
| 77 | } |
| 78 | }); |
| 79 | |
| 80 | router.post('/list/:folder?', (request, response) => { |
| 81 | try { |
| 82 | if (request.params.folder) { |
| 83 | if (request.body.folder) { |
| 84 | return response.status(400).send({ error: 'Folder specified in both URL and body' }); |
| 85 | } |
| 86 | |
| 87 | console.warn('Deprecated: Use POST /api/images/list with folder in request body'); |
| 88 | request.body.folder = request.params.folder; |
| 89 | } |
| 90 | |
| 91 | if (!request.body.folder) { |
| 92 | return response.status(400).send({ error: 'No folder specified' }); |
| 93 | } |
| 94 | |
| 95 | const directoryPath = path.join(request.user.directories.userImages, sanitize(request.body.folder)); |
| 96 | const type = Number(request.body.type ?? MEDIA_REQUEST_TYPE.IMAGE); |
| 97 | const sort = request.body.sortField || 'date'; |
| 98 | const order = request.body.sortOrder || 'asc'; |
| 99 | |
| 100 | if (!fs.existsSync(directoryPath)) { |
| 101 | fs.mkdirSync(directoryPath, { recursive: true }); |
| 102 | } |
| 103 | |
| 104 | const images = getImages(directoryPath, sort, type); |
| 105 | if (order === 'desc') { |
| 106 | images.reverse(); |
| 107 | } |
| 108 | return response.send(images); |
| 109 | } catch (error) { |
| 110 | console.error(error); |
| 111 | return response.status(500).send({ error: 'Unable to retrieve files' }); |
| 112 | } |
| 113 | }); |
| 114 | |
| 115 | router.post('/folders', (request, response) => { |
| 116 | try { |
| 117 | const directoryPath = request.user.directories.userImages; |
| 118 | if (!fs.existsSync(directoryPath)) { |
| 119 | fs.mkdirSync(directoryPath, { recursive: true }); |
| 120 | } |
| 121 | |
| 122 | const folders = fs.readdirSync(directoryPath, { withFileTypes: true }) |
| 123 | .filter(dirent => dirent.isDirectory()) |
| 124 | .map(dirent => dirent.name); |
| 125 | |
| 126 | return response.send(folders); |
| 127 | } catch (error) { |
| 128 | console.error(error); |
| 129 | return response.status(500).send({ error: 'Unable to retrieve folders' }); |
| 130 | } |
| 131 | }); |
| 132 | |
| 133 | router.post('/delete', async (request, response) => { |
| 134 | try { |
| 135 | if (!request.body.path) { |
| 136 | return response.status(400).send('No path specified'); |
| 137 | } |
| 138 | |
| 139 | const pathToDelete = path.join(request.user.directories.root, request.body.path); |
| 140 | if (!isPathUnderParent(request.user.directories.userImages, pathToDelete)) { |
| 141 | return response.status(400).send('Invalid path'); |
| 142 | } |
| 143 | |
| 144 | if (!fs.existsSync(pathToDelete)) { |
| 145 | return response.status(404).send('File not found'); |
| 146 | } |
| 147 | |
| 148 | fs.unlinkSync(pathToDelete); |
| 149 | console.info(`Deleted image: ${request.body.path} from ${request.user.profile.handle}`); |
| 150 | return response.sendStatus(200); |
| 151 | } catch (error) { |
| 152 | console.error(error); |
| 153 | return response.sendStatus(500); |
| 154 | } |
| 155 | }); |