- 应用
- 桌面应用
- CLI
- 存储
【免费下载链接】duplicati
Store securely encrypted backups in the cloud!
本文围绕 Duplicati 仓库中 BuildTools/LicenseUpdater/README.md 所描述的许可证更新工具,从"一行命令完成全仓库年度版权刷新"出发,结合 Program.cs 与 Fragments.cs 的源码实现,逐层拆解它的运行机制、文件筛选规则、正则匹配策略与扩展方式。读完本文,你将掌握如何在本仓库(或任何采用 MIT 许可的 .NET 仓库)中每年一次性批量更新所有源文件的版权年份与许可证头,并能按需自定义版权持有人、扩充目标文件类型。
工具定位:为什么需要一个"一年只跑一次"的许可证更新工具
Duplicati 是一个采用 MIT 许可的开源备份项目(见仓库根目录 LICENSE),其代码库由数百个.cs与.csproj文件组成。按照 MIT 协议的要求,每个源文件头部都应保留版权声明与许可文本;同时,<Copyright>年份需要随新年更迭而更新。如果靠人工逐一修改,几百个文件的版权年份维护既繁琐又极易遗漏。
BuildTools/LicenseUpdater/README.md 用一句话点明了该工具的定位:
This folder contains a small tool that can update the license in all files, using the current year as the copyright year. It is intended to be run once a year, using the root repository folder as input.
也就是说,这是一个面向维护者的年度例行工具:每年运行一次,以仓库根目录为输入,把所有文件中的版权年份统一更新为当前年份,然后由维护者审阅(review)并提交(commit)变更。它并不参与 Duplicati 的运行时逻辑,而是独立的构建/维护辅助程序,位于BuildTools/LicenseUpdater/目录下。
快速上手:三条命令完成年度版权刷新
按 README 的说明,工具的使用方式极其简单。将仓库克隆到本地后,进入BuildTools/LicenseUpdater目录,执行:
dotnet run ../../其中../../是相对路径,指向仓库根目录(即同时包含Duplicati/、BuildTools/、LICENSE等内容的文件夹)。工具会递归扫描该目录下所有符合条件的文件,把其中已有的版权声明年份替换为当前 UTC 年份,并补齐缺失的许可证头。
如果你需要自定义版权持有人(默认是Team Duplicati),可以传入第二个参数:
dotnet run ../../ "Your Name / Company"这一点与 Program.cs 的参数校验逻辑完全对应:
var cmdargs = Environment.GetCommandLineArgs(); if (cmdargs.Length != 2 && cmdargs.Length != 3) throw new Exception($"Usage: dotnet run <path-to-source-folder> [\"copyright name\"]"); if (cmdargs.Length == 3) Fragments.CopyrightHolder = cmdargs[2];注意两个约束:
- 参数数量必须是 2 或 3 个(不含可执行文件本身),否则直接抛出带用法说明的异常;
- 第一个参数必须是真实存在的目录:
Path.GetFullPath解析为绝对路径后,若Directory.Exists返回 false,会抛出Start path not found异常(见 Program.cs)。
README 还强调,运行产生的变更"can then be reviewed and committed"——即不要在无人审阅的情况下直接提交,建议先git diff检查再入库。
运行机制:一次扫描、三类文件、三种处理策略
从 Program.cs 的源码结构看,工具的整体流程可以概括为"1 次根 LICENSE 重写 + 1 次全目录递归扫描 + 3 类文件分别处理"。
1. 重写仓库根LICENSE文件
工具的第一步是直接重写输入目录下的LICENSE文件(Program.cs):
File.WriteAllText(Path.Combine(startpath, "LICENSE"), Fragments.LICENSE_FILE_HEADER + Fragments.GetLicenseTextWithPrefixedLines(string.Empty));其中LICENSE_FILE_HEADER是"MIT License\n\n",而GetLicenseTextWithPrefixedLines(string.Empty)表示用空字符串作为行前缀(即不添加//),生成纯文本形式的 MIT 许可证正文。合并结果即仓库根目录 LICENSE 的典型形态:首行MIT License,随后是完整的 MIT 许可文本。
2. 扫描范围与文件筛选规则
工具通过Directory.EnumerateFiles(startpath, "*", SearchOption.AllDirectories)递归枚举所有文件,再按扩展名、目录黑名单和文件大小三层过滤(Program.cs):
var target_extensions = new[] { ".cs", ".csproj" // ".html", // ".js", // ".css" }.ToHashSet(StringComparer.OrdinalIgnoreCase);当前启用处理的扩展名只有.cs和.csproj;.html、.js、.css处于注释掉的状态,说明作者预留了扩展能力,需要时取消注释即可纳入扫描。筛选规则具体包括:
| 规则 | 行为 | 依据 |
|---|---|---|
| 扩展名过滤 | 只处理.cs与.csproj(大小写不敏感) | Program.cs |
| 目录黑名单 | 路径中包含bin/、obj/、packages/子目录的文件一律跳过 | Program.cs |
| 文件名黑名单 | 跳过所有AssemblyInfo.cs(避免破坏自动生成的程序集信息) | Program.cs |
| 文件大小上限 | 超过 10 MiB(10 * 1024 * 1024字节)的文件跳过,并打印skipped due to size | Program.cs |
3. 已含许可证头的文件:正则匹配与就地替换
对于通过筛选的文件,工具首先尝试用Fragments.MatchAndReplace(ref data)匹配并替换已有许可证块(Program.cs)。若匹配成功,文件被就地重写。这套匹配逻辑的具体正则见下文"核心实现细节"一节。
4. 缺失许可证头的.cs文件:头部追加
若正则匹配失败,且文件是.cs且不以// Copyright开头(大小写不敏感),工具会检测文件的换行风格,再把许可证文本作为头部整体插入到文件最前面(Program.cs):
var linefeed = data.Contains("\r\n") ? "\r\n" : data.Contains("\r") ? "\r" : "\n"; data = Fragments.GetLicenseTextWithPrefixedLines(linefeed: linefeed) + data;这里的换行检测依次判断 CRLF(Windows)、CR(老 Mac)、LF(Unix),确保插入的许可证头与文件原有换行风格保持一致——这是避免后续git diff出现整文件被误改的关键细节。
5..csproj文件:<Copyright>元素更新
对.csproj文件,工具使用CSPROJ_COPYRIGHT_MATCH正则(<Copyright>(.*?)</Copyright>)定位 XML 中的 Copyright 元素,并替换为统一的版权文本(Program.cs):
data = Fragments.CSPROJ_COPYRIGHT_MATCH.Replace(data, $"<Copyright>{Fragments.CopyrightText}</Copyright>");这与仓库中实际 csproj 文件的内容完全吻合,例如 Duplicati/Agent/Duplicati.Agent.csproj 中即为:
<Copyright>Copyright © 2026 Team Duplicati, MIT license</Copyright>CopyrightText由 Fragments.cs 动态生成:
public static readonly string CopyrightText = $"Copyright © {DateTime.UtcNow.Year.ToString()} {Fragments.CopyrightHolder}, MIT license";年份取DateTime.UtcNow.Year,因此无论哪个时区运行,都以 UTC 年份为准。
若上述策略全部未命中,工具会打印skipped, no match并跳过该文件(Program.cs),便于维护者事后排查哪些文件未能自动更新。
核心实现细节:Fragments.cs 中的许可证文本与正则
Fragments.cs 是工具的"许可证引擎",集中定义了模板文本、版权持有人和全部匹配正则。
许可证文本模板与占位符替换
工具内置的 MIT 模板使用两个占位符:YYYY(年份)与$HOLDER(持有人)。模板在类初始化时用当前 UTC 年份替换YYYY(Fragments.cs):
public static string NEW_LICENSE = @"Copyright (c) YYYY $HOLDER Permission is hereby granted, free of charge, ...(MIT 标准文本) ...".Replace("YYYY", DateTime.UtcNow.Year.ToString());而GetLicenseTextWithPrefixedLines在生成每行时再把$HOLDER替换为实际的版权持有人(默认Team Duplicati),并为每行加上//前缀(Fragments.cs):
public static string GetLicenseTextWithPrefixedLines(string linePrefix = "// ", string linefeed = "\n") => string.Join(linefeed, Fragments.NEW_LICENSE.Replace("$HOLDER", Fragments.CopyrightHolder) .Split("\n").Select(x => linePrefix + x).Append(string.Empty));注意CopyrightHolder是静态可变字段(Fragments.cs),默认值为"Team Duplicati",运行时通过命令行参数覆盖。仓库中各.cs文件头部的标准形态与之对应,例如 Duplicati/License/LicenseEntry.cs:
// Copyright (C) 2026, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, ... // DEALINGS IN THE SOFTWARE.四组匹配正则与历史许可证清理
Fragments.cs 定义了四组正则,用于识别不同历史形态的许可证头:
| 正则 | 匹配目标 | 说明 |
|---|---|---|
CS_REGION_MATCH | #region Disclaimer / License ... #endregion且内容含Free Software Foundation | 用于清理旧版 LGPL 风格的#region版权块 |
CS_LGPL_MATCH | 旧式// Copyright (C) YYYY, The Duplicati Team ... Free Software Foundation ... Boston, MA 0211x-130x USA | LGPL 旧头的特征(含 FSF 地址) |
CS_MIT_MATCH | // Copyright (C) YYYY, The Duplicati Team ... IN THE SOFTWARE. | 现有 MIT 注释头 |
CS_MIT_MATCH2 | 同CS_MIT_MATCH,但匹配串中的持有人由CopyrightHolder动态替换 | 支持自定义持有人后的变体匹配 |
而MatchAndReplace的实际匹配循环(Fragments.cs)只遍历了三个正则:
foreach (var m in new[] { CS_REGION_MATCH, CS_MIT_MATCH, CS_MIT_MATCH2 })从源码结构看,CS_LGPL_MATCH虽然被定义,但并未出现在该匹配数组中,属于保留的备用正则;真正参与替换的是 region 块与两种 MIT 头。匹配成功后,工具会把命中的许可证区间整体替换为新的 MIT 文本。
换行符与尾随换行的精细处理
MatchAndReplace中还有一个容易被忽略的细节(Fragments.cs):如果被替换区间的末尾紧跟着\n或\r\n,则新的许可证文本会先TrimEnd(),避免替换后出现重复空行:
if (data[(g.Index + g.Length)..].StartsWith('\n') || data[(g.Index + g.Length)..].StartsWith("\r\n")) newLicense = newLicense.TrimEnd();随后通过字符串拼接(data.Substring(0, g.Index) + newLicense + data.Substring(g.Index + g.Length))完成原地替换,其余文件内容保持不变。
与仓库许可证生态的衔接
LicenseUpdater 负责"写",而仓库中还配套了"读"许可证的运行时机制,理解二者能帮助你把握整个许可证维护闭环。
根 LICENSE 与 proprietary 目录
工具重写后的根 LICENSE 采用标准 MIT 文本,并附带了 Duplicati 特有的三段补充说明:
proprietary/目录下的内容遵循 proprietary/LICENSE 定义的专有许可;- 第三方组件遵循其原始许可证(对应仓库 thirdparty/ 下各组件目录);
- 客户端 JavaScript 以 MIT 许可发布。
也就是说,根 LICENSE 是"MIT + 例外条款"的复合声明,工具重写时仅刷新版权年份与 MIT 正文,不触碰这些例外说明以外的内容。
运行时许可证读取机制
在运行时侧,Duplicati/License/LicenseReader.cs 提供了读取第三方组件许可信息的工具:它在指定目录下按文件夹扫描license.txt(必选)、homepage.txt/download.txt(可选 URL)与licensedata.json(可选元数据),再通过 LicenseEntry.cs 封装为组件条目。这与 thirdparty/ 目录下各组件"3 个 txt/json 文件"的组织方式一一对应。完整的组件致谢清单见 Duplicati/License/acknowledgements.txt。LicenseUpdater 更新的是"我们自己的源码许可",而 LicenseReader 管理的是"引用的第三方组件许可",两者共同构成 Duplicati 的许可证管理体系。
使用建议与注意事项
结合 README 说明与源码实现,给出如下实操建议:
运行前:
- 确保仓库处于干净的、可回滚的状态(如先创建分支或确认
git status无未提交改动),便于事后审阅 diff; - 确认 .NET SDK 版本满足 LicenseUpdater.csproj 中声明的目标框架(当前为
net10.0,OutputType为Exe,启用了ImplicitUsings与Nullable); - 如需自定义版权持有人,准备好名称字符串作为第二参数。
运行后:
- 使用
git diff/git status逐项检查:预期变更包括根LICENSE年份、各.cs文件头部的Copyright (C) YYYY、各.csproj的<Copyright>元素; - 重点关注被追加许可证头的新文件(原本缺失头部)以及
skipped, no match的输出行; - 确认无误后再提交,不要跳过人工审阅环节。
扩展其他文件类型:
如果希望将 HTML、JS、CSS 等文件也纳入年度刷新,只需在 Program.cs 中取消对应扩展名的注释即可;但需要注意,这些语言无法使用//注释前缀的 C# 风格头部,需要额外定制GetLicenseTextWithPrefixedLines的前缀参数(例如 HTML 的<!-- ... -->、CSS 的/* ... */),从源码结构看该函数已预留了linePrefix参数,具备直接扩展的基础。
总而言之,LicenseUpdater 是 Duplicati 维护体系中一个小而精的工具:它把"每年更新全仓库版权"这一机械、易错的工作收敛成一条确定性的命令,并通过精心设计的筛选规则、换行检测与正则匹配,保证批量改写既全面又不破坏原有格式。对于任何同样采用 MIT 许可且由大量 C# 源文件构成的仓库,这套实现都值得作为年度版权维护的参考模板。
- 应用
- 桌面应用
- CLI
- 存储
【免费下载链接】duplicati
Store securely encrypted backups in the cloud!
相关推荐
探索KYCircleMenu核心功能:动画效果与按钮交互全解析
探索KYCircleMenu核心功能:动画效果与按钮交互全解析 KYCircleMenu是一款专为iOS开发者打造的圆形菜单UI控件,能够根据按钮数量(1~6个
Godot Engine 许可证合规指南:MIT 许可要求、第三方版权与 Engine API 实操
Godot Engine 许可证合规指南:MIT 许可要求、第三方版权与 Engine API 实操 导读 本篇指南围绕 Godot Engine 官方文档中的
文档教程游戏开发老 Mac 如何安装最新 macOS:OpenCore Legacy Patcher 实操教程
老 Mac 如何安装最新 macOS:OpenCore Legacy Patcher 实操教程 软件更新页面提示"此 Mac 不受此版本 macOS 支持"的那
操作系统固件驱动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考