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
{
///
/// 族谱全系/小系 .fm 交换文件的规则:目录、文件名、系统因子。
/// 不新增配置节——目录与身份证/头像上传同根(FileServiceSettings:UploadFileDirectory),固定挂在 FamilyTree 子目录下
///
public static class FamilyTreeExchangePolicy
{
/// 交换目录名(身份证用 FamilyPersonAvatar/IdCard,这里同款机制)
public const string EXCHANGE_FOLDER_NAME = "FamilyTree";
public const string FILE_EXTENSION = ".fm";
/// D2 系统因子:前端 PBKDF2(用户密码⊕因子) 派生 AES 密钥,防「只漏密码」离线破解
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);
/// 备份目录名(挂在交换目录下)
public const string BACKUP_FOLDER_NAME = "Backup";
/// R5 备份保留窗口:列表与还原都只认一年内
public static readonly int BACKUP_KEEP_YEARS = 1;
///
/// 备份密文魔数前缀:备份含成员权限行(账号名等隐私),统一用服务端 AES 密钥加密落盘;
/// 前缀区分新老格式(无此前缀视为旧明文库,向后兼容),不新增配置节(复用 EncryptHelper 内置密钥)
///
public const string BACKUP_ENCRYPT_PREFIX = "ENC:";
/// 备份明文 JSON → 密文文本(写盘前调用)
public static string EncryptBackup(string plainJson)
{
return BACKUP_ENCRYPT_PREFIX + EncryptHelper.AesEncrypt(plainJson);
}
/// 备份文件文本 → 明文 JSON(带 ENC: 前缀解密;旧明文备份直接透传)
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);
}
/// 生成 D1 规范文件名
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}";
}
/// 校验文件名:只准 D1 两种格式,且拒绝任何目录成分(防 ../ 路径穿越)
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);
}
/// 从合法文件名反解析 (treeId, anchorPersonId, scope);非法返回 null
public static Tuple 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);
}
/// 生成备份文件名(谱号+秒级时间戳,同谱同秒重名由覆盖兼容:秒内连点两次导入的极小概率场景)
public static string BuildBackupFileName(string treeId)
{
return $"BK{treeId}_{DateTime.Now:yyyyMMddHHmmss}.json";
}
/// 校验备份文件名:只准 BK 格式,拒绝任何目录成分(还原入口防路径穿越/拿 *.fm 灌数据)
public static bool IsValidBackupFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return false;
if (fileName != Path.GetFileName(fileName)) return false;
return BACKUP_FILE.IsMatch(fileName);
}
/// 从合法备份文件名反解析 (treeId, 备份时刻);非法返回 null。时刻以文件名为准(不依赖文件系统时间戳)
public static Tuple 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);
}
}
}