|
1 | 1 | import { Request, Response } from 'express'; |
| 2 | +import { readdir, readFile } from 'fs/promises'; |
| 3 | +import path from 'path'; |
| 4 | +import { fileURLToPath } from 'url'; |
2 | 5 | import { LandscapeService } from '../services/landscape.service'; |
3 | 6 |
|
| 7 | +const __filename = fileURLToPath(import.meta.url); |
| 8 | +const __dirname = path.dirname(__filename); |
| 9 | + |
4 | 10 | export class LandscapeController { |
5 | 11 | private landscapeService: LandscapeService; |
6 | 12 |
|
@@ -48,4 +54,131 @@ export class LandscapeController { |
48 | 54 | res.status(400).json({ error: error.message || 'Failed to update landscape' }); |
49 | 55 | } |
50 | 56 | }; |
| 57 | + |
| 58 | + /** |
| 59 | + * GET /api/landscape/presets - List available preset landscapes |
| 60 | + */ |
| 61 | + listPresets = async (req: Request, res: Response): Promise<void> => { |
| 62 | + try { |
| 63 | + // Try multiple possible paths for preset landscapes |
| 64 | + const possiblePaths = [ |
| 65 | + // Development: from source directory |
| 66 | + path.join(process.cwd(), 'public/resources/preset-landscapes'), |
| 67 | + // Development: relative to compiled controller |
| 68 | + path.join(__dirname, '../../public/resources/preset-landscapes'), |
| 69 | + // Production: from dist |
| 70 | + path.join(__dirname, '../../dist/public/resources/preset-landscapes'), |
| 71 | + // Production: from process.cwd |
| 72 | + path.join(process.cwd(), 'dist/public/resources/preset-landscapes'), |
| 73 | + ]; |
| 74 | + |
| 75 | + let presetDir: string | null = null; |
| 76 | + for (const possiblePath of possiblePaths) { |
| 77 | + try { |
| 78 | + await readdir(possiblePath); |
| 79 | + presetDir = possiblePath; |
| 80 | + break; |
| 81 | + } catch { |
| 82 | + // Continue to next path |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + if (!presetDir) { |
| 87 | + // No preset directory found, return empty array |
| 88 | + res.status(200).json([]); |
| 89 | + return; |
| 90 | + } |
| 91 | + |
| 92 | + const files = await readdir(presetDir); |
| 93 | + const presetFiles = files |
| 94 | + .filter((file) => file.endsWith('.json')) |
| 95 | + .map((file) => ({ |
| 96 | + name: file.replace('.json', ''), |
| 97 | + filename: file, |
| 98 | + })); |
| 99 | + |
| 100 | + res.status(200).json(presetFiles); |
| 101 | + } catch (error: any) { |
| 102 | + res.status(500).json({ error: error.message || 'Failed to list preset landscapes' }); |
| 103 | + } |
| 104 | + }; |
| 105 | + |
| 106 | + /** |
| 107 | + * GET /api/landscape/presets/:name - Load a specific preset landscape |
| 108 | + */ |
| 109 | + loadPreset = async (req: Request, res: Response): Promise<void> => { |
| 110 | + try { |
| 111 | + const presetName = req.params.name; |
| 112 | + if (!presetName || !presetName.match(/^[a-zA-Z0-9_-]+$/)) { |
| 113 | + res.status(400).json({ error: 'Invalid preset name' }); |
| 114 | + return; |
| 115 | + } |
| 116 | + |
| 117 | + // Try multiple possible paths for preset landscapes |
| 118 | + const possiblePaths = [ |
| 119 | + // Development: from source directory |
| 120 | + path.join(process.cwd(), 'public/resources/preset-landscapes', `${presetName}.json`), |
| 121 | + // Development: relative to compiled controller |
| 122 | + path.join(__dirname, '../../public/resources/preset-landscapes', `${presetName}.json`), |
| 123 | + // Production: from dist |
| 124 | + path.join(__dirname, '../../dist/public/resources/preset-landscapes', `${presetName}.json`), |
| 125 | + // Production: from process.cwd |
| 126 | + path.join(process.cwd(), 'dist/public/resources/preset-landscapes', `${presetName}.json`), |
| 127 | + ]; |
| 128 | + |
| 129 | + let presetFile: string | null = null; |
| 130 | + for (const possiblePath of possiblePaths) { |
| 131 | + try { |
| 132 | + await readFile(possiblePath); |
| 133 | + presetFile = possiblePath; |
| 134 | + break; |
| 135 | + } catch { |
| 136 | + // Continue to next path |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + if (!presetFile) { |
| 141 | + res.status(404).json({ error: `Preset landscape "${presetName}" not found` }); |
| 142 | + return; |
| 143 | + } |
| 144 | + |
| 145 | + const fileContent = await readFile(presetFile, 'utf-8'); |
| 146 | + const landscapeData = JSON.parse(fileContent); |
| 147 | + |
| 148 | + if (!Array.isArray(landscapeData)) { |
| 149 | + res.status(400).json({ error: 'Invalid landscape file: must be an array' }); |
| 150 | + return; |
| 151 | + } |
| 152 | + |
| 153 | + // Validate the data structure first by attempting to serialize it |
| 154 | + // This ensures the preset file itself doesn't have circular references |
| 155 | + try { |
| 156 | + JSON.stringify(landscapeData); |
| 157 | + } catch { |
| 158 | + res.status(400).json({ error: 'Preset landscape file contains circular references' }); |
| 159 | + return; |
| 160 | + } |
| 161 | + |
| 162 | + // Deep clone the landscape data to avoid mutating the original |
| 163 | + // This is necessary because updateLandscape will reconstruct parent references |
| 164 | + // which modifies objects in place, creating circular references |
| 165 | + const clonedLandscapeData = JSON.parse(JSON.stringify(landscapeData)); |
| 166 | + |
| 167 | + // Update the landscape store with the cloned preset (this reconstructs parent references internally) |
| 168 | + // The cloned data will be modified, but the original landscapeData remains clean |
| 169 | + this.landscapeService.updateLandscape(clonedLandscapeData); |
| 170 | + |
| 171 | + // Return the original landscape data (already in CleanedLandscape format) |
| 172 | + // This avoids any circular reference issues from the reconstruction process |
| 173 | + res.status(200).json(landscapeData); |
| 174 | + } catch (error: any) { |
| 175 | + if (error.code === 'ENOENT') { |
| 176 | + res.status(404).json({ error: `Preset landscape "${req.params.name}" not found` }); |
| 177 | + } else if (error instanceof SyntaxError) { |
| 178 | + res.status(400).json({ error: 'Invalid JSON in preset landscape file' }); |
| 179 | + } else { |
| 180 | + res.status(500).json({ error: error.message || 'Failed to load preset landscape' }); |
| 181 | + } |
| 182 | + } |
| 183 | + }; |
51 | 184 | } |
0 commit comments