using Model; using Model.Dto.System; using Model.Entity.System; using Service.Interface; using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace Service.Implement { /// /// 本地文件系统存储 Provider /// 落地策略:存到 BasePath/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext},URL = {Endpoint}/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext} /// wwwroot/uploads 由 Program.UseStaticFiles 提供访问;MinIO/OSS 实现见 IFileStorageProvider 扩展点 /// public class LocalFileStorageProvider : IFileStorageProvider { public string ProviderName => "local"; public async Task> UploadAsync(Stream stream, string originalName, long size, FileStorageConfigEntity config) { if (config == null) return Result.Error("存储配置不能为空"); if (stream == null || !stream.CanRead) return Result.Error("文件流不可读"); try { var basePath = ResolveBasePath(config.BasePath); if (string.IsNullOrWhiteSpace(basePath)) return Result.Error("Local 存储必须配置 BasePath"); var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? ""; var now = DateTime.Now; var datePath = $"{now:yyyy}/{now:MM}/{now:dd}"; var fileName = $"{Guid.NewGuid():N}{ext}"; var storedFileName = string.IsNullOrWhiteSpace(config.Bucket) ? $"{datePath}/{fileName}" : $"{config.Bucket}/{datePath}/{fileName}"; var dir = Path.Combine(basePath, string.IsNullOrWhiteSpace(config.Bucket) ? datePath : Path.Combine(config.Bucket, datePath)); Directory.CreateDirectory(dir); var fullPath = Path.Combine(dir, fileName); using (var fs = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { await stream.CopyToAsync(fs); } var url = BuildUrl(config.Endpoint, config.Bucket, storedFileName); return Result.Success(new FileUploadResultDto { FileName = storedFileName, Url = url, Ext = ext, Size = size, Provider = ProviderName }); } catch (Exception ex) { return Result.Error("本地文件上传失败", ex); } } public Task DeleteAsync(string storedFileName, FileStorageConfigEntity config) { if (config == null || string.IsNullOrWhiteSpace(storedFileName)) return Task.FromResult(Result.Error("存储配置或文件名不能为空")); try { var basePath = ResolveBasePath(config.BasePath); var fullPath = Path.Combine(basePath, storedFileName.Replace('/', Path.DirectorySeparatorChar)); if (File.Exists(fullPath)) File.Delete(fullPath); return Task.FromResult(Result.Success()); } catch (Exception ex) { return Task.FromResult(Result.Error("删除本地文件失败", ex)); } } public Task TestConnectionAsync(FileStorageConfigEntity config) { if (config == null) return Task.FromResult(Result.Error("存储配置不能为空")); try { var basePath = ResolveBasePath(config.BasePath); if (string.IsNullOrWhiteSpace(basePath)) return Task.FromResult(Result.Error("Local 存储必须配置 BasePath")); Directory.CreateDirectory(basePath); var testFile = Path.Combine(basePath, $".storage_test_{Guid.NewGuid():N}"); File.WriteAllText(testFile, "ok"); File.Delete(testFile); return Task.FromResult(Result.Success()); } catch (Exception ex) { return Task.FromResult(Result.Error("Local 连接测试失败:" + ex.Message)); } } // ==================== 私有工具 ==================== private static string ResolveBasePath(string? basePath) { if (string.IsNullOrWhiteSpace(basePath)) return string.Empty; // 相对路径以运行目录为基准(如 wwwroot/uploads) return Path.IsPathRooted(basePath) ? basePath : Path.Combine(AppContext.BaseDirectory, basePath); } private static string BuildUrl(string? endpoint, string? bucket, string storedFileName) { var segs = new List(); if (!string.IsNullOrWhiteSpace(endpoint)) segs.Add(endpoint.TrimEnd('/')); if (!string.IsNullOrWhiteSpace(bucket)) segs.Add(bucket.Trim('/')); segs.Add(storedFileName); return string.Join('/', segs); } } }