linux指定目录下保留最新N个文件
实现指定目录下保留最新N个文件的Shell脚本:
使用示例:
实现指定目录下保留最新N个文件的Shell脚本:
#!/bin/bash
# 参数检查
if [ $# -ne 2 ]; then
echo "Usage: $0 <directory> <number_of_files_to_keep>"
exit 1
fi
directory="$1"
n="$2"
# 验证目录是否存在
if [ ! -d "$directory" ]; then
echo "Error: Directory '$directory' does not exist."
exit 1
fi
# 验证保留数量是否为合法数字
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
echo "Error: Invalid number format. Please enter a positive integer."
exit 1
fi
# 安全删除旧文件的命令链
find "$directory" -maxdepth 1 -type f -printf "%T@ %p\0" | \
sort -z -k1nr | \
cut -z -d' ' -f2- | \
tail -z -n +$(($n + 1)) | \
xargs -0 -r rm -f --
echo "Success: Kept latest $n files in '$directory'."
使用示例:
# 保留/var/log目录下最新的5个文件
sudo ./keep_latest.sh /var/log 5
该脚本实现批量删除指定日期内久未删除的分支:
./git-cleanup.sh -d 90(天数)
创建保护分支列表文件:
echo -e "production\nlegacy-system" > ~/.git_cleanup_protected
指定自定义配置文件:
./git-cleanup.sh -c ./project_protected_branches.cfg -d 60
配置文件格式说明:
查看当前保护分支列表
# 使用示例
$ ./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();
})();