Files
Docker/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeUtility/FamilyTreeExchangePolicy.cs
T
2026-10-20 15:44:56 +08:00

123 lines
6.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Master.Data;
using CloudBuilder.Security.Policy;
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace CloudBuilder.Master.Service.FamilyTreeUtility
{
/// <summary>
/// 族谱全系/小系 .fm 交换文件的规则:目录、文件名、系统因子。
/// 不新增配置节——目录与身份证/头像上传同根(FileServiceSettings:UploadFileDirectory),固定挂在 FamilyTree 子目录下
/// </summary>
public static class FamilyTreeExchangePolicy
{
/// <summary>交换目录名(身份证用 FamilyPersonAvatar/IdCard,这里同款机制)</summary>
public const string EXCHANGE_FOLDER_NAME = "FamilyTree";
public const string FILE_EXTENSION = ".fm";
/// <summary>D2 系统因子:前端 PBKDF2(用户密码⊕因子) 派生 AES 密钥,防「只漏密码」离线破解</summary>
public const string EXCHANGE_FACTOR = "YouJiaLa.FamilyTree.Exchange.v1";
// D1 文件名:全系 A{treeId}_{ts}.fm;小系 P{treeId}-{personId}_{ts}.fm(ts=yyyyMMddHHmmss)
private static readonly Regex ALL_FILE = new Regex(@"^AT\w+_\d{14}\.fm$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SUB_FILE = new Regex(@"^PT\w+-P\w+_\d{14}\.fm$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// R6 备份文件名:BK{treeId}_{ts}.json(替换前自动落盘的整谱回滚点,存 Backup 子目录,不进交换文件白名单)
private static readonly Regex BACKUP_FILE = new Regex(@"^BKT\w+_\d{14}\.json$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>备份目录名(挂在交换目录下)</summary>
public const string BACKUP_FOLDER_NAME = "Backup";
/// <summary>R5 备份保留窗口:列表与还原都只认一年内</summary>
public static readonly int BACKUP_KEEP_YEARS = 1;
/// <summary>
/// 备份密文魔数前缀:备份含成员权限行(账号名等隐私),统一用服务端 AES 密钥加密落盘;
/// 前缀区分新老格式(无此前缀视为旧明文库,向后兼容),不新增配置节(复用 EncryptHelper 内置密钥)
/// </summary>
public const string BACKUP_ENCRYPT_PREFIX = "ENC:";
/// <summary>备份明文 JSON → 密文文本(写盘前调用)</summary>
public static string EncryptBackup(string plainJson)
{
return BACKUP_ENCRYPT_PREFIX + EncryptHelper.AesEncrypt(plainJson);
}
/// <summary>备份文件文本 → 明文 JSON(带 ENC: 前缀解密;旧明文备份直接透传)</summary>
public static string DecryptBackup(string fileContent)
{
if (string.IsNullOrEmpty(fileContent)) return fileContent;
return fileContent.StartsWith(BACKUP_ENCRYPT_PREFIX)
? EncryptHelper.AesDecrpt(fileContent.Substring(BACKUP_ENCRYPT_PREFIX.Length))
: fileContent;
}
public static string GetExchangeDirectory(IApplicationService service)
{
return Path.Combine(ServiceSettingsPolicy.GetUploadFileDirectory(service), EXCHANGE_FOLDER_NAME);
}
/// <summary>生成 D1 规范文件名</summary>
public static string BuildFileName(string scope, string treeId, string anchorPersonId)
{
string ts = DateTime.Now.ToString("yyyyMMddHHmmss");
return scope == FamilyTreeExchangeScope.SUB
? $"P{treeId}-{anchorPersonId}_{ts}{FILE_EXTENSION}"
: $"A{treeId}_{ts}{FILE_EXTENSION}";
}
/// <summary>校验文件名:只准 D1 两种格式,且拒绝任何目录成分(防 ../ 路径穿越)</summary>
public static bool IsValidFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return false;
if (fileName != Path.GetFileName(fileName)) return false;
return ALL_FILE.IsMatch(fileName) || SUB_FILE.IsMatch(fileName);
}
/// <summary>从合法文件名反解析 (treeId, anchorPersonId, scope);非法返回 null</summary>
public static Tuple<string, string, string> ParseFileName(string fileName)
{
if (!IsValidFileName(fileName)) return null;
// 去掉前缀字母与时间戳,取中间的 id 段
string body = fileName.Substring(1, fileName.Length - 1 - FILE_EXTENSION.Length).Split('_')[0];
if (fileName[0].ToString().Equals("A", StringComparison.OrdinalIgnoreCase))
return Tuple.Create(body, "", FamilyTreeExchangeScope.ALL);
string[] parts = body.Split('-');
return Tuple.Create(parts[0], parts[1], FamilyTreeExchangeScope.SUB);
}
public static string GetBackupDirectory(IApplicationService service)
{
return Path.Combine(GetExchangeDirectory(service), BACKUP_FOLDER_NAME);
}
/// <summary>生成备份文件名(谱号+秒级时间戳,同谱同秒重名由覆盖兼容:秒内连点两次导入的极小概率场景)</summary>
public static string BuildBackupFileName(string treeId)
{
return $"BK{treeId}_{DateTime.Now:yyyyMMddHHmmss}.json";
}
/// <summary>校验备份文件名:只准 BK 格式,拒绝任何目录成分(还原入口防路径穿越/拿 *.fm 灌数据)</summary>
public static bool IsValidBackupFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return false;
if (fileName != Path.GetFileName(fileName)) return false;
return BACKUP_FILE.IsMatch(fileName);
}
/// <summary>从合法备份文件名反解析 (treeId, 备份时刻);非法返回 null。时刻以文件名为准(不依赖文件系统时间戳)</summary>
public static Tuple<string, DateTime> ParseBackupFileName(string fileName)
{
if (!IsValidBackupFileName(fileName)) return null;
string body = fileName.Substring(2, fileName.Length - 2 - ".json".Length);
string[] parts = body.Split('_');
if (!DateTime.TryParseExact(parts[1], "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime ts))
return null;
return Tuple.Create(parts[0], ts);
}
}
}