feat: adding controllers and base

This commit is contained in:
wzp 2024-11-01 11:12:29 +08:00
parent 41b5061436
commit 5ce5332891
40 changed files with 1022 additions and 22 deletions

View File

@ -38,6 +38,7 @@ dependencies {
implementation("org.springframework.shell:spring-shell-starter")
// https://mvnrepository.com/artifact/com.mybatis-flex/mybatis-flex-spring-boot3-starter
implementation("com.mybatis-flex:mybatis-flex-spring-boot3-starter:1.9.7")
annotationProcessor("com.mybatis-flex:mybatis-flex-processor:1.9.7")
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
implementation("com.alibaba.fastjson2:fastjson2:2.0.53")
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension
@ -55,7 +56,7 @@ dependencies {
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
implementation("com.mysql:mysql-connector-j:9.0.0")
compileOnly("org.projectlombok:lombok")
developmentOnly("org.springframework.boot:spring-boot-devtools")
/*developmentOnly("org.springframework.boot:spring-boot-devtools")*/
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")

View File

@ -1,14 +1,27 @@
package cn.wzpmc.filemanager;
import com.mybatisflex.core.audit.AuditManager;
import com.mybatisflex.core.audit.ConsoleMessageCollector;
import com.mybatisflex.core.audit.MessageCollector;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.shell.command.annotation.EnableCommand;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@ConfigurationPropertiesScan(basePackages = {"cn.wzpmc.filemanager.config"})
@MapperScan("cn.wzpmc.filemanager.mapper")
@EnableTransactionManagement
@EnableCommand
public class FileManagerApplication {
public static void main(String[] args) {
//开启审计功能
AuditManager.setAuditEnable(true);
MessageCollector collector = new ConsoleMessageCollector();
AuditManager.setMessageCollector(collector);
SpringApplication.run(FileManagerApplication.class, args);
}

View File

@ -0,0 +1,12 @@
package cn.wzpmc.filemanager.annotation;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorizationRequired {
Auth level() default Auth.user;
boolean force() default false;
}

View File

@ -0,0 +1,19 @@
package cn.wzpmc.filemanager.commands;
import cn.wzpmc.filemanager.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class AuthorizationCommands {
private final UserService userService;
@Autowired
public AuthorizationCommands(UserService userService) {
this.userService = userService;
}
@ShellMethod("创建一个密钥")
public void key(){
this.userService.genInviteCode();
}
}

View File

@ -0,0 +1,34 @@
package cn.wzpmc.filemanager.configuration;
import cn.wzpmc.filemanager.utils.AuthorizationArgumentResolver;
import cn.wzpmc.filemanager.utils.AuthorizationHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class AuthorizationConfiguration implements WebMvcConfigurer {
private final AuthorizationArgumentResolver authorizationArgumentResolver;
private final AuthorizationHandlerInterceptor authorizationHandlerInterceptor;
@Autowired
public AuthorizationConfiguration(AuthorizationArgumentResolver authorizationArgumentResolver, AuthorizationHandlerInterceptor authorizationHandlerInterceptor) {
this.authorizationArgumentResolver = authorizationArgumentResolver;
this.authorizationHandlerInterceptor = authorizationHandlerInterceptor;
}
@Override
public void addArgumentResolvers(@NonNull List<HandlerMethodArgumentResolver> resolvers) {
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
resolvers.add(authorizationArgumentResolver);
}
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(authorizationHandlerInterceptor);
}
}

View File

@ -0,0 +1,31 @@
package cn.wzpmc.filemanager.configuration;
import cn.wzpmc.filemanager.entities.files.ChunkChecked;
import cn.wzpmc.filemanager.entities.files.ChunkReady;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class RedisConfiguration {
private final RedisConnectionFactory redisConnectionFactory;
@Autowired
public RedisConfiguration(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
@Bean
public RedisTemplate<String, ChunkReady> uploadMapper() {
RedisTemplate<String, ChunkReady> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
public RedisTemplate<String, ChunkChecked> chunkUploadMapper() {
RedisTemplate<String, ChunkChecked> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

View File

@ -0,0 +1,40 @@
package cn.wzpmc.filemanager.controller;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.files.CheckChunkResponse;
import cn.wzpmc.filemanager.entities.files.DoneUploadRequest;
import cn.wzpmc.filemanager.entities.files.FileObject;
import cn.wzpmc.filemanager.entities.files.PrepareUploadRequest;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/file")
public class FileController {
private final FileService fileService;
@Autowired
public FileController(FileService fileService) {
this.fileService = fileService;
}
@PostMapping("/prepare")
public Result<String> prepareUploadChunks(@RequestBody PrepareUploadRequest prepareUploadRequest, @AuthorizationRequired UserVo user){
return fileService.prepareUploadChunks(prepareUploadRequest, user);
}
@PutMapping("/chunk/upload")
public Result<Void> uploadChunk(MultipartFile file,@RequestParam String id){
return fileService.uploadChunk(file, id);
}
@GetMapping("/chunk/check")
public Result<CheckChunkResponse> checkChunk(@RequestParam String hash, @RequestParam String id, @RequestParam long index){
return fileService.checkChunk(hash, id, index);
}
@PostMapping("/done")
public Result<FileObject> doneUpload(@RequestBody DoneUploadRequest data){
return fileService.doneUpload(data.getId());
}
}

View File

@ -0,0 +1,16 @@
package cn.wzpmc.filemanager.controller;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.exceptions.AuthorizationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<Result<Void>> handleAuthorizationException(AuthorizationException e) {
Result<Void> result = e.getResult();
return ResponseEntity.ok(result);
}
}

View File

@ -1,12 +1,35 @@
package cn.wzpmc.filemanager.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.user.UserLoginRequest;
import cn.wzpmc.filemanager.entities.user.UserRegisterRequest;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService= userService;
}
@PostMapping("/login")
public
public void login(@RequestBody UserLoginRequest loginRequest, HttpServletResponse response) {
userService.login(loginRequest, response);
}
@PutMapping("/register")
public void register(@RequestBody UserRegisterRequest registerRequest, HttpServletResponse response) {
userService.register(registerRequest, response);
}
@GetMapping("/invite")
@AuthorizationRequired(level = Auth.admin)
public Result<String> invite(){
return userService.invite();
}
}

View File

@ -0,0 +1,13 @@
package cn.wzpmc.filemanager.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
public class Page<T> {
private int total;
private List<T> data;
}

View File

@ -0,0 +1,105 @@
package cn.wzpmc.filemanager.entities;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@Slf4j
@Data
public class Result<T> {
private int status;
private String msg;
private T data;
private long timestamp;
protected Result() {
this.timestamp = System.currentTimeMillis();
}
protected Result(int status, String msg, T data) {
this();
this.status = status;
this.msg = msg;
this.data = data;
}
protected Result(HttpStatus status) {
this();
this.status = status.value();
this.msg = status.getReasonPhrase();
}
protected Result(HttpStatus status, String msg) {
this(status);
this.msg = msg;
}
protected Result(HttpStatus status, T data) {
this(status);
this.data = data;
}
protected Result(HttpStatus status, String msg, T data) {
this(status, msg);
this.data = data;
}
public static <T> Result<T> success() {
return new Result<>(HttpStatus.OK);
}
public static <T> Result<T> success(String msg) {
return new Result<>(HttpStatus.OK, msg);
}
public static <T> Result<T> success(T data) {
return new Result<>(HttpStatus.OK, data);
}
public static <T> Result<T> success(String msg, T data) {
return new Result<>(HttpStatus.OK, msg, data);
}
public static <T> Result<T> failed() {
return new Result<>(HttpStatus.FORBIDDEN);
}
public static <T> Result<T> failed(String msg) {
return new Result<>(HttpStatus.FORBIDDEN, msg);
}
public static <T> Result<T> failed(HttpStatus status) {
return new Result<>(status);
}
public static <T> Result<T> failed(HttpStatus status, String msg) {
return new Result<>(status, msg);
}
public static <T> Result<T> create() {
return new Result<>();
}
public Result<T> status(int status) {
this.status = status;
return this;
}
public Result<T> status(HttpStatus status) {
this.status = status.value();
this.msg = status.getReasonPhrase();
return this;
}
public Result<T> msg(String msg) {
this.msg = msg;
return this;
}
public Result<T> data(T data) {
this.data = data;
return this;
}
public void writeToResponse(HttpServletResponse response){
try(ServletOutputStream outputStream = response.getOutputStream()){
writeToOutputStream(outputStream);
} catch (IOException e) {
log.trace("写出到流失败,", e);
}
}
public void writeToOutputStream(OutputStream stream) throws IOException {
stream.write(JSON.toJSONString(this).getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -0,0 +1,18 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CheckChunkResponse {
private boolean has;
private String uploadCode;
public static CheckChunkResponse has() {
return new CheckChunkResponse(true, null);
}
public static CheckChunkResponse shouldUpload(String uploadCode) {
return new CheckChunkResponse(false, uploadCode);
}
}

View File

@ -0,0 +1,14 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
@Data
@AllArgsConstructor
public class ChunkChecked implements Serializable {
private String fileId;
private String hash;
private long index;
}

View File

@ -0,0 +1,11 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.Data;
import java.io.Serializable;
@Data
public class ChunkReady implements Serializable {
private long fileId;
private long length;
}

View File

@ -0,0 +1,8 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.Data;
@Data
public class DoneUploadRequest {
private String id;
}

View File

@ -0,0 +1,12 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.Data;
@Data
public class PrepareUploadRequest {
private String name;
private String ext;
private Long size;
private int folder;
private String fullSha1;
}

View File

@ -1,8 +0,0 @@
package cn.wzpmc.filemanager.entities.user;
import lombok.Data;
@Data
public class UserAuthResponse {
private String
}

View File

@ -0,0 +1,9 @@
package cn.wzpmc.filemanager.entities.user;
import lombok.Data;
@Data
public class UserLoginRequest {
private String username;
private String password;
}

View File

@ -0,0 +1,12 @@
package cn.wzpmc.filemanager.entities.user;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import lombok.Data;
@Data
public class UserRegisterRequest {
private String username;
private String password;
private Auth auth;
private String inviteCode;
}

View File

@ -1,5 +1,12 @@
package cn.wzpmc.filemanager.entities.user.enums;
import com.mybatisflex.annotation.EnumValue;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum Auth {
ADMIN, USER
admin(1, "admin"), user(0, "user");
public final int value;
@EnumValue
public final String name;
}

View File

@ -1,4 +1,16 @@
package cn.wzpmc.filemanager.entities.vo;
public record ChunkVo(int id, String sha1, long size) {
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;
@Table("chunk")
@Data
public class ChunkVo {
@Id(keyType = KeyType.Auto)
private int id;
private String sha1;
private long size;
}

View File

@ -1,4 +1,14 @@
package cn.wzpmc.filemanager.entities.vo;
public record FileChunkVo(int file, int chunk, int index) {
import com.mybatisflex.annotation.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
@Table("file_chunks")
@Data
@AllArgsConstructor
public class FileChunkVo {
private long file;
private long chunk;
private long index;
}

View File

@ -1,6 +1,26 @@
package cn.wzpmc.filemanager.entities.vo;
import java.util.Date;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import java.util.Date;
import java.util.Objects;
@Table("file")
@Data
public class FileVo {
@Id(keyType = KeyType.Auto)
private int id;
private String name;
private String ext;
private String mime;
private String sha1;
private int uploader;
private int folder;
@Column(onInsertValue = "now()")
private Date uploadTime;
public record FileVo(int id, String name, String ext, String mime, String sha1, int uploader, int folder, Date uploadTime) {
}

View File

@ -1,6 +1,22 @@
package cn.wzpmc.filemanager.entities.vo;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import java.util.Date;
public record FolderVo(int id, String name, int parent, int creator, Date createTime) {
@Table("folder")
@Data
public class FolderVo {
@Id(keyType = KeyType.Auto)
private int id;
private String name;
private int parent;
private int creator;
@Column(onInsertValue = "now()")
private Date createTime;
}

View File

@ -1,8 +1,19 @@
package cn.wzpmc.filemanager.entities.vo;
import cn.wzpmc.filemanager.entities.statistics.enums.Actions;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import java.util.Date;
public record StatisticsVo(int actor, Actions action, String params, Date time) {
@Table("statistics")
@Data
public class StatisticsVo {
private int actor;
private Actions action;
private String params;
@Column(onInsertValue = "now()")
private Date time;
}

View File

@ -1,6 +1,27 @@
package cn.wzpmc.filemanager.entities.vo;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
@Table("user")
@Data
public class UserVo {
@Id(keyType = KeyType.Auto)
private int id;
private String name;
private String password;
private Auth auth;
@Column(isLogicDelete = true, onInsertValue = "0")
private boolean banned;
public UserVo(String name, String password, Auth auth) {
this.name = name;
this.password = password;
this.auth = auth;
}
public record UserVo(int id, String name, String password, Auth auth, boolean banned) {
}

View File

@ -0,0 +1,13 @@
package cn.wzpmc.filemanager.exceptions;
import cn.wzpmc.filemanager.entities.Result;
import lombok.Getter;
@Getter
public class AuthorizationException extends RuntimeException {
private final Result<Void> result;
public AuthorizationException(Result<Void> result) {
super(result.getMsg());
this.result = result;
}
}

View File

@ -0,0 +1,7 @@
package cn.wzpmc.filemanager.mapper;
import cn.wzpmc.filemanager.entities.vo.ChunkVo;
import com.mybatisflex.core.BaseMapper;
public interface ChunkMapper extends BaseMapper<ChunkVo> {
}

View File

@ -0,0 +1,7 @@
package cn.wzpmc.filemanager.mapper;
import cn.wzpmc.filemanager.entities.vo.FileChunkVo;
import com.mybatisflex.core.BaseMapper;
public interface FileChunksMapper extends BaseMapper<FileChunkVo> {
}

View File

@ -0,0 +1,7 @@
package cn.wzpmc.filemanager.mapper;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import com.mybatisflex.core.BaseMapper;
public interface FileMapper extends BaseMapper<FileVo> {
}

View File

@ -0,0 +1,7 @@
package cn.wzpmc.filemanager.mapper;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import com.mybatisflex.core.BaseMapper;
public interface FolderMapper extends BaseMapper<FolderVo> {
}

View File

@ -0,0 +1,7 @@
package cn.wzpmc.filemanager.mapper;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import com.mybatisflex.core.BaseMapper;
public interface UserMapper extends BaseMapper<UserVo> {
}

View File

@ -0,0 +1,166 @@
package cn.wzpmc.filemanager.service;
import cn.wzpmc.filemanager.config.FileManagerProperties;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.files.*;
import cn.wzpmc.filemanager.entities.vo.ChunkVo;
import cn.wzpmc.filemanager.entities.vo.FileChunkVo;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.mapper.ChunkMapper;
import cn.wzpmc.filemanager.mapper.FileChunksMapper;
import cn.wzpmc.filemanager.mapper.FileMapper;
import cn.wzpmc.filemanager.mapper.FolderMapper;
import cn.wzpmc.filemanager.utils.RandomUtils;
import com.mybatisflex.core.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import static cn.wzpmc.filemanager.entities.vo.table.ChunkVoTableDef.CHUNK_VO;
import static cn.wzpmc.filemanager.entities.vo.table.FileChunkVoTableDef.FILE_CHUNK_VO;
import static cn.wzpmc.filemanager.entities.vo.table.FileVoTableDef.FILE_VO;
@Service
@RequiredArgsConstructor
public class FileService {
private final RedisTemplate<String, ChunkReady> uploadMapper;
private final RedisTemplate<String, ChunkChecked> chunkUploadMapper;
private final FileMapper fileMapper;
private final ChunkMapper chunkMapper;
private final FileChunksMapper fileChunksMapper;
private final FolderMapper folderMapper;
private final RandomUtils randomUtils;
private final FileManagerProperties properties;
private static final String UPLOAD_FILE_PREPARE_HEAD = "UPLOAD_";
private static final String CHUNK_PREPARE_HEAD = "UPLOAD_CHUNK_";
public Result<String> prepareUploadChunks(PrepareUploadRequest prepareUploadRequest, UserVo user) {
String name = prepareUploadRequest.getName();
String ext = prepareUploadRequest.getExt();
int folder = prepareUploadRequest.getFolder();
if (this.fileMapper.selectCountByCondition(FILE_VO.NAME.eq(name).and(FILE_VO.EXT.eq(ext)).and(FILE_VO.FOLDER.eq(folder))) > 0) {
return Result.failed(HttpStatus.CONFLICT, "文件已存在!");
}
String fullSha1 = prepareUploadRequest.getFullSha1();
FileVo otherSameFile = this.fileMapper.selectOneByCondition(FILE_VO.SHA1.eq(fullSha1));
ChunkReady chunkReady = new ChunkReady();
FileVo fileVo = new FileVo();
fileVo.setUploader(user.getId());
fileVo.setName(name);
fileVo.setExt(ext);
fileVo.setFolder(folder);
fileVo.setSha1(fullSha1);
this.fileMapper.insert(fileVo);
int fid = fileVo.getId();
if (otherSameFile != null) {
int id = otherSameFile.getId();
List<FileChunkVo> fileChunkVos = this.fileChunksMapper.selectListByCondition(FILE_CHUNK_VO.FILE.eq(id));
for (FileChunkVo fileChunkVo : fileChunkVos) {
fileChunkVo.setFile(fid);
this.fileChunksMapper.insert(fileChunkVo);
}
return Result.failed(HttpStatus.FOUND, "后台存在相同文件,无需上传!");
}
String uploadId = this.randomUtils.generatorRandomString(40);
chunkReady.setFileId(fid);
chunkReady.setLength(prepareUploadRequest.getSize());
uploadMapper.opsForValue().set(UPLOAD_FILE_PREPARE_HEAD + uploadId, chunkReady);
return Result.success("成功", uploadId);
}
@SneakyThrows
@Transactional
public Result<Void> uploadChunk(MultipartFile file, String id) {
ValueOperations<String, ChunkChecked> chunkOps = chunkUploadMapper.opsForValue();
ChunkChecked chunkData = chunkOps.getAndDelete(CHUNK_PREPARE_HEAD + id);
if (chunkData == null) {
return Result.failed(HttpStatus.NOT_FOUND, "未知的文件块");
}
long size = file.getSize();
if (size > 64 * 1024 * 1024) {
return Result.failed(HttpStatus.PAYLOAD_TOO_LARGE, "文件块不应大于64MB");
}
byte[] bytes = file.getBytes();
String s = DigestUtils.sha1Hex(bytes);
if (!s.equals(chunkData.getHash())) {
return Result.failed(HttpStatus.CONFLICT, "文件块内容错误!");
}
ChunkVo chunkVo = new ChunkVo();
chunkVo.setSize(size);
chunkVo.setSha1(s);
String hashHead = s.substring(0, 2);
File hashHeadFolder = new File(properties.getSavePath(), hashHead);
if (!hashHeadFolder.exists()) {
if (!hashHeadFolder.mkdirs()) {
return Result.failed(HttpStatus.INTERNAL_SERVER_ERROR, "写入文件块出现错误,创建文件夹失败");
}
}
File chunkFile = new File(hashHeadFolder, s);
if (!chunkFile.createNewFile()) {
return Result.failed(HttpStatus.INTERNAL_SERVER_ERROR, "写入文件块出现错误,无法写入块文件");
}
try(FileOutputStream fos = new FileOutputStream(chunkFile)) {
fos.write(bytes);
}
String fileId = chunkData.getFileId();
ChunkReady chunkReady = uploadMapper.opsForValue().get(UPLOAD_FILE_PREPARE_HEAD + fileId);
assert chunkReady != null;
long fileTableId = chunkReady.getFileId();
this.chunkMapper.insert(chunkVo);
int chunkId = chunkVo.getId();
FileChunkVo fileChunkVo = new FileChunkVo(fileTableId, chunkId, chunkData.getIndex());
this.fileChunksMapper.insert(fileChunkVo);
return Result.success("成功");
}
public Result<CheckChunkResponse> checkChunk(String hash, String id, long index) {
ChunkReady chunkReady = uploadMapper.opsForValue().get(UPLOAD_FILE_PREPARE_HEAD + id);
if (chunkReady == null) {
return Result.failed(HttpStatus.NOT_FOUND, "未知的文件ID");
}
long fileId = chunkReady.getFileId();
ChunkVo chunkVo = chunkMapper.selectOneByCondition(CHUNK_VO.SHA1.eq(hash));
if (chunkVo != null) {
FileChunkVo fileChunkVo = new FileChunkVo(fileId, chunkVo.getId(), index);
this.fileChunksMapper.insert(fileChunkVo);
return Result.success(CheckChunkResponse.has());
}
ValueOperations<String, ChunkChecked> chunkOps = chunkUploadMapper.opsForValue();
String chunkId = randomUtils.generatorRandomString(40);
chunkOps.set(CHUNK_PREPARE_HEAD + chunkId, new ChunkChecked(id, hash, index));
return Result.success(CheckChunkResponse.shouldUpload(chunkId));
}
public Result<FileObject> doneUpload(String id) {
ChunkReady andDelete = uploadMapper.opsForValue().getAndDelete(UPLOAD_FILE_PREPARE_HEAD + id);
if (andDelete == null) {
return Result.failed(HttpStatus.NOT_FOUND, "未知的文件ID");
}
long totalLength = andDelete.getLength();
long l = calcFileSize(andDelete.getFileId());
if (l != totalLength) {
fileMapper.deleteById(andDelete.getFileId());
return Result.failed(HttpStatus.LENGTH_REQUIRED, "应收到" + totalLength + "字节,但只收到" + l + "字节!");
}
return Result.success();
}
private long calcFileSize(long id) {
List<Long> longs = this.fileChunksMapper.selectListByQueryAs(new QueryWrapper().select(CHUNK_VO.SIZE).from(FILE_CHUNK_VO).where(FILE_CHUNK_VO.FILE.eq(id)).leftJoin(CHUNK_VO).on(CHUNK_VO.ID.eq(FILE_CHUNK_VO.CHUNK)), Long.class);
long sum = 0L;
for (Long aLong : longs) {
sum += aLong;
}
return sum;
}
}

View File

@ -0,0 +1,96 @@
package cn.wzpmc.filemanager.service;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.user.UserLoginRequest;
import cn.wzpmc.filemanager.entities.user.UserRegisterRequest;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.mapper.UserMapper;
import cn.wzpmc.filemanager.utils.JwtUtils;
import cn.wzpmc.filemanager.utils.RandomUtils;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import static cn.wzpmc.filemanager.entities.vo.table.UserVoTableDef.USER_VO;
@Slf4j
@Service
public class UserService {
private final UserMapper userMapper;
private final JwtUtils jwtUtils;
private final StringRedisTemplate authTemplate;
private final RandomUtils randomUtils;
@Autowired
public UserService(UserMapper userMapper, JwtUtils jwtUtils, StringRedisTemplate authTemplate, RandomUtils randomUtils) {
this.userMapper = userMapper;
this.jwtUtils = jwtUtils;
this.authTemplate = authTemplate;
this.randomUtils = randomUtils;
long count = this.userMapper.selectCountByQuery(new QueryWrapper());
if (count == 0) {
String s = genInviteCode();
log.info("生成了管理员密钥:{}有效期15分钟若失效请使用控制台命令/key或重启后端重新生成", s);
}
}
public void login(UserLoginRequest request, HttpServletResponse response) {
String username = request.getUsername();
String password = request.getPassword();
String sha1edPassword = DigestUtils.sha1Hex(password);
QueryCondition findUserCondition = USER_VO.NAME.eq(username).and(USER_VO.PASSWORD.eq(sha1edPassword));
long count = this.userMapper.selectCountByCondition(findUserCondition);
if (count < 0) {
Result.failed(HttpStatus.UNAUTHORIZED, "账号或密码错误").writeToResponse(response);
return;
}
UserVo userVo = this.userMapper.selectOneByCondition(findUserCondition);
String token = this.jwtUtils.createToken(userVo.getId());
response.addHeader("Add-Authorization", token);
Result.success("登录成功").writeToResponse(response);
}
public void register(UserRegisterRequest request, HttpServletResponse response) {
String username = request.getUsername();
String password = request.getPassword();
Auth auth = request.getAuth();
if (this.userMapper.selectCountByCondition(USER_VO.NAME.eq(username)) > 0) {
Result.failed(HttpStatus.CONFLICT).msg("用户名已存在,若需要修改密码,请联系网站管理员处理").writeToResponse(response);
return;
}
if (auth.equals(Auth.admin)) {
String inviteCode = request.getInviteCode();
ValueOperations<String, String> ops = authTemplate.opsForValue();
String andDelete = ops.getAndDelete(inviteCode);
if (andDelete == null) {
Result.failed(HttpStatus.NOT_FOUND, "过期或无效的邀请码").writeToResponse(response);
return;
}
}
UserVo userVo = new UserVo(username, password, auth);
this.userMapper.insert(userVo);
int id = userVo.getId();
String token = this.jwtUtils.createToken(id);
response.addHeader("Add-Authorization", token);
Result.success("注册成功!").writeToResponse(response);
}
public Result<String> invite() {
String s = genInviteCode();
return Result.success("生成了一个有效期15分钟的邀请码", s);
}
public String genInviteCode() {
ValueOperations<String, String> ops = authTemplate.opsForValue();
String s = this.randomUtils.generatorRandomString(8);
log.info("生成了新的邀请码:{}", s);
ops.set(s, "", 15, TimeUnit.MINUTES);
return s;
}
}

View File

@ -0,0 +1,35 @@
package cn.wzpmc.filemanager.utils;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.exceptions.AuthorizationException;
import jakarta.annotation.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public class AuthorizationArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthorizationUtils authorizationUtils;
@Autowired
public AuthorizationArgumentResolver(AuthorizationUtils authorizationUtils){
this.authorizationUtils = authorizationUtils;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthorizationRequired.class);
}
@Override
@Nullable
public Object resolveArgument(@NonNull MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws AuthorizationException {
AuthorizationRequired parameterAnnotation = parameter.getParameterAnnotation(AuthorizationRequired.class);
assert parameterAnnotation != null;
return this.authorizationUtils.auth(webRequest, parameterAnnotation);
}
}

View File

@ -0,0 +1,33 @@
package cn.wzpmc.filemanager.utils;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.exceptions.AuthorizationException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.reflect.Method;
@Component
public class AuthorizationHandlerInterceptor implements HandlerInterceptor {
private final AuthorizationUtils authorizationUtils;
@Autowired
public AuthorizationHandlerInterceptor(AuthorizationUtils authorizationUtils) {
this.authorizationUtils = authorizationUtils;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws AuthorizationException {
if (handler instanceof HandlerMethod method) {
if (!method.hasMethodAnnotation(AuthorizationRequired.class)) {
return true;
}
AuthorizationRequired annotation = method.getMethodAnnotation(AuthorizationRequired.class);
return authorizationUtils.auth(request, annotation);
}
return true;
}
}

View File

@ -0,0 +1,62 @@
package cn.wzpmc.filemanager.utils;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.exceptions.AuthorizationException;
import cn.wzpmc.filemanager.mapper.UserMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Optional;
@Slf4j
@Component
public class AuthorizationUtils {
private final JwtUtils jwtUtils;
private final UserMapper userMapper;
@Autowired
public AuthorizationUtils(JwtUtils jwtUtils, UserMapper userMapper) {
this.jwtUtils = jwtUtils;
this.userMapper = userMapper;
}
private UserVo auth(String header, AuthorizationRequired authorizationRequired) throws AuthorizationException {
log.info("auth {} with token {}", authorizationRequired, header);
if (header == null) {
throw new AuthorizationException(Result.failed(HttpStatus.UNAUTHORIZED, "未找到token"));
}
Auth level = authorizationRequired.level();
Optional<Integer> user = this.jwtUtils.getUser(header);
if (user.isEmpty()) {
throw new AuthorizationException(Result.failed(HttpStatus.UNAUTHORIZED, "token错误或已过期"));
}
Integer i = user.get();
UserVo userVo = this.userMapper.selectOneById(i);
if (userVo == null) {
throw new AuthorizationException(Result.failed(HttpStatus.UNAUTHORIZED, "用户不存在"));
}
Auth auth = userVo.getAuth();
if (authorizationRequired.force()) {
if (auth.value == level.value) {
return userVo;
}
}else {
if (auth.value >= level.value) {
return userVo;
}
}
throw new AuthorizationException(Result.failed(HttpStatus.UNAUTHORIZED, "权限不足"));
}
public UserVo auth(WebRequest request, AuthorizationRequired authorizationRequired) throws AuthorizationException {
return auth(request.getHeader("Authorization"), authorizationRequired);
}
public boolean auth(HttpServletRequest request, AuthorizationRequired authorizationRequired) throws AuthorizationException {
auth(request.getHeader("Authorization"), authorizationRequired);
return true;
}
}

View File

@ -0,0 +1,56 @@
package cn.wzpmc.filemanager.utils;
import cn.wzpmc.filemanager.config.FileManagerProperties;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Optional;
import java.util.Random;
@Component
@Log4j2
public class JwtUtils {
private final Algorithm hmacKey;
private final RandomUtils randomUtils;
private String generatorHmacKey(){
return this.randomUtils.generatorRandomString(16);
}
@Autowired
public JwtUtils(FileManagerProperties properties, RandomUtils randomUtils){
this.randomUtils = randomUtils;
String hmacKey = properties.getHmacKey();
String key;
if ("RANDOM".equalsIgnoreCase(hmacKey)){
key = this.generatorHmacKey();
log.info("Using Random Hmac Key: {}", key);
}else{
key = hmacKey;
}
this.hmacKey = Algorithm.HMAC512(key);
}
public String createToken(int uid){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.HOUR,24 * 5);
JWTCreator.Builder builder = JWT.create();
builder.withClaim("uid", uid);
builder.withExpiresAt(instance.getTime());
return builder.sign(this.hmacKey);
}
public Optional<Integer> getUser(String token){
DecodedJWT verify;
try {
verify = JWT.require(this.hmacKey).build().verify(token);
}catch (Exception e){
return Optional.empty();
}
Claim uid = verify.getClaim("uid");
return Optional.of(uid.asInt());
}
}

View File

@ -0,0 +1,19 @@
package cn.wzpmc.filemanager.utils;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
@NoArgsConstructor
public class RandomUtils {
public String generatorRandomString(int length) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
int c = new Random().nextInt(33, 126);
builder.append((char) c);
}
return builder.toString();
}
}

View File

@ -13,6 +13,11 @@ spring:
host: "server.wzpmc.cn"
database: 3
password: "MyCraftAdmin123"
servlet:
multipart:
max-file-size: 100GB
max-request-size: 100GB
wzp:
filemanager:
save-path: "./file"
save-path: "./file"
hmac-key: "V(LWJ6D5*4%,Hk{1"