该脚本实现批量删除指定日期内久未删除的分支:

快捷使用

./git-cleanup.sh -d 90(天数)

自定义保护分支列表

  1. 创建保护分支列表文件:

    echo -e "production\nlegacy-system" > ~/.git_cleanup_protected
  2. 指定自定义配置文件:

    ./git-cleanup.sh -c ./project_protected_branches.cfg -d 60
  3. 配置文件格式说明:

    • 每行一个分支名称
    • 支持行末注释(以 # 开头的内容)
    • 自动忽略空行和空格
  4. 查看当前保护分支列表

    # 使用示例
    $ ./git-cleanup.sh -l
    当前受保护分支列表:
    ----------------------------------------
    分支名称                 来源
    ----------------------------------------
    production              [配置文件] /home/user/.git_cleanup_protected
    main                    [系统默认]
    master                  [系统默认]
    develop                 [系统默认]
    ----------------------------------------
    (配置文件路径: /home/user/.git_cleanup_protected)

脚本内容

git-cleanup.sh:

#!/bin/bash

# 默认配置
DEFAULT_PROTECTED=("master" "main" "develop")  # 默认受保护分支
CONFIG_FILE="${HOME}/.git_cleanup_protected"   # 默认配置文件路径
remote="origin"                                # 远程仓库名称
delete_local=true                              # 是否删除本地分支
days_threshold=30                              # 默认过期天数阈值

# 加载配置文件
declare -a protected_branches=()
load_protected_branches() {
    # 清空数组重新加载
    protected_branches=()

    # 读取配置文件
    if [ -f "$CONFIG_FILE" ]; then
        while IFS= read -r line; do
            line=${line%%#*}      # 去除行末注释
            line=${line//$'\r'/}  # 处理Windows换行符
            line=${line// /}      # 去除空格

            [[ -n "$line" ]] && protected_branches+=("$line")
        done < "$CONFIG_FILE"
    fi

    # 合并默认保护分支(去重)
    for branch in "${DEFAULT_PROTECTED[@]}"; do
        if [[ ! " ${protected_branches[@]} " =~ " $branch " ]]; then
            protected_branches+=("$branch")
        fi
    done
}

# 列出保护分支
list_protected_branches() {
    # 读取配置文件内容(不合并默认值)
    declare -a config_branches=()
    if [ -f "$CONFIG_FILE" ]; then
        while IFS= read -r line; do
            line=${line%%#*}
            line=${line// /}
            [[ -n "$line" ]] && config_branches+=("$line")
        done < "$CONFIG_FILE"
    fi

    echo "当前受保护分支列表:"
    echo "----------------------------------------"
    printf "%-25s %s\n" "分支名称" "来源"
    echo "----------------------------------------"

    # 显示配置文件中的分支
    for branch in "${config_branches[@]}"; do
        printf "%-25s %s\n" "$branch" "[配置文件] $CONFIG_FILE"
    done

    # 显示未在配置文件中出现的默认分支
    for default_branch in "${DEFAULT_PROTECTED[@]}"; do
        if [[ ! " ${config_branches[@]} " =~ " $default_branch " ]]; then
            printf "%-25s %s\n" "$default_branch" "[系统默认]"
        fi
    done

    echo "----------------------------------------"
    echo "(配置文件路径: $CONFIG_FILE)"
    exit 0
}

# 解析命令行参数
while [[ $# -gt 0 ]]; do
    case $1 in
        -d|--days)
        days_threshold="$2"
        shift; shift ;;
        -c|--config)
        CONFIG_FILE="$2"
        shift; shift ;;
        -l|--list)
        list_protected_branches ;;
        -h|--help)
        echo "用法: $0 [选项]"
        echo "选项:"
        echo "  -d, --days N     设置过期天数阈值(默认:30)"
        echo "  -c, --config FILE指定配置文件路径(默认:${CONFIG_FILE})"
        echo "  -l, --list       列出所有受保护分支"
        echo "  -h, --help       显示帮助信息"
        exit 0 ;;
        *)
        echo "未知选项: $1"; exit 1 ;;
    esac
done

# 验证天数参数
[[ "$days_threshold" =~ ^[0-9]+$ ]] || { echo "错误:天数参数必须为整数"; exit 1; }

# 加载保护分支列表
load_protected_branches

# 计算截止时间戳
cutoff=$(date -d "$days_threshold days ago" +%s)

# 获取分支信息(含创建者)
declare -a branches_info
while read ts branch; do
    branch_name=${branch#$remote/}

    # 跳过受保护分支
    [[ " ${protected_branches[@]} " =~ " $branch_name " ]] && continue

    # 获取分支创建者信息
    creator=$(git log --reverse --format="%an <%ae>" "$branch" -1 2>/dev/null || echo "未知")
    creator=${creator//|/-}  # 处理特殊字符

    # 存储分支信息:时间戳|分支名|创建者
    branches_info+=("$ts|$branch_name|${creator//$'\n'/}")
done < <(git fetch -pq && git for-each-ref --sort=committerdate \
         --format='%(committerdate:unix) %(refname:short)' \
         "refs/remotes/$remote")

# 筛选待删除分支
branches_to_delete=()
for entry in "${branches_info[@]}"; do
    IFS='|' read ts branch_name creator <<< "$entry"
    [ $ts -lt $cutoff ] && branches_to_delete+=("$branch_name|$creator")
done

# 显示结果
if [ ${#branches_to_delete[@]} -eq 0 ]; then
    echo "没有需要清理的分支(阈值:${days_threshold}天)"
    exit 0
fi

echo "以下分支超过 ${days_threshold} 天未更新(共 ${#branches_to_delete[@]} 个):"
printf "%-30s %s\n" "分支名称" "创建者"
echo "------------------------------------------------"
for entry in "${branches_to_delete[@]}"; do
    IFS='|' read branch creator <<< "$entry"
    printf "%-30s %s\n" "$branch" "$creator"
done

# 用户确认
read -p "是否确认删除这些分支?[y/N] " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "操作已取消"; exit 1; }

# 执行删除
deleted_count=0
for entry in "${branches_to_delete[@]}"; do
    IFS='|' read branch_name creator <<< "$entry"

    # 删除远程分支
    if git push -q "$remote" --delete "$branch_name"; then
        echo "已删除远程分支: $branch_name (创建者: $creator)"
        ((deleted_count++))
    else
        echo "删除远程分支失败: $branch_name" >&2
        continue
    fi

    # 删除本地分支
    if [ "$delete_local" = true ]; then
        git branch -D "$branch_name" >/dev/null 2>&1 && \
        echo "已删除本地分支: $branch_name" || \
        echo "本地分支不存在: $branch_name"
    fi
done

echo "操作完成,共删除 $deleted_count 个远程分支"

脚本install-app-as-service.sh

#!/bin/bash

# 检查参数是否正确
if [ $# -ne 1 ]; then
    echo "Usage: $0 <target-directory or executable-file>"
    exit 1
fi

TARGET="$1"

# 根据输入参数的类型选择操作模式
if [ -f "$TARGET" ] && [ -x "$TARGET" ]; then
    # 单个可执行文件模式
    echo "Single executable file detected. Installing as a service."
    
    # 获取基本信息
    executable="$TARGET"
    service_name=$(basename "$executable").service
    service_path="/etc/systemd/system/$service_name"
    exec_path=$(realpath "$executable")
    work_dir=$(dirname "$exec_path")

    echo "Installing $exec_path as systemd service: $service_name"

    # 生成服务文件内容(包含路径转义)
    service_content="[Unit]
Description=Service for $(basename "$executable")
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=$work_dir
ExecStart=\"$exec_path\"
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target"

    # 写入系统服务文件
    echo "$service_content" | sudo tee "$service_path" > /dev/null

    # 设置文件权限
    sudo chmod 644 "$service_path"

    # 重新加载systemd并启用服务
    sudo systemctl daemon-reload
    sudo systemctl enable "$service_name"
    sudo systemctl start "$service_name"

    echo "Successfully installed and started $service_name"

elif [ -d "$TARGET" ]; then
    # 目录模式,与原来功能一致
    echo "Directory mode: Installing all executables in directory as services."
    
    # 查找所有可执行文件并处理
    find "$TARGET" -maxdepth 1 -type f -executable | while read -r executable; do
        # 获取基本信息
        service_name=$(basename "$executable").service
        service_path="/etc/systemd/system/$service_name"
        exec_path=$(realpath "$executable")
        work_dir=$(dirname "$exec_path")

        echo "Installing $exec_path as systemd service: $service_name"

        # 生成服务文件内容(包含路径转义)
        service_content="[Unit]
Description=Service for $(basename "$executable")
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=$work_dir
ExecStart=\"$exec_path\"
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target"

        # 写入系统服务文件
        echo "$service_content" | sudo tee "$service_path" > /dev/null

        # 设置文件权限
        sudo chmod 644 "$service_path"

        # 重新加载systemd并启用服务
        sudo systemctl daemon-reload
        sudo systemctl enable "$service_name"
        sudo systemctl start "$service_name"

        echo "Successfully installed and started $service_name"
    done

    echo "All executable services installed from $TARGET"
else
    echo "Error: '$TARGET' is neither a valid directory nor an executable file."
    exit 1
fi

使用:

sudo install-app-as-service.sh path-to-the-app(指向单个可执行程序)
sudo install-app-as-service.sh path-to-the-dir(指向可执行程序上级目录,支持多个批量安装)

Serv00服务器经常出现删除cron现象,通过外部SSH访问重设比较麻烦,同一个IP对多服务器SSH访问,还可能会被批量查封。
可以借用web进行应用保活,简单又安全,方法如下:

  • 替换public_html目录下的默认主页
    删除/home/${YOURNAME}/domains/${YOURNAME}.serv00.net/public_html目录下的index.html,增加:
    index.php:

    <?php
    
    // 引入配置文件
    $config = include 'config.php';
    
    // 日志文件路径
    $logFile = 'log/access.log';
    
    // Nginx 默认页面 HTML 内容
    $nginxDefaultHtml = <<<HTML
    <!DOCTYPE html>
    <html>
    <head>
      <title>Welcome to nginx!</title>
      <style>
          body {
              width: 35em;
              margin: 0 auto;
              font-family: Tahoma, Verdana, Arial, sans-serif;
          }
      </style>
    </head>
    <body>
    <h1>Welcome to nginx!</h1>
    <p>If you see this page, the nginx web server is successfully installed and
    working. Further configuration is required.</p>
    
    <p>For online documentation and support please refer to
    <a href="http://nginx.org/">nginx.org</a>.<br/>
    Commercial support is available at
    <a href="http://nginx.com/">nginx.com</a>.</p>
    
    <p><em>Thank you for using nginx.</em></p>
    </body>
    </html>
    HTML;
    
    // 确保 log 目录存在
    if (!is_dir('log')) {
      mkdir('log', 0777, true); // 创建目录,允许所有用户读写执行
    }
    
    // 获取请求路径 (从 GET 参数中获取)
    $requestPath = $_GET['path'] ?? '';
    
    // 记录访问日志
    $logMessage = date('Y-m-d H:i:s') . " - Access: " . ($requestPath ? '?path=' . $requestPath : 'Direct Access') . "\n";
    file_put_contents($logFile, $logMessage, FILE_APPEND);
    
    // 移除多余的斜杠
    $requestPath = trim($requestPath, '/');
    
    // 添加前导斜杠,使其与配置文件中的路由匹配
    if ($requestPath !== '') {
      $requestPath = '/' . $requestPath;
    }
    
    // 查找匹配的配置项
    $action = null;
    foreach ($config['routes'] as $route => $actionConfig) {
      if ($route === $requestPath) {
          $action = $actionConfig;
          break;
      }
    }
    
    if ($action) {
      // 执行对应操作
      switch ($action['type']) {
          case 'command':
              $command = $action['command'];
              $logMessage = date('Y-m-d H:i:s') . " - Executing command: " . $command . "\n";
              file_put_contents($logFile, $logMessage, FILE_APPEND);
    
              // 执行命令并获取输出
              $output = shell_exec($command . ' 2>&1'); //  2>&1  将标准错误重定向到标准输出
    
              $logMessage = date('Y-m-d H:i:s') . " - Command output: " . $output . "\n";
              file_put_contents($logFile, $logMessage, FILE_APPEND);
    
              echo "<pre>" . htmlspecialchars($output) . "</pre>"; // 使用 <pre> 标签保持格式,并进行HTML转义
              break;
    
          case 'static':
              $filePath = $action['file'];
    
              if (is_dir($filePath)) {
                  // 目录托管
                  $requestedFile = realpath($filePath . '/' . ltrim($requestPath, '/'));  // 确保在目录内
                  $logMessage = date('Y-m-d H:i:s') . " - Serving static directory: " . $filePath . ", Requested file: " . $requestedFile . "\n";
                  file_put_contents($logFile, $logMessage, FILE_APPEND);
    
                  if ($requestedFile && strpos($requestedFile, realpath($filePath)) === 0 && file_exists($requestedFile) && is_readable($requestedFile)) {  // 安全检查
                      // 根据文件类型设置 Content-Type
                      $fileExtension = pathinfo($requestedFile, PATHINFO_EXTENSION);
                      $contentType = getContentType($fileExtension);  // 使用函数获取 Content-Type
                      header('Content-Type: ' . $contentType);
                      readfile($requestedFile);
                  } else {
                      header("HTTP/1.0 404 Not Found");
                      echo "<h1>404 Not Found</h1>";
                      $logMessage = date('Y-m-d H:i:s') . " - Static file not found: " . $filePath . '/' . ltrim($requestPath, '/') . "\n";
                      file_put_contents($logFile, $logMessage, FILE_APPEND);
                  }
              } elseif (file_exists($filePath) && is_readable($filePath)) {
                  // 单个文件
                  $logMessage = date('Y-m-d H:i:s') . " - Serving static file: " . $filePath . "\n";
                  file_put_contents($logFile, $logMessage, FILE_APPEND);
    
                  // 根据文件类型设置 Content-Type
                  $fileExtension = pathinfo($filePath, PATHINFO_EXTENSION);
                  $contentType = getContentType($fileExtension);  // 使用函数获取 Content-Type
                  header('Content-Type: ' . $contentType);
    
                  readfile($filePath);
              } else {
                  header("HTTP/1.0 404 Not Found");
                  echo "<h1>404 Not Found</h1>";
                  $logMessage = date('Y-m-d H:i:s') . " - Static file not found: " . $filePath . "\n";
                  file_put_contents($logFile, $logMessage, FILE_APPEND);
              }
              break;
    
          default:
              header("HTTP/1.0 500 Internal Server Error");
              echo "<h1>500 Internal Server Error</h1><p>Invalid action type.</p>";
              $logMessage = date('Y-m-d H:i:s') . " - Invalid action type: " . $action['type'] . "\n";
              file_put_contents($logFile, $logMessage, FILE_APPEND);
              break;
      }
    } else {
      // 未找到匹配的路由,返回 Nginx 默认页面
      header('Content-Type: text/html');
      echo $nginxDefaultHtml;
      $logMessage = date('Y-m-d H:i:s') . " - Route not found, serving Nginx default page.\n";
      file_put_contents($logFile, $logMessage, FILE_APPEND);
    }
    
    /**
     * 根据文件扩展名获取 Content-Type
     * @param string $extension
     * @return string
     */
    function getContentType(string $extension): string {
      $mimeTypes = [
          'html' => 'text/html',
          'css' => 'text/css',
          'js' => 'text/javascript',
          'json' => 'application/json',
          'xml' => 'application/xml',
          'txt' => 'text/plain',
          'jpg' => 'image/jpeg',
          'jpeg' => 'image/jpeg',
          'png' => 'image/png',
          'gif' => 'image/gif',
          'svg' => 'image/svg+xml',
          // 添加更多 mime 类型
      ];
    
      return $mimeTypes[$extension] ?? 'application/octet-stream'; // 默认
    }
    
    ?>

config.php:

<?php

return [
    'routes' => [
        '/status' => [
            'type' => 'command',
            'command' => 'uptime'
        ],
        '/diskspace' => [
            'type' => 'command',
            'command' => 'df -h'
        ],
        '/cron' => [
            'type' => 'command',
            'command' => 'bash ./your_check_cron_shell.sh'
        ],
        '/installcron' => [
            'type' => 'command',
            'command' => '(crontab -l 2>/dev/null; echo "00 08 * * 6 ${your_command} > /dev/null 2>&1") | crontab -'
        ],
        '/back.html' => [
            'type' => 'static',
            'file' => 'your.html'  // 单个文件
        ]
    ]
];

按需修改上面的配置文件对应的脚本或命令,尽情发挥。

  • WEB远程调用示例

    https://${YOURNAME}.serv00.net/?path=cron

// ==UserScript==
// @name         SillyDev Auto Renewal
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Automatically renew servers on panel.sillydev.co.uk
// @author       Your name
// @match        https://panel.sillydev.co.uk/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    
    const RENEWAL_COST = 175; // Cost per renewal
    const DAYS_PER_RENEWAL = 7; // Days added per renewal
    const MAX_RENEWAL_DAYS = 21; // Maximum renewal days allowed
    const CHECK_INTERVAL = 30 * 60 * 1000; // Check every 30 minutes

    // Helper function to get XSRF token from cookie
    function getXsrfToken() {
        const cookies = document.cookie.split(';');
        for (const cookie of cookies) {
            const [name, value] = cookie.trim().split('=');
            if (name === 'XSRF-TOKEN') {
                return decodeURIComponent(value);
            }
        }
        return null;
    }

    // Helper function to make API requests
    async function makeRequest(endpoint, method = 'GET', body = null) {
        const xsrfToken = getXsrfToken();
        if (!xsrfToken) {
            console.error('XSRF token not found in cookies');
            return null;
        }

        const options = {
            method: method,
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-XSRF-TOKEN': xsrfToken
            },
            credentials: 'include'
        };
        
        if (body) {
            options.body = JSON.stringify(body);
        }
        
        try {
            const response = await fetch(`https://panel.sillydev.co.uk${endpoint}`, options);
            
            // Handle non-OK responses
            if (!response.ok) {
                const errorData = await response.json();
                throw new Error(JSON.stringify(errorData));
            }
            
            // For successful responses, check if there's actual content
            const contentType = response.headers.get('content-type');
            if (contentType && contentType.includes('application/json')) {
                return await response.json();
            }
            
            // If no JSON content but response is OK, return true to indicate success
            return true;
            
        } catch (error) {
            console.error(`API request failed:`, error);
            return null;
        }
    }

    // Get user's current balance
    async function getUserBalance() {
        const userData = await makeRequest('/api/client/store');
        return userData?.attributes?.balance || 0;
    }

    // Get list of all servers
    async function getServers() {
        const serversData = await makeRequest('/api/client?page=1');
        return serversData?.data || [];
    }

    // Renew a specific server
    async function renewServer(serverUuid) {
        return await makeRequest(`/api/client/servers/${serverUuid}/renew`, 'POST');
    }

    // Calculate optimal renewals based on current balance and server states
    function calculateOptimalRenewals(servers, balance) {
        // Calculate how many renewals we can afford
        const maxRenewals = Math.floor(balance / RENEWAL_COST);
        if (maxRenewals === 0) return [];

        // Sort servers by a combination of days remaining and potential waste
        const sortedServers = servers.map(server => {
            const daysRemaining = server.attributes.renewal;
            // Calculate potential waste (days that would exceed MAX_RENEWAL_DAYS)
            const potentialWaste = Math.max(0, (daysRemaining + DAYS_PER_RENEWAL) - MAX_RENEWAL_DAYS);
            // Calculate urgency score (lower is more urgent)
            const urgencyScore = (daysRemaining * 2) + (potentialWaste * 3);
            
            return {
                server: server,
                daysRemaining: daysRemaining,
                potentialWaste: potentialWaste,
                urgencyScore: urgencyScore
            };
        }).sort((a, b) => a.urgencyScore - b.urgencyScore);

        const renewalPlan = [];
        let remainingBalance = balance;

        // Consider each server for renewal
        for (const serverInfo of sortedServers) {
            // Stop if we can't afford more renewals
            if (remainingBalance < RENEWAL_COST) break;

            const daysRemaining = serverInfo.daysRemaining;
            const newTotalDays = daysRemaining + DAYS_PER_RENEWAL;

            // Only renew if:
            // 1. The new total won't exceed MAX_RENEWAL_DAYS
            // 2. Current days are less than 14 (to ensure we always have a buffer for renewal)
            if (newTotalDays <= MAX_RENEWAL_DAYS && daysRemaining < 14) {
                renewalPlan.push({
                    uuid: serverInfo.server.attributes.uuid,
                    currentDays: daysRemaining,
                    newTotal: newTotalDays,
                    needsRenewal: true
                });
                remainingBalance -= RENEWAL_COST;
            }
        }

        return renewalPlan;
    }

    // Main function to check and renew servers
    async function checkAndRenewServers() {
        console.log('Checking server renewals...');
        
        // Get current balance and servers
        const balance = await getUserBalance();
        const servers = await getServers();
        
        if (!balance || !servers) {
            console.log('Failed to fetch necessary data');
            return;
        }

        console.log(`Current balance: ${balance}`);
        console.log('Current servers:', servers.map(s => ({
            name: s.attributes.name,
            days: s.attributes.renewal
        })));

        // Calculate optimal renewals
        const renewalPlan = calculateOptimalRenewals(servers, balance);

        // Execute renewals
        for (const plan of renewalPlan) {
            console.log(`Renewing server ${plan.uuid} (current days: ${plan.currentDays}, will have: ${plan.newTotal})`);
            try {
                const result = await renewServer(plan.uuid);
                if (result === true) {
                    console.log(`Successfully renewed server ${plan.uuid}`);
                } else {
                    console.error(`Failed to renew server ${plan.uuid}`);
                }
            } catch (error) {
                console.error(`Failed to renew server ${plan.uuid}:`, error);
            }
            // Add a small delay between renewals to avoid rate limiting
            await new Promise(resolve => setTimeout(resolve, 2000));
        }

        console.log('Renewal check completed');
    }

    // Function to wait for XSRF token to be available in cookies
    function waitForXsrfToken() {
        return new Promise((resolve) => {
            const checkToken = () => {
                const token = getXsrfToken();
                if (token) {
                    resolve();
                } else {
                    setTimeout(checkToken, 100);
                }
            };
            checkToken();
        });
    }

    // Start the periodic checks
    async function initialize() {
        console.log('SillyDev Auto Renewal script started, waiting for XSRF token...');
        await waitForXsrfToken();
        console.log('XSRF token found, starting renewal checks...');
        checkAndRenewServers(); // Initial check
        setInterval(checkAndRenewServers, CHECK_INTERVAL); // Periodic checks
    }

    initialize();
})();