NAS-FTP上传测试工具

从「上传失败」到「链路打通」:用 NasFtpUploadTester 验证 NAS FTP 上传

面向场景:业务站点要把资源文件存到 NAS,再用独立 IIS 资源站对外提供访问。
工具位置:NasFtpUploadTester
技术栈:.NET 8 控制台 + FluentFTP + Microsoft.Extensions.Configuration


一、为什么要单独做一个控制台测试程序?

在资源中心项目里,文件最终要落到 NAS 上。线上配置里常见两种「路径」:

类型示例用途
UNC(Windows 共享)\\Z423-4UWJ\共享名\IIS130IIS / PhysicalFileProvider 读文件
FTPftp://Z423-4UWJ/.../IIS130/Files0/...程序写文件(跨网段、凭据清晰)

问题在于:

  1. UNC 和 FTP 不是一回事——FTP 通,不代表 \\服务器\共享 一定能写。
  2. 业务站点体量大,一旦上传失败,要在完整 Web 链路里排查(IIS、应用池、静态目录、业务代码)成本很高。
  3. 需要一份 可拷贝到部署机、双击就能跑 的探针,把「网络 / 登录 / 路径 / 权限 / 传输模式」逐步拆开验证。

于是有了 NasFtpUploadTester:一个独立 Console,只做一件事——证明「从这台机器,用这组账号,能不能把文件 FTP 到 NAS 指定目录」


二、工具做了什么(v2.2)

程序按固定步骤输出彩色日志,失败时给出排查建议:

[1/6] 检测网络连通性          Ping + TCP:21
[2/6] 对照测试:UNC 写入      与生产 PhysicalPath 对照(可失败,不影响 FTP)
[FTP] 尝试模式 / 多基路径      被动明文优先;自动探测可写目录
[目录] 列出 FTP 根目录         核对 RemoteBasePath 是否填错
[上传] STOR 到目标路径         自动建目录
[6/6] 验证远端文件             FileExists + 大小比对

关键能力:

  • 配置驱动appsettings.json,可用 --config / --file 覆盖。
  • 未指定本地文件时自动生成约 64KB 测试文件。
  • 开启 FluentFTP 控制台协议日志,便于对照 PASV / STOR / 553 / 501
  • 自动重试:被动模式优先(该 NAS 主动模式常返回 501 security requirements)。
  • 多路径候选:根目录无写权限时,会尝试 sata11-xxx/IIS130 等基路径。
  • UNC 对照:提醒「生产若配 UNC,当前机器是否真能访问共享」。

三、目录与发布

NasFtpUploadTester/
├── NasFtpUploadTester.csproj
├── Program.cs                 # 主流程
├── appsettings.json           # 连接与路径配置
├── README.md                  # 简要使用说明
└── publish/                   # dotnet publish 输出

发布到部署机:

cd NasFtpUploadTester
dotnet publish .\NasFtpUploadTester.csproj -c Release -o .\publish

publish 整夹拷到目标机器(例如 C:\Sites\public),执行:

.\NasFtpUploadTester.exe

可选参数:

.\NasFtpUploadTester.exe --config appsettings.json --file D:\demo\test.jpg

四、配置项说明

appsettings.json 核心节点:

{
  "FileServer": {
    "UploadPath": "Files0"
  },
  "NasFtpTester": {
    "Host": "Z423-4UWJ",
    "Port": 21,
    "UserName": "你的FTP账号",
    "Password": "你的FTP密码",
    "UseSsl": false,
    "PassiveMode": true,
    "UploadPathKey": "Files0",
    "RemoteBasePath": "sata11-18914015405/IIS130",
    "AutoRetryModes": true,
    "UncTestPath": "\\\\Z423-4UWJ\\真实SMB共享名\\IIS130"
  }
}
字段含义
Host优先用 主机名,不要死写可能漂移的内网 IP
PassiveMode建议 true;主动模式易被 NAS 安全策略拒绝
RemoteBasePathFTP 侧基目录(不等于 SMB 共享名)
UploadPathKey与业务一致,一般为 Files0
UncTestPath仅作对照;失败只说明 SMB 不通,不代表 FTP 失败

最终远端路径:

/{RemoteBasePath}/{UploadPathKey}/{yyyyMMdd}/{HHmmss_文件名}

成功示例:

/sata11-18914015405/IIS130/Files0/20260819/172018_nas_ftp_test_....txt

对应 IIS 资源站访问(虚拟目录 Files0 → UNC ...\IIS130\Files0):

http://部署机IP:8081/Files0/20260819/172018_nas_ftp_test_....txt

五、实战踩坑(真实排查记录)

下面这些问题,都是在部署机上用本工具一点点打出来的,值得单独记一笔。

1. IP 不通,主机名通

Host = 192.168.112.20   → Ping/TCP:21 失败
Host = Z423-4UWJ        → TCP:21 成功(实际解析到 192.168.113.3)

结论: FTP Host 用 NAS 主机名;IP 可能已变更或跨网段策略不同。

2. 501 Your request does not meet the configured security requirements

出现在 主动模式 PORT

Command:  PORT 192,168,112,100,...
Response: 501 Your request does not meet the configured security requirements

结论: 该 NAS(日志里是 ZFileShare)禁止主动模式;固定被动 PASV

3. 登录成功,但上传 553 no upload permission

根因是 路径写错。FTP 根目录实际是:

/public
/sata11-18914015405
/ftp
/share

/Files0/... 写会被拒绝;正确基路径是:

/sata11-18914015405/IIS130/Files0/...

工具会在登录后 MLSD,并自动尝试多个基路径——这是它比「盲传一次」更有价值的地方。

4. FTPS 不可用

AUTH TLS → 550 Cannot get a TLS config: no TLS certificate configured

结论: 明文 FTP + 被动模式即可,不必强开 SSL。

5. FTP 目录名 ≠ SMB 共享名

这是后续配 IIS 时最容易踩的坑:

通道名称说明
FTPsata11-18914015405上传路径用这个
SMBnet view 里看到的共享名(可能不同)IIS 虚拟目录物理路径用这个

net use \\服务器\FTP目录名 可能报「找不到网络路径」,换 真实共享名 后成功——两边名字不要混用。

6. 上传成功的标志

日志出现类似内容即可认为探针通过:

✓ 上传成功!耗时 xx ms
✓ 成功路径: /sata11-.../IIS130/Files0/日期/文件
✓ 文件大小一致,FTP 上传验证通过!

六、和业务系统怎么衔接

探针通过之后,业务侧可以按同一约定落地:

  1. :API 通过 FTP 上传到
    /{FTP账号可见目录}/IIS130/Files0/{日期}/{文件}
  2. :独立 IIS 资源站(如 :8081)用 UNC 挂载
    \\NAS\SMB共享名\IIS130\Files0
  3. 库里存的相对路径仍是 /Files0/...,对外拼资源站域名:
    http://资源站:8081/Files0/...

本仓库后续已在 FileServer 中增加 Ftp + PublicBaseUrl 配置,与本测试程序验证过的参数对齐。


七、推荐使用流程(Checklist)

  1. 在开发机 dotnet publish,拷到 真正跑业务的那台部署机
  2. appsettings.jsonHost 用主机名,RemoteBasePath 先可留空,看根目录列表。
  3. 跑一遍,根据目录列表把 RemoteBasePath 改成可写路径(通常含 IIS130)。
  4. 确认上传成功 + 大小一致。
  5. net view \\主机名 确认 SMB 共享名,再配 IIS 虚拟目录。
  6. 浏览器访问 http://IP:8081/Files0/日期/文件,与 FTP 路径交叉验证。
  7. 再把同一套 FTP 参数写进业务 appsettings.Production.json

八、小结

NasFtpUploadTester 解决的不是「写业务功能」,而是把 NAS 文件写入链路 从黑盒变成白盒:

  • 网络是否通
  • 账号能否登
  • 被动/主动是否被拒
  • 到底该写哪个 FTP 目录
  • UNC 是否另有一套名字与权限

当你在部署环境看到 553501、错误 53、IIS「用户名不存在」时,先跑这个控制台,往往比改业务代码更快定位问题。


附录:常用排查命令

# FTP 端口
Test-NetConnection Z423-4UWJ -Port 21

# SMB 端口与共享枚举
Test-NetConnection Z423-4UWJ -Port 445
net view \\Z423-4UWJ

# 挂载真实共享名(以 net view 为准)
net use \\Z423-4UWJ\真实共享名 /user:账号 密码
dir \\Z423-4UWJ\真实共享名\IIS130\Files0

本地开发运行:

dotnet run --project .\NasFtpUploadTester.csproj -- --file D:\test\demo.txt

适用环境:Windows 部署机 + ZFileShare / 同类 NAS FTP

源码:

using FluentFTP;
using Microsoft.Extensions.Configuration;
using System.Diagnostics;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;

var configPath = GetArgValue(args, "--config") ?? "appsettings.json";
if (!Path.IsPathRooted(configPath))
    configPath = Path.Combine(AppContext.BaseDirectory, configPath);

if (!File.Exists(configPath))
{
    WriteError($"配置文件不存在: {configPath}");
    return;
}

var configuration = new ConfigurationBuilder()
    .AddJsonFile(configPath, optional: false, reloadOnChange: false)
    .Build();

var tester = configuration.GetSection("NasFtpTester").Get<NasFtpTesterConfig>() ?? new NasFtpTesterConfig();
var fileServer = configuration.GetSection("FileServer").Get<FileServerConfig>() ?? new FileServerConfig();

if (string.IsNullOrWhiteSpace(tester.Host) || string.IsNullOrWhiteSpace(tester.UserName))
{
    WriteError("请在 NasFtpTester 节点配置 Host / UserName / Password。");
    return;
}

var uploadPathKey = string.IsNullOrWhiteSpace(tester.UploadPathKey)
    ? (string.IsNullOrWhiteSpace(fileServer.UploadPath) ? "Files0" : fileServer.UploadPath)
    : tester.UploadPathKey;

var sourceFile = GetArgValue(args, "--file") ?? tester.LocalFilePath;
if (string.IsNullOrWhiteSpace(sourceFile) || !File.Exists(sourceFile))
{
    sourceFile = CreateTempFile(tester.GenerateFileSizeKb);
    WriteInfo($"自动生成测试文件: {sourceFile} ({new FileInfo(sourceFile).Length / 1024}KB)");
}

var fileName = $"{DateTime.Now:HHmmss}_{Path.GetFileName(sourceFile)}";
var relativeTarget = $"{uploadPathKey}/{DateTime.Now:yyyyMMdd}/{fileName}".Replace("\\", "/");
var remotePath = CombineRemotePath(tester.RemoteBasePath, relativeTarget);

Console.WriteLine();
Console.WriteLine("╔══════════════════════════════════════╗");
Console.WriteLine("║     NAS FTP Upload Tester v2.2       ║");
Console.WriteLine("╚══════════════════════════════════════╝");
Console.WriteLine();
Console.WriteLine($"  配置文件      : {configPath}");
Console.WriteLine($"  FTP 地址      : ftp://{tester.Host}:{tester.Port}");
Console.WriteLine($"  用户名        : {tester.UserName}");
Console.WriteLine($"  被动模式      : {tester.PassiveMode}");
Console.WriteLine($"  SSL           : {tester.UseSsl}");
Console.WriteLine($"  自动重试模式  : {tester.AutoRetryModes}");
Console.WriteLine($"  上传根Key     : {uploadPathKey}");
Console.WriteLine($"  远端基路径    : {(string.IsNullOrWhiteSpace(tester.RemoteBasePath) ? "(根目录)" : tester.RemoteBasePath)}");
Console.WriteLine($"  本地文件      : {sourceFile}");
Console.WriteLine($"  目标远端路径  : {remotePath}");
Console.WriteLine($"  UNC对照路径   : {(string.IsNullOrWhiteSpace(tester.UncTestPath) ? "(未配置)" : tester.UncTestPath)}");
Console.WriteLine();

WriteStep("1/6", "检测网络连通性");
if (!TestPing(tester.Host))
    WriteWarn($"Ping {tester.Host} 失败(可能 ICMP 被禁,继续尝试 TCP)");
else
    WriteOk($"Ping {tester.Host} 成功");

if (!TestTcpConnect(tester.Host, tester.Port, tester.ConnectTimeoutMs))
{
    WriteError($"TCP 连接 {tester.Host}:{tester.Port} 失败!");
    WriteError("  请用主机名(如 Z423-4UWJ),不要用可能已失效的 IP");
    PressAnyKeyToExit();
    return;
}
WriteOk($"TCP 连接 {tester.Host}:{tester.Port} 成功");
Console.WriteLine();

WriteStep("2/6", "对照测试:生产同路径 UNC 写入");
TryUncWriteCandidates(tester.Host, tester.UncTestPath, uploadPathKey, sourceFile, fileName);
Console.WriteLine();

var modes = BuildModes(tester);
Exception? lastError = null;
FtpClient? successClient = null;
string? successMode = null;
string? successRemotePath = null;
var remoteListed = false;
string[] pathCandidates = [tester.RemoteBasePath ?? ""];

foreach (var mode in modes)
{
    WriteStep("FTP", $"尝试模式: {mode.Name}");
    FtpClient? client = null;
    try
    {
        client = CreateClient(tester, mode);
        client.Connect();
        WriteOk($"登录成功 ({client.SystemType}),模式: {mode.Name}");

        if (!remoteListed)
        {
            pathCandidates = BuildPathCandidates(client, tester.RemoteBasePath, uploadPathKey);
            remoteListed = true;
            WriteInfo("将按以下基路径依次尝试上传:");
            foreach (var p in pathCandidates)
                Console.WriteLine($"    - /{(string.IsNullOrWhiteSpace(p) ? "(根目录)" : p.Trim('/'))}");
        }

        var uploaded = false;
        foreach (var basePath in pathCandidates)
        {
            var tryRemote = CombineRemotePath(basePath, relativeTarget);
            WriteInfo($"上传到: {tryRemote}");
            try
            {
                var sw = Stopwatch.StartNew();
                var status = client.UploadFile(sourceFile, tryRemote, FtpRemoteExists.Overwrite, true, FtpVerify.None);
                sw.Stop();
                if (status == FtpStatus.Success)
                {
                    WriteOk($"上传成功!耗时 {sw.ElapsedMilliseconds}ms");
                    WriteOk($"成功路径: {tryRemote}");
                    WriteOk($"成功模式: {mode.Name}");
                    successClient = client;
                    successMode = mode.Name;
                    successRemotePath = tryRemote;
                    lastError = null;
                    uploaded = true;
                    break;
                }
                WriteWarn($"上传返回 {status},耗时 {sw.ElapsedMilliseconds}ms");
            }
            catch (Exception pathEx)
            {
                lastError = pathEx;
                WriteWarn($"路径失败: {pathEx.InnerException?.Message ?? pathEx.Message}");
            }
        }

        if (uploaded) break;

        client.Dispose();
    }
    catch (Exception ex)
    {
        lastError = ex;
        WriteWarn($"失败: {ex.Message}");
        if (ex.InnerException != null)
            WriteWarn($"  内部: {ex.InnerException.Message}");
        try { client?.Dispose(); } catch { /* ignore */ }

        var msg = (ex.InnerException?.Message ?? ex.Message) ?? "";
        if (msg.Contains("no TLS certificate", StringComparison.OrdinalIgnoreCase))
            WriteInfo("NAS 未配置 TLS,跳过 FTPS 模式");
        else if (msg.Contains("501", StringComparison.OrdinalIgnoreCase) ||
                 msg.Contains("security requirements", StringComparison.OrdinalIgnoreCase))
            WriteInfo("主动模式被 NAS 安全策略拒绝,继续下一模式...");
    }
    Console.WriteLine();
}

if (successClient == null || string.IsNullOrWhiteSpace(successRemotePath))
{
    WriteError("所有 FTP 路径/模式均上传失败");
    if (lastError != null)
    {
        WriteError($"最后异常: {lastError.GetType().Name}");
        WriteError($"  {lastError.Message}");
        if (lastError.InnerException != null)
            WriteError($"  内部: {lastError.InnerException.Message}");
    }
    WriteError("");
    WriteError("根据日志,当前最可能原因是路径不对:");
    WriteError("  FTP 根目录只有 /public /sata11-18914015405 /ftp /share");
    WriteError("  不能上传到 /Files0,应上传到 /sata11-18914015405/IIS130/Files0/...");
    WriteError("  请把 RemoteBasePath 设为: sata11-18914015405/IIS130");
    PressAnyKeyToExit();
    return;
}

remotePath = successRemotePath;

Console.WriteLine();
WriteStep("6/6", $"验证远端文件(成功模式: {successMode})");
try
{
    using (successClient)
    {
        var exists = successClient.FileExists(remotePath);
        Console.WriteLine($"  FileExists: {exists}");
        if (exists)
        {
            var size = successClient.GetFileSize(remotePath);
            var localSize = new FileInfo(sourceFile).Length;
            Console.WriteLine($"  远端大小: {size} / 本地大小: {localSize}");
            if (size == localSize)
                WriteOk("文件大小一致,FTP 上传验证通过!");
            else
                WriteWarn("文件大小不一致");
        }
        else
        {
            WriteWarn("FileExists=false,但 Upload 返回 Success");
        }
    }
}
catch (Exception ex)
{
    WriteWarn($"验证失败: {ex.Message}");
}

Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("══════ 测试完成 ══════");
Console.ResetColor();
PressAnyKeyToExit();

static List<ConnMode> BuildModes(NasFtpTesterConfig tester)
{
    var preferred = new ConnMode(
        $"配置首选(Passive={tester.PassiveMode}, SSL={tester.UseSsl})",
        tester.PassiveMode,
        tester.UseSsl);

    if (!tester.AutoRetryModes)
        return [preferred];

    // 该 NAS(ZFileShare) 未配置 TLS,主动模式会被 501 拒绝,只重试被动明文
    var list = new List<ConnMode>
    {
        preferred,
        new("被动模式 + 明文", true, false)
    };

    return list
        .GroupBy(m => $"{m.Passive}|{m.UseSsl}")
        .Select(g => g.First())
        .ToList();
}

static string[] BuildPathCandidates(FtpClient client, string? configuredBase, string uploadPathKey)
{
    ListRemoteRoots(client, configuredBase);

    var candidates = new List<string>();
    void Add(string? p)
    {
        var v = (p ?? "").Trim().Trim('/');
        if (!candidates.Any(x => string.Equals(x, v, StringComparison.OrdinalIgnoreCase)))
            candidates.Add(v);
    }

    Add(configuredBase);
    Add("sata11-18914015405/IIS130");
    Add("sata11-18914015405");
    Add("ftp");
    Add("share");
    Add("public");
    Add("");

    // 探测配置基路径是否存在,并列出子目录帮助定位 IIS130
    try
    {
        var share = "/sata11-18914015405";
        if (client.DirectoryExists(share))
        {
            WriteInfo($"列出 {share}:");
            foreach (var item in client.GetListing(share))
            {
                var typeTag = item.Type == FtpObjectType.Directory ? "[DIR]" : "[FILE]";
                Console.WriteLine($"    {typeTag} {item.FullName}");
                if (item.Type == FtpObjectType.Directory)
                {
                    var name = item.Name?.Trim('/') ?? "";
                    if (name.Equals("IIS130", StringComparison.OrdinalIgnoreCase) ||
                        name.Contains("IIS", StringComparison.OrdinalIgnoreCase))
                        Add($"sata11-18914015405/{name}");
                }
            }
        }
    }
    catch (Exception ex)
    {
        WriteWarn($"探测 sata11-18914015405 失败: {ex.Message}");
    }

    _ = uploadPathKey;
    return candidates.ToArray();
}

static FtpClient CreateClient(NasFtpTesterConfig tester, ConnMode mode)
{
    var client = new FtpClient(tester.Host)
    {
        Port = tester.Port,
        Credentials = new NetworkCredential(tester.UserName, tester.Password)
    };
    client.Config.ConnectTimeout = tester.ConnectTimeoutMs;
    client.Config.ReadTimeout = tester.ReadWriteTimeoutMs;
    client.Config.DataConnectionConnectTimeout = tester.ConnectTimeoutMs;
    client.Config.DataConnectionReadTimeout = tester.ReadWriteTimeoutMs;
    client.Config.DataConnectionType = mode.Passive
        ? FtpDataConnectionType.PASV
        : FtpDataConnectionType.PORT;
    client.Config.EncryptionMode = mode.UseSsl ? FtpEncryptionMode.Explicit : FtpEncryptionMode.None;
    if (mode.UseSsl)
        client.Config.ValidateAnyCertificate = true;
    client.Config.LogToConsole = true;
    return client;
}

static void ListRemoteRoots(FtpClient client, string? remoteBasePath)
{
    WriteStep("目录", "列出 FTP 根目录(用于核对 RemoteBasePath)");
    try
    {
        var rootItems = client.GetListing("/");
        if (rootItems == null || rootItems.Length == 0)
            WriteWarn("根目录为空");
        else
        {
            foreach (var item in rootItems)
            {
                var typeTag = item.Type == FtpObjectType.Directory ? "[DIR]" : "[FILE]";
                Console.WriteLine($"    {typeTag} {item.FullName}");
            }
        }
    }
    catch (Exception ex)
    {
        WriteWarn($"列出根目录失败: {ex.Message}");
    }

    if (string.IsNullOrWhiteSpace(remoteBasePath)) return;
    var basePath = "/" + remoteBasePath.Trim('/');
    Console.WriteLine($"  列出 {basePath}:");
    try
    {
        if (!client.DirectoryExists(basePath))
        {
            WriteWarn($"目录不存在: {basePath}");
            return;
        }
        foreach (var item in client.GetListing(basePath))
        {
            var typeTag = item.Type == FtpObjectType.Directory ? "[DIR]" : "[FILE]";
            Console.WriteLine($"    {typeTag} {item.FullName}");
        }
    }
    catch (Exception ex)
    {
        WriteWarn($"列出 {basePath} 失败: {ex.Message}");
    }
}

static void TryUncWriteCandidates(string host, string? configuredUnc, string uploadPathKey, string sourceFile, string fileName)
{
    var candidates = new List<string>();
    void Add(string? p)
    {
        if (string.IsNullOrWhiteSpace(p)) return;
        var v = p.TrimEnd('\\');
        if (!candidates.Any(x => string.Equals(x, v, StringComparison.OrdinalIgnoreCase)))
            candidates.Add(v);
    }

    Add(configuredUnc);
    Add($@"\\{host}\sata11-18914015405\IIS130");
    Add($@"\\{host}\sata11-18914015405");
    Add($@"\\{host}\IIS130");
    Add($@"\\{host}\share");
    Add($@"\\{host}\ftp");
    Add($@"\\{host}\public");

    foreach (var uncRoot in candidates)
    {
        WriteInfo($"尝试 UNC: {uncRoot}");
        try
        {
            if (!Directory.Exists(uncRoot))
            {
                WriteWarn("  目录/共享不存在");
                continue;
            }

            var targetDir = Path.Combine(uncRoot, uploadPathKey, DateTime.Now.ToString("yyyyMMdd"));
            Directory.CreateDirectory(targetDir);
            var targetFile = Path.Combine(targetDir, "unc_" + fileName);
            File.Copy(sourceFile, targetFile, true);
            if (File.Exists(targetFile))
            {
                WriteOk($"UNC 写入成功: {targetFile}");
                return;
            }
            WriteWarn($"  复制后文件不存在: {targetFile}");
        }
        catch (Exception ex)
        {
            WriteWarn($"  失败: {ex.Message}");
        }
    }

    WriteWarn("所有 UNC 候选路径均失败(共享名可能不是 sata11-18914015405,或当前机器无 SMB 访问权限)");
}

static string? GetArgValue(string[] args, string key)
{
    var idx = Array.IndexOf(args, key);
    return (idx >= 0 && idx + 1 < args.Length) ? args[idx + 1] : null;
}

static string CombineRemotePath(string? root, string relative)
{
    var cleanRoot = (root ?? "").Trim().Trim('/');
    var cleanRelative = relative.Trim().Trim('/');
    return "/" + (string.IsNullOrWhiteSpace(cleanRoot) ? cleanRelative : $"{cleanRoot}/{cleanRelative}");
}

static string CreateTempFile(int sizeKb)
{
    var safeSize = sizeKb <= 0 ? 64 : sizeKb;
    var path = Path.Combine(Path.GetTempPath(), $"nas_ftp_test_{DateTime.Now:yyyyMMdd_HHmmss}.txt");
    using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
    using var writer = new StreamWriter(fs);
    writer.WriteLine($"NAS FTP test file generated at {DateTime.Now:O}");
    writer.WriteLine($"Machine: {Environment.MachineName}");
    writer.WriteLine(new string('=', 40));
    var line = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
    while (fs.Length < safeSize * 1024L)
    {
        writer.WriteLine(line);
        writer.Flush();
    }
    return path;
}

static bool TestPing(string host)
{
    try
    {
        using var ping = new Ping();
        var reply = ping.Send(host, 3000);
        return reply.Status == IPStatus.Success;
    }
    catch { return false; }
}

static bool TestTcpConnect(string host, int port, int timeoutMs)
{
    try
    {
        using var tcp = new TcpClient();
        var task = tcp.ConnectAsync(host, port);
        return task.Wait(timeoutMs) && tcp.Connected;
    }
    catch { return false; }
}

static void WriteStep(string step, string msg)
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.Write($"[{step}] ");
    Console.ResetColor();
    Console.WriteLine(msg);
}

static void WriteOk(string msg)
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine($"  ✓ {msg}");
    Console.ResetColor();
}

static void WriteWarn(string msg)
{
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"  ⚠ {msg}");
    Console.ResetColor();
}

static void WriteError(string msg)
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine($"  ✗ {msg}");
    Console.ResetColor();
}

static void WriteInfo(string msg)
{
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"  ℹ {msg}");
    Console.ResetColor();
}

static void PressAnyKeyToExit()
{
    Console.WriteLine();
    Console.WriteLine("按任意键退出...");
    try { Console.ReadKey(true); } catch { }
}

internal sealed record ConnMode(string Name, bool Passive, bool UseSsl);

internal sealed class NasFtpTesterConfig
{
    public string Host { get; set; } = "";
    public int Port { get; set; } = 21;
    public string UserName { get; set; } = "";
    public string Password { get; set; } = "";
    public bool UseSsl { get; set; }
    public bool PassiveMode { get; set; } = true;
    public int ConnectTimeoutMs { get; set; } = 15000;
    public int ReadWriteTimeoutMs { get; set; } = 30000;
    public string UploadPathKey { get; set; } = "";
    public string RemoteBasePath { get; set; } = "";
    public string LocalFilePath { get; set; } = "";
    public int GenerateFileSizeKb { get; set; } = 64;
    public bool AutoRetryModes { get; set; } = true;
    public string UncTestPath { get; set; } = "";
}

internal sealed class FileServerConfig
{
    public string UploadPath { get; set; } = "";
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值