mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-12 02:20:28 +08:00
feat(storage): add WebDAV storage support #4
This commit is contained in:
@@ -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<ActionResult<PaginatedResult<PictureResponse>>> 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<IActionResult> 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<TelegramStorageProvider>();
|
||||
services.AddSingleton<S3StorageProvider>();
|
||||
services.AddSingleton<CosStorageProvider>();
|
||||
services.AddSingleton<WebDavStorageProvider>();
|
||||
services.AddSingleton<IStorageService, StorageService>();
|
||||
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
|
||||
}
|
||||
|
||||
209
Services/Storage/Providers/WebDAVStorageProvider.cs
Normal file
209
Services/Storage/Providers/WebDAVStorageProvider.cs
Normal file
@@ -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<string> 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<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保WebDAV上的目录存在
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public enum StorageType
|
||||
Telegram = 1,
|
||||
S3 = 2,
|
||||
Cos = 3,
|
||||
WebDAV = 4,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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: <CloudOutlined style={{ color: '#0088cc' }} /> },
|
||||
{ value: 'S3', label: '亚马逊 S3', icon: <CloudServerOutlined style={{ color: '#ff9900' }} /> },
|
||||
{ value: 'Cos', label: '腾讯云 COS', icon: <CloudServerOutlined style={{ color: '#00a4ff' }} /> },
|
||||
{ value: 'WebDAV', label: 'WebDAV 存储', icon: <GlobalOutlined style={{ color: '#1890ff' }} /> },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -317,6 +318,28 @@ const SystemConfig: React.FC = () => {
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{storageType === 'WebDAV' && (
|
||||
<ConfigGroup
|
||||
groupName="Storage"
|
||||
configs={{
|
||||
"WebDAVServerUrl": configs.Storage?.WebDAVServerUrl || '',
|
||||
"WebDAVUserName": configs.Storage?.WebDAVUserName || '',
|
||||
"WebDAVPassword": configs.Storage?.WebDAVPassword || '',
|
||||
"WebDAVBasePath": configs.Storage?.WebDAVBasePath || '',
|
||||
"WebDAVPublicUrl": configs.Storage?.WebDAVPublicUrl || '',
|
||||
}}
|
||||
onSave={handleSaveConfig}
|
||||
descriptions={{
|
||||
"WebDAVServerUrl": 'WebDAV 服务器 URL (例如: https://dav.example.com)',
|
||||
"WebDAVUserName": 'WebDAV 用户名',
|
||||
"WebDAVPassword": 'WebDAV 密码',
|
||||
"WebDAVBasePath": 'WebDAV 基础路径 (例如: files/upload)',
|
||||
"WebDAVPublicUrl": 'WebDAV 公共访问 URL (可选,用于文件访问)',
|
||||
}}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user