Blame Raw
· · · 157 lines (5.2 KB)
0 contributors
1import fs from 'node:fs';
2import path from 'node:path';
3
4import express from 'express';
5import sanitize from 'sanitize-filename';
6import _ from 'lodash';
7import { sync as writeFileAtomicSync } from 'write-file-atomic';
8import { tryParse } from '../util.js';
9
10/**
11 * Reads a World Info file and returns its contents
12 * @param {import('../users.js').UserDirectoryList} directories User directories
13 * @param {string} worldInfoName Name of the World Info file
14 * @param {boolean} allowDummy If true, returns an empty object if the file doesn't exist
15 * @returns {object} World Info file contents
16 */
17export function readWorldInfoFile(directories, worldInfoName, allowDummy) {
18 const dummyObject = allowDummy ? { entries: {} } : null;
19
20 if (!worldInfoName) {
21 return dummyObject;
22 }
23
24 const filename = sanitize(`${worldInfoName}.json`);
25 const pathToWorldInfo = path.join(directories.worlds, filename);
26
27 if (!fs.existsSync(pathToWorldInfo)) {
28 console.error(`World info file ${filename} doesn't exist.`);
29 return dummyObject;
30 }
31
32 const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8');
33 const worldInfo = JSON.parse(worldInfoText);
34 return worldInfo;
35}
36
37export const router = express.Router();
38
39router.post('/list', async (request, response) => {
40 try {
41 const data = [];
42 const jsonFiles = (await fs.promises.readdir(request.user.directories.worlds, { withFileTypes: true }))
43 .filter((file) => file.isFile() && path.extname(file.name).toLowerCase() === '.json')
44 .sort((a, b) => a.name.localeCompare(b.name));
45
46 for (const file of jsonFiles) {
47 try {
48 const filePath = path.join(request.user.directories.worlds, file.name);
49 const fileContents = await fs.promises.readFile(filePath, 'utf8');
50 const fileContentsParsed = tryParse(fileContents) || {};
51 const fileExtensions = fileContentsParsed?.extensions || {};
52 const fileNameWithoutExt = path.parse(file.name).name;
53 const fileData = {
54 file_id: fileNameWithoutExt,
55 name: fileContentsParsed?.name || fileNameWithoutExt,
56 extensions: _.isObjectLike(fileExtensions) ? fileExtensions : {},
57 };
58 data.push(fileData);
59 } catch (err) {
60 console.warn(`Error reading or parsing World Info file ${file.name}:`, err);
61 }
62 }
63
64 return response.send(data);
65 } catch (err) {
66 console.error('Error reading World Info directory:', err);
67 return response.sendStatus(500);
68 }
69});
70
71router.post('/get', (request, response) => {
72 if (!request.body?.name) {
73 return response.sendStatus(400);
74 }
75
76 const file = readWorldInfoFile(request.user.directories, request.body.name, true);
77
78 return response.send(file);
79});
80
81router.post('/delete', (request, response) => {
82 if (!request.body?.name) {
83 return response.sendStatus(400);
84 }
85
86 const worldInfoName = request.body.name;
87 const filename = sanitize(`${worldInfoName}.json`);
88 const pathToWorldInfo = path.join(request.user.directories.worlds, filename);
89
90 if (!fs.existsSync(pathToWorldInfo)) {
91 throw new Error(`World info file ${filename} doesn't exist.`);
92 }
93
94 fs.unlinkSync(pathToWorldInfo);
95
96 return response.sendStatus(200);
97});
98
99router.post('/import', (request, response) => {
100 if (!request.file) return response.sendStatus(400);
101
102 const filename = `${path.parse(sanitize(request.file.originalname)).name}.json`;
103
104 let fileContents = null;
105
106 if (request.body.convertedData) {
107 fileContents = request.body.convertedData;
108 } else {
109 const pathToUpload = path.join(request.file.destination, request.file.filename);
110 fileContents = fs.readFileSync(pathToUpload, 'utf8');
111 fs.unlinkSync(pathToUpload);
112 }
113
114 try {
115 const worldContent = JSON.parse(fileContents);
116 if (!('entries' in worldContent)) {
117 throw new Error('File must contain a world info entries list');
118 }
119 } catch (err) {
120 return response.status(400).send('Is not a valid world info file');
121 }
122
123 const pathToNewFile = path.join(request.user.directories.worlds, filename);
124 const worldName = path.parse(pathToNewFile).name;
125
126 if (!worldName) {
127 return response.status(400).send('World file must have a name');
128 }
129
130 writeFileAtomicSync(pathToNewFile, fileContents);
131 return response.send({ name: worldName });
132});
133
134router.post('/edit', (request, response) => {
135 if (!request.body) {
136 return response.sendStatus(400);
137 }
138
139 if (!request.body.name) {
140 return response.status(400).send('World file must have a name');
141 }
142
143 try {
144 if (!('entries' in request.body.data)) {
145 throw new Error('World info must contain an entries list');
146 }
147 } catch (err) {
148 return response.status(400).send('Is not a valid world info file');
149 }
150
151 const filename = sanitize(`${request.body.name}.json`);
152 const pathToFile = path.join(request.user.directories.worlds, filename);
153
154 writeFileAtomicSync(pathToFile, JSON.stringify(request.body.data, null, 4));
155
156 return response.send({ ok: true });
157});