news 2026/10/3 1:57:50

Duplicati LicenseUpdater 实战指南:用 dotnet 工具批量刷新仓库版权年份与 MIT 许可证头

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Duplicati LicenseUpdater 实战指南:用 dotnet 工具批量刷新仓库版权年份与 MIT 许可证头
  • 应用
  • 桌面应用
  • CLI
  • 存储

【免费下载链接】duplicati

Store securely encrypted backups in the cloud!

项目地址:https://gitcode.com/gh_mirrors/du/duplicati
点击查看免费下载

本文围绕 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 sizeProgram.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 USALGPL 旧头的特征(含 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!

项目地址:https://gitcode.com/gh_mirrors/du/duplicati
点击查看免费下载
上一篇:端到端文件传输终极指南:3分钟掌握安全P2P传输工具
下一篇:RPCS3:当开源精神遇见PlayStation 3的复杂灵魂

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 1:55:32

猫抓资源嗅探扩展:网页视频提取,3 分钟搞定

猫抓资源嗅探扩展&#xff1a;网页视频提取&#xff0c;3 分钟搞定 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 地址栏没有直链&#xff0c;下载…

作者头像 李华
网站建设 2026/10/3 1:54:08

darktable 完整指南:免费开源 RAW 处理器的安装与实操

darktable 完整指南&#xff1a;免费开源 RAW 处理器的安装与实操 【免费下载链接】darktable darktable is an open source photography workflow application and raw developer 项目地址: https://gitcode.com/GitHub_Trending/da/darktable RAW 文件动态范围大。未经…

作者头像 李华
网站建设 2026/10/3 1:51:39

Caddy HTTPS 证书链报错?3 条命令定位并强制续期修复

Caddy HTTPS 证书链报错&#xff1f;3 条命令定位并强制续期修复 【免费下载链接】caddy Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS 项目地址: https://gitcode.com/GitHub_Trending/ca/caddy 今天凌晨&#xff0c;生产环境的 Ca…

作者头像 李华