在 .NET MiniAPI (Kestrel) 中实现静态文件服务 + 可排序目录浏览功能

在开发轻量级 API 时,.NET MiniAPI 凭借其简洁高效的特性成为首选。但实际场景中,我们常需要暴露静态文件(如 API 文档、配置文件、共享资源),甚至允许客户端浏览目录下的文件列表。默认情况下,MiniAPI 的静态文件服务不支持目录浏览,且原生目录列表无排序功能,本文将详细记录如何实现「指定目录暴露 + 目录浏览 + 列表排序」的完整方案,附完整代码和优化细节。

一、需求背景

在 MiniAPI 中实现以下核心功能:

  1. 暴露服务器上的指定物理目录(如 GeoIP 文件夹),支持客户端通过 URL 访问文件;
  2. 启用目录浏览功能,允许客户端列出目录下的文件/子目录;
  3. 目录列表支持按「名称、大小、修改时间」排序(点击表头切换升序/降序);
  4. 优化目录列表样式,保持简洁易用,同时兼容 AOT 编译。

二、实现步骤(基于 .NET MiniAPI > 8.0)

1. 环境准备

创建 .NET MiniAPI 项目(若已有项目可跳过):

dotnet new web -n MiniApiStaticFileDemo
cd MiniApiStaticFileDemo

2. 核心配置:暴露指定物理目录 + 启用目录浏览

Program.cs 中配置静态文件中间件和目录浏览功能,核心是指定物理目录、URL 访问路径,并关联自定义排序格式化器。

2.1 Program.cs 完整配置

using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using System.Text;

var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();

// 关键配置:指定要暴露的物理目录
string baseDir = AppContext.BaseDirectory; // 程序运行目录
string exposeDir = Path.Combine(baseDir, "GeoIP"); // 要暴露的目录(如 GeoIP 文件夹)
string urlPrefix = "/geoip"; // 客户端访问前缀(通过 /geoip 访问该目录)

// 1. 配置静态文件服务:暴露指定物理目录
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(exposeDir), // 物理目录路径
    RequestPath = urlPrefix, // URL 访问路径(例:http://localhost:5000/geoip/文件名称)
    OnPrepareResponse = ctx =>
    {
        // 可选:添加响应头,禁止缓存静态文件
        ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store");
    }
});

// 2. 启用目录浏览(默认禁用),并关联自定义排序格式化器
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(exposeDir),
    RequestPath = urlPrefix, // 与静态文件访问路径保持一致
    Formatter = new SortableDirectoryFormatter("GeoIP 目录列表") // 自定义格式化器(支持排序)
});

// 测试接口
app.MapGet("/", () => Results.Ok("MiniAPI 静态文件服务 + 可排序目录浏览已启用"));

app.Run("http://*:5000");

3. 关键实现:自定义可排序目录格式化器

默认的目录浏览列表无排序功能,且样式简陋。我们通过实现 IDirectoryFormatter 接口,自定义目录列表的 HTML 输出,添加「名称、大小、修改时间」排序功能,同时优化样式和安全性。

3.1 完整 SortableDirectoryFormatter 类

创建 SortableDirectoryFormatter.cs 文件,核心功能包括:

  • 过滤隐藏文件(以 . 开头的文件);
  • 目录在前、文件在后的默认排序;
  • 点击表头切换排序方向(升序/降序);
  • 支持按名称、文件大小、修改时间排序;
  • XSS 防护、跨平台路径处理。
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using System.Globalization;
using System.Text;

public class SortableDirectoryFormatter : IDirectoryFormatter
{
    private readonly string _pageTitle;
    // 时间格式:年-月-日 时:分:秒(UTC 时区)
    private const string CustomDateTimeFormat = "yyyy-MM-dd HH:mm:ss +00:00";

    // 构造函数:支持自定义页面标题
    public SortableDirectoryFormatter(string pageTitle = "目录列表")
    {
        _pageTitle = pageTitle ?? "目录列表";
    }

    public async Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
    {
        // 1. 过滤隐藏文件 + 初始排序(目录在前,文件在后)
        var safeContents = contents ?? Enumerable.Empty<IFileInfo>();
        var fileList = new List<IFileInfo>(safeContents.Where(f =>
            !string.IsNullOrEmpty(f.Name) && !f.Name.StartsWith(".")
        ));
        fileList.Sort((a, b) =>
        {
            if (a.IsDirectory != b.IsDirectory)
                return a.IsDirectory ? -1 : 1;
            return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
        });

        // 2. 生成面包屑导航(支持多层目录跳转)
        var breadcrumbBuilder = new StringBuilder();
        string currentPath = "/";
        breadcrumbBuilder.Append($"<a href=\"{HtmlEncode(currentPath)}\">/</a>");
        var requestPath = context.Request.Path.Value;
        if (!string.IsNullOrEmpty(requestPath))
        {
            var pathSegments = requestPath.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
            foreach (var segment in pathSegments)
            {
                currentPath = Path.Combine(currentPath, segment + "/").Replace(Path.DirectorySeparatorChar, '/');
                breadcrumbBuilder.Append($"<a href=\"{HtmlEncode(currentPath)}\">{HtmlEncode(segment)}/</a>");
            }
        }

        // 3. 构建 HTML 页面(含样式 + 排序脚本)
        var htmlBuilder = new StringBuilder(4096);
        htmlBuilder.AppendLine("<!DOCTYPE html>");
        htmlBuilder.AppendLine("<html lang=\"zh-CN\">");
        htmlBuilder.AppendLine("<head>");
        htmlBuilder.AppendLine($"<title>{_pageTitle} - {HtmlEncode(requestPath ?? "/")}</title>");
        // 样式:保持简洁,适配不同设备
        htmlBuilder.AppendLine(@"<style>
            body { font-family: ""Segoe UI"", ""Microsoft YaHei"", sans-serif; font-size: 14px; max-width: 1200px; margin: 0 auto; padding: 20px; }
            header h1 { font-size: 24px; font-weight: 400; margin: 0 0 20px 0; color: #333; }
            #index { width: 100%; border-collapse: separate; border-spacing: 0; border: 1px solid #eee; }
            #index th { background: #f8f9fa; padding: 10px; text-align: center; cursor: pointer; user-select: none; position: relative; border-bottom: 2px solid #ddd; }
            #index td { padding: 8px 10px; border-bottom: 1px solid #eee; }
            #index th:hover { background: #f1f3f5; }
            #index td.length, td.modified { text-align: right; }
            a { color: #127aac; text-decoration: none; }
            a:hover { color: #13709e; text-decoration: underline; }
            .sort-arrow { position: absolute; right: 8px; font-size: 0.8em; color: #999; }
            .dir-name { font-weight: 500; }
        </style>");
        htmlBuilder.AppendLine("</head>");
        htmlBuilder.AppendLine("<body>");
        htmlBuilder.AppendLine($"<header><h1>{_pageTitle}:{breadcrumbBuilder}</h1></header>");
        htmlBuilder.AppendLine("<table id=\"index\">");
        htmlBuilder.AppendLine("<thead><tr>");
        htmlBuilder.AppendLine("<th data-col=\"name\">名称 <span class=\"sort-arrow\">↑</span></th>");
        htmlBuilder.AppendLine("<th data-col=\"size\">大小 <span class=\"sort-arrow\"></span></th>");
        htmlBuilder.AppendLine("<th data-col=\"modified\">最后修改时间 <span class=\"sort-arrow\"></span></th>");
        htmlBuilder.AppendLine("</tr></thead><tbody>");

        // 4. 生成文件/目录列表行
        if (fileList.Count == 0)
        {
            htmlBuilder.AppendLine("<tr><td colspan=\"3\" style=\"text-align:center; padding:20px; color:#666;\">目录无可用文件</td></tr>");
        }
        else
        {
            foreach (var file in fileList)
            {
                var fileName = file.IsDirectory ? $"{file.Name}/" : file.Name;
                var fileClass = file.IsDirectory ? "dir-name" : "";
                var fileSize = file.IsDirectory ? "-" : FormatFileSizeWithComma(file.Length);
                var fileModified = file.LastModified.ToUniversalTime().ToString(CustomDateTimeFormat, CultureInfo.InvariantCulture);
                var encodedFileName = Uri.EscapeDataString(file.Name);
                var fileUrl = $"{context.Request.Path}/{encodedFileName}".Replace("//", "/");

                htmlBuilder.AppendLine("<tr>");
                htmlBuilder.AppendLine($"<td class=\"name {fileClass}\"><a href=\"{HtmlEncode(fileUrl)}\">{HtmlEncode(fileName)}</a></td>");
                htmlBuilder.AppendLine($"<td class=\"length\">{HtmlEncode(fileSize)}</td>");
                htmlBuilder.AppendLine($"<td class=\"modified\">{HtmlEncode(fileModified)}</td>");
                htmlBuilder.AppendLine("</tr>");
            }
        }

        htmlBuilder.AppendLine("</tbody></table>");
        // 5. 排序核心脚本(点击表头切换排序)
        htmlBuilder.AppendLine(@"<script>
            document.addEventListener('DOMContentLoaded', function() {
                const table = document.getElementById('index');
                if (!table) return;
                const headers = table.querySelectorAll('thead th[data-col]');
                const tbody = table.querySelector('tbody');
                let currentSort = { col: 'name', order: 'asc' };

                // 表头点击事件
                headers.forEach(th => {
                    th.addEventListener('click', function() {
                        const newCol = this.dataset.col;
                        currentSort.order = (currentSort.col === newCol) ? (currentSort.order === 'asc' ? 'desc' : 'asc') : 'asc';
                        currentSort.col = newCol;
                        updateSortArrows();
                        sortTable();
                    });
                });

                // 更新排序箭头
                function updateSortArrows() {
                    headers.forEach(th => {
                        const arrow = th.querySelector('.sort-arrow');
                        arrow.textContent = (th.dataset.col === currentSort.col) ? (currentSort.order === 'asc' ? '↑' : '↓') : '';
                    });
                }

                // 排序逻辑
                function sortTable() {
                    const rows = Array.from(tbody.querySelectorAll('tr'));
                    if (!rows.length) return;

                    rows.sort((a, b) => {
                        const cellA = a.querySelector(`td.${getCellClass(currentSort.col)}`).textContent.trim();
                        const cellB = b.querySelector(`td.${getCellClass(currentSort.col)}`).textContent.trim();

                        switch (currentSort.col) {
                            case 'name':
                                const isDirA = cellA.endsWith('/');
                                const isDirB = cellB.endsWith('/');
                                if (isDirA !== isDirB) return isDirA ? -1 : 1;
                                return currentSort.order === 'asc' ? cellA.localeCompare(cellB, 'zh-CN') : cellB.localeCompare(cellA, 'zh-CN');
                            case 'size':
                                if (cellA === '-') return -1;
                                if (cellB === '-') return 1;
                                const sizeA = BigInt(cellA.replace(/,/g, ''));
                                const sizeB = BigInt(cellB.replace(/,/g, ''));
                                return currentSort.order === 'asc' ? (sizeA < sizeB ? -1 : 1) : (sizeA > sizeB ? -1 : 1);
                            case 'modified':
                                const dateA = new Date(cellA.replace(' +00:00', 'Z')).getTime();
                                const dateB = new Date(cellB.replace(' +00:00', 'Z')).getTime();
                                return currentSort.order === 'asc' ? (dateA - dateB) : (dateB - dateA);
                            default: return 0;
                        }
                    });

                    tbody.innerHTML = '';
                    rows.forEach(row => tbody.appendChild(row));
                }

                function getCellClass(colName) {
                    return colName === 'name' ? 'name' : colName === 'size' ? 'length' : 'modified';
                }

                updateSortArrows();
            });
        </script>");
        htmlBuilder.AppendLine("</body></html>");

        // 输出响应
        context.Response.ContentType = "text/html; charset=utf-8";
        await context.Response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
    }

    // 辅助方法:文件大小格式化(带千分位)
    private static string FormatFileSizeWithComma(long bytes)
    {
        return bytes.ToString("N0", CultureInfo.InvariantCulture);
    }

    // 辅助方法:HTML 编码(防 XSS)
    private static string HtmlEncode(string value)
    {
        if (string.IsNullOrEmpty(value)) return string.Empty;
        return value.Replace("&", "&amp;")
                    .Replace("<", "&lt;")
                    .Replace(">", "&gt;")
                    .Replace("\"", "&quot;")
                    .Replace("'", "&#39;")
                    .Replace("`", "&#96;");
    }
}

4. 核心功能说明

4.1 静态文件服务配置

  • PhysicalFileProvider(exposeDir):指定要暴露的物理目录(如 GeoIP 文件夹,位于程序运行目录下);
  • RequestPath = "/geoip":客户端通过 http://localhost:5000/geoip/文件名 访问文件;
  • OnPrepareResponse:可选配置,添加缓存控制头,避免浏览器缓存静态文件。

4.2 目录浏览功能

  • UseDirectoryBrowser:启用目录浏览(默认禁用),需与静态文件服务的 FileProviderRequestPath 保持一致;
  • SortableDirectoryFormatter:自定义目录列表格式化器,替代默认的简陋列表,支持排序和样式优化。

4.3 排序功能实现

  • 前端:通过 JavaScript 监听表头点击事件,切换排序字段(名称/大小/修改时间)和排序方向(升序/降序);
  • 排序逻辑:
    • 名称排序:目录优先,文件在后,按字母/中文拼音排序;
    • 大小排序:目录显示 -,文件按数值大小排序(去除千分位逗号);
    • 时间排序:将 UTC 时间转换为时间戳排序,兼容 2025-10-18 08:16:06 +00:00 格式。

三、关键优化点(兼容生产环境)

1. 安全性优化

  • XSS 防护:通过 HtmlEncode 方法对文件名、路径等用户可控内容进行编码,避免脚本注入;
  • 隐藏文件过滤:默认过滤以 . 开头的文件(如 .gitignore.env),防止敏感文件泄露;
  • 可选:添加身份认证(如 API Key、JWT),限制目录浏览权限(例:RequireAuthorization())。

2. 兼容性优化

  • 跨平台支持:使用 Path.Combine 和路径分隔符替换,兼容 Windows/Linux/macOS;
  • AOT 编译兼容:避免反射和动态代码,支持 .NET 8+ AOT 编译(可直接发布为原生可执行文件);
  • 空值安全:处理 contentsnull、文件名为空等异常场景,避免程序崩溃。

3. 体验优化

  • 面包屑导航:支持多层目录跳转(如 /geoip/GeoLite2/),方便用户回溯;
  • 时间格式标准化:统一使用 yyyy-MM-dd HH:mm:ss +00:00 格式,避免时区混淆;
  • 响应头优化:指定 charset=utf-8,确保中文文件名正常显示。

四、测试效果

  1. 运行项目:dotnet run
  2. 在程序运行目录下创建 GeoIP 文件夹,放入测试文件/子目录;
  3. 访问 http://localhost:5000/geoip,即可看到目录列表:
    1. 默认按名称升序排列,目录在前,文件在后;
    2. 点击表头「名称」「大小」「最后修改时间」可切换排序方式;
    3. 点击文件名/目录名可直接访问(目录会进入下一级列表)。

五、扩展场景

  1. 静态资源托管:用于托管 API 文档(如 Swagger、Redoc)、前端静态文件(Vue/React 构建产物);
  2. 内部文件共享:团队内部临时共享文件(结合身份认证,避免公开访问);
  3. 日志文件查看:暴露日志目录,支持按修改时间排序查看最新日志文件。

六、总结

通过 .NET MiniAPI 的静态文件中间件 + 自定义目录格式化器,我们仅需少量代码就实现了「指定目录暴露 + 可排序目录浏览」功能。该方案简洁高效,兼容生产环境,适用于轻量级文件服务场景。如需进一步增强,可扩展权限控制、文件上传、下载限速等功能。

下载 Demo : MiniApiStaticFileDemo.zip

如果有任何问题或优化建议,欢迎在评论区交流~

使用 hping 时报错的处理

Linux中使用Nmap , hping, ping 提示以下错误时

PHP: [main]	can't	open	raw	socket

Bash: Couldn’t open a raw socket. Error: Permission denied (13)

非root进程调用带有网络原始套接字(raw socket) 的相关命令时,会报错,需要使用getcap 设置 capabilities

(具体参考:https://cloud.tencent.com/developer/article/1539041)

# 以 ping 为例,先 which 查看命令地址
which ping

# 如无返回就是没有设置cap
getcap /usr/bin/ping

# 设置cap
sudo setcap cap_net_raw+ep /usr/bin/ping

# 再次getcap 已有设置值
getcap /usr/bin/ping
#/usr/bin/ping cap_net_raw=ep

setcap 之后问题就解决了。

Apt update : failed to fetch expkeysig abf5bd827bd9bf62 nginx signing key signing-key@nginx.com

apt update ,更新 nginx 时报错。 提示 Nginx 签名无效, 无法获取.

failed to fetch https://nginx.org/packages/mainline/debian/dists/bookworm/inrelease the following signatures were invalid: expkeysig abf5bd827bd9bf62 nginx signing key <signing-key@nginx.com>
解决处理:
  1. 导入 Nginx.org GPG 密钥
curl -fSsL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg > /dev/null

2. 验证密钥是否成功导入

gpg --dry-run --quiet --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg

3. 导入 Nginx.org APT 存储库

bash 下执行以下 echo 命令 :

Mainline 存储库 、稳定存储库 二选一!

要导入 Nginx Mainline 存储库,请使用:

echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/mainline/debian `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.list

或者,对于 Nginx 稳定存储库:

echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.list

使用 update-alternatives 命令切换软件版本

update-alternatives 的功能,类似于在桌面系统上设置默认打开程序。

# update-alternatives --help
用法:update-alternatives [<选项> ...] <命令>

命令:
  --install <链接> <名称> <路径> <优先级>
    [--slave <链接> <名称> <路径>] ...
                           在系统中加入一组候选项。
  --remove <名称> <路径>   从 <名称> 替换组中去除 <路径> 项。
  --remove-all <名称>      从替换系统中删除 <名称> 替换组。
  --auto <名称>            将 <名称> 的主链接切换到自动模式。
  --display <名称>         显示关于 <名称> 替换组的信息。
  --query <名称>           机器可读版的 --display <名称>.
  --list <名称>            列出 <名称> 替换组中所有的可用候选项。
  --get-selections         列出主要候选项名称以及它们的状态。
  --set-selections         从标准输入中读入候选项的状态。
  --config <名称>          列出 <名称> 替换组中的可选项,并就使用其中
                           哪一个,征询用户的意见。
  --set <名称> <路径>      将 <路径> 设置为 <名称> 的候选项。
  --all                    对所有可选项一一调用 --config 命令。

<链接> 是指向 /etc/alternatives/<名称> 的符号链接。
    (如 /usr/bin/pager)
<名称> 是该链接替换组的主控名。
    (如 pager)
<路径> 是候选项目标文件的位置。
    (如 /usr/bin/less)
<优先级> 是一个整数,在自动模式下,这个数字越高的选项,其优先级也就越高。

选项:
  --altdir <目录>          改变候选项目录。
                             (默认是 /etc/alternatives)。
  --admindir <目录>        设置 statoverride 文件的目录。
                             (默认是 /var/lib/dpkg/alternatives)。
  --instdir <目录>         改变安装目录。
  --root <目录>            改变文件系统根目录。
  --log <文件>             改变日志文件。
  --force                  允许使用候选项链接替换文件。
  --skip-auto              在自动模式中跳过设置正确候选项的提示
                           (只与 --config 有关)
  --quiet                  安静模式,输出尽可能少的信息。
  --verbose                启用详细输出。
  --debug                  调试输出,信息更多。
  --help                   显示本帮助信息。
  --version                显示版本信息。

注册软件: update-alternatives –install

以jdk为例,安装了jdk以后,先要在update-alternatives工具中注册;

# update-alternatives --install /usr/bin/java java /opt/jdk1.8.0_91/bin/java 200
# update-alternatives --install /usr/bin/java java /opt/jdk1.8.0_111/bin/java 300

其中:

  • 第一个参数:--install表示向 update-alternatives 注册服务名。
  • 第二个参数:是注册最终地址,成功后将会把命令在这个固定的目的地址做真实命令的软链,以后管理就是管理这个软链;
  • 第三个参数:服务名,以后管理时以它为关联依据。
  • 第四个参数:被管理的命令绝对路径。
  • 第五个参数:优先级,数字越大优先级越高。

常用示例

显示程序的可替换信息:update-alternatives –display <名称>

 # 显示PHP程序的可替换信息
update-alternatives --display php

php - manual mode
  link best version is /usr/bin/php8.3
  link currently points to /usr/bin/php8.2
  link php is /usr/bin/php
  slave php.1.gz is /usr/share/man/man1/php.1.gz
/usr/bin/php8.2 - priority 82
  slave php.1.gz: /usr/share/man/man1/php8.2.1.gz
/usr/bin/php8.3 - priority 83
  slave php.1.gz: /usr/share/man/man1/php8.3.1.gz

注册添加程序的可替换信息:update-alternatives –install <名称>

# 设置PHP
update-alternatives --install /usr/bin/php php /usr/local/lsws/lsphp74/bin/php 74
update-alternatives --install /usr/bin/php php /usr/local/lsws/lsphp81/bin/php 81
update-alternatives --install /usr/bin/php php /usr/local/lsws/lsphp82/bin/php 82

列出 程序替换组中所有的可用选项:update-alternatives –list <名称>

update-alternatives --list php

/usr/bin/php8.2
/usr/bin/php8.3 

交互式修改:update-alternatives –config <名称> 显示可用选项的列表,选择对应的索引确认。

#设置默认浏览器
update-alternatives --config www-browser 

There is 1 choice for the alternative www-browser (providing /usr/bin/www-browser).

  Selection    Path            Priority   Status
------------------------------------------------------------
* 0            /usr/bin/w3m     25        auto mode
  1            /usr/bin/w3m     25        manual mode

Press <enter> to keep the current choice[*], or type selection number:

#设置默认使用的php版本
update-alternatives --config php

There are 2 choices for the alternative php (providing /usr/bin/php).

  Selection    Path             Priority   Status
------------------------------------------------------------
  0            /usr/bin/php8.3   83        auto mode
* 1            /usr/bin/php8.2   82        manual mode
  2            /usr/bin/php8.3   83        manual mode

Press <enter> to keep the current choice[*], or type selection number: 1

删除替代方案

用 remove 去掉编辑器组中的微替代品:

update-alternatives --remove editor /usr/bin/micro

可以直接使用下面的全部清除:

update-alternatives --remove-all java

解决 apt-get update 从 packages.sury.org 更新 apache2 与 php 时签名无效的错误

apt-get update 显示错误,从 packages.sury.org 更新 apache2 与 php 时签名无效,如下显示:

错误:6 https://packages.sury.org/apache2 bookworm InRelease
  下列签名无效: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key <deb@sury.org>

错误:7 https://packages.sury.org/php bookworm InRelease
  下列签名无效: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key <deb@sury.org>

------------------------------------------------------------------

Err:6 https://packages.sury.org/apache2 bookworm InRelease
The following signatures were invalid: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

Err:7 https://packages.sury.org/php bookworm InRelease
The following signatures were invalid: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: 校验数字签名时出错。此仓库未被更新,所以仍然使用此前的索引文件。GPG 错误:https://packages.sury.org/apache2 bookworm InRelease: 下列签名无效: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: 校验数字签名时出错。此仓库未被更新,所以仍然使用此前的索引文件。GPG 错误:https://packages.sury.org/php bookworm InRelease: 下列签名无效: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: 无法下载 https://packages.sury.org/apache2/dists/bookworm/InRelease 下列签名无效: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: 无法下载 https://packages.sury.org/php/dists/bookworm/InRelease 下列签名无效: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: 部分索引文件下载失败。如果忽略它们,那将转而使用旧的索引文件。

————————————————————————————————–

W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://packages.sury.org/apache2 bookworm InRelease: The following signatures were invalid: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://packages.sury.org/php bookworm InRelease: The following signatures were invalid: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: Failed to fetch https://packages.sury.org/apache2/dists/bookworm/InRelease The following signatures were invalid: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: Failed to fetch https://packages.sury.org/php/dists/bookworm/InRelease The following signatures were invalid: EXPKEYSIG B188E2B695BD4743 DEB.SURY.ORG Automatic Signing Key deb@sury.org

W: Some index files failed to download. They have been ignored, or old ones used instead.

解决办法:

参考 packages.sury.org 站点 README.txt 文件的脚本。

apache2 https://packages.sury.org/apache2/README.txt
php https://packages.sury.org/php/README.txt

apache2

apt-get update
apt-get -y install lsb-release ca-certificates curl
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
dpkg -i /tmp/debsuryorg-archive-keyring.deb
sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-apache2.gpg] https://packages.sury.org/apache2/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/apache2.list'
apt-get update

php

apt-get update
apt-get -y install lsb-release ca-certificates curl
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
dpkg -i /tmp/debsuryorg-archive-keyring.deb
sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
apt-get update

【END】

使用Certbot生成Let’s Encrypt证书,为OpenLiteSpeed 配置 SSL

使用 Let’s Encrypt 快速安装 SSL

手动certbot ,单独注册证书,最快获取Let’s Encrypt SSL证书的方式。。。

设置,先安装certbot

sudo apt-get update
sudo apt-get install certbot -y

以非交互式申请证书:

如要申请证书:example.com

certbot certonly --non-interactive --agree-tos -m demo@gmail.com --webroot -w /var/www/html -d example.com

要同时申请证书,并且:example.comwww.example.com

certbot certonly --non-interactive --agree-tos -m demo@gmail.com --webroot -w /var/www/html -d example.com -d www.example.com

OpenLiteSpeed 下的 SSL设置

  • 1. 如果我们只有一个证书,我们可以在侦听器级别设置它。

导航到 OpenLiteSpeed > Web 控制台> 侦听器> SSL > SSL 私钥和证书
设置以下值:

  • 私钥文件/etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
  • 证书文件/etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem

单击“保存”,然后执行“平滑重启”。

  • 2. 配置多个 SSL

虚拟主机中的SSL证书将覆盖侦听器,因此我们只需将证书添加到每个域的虚拟主机即可。

导航到 OpenLiteSpeed > Web 控制台>虚拟主机>您的虚拟主机> SSL > SSL 私钥和证书
设置以下值:

  • 私钥文件/etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
  • 证书文件/etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem

单击“保存”,然后执行平滑重启”。

OpenLiteSpeed 版本升级

CURRENT VERSION:OpenLiteSpeed 1.7.18   New Release: 1.7.19 (current branch)

登陆管理后台,左上角显示 【当前版本 OpenLiteSpeed 1.7.18 最新版本 1.7.19

使用apt-get upgrade openlitespeed 没有找到新版本,如何更新呢?

下载 lsup.sh:

wget https://raw.githubusercontent.com/litespeedtech/openlitespeed/master/dist/admin/misc/lsup.shchmod +x lsup.sh
./lsup.sh

或者 -v 指定版本安装:

/usr/local/lsws/admin/misc/lsup.sh -v 1.7.19

Running ./lsup.sh 将更新之最新版本。

screen后台运行|恢复会话|断开当前会话

screen 控制 ssh 远程会话命令不中断。避免终端窗口关闭/网络断开 后的 进程会话中断。

新建screen

screen -S your_screen_name

Ctrl + a, d :断开当前 screen 会话,但保持会话在后台运行。 Ctrl + a, k :关闭当前窗口或会话。

进入screen

screen -r your_screen_name

Ctrl+D # 在当前screen下,输入Ctrl+D,删除该screen
Ctrl+A,Ctrl+D # 在当前screen下,输入先后Ctrl+A,Ctrl+D,退出该screen

显示screen list

​​​​​​​screen -ls

连接状态为【Attached】的screen

解决恢复会话时出现 There is no screen to be resumed matching 的错误

screen -D -r your_screen_name # 解释:-D -r 先踢掉前一用户,再登陆

判断当前是否在screen中断下,Ubuntu系统,可以这样:

sudo vim /etc/screenrc

文件末尾追加一行即可允许设置screen标题

caption always "%{.bW}%-w%{.rW}%n %t%{-}%+w %=%H %Y/%m/%d "

删除指定screen, your_screen_name为待删除的screen name

​​​​​​​screen -S your_screen_name -X quit

使用service –status-all 或 systemctl status 查询服务状态时卡顿

问题:

在一个运行多年的 debian 系统中,使用 service --status-all 检查服务列表
输出到项目 "[ + ]  cron" 时,会卡顿很久,才继续显示其他服务名。

起初不知道为什么卡顿,今天看到一文,他在 systemctl status 某服务名 时卡顿很久,后来发现这个问题和systemd-journald有关。我感觉这个问题也有可能一样,老系统运行多年,记录的服务信息过多。决定也尝试清理一下journald。

单独查看service --status-all 中卡顿的 cron

systemctl status cron

果真,翻页几千行,才发现看了不到一个月的记录,太多了

cron 是定时执行任务的服务。各种计划任务的执行都会产生大量的服务启动日志。

可以看出 systemctl 输出服务启动时的相关信息。都是从systemd-journald中读取的。

进入文件夹 /var/log/journal,里面有一个长文件名的文件夹:f25c9fe2e9554b54950734c0b92298e0,里面是用户的 system.journal 文件,还有user-1000.journal。

ls -lhm --full-time /var/log/journal/f25c9fe2e9554b54950734c0b92298e0/

查看这个文件夹下文件大小,21,001,486,336 bytes 有19.5 GiB。

下面清理 journal 文件

先停止 systemd-journald 服务

systemctl stop systemd-journald

Warning: Stopping systemd-journald.service, but it can still be activated by:
  systemd-journald-dev-log.socket
  systemd-journald.socket
  systemd-journald-audit.socket

提示警告,为了保险,把这几个都停止掉:
systemctl stop systemd-journald-dev-log.socket
systemctl stop systemd-journald.socket
systemctl stop systemd-journald-audit.socket

然后可以删除 journal 文件(我直接全部删除了,重要的生产服务器不建议这样)
 

最后是要限制systemd-journald日志的大小,避免以后出现这个问题。

编辑文件: /etc/systemd/journald.conf

修改以下两个配置参数:

SystemMaxUse=100M
RuntimeMaxUse=100M

限制全部日志文件加在一起最多可以占用多少空间,相对的问题是,以后的日志记录总量也变少了。

最后重启 systemd-journald

systemctl restart systemd-journald

再次使用 service –status-all 或 systemctl status 查询服务状态时,就不再出现卡顿了。

systemctl daemon-reload #重新加载服务配置文件

[end]

在web页面中使用HTML  Symbol作为URL链接字符

看到一个地址链接中有个图标做链接锚点,尝试下确实可以打开。。。

查看代码里也是符号

<a href="https://api2u.me/tag/📚Curricula.html">📚Curricula</a>

然后直接复制进浏览器地址栏,也是可以的。Google Chrome,Microsoft Edge都可以粘贴。查询下

以前知道在html中显示转义符号,可没想过用Emoji 在代码里可以做连接符号。。。竟然浏览器也都支持了。。。

大致了解了下,我的理解是,其实使用Chromium内核的浏览器本身就可以看作一个 建构在web之上的应用容器,它已经将符号编码转义渲染了一次。所以可以在渲染代码之前就显示符号。

Link Emoji Meaning: Uses, How to Reply & More – wikihow.com

点击上面的链接,打开的页面地址栏会有带有下面图标链接的URL。

我先试试在CMD下的效果。

如果你安装了powershell 可以本机命令,wget https://xn--i-7iq.ws

在命令行窗口内也会显示符号图标

在 linux bash下 curl https://xn--i-7iq.ws 也可以显示,但是与cmd下完全不同。

📚 ❤️🔗

符号在各种编码下的编码

🔗https://www.htmlsymbol.com/unicode-code/1f517.html

Technical information
Name:Link Symbol
Symbol:🔗
Unicode Number:U+1F517
HTML Code:&#128279; &#x1F517;
HTML Entity:
CSS Code:\1F517
UTF-8 Encoding:0x1F517
UTF-16 Encoding:0x0001F517
UTF-32 Encoding:0x000000000001F517
Hex 2700-27BF / Decimal 9984-10175

https://www.w3schools.com/charsets/ref_utf_dingbats.asp

Example

<p>I will display &#9986;</p>
<p>I will display &#x2702;</p>

Will display as:

I will display ✂
I will display ✂

符号在HTML中的使用。

参考站点:

HTML Symbol Unicode symbols, html entities and codes https://www.htmlsymbol.com/

HTML Unicode (UTF-8) Reference https://www.w3schools.com/charsets/ref_html_utf8.asp

符号查询

EmojiXD®️ https://emojixd.com/

EmojiXD是一本线上Emoji百科全书📚,收录了所有emoji,帮你通过分类或者搜索轻松找到合适的表情符号,并了解各种emoji符号的相关知识和使用情景

Letterlike Symbols – https://www.unichar.app/web/

HTML Unicode (UTF-8) Reference – https://graphemica.com/🔗