当前位置: 首页 > news >正文

.NET4通过HTTP操作MINIO

MINIO是提供.NET SDK的,但是这么老的版本没找到,于是使用http的方式直接调用,方便简单。

我这里需求不复杂,只需要上传下载删除即可,如果后续有需求再补充方法。

image

核心代码MinioHttpOperatorDemo如下:

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Collections.Specialized; // For NameValueCollection, though not directly used in this version, good to keep for potential expansion.
using System.Security.Cryptography; // For HMACSHA256, SHA256Managed
using System.Globalization; // For CultureInfo.InvariantCulture
using System.Collections.Generic; // For SortedList
using System.Linq; // For string.Join and other LINQ operations
using System.Xml.Linq;namespace MinioHttpOperatorDemo
{/// <summary>/// MinIO HTTP 操作类,适用于 .NET Framework 4.0 环境,不依赖 MinIO SDK。/// 使用 HttpWebRequest 和 HttpWebResponse 进行文件上传、下载和删除操作。/// 已加入 AWS Signature Version 4 认证的简化实现,并进行了进一步的完善。/// </summary>public class MinioHttpOperator{private readonly string _minioEndpoint; // MinIO 服务器的端点地址,例如: http://localhost:9000private readonly string _accessKey;     // MinIO Access Keyprivate readonly string _secretKey;     // MinIO Secret Keyprivate readonly string _region;        // S3 兼容 API 需要的区域,MinIO 通常用 "us-east-1"private readonly string _service;       // S3 兼容 API 需要的服务名称,通常是 "s3"/// <summary>/// 构造函数,初始化 MinIO 操作器。/// </summary>/// <param name="minioEndpoint">MinIO 服务器的 URL,例如 "http://localhost:9000"</param>/// <param name="accessKey">Access Key,用于认证。</param>/// <param name="secretKey">Secret Key,用于认证。</param>/// <param name="region">S3 兼容 API 需要的区域,默认为 "us-east-1"。</param>/// <param name="service">S3 兼容 API 需要的服务名称,默认为 "s3"。</param>public MinioHttpOperator(string minioEndpoint, string accessKey, string secretKey, string region = "cn-north-1", string service = "s3"){// 移除末尾的斜杠,确保 URL 格式正确_minioEndpoint = minioEndpoint.TrimEnd('/');_accessKey = accessKey;_secretKey = secretKey;_region = region;_service = service;if (string.IsNullOrEmpty(_accessKey) || string.IsNullOrEmpty(_secretKey)){// 抛出异常而不是警告,因为没有凭据就无法认证throw new ArgumentNullException("AccessKey 和 SecretKey 不能为空,因为需要进行认证。");}}/// <summary>/// 上传文件到 MinIO。/// </summary>/// <param name="bucketName">目标桶的名称。</param>/// <param name="objectName">在桶中保存的对象名称(包含路径,例如 "myfolder/myfile.txt")。</param>/// <param name="filePath">本地待上传文件的完整路径。</param>/// <param name="contentType">文件的 MIME 类型,例如 "application/octet-stream"、"image/jpeg"、"text/plain"。</param>/// <returns>如果上传成功返回 true,否则返回 false。</returns>public bool UploadFile(string bucketName, string objectName, string filePath, string contentType = "application/octet-stream"){try{if (!File.Exists(filePath)){Console.WriteLine($"错误:文件未找到,路径:{filePath}");return false;}string url = $"{_minioEndpoint}/{bucketName}/{objectName}";HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);request.Method = "PUT";request.ContentType = contentType;// 计算文件内容的 SHA256 哈希值string contentHash;using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)){using (SHA256 sha256 = new SHA256Managed()){byte[] hashBytes = sha256.ComputeHash(fileStream);contentHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();}fileStream.Position = 0; // 重置流位置以便后续读取request.ContentLength = fileStream.Length; // 设置请求内容长度// 签名请求SignRequest(request, bucketName, objectName, contentHash);// 获取请求流并写入文件内容using (Stream requestStream = request.GetRequestStream()){byte[] buffer = new byte[4096]; // 4KB 缓冲区int bytesRead;while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0){requestStream.Write(buffer, 0, bytesRead);}}}using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()){if (response.StatusCode == HttpStatusCode.OK){Console.WriteLine($"成功上传文件 {objectName} 到桶 {bucketName}。");return true;}else{Console.WriteLine($"上传文件 {objectName} 失败。状态码:{response.StatusCode}");return false;}}}catch (WebException webEx){HandleWebException(webEx, "上传");return false;}catch (Exception ex){Console.WriteLine($"上传时发生未知错误:{ex.Message}");return false;}}/// <summary>/// 从 MinIO 下载文件。/// </summary>/// <param name="bucketName">源桶的名称。</param>/// <param name="objectName">要下载的对象名称。</param>/// <param name="savePath">本地保存文件的完整路径。</param>/// <returns>如果下载成功返回 true,否则返回 false。</returns>public bool DownloadFile(string bucketName, string objectName, string savePath){try{string url = $"{_minioEndpoint}/{bucketName}/{objectName}";HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);request.Method = "GET";// 对于 GET 请求,payload hash 是固定的空字符串的 SHA256 哈希string contentHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // SHA256("")SignRequest(request, bucketName, objectName, contentHash);using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()){if (response.StatusCode == HttpStatusCode.OK){using (Stream responseStream = response.GetResponseStream())using (FileStream fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)){byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0){fileStream.Write(buffer, 0, bytesRead);}}Console.WriteLine($"成功下载文件 {objectName} 到 {savePath}。");return true;}else{Console.WriteLine($"下载文件 {objectName} 失败。状态码:{response.StatusCode}");return false;}}}catch (WebException webEx){HandleWebException(webEx, "下载");return false;}catch (Exception ex){Console.WriteLine($"下载时发生未知错误:{ex.Message}");return false;}}/// <summary>/// 从 MinIO 删除文件。/// </summary>/// <param name="bucketName">文件所在桶的名称。</param>/// <param name="objectName">要删除的对象名称。</param>/// <returns>如果删除成功返回 true,否则返回 false。</returns>public bool DeleteFile(string bucketName, string objectName){try{string url = $"{_minioEndpoint}/{bucketName}/{objectName}";HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);request.Method = "DELETE";// 对于 DELETE 请求,payload hash 是固定的空字符串的 SHA256 哈希string contentHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // SHA256("")SignRequest(request, bucketName, objectName, contentHash);using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()){// 成功的 DELETE 请求通常返回 204 No Contentif (response.StatusCode == HttpStatusCode.NoContent){Console.WriteLine($"成功删除文件 {objectName} 从桶 {bucketName}。");return true;}else{Console.WriteLine($"删除文件 {objectName} 失败。状态码:{response.StatusCode}");return false;}}}catch (WebException webEx){HandleWebException(webEx, "删除");return false;}catch (Exception ex){Console.WriteLine($"删除时发生未知错误:{ex.Message}");return false;}} /// <summary>/// 处理 Web 请求异常并输出详细信息。/// </summary>/// <param name="webEx">WebException 实例。</param>/// <param name="operation">发生异常的操作名称(例如:"上传"、"下载")。</param>private void HandleWebException(WebException webEx, string operation){if (webEx.Response != null){using (StreamReader reader = new StreamReader(webEx.Response.GetResponseStream())){string responseText = reader.ReadToEnd();Console.WriteLine($"{operation}时发生 Web 异常:{webEx.Message}。状态码:{(int)((HttpWebResponse)webEx.Response).StatusCode}。响应内容:{responseText}");}}else{Console.WriteLine($"{operation}时发生 Web 异常:{webEx.Message}");}}/// <summary>/// 为 HttpWebRequest 签名 AWS Signature Version 4 认证头部。/// 这是 AWS Signature Version 4 规范的简化实现,旨在与 MinIO 兼容。/// </summary>/// <param name="request">要签名的 HttpWebRequest 实例。</param>/// <param name="bucketName">桶名称。</param>/// <param name="objectName">对象名称。</param>/// <param name="contentHash">请求体的 SHA256 哈希值。</param>private void SignRequest(HttpWebRequest request, string bucketName, string objectName, string contentHash){// --- 步骤 1: 创建 Canonical Request ---// 1.1 HTTP 方法string httpRequestMethod = request.Method;// 1.2 Canonical URI// 对象名必须进行 URI 编码,但斜杠 (/) 作为路径分隔符不能被编码。// Uri.EscapeDataString 会编码 '/' 为 '%2F',需要将其替换回来。// 确保 objectName 不以斜杠开头,因为 canonicalUri 会添加一个。string cleanedObjectName = objectName.StartsWith("/") ? objectName.Substring(1) : objectName;string encodedObjectName = Uri.EscapeDataString(cleanedObjectName).Replace("%2F", "/");string canonicalUri = $"/{bucketName}/{encodedObjectName}";// 1.3 Canonical Query String (本示例不处理查询参数,因此为空)string canonicalQueryString = "";// 1.4 Canonical Headers// 头部名称必须小写,并按字典序排序。// 头部值必须去除前导/尾随空格,多个空格替换为单个空格。// Host 头部必须包含端口(如果是非默认端口)。var headersToSign = new SortedList<string, string>();// Host 头部string hostHeaderValue = request.RequestUri.Host;if (!request.RequestUri.IsDefaultPort){hostHeaderValue += ":" + request.RequestUri.Port;}headersToSign.Add("host", hostHeaderValue);// x-amz-content-sha256 头部headersToSign.Add("x-amz-content-sha256", contentHash);// x-amz-date 头部DateTime requestDateTime = DateTime.UtcNow;string amzDate = requestDateTime.ToString("yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture);headersToSign.Add("x-amz-date", amzDate);// Content-Type 头部 (仅用于 PUT/POST 请求)if (request.Method == "PUT" || request.Method == "POST"){string actualContentType = request.ContentType;if (string.IsNullOrEmpty(actualContentType)){actualContentType = "application/octet-stream"; // 签名时使用的默认 Content-Type}headersToSign.Add("content-type", actualContentType);}// 构建 canonicalHeaders 字符串StringBuilder canonicalHeadersBuilder = new StringBuilder();foreach (var header in headersToSign){canonicalHeadersBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0}:{1}\n", header.Key, header.Value.Trim()); // trim header values}string canonicalHeaders = canonicalHeadersBuilder.ToString();// 1.5 Signed Headers// 包含在规范化头部中所有头部名称的列表,小写,按字典序排序,用分号分隔。string signedHeaders = string.Join(";", headersToSign.Keys.ToArray());// 1.6 Payload Hash (已在方法参数中提供)// 1.7 组合 Canonical Requeststring canonicalRequest = string.Format(CultureInfo.InvariantCulture,"{0}\n{1}\n{2}\n{3}\n{4}\n{5}",httpRequestMethod,canonicalUri,canonicalQueryString,canonicalHeaders, // 注意这里已经包含了末尾的换行符signedHeaders,contentHash);// --- 步骤 2: 创建 String to Sign ---// Algorithmstring algorithm = "AWS4-HMAC-SHA256";// Credential Scopestring dateStamp = requestDateTime.ToString("yyyyMMdd", CultureInfo.InvariantCulture);string credentialScope = string.Format(CultureInfo.InvariantCulture,"{0}/{1}/{2}/aws4_request",dateStamp,_region,_service);// Hash of Canonical Requeststring hashedCanonicalRequest = ToHex(Hash(Encoding.UTF8.GetBytes(canonicalRequest)));// 组合 String to Signstring stringToSign = string.Format(CultureInfo.InvariantCulture,"{0}\n{1}\n{2}\n{3}",algorithm,amzDate,credentialScope,hashedCanonicalRequest);// --- 步骤 3: 计算签名 ---// Signing Key 派生byte[] kSecret = Encoding.UTF8.GetBytes("AWS4" + _secretKey);byte[] kDate = HmacSha256(kSecret, dateStamp);byte[] kRegion = HmacSha256(kDate, _region);byte[] kService = HmacSha256(kRegion, _service);byte[] kSigning = HmacSha256(kService, "aws4_request");// 计算最终签名byte[] signatureBytes = HmacSha256(kSigning, stringToSign);string signature = ToHex(signatureBytes);// --- 步骤 4: 添加 Authorization 头部 ---string authorizationHeader = string.Format(CultureInfo.InvariantCulture,"{0} Credential={1}/{2}, SignedHeaders={3}, Signature={4}",algorithm,_accessKey,credentialScope,signedHeaders,signature);request.Headers["Authorization"] = authorizationHeader;// 设置 x-amz-date 头部(如果尚未设置)request.Headers["x-amz-date"] = amzDate;// 设置 x-amz-content-sha256 头部(如果尚未设置)request.Headers["x-amz-content-sha256"] = contentHash;}/// <summary>/// 计算字节数组的 SHA256 哈希值。/// </summary>private static byte[] Hash(byte[] bytes){using (SHA256 sha256 = new SHA256Managed()){return sha256.ComputeHash(bytes);}}/// <summary>/// 计算 HMAC-SHA256 哈希值。/// </summary>private static byte[] HmacSha256(byte[] key, string data){using (HMACSHA256 hmac = new HMACSHA256(key)){return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));}}/// <summary>/// 将字节数组转换为十六进制字符串。/// </summary>private static string ToHex(byte[] bytes){return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();}}
}

测试代码如下:

using System;
using System.IO;
using System.Threading;namespace MinioHttpOperatorDemo
{class Program{static void Main(string[] args){   // 替换为您的 MinIO 实例的地址string minioEndpoint = "http://127.xxx.xxx.xxx:9000";// 如果您的 MinIO 实例需要认证,请在这里提供您的 AccessKey 和 SecretKey。// 例如:string accessKey = "accessKey ";string secretKey = "secretKey ";MinioHttpOperator minioOperator = new MinioHttpOperator(minioEndpoint, accessKey, secretKey);// 如果 MinIO 允许匿名访问,则无需提供 AccessKey 和 SecretKey//MinioHttpOperator minioOperator = new MinioHttpOperator(minioEndpoint);// 确保此桶在 MinIO 中存在或 MinIO 服务器允许自动创建桶。string bucketName = "bucketName ";string testFolder = "test";// --- 准备多个测试文件 ---string localFilePath1 = Path.Combine(Path.GetTempPath(), "testfile1.txt");string localFilePath2 = Path.Combine(Path.GetTempPath(), "testfile2.jpg"); // 模拟图片文件string minioObjectName1 = $"{testFolder}/document1.txt";string minioObjectName2 = $"{testFolder}/image.jpg";string content1 = "This is the content for document one.";byte[] content2 = new byte[1024]; // 模拟一个1KB的二进制数据作为图片内容new Random().NextBytes(content2); // 填充随机字节try{File.WriteAllText(localFilePath1, content1);File.WriteAllBytes(localFilePath2, content2);Console.WriteLine($"已在本地创建测试文件:{localFilePath1} 和 {localFilePath2}");}catch (Exception ex){Console.WriteLine($"创建测试文件失败:{ex.Message}");Console.WriteLine("请检查文件路径和权限。程序将退出。");Console.ReadKey();return;}Console.WriteLine("\n--- 开始 MinIO 批量操作 ---");// --- 上传第一个文件 ---Console.WriteLine($"\n尝试上传文件:{localFilePath1} 到 {minioEndpoint}/{bucketName}/{minioObjectName1}...");if (minioOperator.UploadFile(bucketName, minioObjectName1, localFilePath1, "text/plain")){Console.WriteLine("文件上传成功。");}else{Console.WriteLine("文件上传失败。");}Thread.Sleep(1000); // 稍作等待// --- 上传第二个文件 ---Console.WriteLine($"\n尝试上传文件:{localFilePath2} 到 {minioEndpoint}/{bucketName}/{minioObjectName2}...");if (minioOperator.UploadFile(bucketName, minioObjectName2, localFilePath2, "image/jpeg")){Console.WriteLine("文件上传成功。");}else{Console.WriteLine("文件上传失败。");}Thread.Sleep(1000); // 稍作等待Console.WriteLine("\n----------------------------------------");// --- 下载第一个文件 ---string downloadSavePath1 = Path.Combine(Path.GetTempPath(), "downloaded_document1.txt");Console.WriteLine($"\n尝试从 {minioEndpoint}/{bucketName}/{minioObjectName1} 下载文件到 {downloadSavePath1}...");if (minioOperator.DownloadFile(bucketName, minioObjectName1, downloadSavePath1)){Console.WriteLine("文件下载成功。");try{Console.WriteLine($"下载文件的内容:{File.ReadAllText(downloadSavePath1)}");}catch (Exception ex){Console.WriteLine($"读取下载文件内容失败:{ex.Message}");}}else{Console.WriteLine("文件下载失败。");}Thread.Sleep(1000); // 稍作等待// --- 尝试下载一个不存在的文件 ---string nonExistentObject = $"{testFolder}/nonexistent.pdf";string downloadNonExistentPath = Path.Combine(Path.GetTempPath(), "nonexistent.pdf");Console.WriteLine($"\n尝试下载不存在的文件:{nonExistentObject}...");if (!minioOperator.DownloadFile(bucketName, nonExistentObject, downloadNonExistentPath)){Console.WriteLine("下载不存在的文件失败(预期结果)。");}Thread.Sleep(1000); // 稍作等待Console.WriteLine("\n----------------------------------------");// --- 删除第一个文件 ---Console.WriteLine($"\n尝试从 {minioEndpoint}/{bucketName} 删除文件 {minioObjectName1}...");if (minioOperator.DeleteFile(bucketName, minioObjectName1)){Console.WriteLine("文件删除成功。");}else{Console.WriteLine("文件删除失败。");}Thread.Sleep(1000); // 稍作等待// --- 删除第二个文件 ---Console.WriteLine($"\n尝试从 {minioEndpoint}/{bucketName} 删除文件 {minioObjectName2}...");if (minioOperator.DeleteFile(bucketName, minioObjectName2)){Console.WriteLine("文件删除成功。");}else{Console.WriteLine("文件删除失败。");}Console.WriteLine("\n--- MinIO 批量操作结束 ---");// 清理本地创建的测试文件try{if (File.Exists(localFilePath1)){File.Delete(localFilePath1);Console.WriteLine($"已清理本地测试文件:{localFilePath1}");}if (File.Exists(localFilePath2)){File.Delete(localFilePath2);Console.WriteLine($"已清理本地测试文件:{localFilePath2}");}if (File.Exists(downloadSavePath1)){File.Delete(downloadSavePath1);Console.WriteLine($"已清理本地下载文件:{downloadSavePath1}");}if (File.Exists(downloadNonExistentPath)){File.Delete(downloadNonExistentPath);Console.WriteLine($"已清理本地下载文件:{downloadNonExistentPath}");}}catch (Exception ex){Console.WriteLine($"清理本地文件失败:{ex.Message}");}Console.WriteLine("\n按任意键退出程序。");Console.ReadKey();}}
}
http://www.sczhlp.com/news/301.html

相关文章:

  • Gitee:重塑中国企业级研发基础设施的三大战略支点
  • SAP生产订单报工的“最终确认”、“结清未清预留”,你真弄清楚了吗?
  • 基于图像处理与SVM的验证码识别系统实现
  • 基于因子图与和积算法的MATLAB实现
  • 【文献阅读】AnyEdit:编辑语言模型中编码的任何知识
  • Web前端入门第 82 问:JavaScript cookie 有大小限制吗?溢出会怎样?
  • 二分
  • lazarus无法编译Linux下的动态库
  • 微信小程序提示不在合法域名问题
  • Clop勒索团伙针对MoveIt Transfer软件的大规模攻击活动分析
  • 语音解耦技术推动语音AI的多样性与包容性
  • 银河麒麟V10离线安装 tomcat 9 记录
  • fiddler篡改数据
  • Docker
  • SpringMVC具体的工作流程
  • SketchUp 2021+必备插件|AFU321 v5.5.6安装与使用说明
  • SketchUp纹理神器:Architextures插件安装与使用教程(图文详解)
  • redis-基本使用
  • nepCTF2025 pwn题解
  • 论文解读《GradEscape: A Gradient-Based Evader Against AI-Generated Text Detectors》
  • 使用 DeepSpeed ZeRO、LoRA 和 Flash Attention 微调 Falcon 180B
  • 28、快捷键
  • linux系统添加Arial字体
  • 基于卷积神经网络的验证码识别系统设计与实现
  • 【数据库索引标准结构】B+树原理详解与B树对比优势
  • 12N90-ASEMI电源逆变器专用12N90
  • Locust入门及最佳实践
  • Gitee Git自建平台:企业级代码托管的安全之选
  • Java核心面试技术
  • 人力资源各系统的关联与一体化趋势:从独立到协同的必然之路