vesta-web 1.1.2__py3-none-any.whl → 1.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vesta/http/baseServer.py +314 -83
- vesta/scripts/cli.py +19 -2
- vesta/scripts/utils.py +74 -2
- {vesta_web-1.1.2.dist-info → vesta_web-1.1.3.dist-info}/METADATA +2 -1
- {vesta_web-1.1.2.dist-info → vesta_web-1.1.3.dist-info}/RECORD +8 -8
- {vesta_web-1.1.2.dist-info → vesta_web-1.1.3.dist-info}/WHEEL +1 -1
- {vesta_web-1.1.2.dist-info → vesta_web-1.1.3.dist-info}/entry_points.txt +0 -0
- {vesta_web-1.1.2.dist-info → vesta_web-1.1.3.dist-info}/licenses/LICENSE.md +0 -0
vesta/http/baseServer.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Vesta HTTP Server - Base server implementation with routing and request handling."""
|
|
2
|
+
|
|
1
3
|
import fastwsgi
|
|
2
4
|
import inspect
|
|
3
5
|
|
|
@@ -6,40 +8,58 @@ import time
|
|
|
6
8
|
import json
|
|
7
9
|
import hashlib
|
|
8
10
|
import base64
|
|
9
|
-
|
|
10
11
|
import re
|
|
11
|
-
import urllib
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import traceback
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Any, Optional, Callable
|
|
15
16
|
from io import BytesIO
|
|
16
17
|
|
|
18
|
+
import multipart as mp
|
|
17
19
|
from configparser import ConfigParser
|
|
18
20
|
|
|
19
21
|
from vesta.http import response
|
|
20
22
|
from vesta.http import error
|
|
21
23
|
from vesta.http import redirect
|
|
22
24
|
from vesta.db import db_service as db
|
|
25
|
+
|
|
23
26
|
Response = response.Response
|
|
24
27
|
HTTPRedirect = redirect.HTTPRedirect
|
|
25
28
|
HTTPError = error.HTTPError
|
|
26
29
|
|
|
30
|
+
# Compile regex patterns once for performance
|
|
27
31
|
RE_URL = re.compile(r"[\&]")
|
|
28
32
|
RE_PARAM = re.compile(r"[\=]")
|
|
29
33
|
|
|
30
|
-
routes = {}
|
|
31
|
-
|
|
32
34
|
from colorama import Fore, Style
|
|
33
35
|
from colorama import init as colorama_init
|
|
34
36
|
colorama_init()
|
|
35
37
|
|
|
38
|
+
routes: Dict[str, Dict[str, Any]] = {}
|
|
39
|
+
|
|
36
40
|
class BaseServer:
|
|
37
|
-
|
|
41
|
+
"""Base HTTP server with routing, request parsing, and file handling capabilities."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, path: str, configFile: str, noStart: bool = False):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the Vesta server.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
path: Base path for the server
|
|
49
|
+
configFile: Path to configuration file
|
|
50
|
+
noStart: If True, don't start the server (useful for testing)
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If path is invalid
|
|
54
|
+
FileNotFoundError: If config file doesn't exist
|
|
55
|
+
"""
|
|
56
|
+
self.log("Starting Vesta server...")
|
|
38
57
|
|
|
39
|
-
def __init__(self, path, configFile, noStart=False):
|
|
40
|
-
print(Fore.GREEN,"[INFO] starting Vesta server...")
|
|
41
58
|
self.path = path
|
|
42
59
|
|
|
60
|
+
# Instance-specific features
|
|
61
|
+
self.features: Dict[str, Any] = {}
|
|
62
|
+
|
|
43
63
|
self.importConf(configFile)
|
|
44
64
|
|
|
45
65
|
if noStart:
|
|
@@ -47,19 +67,25 @@ class BaseServer:
|
|
|
47
67
|
|
|
48
68
|
self.start()
|
|
49
69
|
|
|
70
|
+
|
|
50
71
|
#----------------------------HTTP SERVER------------------------------------
|
|
51
|
-
def expose(func):
|
|
52
|
-
|
|
72
|
+
def expose(func: Callable) -> Callable:
|
|
73
|
+
"""
|
|
74
|
+
Decorator to expose a method as an HTTP route.
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
Args:
|
|
77
|
+
func: Function to expose as a route
|
|
56
78
|
|
|
79
|
+
Returns:
|
|
80
|
+
Wrapped function
|
|
81
|
+
"""
|
|
82
|
+
def wrapper(self, *args, **kwargs):
|
|
83
|
+
res = func(self, *args, **kwargs)
|
|
57
84
|
self.response.ok()
|
|
58
85
|
if res:
|
|
59
|
-
|
|
60
86
|
return res.encode()
|
|
61
87
|
else:
|
|
62
|
-
return ""
|
|
88
|
+
return b""
|
|
63
89
|
|
|
64
90
|
name = func.__name__
|
|
65
91
|
if func.__name__ == "index":
|
|
@@ -69,19 +95,42 @@ class BaseServer:
|
|
|
69
95
|
else:
|
|
70
96
|
name = "/" + func.__name__
|
|
71
97
|
|
|
72
|
-
routes[name] = {
|
|
98
|
+
routes[name] = {
|
|
99
|
+
"params": inspect.signature(func).parameters,
|
|
100
|
+
"target": wrapper
|
|
101
|
+
}
|
|
73
102
|
return wrapper
|
|
74
103
|
|
|
75
|
-
|
|
104
|
+
|
|
105
|
+
def saveFile(self, content: str, name: str = "", ext: Optional[str] = None,
|
|
106
|
+
category: Optional[str] = None) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Save a base64-encoded file to the attachments' directory.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
content: Base64-encoded file content with data URI prefix
|
|
112
|
+
name: Optional filename (generated from hash if not provided)
|
|
113
|
+
ext: Optional file extension override
|
|
114
|
+
category: Optional subdirectory category
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Relative path to saved file
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If content is invalid or file size exceeds limit
|
|
121
|
+
IOError: If file cannot be saved
|
|
122
|
+
"""
|
|
76
123
|
content = content.split(",")
|
|
77
124
|
extension = content[0].split("/")[1].split(";")[0]
|
|
78
125
|
content = base64.b64decode(content[1])
|
|
79
126
|
|
|
80
|
-
if not name
|
|
127
|
+
if not name:
|
|
81
128
|
hash_object = hashlib.sha256(content)
|
|
82
129
|
hex_dig = hash_object.hexdigest()
|
|
83
130
|
|
|
84
131
|
name = hex_dig
|
|
132
|
+
else:
|
|
133
|
+
name = self._sanitize_filename(name)
|
|
85
134
|
|
|
86
135
|
prefix = self.path + "/static/attachments/"
|
|
87
136
|
if category:
|
|
@@ -93,17 +142,65 @@ class BaseServer:
|
|
|
93
142
|
f.write(content)
|
|
94
143
|
return name
|
|
95
144
|
|
|
96
|
-
|
|
145
|
+
|
|
146
|
+
def _sanitize_filename(self, filename: str) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Sanitize filename to prevent path traversal and other security issues.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
filename: Filename to sanitize
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Sanitized filename
|
|
155
|
+
"""
|
|
156
|
+
# Remove path separators and dangerous characters
|
|
157
|
+
filename = filename.replace('/', '').replace('\\', '').replace('..', '')
|
|
158
|
+
# Keep only alphanumeric, dash, underscore
|
|
159
|
+
filename = re.sub(r'[^a-zA-Z0-9_-]', '', filename)
|
|
160
|
+
if not filename:
|
|
161
|
+
raise ValueError("Invalid filename after sanitization")
|
|
162
|
+
return filename
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parseCookies(self, cookieStr: Optional[str]) -> Dict[str, str]:
|
|
166
|
+
"""
|
|
167
|
+
Parse HTTP cookie header into a dictionary.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
cookieStr: Raw cookie header string
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary of cookie name-value pairs
|
|
174
|
+
"""
|
|
97
175
|
if not cookieStr:
|
|
98
|
-
return
|
|
176
|
+
return {}
|
|
177
|
+
|
|
99
178
|
cookies = {}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
179
|
+
try:
|
|
180
|
+
for cookie in cookieStr.split(';'):
|
|
181
|
+
if '=' in cookie:
|
|
182
|
+
key, value = cookie.split('=', 1)
|
|
183
|
+
cookies[key.strip()] = value.strip()
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.logWarning(f"Error parsing cookies: {e}")
|
|
186
|
+
|
|
103
187
|
return cookies
|
|
104
188
|
|
|
105
189
|
|
|
106
|
-
|
|
190
|
+
|
|
191
|
+
def parseRequest(self, environ: Dict[str, Any]) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Parse HTTP request into a dictionary of parameters.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
environ: WSGI environ dictionary
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dictionary of request parameters
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If content length exceeds maximum
|
|
203
|
+
"""
|
|
107
204
|
self.response.cookies = self.parseCookies(environ.get('HTTP_COOKIE'))
|
|
108
205
|
|
|
109
206
|
if environ.get('CONTENT_TYPE'):
|
|
@@ -112,30 +209,55 @@ class BaseServer:
|
|
|
112
209
|
content_type = ["text/html"]
|
|
113
210
|
|
|
114
211
|
args = {}
|
|
212
|
+
|
|
213
|
+
# Parse query string
|
|
115
214
|
if environ.get('QUERY_STRING'):
|
|
116
215
|
query = re.split(RE_URL, environ['QUERY_STRING'])
|
|
117
|
-
for
|
|
118
|
-
|
|
119
|
-
|
|
216
|
+
for param in query:
|
|
217
|
+
parts = re.split(RE_PARAM, param)
|
|
218
|
+
if len(parts) == 2:
|
|
219
|
+
args[parts[0]] = urllib.parse.unquote_plus(parts[1], encoding='utf-8')
|
|
220
|
+
|
|
221
|
+
content_length = int(environ.get('CONTENT_LENGTH', 0))
|
|
222
|
+
# Parse multipart form data
|
|
120
223
|
if content_type[0] == "multipart/form-data":
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
try:
|
|
225
|
+
body = environ['wsgi.input'].read(content_length)
|
|
226
|
+
sep = content_type[1].split("=")[1]
|
|
227
|
+
parser = mp.MultipartParser(BytesIO(body), sep.encode('utf-8'))
|
|
228
|
+
for part in parser.parts():
|
|
229
|
+
args[part.name] = part.value
|
|
230
|
+
except Exception as e:
|
|
231
|
+
self.logError(f"Error parsing multipart data: {e}")
|
|
232
|
+
raise ValueError("Invalid multipart data")
|
|
233
|
+
|
|
234
|
+
# Parse JSON
|
|
235
|
+
elif content_type[0] == "application/json" and content_length > 0:
|
|
236
|
+
try:
|
|
237
|
+
body = environ['wsgi.input'].read(content_length)
|
|
238
|
+
data = json.loads(body)
|
|
239
|
+
if isinstance(data, dict):
|
|
240
|
+
args.update(data)
|
|
241
|
+
except json.JSONDecodeError as e:
|
|
242
|
+
self.logError(f"Error parsing JSON: {e}")
|
|
243
|
+
raise ValueError("Invalid JSON data")
|
|
133
244
|
|
|
134
245
|
return args
|
|
135
246
|
|
|
136
247
|
|
|
137
|
-
|
|
138
|
-
|
|
248
|
+
|
|
249
|
+
def tryDefault(self, environ: Dict[str, Any], target: str) -> bytes:
|
|
250
|
+
"""
|
|
251
|
+
Try to handle request with default route.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
environ: WSGI environ dictionary
|
|
255
|
+
target: Request target path
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Response bytes
|
|
259
|
+
"""
|
|
260
|
+
self.logInfo("Vesta - using default route")
|
|
139
261
|
|
|
140
262
|
args = self.parseRequest(environ)
|
|
141
263
|
args["target"] = target
|
|
@@ -147,9 +269,19 @@ class BaseServer:
|
|
|
147
269
|
return self.handleUnexpected(e)
|
|
148
270
|
|
|
149
271
|
|
|
150
|
-
def onrequest(self, environ, start_response):
|
|
272
|
+
def onrequest(self, environ: Dict[str, Any], start_response: Callable) -> bytes:
|
|
273
|
+
"""
|
|
274
|
+
Handle incoming HTTP request.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
environ: WSGI environ dictionary
|
|
278
|
+
start_response: WSGI start_response callable
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Response bytes
|
|
282
|
+
"""
|
|
151
283
|
self.response = Response(start_response=start_response)
|
|
152
|
-
|
|
284
|
+
self.log(f"Vesta - request received: '{environ['PATH_INFO']}' with {environ.get('QUERY_STRING')}")
|
|
153
285
|
target = environ['PATH_INFO']
|
|
154
286
|
|
|
155
287
|
if routes.get(target):
|
|
@@ -162,7 +294,7 @@ class BaseServer:
|
|
|
162
294
|
except (HTTPError, HTTPRedirect):
|
|
163
295
|
return self.response.encode()
|
|
164
296
|
except Exception as e:
|
|
165
|
-
|
|
297
|
+
return self.handleUnexpected(e)
|
|
166
298
|
else:
|
|
167
299
|
if routes.get("default"):
|
|
168
300
|
return self.tryDefault(environ, target)
|
|
@@ -170,21 +302,26 @@ class BaseServer:
|
|
|
170
302
|
self.response.ok()
|
|
171
303
|
return self.response.encode()
|
|
172
304
|
|
|
173
|
-
def handleUnexpected(self, e):
|
|
174
|
-
|
|
305
|
+
def handleUnexpected(self, e: Exception) -> bytes:
|
|
306
|
+
"""
|
|
307
|
+
Handle unexpected errors during request processing.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
e: Exception that occurred
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Error response bytes
|
|
314
|
+
"""
|
|
315
|
+
self.logError(f"Vesta - UNEXPECTED ERROR: {e}", exc_info=True)
|
|
175
316
|
self.response.code = 500
|
|
176
317
|
self.response.ok()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
print(f' File "{filename}", line {lineno}, in {funcname}')
|
|
185
|
-
if self.features.get("debug"):
|
|
186
|
-
self.response.content += f' File "{filename}", line {lineno}, in {funcname}'
|
|
187
|
-
tb = tb.tb_next
|
|
318
|
+
|
|
319
|
+
# Only expose detailed errors in debug mode
|
|
320
|
+
if self.config.getboolean("server", "DEBUG"):
|
|
321
|
+
self.response.content = str(e) + "\n\n" + traceback.format_exc()
|
|
322
|
+
else:
|
|
323
|
+
self.response.content = "Internal Server Error"
|
|
324
|
+
|
|
188
325
|
return self.response.encode()
|
|
189
326
|
|
|
190
327
|
def onStart(self):
|
|
@@ -192,66 +329,160 @@ class BaseServer:
|
|
|
192
329
|
|
|
193
330
|
#--------------------------GENERAL USE METHODS------------------------------
|
|
194
331
|
|
|
195
|
-
def importConf(self, configFile):
|
|
332
|
+
def importConf(self, configFile: str):
|
|
333
|
+
"""
|
|
334
|
+
Import configuration from file.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
configFile: Path to configuration file
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
FileNotFoundError: If config file doesn't exist
|
|
341
|
+
"""
|
|
196
342
|
self.config = ConfigParser()
|
|
343
|
+
config_path = self.path + configFile
|
|
344
|
+
|
|
197
345
|
try:
|
|
198
|
-
self.config.read(
|
|
199
|
-
print(Fore.GREEN,"[INFO] Vesta - config at " +
|
|
346
|
+
self.config.read(config_path)
|
|
347
|
+
print(Fore.GREEN,"[INFO] Vesta - config at " + config_path + " loaded")
|
|
200
348
|
except Exception:
|
|
201
349
|
print(Fore.RED,"[ERROR] Vesta - Please create a config file")
|
|
202
350
|
|
|
203
351
|
def start(self):
|
|
352
|
+
"""Start the HTTP server."""
|
|
204
353
|
self.fileCache = {}
|
|
205
354
|
|
|
206
355
|
if self.features.get("errors"):
|
|
207
356
|
for code, page in self.features["errors"].items():
|
|
208
357
|
Response.ERROR_PAGES[code] = self.path + page
|
|
209
358
|
|
|
210
|
-
if self.features.get("orm")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
359
|
+
if self.features.get("orm"):
|
|
360
|
+
try:
|
|
361
|
+
self.db = db.DB(
|
|
362
|
+
user=self.config.get('DB', 'DB_USER'),
|
|
363
|
+
password=self.config.get('DB', 'DB_PASSWORD'),
|
|
364
|
+
host=self.config.get('DB', 'DB_HOST'),
|
|
365
|
+
port=int(self.config.get('DB', 'DB_PORT')),
|
|
366
|
+
db=self.config.get('DB', 'DB_NAME')
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
self.logError(f"Failed to initialize database: {e}")
|
|
370
|
+
raise
|
|
215
371
|
|
|
216
372
|
self.onStart()
|
|
217
373
|
|
|
218
374
|
fastwsgi.server.nowait = 1
|
|
219
375
|
fastwsgi.server.hook_sigint = 1
|
|
220
376
|
|
|
221
|
-
|
|
222
|
-
fastwsgi.server.init(
|
|
223
|
-
|
|
377
|
+
self.logInfo(f"Vesta - server running on PID: {os.getpid()} and port {self.config.get('server', 'PORT')}")
|
|
378
|
+
fastwsgi.server.init(
|
|
379
|
+
app=self.onrequest,
|
|
380
|
+
host=self.config.get('server', 'IP'),
|
|
381
|
+
port=int(self.config.get('server', 'PORT'))
|
|
382
|
+
)
|
|
383
|
+
|
|
224
384
|
while True:
|
|
225
385
|
code = fastwsgi.server.run()
|
|
226
386
|
if code != 0:
|
|
227
387
|
break
|
|
228
388
|
time.sleep(0)
|
|
389
|
+
|
|
229
390
|
self.close()
|
|
230
391
|
|
|
231
392
|
def close(self):
|
|
232
|
-
|
|
393
|
+
"""Shutdown the server gracefully."""
|
|
394
|
+
|
|
395
|
+
self.logInfo("SIGTERM/SIGINT received")
|
|
396
|
+
|
|
397
|
+
# Close database connection if it exists
|
|
398
|
+
if hasattr(self, 'db'):
|
|
399
|
+
try:
|
|
400
|
+
self.db.close()
|
|
401
|
+
except Exception as e:
|
|
402
|
+
self.logError(f"Error closing database: {e}")
|
|
403
|
+
|
|
233
404
|
fastwsgi.server.close()
|
|
234
|
-
|
|
235
|
-
|
|
405
|
+
self.logInfo("SERVER STOPPED")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def file(self, path: str, responseFile: bool = True) -> str:
|
|
409
|
+
"""
|
|
410
|
+
Read a file with caching support.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
path: Path to file (relative to server path or absolute)
|
|
414
|
+
responseFile: If True, set response type to HTML
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
File content as string
|
|
236
418
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
self.response.headers = [('Content-Type', 'text/html')]
|
|
419
|
+
Raises:
|
|
420
|
+
ValueError: If path traversal is detected
|
|
421
|
+
"""
|
|
241
422
|
file = self.fileCache.get(path)
|
|
242
423
|
if file:
|
|
243
424
|
return file
|
|
244
425
|
else:
|
|
426
|
+
# Validate path to prevent traversal
|
|
427
|
+
file_path = Path(path)
|
|
428
|
+
if not file_path.is_absolute():
|
|
429
|
+
file_path = self.path / file_path
|
|
430
|
+
|
|
431
|
+
file_path = file_path.resolve()
|
|
432
|
+
|
|
433
|
+
# Ensure path is within server directory
|
|
434
|
+
if not str(file_path).startswith(str(self.path.resolve())):
|
|
435
|
+
raise ValueError("Invalid file path: path traversal attempt detected")
|
|
436
|
+
|
|
437
|
+
if not file_path.exists():
|
|
438
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
439
|
+
|
|
245
440
|
file = open(path)
|
|
246
441
|
content = file.read()
|
|
247
442
|
file.close()
|
|
248
|
-
self.fileCache[path] = content
|
|
249
443
|
return content
|
|
250
444
|
|
|
445
|
+
|
|
251
446
|
def start_ORM(self):
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
447
|
+
"""
|
|
448
|
+
Start ORM connection (for manual initialization).
|
|
449
|
+
|
|
450
|
+
Raises:
|
|
451
|
+
Exception: If ORM is not enabled in features
|
|
452
|
+
"""
|
|
453
|
+
if not self.features.get("orm"):
|
|
454
|
+
raise Exception("ORM not enabled in server features")
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
self.db = db.DB(
|
|
458
|
+
user=self.config.get('DB', 'DB_USER'),
|
|
459
|
+
password=self.config.get('DB', 'DB_PASSWORD'),
|
|
460
|
+
host=self.config.get('DB', 'DB_HOST'),
|
|
461
|
+
port=int(self.config.get('DB', 'DB_PORT')),
|
|
462
|
+
db=self.config.get('DB', 'DB_NAME')
|
|
463
|
+
)
|
|
464
|
+
self.logInfo("Vesta - ORM database connection established")
|
|
465
|
+
except Exception as e:
|
|
466
|
+
self.logError(f"Failed to start ORM: {e}")
|
|
467
|
+
raise
|
|
468
|
+
|
|
469
|
+
def log(self, message):
|
|
470
|
+
print(Fore.WHITE,"[LOG]", message, Style.RESET_ALL)
|
|
471
|
+
|
|
472
|
+
def logInfo(self, message):
|
|
473
|
+
print(Fore.GREEN,"[INFO]", message, Style.RESET_ALL)
|
|
474
|
+
|
|
475
|
+
def logWarning(self, message):
|
|
476
|
+
print(Fore.ORANGE,"[WARNING]", message, Style.RESET_ALL)
|
|
477
|
+
|
|
478
|
+
def logError(self, message: str, exc_info: bool = False):
|
|
479
|
+
"""
|
|
480
|
+
Log an error message.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
message: Error message to log
|
|
484
|
+
exc_info: If True, include exception traceback
|
|
485
|
+
"""
|
|
486
|
+
print(Fore.RED, "[ERROR]", message, Style.RESET_ALL)
|
|
487
|
+
if exc_info:
|
|
488
|
+
print(Fore.RED, traceback.format_exc(), Style.RESET_ALL)
|
vesta/scripts/cli.py
CHANGED
|
@@ -171,11 +171,16 @@ def main():
|
|
|
171
171
|
# Commande init
|
|
172
172
|
parser_init = subparsers.add_parser('init', help='Initialize a new Vesta project')
|
|
173
173
|
parser_init = subparsers.add_parser('install', help='Import dependencies')
|
|
174
|
-
parser_db = subparsers.add_parser('db', help='Manage the database')
|
|
175
174
|
parser_update = subparsers.add_parser('update', help='Update dependencies')
|
|
176
175
|
parser_test = subparsers.add_parser('test', help='Run tests')
|
|
177
176
|
parser_add_feature = subparsers.add_parser('add-feature', help='Add a feature to the project')
|
|
178
177
|
|
|
178
|
+
parser_db = subparsers.add_parser('db', help='Manage the database')
|
|
179
|
+
db_subparsers = parser_db.add_subparsers(dest='db_command')
|
|
180
|
+
parser_db_create = db_subparsers.add_parser('create', help='Create and initialize the database')
|
|
181
|
+
parser_db_init = db_subparsers.add_parser('init', help='Initialize the database')
|
|
182
|
+
parser_db_reset = db_subparsers.add_parser('reset', help='Reset the database ')
|
|
183
|
+
|
|
179
184
|
parser_nginx = subparsers.add_parser('nginx', help='Setup/manage nginx configuration')
|
|
180
185
|
nginx_subparsers = parser_nginx.add_subparsers(dest='nginx_command')
|
|
181
186
|
parser_nginx_setup = nginx_subparsers.add_parser('setup', help='Install nginx config')
|
|
@@ -192,7 +197,19 @@ def main():
|
|
|
192
197
|
if args.command == 'init':
|
|
193
198
|
init_project()
|
|
194
199
|
elif args.command == 'db':
|
|
195
|
-
|
|
200
|
+
installer = Installer(PATH + "/server.ini", PATH)
|
|
201
|
+
|
|
202
|
+
if args.db_command == 'create':
|
|
203
|
+
installer.createDB()
|
|
204
|
+
installer.createUniauth()
|
|
205
|
+
installer.initDB()
|
|
206
|
+
elif args.nginx_command == 'init':
|
|
207
|
+
installer.initDB()
|
|
208
|
+
elif args.nginx_command == 'reset':
|
|
209
|
+
installer.resetDB()
|
|
210
|
+
else:
|
|
211
|
+
parser_db.print_help()
|
|
212
|
+
|
|
196
213
|
elif args.command == 'update':
|
|
197
214
|
updateDeps()
|
|
198
215
|
elif args.command == 'install':
|
vesta/scripts/utils.py
CHANGED
|
@@ -2,6 +2,10 @@ import subprocess
|
|
|
2
2
|
from configparser import ConfigParser
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
from vesta import Server
|
|
6
|
+
import sys
|
|
7
|
+
from psycopg import sql
|
|
8
|
+
|
|
5
9
|
def ex(command):
|
|
6
10
|
subprocess.run(command, shell=True, check=True)
|
|
7
11
|
|
|
@@ -9,7 +13,7 @@ class Installer:
|
|
|
9
13
|
def __init__(self, configFile, path):
|
|
10
14
|
self.PATH = path
|
|
11
15
|
self.importConf(configFile)
|
|
12
|
-
self.uniauth =
|
|
16
|
+
self.uniauth = False
|
|
13
17
|
self.name = self.config.get("server", "SERVICE_NAME").replace(" ", "_").lower()
|
|
14
18
|
|
|
15
19
|
def installNginx(self, link=True):
|
|
@@ -81,4 +85,72 @@ class Installer:
|
|
|
81
85
|
for key in templates:
|
|
82
86
|
data = data.replace(key, templates[key])
|
|
83
87
|
with open(file+"_filled", "w+") as f:
|
|
84
|
-
f.write(data)
|
|
88
|
+
f.write(data)
|
|
89
|
+
|
|
90
|
+
# -----------------__DB METHODS__----------------- #
|
|
91
|
+
def initDB(self):
|
|
92
|
+
initializer = DBInitializer(path=self.PATH, configFile=self.PATH + "/server.ini", noStart=True)
|
|
93
|
+
if self.uniauth:
|
|
94
|
+
initializer.initUniauth()
|
|
95
|
+
initializer.initDB()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def resetDB(self):
|
|
99
|
+
ex("sudo -u postgres dropdb " + self.config.get("DB", "DB_NAME"))
|
|
100
|
+
self.initDB()
|
|
101
|
+
|
|
102
|
+
def createUniauth(self):
|
|
103
|
+
while True:
|
|
104
|
+
uniauth = input("Do you want to create a uniauth database? (y/n)")
|
|
105
|
+
if uniauth.upper() == 'Y' or uniauth.upper() == 'N':
|
|
106
|
+
self.uniauth = uniauth.upper()
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if self.uniauth == 'Y':
|
|
110
|
+
ex("sudo -u postgres createdb " + self.config.get("UNIAUTH", "DB_NAME"))
|
|
111
|
+
|
|
112
|
+
def createDB(self):
|
|
113
|
+
ex("sudo -u postgres createdb " + self.config.get("DB", "DB_NAME"))
|
|
114
|
+
|
|
115
|
+
def createUser(self):
|
|
116
|
+
ex("sudo -u postgres createuser " + self.config.get("DB", "DB_USER") + " -s --pwprompt ")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class DBInitializer(Server):
|
|
120
|
+
def initUniauth(self):
|
|
121
|
+
self.uniauth.initUniauth()
|
|
122
|
+
|
|
123
|
+
def initDB(self):
|
|
124
|
+
self.referenceUniauth()
|
|
125
|
+
self.db.cur.execute(open(self.path + "/db/schema.sql", "r").read())
|
|
126
|
+
self.db.conn.commit()
|
|
127
|
+
|
|
128
|
+
def referenceUniauth(self):
|
|
129
|
+
self.db.cur.execute("CREATE EXTENSION if not exists postgres_fdw;")
|
|
130
|
+
self.db.cur.execute(
|
|
131
|
+
sql.SQL("""
|
|
132
|
+
CREATE SERVER if not exists uniauth
|
|
133
|
+
FOREIGN DATA WRAPPER postgres_fdw
|
|
134
|
+
OPTIONS (host %s, port %s, dbname %s);
|
|
135
|
+
"""),
|
|
136
|
+
(
|
|
137
|
+
self.config.get('UNIAUTH', 'DB_HOST'),
|
|
138
|
+
self.config.get('UNIAUTH', 'DB_PORT'),
|
|
139
|
+
self.config.get('UNIAUTH', 'DB_NAME')
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
self.db.cur.execute(
|
|
143
|
+
sql.SQL("""
|
|
144
|
+
CREATE USER MAPPING if not exists FOR CURRENT_USER SERVER uniauth
|
|
145
|
+
OPTIONS (user %s, password %s);
|
|
146
|
+
"""),
|
|
147
|
+
(
|
|
148
|
+
self.config.get('DB', 'DB_USER'),
|
|
149
|
+
self.config.get('DB', 'DB_PASSWORD')
|
|
150
|
+
))
|
|
151
|
+
self.db.cur.execute(
|
|
152
|
+
"""
|
|
153
|
+
CREATE FOREIGN TABLE if not exists account (id bigserial NOT NULL)
|
|
154
|
+
SERVER uniauth
|
|
155
|
+
OPTIONS (schema_name 'public', table_name 'account');
|
|
156
|
+
""")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vesta-web
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: An extensive web framework adding every feature needed for Carbonlab
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/Louciole/vesta
|
|
6
6
|
Project-URL: Issues, https://gitlab.com/Louciole/vesta/-/issues
|
|
@@ -23,6 +23,7 @@ Requires-Dist: fastwsgi
|
|
|
23
23
|
Requires-Dist: multipart
|
|
24
24
|
Requires-Dist: psycopg
|
|
25
25
|
Requires-Dist: pyjwt
|
|
26
|
+
Requires-Dist: setuptools
|
|
26
27
|
Requires-Dist: websockets
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
28
29
|
|
|
@@ -45,18 +45,18 @@ vesta/emptyProject/static/translations/fr.mjs,sha256=ouMluPVTgB4Q5vmb7zGE6YGTH4U
|
|
|
45
45
|
vesta/emptyProject/static/translations/translation.mjs,sha256=JxJ2peSlYVQK-bUKpfddPLXm0XZiz2yu6A6iWIqpKyM,1422
|
|
46
46
|
vesta/emptyProject/static/ws/onMessage.mjs,sha256=ow5nwSEdiBcvm-Y2zOUMhnqLp-5xWgo11kHviaTRlTw,658
|
|
47
47
|
vesta/emptyProject/tests/example/foo.py,sha256=NS9oIXFBOvIyWK1LHwkJm9amJuSMN4cxJwouBrJlh2I,115
|
|
48
|
-
vesta/http/baseServer.py,sha256=
|
|
48
|
+
vesta/http/baseServer.py,sha256=To64gSOIvpgEeMz5shQnqzexgVVgJcGOBsEbGA_IN8c,14785
|
|
49
49
|
vesta/http/error.py,sha256=fWdp-oI2ObJD2mHHuxs1yVJvhON5oHYgYFRLAcUMs-I,180
|
|
50
50
|
vesta/http/redirect.py,sha256=OiDeOmU-X5Mos8a0BQIeOIJqvgWjDEtaYrM4-x4MXl0,177
|
|
51
51
|
vesta/http/response.py,sha256=G7cmbrXFNbIbQoqNxNkR06I5VymIwjFSAe3LtVa56Ok,3760
|
|
52
52
|
vesta/mailing/mailing_service.py,sha256=GBO5Hnspm9Pqwd5kGB0iekZaMoIrfQvrhMUf8tVma7g,5386
|
|
53
|
-
vesta/scripts/cli.py,sha256=
|
|
53
|
+
vesta/scripts/cli.py,sha256=3vKp-imzaAxLHkOwFcNznQ8t9j3St9m0G6RJMpErJIc,8230
|
|
54
54
|
vesta/scripts/initDB.py,sha256=TKaK4RZM6CycBEsHeGb9Q9PdphkQgaJDnEWhvRnGC9k,1659
|
|
55
55
|
vesta/scripts/install.py,sha256=GvH_HHx5aU5_54RQ1_2vz4DaLCh42AHfUKy-m0q21vY,2125
|
|
56
56
|
vesta/scripts/testsRun.py,sha256=bXJImdexKQUDW8CR8F9VIKTrgkd7QfnvHQPENEV4x38,2463
|
|
57
|
-
vesta/scripts/utils.py,sha256=
|
|
58
|
-
vesta_web-1.1.
|
|
59
|
-
vesta_web-1.1.
|
|
60
|
-
vesta_web-1.1.
|
|
61
|
-
vesta_web-1.1.
|
|
62
|
-
vesta_web-1.1.
|
|
57
|
+
vesta/scripts/utils.py,sha256=g5QKt1Xeezqf56m4ETLvYBhbZ82eg3qHRPKdjE6mjAQ,5860
|
|
58
|
+
vesta_web-1.1.3.dist-info/METADATA,sha256=7EY_IDwbSTQEmK2TXoIcm6A5sCt9Bojo_dhDR6s_mcc,1584
|
|
59
|
+
vesta_web-1.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
60
|
+
vesta_web-1.1.3.dist-info/entry_points.txt,sha256=MHMrWJwtkb4FmNz0CTpxZzwQ3LTqndXBh8YBPDfXlW4,49
|
|
61
|
+
vesta_web-1.1.3.dist-info/licenses/LICENSE.md,sha256=zoPFEFUUoSgosmDBK5fGTWGRHHBaSVuuJT2ZQIYXuIk,177
|
|
62
|
+
vesta_web-1.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|