Files
BetterLyrics/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/FileSystemService/Providers/SMBFileSystem.cs
Zhe Fang 6ca2d1f897 chores
2025-12-30 19:49:54 -05:00

204 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using SMBLibrary;
using SMBLibrary.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
public partial class SMBFileSystem : IUnifiedFileSystem
{
private SMB2Client? _client;
private ISMBFileStore? _fileStore;
// 保存配置对象的引用
private readonly MediaFolder _config;
// 缓存解析出来的 Share 名称,因为 TreeConnect 要用
private string _shareName;
public SMBFileSystem(MediaFolder config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
// 在构造时就解析好 Share 名称,避免后续重复解析
var uri = _config.GetStandardUri();
// Segments[0] 是 "/", Segments[1] 是 "ShareName/"
if (uri.Segments.Length > 1)
{
_shareName = uri.Segments[1].TrimEnd('/');
}
else
{
// 如果没有 ShareName这在 SMB 中通常是不合法的,但在根目录下可能发生
_shareName = "";
}
}
public async Task<bool> ConnectAsync()
{
_client = new SMB2Client();
// 连接主机
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
// 登录
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 连接共享目录 (TreeConnect)
// SMBLibrary 必须先连接到 Share后续所有文件操作都是基于这个 Share 的相对路径
if (string.IsNullOrEmpty(_shareName)) return false;
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
/// <summary>
/// 获取文件列表
/// </summary>
/// <param name="parentFolder">
/// 传入要列出的文件夹实体。
/// 如果传入 null则默认列出 MediaFolder 配置的根目录。
/// </param>
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FilesIndexItem>();
if (_fileStore == null) return result;
string smbPath = GetPathRelativeToShare(parentFolder);
var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, smbPath,
AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read,
CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
if (statusRet != NTStatus.STATUS_SUCCESS) return result;
string parentUriString = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri;
List<QueryDirectoryFileInformation> fileInfo;
do
{
statusRet = _fileStore.QueryDirectory(out fileInfo, handle, "*", FileInformationClass.FileDirectoryInformation);
// 如果查询失败或者没有更多文件fileInfo 可能是 null直接跳出
if (statusRet != NTStatus.STATUS_SUCCESS && statusRet != NTStatus.STATUS_NO_MORE_FILES)
{
break;
}
// 如果是 NO_MORE_FILES 但 fileInfo 依然有残留数据(极少见),或者是 SUCCESS
if (fileInfo != null)
{
foreach (var item in fileInfo.Cast<FileDirectoryInformation>())
{
if (item.FileName == "." || item.FileName == "..") continue;
// 过滤隐藏文件和系统文件
if ((item.FileAttributes & SMBLibrary.FileAttributes.Hidden) == SMBLibrary.FileAttributes.Hidden ||
(item.FileAttributes & SMBLibrary.FileAttributes.System) == SMBLibrary.FileAttributes.System)
{
continue;
}
bool isDir = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory;
// 后缀名过滤
if (!isDir)
{
string extension = Path.GetExtension(item.FileName);
if (string.IsNullOrEmpty(extension) || !FileHelper.AllSupportedExtensions.Contains(extension)) continue;
}
if (!parentUriString.EndsWith("/")) parentUriString += "/";
var baseUri = new Uri(parentUriString);
var newUri = new Uri(baseUri, item.FileName);
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
Uri = newUri.AbsoluteUri,
FileName = item.FileName,
IsDirectory = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
FileSize = item.AllocationSize,
LastModified = item.ChangeTime
});
}
}
if (statusRet == NTStatus.STATUS_NO_MORE_FILES) break;
} while (statusRet == NTStatus.STATUS_SUCCESS);
_fileStore.CloseFile(handle);
return result;
}
/// <summary>
/// 打开文件流
/// </summary>
/// <param name="file">只需要传入文件实体即可</param>
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (_fileStore == null || file == null) return null;
string smbPath = GetPathRelativeToShare(file);
var ret = _fileStore.CreateFile(out object handle, out FileStatus status, smbPath,
AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, 0, null);
if (ret != NTStatus.STATUS_SUCCESS)
throw new IOException($"SMB Open Error: {ret}");
return new SMBReadOnlyStream(_fileStore, handle);
}
public async Task DisconnectAsync()
{
_client?.Disconnect();
await Task.CompletedTask;
}
public void Dispose()
{
_client?.Disconnect();
}
private string GetPathRelativeToShare(FilesIndexItem? entity)
{
Uri targetUri;
if (entity == null)
{
targetUri = _config.GetStandardUri();
}
else
{
targetUri = new Uri(entity.Uri);
}
string absolutePath = Uri.UnescapeDataString(targetUri.AbsolutePath);
string cleanPath = absolutePath.TrimStart('/');
int slashIndex = cleanPath.IndexOf('/');
if (slashIndex == -1)
{
return string.Empty;
}
string relativePath = cleanPath.Substring(slashIndex + 1);
return relativePath.Replace("/", "\\");
}
}
}