diff --git a/.gitignore b/.gitignore index a1c2a238a965f004ff76978ac1086aa6fe95caea..52617b4c0ad1e61464aae1fa2dccab344f795e4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,39 @@ -# Compiled class file -*.class +HELP.md +.gradle +build/ +logs/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ -# Log file -*.log +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ -# BlueJ files -*.ctxt +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +### VS Code ### +.vscode/ +.idea/ \ No newline at end of file diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 01ee3a1c5bb74d4d1f13f2a1d0a5cc69279c751e..0000000000000000000000000000000000000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# cloud-file-server - -#### Description -Java 基于 MongoDB GridFS 实现文件服务器 - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index 25497e734eda1dc35eed0a1998517a34eb12d9ce..1c8b358d614265be4719313bb1777449565a3403 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,32 @@ -# cloud-file-server - -#### 介绍 -Java 基于 MongoDB GridFS 实现文件服务器 - -#### 软件架构 -软件架构说明 - - -#### 安装教程 - -1. xxxx -2. xxxx -3. xxxx - -#### 使用说明 - -1. xxxx -2. xxxx -3. xxxx - -#### 参与贡献 - -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request - - -#### 特技 - -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +# 文件服务 + +## 技术栈 + +- SpringBoot +- SpringCloud +- MongoDB +- JDK8 +- SpringData MongoDB + +## 功能描述 +- 单文件上传 +- 批量上传 +- 文件预览 仅支持,浏览器预览支持的文件格式 +- 文件下载 +- 文件删除 +- 文件重命名 +- 批量删除 +- 获取文件内容 +- 分片上传 +- 断点续传上传 +- 断点续传下载 + +## MongoDB GridFS 简介 +- GridFS是用于存储和检索超过16 MB BSON文档大小限制的文件 +- GridFS 会将大文件对象分割成多个小的chunk(文件片段),一般为256k/个,每个chunk将作为MongoDB的一个文档(document)被存储在chunks集合中。 + +## 何时使用GridFS +- 如果文件系统限制了目录中文件的数量,则可以使用GridFS来存储所需数量的文件。 +- 当您要访问大文件部分的信息而不必将整个文件加载到内存中时,可以使用GridFS来调用文件的某些部分,而无需将整个文件读入内存。 +- 当您想要使文件和元数据自动同步并在多个系统和设施中部署时,可以使用GridFS。使用地理上分散的副本集时,MongoDB可以自动将文件及其元数据分发到许多 mongod实例和设施。 +- 如果文件都小于16 MB的限制,请考虑将每个文件存储在单个文档中,而不要使用GridFS。您可以使用BinData数据类型存储二进制数据。 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..cb0bcdcd70161a8a230a785731a8055052674949 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +buildscript { + ext { + springBootVersion = '2.2.2.RELEASE' + springGradleVersion = '1.0.10.RELEASE' + swaggerVersion = '3.0.0' + springCloudVersion = 'Hoxton.RELEASE' + springCloudAlibabaVersion = '2.2.0.RELEASE' + } + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" + classpath "io.spring.gradle:dependency-management-plugin:$springGradleVersion" + } +} + +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + + +group = 'com.jjsk' +version = '0.0.1' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + implementation 'org.springframework.boot:spring-boot-devtools' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + compile "io.springfox:springfox-boot-starter:$swaggerVersion" + compile 'org.apache.commons:commons-lang3' + compile 'commons-io:commons-io:2.8.0' + compile 'commons-codec:commons-codec:1.9' + compile 'com.alibaba:fastjson:1.2.9' + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion" + mavenBom "com.alibaba.cloud:spring-cloud-alibaba-dependencies:$springCloudAlibabaVersion" + } + } + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +test { +// include '**/Test*.class' + exclude '**/FilesystemApplicationTests.class' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..4d9ca1649142b0c20144adce78e2472e2da01c30 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000000000000000000000000000000000..4f906e0c811fc9e230eb44819f509cd0627f2600 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000000000000000000000000000000000..107acd32c4e687021ef32db511e8a206129b88ec --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000000000000000000000000000000000..34cf56eb9f67a8ba93d83c138f0316a907e797f5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'cloud-file-server' + diff --git a/src/main/java/com/jjsk/FilesystemApplication.java b/src/main/java/com/jjsk/FilesystemApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..57839030825dd682a964de8e34b68566e02ef9d4 --- /dev/null +++ b/src/main/java/com/jjsk/FilesystemApplication.java @@ -0,0 +1,12 @@ +package com.jjsk; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FilesystemApplication { + + public static void main(String[] args) { + SpringApplication.run(FilesystemApplication.class, args); + } +} diff --git a/src/main/java/com/jjsk/api/FileUploadControllerApi.java b/src/main/java/com/jjsk/api/FileUploadControllerApi.java new file mode 100644 index 0000000000000000000000000000000000000000..bec91f8e6575d09ef08e8c5b89a0885c5dbf3cf6 --- /dev/null +++ b/src/main/java/com/jjsk/api/FileUploadControllerApi.java @@ -0,0 +1,51 @@ +package com.jjsk.api; + +import com.jjsk.common.domain.ApiResult; +import com.jjsk.entity.bo.UploadPartBo; +import com.jjsk.entity.vo.CheckPartVo; +import com.jjsk.entity.vo.FileUploadVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @author Tadashi + * @descroption + * @date 2021/1/22 10:44 + */ +@Api(tags = "GridFS存储服务接口") +public interface FileUploadControllerApi { + + @ApiOperation("上传文件") + public ApiResult upload(MultipartFile file); + + @ApiOperation("批量上传文件") + public ApiResult batchUpload(MultipartFile[] file); + + @ApiOperation("文件预览") + public void preview(String id, boolean inline, HttpServletResponse response); + + @ApiOperation("文件删除") + public ApiResult delete(String id); + + @ApiOperation("批量删除") + public ApiResult batchDelete(List ids); + + @ApiOperation("获取文件二进制内容") + public ApiResult getFileById(String id); + + @ApiOperation("获取文件信息") + public ApiResult getFileInfoById(String id); + + @ApiOperation("重命名文件") + public ApiResult rename(String id, String name); + + @ApiOperation("分片上传") + public ApiResult uploadPart(UploadPartBo file); + + @ApiOperation("检查分片断点") + public ApiResult checkFileMd5(String md5, String name, Integer chunkTotal); +} diff --git a/src/main/java/com/jjsk/common/domain/ApiResult.java b/src/main/java/com/jjsk/common/domain/ApiResult.java new file mode 100644 index 0000000000000000000000000000000000000000..355375c42edc48fd20c230c4aeda597eafd72ff8 --- /dev/null +++ b/src/main/java/com/jjsk/common/domain/ApiResult.java @@ -0,0 +1,93 @@ +package com.jjsk.common.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 业务响应实体 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel("业务响应") +public class ApiResult implements Serializable { + + @ApiModelProperty("返回编码") + private int code; + + @ApiModelProperty("是否成功") + private boolean success; + + @ApiModelProperty("返回消息") + private String msg; + + @ApiModelProperty("返回结果") + private T result; + + @ApiModelProperty("当前时间戳") + private Long timestamp = System.currentTimeMillis(); + + public boolean isSuccess() { + return code == ResultMsg.SUCCESS.getCode(); + } + + public ApiResult(T result) { + this.code = ResultMsg.SUCCESS.getCode(); + this.msg = ResultMsg.SUCCESS.getMsg(); + this.result = result; + } + + public ApiResult(ResultMsg resultMsg) { + this.code = resultMsg.getCode(); + this.msg = resultMsg.getMsg(); + } + + public static ApiResult of(ResultMsg resultMsg) { + return new ApiResult<>(resultMsg); + } + + public static ApiResult of(ResultMsg resultMsg, T data) { + ApiResult r = new ApiResult<>(resultMsg); + r.setResult(data); + return r; + } + + public static ApiResult ok() { + return new ApiResult<>(ResultMsg.SUCCESS); + } + + public static ApiResult ok(T result) { + return new ApiResult<>(result); + } + + public static ApiResult error() { + return new ApiResult<>(ResultMsg.ERROR); + } + + public static ApiResult error(String message) { + ApiResult r = new ApiResult<>(ResultMsg.ERROR); + r.setMsg(message); + return r; + } + + public static ApiResult fail() { + return new ApiResult<>(ResultMsg.FAIL); + } + + public static ApiResult fail(String msg) { + ApiResult r = new ApiResult<>(ResultMsg.FAIL); + r.setMsg(msg); + return r; + } + + public static ApiResult fail(T result) { + ApiResult r = new ApiResult<>(ResultMsg.FAIL); + r.setResult(result); + return r; + } +} diff --git a/src/main/java/com/jjsk/common/domain/BaseConstants.java b/src/main/java/com/jjsk/common/domain/BaseConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..d05ad822b868ff22a4680610538ed21b38d4ea64 --- /dev/null +++ b/src/main/java/com/jjsk/common/domain/BaseConstants.java @@ -0,0 +1,18 @@ +package com.jjsk.common.domain; + +/** + * 基础常量 + */ +public interface BaseConstants { + + /** + * 小文件最大大小 + */ + Integer DEFAULT_CHUNK_SIZE_BYTES = 255 * 1024; + + /** + * 文件元数据 + */ + String FILE_METADATA_CONTENT_TYPE = "_contentType"; + String FILE_METADATA_SUFFIX = "_suffix"; +} diff --git a/src/main/java/com/jjsk/common/domain/PageBo.java b/src/main/java/com/jjsk/common/domain/PageBo.java new file mode 100644 index 0000000000000000000000000000000000000000..9efc773c654310ec4ef5a62e2b3c30a6f2cd0e81 --- /dev/null +++ b/src/main/java/com/jjsk/common/domain/PageBo.java @@ -0,0 +1,28 @@ +package com.jjsk.common.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.HashMap; + +/** + * 分页入参 + */ +@Data +@ApiModel("分页入参") +public class PageBo { + /** + * 当前页 + */ + @ApiModelProperty(value = "当前页", required = true, example = "1") + protected Long current = 1L; + /** + * 每页显示条数,默认 10 + */ + @ApiModelProperty(value = "页码大小", required = true, example = "10") + protected Long size = 10L; + + @ApiModelProperty("排序 key:字段名,value:DESC/ASC ") + protected HashMap sorter; +} \ No newline at end of file diff --git a/src/main/java/com/jjsk/common/domain/PageVo.java b/src/main/java/com/jjsk/common/domain/PageVo.java new file mode 100644 index 0000000000000000000000000000000000000000..b8ed81d59ae92fa96598ddc0de41f1f72ec6c8f8 --- /dev/null +++ b/src/main/java/com/jjsk/common/domain/PageVo.java @@ -0,0 +1,53 @@ +package com.jjsk.common.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 分页响应实体 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel("分页响应") +public class PageVo implements Serializable { + + @ApiModelProperty("当前页") + private Long current; + + @ApiModelProperty("页码大小") + private Long size; + + @ApiModelProperty("总页数") + private Long totalPage; + + @ApiModelProperty("总数") + private Long total; + + @ApiModelProperty("结果集合") + private List list; + + public PageVo(Long current, Long size, Long total, List list) { + this.current = current; + this.size = size; + this.total = total; + this.list = list; + } + + public PageVo(Long current, Long size, Long total) { + this.current = current; + this.size = size; + this.total = total; + } + + public Long getTotalPage() { + return total % size == 0 ? total / size : total / size + 1; + } + +} diff --git a/src/main/java/com/jjsk/common/domain/ResultMsg.java b/src/main/java/com/jjsk/common/domain/ResultMsg.java new file mode 100644 index 0000000000000000000000000000000000000000..d844d554546a66e7e929a88828dda89695dc813d --- /dev/null +++ b/src/main/java/com/jjsk/common/domain/ResultMsg.java @@ -0,0 +1,37 @@ +package com.jjsk.common.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 状态码 + *

+ * 状态码规则: + * 1xxx: 公共状态码 + */ +@Getter +@AllArgsConstructor +public enum ResultMsg { + + /** + * 公共状态码 + */ + SUCCESS(200, "操作成功"), + FAIL(402, "请求失败"), + ILLEGAL_REQUEST(400, "非法请求"), + NOT_AUTHORIZATION(401, "未授权"), + ILLEGAL_PARAM(403, "参数异常"), + FALL_BACK(405, "断路返回"), + SERVER_ERROR(500, "服务器异常"), + ERROR(-1, "系统开小差了"), + + ARGUMENT_NOT_INVALID(1001, "参数无效"), + DATA_NOT_FOUND(1002, "没有找到记录"), + PARAM_IS_ILLEGAL(1003, "包含非法字符"), + VALIDATOR_ERROR(1004, "数据校验异常"), + REPEAT_OPERATION(1005, "亲,您已操作过,请勿重复操作"), + ; + + private final Integer code; + private final String msg; +} diff --git a/src/main/java/com/jjsk/config/MongoConf.java b/src/main/java/com/jjsk/config/MongoConf.java new file mode 100644 index 0000000000000000000000000000000000000000..5cfad58e3e3483c03e91849a1b2a53342c91c0aa --- /dev/null +++ b/src/main/java/com/jjsk/config/MongoConf.java @@ -0,0 +1,28 @@ +package com.jjsk.config; + +import com.mongodb.MongoClient; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.GridFSBuckets; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * mongo 配置 + */ +@Configuration +public class MongoConf { + + @Value("${spring.data.mongodb.database}") + String db; + + @Bean + public GridFSBucket getGridFSBucket(MongoClient mongoClient){ + MongoDatabase database = mongoClient.getDatabase(db); + GridFSBucket bucket = GridFSBuckets.create(database); + return bucket; + } + + +} diff --git a/src/main/java/com/jjsk/config/ServerConfig.java b/src/main/java/com/jjsk/config/ServerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6e9c9f131f633b09c5435a773585aa72e4d5ec78 --- /dev/null +++ b/src/main/java/com/jjsk/config/ServerConfig.java @@ -0,0 +1,28 @@ +package com.jjsk.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * @author Tadashi + * @descroption + * @date 2021/1/27 12:27 + */ +@Component +public class ServerConfig { + @Value("${server.port}") + private int serverPort; + + public String getUrl() { + InetAddress address = null; + try { + address = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + return "http://" + address.getHostAddress() + ":" + this.serverPort; + } +} diff --git a/src/main/java/com/jjsk/config/Swagger2Config.java b/src/main/java/com/jjsk/config/Swagger2Config.java new file mode 100644 index 0000000000000000000000000000000000000000..7419abbed3151900eb70ccb0b236f540569ea370 --- /dev/null +++ b/src/main/java/com/jjsk/config/Swagger2Config.java @@ -0,0 +1,34 @@ +package com.jjsk.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * Swagger2API文档的配置 + */ +@Configuration +public class Swagger2Config { + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.jjsk")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("FileSystem Api") + .description("FileSystem Api SpringCloud") + .version("1.0") + .build(); + } +} diff --git a/src/main/java/com/jjsk/controller/FileUploadController.java b/src/main/java/com/jjsk/controller/FileUploadController.java new file mode 100644 index 0000000000000000000000000000000000000000..ff7a61d02de0a6fa6bdd9d35ee70f57edd595b63 --- /dev/null +++ b/src/main/java/com/jjsk/controller/FileUploadController.java @@ -0,0 +1,91 @@ +package com.jjsk.controller; + +import com.jjsk.api.FileUploadControllerApi; +import com.jjsk.common.domain.ApiResult; +import com.jjsk.entity.bo.UploadPartBo; +import com.jjsk.entity.vo.CheckPartVo; +import com.jjsk.entity.vo.FileUploadVo; +import com.jjsk.service.FileUploadService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @author Tadashi + * @descroption + * @date 2021/1/22 10:47 + */ +@Slf4j +@RestController +@RequestMapping("file") +@RequiredArgsConstructor +public class FileUploadController implements FileUploadControllerApi { + + private final FileUploadService service; + + @Override + @PostMapping("upload") + public ApiResult upload(@RequestParam MultipartFile file) { + return ApiResult.ok(service.upload(file)); + } + + @Override + @PostMapping("batch/upload") + public ApiResult batchUpload(@RequestParam MultipartFile[] file) { + return ApiResult.ok(service.batchUpload(file)); + } + + @Override + @GetMapping("preview/{id}.*") + public void preview(@PathVariable String id, @RequestParam(required = false,defaultValue = "false") boolean inline, HttpServletResponse response) { + service.preview(id, inline, response); + } + + @Override + @DeleteMapping("delete/{id}") + public ApiResult delete(@PathVariable String id) { + return service.delete(id); + } + + @Override + @DeleteMapping("batchDelete") + public ApiResult batchDelete(@RequestParam List ids) { + return service.batchDelete(ids); + } + + @Override + @GetMapping("get/{id}") + public ApiResult getFileById(@PathVariable String id) { + return service.getFileById(id); + } + + @Override + @GetMapping("getFileInfo/{id}") + public ApiResult getFileInfoById(@PathVariable String id) { + return service.getFileInfo(id); + } + + @Override + @PutMapping("rename/{id}/{name}") + public ApiResult rename(@PathVariable String id, @PathVariable String name) { + return service.rename(id, name); + } + + @Override + @PostMapping("uploadPart") + public ApiResult uploadPart(UploadPartBo file) { + FileUploadVo vo = service.uploadPart(file); + log.info(" uploadPart {}", vo); + return ApiResult.ok(vo); + } + + @Override + @GetMapping("checkFileMd5/{md5}/{name}/{chunkTotal}") + public ApiResult checkFileMd5(@PathVariable String md5, @PathVariable String name, @PathVariable Integer chunkTotal) { + return service.checkFileMd5(md5, name, chunkTotal); + } +} diff --git a/src/main/java/com/jjsk/entity/bo/UploadPartBo.java b/src/main/java/com/jjsk/entity/bo/UploadPartBo.java new file mode 100644 index 0000000000000000000000000000000000000000..0a79f3b3bec1e099c0feee287eba6967af951b5e --- /dev/null +++ b/src/main/java/com/jjsk/entity/bo/UploadPartBo.java @@ -0,0 +1,44 @@ +package com.jjsk.entity.bo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + + +@Data +@ApiModel("File-Vo") +public class UploadPartBo { + + @ApiModelProperty("文件Id,第一片上传后会返回,后续片段必须带上") + private String fileId; + + @ApiModelProperty("文件名称,第一片上传必须带上") + private String name; + + @ApiModelProperty("文件大小,第一片上传必须带上") + private Long size; + + @ApiModelProperty(value = "分片块大小", required = true) + private Integer chunkSize; + + @ApiModelProperty(value = "分片块索引, 从零开始", required = true) + private Integer chunkIndex; + + @ApiModelProperty("文件MD5") + private String md5; + + @ApiModelProperty(value = "文件", required = true) + private MultipartFile file; + + @Override + public String toString() { + return "UploadPartBo{" + + "fileId='" + fileId + '\'' + + ", name='" + name + '\'' + + ", size=" + size + + ", chunkSize=" + chunkSize + + ", chunkIndex=" + chunkIndex + + '}'; + } +} diff --git a/src/main/java/com/jjsk/entity/vo/CheckPartVo.java b/src/main/java/com/jjsk/entity/vo/CheckPartVo.java new file mode 100644 index 0000000000000000000000000000000000000000..37e376b270be1c16cb7f5393ca1be288b3844d13 --- /dev/null +++ b/src/main/java/com/jjsk/entity/vo/CheckPartVo.java @@ -0,0 +1,28 @@ +package com.jjsk.entity.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; + +/** + * @author chonghui.tian + * @descroption + * @date 2021/5/20 17:48 + */ +@Data +@Builder +@ApiModel("Check-Vo") +public class CheckPartVo{ + @ApiModelProperty("分块索引") + private Integer chunkIndex; + + @ApiModelProperty("文件Id") + private String fileId; + + @ApiModelProperty("状态: 0=从断点开始传, 1=秒传, 2=从新上传") + private Integer status; + + @ApiModelProperty("元数据信息") + private FileUploadVo fileInfo; +} diff --git a/src/main/java/com/jjsk/entity/vo/FileUploadVo.java b/src/main/java/com/jjsk/entity/vo/FileUploadVo.java new file mode 100644 index 0000000000000000000000000000000000000000..6376073a72d2aff431ebfb05a16004137df33831 --- /dev/null +++ b/src/main/java/com/jjsk/entity/vo/FileUploadVo.java @@ -0,0 +1,42 @@ +package com.jjsk.entity.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * file - vo + */ +@Data +@Builder +@ApiModel("File-Vo") +public class FileUploadVo { + + @ApiModelProperty("文件Id") + private String fileId; + + @ApiModelProperty("文件名称") + private String name; + + @ApiModelProperty("文件大小") + private Long size; + + @ApiModelProperty("文件类型") + private String contentType; + + @ApiModelProperty("文件后缀") + private String suffix; + + @ApiModelProperty("创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime gmtCreated; + + @ApiModelProperty("预览地址") + private String previewUrl; + + +} diff --git a/src/main/java/com/jjsk/exception/BusinessException.java b/src/main/java/com/jjsk/exception/BusinessException.java new file mode 100644 index 0000000000000000000000000000000000000000..04ad1ce38389c11501834a8513b76309bb459a9a --- /dev/null +++ b/src/main/java/com/jjsk/exception/BusinessException.java @@ -0,0 +1,47 @@ +package com.jjsk.exception; + +import com.jjsk.common.domain.ResultMsg; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 业务异常 + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class BusinessException extends RuntimeException { + + private ResultMsg resultMsg; + + public BusinessException(String msg) { + super(msg); + } + + public BusinessException(ResultMsg result) { + super(result.getMsg()); + this.resultMsg = result; + } + + public BusinessException(String message, Exception e) { + super(message, e); + } + + public BusinessException(ResultMsg result, String message) { + super(message); + this.resultMsg = result; + } + + public static BusinessException of(ResultMsg msg) { + return new BusinessException(msg); + } + + public static BusinessException of(String message) { + return new BusinessException(ResultMsg.FAIL, message); + } + + public static BusinessException of(String message, Exception e) { + return new BusinessException(message, e); + } +} diff --git a/src/main/java/com/jjsk/exception/GlobalExceptionHandler.java b/src/main/java/com/jjsk/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..90d84f52d9f458e3c97a12aa78ffa744e715a48e --- /dev/null +++ b/src/main/java/com/jjsk/exception/GlobalExceptionHandler.java @@ -0,0 +1,122 @@ +package com.jjsk.exception; + +import com.jjsk.common.domain.ApiResult; +import com.jjsk.common.domain.ResultMsg; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 全局异常拦截 + */ +@Slf4j +@Component +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理业务异常 + * + * @param ex {@link BusinessException} + * @return ApiResult + */ + @ExceptionHandler(BusinessException.class) + public ApiResult handleException(BusinessException ex) { + ApiResult restVo = ApiResult.fail(); + if (ex.getResultMsg() != null) { + restVo.setCode(ex.getResultMsg().getCode()); + } + restVo.setMsg(ex.getMessage()); + log.error("业务异常 [code => {}, msg => {}]", restVo.getCode(), restVo.getMsg(), ex); + return restVo; + } + + /** + * nacos连接异常 + * + * @param ex + * @return + */ + @ExceptionHandler({ConnectException.class, SocketTimeoutException.class}) + public ApiResult handleException(ConnectException ex) { + ApiResult restVo = ApiResult.fail(); + if (ex.getMessage() != null) { + restVo.setCode(ResultMsg.SERVER_ERROR.getCode()); + } + restVo.setMsg("nacos服务连接异常, 正在尝试重连"); + return restVo; + } + + /** + * 请求方式不支持异常 + * + * @param ex + * @return + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ApiResult handleException(HttpRequestMethodNotSupportedException ex) { + ApiResult restVo = ApiResult.fail(); + if (ex.getMessage() != null) { + restVo.setCode(ResultMsg.FAIL.getCode()); + } + restVo.setMsg("不支持" + ex.getMethod() + "请求"); + return restVo; + } + + /** + * 参数校验异常处理 + * + * @param ex {@link MethodArgumentNotValidException} + * @return ApiResult + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResult> handleException(MethodArgumentNotValidException ex) { + log.error("Argument Not Valid Exception"); + BindingResult bindingResult = ex.getBindingResult(); + Map data = new HashMap<>(8); + bindingResult.getAllErrors().forEach(it -> { + FieldError fieldError = (FieldError) it; + log.error("objectName: {}, field: {}, message: {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage()); + data.put(fieldError.getField(), fieldError.getDefaultMessage()); + }); + return ApiResult.of(ResultMsg.ARGUMENT_NOT_INVALID, data); + } + + /** + * 处理系统异常 + * + * @param ex {@link Exception} + * @param request 请求 + * @return ApiResult + */ + @ExceptionHandler(Exception.class) + public ApiResult handleException(Exception ex, HttpServletRequest request) throws UnsupportedEncodingException { + StringBuilder message = new StringBuilder(); + message.append("\n######################### Error #########################\n"); + message.append("RequestURI: ").append(request.getRequestURI()).append("\n"); + message.append("Method: ").append(request.getMethod()).append("\n"); + message.append("Headers: \n"); + Iterator headerIterator = CollectionUtils.toIterator(request.getHeaderNames()); + while (headerIterator.hasNext()) { + String name = headerIterator.next(); + message.append("\t").append(name).append(": ").append(URLDecoder.decode(request.getHeader(name), "UTF-8")).append("\n"); + } + log.info(message.toString(), ex); + return ApiResult.error(); + } +} diff --git a/src/main/java/com/jjsk/service/FileUploadService.java b/src/main/java/com/jjsk/service/FileUploadService.java new file mode 100644 index 0000000000000000000000000000000000000000..24e470e98f13ad11268e603df941f9298c1b9783 --- /dev/null +++ b/src/main/java/com/jjsk/service/FileUploadService.java @@ -0,0 +1,334 @@ +package com.jjsk.service; + +import com.alibaba.fastjson.JSON; +import com.jjsk.common.domain.ApiResult; +import com.jjsk.common.domain.BaseConstants; +import com.jjsk.common.domain.ResultMsg; +import com.jjsk.entity.bo.UploadPartBo; +import com.jjsk.entity.vo.CheckPartVo; +import com.jjsk.entity.vo.FileUploadVo; +import com.jjsk.exception.BusinessException; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.GridFSDownloadStream; +import com.mongodb.client.gridfs.model.GridFSFile; +import com.mongodb.client.model.Filters; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.bson.BsonObjectId; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.types.Binary; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsResource; +import org.springframework.data.mongodb.gridfs.GridFsTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; + +/** + * @author Tadashi + * @descroption + * @date 2021/1/22 10:49 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileUploadService { + + private final GridFsTemplate gridFsTemplate; + private final MongoClient mongoClient; + private final GridFSBucket gridFSBucket; + @Value("${base.fs.base-preview-url}") + private String previewUrl; + + @Value("${base.fs.download-file-path}") + private String downloadTempPath;// = "D:/work"; + + @Value("${spring.data.mongodb.database}") + String database; + + public FileUploadVo upload(MultipartFile file) { + try { + String fileName = Optional.ofNullable(file.getOriginalFilename()).orElse(file.getName()); + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("md5").is(DigestUtils.md5Hex(file.getInputStream())).and("filename").is(fileName))); + if (Objects.nonNull(gridFSFile)) { + return FileUploadVo.builder() + .fileId(gridFSFile.getObjectId().toHexString()) + .name(fileName) + .size(gridFSFile.getLength()) + .contentType(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)) + .suffix(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .gmtCreated(LocalDateTime.ofInstant(gridFSFile.getUploadDate().toInstant(), ZoneId.systemDefault())) + .previewUrl(previewUrl + "/file/preview/" + gridFSFile.getObjectId().toHexString() + gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .build(); + } else { + Document metadata = new Document() + .append(BaseConstants.FILE_METADATA_CONTENT_TYPE, file.getContentType()) + .append(BaseConstants.FILE_METADATA_SUFFIX, fileName.substring(fileName.lastIndexOf("."))); + log.info("name: {}, size: {}", fileName, file.getSize()); + ObjectId objectId = gridFsTemplate.store(file.getInputStream(), fileName, metadata); + log.info("upload success {}, objectId: {}", fileName, objectId.toHexString()); + return FileUploadVo.builder() + .fileId(objectId.toHexString()) + .name(fileName) + .size(file.getSize()) + .contentType(metadata.getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)) + .suffix(metadata.getString(BaseConstants.FILE_METADATA_SUFFIX)) + .gmtCreated(LocalDateTime.now()) + .previewUrl(previewUrl + "/file/preview/" + objectId.toHexString() + metadata.getString(BaseConstants.FILE_METADATA_SUFFIX)) + .build(); + } + } catch (Exception e) { + log.info("upload file exception <<<=== ", e); + throw new BusinessException("File upload failed!Error: " + e.getMessage()); + } + } + + public List batchUpload(MultipartFile[] file) { + return Stream.of(file).map(this::upload).collect(Collectors.toList()); + } + + public void preview(String id, boolean inline, HttpServletResponse response) { + try (OutputStream out = response.getOutputStream()) { + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id))); + if (null != gridFSFile) { + Document document = Optional.ofNullable(gridFSFile.getMetadata()).orElse(new Document()); + response.setCharacterEncoding("UTF-8"); + if (inline) { + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;fileName=" + URLEncoder.encode(gridFSFile.getFilename(), String.valueOf(StandardCharsets.UTF_8))); + } else { + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;fileName=" + URLEncoder.encode(gridFSFile.getFilename(), String.valueOf(StandardCharsets.UTF_8))); + + } + response.addHeader(HttpHeaders.CONTENT_TYPE, Optional.ofNullable(document.getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)).orElse("image/jpeg")); + response.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(gridFSFile.getLength())); + gridFSBucket.downloadToStream(gridFSFile.getObjectId(), out); + } else { + out.write(JSON.toJSONString(ApiResult.error("file does not exist!")).getBytes(StandardCharsets.UTF_8)); + } + } catch (Exception e) { + log.error("file preview exception <<<=== ", e); + throw new BusinessException("File preview failed!Error: " + e.getMessage()); + } + } + + public ApiResult delete(String id) { + try { + gridFsTemplate.delete(new Query().addCriteria(Criteria.where("_id").is(id))); + return ApiResult.ok(); + } catch (Exception e) { + log.debug("删除文件失败: fileId = {}", id); + throw BusinessException.of(ResultMsg.DATA_NOT_FOUND); + } + } + + public ApiResult batchDelete(List ids) { + return ApiResult.ok(ids.stream().map(this::delete).collect(Collectors.toList())); + } + + public ApiResult getFileById(String id) { + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id))); + if (null == gridFSFile) { + return ApiResult.ok(ResultMsg.DATA_NOT_FOUND.getMsg()); + } + GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId()); + + GridFsResource gridFsResource = new GridFsResource(gridFSFile, gridFSDownloadStream); + + try { + byte[] bytes = IOUtils.toByteArray(gridFsResource.getInputStream()); + return ApiResult.ok(new String(bytes, "ISO-8859-1")); + } catch (IOException e) { + log.error("file fetch exception <<<=== ", e); + throw new BusinessException("File fetch failed!Error: " + e.getMessage()); + } + } + + public ApiResult getFileInfo(String id) { + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id))); + if (null == gridFSFile) { + return ApiResult.ok(ResultMsg.DATA_NOT_FOUND.getMsg()); + } + return ApiResult.ok(FileUploadVo.builder() + .fileId(gridFSFile.getObjectId().toHexString()) + .name(gridFSFile.getFilename()) + .size(Long.valueOf(gridFSFile.getChunkSize())) + .contentType(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)) + .suffix(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .gmtCreated(LocalDateTime.ofInstant(gridFSFile.getUploadDate().toInstant(), ZoneId.systemDefault())) + .previewUrl(previewUrl + "/file/preview/" + gridFSFile.getObjectId().toHexString() + gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .build()); + } + + public ApiResult rename(String id, String name) { + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id))); + if (null == gridFSFile) { + return ApiResult.ok(ResultMsg.DATA_NOT_FOUND.getMsg()); + } + gridFSBucket.rename(new ObjectId(id), name); + return ApiResult.ok(FileUploadVo.builder() + .fileId(gridFSFile.getObjectId().toHexString()) + .name(name) + .size(Long.valueOf(gridFSFile.getChunkSize())) + .contentType(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)) + .suffix(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .gmtCreated(LocalDateTime.ofInstant(gridFSFile.getUploadDate().toInstant(), ZoneId.systemDefault())) + .previewUrl(previewUrl + "/file/preview/" + gridFSFile.getObjectId().toHexString() + gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .build()); + } + + public FileUploadVo uploadPart(UploadPartBo part) { + log.info("uploadPart ===>>> part: {}, chunkSize: {}, ContentType: {}", part, part.getFile().getSize(), part.getFile().getContentType()); + try { + MultipartFile file = part.getFile(); + if (StringUtils.hasText(part.getFileId())) { + // TODO 块验证 + ObjectId objectId = new ObjectId(part.getFileId()); + writeChunk(new BsonObjectId(objectId), part.getChunkIndex(), file.getBytes()); + log.info("第 {} 片上传完毕, 文件Id: {}", part.getChunkIndex(), objectId); + return FileUploadVo.builder().fileId(objectId.toHexString()).build(); + } else { + ObjectId objectId = new ObjectId(); + BsonValue fileId = new BsonObjectId(objectId); + Document metadata = new Document() + .append(BaseConstants.FILE_METADATA_CONTENT_TYPE, file.getContentType()) + .append(BaseConstants.FILE_METADATA_SUFFIX, file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))); + log.info(" --metadata-- {}", metadata); + + String fileName = part.getName(); + MongoCollection filesCollection = getFilesCollection(mongoClient.getDatabase(database), gridFSBucket.getBucketName()); + GridFSFile gridFSFile = new GridFSFile(fileId, fileName, part.getSize(), part.getChunkSize(), new Date(), part.getMd5(), metadata); + filesCollection.insertOne(gridFSFile); + writeChunk(fileId, part.getChunkIndex(), file.getBytes()); + return FileUploadVo.builder() + .fileId(objectId.toHexString()) + .name(fileName) + .size(part.getSize()) + .contentType(metadata.getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)) + .suffix(metadata.getString(BaseConstants.FILE_METADATA_SUFFIX)) + .gmtCreated(LocalDateTime.now()) + .previewUrl(previewUrl + "/file/preview/" + objectId.toHexString() + metadata.getString(BaseConstants.FILE_METADATA_SUFFIX)) + .build(); + } + } catch (IOException e) { + log.error("fragment upload exception <<<=== part: {}, msg: {}", part, e.getMessage()); + throw new BusinessException("Fragment upload failed!Error: " + e.getMessage()); + } + } + + public ApiResult checkFileMd5(String md5, String name, Integer chunkTotal) { + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("md5").is(md5).and("filename").is(name))); + CheckPartVo build = CheckPartVo.builder().chunkIndex(0).status(2).build(); + if (Objects.nonNull(gridFSFile)) { + MongoCollection chunksCollection = getChunksCollection(mongoClient.getDatabase(database), gridFSBucket.getBucketName()); + FindIterable findIterable = chunksCollection + .find(Filters.eq("files_id", gridFSFile.getId())) + .sort(Filters.eq("n", -1)) + .limit(1); + int chunkIndex = findIterable.first().getInteger("n", 0); + // 当前片数 < 总片数 说明没有上传完成 + // 等于 至直接返回文件Id + if (chunkIndex < chunkTotal) { + build.setFileId(gridFSFile.getObjectId().toHexString()); + build.setChunkIndex(chunkIndex); + build.setStatus(0); + } else { + build.setStatus(1); + build.setFileInfo(FileUploadVo.builder() + .fileId(gridFSFile.getObjectId().toHexString()) + .name(name) + .size(gridFSFile.getLength()) + .contentType(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_CONTENT_TYPE)) + .suffix(gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .gmtCreated(LocalDateTime.ofInstant(gridFSFile.getUploadDate().toInstant(), ZoneId.systemDefault())) + .previewUrl(previewUrl + "/file/preview/" + gridFSFile.getObjectId().toHexString() + gridFSFile.getMetadata().getString(BaseConstants.FILE_METADATA_SUFFIX)) + .build()); + } + } + return ApiResult.ok(build); + } + + + private void writeChunk(BsonValue fileId, Integer chunkIndex, byte[] bytes) { + log.info("writeChunk ===>>> fileId: {} , chunkIndex: {} , length: {} ", fileId, chunkIndex, bytes.length); + MongoCollection chunksCollection = getChunksCollection(mongoClient.getDatabase(database), gridFSBucket.getBucketName()); + chunksCollection.insertOne(new Document("files_id", fileId).append("n", chunkIndex).append("data", new Binary(bytes))); + } + + private static MongoCollection getFilesCollection(final MongoDatabase database, final String bucketName) { + return database.getCollection(bucketName + ".files", GridFSFile.class).withCodecRegistry( + fromRegistries(database.getCodecRegistry(), MongoClientSettings.getDefaultCodecRegistry()) + ); + } + + private static MongoCollection getChunksCollection(final MongoDatabase database, final String bucketName) { + return database.getCollection(bucketName + ".chunks").withCodecRegistry(MongoClientSettings.getDefaultCodecRegistry()); + } + + + private byte[] getBytes(InputStream inputStream) throws Exception{ + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] b = new byte[1024]; + int i = 0; + while (-1!=(i=inputStream.read(b))){ + bos.write(b,0,i); + } + return bos.toByteArray(); + } + + /** + * 文件下载 + * + * @param response + * @param zipFileName + */ + private void downFile(HttpServletResponse response, String zipFileName, Long totalBytes) { + try { + String path = downloadTempPath + zipFileName; + File file = new File(path); + if (file.exists()) { + try (InputStream ins = new FileInputStream(path); + BufferedInputStream bins = new BufferedInputStream(ins); + OutputStream outs = response.getOutputStream(); + BufferedOutputStream bouts = new BufferedOutputStream(outs)) { + response.setContentType("application/x-download"); + response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(zipFileName, "UTF-8")); + int bytesRead = 0; + byte[] buffer = new byte[totalBytes.intValue()]; + while ((bytesRead = bins.read(buffer, 0, totalBytes.intValue())) != -1) { + bouts.write(buffer, 0, bytesRead); + } + bouts.flush(); + } + } + } catch (Exception e) { + log.error("文件下载出错", e); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..f285887b6759023667ecb7faff306ed086b92694 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,16 @@ +server: + port: 30301 +spring: + data: + mongodb: + uri: mongodb://192.168.11.118:31017/ + database: jjsk_mp_fileserver + servlet: + # limit upload file size + multipart: + max-file-size: 16GB + max-request-size: 1024GB +base: + fs: + base-preview-url: http://192.168.11.118:30071/file/no_token + download-file-path: /opt \ No newline at end of file diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..f36375563cc88e613d5f167a8fb5cf11705d094a --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,191 @@ + + + + + + + logback + + + + + + + + + + + + + + + + + debug + + + ${CONSOLE_LOG_PATTERN} + + UTF-8 + + + + + + + + ${log.path}/debug.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + ${log.path}/debug-%d{yyyy-MM-dd}.%i.log + + 100MB + + + 15 + + + + debug + ACCEPT + DENY + + + + + + + ${log.path}/info.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + ${log.path}/info-%d{yyyy-MM-dd}.%i.log + + 100MB + + + 15 + + + + info + ACCEPT + DENY + + + + + + + ${log.path}/warn.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + ${log.path}/warn-%d{yyyy-MM-dd}.%i.log + + 100MB + + + 15 + + + + warn + ACCEPT + DENY + + + + + + + ${log.path}/error.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + ${log.path}/error-%d{yyyy-MM-dd}.%i.log + + 100MB + + + 15 + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/jjsk/FilesystemApplicationTests.java b/src/test/java/com/jjsk/FilesystemApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..b420233a0b099dff4225980c420227bd7548050e --- /dev/null +++ b/src/test/java/com/jjsk/FilesystemApplicationTests.java @@ -0,0 +1,98 @@ +package com.jjsk; + +import com.mongodb.MongoClient; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.GridFSBuckets; +import com.mongodb.client.gridfs.GridFSFindIterable; +import com.mongodb.client.gridfs.model.GridFSFile; +import com.mongodb.client.model.Filters; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.annotation.Resource; + +import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class FilesystemApplicationTests { + + Log log = LogFactory.getLog(FilesystemApplicationTests.class); + + @Value("${spring.data.mongodb.database}") + String database; + + @Resource + private MongoClient mongoClient; + + @Resource + private GridFSBucket gridFsBucket; + + @Resource + GridFsTemplate gridFsTemplate; + + @Test + public void findOne() throws Exception { + // 获取文件ID + String objectId = "60a2595236147e43e622330c"; + // 创建一个容器,传入一个`MongoDatabase`类实例db + GridFSBucket bucket = GridFSBuckets.create(mongoClient.getDatabase(database)); + // 获取内容 + GridFSFindIterable gridFSFindIterable = bucket.find(Filters.eq("_id", new ObjectId(objectId))); + GridFSFile gridFSFile = gridFSFindIterable.first(); + System.out.println("fileInfo: " + gridFSFile); + + } + + public int checkFileMd5(String md5){ + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("md5").is(md5)).with(Sort.by(Sort.Order.desc("uploadDate")))); + System.out.println(gridFSFile); + MongoCollection chunksCollection = getChunksCollection(mongoClient.getDatabase(database), gridFsBucket.getBucketName()); + FindIterable findIterable = chunksCollection + .find(Filters.eq("files_id", gridFSFile.getId())) + .sort(Filters.eq("n", -1)) + .limit(1); + int chunkIndex = findIterable.first().getInteger("n", 0); + int multiply = chunkIndex * 255; + return Integer.valueOf(Math.abs(multiply / 10485760)); + } + + + @Test + public void test01() { + MongoCollection filesCollection = getFilesCollection(mongoClient.getDatabase(database), gridFsBucket.getBucketName()); + GridFSFile gridFSFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is("607e7193f7469561976fdeda"))); + MongoCollection chunksCollection = getChunksCollection(mongoClient.getDatabase(database), gridFsBucket.getBucketName()); + FindIterable findIterable = chunksCollection.find(new Document().append("files_id", gridFSFile.getObjectId())); + MongoCursor cursor1 = findIterable.iterator(); + while (cursor1.hasNext()){ + System.out.println(cursor1.next()); + } + } + + private static MongoCollection getFilesCollection(final MongoDatabase database, final String bucketName) { + return database.getCollection(bucketName + ".files", GridFSFile.class).withCodecRegistry( + fromRegistries(database.getCodecRegistry(), MongoClientSettings.getDefaultCodecRegistry()) + ); + } + + private static MongoCollection getChunksCollection(final MongoDatabase database, final String bucketName) { + return database.getCollection(bucketName + ".chunks").withCodecRegistry(MongoClientSettings.getDefaultCodecRegistry()); + } +} diff --git "a/\346\226\207\344\273\266\346\234\215\345\212\241Api\346\226\207\346\241\243.md" "b/\346\226\207\344\273\266\346\234\215\345\212\241Api\346\226\207\346\241\243.md" new file mode 100644 index 0000000000000000000000000000000000000000..0c993727585e87a4336bb38b0d0da6fe042a009a --- /dev/null +++ "b/\346\226\207\344\273\266\346\234\215\345\212\241Api\346\226\207\346\241\243.md" @@ -0,0 +1,611 @@ +# 文件服务Api文档 + +## 概览 + +FileServer Api + + + +FileServer Api SpringCloud + +版本信息 + +*版本* : 1.0 + +通用说明: + +①接口通用前缀: + +> 开发环境: http://IP:端口/网关前缀 +> 测试环境 http://IP:端口/网关前缀 + +② 接口通用返回: + +```json +{ + "code": 200, // 正确码为0000 错误码为0001 + "msg": "操作成功", // 如发生错误此处会显示相关错误信息 + "result": {},// 所有需要的结果集都会放在该对象中 + "success": true,// 是否成功 + "timestamp": 0// 返回当前时间 +} +``` + + + +## 获取文件信息 + +- **请求URL** + +> ``` +> file/getFileInfo/{id} +> ``` + +- **请求方式** + +> **GET** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------- | :--- | -------- | ------ | +| **id** | 是 | String | 文件ID | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "fileId": "606c0dd30ad8e3293c28e376", + "name": "企业微信截图_e3745bc4-7c76-45f7-9d7f-a5cb82e9a347.png", + "size": 261120, + "contentType": "image/png", + "suffix": ".png", + "gmtCreated": "2021-04-06T15:29:23.61", + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/606c0dd30ad8e3293c28e376.png" + }, + "timestamp": 1625536627434 +} +// 未找到记录 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": "没有找到记录", + "timestamp": 1618905924435 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} +``` + +------ + +## 获取文件二进制数据 + +- **请求URL** + +> ``` +> file/get/{id} +> ``` + +- **请求方式** + +> **GET** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------- | :--- | -------- | ------ | +| **id** | 是 | String | 文件ID | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "msg": "操作成功", + "result": { + // 返回文件内容: Byte数组 ps: 后端程序员使用'ISO-8859-1'编码进行解析 + // Java代码示例: byte[] bytes = result.getResult().toString().getBytes("ISO-8859-1"); + }, + "success": true, + "timestamp": 0 +} +// 未找到记录 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": "没有找到记录", + "timestamp": 1618905924435 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} +``` + +------ + +## 单文件上传 + +- **请求URL** + +> ``` +> file/upload +> ``` + +- **请求方式** + +> **POST** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | +| :------- | :--- | ------------------- | +| **file** | 是 | multipart/form-data | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "fileId": "607e78b2f7469561976fdf1f", //文件ID + "name": "Blade部署手册.pdf", //文件名称 + "size": 17522289, //文件大小 + "contentType": "application/pdf", //文件类型 + "suffix": ".pdf", //文件后缀 + "gmtCreated": "2021-04-20T14:46:10.76",//文件上传时间 + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/607e78b2f7469561976fdf1f.pdf" + //预览地址 + }, + "timestamp": 1618901170760 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} +``` + +------ + +## 批量上传文件 + +- **请求URL** + +> ``` +> file/batch/upload +> ``` + +- **请求方式** + +> **POST** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | +| :------- | :--- | ------------------- | +| **file** | 是 | multipart/form-data | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": [ + { + "fileId": "607e78b2f7469561976fdf1f", + "name": "Blade部署手册.pdf", + "size": 17522289, + "contentType": "application/pdf", + "suffix": ".pdf", + "gmtCreated": "2021-04-20T14:46:10.76", + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/607e78b2f7469561976fdf1f.pdf" + }, + { + "fileId": "607e78b2f7469561976fdf1f", + "name": "Blade部署手册.pdf", + "size": 17522289, + "contentType": "application/pdf", + "suffix": ".pdf", + "gmtCreated": "2021-04-20T14:46:10.76", + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/607e78b2f7469561976fdf1f.pdf" + } + ], + "timestamp": 1618901170760 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} +``` + +------ + +## 文件重命名 + +- **请求URL** + +> ``` +> file/rename/{id}/{name} +> ``` + +- **请求方式** + +> **PUT** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------- | :--- | -------- | ------------ | +| **id** | 是 | String | 文件ID | +| **name** | 是 | String | 修改的文件名 | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "fileId": "606c0dd30ad8e3293c28e376", + "name": "企业微信截图_e3745bc4-7c76-45f7-9d7f-a5cb82e9a347.png", + "size": 261120, + "contentType": "image/png", + "suffix": ".png", + "gmtCreated": "2021-04-06T15:29:23.61", + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/606c0dd30ad8e3293c28e376.png" + }, + "timestamp": 1625536627434 +} +// 未找到记录 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": "没有找到记录", + "timestamp": 1618905924435 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} +``` + +------ + +## 删除文件 + +- **请求URL** + +> ``` +> file/delete/{id} +> ``` + +- **请求方式** + +> **DELETE** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------- | :--- | -------- | ------ | +| **id** | 是 | String | 文件ID | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": null, + "timestamp": 1619081394608 +} +// 没找到记录 +{ + "code": 402, + "success": false, + "msg": "没有找到记录", + "result": null, + "timestamp": 1619081603366 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} + +``` + +------ + +## 批量删除文件 + +- **请求URL** + +> ``` +> file/batchDelete +> ``` + +- **请求方式** + +> **DELETE** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------- | :--- | -------- | ------ | +| **ids** | 是 | String[] | 文件ID | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": [ + { + "code": 200, + "success": true, + "msg": "操作成功", + "result": null, + "timestamp": 1621319196454 + }, + { + "code": 200, + "success": true, + "msg": "操作成功", + "result": null, + "timestamp": 1621319196458 + } + ], + "timestamp": 1621319196458 +} +// 没找到记录 +{ + "code": 402, + "success": false, + "msg": "没有找到记录", + "result": null, + "timestamp": 1619081603366 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} + +``` + +------ + +## 分片上传 + +- **请求URL** + +> ``` +> file/uploadPart +> +> ``` + +- **请求方式** + +> **POST** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------------- | :--- | ------------------- | ------------------------------------------ | +| **chunkIndex** | 是 | Integer | 分片块索引, 从零开始 | +| **chunkSize** | 是 | Integer | 分片块大小 单位: Bytes | +| **file** | 是 | multipart/form-data | 文件 | +| **md5** | 是 | String | 文件md5 | +| fileId | 否 | String | 文件Id,第一片上传后会返回,后续片段必须带上 | +| name | 否 | String | 文件名称,第一片上传必须带上 | +| size | 否 | Integer | 文件大小,第一片上传必须带上 单位: Bytes | + +- **返回示例** + +```json +// 正确示例 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "fileId": "60814509edf08973852c2ee4", + "name": "哈哈哈.pdf", + "size": 123123, + "contentType": "application/pdf", + "suffix": ".pdf", + "gmtCreated": null, + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/60814509edf08973852c2ee4.pdf" + }, + "timestamp": 1619084553272 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} + +``` + +------ + +## 检查分片断点 + +- **请求URL** + +> ``` +> file/checkFileMd5/{md5}/{name}/{chunkTotal} +> +> ``` + +- **请求方式** + +> **GET** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------------- | :--- | -------- | --------- | +| **chunkTotal** | 是 | Integer | 分片总数 | +| md5 | 是 | String | 文件md5值 | +| name | 是 | String | 文件名称 | + +- **返回示例** + +```json +// 秒传 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "chunkIndex": 0, + "status": 1, + "fileInfo" : { + "fileId": "607e78b2f7469561976fdf1f", + "name": "Blade部署手册.pdf", + "size": 17522289, + "contentType": "application/pdf", + "suffix": ".pdf", + "gmtCreated": "2021-04-20T14:46:10.76", + "previewUrl": "http://192.168.11.118:30071/file/no_token/file/preview/607e78b2f7469561976fdf1f.pdf" + } + }, + "timestamp": 1621505490410 +} +// 从断点开始传 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "chunkIndex": 67, + "fileId": "606295fc1620152f08bb6438", + "status": 0 + }, + "timestamp": 1621505738527 +} +// 从新上传 +{ + "code": 200, + "success": true, + "msg": "操作成功", + "result": { + "chunkIndex": 0, + "fileId": null, + "status": 2 + }, + "timestamp": 1621505669103 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} + +``` + +------ + +## 预览下载 + +简要描述 + +- file/preview/文件ID.*?inline=true //需要下载时添加inline参数 + +- **请求URL** + +> ``` +> file/preview/{id}.* +> +> ``` + +- **请求方式** + +> **GET** + +- **请求参数** + +| 请求参数 | 必选 | 参数类型 | 说明 | +| :------- | :--- | -------- | ------------------------------------------ | +| **id** | 是 | String | 分片块索引, 从零开始 | +| inline | 否 | Boolean | 是否下载, 可不传默认为预览
true为下载 | + +- **返回示例** + +```json +// 文件不存在 +{ + "code": -1, + "msg": "file does not exist!", + "success": false, + "timestamp": 1619083595233 +} +// 错误示例 +{ + "code": -1, + "success": false, + "msg": "系统开小差了", + "result": null, + "timestamp": 1618906394567 +} + +``` + +------ \ No newline at end of file