diff options
Diffstat (limited to 'app/Controllers/Http/RecipeController.ts')
-rw-r--r-- | app/Controllers/Http/RecipeController.ts | 179 |
1 files changed, 78 insertions, 101 deletions
diff --git a/app/Controllers/Http/RecipeController.ts b/app/Controllers/Http/RecipeController.ts index 5186a11..e43bcf8 100644 --- a/app/Controllers/Http/RecipeController.ts +++ b/app/Controllers/Http/RecipeController.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; | 1 | import type { HttpContext } from '@adonisjs/core/http' |
2 | import fs from 'fs-extra'; | 2 | import fs from 'fs-extra' |
3 | import Application from '@ioc:Adonis/Core/Application'; | 3 | import { app } from '@adonisjs/core/services/app' |
4 | import path from 'node:path'; | 4 | import path from 'node:path' |
5 | import Recipe from 'App/Models/Recipe'; | 5 | import Recipe from '#app/Models/Recipe' |
6 | import { isCreationEnabled } from 'Config/app'; | 6 | import { isCreationEnabled } from '#config/app' |
7 | import { validator, schema, rules } from '@ioc:Adonis/Core/Validator'; | 7 | import { validator, schema, rules } from '@adonisjs/validator' |
8 | import targz from 'targz'; | 8 | import targz from 'targz' |
9 | import semver from 'semver'; | 9 | import semver from 'semver' |
10 | import Drive from '@ioc:Adonis/Core/Drive'; | 10 | import Drive from '@ioc:Adonis/Core/Drive' |
11 | 11 | ||
12 | // TODO: This file needs to be refactored and cleaned up to include types | 12 | // TODO: This file needs to be refactored and cleaned up to include types |
13 | 13 | ||
@@ -18,17 +18,17 @@ const createSchema = schema.create({ | |||
18 | // author: 'required|accepted', | 18 | // author: 'required|accepted', |
19 | author: schema.string(), | 19 | author: schema.string(), |
20 | svg: schema.string([rules.url()]), | 20 | svg: schema.string([rules.url()]), |
21 | }); | 21 | }) |
22 | 22 | ||
23 | const searchSchema = schema.create({ | 23 | const searchSchema = schema.create({ |
24 | needle: schema.string(), | 24 | needle: schema.string(), |
25 | }); | 25 | }) |
26 | 26 | ||
27 | const downloadSchema = schema.create({ | 27 | const downloadSchema = schema.create({ |
28 | // TODO: Check if this is correct | 28 | // TODO: Check if this is correct |
29 | // recipe: 'required|accepted', | 29 | // recipe: 'required|accepted', |
30 | recipe: schema.string(), | 30 | recipe: schema.string(), |
31 | }); | 31 | }) |
32 | 32 | ||
33 | const compress = (src: string, dest: string) => | 33 | const compress = (src: string, dest: string) => |
34 | new Promise((resolve, reject) => { | 34 | new Promise((resolve, reject) => { |
@@ -37,87 +37,76 @@ const compress = (src: string, dest: string) => | |||
37 | src, | 37 | src, |
38 | dest, | 38 | dest, |
39 | }, | 39 | }, |
40 | err => { | 40 | (err) => { |
41 | if (err) { | 41 | if (err) { |
42 | reject(err); | 42 | reject(err) |
43 | } else { | 43 | } else { |
44 | resolve(dest); | 44 | resolve(dest) |
45 | } | 45 | } |
46 | }, | 46 | } |
47 | ); | 47 | ) |
48 | }); | 48 | }) |
49 | 49 | ||
50 | export default class RecipesController { | 50 | export default class RecipesController { |
51 | // List official and custom recipes | 51 | // List official and custom recipes |
52 | public async list({ response }: HttpContextContract) { | 52 | public async list({ response }: HttpContext) { |
53 | const officialRecipes = fs.readJsonSync( | 53 | const officialRecipes = fs.readJsonSync(path.join(app.appRoot, 'recipes', 'all.json')) |
54 | path.join(Application.appRoot, 'recipes', 'all.json'), | 54 | const customRecipesArray = await Recipe.all() |
55 | ); | 55 | const customRecipes = customRecipesArray.map((recipe) => ({ |
56 | const customRecipesArray = await Recipe.all(); | ||
57 | const customRecipes = customRecipesArray.map(recipe => ({ | ||
58 | id: recipe.recipeId, | 56 | id: recipe.recipeId, |
59 | name: recipe.name, | 57 | name: recipe.name, |
60 | ...(typeof recipe.data === 'string' | 58 | ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data), |
61 | ? JSON.parse(recipe.data) | 59 | })) |
62 | : recipe.data), | ||
63 | })); | ||
64 | 60 | ||
65 | const recipes = [...officialRecipes, ...customRecipes]; | 61 | const recipes = [...officialRecipes, ...customRecipes] |
66 | 62 | ||
67 | return response.send(recipes); | 63 | return response.send(recipes) |
68 | } | 64 | } |
69 | 65 | ||
70 | // TODO: Test this endpoint | 66 | // TODO: Test this endpoint |
71 | // Create a new recipe using the new.html page | 67 | // Create a new recipe using the new.html page |
72 | public async create({ request, response }: HttpContextContract) { | 68 | public async create({ request, response }: HttpContext) { |
73 | // Check if recipe creation is enabled | 69 | // Check if recipe creation is enabled |
74 | if (isCreationEnabled === 'false') { | 70 | if (isCreationEnabled === 'false') { |
75 | return response.send( | 71 | return response.send("This server doesn't allow the creation of new recipes.") |
76 | 'This server doesn\'t allow the creation of new recipes.', | ||
77 | ); | ||
78 | } | 72 | } |
79 | 73 | ||
80 | // Validate user input | 74 | // Validate user input |
81 | let data; | 75 | let data |
82 | try { | 76 | try { |
83 | data = await request.validate({ schema: createSchema }); | 77 | data = await request.validate({ schema: createSchema }) |
84 | } catch (error) { | 78 | } catch (error) { |
85 | return response.status(401).send({ | 79 | return response.status(401).send({ |
86 | message: 'Invalid POST arguments', | 80 | message: 'Invalid POST arguments', |
87 | messages: error.messages, | 81 | messages: error.messages, |
88 | status: 401, | 82 | status: 401, |
89 | }); | 83 | }) |
90 | } | 84 | } |
91 | 85 | ||
92 | if (!data.id) { | 86 | if (!data.id) { |
93 | return response.send('Please provide an ID'); | 87 | return response.send('Please provide an ID') |
94 | } | 88 | } |
95 | 89 | ||
96 | // Check for invalid characters | 90 | // Check for invalid characters |
97 | if (/\.+/.test(data.id) || /\/+/.test(data.id)) { | 91 | if (/\.+/.test(data.id) || /\/+/.test(data.id)) { |
98 | return response.send( | 92 | return response.send('Invalid recipe name. Your recipe name may not contain "." or "/"') |
99 | 'Invalid recipe name. Your recipe name may not contain "." or "/"', | ||
100 | ); | ||
101 | } | 93 | } |
102 | 94 | ||
103 | // Clear temporary recipe folder | 95 | // Clear temporary recipe folder |
104 | await fs.emptyDir(Application.tmpPath('recipe')); | 96 | await fs.emptyDir(app.tmpPath('recipe')) |
105 | 97 | ||
106 | // Move uploaded files to temporary path | 98 | // Move uploaded files to temporary path |
107 | const files = request.file('files'); | 99 | const files = request.file('files') |
108 | if (!files) { | 100 | if (!files) { |
109 | return response.abort('Error processsing files.'); | 101 | return response.abort('Error processsing files.') |
110 | } | 102 | } |
111 | await files.move(Application.tmpPath('recipe')); | 103 | await files.move(app.tmpPath('recipe')) |
112 | 104 | ||
113 | // Compress files to .tar.gz file | 105 | // Compress files to .tar.gz file |
114 | const source = Application.tmpPath('recipe'); | 106 | const source = app.tmpPath('recipe') |
115 | const destination = path.join( | 107 | const destination = path.join(app.appRoot, `/recipes/archives/${data.id}.tar.gz`) |
116 | Application.appRoot, | ||
117 | `/recipes/archives/${data.id}.tar.gz`, | ||
118 | ); | ||
119 | 108 | ||
120 | compress(source, destination); | 109 | compress(source, destination) |
121 | 110 | ||
122 | // Create recipe in db | 111 | // Create recipe in db |
123 | await Recipe.create({ | 112 | await Recipe.create({ |
@@ -132,123 +121,111 @@ export default class RecipesController { | |||
132 | svg: data.svg, | 121 | svg: data.svg, |
133 | }, | 122 | }, |
134 | }), | 123 | }), |
135 | }); | 124 | }) |
136 | 125 | ||
137 | return response.send('Created new recipe'); | 126 | return response.send('Created new recipe') |
138 | } | 127 | } |
139 | 128 | ||
140 | // Search official and custom recipes | 129 | // Search official and custom recipes |
141 | public async search({ request, response }: HttpContextContract) { | 130 | public async search({ request, response }: HttpContext) { |
142 | // Validate user input | 131 | // Validate user input |
143 | let data; | 132 | let data |
144 | try { | 133 | try { |
145 | data = await request.validate({ schema: searchSchema }); | 134 | data = await request.validate({ schema: searchSchema }) |
146 | } catch (error) { | 135 | } catch (error) { |
147 | return response.status(401).send({ | 136 | return response.status(401).send({ |
148 | message: 'Please provide a needle', | 137 | message: 'Please provide a needle', |
149 | messages: error.messages, | 138 | messages: error.messages, |
150 | status: 401, | 139 | status: 401, |
151 | }); | 140 | }) |
152 | } | 141 | } |
153 | 142 | ||
154 | const { needle } = data; | 143 | const { needle } = data |
155 | 144 | ||
156 | // Get results | 145 | // Get results |
157 | let results; | 146 | let results |
158 | 147 | ||
159 | if (needle === 'ferdium:custom') { | 148 | if (needle === 'ferdium:custom') { |
160 | const dbResults = await Recipe.all(); | 149 | const dbResults = await Recipe.all() |
161 | results = dbResults.map(recipe => ({ | 150 | results = dbResults.map((recipe) => ({ |
162 | id: recipe.recipeId, | 151 | id: recipe.recipeId, |
163 | name: recipe.name, | 152 | name: recipe.name, |
164 | ...(typeof recipe.data === 'string' | 153 | ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data), |
165 | ? JSON.parse(recipe.data) | 154 | })) |
166 | : recipe.data), | ||
167 | })); | ||
168 | } else { | 155 | } else { |
169 | const localResultsArray = await Recipe.query().where( | 156 | const localResultsArray = await Recipe.query().where('name', 'LIKE', `%${needle}%`) |
170 | 'name', | 157 | results = localResultsArray.map((recipe) => ({ |
171 | 'LIKE', | ||
172 | `%${needle}%`, | ||
173 | ); | ||
174 | results = localResultsArray.map(recipe => ({ | ||
175 | id: recipe.recipeId, | 158 | id: recipe.recipeId, |
176 | name: recipe.name, | 159 | name: recipe.name, |
177 | ...(typeof recipe.data === 'string' | 160 | ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data), |
178 | ? JSON.parse(recipe.data) | 161 | })) |
179 | : recipe.data), | ||
180 | })); | ||
181 | } | 162 | } |
182 | 163 | ||
183 | return response.send(results); | 164 | return response.send(results) |
184 | } | 165 | } |
185 | 166 | ||
186 | public popularRecipes({ response }: HttpContextContract) { | 167 | public popularRecipes({ response }: HttpContext) { |
187 | return response.send( | 168 | return response.send( |
188 | fs | 169 | fs |
189 | .readJsonSync(path.join(Application.appRoot, 'recipes', 'all.json')) | 170 | .readJsonSync(path.join(app.appRoot, 'recipes', 'all.json')) |
190 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | 171 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
191 | .filter((recipe: any) => recipe.featured), | 172 | .filter((recipe: any) => recipe.featured) |
192 | ); | 173 | ) |
193 | } | 174 | } |
194 | 175 | ||
195 | // TODO: test this endpoint | 176 | // TODO: test this endpoint |
196 | public update({ request, response }: HttpContextContract) { | 177 | public update({ request, response }: HttpContext) { |
197 | const updates = []; | 178 | const updates = [] |
198 | const recipes = request.all(); | 179 | const recipes = request.all() |
199 | const allJson = fs.readJsonSync( | 180 | const allJson = fs.readJsonSync(path.join(app.appRoot, 'recipes', 'all.json')) |
200 | path.join(Application.appRoot, 'recipes', 'all.json'), | ||
201 | ); | ||
202 | 181 | ||
203 | for (const recipe of Object.keys(recipes)) { | 182 | for (const recipe of Object.keys(recipes)) { |
204 | const version = recipes[recipe]; | 183 | const version = recipes[recipe] |
205 | 184 | ||
206 | // Find recipe in local recipe repository | 185 | // Find recipe in local recipe repository |
207 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | 186 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
208 | const localRecipe = allJson.find((r: any) => r.id === recipe); | 187 | const localRecipe = allJson.find((r: any) => r.id === recipe) |
209 | if (localRecipe && semver.lt(version, localRecipe.version)) { | 188 | if (localRecipe && semver.lt(version, localRecipe.version)) { |
210 | updates.push(recipe); | 189 | updates.push(recipe) |
211 | } | 190 | } |
212 | } | 191 | } |
213 | 192 | ||
214 | return response.send(updates); | 193 | return response.send(updates) |
215 | } | 194 | } |
216 | 195 | ||
217 | // TODO: test this endpoint | 196 | // TODO: test this endpoint |
218 | // Download a recipe | 197 | // Download a recipe |
219 | public async download({ response, params }: HttpContextContract) { | 198 | public async download({ response, params }: HttpContext) { |
220 | // Validate user input | 199 | // Validate user input |
221 | let data; | 200 | let data |
222 | try { | 201 | try { |
223 | data = await validator.validate({ | 202 | data = await validator.validate({ |
224 | data: params, | 203 | data: params, |
225 | schema: downloadSchema, | 204 | schema: downloadSchema, |
226 | }); | 205 | }) |
227 | } catch (error) { | 206 | } catch (error) { |
228 | return response.status(401).send({ | 207 | return response.status(401).send({ |
229 | message: 'Please provide a recipe ID', | 208 | message: 'Please provide a recipe ID', |
230 | messages: error.messages, | 209 | messages: error.messages, |
231 | status: 401, | 210 | status: 401, |
232 | }); | 211 | }) |
233 | } | 212 | } |
234 | 213 | ||
235 | const service = data.recipe; | 214 | const service = data.recipe |
236 | 215 | ||
237 | // Check for invalid characters | 216 | // Check for invalid characters |
238 | if (/\.+/.test(service) || /\/+/.test(service)) { | 217 | if (/\.+/.test(service) || /\/+/.test(service)) { |
239 | return response.send('Invalid recipe name'); | 218 | return response.send('Invalid recipe name') |
240 | } | 219 | } |
241 | 220 | ||
242 | // Check if recipe exists in recipes folder | 221 | // Check if recipe exists in recipes folder |
243 | if (await Drive.exists(`${service}.tar.gz`)) { | 222 | if (await Drive.exists(`${service}.tar.gz`)) { |
244 | return response | 223 | return response.type('.tar.gz').send(await Drive.get(`${service}.tar.gz`)) |
245 | .type('.tar.gz') | ||
246 | .send(await Drive.get(`${service}.tar.gz`)); | ||
247 | } | 224 | } |
248 | 225 | ||
249 | return response.status(400).send({ | 226 | return response.status(400).send({ |
250 | message: 'Recipe not found', | 227 | message: 'Recipe not found', |
251 | code: 'recipe-not-found', | 228 | code: 'recipe-not-found', |
252 | }); | 229 | }) |
253 | } | 230 | } |
254 | } | 231 | } |