From c00812e653359a61c6c06fb306bb5d1811b61fb8 Mon Sep 17 00:00:00 2001 From: ShiYu Date: Thu, 22 May 2025 21:48:07 +0800 Subject: [PATCH] feat(storage): add WebDAV storage support #4 --- Controllers/PictureController.cs | 53 ++++- Extensions/ServiceCollectionExtensions.cs | 1 + .../Providers/WebDAVStorageProvider.cs | 209 ++++++++++++++++++ Services/Storage/StorageProviderAttribute.cs | 1 + View/src/pages/settings/SystemConfig.tsx | 25 ++- 5 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 Services/Storage/Providers/WebDAVStorageProvider.cs diff --git a/Controllers/PictureController.cs b/Controllers/PictureController.cs index cf7c2af..1b401d0 100644 --- a/Controllers/PictureController.cs +++ b/Controllers/PictureController.cs @@ -8,12 +8,15 @@ using System.Text.Json; using System.Text.Json.Serialization; using Foxel.Services.Configuration; using Foxel.Services.Media; +using Foxel.Services.Storage; +using System.IO; +using Foxel.Services.Attributes; namespace Foxel.Controllers; [Authorize] [Route("api/picture")] -public class PictureController(IPictureService pictureService,IConfigService configService) : BaseApiController +public class PictureController(IPictureService pictureService, IConfigService configService, IStorageService storageService) : BaseApiController { [HttpGet("get_pictures")] public async Task>> GetPictures( @@ -321,4 +324,52 @@ public class PictureController(IPictureService pictureService,IConfigService con [JsonPropertyName("file_path")] public string? FilePath { get; set; } } + + [HttpGet("proxy")] + [AllowAnonymous] + public async Task GetWebDavFile([FromQuery] string path) + { + try + { + if (string.IsNullOrEmpty(path)) + { + return BadRequest("文件路径不能为空"); + } + + // 下载文件到临时位置 + string filePath = await storageService.DownloadFileAsync(StorageType.WebDAV,path); + + // 确定内容类型 + string contentType = GetContentTypeFromPath(path); + + // 返回文件内容 + return PhysicalFile(filePath, contentType, Path.GetFileName(path)); + } + catch (Exception ex) + { + return StatusCode(500, $"代理获取WebDAV文件失败: {ex.Message}"); + } + } + + private string GetContentTypeFromPath(string path) + { + string extension = Path.GetExtension(path).ToLowerInvariant(); + + return extension switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".bmp" => "image/bmp", + ".webp" => "image/webp", + ".svg" => "image/svg+xml", + ".mp4" => "video/mp4", + ".avi" => "video/x-msvideo", + ".mov" => "video/quicktime", + ".pdf" => "application/pdf", + ".doc" or ".docx" => "application/msword", + ".xls" or ".xlsx" => "application/vnd.ms-excel", + _ => "application/octet-stream" + }; + } } \ No newline at end of file diff --git a/Extensions/ServiceCollectionExtensions.cs b/Extensions/ServiceCollectionExtensions.cs index 37e2c3d..d7fc7fe 100644 --- a/Extensions/ServiceCollectionExtensions.cs +++ b/Extensions/ServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } diff --git a/Services/Storage/Providers/WebDAVStorageProvider.cs b/Services/Storage/Providers/WebDAVStorageProvider.cs new file mode 100644 index 0000000..5010819 --- /dev/null +++ b/Services/Storage/Providers/WebDAVStorageProvider.cs @@ -0,0 +1,209 @@ +using System.Net.Http.Headers; +using System.Text; +using Foxel.Services.Attributes; +using Foxel.Services.Configuration; + +namespace Foxel.Services.Storage.Providers; + +[StorageProvider(StorageType.WebDAV)] +public class WebDavStorageProvider : IStorageProvider +{ + private readonly string _webDavServerUrl; + private readonly string _serverUrl; + private readonly string _basePath; + private readonly string _publicUrl; + private readonly HttpClient _httpClient; + + public WebDavStorageProvider(IConfigService configService) + { + _webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/'); + var userName = configService["Storage:WebDAVUserName"]; + var password = configService["Storage:WebDAVPassword"]; + _basePath = configService["Storage:WebDAVBasePath"].Trim('/'); + _publicUrl = configService["Storage:WebDAVPublicUrl"].TrimEnd('/'); + _serverUrl = configService["AppSettings:ServerUrl"]; + _httpClient = new HttpClient(); + if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password)) + { + var authValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}")); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue); + } + } + + public async Task SaveAsync(Stream fileStream, string fileName, string contentType) + { + try + { + // 创建唯一的文件存储路径 + string currentDate = DateTime.Now.ToString("yyyy/MM"); + string ext = Path.GetExtension(fileName); + string newFileName = $"{Guid.NewGuid()}{ext}"; + string relativePath = $"{_basePath}/{currentDate}/{newFileName}"; + + // 确保目录存在 + await EnsureDirectoryExistsAsync($"{_basePath}/{currentDate}"); + + // 上传文件内容 + var requestUri = $"{_webDavServerUrl}/{relativePath}"; + using var content = new StreamContent(fileStream); + content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + + using var request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Content = content; + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + return relativePath; + } + catch (Exception ex) + { + Console.WriteLine($"上传文件到WebDAV时出错: {ex.Message}"); + throw; + } + } + + public async Task DeleteAsync(string storagePath) + { + try + { + if (string.IsNullOrEmpty(storagePath)) + return; + + var requestUri = $"{_webDavServerUrl}/{storagePath}"; + var response = await _httpClient.DeleteAsync(requestUri); + + if (response.StatusCode != System.Net.HttpStatusCode.NotFound) + { + response.EnsureSuccessStatusCode(); + } + } + catch (Exception ex) + { + Console.WriteLine($"从WebDAV删除文件时出错: {ex.Message}"); + } + } + + public string GetUrl(string storagePath) + { + try + { + if (string.IsNullOrEmpty(storagePath)) + return "/images/unavailable.gif"; + + if (!string.IsNullOrEmpty(_publicUrl)) + { + return $"{_publicUrl}/{storagePath}"; + } + + return $"{_serverUrl}/api/picture/proxy?path={Uri.EscapeDataString(storagePath)}"; + } + catch (Exception ex) + { + Console.WriteLine($"生成WebDAV文件URL时出错: {ex.Message}"); + return "/images/unavailable.gif"; + } + } + + public async Task DownloadFileAsync(string storagePath) + { + try + { + if (string.IsNullOrEmpty(storagePath)) + { + throw new ArgumentException("存储路径不能为空"); + } + + // 创建临时目录 + var tempDir = Path.Combine(Path.GetTempPath(), "FoxelWebDAVTemp"); + if (!Directory.Exists(tempDir)) + { + Directory.CreateDirectory(tempDir); + } + + // 创建临时文件名 + string fileName = Path.GetFileName(storagePath); + string tempFilePath = Path.Combine(tempDir, fileName); + + // 下载文件 + var requestUri = $"{_webDavServerUrl}/{storagePath}"; + using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + await using var fileStream = new FileStream(tempFilePath, FileMode.Create); + await response.Content.CopyToAsync(fileStream); + + return tempFilePath; + } + catch (Exception ex) + { + Console.WriteLine($"从WebDAV下载文件时出错: {ex.Message}"); + throw; + } + } + + /// + /// 确保WebDAV上的目录存在 + /// + private async Task EnsureDirectoryExistsAsync(string directoryPath) + { + try + { + var requestUri = $"{_webDavServerUrl}/{directoryPath}"; + + // 检查目录是否存在 - 使用新的请求对象 + using var headRequest = new HttpRequestMessage(HttpMethod.Head, requestUri); + var response = await _httpClient.SendAsync(headRequest); + + if (response.IsSuccessStatusCode) + return; + + // 创建目录 - 使用新的请求对象 + using var mkcolRequest = new HttpRequestMessage(new HttpMethod("MKCOL"), requestUri); + response = await _httpClient.SendAsync(mkcolRequest); + + // 处理状态码 + if (response.StatusCode == System.Net.HttpStatusCode.Conflict || + response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + // 递归创建父目录 + var parentPath = Path.GetDirectoryName(directoryPath.TrimEnd('/'))?.Replace('\\', '/'); + if (!string.IsNullOrEmpty(parentPath)) + { + await EnsureDirectoryExistsAsync(parentPath); + + using var retryRequest = new HttpRequestMessage(new HttpMethod("MKCOL"), requestUri); + response = await _httpClient.SendAsync(retryRequest); + + if (response.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed) + { + using var putRequest = new HttpRequestMessage(HttpMethod.Put, $"{requestUri}/.dummy"); + putRequest.Content = new StringContent(string.Empty); + await _httpClient.SendAsync(putRequest); + } + else + { + response.EnsureSuccessStatusCode(); + } + } + } + else if (response.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed) + { + using var putRequest = new HttpRequestMessage(HttpMethod.Put, $"{requestUri}/.dummy"); + putRequest.Content = new StringContent(string.Empty); + var putResponse = await _httpClient.SendAsync(putRequest); + putResponse.EnsureSuccessStatusCode(); + } + else + { + response.EnsureSuccessStatusCode(); + } + } + catch (Exception ex) + { + Console.WriteLine($"确保WebDAV目录存在时出错: {ex.Message}"); + throw; + } + } +} \ No newline at end of file diff --git a/Services/Storage/StorageProviderAttribute.cs b/Services/Storage/StorageProviderAttribute.cs index 655b857..cb3692b 100644 --- a/Services/Storage/StorageProviderAttribute.cs +++ b/Services/Storage/StorageProviderAttribute.cs @@ -8,6 +8,7 @@ public enum StorageType Telegram = 1, S3 = 2, Cos = 3, + WebDAV = 4, } diff --git a/View/src/pages/settings/SystemConfig.tsx b/View/src/pages/settings/SystemConfig.tsx index f3f70f5..932e1dd 100644 --- a/View/src/pages/settings/SystemConfig.tsx +++ b/View/src/pages/settings/SystemConfig.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Tabs, Card, message, Spin, Select } from 'antd'; -import { CloudOutlined, DatabaseOutlined, CloudServerOutlined } from '@ant-design/icons'; +import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined } from '@ant-design/icons'; import { getAllConfigs, setConfig } from '../../api'; import ConfigGroup from './ConfigGroup.tsx'; import useIsMobile from '../../hooks/useIsMobile'; @@ -88,6 +88,7 @@ const SystemConfig: React.FC = () => { { value: 'Telegram', label: 'Telegram 频道', icon: }, { value: 'S3', label: '亚马逊 S3', icon: }, { value: 'Cos', label: '腾讯云 COS', icon: }, + { value: 'WebDAV', label: 'WebDAV 存储', icon: }, ]; useEffect(() => { @@ -317,6 +318,28 @@ const SystemConfig: React.FC = () => { isMobile={isMobile} /> )} + + {storageType === 'WebDAV' && ( + + )} )}