Prep admin form for production deployment
Only display Wifi Settings if raspap is installed. Change_manager is now more standalone and can be run to monitor when settings.txt is last modified.
This commit is contained in:
parent
0cfd475031
commit
83fd4c4ec5
5 changed files with 106 additions and 69 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
from app import app
|
from app import app
|
||||||
from flask import render_template, flash, redirect, url_for, request, session, send_file
|
from flask import render_template, flash, redirect, url_for, send_file
|
||||||
from app.forms import LoginForm, SettingsForm
|
from app.forms import LoginForm, SettingsForm
|
||||||
from flask_login import login_user, current_user, logout_user, login_required
|
from flask_login import login_user, current_user, logout_user, login_required
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
@ -16,7 +16,7 @@ import string
|
||||||
import glob
|
import glob
|
||||||
import time
|
import time
|
||||||
import qrcode
|
import qrcode
|
||||||
from app.change_manager import CHANGES_REQUIRING_RESTART, check_settings
|
from app.change_manager import CHANGES_REQUIRING_RESTART
|
||||||
|
|
||||||
def gen_username() -> str:
|
def gen_username() -> str:
|
||||||
words = top_n_list("en", 5000)
|
words = top_n_list("en", 5000)
|
||||||
|
|
@ -155,6 +155,7 @@ def logout():
|
||||||
@app.route('/admin', methods=['GET', 'POST'])
|
@app.route('/admin', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin():
|
def admin():
|
||||||
|
raspap_installed = os.path.exists("/var/www/html/raspap")
|
||||||
form = SettingsForm()
|
form = SettingsForm()
|
||||||
populate_settings = ['butterbox_name', 'wifi_password', 'ssid', 'butterbox_hostname', 'root_account_settings', 'ssh_access_settings']
|
populate_settings = ['butterbox_name', 'wifi_password', 'ssid', 'butterbox_hostname', 'root_account_settings', 'ssh_access_settings']
|
||||||
bool_settings = ['enable_access_point','enable_file_viewer', 'enable_map_viewer', 'enable_app_store', 'enable_chat', 'enable_deltachat', 'enable_wifi_sharing']
|
bool_settings = ['enable_access_point','enable_file_viewer', 'enable_map_viewer', 'enable_app_store', 'enable_chat', 'enable_deltachat', 'enable_wifi_sharing']
|
||||||
|
|
@ -214,10 +215,9 @@ def admin():
|
||||||
if form.apply_changes.data:
|
if form.apply_changes.data:
|
||||||
set_setting('apply_changes', "true")
|
set_setting('apply_changes', "true")
|
||||||
dump_settings("settings.txt")
|
dump_settings("settings.txt")
|
||||||
check_settings()
|
|
||||||
flash(_("⚠️ Changes applied! Please wait for the box to restart."))
|
flash(_("⚠️ Changes applied! Please wait for the box to restart."))
|
||||||
|
|
||||||
return render_template('admin.html', get_setting=get_setting, form=form)
|
return render_template('admin.html', raspap_installed=raspap_installed, get_setting=get_setting, form=form)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/messaging', methods=['GET', 'POST'])
|
@app.route('/messaging', methods=['GET', 'POST'])
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
.butter-title {
|
.butter-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.butter-form-margin {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
.butter-service {
|
.butter-service {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,61 +16,90 @@
|
||||||
{{ form.apply_changes(class="button is-warning") }}
|
{{ form.apply_changes(class="button is-warning") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
{{ wtf.form_input_field(form.ssid, form.ssid.errors) }}
|
|
||||||
<p class="help"> This is the name of the advertised Wi-Fi network. Current SSID: {{ get_setting('ssid') }}</p>
|
<label class="label is-large">Services</label>
|
||||||
|
|
||||||
|
<div class="field checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.enable_map_viewer) }}
|
||||||
|
<p class="help butter-form-margin">Whether map services are enabled.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="password">
|
<div class="field checkbox">
|
||||||
{{ wtf.form_input_field(form.wifi_password, form.wifi_password.errors) }}
|
{{ wtf.form_bool_field(form.enable_chat) }}
|
||||||
<p class="help"> This is the secret key needed to connect to the Wi-Fi network. By default, this is not set and everyone can join.
|
<p class="help butter-form-margin">Whether Matrix chat services are enabled.</p>
|
||||||
Current password: {{ get_setting('wifi_password') or 'Not set' }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.enable_deltachat) }}
|
||||||
|
<p class="help butter-form-margin">Whether messaging using DeltaChat is enabled.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.enable_file_viewer) }}
|
||||||
|
<p class="help butter-form-margin">Whether files services via USB are enabled.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.enable_app_store) }}
|
||||||
|
<p class="help">Whether app store services are enabled.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<label class="label is-large">Branding and name</label>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ wtf.form_input_field(form.butterbox_name, form.butterbox_name.errors) }}
|
{{ wtf.form_input_field(form.butterbox_name, form.butterbox_name.errors) }}
|
||||||
<p class="help">This is the name shown in the UI.
|
<p class="help">This is the name shown in the UI.
|
||||||
Current name: {{ get_setting('butterbox_name') }}, accessed at {{ get_setting('butterbox_name') }}.local.</p>
|
Current name: {{ get_setting('butterbox_name') }}, accessed at {{ get_setting('butterbox_name') }}.local.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{ wtf.form_input_field(form.butterbox_hostname, form.butterbox_hostname.errors) }}
|
{{ wtf.form_input_field(form.butterbox_hostname, form.butterbox_hostname.errors) }}
|
||||||
<p class="help">This is used to access the box locally by adding .local or .lan in your browser.
|
<p class="help">This is the URL used to access the box by adding .local in your browser.
|
||||||
Current hostname: {{ get_setting('butterbox_hostname') }}.local.</p>
|
Current hostname: {{ get_setting('butterbox_hostname') }}.local.</p>
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_access_point) }}
|
|
||||||
<p class="help">Whether this box will advertise a Wi-Fi network.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_wifi_sharing) }}
|
|
||||||
<p class="help">Whether a share button for the Wi-Fi network is available.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_map_viewer) }}
|
|
||||||
<p class="help">Whether map services are enabled.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_chat) }}
|
|
||||||
<p class="help">Whether chat services are enabled.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_deltachat) }}
|
|
||||||
<p class="help">Whether secure messaging using DeltaChat is enabled.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_file_viewer) }}
|
|
||||||
<p class="help">Whether files services via USB are enabled.</p>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
{{ wtf.form_bool_field(form.enable_app_store) }}
|
|
||||||
<p class="help">Whether app store services are enabled.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
<label class="label">{{ form.butterbox_logo.label }} </label>
|
||||||
|
<div class="control block">{{ form.butterbox_logo(class='label', style="width: 280px") }}</div>
|
||||||
|
{{ wtf.field_errors(form.butterbox_logo.errors) }}
|
||||||
|
<div class="block"><p class="help">This is the logo shown in the UI. Current logo:</p></div>
|
||||||
|
<img src="{{ get_setting('butterbox_logo') }}" style="height: 50px">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<label class="label is-large">Wi-Fi and access point</label>
|
||||||
|
|
||||||
|
{% if raspap_installed %}
|
||||||
|
<div class="field">
|
||||||
|
{{ wtf.form_input_field(form.ssid, form.ssid.errors) }}
|
||||||
|
<p class="help"> This is the name of the advertised Wi-Fi network. Current SSID: {{ get_setting('ssid') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field password">
|
||||||
|
{{ wtf.form_input_field(form.wifi_password, form.wifi_password.errors) }}
|
||||||
|
<p class="help"> This is the secret key needed to connect to the Wi-Fi network. By default, this is not set and everyone can join.
|
||||||
|
Current password: {{ get_setting('wifi_password') or 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.enable_access_point) }}
|
||||||
|
<p class="butter-form-margin help">Whether this box will advertise a Wi-Fi network.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.enable_wifi_sharing) }}
|
||||||
|
<p class="butter-form-margin help">Whether a share button for the Wi-Fi network is available.</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p> Access point is only enabled when using a Raspberry Pi. </p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<label class="label is-large">Access and security</label>
|
||||||
|
|
||||||
|
<div class="control field">
|
||||||
{{ wtf.form_password_field(form.admin_password, form.admin_password.errors) }}
|
{{ wtf.form_password_field(form.admin_password, form.admin_password.errors) }}
|
||||||
<p class="help">Password for accessing this browser interface.</p>
|
<p class="help">Password for accessing this browser interface.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field block">
|
<div class="control block">
|
||||||
<label class="label">{{ form.root_account_settings.label }} </label>
|
<label class="label">{{ form.root_account_settings.label }} </label>
|
||||||
{% for subfield in form.root_account_settings %}
|
{% for subfield in form.root_account_settings %}
|
||||||
<label class="radio">
|
<label class="radio butter-form-margin">
|
||||||
{% if get_setting('root_account_settings') == subfield._value() %}
|
{% if get_setting('root_account_settings') == subfield._value() %}
|
||||||
<input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/>
|
<input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -85,7 +114,7 @@
|
||||||
<div class="control block">
|
<div class="control block">
|
||||||
<label class="label">{{ form.ssh_access_settings.label }} </label>
|
<label class="label">{{ form.ssh_access_settings.label }} </label>
|
||||||
{% for subfield in form.ssh_access_settings %}
|
{% for subfield in form.ssh_access_settings %}
|
||||||
<label class="radio">
|
<label class="radio butter-form-margin">
|
||||||
{% if get_setting('ssh_access_settings') == subfield._value() %}
|
{% if get_setting('ssh_access_settings') == subfield._value() %}
|
||||||
<input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/>
|
<input id='{{subfield.id}}' type='radio' name='{{subfield.name}}' value='{{subfield._value()}}' checked/>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -96,13 +125,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ wtf.field_errors(form.ssh_access_settings.errors) }}
|
{{ wtf.field_errors(form.ssh_access_settings.errors) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ form.butterbox_logo.label }} </label>
|
|
||||||
<div class="control block">{{ form.butterbox_logo(class='label', style="width: 280px") }}</div>
|
|
||||||
{{ wtf.field_errors(form.butterbox_logo.errors) }}
|
|
||||||
<div class="block"><p class="help">This is the logo shown in the UI. Current logo:</p></div>
|
|
||||||
<img src="{{ get_setting('butterbox_logo') }}" style="height: 50px">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
import json
|
import json
|
||||||
from app import app
|
|
||||||
|
|
||||||
CHANGES_REQUIRING_RESTART = ['wifi_password', 'ssid', 'enable_access_point', 'enable_chat', 'enable_delta_chat', 'butterbox_hostname']
|
CHANGES_REQUIRING_RESTART = ['wifi_password', 'ssid', 'enable_access_point', 'enable_chat', 'enable_delta_chat', 'butterbox_hostname']
|
||||||
# 'ssh_access_settings', 'root_account_settings']
|
# 'ssh_access_settings', 'root_account_settings']
|
||||||
|
|
@ -15,6 +16,8 @@ def lock_root_account():
|
||||||
|
|
||||||
def enable_service(service: str):
|
def enable_service(service: str):
|
||||||
is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True)
|
is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True)
|
||||||
|
if 'not_found' in is_enabled.stdout:
|
||||||
|
return False
|
||||||
if 'disabled' in is_enabled.stdout:
|
if 'disabled' in is_enabled.stdout:
|
||||||
enable = run(["sudo", "systemctl", "enable", service], capture_output = True, text = True)
|
enable = run(["sudo", "systemctl", "enable", service], capture_output = True, text = True)
|
||||||
if enable.returncode != 0:
|
if enable.returncode != 0:
|
||||||
|
|
@ -23,6 +26,8 @@ def enable_service(service: str):
|
||||||
|
|
||||||
def disable_service(service: str):
|
def disable_service(service: str):
|
||||||
is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True)
|
is_enabled = run(["sudo", "systemctl", "is-enabled", service], capture_output = True, text = True)
|
||||||
|
if 'not_found' in is_enabled.stdout:
|
||||||
|
return False
|
||||||
if 'enabled' in is_enabled.stdout:
|
if 'enabled' in is_enabled.stdout:
|
||||||
result = run(["sudo", "systemctl", "disable", service])
|
result = run(["sudo", "systemctl", "disable", service])
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
|
@ -30,7 +35,7 @@ def disable_service(service: str):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load_setting(setting):
|
def load_setting(setting):
|
||||||
with open("settings.txt", "r") as f:
|
with open("./app/settings.txt", "r") as f:
|
||||||
settings = json.load(f)
|
settings = json.load(f)
|
||||||
return settings[setting]
|
return settings[setting]
|
||||||
|
|
||||||
|
|
@ -61,7 +66,6 @@ def change_line_in_file(target_file: str, regex: str, replacement: str):
|
||||||
else:
|
else:
|
||||||
with open(target_file, "r") as f:
|
with open(target_file, "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
print(f"Existing lines are: {lines}")
|
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
match = re.fullmatch(replacement, line)
|
match = re.fullmatch(replacement, line)
|
||||||
if match:
|
if match:
|
||||||
|
|
@ -71,24 +75,24 @@ def change_line_in_file(target_file: str, regex: str, replacement: str):
|
||||||
match = re.fullmatch(regex, line)
|
match = re.fullmatch(regex, line)
|
||||||
if match:
|
if match:
|
||||||
lines.pop(i)
|
lines.pop(i)
|
||||||
print(f"Found a match at line {i}, {match.string}")
|
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
lines.append(replacement)
|
lines.append(replacement)
|
||||||
except NameError:
|
except NameError:
|
||||||
raise NameError(f"File {target_file} is empty.")
|
raise NameError(f"File {target_file} is empty.")
|
||||||
print(f"Lines to be written to file are {lines}")
|
|
||||||
new_lines = "".join(lines)
|
new_lines = "".join(lines)
|
||||||
with open(target_file, "w") as f:
|
with open(target_file, "w") as f:
|
||||||
f.write(new_lines)
|
f.write(new_lines)
|
||||||
|
|
||||||
|
|
||||||
def check_settings():
|
def check_settings(raspap_installed: bool):
|
||||||
print(f"Checking settings...")
|
if not os.path.exists("./settings.txt"):
|
||||||
print(f"App config is {app.config['SETTINGS_CHANGED']}")
|
return
|
||||||
if app.config['SETTINGS_CHANGED']:
|
last_modified = os.path.getmtime('./settings.txt')
|
||||||
|
diff_in_minutes = (datetime.now().timestamp() - last_modified)/60
|
||||||
|
if diff_in_minutes < 2:
|
||||||
for s in CHANGES_REQUIRING_RESTART:
|
for s in CHANGES_REQUIRING_RESTART:
|
||||||
if s == "wifi_password":
|
if s == "wifi_password" and raspap_installed:
|
||||||
regex_wpa_method = "wpa=.*?\n"
|
regex_wpa_method = "wpa=.*?\n"
|
||||||
if load_setting("wifi_password") == "":
|
if load_setting("wifi_password") == "":
|
||||||
change_line_in_file("/etc/hostapd/hostapd.conf", regex_wpa_method, f"wpa=none\n")
|
change_line_in_file("/etc/hostapd/hostapd.conf", regex_wpa_method, f"wpa=none\n")
|
||||||
|
|
@ -98,25 +102,26 @@ def check_settings():
|
||||||
change_line_in_file("/etc/hostapd/hostapd.conf", regex_pass,
|
change_line_in_file("/etc/hostapd/hostapd.conf", regex_pass,
|
||||||
f"wpa_passphrase={load_setting("wifi_password")}\n")
|
f"wpa_passphrase={load_setting("wifi_password")}\n")
|
||||||
|
|
||||||
if s == "ssid":
|
if s == "ssid" and raspap_installed:
|
||||||
regex_ssid = "ssid=.*?\n"
|
regex_ssid = "ssid=.*?\n"
|
||||||
change_line_in_file("/etc/hostapd/hostapd.conf", regex_ssid, f"ssid={load_setting("ssid")}\n")
|
change_line_in_file("/etc/hostapd/hostapd.conf", regex_ssid, f"ssid={load_setting("ssid")}\n")
|
||||||
if s == "enable_chat":
|
if s == "enable_chat":
|
||||||
change_service_status("enable_chat", "dendrite")
|
change_service_status("enable_chat", "dendrite")
|
||||||
if s == "enable_access_point":
|
if s == "enable_access_point" and raspap_installed:
|
||||||
change_service_status("enable_access_point", "raspapd")
|
change_service_status("enable_access_point", "raspapd")
|
||||||
if s == "enable_delta_chat":
|
if s == "enable_delta_chat":
|
||||||
change_service_status("enable_delta_chat", "madmail")
|
change_service_status("enable_delta_chat", "madmail")
|
||||||
if s == "butterbox_hostname":
|
if s == "butterbox_hostname":
|
||||||
pass
|
pass
|
||||||
# change in butterbox-dendrite.conf
|
# change in butterbox-dendrite.conf
|
||||||
regex_matrix_server = "server_name:.*?.lan\n"
|
regex_matrix_server = "server_name:.*?.local\n"
|
||||||
change_line_in_file("../dendrite/butterbox-dendrite.conf", regex_matrix_server,
|
change_line_in_file("../dendrite/butterbox-dendrite.conf", regex_matrix_server,
|
||||||
f"server_name: {load_setting("butterbox_hostname")}.lan\n")
|
f"server_name: {load_setting("butterbox_hostname")}.local\n")
|
||||||
# change in butterbox-dnsmasq.conf
|
# change in butterbox-dnsmasq.conf
|
||||||
regex_dns = "address=/.*?.lan/10.3.141.1\n"
|
if raspap_installed:
|
||||||
|
regex_dns = "address=/.*?.local/10.3.141.1\n"
|
||||||
change_line_in_file("/etc/dnsmasq.d/butterbox-dnsmasq.conf", regex_dns,
|
change_line_in_file("/etc/dnsmasq.d/butterbox-dnsmasq.conf", regex_dns,
|
||||||
f"address=/{load_setting("butterbox_hostname")}.lan/10.3.141.1\n")
|
f"address=/{load_setting("butterbox_hostname")}.local/10.3.141.1\n")
|
||||||
# change in keanu-weblite compiled assets
|
# change in keanu-weblite compiled assets
|
||||||
change_keanu_weblite_config(load_setting("butterbox_hostname"))
|
change_keanu_weblite_config(load_setting("butterbox_hostname"))
|
||||||
if s == "ssh_access_settings":
|
if s == "ssh_access_settings":
|
||||||
|
|
@ -140,6 +145,13 @@ def check_settings():
|
||||||
if load_setting("root_account_settings") == "lock_root_account":
|
if load_setting("root_account_settings") == "lock_root_account":
|
||||||
lock_root_account()
|
lock_root_account()
|
||||||
else:
|
else:
|
||||||
|
# root password implementation here
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raspap_installed = os.path.exists("/var/www/html/raspapd")
|
||||||
|
while True:
|
||||||
|
check_settings(raspap_installed)
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
@ -24,4 +24,4 @@ class Config:
|
||||||
ENABLE_FILE_VIEWER = "true"
|
ENABLE_FILE_VIEWER = "true"
|
||||||
ENABLE_DELTACHAT = "true"
|
ENABLE_DELTACHAT = "true"
|
||||||
CONVENE_INSTALL_PATH = "/chat"
|
CONVENE_INSTALL_PATH = "/chat"
|
||||||
ENABLE_WIFI_SHARING = "true"
|
ENABLE_WIFI_SHARING = "true" if os.path.exists("/var/www/html/raspap") else "false"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue