feat(gitea): Add self-hosted Git platform for OpenWrt
Add secubox-app-gitea and luci-app-gitea packages: secubox-app-gitea: - LXC container with Alpine 3.21 rootfs - Gitea 1.22.6 binary (auto-detect amd64/arm64/armv7) - HTTP (3000) and SSH (2222) ports - SQLite database (embedded) - giteactl: install/uninstall/update/backup/restore luci-app-gitea: - Cyberpunk themed dashboard - Repository browser with clone URLs - User management interface - Server and security settings - Backup/restore functionality - 18 RPCD methods Resource requirements: 256MB RAM minimum, ~100MB storage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
814a85754d
commit
d43a02a397
29
package/secubox/luci-app-gitea/Makefile
Normal file
29
package/secubox/luci-app-gitea/Makefile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
#
|
||||||
|
# LuCI Gitea Dashboard
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-gitea
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_ARCH:=all
|
||||||
|
|
||||||
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI Gitea Dashboard
|
||||||
|
LUCI_DESCRIPTION:=Modern dashboard for Gitea Platform management on OpenWrt
|
||||||
|
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-gitea
|
||||||
|
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.gitea:root:root:755
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
# Note: /etc/config/gitea is in secubox-app-gitea
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,luci-app-gitea))
|
||||||
@ -0,0 +1,244 @@
|
|||||||
|
'use strict';
|
||||||
|
'require rpc';
|
||||||
|
'require baseclass';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitea Platform API Module
|
||||||
|
* RPCD interface for Gitea Platform
|
||||||
|
*/
|
||||||
|
|
||||||
|
var callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'get_status',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetStats = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'get_stats',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetConfig = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'get_config',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSaveConfig = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'save_config',
|
||||||
|
params: ['http_port', 'ssh_port', 'http_host', 'data_path', 'memory_limit', 'enabled', 'app_name', 'domain', 'protocol', 'disable_registration', 'require_signin', 'landing_page'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callStart = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'start',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callStop = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'stop',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRestart = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'restart',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstall = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'install',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUninstall = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'uninstall',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpdate = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'update',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetLogs = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'get_logs',
|
||||||
|
params: ['lines'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListRepos = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'list_repos',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetRepo = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'get_repo',
|
||||||
|
params: ['name', 'owner'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListUsers = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'list_users',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCreateAdmin = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'create_admin',
|
||||||
|
params: ['username', 'password', 'email'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCreateBackup = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'create_backup',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListBackups = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'list_backups',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRestoreBackup = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'restore_backup',
|
||||||
|
params: ['file'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetInstallProgress = rpc.declare({
|
||||||
|
object: 'luci.gitea',
|
||||||
|
method: 'get_install_progress',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
return baseclass.extend({
|
||||||
|
getStatus: function() {
|
||||||
|
return callGetStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
getStats: function() {
|
||||||
|
return callGetStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfig: function() {
|
||||||
|
return callGetConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
saveConfig: function(config) {
|
||||||
|
return callSaveConfig(
|
||||||
|
config.http_port,
|
||||||
|
config.ssh_port,
|
||||||
|
config.http_host,
|
||||||
|
config.data_path,
|
||||||
|
config.memory_limit,
|
||||||
|
config.enabled,
|
||||||
|
config.app_name,
|
||||||
|
config.domain,
|
||||||
|
config.protocol,
|
||||||
|
config.disable_registration,
|
||||||
|
config.require_signin,
|
||||||
|
config.landing_page
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
start: function() {
|
||||||
|
return callStart();
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: function() {
|
||||||
|
return callStop();
|
||||||
|
},
|
||||||
|
|
||||||
|
restart: function() {
|
||||||
|
return callRestart();
|
||||||
|
},
|
||||||
|
|
||||||
|
install: function() {
|
||||||
|
return callInstall();
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall: function() {
|
||||||
|
return callUninstall();
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function() {
|
||||||
|
return callUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogs: function(lines) {
|
||||||
|
return callGetLogs(lines || 100).then(function(res) {
|
||||||
|
return res.logs || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
listRepos: function() {
|
||||||
|
return callListRepos().then(function(res) {
|
||||||
|
return {
|
||||||
|
repos: res.repos || [],
|
||||||
|
repo_root: res.repo_root || '/srv/gitea/git/repositories'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRepo: function(name, owner) {
|
||||||
|
return callGetRepo(name, owner);
|
||||||
|
},
|
||||||
|
|
||||||
|
listUsers: function() {
|
||||||
|
return callListUsers().then(function(res) {
|
||||||
|
return res.users || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createAdmin: function(username, password, email) {
|
||||||
|
return callCreateAdmin(username, password, email);
|
||||||
|
},
|
||||||
|
|
||||||
|
createBackup: function() {
|
||||||
|
return callCreateBackup();
|
||||||
|
},
|
||||||
|
|
||||||
|
listBackups: function() {
|
||||||
|
return callListBackups().then(function(res) {
|
||||||
|
return res.backups || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreBackup: function(file) {
|
||||||
|
return callRestoreBackup(file);
|
||||||
|
},
|
||||||
|
|
||||||
|
getInstallProgress: function() {
|
||||||
|
return callGetInstallProgress();
|
||||||
|
},
|
||||||
|
|
||||||
|
getDashboardData: function() {
|
||||||
|
var self = this;
|
||||||
|
return Promise.all([
|
||||||
|
self.getStatus(),
|
||||||
|
self.getStats(),
|
||||||
|
self.getLogs(50)
|
||||||
|
]).then(function(results) {
|
||||||
|
return {
|
||||||
|
status: results[0] || {},
|
||||||
|
stats: results[1] || {},
|
||||||
|
logs: results[2] || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,469 @@
|
|||||||
|
/* Gitea Dashboard - Cyberpunk Theme */
|
||||||
|
|
||||||
|
.gitea-dashboard {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #0a0a0a 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.gt-header {
|
||||||
|
background: linear-gradient(90deg, rgba(255, 95, 31, 0.1) 0%, rgba(255, 95, 31, 0.05) 100%);
|
||||||
|
border: 1px solid #ff5f1f;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 0 20px rgba(255, 95, 31, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-logo {
|
||||||
|
font-size: 48px;
|
||||||
|
text-shadow: 0 0 10px #ff5f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff5f1f;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 95, 31, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-subtitle {
|
||||||
|
color: #888;
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badge */
|
||||||
|
.gt-status-badge {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-status-badge.running {
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
border: 1px solid #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-status-badge.stopped {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ff4444;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-status-badge.not-installed {
|
||||||
|
background: rgba(255, 170, 0, 0.2);
|
||||||
|
border: 1px solid #ffaa00;
|
||||||
|
color: #ffaa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Grid */
|
||||||
|
.gt-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-card {
|
||||||
|
background: rgba(20, 20, 30, 0.8);
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-card:hover {
|
||||||
|
border-color: #ff5f1f;
|
||||||
|
box-shadow: 0 0 15px rgba(255, 95, 31, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-card.success {
|
||||||
|
border-color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-card.error {
|
||||||
|
border-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Grid */
|
||||||
|
.gt-main-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.gt-card {
|
||||||
|
background: rgba(20, 20, 30, 0.9);
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-card-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-card-header {
|
||||||
|
background: rgba(255, 95, 31, 0.1);
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff5f1f;
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-card-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.gt-btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-primary {
|
||||||
|
border-color: #ff5f1f;
|
||||||
|
color: #ff5f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-primary:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 95, 31, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 95, 31, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-success {
|
||||||
|
border-color: #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-success:hover:not(:disabled) {
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-danger {
|
||||||
|
border-color: #ff4444;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-danger:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-warning {
|
||||||
|
border-color: #ffaa00;
|
||||||
|
color: #ffaa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-btn-warning:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 170, 0, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 170, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info List */
|
||||||
|
.gt-info-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-info-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-info-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-info-label {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-info-value {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-info-value a {
|
||||||
|
color: #ff5f1f;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-info-value a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logs */
|
||||||
|
.gt-logs {
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid #222;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-logs-line {
|
||||||
|
color: #00ff88;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-logs-line:nth-child(odd) {
|
||||||
|
color: #0cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress */
|
||||||
|
.gt-progress {
|
||||||
|
height: 8px;
|
||||||
|
background: #222;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #ff5f1f, #ffaa00);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-progress-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.gt-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.gt-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-table th,
|
||||||
|
.gt-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-table th {
|
||||||
|
background: rgba(255, 95, 31, 0.1);
|
||||||
|
color: #ff5f1f;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-table tr:hover {
|
||||||
|
background: rgba(255, 95, 31, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-table .gt-repo-name {
|
||||||
|
color: #ff5f1f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-table .gt-clone-url {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-table .gt-clone-url:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.gt-form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-form-input,
|
||||||
|
.gt-form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-form-input:focus,
|
||||||
|
.gt-form-select:focus {
|
||||||
|
border-color: #ff5f1f;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 10px rgba(255, 95, 31, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-form-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-form-checkbox input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #ff5f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-form-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.gt-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-badge-admin {
|
||||||
|
background: rgba(255, 95, 31, 0.2);
|
||||||
|
border: 1px solid #ff5f1f;
|
||||||
|
color: #ff5f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-badge-user {
|
||||||
|
background: rgba(0, 204, 204, 0.2);
|
||||||
|
border: 1px solid #0cc;
|
||||||
|
color: #0cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Actions */
|
||||||
|
.gt-quick-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.gt-header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-status-badge {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-stats-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gt-main-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,488 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
'require poll';
|
||||||
|
'require gitea.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
statusData: null,
|
||||||
|
statsData: null,
|
||||||
|
logsData: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return this.refreshData();
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData: function() {
|
||||||
|
var self = this;
|
||||||
|
return api.getDashboardData().then(function(data) {
|
||||||
|
self.statusData = data.status || {};
|
||||||
|
self.statsData = data.stats || {};
|
||||||
|
self.logsData = data.logs || [];
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Inject CSS
|
||||||
|
var cssLink = E('link', {
|
||||||
|
'rel': 'stylesheet',
|
||||||
|
'type': 'text/css',
|
||||||
|
'href': L.resource('gitea/dashboard.css')
|
||||||
|
});
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'gitea-dashboard' }, [
|
||||||
|
cssLink,
|
||||||
|
this.renderHeader(),
|
||||||
|
this.renderStatsGrid(),
|
||||||
|
this.renderMainGrid()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Poll for updates
|
||||||
|
poll.add(function() {
|
||||||
|
return self.refreshData().then(function() {
|
||||||
|
self.updateDynamicContent();
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHeader: function() {
|
||||||
|
var status = this.statusData;
|
||||||
|
var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped');
|
||||||
|
var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped'));
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-header' }, [
|
||||||
|
E('div', { 'class': 'gt-header-content' }, [
|
||||||
|
E('div', { 'class': 'gt-logo' }, '\uD83D\uDCE6'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('h1', { 'class': 'gt-title' }, _('GITEA PLATFORM')),
|
||||||
|
E('p', { 'class': 'gt-subtitle' }, _('Self-Hosted Git Service for SecuBox'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-status-badge ' + statusClass, 'id': 'gt-status-badge' }, [
|
||||||
|
E('span', {}, statusClass === 'running' ? '\u25CF' : '\u25CB'),
|
||||||
|
' ' + statusText
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatsGrid: function() {
|
||||||
|
var status = this.statusData;
|
||||||
|
var stats = this.statsData;
|
||||||
|
|
||||||
|
var statItems = [
|
||||||
|
{
|
||||||
|
icon: '\uD83D\uDD0C',
|
||||||
|
label: _('Status'),
|
||||||
|
value: status.running ? _('Online') : _('Offline'),
|
||||||
|
id: 'stat-status',
|
||||||
|
cardClass: status.running ? 'success' : 'error'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '\uD83D\uDCE6',
|
||||||
|
label: _('Repositories'),
|
||||||
|
value: stats.repo_count || status.repo_count || 0,
|
||||||
|
id: 'stat-repos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '\uD83D\uDC65',
|
||||||
|
label: _('Users'),
|
||||||
|
value: stats.user_count || 0,
|
||||||
|
id: 'stat-users'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '\uD83D\uDCBE',
|
||||||
|
label: _('Disk Usage'),
|
||||||
|
value: stats.disk_usage || status.disk_usage || '0',
|
||||||
|
id: 'stat-disk'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '\uD83C\uDF10',
|
||||||
|
label: _('HTTP Port'),
|
||||||
|
value: status.http_port || '3000',
|
||||||
|
id: 'stat-http'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '\uD83D\uDD12',
|
||||||
|
label: _('SSH Port'),
|
||||||
|
value: status.ssh_port || '2222',
|
||||||
|
id: 'stat-ssh'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-stats-grid' },
|
||||||
|
statItems.map(function(stat) {
|
||||||
|
return E('div', { 'class': 'gt-stat-card ' + (stat.cardClass || '') }, [
|
||||||
|
E('div', { 'class': 'gt-stat-icon' }, stat.icon),
|
||||||
|
E('div', { 'class': 'gt-stat-content' }, [
|
||||||
|
E('div', { 'class': 'gt-stat-value', 'id': stat.id }, String(stat.value)),
|
||||||
|
E('div', { 'class': 'gt-stat-label' }, stat.label)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMainGrid: function() {
|
||||||
|
return E('div', { 'class': 'gt-main-grid' }, [
|
||||||
|
this.renderControlCard(),
|
||||||
|
this.renderInfoCard(),
|
||||||
|
this.renderLogsCard()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderControlCard: function() {
|
||||||
|
var self = this;
|
||||||
|
var status = this.statusData;
|
||||||
|
|
||||||
|
var buttons = [];
|
||||||
|
|
||||||
|
if (!status.installed) {
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-primary',
|
||||||
|
'id': 'btn-install',
|
||||||
|
'click': function() { self.handleInstall(); }
|
||||||
|
}, [E('span', {}, '\uD83D\uDCE5'), ' ' + _('Install')])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (status.running) {
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-danger',
|
||||||
|
'id': 'btn-stop',
|
||||||
|
'click': function() { self.handleStop(); }
|
||||||
|
}, [E('span', {}, '\u23F9'), ' ' + _('Stop')])
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-warning',
|
||||||
|
'id': 'btn-restart',
|
||||||
|
'click': function() { self.handleRestart(); }
|
||||||
|
}, [E('span', {}, '\uD83D\uDD04'), ' ' + _('Restart')])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-success',
|
||||||
|
'id': 'btn-start',
|
||||||
|
'click': function() { self.handleStart(); }
|
||||||
|
}, [E('span', {}, '\u25B6'), ' ' + _('Start')])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-primary',
|
||||||
|
'id': 'btn-update',
|
||||||
|
'click': function() { self.handleUpdate(); }
|
||||||
|
}, [E('span', {}, '\u2B06'), ' ' + _('Update')])
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-primary',
|
||||||
|
'id': 'btn-backup',
|
||||||
|
'click': function() { self.handleBackup(); }
|
||||||
|
}, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Backup')])
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-danger',
|
||||||
|
'id': 'btn-uninstall',
|
||||||
|
'click': function() { self.handleUninstall(); }
|
||||||
|
}, [E('span', {}, '\uD83D\uDDD1'), ' ' + _('Uninstall')])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83C\uDFAE'),
|
||||||
|
' ' + _('Controls')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-btn-group', 'id': 'gt-controls' }, buttons),
|
||||||
|
E('div', { 'class': 'gt-progress', 'id': 'gt-progress-container', 'style': 'display:none' }, [
|
||||||
|
E('div', { 'class': 'gt-progress-bar', 'id': 'gt-progress-bar', 'style': 'width:0%' })
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-progress-text', 'id': 'gt-progress-text', 'style': 'display:none' })
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderInfoCard: function() {
|
||||||
|
var status = this.statusData;
|
||||||
|
|
||||||
|
var infoItems = [
|
||||||
|
{ label: _('Container'), value: status.container_name || 'gitea' },
|
||||||
|
{ label: _('Version'), value: status.version || '1.22.x' },
|
||||||
|
{ label: _('Data Path'), value: status.data_path || '/srv/gitea' },
|
||||||
|
{ label: _('Memory Limit'), value: status.memory_limit || '512M' },
|
||||||
|
{ label: _('Web Interface'), value: status.http_url, isLink: true },
|
||||||
|
{ label: _('SSH Clone'), value: status.ssh_url }
|
||||||
|
];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\u2139\uFE0F'),
|
||||||
|
' ' + _('Information')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('ul', { 'class': 'gt-info-list', 'id': 'gt-info-list' },
|
||||||
|
infoItems.map(function(item) {
|
||||||
|
var valueEl;
|
||||||
|
if (item.isLink && item.value) {
|
||||||
|
valueEl = E('a', { 'href': item.value, 'target': '_blank' }, item.value);
|
||||||
|
} else {
|
||||||
|
valueEl = item.value || '-';
|
||||||
|
}
|
||||||
|
return E('li', {}, [
|
||||||
|
E('span', { 'class': 'gt-info-label' }, item.label),
|
||||||
|
E('span', { 'class': 'gt-info-value' }, valueEl)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLogsCard: function() {
|
||||||
|
var logs = this.logsData || [];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card gt-card-full' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDCDC'),
|
||||||
|
' ' + _('Recent Logs')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
logs.length > 0 ?
|
||||||
|
E('div', { 'class': 'gt-logs', 'id': 'gt-logs' },
|
||||||
|
logs.slice(-20).map(function(line) {
|
||||||
|
return E('div', { 'class': 'gt-logs-line' }, line);
|
||||||
|
})
|
||||||
|
) :
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDCED'),
|
||||||
|
E('div', {}, _('No logs available'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDynamicContent: function() {
|
||||||
|
var status = this.statusData;
|
||||||
|
var stats = this.statsData;
|
||||||
|
|
||||||
|
// Update status badge
|
||||||
|
var badge = document.getElementById('gt-status-badge');
|
||||||
|
if (badge) {
|
||||||
|
var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped');
|
||||||
|
var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped'));
|
||||||
|
badge.className = 'gt-status-badge ' + statusClass;
|
||||||
|
badge.innerHTML = '';
|
||||||
|
badge.appendChild(E('span', {}, statusClass === 'running' ? '\u25CF' : '\u25CB'));
|
||||||
|
badge.appendChild(document.createTextNode(' ' + statusText));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
var statStatus = document.getElementById('stat-status');
|
||||||
|
if (statStatus) {
|
||||||
|
statStatus.textContent = status.running ? _('Online') : _('Offline');
|
||||||
|
}
|
||||||
|
|
||||||
|
var statRepos = document.getElementById('stat-repos');
|
||||||
|
if (statRepos) {
|
||||||
|
statRepos.textContent = stats.repo_count || status.repo_count || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statUsers = document.getElementById('stat-users');
|
||||||
|
if (statUsers) {
|
||||||
|
statUsers.textContent = stats.user_count || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update logs
|
||||||
|
var logsContainer = document.getElementById('gt-logs');
|
||||||
|
if (logsContainer && this.logsData) {
|
||||||
|
logsContainer.innerHTML = '';
|
||||||
|
this.logsData.slice(-20).forEach(function(line) {
|
||||||
|
logsContainer.appendChild(E('div', { 'class': 'gt-logs-line' }, line));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleInstall: function() {
|
||||||
|
var self = this;
|
||||||
|
var btn = document.getElementById('btn-install');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
|
ui.showModal(_('Installing Gitea Platform'), [
|
||||||
|
E('p', {}, _('This will download Alpine Linux rootfs and the Gitea binary. This may take several minutes.')),
|
||||||
|
E('div', { 'class': 'gt-progress' }, [
|
||||||
|
E('div', { 'class': 'gt-progress-bar', 'id': 'modal-progress', 'style': 'width:0%' })
|
||||||
|
]),
|
||||||
|
E('p', { 'id': 'modal-status' }, _('Starting installation...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.install().then(function(result) {
|
||||||
|
if (result && result.started) {
|
||||||
|
self.pollInstallProgress();
|
||||||
|
} else {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Installation failed')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Installation failed: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pollInstallProgress: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var checkProgress = function() {
|
||||||
|
api.getInstallProgress().then(function(result) {
|
||||||
|
var progressBar = document.getElementById('modal-progress');
|
||||||
|
var statusText = document.getElementById('modal-status');
|
||||||
|
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = (result.progress || 0) + '%';
|
||||||
|
}
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = result.message || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'completed') {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Gitea Platform installed successfully!')), 'success');
|
||||||
|
self.refreshData();
|
||||||
|
location.reload();
|
||||||
|
} else if (result.status === 'error') {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Installation failed: ') + result.message), 'error');
|
||||||
|
} else if (result.status === 'running') {
|
||||||
|
setTimeout(checkProgress, 3000);
|
||||||
|
} else {
|
||||||
|
setTimeout(checkProgress, 3000);
|
||||||
|
}
|
||||||
|
}).catch(function() {
|
||||||
|
setTimeout(checkProgress, 5000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(checkProgress, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleStart: function() {
|
||||||
|
var self = this;
|
||||||
|
api.start().then(function(result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Gitea Platform started')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to start')), 'error');
|
||||||
|
}
|
||||||
|
self.refreshData();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleStop: function() {
|
||||||
|
var self = this;
|
||||||
|
api.stop().then(function(result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Gitea Platform stopped')), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to stop')), 'error');
|
||||||
|
}
|
||||||
|
self.refreshData();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRestart: function() {
|
||||||
|
var self = this;
|
||||||
|
api.restart().then(function(result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Gitea Platform restarted')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to restart')), 'error');
|
||||||
|
}
|
||||||
|
self.refreshData();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUpdate: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal(_('Updating Gitea'), [
|
||||||
|
E('p', {}, _('Downloading and installing the latest Gitea binary...')),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.update().then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.started) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Update started. The server will restart automatically.')), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Update failed')), 'error');
|
||||||
|
}
|
||||||
|
self.refreshData();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBackup: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal(_('Creating Backup'), [
|
||||||
|
E('p', {}, _('Backing up repositories and database...')),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.createBackup().then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Backup created: ') + (result.file || '')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Backup failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUninstall: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal(_('Confirm Uninstall'), [
|
||||||
|
E('p', {}, _('Are you sure you want to uninstall Gitea Platform? Your repositories will be preserved.')),
|
||||||
|
E('div', { 'class': 'right' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn',
|
||||||
|
'click': ui.hideModal
|
||||||
|
}, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-negative',
|
||||||
|
'click': function() {
|
||||||
|
ui.hideModal();
|
||||||
|
api.uninstall().then(function(result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Gitea Platform uninstalled')), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Uninstall failed')), 'error');
|
||||||
|
}
|
||||||
|
self.refreshData();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Uninstall'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,212 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
'require poll';
|
||||||
|
'require gitea.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
reposData: null,
|
||||||
|
statusData: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
var self = this;
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.listRepos()
|
||||||
|
]).then(function(results) {
|
||||||
|
self.statusData = results[0] || {};
|
||||||
|
self.reposData = results[1] || {};
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Inject CSS
|
||||||
|
var cssLink = E('link', {
|
||||||
|
'rel': 'stylesheet',
|
||||||
|
'type': 'text/css',
|
||||||
|
'href': L.resource('gitea/dashboard.css')
|
||||||
|
});
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'gitea-dashboard' }, [
|
||||||
|
cssLink,
|
||||||
|
this.renderHeader(),
|
||||||
|
this.renderContent()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Poll for updates
|
||||||
|
poll.add(function() {
|
||||||
|
return api.listRepos().then(function(data) {
|
||||||
|
self.reposData = data;
|
||||||
|
self.updateRepoList();
|
||||||
|
});
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHeader: function() {
|
||||||
|
var repos = this.reposData.repos || [];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-header' }, [
|
||||||
|
E('div', { 'class': 'gt-header-content' }, [
|
||||||
|
E('div', { 'class': 'gt-logo' }, '\uD83D\uDCE6'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('h1', { 'class': 'gt-title' }, _('REPOSITORIES')),
|
||||||
|
E('p', { 'class': 'gt-subtitle' }, _('Git Repository Browser'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-status-badge running' }, [
|
||||||
|
E('span', {}, '\uD83D\uDCE6'),
|
||||||
|
' ' + repos.length + ' ' + _('Repositories')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderContent: function() {
|
||||||
|
var repos = this.reposData.repos || [];
|
||||||
|
var status = this.statusData;
|
||||||
|
|
||||||
|
if (!status.installed) {
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDCE6'),
|
||||||
|
E('div', {}, _('Gitea is not installed')),
|
||||||
|
E('p', {}, _('Install Gitea from the Overview page to manage repositories.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDCED'),
|
||||||
|
E('div', {}, _('No repositories found')),
|
||||||
|
E('p', {}, _('Create your first repository through the Gitea web interface.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card gt-card-full' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDCC2'),
|
||||||
|
' ' + _('Repository List')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
this.renderRepoTable(repos)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderRepoTable: function(repos) {
|
||||||
|
var self = this;
|
||||||
|
var status = this.statusData;
|
||||||
|
var lanIp = status.http_url ? status.http_url.replace(/^https?:\/\//, '').split(':')[0] : 'localhost';
|
||||||
|
var httpPort = status.http_port || 3000;
|
||||||
|
var sshPort = status.ssh_port || 2222;
|
||||||
|
|
||||||
|
return E('table', { 'class': 'gt-table', 'id': 'repo-table' }, [
|
||||||
|
E('thead', {}, [
|
||||||
|
E('tr', {}, [
|
||||||
|
E('th', {}, _('Repository')),
|
||||||
|
E('th', {}, _('Owner')),
|
||||||
|
E('th', {}, _('Size')),
|
||||||
|
E('th', {}, _('Clone URL'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tbody', {},
|
||||||
|
repos.map(function(repo) {
|
||||||
|
var httpClone = 'http://' + lanIp + ':' + httpPort + '/' + repo.owner + '/' + repo.name + '.git';
|
||||||
|
var sshClone = 'git@' + lanIp + ':' + sshPort + '/' + repo.owner + '/' + repo.name + '.git';
|
||||||
|
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', { 'class': 'gt-repo-name' }, repo.name),
|
||||||
|
E('td', {}, repo.owner || '-'),
|
||||||
|
E('td', {}, repo.size || '-'),
|
||||||
|
E('td', {}, [
|
||||||
|
E('div', {
|
||||||
|
'class': 'gt-clone-url',
|
||||||
|
'title': _('Click to copy'),
|
||||||
|
'click': function() { self.copyToClipboard(httpClone); }
|
||||||
|
}, httpClone),
|
||||||
|
E('div', {
|
||||||
|
'class': 'gt-clone-url',
|
||||||
|
'title': _('Click to copy SSH URL'),
|
||||||
|
'click': function() { self.copyToClipboard(sshClone); },
|
||||||
|
'style': 'margin-top: 4px; font-size: 10px;'
|
||||||
|
}, sshClone)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRepoList: function() {
|
||||||
|
var table = document.getElementById('repo-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
var repos = this.reposData.repos || [];
|
||||||
|
var tbody = table.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
// Update table content
|
||||||
|
var self = this;
|
||||||
|
var status = this.statusData;
|
||||||
|
var lanIp = status.http_url ? status.http_url.replace(/^https?:\/\//, '').split(':')[0] : 'localhost';
|
||||||
|
var httpPort = status.http_port || 3000;
|
||||||
|
var sshPort = status.ssh_port || 2222;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
repos.forEach(function(repo) {
|
||||||
|
var httpClone = 'http://' + lanIp + ':' + httpPort + '/' + repo.owner + '/' + repo.name + '.git';
|
||||||
|
var sshClone = 'git@' + lanIp + ':' + sshPort + '/' + repo.owner + '/' + repo.name + '.git';
|
||||||
|
|
||||||
|
var row = E('tr', {}, [
|
||||||
|
E('td', { 'class': 'gt-repo-name' }, repo.name),
|
||||||
|
E('td', {}, repo.owner || '-'),
|
||||||
|
E('td', {}, repo.size || '-'),
|
||||||
|
E('td', {}, [
|
||||||
|
E('div', {
|
||||||
|
'class': 'gt-clone-url',
|
||||||
|
'title': _('Click to copy'),
|
||||||
|
'click': function() { self.copyToClipboard(httpClone); }
|
||||||
|
}, httpClone),
|
||||||
|
E('div', {
|
||||||
|
'class': 'gt-clone-url',
|
||||||
|
'title': _('Click to copy SSH URL'),
|
||||||
|
'click': function() { self.copyToClipboard(sshClone); },
|
||||||
|
'style': 'margin-top: 4px; font-size: 10px;'
|
||||||
|
}, sshClone)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
copyToClipboard: function(text) {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Copied to clipboard: ') + text), 'info');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.value = text;
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(input);
|
||||||
|
ui.addNotification(null, E('p', {}, _('Copied to clipboard: ') + text), 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,351 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
'require gitea.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
configData: null,
|
||||||
|
statusData: null,
|
||||||
|
backupsData: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
var self = this;
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.getConfig(),
|
||||||
|
api.listBackups()
|
||||||
|
]).then(function(results) {
|
||||||
|
self.statusData = results[0] || {};
|
||||||
|
self.configData = results[1] || {};
|
||||||
|
self.backupsData = results[2] || [];
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Inject CSS
|
||||||
|
var cssLink = E('link', {
|
||||||
|
'rel': 'stylesheet',
|
||||||
|
'type': 'text/css',
|
||||||
|
'href': L.resource('gitea/dashboard.css')
|
||||||
|
});
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'gitea-dashboard' }, [
|
||||||
|
cssLink,
|
||||||
|
this.renderHeader(),
|
||||||
|
this.renderContent()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHeader: function() {
|
||||||
|
return E('div', { 'class': 'gt-header' }, [
|
||||||
|
E('div', { 'class': 'gt-header-content' }, [
|
||||||
|
E('div', { 'class': 'gt-logo' }, '\u2699\uFE0F'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('h1', { 'class': 'gt-title' }, _('SETTINGS')),
|
||||||
|
E('p', { 'class': 'gt-subtitle' }, _('Gitea Platform Configuration'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderContent: function() {
|
||||||
|
var status = this.statusData;
|
||||||
|
|
||||||
|
if (!status.installed) {
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\u2699\uFE0F'),
|
||||||
|
E('div', {}, _('Gitea is not installed')),
|
||||||
|
E('p', {}, _('Install Gitea from the Overview page to configure settings.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-main-grid' }, [
|
||||||
|
this.renderServerSettings(),
|
||||||
|
this.renderSecuritySettings(),
|
||||||
|
this.renderBackupCard()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderServerSettings: function() {
|
||||||
|
var self = this;
|
||||||
|
var config = this.configData;
|
||||||
|
var main = config.main || {};
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDDA5\uFE0F'),
|
||||||
|
' ' + _('Server Settings')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('App Name')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'cfg-app-name',
|
||||||
|
'value': main.app_name || 'SecuBox Git'
|
||||||
|
}),
|
||||||
|
E('div', { 'class': 'gt-form-hint' }, _('Display name for the Gitea instance'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('Domain')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'cfg-domain',
|
||||||
|
'value': main.domain || 'git.local'
|
||||||
|
}),
|
||||||
|
E('div', { 'class': 'gt-form-hint' }, _('Domain name for URLs'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('HTTP Port')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'number',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'cfg-http-port',
|
||||||
|
'value': main.http_port || 3000
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('SSH Port')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'number',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'cfg-ssh-port',
|
||||||
|
'value': main.ssh_port || 2222
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('Memory Limit')),
|
||||||
|
E('select', { 'class': 'gt-form-select', 'id': 'cfg-memory' }, [
|
||||||
|
E('option', { 'value': '256M', 'selected': main.memory_limit === '256M' }, '256 MB'),
|
||||||
|
E('option', { 'value': '512M', 'selected': main.memory_limit === '512M' || !main.memory_limit }, '512 MB'),
|
||||||
|
E('option', { 'value': '1G', 'selected': main.memory_limit === '1G' }, '1 GB'),
|
||||||
|
E('option', { 'value': '2G', 'selected': main.memory_limit === '2G' }, '2 GB')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-checkbox' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'checkbox',
|
||||||
|
'id': 'cfg-enabled',
|
||||||
|
'checked': main.enabled
|
||||||
|
}),
|
||||||
|
_('Enable service on boot')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-primary',
|
||||||
|
'click': function() { self.handleSaveConfig(); }
|
||||||
|
}, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Save Settings')])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSecuritySettings: function() {
|
||||||
|
var self = this;
|
||||||
|
var config = this.configData;
|
||||||
|
var server = config.server || {};
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDD12'),
|
||||||
|
' ' + _('Security Settings')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-checkbox' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'checkbox',
|
||||||
|
'id': 'cfg-disable-reg',
|
||||||
|
'checked': server.disable_registration
|
||||||
|
}),
|
||||||
|
_('Disable user registration')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-hint' }, _('Prevent new users from signing up'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-checkbox' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'checkbox',
|
||||||
|
'id': 'cfg-require-signin',
|
||||||
|
'checked': server.require_signin
|
||||||
|
}),
|
||||||
|
_('Require sign-in to view')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-hint' }, _('Require authentication to browse repositories'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('Landing Page')),
|
||||||
|
E('select', { 'class': 'gt-form-select', 'id': 'cfg-landing' }, [
|
||||||
|
E('option', { 'value': 'explore', 'selected': server.landing_page === 'explore' }, _('Explore')),
|
||||||
|
E('option', { 'value': 'home', 'selected': server.landing_page === 'home' }, _('Home')),
|
||||||
|
E('option', { 'value': 'organizations', 'selected': server.landing_page === 'organizations' }, _('Organizations')),
|
||||||
|
E('option', { 'value': 'login', 'selected': server.landing_page === 'login' }, _('Login'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('p', { 'class': 'gt-form-hint', 'style': 'margin-top: 15px; color: #ff5f1f;' },
|
||||||
|
_('Note: Changes to security settings require a service restart to take effect.'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderBackupCard: function() {
|
||||||
|
var self = this;
|
||||||
|
var backups = this.backupsData || [];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDCBE'),
|
||||||
|
' ' + _('Backup & Restore')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-btn-group' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-primary',
|
||||||
|
'click': function() { self.handleBackup(); }
|
||||||
|
}, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Create Backup')])
|
||||||
|
]),
|
||||||
|
backups.length > 0 ?
|
||||||
|
E('div', { 'style': 'margin-top: 20px' }, [
|
||||||
|
E('h4', { 'style': 'color: #888; font-size: 12px; margin-bottom: 10px;' }, _('Available Backups')),
|
||||||
|
E('table', { 'class': 'gt-table' }, [
|
||||||
|
E('thead', {}, [
|
||||||
|
E('tr', {}, [
|
||||||
|
E('th', {}, _('Filename')),
|
||||||
|
E('th', {}, _('Size')),
|
||||||
|
E('th', {}, _('Date')),
|
||||||
|
E('th', {}, _('Actions'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tbody', {},
|
||||||
|
backups.map(function(backup) {
|
||||||
|
var date = backup.mtime ? new Date(backup.mtime * 1000).toLocaleString() : '-';
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', {}, backup.name),
|
||||||
|
E('td', {}, backup.size || '-'),
|
||||||
|
E('td', {}, date),
|
||||||
|
E('td', {}, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-warning',
|
||||||
|
'style': 'padding: 4px 8px; font-size: 10px;',
|
||||||
|
'click': function() { self.handleRestore(backup.path); }
|
||||||
|
}, _('Restore'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]) :
|
||||||
|
E('div', { 'class': 'gt-empty', 'style': 'padding: 20px' }, [
|
||||||
|
E('div', {}, _('No backups found'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveConfig: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
app_name: document.getElementById('cfg-app-name').value,
|
||||||
|
domain: document.getElementById('cfg-domain').value,
|
||||||
|
http_port: parseInt(document.getElementById('cfg-http-port').value) || 3000,
|
||||||
|
ssh_port: parseInt(document.getElementById('cfg-ssh-port').value) || 2222,
|
||||||
|
memory_limit: document.getElementById('cfg-memory').value,
|
||||||
|
enabled: document.getElementById('cfg-enabled').checked ? '1' : '0',
|
||||||
|
disable_registration: document.getElementById('cfg-disable-reg').checked ? 'true' : 'false',
|
||||||
|
require_signin: document.getElementById('cfg-require-signin').checked ? 'true' : 'false',
|
||||||
|
landing_page: document.getElementById('cfg-landing').value
|
||||||
|
};
|
||||||
|
|
||||||
|
api.saveConfig(config).then(function(result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Configuration saved. Restart the service for changes to take effect.')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to save configuration')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to save configuration: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBackup: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal(_('Creating Backup'), [
|
||||||
|
E('p', {}, _('Backing up repositories and database...')),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.createBackup().then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Backup created successfully')), 'success');
|
||||||
|
// Refresh backup list
|
||||||
|
return api.listBackups().then(function(data) {
|
||||||
|
self.backupsData = data;
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Backup failed')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Backup failed: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRestore: function(file) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal(_('Confirm Restore'), [
|
||||||
|
E('p', {}, _('Are you sure you want to restore from this backup? This will overwrite current data.')),
|
||||||
|
E('p', { 'style': 'color: #ffaa00' }, file),
|
||||||
|
E('div', { 'class': 'right' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn',
|
||||||
|
'click': ui.hideModal
|
||||||
|
}, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-negative',
|
||||||
|
'click': function() {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.showModal(_('Restoring Backup'), [
|
||||||
|
E('p', {}, _('Restoring data...')),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.restoreBackup(file).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Restore completed successfully')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Restore failed')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Restore failed: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Restore'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,262 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require dom';
|
||||||
|
'require poll';
|
||||||
|
'require gitea.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
usersData: null,
|
||||||
|
statusData: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
var self = this;
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.listUsers()
|
||||||
|
]).then(function(results) {
|
||||||
|
self.statusData = results[0] || {};
|
||||||
|
self.usersData = results[1] || [];
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Inject CSS
|
||||||
|
var cssLink = E('link', {
|
||||||
|
'rel': 'stylesheet',
|
||||||
|
'type': 'text/css',
|
||||||
|
'href': L.resource('gitea/dashboard.css')
|
||||||
|
});
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'gitea-dashboard' }, [
|
||||||
|
cssLink,
|
||||||
|
this.renderHeader(),
|
||||||
|
this.renderContent()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Poll for updates
|
||||||
|
poll.add(function() {
|
||||||
|
return api.listUsers().then(function(data) {
|
||||||
|
self.usersData = data;
|
||||||
|
self.updateUserList();
|
||||||
|
});
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHeader: function() {
|
||||||
|
var self = this;
|
||||||
|
var users = this.usersData || [];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-header' }, [
|
||||||
|
E('div', { 'class': 'gt-header-content' }, [
|
||||||
|
E('div', { 'class': 'gt-logo' }, '\uD83D\uDC65'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('h1', { 'class': 'gt-title' }, _('USER MANAGEMENT')),
|
||||||
|
E('p', { 'class': 'gt-subtitle' }, _('Gitea User Administration'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-status-badge running' }, [
|
||||||
|
E('span', {}, '\uD83D\uDC65'),
|
||||||
|
' ' + users.length + ' ' + _('Users')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderContent: function() {
|
||||||
|
var self = this;
|
||||||
|
var status = this.statusData;
|
||||||
|
var users = this.usersData || [];
|
||||||
|
|
||||||
|
if (!status.installed) {
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDC65'),
|
||||||
|
E('div', {}, _('Gitea is not installed')),
|
||||||
|
E('p', {}, _('Install Gitea from the Overview page to manage users.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.running) {
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\u26A0\uFE0F'),
|
||||||
|
E('div', {}, _('Gitea is not running')),
|
||||||
|
E('p', {}, _('Start Gitea to manage users.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-main-grid' }, [
|
||||||
|
this.renderCreateAdminCard(),
|
||||||
|
this.renderUserListCard(users)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCreateAdminCard: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDC64'),
|
||||||
|
' ' + _('Create Admin User')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('Username')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'new-username',
|
||||||
|
'placeholder': 'admin'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('Password')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'password',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'new-password',
|
||||||
|
'placeholder': '********'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-form-group' }, [
|
||||||
|
E('label', { 'class': 'gt-form-label' }, _('Email')),
|
||||||
|
E('input', {
|
||||||
|
'type': 'email',
|
||||||
|
'class': 'gt-form-input',
|
||||||
|
'id': 'new-email',
|
||||||
|
'placeholder': 'admin@localhost'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'gt-btn gt-btn-success',
|
||||||
|
'click': function() { self.handleCreateAdmin(); }
|
||||||
|
}, [E('span', {}, '\u2795'), ' ' + _('Create Admin')])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUserListCard: function(users) {
|
||||||
|
if (users.length === 0) {
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDC65'),
|
||||||
|
' ' + _('User List')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('div', { 'class': 'gt-empty' }, [
|
||||||
|
E('div', { 'class': 'gt-empty-icon' }, '\uD83D\uDC64'),
|
||||||
|
E('div', {}, _('No users found')),
|
||||||
|
E('p', {}, _('Create your first admin user above, or through the Gitea web interface.'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'gt-card' }, [
|
||||||
|
E('div', { 'class': 'gt-card-header' }, [
|
||||||
|
E('div', { 'class': 'gt-card-title' }, [
|
||||||
|
E('span', {}, '\uD83D\uDC65'),
|
||||||
|
' ' + _('User List')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'gt-card-body' }, [
|
||||||
|
E('table', { 'class': 'gt-table', 'id': 'user-table' }, [
|
||||||
|
E('thead', {}, [
|
||||||
|
E('tr', {}, [
|
||||||
|
E('th', {}, _('Username')),
|
||||||
|
E('th', {}, _('Email')),
|
||||||
|
E('th', {}, _('Role')),
|
||||||
|
E('th', {}, _('Created'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tbody', {},
|
||||||
|
users.map(function(user) {
|
||||||
|
var created = user.created ? new Date(user.created * 1000).toLocaleDateString() : '-';
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', { 'class': 'gt-repo-name' }, user.name),
|
||||||
|
E('td', {}, user.email || '-'),
|
||||||
|
E('td', {}, [
|
||||||
|
user.is_admin ?
|
||||||
|
E('span', { 'class': 'gt-badge gt-badge-admin' }, 'Admin') :
|
||||||
|
E('span', { 'class': 'gt-badge gt-badge-user' }, 'User')
|
||||||
|
]),
|
||||||
|
E('td', {}, created)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUserList: function() {
|
||||||
|
var table = document.getElementById('user-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
var users = this.usersData || [];
|
||||||
|
var tbody = table.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
users.forEach(function(user) {
|
||||||
|
var created = user.created ? new Date(user.created * 1000).toLocaleDateString() : '-';
|
||||||
|
var row = E('tr', {}, [
|
||||||
|
E('td', { 'class': 'gt-repo-name' }, user.name),
|
||||||
|
E('td', {}, user.email || '-'),
|
||||||
|
E('td', {}, [
|
||||||
|
user.is_admin ?
|
||||||
|
E('span', { 'class': 'gt-badge gt-badge-admin' }, 'Admin') :
|
||||||
|
E('span', { 'class': 'gt-badge gt-badge-user' }, 'User')
|
||||||
|
]),
|
||||||
|
E('td', {}, created)
|
||||||
|
]);
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCreateAdmin: function() {
|
||||||
|
var self = this;
|
||||||
|
var username = document.getElementById('new-username').value;
|
||||||
|
var password = document.getElementById('new-password').value;
|
||||||
|
var email = document.getElementById('new-email').value;
|
||||||
|
|
||||||
|
if (!username || !password || !email) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please fill in all fields')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.createAdmin(username, password, email).then(function(result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Admin user created: ') + username), 'success');
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('new-username').value = '';
|
||||||
|
document.getElementById('new-password').value = '';
|
||||||
|
document.getElementById('new-email').value = '';
|
||||||
|
// Refresh user list
|
||||||
|
return api.listUsers().then(function(data) {
|
||||||
|
self.usersData = data;
|
||||||
|
self.updateUserList();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to create user')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Failed to create user: ') + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
723
package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea
Normal file
723
package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# LuCI RPC backend for Gitea Platform
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
CONFIG="gitea"
|
||||||
|
LXC_NAME="gitea"
|
||||||
|
LXC_PATH="/srv/lxc"
|
||||||
|
DATA_PATH="/srv/gitea"
|
||||||
|
GITEA_VERSION="1.22.6"
|
||||||
|
|
||||||
|
# JSON helpers
|
||||||
|
json_init_obj() { json_init; json_add_object "result"; }
|
||||||
|
json_close_obj() { json_close_object; json_dump; }
|
||||||
|
|
||||||
|
json_error() {
|
||||||
|
json_init
|
||||||
|
json_add_object "error"
|
||||||
|
json_add_string "message" "$1"
|
||||||
|
json_close_object
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
json_success() {
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
[ -n "$1" ] && json_add_string "message" "$1"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if container is running
|
||||||
|
lxc_running() {
|
||||||
|
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if container exists
|
||||||
|
lxc_exists() {
|
||||||
|
[ -f "$LXC_PATH/$LXC_NAME/config" ] && [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service status
|
||||||
|
get_status() {
|
||||||
|
local enabled running installed uptime
|
||||||
|
local http_port ssh_port data_path memory_limit app_name domain
|
||||||
|
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get enabled main enabled "0"
|
||||||
|
config_get http_port main http_port "3000"
|
||||||
|
config_get ssh_port main ssh_port "2222"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
config_get memory_limit main memory_limit "512M"
|
||||||
|
config_get app_name main app_name "SecuBox Git"
|
||||||
|
config_get domain main domain "git.local"
|
||||||
|
|
||||||
|
running="false"
|
||||||
|
installed="false"
|
||||||
|
uptime=""
|
||||||
|
|
||||||
|
if lxc_exists; then
|
||||||
|
installed="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if lxc_running; then
|
||||||
|
running="true"
|
||||||
|
uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count repositories
|
||||||
|
local repo_count=0
|
||||||
|
DATA_PATH="$data_path"
|
||||||
|
if [ -d "$DATA_PATH/git/repositories" ]; then
|
||||||
|
repo_count=$(find "$DATA_PATH/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get disk usage
|
||||||
|
local disk_usage="0"
|
||||||
|
if [ -d "$DATA_PATH" ]; then
|
||||||
|
disk_usage=$(du -sh "$DATA_PATH" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get LAN IP for URL
|
||||||
|
local lan_ip
|
||||||
|
lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_boolean "running" "$( [ "$running" = "true" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_boolean "installed" "$( [ "$installed" = "true" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_string "uptime" "$uptime"
|
||||||
|
json_add_int "http_port" "$http_port"
|
||||||
|
json_add_int "ssh_port" "$ssh_port"
|
||||||
|
json_add_string "data_path" "$data_path"
|
||||||
|
json_add_string "memory_limit" "$memory_limit"
|
||||||
|
json_add_string "app_name" "$app_name"
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
|
json_add_int "repo_count" "$repo_count"
|
||||||
|
json_add_string "disk_usage" "$disk_usage"
|
||||||
|
json_add_string "http_url" "http://${lan_ip}:${http_port}"
|
||||||
|
json_add_string "ssh_url" "ssh://git@${lan_ip}:${ssh_port}"
|
||||||
|
json_add_string "container_name" "$LXC_NAME"
|
||||||
|
json_add_string "version" "$GITEA_VERSION"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get statistics
|
||||||
|
get_stats() {
|
||||||
|
local data_path
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
|
||||||
|
local repo_count=0
|
||||||
|
local user_count=0
|
||||||
|
local disk_usage="0"
|
||||||
|
|
||||||
|
# Count repositories
|
||||||
|
if [ -d "$data_path/git/repositories" ]; then
|
||||||
|
repo_count=$(find "$data_path/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get disk usage
|
||||||
|
if [ -d "$data_path" ]; then
|
||||||
|
disk_usage=$(du -sh "$data_path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count users (if we can query the database)
|
||||||
|
if [ -f "$data_path/gitea.db" ] && command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
user_count=$(sqlite3 "$data_path/gitea.db" "SELECT COUNT(*) FROM user" 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_int "repo_count" "$repo_count"
|
||||||
|
json_add_int "user_count" "$user_count"
|
||||||
|
json_add_string "disk_usage" "$disk_usage"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
|
get_config() {
|
||||||
|
local http_port ssh_port http_host data_path memory_limit enabled app_name domain
|
||||||
|
local protocol disable_registration require_signin landing_page
|
||||||
|
|
||||||
|
config_load "$CONFIG"
|
||||||
|
|
||||||
|
# Main settings
|
||||||
|
config_get http_port main http_port "3000"
|
||||||
|
config_get ssh_port main ssh_port "2222"
|
||||||
|
config_get http_host main http_host "0.0.0.0"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
config_get memory_limit main memory_limit "512M"
|
||||||
|
config_get enabled main enabled "0"
|
||||||
|
config_get app_name main app_name "SecuBox Git"
|
||||||
|
config_get domain main domain "git.local"
|
||||||
|
|
||||||
|
# Server settings
|
||||||
|
config_get protocol server protocol "http"
|
||||||
|
config_get disable_registration server disable_registration "false"
|
||||||
|
config_get require_signin server require_signin "false"
|
||||||
|
config_get landing_page server landing_page "explore"
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_object "main"
|
||||||
|
json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_int "http_port" "$http_port"
|
||||||
|
json_add_int "ssh_port" "$ssh_port"
|
||||||
|
json_add_string "http_host" "$http_host"
|
||||||
|
json_add_string "data_path" "$data_path"
|
||||||
|
json_add_string "memory_limit" "$memory_limit"
|
||||||
|
json_add_string "app_name" "$app_name"
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "server"
|
||||||
|
json_add_string "protocol" "$protocol"
|
||||||
|
json_add_boolean "disable_registration" "$( [ "$disable_registration" = "true" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_boolean "require_signin" "$( [ "$require_signin" = "true" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_string "landing_page" "$landing_page"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save configuration
|
||||||
|
save_config() {
|
||||||
|
read -r input
|
||||||
|
|
||||||
|
local http_port ssh_port http_host data_path memory_limit enabled app_name domain
|
||||||
|
local protocol disable_registration require_signin landing_page
|
||||||
|
|
||||||
|
http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null)
|
||||||
|
ssh_port=$(echo "$input" | jsonfilter -e '@.ssh_port' 2>/dev/null)
|
||||||
|
http_host=$(echo "$input" | jsonfilter -e '@.http_host' 2>/dev/null)
|
||||||
|
data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null)
|
||||||
|
memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null)
|
||||||
|
enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null)
|
||||||
|
app_name=$(echo "$input" | jsonfilter -e '@.app_name' 2>/dev/null)
|
||||||
|
domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||||
|
protocol=$(echo "$input" | jsonfilter -e '@.protocol' 2>/dev/null)
|
||||||
|
disable_registration=$(echo "$input" | jsonfilter -e '@.disable_registration' 2>/dev/null)
|
||||||
|
require_signin=$(echo "$input" | jsonfilter -e '@.require_signin' 2>/dev/null)
|
||||||
|
landing_page=$(echo "$input" | jsonfilter -e '@.landing_page' 2>/dev/null)
|
||||||
|
|
||||||
|
[ -n "$http_port" ] && uci set "${CONFIG}.main.http_port=$http_port"
|
||||||
|
[ -n "$ssh_port" ] && uci set "${CONFIG}.main.ssh_port=$ssh_port"
|
||||||
|
[ -n "$http_host" ] && uci set "${CONFIG}.main.http_host=$http_host"
|
||||||
|
[ -n "$data_path" ] && uci set "${CONFIG}.main.data_path=$data_path"
|
||||||
|
[ -n "$memory_limit" ] && uci set "${CONFIG}.main.memory_limit=$memory_limit"
|
||||||
|
[ -n "$enabled" ] && uci set "${CONFIG}.main.enabled=$enabled"
|
||||||
|
[ -n "$app_name" ] && uci set "${CONFIG}.main.app_name=$app_name"
|
||||||
|
[ -n "$domain" ] && uci set "${CONFIG}.main.domain=$domain"
|
||||||
|
[ -n "$protocol" ] && uci set "${CONFIG}.server.protocol=$protocol"
|
||||||
|
[ -n "$disable_registration" ] && uci set "${CONFIG}.server.disable_registration=$disable_registration"
|
||||||
|
[ -n "$require_signin" ] && uci set "${CONFIG}.server.require_signin=$require_signin"
|
||||||
|
[ -n "$landing_page" ] && uci set "${CONFIG}.server.landing_page=$landing_page"
|
||||||
|
|
||||||
|
uci commit "$CONFIG"
|
||||||
|
|
||||||
|
json_success "Configuration saved"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
start_service() {
|
||||||
|
if lxc_running; then
|
||||||
|
json_error "Service is already running"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! lxc_exists; then
|
||||||
|
json_error "Container not installed. Run install first."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
/etc/init.d/gitea start >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
if lxc_running; then
|
||||||
|
json_success "Service started"
|
||||||
|
else
|
||||||
|
json_error "Failed to start service"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
stop_service() {
|
||||||
|
if ! lxc_running; then
|
||||||
|
json_error "Service is not running"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
/etc/init.d/gitea stop >/dev/null 2>&1
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
if ! lxc_running; then
|
||||||
|
json_success "Service stopped"
|
||||||
|
else
|
||||||
|
json_error "Failed to stop service"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restart service
|
||||||
|
restart_service() {
|
||||||
|
/etc/init.d/gitea restart >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
sleep 4
|
||||||
|
if lxc_running; then
|
||||||
|
json_success "Service restarted"
|
||||||
|
else
|
||||||
|
json_error "Service restart failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install Gitea
|
||||||
|
install() {
|
||||||
|
if lxc_exists; then
|
||||||
|
json_error "Already installed. Use update to refresh."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run install in background
|
||||||
|
/usr/sbin/giteactl install >/var/log/gitea-install.log 2>&1 &
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "started" 1
|
||||||
|
json_add_string "message" "Installation started in background"
|
||||||
|
json_add_string "log_file" "/var/log/gitea-install.log"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uninstall Gitea
|
||||||
|
uninstall() {
|
||||||
|
/usr/sbin/giteactl uninstall >/dev/null 2>&1
|
||||||
|
|
||||||
|
if ! lxc_exists; then
|
||||||
|
json_success "Uninstalled successfully"
|
||||||
|
else
|
||||||
|
json_error "Uninstall failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update Gitea
|
||||||
|
update() {
|
||||||
|
if ! lxc_exists; then
|
||||||
|
json_error "Not installed. Run install first."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run update in background
|
||||||
|
/usr/sbin/giteactl update >/var/log/gitea-update.log 2>&1 &
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "started" 1
|
||||||
|
json_add_string "message" "Update started in background"
|
||||||
|
json_add_string "log_file" "/var/log/gitea-update.log"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get logs
|
||||||
|
get_logs() {
|
||||||
|
read -r input
|
||||||
|
local lines
|
||||||
|
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
|
||||||
|
[ -z "$lines" ] && lines=100
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_array "logs"
|
||||||
|
|
||||||
|
# Get container logs if running
|
||||||
|
if lxc_running; then
|
||||||
|
local data_path
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
|
||||||
|
if [ -f "$data_path/log/gitea.log" ]; then
|
||||||
|
tail -n "$lines" "$data_path/log/gitea.log" 2>/dev/null | while IFS= read -r line; do
|
||||||
|
json_add_string "" "$line"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also check install/update logs
|
||||||
|
for logfile in /var/log/gitea-install.log /var/log/gitea-update.log; do
|
||||||
|
[ -f "$logfile" ] || continue
|
||||||
|
tail -n 50 "$logfile" 2>/dev/null | while IFS= read -r line; do
|
||||||
|
json_add_string "" "$line"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# List repositories
|
||||||
|
list_repos() {
|
||||||
|
local data_path
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_array "repos"
|
||||||
|
|
||||||
|
local repo_root="$data_path/git/repositories"
|
||||||
|
if [ -d "$repo_root" ]; then
|
||||||
|
find "$repo_root" -name "*.git" -type d 2>/dev/null | while read -r repo; do
|
||||||
|
local rel_path="${repo#$repo_root/}"
|
||||||
|
local name=$(basename "$repo" .git)
|
||||||
|
local owner=$(dirname "$rel_path")
|
||||||
|
local size=$(du -sh "$repo" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
|
||||||
|
# Get last commit time if possible
|
||||||
|
local mtime=""
|
||||||
|
if [ -f "$repo/refs/heads/master" ]; then
|
||||||
|
mtime=$(stat -c %Y "$repo/refs/heads/master" 2>/dev/null || echo "")
|
||||||
|
elif [ -f "$repo/refs/heads/main" ]; then
|
||||||
|
mtime=$(stat -c %Y "$repo/refs/heads/main" 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "owner" "$owner"
|
||||||
|
json_add_string "path" "$repo"
|
||||||
|
json_add_string "size" "$size"
|
||||||
|
[ -n "$mtime" ] && json_add_int "mtime" "$mtime"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_add_string "repo_root" "$repo_root"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get repository details
|
||||||
|
get_repo() {
|
||||||
|
read -r input
|
||||||
|
local name owner
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
owner=$(echo "$input" | jsonfilter -e '@.owner' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
json_error "Missing repository name"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local data_path
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
|
||||||
|
local repo_path="$data_path/git/repositories"
|
||||||
|
[ -n "$owner" ] && repo_path="$repo_path/$owner"
|
||||||
|
repo_path="$repo_path/${name}.git"
|
||||||
|
|
||||||
|
if [ ! -d "$repo_path" ]; then
|
||||||
|
json_error "Repository not found"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local size=$(du -sh "$repo_path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
local branches=$(ls -1 "$repo_path/refs/heads" 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
# Get LAN IP
|
||||||
|
local lan_ip
|
||||||
|
local http_port ssh_port
|
||||||
|
config_get http_port main http_port "3000"
|
||||||
|
config_get ssh_port main ssh_port "2222"
|
||||||
|
lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
|
||||||
|
local clone_url="http://${lan_ip}:${http_port}/${owner}/${name}.git"
|
||||||
|
local ssh_clone="git@${lan_ip}:${ssh_port}/${owner}/${name}.git"
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "owner" "$owner"
|
||||||
|
json_add_string "path" "$repo_path"
|
||||||
|
json_add_string "size" "$size"
|
||||||
|
json_add_int "branches" "$branches"
|
||||||
|
json_add_string "clone_url" "$clone_url"
|
||||||
|
json_add_string "ssh_clone" "$ssh_clone"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# List users (from SQLite if available)
|
||||||
|
list_users() {
|
||||||
|
local data_path
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_array "users"
|
||||||
|
|
||||||
|
local db_file="$data_path/gitea.db"
|
||||||
|
if [ -f "$db_file" ] && command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
sqlite3 -separator '|' "$db_file" \
|
||||||
|
"SELECT id, name, lower_name, email, is_admin, created_unix FROM user" 2>/dev/null | \
|
||||||
|
while IFS='|' read -r id name lower_name email is_admin created; do
|
||||||
|
json_add_object ""
|
||||||
|
json_add_int "id" "$id"
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "email" "$email"
|
||||||
|
json_add_boolean "is_admin" "$is_admin"
|
||||||
|
[ -n "$created" ] && json_add_int "created" "$created"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create admin user
|
||||||
|
create_admin() {
|
||||||
|
read -r input
|
||||||
|
local username password email
|
||||||
|
username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null)
|
||||||
|
password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null)
|
||||||
|
email=$(echo "$input" | jsonfilter -e '@.email' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$username" ] || [ -z "$password" ] || [ -z "$email" ]; then
|
||||||
|
json_error "Missing username, password, or email"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! lxc_running; then
|
||||||
|
json_error "Service must be running to create users"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc-attach -n "$LXC_NAME" -- su-exec git /usr/local/bin/gitea admin user create \
|
||||||
|
--username "$username" \
|
||||||
|
--password "$password" \
|
||||||
|
--email "$email" \
|
||||||
|
--admin \
|
||||||
|
--config /data/custom/conf/app.ini >/dev/null 2>&1
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_success "Admin user created: $username"
|
||||||
|
else
|
||||||
|
json_error "Failed to create admin user"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
create_backup() {
|
||||||
|
local result
|
||||||
|
result=$(/usr/sbin/giteactl backup 2>&1)
|
||||||
|
|
||||||
|
if echo "$result" | grep -q "Backup created"; then
|
||||||
|
local backup_file=$(echo "$result" | grep -o '/srv/gitea/backups/[^ ]*')
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Backup created"
|
||||||
|
json_add_string "file" "$backup_file"
|
||||||
|
json_close_obj
|
||||||
|
else
|
||||||
|
json_error "Backup failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# List backups
|
||||||
|
list_backups() {
|
||||||
|
local data_path
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get data_path main data_path "/srv/gitea"
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_array "backups"
|
||||||
|
|
||||||
|
local backup_dir="$data_path/backups"
|
||||||
|
if [ -d "$backup_dir" ]; then
|
||||||
|
ls -1 "$backup_dir"/*.tar.gz 2>/dev/null | while read -r backup; do
|
||||||
|
[ -f "$backup" ] || continue
|
||||||
|
local name=$(basename "$backup")
|
||||||
|
local size=$(ls -lh "$backup" 2>/dev/null | awk '{print $5}')
|
||||||
|
local mtime=$(stat -c %Y "$backup" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "path" "$backup"
|
||||||
|
json_add_string "size" "$size"
|
||||||
|
json_add_int "mtime" "$mtime"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restore backup
|
||||||
|
restore_backup() {
|
||||||
|
read -r input
|
||||||
|
local file
|
||||||
|
file=$(echo "$input" | jsonfilter -e '@.file' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$file" ] || [ ! -f "$file" ]; then
|
||||||
|
json_error "Missing or invalid backup file"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
/usr/sbin/giteactl restore "$file" >/dev/null 2>&1
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_success "Restore completed"
|
||||||
|
else
|
||||||
|
json_error "Restore failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check install progress
|
||||||
|
get_install_progress() {
|
||||||
|
local log_file="/var/log/gitea-install.log"
|
||||||
|
local status="unknown"
|
||||||
|
local progress=0
|
||||||
|
local message=""
|
||||||
|
|
||||||
|
if [ -f "$log_file" ]; then
|
||||||
|
# Check for completion markers
|
||||||
|
if grep -q "Installation complete" "$log_file" 2>/dev/null; then
|
||||||
|
status="completed"
|
||||||
|
progress=100
|
||||||
|
message="Installation completed successfully"
|
||||||
|
elif grep -q "ERROR" "$log_file" 2>/dev/null; then
|
||||||
|
status="error"
|
||||||
|
message=$(grep "ERROR" "$log_file" | tail -1)
|
||||||
|
else
|
||||||
|
status="running"
|
||||||
|
# Estimate progress based on log content
|
||||||
|
if grep -q "LXC config created" "$log_file" 2>/dev/null; then
|
||||||
|
progress=90
|
||||||
|
message="Finalizing setup..."
|
||||||
|
elif grep -q "Gitea binary installed" "$log_file" 2>/dev/null; then
|
||||||
|
progress=70
|
||||||
|
message="Configuring container..."
|
||||||
|
elif grep -q "Downloading Gitea" "$log_file" 2>/dev/null; then
|
||||||
|
progress=50
|
||||||
|
message="Downloading Gitea binary..."
|
||||||
|
elif grep -q "Rootfs created" "$log_file" 2>/dev/null; then
|
||||||
|
progress=40
|
||||||
|
message="Setting up container..."
|
||||||
|
elif grep -q "Extracting rootfs" "$log_file" 2>/dev/null; then
|
||||||
|
progress=30
|
||||||
|
message="Extracting container rootfs..."
|
||||||
|
elif grep -q "Downloading Alpine" "$log_file" 2>/dev/null; then
|
||||||
|
progress=20
|
||||||
|
message="Downloading Alpine rootfs..."
|
||||||
|
elif grep -q "Installing Gitea" "$log_file" 2>/dev/null; then
|
||||||
|
progress=10
|
||||||
|
message="Starting installation..."
|
||||||
|
else
|
||||||
|
progress=5
|
||||||
|
message="Initializing..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
status="not_started"
|
||||||
|
message="Installation has not been started"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if process is still running
|
||||||
|
if pgrep -f "giteactl install" >/dev/null 2>&1; then
|
||||||
|
status="running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_string "status" "$status"
|
||||||
|
json_add_int "progress" "$progress"
|
||||||
|
json_add_string "message" "$message"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main RPC handler
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
cat <<-EOF
|
||||||
|
{
|
||||||
|
"get_status": {},
|
||||||
|
"get_stats": {},
|
||||||
|
"get_config": {},
|
||||||
|
"save_config": {"http_port": 3000, "ssh_port": 2222, "http_host": "str", "data_path": "str", "memory_limit": "str", "enabled": "str", "app_name": "str", "domain": "str", "protocol": "str", "disable_registration": "str", "require_signin": "str", "landing_page": "str"},
|
||||||
|
"start": {},
|
||||||
|
"stop": {},
|
||||||
|
"restart": {},
|
||||||
|
"install": {},
|
||||||
|
"uninstall": {},
|
||||||
|
"update": {},
|
||||||
|
"get_logs": {"lines": 100},
|
||||||
|
"list_repos": {},
|
||||||
|
"get_repo": {"name": "str", "owner": "str"},
|
||||||
|
"list_users": {},
|
||||||
|
"create_admin": {"username": "str", "password": "str", "email": "str"},
|
||||||
|
"create_backup": {},
|
||||||
|
"list_backups": {},
|
||||||
|
"restore_backup": {"file": "str"},
|
||||||
|
"get_install_progress": {}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
get_status)
|
||||||
|
get_status
|
||||||
|
;;
|
||||||
|
get_stats)
|
||||||
|
get_stats
|
||||||
|
;;
|
||||||
|
get_config)
|
||||||
|
get_config
|
||||||
|
;;
|
||||||
|
save_config)
|
||||||
|
save_config
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
start_service
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_service
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_service
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
install
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
uninstall
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
update
|
||||||
|
;;
|
||||||
|
get_logs)
|
||||||
|
get_logs
|
||||||
|
;;
|
||||||
|
list_repos)
|
||||||
|
list_repos
|
||||||
|
;;
|
||||||
|
get_repo)
|
||||||
|
get_repo
|
||||||
|
;;
|
||||||
|
list_users)
|
||||||
|
list_users
|
||||||
|
;;
|
||||||
|
create_admin)
|
||||||
|
create_admin
|
||||||
|
;;
|
||||||
|
create_backup)
|
||||||
|
create_backup
|
||||||
|
;;
|
||||||
|
list_backups)
|
||||||
|
list_backups
|
||||||
|
;;
|
||||||
|
restore_backup)
|
||||||
|
restore_backup
|
||||||
|
;;
|
||||||
|
get_install_progress)
|
||||||
|
get_install_progress
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_error "Unknown method: $2"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"admin/services/gitea": {
|
||||||
|
"title": "Gitea",
|
||||||
|
"order": 87,
|
||||||
|
"action": {
|
||||||
|
"type": "firstchild"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-gitea"],
|
||||||
|
"uci": {"gitea": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/gitea/overview": {
|
||||||
|
"title": "Overview",
|
||||||
|
"order": 10,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "gitea/overview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/gitea/repos": {
|
||||||
|
"title": "Repositories",
|
||||||
|
"order": 20,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "gitea/repos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/gitea/users": {
|
||||||
|
"title": "Users",
|
||||||
|
"order": 30,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "gitea/users"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/gitea/settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"order": 40,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "gitea/settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"luci-app-gitea": {
|
||||||
|
"description": "Grant access to Gitea Platform management",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.gitea": ["get_status", "get_config", "get_logs", "list_repos", "get_repo", "list_users", "get_stats", "get_install_progress", "list_backups"]
|
||||||
|
},
|
||||||
|
"uci": ["gitea"]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.gitea": ["save_config", "start", "stop", "restart", "install", "uninstall", "update", "create_backup", "restore_backup", "create_admin"]
|
||||||
|
},
|
||||||
|
"uci": ["gitea"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
package/secubox/secubox-app-gitea/Makefile
Normal file
80
package/secubox/secubox-app-gitea/Makefile
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
#
|
||||||
|
# SecuBox Gitea App - Self-hosted Git platform
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-gitea
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_ARCH:=all
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-gitea
|
||||||
|
SECTION:=utils
|
||||||
|
CATEGORY:=Utilities
|
||||||
|
PKGARCH:=all
|
||||||
|
SUBMENU:=SecuBox Apps
|
||||||
|
TITLE:=SecuBox Gitea Platform
|
||||||
|
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl +tar +lxc +lxc-common +git
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-gitea/description
|
||||||
|
Gitea Git Platform - Self-hosted lightweight Git service
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Run Gitea in LXC container
|
||||||
|
- Git HTTP and SSH support
|
||||||
|
- Repository management
|
||||||
|
- User management with web UI
|
||||||
|
- SQLite database (embedded)
|
||||||
|
- Backup and restore
|
||||||
|
|
||||||
|
Runs in LXC container with Alpine Linux.
|
||||||
|
Configure in /etc/config/gitea.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-gitea/conffiles
|
||||||
|
/etc/config/gitea
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-gitea/install
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./files/etc/config/gitea $(1)/etc/config/gitea
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./files/etc/init.d/gitea $(1)/etc/init.d/gitea
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/giteactl $(1)/usr/sbin/giteactl
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-gitea/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
echo ""
|
||||||
|
echo "Gitea Platform installed."
|
||||||
|
echo ""
|
||||||
|
echo "To install and start Gitea:"
|
||||||
|
echo " giteactl install"
|
||||||
|
echo " /etc/init.d/gitea start"
|
||||||
|
echo ""
|
||||||
|
echo "Web interface: http://<router-ip>:3000"
|
||||||
|
echo "SSH Git access: ssh://git@<router-ip>:2222"
|
||||||
|
echo ""
|
||||||
|
echo "Create admin user: giteactl admin create-user --username admin --password secret --email admin@localhost"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-gitea))
|
||||||
23
package/secubox/secubox-app-gitea/files/etc/config/gitea
Normal file
23
package/secubox/secubox-app-gitea/files/etc/config/gitea
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
config gitea 'main'
|
||||||
|
option enabled '0'
|
||||||
|
option http_port '3000'
|
||||||
|
option ssh_port '2222'
|
||||||
|
option http_host '0.0.0.0'
|
||||||
|
option data_path '/srv/gitea'
|
||||||
|
option memory_limit '512M'
|
||||||
|
option app_name 'SecuBox Git'
|
||||||
|
option domain 'git.local'
|
||||||
|
|
||||||
|
config server 'server'
|
||||||
|
option protocol 'http'
|
||||||
|
option disable_registration 'false'
|
||||||
|
option require_signin 'false'
|
||||||
|
option landing_page 'explore'
|
||||||
|
|
||||||
|
config database 'database'
|
||||||
|
option type 'sqlite3'
|
||||||
|
option path '/data/gitea.db'
|
||||||
|
|
||||||
|
config admin 'admin'
|
||||||
|
option username 'admin'
|
||||||
|
option email 'admin@localhost'
|
||||||
45
package/secubox/secubox-app-gitea/files/etc/init.d/gitea
Normal file
45
package/secubox/secubox-app-gitea/files/etc/init.d/gitea
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
# SecuBox Gitea Platform - Self-hosted Git service
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
|
||||||
|
START=95
|
||||||
|
STOP=10
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
PROG=/usr/sbin/giteactl
|
||||||
|
CONFIG=gitea
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
config_load "$CONFIG"
|
||||||
|
config_get enabled main enabled '0'
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || {
|
||||||
|
echo "Gitea is disabled. Enable with: uci set gitea.main.enabled=1"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command "$PROG" service-run
|
||||||
|
procd_set_param respawn 3600 5 5
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
"$PROG" service-stop
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "$CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
"$PROG" status
|
||||||
|
}
|
||||||
727
package/secubox/secubox-app-gitea/files/usr/sbin/giteactl
Normal file
727
package/secubox/secubox-app-gitea/files/usr/sbin/giteactl
Normal file
@ -0,0 +1,727 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SecuBox Gitea Platform Controller
|
||||||
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
#
|
||||||
|
# Manages Gitea in LXC container
|
||||||
|
|
||||||
|
CONFIG="gitea"
|
||||||
|
LXC_NAME="gitea"
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
LXC_PATH="/srv/lxc"
|
||||||
|
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
||||||
|
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
||||||
|
DATA_PATH="/srv/gitea"
|
||||||
|
BACKUP_PATH="/srv/gitea/backups"
|
||||||
|
GITEA_VERSION="1.22.6"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_info() { echo "[INFO] $*"; logger -t gitea "$*"; }
|
||||||
|
log_error() { echo "[ERROR] $*" >&2; logger -t gitea -p err "$*"; }
|
||||||
|
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
require_root() {
|
||||||
|
[ "$(id -u)" -eq 0 ] || {
|
||||||
|
log_error "This command requires root privileges"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
||||||
|
|
||||||
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||||
|
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
load_config() {
|
||||||
|
http_port="$(uci_get main.http_port)" || http_port="3000"
|
||||||
|
ssh_port="$(uci_get main.ssh_port)" || ssh_port="2222"
|
||||||
|
http_host="$(uci_get main.http_host)" || http_host="0.0.0.0"
|
||||||
|
data_path="$(uci_get main.data_path)" || data_path="/srv/gitea"
|
||||||
|
memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M"
|
||||||
|
app_name="$(uci_get main.app_name)" || app_name="SecuBox Git"
|
||||||
|
domain="$(uci_get main.domain)" || domain="git.local"
|
||||||
|
|
||||||
|
# Server settings
|
||||||
|
protocol="$(uci_get server.protocol)" || protocol="http"
|
||||||
|
disable_registration="$(uci_get server.disable_registration)" || disable_registration="false"
|
||||||
|
require_signin="$(uci_get server.require_signin)" || require_signin="false"
|
||||||
|
landing_page="$(uci_get server.landing_page)" || landing_page="explore"
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
db_type="$(uci_get database.type)" || db_type="sqlite3"
|
||||||
|
db_path="$(uci_get database.path)" || db_path="/data/gitea.db"
|
||||||
|
|
||||||
|
DATA_PATH="$data_path"
|
||||||
|
BACKUP_PATH="$data_path/backups"
|
||||||
|
|
||||||
|
ensure_dir "$data_path"
|
||||||
|
ensure_dir "$data_path/git"
|
||||||
|
ensure_dir "$data_path/custom"
|
||||||
|
ensure_dir "$data_path/custom/conf"
|
||||||
|
ensure_dir "$BACKUP_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
SecuBox Gitea Platform Controller
|
||||||
|
|
||||||
|
Usage: $(basename $0) <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
install Download Alpine rootfs and setup LXC container
|
||||||
|
uninstall Remove container (preserves repositories)
|
||||||
|
update Update Gitea binary to latest version
|
||||||
|
start Start Gitea service (via init)
|
||||||
|
stop Stop Gitea service (via init)
|
||||||
|
restart Restart Gitea service
|
||||||
|
status Show service status (JSON format)
|
||||||
|
logs Show container logs
|
||||||
|
shell Open shell in container
|
||||||
|
|
||||||
|
backup Create backup of repos and database
|
||||||
|
restore <file> Restore from backup
|
||||||
|
|
||||||
|
admin create-user Create admin user
|
||||||
|
--username <name>
|
||||||
|
--password <pass>
|
||||||
|
--email <email>
|
||||||
|
|
||||||
|
service-run Start service (used by init)
|
||||||
|
service-stop Stop service (used by init)
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
/etc/config/gitea
|
||||||
|
|
||||||
|
Data directory:
|
||||||
|
/srv/gitea
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
lxc_check_prereqs() {
|
||||||
|
if ! has_lxc; then
|
||||||
|
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect architecture for Gitea download
|
||||||
|
get_gitea_arch() {
|
||||||
|
local arch=$(uname -m)
|
||||||
|
case "$arch" in
|
||||||
|
x86_64) echo "linux-amd64" ;;
|
||||||
|
aarch64) echo "linux-arm64" ;;
|
||||||
|
armv7l) echo "linux-arm-6" ;;
|
||||||
|
*) log_error "Unsupported architecture: $arch"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create LXC rootfs from Alpine
|
||||||
|
lxc_create_rootfs() {
|
||||||
|
local rootfs="$LXC_ROOTFS"
|
||||||
|
local arch=$(uname -m)
|
||||||
|
|
||||||
|
log_info "Creating Alpine rootfs for Gitea..."
|
||||||
|
|
||||||
|
ensure_dir "$rootfs"
|
||||||
|
|
||||||
|
# Use Alpine mini rootfs
|
||||||
|
local alpine_version="3.21"
|
||||||
|
case "$arch" in
|
||||||
|
x86_64) alpine_arch="x86_64" ;;
|
||||||
|
aarch64) alpine_arch="aarch64" ;;
|
||||||
|
armv7l) alpine_arch="armv7" ;;
|
||||||
|
*) log_error "Unsupported architecture: $arch"; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz"
|
||||||
|
local tmpfile="/tmp/alpine-rootfs.tar.gz"
|
||||||
|
|
||||||
|
log_info "Downloading Alpine ${alpine_version} rootfs..."
|
||||||
|
wget -q -O "$tmpfile" "$alpine_url" || {
|
||||||
|
log_error "Failed to download Alpine rootfs"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info "Extracting rootfs..."
|
||||||
|
tar -xzf "$tmpfile" -C "$rootfs" || {
|
||||||
|
log_error "Failed to extract rootfs"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
|
||||||
|
# Setup resolv.conf
|
||||||
|
cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \
|
||||||
|
echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf"
|
||||||
|
|
||||||
|
log_info "Rootfs created successfully"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download and install Gitea binary
|
||||||
|
install_gitea_binary() {
|
||||||
|
local rootfs="$LXC_ROOTFS"
|
||||||
|
local gitea_arch=$(get_gitea_arch)
|
||||||
|
|
||||||
|
[ -z "$gitea_arch" ] && return 1
|
||||||
|
|
||||||
|
log_info "Downloading Gitea ${GITEA_VERSION}..."
|
||||||
|
local gitea_url="https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-${gitea_arch}"
|
||||||
|
|
||||||
|
ensure_dir "$rootfs/usr/local/bin"
|
||||||
|
|
||||||
|
wget -q -O "$rootfs/usr/local/bin/gitea" "$gitea_url" || {
|
||||||
|
log_error "Failed to download Gitea binary"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod +x "$rootfs/usr/local/bin/gitea"
|
||||||
|
log_info "Gitea binary installed"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install Alpine packages inside container
|
||||||
|
install_container_packages() {
|
||||||
|
local rootfs="$LXC_ROOTFS"
|
||||||
|
|
||||||
|
log_info "Installing container packages..."
|
||||||
|
|
||||||
|
# Create install script
|
||||||
|
cat > "$rootfs/tmp/install-deps.sh" << 'SCRIPT'
|
||||||
|
#!/bin/sh
|
||||||
|
apk update
|
||||||
|
apk add --no-cache git git-lfs openssh sqlite bash su-exec
|
||||||
|
# Create git user
|
||||||
|
adduser -D -s /bin/bash -h /data git 2>/dev/null || true
|
||||||
|
# Setup SSH directory
|
||||||
|
mkdir -p /data/ssh
|
||||||
|
chmod 700 /data/ssh
|
||||||
|
touch /tmp/.deps-installed
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$rootfs/tmp/install-deps.sh"
|
||||||
|
|
||||||
|
# Run in a temporary container
|
||||||
|
lxc-execute -n "$LXC_NAME" -f "$LXC_CONFIG" -- /tmp/install-deps.sh 2>/dev/null || {
|
||||||
|
# Fallback: run via start/attach
|
||||||
|
lxc-start -n "$LXC_NAME" -f "$LXC_CONFIG" -d
|
||||||
|
sleep 2
|
||||||
|
lxc-attach -n "$LXC_NAME" -- /tmp/install-deps.sh
|
||||||
|
lxc-stop -n "$LXC_NAME" -k 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f "$rootfs/tmp/install-deps.sh"
|
||||||
|
log_info "Container packages installed"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create startup script
|
||||||
|
create_startup_script() {
|
||||||
|
local rootfs="$LXC_ROOTFS"
|
||||||
|
|
||||||
|
cat > "$rootfs/opt/start-gitea.sh" << 'STARTUP'
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export GITEA_WORK_DIR=/data
|
||||||
|
export USER=git
|
||||||
|
|
||||||
|
# Ensure git user exists
|
||||||
|
id -u git >/dev/null 2>&1 || adduser -D -s /bin/bash -h /data git
|
||||||
|
|
||||||
|
# Ensure directories have correct ownership
|
||||||
|
chown -R git:git /data/git 2>/dev/null || true
|
||||||
|
chown -R git:git /data/custom 2>/dev/null || true
|
||||||
|
|
||||||
|
# Generate SSH host keys if needed
|
||||||
|
if [ ! -f /data/ssh/ssh_host_rsa_key ]; then
|
||||||
|
echo "Generating SSH host keys..."
|
||||||
|
mkdir -p /data/ssh
|
||||||
|
ssh-keygen -A
|
||||||
|
mv /etc/ssh/ssh_host_* /data/ssh/ 2>/dev/null || true
|
||||||
|
chown root:root /data/ssh/ssh_host_*
|
||||||
|
chmod 600 /data/ssh/ssh_host_*_key
|
||||||
|
chmod 644 /data/ssh/ssh_host_*_key.pub
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create sshd config for git
|
||||||
|
cat > /data/ssh/sshd_config << 'SSHD'
|
||||||
|
Port ${GITEA_SSH_PORT:-2222}
|
||||||
|
ListenAddress 0.0.0.0
|
||||||
|
HostKey /data/ssh/ssh_host_rsa_key
|
||||||
|
HostKey /data/ssh/ssh_host_ecdsa_key
|
||||||
|
HostKey /data/ssh/ssh_host_ed25519_key
|
||||||
|
PermitRootLogin no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
AuthorizedKeysFile /data/git/.ssh/authorized_keys
|
||||||
|
PasswordAuthentication no
|
||||||
|
ChallengeResponseAuthentication no
|
||||||
|
UsePAM no
|
||||||
|
PrintMotd no
|
||||||
|
AcceptEnv LANG LC_*
|
||||||
|
Subsystem sftp /usr/lib/ssh/sftp-server
|
||||||
|
SSHD
|
||||||
|
|
||||||
|
# Start SSH server for git operations (optional, Gitea has built-in SSH)
|
||||||
|
# /usr/sbin/sshd -f /data/ssh/sshd_config
|
||||||
|
|
||||||
|
# Generate app.ini if not exists
|
||||||
|
if [ ! -f /data/custom/conf/app.ini ]; then
|
||||||
|
mkdir -p /data/custom/conf
|
||||||
|
cat > /data/custom/conf/app.ini << EOF
|
||||||
|
[server]
|
||||||
|
APP_NAME = ${GITEA_APP_NAME:-SecuBox Git}
|
||||||
|
DOMAIN = ${GITEA_DOMAIN:-git.local}
|
||||||
|
HTTP_ADDR = ${GITEA_HTTP_HOST:-0.0.0.0}
|
||||||
|
HTTP_PORT = ${GITEA_HTTP_PORT:-3000}
|
||||||
|
ROOT_URL = http://${GITEA_DOMAIN:-git.local}:${GITEA_HTTP_PORT:-3000}/
|
||||||
|
DISABLE_SSH = false
|
||||||
|
START_SSH_SERVER = true
|
||||||
|
SSH_PORT = ${GITEA_SSH_PORT:-2222}
|
||||||
|
SSH_LISTEN_HOST = 0.0.0.0
|
||||||
|
LFS_START_SERVER = true
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = sqlite3
|
||||||
|
PATH = /data/gitea.db
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
ROOT = /data/git/repositories
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = true
|
||||||
|
SECRET_KEY = $(head -c 32 /dev/urandom | base64 | tr -d '\n')
|
||||||
|
INTERNAL_TOKEN = $(head -c 64 /dev/urandom | base64 | tr -d '\n')
|
||||||
|
|
||||||
|
[service]
|
||||||
|
DISABLE_REGISTRATION = ${GITEA_DISABLE_REGISTRATION:-false}
|
||||||
|
REQUIRE_SIGNIN_VIEW = ${GITEA_REQUIRE_SIGNIN:-false}
|
||||||
|
|
||||||
|
[log]
|
||||||
|
MODE = console
|
||||||
|
LEVEL = Info
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
DEFAULT_THEME = gitea-dark
|
||||||
|
EOF
|
||||||
|
chown git:git /data/custom/conf/app.ini
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Gitea
|
||||||
|
echo "Starting Gitea..."
|
||||||
|
cd /data
|
||||||
|
exec su-exec git /usr/local/bin/gitea web --config /data/custom/conf/app.ini
|
||||||
|
STARTUP
|
||||||
|
chmod +x "$rootfs/opt/start-gitea.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create LXC config
|
||||||
|
lxc_create_config() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
ensure_dir "$(dirname "$LXC_CONFIG")"
|
||||||
|
|
||||||
|
# Convert memory limit to bytes
|
||||||
|
local mem_bytes
|
||||||
|
case "$memory_limit" in
|
||||||
|
*G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;;
|
||||||
|
*M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;;
|
||||||
|
*K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;;
|
||||||
|
*) mem_bytes="$memory_limit" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cat > "$LXC_CONFIG" << EOF
|
||||||
|
# Gitea Platform LXC Configuration
|
||||||
|
lxc.uts.name = $LXC_NAME
|
||||||
|
lxc.rootfs.path = dir:$LXC_ROOTFS
|
||||||
|
lxc.arch = $(uname -m)
|
||||||
|
|
||||||
|
# Network: use host network
|
||||||
|
lxc.net.0.type = none
|
||||||
|
|
||||||
|
# Mount points
|
||||||
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||||
|
lxc.mount.entry = $data_path/git data/git none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = $data_path/custom data/custom none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = $data_path data none bind,create=dir 0 0
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
lxc.environment = GITEA_HTTP_HOST=$http_host
|
||||||
|
lxc.environment = GITEA_HTTP_PORT=$http_port
|
||||||
|
lxc.environment = GITEA_SSH_PORT=$ssh_port
|
||||||
|
lxc.environment = GITEA_APP_NAME=$app_name
|
||||||
|
lxc.environment = GITEA_DOMAIN=$domain
|
||||||
|
lxc.environment = GITEA_DISABLE_REGISTRATION=$disable_registration
|
||||||
|
lxc.environment = GITEA_REQUIRE_SIGNIN=$require_signin
|
||||||
|
|
||||||
|
# Security
|
||||||
|
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
lxc.cgroup.memory.limit_in_bytes = $mem_bytes
|
||||||
|
|
||||||
|
# Init command
|
||||||
|
lxc.init.cmd = /opt/start-gitea.sh
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_info "LXC config created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Container control
|
||||||
|
lxc_running() {
|
||||||
|
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_exists() {
|
||||||
|
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_stop() {
|
||||||
|
if lxc_running; then
|
||||||
|
log_info "Stopping Gitea container..."
|
||||||
|
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_run() {
|
||||||
|
load_config
|
||||||
|
lxc_stop
|
||||||
|
|
||||||
|
if ! lxc_exists; then
|
||||||
|
log_error "Container not installed. Run: giteactl install"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Regenerate config in case settings changed
|
||||||
|
lxc_create_config
|
||||||
|
|
||||||
|
log_info "Starting Gitea container..."
|
||||||
|
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
cmd_install() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
log_info "Installing Gitea Platform..."
|
||||||
|
|
||||||
|
lxc_check_prereqs || exit 1
|
||||||
|
|
||||||
|
# Create container
|
||||||
|
if ! lxc_exists; then
|
||||||
|
lxc_create_rootfs || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Gitea binary
|
||||||
|
install_gitea_binary || exit 1
|
||||||
|
|
||||||
|
# Create startup script
|
||||||
|
create_startup_script
|
||||||
|
|
||||||
|
# Create config
|
||||||
|
lxc_create_config || exit 1
|
||||||
|
|
||||||
|
# Install container packages (do this separately as it needs a running container)
|
||||||
|
# We'll let the startup script handle package installation on first run instead
|
||||||
|
|
||||||
|
# Enable service
|
||||||
|
uci_set main.enabled '1'
|
||||||
|
/etc/init.d/gitea enable 2>/dev/null || true
|
||||||
|
|
||||||
|
log_info "Installation complete!"
|
||||||
|
log_info ""
|
||||||
|
log_info "Start with: /etc/init.d/gitea start"
|
||||||
|
log_info "Web interface: http://<router-ip>:$http_port"
|
||||||
|
log_info "SSH Git access: ssh://git@<router-ip>:$ssh_port"
|
||||||
|
log_info ""
|
||||||
|
log_info "Create admin: giteactl admin create-user --username admin --password secret --email admin@localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_uninstall() {
|
||||||
|
require_root
|
||||||
|
|
||||||
|
log_info "Uninstalling Gitea Platform..."
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
/etc/init.d/gitea stop 2>/dev/null || true
|
||||||
|
/etc/init.d/gitea disable 2>/dev/null || true
|
||||||
|
|
||||||
|
lxc_stop
|
||||||
|
|
||||||
|
# Remove container (keep data)
|
||||||
|
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
|
||||||
|
rm -rf "$LXC_PATH/$LXC_NAME"
|
||||||
|
log_info "Container removed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci_set main.enabled '0'
|
||||||
|
|
||||||
|
log_info "Gitea Platform uninstalled"
|
||||||
|
log_info "Data preserved in: $(uci_get main.data_path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if ! lxc_exists; then
|
||||||
|
log_error "Container not installed. Run: giteactl install"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating Gitea binary..."
|
||||||
|
|
||||||
|
# Download new binary
|
||||||
|
install_gitea_binary || exit 1
|
||||||
|
|
||||||
|
# Restart if running
|
||||||
|
if [ "$(uci_get main.enabled)" = "1" ]; then
|
||||||
|
/etc/init.d/gitea restart
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Update complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
local enabled="$(uci_get main.enabled)"
|
||||||
|
local running="false"
|
||||||
|
local installed="false"
|
||||||
|
local uptime=""
|
||||||
|
|
||||||
|
if lxc_exists; then
|
||||||
|
installed="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if lxc_running; then
|
||||||
|
running="true"
|
||||||
|
uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get LAN IP for URL
|
||||||
|
local lan_ip
|
||||||
|
lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
|
||||||
|
# Count repositories
|
||||||
|
local repo_count=0
|
||||||
|
if [ -d "$data_path/git/repositories" ]; then
|
||||||
|
repo_count=$(find "$data_path/git/repositories" -name "*.git" -type d 2>/dev/null | wc -l)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get disk usage
|
||||||
|
local disk_usage="0"
|
||||||
|
if [ -d "$data_path" ]; then
|
||||||
|
disk_usage=$(du -sh "$data_path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"running": $running,
|
||||||
|
"installed": $installed,
|
||||||
|
"uptime": "$uptime",
|
||||||
|
"http_port": $http_port,
|
||||||
|
"ssh_port": $ssh_port,
|
||||||
|
"data_path": "$data_path",
|
||||||
|
"memory_limit": "$memory_limit",
|
||||||
|
"app_name": "$app_name",
|
||||||
|
"domain": "$domain",
|
||||||
|
"repo_count": $repo_count,
|
||||||
|
"disk_usage": "$disk_usage",
|
||||||
|
"http_url": "http://${lan_ip}:${http_port}",
|
||||||
|
"ssh_url": "ssh://git@${lan_ip}:${ssh_port}",
|
||||||
|
"container_name": "$LXC_NAME",
|
||||||
|
"version": "$GITEA_VERSION"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
local lines="${1:-100}"
|
||||||
|
|
||||||
|
# Check for gitea logs
|
||||||
|
if lxc_running; then
|
||||||
|
log_info "Container logs (last $lines lines):"
|
||||||
|
lxc-attach -n "$LXC_NAME" -- cat /data/log/gitea.log 2>/dev/null | tail -n "$lines" || \
|
||||||
|
echo "No logs available"
|
||||||
|
else
|
||||||
|
echo "Container not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also check install logs
|
||||||
|
for logfile in /var/log/gitea-install.log /var/log/gitea-update.log; do
|
||||||
|
if [ -f "$logfile" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== $logfile ==="
|
||||||
|
tail -n 50 "$logfile"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_shell() {
|
||||||
|
require_root
|
||||||
|
|
||||||
|
if ! lxc_running; then
|
||||||
|
log_error "Container not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc-attach -n "$LXC_NAME" -- /bin/sh
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_backup() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
local backup_file="$BACKUP_PATH/gitea-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
|
||||||
|
log_info "Creating backup..."
|
||||||
|
ensure_dir "$BACKUP_PATH"
|
||||||
|
|
||||||
|
# Stop service for consistent backup
|
||||||
|
local was_running=0
|
||||||
|
if lxc_running; then
|
||||||
|
was_running=1
|
||||||
|
lxc_stop
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
tar -czf "$backup_file" -C "$data_path" \
|
||||||
|
git \
|
||||||
|
custom \
|
||||||
|
gitea.db 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ $was_running -eq 1 ]; then
|
||||||
|
/etc/init.d/gitea start &
|
||||||
|
fi
|
||||||
|
|
||||||
|
local size=$(ls -lh "$backup_file" 2>/dev/null | awk '{print $5}')
|
||||||
|
log_info "Backup created: $backup_file ($size)"
|
||||||
|
echo "$backup_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restore() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
local backup_file="$1"
|
||||||
|
|
||||||
|
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
|
||||||
|
log_error "Usage: giteactl restore <backup-file>"
|
||||||
|
log_error "Available backups:"
|
||||||
|
ls -la "$BACKUP_PATH"/*.tar.gz 2>/dev/null || echo "No backups found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Restoring from: $backup_file"
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
local was_running=0
|
||||||
|
if lxc_running; then
|
||||||
|
was_running=1
|
||||||
|
lxc_stop
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore backup
|
||||||
|
tar -xzf "$backup_file" -C "$data_path"
|
||||||
|
|
||||||
|
if [ $was_running -eq 1 ]; then
|
||||||
|
/etc/init.d/gitea start &
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Restore complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_admin_create_user() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
local username=""
|
||||||
|
local password=""
|
||||||
|
local email=""
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--username) username="$2"; shift 2 ;;
|
||||||
|
--password) password="$2"; shift 2 ;;
|
||||||
|
--email) email="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$username" ] || [ -z "$password" ] || [ -z "$email" ]; then
|
||||||
|
log_error "Usage: giteactl admin create-user --username <name> --password <pass> --email <email>"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! lxc_running; then
|
||||||
|
log_error "Container must be running to create users"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating admin user: $username"
|
||||||
|
|
||||||
|
lxc-attach -n "$LXC_NAME" -- su-exec git /usr/local/bin/gitea admin user create \
|
||||||
|
--username "$username" \
|
||||||
|
--password "$password" \
|
||||||
|
--email "$email" \
|
||||||
|
--admin \
|
||||||
|
--config /data/custom/conf/app.ini
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log_info "Admin user created successfully"
|
||||||
|
else
|
||||||
|
log_error "Failed to create admin user"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_service_run() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
lxc_check_prereqs || exit 1
|
||||||
|
lxc_run
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_service_stop() {
|
||||||
|
require_root
|
||||||
|
lxc_stop
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
case "${1:-}" in
|
||||||
|
install) shift; cmd_install "$@" ;;
|
||||||
|
uninstall) shift; cmd_uninstall "$@" ;;
|
||||||
|
update) shift; cmd_update "$@" ;;
|
||||||
|
start) /etc/init.d/gitea start ;;
|
||||||
|
stop) /etc/init.d/gitea stop ;;
|
||||||
|
restart) /etc/init.d/gitea restart ;;
|
||||||
|
status) shift; cmd_status "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
shell) shift; cmd_shell "$@" ;;
|
||||||
|
backup) shift; cmd_backup "$@" ;;
|
||||||
|
restore) shift; cmd_restore "$@" ;;
|
||||||
|
admin)
|
||||||
|
shift
|
||||||
|
case "${1:-}" in
|
||||||
|
create-user) shift; cmd_admin_create_user "$@" ;;
|
||||||
|
*) echo "Usage: giteactl admin create-user --username <name> --password <pass> --email <email>"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
service-run) shift; cmd_service_run "$@" ;;
|
||||||
|
service-stop) shift; cmd_service_stop "$@" ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue
Block a user