Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b5ad11185 | |||
| a28d7a6954 | |||
| 009506f915 | |||
| e84d77f248 | |||
| f52e035d8a | |||
| 2e1cd86e99 | |||
| 927a04f24a | |||
| 600b398a5a | |||
| 97a8c41fc0 | |||
| b2cc357dca | |||
| fc8fbc976f |
@@ -1,3 +1,4 @@
|
|||||||
checker/
|
checker/
|
||||||
client/.env
|
client/.env
|
||||||
client/coverage/
|
client/coverage/
|
||||||
|
backups/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||||
|
|
||||||
  [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
  [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||||
|
|
||||||
# 3DP-MANAGER
|
# 3DP-MANAGER
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
**Поддержать проект**
|
**Поддержать проект**
|
||||||
|
|
||||||
- Банковским переводом:
|
- Банковским переводом:
|
||||||
- :credit_card: Карта МИР: `2204320436318077`
|
- :credit_card: Карта МИР: [finance.ozon.ru](https://finance.ozon.ru/apps/sbp/ozonbankpay/019e46e5-c08d-734a-a056-6b340969760d)
|
||||||
- :credit_card: Карта MasterCard: `5395452209474530`
|
- :credit_card: Карта MasterCard: `5395452209474530`
|
||||||
- На электронный кошелек:
|
- На электронный кошелек:
|
||||||
- :moneybag: ЮМоney: `4100116897060652`
|
- :moneybag: ЮМоney: `4100116897060652`
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ interface InboundConfigUI {
|
|||||||
name?: string;
|
name?: string;
|
||||||
certificateFile?: string;
|
certificateFile?: string;
|
||||||
keyFile?: string;
|
keyFile?: string;
|
||||||
|
routingProfile?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Domain {
|
interface Domain {
|
||||||
@@ -106,6 +107,7 @@ const CONNECTION_OPTIONS = [
|
|||||||
'shadowsocks-tcp',
|
'shadowsocks-tcp',
|
||||||
'trojan-tcp-reality',
|
'trojan-tcp-reality',
|
||||||
'custom',
|
'custom',
|
||||||
|
'happ-routing',
|
||||||
];
|
];
|
||||||
|
|
||||||
const generateId = () => Math.random().toString(36).substring(7);
|
const generateId = () => Math.random().toString(36).substring(7);
|
||||||
@@ -221,17 +223,30 @@ export default function SubscriptionsPage() {
|
|||||||
|
|
||||||
const createInbound = (type = 'vless-tcp-reality'): InboundConfigUI => {
|
const createInbound = (type = 'vless-tcp-reality'): InboundConfigUI => {
|
||||||
const nodeId = getDefaultNodeId();
|
const nodeId = getDefaultNodeId();
|
||||||
|
|
||||||
|
// HAPP ROUTING - специальная обработка
|
||||||
|
if (type === 'happ-routing') {
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
type,
|
||||||
|
port: '0',
|
||||||
|
sni: '',
|
||||||
|
link: '',
|
||||||
|
routingProfile: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const certDefaults = type === 'hysteria2-udp' ? getHysteriaCertDefaults(nodeId) : {};
|
const certDefaults = type === 'hysteria2-udp' ? getHysteriaCertDefaults(nodeId) : {};
|
||||||
return {
|
return {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
type,
|
type,
|
||||||
port: 'random',
|
port: 'random',
|
||||||
sni: type === 'hysteria2-udp' ? '' : 'random',
|
sni: type === 'hysteria2-udp' ? '' : 'random',
|
||||||
link: '',
|
link: '',
|
||||||
nodeId,
|
nodeId,
|
||||||
flag: getNodeFlag(nodeId),
|
flag: getNodeFlag(nodeId),
|
||||||
name: '',
|
name: '',
|
||||||
...certDefaults,
|
...certDefaults,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -280,6 +295,7 @@ export default function SubscriptionsPage() {
|
|||||||
relayServerId: item.relayServerId ? item.relayServerId.toString() : '',
|
relayServerId: item.relayServerId ? item.relayServerId.toString() : '',
|
||||||
flag: item.flag || getNodeFlag(nodeId),
|
flag: item.flag || getNodeFlag(nodeId),
|
||||||
name: item.name || '',
|
name: item.name || '',
|
||||||
|
routingProfile: item.routingProfile || '',
|
||||||
certificateFile:
|
certificateFile:
|
||||||
item.type === 'hysteria2-udp'
|
item.type === 'hysteria2-udp'
|
||||||
? item.certificateFile || certDefaults.certificateFile
|
? item.certificateFile || certDefaults.certificateFile
|
||||||
@@ -323,7 +339,7 @@ export default function SubscriptionsPage() {
|
|||||||
next.keyFile = next.keyFile || defaults.keyFile;
|
next.keyFile = next.keyFile || defaults.keyFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'type' && value !== 'custom' && !next.nodeId) {
|
if (field === 'type' && value !== 'custom' && value !== 'happ-routing' && !next.nodeId) {
|
||||||
next.nodeId = getDefaultNodeId();
|
next.nodeId = getDefaultNodeId();
|
||||||
next.flag = getNodeFlag(next.nodeId);
|
next.flag = getNodeFlag(next.nodeId);
|
||||||
}
|
}
|
||||||
@@ -363,7 +379,7 @@ export default function SubscriptionsPage() {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const nextPortErrors = inbounds.reduce<Record<string, string>>((acc, inbound) => {
|
const nextPortErrors = inbounds.reduce<Record<string, string>>((acc, inbound) => {
|
||||||
if (inbound.type !== 'custom' && !isValidPort(inbound.port)) {
|
if (inbound.type !== 'custom' && inbound.type !== 'happ-routing' && !isValidPort(inbound.port)) {
|
||||||
acc[inbound.id] = 'Порт: число 1-65535 или random';
|
acc[inbound.id] = 'Порт: число 1-65535 или random';
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -385,29 +401,36 @@ export default function SubscriptionsPage() {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
inboundsConfig: inbounds.map((inbound) =>
|
inboundsConfig: inbounds.map((inbound) => {
|
||||||
inbound.type === 'custom'
|
if (inbound.type === 'custom') {
|
||||||
? { type: inbound.type, link: inbound.link }
|
return { type: inbound.type, link: inbound.link };
|
||||||
: {
|
}
|
||||||
type: inbound.type,
|
if (inbound.type === 'happ-routing') {
|
||||||
port: inbound.port === 'random' ? 'random' : parseInt(inbound.port, 10),
|
return {
|
||||||
sni: inbound.type === 'hysteria2-udp' ? undefined : inbound.sni,
|
type: inbound.type,
|
||||||
nodeId: inbound.nodeId || undefined,
|
routingProfile: inbound.routingProfile,
|
||||||
relayServerId: inbound.relayServerId
|
};
|
||||||
? parseInt(inbound.relayServerId, 10)
|
}
|
||||||
: undefined,
|
return {
|
||||||
flag: inbound.flag || getNodeFlag(inbound.nodeId) || undefined,
|
type: inbound.type,
|
||||||
name: inbound.name?.trim() || undefined,
|
port: inbound.port === 'random' ? 'random' : parseInt(inbound.port, 10),
|
||||||
certificateFile:
|
sni: inbound.type === 'hysteria2-udp' ? undefined : inbound.sni,
|
||||||
inbound.type === 'hysteria2-udp'
|
nodeId: inbound.nodeId || undefined,
|
||||||
? inbound.certificateFile?.trim() || undefined
|
relayServerId: inbound.relayServerId
|
||||||
: undefined,
|
? parseInt(inbound.relayServerId, 10)
|
||||||
keyFile:
|
: undefined,
|
||||||
inbound.type === 'hysteria2-udp'
|
flag: inbound.flag || getNodeFlag(inbound.nodeId) || undefined,
|
||||||
? inbound.keyFile?.trim() || undefined
|
name: inbound.name?.trim() || undefined,
|
||||||
: undefined,
|
certificateFile:
|
||||||
},
|
inbound.type === 'hysteria2-udp'
|
||||||
),
|
? inbound.certificateFile?.trim() || undefined
|
||||||
|
: undefined,
|
||||||
|
keyFile:
|
||||||
|
inbound.type === 'hysteria2-udp'
|
||||||
|
? inbound.keyFile?.trim() || undefined
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -735,6 +758,18 @@ export default function SubscriptionsPage() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
{inbound.type === 'custom' ? (
|
{inbound.type === 'custom' ? (
|
||||||
<TextField size="small" label="Ссылка на подключение" placeholder="vless://..." value={inbound.link || ''} onChange={(e) => handleInboundChange(inbound.id, 'link', e.target.value)} sx={{ width: 460, flexShrink: 0 }} />
|
<TextField size="small" label="Ссылка на подключение" placeholder="vless://..." value={inbound.link || ''} onChange={(e) => handleInboundChange(inbound.id, 'link', e.target.value)} sx={{ width: 460, flexShrink: 0 }} />
|
||||||
|
) : inbound.type === 'happ-routing' ? (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label="Routing профиль (JSON или base64)"
|
||||||
|
placeholder='{"Name":"MyProfile","GlobalProxy":"true","DirectSites":["geosite:cn"]}'
|
||||||
|
value={inbound.routingProfile || ''}
|
||||||
|
onChange={(e) => handleInboundChange(inbound.id, 'routingProfile', e.target.value)}
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
sx={{ width: 460, flexShrink: 0 }}
|
||||||
|
helperText="Вставьте JSON профиль или готовую ссылку happ://routing/..."
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FormControl size="small" sx={{ width: 170, flexShrink: 0 }}>
|
<FormControl size="small" sx={{ width: 170, flexShrink: 0 }}>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ services:
|
|||||||
DB_USERNAME: ${POSTGRES_USER}
|
DB_USERNAME: ${POSTGRES_USER}
|
||||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
DB_NAME: ${POSTGRES_DB}
|
DB_NAME: ${POSTGRES_DB}
|
||||||
|
DB_SYNCHRONIZE: "true"
|
||||||
|
DB_MIGRATIONS_RUN: "false"
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
ADMIN_LOGIN: ${ADMIN_LOGIN}
|
ADMIN_LOGIN: ${ADMIN_LOGIN}
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||||
|
|||||||
+6
-2
@@ -5,8 +5,8 @@ set -euo pipefail
|
|||||||
# КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ
|
# КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ
|
||||||
#################################
|
#################################
|
||||||
PROJECT_DIR="/opt/3dp-manager"
|
PROJECT_DIR="/opt/3dp-manager"
|
||||||
DOCKER_USER="denpiligrim"
|
DOCKER_USER="zangetsuka"
|
||||||
DOCKER_TAG="main"
|
DOCKER_TAG="feature-happ-routing"
|
||||||
IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}"
|
IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}"
|
||||||
IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}"
|
IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}"
|
||||||
|
|
||||||
@@ -469,6 +469,8 @@ services:
|
|||||||
DB_USERNAME: admin
|
DB_USERNAME: admin
|
||||||
DB_PASSWORD: ${DB_PASS}
|
DB_PASSWORD: ${DB_PASS}
|
||||||
DB_NAME: 3dp_manager
|
DB_NAME: 3dp_manager
|
||||||
|
DB_SYNCHRONIZE: "true"
|
||||||
|
DB_MIGRATIONS_RUN: "false"
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
ADMIN_LOGIN: ${ADMIN_USER}
|
ADMIN_LOGIN: ${ADMIN_USER}
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASS}
|
ADMIN_PASSWORD: ${ADMIN_PASS}
|
||||||
@@ -580,6 +582,8 @@ services:
|
|||||||
DB_USERNAME: admin
|
DB_USERNAME: admin
|
||||||
DB_PASSWORD: ${DB_PASS}
|
DB_PASSWORD: ${DB_PASS}
|
||||||
DB_NAME: 3dp_manager
|
DB_NAME: 3dp_manager
|
||||||
|
DB_SYNCHRONIZE: "true"
|
||||||
|
DB_MIGRATIONS_RUN: "false"
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
ADMIN_LOGIN: ${ADMIN_USER}
|
ADMIN_LOGIN: ${ADMIN_USER}
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASS}
|
ADMIN_PASSWORD: ${ADMIN_PASS}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import { Tunnel } from './tunnels/entities/tunnel.entity';
|
|||||||
import { SessionModule } from './session/session.module';
|
import { SessionModule } from './session/session.module';
|
||||||
import { Node } from './nodes/entities/node.entity';
|
import { Node } from './nodes/entities/node.entity';
|
||||||
import { NodesModule } from './nodes/nodes.module';
|
import { NodesModule } from './nodes/nodes.module';
|
||||||
|
import { AddNodesAndNodeRelations1765960000000 } from './migrations/1765960000000-add-nodes-and-node-relations';
|
||||||
|
import { AddNodeIpFlagAndInboundLabels1770000000000 } from './migrations/1770000000000-add-node-ip-flag-and-inbound-labels';
|
||||||
|
import { AddNodeDomain1770000000001 } from './migrations/1770000000001-add-node-domain';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -44,7 +47,13 @@ import { NodesModule } from './nodes/nodes.module';
|
|||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
entities: [Setting, Domain, Subscription, Inbound, Tunnel, Node],
|
entities: [Setting, Domain, Subscription, Inbound, Tunnel, Node],
|
||||||
synchronize: true,
|
migrations: [
|
||||||
|
AddNodesAndNodeRelations1765960000000,
|
||||||
|
AddNodeIpFlagAndInboundLabels1770000000000,
|
||||||
|
AddNodeDomain1770000000001,
|
||||||
|
],
|
||||||
|
synchronize: process.env.DB_SYNCHRONIZE !== 'false',
|
||||||
|
migrationsRun: process.env.DB_MIGRATIONS_RUN === 'true',
|
||||||
}),
|
}),
|
||||||
SessionModule,
|
SessionModule,
|
||||||
XuiModule,
|
XuiModule,
|
||||||
|
|||||||
@@ -469,7 +469,6 @@ export class InboundBuilderService {
|
|||||||
content: '',
|
content: '',
|
||||||
dir: '',
|
dir: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
insecure: true,
|
|
||||||
rewriteHost: false,
|
rewriteHost: false,
|
||||||
statusCode: 0,
|
statusCode: 0,
|
||||||
type: 'proxy',
|
type: 'proxy',
|
||||||
@@ -703,7 +702,6 @@ export class InboundBuilderService {
|
|||||||
const auth = settings.clients?.[0]?.auth || settings.clients?.[0]?.password || password;
|
const auth = settings.clients?.[0]?.auth || settings.clients?.[0]?.password || password;
|
||||||
const finalmask = stream.finalmask?.udp?.[0];
|
const finalmask = stream.finalmask?.udp?.[0];
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('insecure', '1');
|
|
||||||
params.set('security', 'tls');
|
params.set('security', 'tls');
|
||||||
params.set('fp', 'chrome');
|
params.set('fp', 'chrome');
|
||||||
params.set('alpn', 'h3');
|
params.set('alpn', 'h3');
|
||||||
@@ -769,7 +767,6 @@ export class InboundBuilderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('insecure', '1');
|
|
||||||
params.set('security', 'tls');
|
params.set('security', 'tls');
|
||||||
params.set('fp', 'chrome');
|
params.set('fp', 'chrome');
|
||||||
params.set('alpn', 'h3');
|
params.set('alpn', 'h3');
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddNodeDomain1770000000001 implements MigrationInterface {
|
||||||
|
name = 'AddNodeDomain1770000000001';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "node"
|
||||||
|
ADD COLUMN IF NOT EXISTS "domain" character varying
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "node" DROP COLUMN IF EXISTS "domain"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -257,6 +257,7 @@ export class RotationService implements OnModuleInit {
|
|||||||
sni = config.sni === 'random' ? this.pickDomain(domains) : config.sni;
|
sni = config.sni === 'random' ? this.pickDomain(domains) : config.sni;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// === 2. Обработка Hysteria2 ===
|
// === 2. Обработка Hysteria2 ===
|
||||||
if (type === 'hysteria2-udp') {
|
if (type === 'hysteria2-udp') {
|
||||||
let port = 0;
|
let port = 0;
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ export class InboundConfigDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
keyFile?: string;
|
keyFile?: string;
|
||||||
|
|
||||||
|
// ⬇️ НОВОЕ ПОЛЕ ДЛЯ HAPP ROUTING ⬇️
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
routingProfile?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateSubscriptionDto {
|
export class CreateSubscriptionDto {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class Subscription {
|
|||||||
name?: string;
|
name?: string;
|
||||||
certificateFile?: string;
|
certificateFile?: string;
|
||||||
keyFile?: string;
|
keyFile?: string;
|
||||||
|
routingProfile?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
|
|||||||
@@ -38,9 +38,8 @@ check_containers_running() {
|
|||||||
# Формат: NAME\tSTATUS (например: "3dp-postgres\tUp 2 days" или "3dp-postgres\tError")
|
# Формат: NAME\tSTATUS (например: "3dp-postgres\tUp 2 days" или "3dp-postgres\tError")
|
||||||
while IFS=$'\t' read -r container_name status; do
|
while IFS=$'\t' read -r container_name status; do
|
||||||
if [ -n "$container_name" ] && [ -n "$status" ]; then
|
if [ -n "$container_name" ] && [ -n "$status" ]; then
|
||||||
# Проверяем, что статус содержит Up/running/healthy/restarting
|
# Restarting означает, что контейнер не смог стабильно запуститься.
|
||||||
# Up, Up 2 days, Up Less than a second, (healthy), running, restarting
|
if ! echo "$status" | grep -qiE "^up|running|healthy"; then
|
||||||
if ! echo "$status" | grep -qiE "^up|running|healthy|restarting"; then
|
|
||||||
failed=1
|
failed=1
|
||||||
warn "Контейнер $container_name в статусе: $status"
|
warn "Контейнер $container_name в статусе: $status"
|
||||||
fi
|
fi
|
||||||
@@ -284,6 +283,52 @@ remove_hysteria_mount() {
|
|||||||
mv "$tmp_file" "$compose_file"
|
mv "$tmp_file" "$compose_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_safe_database_mode() {
|
||||||
|
local compose_file="$1"
|
||||||
|
[[ -f "$compose_file" ]] || return 0
|
||||||
|
|
||||||
|
local tmp_file
|
||||||
|
tmp_file="$(mktemp)"
|
||||||
|
|
||||||
|
awk '
|
||||||
|
/^[[:space:]]+DB_SYNCHRONIZE:/ { next }
|
||||||
|
/^[[:space:]]+DB_MIGRATIONS_RUN:/ { next }
|
||||||
|
{
|
||||||
|
print $0
|
||||||
|
if ($0 ~ /^[[:space:]]+DB_NAME:/) {
|
||||||
|
match($0, /^[[:space:]]+/)
|
||||||
|
indent = substr($0, RSTART, RLENGTH)
|
||||||
|
print indent "DB_SYNCHRONIZE: \"false\""
|
||||||
|
print indent "DB_MIGRATIONS_RUN: \"true\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$compose_file" > "$tmp_file"
|
||||||
|
|
||||||
|
mv "$tmp_file" "$compose_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_node_count() {
|
||||||
|
docker exec 3dp-postgres sh -c '
|
||||||
|
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atqc "
|
||||||
|
SELECT CASE
|
||||||
|
WHEN to_regclass('\''public.node'\'') IS NULL THEN 0
|
||||||
|
ELSE (SELECT count(*) FROM node)
|
||||||
|
END
|
||||||
|
"
|
||||||
|
' 2>/dev/null | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_database() {
|
||||||
|
local backup_dir="$PROJECT_DIR/backups"
|
||||||
|
BACKUP_FILE="$backup_dir/pre-update-$(date +%Y%m%d-%H%M%S).sql.gz"
|
||||||
|
|
||||||
|
mkdir -p "$backup_dir"
|
||||||
|
log "Создание резервной копии базы данных: $BACKUP_FILE"
|
||||||
|
docker exec 3dp-postgres sh -c \
|
||||||
|
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' | gzip > "$BACKUP_FILE"
|
||||||
|
chmod 600 "$BACKUP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
need_root() {
|
need_root() {
|
||||||
[[ $EUID -eq 0 ]] || die "Запускать только от root"
|
[[ $EUID -eq 0 ]] || die "Запускать только от root"
|
||||||
}
|
}
|
||||||
@@ -314,7 +359,11 @@ log "Compose команда: ${COMPOSE_CMD[*]}"
|
|||||||
#################################
|
#################################
|
||||||
# CHECK AND FIX CREDENTIALS
|
# CHECK AND FIX CREDENTIALS
|
||||||
#################################
|
#################################
|
||||||
check_and_fix_credentials || true
|
# Обновление не должно менять пароль уже инициализированной PostgreSQL:
|
||||||
|
# изменение только .env делает существующую базу недоступной.
|
||||||
|
if [[ ! -f ".env" ]]; then
|
||||||
|
die "Файл .env не найден. Обновление остановлено, чтобы не потерять доступ к существующей базе данных"
|
||||||
|
fi
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
# FIX NGINX CONFIG
|
# FIX NGINX CONFIG
|
||||||
@@ -322,6 +371,16 @@ check_and_fix_credentials || true
|
|||||||
ensure_nginx_api_timeouts "$PROJECT_DIR/client/nginx-client.conf"
|
ensure_nginx_api_timeouts "$PROJECT_DIR/client/nginx-client.conf"
|
||||||
ensure_bus_location "$PROJECT_DIR/client/nginx-client.conf"
|
ensure_bus_location "$PROJECT_DIR/client/nginx-client.conf"
|
||||||
remove_hysteria_mount "$PROJECT_DIR/docker-compose.yml"
|
remove_hysteria_mount "$PROJECT_DIR/docker-compose.yml"
|
||||||
|
ensure_safe_database_mode "$PROJECT_DIR/docker-compose.yml"
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# BACKUP DATABASE
|
||||||
|
#################################
|
||||||
|
node_count_before="$(get_node_count)"
|
||||||
|
[[ "$node_count_before" =~ ^[0-9]+$ ]] || die "Не удалось проверить количество нод перед обновлением"
|
||||||
|
backup_database
|
||||||
|
backup_file="$BACKUP_FILE"
|
||||||
|
[[ -s "$backup_file" ]] || die "Не удалось создать резервную копию базы данных"
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
# REBUILD BACKEND
|
# REBUILD BACKEND
|
||||||
@@ -341,11 +400,18 @@ log "Пересоздание контейнеров..."
|
|||||||
|
|
||||||
# Проверка: все ли контейнеры запустились
|
# Проверка: все ли контейнеры запустились
|
||||||
if ! check_containers_running 60; then
|
if ! check_containers_running 60; then
|
||||||
error "Не удалось запустить контейнеры. Логи:"
|
warn "Не удалось запустить контейнеры. Логи:"
|
||||||
"${COMPOSE_CMD[@]}" logs --tail=50
|
"${COMPOSE_CMD[@]}" logs --tail=50
|
||||||
die "Обновление прервано из-за ошибки запуска контейнеров"
|
die "Обновление прервано из-за ошибки запуска контейнеров"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
node_count_after="$(get_node_count)"
|
||||||
|
[[ "$node_count_after" =~ ^[0-9]+$ ]] || die "Не удалось проверить количество нод после обновления. Резервная копия: $backup_file"
|
||||||
|
if (( node_count_after < node_count_before )); then
|
||||||
|
"${COMPOSE_CMD[@]}" stop backend || true
|
||||||
|
die "Количество нод уменьшилось с $node_count_before до $node_count_after. Обновление остановлено, резервная копия: $backup_file"
|
||||||
|
fi
|
||||||
|
|
||||||
log "Очистка старых Docker-образов (освобождение места)..."
|
log "Очистка старых Docker-образов (освобождение места)..."
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user