Compare commits

...

5 Commits

Author SHA1 Message Date
yyhuni
f287f18134 更新锁定镜像 2026-01-03 18:33:25 +08:00
yyhuni
de27230b7a 更新构建ci 2026-01-03 18:28:57 +08:00
github-actions[bot]
15a6295189 chore: bump version to v1.3.8-dev 2026-01-03 10:24:17 +00:00
yyhuni
674acdac66 refactor(asset): move database extension initialization to migrations
- Remove pg_trgm and pg_ivm extension setup from AssetConfig.ready() method
- Move extension creation to migration 0002 using RunSQL operations
- Add pg_trgm extension creation for text search index support
- Add pg_ivm extension creation for IMMV incremental maintenance
- Generate unique cursor names in search_service to prevent concurrent request conflicts
- Add @transaction.non_atomic_requests decorator to export view for server-side cursor compatibility
- Simplify app initialization by delegating extension setup to database migrations
- Improve thread safety and concurrency handling for streaming exports
2026-01-03 18:20:27 +08:00
github-actions[bot]
c59152bedf chore: bump version to v1.3.7-dev 2026-01-03 09:56:39 +00:00
8 changed files with 160 additions and 150 deletions

View File

@@ -19,7 +19,8 @@ permissions:
contents: write contents: write
jobs: jobs:
build: # AMD64 构建(原生 x64 runner
build-amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@@ -27,43 +28,30 @@ jobs:
- image: xingrin-server - image: xingrin-server
dockerfile: docker/server/Dockerfile dockerfile: docker/server/Dockerfile
context: . context: .
platforms: linux/amd64,linux/arm64
- image: xingrin-frontend - image: xingrin-frontend
dockerfile: docker/frontend/Dockerfile dockerfile: docker/frontend/Dockerfile
context: . context: .
platforms: linux/amd64 # ARM64 构建时 Next.js 在 QEMU 下会崩溃
- image: xingrin-worker - image: xingrin-worker
dockerfile: docker/worker/Dockerfile dockerfile: docker/worker/Dockerfile
context: . context: .
platforms: linux/amd64,linux/arm64
- image: xingrin-nginx - image: xingrin-nginx
dockerfile: docker/nginx/Dockerfile dockerfile: docker/nginx/Dockerfile
context: . context: .
platforms: linux/amd64,linux/arm64
- image: xingrin-agent - image: xingrin-agent
dockerfile: docker/agent/Dockerfile dockerfile: docker/agent/Dockerfile
context: . context: .
platforms: linux/amd64,linux/arm64
- image: xingrin-postgres - image: xingrin-postgres
dockerfile: docker/postgres/Dockerfile dockerfile: docker/postgres/Dockerfile
context: docker/postgres context: docker/postgres
platforms: linux/amd64,linux/arm64
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Free disk space (for large builds like worker) - name: Free disk space
run: | run: |
echo "=== Before cleanup ===" sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune -af sudo docker image prune -af
echo "=== After cleanup ==="
df -h
- name: Generate SSL certificates for nginx build - name: Generate SSL certificates for nginx build
if: matrix.image == 'xingrin-nginx' if: matrix.image == 'xingrin-nginx'
@@ -73,10 +61,6 @@ jobs:
-keyout docker/nginx/ssl/privkey.pem \ -keyout docker/nginx/ssl/privkey.pem \
-out docker/nginx/ssl/fullchain.pem \ -out docker/nginx/ssl/fullchain.pem \
-subj "/CN=localhost" -subj "/CN=localhost"
echo "SSL certificates generated for CI build"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -87,7 +71,120 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from git tag - name: Get version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
echo "VERSION=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
fi
- name: Build and push AMD64
uses: docker/build-push-action@v5
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
push: true
tags: ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }}-amd64
build-args: IMAGE_TAG=${{ steps.version.outputs.VERSION }}
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-amd64
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-amd64,mode=max
provenance: false
sbom: false
# ARM64 构建(原生 ARM64 runner
build-arm64:
runs-on: ubuntu-22.04-arm
strategy:
matrix:
include:
- image: xingrin-server
dockerfile: docker/server/Dockerfile
context: .
- image: xingrin-frontend
dockerfile: docker/frontend/Dockerfile
context: .
- image: xingrin-worker
dockerfile: docker/worker/Dockerfile
context: .
- image: xingrin-nginx
dockerfile: docker/nginx/Dockerfile
context: .
- image: xingrin-agent
dockerfile: docker/agent/Dockerfile
context: .
- image: xingrin-postgres
dockerfile: docker/postgres/Dockerfile
context: docker/postgres
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate SSL certificates for nginx build
if: matrix.image == 'xingrin-nginx'
run: |
mkdir -p docker/nginx/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout docker/nginx/ssl/privkey.pem \
-out docker/nginx/ssl/fullchain.pem \
-subj "/CN=localhost"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
echo "VERSION=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
fi
- name: Build and push ARM64
uses: docker/build-push-action@v5
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
platforms: linux/arm64
push: true
tags: ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }}-arm64
build-args: IMAGE_TAG=${{ steps.version.outputs.VERSION }}
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-arm64
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-arm64,mode=max
provenance: false
sbom: false
# 合并多架构 manifest
merge-manifests:
runs-on: ubuntu-latest
needs: [build-amd64, build-arm64]
strategy:
matrix:
image:
- xingrin-server
- xingrin-frontend
- xingrin-worker
- xingrin-nginx
- xingrin-agent
- xingrin-postgres
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version
id: version id: version
run: | run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then if [[ $GITHUB_REF == refs/tags/* ]]; then
@@ -98,28 +195,27 @@ jobs:
echo "IS_RELEASE=false" >> $GITHUB_OUTPUT echo "IS_RELEASE=false" >> $GITHUB_OUTPUT
fi fi
- name: Build and push - name: Create and push multi-arch manifest
uses: docker/build-push-action@v5 run: |
with: VERSION=${{ steps.version.outputs.VERSION }}
context: ${{ matrix.context }} IMAGE=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }} docker manifest create ${IMAGE}:${VERSION} \
push: true ${IMAGE}:${VERSION}-amd64 \
tags: | ${IMAGE}:${VERSION}-arm64
${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }} docker manifest push ${IMAGE}:${VERSION}
${{ steps.version.outputs.IS_RELEASE == 'true' && format('{0}/{1}:latest', env.IMAGE_PREFIX, matrix.image) || '' }}
build-args: | if [[ "${{ steps.version.outputs.IS_RELEASE }}" == "true" ]]; then
IMAGE_TAG=${{ steps.version.outputs.VERSION }} docker manifest create ${IMAGE}:latest \
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache ${IMAGE}:${VERSION}-amd64 \
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache,mode=max ${IMAGE}:${VERSION}-arm64
provenance: false docker manifest push ${IMAGE}:latest
sbom: false fi
# 所有镜像构建成功后,更新 VERSION 文件 # 更新 VERSION 文件
# 根据 tag 所在的分支更新对应分支的 VERSION 文件
update-version: update-version:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build needs: merge-manifests
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@@ -1 +1 @@
v1.3.5-dev v1.3.8-dev

View File

@@ -1,106 +1,6 @@
import logging
import sys
from django.apps import AppConfig from django.apps import AppConfig
logger = logging.getLogger(__name__)
class AssetConfig(AppConfig): class AssetConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.asset' name = 'apps.asset'
def ready(self):
# 导入所有模型以确保Django发现并注册
from . import models
# 启用 pg_trgm 扩展(用于文本模糊搜索索引)
# 用于已有数据库升级场景
self._ensure_pg_trgm_extension()
# 验证 pg_ivm 扩展是否可用(用于 IMMV 增量维护)
self._verify_pg_ivm_extension()
def _ensure_pg_trgm_extension(self):
"""
确保 pg_trgm 扩展已启用。
该扩展用于 response_body 和 response_headers 字段的 GIN 索引,
支持高效的文本模糊搜索。
"""
from django.db import connection
# 检查是否为 PostgreSQL 数据库
if connection.vendor != 'postgresql':
logger.debug("跳过 pg_trgm 扩展:当前数据库不是 PostgreSQL")
return
try:
with connection.cursor() as cursor:
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;")
logger.debug("pg_trgm 扩展已启用")
except Exception as e:
# 记录错误但不阻止应用启动
# 常见原因:权限不足(需要超级用户权限)
logger.warning(
"无法创建 pg_trgm 扩展: %s"
"这可能导致 response_body 和 response_headers 字段的 GIN 索引无法正常工作。"
"请手动执行: CREATE EXTENSION IF NOT EXISTS pg_trgm;",
str(e)
)
def _verify_pg_ivm_extension(self):
"""
验证 pg_ivm 扩展是否可用。
pg_ivm 用于 IMMV增量维护物化视图是系统必需的扩展。
如果不可用,将记录错误并退出。
"""
from django.db import connection
# 检查是否为 PostgreSQL 数据库
if connection.vendor != 'postgresql':
logger.debug("跳过 pg_ivm 验证:当前数据库不是 PostgreSQL")
return
# 跳过某些管理命令(如 migrate、makemigrations
import sys
if len(sys.argv) > 1 and sys.argv[1] in ('migrate', 'makemigrations', 'collectstatic', 'check'):
logger.debug("跳过 pg_ivm 验证:当前为管理命令")
return
try:
with connection.cursor() as cursor:
# 检查 pg_ivm 扩展是否已安装
cursor.execute("""
SELECT COUNT(*) FROM pg_extension WHERE extname = 'pg_ivm'
""")
count = cursor.fetchone()[0]
if count > 0:
logger.info("✓ pg_ivm 扩展已启用")
else:
# 尝试创建扩展
try:
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_ivm;")
logger.info("✓ pg_ivm 扩展已创建并启用")
except Exception as create_error:
logger.error(
"=" * 60 + "\n"
"错误: pg_ivm 扩展未安装\n"
"=" * 60 + "\n"
"pg_ivm 是系统必需的扩展,用于增量维护物化视图。\n\n"
"请在 PostgreSQL 服务器上安装 pg_ivm\n"
" curl -sSL https://raw.githubusercontent.com/yyhuni/xingrin/main/docker/scripts/install-pg-ivm.sh | sudo bash\n\n"
"或手动安装:\n"
" 1. apt install build-essential postgresql-server-dev-15 git\n"
" 2. git clone https://github.com/sraoss/pg_ivm.git && cd pg_ivm && make && make install\n"
" 3. 在 postgresql.conf 中添加: shared_preload_libraries = 'pg_ivm'\n"
" 4. 重启 PostgreSQL\n"
"=" * 60
)
# 在生产环境中退出,开发环境中仅警告
from django.conf import settings
if not settings.DEBUG:
sys.exit(1)
except Exception as e:
logger.error(f"pg_ivm 扩展验证失败: {e}")

View File

@@ -18,7 +18,13 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
# 1. 确保 pg_ivm 扩展已启用 # 1. 确保 pg_trgm 扩展已启用(用于文本模糊搜索索引)
migrations.RunSQL(
sql="CREATE EXTENSION IF NOT EXISTS pg_trgm;",
reverse_sql="-- pg_trgm extension kept for other uses"
),
# 2. 确保 pg_ivm 扩展已启用(用于 IMMV 增量维护)
migrations.RunSQL( migrations.RunSQL(
sql="CREATE EXTENSION IF NOT EXISTS pg_ivm;", sql="CREATE EXTENSION IF NOT EXISTS pg_ivm;",
reverse_sql="-- pg_ivm extension kept for other uses" reverse_sql="-- pg_ivm extension kept for other uses"

View File

@@ -11,7 +11,8 @@
import logging import logging
import re import re
from typing import Optional, List, Dict, Any, Tuple, Literal import uuid
from typing import Optional, List, Dict, Any, Tuple, Literal, Iterator
from django.db import connection from django.db import connection
@@ -400,7 +401,7 @@ class AssetSearchService:
query: str, query: str,
asset_type: AssetType = 'website', asset_type: AssetType = 'website',
batch_size: int = 1000 batch_size: int = 1000
): ) -> Iterator[Dict[str, Any]]:
""" """
流式搜索资产(使用服务端游标,内存友好) 流式搜索资产(使用服务端游标,内存友好)
@@ -425,9 +426,12 @@ class AssetSearchService:
ORDER BY created_at DESC ORDER BY created_at DESC
""" """
# 生成唯一的游标名称,避免并发请求冲突
cursor_name = f'export_cursor_{uuid.uuid4().hex[:8]}'
try: try:
# 使用服务端游标,避免一次性加载所有数据到内存 # 使用服务端游标,避免一次性加载所有数据到内存
with connection.cursor(name='export_cursor') as cursor: with connection.cursor(name=cursor_name) as cursor:
cursor.itersize = batch_size cursor.itersize = batch_size
cursor.execute(sql, params) cursor.execute(sql, params)
columns = [col[0] for col in cursor.description] columns = [col[0] for col in cursor.description]

View File

@@ -34,7 +34,7 @@ from rest_framework import status
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
from django.db import connection from django.db import connection, transaction
from apps.common.response_helpers import success_response, error_response from apps.common.response_helpers import success_response, error_response
from apps.common.error_codes import ErrorCodes from apps.common.error_codes import ErrorCodes
@@ -286,6 +286,9 @@ class AssetSearchExportView(APIView):
Response: Response:
CSV 文件流(使用服务端游标,支持大数据量导出) CSV 文件流(使用服务端游标,支持大数据量导出)
注意:使用 @transaction.non_atomic_requests 装饰器,
因为服务端游标不能在事务块内使用。
""" """
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -312,6 +315,7 @@ class AssetSearchExportView(APIView):
return headers, formatters return headers, formatters
@transaction.non_atomic_requests
def get(self, request: Request): def get(self, request: Request):
"""导出搜索结果为 CSV流式导出无数量限制""" """导出搜索结果为 CSV流式导出无数量限制"""
from apps.common.utils import generate_csv_rows from apps.common.utils import generate_csv_rows

View File

@@ -219,6 +219,8 @@ REST_FRAMEWORK = {
# 允许所有来源(前后端分离项目,安全性由认证系统保障) # 允许所有来源(前后端分离项目,安全性由认证系统保障)
CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'True').lower() == 'true' CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'True').lower() == 'true'
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
# 暴露额外的响应头给前端Content-Disposition 用于文件下载获取文件名)
CORS_EXPOSE_HEADERS = ['Content-Disposition']
# ==================== CSRF 配置 ==================== # ==================== CSRF 配置 ====================
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000').split(',') CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000').split(',')

View File

@@ -1,6 +1,5 @@
# 第一阶段:使用 Go 官方镜像编译工具 # 第一阶段:使用 Go 官方镜像编译工具
# 锁定 digest 避免上游更新导致缓存失效 FROM golang:1.24 AS go-builder
FROM golang:1.24@sha256:7e050c14ae9ca5ae56408a288336545b18632f51402ab0ec8e7be0e649a1fc42 AS go-builder
ENV GOPROXY=https://goproxy.cn,direct ENV GOPROXY=https://goproxy.cn,direct
# Naabu 需要 CGO 和 libpcap # Naabu 需要 CGO 和 libpcap
@@ -37,8 +36,7 @@ RUN CGO_ENABLED=0 go install -v github.com/owasp-amass/amass/v5/cmd/amass@main
RUN go install github.com/hahwul/dalfox/v2@latest RUN go install github.com/hahwul/dalfox/v2@latest
# 第二阶段:运行时镜像 # 第二阶段:运行时镜像
# 锁定 digest 避免上游更新导致缓存失效 FROM ubuntu:24.04
FROM ubuntu:24.04@sha256:4fdf0125919d24aec972544669dcd7d6a26a8ad7e6561c73d5549bd6db258ac2
# 避免交互式提示 # 避免交互式提示
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive