MinIO分布式存储(从0到Vue+SpringBoot整合开发)2024版

MinIO分布式存储与Vue+SpringBoot整合开发实战指南

一、MinIO简介与环境搭建

1.1 MinIO概述

MinIO是一个高性能、分布式对象存储系统,专为云原生和AI工作负载设计。它兼容Amazon S3 API,是构建私有云存储的理想选择。

2.2 Docker部署MinIO

bash 复制代码
# 拉取MinIO镜像
docker pull minio/minio

# 运行MinIO容器
docker run -p 9000:9000 -p 9001:9001 \
  --name minio \
  -v /mnt/data:/data \
  -e "MINIO_ROOT_USER=admin" \
  -e "MINIO_ROOT_PASSWORD=password" \
  minio/minio server /data --console-address ":9001"

访问 http://localhost:9001 即可进入MinIO管理控制台。

二、SpringBoot后端集成

2.1 添加依赖配置

xml 复制代码
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.2</version>
</dependency>

2.2 MinIO配置类

java 复制代码
@Configuration
public class MinioConfig {
    
    @Value("${minio.endpoint}")
    private String endpoint;
    
    @Value("${minio.accessKey}")
    private String accessKey;
    
    @Value("${minio.secretKey}")
    private String secretKey;
    
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

2.3 文件服务实现

java 复制代码
@Service
public class FileStorageService {
    
    @Autowired
    private MinioClient minioClient;
    
    @Value("${minio.bucketName}")
    private String bucketName;
    
    /**
     * 上传文件
     */
    public String uploadFile(MultipartFile file, String fileName) throws Exception {
        // 检查存储桶是否存在
        boolean found = minioClient.bucketExists(BucketExistsArgs.builder()
                .bucket(bucketName).build());
        if (!found) {
            minioClient.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName).build());
        }
        
        // 上传文件
        minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
        
        return endpoint + "/" + bucketName + "/" + fileName;
    }
    
    /**
     * 下载文件
     */
    public InputStream downloadFile(String fileName) throws Exception {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }
    
    /**
     * 删除文件
     */
    public void deleteFile(String fileName) throws Exception {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }
}

2.4 控制器层

java 复制代码
@RestController
@RequestMapping("/api/file")
public class FileController {
    
    @Autowired
    private FileStorageService fileStorageService;
    
    @PostMapping("/upload")
    public ResponseEntity<Map<String, String>> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "fileName", required = false) String fileName) {
        try {
            if (fileName == null || fileName.isEmpty()) {
                fileName = file.getOriginalFilename();
            }
            
            String fileUrl = fileStorageService.uploadFile(file, fileName);
            
            Map<String, String> response = new HashMap<>();
            response.put("url", fileUrl);
            response.put("fileName", fileName);
            response.put("message", "文件上传成功");
            
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("error", "文件上传失败: " + e.getMessage()));
        }
    }
    
    @GetMapping("/download/{fileName}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
        try {
            InputStream fileStream = fileStorageService.downloadFile(fileName);
            InputStreamResource resource = new InputStreamResource(fileStream);
            
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "attachment; filename=\"" + fileName + "\"")
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(resource);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
}

三、Vue前端实现

3.1 文件上传组件

vue 复制代码
<template>
  <div class="file-upload">
    <div class="upload-area" 
         @drop="handleDrop"
         @dragover="handleDragOver"
         @click="triggerFileInput">
      <input type="file" 
             ref="fileInput" 
             @change="handleFileSelect" 
             style="display: none" 
             multiple>
      <div class="upload-content">
        <i class="el-icon-upload"></i>
        <div class="upload-text">将文件拖到此处,或<em>点击上传</em></div>
      </div>
    </div>
    
    <div v-if="uploading" class="upload-progress">
      <el-progress :percentage="uploadProgress"></el-progress>
    </div>
    
    <div v-if="uploadedFiles.length > 0" class="file-list">
      <h3>已上传文件</h3>
      <div v-for="file in uploadedFiles" :key="file.url" class="file-item">
        <div class="file-info">
          <span class="file-name">{{ file.fileName }}</span>
          <span class="file-url">{{ file.url }}</span>
        </div>
        <div class="file-actions">
          <el-button size="small" @click="downloadFile(file)">下载</el-button>
          <el-button size="small" type="danger" @click="deleteFile(file)">删除</el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'FileUpload',
  data() {
    return {
      uploading: false,
      uploadProgress: 0,
      uploadedFiles: []
    };
  },
  methods: {
    triggerFileInput() {
      this.$refs.fileInput.click();
    },
    
    handleFileSelect(event) {
      const files = event.target.files;
      this.uploadFiles(files);
    },
    
    handleDragOver(event) {
      event.preventDefault();
      event.stopPropagation();
    },
    
    handleDrop(event) {
      event.preventDefault();
      event.stopPropagation();
      const files = event.dataTransfer.files;
      this.uploadFiles(files);
    },
    
    async uploadFiles(files) {
      this.uploading = true;
      this.uploadProgress = 0;
      
      const formData = new FormData();
      for (let i = 0; i < files.length; i++) {
        formData.append('files', files[i]);
      }
      
      try {
        const response = await axios.post('/api/file/upload', formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          },
          onUploadProgress: (progressEvent) => {
            this.uploadProgress = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
          }
        });
        
        this.uploadedFiles.unshift(...response.data);
        this.$message.success('文件上传成功');
      } catch (error) {
        console.error('上传失败:', error);
        this.$message.error('文件上传失败');
      } finally {
        this.uploading = false;
        this.uploadProgress = 0;
        this.$refs.fileInput.value = '';
      }
    },
    
    async downloadFile(file) {
      try {
        const response = await axios.get(`/api/file/download/${file.fileName}`, {
          responseType: 'blob'
        });
        
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', file.fileName);
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.URL.revokeObjectURL(url);
      } catch (error) {
        console.error('下载失败:', error);
        this.$message.error('文件下载失败');
      }
    },
    
    async deleteFile(file) {
      try {
        await axios.delete(`/api/file/delete/${file.fileName}`);
        this.uploadedFiles = this.uploadedFiles.filter(f => f.url !== file.url);
        this.$message.success('文件删除成功');
      } catch (error) {
        console.error('删除失败:', error);
        this.$message.error('文件删除失败');
      }
    }
  }
};
</script>

<style scoped>
.upload-area {
  border: 2px dashed #dcdfe6;
  border-radius: 6px;
  padding: 40px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.3s;
}

.upload-area:hover {
  border-color: #409eff;
}

.upload-content {
  color: #606266;
}

.upload-text em {
  color: #409eff;
  font-style: normal;
}

.file-list {
  margin-top: 20px;
}

.file-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  margin-bottom: 10px;
}

.file-info {
  flex: 1;
}

.file-name {
  font-weight: bold;
  display: block;
}

.file-url {
  color: #909399;
  font-size: 12px;
  display: block;
  margin-top: 5px;
}
</style>

3.2 配置文件

javascript 复制代码
// src/api/config.js
const API_BASE_URL = process.env.VUE_APP_API_BASE_URL || 'http://localhost:8080';

export default {
  UPLOAD_URL: `${API_BASE_URL}/api/file/upload`,
  DOWNLOAD_URL: `${API_BASE_URL}/api/file/download`,
  DELETE_URL: `${API_BASE_URL}/api/file/delete`
};

四、高级功能实现

4.1 大文件分片上传

java 复制代码
// SpringBoot后端分片上传支持
@PostMapping("/chunk-upload")
public ResponseEntity<?> chunkUpload(
        @RequestParam("file") MultipartFile file,
        @RequestParam("chunkNumber") Integer chunkNumber,
        @RequestParam("totalChunks") Integer totalChunks,
        @RequestParam("identifier") String identifier) {
    try {
        // 存储分片文件
        String chunkName = identifier + "_" + chunkNumber;
        fileStorageService.uploadChunk(file, chunkName);
        
        // 检查是否所有分片都已上传完成
        if (chunkNumber.equals(totalChunks)) {
            String finalFileName = fileStorageService.mergeChunks(identifier, totalChunks);
            return ResponseEntity.ok(Map.of("url", finalFileName, "message", "文件上传完成"));
        }
        
        return ResponseEntity.ok(Map.of("message", "分片上传成功"));
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", e.getMessage()));
    }
}

4.2 文件预览功能

vue 复制代码
<template>
  <div class="file-preview">
    <div v-if="isImage(file)" class="image-preview">
      <img :src="file.url" :alt="file.fileName" @click="showPreview = true">
    </div>
    <div v-else-if="isPdf(file)" class="pdf-preview">
      <embed :src="file.url" type="application/pdf" width="100%" height="600px">
    </div>
    <div v-else class="default-preview">
      <i class="el-icon-document"></i>
      <span>{{ file.fileName }}</span>
    </div>
    
    <el-dialog :visible.sync="showPreview" :title="file.fileName">
      <img :src="file.url" style="width: 100%">
    </el-dialog>
  </div>
</template>

<script>
export default {
  props: {
    file: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      showPreview: false
    };
  },
  methods: {
    isImage(file) {
      return file.fileName.match(/\.(jpg|jpeg|png|gif|webp)$/i);
    },
    isPdf(file) {
      return file.fileName.match(/\.pdf$/i);
    }
  }
};
</script>

五、部署与配置

5.1 应用配置

yaml 复制代码
# application.yml
minio:
  endpoint: http://localhost:9000
  accessKey: admin
  secretKey: password
  bucketName: my-bucket

spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

5.2 跨域配置

java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:8081")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }
}

六、最佳实践建议

  1. 安全性:在生产环境中使用HTTPS,定期轮换访问密钥
  2. 错误处理:实现完善的异常处理和日志记录
  3. 性能优化:对大文件使用分片上传,配置合适的超时时间
  4. 监控告警:监控MinIO集群状态和存储使用情况
  5. 备份策略:制定定期备份和灾难恢复计划

这个完整的实现方案提供了从MinIO部署到前后端整合的全套代码,可以直接用于项目开发。根据实际需求,您可以进一步扩展和优化这些功能。

相关推荐
Mr.456719 分钟前
Spring Boot 集成 PostgreSQL 表级备份与恢复实战
java·spring boot·后端·postgresql
白露与泡影23 分钟前
探索springboot程序打包docker的最佳方式
spring boot·后端·docker
sthnyph1 小时前
SpringBoot Test详解
spring boot·后端·log4j
brucelee1862 小时前
Spring Boot 测试最佳实践
spring boot·后端·log4j
DROm RAPS3 小时前
十七:Spring Boot依赖 (2)-- spring-boot-starter-web 依赖详解
前端·spring boot·后端
TlYf NTLE4 小时前
Spring Boot spring-boot-maven-plugin 参数配置详解
spring boot·后端·maven
花千树-0104 小时前
5分钟用 Java 构建你的第一个 AI 应用
java·人工智能·spring boot·langchain·aigc·ai编程
a8a3024 小时前
Spring Boot(快速上手)
java·spring boot·后端
野生技术架构师5 小时前
Spring Boot 4 与 Spring Framework 7 全面解析:新特性、升级要点与实战指南
spring boot·后端·spring
菜鸟程序员专写BUG5 小时前
SpringBoot跨域报错全集|CORS、OPTIONS预检、无Access-Control报错全解决
spring boot·后端·状态模式