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));
}
}
}