diff --git a/java/pom.xml b/java/pom.xml index 49eab6e..aecd25c 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -99,12 +99,6 @@ 30.0-jre - - - org.springframework.boot - spring-boot-starter-tomcat - provided - @@ -159,6 +153,14 @@ 1.2.3 + + javax.servlet + javax.servlet-api + 3.1.0 + + + + org.springframework.boot diff --git a/java/src/main/java/com/yfd/platform/modules/storage/controller/base/StorageSourceController.java b/java/src/main/java/com/yfd/platform/modules/storage/controller/base/StorageSourceController.java index 6305fb9..4637e78 100644 --- a/java/src/main/java/com/yfd/platform/modules/storage/controller/base/StorageSourceController.java +++ b/java/src/main/java/com/yfd/platform/modules/storage/controller/base/StorageSourceController.java @@ -1,13 +1,11 @@ package com.yfd.platform.modules.storage.controller.base; -import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import com.github.xiaoymin.knife4j.annotations.ApiSort; import com.yfd.platform.config.ResponseResult; import com.yfd.platform.modules.config.model.request.SaveStorageSourceRequest; import com.yfd.platform.modules.config.model.request.UpdateStorageSortRequest; -import com.yfd.platform.modules.experimentalData.domain.TsFiles; import com.yfd.platform.modules.storage.convert.StorageSourceConvert; import com.yfd.platform.modules.storage.model.dto.StorageSourceDTO; import com.yfd.platform.modules.storage.model.entity.StorageSource; @@ -22,7 +20,6 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; -import java.util.stream.Collectors; /** * 存储源基础设置模块接口 diff --git a/java/src/main/java/com/yfd/platform/modules/storage/convert/impl/StorageSourceConvertImpl.java b/java/src/main/java/com/yfd/platform/modules/storage/convert/impl/StorageSourceConvertImpl.java index 76a2971..42df9f2 100644 --- a/java/src/main/java/com/yfd/platform/modules/storage/convert/impl/StorageSourceConvertImpl.java +++ b/java/src/main/java/com/yfd/platform/modules/storage/convert/impl/StorageSourceConvertImpl.java @@ -18,21 +18,36 @@ import com.yfd.platform.modules.storage.model.result.StorageSourceAdminResult; import com.yfd.platform.modules.storage.model.result.StorageSourceConfigResult; import com.yfd.platform.modules.storage.model.result.StorageSourceResult; import com.yfd.platform.utils.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; import org.springframework.stereotype.Service; import javax.annotation.Resource; -import java.io.File; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.stream.Collectors; - +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; /** * @Date: 2025/1/10 12:44 * @Description: @@ -161,35 +176,54 @@ public class StorageSourceConvertImpl implements StorageSourceConvert { } storageSource.setStoreContent(String.valueOf(result)); //存储路径 - LambdaQueryWrapper queryWrapperData = new LambdaQueryWrapper<>(); - queryWrapperData.eq(StorageSourceConfig::getStorageId, storageSource.getId()); - queryWrapperData.eq(StorageSourceConfig::getName, "filePath"); - String value = storageSourceConfigMapper.selectOne(queryWrapperData).getValue(); + // 使用方式 + String value = getConfigValue(storageSource.getId(), "filePath"); storageSource.setValueData(value); + String accessKeyValue = getConfigValue(storageSource.getId(), "accessKey"); + String secretKeyValue = getConfigValue(storageSource.getId(), "secretKey"); + String endPointValue = getConfigValue(storageSource.getId(), "endPoint"); + Map minioConfig = new HashMap<>(); + minioConfig.put("endPoint", endPointValue); + minioConfig.put("accessKey", accessKeyValue); + minioConfig.put("secretKey", secretKeyValue); + minioConfig.put("region", "us-east-1"); + try { + Map diskSpace = getMinioDiskSpace(minioConfig); - if(StringUtils.isNotBlank(value)){ - String[] values = value.split(";"); - // 将结果存入集合 - List valueList = Arrays.asList(values); + // 在main方法中添加计算逻辑 + double freePercentage = (double)diskSpace.get("freeSpace") / diskSpace.get("totalSpace") * 100; + double freeSpaceGB = (double)diskSpace.get("freeSpace") / (1024 * 1024 * 1024); + storageSource.setSpaceOccupancyRatio(String.format("%.2f", freePercentage) + "%"); + storageSource.setRemainingSpaceSize(String.format("%.1f", freeSpaceGB) + " GB"); - StringBuilder spaceOccupancyRatio = new StringBuilder(); - StringBuilder remainingSpaceSize = new StringBuilder(); - - for (String valueData : valueList){ - String percentage = calculateLocalStorageUsage(valueData); - String spaceSize = calculateFreeSpaceInGB(valueData); - if (spaceOccupancyRatio.length() > 0) { - spaceOccupancyRatio.append(";"); // 添加分号分隔 - } - spaceOccupancyRatio.append(percentage); - remainingSpaceSize.append(spaceSize); - } - //空间使用率 - storageSource.setSpaceOccupancyRatio(spaceOccupancyRatio.toString()); - storageSource.setRemainingSpaceSize(remainingSpaceSize.toString()); + } catch (Exception e) { + e.printStackTrace(); } +// if(StringUtils.isNotBlank(value)){ +// String[] values = value.split(";"); +// // 将结果存入集合 +// List valueList = Arrays.asList(values); +// +// StringBuilder spaceOccupancyRatio = new StringBuilder(); +// StringBuilder remainingSpaceSize = new StringBuilder(); +// +// for (String valueData : valueList){ +// String percentage = calculateLocalStorageUsage(valueData); +// String spaceSize = calculateFreeSpaceInGB(valueData); +// if (spaceOccupancyRatio.length() > 0) { +// spaceOccupancyRatio.append(";"); // 添加分号分隔 +// } +// spaceOccupancyRatio.append(percentage); +// remainingSpaceSize.append(spaceSize); +// } +// //空间使用率 +// storageSource.setSpaceOccupancyRatio(spaceOccupancyRatio.toString()); +// storageSource.setRemainingSpaceSize(remainingSpaceSize.toString()); +// } + + } } @@ -216,6 +250,263 @@ public class StorageSourceConvertImpl implements StorageSourceConvert { return resultPage; } + private String getConfigValue(int storageId, String configName) { + return Optional.ofNullable(storageSourceConfigMapper.selectOne( + new LambdaQueryWrapper() + .eq(StorageSourceConfig::getStorageId, storageId) + .eq(StorageSourceConfig::getName, configName) + )).map(StorageSourceConfig::getValue).orElse(null); + } + + + /** + * 获取Minio存储服务的磁盘空间信息 + * + * @param minioConfig Minio配置信息,包含以下键值: + * - endPoint: Minio服务地址 (e.g., "http://192.168.1.208:9000") + * - accessKey: 访问密钥 + * - secretKey: 秘密密钥 + * - region: 区域(可选,默认为"us-east-1") + * + * @return 包含磁盘空间信息的Map: + * - totalSpace: 总空间(字节) + * - freeSpace: 可用空间(字节) + * - usedSpace: 已用空间(字节) + * + * @throws Exception 如果请求失败或解析出错 + */ + public static Map getMinioDiskSpace(Map minioConfig) throws Exception { + // 从配置中获取Minio连接信息 + String endPoint = minioConfig.get("endPoint"); + String accessKey = minioConfig.get("accessKey"); + String secretKey = minioConfig.get("secretKey"); + + // 1. 构建Admin API URL + // 解析URI获取主机名、端口和协议 + URI uri = new URI(endPoint); + String host = uri.getHost(); + int port = uri.getPort(); + String protocol = uri.getScheme(); + + // 组合完整的Admin API URL + String adminUrl = protocol + "://" + host + ":" + port + "/minio/admin/v3/info"; + + // 2. 生成签名所需的时间参数(使用UTC时区) + // 日期格式要求: + // amzDate: yyyyMMdd'T'HHmmss'Z' (e.g., "20230718T093000Z") + // dateStamp: yyyyMMdd (e.g., "20230718") + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + String amzDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")); + String dateStamp = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + + // 3. 准备请求头 + // 使用TreeMap确保头字段按小写字母排序(AWS签名要求) + Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + headers.put("Host", host + ":" + port); // 主机头(必需) + headers.put("X-Amz-Date", amzDate); // AWS格式的时间戳 + headers.put("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD"); // 表示没有请求体 + + // 4. 生成AWS V4签名 + // 关键点:服务名使用"s3"(Minio Admin API使用S3签名方案) + String region = minioConfig.getOrDefault("region", "us-east-1"); // 获取区域或使用默认值 + String authorization = getV4Signature( + accessKey, + secretKey, + "s3", // 服务名称(固定为"s3") + region, // 区域(如"us-east-1") + "GET", // HTTP方法 + "/minio/admin/v3/info", // API路径 + headers, // 请求头集合 + dateStamp, // 日期戳 + amzDate // 完整时间戳 + ); + headers.put("Authorization", authorization); // 添加签名到头信息 + + // 5. 配置HTTP客户端超时参数 + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(5000) // 连接超时5秒 + .setSocketTimeout(15000) // 响应超时15秒 + .build(); + + // 使用try-with-resources确保HttpClient正确关闭 + try (CloseableHttpClient client = HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .build()) { + + // 创建GET请求 + HttpGet request = new HttpGet(adminUrl); + + // 添加所有请求头 + headers.forEach(request::addHeader); + + // 执行请求并获取响应 + HttpResponse response = client.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + + // 读取响应内容 + HttpEntity entity = response.getEntity(); + String result = EntityUtils.toString(entity); + + // 6. 检查HTTP状态码 + if (statusCode != 200) { + // 错误处理:打印详细信息 + System.err.println("Minio API请求失败. 状态码: " + statusCode); + System.err.println("响应内容: " + result); + System.err.println("请求URL: " + adminUrl); + System.err.println("签名参数: dateStamp=" + dateStamp + ", amzDate=" + amzDate); + System.err.println("签名服务: s3"); + + // 抛出异常 + throw new RuntimeException("Minio API请求失败. 状态码: " + statusCode + ", 响应: " + result); + } + + // 7. 解析磁盘空间数据 + return parseDiskSpace(result); + } + } + + /** + * 生成AWS V4签名 + * + * 参考AWS官方文档:https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4_signing.html + */ + private static String getV4Signature(String accessKey, String secretKey, String service, + String region, String method, String canonicalUri, + Map headers, String dateStamp, String amzDate) + throws NoSuchAlgorithmException, InvalidKeyException { + + // 1. 创建规范请求 (Canonical Request) + String canonicalQueryString = ""; // 没有查询参数 + + // 获取签名头列表(按小写字母排序) + String signedHeaders = getSignedHeaders(headers); + + // 构建规范头(按小写字段名排序) + StringBuilder canonicalHeaders = new StringBuilder(); + for (Map.Entry entry : headers.entrySet()) { + String key = entry.getKey().toLowerCase(); // 转换为小写 + String value = entry.getValue().trim().replaceAll("\\s+", " "); // 标准化空格 + canonicalHeaders.append(key).append(":").append(value).append("\n"); + } + + // 组装规范请求 + String canonicalRequest = method + "\n" + // HTTP方法 + canonicalUri + "\n" + // 规范路径 + canonicalQueryString + "\n" + // 查询字符串 + canonicalHeaders + "\n" + // 规范头 + signedHeaders + "\n" + // 签名头列表 + "UNSIGNED-PAYLOAD"; // 请求体哈希(无内容) + + // 2. 创建待签名字符串 (String to Sign) + String credentialScope = dateStamp + "/" + region + "/" + service + "/aws4_request"; + String stringToSign = "AWS4-HMAC-SHA256\n" + // 签名算法 + amzDate + "\n" + // 请求时间戳 + credentialScope + "\n" + // 凭证范围 + sha256Hex(canonicalRequest); // 规范请求的SHA256哈希 + + // 3. 计算签名 + // 生成签名密钥 + byte[] kSecret = ("AWS4" + secretKey).getBytes(StandardCharsets.UTF_8); + byte[] kDate = hmacSha256(dateStamp, kSecret); // 日期密钥 + byte[] kRegion = hmacSha256(region, kDate); // 区域密钥 + byte[] kService = hmacSha256(service, kRegion); // 服务密钥 + byte[] kSigning = hmacSha256("aws4_request", kService); // 最终签名密钥 + + // 计算签名 + byte[] signature = hmacSha256(stringToSign, kSigning); + + // 4. 构建Authorization头 + return "AWS4-HMAC-SHA256 " + + "Credential=" + accessKey + "/" + credentialScope + ", " + + "SignedHeaders=" + signedHeaders + ", " + + "Signature=" + bytesToHex(signature); + } + + /** + * 获取签名头列表(按小写字母排序) + */ + private static String getSignedHeaders(Map headers) { + StringBuilder signedHeaders = new StringBuilder(); + for (String key : headers.keySet()) { + if (signedHeaders.length() > 0) { + signedHeaders.append(";"); + } + signedHeaders.append(key.toLowerCase()); // 全部转换为小写 + } + return signedHeaders.toString(); + } + + /** + * 解析磁盘空间信息 + */ + private static Map parseDiskSpace(String jsonResponse) { + Map diskSpace = new HashMap<>(); + + // 解析JSON响应 + JSONObject json = new JSONObject(jsonResponse); + + // 提取磁盘信息(假设单节点部署) + JSONObject drive = json.getJSONArray("servers") // 获取servers数组 + .getJSONObject(0) // 获取第一个服务器 + .getJSONArray("drives") // 获取drives数组 + .getJSONObject(0); // 获取第一个驱动器 + + // 获取空间信息 + long total = drive.getLong("totalspace"); // 总空间 + long used = drive.getLong("usedspace"); // 已用空间 + long free = drive.getLong("availspace"); // 可用空间 + + // 存入结果Map + diskSpace.put("totalSpace", total); + diskSpace.put("freeSpace", free); + diskSpace.put("usedSpace", used); + + return diskSpace; + } + +// ===== 辅助方法 ===== + + /** + * 计算HMAC-SHA256签名 + */ + private static byte[] hmacSha256(String data, byte[] key) + throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 计算字符串的SHA256哈希 + */ + private static String sha256Hex(String data) throws NoSuchAlgorithmException { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hash); + } + + /** + * 字节数组转十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); // 转换为无符号十六进制 + if (hex.length() == 1) { + hexString.append('0'); // 补零 + } + hexString.append(hex); + } + return hexString.toString(); + } + + + + + + + + // 计算本地路径的空间剩余率 public static String calculateLocalStorageUsage(String path) { try { diff --git a/java/src/main/java/com/yfd/platform/modules/storage/model/param/MinIOParam.java b/java/src/main/java/com/yfd/platform/modules/storage/model/param/MinIOParam.java index cd2ab70..259ce58 100644 --- a/java/src/main/java/com/yfd/platform/modules/storage/model/param/MinIOParam.java +++ b/java/src/main/java/com/yfd/platform/modules/storage/model/param/MinIOParam.java @@ -20,7 +20,7 @@ public class MinIOParam extends S3BaseParam { @StorageParamItem(name = "服务地址", order = 5, description = "为 minio 的服务地址,非 web 访问地址,一般为 http://ip:9000") private String endPoint; - @StorageParamItem(name = "minio宿主机挂载路径", order = 0,description = "只支持绝对路径
Docker 方式部署的话需提前映射宿主机路径! " ) - private String filePath; +// @StorageParamItem(name = "minio宿主机挂载路径", order = 0,description = "只支持绝对路径
Docker 方式部署的话需提前映射宿主机路径! " ) +// private String filePath; }