自建云盘(1):Spring boot 文件上传下载

版本

名称 版本
jdk 1.8.2
spring boot 2.7.2
knife4j 3.0.3
swagger 3.0.0
hutool 5.8.3

maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jingjing</groupId>
    <artifactId>jingjing-cloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <description>京京云盘</description>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <log4j2.version>2.18.0</log4j2.version>
        <slf4j-log4j12.version>1.7.36</slf4j-log4j12.version>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
        <relativePath/>
    </parent>


    <dependencies>
       <!--spring boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--工具-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.3</version>
        </dependency>

    </dependencies>

</project>

文件上传

接口层

@Slf4j
@Validated
@Api(tags = "文件上传")
@RestController
@RequestMapping("upload")
public class UploadController extends BaseController {

    @Resource
    FileService fileService;

    @PutMapping(value = "/file", headers = "content-type=multipart/form-data")
    @ApiOperationSupport(order = 1)
    @ApiOperation(value = "上传", response = Result.class)
    @ApiImplicitParam(name = "file", value = "上传的文件", dataTypeClass = MultipartFile.class, required = true)
    public Result<Void> upload(@NotNull(message = "目标文件不能为空") @RequestPart MultipartFile file) {
        fileService.uploadFile(file);
        return resultSuccess();
    }

}

service实现

    @Value("${jingjing.file.localBasePath}")
    public String baseFilePath;

    @Override
    public void uploadFile(String path, InputStream inputStream) {
        String uploadPath = this.getSystemPath(path);
        // 如果目录不存在则创建
        String parentPath = FileUtil.getParent(uploadPath, 1);
        if (!FileUtil.exist(parentPath)) {
            FileUtil.mkdir(parentPath);
        }

        File tempFile = new File(uploadPath);
        BufferedOutputStream outputStream = FileUtil.getOutputStream(tempFile);
        IoUtil.copy(inputStream, outputStream);
    }

    @Override
    public void uploadFile(MultipartFile file) {
        String name = file.getOriginalFilename();
        long size = file.getSize();
        log.info("上传文件,文件名={},文件大小={}", name, size);
        try {
            this.uploadFile(name, file.getInputStream());
        } catch (IOException e) {
            log.error("文件上传时,解析失败", e);
            throw new JingJingException(JingJingErrorCodeEnmu.FILE_PARSE_ERROR);
        }
    }

关于IO的核心代码,选择使用工具类实现,没有选择自己实现,自己写IO的实现,并不会比现有的工具类好很多,并且需要维护大量的代码。 这里使用的工具方法是: cn.hutool.core.io.IoUtil#copy(java.io.InputStream, java.io.OutputStream); 如果后期发现使用的工具方法有性能问题或者漏洞时,再选择自己重写或更换工具类。

完成后端的接口开发后,同样使用工具来进行基本的调试。此项目选择swagger,但并不是原生的swagger,而是二次封装的knife4j,在配置上,与swagger略有不同,尤其是上传下载类的接口。此处swagger配置可见Controller方法的注解。

验证使用

image

文件下载

接口层

@Slf4j
@Validated
@Api(tags = "文件上传")
@RestController
@RequestMapping("download")
public class DownloadController extends BaseController {

    @Resource
    FileService fileService;

    /**
     * 文件下载*
     * 文件流下线
     * @param path 文件路径
     */
    @ApiOperationSupport(order = 1)
    @ApiOperation(value = "下载文件到本地")

    @GetMapping("file/{path}")
    @ResponseBody
    public ResponseEntity<org.springframework.core.io.Resource> downloadFile(@PathVariable("path") String path) {
        log.info("文件下载:{}", path);
        return fileService.downloadFile(path);
    }
}

service实现

    @Override
    public ResponseEntity<Resource> downloadFile(String path) {
        File file = new File(this.getSystemPath(path));
        if (!file.exists()) {
            throw new JingJingException(JingJingErrorCodeEnmu.FILE_NO_EXISTS);
        }
        HttpHeaders headers = new HttpHeaders();
        String fileName = file.getName();
        headers.setContentDispositionFormData("attachment", StringUtils.encodeAllIgnoreSlashes(fileName));
        return ResponseEntity
                .ok()
                .headers(headers)
                .contentLength(file.length())
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(new FileSystemResource(file));
    }

此处的实现,没有使用response直接写出去,而选择了spring的工具类org.springframework.http.ResponseEntity。

优化点

文件下载的逻辑是根据文件的绝对路径找到文件,并通过流的形式返回给用户。 此处的实现是直接传递文件路径,不利于信息安全。

可在上传的时候,根据文件的路径,转换成文件唯一标识的映射,映射关系需要借助mysql实现,下载文件时,根据文件的唯一标识获取。

验证使用

image

关于下载,可以使用swagger进行调试,也可以使用浏览器的get请求直接下载。如图:

image

文件预览

借助静态资源服务器实现文件预览

可使用nginx搭建一个静态资源服务器,实现文件预览的功能,比较简单好用 操作链接:https://www.cnblogs.com/chenglc/p/16607579.html

效果

文件夹 image

文件预览 image

读取磁盘目录

正常读取磁盘目录和文件列表,是根据用户的维度,查询数据库中存储的数据。此处不再展示。

对于云盘的绝对管理员,可以读取磁盘的文件列表。

接口层
@Api(tags = "存储资源")
@Slf4j
@RestController
@RequestMapping("storage")
public class StorageSourceController {

    @Resource
    StorageSourceService service;

    /**
     * 读取文件列表*
     *
     * @param path
     * @return
     */
    @ApiOperationSupport(order = 1)
    @ApiOperation(value = "读取磁盘文件", response = Result.class)
    @GetMapping("readPhysicsFiles")
    public Result<FileVo> readPhysicsFiles(String path) {
        log.info("读取文件列表{}", path);
        FileVo vo = service.readFile(path);
        return new Result<>(vo);
    }
}
servie实现
    @Value("${jingjing.file.localBasePath}")
    public String baseFilePath;

    @Override
    public FileVo readFile(String path) {
        if (!path.startsWith(baseFilePath)) {
            throw new JingJingException(JingJingErrorCodeEnmu.NOT_AUTHORIZED_TO_READ);
        }
        File file = new File(path);
        FileVo vo = buildFileVoByFile(file);
        vo.setSubFiles(Arrays.stream(Objects.requireNonNull(file.listFiles()))
                .map(this::buildFileVoByFile).collect(Collectors.toList()));
        return vo;
    }


    private final FileVo buildFileVoByFile(File file) {
        return FileVo.builder()
                .diskPath(file.getPath())
                .name(file.getName())
                .isDirectory(file.isDirectory())
                .parentPath(file.getName())
                .path(file.getPath().replace(baseFilePath,""))
                .size(file.length())
                .build();
    }
接口调用

image