11import base64
22import copy
33import datetime
4+ import functools
45import io
56import json
67import os
78import shutil
89import subprocess
910import sys
1011import tempfile
12+ import traceback
1113import uuid
1214import zipfile
1315
@@ -63,6 +65,20 @@ def wrapper():
6365 field .update = make_safe_field (field .update )
6466
6567
68+ def catch_errors (func ):
69+ """Wraps APIs that return a status=success/error and includes a traceback in response"""
70+
71+ @functools .wraps (func )
72+ def wrapper (* args , ** kwargs ):
73+ try :
74+ result = func (* args , ** kwargs )
75+ except Exception :
76+ result = {"status" : "error" , "traceback" : traceback .format_exc ()}
77+ return result
78+
79+ return wrapper
80+
81+
6682def run (command , project ):
6783 """for runing git commands inside an app (project)"""
6884 return subprocess .check_output (
@@ -219,6 +235,7 @@ def make_grid():
219235
220236 @action ("info" )
221237 @session_secured
238+ @catch_errors
222239 def info ():
223240 vars = [{"name" : "python" , "version" : sys .version }]
224241 for module in sorted (sys .modules ):
@@ -233,6 +250,7 @@ def info():
233250
234251 @action ("routes" )
235252 @session_secured
253+ @catch_errors
236254 def routes ():
237255 """Returns current registered routes"""
238256 sorted_routes = {
@@ -243,6 +261,7 @@ def routes():
243261
244262 @action ("apps" )
245263 @session_secured
264+ @catch_errors
246265 def apps ():
247266 """Returns a list of installed apps"""
248267 apps = os .listdir (FOLDER )
@@ -260,6 +279,7 @@ def apps():
260279
261280 @action ("delete_app/<name:re:\\ w+>" , method = "POST" )
262281 @session_secured
282+ @catch_errors
263283 def delete_app (name ):
264284 """delete the app"""
265285 path = os .path .join (FOLDER , name )
@@ -298,6 +318,7 @@ def new_file(name, file_name):
298318
299319 @action ("walk/<path:path>" )
300320 @session_secured
321+ @catch_errors
301322 def walk (path ):
302323 """Returns a nested folder structure as a tree"""
303324 top = os .path .join (FOLDER , path )
@@ -330,6 +351,7 @@ def walk(path):
330351
331352 @action ("load/<path:path>" )
332353 @session_secured
354+ @catch_errors
333355 def load (path ):
334356 """Loads a text file"""
335357 path = safe_join (FOLDER , path ) or abort ()
@@ -370,10 +392,11 @@ def packed(path):
370392
371393 @action ("tickets" )
372394 @session_secured
395+ @catch_errors
373396 def tickets ():
374397 """Returns most recent tickets grouped by path+error"""
375398 tickets = safely (error_logger .database_logger .get ) if MODE != "DEMO" else None
376- return {"payload" : tickets or []}
399+ return {"payload" : tickets or [], "status" : "success" }
377400
378401 @action ("clear" )
379402 @session_secured
@@ -396,6 +419,7 @@ def error_ticket(ticket_uuid):
396419
397420 @action ("rest/<path:path>" , method = ["GET" , "POST" , "PUT" , "DELETE" ])
398421 @session_secured
422+ @catch_errors
399423 def api (path ):
400424 # this is not final, requires pydal 19.5
401425 args = path .split ("/" )
@@ -405,7 +429,7 @@ def api(path):
405429 module = Reloader .MODULES .get (app_name )
406430
407431 if not module :
408- raise HTTP ( 404 )
432+ return { "status" : "success" , "databases" : []}
409433 databases = [
410434 name for name in dir (module ) if isinstance (getattr (module , name ), DAL )
411435 ]
@@ -423,18 +447,16 @@ def tables(name):
423447 ]
424448
425449 return {
426- "databases" : [{"name" : name , "tables" : tables (name )} for name in databases ]
450+ "status" : "success" ,
451+ "databases" : [{"name" : name , "tables" : tables (name )} for name in databases ],
427452 }
428453
429454
430455def extract (source , target_dir ):
431456 with zipfile .ZipFile (source , "r" ) as zfile :
432457 allfiles = [info .filename for info in zfile .infolist ()]
433- if "__init__.py" in allfiles :
434- # the app is at top level
435- zfile .extractall (target_dir )
436- zfile .close ()
437- else :
458+ roots = None
459+ if not "__init__.py" in allfiles :
438460 # check for subfolders that contain __init__.py
439461 roots = list (
440462 set (
@@ -446,29 +468,36 @@ def extract(source, target_dir):
446468 # there can be only one
447469 if len (roots ) != 1 :
448470 abort (500 )
449- # extract only the subfolder
450- with tempfile .TemporaryDirectory () as tmpdir :
451- zfile .extractall (tmpdir )
452- zfile .close ()
453- shutil .copytree (
454- os .path .join (tmpdir , roots [0 ]),
455- target_dir ,
456- dirs_exist_ok = True ,
457- )
471+ # extract only the subfolder
472+ with tempfile .TemporaryDirectory () as tmpdir :
473+ zfile .extractall (tmpdir )
474+ zfile .close ()
475+ source_dir = tmpdir if roots is None else os .path .join (tmpdir , roots [0 ])
476+ # make sure we do not override the databases and uploads folders:
477+ for folder in ["databases" , "uploads" ]:
478+ if os .path .exists (os .path .join (target_dir , folder )):
479+ shutil .rmtree (os .path .join (source_dir , folder ))
480+ shutil .copytree (
481+ source_dir ,
482+ target_dir ,
483+ dirs_exist_ok = True ,
484+ )
458485
459486
460487if MODE == "full" :
461488
462489 @action ("reload" )
463490 @action ("reload/<name>" )
464491 @session_secured
492+ @catch_errors
465493 def reload (name = None ):
466494 """Reloads installed apps"""
467495 Reloader .import_app (name ) if name else Reloader .import_apps ()
468- return {"status" : "ok " }
496+ return {"status" : "success " }
469497
470498 @action ("save/<path:path>" , method = "POST" )
471499 @session_secured
500+ @catch_errors
472501 def save (path , reload_app = True ):
473502 """Saves a file"""
474503 app_name = path .split ("/" )[0 ]
@@ -482,6 +511,7 @@ def save(path, reload_app=True):
482511
483512 @action ("delete/<path:path>" , method = "POST" )
484513 @session_secured
514+ @catch_errors
485515 def delete (path ):
486516 """Deletes a file"""
487517 fullpath = safe_join (FOLDER , path ) or abort ()
@@ -502,14 +532,13 @@ def prepare_target_dir(form, target_dir):
502532 if form ["mode" ] == "new" :
503533 if os .path .exists (target_dir ):
504534 abort (500 ) # already validated client side
505- elif form ["mode" ] == "replace" :
506- if os .path .exists (target_dir ):
507- shutil .rmtree (target_dir )
508- else :
509- abort (500 ) # not a replacement
535+ elif form ["mode" ] == "update" :
536+ if not os .path .exists (target_dir ):
537+ abort (500 ) # not an update
510538
511539 @action ("new_app" , method = "POST" )
512540 @session_secured
541+ @catch_errors
513542 def new_app ():
514543 form = request .json
515544 # Directory for zipped assets
@@ -566,13 +595,18 @@ def new_app():
566595
567596 @action ("gitlog/<project>" )
568597 @action .uses (Logged (session ), "gitlog.html" )
598+ @catch_errors
569599 def gitlog (project ):
570600 if not is_git_repo (project ):
571601 return "Project is not a GIT repo"
572602 branches = get_branches (project )
573603 commits = get_commits (project )
574604 return dict (
575- commits = commits , checkout = checkout , project = project , branches = branches
605+ status = "success" ,
606+ commits = commits ,
607+ checkout = checkout ,
608+ project = project ,
609+ branches = branches ,
576610 )
577611
578612 @authenticated .callback ()
0 commit comments