Blame Raw
· · · 155 lines (5.5 KB)
0 contributors
1import fs from 'node:fs';
2import path from 'node:path';
3import { Buffer } from 'node:buffer';
4
5import express from 'express';
6import sanitize from 'sanitize-filename';
7
8import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js';
9import { 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 */
17function 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
26export 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 */
39router.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
80router.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
115router.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
133router.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});