feat: adding path system

This commit is contained in:
Wzp-2008 2024-11-19 11:02:20 +08:00
parent 5ce5332891
commit 5b01a66371
46 changed files with 894 additions and 341 deletions

View File

@ -7,6 +7,7 @@ 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.scheduling.annotation.EnableAsync;
import org.springframework.shell.command.annotation.EnableCommand;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@MapperScan("cn.wzpmc.filemanager.mapper")
@EnableTransactionManagement
@EnableCommand
@EnableAsync
public class FileManagerApplication {
public static void main(String[] args) {

View File

@ -0,0 +1,11 @@
package cn.wzpmc.filemanager.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Address {
}

View File

@ -1,5 +1,6 @@
package cn.wzpmc.filemanager.commands;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.standard.ShellComponent;
@ -14,6 +15,6 @@ public class AuthorizationCommands {
}
@ShellMethod("创建一个密钥")
public void key(){
this.userService.genInviteCode();
this.userService.genInviteCode(UserVo.CONSOLE, "0.0.0.0");
}
}

View File

@ -1,6 +1,7 @@
package cn.wzpmc.filemanager.config;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.File;
@ -8,11 +9,8 @@ import java.io.File;
@ConfigurationProperties(prefix = "wzp.filemanager")
@Data
public class FileManagerProperties {
@Getter
private File savePath;
private String hmacKey = "RANDOM";
private FFmpegConfiguration ffmpeg;
public File getSavePath() {
System.out.println(this.savePath);
return this.savePath;
}
}

View File

@ -0,0 +1,21 @@
package cn.wzpmc.filemanager.configuration;
import cn.wzpmc.filemanager.utils.AddressArgumentResolver;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class AddressConfiguration implements WebMvcConfigurer {
private final AddressArgumentResolver addressArgumentResolver;
@Override
public void addArgumentResolvers(@NonNull List<HandlerMethodArgumentResolver> resolvers) {
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
resolvers.add(addressArgumentResolver);
}
}

View File

@ -1,7 +1,7 @@
package cn.wzpmc.filemanager.configuration;
import cn.wzpmc.filemanager.entities.files.ChunkChecked;
import cn.wzpmc.filemanager.entities.files.ChunkReady;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -9,6 +9,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
@Getter
public class RedisConfiguration {
private final RedisConnectionFactory redisConnectionFactory;
@ -17,14 +18,8 @@ public class RedisConfiguration {
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<>();
public RedisTemplate<String, FileVo> linkMapper() {
RedisTemplate<String, FileVo> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

View File

@ -1,40 +1,78 @@
package cn.wzpmc.filemanager.controller;
import cn.wzpmc.filemanager.annotation.Address;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.entities.PageResult;
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.files.DeleteRequest;
import cn.wzpmc.filemanager.entities.files.FolderCreateRequest;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import cn.wzpmc.filemanager.entities.files.enums.FileType;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import java.util.Date;
@RestController
@RequestMapping("/api/file")
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@Autowired
public FileController(FileService fileService) {
this.fileService = fileService;
@PutMapping("/upload")
public Result<FileVo> simpleUpload(MultipartHttpServletRequest file, @AuthorizationRequired UserVo user, @Address String address) {
return fileService.simpleUpload(file, user, address);
}
@PostMapping("/prepare")
public Result<String> prepareUploadChunks(@RequestBody PrepareUploadRequest prepareUploadRequest, @AuthorizationRequired UserVo user){
return fileService.prepareUploadChunks(prepareUploadRequest, user);
@GetMapping("/get")
public Result<PageResult<RawFileObject>> getFilePager(@RequestParam long page, @RequestParam int num, @RequestParam long folder, @Address String address) {
return fileService.getFilePager(page, num, folder, address);
}
@PutMapping("/chunk/upload")
public Result<Void> uploadChunk(MultipartFile file,@RequestParam String id){
return fileService.uploadChunk(file, id);
@PostMapping("/mkdir")
public Result<FolderVo> mkdir(@RequestBody FolderCreateRequest request, @AuthorizationRequired UserVo user, @Address String address) {
return fileService.mkdir(request, user, address);
}
@GetMapping("/chunk/check")
public Result<CheckChunkResponse> checkChunk(@RequestParam String hash, @RequestParam String id, @RequestParam long index){
return fileService.checkChunk(hash, id, index);
@GetMapping("/detail")
public Result<FileVo> getFileDetail(@RequestParam long id) {
return fileService.getFileDetail(id);
}
@PostMapping("/done")
public Result<FileObject> doneUpload(@RequestBody DoneUploadRequest data){
return fileService.doneUpload(data.getId());
@DeleteMapping("/rm")
public Result<Void> delete(@RequestBody DeleteRequest request, @AuthorizationRequired UserVo user, @Address String address) {
return fileService.delete(request, user, address);
}
@GetMapping("/link")
public Result<String> getFileLink(@RequestParam long id, @Address String address, HttpServletRequest request) {
return fileService.getFileLink(id, address, request);
}
@GetMapping("/download/{id}")
public void downloadFile(@PathVariable String id, @RequestHeader(value = "Range", defaultValue = "null") String range, HttpServletResponse response) {
fileService.downloadFile(id, range, response);
}
@GetMapping("/share")
public Result<String> shareFile(@RequestParam Long id, @RequestParam(defaultValue = "9999-12-31") Date lastCouldDownloadTime, @RequestParam(defaultValue = "-1") int maxDownloadCount) {
return fileService.shareFile(id, lastCouldDownloadTime, maxDownloadCount);
}
@GetMapping("/path/resolve")
public Result<RawFileObject> resolveFileDetail(@RequestParam String path) {
return fileService.resolveFileDetail(path);
}
@GetMapping("/path/{id}")
public Result<String> findFilePathById(@PathVariable("id") long id, @RequestParam(value = "type", defaultValue = "FILE") FileType type) {
return type.equals(FileType.FILE) ? fileService.findFilePathById(id) : fileService.findFolderPathById(id);
}
}

View File

@ -1,5 +1,6 @@
package cn.wzpmc.filemanager.controller;
import cn.wzpmc.filemanager.annotation.Address;
import cn.wzpmc.filemanager.annotation.AuthorizationRequired;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.user.UserLoginRequest;
@ -8,28 +9,33 @@ 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService= userService;
this.userService = userService;
}
@PostMapping("/login")
public void login(@RequestBody UserLoginRequest loginRequest, HttpServletResponse response) {
userService.login(loginRequest, response);
public void login(@RequestBody UserLoginRequest loginRequest, HttpServletResponse response, @Address String address) {
userService.login(loginRequest, response, address);
}
@PutMapping("/register")
public void register(@RequestBody UserRegisterRequest registerRequest, HttpServletResponse response) {
userService.register(registerRequest, response);
public void register(@RequestBody UserRegisterRequest registerRequest, HttpServletResponse response, @Address String address) {
userService.register(registerRequest, response, address);
}
@GetMapping("/invite")
@AuthorizationRequired(level = Auth.admin)
public Result<String> invite(){
return userService.invite();
public Result<String> invite(@AuthorizationRequired(level = Auth.admin) UserVo userVo, @Address String address) {
return userService.invite(userVo, address);
}
}

View File

@ -7,7 +7,7 @@ import java.util.List;
@Data
@AllArgsConstructor
public class Page<T> {
private int total;
public class PageResult<T> {
private long total;
private List<T> data;
}

View File

@ -3,10 +3,7 @@ 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;
@ -93,6 +90,7 @@ public class Result<T> {
}
public void writeToResponse(HttpServletResponse response){
response.addHeader("Content-Type", "application/json; charset=utf-8");
try(ServletOutputStream outputStream = response.getOutputStream()){
writeToOutputStream(outputStream);
} catch (IOException e) {

View File

@ -1,18 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,11 +0,0 @@
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,10 @@
package cn.wzpmc.filemanager.entities.files;
import cn.wzpmc.filemanager.entities.files.enums.FileType;
import lombok.Data;
@Data
public class DeleteRequest {
private FileType type;
private long id;
}

View File

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

View File

@ -1,11 +0,0 @@
package cn.wzpmc.filemanager.entities.files;
import cn.wzpmc.filemanager.entities.vo.ChunkVo;
import lombok.Data;
import java.util.List;
@Data
public class FileData {
private List<ChunkVo> chunks;
}

View File

@ -1,12 +0,0 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class FileObject extends RawFileObject {
private String ext;
private String mime;
private String sha1;
}

View File

@ -0,0 +1,9 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.Data;
@Data
public class FolderCreateRequest {
private long parent;
private String name;
}

View File

@ -1,12 +0,0 @@
package cn.wzpmc.filemanager.entities.files;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class FolderObject extends RawFileObject {
private List<RawFileObject> children;
}

View File

@ -1,12 +0,0 @@
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,14 +1,35 @@
package cn.wzpmc.filemanager.entities.files;
import cn.wzpmc.filemanager.entities.files.enums.FileType;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
public abstract class RawFileObject {
@AllArgsConstructor
@NoArgsConstructor
public class RawFileObject {
private long id;
private String name;
private Integer id;
private String uploader;
private Date createTime;
private String ext;
private long size;
private long owner;
private long parent;
private Date time;
private FileType type;
public static RawFileObject of(FileVo file) {
return new RawFileObject(file.getId(), file.getName(), file.getExt(), file.getSize(), file.getUploader(), file.getFolder(), file.getUploadTime(), FileType.FILE);
}
public static RawFileObject of(FolderVo folder) {
return new RawFileObject(folder.getId(), folder.getName(), null, -1, folder.getCreator(), folder.getParent(), folder.getCreateTime(), FileType.FOLDER);
}
public static String getRawFileName(RawFileObject object){
return object.type.equals(FileType.FILE) ? object.getName() + '.' + object.getExt() : object.getName();
}
}

View File

@ -1,5 +1,5 @@
package cn.wzpmc.filemanager.entities.files.enums;
public enum FileType {
FILE, FOLDER;
FILE, FOLDER
}

View File

@ -1,5 +1,5 @@
package cn.wzpmc.filemanager.entities.statistics.enums;
public enum Actions {
UPLOAD, DELETE, ACCESS, DOWNLOAD, SEARCH
UPLOAD, DELETE, ACCESS, DOWNLOAD, SEARCH, LOGIN, INVITE, REGISTER
}

View File

@ -1,16 +0,0 @@
package cn.wzpmc.filemanager.entities.vo;
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,14 +0,0 @@
package cn.wzpmc.filemanager.entities.vo;
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

@ -6,20 +6,21 @@ import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
@Table("file")
@Data
public class FileVo {
public class FileVo implements Serializable {
@Id(keyType = KeyType.Auto)
private int id;
private long id;
private String name;
private String ext;
private String mime;
private String sha1;
private int uploader;
private int folder;
private String hash;
private long uploader;
private long folder;
private long size;
@Column(onInsertValue = "now()")
private Date uploadTime;

View File

@ -12,10 +12,10 @@ import java.util.Date;
@Data
public class FolderVo {
@Id(keyType = KeyType.Auto)
private int id;
private long id;
private String name;
private int parent;
private int creator;
private long parent;
private long creator;
@Column(onInsertValue = "now()")
private Date createTime;

View File

@ -4,16 +4,23 @@ import cn.wzpmc.filemanager.entities.statistics.enums.Actions;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Table("statistics")
@Data
@NoArgsConstructor
public class StatisticsVo {
private int actor;
private Long actor;
private Actions action;
private String params;
@Column(onInsertValue = "now()")
private Date time;
public StatisticsVo(Long actor, Actions action, String params) {
this.actor = actor;
this.action = action;
this.params = params;
}
}

View File

@ -10,9 +10,10 @@ import lombok.Data;
@Table("user")
@Data
@AllArgsConstructor
public class UserVo {
@Id(keyType = KeyType.Auto)
private int id;
private long id;
private String name;
private String password;
private Auth auth;
@ -23,5 +24,13 @@ public class UserVo {
this.password = password;
this.auth = auth;
}
private UserVo(long id, String name, Auth auth){
this.id = id;
this.name = name;
this.auth = auth;
}
public UserVo(long id) {
this.id = id;
}
public static final UserVo CONSOLE = new UserVo(0L, "CONSOLE", Auth.admin);
}

View File

@ -0,0 +1,13 @@
package cn.wzpmc.filemanager.interfaces;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
public interface FilePathService {
@NonNull
String getFilePath(@NonNull RawFileObject file);
@Nullable
RawFileObject resolveFile(@NonNull String[] path);
}

View File

@ -0,0 +1,60 @@
package cn.wzpmc.filemanager.interfaces.impl;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import cn.wzpmc.filemanager.mapper.FileMapper;
import cn.wzpmc.filemanager.mapper.FolderMapper;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Date;
import static cn.wzpmc.filemanager.entities.vo.table.FileVoTableDef.FILE_VO;
import static cn.wzpmc.filemanager.entities.vo.table.FolderVoTableDef.FOLDER_VO;
@Component
@Primary
@Log4j2
public class ComplexResolver extends SimplePathResolver {
private final ReverseResolver reverseResolver;
private final SimpleResolver simpleResolver;
@Autowired
public ComplexResolver(FileMapper fileMapper, FolderMapper folderMapper, ReverseResolver reverseResolver, SimpleResolver simpleResolver) {
super(fileMapper, folderMapper);
this.reverseResolver = reverseResolver;
this.simpleResolver = simpleResolver;
}
@Override
@Nullable
public RawFileObject resolveFile(@NonNull String[] path) {
String strPath = Arrays.toString(path);
String targetFileName = path[path.length - 1];
int lastDotIndex = targetFileName.lastIndexOf('.');
String name = targetFileName;
String ext = "";
if (lastDotIndex != -1) {
name = targetFileName.substring(0, lastDotIndex);
ext = targetFileName.substring(lastDotIndex + 1);
}
long start = new Date().getTime();
long totalRawFileCount = this.fileMapper.selectCountByCondition(FILE_VO.NAME.eq(name).and(FILE_VO.EXT.eq(ext))) + this.folderMapper.selectCountByCondition(FOLDER_VO.NAME.eq(name));
if (totalRawFileCount == 0) return null;
if (totalRawFileCount > path.length) {
log.info("use simple resolver to solve path with {}", strPath);
RawFileObject rawFileObject = simpleResolver.resolveFile(path);
long end = new Date().getTime();
log.info("solve path {} cost {}ms", strPath, end - start);
return rawFileObject;
}
log.info("use reverse resolver to solve path with {}", strPath);
RawFileObject rawFileObject = reverseResolver.resolveFile(path);
long end = new Date().getTime();
log.info("solve path {} cost {}ms", strPath, end - start);
return rawFileObject;
}
}

View File

@ -0,0 +1,72 @@
package cn.wzpmc.filemanager.interfaces.impl;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import cn.wzpmc.filemanager.mapper.FileMapper;
import cn.wzpmc.filemanager.mapper.FolderMapper;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.*;
import static cn.wzpmc.filemanager.entities.vo.table.FileVoTableDef.FILE_VO;
import static cn.wzpmc.filemanager.entities.vo.table.FolderVoTableDef.FOLDER_VO;
@Component
public class ReverseResolver extends SimplePathResolver {
public ReverseResolver(FileMapper fileMapper, FolderMapper folderMapper) {
super(fileMapper, folderMapper);
}
@Nullable
@Override
public RawFileObject resolveFile(@NonNull String[] path) {
List<String> pathList = removeEmptyPath(path);
String targetFileName = pathList.get(pathList.size() - 1);
int lastDotIndex = targetFileName.lastIndexOf('.');
String name = targetFileName;
String ext = "";
if (lastDotIndex != -1) {
name = targetFileName.substring(0, lastDotIndex);
ext = targetFileName.substring(lastDotIndex + 1);
}
List<RawFileObject> rawFileObjects = new ArrayList<>();
fileMapper.selectListByCondition(FILE_VO.NAME.eq(name).and(FILE_VO.EXT.eq(ext))).stream().map(RawFileObject::of).forEach(rawFileObjects::add);
folderMapper.selectListByCondition(FOLDER_VO.NAME.eq(name)).stream().map(RawFileObject::of).forEach(rawFileObjects::add);
if (rawFileObjects.isEmpty()) return null;
if (rawFileObjects.size() == 1) return rawFileObjects.get(0);
List<Long> possibleParents = rawFileObjects.stream().map(RawFileObject::getParent).toList();
Optional<RawFileObject> inRoot = rawFileObjects.stream().filter(e -> e.getParent() == -1).findFirst();
if (inRoot.isPresent()) {
if (pathList.size() <= 1) {
return inRoot.get();
}
}
List<FolderVo> folderVos = folderMapper.selectListByIds(possibleParents);
FolderVo parent = reverseFindFileParent(folderVos, pathList.subList(0, pathList.size() - 1));
if (parent == null) return null;
Optional<RawFileObject> first = rawFileObjects.stream().filter(e -> e.getParent() == parent.getId()).findFirst();
return first.orElse(null);
}
private FolderVo reverseFindFileParent(List<FolderVo> possibleParent, List<String> path) {
if (path.isEmpty()) return null;
if (possibleParent.size() == 1) return possibleParent.get(0);
String currentLayerName = path.get(path.size() - 1);
List<FolderVo> folderVoStream = possibleParent.stream().filter(e -> e.getName().equals(currentLayerName)).toList();
Optional<FolderVo> inRoot = folderVoStream.stream().filter(e -> e.getParent() == -1).findFirst();
if (inRoot.isPresent()) {
if (path.size() <= 1) {
return inRoot.get();
}
}
List<Long> list = folderVoStream.stream().map(FolderVo::getParent).toList();
if (list.isEmpty()) return null;
List<FolderVo> parents = folderMapper.selectListByIds(list);
FolderVo parent = reverseFindFileParent(parents, path.subList(0, path.size() - 1));
if (parent == null) return null;
Optional<FolderVo> first = folderVoStream.stream().filter(e -> e.getParent() == parent.getId()).findFirst();
return first.orElse(null);
}
}

View File

@ -0,0 +1,38 @@
package cn.wzpmc.filemanager.interfaces.impl;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import cn.wzpmc.filemanager.interfaces.FilePathService;
import cn.wzpmc.filemanager.mapper.FileMapper;
import cn.wzpmc.filemanager.mapper.FolderMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import java.util.Arrays;
import java.util.List;
@RequiredArgsConstructor
public abstract class SimplePathResolver implements FilePathService {
protected final FileMapper fileMapper;
protected final FolderMapper folderMapper;
@NonNull
@Override
public String getFilePath(@NonNull RawFileObject file) {
return resolvePath(file.getParent()) + RawFileObject.getRawFileName(file);
}
private String resolvePath(long id) {
if (id == -1) {
return "/";
}
FolderVo folderVo = folderMapper.selectOneById(id);
long parent = folderVo.getParent();
String name = folderVo.getName();
return resolvePath(parent) + name + "/";
}
protected List<String> removeEmptyPath(String[] path) {
return Arrays.stream(path).filter(e -> !e.isEmpty()).toList();
}
}

View File

@ -0,0 +1,53 @@
package cn.wzpmc.filemanager.interfaces.impl;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import cn.wzpmc.filemanager.mapper.FileMapper;
import cn.wzpmc.filemanager.mapper.FolderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.List;
import static cn.wzpmc.filemanager.entities.vo.table.FileVoTableDef.FILE_VO;
import static cn.wzpmc.filemanager.entities.vo.table.FolderVoTableDef.FOLDER_VO;
@Component
public class SimpleResolver extends SimplePathResolver {
@Autowired
public SimpleResolver(FileMapper fileMapper, FolderMapper folderMapper) {
super(fileMapper, folderMapper);
}
@Nullable
@Override
public RawFileObject resolveFile(@NonNull String[] path) {
return resolveFile(removeEmptyPath(path), -1);
}
private RawFileObject resolveFile(List<String> path, long parentId) {
String currentLayerName = path.get(0);
if (path.size() == 1) {
int lastDotIndex = currentLayerName.lastIndexOf('.');
String name = currentLayerName;
String ext = "";
if (lastDotIndex != -1) {
name = currentLayerName.substring(0, lastDotIndex);
ext = currentLayerName.substring(lastDotIndex + 1);
}
FileVo file = fileMapper.selectOneByCondition(FILE_VO.NAME.eq(name).and(FILE_VO.EXT.eq(ext)).and(FILE_VO.FOLDER.eq(parentId)));
if (file != null) {
return RawFileObject.of(file);
}
return RawFileObject.of(folderMapper.selectOneById(FOLDER_VO.NAME.eq(name).and(FOLDER_VO.PARENT.eq(parentId))));
}
FolderVo folderVo = folderMapper.selectOneByCondition(FOLDER_VO.NAME.eq(currentLayerName).and(FOLDER_VO.PARENT.eq(parentId)));
if (folderVo == null) {
return null;
}
return resolveFile(path.subList(1, path.size()), folderVo.getId());
}
}

View File

@ -1,7 +0,0 @@
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

@ -1,7 +0,0 @@
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.StatisticsVo;
import com.mybatisflex.core.BaseMapper;
public interface StatisticsMapper extends BaseMapper<StatisticsVo> {
}

View File

@ -1,166 +1,372 @@
package cn.wzpmc.filemanager.service;
import cn.wzpmc.filemanager.config.FileManagerProperties;
import cn.wzpmc.filemanager.entities.PageResult;
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.files.DeleteRequest;
import cn.wzpmc.filemanager.entities.files.FolderCreateRequest;
import cn.wzpmc.filemanager.entities.files.RawFileObject;
import cn.wzpmc.filemanager.entities.files.enums.FileType;
import cn.wzpmc.filemanager.entities.statistics.enums.Actions;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
import cn.wzpmc.filemanager.entities.vo.FileVo;
import cn.wzpmc.filemanager.entities.vo.FolderVo;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.mapper.ChunkMapper;
import cn.wzpmc.filemanager.mapper.FileChunksMapper;
import cn.wzpmc.filemanager.interfaces.FilePathService;
import cn.wzpmc.filemanager.mapper.FileMapper;
import cn.wzpmc.filemanager.mapper.FolderMapper;
import cn.wzpmc.filemanager.utils.JwtUtils;
import cn.wzpmc.filemanager.utils.RandomUtils;
import cn.wzpmc.filemanager.utils.SizeStatisticsDigestInputStream;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.audit.http.HashUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.tika.Tika;
import org.apache.tomcat.util.http.fileupload.FileItemStream;
import org.apache.tomcat.util.http.fileupload.FileUpload;
import org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
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 org.springframework.web.multipart.MultipartHttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.TimeUnit;
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;
import static cn.wzpmc.filemanager.entities.vo.table.FolderVoTableDef.FOLDER_VO;
import static com.mybatisflex.core.query.QueryMethods.*;
@Slf4j
@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, "文件已存在!");
private final StatisticsService statisticsService;
private final RedisTemplate<String, FileVo> linkMapper;
/*private final RedisTemplate<String, Long> linkCountMapper;*/
private final StringRedisTemplate idAddrLinkMapper;
private final JwtUtils jwtUtils;
private final FilePathService pathService;
public static final String ID_ADDR_PREFIX = "ID_ADDR_";
public static final String SHARE_PREFIX = "SHARE_";
public static final char PATH_SEPARATOR_CHAR = '/';
public static final String PATH_SEPARATOR = "" + PATH_SEPARATOR_CHAR;
protected void tryDeleteOrDeleteOnExit(File tmpFile) {
if (!tmpFile.delete()) {
log.error("delete tmp file error");
tmpFile.deleteOnExit();
}
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, "未知的文件块");
public Result<FileVo> simpleUpload(MultipartHttpServletRequest request, UserVo user, String address) {
long folderParams = getFolderParams(request);
ServletRequestContext servletRequestContext = new ServletRequestContext(request);
FileUpload upload = new FileUpload();
FileItemIteratorImpl fileItemIterator = new FileItemIteratorImpl(upload, servletRequestContext);
FileVo lastUploadFile = null;
while (fileItemIterator.hasNext()) {
FileItemStream next = fileItemIterator.next();
String fieldName = next.getFieldName();
if (fieldName.equals("file")) {
String name = next.getName();
int i = name.lastIndexOf(".");
String extName = null;
String start = name;
if (i != -1) {
start = name.substring(0, i);
}
long size = file.getSize();
if (size > 64 * 1024 * 1024) {
return Result.failed(HttpStatus.PAYLOAD_TOO_LARGE, "文件块不应大于64MB");
if (!(i == -1 || i == name.length() - 1)) {
extName = name.substring(i + 1);
}
byte[] bytes = file.getBytes();
String s = DigestUtils.sha1Hex(bytes);
if (!s.equals(chunkData.getHash())) {
return Result.failed(HttpStatus.CONFLICT, "文件块内容错误!");
if (fileMapper.selectCountByCondition(FILE_VO.NAME.eq(start).and(FILE_VO.EXT.eq(extName)).and(FILE_VO.FOLDER.eq(folderParams))) > 0) {
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, "写入文件块出现错误,创建文件夹失败");
InputStream inputStream = next.openStream();
SizeStatisticsDigestInputStream digestInputStream = new SizeStatisticsDigestInputStream(inputStream, DigestUtils.getSha512Digest());
File savePath = properties.getSavePath();
File tmpFile = new File(savePath, "cache-" + randomUtils.generatorRandomFileName(20));
try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
StreamUtils.copy(digestInputStream, fileOutputStream);
}
digestInputStream.close();
String hex = HashUtil.toHex(digestInputStream.getMessageDigest().digest());
long size = digestInputStream.getSize();
if (size == 0) {
tryDeleteOrDeleteOnExit(tmpFile);
return Result.failed(HttpStatus.LENGTH_REQUIRED, "请勿上传空文件!");
}
Tika tika = new Tika();
String detect = tika.detect(tmpFile);
FileVo fileVo = new FileVo();
fileVo.setUploader(user.getId());
fileVo.setMime(detect);
fileVo.setSize(size);
fileVo.setName(start);
fileVo.setExt(extName);
fileVo.setHash(hex);
fileVo.setFolder(folderParams);
fileMapper.insert(fileVo);
statisticsService.insertAction(user, Actions.UPLOAD, JSONObject.of("id", fileVo.getId(), "hex", hex, "address", address));
File targetFile = new File(savePath, hex);
lastUploadFile = fileVo;
if (targetFile.isFile()) {
tryDeleteOrDeleteOnExit(tmpFile);
continue;
}
if (!tmpFile.renameTo(targetFile)) {
throw new RuntimeException("error while moving file");
}
}
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);
if (lastUploadFile == null) {
return Result.failed(HttpStatus.BAD_REQUEST, "未找到文件参数");
}
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("成功");
return Result.success("上传成功!", lastUploadFile);
}
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");
private static long getFolderParams(MultipartHttpServletRequest request) {
String params = request.getQueryString();
long folderParams = -1L;
if (params != null && !params.isEmpty()) {
String[] param = params.split("&");
for (String s : param) {
String[] keyValue = s.split("=");
if (keyValue.length == 2) {
String key = keyValue[0];
String value = keyValue[1];
if (key.equals("folder")) {
folderParams = Long.parseLong(value);
}
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));
}
}
return folderParams;
}
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");
public Result<PageResult<RawFileObject>> getFilePager(long page, int num, long folder, String address) {
QueryWrapper queryWrapper = select(
FILE_VO.ID.as("id"),
FILE_VO.NAME.as("name"),
FILE_VO.EXT.as("ext"),
FILE_VO.SIZE.as("size"),
FILE_VO.FOLDER.as("parent"),
FILE_VO.UPLOADER.as("owner"),
FILE_VO.UPLOAD_TIME.as("time"),
string("FILE").as("type")
).from(FILE_VO).
where(FILE_VO.FOLDER.eq(folder)).
unionAll(
select(
FOLDER_VO.ID.as("id"),
FOLDER_VO.NAME.as("name"),
null_().as("ext"),
number(-1).as("size"),
FOLDER_VO.PARENT.as("parent"),
FOLDER_VO.CREATOR.as("owner"),
FOLDER_VO.CREATE_TIME.as("time"),
string("FOLDER").as("type")
).from(FOLDER_VO).
where(FOLDER_VO.PARENT.eq(folder))
).
orderBy(
column("time").
asc(),
column("id").
asc()
);
Page<RawFileObject> paginate = fileMapper.paginateAs(page, num, queryWrapper, RawFileObject.class);
PageResult<RawFileObject> result = new PageResult<>(paginate.getTotalRow(), paginate.getRecords());
return Result.success(result);
}
long totalLength = andDelete.getLength();
long l = calcFileSize(andDelete.getFileId());
if (l != totalLength) {
fileMapper.deleteById(andDelete.getFileId());
return Result.failed(HttpStatus.LENGTH_REQUIRED, "应收到" + totalLength + "字节,但只收到" + l + "字节!");
public Result<FolderVo> mkdir(FolderCreateRequest request, UserVo user, String address) {
String name = request.getName();
long parent = request.getParent();
if (fileMapper.selectCountByCondition(FILE_VO.EXT.eq(null_()).and(FILE_VO.NAME.eq(name)).and(FILE_VO.FOLDER.eq(parent))) > 0) {
return Result.failed(HttpStatus.CONFLICT, "创建文件夹失败,同名文件已存在!");
}
if (folderMapper.selectCountByCondition(FOLDER_VO.NAME.eq(name).and(FOLDER_VO.PARENT.eq(parent))) > 0) {
return Result.failed(HttpStatus.CONFLICT, "创建文件夹失败,同名文件已存在!");
}
FolderVo folderVo = new FolderVo();
folderVo.setCreator(user.getId());
folderVo.setName(name);
folderVo.setParent(parent);
folderMapper.insert(folderVo);
statisticsService.insertAction(user, Actions.UPLOAD, JSONObject.of("type", "folder", "id", folderVo.getId(), "address", address));
return Result.success("创建成功", folderVo);
}
protected void deleteFile(FileVo fileVo) {
if (fileMapper.selectCountByCondition(FILE_VO.HASH.eq(fileVo.getHash())) <= 0) {
String hash = fileVo.getHash();
File file = new File(properties.getSavePath(), hash);
if (file.exists()) {
if (!file.delete()) {
log.error("删除文件 {} 失败!", file);
}
} else {
log.error("存储目录可能损坏,在删除文件 {} 时未发现文件", file);
}
}
}
@Transactional
public Result<Void> delete(DeleteRequest request, UserVo user, String address) {
long id = request.getId();
FileType type = request.getType();
long actorId = user.getId();
if (type.equals(FileType.FILE)) {
FileVo fileVo = fileMapper.selectOneById(id);
if (fileVo == null) {
return Result.failed(HttpStatus.NOT_FOUND, "文件不存在!");
}
if (user.getAuth().equals(Auth.user)) {
if (fileMapper.selectCountByCondition(FILE_VO.ID.eq(id).and(FILE_VO.UPLOADER.eq(actorId))) <= 0) {
return Result.failed(HttpStatus.UNAUTHORIZED, "权限不足!");
}
}
fileMapper.deleteById(fileVo.getId());
deleteFile(fileVo);
} else {
FolderVo folder = folderMapper.selectOneById(id);
if (folder == null) {
return Result.failed(HttpStatus.NOT_FOUND, "文件不存在!");
}
if (user.getAuth().equals(Auth.user)) {
if (folderMapper.selectCountByCondition(FOLDER_VO.ID.eq(id).and(FOLDER_VO.CREATOR.eq(actorId))) <= 0) {
return Result.failed(HttpStatus.UNAUTHORIZED, "权限不足!");
}
}
fileMapper.deleteByCondition(FILE_VO.FOLDER.eq(id));
for (FileVo fileVo : fileMapper.selectListByCondition(FILE_VO.FOLDER.eq(id))) {
deleteFile(fileVo);
}
}
statisticsService.insertAction(user, Actions.DELETE, JSONObject.of("id", id, "type", type, "address", address));
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;
public Result<FileVo> getFileDetail(long id) {
FileVo fileVo = this.fileMapper.selectOneById(id);
if (fileVo == null) {
return Result.failed(HttpStatus.NOT_FOUND, "未知文件");
}
return sum;
return Result.success(fileVo);
}
@SneakyThrows
public void downloadFile(String id, String range, HttpServletResponse response) {
FileVo fileVo = linkMapper.opsForValue().get(id);
if (fileVo == null) {
Result.failed(HttpStatus.NOT_FOUND, "未知文件!").writeToResponse(response);
return;
}
long size = fileVo.getSize();
long min = 0;
long max = size;
if (!range.equals("null")) {
String[] unitRanges = range.split("=");
String[] minMax = unitRanges[1].split("-");
if (minMax.length > 0) {
min = Long.parseLong(minMax[0]);
}
if (minMax.length > 1) {
max = Long.parseLong(minMax[1]);
}
}
String hash = fileVo.getHash();
File file = new File(properties.getSavePath(), hash);
String fullName = fileVo.getName();
String ext = fileVo.getExt();
if (ext != null) {
fullName += '.' + ext;
}
response.setStatus(206);
response.addHeader("Content-Length", String.valueOf(max - min));
response.addHeader("Content-Range", "bytes " + min + "-" + max + "/" + size);
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fullName, StandardCharsets.UTF_8));
ServletOutputStream outputStream = response.getOutputStream();
try (FileInputStream fis = new FileInputStream(file)) {
StreamUtils.copyRange(fis, outputStream, min, max);
}
outputStream.flush();
}
public Result<String> getFileLink(long id, String address, HttpServletRequest request) {
FileVo fileVo = fileMapper.selectOneById(id);
long fileId = fileVo.getId();
String identify = ID_ADDR_PREFIX + fileId + address;
String link = idAddrLinkMapper.opsForValue().get(identify);
if (link == null) {
link = randomUtils.generatorRandomFileName(8);
String authorization = request.getHeader("Authorization");
if (authorization != null) {
jwtUtils.getUser(authorization).ifPresent(uid -> statisticsService.insertAction(new UserVo(uid), Actions.DOWNLOAD, JSONObject.of("id", fileId, "address", address)));
} else {
statisticsService.insertAction(Actions.DOWNLOAD, JSONObject.of("id", fileId, "address", address));
}
linkMapper.opsForValue().set(link, fileVo, 30, TimeUnit.MINUTES);
idAddrLinkMapper.opsForValue().set(identify, link, 30, TimeUnit.MINUTES);
}
return Result.success("成功", link);
}
public Result<String> shareFile(Long id, Date lastCouldDownloadTime, int maxDownloadCount) {
FileVo fileVo = this.fileMapper.selectOneById(id);
if (fileVo == null) {
return Result.failed(HttpStatus.NOT_FOUND, "未知文件");
}
String linkName = randomUtils.generatorRandomFileName(10);
String innerId = randomUtils.generatorRandomString(30);
long expireAfterMs = lastCouldDownloadTime.getTime() - new Date().getTime();
return Result.success(linkName);
}
public Result<RawFileObject> resolveFileDetail(String path) {
String[] split = path.split(PATH_SEPARATOR);
RawFileObject fileObj = pathService.resolveFile(split);
if (fileObj == null) {
return Result.failed(HttpStatus.NOT_FOUND, "文件不存在!");
}
return Result.success(fileObj);
}
public Result<String> findFilePathById(long id) {
FileVo fileVo = fileMapper.selectOneById(id);
if (fileVo == null) {
return Result.failed(HttpStatus.NOT_FOUND, "文件不存在!");
}
return Result.success("成功", pathService.getFilePath(RawFileObject.of(fileVo)));
}
public Result<String> findFolderPathById(long id) {
FolderVo folderVo = folderMapper.selectOneById(id);
if (folderVo == null) {
return Result.failed(HttpStatus.NOT_FOUND, "文件夹不存在");
}
return Result.success("成功", pathService.getFilePath(RawFileObject.of(folderVo)));
}
}

View File

@ -0,0 +1,31 @@
package cn.wzpmc.filemanager.service;
import cn.wzpmc.filemanager.entities.statistics.enums.Actions;
import cn.wzpmc.filemanager.entities.vo.StatisticsVo;
import cn.wzpmc.filemanager.entities.vo.UserVo;
import cn.wzpmc.filemanager.mapper.StatisticsMapper;
import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class StatisticsService {
private final StatisticsMapper statisticsMapper;
public void insertAction(@Nullable UserVo actor, Actions actions, @Nullable Object params) {
statisticsMapper.insert(new StatisticsVo(actor != null ? actor.getId() : null, actions, params != null ? params.toString() : null));
}
public void insertAction(@Nullable UserVo actor, Actions actions) {
this.insertAction(actor, actions, null);
}
public void insertAction(Actions actions, @Nullable Object params) {
this.insertAction(null, actions, params);
}
public void insertAction(Actions actions) {
this.insertAction(actions, null);
}
}

View File

@ -1,6 +1,7 @@
package cn.wzpmc.filemanager.service;
import cn.wzpmc.filemanager.entities.Result;
import cn.wzpmc.filemanager.entities.statistics.enums.Actions;
import cn.wzpmc.filemanager.entities.user.UserLoginRequest;
import cn.wzpmc.filemanager.entities.user.UserRegisterRequest;
import cn.wzpmc.filemanager.entities.user.enums.Auth;
@ -8,6 +9,7 @@ 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.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletResponse;
@ -30,66 +32,77 @@ public class UserService {
private final JwtUtils jwtUtils;
private final StringRedisTemplate authTemplate;
private final RandomUtils randomUtils;
private final StatisticsService statisticsService;
@Autowired
public UserService(UserMapper userMapper, JwtUtils jwtUtils, StringRedisTemplate authTemplate, RandomUtils randomUtils) {
public UserService(UserMapper userMapper, JwtUtils jwtUtils, StringRedisTemplate authTemplate, RandomUtils randomUtils, StatisticsService statisticsService) {
this.userMapper = userMapper;
this.jwtUtils = jwtUtils;
this.authTemplate = authTemplate;
this.randomUtils = randomUtils;
this.statisticsService = statisticsService;
long count = this.userMapper.selectCountByQuery(new QueryWrapper());
if (count == 0) {
String s = genInviteCode();
String s = genInviteCode(UserVo.CONSOLE, "0.0.0.0");
log.info("生成了管理员密钥:{}有效期15分钟若失效请使用控制台命令/key或重启后端重新生成", s);
}
}
public void login(UserLoginRequest request, HttpServletResponse response) {
public void login(UserLoginRequest request, HttpServletResponse response, String address) {
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) {
if (count <= 0) {
this.statisticsService.insertAction(Actions.LOGIN, JSONObject.of("status", "error", "msg", "账号或密码错误", "address", address));
Result.failed(HttpStatus.UNAUTHORIZED, "账号或密码错误").writeToResponse(response);
return;
}
UserVo userVo = this.userMapper.selectOneByCondition(findUserCondition);
String token = this.jwtUtils.createToken(userVo.getId());
long id = userVo.getId();
String token = this.jwtUtils.createToken(id);
response.addHeader("Add-Authorization", token);
this.statisticsService.insertAction(userVo, Actions.LOGIN, JSONObject.of("status", "success", "address", address));
Result.success("登录成功").writeToResponse(response);
}
public void register(UserRegisterRequest request, HttpServletResponse response) {
public void register(UserRegisterRequest request, HttpServletResponse response, String address) {
String username = request.getUsername();
String password = request.getPassword();
Auth auth = request.getAuth();
if (this.userMapper.selectCountByCondition(USER_VO.NAME.eq(username)) > 0) {
this.statisticsService.insertAction(Actions.REGISTER, JSONObject.of("status", "error", "auth", auth, "msg", "用户名已存在", "address", address));
Result.failed(HttpStatus.CONFLICT).msg("用户名已存在,若需要修改密码,请联系网站管理员处理").writeToResponse(response);
return;
}
JSONObject statisticsData = JSONObject.of("status", "success", "auth", auth, "address", address);
if (auth.equals(Auth.admin)) {
String inviteCode = request.getInviteCode();
ValueOperations<String, String> ops = authTemplate.opsForValue();
String andDelete = ops.getAndDelete(inviteCode);
if (andDelete == null) {
this.statisticsService.insertAction(Actions.REGISTER, JSONObject.of("status", "error", "auth", auth, "msg", "邀请码错误", "inviteCode", inviteCode, "address", address));
Result.failed(HttpStatus.NOT_FOUND, "过期或无效的邀请码").writeToResponse(response);
return;
}
statisticsData.put("inviteCode", inviteCode);
}
UserVo userVo = new UserVo(username, password, auth);
this.userMapper.insert(userVo);
int id = userVo.getId();
long id = userVo.getId();
String token = this.jwtUtils.createToken(id);
response.addHeader("Add-Authorization", token);
this.statisticsService.insertAction(userVo, Actions.REGISTER, statisticsData);
Result.success("注册成功!").writeToResponse(response);
}
public Result<String> invite() {
String s = genInviteCode();
public Result<String> invite(UserVo userVo, String address) {
String s = genInviteCode(userVo, address);
return Result.success("生成了一个有效期15分钟的邀请码", s);
}
public String genInviteCode() {
public String genInviteCode(UserVo actor, String address) {
ValueOperations<String, String> ops = authTemplate.opsForValue();
String s = this.randomUtils.generatorRandomString(8);
log.info("生成了新的邀请码:{}", s);
statisticsService.insertAction(actor, Actions.INVITE, address);
ops.set(s, "", 15, TimeUnit.MINUTES);
return s;
}

View File

@ -0,0 +1,31 @@
package cn.wzpmc.filemanager.utils;
import cn.wzpmc.filemanager.annotation.Address;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
@Slf4j
@Component
public class AddressArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Address.class);
}
@Override
@Nullable
public Object resolveArgument(@NonNull MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
if (webRequest instanceof ServletWebRequest servletWebRequest) {
return servletWebRequest.getRequest().getRemoteAddr();
}
return null;
}
}

View File

@ -12,7 +12,6 @@ import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Optional;
import java.util.Random;
@Component
@Log4j2
@ -35,7 +34,7 @@ public class JwtUtils {
}
this.hmacKey = Algorithm.HMAC512(key);
}
public String createToken(int uid){
public String createToken(long uid){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.HOUR,24 * 5);
JWTCreator.Builder builder = JWT.create();

View File

@ -16,4 +16,18 @@ public class RandomUtils {
}
return builder.toString();
}
public String generatorRandomFileName(int length) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
double random = Math.random();
int c = new Random().nextInt(97,122);
if (random < 0.3) {
c = new Random().nextInt(48, 57);
}else if(random < 0.6) {
c = new Random().nextInt(65, 90);
}
builder.append((char) c);
}
return builder.toString();
}
}

View File

@ -0,0 +1,30 @@
package cn.wzpmc.filemanager.utils;
import lombok.Getter;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
@Getter
public class SizeStatisticsDigestInputStream extends DigestInputStream {
protected long size = 0;
public SizeStatisticsDigestInputStream(InputStream stream, MessageDigest digest) {
super(stream, digest);
}
@Override
public int read() throws IOException {
int read = super.read();
if (read != -1) size++;
return read;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
if (read != -1) size += read;
return read;
}
}

View File

@ -15,8 +15,9 @@ spring:
password: "MyCraftAdmin123"
servlet:
multipart:
max-file-size: 100GB
max-request-size: 100GB
max-file-size: 400GB
max-request-size: 400GB
resolve-lazily: true
wzp:
filemanager:
save-path: "./file"