123 lines
6.4 KiB
C#
123 lines
6.4 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|