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部署到前后端整合的全套代码,可以直接用于项目开发。根据实际需求,您可以进一步扩展和优化这些功能。

相关推荐
爱玩代码的小张3 小时前
DLJD MinIO分布式存储从0到Vue+SpringBoot整合开发2024年4月
spring boot
Slow菜鸟5 小时前
SpringBoot集成Elasticsearch | Spring官方场景启动器(Spring Data Elasticsearch)方式
spring boot·spring·elasticsearch
星辰h5 小时前
基于JWT的RESTful登录系统实现
前端·spring boot·后端·mysql·restful·jwt
YDS8295 小时前
苍穹外卖 —— 文件上传和菜品的CRUD
java·spring boot·后端
Java陈序员5 小时前
完全开源!一款基于 SpringBoot + Vue 构建的社区平台!
vue.js·spring boot·github·社区
.柒宇.6 小时前
《云岚到家》第一章个人总结
spring boot·spring·spring cloud
刘一说6 小时前
深入掌握 Spring Boot Web 开发:构建高性能 RESTful API 的最佳实践
前端·spring boot·restful
摇滚侠6 小时前
Spring Boot3零基础教程,Actuator 导入,笔记82
java·spring boot·笔记
JosieBook6 小时前
【SpringBoot】29 核心功能 - 数据访问 - Spring Boot 2 操作 Redis 实践指南:本地安装与阿里云 Redis 对比应用
spring boot·redis·阿里云