Kaynağa Gözat

将 tnb 中的 file 服务抽离出来, 作为一个独立的 dfs

reghao 4 yıl önce
ebeveyn
işleme
c3493c7dc1
89 değiştirilmiş dosya ile 3936 ekleme ve 0 silme
  1. 5 0
      .gitignore
  2. 10 0
      Dockerfile
  3. 221 0
      pom.xml
  4. 11 0
      src/main/java/cn/reghao/dfs/store/DfsStoreApplication.java
  5. 30 0
      src/main/java/cn/reghao/dfs/store/config/BaseDocument.java
  6. 37 0
      src/main/java/cn/reghao/dfs/store/config/ConfigMap.java
  7. 35 0
      src/main/java/cn/reghao/dfs/store/config/DfsConfig.java
  8. 33 0
      src/main/java/cn/reghao/dfs/store/config/DfsProperties.java
  9. 18 0
      src/main/java/cn/reghao/dfs/store/config/PathUrl.java
  10. 43 0
      src/main/java/cn/reghao/dfs/store/config/SwaggerConfig.java
  11. 52 0
      src/main/java/cn/reghao/dfs/store/config/WebConfig.java
  12. 46 0
      src/main/java/cn/reghao/dfs/store/controller/FileAutoController.java
  13. 30 0
      src/main/java/cn/reghao/dfs/store/controller/FileController.java
  14. 33 0
      src/main/java/cn/reghao/dfs/store/controller/FileDownloadController.java
  15. 47 0
      src/main/java/cn/reghao/dfs/store/controller/FileUploadController.java
  16. 50 0
      src/main/java/cn/reghao/dfs/store/controller/MediaUploadController.java
  17. 36 0
      src/main/java/cn/reghao/dfs/store/controller/VideoFileController.java
  18. 19 0
      src/main/java/cn/reghao/dfs/store/db/mapper/FileInfoMapper.java
  19. 15 0
      src/main/java/cn/reghao/dfs/store/db/mapper/FileUrlMapper.java
  20. 18 0
      src/main/java/cn/reghao/dfs/store/db/mapper/FileUserMapper.java
  21. 20 0
      src/main/java/cn/reghao/dfs/store/db/mapper/ImageFileMapper.java
  22. 22 0
      src/main/java/cn/reghao/dfs/store/db/mapper/VideoFileMapper.java
  23. 94 0
      src/main/java/cn/reghao/dfs/store/db/repository/FileRepository.java
  24. 52 0
      src/main/java/cn/reghao/dfs/store/inerceptor/StaticFileInterceptor.java
  25. 22 0
      src/main/java/cn/reghao/dfs/store/model/dto/DownloadFile.java
  26. 28 0
      src/main/java/cn/reghao/dfs/store/model/dto/FileDto.java
  27. 19 0
      src/main/java/cn/reghao/dfs/store/model/dto/FileInfoDto.java
  28. 19 0
      src/main/java/cn/reghao/dfs/store/model/dto/FileUrlDto.java
  29. 20 0
      src/main/java/cn/reghao/dfs/store/model/dto/ImgFileRetDto.java
  30. 24 0
      src/main/java/cn/reghao/dfs/store/model/dto/UploadFile.java
  31. 33 0
      src/main/java/cn/reghao/dfs/store/model/dto/UploadFilePart.java
  32. 13 0
      src/main/java/cn/reghao/dfs/store/model/dto/UploadFilePartRet.java
  33. 17 0
      src/main/java/cn/reghao/dfs/store/model/dto/UploadFileRet.java
  34. 30 0
      src/main/java/cn/reghao/dfs/store/model/dto/UploadPrepare.java
  35. 14 0
      src/main/java/cn/reghao/dfs/store/model/dto/UploadPrepareRet.java
  36. 28 0
      src/main/java/cn/reghao/dfs/store/model/dto/VidFileRetDto.java
  37. 30 0
      src/main/java/cn/reghao/dfs/store/model/dto/VidFileSbtDto.java
  38. 58 0
      src/main/java/cn/reghao/dfs/store/model/dto/auto/VideoFileDto.java
  39. 22 0
      src/main/java/cn/reghao/dfs/store/model/po/AudioFile.java
  40. 36 0
      src/main/java/cn/reghao/dfs/store/model/po/FileInfo.java
  41. 31 0
      src/main/java/cn/reghao/dfs/store/model/po/FileUrl.java
  42. 19 0
      src/main/java/cn/reghao/dfs/store/model/po/FileUser.java
  43. 32 0
      src/main/java/cn/reghao/dfs/store/model/po/ImageFile.java
  44. 51 0
      src/main/java/cn/reghao/dfs/store/model/po/VideoFile.java
  45. 26 0
      src/main/java/cn/reghao/dfs/store/rpc/FileInfoServiceImpl.java
  46. 80 0
      src/main/java/cn/reghao/dfs/store/rpc/VideoFileServiceImpl.java
  47. 120 0
      src/main/java/cn/reghao/dfs/store/service/FileAutoService.java
  48. 15 0
      src/main/java/cn/reghao/dfs/store/service/FileContentType.java
  49. 70 0
      src/main/java/cn/reghao/dfs/store/service/FileDownloadService.java
  50. 76 0
      src/main/java/cn/reghao/dfs/store/service/FileHeader.java
  51. 33 0
      src/main/java/cn/reghao/dfs/store/service/FileSignatures.java
  52. 75 0
      src/main/java/cn/reghao/dfs/store/service/FileStoreService.java
  53. 31 0
      src/main/java/cn/reghao/dfs/store/service/FileTypeService.java
  54. 150 0
      src/main/java/cn/reghao/dfs/store/service/FileUploadService.java
  55. 38 0
      src/main/java/cn/reghao/dfs/store/service/FileUrlService.java
  56. 15 0
      src/main/java/cn/reghao/dfs/store/service/RedisKey.java
  57. 11 0
      src/main/java/cn/reghao/dfs/store/service/media/AudioFileService.java
  58. 53 0
      src/main/java/cn/reghao/dfs/store/service/media/ImageFileService.java
  59. 115 0
      src/main/java/cn/reghao/dfs/store/service/media/VideoFileService.java
  60. 64 0
      src/main/java/cn/reghao/dfs/store/service/part/TmpFile.java
  61. 82 0
      src/main/java/cn/reghao/dfs/store/service/part/WholeFile.java
  62. 28 0
      src/main/java/cn/reghao/dfs/store/util/FileLifecycle.java
  63. 16 0
      src/main/java/cn/reghao/dfs/store/util/HertubePrefix.java
  64. 50 0
      src/main/java/cn/reghao/dfs/store/util/LoadBalancer.java
  65. 47 0
      src/main/java/cn/reghao/dfs/store/util/ServletUtil.java
  66. 19 0
      src/main/java/cn/reghao/dfs/store/util/StringUtil.java
  67. 24 0
      src/main/java/cn/reghao/dfs/store/util/UserContext.java
  68. 73 0
      src/main/java/cn/reghao/dfs/store/util/WebResult.java
  69. 220 0
      src/main/java/cn/reghao/dfs/store/util/image/Captcha.java
  70. 44 0
      src/main/java/cn/reghao/dfs/store/util/image/ImageUtil.java
  71. 44 0
      src/main/java/cn/reghao/dfs/store/util/image/QrCode.java
  72. 110 0
      src/main/java/cn/reghao/dfs/store/util/media/ImageOps.java
  73. 136 0
      src/main/java/cn/reghao/dfs/store/util/media/VideoOps.java
  74. 61 0
      src/main/java/cn/reghao/dfs/store/util/redis/RedisConfig.java
  75. 19 0
      src/main/java/cn/reghao/dfs/store/util/redis/RedisKeys.java
  76. 36 0
      src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisHash.java
  77. 35 0
      src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisKey.java
  78. 22 0
      src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisList.java
  79. 61 0
      src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisSet.java
  80. 36 0
      src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisString.java
  81. 31 0
      src/main/resources/application-dev.yml
  82. 28 0
      src/main/resources/application.yml
  83. 68 0
      src/main/resources/logback-spring.xml
  84. 37 0
      src/main/resources/mapper/FileInfoMapper.xml
  85. 19 0
      src/main/resources/mapper/FileUrlMapper.xml
  86. 28 0
      src/main/resources/mapper/FileUserMapper.xml
  87. 31 0
      src/main/resources/mapper/ImageFileMapper.xml
  88. 27 0
      src/main/resources/mapper/VideoFileMapper.xml
  89. 9 0
      src/test/java/FileTest.java

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+.idea/
+*target*/
+*logs*/
+*.jar
+*.iml

+ 10 - 0
Dockerfile

@@ -0,0 +1,10 @@
+#FROM adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.15_10
+FROM adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.15_10
+
+WORKDIR /app
+RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
+# TODO 应该挂载到宿主机目录
+RUN mkdir -p /var/tmp/tnb
+COPY target/tnb-file.jar /app/tnb-file.jar
+
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/tnb-file.jar"]

+ 221 - 0
pom.xml

@@ -0,0 +1,221 @@
+<?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>cn.reghao.dfs</groupId>
+    <artifactId>dfs-store</artifactId>
+    <version>1.0.0</version>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.8.RELEASE</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.reghao.jutil</groupId>
+            <artifactId>jdk</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.reghao.jutil</groupId>
+            <artifactId>tool</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.reghao.dfs</groupId>
+            <artifactId>dfs-api</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.dubbo</groupId>
+            <artifactId>dubbo-spring-boot-starter</artifactId>
+            <version>2.7.8</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-framework</artifactId>
+            <version>4.0.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-recipes</artifactId>
+            <version>4.0.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.zookeeper</groupId>
+            <artifactId>zookeeper</artifactId>
+            <version>3.4.6</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>1.3.2</version>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>8.0.17</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zaxxer</groupId>
+            <artifactId>HikariCP</artifactId>
+            <version>3.3.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>31.1-jre</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>3.3.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv</artifactId>
+            <version>1.5.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacpp</artifactId>
+            <version>1.5.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>ffmpeg</artifactId>
+            <version>4.2.1-1.5.2</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <profile.active>dev</profile.active>
+            </properties>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+        <profile>
+            <id>test</id>
+            <properties>
+                <profile.active>test</profile.active>
+            </properties>
+        </profile>
+        <profile>
+            <id>prod</id>
+            <properties>
+                <profile.active>prod</profile.active>
+            </properties>
+        </profile>
+    </profiles>
+
+    <build>
+        <finalName>tnb-file</finalName>
+        <extensions>
+            <extension>
+                <groupId>kr.motd.maven</groupId>
+                <artifactId>os-maven-plugin</artifactId>
+                <version>1.5.0.Final</version>
+            </extension>
+        </extensions>
+
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>application.yml</include>
+                    <include>application-${profile.active}.yml</include>
+                    <include>mapper/**</include>
+                    <include>*.xml</include>
+                </includes>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <executable>true</executable>
+                    <fork>true</fork>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 11 - 0
src/main/java/cn/reghao/dfs/store/DfsStoreApplication.java

@@ -0,0 +1,11 @@
+package cn.reghao.dfs.store;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class DfsStoreApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(DfsStoreApplication.class, args);
+    }
+}

+ 30 - 0
src/main/java/cn/reghao/dfs/store/config/BaseDocument.java

@@ -0,0 +1,30 @@
+package cn.reghao.dfs.store.config;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @author reghao
+ * @date 2019-10-18 14:42:48
+ */
+@Data
+public class BaseDocument implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @Id
+    private String id;
+    // 逻辑删除
+    private Boolean isDelete;
+
+    protected LocalDateTime createTime;
+    protected LocalDateTime updateTime;
+
+    public BaseDocument() {
+        this.isDelete = false;
+        LocalDateTime now = LocalDateTime.now();
+        this.createTime = now;
+        this.updateTime = now;
+    }
+}

+ 37 - 0
src/main/java/cn/reghao/dfs/store/config/ConfigMap.java

@@ -0,0 +1,37 @@
+package cn.reghao.dfs.store.config;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2022-03-22 16:49:08
+ */
+public class ConfigMap {
+    public static String baseUrl;
+    private static final Map<String, DfsConfig> map = new HashMap<>();
+
+    public static void init(List<DfsConfig> list) {
+        list.forEach(dfsConfig -> {
+            String baseDir = dfsConfig.getBaseDir();
+            map.putIfAbsent(baseDir, dfsConfig);
+        });
+    }
+
+    public static List<String> baseDirs() {
+        return new ArrayList<>(map.keySet());
+    }
+
+    public static PathUrl getPathUrl(String baseDir, String filePath) {
+        String relativePath = filePath.replace(baseDir, "");
+        DfsConfig dfsConfig = map.get(baseDir);
+        String group = dfsConfig.getGroup();
+        String node = dfsConfig.getNode();
+
+        String url = String.format("%s%s/%s/%s", baseUrl, group, node, relativePath);
+        String path = String.format("/%s/%s/%s", group, node, relativePath);
+        return new PathUrl(filePath, url, path);
+    }
+}

+ 35 - 0
src/main/java/cn/reghao/dfs/store/config/DfsConfig.java

@@ -0,0 +1,35 @@
+package cn.reghao.dfs.store.config;
+
+/**
+ * @author reghao
+ * @date 2022-03-22 16:35:28
+ */
+public class DfsConfig {
+    private String baseDir;
+    private String group;
+    private String node;
+
+    public void setBaseDir(String baseDir) {
+        this.baseDir = baseDir;
+    }
+
+    public String getBaseDir() {
+        return baseDir;
+    }
+
+    public void setGroup(String group) {
+        this.group = group;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+
+    public void setNode(String node) {
+        this.node = node;
+    }
+
+    public String getNode() {
+        return node;
+    }
+}

+ 33 - 0
src/main/java/cn/reghao/dfs/store/config/DfsProperties.java

@@ -0,0 +1,33 @@
+package cn.reghao.dfs.store.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-12-30 11:01:46
+ */
+@Component
+@ConfigurationProperties(prefix = "dfs")
+public class DfsProperties {
+    private String domain;
+    private List<DfsConfig> dfsConfigs;
+
+    public void setDomain(String domain) {
+        this.domain = domain;
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public void setDfsConfigs(List<DfsConfig> dfsConfigs) {
+        this.dfsConfigs = dfsConfigs;
+    }
+
+    public List<DfsConfig> getDfsConfigs() {
+        return dfsConfigs;
+    }
+}

+ 18 - 0
src/main/java/cn/reghao/dfs/store/config/PathUrl.java

@@ -0,0 +1,18 @@
+package cn.reghao.dfs.store.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 15:10:04
+ */
+@AllArgsConstructor
+@Getter
+public class PathUrl {
+    // 本地文件路径
+    private String filePath;
+    private String url;
+    // 不包括域名的路径
+    private String path;
+}

+ 43 - 0
src/main/java/cn/reghao/dfs/store/config/SwaggerConfig.java

@@ -0,0 +1,43 @@
+package cn.reghao.dfs.store.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+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;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * 配置 Swagger
+ * Swagger 仅在 dev 和 test 环境生效
+ *
+ * @author reghao
+ * @date 2019-05-14 17:01:07
+ */
+@Profile({"dev", "test"})
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+    @Bean
+    public Docket createRestApi() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .apiInfo(apiInfo())
+                .select()
+                .apis(RequestHandlerSelectors.basePackage("cn.reghao.dfs.store.controller"))
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("FileService APIs")
+                .description("")
+                .termsOfServiceUrl("")
+                .version("1.0.0")
+                .build();
+    }
+}

+ 52 - 0
src/main/java/cn/reghao/dfs/store/config/WebConfig.java

@@ -0,0 +1,52 @@
+package cn.reghao.dfs.store.config;
+
+import cn.reghao.dfs.store.inerceptor.StaticFileInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-12-30 12:34:26
+ */
+@Configuration
+public class WebConfig extends WebMvcConfigurationSupport {
+    private final DfsProperties dfsProperties;
+    private final StaticFileInterceptor staticFileInterceptor;
+
+    public WebConfig(DfsProperties dfsProperties, StaticFileInterceptor staticFileInterceptor) {
+        this.dfsProperties = dfsProperties;
+        this.staticFileInterceptor = staticFileInterceptor;
+    }
+
+    @Override
+    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
+        List<DfsConfig> dfsConfigs = dfsProperties.getDfsConfigs();
+        ConfigMap.baseUrl = String.format("//%s/", dfsProperties.getDomain());
+        ConfigMap.init(dfsConfigs);
+        dfsConfigs.forEach(dfsConfig -> {
+            String resourceLocation = String.format("file:%s", dfsConfig.getBaseDir());
+            String pathPattern = String.format("/%s/%s/**", dfsConfig.getGroup(), dfsConfig.getNode());
+            registry.addResourceHandler(pathPattern).addResourceLocations(resourceLocation);
+        });
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(staticFileInterceptor);
+    }
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("*")
+                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
+                .allowCredentials(true)
+                .maxAge(3600)
+                .allowedHeaders("*");
+    }
+}

+ 46 - 0
src/main/java/cn/reghao/dfs/store/controller/FileAutoController.java

@@ -0,0 +1,46 @@
+package cn.reghao.dfs.store.controller;
+
+import cn.reghao.dfs.store.service.FileAutoService;
+import cn.reghao.jutil.jdk.result.WebBody;
+import cn.reghao.dfs.store.model.dto.auto.VideoFileDto;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * TODO 应该记录上传文件的用户 ID,当多个用户上传同一个文件时,也应该记录
+ *
+ * @author reghao
+ * @date 2020-03-11 16:10:16
+ */
+@Slf4j
+@Api(tags = "文件服务上传接口")
+@RestController
+@RequestMapping("/api/file/upload/auto")
+@Deprecated
+public class FileAutoController {
+    private final FileAutoService fileAutoService;
+
+    public FileAutoController(FileAutoService fileAutoService) {
+        this.fileAutoService = fileAutoService;
+    }
+
+    @Deprecated
+    @ApiOperation(value = "自动上传视频文件")
+    @PostMapping(value = "/video", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String uploadVideoFileAuto1(@Validated @RequestBody VideoFileDto videoFileDto) throws Exception {
+        Map<String, String> result = fileAutoService.processVideoFile(videoFileDto);
+
+        Map<String, String> map = new HashMap<>();
+        map.put("sha256sum", videoFileDto.getSha256sum());
+        map.put("videoFileId", result.get("videoFileId"));
+        map.put("sha256sumCover", videoFileDto.getSha256sumCover());
+        map.put("coverFileId", result.get("coverFileId"));
+        return WebBody.success(map);
+    }
+}

+ 30 - 0
src/main/java/cn/reghao/dfs/store/controller/FileController.java

@@ -0,0 +1,30 @@
+package cn.reghao.dfs.store.controller;
+
+import cn.reghao.jutil.jdk.result.WebBody;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author reghao
+ * @date 2020-03-11 16:10:16
+ */
+@Slf4j
+@Api(tags = "文件服务接口")
+@RestController
+@RequestMapping("/api/file")
+public class FileController {
+    @ApiOperation("检查文件是否已存在")
+    @GetMapping("/exist")
+    public String isFileExist(@RequestParam("sha256sum") String sha256sum) {
+        return WebBody.success();
+    }
+
+    @ApiOperation(value = "删除已上传视频的文件")
+    @DeleteMapping(value = "/rm", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteVideoFile(@RequestParam("fileId") String fileId) {
+        return WebBody.success();
+    }
+}

+ 33 - 0
src/main/java/cn/reghao/dfs/store/controller/FileDownloadController.java

@@ -0,0 +1,33 @@
+package cn.reghao.dfs.store.controller;
+
+import cn.reghao.dfs.store.model.dto.DownloadFile;
+import cn.reghao.dfs.store.service.FileDownloadService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.*;
+
+/**
+ * @author reghao
+ * @date 2020-03-11 16:10:16
+ */
+@Slf4j
+@Api(tags = "文件服务下载接口")
+@RestController
+@RequestMapping("/api/file/dl")
+public class FileDownloadController {
+    private final FileDownloadService fileDownloadService;
+
+    public FileDownloadController(FileDownloadService fileDownloadService) {
+        this.fileDownloadService = fileDownloadService;
+    }
+
+    @ApiOperation(value = "下载文件")
+    @GetMapping("/single")
+    public void download(@Validated DownloadFile downloadFile) throws IOException {
+        fileDownloadService.download(downloadFile);
+    }
+}

+ 47 - 0
src/main/java/cn/reghao/dfs/store/controller/FileUploadController.java

@@ -0,0 +1,47 @@
+package cn.reghao.dfs.store.controller;
+
+import cn.reghao.dfs.store.model.dto.*;
+import cn.reghao.jutil.jdk.result.WebBody;
+import cn.reghao.dfs.store.model.dto.*;
+import cn.reghao.dfs.store.service.FileUploadService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author reghao
+ * @date 2020-03-11 16:10:16
+ */
+@Api(tags = "文件服务上传接口")
+@RestController
+@RequestMapping("/api/file/upload")
+public class FileUploadController {
+    private final FileUploadService fileUploadService;
+
+    public FileUploadController(FileUploadService fileUploadService) {
+        this.fileUploadService = fileUploadService;
+    }
+
+    @ApiOperation("文件上传前的准备, 客户端传递文件的 sha256sum, 文件若存在则直接返回文件关联的 id")
+    @PostMapping("/prepare")
+    public String uploadFilePrepare(@RequestBody @Validated UploadPrepare uploadPrepare) {
+        UploadPrepareRet uploadPrepareRet = fileUploadService.prepareUpload(uploadPrepare);
+        return WebBody.success(uploadPrepareRet);
+    }
+
+    @ApiOperation(value = "单个文件上传")
+    @PostMapping(value = "/single", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String uploadFile(@Validated UploadFile uploadFile) throws Exception {
+        fileUploadService.putFile(uploadFile);
+        return WebBody.success(new UploadFileRet(uploadFile.getUploadId(), null));
+    }
+
+    @ApiOperation("分片文件上传")
+    @PostMapping("/part")
+    public String uploadFilePart(@Validated UploadFilePart uploadFilePart) throws Exception {
+        UploadFilePartRet uploadFilePartRet = fileUploadService.putFilePart(uploadFilePart);
+        return WebBody.success(uploadFilePartRet);
+    }
+}

+ 50 - 0
src/main/java/cn/reghao/dfs/store/controller/MediaUploadController.java

@@ -0,0 +1,50 @@
+package cn.reghao.dfs.store.controller;
+
+import cn.reghao.jutil.jdk.result.WebBody;
+import cn.reghao.dfs.store.service.media.AudioFileService;
+import cn.reghao.dfs.store.service.media.ImageFileService;
+import cn.reghao.dfs.store.service.media.VideoFileService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author reghao
+ * @date 2022-04-28 13:56:13
+ */
+@Api(tags = "媒体文件上传接口")
+@RestController
+@RequestMapping("/api/file/upload")
+public class MediaUploadController {
+    private ImageFileService imageService;
+    private AudioFileService audioService;
+    private VideoFileService videoService;
+
+    public MediaUploadController(ImageFileService imageService, AudioFileService audioService,
+                                 VideoFileService videoService) {
+        this.imageService = imageService;
+        this.audioService = audioService;
+        this.videoService = videoService;
+    }
+
+    @ApiOperation(value = "上传图片文件")
+    @PostMapping(value = "/image", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String uploadImageFile() throws Exception {
+        return WebBody.success();
+    }
+
+    @ApiOperation(value = "上传音频文件")
+    @PostMapping(value = "/audio", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String uploadAudioFile() throws Exception {
+        return WebBody.success();
+    }
+
+    @ApiOperation(value = "上传视频文件")
+    @PostMapping(value = "/video", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String uploadVideoFile() throws Exception {
+        return WebBody.success();
+    }
+}

+ 36 - 0
src/main/java/cn/reghao/dfs/store/controller/VideoFileController.java

@@ -0,0 +1,36 @@
+package cn.reghao.dfs.store.controller;
+
+import io.swagger.annotations.ApiOperation;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.FileInputStream;
+
+/**
+ * @author reghao
+ * @date 2022-04-24 16:13:10
+ */
+@RestController
+@RequestMapping("/api/file/video")
+public class VideoFileController {
+    @ApiOperation("检查视频文件是否已存在")
+    @GetMapping("/file")
+    public ResponseEntity<InputStreamResource> isFileExist(@RequestParam("videoId") String videoId) throws Exception {
+        FileInputStream fis = new FileInputStream("/home/reghao/Downloads/mp4/test/0");
+        InputStreamResource inputStreamResource = new InputStreamResource(fis);
+
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.set("Pragma", "No-cache");
+        httpHeaders.set("Cache-Control", "no-cache");
+        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
+                .headers(httpHeaders).contentType(MediaType.parseMediaType("video/mp4"))
+                .body(inputStreamResource);
+    }
+}

+ 19 - 0
src/main/java/cn/reghao/dfs/store/db/mapper/FileInfoMapper.java

@@ -0,0 +1,19 @@
+package cn.reghao.dfs.store.db.mapper;
+
+import cn.reghao.jutil.jdk.db.BaseMapper;
+import cn.reghao.dfs.store.model.dto.FileDto;
+import cn.reghao.dfs.store.model.po.FileInfo;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author reghao
+ * @date 2021-11-22 14:30:12
+ */
+@Mapper
+public interface FileInfoMapper extends BaseMapper<FileInfo> {
+    void updateSetUploaded(String fileId, int fileType, String contentType);
+
+    FileDto findBySha256sum(String sha256sum);
+    FileInfo findFileInfoBySha256sum(String sha256sum);
+    FileInfo findByFileId(String fileId);
+}

+ 15 - 0
src/main/java/cn/reghao/dfs/store/db/mapper/FileUrlMapper.java

@@ -0,0 +1,15 @@
+package cn.reghao.dfs.store.db.mapper;
+
+import cn.reghao.dfs.store.model.dto.FileUrlDto;
+import cn.reghao.dfs.store.model.po.FileUrl;
+import cn.reghao.jutil.jdk.db.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author reghao
+ * @date 2021-12-08 14:20:12
+ */
+@Mapper
+public interface FileUrlMapper extends BaseMapper<FileUrl> {
+    FileUrlDto findByUploadId(String uploadId);
+}

+ 18 - 0
src/main/java/cn/reghao/dfs/store/db/mapper/FileUserMapper.java

@@ -0,0 +1,18 @@
+package cn.reghao.dfs.store.db.mapper;
+
+import cn.reghao.dfs.store.model.po.FileInfo;
+import cn.reghao.dfs.store.model.po.FileUser;
+import cn.reghao.jutil.jdk.db.BaseMapper;
+import cn.reghao.dfs.api.dto.FileInfoDto;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 20:05:30
+ */
+@Mapper
+public interface FileUserMapper extends BaseMapper<FileUser> {
+    FileInfoDto findByUploadId(String uploadId);
+    FileUser findByFileAndUserId(String fileId, long userId);
+    FileInfo findFileInfoByUploadId(String uploadId);
+}

+ 20 - 0
src/main/java/cn/reghao/dfs/store/db/mapper/ImageFileMapper.java

@@ -0,0 +1,20 @@
+package cn.reghao.dfs.store.db.mapper;
+
+import cn.reghao.dfs.store.model.po.ImageFile;
+import cn.reghao.jutil.jdk.db.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-12-08 14:41:35
+ */
+@Mapper
+public interface ImageFileMapper extends BaseMapper<ImageFile> {
+    @Deprecated
+    void updateSetUrl(ImageFile imageFile);
+
+    ImageFile findByFileId(String fileId);
+    List<ImageFile> findByFileIds(List<String> list);
+}

+ 22 - 0
src/main/java/cn/reghao/dfs/store/db/mapper/VideoFileMapper.java

@@ -0,0 +1,22 @@
+package cn.reghao.dfs.store.db.mapper;
+
+import cn.reghao.dfs.store.model.po.VideoFile;
+import cn.reghao.jutil.jdk.db.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-12-07 18:59:11
+ */
+@Mapper
+public interface VideoFileMapper extends BaseMapper<VideoFile> {
+    void updateSetCover(@Param("fileId") String fileId, @Param("coverUrl") String coverUrl);
+    @Deprecated
+    void updateSetUrl(VideoFile videoFile);
+
+    VideoFile findByFileId(String fileId);
+    List<VideoFile> findByMatchBaseUrl(String pattern);
+}

+ 94 - 0
src/main/java/cn/reghao/dfs/store/db/repository/FileRepository.java

@@ -0,0 +1,94 @@
+package cn.reghao.dfs.store.db.repository;
+
+import cn.reghao.dfs.store.config.PathUrl;
+import cn.reghao.dfs.store.db.mapper.FileInfoMapper;
+import cn.reghao.dfs.store.db.mapper.FileUrlMapper;
+import cn.reghao.dfs.store.db.mapper.FileUserMapper;
+import cn.reghao.dfs.store.model.dto.FileUrlDto;
+import cn.reghao.dfs.store.model.dto.UploadPrepare;
+import cn.reghao.dfs.store.model.po.FileInfo;
+import cn.reghao.dfs.store.model.po.FileUrl;
+import cn.reghao.dfs.store.model.po.FileUser;
+import cn.reghao.dfs.store.service.FileContentType;
+import cn.reghao.dfs.store.util.StringUtil;
+import cn.reghao.jutil.tool.id.IdGenerator;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 15:46:08
+ */
+@Repository
+public class FileRepository {
+    private final IdGenerator idGenerator;
+    private final FileInfoMapper fileInfoMapper;
+    private final FileUrlMapper fileUrlMapper;
+    private final FileUserMapper fileUserMapper;
+
+    public FileRepository(FileInfoMapper fileInfoMapper, FileUrlMapper fileUrlMapper, FileUserMapper fileUserMapper) {
+        this.idGenerator = new IdGenerator("file-id");
+        this.fileInfoMapper = fileInfoMapper;
+        this.fileUrlMapper = fileUrlMapper;
+        this.fileUserMapper = fileUserMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public String createFileUploadId(UploadPrepare uploadPrepare) {
+        String filename = uploadPrepare.getFileName();
+        long size = uploadPrepare.getFileSize();
+        String sha256sum = uploadPrepare.getFileSha256sum();
+        String fileId = idGenerator.getUuid();
+        String suffix = StringUtil.getSuffix(filename);
+        FileInfo fileInfo = new FileInfo(fileId, sha256sum, filename, suffix, size);
+
+        String uploadId = idGenerator.getUuid();
+        long currentUserId = 10000L;
+        FileUser fileUser = new FileUser(uploadId, currentUserId, fileId);
+
+        fileInfoMapper.save(fileInfo);
+        fileUserMapper.save(fileUser);
+        return uploadId;
+    }
+
+    public String getOrCreateUploadId(String fileId) {
+        long currentUserId = 10000L;
+        FileUser fileUser = fileUserMapper.findByFileAndUserId(fileId, currentUserId);
+        if (fileUser != null) {
+            return fileUser.getUploadId();
+        } else {
+            String uploadId = idGenerator.getUuid();
+            fileUser = new FileUser(uploadId, currentUserId, fileId);
+            fileUserMapper.save(fileUser);
+            return uploadId;
+        }
+    }
+
+    public FileInfo getFileInfo(String sha256sum) {
+        return fileInfoMapper.findFileInfoBySha256sum(sha256sum);
+    }
+
+    public FileInfo getFileInfoByUploadId(String uploadId) {
+        return fileUserMapper.findFileInfoByUploadId(uploadId);
+    }
+
+    public FileInfo getFileInfoByFileId(String uploadId) {
+        return fileUserMapper.findFileInfoByUploadId(uploadId);
+    }
+
+    public FileUrlDto findFilePath(String uploadId) {
+        return fileUrlMapper.findByUploadId(uploadId);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void saveFile(String fileId, FileContentType fileContentType, PathUrl pathUrl) {
+        int fileType = fileContentType.getFileType();
+        String contentType = fileContentType.getContentType();
+        FileUrl fileUrl = new FileUrl(fileId, pathUrl.getFilePath(), pathUrl.getUrl(), pathUrl.getFilePath());
+
+        // TODO 文件上传完成后, 立即同步到其它节点
+        // TODO 事务回滚时会在 FileStore 中产生碎片文件,即在数据库中找不到该文件的任何信息
+        fileInfoMapper.updateSetUploaded(fileId, fileType, contentType);
+        fileUrlMapper.save(fileUrl);
+    }
+}

+ 52 - 0
src/main/java/cn/reghao/dfs/store/inerceptor/StaticFileInterceptor.java

@@ -0,0 +1,52 @@
+package cn.reghao.dfs.store.inerceptor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @author reghao
+ * @date 2021-12-30 12:19:07
+ */
+@Slf4j
+@Component
+public class StaticFileInterceptor implements HandlerInterceptor {
+    // TODO 添加防盗链处理
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+            throws Exception {
+        String uri = request.getRequestURI();
+        String method = request.getMethod();
+        if ("/api/file/upload/video/auto".equals(uri)) {
+            //Jwt.check();
+        }
+
+        String userAgent = request.getHeader("user-agent");
+        String ipv4 = request.getRemoteAddr();
+
+        String referer = request.getHeader("referer");
+        String sign = request.getParameter("sign");
+        return true;
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response,
+                           Object handler, @Nullable ModelAndView modelAndView) throws Exception {
+
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+                                Object handler, @Nullable Exception ex) throws Exception {
+        String uri = request.getRequestURI();
+        if (!uri.startsWith("/api")) {
+            int statusCode = response.getStatus();
+            log.info("{} -> {}", uri, statusCode);
+        }
+    }
+}

+ 22 - 0
src/main/java/cn/reghao/dfs/store/model/dto/DownloadFile.java

@@ -0,0 +1,22 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 19:51:17
+ */
+@Setter
+@Getter
+public class DownloadFile implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank
+    private String uploadId;
+    @NotBlank
+    private String token;
+}

+ 28 - 0
src/main/java/cn/reghao/dfs/store/model/dto/FileDto.java

@@ -0,0 +1,28 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-12-06 00:32:03
+ */
+@NoArgsConstructor
+@Getter
+public class FileDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String fileId;
+    private String filename;
+    private String filePath;
+    private String url;
+
+    public FileDto(String fileId, String filename, String filePath, String url) {
+        this.fileId = fileId;
+        this.filename = filename;
+        this.filePath = filePath;
+        this.url = url;
+    }
+}

+ 19 - 0
src/main/java/cn/reghao/dfs/store/model/dto/FileInfoDto.java

@@ -0,0 +1,19 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-12-06 00:32:03
+ */
+@Getter
+public class FileInfoDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String fileId;
+    private String filename;
+    private String url;
+    private byte[] bytes;
+}

+ 19 - 0
src/main/java/cn/reghao/dfs/store/model/dto/FileUrlDto.java

@@ -0,0 +1,19 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 10:59:16
+ */
+@Getter
+public class FileUrlDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String fileId;
+    private String filename;
+    private String path;
+    private String url;
+}

+ 20 - 0
src/main/java/cn/reghao/dfs/store/model/dto/ImgFileRetDto.java

@@ -0,0 +1,20 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2021-12-05 22:04:05
+ */
+@Getter
+@Setter
+public class ImgFileRetDto {
+    private String fileId;
+    private String url;
+
+    public ImgFileRetDto(String fileId, String url) {
+        this.fileId = fileId;
+        this.url = url;
+    }
+}

+ 24 - 0
src/main/java/cn/reghao/dfs/store/model/dto/UploadFile.java

@@ -0,0 +1,24 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 15:22:49
+ */
+@Getter
+@Setter
+public class UploadFile implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank
+    private String uploadId;
+    @NotNull
+    private MultipartFile file;
+}

+ 33 - 0
src/main/java/cn/reghao/dfs/store/model/dto/UploadFilePart.java

@@ -0,0 +1,33 @@
+package cn.reghao.dfs.store.model.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-25 10:42:38
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class UploadFilePart implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotNull
+    private MultipartFile file;
+    @NotBlank
+    private String sha256sum;
+    @NotBlank
+    private String uploadId;
+    @NotNull
+    private Integer splitIndex;
+    @NotNull
+    private Integer splitNum;
+}

+ 13 - 0
src/main/java/cn/reghao/dfs/store/model/dto/UploadFilePartRet.java

@@ -0,0 +1,13 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.AllArgsConstructor;
+
+/**
+ * @author reghao
+ * @date 2022-04-21 09:32:00
+ */
+@AllArgsConstructor
+public class UploadFilePartRet {
+    private String uploadId;
+    private boolean isMerge;
+}

+ 17 - 0
src/main/java/cn/reghao/dfs/store/model/dto/UploadFileRet.java

@@ -0,0 +1,17 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.AllArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 15:22:49
+ */
+@AllArgsConstructor
+public class UploadFileRet implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String uploadId;
+    private String url;
+}

+ 30 - 0
src/main/java/cn/reghao/dfs/store/model/dto/UploadPrepare.java

@@ -0,0 +1,30 @@
+package cn.reghao.dfs.store.model.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 分片文件
+ *
+ * @author reghao
+ * @date 2021-11-23 10:23:00
+ */
+@Setter
+@Getter
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class UploadPrepare implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank
+    private String fileName;
+    @NotNull
+    private Long fileSize;
+    @NotNull
+    private String fileSha256sum;
+}

+ 14 - 0
src/main/java/cn/reghao/dfs/store/model/dto/UploadPrepareRet.java

@@ -0,0 +1,14 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.AllArgsConstructor;
+
+/**
+ * @author reghao
+ * @date 2022-04-21 09:27:55
+ */
+@AllArgsConstructor
+public class UploadPrepareRet {
+    private String uploadId;
+    private long splitSize;
+    private boolean exist;
+}

+ 28 - 0
src/main/java/cn/reghao/dfs/store/model/dto/VidFileRetDto.java

@@ -0,0 +1,28 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2021-12-05 22:04:05
+ */
+@Getter
+@Setter
+public class VidFileRetDto {
+    private String fileId;
+    // 上传时自行获取本地文件的名字
+    @Deprecated
+    private String filename;
+    private String coverUrl;
+    private int duration;
+    private String videoId;
+
+    public VidFileRetDto(String fileId, String filename, String coverUrl, int duration, String videoId) {
+        this.fileId = fileId;
+        this.filename = filename;
+        this.coverUrl = coverUrl;
+        this.duration = duration;
+        this.videoId = videoId;
+    }
+}

+ 30 - 0
src/main/java/cn/reghao/dfs/store/model/dto/VidFileSbtDto.java

@@ -0,0 +1,30 @@
+package cn.reghao.dfs.store.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-12-05 22:04:05
+ */
+@Getter
+@Setter
+public class VidFileSbtDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank
+    private String sha256sum;
+    @NotNull
+    private Integer width;
+    @NotNull
+    private Integer height;
+    @NotNull
+    private Double fps;
+    @NotNull
+    private Integer duration;
+    private String rotate;
+}

+ 58 - 0
src/main/java/cn/reghao/dfs/store/model/dto/auto/VideoFileDto.java

@@ -0,0 +1,58 @@
+package cn.reghao.dfs.store.model.dto.auto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2022-03-24 13:04:05
+ */
+@Getter
+@Setter
+public class VideoFileDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank
+    private String sha256sum;
+    @NotBlank
+    private String filename;
+    @NotNull
+    private Long size;
+    @NotBlank
+    private String fileUrl;
+    @NotNull
+    private Integer width;
+    @NotNull
+    private Integer height;
+    @NotNull
+    private Double fps;
+    @NotNull
+    private Integer duration;
+    private String rotate;
+
+    @NotBlank
+    private String sha256sumCover;
+    @NotBlank
+    private String coverUrl;
+    @NotNull
+    private Long imgSize;
+    @NotNull
+    private Integer imgWidth;
+    @NotNull
+    private Integer imgHeight;
+
+    @NotBlank
+    private String title;
+    @NotBlank
+    private String tags;
+    @NotNull
+    private Integer categoryPid;
+    @NotNull
+    private Integer categoryId;
+    @NotNull
+    private Long userId;
+}

+ 22 - 0
src/main/java/cn/reghao/dfs/store/model/po/AudioFile.java

@@ -0,0 +1,22 @@
+package cn.reghao.dfs.store.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+/**
+ * 音频文件
+ *
+ * @author reghao
+ * @date 2022-04-26 16:44:38
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AudioFile extends BaseObject<Integer> {
+    private String fileId;
+    private String baseUrl;
+}

+ 36 - 0
src/main/java/cn/reghao/dfs/store/model/po/FileInfo.java

@@ -0,0 +1,36 @@
+package cn.reghao.dfs.store.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2021-11-22 09:57:23
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Setter
+@Getter
+public class FileInfo extends BaseObject<Integer> {
+    private String fileId;
+    private String sha256sum;
+    private String filename;
+    private String suffix;
+    private Long size;
+    private int fileType;
+    // http content-type
+    private String contentType;
+    private Boolean uploaded;
+
+    public FileInfo(String fileId, String sha256sum, String filename, String suffix, long size) {
+        this.fileId = fileId;
+        this.sha256sum = sha256sum;
+        this.filename = filename;
+        this.suffix = suffix;
+        this.size = size;
+        this.uploaded = false;
+    }
+}

+ 31 - 0
src/main/java/cn/reghao/dfs/store/model/po/FileUrl.java

@@ -0,0 +1,31 @@
+package cn.reghao.dfs.store.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2021-12-08 13:57:23
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+public class FileUrl extends BaseObject<Integer> {
+    private String fileId;
+    private String filePath;
+    private String url;
+    private String path;
+    private String dcId;
+    private String nodeId;
+
+    public FileUrl(String fileId, String filePath, String url, String path) {
+        this.fileId = fileId;
+        this.filePath = filePath;
+        this.url = url;
+        this.path = path;
+    }
+}

+ 19 - 0
src/main/java/cn/reghao/dfs/store/model/po/FileUser.java

@@ -0,0 +1,19 @@
+package cn.reghao.dfs.store.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 20:04:50
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+public class FileUser extends BaseObject<Integer> {
+    private String uploadId;
+    private Long uploadBy;
+    private String fileId;
+}

+ 32 - 0
src/main/java/cn/reghao/dfs/store/model/po/ImageFile.java

@@ -0,0 +1,32 @@
+package cn.reghao.dfs.store.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import cn.reghao.dfs.api.dto.ImageFileDto;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+/**
+ * 图像文件
+ *
+ * @author reghao
+ * @date 2021-12-08 13:52:01
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ImageFile extends BaseObject<Integer> {
+    private String fileId;
+    private String baseUrl;
+    private Integer width;
+    private Integer height;
+
+    public ImageFile(ImageFileDto imageFileDto) {
+        this.fileId = imageFileDto.getFileId();
+        this.baseUrl = imageFileDto.getUrl();
+        this.width = imageFileDto.getWidth();
+        this.height = imageFileDto.getHeight();
+    }
+}

+ 51 - 0
src/main/java/cn/reghao/dfs/store/model/po/VideoFile.java

@@ -0,0 +1,51 @@
+package cn.reghao.dfs.store.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import cn.reghao.dfs.api.dto.VideoFileDto;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * 视频文件
+ *
+ * @author reghao
+ * @date 2021-11-22 10:21:15
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+public class VideoFile extends BaseObject<Integer> {
+    // TODO 后期提供一个 fileId List 表示其他分辨率的视频文件
+    private String fileId;
+    private Integer bandwidth;
+    private Double frameRate;
+    private Integer width;
+    private Integer height;
+    private Boolean horizontal;
+    private String rotate;
+    private String urlType;
+    private String baseUrl;
+    private String backupUrl;
+    // 单位秒
+    private Integer duration;
+    private String baseCoverUrl;
+
+    public VideoFile(String fileId, String urlType, String baseUrl) {
+        this.fileId = fileId;
+        this.urlType = urlType;
+        this.baseUrl = baseUrl;
+    }
+
+    public VideoFile(VideoFileDto videoFileDto) {
+        this.fileId = videoFileDto.getVideoId();
+        this.frameRate = videoFileDto.getFrameRate();
+        this.width = videoFileDto.getWidth();
+        this.height = videoFileDto.getHeight();
+        this.horizontal = width > height;
+        this.urlType = videoFileDto.getUrlType();
+        this.baseUrl = videoFileDto.getVideoUrl();
+        this.duration = videoFileDto.getDuration();
+        this.baseCoverUrl = videoFileDto.getCoverUrl();
+    }
+}

+ 26 - 0
src/main/java/cn/reghao/dfs/store/rpc/FileInfoServiceImpl.java

@@ -0,0 +1,26 @@
+package cn.reghao.dfs.store.rpc;
+
+import cn.reghao.dfs.api.dto.FileInfoDto;
+import cn.reghao.dfs.api.iface.FileInfoService;
+import cn.reghao.dfs.store.db.mapper.FileUserMapper;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author reghao
+ * @date 2022-04-25 17:01:08
+ */
+@DubboService
+@Service
+public class FileInfoServiceImpl implements FileInfoService {
+    private final FileUserMapper fileUserMapper;
+
+    public FileInfoServiceImpl(FileUserMapper fileUserMapper) {
+        this.fileUserMapper = fileUserMapper;
+    }
+
+    @Override
+    public FileInfoDto getFileInfo(String uploadId) {
+        return fileUserMapper.findByUploadId(uploadId);
+    }
+}

+ 80 - 0
src/main/java/cn/reghao/dfs/store/rpc/VideoFileServiceImpl.java

@@ -0,0 +1,80 @@
+package cn.reghao.dfs.store.rpc;
+
+import cn.reghao.dfs.store.db.mapper.FileInfoMapper;
+import cn.reghao.dfs.store.db.mapper.FileUrlMapper;
+import cn.reghao.dfs.store.db.mapper.ImageFileMapper;
+import cn.reghao.dfs.store.db.mapper.VideoFileMapper;
+import cn.reghao.dfs.store.model.po.FileInfo;
+import cn.reghao.dfs.store.model.po.FileUrl;
+import cn.reghao.dfs.store.model.po.ImageFile;
+import cn.reghao.dfs.store.model.po.VideoFile;
+import cn.reghao.dfs.store.util.StringUtil;
+import cn.reghao.jutil.jdk.http.util.UrlFormatter;
+import cn.reghao.jutil.tool.id.SnowFlake;
+import cn.reghao.dfs.api.dto.ImageFileDto;
+import cn.reghao.dfs.api.dto.VideoFileDto;
+import cn.reghao.dfs.api.iface.VideoFileService;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-12-31 11:26:21
+ */
+@DubboService
+@Service
+public class VideoFileServiceImpl implements VideoFileService {
+    private final SnowFlake snowFlake;
+    private final FileInfoMapper fileInfoMapper;
+    private final FileUrlMapper fileUrlMapper;
+    private final VideoFileMapper videoFileMapper;
+    private final ImageFileMapper imageFileMapper;
+
+    public VideoFileServiceImpl(FileInfoMapper fileInfoMapper, FileUrlMapper fileUrlMapper,
+                                VideoFileMapper videoFileMapper, ImageFileMapper imageFileMapper) {
+        this.snowFlake = new SnowFlake(1, 1);
+        this.fileInfoMapper = fileInfoMapper;
+        this.fileUrlMapper = fileUrlMapper;
+        this.videoFileMapper = videoFileMapper;
+        this.imageFileMapper = imageFileMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public String addVideoFile(VideoFileDto videoFileDto) {
+        Long userId = videoFileDto.getUserId();
+        String videoId = videoFileDto.getVideoId();
+        String videoUrl = videoFileDto.getVideoUrl();
+
+        FileInfo fileInfo = fileInfoMapper.findByFileId(videoId);
+        if (fileInfo != null) {
+            return fileInfo.getFileId();
+        }
+
+        String filename = UrlFormatter.getFilename(videoUrl);
+        String suffix = StringUtil.getSuffix(filename);
+        String contentType = videoFileDto.getUrlType();
+
+        String sha256sum = String.valueOf(snowFlake.nextId());
+        fileInfo = new FileInfo();
+        FileUrl fileUrl = new FileUrl(videoId, "", videoUrl, "");
+        VideoFile videoFile = new VideoFile(videoFileDto);
+
+        fileInfoMapper.save(fileInfo);
+        fileUrlMapper.save(fileUrl);
+        videoFileMapper.save(videoFile);
+        return fileInfo.getFileId();
+    }
+
+    @Override
+    public void addImageFile(List<ImageFileDto> list) {
+        List<ImageFile> list1 = list.stream().map(ImageFile::new).collect(Collectors.toList());
+        if (!list1.isEmpty()) {
+            imageFileMapper.saveAll(list1);
+        }
+    }
+}

+ 120 - 0
src/main/java/cn/reghao/dfs/store/service/FileAutoService.java

@@ -0,0 +1,120 @@
+package cn.reghao.dfs.store.service;
+
+import cn.reghao.dfs.store.model.dto.FileDto;
+import cn.reghao.jutil.tool.id.IdGenerator;
+import cn.reghao.dfs.api.constant.FileType;
+import cn.reghao.dfs.api.constant.VideoUrlType;
+import cn.reghao.dfs.store.db.mapper.FileInfoMapper;
+import cn.reghao.dfs.store.db.mapper.FileUrlMapper;
+import cn.reghao.dfs.store.db.mapper.ImageFileMapper;
+import cn.reghao.dfs.store.db.mapper.VideoFileMapper;
+import cn.reghao.dfs.store.model.dto.*;
+import cn.reghao.dfs.store.model.dto.auto.VideoFileDto;
+import cn.reghao.dfs.store.model.po.FileInfo;
+import cn.reghao.dfs.store.model.po.FileUrl;
+import cn.reghao.dfs.store.model.po.ImageFile;
+import cn.reghao.dfs.store.model.po.VideoFile;
+import cn.reghao.dfs.store.util.StringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-08-04 17:29:47
+ */
+@Slf4j
+@Service
+@Deprecated
+public class FileAutoService {
+    private final IdGenerator idGenerator;
+    private final FileInfoMapper fileInfoMapper;
+    private final FileUrlMapper fileUrlMapper;
+    private final VideoFileMapper videoFileMapper;
+    private final ImageFileMapper imageFileMapper;
+
+    public FileAutoService(FileInfoMapper fileInfoMapper, FileUrlMapper fileUrlMapper,
+                           VideoFileMapper videoFileMapper, ImageFileMapper imageFileMapper) {
+        this.idGenerator = new IdGenerator("file-id");
+        this.fileInfoMapper = fileInfoMapper;
+        this.fileUrlMapper = fileUrlMapper;
+        this.videoFileMapper = videoFileMapper;
+        this.imageFileMapper = imageFileMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, String> processVideoFile(VideoFileDto videoFileDto) throws Exception {
+        String sha256sum = videoFileDto.getSha256sum();
+        if (fileInfoMapper.findBySha256sum(sha256sum) != null) {
+            String msg = String.format("%s exists", sha256sum);
+            throw new Exception(msg);
+        }
+
+        String filename = videoFileDto.getFilename();
+        Long size = videoFileDto.getSize();
+        String fileUrl = videoFileDto.getFileUrl();
+        String videoFileType = FileType.mp4.getContentType();
+        FileDto fileDto = saveFile(sha256sum, filename, size, 3, fileUrl);
+        String videoFileId = fileDto.getFileId();
+        String videoUrl = fileDto.getUrl();
+
+        String sha256sumCover = videoFileDto.getSha256sumCover();
+        String coverUrl = videoFileDto.getCoverUrl();
+        long imgSize = videoFileDto.getImgSize();
+        int imgWidth = videoFileDto.getImgWidth();
+        int imgHeight = videoFileDto.getImgHeight();
+        String imgFileType = FileType.jpg.getContentType();
+        FileDto fileDto1 = saveFile(sha256sumCover, "default.jpg", imgSize, 1, coverUrl);
+        saveVideoCover(fileDto1.getFileId(), fileDto1.getUrl(), imgWidth, imgHeight);
+        String coverFileId = fileDto1.getFileId();
+
+        VideoFile videoFile = new VideoFile(videoFileId, VideoUrlType.mp4.getName(), videoUrl);
+        videoFile.setWidth(videoFileDto.getWidth());
+        videoFile.setHeight(videoFileDto.getHeight());
+        videoFile.setHorizontal(videoFileDto.getWidth() >= videoFileDto.getHeight());
+        videoFile.setFrameRate(videoFileDto.getFps());
+        videoFile.setDuration(videoFileDto.getDuration());
+        videoFile.setRotate(videoFileDto.getRotate());
+        videoFile.setBaseCoverUrl(fileDto1.getUrl());
+        videoFileMapper.save(videoFile);
+
+        addVideoPost(videoFile.getFileId(), videoFileDto);
+        Map<String, String> map = new HashMap<>();
+        map.put("videoFileId", videoFileId);
+        map.put("coverFileId", coverFileId);
+        return  map;
+    }
+
+    public void saveVideoCover(String fileId, String url, int width, int height) {
+        ImageFile imageFile = new ImageFile(fileId, url, width, height);
+        imageFileMapper.save(imageFile);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public FileDto saveFile(String sha256sum, String filename, long size, int fileType, String fileUrl) {
+        String suffix = StringUtil.getSuffix(filename);
+        String fileId = idGenerator.getUuid();
+        String url = fileUrl.substring(0, fileUrl.lastIndexOf("/")+1) + fileId + suffix;
+        String url1 = "//static.reghao.cn/group0/node0" + url;
+
+        FileInfo fileInfo = new FileInfo();
+        FileUrl fileUrl1 = new FileUrl(fileId, "", url1, "");
+        fileInfoMapper.save(fileInfo);
+        fileUrlMapper.save(fileUrl1);
+        return new FileDto(fileId, "", "", url1);
+    }
+
+    public void addVideoPost(String fileId, VideoFileDto videoFileDto) {
+        String title = videoFileDto.getTitle();
+        String tags = videoFileDto.getTags();
+        int categoryPid = videoFileDto.getCategoryPid();
+        int categoryId = videoFileDto.getCategoryId();
+        long userId = videoFileDto.getUserId();
+
+        //VideoAutoPostDto videoAutoPostDto = new VideoAutoPostDto(fileId, userId, title, tags, categoryPid, categoryId);
+        //videoPostService.createAndPublish(videoAutoPostDto);
+    }
+}

+ 15 - 0
src/main/java/cn/reghao/dfs/store/service/FileContentType.java

@@ -0,0 +1,15 @@
+package cn.reghao.dfs.store.service;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 17:08:19
+ */
+@AllArgsConstructor
+@Getter
+public class FileContentType {
+    private int fileType;
+    private String contentType;
+}

+ 70 - 0
src/main/java/cn/reghao/dfs/store/service/FileDownloadService.java

@@ -0,0 +1,70 @@
+package cn.reghao.dfs.store.service;
+
+import cn.reghao.dfs.store.util.ServletUtil;
+import cn.reghao.dfs.store.db.repository.FileRepository;
+import cn.reghao.dfs.store.model.dto.DownloadFile;
+import cn.reghao.dfs.store.model.dto.FileUrlDto;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author reghao
+ * @date 2021-06-04 10:18:11
+ */
+@Service
+public class FileDownloadService {
+    private final FileRepository fileRepository;
+
+    public FileDownloadService(FileRepository fileRepository) {
+        this.fileRepository = fileRepository;
+    }
+
+    public void download(DownloadFile downloadFile) throws IOException {
+        String token = downloadFile.getToken();
+        if (token == null) {
+            // TODO 返回 403
+        }
+
+        String uploadId = downloadFile.getUploadId();
+        FileUrlDto fileUrlDto = fileRepository.findFilePath(uploadId);
+        if (fileUrlDto == null) {
+            return;
+        }
+
+        String filename = fileUrlDto.getFilename();
+        String path = fileUrlDto.getPath();
+        download(filename, new File(path));
+    }
+
+    public void download(String filename, File file) throws IOException {
+        HttpServletResponse response = ServletUtil.getResponse();
+        prepareResponse(filename, response);
+
+        FileInputStream fis = new FileInputStream(file);
+        BufferedInputStream bis = new BufferedInputStream(fis);
+        OutputStream os = response.getOutputStream();
+        download(bis, os);
+    }
+
+    private void prepareResponse(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
+        response.setHeader("content-type", "application/octet-stream");
+        response.setContentType("application/octet-stream");
+        response.setHeader("Content-Disposition",
+                "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()));
+    }
+
+    private void download(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[1024];
+        int i = in.read(buffer);
+        while (i != -1) {
+            out.write(buffer, 0, i);
+            i = in.read(buffer);
+        }
+        out.close();
+        in.close();
+    }
+}

+ 76 - 0
src/main/java/cn/reghao/dfs/store/service/FileHeader.java

@@ -0,0 +1,76 @@
+package cn.reghao.dfs.store.service;
+
+import java.io.*;
+
+/**
+ * @author reghao
+ * @date 2022-04-24 13:15:26
+ */
+public class FileHeader {
+    private static final char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+    static String byte2Hex(byte b) {
+        String hex = Integer.toString(b & 0xFF);
+        return hex.length() == 2 ? hex : "0" + hex;
+    }
+
+    static String bytes2Hex(byte[] bytes) {
+        StringBuilder hexStr = new StringBuilder();
+        for (byte b : bytes) {
+            StringBuilder sb = new StringBuilder();
+            int num = b < 0 ? 256 + b : b;
+            hexStr.append(HEX[num/16]).append(HEX[num%16]);
+            sb.append(HEX[num/16]).append(HEX[num%16]);
+        }
+        return hexStr.toString();
+    }
+
+    static String hexdump(byte[] data) {
+        int lineNum = 0;
+        StringBuilder res = new StringBuilder();
+        for (byte b : data) {
+            if (lineNum  % 16 == 0){
+                res.append(String.format("%05x: ", lineNum));
+            }
+
+            int num = b < 0 ? 256 + b : b;
+            res.append(HEX[num/16]).append(HEX[num%16]);
+
+            lineNum++;
+            if (lineNum % 16 == 0){
+                res.append(System.lineSeparator());
+            }
+        }
+
+        res.append(System.lineSeparator());
+        return res.toString();
+    }
+
+    public static String getFileHeader(InputStream in, int headerLen, int offset) throws IOException {
+        byte[] bytes = new byte[headerLen];
+        in.skip(offset);
+        in.read(bytes, 0, headerLen);
+        return bytes2Hex(bytes);
+    }
+
+    public static String getFirst1K(InputStream in) throws IOException {
+        byte[] bytes = new byte[1024];
+        in.read(bytes, 0, bytes.length);
+        return bytes2Hex(bytes);
+    }
+
+    public static void main(String[] args) throws IOException {
+        System.getProperty("sun.cpu.endian");
+        System.getProperty("file.encoding");
+        System.getProperty("sun.io.unicode.encoding");
+
+        String filePath = "/home/reghao/Downloads/mp4/test.mp4";
+        FileInputStream fis = new FileInputStream(filePath);
+        String hexStr = getFirst1K(fis);
+        String hexStr1 = hexStr.substring(8, 24);
+
+        fis = new FileInputStream(filePath);
+        String hexStr2 = getFileHeader(fis, 8, 4);
+        System.out.println();
+    }
+}

+ 33 - 0
src/main/java/cn/reghao/dfs/store/service/FileSignatures.java

@@ -0,0 +1,33 @@
+package cn.reghao.dfs.store.service;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * https://en.wikipedia.org/wiki/List_of_file_signatures
+ *
+ * @author reghao
+ * @date 2022-04-24 15:48:08
+ */
+public class FileSignatures {
+    static Map<String, FileSignature> map = new HashMap<>();
+    static {
+        //map.put("", new FileSignature());
+    }
+
+    static void getFileType(InputStream in) {
+    }
+
+    static class FileSignature {
+        private String hex;
+        private int offset;
+        private String suffix;
+
+        public FileSignature(String hex, int offset, String suffix) {
+            this.hex = hex;
+            this.offset = offset;
+            this.suffix = suffix;
+        }
+    }
+}

+ 75 - 0
src/main/java/cn/reghao/dfs/store/service/FileStoreService.java

@@ -0,0 +1,75 @@
+package cn.reghao.dfs.store.service;
+
+import org.apache.commons.io.FileUtils;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 15:09:06
+ */
+@Service
+public class FileStoreService {
+    public File createFile(String filePath, long len) throws IOException {
+        File file = new File(filePath);
+        if (!file.exists()) {
+            FileUtils.forceMkdirParent(file);
+            file.createNewFile();
+            RandomAccessFile raf = new RandomAccessFile(file, "rw");
+            raf.setLength(len);
+            raf.close();
+        }
+        return file;
+    }
+
+    public void writeToFile(InputStream in, File file, long pos) throws IOException {
+        RandomAccessFile raf = new RandomAccessFile(file, "rw");
+        raf.seek(pos);
+        byte[] buf = new byte[1024*1024];
+        int len;
+        while ((len = in.read(buf)) != -1) {
+            raf.write(buf, 0, len);
+        }
+        raf.close();
+        in.close();
+    }
+
+    public void saveFile(String filePath, InputStream in) throws IOException {
+        File file = new File(filePath);
+        if (file.exists()) {
+            return;
+        }
+
+        File parentDir = file.getParentFile();
+        if (!parentDir.exists()) {
+            FileUtils.forceMkdir(parentDir);
+        }
+
+        FileOutputStream fos = new FileOutputStream(file);
+        // 1MiB
+        int len = 1024*1024;
+        byte[] buf = new byte[len];
+        int readLen;
+        while ((readLen = in.read(buf, 0, len)) != -1) {
+            fos.write(buf, 0, readLen);
+        }
+        fos.close();
+    }
+
+    public void saveFile(String filePath, byte[] bytes) throws IOException {
+        File file = new File(filePath);
+        if (file.exists()) {
+            return;
+        }
+
+        File parentDir = file.getParentFile();
+        if (!parentDir.exists()) {
+            FileUtils.forceMkdir(parentDir);
+        }
+
+        FileOutputStream fos = new FileOutputStream(file);
+        fos.write(bytes);
+        fos.close();
+    }
+}

+ 31 - 0
src/main/java/cn/reghao/dfs/store/service/FileTypeService.java

@@ -0,0 +1,31 @@
+package cn.reghao.dfs.store.service;
+
+import org.springframework.stereotype.Service;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 16:33:09
+ */
+@Service
+public class FileTypeService {
+    public FileContentType getFileType(String contentType, String filePath) throws Exception {
+        if (contentType == null) {
+            contentType = Files.probeContentType(Paths.get(filePath));
+        }
+
+        int fileType = 4;
+        if (contentType == null) {
+            contentType = "application/octet-stream";
+        } else if (contentType.startsWith("image")) {
+            fileType = 1;
+        } else if (contentType.startsWith("audio")) {
+            fileType = 2;
+        } else if (contentType.startsWith("video")) {
+            fileType = 3;
+        }
+        return new FileContentType(fileType, contentType);
+    }
+}

+ 150 - 0
src/main/java/cn/reghao/dfs/store/service/FileUploadService.java

@@ -0,0 +1,150 @@
+package cn.reghao.dfs.store.service;
+
+import cn.reghao.dfs.store.model.dto.*;
+import cn.reghao.dfs.store.util.redis.ds.RedisSet;
+import cn.reghao.jutil.jdk.security.DigestUtil;
+import cn.reghao.dfs.store.config.PathUrl;
+import cn.reghao.dfs.store.db.repository.FileRepository;
+import cn.reghao.dfs.store.model.dto.*;
+import cn.reghao.dfs.store.model.po.FileInfo;
+import cn.reghao.dfs.store.util.media.ImageOps;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+
+/**
+ * @author reghao
+ * @date 2021-08-04 17:29:47
+ */
+@Slf4j
+@Service
+public class FileUploadService {
+    // 10MiB
+    private final static long PART_SIZE = 1024*1024*10;
+
+    private final FileUrlService fileUrlService;
+    private final FileStoreService fileStoreService;
+    private final FileTypeService fileTypeService;
+    private final FileRepository fileRepository;
+    private final RedisSet redisSet;
+
+    public FileUploadService(FileUrlService fileUrlService, FileStoreService fileStoreService,
+                             FileTypeService fileTypeService, FileRepository fileRepository,
+                             RedisSet redisSet) {
+        this.fileUrlService = fileUrlService;
+        this.fileStoreService = fileStoreService;
+        this.fileTypeService = fileTypeService;
+        this.fileRepository = fileRepository;
+        this.redisSet = redisSet;
+    }
+
+    public synchronized UploadPrepareRet prepareUpload(UploadPrepare uploadPrepare) {
+        // TODO 避免用户通过 sha256sum 查询文件是否存在, 这是一个不安全的行为, 用户可能会伪造 sha256sum 来获取不属于他权限的文件
+        // TODO 解决办法是检查用户已上传文件的 sha256sum, 这样可确保他拥有 sha256sum 所属文件的权限, 但这会带来服务器带宽和磁盘 IO 的性能开销
+        String sha256sum = uploadPrepare.getFileSha256sum();
+        FileInfo fileInfo = fileRepository.getFileInfo(sha256sum);
+        if (fileInfo == null) {
+            String uploadId = fileRepository.createFileUploadId(uploadPrepare);
+            return new UploadPrepareRet(uploadId, PART_SIZE, false);
+        }
+
+        String uploadId = fileRepository.getOrCreateUploadId(fileInfo.getFileId());
+        // uploaded 为 true 则 exist 也应该为 true, 但这里统一将 exist 设置为 false, 表示客户端总是要传输文件流
+        boolean uploaded = fileInfo.getUploaded();
+        return new UploadPrepareRet(uploadId, PART_SIZE, false);
+    }
+
+    /**
+     * 将文件存放到 FileStore,并在数据库中记录文件相关信息
+     *
+     * @param
+     * @return
+     * @date 2021-12-08 下午3:30
+     */
+    public void putFile(UploadFile uploadFile) throws Exception {
+        String uploadId = uploadFile.getUploadId();
+        FileInfo fileInfo = fileRepository.getFileInfoByUploadId(uploadId);
+        if (!fileInfo.getUploaded()) {
+            put(uploadFile.getFile(), fileInfo);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public synchronized String put(MultipartFile multipartFile, FileInfo fileInfo) throws Exception {
+        byte[] bytes = multipartFile.getBytes();
+        String sha256sum = DigestUtil.sha256sum(bytes);
+        if (!sha256sum.equals(fileInfo.getSha256sum())) {
+            throw new Exception("uploadId 和 sha256sum 不匹配!");
+        }
+
+        String contentType = multipartFile.getContentType();
+        String fileId = fileInfo.getFileId();
+        String suffix = fileInfo.getSuffix();
+        PathUrl pathUrl;
+        if (contentType != null && contentType.startsWith("image")) {
+            ImageOps.Size size1 = ImageOps.info(new ByteArrayInputStream(bytes));
+            pathUrl = fileUrlService.getImagePathAndUrl(sha256sum, fileId, suffix, size1.getWidth(), size1.getHeight());
+        } else {
+            pathUrl = fileUrlService.genPathAndUrl(sha256sum, fileId, suffix);
+        }
+
+        FileContentType fileType = fileTypeService.getFileType(contentType, pathUrl.getFilePath());
+        fileStoreService.saveFile(pathUrl.getFilePath(), bytes);
+        fileRepository.saveFile(fileId, fileType, pathUrl);
+        return pathUrl.getUrl();
+    }
+
+    /**
+     * 处理通过 HTTP 请求提交的分片文件
+     *
+     * @param
+     * @return
+     * @date 2021-12-08 下午3:31
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public synchronized UploadFilePartRet putFilePart(UploadFilePart uploadFilePart) throws Exception {
+        int partIndex = uploadFilePart.getSplitIndex();
+        int partNum = uploadFilePart.getSplitNum();
+        MultipartFile multipartFile = uploadFilePart.getFile();
+        String uploadId = uploadFilePart.getUploadId();
+        FileInfo fileInfo = fileRepository.getFileInfoByFileId(uploadId);
+        // 没有检查文件的 sha256sum, 存在安全隐患
+        if (fileInfo.getUploaded()) {
+            return new UploadFilePartRet(uploadId, true);
+        }
+
+        String fileId = fileInfo.getFileId();
+        String sha256sum = fileInfo.getSha256sum();
+        String suffix = fileInfo.getSuffix();
+        PathUrl pathUrl = fileUrlService.genPathAndUrl(sha256sum, fileId, suffix);
+        String key = RedisKey.filePartKey(fileId);
+        if (!redisSet.sismember(key, partIndex)) {
+            redisSet.sadd(key, partIndex);
+
+            long size = fileInfo.getSize();
+            File file = fileStoreService.createFile(pathUrl.getFilePath(), size);
+            long pos = partIndex * PART_SIZE;
+            fileStoreService.writeToFile(multipartFile.getInputStream(), file, pos);
+        }
+
+        long len = redisSet.scard(key);
+        if (len != partNum) {
+            return new UploadFilePartRet(fileId, false);
+        } else {
+            String path = pathUrl.getFilePath();
+            FileInputStream fis = new FileInputStream(path);
+            String sha256sumMerged = DigestUtil.sha256sum(fis);
+            if (!sha256sum.equals(sha256sumMerged)) {
+                throw new Exception("uploadId 和 sha256sum 不匹配!");
+            }
+
+            FileContentType fileType = fileTypeService.getFileType(fileInfo.getContentType(), pathUrl.getFilePath());
+            fileRepository.saveFile(fileId, fileType, pathUrl);
+            redisSet.spop(key, len);
+            return new UploadFilePartRet(uploadId, true);
+        }
+    }
+}

+ 38 - 0
src/main/java/cn/reghao/dfs/store/service/FileUrlService.java

@@ -0,0 +1,38 @@
+package cn.reghao.dfs.store.service;
+
+import cn.reghao.dfs.store.config.ConfigMap;
+import cn.reghao.dfs.store.config.PathUrl;
+import cn.reghao.dfs.store.util.LoadBalancer;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDate;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 15:06:29
+ */
+@Service
+public class FileUrlService {
+    private final LoadBalancer loadBalancer;
+    
+    public FileUrlService(LoadBalancer loadBalancer) {
+        this.loadBalancer = loadBalancer;
+    }
+
+    public PathUrl genPathAndUrl(String sha256sum, String fileId, String suffix) throws IOException, NoSuchAlgorithmException {
+        String baseDir = loadBalancer.getBaseDir(sha256sum);
+        String dateStr = LocalDate.now().toString().replace("-", "");
+        String filePath = String.format("%s%s/%s.%s", baseDir, dateStr, fileId, suffix);
+        return ConfigMap.getPathUrl(baseDir, filePath);
+    }
+
+    public PathUrl getImagePathAndUrl(String sha256sum, String fileId, String suffix, int width, int height)
+            throws IOException, NoSuchAlgorithmException {
+        String baseDir = loadBalancer.getBaseDir(sha256sum);
+        String dateStr = LocalDate.now().toString().replace("-", "");
+        String filePath = String.format("%s%s/%s_%sx%s.%s", baseDir, dateStr, fileId, width, height, suffix);
+        return ConfigMap.getPathUrl(baseDir, filePath);
+    }
+}

+ 15 - 0
src/main/java/cn/reghao/dfs/store/service/RedisKey.java

@@ -0,0 +1,15 @@
+package cn.reghao.dfs.store.service;
+
+/**
+ * @author reghao
+ * @date 2021-11-24 14:07:37
+ */
+public class RedisKey {
+    public static String fileSha256sumKey(String sha256sum) {
+        return String.format("file:sha256sum:%s", sha256sum);
+    }
+
+    public static String filePartKey(String fileId) {
+        return String.format("file:upload:part:%s", fileId);
+    }
+}

+ 11 - 0
src/main/java/cn/reghao/dfs/store/service/media/AudioFileService.java

@@ -0,0 +1,11 @@
+package cn.reghao.dfs.store.service.media;
+
+import org.springframework.stereotype.Service;
+
+/**
+ * @author reghao
+ * @date 2022-04-28 13:59:08
+ */
+@Service
+public class AudioFileService {
+}

+ 53 - 0
src/main/java/cn/reghao/dfs/store/service/media/ImageFileService.java

@@ -0,0 +1,53 @@
+package cn.reghao.dfs.store.service.media;
+
+import cn.reghao.dfs.store.config.PathUrl;
+import cn.reghao.dfs.store.db.mapper.ImageFileMapper;
+import cn.reghao.dfs.store.model.dto.FileDto;
+import cn.reghao.dfs.store.model.po.ImageFile;
+import cn.reghao.dfs.store.model.dto.ImgFileRetDto;
+import cn.reghao.dfs.store.util.media.ImageOps;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ * @author reghao
+ * @date 2021-08-04 17:29:47
+ */
+@Slf4j
+@Service
+public class ImageFileService {
+    private final ImageFileMapper imageFileMapper;
+
+    public ImageFileService(ImageFileMapper imageFileMapper) {
+        this.imageFileMapper = imageFileMapper;
+    }
+
+    public ImgFileRetDto process(String fileId, PathUrl pathUrl) throws Exception {
+        String path = pathUrl.getFilePath();
+        String url = pathUrl.getUrl();
+        File imgLocalFile = new File(path);
+
+        ImageOps.Size size1 = ImageOps.info(imgLocalFile);
+        ImageFile imageFile = new ImageFile(fileId, url, size1.getWidth(), size1.getHeight());
+        imageFileMapper.save(imageFile);
+        return new ImgFileRetDto(imageFile.getFileId(), imageFile.getBaseUrl());
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public ImgFileRetDto put(String sha256sum, InputStream in) throws Exception {
+        String filename = "default.jpg";
+        //FileDto fileDto = fileUploadService.put(sha256sum, in, filename, "image/jpeg");
+        FileDto fileDto = new FileDto();
+        String fileId = fileDto.getFileId();
+        String url = fileDto.getUrl();
+
+        ImageOps.Size size1 = ImageOps.info(new File(fileDto.getFilePath()));
+        ImageFile imageFile = new ImageFile(fileId, url, size1.getWidth(), size1.getHeight());
+        imageFileMapper.save(imageFile);
+        return new ImgFileRetDto(imageFile.getFileId(), imageFile.getBaseUrl());
+    }
+}

+ 115 - 0
src/main/java/cn/reghao/dfs/store/service/media/VideoFileService.java

@@ -0,0 +1,115 @@
+package cn.reghao.dfs.store.service.media;
+
+import cn.reghao.dfs.store.model.dto.ImgFileRetDto;
+import cn.reghao.jutil.jdk.security.DigestUtil;
+import cn.reghao.dfs.api.constant.VideoUrlType;
+import cn.reghao.dfs.store.config.PathUrl;
+import cn.reghao.dfs.store.db.mapper.VideoFileMapper;
+import cn.reghao.dfs.store.model.dto.*;
+import cn.reghao.dfs.store.model.po.VideoFile;
+import cn.reghao.dfs.store.util.media.VideoOps;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-08-04 17:29:47
+ */
+@Slf4j
+@Service
+public class VideoFileService {
+    private final ImageFileService imageFileService;
+    private final VideoFileMapper videoFileMapper;
+
+    public VideoFileService(ImageFileService imageFileService, VideoFileMapper videoFileMapper) {
+        this.imageFileService = imageFileService;
+        this.videoFileMapper = videoFileMapper;
+    }
+
+    /**
+     * 将视频文件保存到 FileStore,并将文件的信息保存到数据库
+     * 本方法内的所有 insert 操作都应该在一个事务内完成
+     *
+     * 文件上传前需要通过接口确定 sha256sum 是否已在数据库中
+     *
+     * @param
+     * @return
+     * @date 2021-12-09 下午8:45
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public VideoFile process(String fileId, PathUrl pathUrl) throws Exception {
+        String path = pathUrl.getFilePath();
+        String url = pathUrl.getUrl();
+
+        VideoFile videoFile = new VideoFile(fileId, VideoUrlType.mp4.getName(), url);
+        File videoLocalFile = new File(path);
+        log.info("process video {} with FFmpeg...", videoFile.getFileId());
+        setVideoProps(videoFile, videoLocalFile);
+        setVideoCover(videoFile, videoLocalFile);
+        log.info("video {} processed...", videoFile.getFileId());
+
+        videoFileMapper.save(videoFile);
+        return videoFile;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public String setVideoCover(String fileId, MultipartFile multipartFile) throws Exception {
+        VideoFile videoFile = videoFileMapper.findByFileId(fileId);
+        if (videoFile == null) {
+            return null;
+        }
+
+        ImgFileRetDto imgFileRetDto = imageFileService.put(null, multipartFile.getInputStream());
+        String url = imgFileRetDto.getUrl();
+        videoFile.setBaseCoverUrl(url);
+        videoFileMapper.updateSetCover(fileId, url);
+        return url;
+    }
+
+    /**
+     * 设置视频属性
+     *
+     * @param
+     * @return
+     * @date 2021-08-18 上午10:08
+     */
+    private void setVideoProps(VideoFile videoFile, File file) throws IOException {
+        Map<String, Object> map = VideoOps.videoProps(file);
+        int width = (Integer) map.get("width");
+        int height = (Integer) map.get("height");
+        double fps = (Double) map.get("fps");
+        int frameLength = (Integer) map.get("frameLength");
+        int length = (Integer) map.get("length");
+        int duration = ((Long) map.get("duration")).intValue();
+        String rotate = (String) map.get("rotate");
+
+        videoFile.setDuration(duration);
+        videoFile.setFrameRate(fps);
+        videoFile.setWidth(width);
+        videoFile.setHeight(height);
+        videoFile.setHorizontal(width >= height);
+        videoFile.setRotate(rotate);
+    }
+
+    /**
+     * 设置视频缩略图
+     *
+     * @param
+     * @return
+     * @date 2021-08-18 上午10:08
+     */
+    private void setVideoCover(VideoFile videoFile, File file) throws Exception {
+        ByteArrayOutputStream outputStream = VideoOps.thumbnailCover(file);
+        byte[] bytes = outputStream.toByteArray();
+        outputStream.close();
+
+        String sha256sum = DigestUtil.sha256sum(bytes);
+        ImgFileRetDto imgFileRetDto = imageFileService.put(sha256sum, new ByteArrayInputStream(bytes));
+        videoFile.setBaseCoverUrl(imgFileRetDto.getUrl());
+    }
+}

+ 64 - 0
src/main/java/cn/reghao/dfs/store/service/part/TmpFile.java

@@ -0,0 +1,64 @@
+package cn.reghao.dfs.store.service.part;
+
+import org.apache.commons.io.FileUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/**
+ * 临时文件,使用完成后自动删除
+ *
+ * @author reghao
+ * @date 2021-11-24 10:28:06
+ */
+public class TmpFile implements AutoCloseable {
+    private final String BASE_PATH = "/var/tmp/tnb";
+    private final File file;
+    // 分片文件标志
+    private final boolean isFrag;
+
+    public TmpFile(String fileId, byte[] bytes) throws IOException {
+        this.file = createFromStream(fileId, bytes);
+        this.isFrag = false;
+    }
+
+    public TmpFile(String fileId, InputStream in) throws IOException {
+        this.file = createFromStream(fileId, in);
+        this.isFrag = false;
+    }
+
+    public TmpFile(File file) {
+        this.file = file;
+        this.isFrag = true;
+    }
+
+    public File get() {
+        return file;
+    }
+
+    private File createFromStream(String fileId, byte[] bytes) throws IOException {
+        File tmpFile = new File(String.format("%s/%s", BASE_PATH, fileId));
+        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+        Files.copy(bais, tmpFile.toPath());
+        return tmpFile;
+    }
+
+    private File createFromStream(String fileId, InputStream in) throws IOException {
+        File tmpFile = new File(String.format("%s/%s", BASE_PATH, fileId));
+        Files.copy(in, tmpFile.toPath());
+        return tmpFile;
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (isFrag) {
+            String parentDirPath = file.getParent();
+            FileUtils.deleteDirectory(new File(parentDirPath));
+        } else {
+            FileUtils.deleteQuietly(file);
+        }
+    }
+}

+ 82 - 0
src/main/java/cn/reghao/dfs/store/service/part/WholeFile.java

@@ -0,0 +1,82 @@
+package cn.reghao.dfs.store.service.part;
+
+import org.apache.commons.io.FileUtils;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 完整文件和分片文件
+ *
+ * @author reghao
+ * @date 2021-11-24 11:25:49
+ */
+public class WholeFile {
+    private final static String BASE_PATH = "/var/tmp/tnb";
+
+    public static String getFragFileDir(String fileId) {
+        return BASE_PATH + File.separator + fileId + File.separator + "frags";
+    }
+
+    public static File createTmpFragFile(String fileId, String fragFilename, InputStream in) throws IOException {
+        File fragFileDir = new File(BASE_PATH + File.separator + fileId + File.separator + "frags");
+        if (!fragFileDir.exists()) {
+            FileUtils.forceMkdir(fragFileDir);
+        }
+
+        String tmpFilePath = fragFileDir.getAbsolutePath() + File.separator + fragFilename;
+        File tmpFile = new File(tmpFilePath);
+        FileOutputStream fos = new FileOutputStream(tmpFile);
+        // 1MiB
+        int len = 1024*1024;
+        byte[] buf = new byte[len];
+        int readLen;
+        while ((readLen = in.read(buf, 0, len)) != -1) {
+            fos.write(buf, 0, readLen);
+        }
+        fos.close();
+        return tmpFile;
+    }
+
+    /**
+     * 合并分片文件,一个耗时操作
+     *
+     * @param
+     * @return
+     * @date 2021-11-24 下午1:26
+     */
+    public static TmpFile merge(String fragFileDirPath, String filename) throws IOException {
+        File fragFileDir = new File(fragFileDirPath);
+        String filePath = fragFileDir.getParent() + File.separator + filename;
+        File destFile = new File(filePath);
+        boolean isCreated = destFile.createNewFile();
+        if (!isCreated) {
+            throw new IOException("文件创建失败");
+        }
+
+        List<File> list = Arrays.stream(Objects.requireNonNull(fragFileDir.listFiles()))
+                .sorted(Comparator.comparingInt(file -> Integer.parseInt(file.getName())))
+                .collect(Collectors.toList());
+
+        RandomAccessFile rafWrite = new RandomAccessFile(destFile, "rw");
+        rafWrite.seek(0);
+        byte[] buf = new byte[1024];
+        for (File file : list) {
+            RandomAccessFile rafRead = new RandomAccessFile(file, "rw");
+            int len;
+            while ((len = rafRead.read(buf)) != -1) {
+                rafWrite.write(buf, 0, len);
+            }
+            rafRead.close();
+        }
+        rafWrite.close();
+
+        // 删除分片文件
+        FileUtils.deleteDirectory(fragFileDir);
+        return new TmpFile(destFile);
+    }
+}

+ 28 - 0
src/main/java/cn/reghao/dfs/store/util/FileLifecycle.java

@@ -0,0 +1,28 @@
+package cn.reghao.dfs.store.util;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author reghao
+ * @date 2022-03-23 09:22:01
+ */
+@Component
+public class FileLifecycle implements ApplicationRunner, DisposableBean {
+    private final LoadBalancer loadBalancer;
+
+    public FileLifecycle(LoadBalancer loadBalancer) {
+        this.loadBalancer = loadBalancer;
+    }
+
+    @Override
+    public void run(ApplicationArguments args) {
+        loadBalancer.init();
+    }
+
+    @Override
+    public void destroy() {
+    }
+}

+ 16 - 0
src/main/java/cn/reghao/dfs/store/util/HertubePrefix.java

@@ -0,0 +1,16 @@
+package cn.reghao.dfs.store.util;
+
+/**
+ * hertube 应用静态文件的存储位置前缀
+ *
+ * @author reghao
+ * @date 2021-08-05 15:41:45
+ */
+public class HertubePrefix {
+    public final static String MP4_VIDEO = "hertube/vid/mp4";
+    public final static String HLS_VIDEO = "hertube/vid/hls";
+    public final static String DASH_VIDEO = "hertube/vid/dash";
+    public final static String VIDCOVER_IMAGE = "hertube/img/vidcover";
+    public final static String AVATAR_IMAGE = "hertube/img/avatar";
+    public final static String PHOTO_IMAGE = "hertube/img/photo";
+}

+ 50 - 0
src/main/java/cn/reghao/dfs/store/util/LoadBalancer.java

@@ -0,0 +1,50 @@
+package cn.reghao.dfs.store.util;
+
+import cn.reghao.dfs.store.config.ConfigMap;
+import cn.reghao.jutil.jdk.security.DigestUtil;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2022-03-22 13:53:08
+ */
+@Component
+public class LoadBalancer {
+    private final List<String> nodes = new ArrayList<>();
+    private final HashFunction hashFunction = Hashing.murmur3_32_fixed();
+
+    public void init() {
+        nodes.addAll(ConfigMap.baseDirs());
+    }
+
+    public String getBaseDir(Object object) throws IOException, NoSuchAlgorithmException {
+        int hash = hash(object);
+        int nodeIdx = hash % nodes.size();
+        return nodes.get(nodeIdx);
+    }
+
+    private int hash(Object object) throws IOException, NoSuchAlgorithmException {
+        int ret = 0;
+        if (object instanceof Integer) {
+            Integer i = (Integer) object;
+            ret = hashFunction.hashInt(i).asInt();
+        } else if (object instanceof String) {
+            String str = (String) object;
+            ret = hashFunction.hashUnencodedChars(str).asInt();
+        } else if (object instanceof File) {
+            File file = (File) object;
+            FileInputStream fis = new FileInputStream(file);
+            String sha256sum = DigestUtil.sha256sum(fis);
+            ret = hashFunction.hashUnencodedChars(sha256sum).asInt();
+        }
+        return Math.abs(ret);
+    }
+}

+ 47 - 0
src/main/java/cn/reghao/dfs/store/util/ServletUtil.java

@@ -0,0 +1,47 @@
+package cn.reghao.dfs.store.util;
+
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * @author reghao
+ * @date 2021-06-02 13:16:58
+ */
+public class ServletUtil {
+    public static HttpSession getSession() {
+        return getRequest().getSession();
+    }
+
+    public static String getSessionId() {
+        return getRequest().getSession().getId();
+    }
+
+    /**
+     * 获取 query 参数值
+     *
+     * @param
+     * @return
+     * @date 2021-06-02 下午1:19
+     */
+    public static String getRequestParam(String param, String defaultValue){
+        String parameter = getRequest().getParameter(param);
+        return StringUtils.isEmpty(parameter) ? defaultValue : parameter;
+    }
+
+    public static HttpServletRequest getRequest(){
+        return getServletRequest().getRequest();
+    }
+
+    public static HttpServletResponse getResponse(){
+        return getServletRequest().getResponse();
+    }
+
+    private static ServletRequestAttributes getServletRequest(){
+        return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+    }
+}

+ 19 - 0
src/main/java/cn/reghao/dfs/store/util/StringUtil.java

@@ -0,0 +1,19 @@
+package cn.reghao.dfs.store.util;
+
+/**
+ * @author reghao
+ * @date 2022-01-06 13:02:04
+ */
+public class StringUtil {
+    /**
+     * 获取文件的后缀名
+     *
+     * @param
+     * @return
+     * @date 2021-12-08 下午3:32
+     */
+    public static String getSuffix(String filename) {
+        int idx = filename.lastIndexOf(".");
+        return idx == -1 ? "" : filename.substring(idx+1);
+    }
+}

+ 24 - 0
src/main/java/cn/reghao/dfs/store/util/UserContext.java

@@ -0,0 +1,24 @@
+package cn.reghao.dfs.store.util;
+
+/**
+ * 获取当前请求的用户
+ *
+ * @author reghao
+ * @date 2020-06-23 19:20:56
+ */
+public class UserContext implements AutoCloseable {
+    static final ThreadLocal<Long> CURRENT = new ThreadLocal<>();
+
+    public UserContext(Long Long) {
+        CURRENT.set(Long);
+    }
+
+    public static Long currentUserId() {
+        return CURRENT.get();
+    }
+
+    @Override
+    public void close() {
+        CURRENT.remove();
+    }
+}

+ 73 - 0
src/main/java/cn/reghao/dfs/store/util/WebResult.java

@@ -0,0 +1,73 @@
+package cn.reghao.dfs.store.util;
+
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.jutil.jdk.result.ResultStatus;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * @author reghao
+ * @date 2022-04-16 20:38:19
+ */
+public class WebResult {
+    private final int code;
+    private final String message;
+    private final long timestamp;
+    private Object data;
+    private static final Gson gson = new GsonBuilder()
+            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+
+    private WebResult(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    private void setData(Object data) {
+        this.data = data;
+    }
+
+    public static String success() {
+        WebResult webBody = new WebResult(200, ResultStatus.SUCCESS.getMsg());
+        return gson.toJson(webBody);
+    }
+
+    public static String successWithMsg(String message) {
+        WebResult webBody = new WebResult(200, message);
+        return gson.toJson(webBody);
+    }
+
+    public static String result(Result result) {
+        WebResult webBody = new WebResult(result.getCode(), result.getMsg());
+        return gson.toJson(webBody);
+    }
+
+    public static String success(Object data) {
+        WebResult webBody = new WebResult(200, ResultStatus.SUCCESS.getMsg());
+        webBody.setData(data);
+        return gson.toJson(webBody);
+    }
+
+    public static String fail(Object data) {
+        WebResult webBody = new WebResult(400, ResultStatus.FAIL.getMsg());
+        webBody.setData(data);
+        return gson.toJson(webBody);
+    }
+
+    public static String failWithMsg(String message) {
+        WebResult webBody = new WebResult(400, message);
+        return gson.toJson(webBody);
+    }
+
+    public static String error(Object data) {
+        WebResult webBody = new WebResult(ResultStatus.ERROR.getCode(), ResultStatus.ERROR.getMsg());
+        webBody.setData(data);
+        return gson.toJson(webBody);
+    }
+
+    public static String errorMsg(String message) {
+        WebResult webBody = new WebResult(ResultStatus.ERROR.getCode(), message);
+        return gson.toJson(webBody);
+    }
+}

+ 220 - 0
src/main/java/cn/reghao/dfs/store/util/image/Captcha.java

@@ -0,0 +1,220 @@
+package cn.reghao.dfs.store.util.image;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.geom.CubicCurve2D;
+import java.awt.geom.QuadCurve2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * @author reghao
+ * @date 2021-07-24 12:52:18
+ */
+public class Captcha {
+    private final Random random = new SecureRandom();
+    private final String[] fontNames = {"宋体", "华文楷体", "黑体", "Georgia", "微软雅黑", "楷体_GB2312"};
+    /**
+     * 常用颜色
+     * */
+    public static final int[][] COLOR = {{0, 135, 255}, {51, 153, 51}, {255, 102, 102}, {255, 153, 0}, {153, 102, 0},
+            {153, 102, 153}, {51, 153, 153}, {102, 102, 255}, {0, 102, 204}, {204, 51, 51}, {0, 153, 204}, {0, 51, 102}
+    };
+
+    /**
+     * 验证码显示宽度
+     * */
+    protected int width = 130;
+
+    /**
+     * 验证码显示高度
+     * */
+    protected int height = 48;
+
+    public Image imageWithDisturb(String string, int width, int height) {
+        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2d = (Graphics2D) bi.getGraphics();
+        // 填充背景
+        g2d.setColor(Color.WHITE);
+        g2d.fillRect(0, 0, width, height);
+        // 抗锯齿
+        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        // 画干扰圆
+        drawOval(2, null, g2d);
+        // 画干扰线
+        g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+        drawBesselLine(1, null, g2d);
+
+        FontMetrics fontMetrics = g2d.getFontMetrics();
+        // 每一个字符所占的宽度
+        int fW = width / string.length();
+        // 字符的左右边距
+        int fSp = (fW - (int) fontMetrics.getStringBounds("W", g2d).getWidth()) / 2;
+        for (int i = 0; i < string.length(); i++) {
+            g2d.setColor(color());
+            // 文字的纵坐标
+            int fY = height - ((height - (int) fontMetrics.getStringBounds(String.valueOf(string.charAt(i)), g2d).getHeight()) >> 1);
+            g2d.drawString(String.valueOf(string.charAt(i)), i * fW + fSp + 3, fY - 3);
+        }
+        g2d.dispose();
+        return bi;
+    }
+
+    /**
+     * 随机画干扰圆
+     *
+     * @param num   数量
+     * @param color 颜色
+     * @param g     Graphics2D
+     */
+    public void drawOval(int num, Color color, Graphics2D g) {
+        for (int i = 0; i < num; i++) {
+            g.setColor(color == null ? color() : color);
+            int w = 5 + num(10);
+            g.drawOval(num(width - 25), num(height - 15), w, w);
+        }
+    }
+
+    public static int num(int min, int max) {
+        return min + new Random().nextInt(max - min);
+    }
+
+    public static int num(int num) {
+        return new Random().nextInt(num);
+    }
+
+    /**
+     * 随机画贝塞尔曲线
+     *
+     * @param num   数量
+     * @param color 颜色
+     * @param g     Graphics2D
+     */
+    public void drawBesselLine(int num, Color color, Graphics2D g) {
+        for (int i = 0; i < num; i++) {
+            g.setColor(color == null ? color() : color);
+            int x1 = 5, y1 = num(5, height / 2);
+            int x2 = width - 5, y2 = num(height / 2, height - 5);
+            int ctrlx = num(width / 4, width / 4 * 3), ctrly = num(5, height - 5);
+            if (num(2) == 0) {
+                int ty = y1;
+                y1 = y2;
+                y2 = ty;
+            }
+            // 二阶贝塞尔曲线
+            if (num(2) == 0) {
+                QuadCurve2D shape = new QuadCurve2D.Double();
+                shape.setCurve(x1, y1, ctrlx, ctrly, x2, y2);
+                g.draw(shape);
+            } else {
+                // 三阶贝塞尔曲线
+                int ctrlx1 = num(width / 4, width / 4 * 3), ctrly1 = num(5, height - 5);
+                CubicCurve2D shape = new CubicCurve2D.Double(x1, y1, ctrlx, ctrly, ctrlx1, ctrly1, x2, y2);
+                g.draw(shape);
+            }
+        }
+    }
+
+    /**
+     * 给定范围获得随机颜色
+     *
+     * @param fc 0-255
+     * @param bc 0-255
+     * @return 随机颜色
+     */
+    public Color color(int fc, int bc) {
+        if (fc > 255) {
+            fc = 255;
+        }
+
+        if (bc > 255) {
+            bc = 255;
+        }
+
+        int r = fc + num(bc - fc);
+        int g = fc + num(bc - fc);
+        int b = fc + num(bc - fc);
+        return new Color(r, g, b);
+    }
+
+    /**
+     * 获取随机常用颜色
+     *
+     * @return 随机颜色
+     */
+    public Color color() {
+        int[] color = COLOR[num(COLOR.length)];
+        return new Color(color[0], color[1], color[2]);
+    }
+
+    public String drawImage(ByteArrayOutputStream output) {
+        int width = 96;
+        int height = 64;
+
+        //创建图片缓冲区
+        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+        Graphics2D g = bi.createGraphics();
+
+        //设置背景颜色
+        g.setBackground(new Color(255, 255, 255));
+        g.clearRect(0, 0, width, height);
+
+        StringBuilder sb = new StringBuilder();
+        //这里只画入四个字符
+        for (int i = 0; i < 4; i++) {
+            String s = randomChar() + "";      //随机生成字符,因为只有画字符串的方法,没有画字符的方法,所以需要将字符变成字符串再画
+            sb.append(s);           //添加到StringBuilder里面
+            float x = i * 1.0F * width / 4;   //定义字符的x坐标
+            g.setFont(randomFont());           //设置字体,随机
+            g.setColor(randomColor());         //设置颜色,随机
+            g.drawString(s, x, height - 5);
+        }
+
+        //定义干扰线
+        //定义干扰线的数量(3-5条)int num = random.nextInt(max)%(max-min+1) + min;
+        int num = random.nextInt(5) % 3 + 3;
+        Graphics2D graphics = (Graphics2D) bi.getGraphics();
+        for (int i = 0; i < num; i++) {
+            int x1 = random.nextInt(width);
+            int y1 = random.nextInt(height);
+            int x2 = random.nextInt(width);
+            int y2 = random.nextInt(height);
+            graphics.setColor(randomColor());
+            graphics.drawLine(x1, y1, x2, y2);
+        }
+        // 释放图形上下文
+        g.dispose();
+        try {
+            ImageIO.write(bi, "jpg", output);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        // 返回验证码字符串
+        return sb.toString();
+    }
+
+    private Font randomFont() {
+        int index = random.nextInt(fontNames.length);
+        String fontName = fontNames[index];
+        int style = random.nextInt(4);         //随机获取4种字体的样式
+        int size = random.nextInt(20) % 6 + 15;    //随机获取字体的大小(10-20之间的值)
+        return new Font(fontName, style, size);
+    }
+
+    private Color randomColor() {
+        int r = random.nextInt(225);
+        int g = random.nextInt(225);
+        int b = random.nextInt(225);
+        return new Color(r, g, b);
+    }
+
+    private char randomChar() {
+        //A-Z,a-z,0-9,可剔除一些难辨认的字母与数字
+        String str = "0123456789ABCdefghiDEFGHIJopPQRVWXYZabcjklSTUmnqrstKLMNOvuwxyz";
+        return str.charAt(random.nextInt(str.length()));
+    }
+}

+ 44 - 0
src/main/java/cn/reghao/dfs/store/util/image/ImageUtil.java

@@ -0,0 +1,44 @@
+package cn.reghao.dfs.store.util.image;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.font.FontRenderContext;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * @author reghao
+ * @date 2022-01-07 15:52:07
+ */
+public class ImageUtil {
+    public static ByteArrayOutputStream getImage(int width, int height) throws IOException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
+        Date date = new Date();
+        String text = sdf.format(date);
+
+        Font font = new Font("Serif", Font.BOLD, 10);
+        // 创建一个画布
+        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        // 获取画布的画笔
+        Graphics2D g2 = (Graphics2D) bi.getGraphics();
+        // 开始绘图
+        g2.setBackground(Color.WHITE);
+        g2.clearRect(0, 0, width, height);
+        g2.setPaint(new Color(0, 0, 0));
+        FontRenderContext context = g2.getFontRenderContext();
+        Rectangle2D bounds = font.getStringBounds(text, context);
+        double x = (width - bounds.getWidth()) / 2;
+        double y = (height - bounds.getHeight()) / 2;
+        double ascent = -bounds.getY();
+        double baseY = y + ascent;
+        g2.drawString(text, (int) x, (int) baseY);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(bi, "jpg", baos);
+        return baos;
+    }
+}

+ 44 - 0
src/main/java/cn/reghao/dfs/store/util/image/QrCode.java

@@ -0,0 +1,44 @@
+package cn.reghao.dfs.store.util.image;
+
+import com.google.zxing.*;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.common.HybridBinarizer;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2022-01-07 15:52:07
+ */
+@Slf4j
+public class QrCode {
+    public ByteArrayOutputStream getOne(String text, int width, int height) throws Exception {
+        Map<EncodeHintType, Object> hints = new HashMap<>();
+        hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);
+        BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        MatrixToImageWriter.writeToStream(bitMatrix, "jpg", baos);
+        return baos;
+    }
+
+    public String decode(InputStream in) throws IOException, NotFoundException {
+        BufferedImage bi = ImageIO.read(in);
+        LuminanceSource source = new BufferedImageLuminanceSource(bi);
+        Binarizer binarizer = new HybridBinarizer(source);
+        BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
+
+        Map<DecodeHintType, String> hints = new HashMap<>();
+        hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
+        Result result = new MultiFormatReader().decode(binaryBitmap, hints);
+        return result.getText();
+    }
+}

+ 110 - 0
src/main/java/cn/reghao/dfs/store/util/media/ImageOps.java

@@ -0,0 +1,110 @@
+package cn.reghao.dfs.store.util.media;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.List;
+
+/**
+ * 对图像的操作
+ *
+ * @author reghao
+ * @date 2021-08-04 16:26:13
+ */
+public class ImageOps {
+    public static Size info(InputStream in) throws IOException {
+        BufferedImage bi = ImageIO.read(in);
+        return new Size(bi.getWidth(), bi.getHeight());
+    }
+
+    public static Size info(File file) throws IOException {
+        BufferedImage bi = ImageIO.read(file);
+        return new Size(bi.getWidth(), bi.getHeight());
+    }
+
+    public static BufferedImage merge(List<BufferedImage> bufferedImages, boolean isVertical) {
+        int size = bufferedImages.size();
+        int[][] imageArray = new int[size][];
+        for (int i = 0; i < size; i++) {
+            int width = bufferedImages.get(i).getWidth();
+            int height = bufferedImages.get(i).getHeight();
+            imageArray[i] = new int[width*height];
+            imageArray[i] = bufferedImages.get(i).getRGB(0, 0, width, height, imageArray[i], 0, width);
+        }
+
+        int newHeight = 0, newWidth = 0;
+        for (BufferedImage bufferedImage : bufferedImages) {
+            if (!isVertical) {
+                // 横向拼接,height 不变,width 增加
+                newHeight = Math.max(newHeight, bufferedImage.getHeight());
+                newWidth += bufferedImage.getWidth();
+            } else {
+                // 纵向拼接,width 不变,height 增加
+                newWidth = Math.max(newWidth, bufferedImage.getWidth());
+                newHeight += bufferedImage.getHeight();
+            }
+        }
+
+        BufferedImage newImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
+        int width = 0, height = 0;
+        for (int i = 0; i < size; i++) {
+            if (!isVertical) {
+                // 横向拼接
+                newImage.setRGB(width, 0, bufferedImages.get(i).getWidth(), newHeight, imageArray[i], 0, bufferedImages.get(i).getWidth());
+                width += bufferedImages.get(i).getWidth();
+            } else {
+                // 纵向拼接
+                newImage.setRGB(0, height, newWidth, bufferedImages.get(i).getHeight(), imageArray[i], 0, newWidth);
+                height += bufferedImages.get(i).getHeight();
+            }
+        }
+
+        return newImage;
+    }
+
+    /**
+     * 缩小图片
+     *
+     * @param
+     * @return
+     * @date 2021-08-18 下午1:45
+     */
+    public static BufferedImage resize(BufferedImage srcImage, int size) {
+        int width = srcImage.getWidth()/size;
+        int height = srcImage.getHeight()/size;
+
+        BufferedImage newImage = new BufferedImage(width, height, srcImage.getType());
+        Graphics g = newImage.getGraphics();
+        g.drawImage(srcImage, 0, 0, width, height, null);
+        g.dispose();
+        return newImage;
+    }
+
+    public static void saveImage(BufferedImage bufferedImage, String filePath) throws IOException {
+        ImageIO.write(bufferedImage, "jpg", new File(filePath));
+    }
+
+    public static void saveImage(ByteArrayOutputStream baos, String filePath) throws IOException {
+        BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(baos.toByteArray()));
+        ImageIO.write(bufferedImage, "jpg", new File(filePath));
+    }
+
+    public static class Size {
+        private final int width;
+        private final int height;
+
+        public Size(int width, int height) {
+            this.width = width;
+            this.height = height;
+        }
+
+        public int getWidth() {
+            return width;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+    }
+}

+ 136 - 0
src/main/java/cn/reghao/dfs/store/util/media/VideoOps.java

@@ -0,0 +1,136 @@
+package cn.reghao.dfs.store.util.media;
+
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-08-04 09:51:30
+ */
+public class VideoOps {
+    private static final Java2DFrameConverter frameConverter = new Java2DFrameConverter();
+
+    /**
+     * 视频属性
+     *
+     * @param
+     * @return
+     * @date 2021-08-13 上午9:08
+     */
+    public static Map<String, Object> videoProps(File file) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        // 截取第一帧
+        Frame frame = grabber.grabImage();
+        BufferedImage bufferedImage = frameConverter.getBufferedImage(frame);
+
+        Map<String, Object> map = new HashMap<>();
+        int width = bufferedImage.getWidth();
+        map.put("width", width);
+        int height = bufferedImage.getHeight();
+        map.put("height", height);
+        double fps = grabber.getFrameRate();
+        map.put("fps", fps);
+        int frameLength = grabber.getLengthInFrames();
+        map.put("frameLength", frameLength);
+        int length = frameLength / (int) fps;
+        map.put("length", length);
+        long duration = grabber.getLengthInTime() / (1000 * 1000);
+        map.put("duration", duration);
+        String format = grabber.getFormat();
+        map.put("format", format);
+        // 视频旋转的角度
+        String rotate = grabber.getVideoMetadata("rotate");
+        map.put("rotate", rotate);
+        grabber.stop();
+        return map;
+    }
+
+    /**
+     * 取出指定位置的帧生成视频封面缩略图
+     *
+     * @param
+     * @return
+     * @date 2021-08-18 上午11:12
+     */
+    public static ByteArrayOutputStream thumbnailCover(File file) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        int frameLength = grabber.getLengthInFrames();
+        int interval = frameLength/16;
+        List<Integer> frames = new ArrayList<>();
+        for (int i = 1; i < frameLength; i += interval) {
+            frames.add(i);
+        }
+
+        if (frames.size() > 16) {
+            frames = frames.subList(0, 16);
+        }
+
+        List<List<BufferedImage>> lists = new ArrayList<>();
+        List<BufferedImage> list = new ArrayList<>();
+        Frame frame;
+        for (int i : frames) {
+            grabber.setFrameNumber(i);
+            frame = grabber.grabImage();
+            if (list.size() == 4) {
+                lists.add(new ArrayList<>(list));
+                list.clear();
+            }
+
+            if (frame != null) {
+                // TODO 在截取的帧上添加视频中所处的时间水印
+                list.add(ImageOps.resize(frameConverter.getBufferedImage(frame), 4));
+            }
+        }
+        grabber.stop();
+        if (list.size() == 4) {
+            lists.add(new ArrayList<>(list));
+            list.clear();
+        }
+
+        List<BufferedImage> list1 = new ArrayList<>();
+        for (List<BufferedImage> tmp : lists) {
+            list1.add(ImageOps.merge(tmp, false));
+        }
+
+        BufferedImage resultImage = ImageOps.merge(list1, true);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(resultImage, "jpg", baos);
+        return baos;
+    }
+
+    public static ByteArrayOutputStream videoCover(File file) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        int frameNumber = grabber.getLengthInFrames()/100;
+        grabber.setFrameNumber(frameNumber);
+        // 截取帧总长度的 1/100 位置处作为视频封面
+        Frame frame = grabber.grabImage();
+        BufferedImage bufferedImage = frameConverter.getBufferedImage(frame);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(bufferedImage, "jpg", baos);
+        return baos;
+    }
+
+    public static void videoCover(File file, String savedFilePath) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        // 截取第一帧作为视频封面
+        Frame frame = grabber.grabImage();
+        BufferedImage bufferedImage = frameConverter.getBufferedImage(frame);
+        ImageOps.saveImage(bufferedImage, savedFilePath);
+    }
+}

+ 61 - 0
src/main/java/cn/reghao/dfs/store/util/redis/RedisConfig.java

@@ -0,0 +1,61 @@
+package cn.reghao.dfs.store.util.redis;
+
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.serializer.support.DeserializingConverter;
+import org.springframework.core.serializer.support.SerializingConverter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * Redis 连接和序列化配置
+ *
+ * @author reghao
+ * @date 2021-11-15 14:40:57
+ */
+@Configuration
+public class RedisConfig extends CachingConfigurerSupport {
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(factory);
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new RedisObjectSerializer());
+        return template;
+    }
+
+    /**
+     * 序列化对象
+     *
+     * @param
+     * @return
+     * @date 2021-12-09 下午12:02
+     */
+    @Configuration
+    static class RedisObjectSerializer implements RedisSerializer<Object> {
+        private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+        private final Converter<Object, byte[]> serializingConverter = new SerializingConverter();
+        private final Converter<byte[], Object> deserializingConverter = new DeserializingConverter();
+
+        @Override
+        public byte[] serialize(Object obj) throws SerializationException {
+            if (obj == null) {
+                return EMPTY_BYTE_ARRAY ;
+            }
+            return this.serializingConverter.convert(obj);
+        }
+
+        @Override
+        public Object deserialize(byte[] data) throws SerializationException {
+            if (data == null || data.length == 0) {
+                return null;
+            }
+            return this.deserializingConverter.convert(data);
+        }
+    }
+}

+ 19 - 0
src/main/java/cn/reghao/dfs/store/util/redis/RedisKeys.java

@@ -0,0 +1,19 @@
+package cn.reghao.dfs.store.util.redis;
+
+/**
+ * @author reghao
+ * @date 2021-12-11 19:20:25
+ */
+public class RedisKeys {
+    public static String captchaCodeKey(String r) {
+        return "captchaCode.r:" + r;
+    }
+
+    public static String verifyCodeKey(String receiver) {
+        return "verifyCode.receiver:" + receiver;
+    }
+
+    public static String userTokenKey(String userId) {
+        return "user.token:" + userId;
+    }
+}

+ 36 - 0
src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisHash.java

@@ -0,0 +1,36 @@
+package cn.reghao.dfs.store.util.redis.ds;
+
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-03-07 17:44:23
+ */
+@Component
+public class RedisHash {
+    private final HashOperations<String, String, String> hashOps;
+
+    public RedisHash(RedisTemplate<String, String> redisTemplate) {
+        this.hashOps = redisTemplate.opsForHash();
+    }
+
+    public void set(String key, String hashKey, String hashValue) {
+        hashOps.put(key, hashKey, hashValue);
+    }
+
+    public String get(String key, String hashKey) {
+        return hashOps.get(key, hashKey);
+    }
+
+    public Map<String, String> entries(String key) {
+        return hashOps.entries(key);
+    }
+
+    public void delete(String key, String hashKey) {
+        hashOps.delete(key, hashKey);
+    }
+}

+ 35 - 0
src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisKey.java

@@ -0,0 +1,35 @@
+package cn.reghao.dfs.store.util.redis.ds;
+
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author reghao
+ * @date 2021-03-07 17:44:23
+ */
+@Component
+public class RedisKey {
+    private final RedisOperations<String, String> redisOps;
+
+    public RedisKey(RedisTemplate<String, String> redisTemplate) {
+        this.redisOps = redisTemplate;
+    }
+
+    public Long eval(String lua, String[] keys, Object... values) {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(lua, Long.class);
+        return redisOps.execute(redisScript, Arrays.asList(keys), values);
+    }
+
+    public Boolean expire(String key, long timeout) {
+        return redisOps.expire(key, timeout, TimeUnit.SECONDS);
+    }
+
+    public Boolean exists(String key) {
+        return false;
+    }
+}

+ 22 - 0
src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisList.java

@@ -0,0 +1,22 @@
+package cn.reghao.dfs.store.util.redis.ds;
+
+import org.springframework.data.redis.core.ListOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author reghao
+ * @date 2021-03-07 17:44:23
+ */
+@Component
+public class RedisList {
+    private final ListOperations<String, Object> listOps;
+
+    public RedisList(RedisTemplate<String, Object> redisTemplate) {
+        this.listOps = redisTemplate.opsForList();
+    }
+
+    public Object blpop(String key) {
+        return listOps.leftPop(key);
+    }
+}

+ 61 - 0
src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisSet.java

@@ -0,0 +1,61 @@
+package cn.reghao.dfs.store.util.redis.ds;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.SetOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Redis set 操作
+ *
+ * @author reghao
+ * @date 2021-03-07 17:40:50
+ */
+@Component
+public class RedisSet {
+    private final SetOperations<String, Object> setOps;
+
+    public RedisSet(RedisTemplate<String, Object> redisTemplate) {
+        this.setOps = redisTemplate.opsForSet();
+    }
+
+    public void sadd(String key, Object value) {
+        setOps.add(key, value);
+    }
+
+    public void smove(String srcKey, Object value, String dstKey) {
+        setOps.move(srcKey, value, dstKey);
+    }
+
+    public void sadd(String key, Object... values) {
+        setOps.add(key, values);
+    }
+
+    public Object spop(String key) {
+        return setOps.pop(key);
+    }
+
+    public List<Object> spop(String key, long count) {
+        return setOps.pop(key, count);
+    }
+
+    public Set<Object> smembers(String key) {
+        return setOps.members(key);
+    }
+
+    public boolean sismember(String key, Object value) {
+        Boolean ret = setOps.isMember(key, value);
+        return ret != null && ret;
+    }
+
+    public long scard(String key) {
+        Long size = setOps.size(key);
+        return size == null ? 0 : size;
+    }
+
+    public void remove(String key, Object value) {
+        setOps.remove(key, value);
+    }
+}

+ 36 - 0
src/main/java/cn/reghao/dfs/store/util/redis/ds/RedisString.java

@@ -0,0 +1,36 @@
+package cn.reghao.dfs.store.util.redis.ds;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author reghao
+ * @date 2021-03-07 17:44:23
+ */
+@Component
+public class RedisString {
+    private final ValueOperations<String, String> stringOps;
+
+    public RedisString(RedisTemplate<String, String> redisTemplate) {
+        this.stringOps = redisTemplate.opsForValue();
+    }
+
+    public void set(String key, String value) {
+        stringOps.set(key, value);
+    }
+
+    public void setWithTimeout(String key, String value, long timeout) {
+        stringOps.set(key, value, timeout, TimeUnit.SECONDS);
+    }
+
+    public Boolean setIfAbsent(String key, String value, long timeout) {
+        return stringOps.setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
+    }
+
+    public String get(String key) {
+        return stringOps.get(key);
+    }
+}

+ 31 - 0
src/main/resources/application-dev.yml

@@ -0,0 +1,31 @@
+spring:
+  redis:
+    database: 0
+    host: localhost
+    port: 6379
+    password: Dev@123456
+  datasource:
+    url: jdbc:mysql://localhost:3306/reghao_tnb_rdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
+    username: dev
+    password: Dev@123456
+dubbo:
+  scan:
+    base-packages: cn.reghao.dfs.store.rpc
+  protocol:
+    name: dubbo
+    port: 8102
+    host: 172.16.90.200
+  registry:
+    address: zookeeper://localhost:2181
+dfs:
+  domain: file.reghao.cn
+  dfsConfigs:
+    - baseDir: /home/reghao/opt/file/dfs/0/
+      group: group0
+      node: node0
+    - baseDir: /home/reghao/opt/file/dfs/1/
+      group: group1
+      node: node0
+    - baseDir: /home/reghao/opt/file/dfs/2/
+      group: group2
+      node: node0

+ 28 - 0
src/main/resources/application.yml

@@ -0,0 +1,28 @@
+server:
+  port: 8002
+spring:
+  servlet:
+    multipart:
+      max-file-size: 10GB
+      max-request-size: 10GB
+  application:
+    name: file-service
+  profiles:
+    active: @profile.active@
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    type: com.zaxxer.hikari.HikariDataSource
+    hikari:
+      minimum-idle: 5
+      maximum-pool-size: 10
+      auto-commit: true
+      idle-timeout: 30000
+      pool-name: EvaluationHikariCP
+      max-lifetime: 1800000
+      connection-timeout: 30000
+      connection-test-query: SELECT 1
+mybatis:
+  configuration:
+    map-underscore-to-camel-case: true
+  mapper-locations: classpath*:mapper/**/**.xml
+  type-aliases-package: cn.reghao.dfs.store.model.po

+ 68 - 0
src/main/resources/logback-spring.xml

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
+        <layout class="ch.qos.logback.classic.PatternLayout">
+            <pattern>
+                %d{HH:mm:ss.SSS} [%thread] %-5level %c %M %L - %msg%n
+            </pattern>
+        </layout>
+    </appender>
+
+    <!-- info 日志文件 -->
+    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>DENY</onMatch>
+            <onMismatch>ACCEPT</onMismatch>
+        </filter>
+        <encoder>
+            <pattern>
+                %d{HH:mm:ss.SSS} %-5level %c %M %L - %msg%n
+            </pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <!-- 滚动策略 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>
+                logs/info.%d.log
+            </fileNamePattern>
+        </rollingPolicy>
+    </appender>
+
+    <!-- error 日志文件 -->
+    <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>ERROR</level>
+        </filter>
+        <encoder>
+            <pattern>
+                %d{HH:mm:ss.SSS} %-5level %c %M %L - %msg%n
+            </pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>
+                logs/error.%d.log
+            </fileNamePattern>
+        </rollingPolicy>
+    </appender>
+
+    <springProfile name="dev">
+        <root level="info">
+            <appender-ref ref="consoleLog"></appender-ref>
+        </root>
+    </springProfile>
+    <springProfile name="test">
+        <root level="info">
+            <appender-ref ref="fileInfoLog"></appender-ref>
+            <appender-ref ref="fileErrorLog"></appender-ref>
+        </root>
+    </springProfile>
+    <springProfile name="prod">
+        <root level="info">
+            <appender-ref ref="fileInfoLog"></appender-ref>
+            <appender-ref ref="fileErrorLog"></appender-ref>
+        </root>
+    </springProfile>
+</configuration>

+ 37 - 0
src/main/resources/mapper/FileInfoMapper.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.dfs.store.db.mapper.FileInfoMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into file_info
+        (`id`,`deleted`,`create_time`,`update_time`,`file_id`,`sha256sum`,`filename`,`suffix`,`size`,`file_type`,`content_type`,`uploaded`)
+        values 
+        (#{id},#{deleted},#{createTime},#{updateTime},#{fileId},#{sha256sum},#{filename},#{suffix},#{size},#{fileType},#{contentType},#{uploaded})
+    </insert>
+    <insert id="saveAll" useGeneratedKeys="true" keyProperty="id">
+        insert into file_info
+        (`id`,`deleted`,`create_time`,`update_time`,`file_id`,`sha256sum`,`filename`,`suffix`,`size`,`file_type`)
+        values
+        <foreach collection="list" item="item" index="index" separator=",">
+            (#{item.id},#{item.deleted},#{item.createTime},#{item.updateTime},#{item.fileId},#{item.sha256sum},#{item.filename},#{item.suffix},#{item.size},#{item.fileType})
+        </foreach>
+    </insert>
+
+    <update id="updateSetUploaded">
+        update file_info set update_time=now(),file_type=#{fileType},content_type=#{contentType},uploaded=1
+        where file_id=#{fileId}
+    </update>
+
+    <select id="findBySha256sum" resultType="cn.reghao.dfs.store.model.dto.FileDto">
+        select file.file_id as fileId,file.filename,url.url from file_info file
+        inner join file_url url
+        on file.file_id=url.file_id and file.sha256sum=#{sha256sum}
+        order by url.create_time desc limit 1
+    </select>
+    <select id="findFileInfoBySha256sum" resultType="cn.reghao.dfs.store.model.po.FileInfo">
+        select * from file_info where sha256sum=#{sha256sum}
+    </select>
+    <select id="findByFileId" resultType="cn.reghao.dfs.store.model.po.FileInfo">
+        select * from file_info where file_id=#{fileId}
+    </select>
+</mapper>

+ 19 - 0
src/main/resources/mapper/FileUrlMapper.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.dfs.store.db.mapper.FileUrlMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into file_url
+        (`id`,`deleted`,`create_time`,`update_time`,`file_id`,`file_path`,`url`,`path`,`dc_id`,`node_id`)
+        values 
+        (#{id},#{deleted},#{createTime},#{updateTime},#{fileId},#{filePath},#{url},#{path},#{dcId},#{nodeId})
+    </insert>
+
+    <select id="findByUploadId" resultType="cn.reghao.dfs.store.model.dto.FileUrlDto">
+        select info.id,info.filename,url.path,url.url from file_url url
+        inner join file_user fileUser
+        inner join file_info info
+        on fileUser.file_id=url.file_id and info.file_id=url.file_id and fileUser.upload_id=#{uploadId}
+        limit 1
+    </select>
+</mapper>

+ 28 - 0
src/main/resources/mapper/FileUserMapper.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.dfs.store.db.mapper.FileUserMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into file_user
+        (`id`,`deleted`,`create_time`,`update_time`,`upload_id`,`upload_by`,`file_id`)
+        values 
+        (#{id},#{deleted},#{createTime},#{updateTime},#{uploadId},#{uploadBy},#{fileId})
+    </insert>
+
+    <select id="findByUploadId" resultType="cn.reghao.dfs.api.dto.FileInfoDto">
+        select info.filename,info.suffix,info.size,info.file_type as fileType,url.url
+        from file_info info
+        inner join file_url url
+        inner join file_user fileUser
+        on info.file_id=url.file_id and fileUser.file_id=info.file_id and fileUser.upload_id=#{uploadId}
+    </select>
+    <select id="findByFileAndUserId" resultType="cn.reghao.dfs.store.model.po.FileUser">
+        select * from file_user
+        where file_id=#{fileId} and upload_by=#{userId}
+    </select>
+    <select id="findFileInfoByUploadId" resultType="cn.reghao.dfs.store.model.po.FileInfo">
+        select fileInfo.* from file_info fileInfo
+        inner join file_user fileUser
+        on fileUser.file_id=fileInfo.file_id and fileUser.upload_id=#{uploadId}
+    </select>
+</mapper>

+ 31 - 0
src/main/resources/mapper/ImageFileMapper.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.dfs.store.db.mapper.ImageFileMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into image_file
+        (`id`,`deleted`,`create_time`,`update_time`,`file_id`,`base_url`,`width`,`height`)
+        values 
+        (#{id},#{deleted},#{createTime},#{updateTime},#{fileId},#{baseUrl},#{width},#{height})
+    </insert>
+    <insert id="saveAll" useGeneratedKeys="true" keyProperty="id">
+        insert into image_file
+        (`id`,`deleted`,`create_time`,`update_time`,`file_id`,`base_url`,`width`,`height`)
+        values
+        <foreach collection="list" item="item" index="index" separator=",">
+            (#{item.id},#{item.deleted},#{item.createTime},#{item.updateTime},#{item.fileId},#{item.baseUrl},#{item.width},#{item.height})
+        </foreach>
+    </insert>
+
+    <update id="updateSetUrl">
+        update image_file set update_time=now(), url=#{url}
+        where file_id=#{fileId}
+    </update>
+
+    <select id="findByFileId" resultType="cn.reghao.dfs.store.model.po.ImageFile">
+        select * from image_file where file_id=#{fileId}
+    </select>
+    <select id="findByFileIds" resultType="cn.reghao.dfs.store.model.po.ImageFile">
+        select * from image_file where file_id in #{list}
+    </select>
+</mapper>

+ 27 - 0
src/main/resources/mapper/VideoFileMapper.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="cn.reghao.dfs.store.db.mapper.VideoFileMapper">
+    <insert id="save" useGeneratedKeys="true" keyProperty="id">
+        insert into video_file
+        (`id`,`deleted`,`create_time`,`update_time`,`file_id`,`bandwidth`,`frame_rate`,`width`,`height`,`horizontal`,`rotate`,`url_type`,`base_url`,`backup_url`,`duration`,`base_cover_url`)
+        values 
+        (#{id},#{deleted},#{createTime},#{updateTime},#{fileId},#{bandwidth},#{frameRate},#{width},#{height},#{horizontal},#{rotate},#{urlType},#{baseUrl},#{backupUrl},#{duration},#{baseCoverUrl})
+    </insert>
+
+    <update id="updateSetCover">
+        update video_file set update_time=now(), base_cover_url=#{coverUrl}
+        where file_id=#{fileId}
+    </update>
+    <update id="updateSetUrl">
+        update video_file set update_time=now(), base_url=#{baseUrl}, base_cover_url=#{baseCoverUrl}
+        where file_id=#{fileId}
+    </update>
+
+    <select id="findByFileId" resultType="cn.reghao.dfs.store.model.po.VideoFile">
+        select * from video_file where file_id=#{fileId} order by create_time desc limit 1
+    </select>
+    <select id="findByMatchBaseUrl" resultType="cn.reghao.dfs.store.model.po.VideoFile">
+        select * from video_file where base_url like concat('%',#{pattern},'%')
+    </select>
+</mapper>

+ 9 - 0
src/test/java/FileTest.java

@@ -0,0 +1,9 @@
+
+/**
+ * @author reghao
+ * @date 2022-04-23 17:17:50
+ */
+public class FileTest {
+    public static void main(String[] args) {
+    }
+}