|
@@ -0,0 +1,432 @@
|
|
|
+package com.txz.operating.config.minio;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.google.common.collect.HashMultimap;
|
|
|
+import io.minio.*;
|
|
|
+import io.minio.errors.*;
|
|
|
+import io.minio.http.Method;
|
|
|
+import io.minio.messages.Bucket;
|
|
|
+import io.minio.messages.Item;
|
|
|
+import io.minio.messages.Part;
|
|
|
+
|
|
|
+import org.apache.http.client.methods.CloseableHttpResponse;
|
|
|
+import org.apache.http.client.methods.HttpPut;
|
|
|
+import org.apache.http.entity.ByteArrayEntity;
|
|
|
+import org.apache.http.impl.client.CloseableHttpClient;
|
|
|
+import org.apache.http.impl.client.HttpClients;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.io.*;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.security.InvalidKeyException;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+@Component
|
|
|
+public class MinioUtil {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private CustomMinioClient minioClient;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建一个桶
|
|
|
+ */
|
|
|
+ public void createBucket(String bucket) throws Exception {
|
|
|
+ boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
|
|
|
+ if (!found) {
|
|
|
+ minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传一个文件
|
|
|
+ */
|
|
|
+ public void uploadFile(InputStream stream, String bucket, String objectName) throws Exception {
|
|
|
+ minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
|
|
|
+ .stream(stream, -1, 10485760).build());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 列出所有的桶
|
|
|
+ */
|
|
|
+ public List<String> listBuckets() throws Exception {
|
|
|
+ List<Bucket> list = minioClient.listBuckets();
|
|
|
+ List<String> names = new ArrayList<>();
|
|
|
+ list.forEach(b -> {
|
|
|
+ names.add(b.name());
|
|
|
+ });
|
|
|
+ return names;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 列出一个桶中的所有文件和目录
|
|
|
+ */
|
|
|
+ public List<FileInfo> listFiles(String bucket) throws Exception {
|
|
|
+ Iterable<Result<Item>> results = minioClient.listObjects(
|
|
|
+ ListObjectsArgs.builder().bucket(bucket).recursive(true).build());
|
|
|
+
|
|
|
+ List<FileInfo> infos = new ArrayList<>();
|
|
|
+ results.forEach(r -> {
|
|
|
+ FileInfo info = new FileInfo();
|
|
|
+ try {
|
|
|
+ Item item = r.get();
|
|
|
+ info.setFilename(item.objectName());
|
|
|
+ info.setDirectory(item.isDir());
|
|
|
+ infos.add(info);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return infos;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载一个文件
|
|
|
+ */
|
|
|
+ public InputStream download(String bucket, String objectName) throws Exception {
|
|
|
+ InputStream stream = minioClient.getObject(
|
|
|
+ GetObjectArgs.builder().bucket(bucket).object(objectName).build());
|
|
|
+ return stream;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除一个桶
|
|
|
+ */
|
|
|
+ public void deleteBucket(String bucket) throws Exception {
|
|
|
+ minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除一个对象
|
|
|
+ */
|
|
|
+ public void deleteObject(String bucket, String objectName) throws Exception {
|
|
|
+ minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 复制文件
|
|
|
+ *
|
|
|
+ * @Param: [sourceBucket, sourceObject, targetBucket, targetObject]
|
|
|
+ * @return: void
|
|
|
+ * @Author: MrFugui
|
|
|
+ * @Date: 2021/11/15
|
|
|
+ */
|
|
|
+ public void copyObject(String sourceBucket, String sourceObject, String targetBucket, String targetObject) throws Exception {
|
|
|
+ this.createBucket(targetBucket);
|
|
|
+ minioClient.copyObject(CopyObjectArgs.builder().bucket(targetBucket).object(targetObject)
|
|
|
+ .source(CopySource.builder().bucket(sourceBucket).object(sourceObject).build()).build());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件信息
|
|
|
+ *
|
|
|
+ * @Param: [bucket, objectName]
|
|
|
+ * @return: java.lang.String
|
|
|
+ * @Author: MrFugui
|
|
|
+ * @Date: 2021/11/15
|
|
|
+ */
|
|
|
+ public String getObjectInfo(String bucket, String objectName) throws Exception {
|
|
|
+
|
|
|
+ return minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(objectName).build()).toString();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成一个给HTTP GET请求用的presigned URL。浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。
|
|
|
+ *
|
|
|
+ * @Param: [bucketName, objectName, expires]
|
|
|
+ * @return: java.lang.String
|
|
|
+ * @Author: MrFugui
|
|
|
+ * @Date: 2021/11/15
|
|
|
+ */
|
|
|
+ public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
|
|
|
+ GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs
|
|
|
+ .builder().bucket(bucketName).object(objectName).expiry(expires).method(Method.GET).build();
|
|
|
+ return minioClient.getPresignedObjectUrl(build);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取minio中所有的文件
|
|
|
+ *
|
|
|
+ * @Param: []
|
|
|
+ * @return: java.util.List<boot.spring.domain.FileInfo>
|
|
|
+ * @Author: MrFugui
|
|
|
+ * @Date: 2021/11/15
|
|
|
+ */
|
|
|
+ public List<FileInfo> listAllFile() throws Exception {
|
|
|
+ List<String> list = this.listBuckets();
|
|
|
+ List<FileInfo> FileInfos = new ArrayList<>();
|
|
|
+ for (String bucketName : list) {
|
|
|
+ FileInfos.addAll(this.listFiles(bucketName));
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ return FileInfos;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public void splitFile() throws Exception {
|
|
|
+ long CHUNK_SIZE = 30 * 1024 * 1024;
|
|
|
+ // 将文件分片存储
|
|
|
+ String filePath = "D:\\xiazai\\18a7bc018f35471f927ae205301c9442.mp4";
|
|
|
+ File file = new File(filePath);
|
|
|
+ long fileSize = file.length();
|
|
|
+ int chunkCount = (int) Math.ceil((double) fileSize / CHUNK_SIZE);
|
|
|
+
|
|
|
+ try (FileInputStream fis = new FileInputStream(file)) {
|
|
|
+ byte[] buffer = new byte[(int) CHUNK_SIZE];
|
|
|
+ for (int i = 0; i < chunkCount; i++) {
|
|
|
+ String chunkFileName = filePath + ".part" + (i + 1);
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
|
|
|
+ int bytesRead = fis.read(buffer);
|
|
|
+ fos.write(buffer, 0, bytesRead);
|
|
|
+ System.out.println(chunkFileName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<InputStream> splitFileToInputStreams(MultipartFile uploadfile) throws IOException {
|
|
|
+ long CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片的大小为5MB
|
|
|
+ List<InputStream> inputStreams = new ArrayList<>(); // 用于存储分片的 InputStream
|
|
|
+
|
|
|
+ try (BufferedInputStream bis = new BufferedInputStream(uploadfile.getInputStream())) {
|
|
|
+ byte[] buffer = new byte[(int) CHUNK_SIZE];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = bis.read(buffer)) != -1) {
|
|
|
+ byte[] chunkData = new byte[bytesRead];
|
|
|
+ System.arraycopy(buffer, 0, chunkData, 0, bytesRead);
|
|
|
+ InputStream chunkInputStream = new ByteArrayInputStream(chunkData);
|
|
|
+ inputStreams.add(chunkInputStream);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return inputStreams; // 返回分片的 InputStream 列表
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 第二步,申请一个大文件上传
|
|
|
+ * 该函数用于申请一个大文件的分块上传,生成每个分块的预签名URL,并返回包含上传ID和分块URL列表的DTO对象。
|
|
|
+ *
|
|
|
+ * @param fileName 文件名,用于生成上传ID和预签名URL
|
|
|
+ * @param chunkCount 文件分块的总数,用于生成对应数量的预签名URL
|
|
|
+ * @param bucketName MinIO存储桶的名称,用于指定文件上传的目标存储桶
|
|
|
+ * @return SplitFileDto 包含上传ID和分块预签名URL列表的DTO对象
|
|
|
+ * @throws ServerException 服务器异常
|
|
|
+ * @throws InsufficientDataException 数据不足异常
|
|
|
+ * @throws ErrorResponseException 错误响应异常
|
|
|
+ * @throws IOException IO异常
|
|
|
+ * @throws NoSuchAlgorithmException 无此算法异常
|
|
|
+ * @throws InvalidKeyException 无效密钥异常
|
|
|
+ * @throws XmlParserException XML解析异常
|
|
|
+ * @throws InvalidResponseException 无效响应异常
|
|
|
+ * @throws InternalException 内部异常
|
|
|
+ */
|
|
|
+ public SplitFileDto applyUploadPsiResult2Minio(String fileName, Integer chunkCount, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
|
|
|
+ // 获取文件上传的唯一ID
|
|
|
+ String uploadId = getUploadId(fileName, bucketName);
|
|
|
+ SplitFileDto splitFileDto = new SplitFileDto();
|
|
|
+ Map<String, String> reqParams = new HashMap<>();
|
|
|
+ splitFileDto.setUploadId(uploadId);
|
|
|
+ reqParams.put("uploadId", uploadId);
|
|
|
+
|
|
|
+ // 生成每个分块的预签名URL
|
|
|
+ List<String> uploadUrlList = new ArrayList<>();
|
|
|
+ for (int i = 1; i <= chunkCount; i++) {
|
|
|
+ reqParams.put("partNumber", String.valueOf(i));
|
|
|
+ String uploadUrl = getPresignedObjectUrl(fileName, reqParams, bucketName);
|
|
|
+ uploadUrlList.add(uploadUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置分块预签名URL列表并返回DTO对象
|
|
|
+ splitFileDto.setChunkUploadUrls(uploadUrlList);
|
|
|
+ return splitFileDto;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 准备分片上传时,在此先获取上传任务id
|
|
|
+ */
|
|
|
+ private String getUploadId(String objectName, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
|
|
|
+ String contentType = "application/octet-stream";
|
|
|
+ HashMultimap<String, String> headers = HashMultimap.create();
|
|
|
+ headers.put("Content-Type", contentType);
|
|
|
+ return minioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取分片上传的预签名URL。
|
|
|
+ * <p>
|
|
|
+ * 该方法通过MinIO客户端生成一个用于分片上传的预签名URL。预签名URL允许客户端在指定的时间内直接上传文件到指定的存储桶,而无需通过服务器进行身份验证。
|
|
|
+ *
|
|
|
+ * @param fileName 要上传的文件名,该文件名将作为对象存储在MinIO中。
|
|
|
+ * @param reqParams 额外的查询参数,这些参数将包含在生成的预签名URL中。
|
|
|
+ * @param bucketName 目标存储桶的名称,文件将被上传到该存储桶中。
|
|
|
+ * @return 返回一个预签名的URL,客户端可以使用该URL直接上传文件。
|
|
|
+ * @throws ServerException 如果与MinIO服务器的通信失败。
|
|
|
+ * @throws InsufficientDataException 如果从服务器接收的数据不完整。
|
|
|
+ * @throws ErrorResponseException 如果服务器返回错误响应。
|
|
|
+ * @throws IOException 如果发生I/O错误。
|
|
|
+ * @throws NoSuchAlgorithmException 如果请求的加密算法不可用。
|
|
|
+ * @throws InvalidKeyException 如果提供的密钥无效。
|
|
|
+ * @throws InvalidResponseException 如果服务器返回的响应无效。
|
|
|
+ * @throws XmlParserException 如果解析XML响应时发生错误。
|
|
|
+ * @throws InternalException 如果发生内部错误。
|
|
|
+ */
|
|
|
+ private String getPresignedObjectUrl(String fileName, Map<String, String> reqParams, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
|
|
|
+ // 使用MinIO客户端生成预签名URL,设置HTTP方法为PUT,并指定文件、存储桶、过期时间及额外查询参数
|
|
|
+ return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
|
|
+ .method(Method.PUT)
|
|
|
+ .bucket(bucketName)
|
|
|
+ .object(fileName)
|
|
|
+ .expiry(1, TimeUnit.DAYS)
|
|
|
+ .extraQueryParams(reqParams)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将分片文件逐个上传到指定的URL。每个分片文件通过HTTP PUT请求直接上传到MinIO存储,避免通过服务端转发,减少网络IO开销。
|
|
|
+ * 在文件合并之前,分片文件可以重复覆盖上传。
|
|
|
+ *
|
|
|
+ * @param uploadUrlList 包含每个分片文件上传URL的列表,URL已签名。
|
|
|
+ * @param chunkFiles 包含每个分片文件输入流的列表,每个输入流对应一个分片文件。
|
|
|
+ * @throws IOException 如果在上传过程中发生IO错误,则抛出此异常。
|
|
|
+ */
|
|
|
+ public void upload(List<String> uploadUrlList, List<InputStream> chunkFiles) throws IOException {
|
|
|
+
|
|
|
+ // 创建默认的HTTP客户端,用于执行上传请求
|
|
|
+ CloseableHttpClient httpClient = HttpClients.createDefault();
|
|
|
+
|
|
|
+ // 遍历每个分片文件的上传URL和对应的输入流
|
|
|
+ for (int i = 0; i < uploadUrlList.size(); i++) {
|
|
|
+ // 创建HTTP PUT请求,将分片文件上传到指定的URL
|
|
|
+ String chunkUploadUrl = uploadUrlList.get(i);
|
|
|
+ HttpPut httpPut = new HttpPut(chunkUploadUrl);
|
|
|
+ httpPut.setHeader("Content-Type", "video/mp4");
|
|
|
+
|
|
|
+ // 生成随机的文件名,并设置Content-Disposition头
|
|
|
+ UUID uuid = UUID.randomUUID();
|
|
|
+ //todo 可以根据自己的文件加后缀
|
|
|
+ String name = uuid + ".mp4";
|
|
|
+ httpPut.addHeader("Content-Disposition", "filename=" + urlEncode(name, "UTF-8"));
|
|
|
+
|
|
|
+ // 将输入流转换为字节数组,并设置为请求体
|
|
|
+ byte[] chunkData = toByteArray(chunkFiles.get(i));
|
|
|
+ ByteArrayEntity byteArrayEntity = new ByteArrayEntity(chunkData);
|
|
|
+ httpPut.setEntity(byteArrayEntity);
|
|
|
+
|
|
|
+ // 执行上传请求,并获取响应
|
|
|
+ CloseableHttpResponse chunkUploadResp = httpClient.execute(httpPut);
|
|
|
+
|
|
|
+ // 打印上传响应信息
|
|
|
+ System.out.println("[分片" + (i + 1) + "]上传响应:" + JSON.toJSONString(chunkUploadResp));
|
|
|
+
|
|
|
+ // 释放连接资源
|
|
|
+ httpPut.releaseConnection();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将输入流(InputStream)转换为字节数组(byte[])。
|
|
|
+ * <p>
|
|
|
+ * 该函数通过读取输入流中的数据,并将其写入到字节数组输出流(ByteArrayOutputStream)中,
|
|
|
+ * 最终将输出流的内容转换为字节数组返回。
|
|
|
+ *
|
|
|
+ * @param inputStream 要转换的输入流,不能为null。
|
|
|
+ * @return 包含输入流数据的字节数组。
|
|
|
+ * @throws IOException 如果读取输入流时发生I/O错误。
|
|
|
+ */
|
|
|
+ public static byte[] toByteArray(InputStream inputStream) throws IOException {
|
|
|
+ // 创建一个字节数组输出流,用于缓存从输入流读取的数据
|
|
|
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
|
+ int nRead;
|
|
|
+ byte[] data = new byte[1024];
|
|
|
+
|
|
|
+ // 从输入流中读取数据,直到读取到流的末尾(返回-1)
|
|
|
+ while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
|
|
|
+ // 将读取到的数据写入到字节数组输出流中
|
|
|
+ buffer.write(data, 0, nRead);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保所有数据都已写入到输出流中
|
|
|
+ buffer.flush();
|
|
|
+
|
|
|
+ // 将字节数组输出流的内容转换为字节数组并返回
|
|
|
+ return buffer.toByteArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对给定的字符串进行URL编码,并使用指定的字符编码。
|
|
|
+ * 编码过程中,会将字符串中的特殊字符替换为对应的百分号编码形式。
|
|
|
+ * 如果输入的字符串为null,则返回空字符串。
|
|
|
+ *
|
|
|
+ * @param value 需要编码的字符串,可以为null
|
|
|
+ * @param encoding 使用的字符编码,例如"UTF-8"
|
|
|
+ * @return 编码后的字符串,如果输入为null则返回空字符串
|
|
|
+ * @throws IllegalArgumentException 如果指定的字符编码不被支持
|
|
|
+ */
|
|
|
+ public static String urlEncode(String value, String encoding) {
|
|
|
+ if (value == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 使用URLEncoder对字符串进行编码
|
|
|
+ String encoded = URLEncoder.encode(value, encoding);
|
|
|
+
|
|
|
+ // 替换编码后的字符串中的特定字符为对应的百分号编码形式
|
|
|
+ return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F");
|
|
|
+ } catch (UnsupportedEncodingException e) {
|
|
|
+ // 如果指定的字符编码不被支持,抛出IllegalArgumentException异常
|
|
|
+ throw new IllegalArgumentException();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分片上传完后合并
|
|
|
+ * <p>
|
|
|
+ * 该函数用于在分片上传完成后,将所有分片合并为一个完整的对象。
|
|
|
+ *
|
|
|
+ * @param objectName 对象名称,即上传的文件名
|
|
|
+ * @param uploadId 上传任务的唯一标识符
|
|
|
+ * @param bucketName 存储桶名称,即文件存储的容器
|
|
|
+ * @return ObjectWriteResponse 对象写入响应,包含合并后的对象信息
|
|
|
+ * @throws ServerException 服务器异常
|
|
|
+ * @throws InsufficientDataException 数据不足异常
|
|
|
+ * @throws ErrorResponseException 错误响应异常
|
|
|
+ * @throws IOException 输入输出异常
|
|
|
+ * @throws NoSuchAlgorithmException 无此算法异常
|
|
|
+ * @throws InvalidKeyException 无效密钥异常
|
|
|
+ * @throws XmlParserException XML解析异常
|
|
|
+ * @throws InvalidResponseException 无效响应异常
|
|
|
+ * @throws InternalException 内部异常
|
|
|
+ */
|
|
|
+ public ObjectWriteResponse mergeMultipartUpload(String objectName, String uploadId, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
|
|
|
+ System.out.println("ready to merge <" + objectName + " - " + uploadId + " - " + bucketName + ">");
|
|
|
+
|
|
|
+ // 查询上传后的分片数据
|
|
|
+ ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
|
|
|
+ int chunkCount = partResult.result().partList().size();
|
|
|
+ Part[] parts = new Part[chunkCount];
|
|
|
+ int partNumber = 1;
|
|
|
+
|
|
|
+ // 将分片数据转换为合并所需的格式
|
|
|
+ for (Part part : partResult.result().partList()) {
|
|
|
+ parts[partNumber - 1] = new Part(partNumber, part.etag());
|
|
|
+ partNumber++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并分片并返回合并后的对象信息
|
|
|
+ ObjectWriteResponse objectWriteResponse = minioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
|
|
|
+ return objectWriteResponse;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|