Skip to content

Commit 63d04d7

Browse files
committed
app update support and better error reporing in _dashboard
1 parent 3ccd161 commit 63d04d7

4 files changed

Lines changed: 130 additions & 54 deletions

File tree

apps/_dashboard/__init__.py

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import base64
22
import copy
33
import datetime
4+
import functools
45
import io
56
import json
67
import os
78
import shutil
89
import subprocess
910
import sys
1011
import tempfile
12+
import traceback
1113
import uuid
1214
import 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+
6682
def 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

430455
def 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

460487
if 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()

apps/_dashboard/static/css/future.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ html {
99
font-size: 14px;
1010
font-family: helvetica;
1111
}
12+
[v-cloak] { display: none; }
1213
.scrollx {
1314
overflow: auto;
1415
}
@@ -254,6 +255,21 @@ label {
254255
background-repeat: no-repeat;
255256
background-position: center center;
256257
}
258+
.dialog-error {
259+
position: fixed;
260+
z-index: 1000;
261+
top:20px;
262+
left:20px;
263+
right:20px;
264+
padding: 20px;
265+
border: 5px solid red;
266+
background: rgba(0,0,0,0.8);
267+
min-height: 50vw;
268+
}
269+
.dialog-error pre {
270+
margin: 20px 0;
271+
font-size: 1.2em;
272+
}
257273
.right {
258274
text-align: right;
259275
margin-bottom: 10px;

0 commit comments

Comments
 (0)