多服务器文件本地上传及读取

多服务器文件管理系统的实现方案

在没有对象存储服务(OSS)的情况下,本文实现了一个基于多台服务器的文件管理系统。系统通过数据库表维护文件存储位置信息,主要功能包括:

  1. 文件上传:检查文件大小限制,计算MD5值,按日期目录存储文件
  2. 文件下载:根据ID获取文件实体,返回文件资源流
  3. 文件去重:通过MD5校验避免重复存储
  4. IP管理:记录文件所在服务器IP,便于跨服务器访问

系统使用Spring Boot框架实现,数据库采用MySQL,表结构包含文件URL、MD5、创建时间、服务器IP等关键字段。通过本地存储+数据库索引的方式,解决了多服务器环境下文件定位问题。

以下为代码:

package com.hwxc.wms.server.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hwxc.wms.common.enums.ResultCode;
import com.hwxc.wms.common.exception.BusinessException;
import com.hwxc.wms.common.result.CommonResult;
import com.hwxc.wms.common.utils.FileUtil;
import com.hwxc.wms.common.vo.FileVo;
import com.hwxc.wms.server.entity.FileEntity;
import com.hwxc.wms.server.mapper.FileMapper;
import com.hwxc.wms.server.service.FileService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;

/**
* @description 针对表【file(文件表)】的数据库操作Service实现
* @createDate 2025-05-26 16:50:45
*/
@Service
public class FileServiceImpl extends ServiceImpl
    implements FileService {

    @Value("${file.ip:10.1.12.10}")
    private String ip;

    @Value("${file.maxSize:10}")
    private Integer maxSize;

    @Value("${zt.imagesTmpDir:/images}")
    private String imagesTmpDir;

    /**
     * 文件上传,及信息返回
     * @param uploadFile
     * @return
     */
    @Override
    public CommonResult upload(MultipartFile uploadFile, String type) throws IOException {
        if (uploadFile.isEmpty()) {
            throw new BusinessException(ResultCode.FAILED.getCode(), "上传失败,请选择文件");
        }
        if (uploadFile.getSize() > this.maxSize * 1024 * 1024) {
            throw new BusinessException(ResultCode.FAILED.getCode(), "上传文件大小不能超过"+this.maxSize+"MB");
        }

        String fileDir = this.imagesTmpDir + "/" + DateUtil.format(DateUtil.date(), "yyyyMMdd");
        File uploadDir = new File(fileDir);
        if(!uploadDir.exists())
        {
            uploadDir.mkdirs();
        }

        String md5 = FileUtil.getMd5(uploadFile);

        //查看数据库是否已经存在,存在直接返回
        QueryWrapper query = Wrappers.query();
        query.eq("file_md5", md5);
        query.last("limit 1");
        FileEntity one = this.getOne(query);
        FileVo fileVo = new FileVo();
        String url = "";
        HashMap dateKv = null;
        if( null == one ){
            //上传文件到本地
            String originalFilename = uploadFile.getOriginalFilename();
            String suffixName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
            String newFile = fileDir + "/" + md5 + "." +suffixName;
            File newFileObj = new File(newFile);
            uploadFile.transferTo(newFileObj);

            if( !newFileObj.exists() ){
                throw new BusinessException(ResultCode.FAILED.getCode(), "保存文件失败");
            }

            FileEntity fileEntity = new FileEntity();
            fileEntity.setFileUrl(newFile);
            fileEntity.setFileMd5(md5);
            fileEntity.setCreateTime(DateUtil.date());
            fileEntity.setUpdateTime(DateUtil.date());
            fileEntity.setIp(this.getLocalIp());
            fileEntity.setExt(suffixName);
            this.save(fileEntity);
            BeanUtils.copyProperties(fileEntity, fileVo);
            url = "http://" + this.ip + ":9015/file/url/" + fileEntity.getId() + "." + suffixName;

        }else{
            BeanUtils.copyProperties(one, fileVo);
            url = "http://" + this.ip + ":9015/file/url/" + one.getId() + "." + one.getExt();
        }

        //得到文件地址
        fileVo.setUrl(url);
        return CommonResult.success(fileVo);
    }

    @Override
    public ResponseEntity url(String id) {
        //截取id字符串.之前的内容
        id = id.substring(0, id.lastIndexOf("."));
        FileEntity fileEntity = this.getById(id);
        if (fileEntity == null) {
            throw new BusinessException(ResultCode.FAILED.getCode(), "id对应的资源不存在");
        }

        String fileUrl = fileEntity.getFileUrl();
        String imageType = FileUtil.getImageType(fileUrl);
        MediaType mediaType = FileUtil.getMediaType(fileUrl);

        try {
            File file = new File(fileEntity.getFileUrl());
            Path path = file.toPath();
            Resource resource = new InputStreamResource(Files.newInputStream(path));
            if (!resource.exists() || !resource.isReadable()) {
                throw new RuntimeException("找不到文件");
            }

            ResponseEntity.BodyBuilder builder = ResponseEntity.ok();
            builder.contentType(mediaType);

            //文件格式
            if (imageType.equals(MediaType.APPLICATION_OCTET_STREAM_VALUE)) {
                builder.header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"" + file.getName() + "\"");
            }
            return builder.body(resource);

        } catch (Exception e) {
            throw new BusinessException(ResultCode.FAILED.getCode(), e.getMessage());
        }
    }


    public String getLocalIp() {
        LinkedHashSet localIpv4s = NetUtil.localIpv4s();
        if( null == localIpv4s ){
            return "";
        }

        //结果集遍历
        String ip = this.ip;
        if( StrUtil.isBlank(ip) ){
            return "";
        }
        List ips = Arrays.asList(ip.split(","));
        for (int i = 0; i < ips.size(); i++) {
            String cip = ips.get(i);

            for (String localIp : localIpv4s) {
                if( cip.equals(localIp) ){
                    return localIp;
                }
            }
        }

        return "";
    }
}




FileUtil
package com.hwxc.wms.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description
 */
@Slf4j
public class FileUtil {

    public static String getFileExtension(String fileName) {
        if (StringUtils.isBlank(fileName)) {
            return "";
        }
        int index = fileName.lastIndexOf(".");
        return fileName.substring(index + 1);
    }

    public static String getMd5(MultipartFile file) {
        try {
            byte[] uploadBytes = file.getBytes();
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] digest = md5.digest(uploadBytes);
            String hashString = new BigInteger(1, digest).toString(16);
            return hashString;
        } catch (Exception e) {
            log.info(e.toString(), e);
        }
        return null;
    }

    /**
     * 获取文件类型
     * @param filename
     * @return
     */
    public static String getImageType(String filename) {
        HashMap map = new HashMap<>();
        map.put("jpg", MediaType.IMAGE_JPEG_VALUE);
        map.put("png", MediaType.IMAGE_PNG_VALUE);
        map.put("gif", MediaType.IMAGE_GIF_VALUE);

        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex == -1) return MediaType.APPLICATION_OCTET_STREAM_VALUE;

        String ext = filename.substring(dotIndex + 1).toLowerCase();
        return map.getOrDefault(ext, MediaType.APPLICATION_OCTET_STREAM_VALUE);
    }

    public static MediaType getMediaType(String filename) {
        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex == -1) return MediaType.APPLICATION_OCTET_STREAM;

        String ext = filename.substring(dotIndex + 1).toLowerCase();
        switch (ext){
            case "jpg":
                return MediaType.IMAGE_JPEG;
            case "png":
                return MediaType.IMAGE_PNG;
            case "gif":
                return MediaType.IMAGE_GIF;
            default:
                return MediaType.APPLICATION_OCTET_STREAM;
        }
    }

    /**
     * 获取文件目录
     * @param filename
     * @return
     */
    public static String getDir(String filename) {
        return filename.substring(0, filename.lastIndexOf('/'));
    }

    /**
     * 获取文件名称
     * @param filename
     * @return
     */
    public static String getName(String filename) {
        return filename.substring(filename.lastIndexOf('/') + 1);
    }

}

对应的数据库表:

create table file
(
    id           bigint auto_increment comment '主键'
        primary key,
    file_url     varchar(300) default ''                not null comment '文件url',
    file_md5     varchar(32)  default ''                not null comment '文件md5',
    create_time  datetime     default CURRENT_TIMESTAMP null,
    update_time  datetime     default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    ip           varchar(20)  default ''                not null,
    ext          varchar(10)  default ''                not null comment '图片类型'
)
    comment '文件表';

你可能感兴趣的:(服务器,运维)