LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

C#.NET 程序中增加 SFTP 服务器断点续传上传文件功能函数

admin
2026年10月31日 12:9 本文热度 244

说明:下面使用 SSH.NET(主流、稳定,支持 SFTP、断点续传,.NET Framework /.NET Core /.NET 5+ 都能用) 原理:

  1. ✅ 递归多级目录创建(mkdir -p)

  2. ✅ 断点续传、流式上传,大文件不占内存

  3. ✅ 远端大于本地文件:参数控制是否删除重传

  4. ✅ 上传完成同步 UTC 修改时间戳,时间戳异常仅告警

  5. ✅ 兼容 VS2015 + .NET Framework 4.5 / 4.5.2

1、先安装 NuGet 包

Install-Package SSH.NET

2、SFTP 断点续传上传函数

using Renci.SshNet;

using Renci.SshNet.Sftp;

using System;

using System.IO;


public class SftpHelper

{

    /// <summary>

    /// SFTP断点续传上传大文件(兼容VS2015 .NET Framework4.5)

    /// 支持递归创建多级远程目录、同步本地文件修改时间、可配置远端偏大时是否删除重传

    /// 注意:Linux SFTP一般无法修改文件创建时间(birthtime),仅同步修改时间mtime

    /// </summary>

    /// <param name="host">SFTP地址</param>

    /// <param name="port">端口</param>

    /// <param name="userName">用户名</param>

    /// <param name="password">密码</param>

    /// <param name="remoteDir">远程目标目录,支持多级如 /a/b/c/d</param>

    /// <param name="remoteFileName">远程文件名</param>

    /// <param name="localFilePath">本地文件完整路径</param>

    /// <param name="deleteRemoteWhenBigger">远端文件大小大于本地时:true=删远端重传;false=直接返回失败(防恶意篡改)</param>

    /// <param name="progressCallback">进度回调(已上传字节,总字节)</param>

    /// <param name="bufferSize">缓冲区,默认4MB</param>

    /// <returns>是否成功</returns>

    public static bool SftpUploadResumeWithTimeStamp(

        string host,

        int port,

        string userName,

        string password,

        string remoteDir,

        string remoteFileName,

        string localFilePath,

        bool deleteRemoteWhenBigger,

        Action<long, long> progressCallback,

        int bufferSize = 4 * 1024 * 1024)

    {

        if (!File.Exists(localFilePath))

        {

            Console.WriteLine(string.Format("本地文件不存在:{0}", localFilePath));

            return false;

        }


        PasswordConnectionInfo connectionInfo = new PasswordConnectionInfo(host, port, userName, password);

        using (SftpClient sftpClient = new SftpClient(connectionInfo))

        {

            try

            {

                sftpClient.Connect();

                if (!sftpClient.IsConnected)

                {

                    Console.WriteLine("SFTP连接失败");

                    return false;

                }


                string remoteFullPath = Path.Combine(remoteDir, remoteFileName).Replace("\\", "/");


                // 递归创建多级目录(mkdir -p)

                CreateDirectoryRecursive(sftpClient, remoteDir);


                long remoteFileSize = 0;

                bool remoteFileExists = sftpClient.Exists(remoteFullPath);

                if (remoteFileExists)

                {

                    SftpFileAttributes attr = sftpClient.GetAttributes(remoteFullPath);

                    remoteFileSize = attr.Size;

                    Console.WriteLine(string.Format("远端已有文件大小:{0}", remoteFileSize));

                }


                FileInfo localFileInfo = new FileInfo(localFilePath);

                long localTotalSize = localFileInfo.Length;

                DateTime localModifyTimeUtc = localFileInfo.LastWriteTimeUtc;


                // 场景1:远端文件和本地一样大

                if (remoteFileSize == localTotalSize)

                {

                    FixRemoteFileTime(sftpClient, remoteFullPath, localModifyTimeUtc);

                    if (progressCallback != null)

                    {

                        progressCallback(localTotalSize, localTotalSize);

                    }

                    Console.WriteLine("文件已完整存在,校验并同步时间戳完成");

                    return true;

                }


                // 场景2:远端文件 > 本地文件(重点优化分支)

                if (remoteFileSize > localTotalSize)

                {

                    if (deleteRemoteWhenBigger)

                    {

                        Console.WriteLine(string.Format("远端文件({0}B)大于本地({1}B),配置允许删除,准备删除远端文件重新上传", remoteFileSize, localTotalSize));

                        sftpClient.DeleteFile(remoteFullPath);

                        remoteFileSize = 0; // 删除后从头上传

                    }

                    else

                    {

                        Console.WriteLine(string.Format("远端文件({0}B)大于本地({1}B),配置禁止删除,终止上传,返回失败", remoteFileSize, localTotalSize));

                        return false;

                    }

                }


                // 场景3:远端文件 < 本地文件,断点续传

                using (FileStream localStream = File.OpenRead(localFilePath))

                {

                    localStream.Seek(remoteFileSize, SeekOrigin.Begin);

                    using (SftpFileStream remoteStream = sftpClient.Open(remoteFullPath, FileMode.Append))

                    {

                        byte[] buffer = new byte[bufferSize];

                        int readLen;

                        long uploaded = remoteFileSize;

                        while ((readLen = localStream.Read(buffer, 0, buffer.Length)) > 0)

                        {

                            remoteStream.Write(buffer, 0, readLen);

                            uploaded += readLen;

                            if (progressCallback != null)

                            {

                                progressCallback(uploaded, localTotalSize);

                            }

                        }

                        remoteStream.Flush();

                    }

                }


                // 上传完成同步时间戳

                FixRemoteFileTime(sftpClient, remoteFullPath, localModifyTimeUtc);


                if (progressCallback != null)

                {

                    progressCallback(localTotalSize, localTotalSize);

                }

                Console.WriteLine("断点续传上传 + 时间戳同步完成");

                return true;

            }

            catch (Exception ex)

            {

                Console.WriteLine(string.Format("上传异常:{0}", ex.Message));

                return false;

            }

            finally

            {

                if (sftpClient.IsConnected)

                {

                    sftpClient.Disconnect();

                }

            }

        }

    }


    /// <summary>

    /// 递归创建SFTP远程目录,等价 mkdir -p

    /// </summary>

    private static void CreateDirectoryRecursive(SftpClient sftp, string remotePath)

    {

        if (string.IsNullOrWhiteSpace(remotePath))

        {

            return;

        }

        remotePath = remotePath.Replace("\\", "/");

        if (remotePath == "/")

        {

            return;

        }


        if (sftp.Exists(remotePath))

        {

            SftpFileAttributes attr = sftp.GetAttributes(remotePath);

            if (attr.IsDirectory)

            {

                return;

            }

            throw new IOException(string.Format("路径[{0}]已经存在,但不是目录", remotePath));

        }


        string parent = GetParentSftpPath(remotePath);

        if (!string.IsNullOrEmpty(parent))

        {

            CreateDirectoryRecursive(sftp, parent);

        }

        sftp.CreateDirectory(remotePath);

        Console.WriteLine(string.Format("创建远程目录:{0}", remotePath));

    }


    /// <summary>

    /// 取SFTP路径的父目录(兼容 /a/b/c 格式)

    /// </summary>

    private static string GetParentSftpPath(string path)

    {

        path = path.TrimEnd('/');

        int idx = path.LastIndexOf('/');

        if (idx <= 0)

        {

            return "";

        }

        return path.Substring(0, idx);

    }


    /// <summary>

    /// 设置远端SFTP文件的修改时间(UTC),内部吞异常:时间戳失败不判定上传失败

    /// </summary>

    private static void FixRemoteFileTime(SftpClient sftp, string remotePath, DateTime utcLastWrite)

    {

        try

        {

            SftpFileAttributes attr = sftp.GetAttributes(remotePath);

            attr.LastWriteTimeUtc = utcLastWrite;

            sftp.SetAttributes(remotePath, attr);

        }

        catch (Exception ex)

        {

            Console.WriteLine(string.Format("警告:时间戳同步失败(权限/服务端不支持):{0}", ex.Message));

        }

    }

}

参数 bool deleteRemoteWhenBigger:

  • deleteRemoteWhenBigger = true:远端文件 > 本地 → 删除远端文件,从头上传

  • deleteRemoteWhenBigger = false:远端文件 > 本地 → 直接返回失败(原有逻辑,用于规避恶意篡改小文件场景)

3、重要关键点(针对 1GB + 大文件)

  1. 不加载全文件到内存:使用 File.OpenRead 流式读取,内存占用稳定等于缓冲区大小;

  2. 续传逻辑:FileMode.Append 打开远程文件,从末尾追加;先比对远端已有长度;如果上次传到一半中断,本次续传补齐剩余字节,全部写完之后才覆盖时间戳; 如果文件已经传完了,再次调用函数,会自动校验并且修复时间戳,防止上次时间戳设置失败。

  3. 目录处理:逐级递归创建 SFTP 远程目录,自动判断远程目录是否存在,不存在自动创建;

  4. 边界判断:远端大小等于本地直接返回成功;远端大于本地视为异常;

但是有一个重要限制: SFTP 协议(SSH.NET)只能设置文件的修改时间(mtime)、访问时间(atime);Linux 服务端一般不能单独设置 “创建时间(birth time /ctime)”(Linux 原生文件系统很多不支持单独修改 birth,这是 Linux 系统限制,不是SSH.NET的 bug)。 

✅ 我们可以做到:同步【本地文件修改时间】到远端文件(最常用,业务上一般用这个),这也是绝大多数文件同步工具(WinSCP、rsync)做的。

WinSCP 里的 “保留文件时间戳” 就是设置 mtime。

原理:SSH.NET SftpClient.SetAttributes() 可以修改远端文件的时间戳。 时机:全部字节上传写完、Flush 之后,再调用设置属性。

VS2015 调用示例(传统写法,不能用命名参数!这点非常重要!VS2015 C#6 不支持命名实参调用)

​❌ 下面这种写法 VS2015 编译报错,禁止使用:

// 错误写法(命名参数)
bool ok = SftpHelper.SftpUploadResumeWithTimeStamp(host:"127.0.0.1", ...);

✅ VS2015 正确调用写法:

// 示例1:安全模式,远端偏大直接失败(防病毒篡改场景)
bool ok1 = SftpHelper.SftpUploadResumeWithTimeStamp(
    "192.168.1.100",
    22,
    "sftpuser",
    "123456",
    "/upload/2026/10/data",
    "bigfile.iso",
    @"D:\data\bigfile.iso",
    false,
    (uploaded, total) =>
    {
        double pct = uploaded * 100.0 / total;
        Console.WriteLine(string.Format("进度:{0:F2}%  {1}/{2}", pct, uploaded, total));
    },
    4 * 1024 * 1024);
// 示例2:信任内网环境,远端偏大自动删除重传
bool ok2 = SftpHelper.SftpUploadResumeWithTimeStamp(
    "192.168.1.100",
    22,
    "sftpuser",
    "123456",
    "/upload/2026/10/data",
    "bigfile.iso",
    @"D:\data\bigfile.iso",
    true,
    (uploaded, total) =>
    {
        double pct = uploaded * 100.0 / total;
        Console.WriteLine(string.Format("进度:{0:F2}%  {1}/{2}", pct, uploaded, total));
    },
    4 * 1024 * 1024);

配套部署注意事项

  1. 项目目标框架设置:.NET Framework 4.5 或者 4.5.2(VS2015 原生支持);

  2. ​SSH.NET 版本锁定 2016.1.0,更高版本会要求 .NETStandard,在 4.5 下报错;

  3. 所有语法全部向下兼容:取消了字符串插值 $""、取消调用时命名参数、所有变量显式声明、没有 C#7 语法;

  4. 缓冲区参数有默认值,调用时可以省略最后一个 bufferSize 参数。


该文章在 2026/10/31 12:16:01 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-9  粤公网安备44030602007207号