Check Update
This commit is contained in:
@@ -286,6 +286,15 @@ https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git
|
||||
|
||||
Depois ele recompila o binário e atualiza o painel web e os scripts auxiliares, mantendo as configurações e dados existentes.
|
||||
|
||||
O painel do superadmin também mostra um cartão **Atualizações do painel** na tela inicial. Ele compara o commit compilado no servidor com o commit mais recente da branch configurada no Git. A verificação usa cache de 5 minutos; o botão **Verificar agora** força uma nova consulta.
|
||||
|
||||
Variáveis opcionais do serviço para apontar a verificação para outro Git/branch:
|
||||
|
||||
```text
|
||||
DRAGON_UPDATE_REPO_URL=https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git
|
||||
DRAGON_UPDATE_BRANCH=main
|
||||
```
|
||||
|
||||
O update preserva:
|
||||
|
||||
```text
|
||||
@@ -788,6 +797,15 @@ https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git
|
||||
|
||||
Then it rebuilds the binary and updates the web panel and helper scripts while keeping existing configuration and user data.
|
||||
|
||||
The superadmin dashboard also shows a **Panel Updates** card. It compares the commit compiled into the running server with the latest commit on the configured Git branch. Results are cached for 5 minutes; **Check now** forces a fresh lookup.
|
||||
|
||||
Optional service variables for a different repository/branch:
|
||||
|
||||
```text
|
||||
DRAGON_UPDATE_REPO_URL=https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git
|
||||
DRAGON_UPDATE_BRANCH=main
|
||||
```
|
||||
|
||||
The update preserves:
|
||||
|
||||
```text
|
||||
|
||||
@@ -638,3 +638,14 @@ select:disabled {
|
||||
color:#f3f7ff;
|
||||
font-size:.78rem;
|
||||
}
|
||||
|
||||
/* Git update status card */
|
||||
.update-commit{
|
||||
font-family:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;
|
||||
font-size:1rem!important;
|
||||
letter-spacing:.01em!important;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
.update-check-time{font-size:.9rem!important;letter-spacing:0!important;}
|
||||
.update-statusbar{align-items:center;gap:12px;flex-wrap:wrap;}
|
||||
.btn-xs{padding:5px 8px!important;font-size:.67rem!important;border-radius:9px!important;}
|
||||
|
||||
@@ -96,6 +96,22 @@ Object.assign(I18N_ALIASES, {
|
||||
"Public Key — share with dnstt clients":"Public Key — share with dnstt clients","Chave pública — compartilhe com clientes DNSTT":"Public Key — share with dnstt clients",
|
||||
"Máx. sessões UDP por cliente":"Max UDP Sessions Per Client","(não é o total de usuários do servidor)":"(not total server users)","Nome do serviço":"Service Name","Modo":"Mode","Protocolo":"Protocol","Porta":"Port","IP de listen":"Listen IP","Método":"Method","Caminho":"Path","Destino":"Dest","ID curto":"Short ID","Nome do servidor":"Server Name","Caminho do arquivo cert":"Cert File Path","Caminho do arquivo key":"Key File Path","Fonte do certificado:":"Certificate source:","Autoassinado":"Self-Signed","Colar PEM":"Paste PEM","Caminho do arquivo":"File Path","Salvar PEM":"Save PEM","Intervalo de reinício automático":"Auto Restart Interval","Atraso para reiniciar":"Restart Grace Delay","0s/off desativa":"0s/off disables","Chave pública":"Public Key","Nome do domínio":"Domain Name"
|
||||
});
|
||||
Object.assign(I18N_TEXT["en-US"], {
|
||||
"Panel Updates":"Panel Updates","Checking…":"Checking…","Open Git":"Open Git","Check now":"Check now","Installed version":"Installed version","Latest Git version":"Latest Git version","Branch":"Branch","Last checked":"Last checked",
|
||||
"Comparing the installed version with the Git repository.":"Comparing the installed version with the Git repository.","To update:":"To update:","Copy command":"Copy command","Up to date":"Up to date","Update available":"Update available","Local changes":"Local changes","Unknown":"Unknown",
|
||||
"The installed version matches the latest commit on {branch}.":"The installed version matches the latest commit on {branch}.","A newer commit is available on {branch}.":"A newer commit is available on {branch}.","This build contains local changes, so it cannot be compared safely.":"This build contains local changes, so it cannot be compared safely.",
|
||||
"Could not check for updates: {error}":"Could not check for updates: {error}","The update status could not be determined.":"The update status could not be determined.","Checking repository…":"Checking repository…","Update check timed out.":"Update check timed out.","Could not reach the Git repository.":"Could not reach the Git repository.","Current build commit is unavailable.":"Current build commit is unavailable.","Unknown update-check error.":"Unknown update-check error.","Copied":"Copied"
|
||||
});
|
||||
Object.assign(I18N_TEXT["pt-BR"], {
|
||||
"Panel Updates":"Atualizações do painel","Checking…":"Verificando…","Open Git":"Abrir Git","Check now":"Verificar agora","Installed version":"Versão instalada","Latest Git version":"Última versão no Git","Branch":"Branch","Last checked":"Última verificação",
|
||||
"Comparing the installed version with the Git repository.":"Comparando a versão instalada com o repositório Git.","To update:":"Para atualizar:","Copy command":"Copiar comando","Up to date":"Atualizado","Update available":"Atualização disponível","Local changes":"Alterações locais","Unknown":"Desconhecido",
|
||||
"The installed version matches the latest commit on {branch}.":"A versão instalada corresponde ao commit mais recente da branch {branch}.","A newer commit is available on {branch}.":"Existe um commit mais recente disponível na branch {branch}.","This build contains local changes, so it cannot be compared safely.":"Esta compilação contém alterações locais e não pode ser comparada com segurança.",
|
||||
"Could not check for updates: {error}":"Não foi possível verificar atualizações: {error}","The update status could not be determined.":"Não foi possível determinar o status da atualização.","Checking repository…":"Verificando o repositório…","Update check timed out.":"A verificação de atualização excedeu o tempo limite.","Could not reach the Git repository.":"Não foi possível acessar o repositório Git.","Current build commit is unavailable.":"O commit da compilação atual não está disponível.","Unknown update-check error.":"Erro desconhecido ao verificar atualização.","Copied":"Copiado"
|
||||
});
|
||||
Object.assign(I18N_ALIASES, {
|
||||
"Atualizações do painel":"Panel Updates","Verificando…":"Checking…","Abrir Git":"Open Git","Verificar agora":"Check now","Versão instalada":"Installed version","Última versão no Git":"Latest Git version","Última verificação":"Last checked",
|
||||
"Comparando a versão instalada com o repositório Git.":"Comparing the installed version with the Git repository.","Para atualizar:":"To update:","Copiar comando":"Copy command","Atualizado":"Up to date","Atualização disponível":"Update available","Alterações locais":"Local changes","Desconhecido":"Unknown"
|
||||
});
|
||||
const I18N_REVERSE = Object.fromEntries(SUPPORTED_LANGS.map(lang => [lang, Object.fromEntries(Object.entries(I18N_TEXT[lang] || {}).map(([k, v]) => [v, k]))]));
|
||||
let currentLang = detectInitialLanguage();
|
||||
let i18nTranslating = false;
|
||||
|
||||
@@ -124,6 +124,7 @@ function initAfterLogin() {
|
||||
|
||||
if (currentRole === "superadmin") {
|
||||
loadDashboardStats();
|
||||
if (typeof loadUpdateStatus === "function") loadUpdateStatus();
|
||||
statsTimer = setInterval(() => {
|
||||
loadDashboardStats();
|
||||
if (currentTab === "stats") loadStats();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// ─── Git update status ───────────────────────────────────────────────────────
|
||||
const DRAGON_UPDATE_COMMAND = "sudo bash /opt/sshpanel/update.sh";
|
||||
|
||||
function updateStatusErrorText(error) {
|
||||
switch (String(error || "")) {
|
||||
case "remote update check timed out":
|
||||
return t("Update check timed out.");
|
||||
case "could not read the remote Git branch":
|
||||
return t("Could not reach the Git repository.");
|
||||
case "current build commit is unavailable":
|
||||
return t("Current build commit is unavailable.");
|
||||
default:
|
||||
return error || t("Unknown update-check error.");
|
||||
}
|
||||
}
|
||||
|
||||
function setUpdateState(label, tone = "") {
|
||||
const chip = document.getElementById("updateStateChip");
|
||||
if (!chip) return;
|
||||
chip.className = "chip" + (tone ? ` ${tone}` : "");
|
||||
chip.textContent = label;
|
||||
}
|
||||
|
||||
function setCommitValue(elementID, shortValue, fullValue) {
|
||||
const el = document.getElementById(elementID);
|
||||
if (!el) return;
|
||||
el.textContent = shortValue || "--";
|
||||
el.title = fullValue || "";
|
||||
}
|
||||
|
||||
function renderUpdateStatus(data) {
|
||||
setCommitValue("updateCurrentCommit", data.current_commit_short, data.current_commit);
|
||||
setCommitValue("updateLatestCommit", data.latest_commit_short, data.latest_commit);
|
||||
|
||||
const branch = document.getElementById("updateBranch");
|
||||
if (branch) branch.textContent = data.branch || "main";
|
||||
|
||||
const checked = document.getElementById("updateCheckedAt");
|
||||
if (checked) {
|
||||
const date = data.checked_at ? new Date(data.checked_at) : null;
|
||||
checked.textContent = date && Number.isFinite(date.getTime()) ? date.toLocaleString() : "--";
|
||||
}
|
||||
|
||||
const repoLink = document.getElementById("updateRepoLink");
|
||||
if (repoLink && data.repo_web_url) repoLink.href = data.repo_web_url;
|
||||
|
||||
const text = document.getElementById("updateStatusText");
|
||||
const commandWrap = document.getElementById("updateCommandWrap");
|
||||
commandWrap?.classList.toggle("hidden", !data.update_available);
|
||||
|
||||
if (data.status === "up_to_date") {
|
||||
setUpdateState(t("Up to date"), "green");
|
||||
if (text) text.textContent = t("The installed version matches the latest commit on {branch}.", { branch: data.branch || "main" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === "update_available") {
|
||||
setUpdateState(t("Update available"), "warn");
|
||||
if (text) text.textContent = t("A newer commit is available on {branch}.", { branch: data.branch || "main" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === "local_changes") {
|
||||
setUpdateState(t("Local changes"), "warn");
|
||||
if (text) text.textContent = t("This build contains local changes, so it cannot be compared safely.");
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdateState(t("Unknown"), "red");
|
||||
if (text) {
|
||||
text.textContent = data.error
|
||||
? t("Could not check for updates: {error}", { error: updateStatusErrorText(data.error) })
|
||||
: t("The update status could not be determined.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUpdateStatus(force = false) {
|
||||
if (currentRole !== "superadmin") return;
|
||||
|
||||
const button = document.getElementById("checkUpdateBtn");
|
||||
const text = document.getElementById("updateStatusText");
|
||||
if (button) button.disabled = true;
|
||||
setUpdateState(t("Checking…"));
|
||||
if (text) text.textContent = t("Checking repository…");
|
||||
|
||||
try {
|
||||
const path = "/api/system/update-status" + (force ? "?refresh=1" : "");
|
||||
const res = await api(path);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
renderUpdateStatus(await res.json());
|
||||
} catch (error) {
|
||||
setUpdateState(t("Unknown"), "red");
|
||||
if (text) text.textContent = t("Could not check for updates: {error}", { error: error.message || t("Network error.") });
|
||||
} finally {
|
||||
if (button) button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("checkUpdateBtn")?.addEventListener("click", () => loadUpdateStatus(true));
|
||||
document.getElementById("copyUpdateCommandBtn")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(DRAGON_UPDATE_COMMAND);
|
||||
} catch {
|
||||
const input = document.createElement("textarea");
|
||||
input.value = DRAGON_UPDATE_COMMAND;
|
||||
input.style.position = "fixed";
|
||||
input.style.opacity = "0";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
input.remove();
|
||||
}
|
||||
const button = document.getElementById("copyUpdateCommandBtn");
|
||||
if (button) {
|
||||
const oldText = button.textContent;
|
||||
button.textContent = t("Copied");
|
||||
setTimeout(() => { button.textContent = oldText; }, 1400);
|
||||
}
|
||||
});
|
||||
+32
-11
@@ -16,7 +16,7 @@
|
||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/app.css?v=20260511visualsafesave1"/>
|
||||
<link rel="stylesheet" href="assets/app.css?v=20260711updatecheck1"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -158,6 +158,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card superadmin-only hidden" id="updateStatusCard" style="margin-top:18px;">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">Panel Updates <span class="chip" id="updateStateChip">Checking…</span></div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-ghost btn-sm" id="updateRepoLink" href="https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB" target="_blank" rel="noopener noreferrer">Open Git</a>
|
||||
<button class="btn btn-ghost btn-sm" type="button" id="checkUpdateBtn">Check now</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="m-label">Installed version</div><div class="m-val update-commit" id="updateCurrentCommit">--</div></div>
|
||||
<div class="metric"><div class="m-label">Latest Git version</div><div class="m-val update-commit" id="updateLatestCommit">--</div></div>
|
||||
<div class="metric"><div class="m-label">Branch</div><div class="m-val update-commit" id="updateBranch">main</div></div>
|
||||
<div class="metric"><div class="m-label">Last checked</div><div class="m-val update-check-time" id="updateCheckedAt">--</div></div>
|
||||
</div>
|
||||
<div class="statusbar update-statusbar">
|
||||
<span id="updateStatusText">Comparing the installed version with the Git repository.</span>
|
||||
<span class="hint hidden" id="updateCommandWrap">To update: <code>sudo bash /opt/sshpanel/update.sh</code> <button class="btn btn-ghost btn-xs" type="button" id="copyUpdateCommandBtn">Copy command</button></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid2 dashboard-lower">
|
||||
<div class="card hidden" id="dnsttDashboardCard">
|
||||
<div class="card-hdr">
|
||||
@@ -1241,15 +1261,16 @@
|
||||
<!-- app.js was split into ordered modules for maintainability. They are plain
|
||||
classic scripts sharing one global scope; `defer` preserves execution order,
|
||||
so behavior is identical to the old single file. Keep this load order. -->
|
||||
<script defer src="assets/js/01-core.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/02-shell.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/03-ssh-users.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/04-xray.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/05-resellers.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/06-servers.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/07-stats-logs.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/08-server-config.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/09-xray-wizard.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/10-boot.js?v=20260704ytquicfix1"></script>
|
||||
<script defer src="assets/js/01-core.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/02-shell.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/03-ssh-users.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/04-xray.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/05-resellers.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/06-servers.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/07-stats-logs.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/08-server-config.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/09-xray-wizard.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/11-update-status.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/10-boot.js?v=20260711updatecheck1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+17
-1
@@ -15,6 +15,7 @@ LOG_TMPFS_SIZE="${LOG_TMPFS_SIZE:-15m}"
|
||||
PANEL_LOG_MAX_BYTES="${PANEL_LOG_MAX_BYTES:-1048576}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GO_VERSION="${GO_VERSION:-$(awk '$1 == "go" {print $2; exit}' "$SCRIPT_DIR/go.mod" 2>/dev/null || echo "1.22.5")}"
|
||||
REPO_URL="${REPO_URL:-https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git}"
|
||||
MKDIR_BIN="$(command -v mkdir 2>/dev/null || true)"
|
||||
[[ -n "$MKDIR_BIN" ]] || MKDIR_BIN="/bin/mkdir"
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -264,10 +265,25 @@ info "[5/10] Building SSH Panel binary…"
|
||||
cd "$SCRIPT_DIR"
|
||||
export GOPATH=/tmp/gopath_sshpanel
|
||||
export GOCACHE=/tmp/gocache_sshpanel
|
||||
BUILD_COMMIT="$(git -C "$SCRIPT_DIR" rev-parse HEAD 2>/dev/null || true)"
|
||||
BUILD_BRANCH="$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
||||
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
BUILD_REPO_URL="$(git -C "$SCRIPT_DIR" config --get remote.origin.url 2>/dev/null || true)"
|
||||
[[ -n "$BUILD_COMMIT" ]] || BUILD_COMMIT="unknown"
|
||||
[[ -n "$BUILD_BRANCH" && "$BUILD_BRANCH" != "HEAD" ]] || BUILD_BRANCH="main"
|
||||
[[ -n "$BUILD_REPO_URL" ]] || BUILD_REPO_URL="$REPO_URL"
|
||||
|
||||
go mod download
|
||||
go mod tidy
|
||||
go build -ldflags="-s -w" -o "$INSTALL_DIR/sshpanel" .
|
||||
go build -ldflags="-s -w -X main.buildCommit=$BUILD_COMMIT -X main.buildBranch=$BUILD_BRANCH -X main.buildTime=$BUILD_TIME" -o "$INSTALL_DIR/sshpanel" .
|
||||
printf '%s\n' "$BUILD_COMMIT" > "$INSTALL_DIR/.installed_commit"
|
||||
printf '%s\n' "$BUILD_BRANCH" > "$INSTALL_DIR/.installed_branch"
|
||||
printf '%s\n' "$BUILD_TIME" > "$INSTALL_DIR/.installed_build_time"
|
||||
printf '%s\n' "$BUILD_REPO_URL" > "$INSTALL_DIR/.installed_repo_url"
|
||||
chmod 0644 "$INSTALL_DIR/.installed_commit" "$INSTALL_DIR/.installed_branch" "$INSTALL_DIR/.installed_build_time"
|
||||
chmod 0600 "$INSTALL_DIR/.installed_repo_url"
|
||||
info " Binary: $INSTALL_DIR/sshpanel"
|
||||
info " Build commit: $BUILD_COMMIT ($BUILD_BRANCH)"
|
||||
cp -r "$SCRIPT_DIR/admin/"* "$INSTALL_DIR/admin/"
|
||||
info " Admin panel copied"
|
||||
if [[ -f "$SCRIPT_DIR/update.sh" ]]; then
|
||||
|
||||
@@ -1567,6 +1567,7 @@ func startAdminAPI(store *Store, addr string, adminDir string) {
|
||||
mux.Handle("/api/vnstat/reset", saSession(http.HandlerFunc(handleVnstatReset(store))))
|
||||
mux.Handle("/api/system/logs", saSession(http.HandlerFunc(handleSystemLogs)))
|
||||
mux.Handle("/api/system/logs/reset", saSession(http.HandlerFunc(handleSystemLogsReset)))
|
||||
mux.Handle("/api/system/update-status", saSession(http.HandlerFunc(handleUpdateStatus)))
|
||||
mux.Handle("/api/dnstt", saSession(http.HandlerFunc(handleDnsttStats)))
|
||||
mux.Handle("/api/dnstt/logs", saSession(http.HandlerFunc(handleDnsttLogs)))
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ MKDIR_BIN="$(command -v mkdir 2>/dev/null || true)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_DIR=""
|
||||
RESTART_NEEDED=false
|
||||
BUILD_COMMIT=""
|
||||
BUILD_BRANCH=""
|
||||
BUILD_TIME=""
|
||||
BUILD_REPO_URL=""
|
||||
|
||||
[[ $EUID -ne 0 ]] && error "Run as root: sudo bash $0"
|
||||
|
||||
@@ -288,9 +292,18 @@ build_binary() {
|
||||
cd "$SOURCE_DIR"
|
||||
export GOPATH=/tmp/gopath_sshpanel
|
||||
export GOCACHE=/tmp/gocache_sshpanel
|
||||
|
||||
BUILD_COMMIT="$(git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || true)"
|
||||
BUILD_BRANCH="${UPDATE_REF:-$(git -C "$SOURCE_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)}"
|
||||
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
BUILD_REPO_URL="$REPO_URL"
|
||||
[[ -n "$BUILD_COMMIT" ]] || BUILD_COMMIT="unknown"
|
||||
[[ -n "$BUILD_BRANCH" && "$BUILD_BRANCH" != "HEAD" ]] || BUILD_BRANCH="main"
|
||||
|
||||
go mod download
|
||||
go mod tidy
|
||||
go build -ldflags="-s -w" -o /tmp/sshpanel_new .
|
||||
go build -ldflags="-s -w -X main.buildCommit=$BUILD_COMMIT -X main.buildBranch=$BUILD_BRANCH -X main.buildTime=$BUILD_TIME" -o /tmp/sshpanel_new .
|
||||
info " Build commit: $BUILD_COMMIT ($BUILD_BRANCH)"
|
||||
info " Build complete."
|
||||
}
|
||||
|
||||
@@ -332,6 +345,14 @@ apply_update() {
|
||||
chmod 755 "$INSTALL_DIR/sshpanel"
|
||||
info " Binary updated."
|
||||
|
||||
printf '%s\n' "$BUILD_COMMIT" > "$INSTALL_DIR/.installed_commit"
|
||||
printf '%s\n' "$BUILD_BRANCH" > "$INSTALL_DIR/.installed_branch"
|
||||
printf '%s\n' "$BUILD_TIME" > "$INSTALL_DIR/.installed_build_time"
|
||||
printf '%s\n' "$BUILD_REPO_URL" > "$INSTALL_DIR/.installed_repo_url"
|
||||
chmod 0644 "$INSTALL_DIR/.installed_commit" "$INSTALL_DIR/.installed_branch" "$INSTALL_DIR/.installed_build_time"
|
||||
chmod 0600 "$INSTALL_DIR/.installed_repo_url"
|
||||
info " Build metadata updated."
|
||||
|
||||
rsync -a --delete "$SOURCE_DIR/admin/" "$INSTALL_DIR/admin/"
|
||||
info " Admin panel updated."
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUpdateRepoURL = "https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git"
|
||||
defaultUpdateBranch = "main"
|
||||
updateStatusCacheTTL = 5 * time.Minute
|
||||
updateCheckTimeout = 12 * time.Second
|
||||
)
|
||||
|
||||
// These values are injected by install.sh/update.sh with -ldflags. The
|
||||
// runtime/debug fallback keeps the endpoint useful for normal git builds.
|
||||
var (
|
||||
buildCommit = ""
|
||||
buildBranch = ""
|
||||
buildTime = ""
|
||||
buildRepoURL = ""
|
||||
)
|
||||
|
||||
type updateStatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
UpToDate bool `json:"up_to_date"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
LocalModified bool `json:"local_modified"`
|
||||
CurrentCommit string `json:"current_commit,omitempty"`
|
||||
CurrentCommitShort string `json:"current_commit_short,omitempty"`
|
||||
LatestCommit string `json:"latest_commit,omitempty"`
|
||||
LatestCommitShort string `json:"latest_commit_short,omitempty"`
|
||||
Branch string `json:"branch"`
|
||||
BuildTime string `json:"build_time,omitempty"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
RepoWebURL string `json:"repo_web_url"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
Cached bool `json:"cached"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var updateStatusCache struct {
|
||||
sync.Mutex
|
||||
checkedAt time.Time
|
||||
response updateStatusResponse
|
||||
}
|
||||
|
||||
func handleUpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
force := r.URL.Query().Get("refresh") == "1"
|
||||
resp := getUpdateStatus(r.Context(), force)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func getUpdateStatus(parent context.Context, force bool) updateStatusResponse {
|
||||
updateStatusCache.Lock()
|
||||
defer updateStatusCache.Unlock()
|
||||
|
||||
if !force && !updateStatusCache.checkedAt.IsZero() && time.Since(updateStatusCache.checkedAt) < updateStatusCacheTTL {
|
||||
resp := updateStatusCache.response
|
||||
resp.Cached = true
|
||||
return resp
|
||||
}
|
||||
|
||||
resp := checkRemoteUpdate(parent)
|
||||
updateStatusCache.checkedAt = time.Now()
|
||||
updateStatusCache.response = resp
|
||||
return resp
|
||||
}
|
||||
|
||||
func checkRemoteUpdate(parent context.Context) updateStatusResponse {
|
||||
repoURL := firstNonEmpty(
|
||||
strings.TrimSpace(os.Getenv("DRAGON_UPDATE_REPO_URL")),
|
||||
readSingleLineFile("/opt/sshpanel/.installed_repo_url"),
|
||||
strings.TrimSpace(buildRepoURL),
|
||||
defaultUpdateRepoURL,
|
||||
)
|
||||
branch := firstNonEmpty(
|
||||
strings.TrimSpace(os.Getenv("DRAGON_UPDATE_BRANCH")),
|
||||
strings.TrimSpace(buildBranch),
|
||||
readSingleLineFile("/opt/sshpanel/.installed_branch"),
|
||||
defaultUpdateBranch,
|
||||
)
|
||||
currentCommit, localModified, resolvedBuildTime := resolveCurrentBuildInfo()
|
||||
if resolvedBuildTime == "" {
|
||||
resolvedBuildTime = readSingleLineFile("/opt/sshpanel/.installed_build_time")
|
||||
}
|
||||
if currentCommit == "" {
|
||||
currentCommit = normalizeGitCommit(readSingleLineFile("/opt/sshpanel/.installed_commit"))
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
resp := updateStatusResponse{
|
||||
Status: "unknown",
|
||||
CurrentCommit: currentCommit,
|
||||
CurrentCommitShort: shortCommit(currentCommit),
|
||||
Branch: branch,
|
||||
BuildTime: resolvedBuildTime,
|
||||
RepoURL: safeRepoURL(repoURL),
|
||||
RepoWebURL: repoWebURL(repoURL),
|
||||
CheckedAt: now,
|
||||
LocalModified: localModified,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(parent, updateCheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
latestCommit, err := queryRemoteCommit(ctx, repoURL, branch)
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
resp.Error = "remote update check timed out"
|
||||
} else {
|
||||
resp.Error = "could not read the remote Git branch"
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.LatestCommit = latestCommit
|
||||
resp.LatestCommitShort = shortCommit(latestCommit)
|
||||
|
||||
if currentCommit == "" {
|
||||
resp.Error = "current build commit is unavailable"
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.Status, resp.UpToDate, resp.UpdateAvailable = classifyUpdateStatus(currentCommit, latestCommit, localModified)
|
||||
return resp
|
||||
}
|
||||
|
||||
func classifyUpdateStatus(currentCommit, latestCommit string, localModified bool) (status string, upToDate bool, updateAvailable bool) {
|
||||
if currentCommit == "" || latestCommit == "" {
|
||||
return "unknown", false, false
|
||||
}
|
||||
if strings.EqualFold(currentCommit, latestCommit) {
|
||||
if localModified {
|
||||
return "local_changes", false, false
|
||||
}
|
||||
return "up_to_date", true, false
|
||||
}
|
||||
return "update_available", false, true
|
||||
}
|
||||
|
||||
func queryRemoteCommit(ctx context.Context, repoURL, branch string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", "--heads", repoURL, "refs/heads/"+branch)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GIT_ASKPASS=/bin/false",
|
||||
)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fields := strings.Fields(string(out))
|
||||
if len(fields) < 2 {
|
||||
return "", errors.New("invalid git ls-remote response")
|
||||
}
|
||||
commit := normalizeGitCommit(fields[0])
|
||||
if commit == "" {
|
||||
return "", errors.New("invalid remote commit")
|
||||
}
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
func resolveCurrentBuildInfo() (commit string, modified bool, builtAt string) {
|
||||
commit = normalizeGitCommit(buildCommit)
|
||||
builtAt = strings.TrimSpace(buildTime)
|
||||
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return commit, false, builtAt
|
||||
}
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
if commit == "" {
|
||||
commit = normalizeGitCommit(setting.Value)
|
||||
}
|
||||
case "vcs.modified":
|
||||
modified = strings.EqualFold(setting.Value, "true")
|
||||
case "vcs.time":
|
||||
if builtAt == "" {
|
||||
builtAt = strings.TrimSpace(setting.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return commit, modified, builtAt
|
||||
}
|
||||
|
||||
func normalizeGitCommit(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if strings.EqualFold(value, "unknown") || len(value) < 7 || len(value) > 64 {
|
||||
return ""
|
||||
}
|
||||
for _, r := range value {
|
||||
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return strings.ToLower(value)
|
||||
}
|
||||
|
||||
func shortCommit(commit string) string {
|
||||
if len(commit) <= 12 {
|
||||
return commit
|
||||
}
|
||||
return commit[:12]
|
||||
}
|
||||
|
||||
func readSingleLineFile(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
line := strings.TrimSpace(string(data))
|
||||
if idx := strings.IndexByte(line, '\n'); idx >= 0 {
|
||||
line = strings.TrimSpace(line[:idx])
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func safeRepoURL(raw string) string {
|
||||
u, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return strings.TrimSpace(raw)
|
||||
}
|
||||
u.User = nil
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func repoWebURL(raw string) string {
|
||||
value := safeRepoURL(raw)
|
||||
return strings.TrimSuffix(value, ".git")
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeGitCommit(t *testing.T) {
|
||||
valid := "CF49340B9A1234567890ABCDEF1234567890ABCD"
|
||||
got := normalizeGitCommit(valid)
|
||||
want := "cf49340b9a1234567890abcdef1234567890abcd"
|
||||
if got != want {
|
||||
t.Fatalf("normalizeGitCommit() = %q, want %q", got, want)
|
||||
}
|
||||
for _, value := range []string{"", "unknown", "xyz1234", "123 4567", "123456"} {
|
||||
if got := normalizeGitCommit(value); got != "" {
|
||||
t.Fatalf("normalizeGitCommit(%q) = %q, want empty", value, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyUpdateStatus(t *testing.T) {
|
||||
const current = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
const latest = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
status, upToDate, updateAvailable := classifyUpdateStatus(current, current, false)
|
||||
if status != "up_to_date" || !upToDate || updateAvailable {
|
||||
t.Fatalf("same clean commit classified as %q, upToDate=%v updateAvailable=%v", status, upToDate, updateAvailable)
|
||||
}
|
||||
|
||||
status, upToDate, updateAvailable = classifyUpdateStatus(current, current, true)
|
||||
if status != "local_changes" || upToDate || updateAvailable {
|
||||
t.Fatalf("modified commit classified as %q, upToDate=%v updateAvailable=%v", status, upToDate, updateAvailable)
|
||||
}
|
||||
|
||||
status, upToDate, updateAvailable = classifyUpdateStatus(current, latest, false)
|
||||
if status != "update_available" || upToDate || !updateAvailable {
|
||||
t.Fatalf("different commit classified as %q, upToDate=%v updateAvailable=%v", status, upToDate, updateAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoWebURLRemovesCredentialsAndGitSuffix(t *testing.T) {
|
||||
got := repoWebURL("https://user:secret@git.example.test/owner/repo.git")
|
||||
want := "https://git.example.test/owner/repo"
|
||||
if got != want {
|
||||
t.Fatalf("repoWebURL() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRemoteCommit(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("shell-script git stub is Unix-specific")
|
||||
}
|
||||
const want = "cf49340b9a1234567890abcdef1234567890abcd"
|
||||
dir := t.TempDir()
|
||||
gitPath := filepath.Join(dir, "git")
|
||||
script := "#!/bin/sh\nprintf '%s\\trefs/heads/main\\n' '" + want + "'\n"
|
||||
if err := os.WriteFile(gitPath, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
got, err := queryRemoteCommit(context.Background(), "https://git.example.test/owner/repo.git", "main")
|
||||
if err != nil {
|
||||
t.Fatalf("queryRemoteCommit() error = %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("queryRemoteCommit() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user