Files
fanhongcai 1bfc72f9cb feat: 文传易匿名临时文件传输站初始版本
- 后端 .NET 8 + FreeSql + 阿里云 OSS,支持共享/私密/标签三种文件模式
- 前端 Vue3 + Vite + TDesign,上传/取件/管理三页
- 取件码/二维码/海报合成(含保存二维码为图片)
- deploy/ 部署文档与一键 FTP 发布脚本
- appsettings.json 含真实凭据,已 gitignore 不入库
2026-08-24 00:03:28 +08:00

109 lines
4.7 KiB
C#

using System.Text.RegularExpressions;
using WenChuanyi.Api.Models;
namespace WenChuanyi.Api.Services;
/// <summary>业务规则共享静态工具:密码/标签校验、模式判定、预览分类</summary>
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
/// <summary>清洗文件名:去除 Windows 非法字符/控制字符、首尾空格与点、保留名与超长限制,防止存储路径注入</summary>
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;
}
/// <summary>校验密码(4-12 位),null 表示通过</summary>
public static string? ValidatePassword(string? pwd)
{
if (string.IsNullOrEmpty(pwd)) return null;
if (pwd.Length < 4 || pwd.Length > 12)
{
return "密码长度须为 4-12 位";
}
return null;
}
/// <summary>校验标签:仅英文+数字;纯数字须 &gt;8 位;含英文须 &gt;4 位;null 表示通过</summary>
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;
}
/// <summary>模式判定:有密码→private;仅标签→tagged;否则 standard</summary>
public static string ResolveFileType(bool hasPassword, bool hasTag)
=> hasPassword ? "private" : hasTag ? "tagged" : "standard";
/// <summary>取件码位数:私密 6 位,其余 8 位</summary>
public static int PickCodeLength(string fileType) => fileType == "private" ? 6 : 8;
/// <summary>剩余有效期文本</summary>
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";
}