简介:这是一份面向C#桌面开发者的Winform文件上传实例,聚焦局域网内将本地文件写入服务器共享文件夹这一常见需求,可用于企业内部数据传递、文档归档等场景。示例以VS2010解决方案组织,串联OpenFileDialog选取文件、NetworkCredential配合UNC路径建立访问凭据、FileStream与NetworkStream完成流式读写,并用BackgroundWorker异步上报进度,同时兼顾异常提示与文件类型、大小校验,适合需要落地内网传输功能的初中级开发者参考。压缩包共23个文件、约36KB,以6个cs源码和Form1界面资源为核心,另含sln、csproj工程配置与exe、pdb等编译产物,解压即可打开对照调试。目前已有1429人学习下载,可借此理解共享目录读写、网络凭据与跨线程更新界面的配合方式,并提炼权限不足、连接失败、文件不存在等场景的排错思路。
1. 共享文件夹不是网盘:WinForm 上传文件到共享文件夹先跨过 UNC 与权限两道门
WinForm 上传文件到共享文件夹,难点从来不在File.Copy那一行。真实场景是:档案归集客户端选完本地文件,点上传,文件要落到内网文件服务器的共享目录里;用户只看到一根进度条,背后却要穿过 UNC 路径、SMB 会话、共享权限、NTFS 权限和 WinForm 的 UI 线程。文件上传到服务器在局域网里经常就是“写共享盘”,它适合内部工具、桌面客户端、小型管理系统做附件归集,不适合直接暴露到公网。这套 WinForm 上传模块把路径探测、凭据连接、分块进度、临时文件和断点续传拆成可替换的类,拿来改服务器地址和凭据就能嵌进现有工程,也方便逐段排查翻车点。
2. 从 UNC 路径到凭据连接:文件上传到共享文件夹的第一公里
2.1 UNC 路径不是普通字符串:共享权限与 NTFS 权限的叠加关系
\\server\share\folder\file.txt这种 UNC 路径看起来只是字符串,实际每次读写都在触发一次 SMB 会话。WinForm 进程默认拿当前 Windows 登录令牌去连接,所以“同域账号能打开共享”和“这个客户端能写进去”是两件事。共享权限管的是“能不能通过这个共享入口进来”,NTFS 权限管的是“进来之后能不能在这个目录创建、修改、删除文件”,两者取交集。常见翻车是共享权限给了“完全控制”,NTFS 只给了“读取和执行”,资源管理器能浏览目录,上传时却报拒绝访问。
选型上,同域环境优先直接用 UNC 路径,少一层映射盘符,少一个“提权后看不到映射盘”的玄学问题。需要不同凭据时,常见做法是用WNetAddConnection2建立临时连接,而不是在代码里拼net use命令。临时连接不持久化,进程退出就释放,适合 WinForm 客户端。这里有一条血泪经验:不要用管理员身份运行客户端去测试普通用户的上传,UAC 提权后网络会话可能换了一套令牌,原本能写的目录突然写不进去,排查半天以为是权限配置错了。
上传前先确认三件事:UNC 目录是否存在、当前进程能否创建文件、目标文件是否被占用。Directory.Exists只回答第一件,后两件必须用探针文件实测。探针文件不要用固定名字,多个客户端同时启动会互相覆盖;也不要把探针文件留在业务目录里,异常退出后会被业务扫描当成脏数据。我一般用 GUID 命名,写完立刻Flush(true),再在finally里删除。
提示:共享目录不要给
Everyone完全控制,按业务账号或用户组授权;客户端也不要把密码写进配置文件明文,能走域账号就走域账号。
2.2 用 WNetAddConnection2 建立临时凭据连接
不同凭据的场景很常见:客户端跑在普通办公账号下,共享盘只允许文件归集账号写入。这时可以在上传前建立临时连接,上传结束释放。下面这段代码只做连接管理,不负责上传,目的是把“认证”和“复制”分开,出问题时能快速判断是连不上还是写不进。
using System; using System.ComponentModel; using System.Runtime.InteropServices; public sealed class UncConnection : IDisposable { private readonly string _remoteName; private bool _connected; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private class NETRESOURCE { public int dwScope; public int dwType; public int dwDisplayType; public int dwUsage; public string lpLocalName; public string lpRemoteName; public string lpComment; public string lpProvider; } [DllImport("mpr.dll", CharSet = CharSet.Unicode)] private static extern int WNetAddConnection2( NETRESOURCE lpNetResource, string lpPassword, string lpUsername, int dwFlags); [DllImport("mpr.dll", CharSet = CharSet.Unicode)] private static extern int WNetCancelConnection2( string lpName, int dwFlags, bool fForce); private const int RESOURCETYPE_DISK = 0x00000001; private const int CONNECT_TEMPORARY = 0x00000004; public UncConnection(string remoteName, string username, string password) { _remoteName = remoteName; var nr = new NETRESOURCE { dwType = RESOURCETYPE_DISK, lpRemoteName = remoteName }; int code = WNetAddConnection2(nr, password, username, CONNECT_TEMPORARY); if (code != 0) { throw new Win32Exception(code, $"连接共享失败,错误码 {code}"); } _connected = true; } public void Dispose() { if (!_connected) return; WNetCancelConnection2(_remoteName, 0, true); _connected = false; } }这段代码调用mpr.dll的WNetAddConnection2,把共享路径作为远程资源传进去,CONNECT_TEMPORARY表示临时连接,不写入用户持久凭据。remoteName传共享根路径,例如\\server\share,不要传文件全路径;username可以是domain\user或server\user,取决于共享所在环境;password只在调用瞬间使用,不要拼进日志。Dispose里用WNetCancelConnection2释放连接,fForce传true是为了避免句柄未释放时取消失败。
错误码要会看:53 通常是找不到网络路径,先查服务器名、DNS 和共享名;67 是找不到网络名,多半共享名拼错;1219 是同一服务器已有不同用户名连接,先取消旧连接或改用同一个账号;1326 是用户名或密码错误,检查账号格式和密码状态;5 是拒绝访问,回到共享权限和 NTFS 权限。不要把所有错误都提示成“网络异常”,用户看到这句话没法配合排查。
2.3 上传前做一次可写探测,不只看 Directory.Exists
路径存在不代表能写,能写不代表目标文件没被占用。上传前做一次创建、写入、删除的探针,能提前暴露大部分权限问题和杀软拦截问题。探针文件放在目标目录下,不要放到共享根目录,避免污染其他业务。
using System; using System.IO; public static class ShareProbe { public static void EnsureWritable(string uncDirectory) { if (!Directory.Exists(uncDirectory)) { throw new DirectoryNotFoundException($"共享目录不存在:{uncDirectory}"); } string probePath = Path.Combine( uncDirectory, $".write_probe_{Guid.NewGuid():N}.tmp"); try { using (var fs = new FileStream( probePath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { fs.WriteByte(0); fs.Flush(true); } } finally { if (File.Exists(probePath)) { File.Delete(probePath); } } } }uncDirectory必须是目录路径,不能传成文件路径;探针名带 GUID,避免多客户端冲突;FileMode.CreateNew保证不会覆盖已有文件,FileShare.None能顺带验证能否独占创建;Flush(true)让数据真正落到服务端,而不是只写进客户端缓存。删探针放在finally,即使写失败也尽量清理。若这一步就抛UnauthorizedAccessException,不用继续查上传代码,先查共享权限、NTFS 权限和账号是否被禁用。
3. 把复制拆成流:WinForm 分块上传、进度回调与取消的落地代码
3.1 File.Copy 为什么不适合 WinForm 上传大文件
File.Copy写起来最快,但它有三个硬伤:没有进度回调,UI 上只能转圈;调用期间阻塞当前线程,WinForm 界面直接假死;取消只能靠杀进程,大文件传到一半留下半截目标文件。文件上传到共享文件夹一旦涉及几百 MB 的附件,用户第一反应就是“是不是卡死了”,没有进度条和取消按钮,体验和可维护性都很差。
分块流式复制把大文件拆成固定大小的缓冲区,读一块写一块,每块之间检查取消令牌并上报进度。缓冲区不是越大越好:局域网千兆环境常用 1MB 到 4MB,跨网段或 SMB 延迟高时用 256KB 到 1MB,太小会增加往返次数,太大则单次写阻塞变长、进度更新变粗。目标文件先写成.uploading临时文件,全部写完再重命名,业务侧不会读到半截文件。这个临时文件也是后面断点续传的基础。
3.2 一个可复用的分块上传类
下面这个类只依赖System.IO,不依赖 WinForm 控件,方便放进任何客户端工程。进度对象带总字节数、已上传字节数和当前文件名,UI 层自己决定怎么展示。
using System; using System.IO; using System.Threading; using System.Threading.Tasks; public sealed class UploadProgress { public long TotalBytes { get; init; } public long UploadedBytes { get; init; } public string CurrentFile { get; init; } = string.Empty; public double Percent => TotalBytes == 0 ? 100 : UploadedBytes * 100.0 / TotalBytes; } public sealed class SharedFolderUploader { private readonly int _bufferSize; public SharedFolderUploader(int bufferSize = 1024 * 1024) { _bufferSize = bufferSize; } public async Task UploadAsync( string sourcePath, string targetDirectory, string targetFileName, IProgress<UploadProgress> progress, CancellationToken cancellationToken) { if (!File.Exists(sourcePath)) { throw new FileNotFoundException("源文件不存在", sourcePath); } Directory.CreateDirectory(targetDirectory); string finalPath = Path.Combine(targetDirectory, targetFileName); string tempPath = finalPath + ".uploading"; long total = new FileInfo(sourcePath).Length; long uploaded = 0; using (var source = new FileStream( sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, _bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan)) using (var target = new FileStream( tempPath, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan)) { byte[] buffer = new byte[_bufferSize]; int read; while ((read = await source.ReadAsync( buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0) { await target.WriteAsync( buffer.AsMemory(0, read), cancellationToken); uploaded += read; progress?.Report(new UploadProgress { TotalBytes = total, UploadedBytes = uploaded, CurrentFile = Path.GetFileName(sourcePath) }); } await target.FlushAsync(cancellationToken); } if (File.Exists(finalPath)) { File.Delete(finalPath); } File.Move(tempPath, finalPath); } }bufferSize默认 1MB,局域网内可以调到 4MB;source用FileShare.Read允许其他程序读源文件,target用FileShare.None防止多个客户端同时写同一个临时文件。FileOptions.Asynchronous让异步读写真正走异步路径,SequentialScan提示顺序访问,对 SMB 大文件比较友好。进度在每块写完后上报,IProgress<T>会自动回到创建它的同步上下文,WinForm 里可以直接更新控件。
File.Move(tempPath, finalPath)之前先删目标文件,是为了兼容 .NET Framework 没有覆盖重载的情况。严格来说这会有一个极短窗口:目标文件已删、新文件未就位。如果业务要求原子替换,目标文件存在时改用File.Replace(tempPath, finalPath, null),不存在时再File.Move。这个差异在 .NET 6 和 .NET Framework 4.8 上都存在,迁移时别只看编译通过。
3.3 WinForm 事件处理器里的 async、进度条与取消
UI 层只做三件事:选文件、启动上传、更新进度。不要在按钮事件里写Thread.Sleep或同步等待,也不要在后台线程直接改控件。下面这段代码用async void只因为它是事件处理器,内部所有异常都捕获,按钮同时承担“上传”和“取消”两个状态。
using System; using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; private CancellationTokenSource? _cts; private async void btnUpload_Click(object sender, EventArgs e) { if (_cts != null) { _cts.Cancel(); return; } _cts = new CancellationTokenSource(); btnUpload.Text = "取消"; progressBar.Value = 0; var progress = new Progress<UploadProgress>(p => { progressBar.Value = (int)Math.Clamp(p.Percent, 0, 100); lblStatus.Text = $"{p.CurrentFile} " + $"{p.UploadedBytes / 1024 / 1024}MB / " + $"{p.TotalBytes / 1024 / 1024}MB"; }); try { var uploader = new SharedFolderUploader(4 * 1024 * 1024); await uploader.UploadAsync( txtSource.Text, txtTargetDir.Text, Path.GetFileName(txtSource.Text), progress, _cts.Token); lblStatus.Text = "上传完成"; } catch (OperationCanceledException) { lblStatus.Text = "已取消"; } catch (Exception ex) { MessageBox.Show(ex.Message, "上传失败", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _cts.Dispose(); _cts = null; btnUpload.Text = "上传"; } }Progress<UploadProgress>在 UI 线程创建,回调会自动回到 UI 线程,所以可以直接改progressBar和lblStatus。取消按钮复用同一个按钮,第二次点击调用_cts.Cancel(),上传循环在ReadAsync或WriteAsync处抛出OperationCanceledException,临时文件会留在目标目录,后面可以接断点续传或清理策略。async void的异常必须自己接住,否则会直接把进程打崩,这是 WinForm 异步代码里最容易忽略的细节。
4. 大文件不断在半路:临时文件、断点续传与重试的四个参数
4.1 临时文件与原子落盘:别让业务读到半截文件
共享目录里最怕的是“文件已经出现在列表里,但内容不完整”。业务系统如果按文件名轮询,读到半截文件会直接解析失败,甚至把脏文件归档。临时文件命名建议固定后缀,例如.uploading或.part,业务扫描时跳过这个后缀。上传完成后重命名成目标文件名,同一目录内的重命名在 SMB 上通常是原子操作,业务侧要么看到旧文件,要么看到完整新文件。
同名冲突要提前定策略,不要等用户上传时弹一个“文件已存在”就结束。常见做法有四种:覆盖、跳过、改名、拒绝。覆盖适合固定报表或每日全量文件;跳过适合幂等上传;改名适合多客户端往同一个目录投递;拒绝适合归档目录要求唯一文件名。下面的表可以直接拿去做配置项。
| 策略 | 实现方式 | 适用场景 | 注意点 |
|---|---|---|---|
| 覆盖 | File.Replace或先删后移 | 固定文件名、每日更新 | 先删后移有短暂空窗 |
| 跳过 | 上传前判断目标存在 | 幂等重试、重复提交 | 判断和写入之间仍有竞争 |
| 改名 | 追加时间戳或 GUID | 多客户端并发投递 | 文件名会变长,业务要能解析 |
| 拒绝 | 目标存在直接抛异常 | 归档唯一性要求高 | 需要给用户明确提示 |
注意:
File.Exists判断目标文件是否存在,只能作为提示,不能作为并发控制的唯一依据;两个客户端同时判断“不存在”再同时写入,仍然会冲突。
4.2 断点续传的偏移计算与长度校验
断点续传的核心不是“记住进度条”,而是“临时文件已经写了多少字节”。上传前读临时文件长度,源文件Seek到同样偏移,目标文件也Seek到同样偏移,继续往后写。临时文件比源文件还大,说明上一次写入异常或源文件被替换,直接截断重来。全部写完后比较临时文件长度和源文件长度,不一致就抛异常,不要直接重命名。
using System; using System.IO; using System.Threading; using System.Threading.Tasks; public async Task ResumeUploadAsync( string sourcePath, string targetDirectory, string targetFileName, IProgress<UploadProgress> progress, CancellationToken cancellationToken) { string finalPath = Path.Combine(targetDirectory, targetFileName); string tempPath = finalPath + ".uploading"; int bufferSize = 1024 * 1024; long total = new FileInfo(sourcePath).Length; long offset = File.Exists(tempPath) ? new FileInfo(tempPath).Length : 0; using (var source = new FileStream( sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan)) using (var target = new FileStream( tempPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan)) { if (offset > total) { target.SetLength(0); offset = 0; } source.Seek(offset, SeekOrigin.Begin); target.Seek(offset, SeekOrigin.Begin); byte[] buffer = new byte[bufferSize]; int read; while ((read = await source.ReadAsync( buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0) { await target.WriteAsync( buffer.AsMemory(0, read), cancellationToken); offset += read; progress?.Report(new UploadProgress { TotalBytes = total, UploadedBytes = offset, CurrentFile = Path.GetFileName(sourcePath) }); } await target.FlushAsync(cancellationToken); } if (new FileInfo(tempPath).Length != total) { throw new IOException("断点续传长度校验失败,临时文件长度与源文件不一致"); } if (File.Exists(finalPath)) { File.Delete(finalPath); } File.Move(tempPath, finalPath); }offset来自临时文件长度,不是来自配置文件或数据库,因为临时文件才是真实写入结果。Seek在 SMB 上不一定便宜,但比重新传整个文件划算。长度校验只能证明字节数一致,不能证明内容一致;对完整性要求高的场景,再追加 SHA256,但大文件算哈希很耗时,建议放在业务低峰或只对关键文件开启。断点续传还依赖连接可用,SMB 会话断开后要先重新建立凭据连接,再继续写,否则会一直报网络错误。
4.3 重试与超时:失败分类比循环次数更重要
重试最怕两种写法:一种是所有异常都重试,认证失败也重试,越重试账号越容易被锁;另一种是死循环不设上限,客户端卡在“正在上传”一下午。正确做法是先分类:UnauthorizedAccessException和明确的认证错误码不重试;IOException里的网络抖动、共享暂时不可用可以重试;OperationCanceledException立即抛出。重试间隔用指数退避,给服务端一点恢复时间。
using System; using System.IO; using System.Threading; using System.Threading.Tasks; public static async Task<T> RetryAsync<T>( Func<Task<T>> action, int maxAttempts = 3, CancellationToken cancellationToken = default) { Exception? last = null; for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { return await action(); } catch (OperationCanceledException) { throw; } catch (UnauthorizedAccessException ex) { throw new IOException("权限或凭据失败,不重试", ex); } catch (IOException ex) when (attempt < maxAttempts) { last = ex; int delay = 500 * (int)Math.Pow(2, attempt - 1); await Task.Delay(delay, cancellationToken); } } throw last ?? new IOException("上传重试失败"); }maxAttempts默认 3 次,延迟分别是 500ms、1000ms、2000ms;UnauthorizedAccessException直接包装成IOException抛出,避免上层误以为还能继续重试。超时用CancellationTokenSource.CancelAfter控制整体上传时间,不要在每个缓冲区读写上单独设超时,否则大文件会被切得很难管理。重试次数和超时时间建议做成配置项,不同共享服务器的网络质量差别很大,硬编码一套参数迟早翻车。
5. 避坑与排查:权限、占位、长路径、并发、认证失败的五类翻车现场
5.1 能打开共享却写不进去:共享权限与 NTFS 的叠加
现象:资源管理器能打开\\server\share\folder,能看到里面的文件,WinForm 上传时抛UnauthorizedAccessException,提示拒绝访问。原因通常是共享权限给了入口,但 NTFS 权限只允许读取,或者上传账号对目标子目录没有“写入”和“修改”权限。还有一种情况是客户端以管理员身份运行,网络会话使用的令牌和普通用户不同,测试结果不能代表最终用户。
解决:先确认上传进程用的是哪个 Windows 账号,再在服务端同时看共享权限和 NTFS 权限,不要只看其中一层。用探针文件实测创建和删除,比看目录属性更直接。如果必须用不同账号,用WNetAddConnection2显式连接,连接后立刻跑一次EnsureWritable,把认证和写权限分开验证。
5.2 文件上传到一半消失:临时文件命名与杀软扫描
现象:上传大文件时,目标目录里偶尔出现.uploading文件,过一会儿又没了;或者业务系统读到半截文件解析失败。原因可能是临时文件后缀被清理脚本或杀软扫描删除,也可能是上传完成后先删目标文件再移动,中间被业务轮询读到空窗。还有客户端异常退出,临时文件留在目录里,下次上传没有清理策略。
解决:临时文件固定后缀并加入清理白名单,业务扫描明确跳过;上传完成用File.Replace或同目录重命名,减少空窗;客户端启动时清理超过一定时间的.uploading文件,但不要删正在被其他进程占用的文件。杀软拦截只能通过加目录信任或换后缀验证,不要靠反复重试硬扛。
5.3 中文路径与长路径:不是所有 WinForm 都默认支持
现象:本地文件路径包含中文、空格或超过 260 个字符时,上传抛出PathTooLongException或找不到文件。原因通常是 .NET Framework 默认没有启用长路径支持,或者代码里手工拼接路径时漏了转义。共享路径本身也可能很长,尤其是多级部门目录。
解决:优先用Path.Combine拼接,不要用字符串加斜杠;目标文件名做非法字符过滤;需要长路径时在应用配置里启用长路径支持,或者使用\\?\UNC\server\share\...前缀,但这个前缀对某些 API 不友好,要先小范围验证。中文路径本身不是问题,问题多是编码转换和日志输出时被截断,日志里记录完整路径能省很多排查时间。
5.4 多客户端并发覆盖:同名文件与最后写入者
现象:两个客户端同时上传同名文件,最后目标文件内容属于其中一个,另一个用户以为自己的文件上传成功了。原因是没有冲突策略,或者只在上传前用File.Exists判断,判断和写入之间有时间差。共享目录没有数据库行锁,靠文件系统本身很难做到“先到先得”。
解决:按业务定策略。固定报表可以覆盖,但要在文件名里带日期或版本;多客户端投递用时间戳或 GUID 改名;归档目录用“目标存在就拒绝”并在 UI 上提示用户改名。服务端目录按日期或业务号分桶,减少同名概率。不要依赖File.Exists做并发控制,它只能做提示。
5.5 认证失败被当成网络故障:1326/1219 的排查顺序
现象:上传时报“网络路径找不到”或“连接共享失败”,用户反复点重试,账号被锁。原因是客户端把认证失败也当成网络抖动重试,或者同一服务器已经用另一个账号建立过连接,返回 1219。还有客户端缓存了旧凭据,改了密码仍然用旧会话。
解决:先看错误码再重试。1326 查账号格式、密码过期、账号锁定;1219 先取消已有连接或统一使用一个账号;53/67 查服务器名和共享名;5 查权限。重试逻辑里把认证失败排除掉,日志记录错误码和当前用户名,不要记录密码。上传结束后释放连接,避免下一次换账号时被旧会话挡住。
6. 进阶验证:把上传成功率、耗时和失败码变成可观测数据
上传功能写完不等于能交付。我一般会做一轮小验证:准备 10MB、100MB、1GB 三个文件,分别在同一网段和跨网段各跑三次;再模拟目标目录只读、共享名错误、上传中取消、同名文件已存在四种异常。每次记录总耗时、平均速度、重试次数、错误码和临时文件是否清理。这样做的目的不是追求漂亮数字,而是把“用户说上传慢”变成可比较的指标。
| 验证项 | 输入 | 观察指标 | 通过标准 |
|---|---|---|---|
| 小文件 | 10MB | 耗时、进度是否到 100% | 无残留临时文件 |
| 大文件 | 1GB | 平均速度、内存占用 | UI 不卡死、可取消 |
| 权限拒绝 | 只读目录 | 错误码、提示文案 | 不重试认证错误 |
| 网络中断 | 上传中禁用网卡 | 临时文件长度、重试次数 | 可续传或可清理 |
| 同名冲突 | 目标已存在 | 覆盖/跳过/改名行为 | 与配置一致 |
日志不要只写“上传失败”,至少带上文件名、源文件大小、目标路径、缓冲区大小、尝试次数、耗时和错误码。下面这段日志结构可以直接落 JSON Lines,一行一条,方便后面用脚本统计。
using System; using System.IO; using System.Text.Json; var logItem = new { Time = DateTimeOffset.Now, FileName = Path.GetFileName(sourcePath), SourceBytes = total, TargetPath = finalPath, BufferSize = bufferSize, Attempt = attempt, ElapsedMs = (int)stopwatch.ElapsedMilliseconds, ErrorCode = errorCode }; File.AppendAllText( logPath, JsonSerializer.Serialize(logItem) + Environment.NewLine);Time用来对齐用户反馈时间,SourceBytes和ElapsedMs能算平均速度,BufferSize方便比较不同参数,Attempt和ErrorCode用来区分是网络抖动还是权限问题。日志文件不要放在共享目录里,防止上传失败时连日志都写不进去。验证时也别只用管理员账号跑一遍就结束,普通用户、不同网段、目标目录已存在文件,这三种情况最容易把问题留到上线后。
从那以后我每次接入新的共享目录,都先跑一遍可写探测和 100MB 压测,再让业务方用自己的账号试一次,确认进度条、取消和错误提示都能对上,才把客户端发出去。希望帮到你。
本文还有配套的精品资源,点击获取