using System.Text.RegularExpressions;
using WenChuanyi.Api.Models;
namespace WenChuanyi.Api.Services;
/// 业务规则共享静态工具:密码/标签校验、模式判定、预览分类
public static class FileRules
{
public const long MaxFileBytes = 200L * 1024 * 1024; // 200MB
public const long MaxTextPreview = 2L * 1024 * 1024; // 文本预览 2MB
public const long MaxPdfImagePreview = 30L * 1024 * 1024; // PDF/图片预览 30MB
/// 清洗文件名:去除 Windows 非法字符/控制字符、首尾空格与点、保留名与超长限制,防止存储路径注入
public static string SanitizeFileName(string? name)
{
if (string.IsNullOrWhiteSpace(name)) return "未命名";
var dot = name.LastIndexOf('.');
var ext = dot >= 0 ? name[(dot + 1)..] : "";
var baseName = dot >= 0 ? name[..dot] : name;
var cleanBase = new string(baseName
.Where(c => c >= 32 && c != 127 && c != '<' && c != '>' && c != ':' && c != '"'
&& c != '/' && c != '\\' && c != '|' && c != '?' && c != '*')
.ToArray()).Trim().TrimEnd('.', ' ');
var result = string.IsNullOrEmpty(cleanBase) ? "未命名" : cleanBase;
// Windows 保留设备名:CON PRN AUX NUL COM1-9 LPT1-9(含扩展名也禁止)
var upperBase = result.Split('.')[0].ToUpperInvariant();
if (upperBase is "CON" or "PRN" or "AUX" or "NUL"
|| Regex.IsMatch(upperBase, @"^COM[1-9]$") || Regex.IsMatch(upperBase, @"^LPT[1-9]$"))
{
result = "_" + result;
}
// 长度限制(保留扩展名,主体最长 180 字符)
if (result.Length > 180) result = result[..180];
// 扩展名仅保留字母/数字/点,最长 10 字符
var extRaw = new string(ext.Where(c => char.IsLetterOrDigit(c) || c == '.').ToArray()).TrimStart('.');
var cleanExt = extRaw.Length > 0 ? "." + (extRaw.Length > 10 ? extRaw[..10] : extRaw) : "";
return result + cleanExt;
}
/// 校验密码(4-12 位),null 表示通过
public static string? ValidatePassword(string? pwd)
{
if (string.IsNullOrEmpty(pwd)) return null;
if (pwd.Length < 4 || pwd.Length > 12)
{
return "密码长度须为 4-12 位";
}
return null;
}
/// 校验标签:仅英文+数字;纯数字须 >8 位;含英文须 >4 位;null 表示通过
public static string? ValidateTag(string? tag)
{
if (string.IsNullOrEmpty(tag)) return null;
if (!Regex.IsMatch(tag, "^[A-Za-z0-9]+$"))
{
return "标签仅允许英文与数字,不能包含符号或空格";
}
if (Regex.IsMatch(tag, "^[0-9]+$"))
{
if (tag.Length <= 8)
{
return "纯数字标签须超过 8 位(建议使用 11 位手机号码)";
}
}
else if (tag.Length <= 4)
{
return "含英文字母的标签须超过 4 位";
}
return null;
}
/// 模式判定:有密码→private;仅标签→tagged;否则 standard
public static string ResolveFileType(bool hasPassword, bool hasTag)
=> hasPassword ? "private" : hasTag ? "tagged" : "standard";
/// 取件码位数:私密 6 位,其余 8 位
public static int PickCodeLength(string fileType) => fileType == "private" ? 6 : 8;
/// 剩余有效期文本
public static string ExpireText(FileItem f)
{
if (f.IsPermanent || f.ExpiresAt == null) return "永久";
var remain = f.ExpiresAt.Value - DateTime.Now;
if (remain <= TimeSpan.Zero) return "已过期";
if (remain.TotalDays >= 1) return $"{(int)remain.TotalDays} 天";
if (remain.TotalHours >= 1) return $"{(int)remain.TotalHours} 小时";
return $"{(int)Math.Max(1, remain.TotalMinutes)} 分钟";
}
public static bool IsText(string ext)
=> ext is ".txt" or ".md" or ".xml" or ".json" or ".csv" or ".log" or ".ini" or ".conf"
or ".srt" or ".html" or ".htm" or ".css" or ".js" or ".ts" or ".cs" or ".java" or ".sql"
or ".yml" or ".yaml" or ".sh" or ".bat" or ".ps1" or ".py" or ".go" or ".c" or ".h" or ".php";
public static bool IsPdfOrImage(string mime)
=> mime is "application/pdf"
or "image/png" or "image/jpeg" or "image/gif" or "image/webp" or "image/bmp" or "image/svg+xml";
public static bool IsVideoWhitelist(string ext) => ext is ".mp4" or ".webm";
public static bool IsAudioWhitelist(string ext) => ext is ".mp3" or ".wav" or ".m4a" or ".aac" or ".ogg";
}