diff --git a/.gitignore b/.gitignore index 6969ba34ee37fa2f696d493689b8d4930d352895..259cb468440007d30c8ccced7b350c23f55699b1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ build/ .vscode/ src/main/resources/application.yml +/script/start.sh +/script/start.bat diff --git a/pom.xml b/pom.xml index 74cd4818ce9c3c71e2a39809c1f2e01cad4ea6aa..f1d2be95922d11c64c8c6c2af4bbb45a97068169 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.xiaotao saltedfishcloud - 1.3.2.2-RELEASE + 1.3.3.2-RELEASE saltedfishcloud 咸鱼云网盘 diff --git a/script/build.bat b/script/build.bat index 8a4cc82e43063d579dc72954cab5e6a9a3bfa397..c8fe71621b919aaa8ee9bf3b64f0b9f283df6af8 100644 --- a/script/build.bat +++ b/script/build.bat @@ -10,4 +10,8 @@ if exist ..\target\*.jar ( ) cd .. call mvn package -echo ɣһ޸start.batűĿĿ \ No newline at end of file +if errorlevel 1 ( + echo ʧ +) else ( + echo ɣһԲοstart.bat.templateļĿűĿ +) \ No newline at end of file diff --git a/script/build.sh b/script/build.sh index f2088eb3f7ccfbcaf9830d76be54ef80bb9756c7..f8a90207aa70b93e7d7154ca76c8bca1104850a8 100755 --- a/script/build.sh +++ b/script/build.sh @@ -11,4 +11,8 @@ fi cd .. mvn package -echo "构建完成,下一步可以修改start.sh脚本配置项目启动参数来启动项目了" \ No newline at end of file +if [ $? == 0 ]; then + echo "构建完成,下一步可以参考start.sh.template文件配置项目启动脚本来启动项目了" +else + echo "构建失败" +fi \ No newline at end of file diff --git a/script/start.bat b/script/start.bat.template similarity index 95% rename from script/start.bat rename to script/start.bat.template index 45ae897e1002e0c2c3381adf5be52e5d5b28e8e3..25f2608035e524e7ef05f8b600159631eb69f428 100644 --- a/script/start.bat +++ b/script/start.bat.template @@ -36,9 +36,10 @@ set db_username=root set db_password=mojintao233 set db_params="useSSL=false&serverTimezone=UTC" +@REM Redis set redis_host=127.0.0.1 set redis_port=6379 - +set redis_password="" set jdbc_url=jdbc:mysql://%db_host%:%db_port%/%db_name%?%db_params% @@ -50,7 +51,7 @@ java -jar ../target/%jar_name% ^ --spring.datasource.druid.password=%db_password% ^ --spring.datasource.redis.host=%redis_host% ^ --spring.datasource.redis.part=%redis_port% ^ ---spring.datasource.redis.password=% ^ +--spring.datasource.redis.password=%redis_password% ^ --public-root=%public_root% ^ --store-root=%store_root% ^ --store-type=%store_type% ^ diff --git a/script/start.sh b/script/start.sh.template old mode 100755 new mode 100644 similarity index 95% rename from script/start.sh rename to script/start.sh.template index fca1ff941cd5d7045e8f0ccb82fb17f0be35c29c..d9a1c0d5e7065ebb8b390792fd63c219c5fd4b14 --- a/script/start.sh +++ b/script/start.sh.template @@ -35,8 +35,10 @@ db_username="root" db_password="" db_params="useSSL=false&serverTimezone=UTC" +# Redis连接设置 redis_host="127.0.0.1" redis_port="6379" +redis_password="" jdbc_url="jdbc:mysql://${db_host}:${db_port}/${db_name}?${db_params}" @@ -49,6 +51,7 @@ java -Dfile.encoding=utf-8 -jar $jar_name \ --spring.datasource.druid.password="$db_password" \ --spring.datasource.redis.host="$redis_host" \ --spring.datasource.redis.part="$redis_port" \ +--spring.datasource.redis.password="$redis_password" \ --spring.datasource.redis.password="" \ --public-root="$public_root" \ --store-root="$store_root" \ diff --git a/src/main/java/com/xiaotao/saltedfishcloud/config/CommandLineOption.java b/src/main/java/com/xiaotao/saltedfishcloud/config/CommandLineOption.java index 58e3daa004d330d10ccc6c766cf2845a58399f58..1a9c072e360dd60dd3a0441cf0676bfd355300e4 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/config/CommandLineOption.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/config/CommandLineOption.java @@ -14,11 +14,11 @@ public class CommandLineOption { ARGS = applicationArguments; } - public static String getValue(String key) { + public String getValue(String key) { return getValue(key, null); } - public static String getValue(String key, String defaultValue) { + public String getValue(String key, String defaultValue) { List val = ARGS.getOptionValues(key); if (val == null || val.isEmpty()) { return defaultValue; diff --git a/src/main/java/com/xiaotao/saltedfishcloud/config/DiskConfig.java b/src/main/java/com/xiaotao/saltedfishcloud/config/DiskConfig.java index db9fea95b4f66b76e60a74f60862a99d4020a04c..4e6588c812f4cab94b9ed99ccfc9896ae8074db1 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/config/DiskConfig.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/config/DiskConfig.java @@ -175,6 +175,11 @@ public class DiskConfig { return getUserPrivateDiskRoot(Objects.requireNonNull(SecureUtils.getSpringSecurityUser()).getUsername()); } + /** + * 获取RAW存储下用户私人网盘根目录 + * @TODO 使用uid替代username + * @param username 用户名 + */ public static String getUserPrivateDiskRoot(String username) { return getRawStoreRoot() + username; } @@ -207,6 +212,12 @@ public class DiskConfig { return getUserProfileRoot(Objects.requireNonNull(SecureUtils.getSpringSecurityUser()).getUsername()); } + /** + * 获取用户profile根 + * @TODO 使用uid替代username + * @param username 用户名 + * @return 本次文件系统路径 + */ public static String getUserProfileRoot(String username) { return DiskConfig.USER_PROFILE_ROOT + "/" + username; } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/config/RedisConfig.java b/src/main/java/com/xiaotao/saltedfishcloud/config/RedisConfig.java index d4436adfd686745910d3520b3e0b22d80bfa4b9e..b28bcf695b80c5c4b1e13f29d0dab05015327d94 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/config/RedisConfig.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/config/RedisConfig.java @@ -35,12 +35,16 @@ public class RedisConfig { @Bean public RedisCacheManager cacheManager() { return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(Objects.requireNonNull(redisTemplate.getConnectionFactory())) - .cacheDefaults(getRedisCacheConfigurationWithTtl(Duration.ofHours(3))) - .withCacheConfiguration("token", getRedisCacheConfigurationWithTtl(Duration.ofDays(1))) + .cacheDefaults(getCacheConfigWitTtl(Duration.ofHours(3))) .build(); } - private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Duration time) { + /** + * 获取一个带过期时间的Redis缓存配置 + * @param time 过期时间 + * @return Redis缓存配置 + */ + private RedisCacheConfiguration getCacheConfigWitTtl(Duration time) { return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()) diff --git a/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtLoginFilter.java b/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtLoginFilter.java index 8448fdc8c4752561b0a8fd391348f9041f96401a..f08b086bf8bbb2a0b5d66bb397bfaee68699d438 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtLoginFilter.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtLoginFilter.java @@ -1,6 +1,7 @@ package com.xiaotao.saltedfishcloud.config.security; import com.fasterxml.jackson.databind.ObjectMapper; +import com.xiaotao.saltedfishcloud.dao.redis.TokenDao; import com.xiaotao.saltedfishcloud.po.JsonResult; import com.xiaotao.saltedfishcloud.po.User; import com.xiaotao.saltedfishcloud.utils.JwtUtils; @@ -21,14 +22,16 @@ import java.io.IOException; * 在SpringSecurity过滤器链中处理用户登录的过滤器 */ public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { + private final TokenDao tokenDao; - protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { + protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager, TokenDao tokenDao) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); + this.tokenDao = tokenDao; } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String user = request.getParameter("user"); String passwd = request.getParameter("passwd"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, passwd); @@ -36,17 +39,18 @@ public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { } @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException { response.setContentType("application/json;charset=utf-8"); ObjectMapper mapper = new ObjectMapper(); User user = (User)authResult.getPrincipal(); user.setPwd(null); - String token = mapper.writeValueAsString(user); - response.getWriter().print(JsonResult.getInstance(JwtUtils.generateToken(token)).toString()); + String token = JwtUtils.generateToken(mapper.writeValueAsString(user)); + tokenDao.setToken(user.getUsername(), token); + response.getWriter().print(JsonResult.getInstance(token).toString()); } @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException { response.setContentType("application/json;charset=utf-8"); response.setStatus(400); response.getWriter().print(JsonResult.getInstance(400, null, "用户名或密码错误")); diff --git a/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtValidateFilter.java b/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtValidateFilter.java index aafdfa7626d0cdebab3261959c9e7761ef15c1b7..e5865b6498bb5fc2dabc306e02e991b222496c91 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtValidateFilter.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/config/security/JwtValidateFilter.java @@ -1,6 +1,7 @@ package com.xiaotao.saltedfishcloud.config.security; import com.fasterxml.jackson.databind.ObjectMapper; +import com.xiaotao.saltedfishcloud.dao.redis.TokenDao; import com.xiaotao.saltedfishcloud.po.User; import com.xiaotao.saltedfishcloud.utils.JwtUtils; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -18,6 +19,12 @@ import java.io.IOException; * 在SpringSecurity过滤器链中验证是否存在token且token是否有效,若有效则设置SpringSecurity用户认证信息 */ public class JwtValidateFilter extends OncePerRequestFilter { + private final static ObjectMapper MAPPER = new ObjectMapper(); + private final TokenDao tokenDao; + + public JwtValidateFilter(TokenDao tokenDao) { + this.tokenDao = tokenDao; + } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { @@ -30,14 +37,16 @@ public class JwtValidateFilter extends OncePerRequestFilter { return; } else { try { + // 将其token的负载数据json反序列化为User对象 + User user = MAPPER.readValue(JwtUtils.parse(token), User.class); - // 解析token,获取其中的负载数据字符串(这里是User对象的json序列化字符串) - String data = (String)JwtUtils.parse(token); + if (tokenDao.isTokenValid(user.getUsername(), token)) { + SecurityContextHolder.getContext() + .setAuthentication( + new UsernamePasswordAuthenticationToken( user, null, user.getAuthorities()) + ); + } - // 将其json反序列化为User对象 - ObjectMapper mapper = new ObjectMapper(); - User user = mapper.readValue(data, User.class); - SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken( user, null, user.getAuthorities()) ); } catch (Exception ignored) { diff --git a/src/main/java/com/xiaotao/saltedfishcloud/config/security/SecurityConfig.java b/src/main/java/com/xiaotao/saltedfishcloud/config/security/SecurityConfig.java index 12005b29cf10cc10470a792d3a63fc8c1fe10b00..aba2c3f7fff06a956e90c3972b25a116676a92a7 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/config/security/SecurityConfig.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/config/security/SecurityConfig.java @@ -1,6 +1,7 @@ package com.xiaotao.saltedfishcloud.config.security; import com.xiaotao.saltedfishcloud.config.security.service.UserDetailsServiceImpl; +import com.xiaotao.saltedfishcloud.dao.redis.TokenDao; import com.xiaotao.saltedfishcloud.po.JsonResult; import com.xiaotao.saltedfishcloud.utils.SecureUtils; import org.springframework.context.annotation.Bean; @@ -37,7 +38,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { private SecureUtils secureUtils; @Resource - PasswordEncoder myPasswordEncoder; + private PasswordEncoder myPasswordEncoder; + + @Resource + private TokenDao tokenDao; @Bean @Override @@ -60,8 +64,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { // 添加Jwt登录和验证过滤器 - http.addFilterBefore(new JwtLoginFilter(LOGIN_URI, authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(new JwtValidateFilter(), UsernamePasswordAuthenticationFilter.class) + http.addFilterBefore(new JwtLoginFilter(LOGIN_URI, authenticationManagerBean(), tokenDao), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JwtValidateFilter(tokenDao), UsernamePasswordAuthenticationFilter.class) .csrf().disable(); // 处理过滤器链中出现的异常 @@ -92,7 +96,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } @Override - public void configure(WebSecurity web) throws Exception { + public void configure(WebSecurity web) { web.ignoring().antMatchers("/src/**"); } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/controller/ControllerAdvice.java b/src/main/java/com/xiaotao/saltedfishcloud/controller/ControllerAdvice.java index 93963fd09c36953ea4d129bacbec5195193d0b3e..5e36a7df7c2024ad274c1512ac3e4888f183c48c 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/controller/ControllerAdvice.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/controller/ControllerAdvice.java @@ -53,6 +53,9 @@ public class ControllerAdvice { @ExceptionHandler({ConstraintViolationException.class, IllegalArgumentException.class}) public JsonResult paramsError(Exception e) { + if (log.isDebugEnabled()) { + e.printStackTrace(); + } return responseError(422, e.getMessage()); } @@ -60,6 +63,9 @@ public class ControllerAdvice { @ExceptionHandler(JsonException.class) public JsonResult handle(JsonException e) { + if (log.isDebugEnabled()) { + e.printStackTrace(); + } response.setStatus(e.getRes().getCode()); return e.getRes(); } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/controller/FileController.java b/src/main/java/com/xiaotao/saltedfishcloud/controller/FileController.java index d2e51cc76a9b0450b5ed77d67748beaff1223d91..3012de94cfddef015a8e5dca411ec8eb06dd6754 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/controller/FileController.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/controller/FileController.java @@ -2,8 +2,8 @@ package com.xiaotao.saltedfishcloud.controller; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; -import com.xiaotao.saltedfishcloud.annotations.ReadOnlyBlock; import com.xiaotao.saltedfishcloud.annotations.NotBlock; +import com.xiaotao.saltedfishcloud.annotations.ReadOnlyBlock; import com.xiaotao.saltedfishcloud.config.security.AllowAnonymous; import com.xiaotao.saltedfishcloud.enums.ReadOnlyLevel; import com.xiaotao.saltedfishcloud.exception.JsonException; @@ -15,8 +15,8 @@ import com.xiaotao.saltedfishcloud.po.param.FileNameList; import com.xiaotao.saltedfishcloud.po.param.NamePair; import com.xiaotao.saltedfishcloud.service.breakpoint.annotation.BreakPoint; import com.xiaotao.saltedfishcloud.service.breakpoint.annotation.MergeFile; -import com.xiaotao.saltedfishcloud.service.file.FileService; -import com.xiaotao.saltedfishcloud.service.file.exception.DirectoryAlreadyExistsException; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystem; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import com.xiaotao.saltedfishcloud.service.http.ResponseService; import com.xiaotao.saltedfishcloud.utils.URLUtils; import com.xiaotao.saltedfishcloud.validator.FileName; @@ -33,7 +33,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URLDecoder; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.NoSuchFileException; import java.util.Collection; import java.util.List; @@ -49,7 +48,7 @@ public class FileController { public static final String PREFIX = "/api/diskFile/"; @Resource - private FileService fileService; + private DiskFileSystemFactory fileService; @Resource private ResponseService responseService; @@ -71,10 +70,10 @@ public class FileController { @PutMapping("dir/**") public JsonResult mkdir(@PathVariable @UID(true) int uid, HttpServletRequest request, - @RequestParam("name") @FileName String name) throws JsonException, NoSuchFileException, FileAlreadyExistsException, DirectoryAlreadyExistsException { + @RequestParam("name") @FileName String name) throws JsonException, IOException { String requestPath = URLUtils.getRequestFilePath(PREFIX + uid + "/dir", request); - fileService.mkdirs(uid, requestPath); - fileService.mkdir(uid, requestPath, name); + DiskFileSystem fileSystem = fileService.getFileSystem(); + fileSystem.mkdirs(uid, requestPath + "/" + name); return JsonResult.getInstance(); } @@ -94,7 +93,7 @@ public class FileController { throw new JsonException(400, "文件为空"); } String requestPath = URLUtils.getRequestFilePath(PREFIX + uid + "/file", request); - int i = fileService.saveFile(uid, file, requestPath, md5); + int i = fileService.getFileSystem().saveFile(uid, file, requestPath, md5); return JsonResult.getInstance(i); } @@ -113,7 +112,7 @@ public class FileController { @NotBlock public JsonResult getFileList(HttpServletRequest request, @PathVariable @UID int uid) throws IOException { String requestPath = URLUtils.getRequestFilePath(PREFIX + uid + "/fileList/byPath", request); - Collection[] fileList = fileService.getUserFileList(uid, requestPath); + Collection[] fileList = fileService.getFileSystem().getUserFileList(uid, requestPath); return JsonResult.getInstance(fileList); } @@ -130,7 +129,7 @@ public class FileController { @PathVariable @UID int uid, @RequestParam(value = "page", defaultValue = "1") Integer page) { PageHelper.startPage(page, 10); - List res = fileService.search(uid, key); + List res = fileService.getFileSystem().search(uid, key); PageInfo pageInfo = new PageInfo<>(res); return JsonResult.getInstance(pageInfo); } @@ -168,24 +167,25 @@ public class FileController { String source = URLDecoder.decode(requestPath, "UTF-8"); String target = URLDecoder.decode(info.getTarget(), "UTF-8"); for (NamePair file : info.getFiles()) { - fileService.copy(uid, source, target, uid, file.getSource(), file.getTarget(), info.isOverwrite()); + fileService.getFileSystem().copy(uid, source, target, uid, file.getSource(), file.getTarget(), info.isOverwrite()); } return JsonResult.getInstance(); } /** * 移动文件或目录到指定目录下 + * @TODO 允许空参数target * @param uid 用户ID */ @PutMapping("/fromPath/**") public JsonResult move(HttpServletRequest request, @PathVariable("uid") @UID(true) int uid, @RequestBody @Valid FileCopyOrMoveInfo info) - throws UnsupportedEncodingException { + throws IOException { String source = URLUtils.getRequestFilePath(PREFIX + uid + "/fromPath", request); String target = URLDecoder.decode(info.getTarget(), "UTF-8"); for (NamePair file : info.getFiles()) { - fileService.move(uid, source, target, file.getSource(), info.isOverwrite()); + fileService.getFileSystem().move(uid, source, target, file.getSource(), info.isOverwrite()); } return JsonResult.getInstance(); } @@ -197,12 +197,12 @@ public class FileController { public JsonResult rename(HttpServletRequest request, @PathVariable @UID(true) int uid, @RequestParam("oldName") @Valid @FileName String oldName, - @RequestParam("newName") @Valid @FileName String newName) throws JsonException, NoSuchFileException { + @RequestParam("newName") @Valid @FileName String newName) throws IOException { String from = URLUtils.getRequestFilePath(PREFIX + uid + "/name", request); if (newName.length() < 1) { throw new JsonException(400, "文件名不能为空"); } - fileService.rename(uid, from, oldName, newName); + fileService.getFileSystem().rename(uid, from, oldName, newName); return JsonResult.getInstance(); } @@ -223,7 +223,7 @@ public class FileController { @PathVariable @UID(true) int uid, @RequestBody @Validated FileNameList fileName) throws IOException { String path = URLUtils.getRequestFilePath(PREFIX + uid + "/content", request); - long res = fileService.deleteFile(uid, path, fileName.getFileName()); + long res = fileService.getFileSystem().deleteFile(uid, path, fileName.getFileName()); return JsonResult.getInstance(res); } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/controller/ResourceController.java b/src/main/java/com/xiaotao/saltedfishcloud/controller/ResourceController.java index 6c7c5f3bef5acd99e2d95e4398d91c833b463cf6..ccb23e463838f09656ea88f198da7f9db5bc3aab 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/controller/ResourceController.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/controller/ResourceController.java @@ -8,19 +8,17 @@ import com.xiaotao.saltedfishcloud.enums.ReadOnlyLevel; import com.xiaotao.saltedfishcloud.po.JsonResult; import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; import com.xiaotao.saltedfishcloud.po.file.FileInfo; -import com.xiaotao.saltedfishcloud.service.file.FileRecordService; -import com.xiaotao.saltedfishcloud.service.file.FileService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import com.xiaotao.saltedfishcloud.service.http.ResponseService; import com.xiaotao.saltedfishcloud.service.node.NodeService; import com.xiaotao.saltedfishcloud.utils.URLUtils; import com.xiaotao.saltedfishcloud.validator.FileName; import com.xiaotao.saltedfishcloud.validator.UID; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import javax.annotation.Resource; -import javax.annotation.security.RolesAllowed; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.io.IOException; @@ -35,18 +33,12 @@ import java.nio.file.NoSuchFileException; @RequestMapping(ResourceController.PREFIX + "{uid}") @Validated @ReadOnlyBlock +@RequiredArgsConstructor public class ResourceController { public static final String PREFIX = "/api/resource/"; - private final FileService fileService; + private final DiskFileSystemFactory fileService; private final NodeService nodeService; private final ResponseService responseService; - - public ResourceController(FileService fileService, NodeService nodeService, ResponseService responseService) { - this.fileService = fileService; - this.nodeService = nodeService; - this.responseService = responseService; - } - /** * 解析节点ID,获取节点ID对应的文件夹路径 * @param uid 用户ID @@ -70,10 +62,10 @@ public class ResourceController { HttpServletRequest request, @RequestParam("md5") String md5, @RequestParam("name") @Valid @FileName String name, - @RequestParam(value = "expr", defaultValue = "1") int expr) throws JsonProcessingException { + @RequestParam(value = "expr", defaultValue = "1") int expr) throws IOException { String filePath = URLUtils.getRequestFilePath(PREFIX + uid + "/FDC", request); BasicFileInfo fileInfo = new BasicFileInfo(name, md5); - String dc = fileService.getFileDC(uid, filePath, fileInfo, expr); + String dc = fileService.getFileSystem().getFileDC(uid, filePath, fileInfo, expr); return JsonResult.getInstance(dc); } @@ -104,8 +96,8 @@ public class ResourceController { @PathVariable("uid") int uid, HttpServletRequest request ) - throws NoSuchFileException, MalformedURLException, UnsupportedEncodingException { - FileInfo file = fileService.getFileByMD5(md5); + throws IOException { + FileInfo file = fileService.getFileSystem().getFileByMD5(md5); String path = URLUtils.getRequestFilePath(PREFIX + uid + "/fileContentByMD5/" + md5, request); String name; if (path.length() > 1) { diff --git a/src/main/java/com/xiaotao/saltedfishcloud/controller/UserController.java b/src/main/java/com/xiaotao/saltedfishcloud/controller/UserController.java index 8d909fb8adf81df9c1b5b6968f7240a17b1187bf..992aa83eaf2ad6de234b38e11aacfc013b33f29c 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/controller/UserController.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/controller/UserController.java @@ -5,16 +5,18 @@ import com.github.pagehelper.PageInfo; import com.xiaotao.saltedfishcloud.config.DiskConfig; import com.xiaotao.saltedfishcloud.config.security.AllowAnonymous; import com.xiaotao.saltedfishcloud.dao.mybatis.UserDao; +import com.xiaotao.saltedfishcloud.dao.redis.TokenDao; import com.xiaotao.saltedfishcloud.exception.JsonException; import com.xiaotao.saltedfishcloud.exception.UserNoExistException; import com.xiaotao.saltedfishcloud.po.JsonResult; import com.xiaotao.saltedfishcloud.po.QuotaInfo; import com.xiaotao.saltedfishcloud.po.User; -import com.xiaotao.saltedfishcloud.service.file.FileService; import com.xiaotao.saltedfishcloud.service.http.ResponseService; import com.xiaotao.saltedfishcloud.service.user.UserService; +import com.xiaotao.saltedfishcloud.utils.JwtUtils; import com.xiaotao.saltedfishcloud.utils.SecureUtils; import com.xiaotao.saltedfishcloud.validator.UID; +import lombok.RequiredArgsConstructor; import lombok.var; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; @@ -23,8 +25,8 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import javax.annotation.Resource; import javax.annotation.security.RolesAllowed; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import javax.validation.constraints.Max; @@ -37,28 +39,24 @@ import java.util.List; @RequestMapping(UserController.PREFIX) @ResponseBody @Validated +@RequiredArgsConstructor public class UserController { public static final String PREFIX = "/api/user"; - @Resource - UserService userService; - @Resource - FileService fileService; - - @Resource - ResponseService responseService; - - @Resource - UserDao userDao; + private final UserService userService; + private final ResponseService responseService; + private final UserDao userDao; + private final TokenDao tokenDao; /** - * 获取用户基本信息 + * 获取用户基本信息,并刷新token有效期 */ @GetMapping - public JsonResult getUserInfo() throws UserNoExistException { + public JsonResult getUserInfo(HttpServletRequest request) throws UserNoExistException { var user = SecureUtils.getSpringSecurityUser(); if (user == null) { throw new JsonException(401, "未登录"); } + tokenDao.setToken(user.getUsername(), request.getHeader(JwtUtils.AUTHORIZATION)); return JsonResult.getInstance(user); } @@ -136,14 +134,17 @@ public class UserController { @RequestParam("new") String newPasswd, @PathVariable("uid") @UID int uid, @RequestParam(value = "force", defaultValue = "false") boolean force) throws AccessDeniedException { + User user = SecureUtils.getSpringSecurityUser(); if (force) { - if ( SecureUtils.getSpringSecurityUser().getType() != User.TYPE_ADMIN) { + if ( user.getType() != User.TYPE_ADMIN) { throw new AccessDeniedException("非管理员不允许使用force参数"); } else { userDao.modifyPassword(uid, SecureUtils.getPassswd(newPasswd)); + tokenDao.cleanUserToken(user.getUsername()); return JsonResult.getInstance(200, null, "force reset"); } } else { + tokenDao.cleanUserToken(user.getUsername()); int i = userService.modifyPasswd(uid, oldPasswd, newPasswd); return JsonResult.getInstance(200, i, "ok"); } @@ -158,6 +159,10 @@ public class UserController { @RolesAllowed({"ADMIN"}) public JsonResult grant(@PathVariable("uid") int uid, @PathVariable("typeCode") int type) { + User user = userDao.getUserById(uid); + if (user == null) { + throw new UserNoExistException(); + } userService.grant(uid, type); return JsonResult.getInstance(); } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/dao/redis/RedisDao.java b/src/main/java/com/xiaotao/saltedfishcloud/dao/redis/RedisDao.java new file mode 100644 index 0000000000000000000000000000000000000000..7b6947eea84a1e8cdcbb7be6c632f12fae72a7ff --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/dao/redis/RedisDao.java @@ -0,0 +1,30 @@ +package com.xiaotao.saltedfishcloud.dao.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Set; + +@Component +@RequiredArgsConstructor +public class RedisDao { + private final RedisTemplate redisTemplate; + + /** + * 通过表达式使用scan方法扫描匹配的key(而不是keys) + * @param pattern key匹配表达式 + * @return 匹配的key集合 + */ + public Set scanKeys(String pattern) { + ScanOptions opts = ScanOptions.scanOptions().match(pattern).count(1000).build(); + Set res = new HashSet<>(); + return redisTemplate.execute((RedisCallback>) e -> { + e.scan(opts).forEachRemaining(r -> res.add(new String(r))); + return res; + }); + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/dao/redis/TokenDao.java b/src/main/java/com/xiaotao/saltedfishcloud/dao/redis/TokenDao.java new file mode 100644 index 0000000000000000000000000000000000000000..5a48d3d1efc3b3d2dc8674ffd664b0a8a589fae7 --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/dao/redis/TokenDao.java @@ -0,0 +1,54 @@ +package com.xiaotao.saltedfishcloud.dao.redis; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TokenDao { + private final RedisTemplate redisTemplate; + private final RedisDao redisDao; + + /** + * 获取用户鉴权token在redis中的key + * @param username token对应的用户名 + * @param token token + * @return key + */ + public static String getTokenKey(String username, String token) { + return "xyy::token::" + username + "::" + token; + } + + /** + * 添加用户鉴权token到Redis缓存 + * @param username token对应的用户名 + * @param token token + */ + public void setToken(String username, String token) { + redisTemplate.opsForValue().set(getTokenKey(username, token), "1", Duration.ofDays(2)); + } + + /** + * 清理指定用户的所有已注册token,操作将导致用户需要重新登录 + * @param username 用户名 + */ + public void cleanUserToken(String username) { + redisTemplate.delete(redisDao.scanKeys("xyy::token::" + username)); + } + + /** + * 判断用户鉴权token是否有效 + * @param username token对应的用户名 + * @param token token + * @return token有效返回true,否则返回false + */ + public boolean isTokenValid(String username, String token) { + return redisTemplate.opsForValue().get(getTokenKey(username, token)) != null; + } + +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/init/ConfigureInitializer.java b/src/main/java/com/xiaotao/saltedfishcloud/init/ConfigureInitializer.java index 10b98acc8b3f99a93e9b51407b5c721c73608a31..953e2a76cd9ba6f64d805cb4fa8fbcde7e09e9b1 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/init/ConfigureInitializer.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/init/ConfigureInitializer.java @@ -8,6 +8,9 @@ import com.xiaotao.saltedfishcloud.service.config.ConfigName; import com.xiaotao.saltedfishcloud.service.config.ConfigService; import com.xiaotao.saltedfishcloud.service.config.version.Version; import com.xiaotao.saltedfishcloud.service.config.version.VersionTag; +import com.xiaotao.saltedfishcloud.utils.JwtUtils; +import com.xiaotao.saltedfishcloud.utils.StringUtils; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; @@ -16,14 +19,18 @@ import org.springframework.stereotype.Component; import javax.annotation.Resource; +/** + * 配置信息初始化器 + * @TODO 优化代码,封装每个配置项的配置和初始化流程 + */ @Component @Slf4j @Order(2) +@RequiredArgsConstructor public class ConfigureInitializer implements ApplicationRunner { - @Resource - private ConfigDao configDao; - @Resource - private ConfigService configService; + private final ConfigDao configDao; + private final ConfigService configService; + private final CommandLineOption commandLineOption; @Override public void run(ApplicationArguments args) throws Exception { @@ -44,7 +51,7 @@ public class ConfigureInitializer implements ApplicationRunner { storeType = DiskConfig.STORE_TYPE.toString(); } else { - String modeSwitch = CommandLineOption.getValue(CommandLineOption.SWITCH); + String modeSwitch = commandLineOption.getValue(CommandLineOption.SWITCH); if (modeSwitch != null) { if (!configService.setStoreType(StoreType.valueOf(modeSwitch))) { log.warn("系统当前已处于" + modeSwitch + "存储模式下,切换行为已忽略"); @@ -62,6 +69,14 @@ public class ConfigureInitializer implements ApplicationRunner { } } + String secret = configDao.getConfigure(ConfigName.TOKEN_SECRET); + if (secret == null) { + secret = StringUtils.getRandomString(32, true); + log.info("[初始化]生成token密钥"); + configDao.setConfigure(ConfigName.TOKEN_SECRET, secret); + } + JwtUtils.setSecret(secret); + // 服务器配置记录覆盖默认的开局配置记录 DiskConfig.STORE_TYPE = StoreType.valueOf(storeType); DiskConfig.REG_CODE = regCode; @@ -74,5 +89,7 @@ public class ConfigureInitializer implements ApplicationRunner { log.warn("正在使用非发行版本,系统运行可能存在不稳定甚至出现数据损坏,请勿用于线上正式环境"); log.warn("正在使用非发行版本,系统运行可能存在不稳定甚至出现数据损坏,请勿用于线上正式环境"); } + + } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/config/ConfigName.java b/src/main/java/com/xiaotao/saltedfishcloud/service/config/ConfigName.java index 11c27fd5ff6f26720dd459adae979214d50594a1..f2990d99c20875a4d37e340a3205c94cad564775 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/config/ConfigName.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/config/ConfigName.java @@ -4,5 +4,6 @@ public enum ConfigName { REG_CODE, // 注册邀请码 STORE_TYPE, // 存储模式 SYNC_DELAY, // 同步延迟 - VERSION // 上次运行的系统版本 + VERSION, // 上次运行的系统版本 + TOKEN_SECRET // token密钥 } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/config/StoreTypeSwitch.java b/src/main/java/com/xiaotao/saltedfishcloud/service/config/StoreTypeSwitch.java index e470b3297888e1d0d73ff30b450947abd7d28f16..71885e80fda13abc346aabc587b636c9a5e978c3 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/config/StoreTypeSwitch.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/config/StoreTypeSwitch.java @@ -6,8 +6,8 @@ import com.xiaotao.saltedfishcloud.dao.mybatis.ConfigDao; import com.xiaotao.saltedfishcloud.dao.mybatis.UserDao; import com.xiaotao.saltedfishcloud.po.User; import com.xiaotao.saltedfishcloud.po.file.FileInfo; -import com.xiaotao.saltedfishcloud.service.file.FileService; -import com.xiaotao.saltedfishcloud.service.file.StoreService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; +import com.xiaotao.saltedfishcloud.service.file.localstore.StoreServiceFactory; import com.xiaotao.saltedfishcloud.utils.FileUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -18,7 +18,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @Component @Slf4j @@ -26,11 +28,11 @@ public class StoreTypeSwitch { @Resource private UserDao userDao; @Resource - private StoreService storeService; + private StoreServiceFactory storeServiceFactory; @Resource private ConfigDao configDao; @Resource - private FileService fileService; + private DiskFileSystemFactory fileService; public void switchTo(StoreType targetType) throws IOException { if (targetType == StoreType.RAW) switchToRaw(); @@ -50,7 +52,7 @@ public class StoreTypeSwitch { for (User user : users) { int uid = user.getId(); log.info("Processing user data: " + user.getUsername()); - Map> allFile = fileService.collectFiles(uid, false); + Map> allFile = fileService.getFileSystem().collectFiles(uid, false); for (Map.Entry> entry : allFile.entrySet()) { String p = entry.getKey(); List files = entry.getValue(); @@ -84,7 +86,7 @@ public class StoreTypeSwitch { users.add(User.getPublicUser()); for (User user : users) { log.info("Processing user data: " + user.getUsername()); - LinkedHashMap> allFile = fileService.collectFiles(user.getId(), false); + LinkedHashMap> allFile = fileService.getFileSystem().collectFiles(user.getId(), false); // 创建本地文件 for (Map.Entry> entry : allFile.entrySet()) { String path = entry.getKey(); @@ -98,7 +100,7 @@ public class StoreTypeSwitch { continue; } log.info("Copy file: " + source + " -> " + target); - storeService.store(user.getId(), Files.newInputStream(source), path, fileInfo); + storeServiceFactory.getService().store(user.getId(), Files.newInputStream(source), path, fileInfo); } } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/download/DownloadService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/download/DownloadService.java index 6185051a0d12dc8d0d2b60be27f9eeb8db0f651d..cf6d6052c3a75e194bb3b599295d760fbc484d9e 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/download/DownloadService.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/download/DownloadService.java @@ -11,7 +11,8 @@ import com.xiaotao.saltedfishcloud.po.param.TaskType; import com.xiaotao.saltedfishcloud.service.async.context.TaskContext; import com.xiaotao.saltedfishcloud.service.async.context.TaskContextFactory; import com.xiaotao.saltedfishcloud.service.async.context.TaskManager; -import com.xiaotao.saltedfishcloud.service.file.FileService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystem; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import com.xiaotao.saltedfishcloud.service.node.NodeService; import lombok.extern.slf4j.Slf4j; import lombok.var; @@ -47,7 +48,7 @@ public class DownloadService { @Resource private NodeService nodeService; @Resource - private FileService fileService; + private DiskFileSystemFactory fileService; private final TaskManager taskManager; /** @@ -147,6 +148,7 @@ public class DownloadService { downloadDao.save(info); log.debug("Task ON Ready"); }); + DiskFileSystem fileService = this.fileService.getFileSystem(); context.onSuccess(() -> { info.state = DownloadTaskInfo.State.FINISH; diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/FileRecordService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/FileRecordService.java index 126a69bd04447bdf27618e2ce015a37981cdda3f..e75e000304f70e55894212e7f5e147a8c05f3abc 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/file/FileRecordService.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/FileRecordService.java @@ -7,6 +7,7 @@ import com.xiaotao.saltedfishcloud.helper.PathBuilder; import com.xiaotao.saltedfishcloud.po.NodeInfo; import com.xiaotao.saltedfishcloud.po.file.FileInfo; import com.xiaotao.saltedfishcloud.service.node.NodeService; +import com.xiaotao.saltedfishcloud.utils.PathUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; @@ -14,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -25,7 +27,6 @@ import java.util.List; */ @Service @Slf4j -@Transactional(rollbackFor = Exception.class) public class FileRecordService { @Resource private FileDao fileDao; @@ -36,9 +37,11 @@ public class FileRecordService { @Resource private NodeDao nodeDao; - @Resource - private FileService fileService; - + static class PathIdPair { + public String path; + public String nid; + public PathIdPair(String path, String nid) {this.path = path;this.nid = nid;} + } /** * 操作数据库复制网盘文件或目录到指定目录下 * @param uid 用户ID @@ -48,16 +51,13 @@ public class FileRecordService { * @param sourceName 要复制的文件或目录名 * @param overwrite 是否覆盖已存在的文件 */ + @Transactional(rollbackFor = Exception.class) public void copy(int uid, String source, String target, int targetId, String sourceName, String targetName, boolean overwrite) throws NoSuchFileException { - class PathIdPair { - public String path; - public String nid; - public PathIdPair(String path, String nid) {this.path = path;this.nid = nid;} - } PathBuilder pathBuilder = new PathBuilder(); pathBuilder.setForcePrefix(true); int prefixLength = source.length() + 1 + sourceName.length(); + FileInfo sourceInfo = fileDao.getFileInfo(uid, sourceName, nodeService.getLastNodeInfoByPath(uid, source).getId()); if (sourceInfo == null) throw new NoSuchFileException("文件 " + source + "/" + sourceName + " 不存在"); // 文件直接添加单条记录 @@ -66,6 +66,10 @@ public class FileRecordService { return ; } + if (targetId == uid && sourceName.equals(targetName) && PathUtils.isSubDir(source + "/" + sourceName, target + "/" + targetName)) { + throw new IllegalArgumentException("目标目录不能是源目录的子目录"); + } + // 需要遍历的目录列表 LinkedList t = new LinkedList<>(); String sourceRoot = source + "/" + sourceName; @@ -113,13 +117,20 @@ public class FileRecordService { * @param overwrite 是否覆盖原文件信息 * @throws NoSuchFileException 当原目录或目标目录不存在时抛出 */ + @Transactional(rollbackFor = Exception.class) public void move(int uid, String source, String target, String name, boolean overwrite) throws NoSuchFileException { NodeInfo sourceInfo = nodeService.getLastNodeInfoByPath(uid, source); NodeInfo targetInfo = nodeService.getLastNodeInfoByPath(uid, target); FileInfo sourceFileInfo = fileDao.getFileInfo(uid, name, sourceInfo.getId()); + if (sourceFileInfo == null) { + throw new NoSuchFileException("资源不存在,目录" + source + " 文件名:" + name); + } FileInfo targetFileInfo = fileDao.getFileInfo(uid, name, targetInfo.getId()); if (sourceFileInfo.isDir()) { + if (PathUtils.isSubDir(source + "/" + name, target + "/" + name)) { + throw new IllegalArgumentException("目标目录不能为源目录的子目录"); + } if (targetFileInfo != null) { // 当移动目录时存在同名文件或目录 if (targetFileInfo.isDir()) { @@ -164,6 +175,7 @@ public class FileRecordService { * @param path 文件所在路径 * @return 添加数量 */ + @Transactional(rollbackFor = Exception.class) public int addRecord(int uid, String name, Long size, String md5, String path) throws NoSuchFileException { NodeInfo node = nodeService.getLastNodeInfoByPath(uid, path); return fileDao.addRecord(uid, name, size, md5, node.getId()); @@ -178,7 +190,8 @@ public class FileRecordService { * @param newMd5 新的文件MD5 * @return 影响行数 */ - int updateFileRecord(int uid, String name, String path, Long newSize, String newMd5) throws NoSuchFileException { + @Transactional(rollbackFor = Exception.class) + public int updateFileRecord(int uid, String name, String path, Long newSize, String newMd5) throws NoSuchFileException { NodeInfo node = nodeService.getLastNodeInfoByPath(uid, path); return fileDao.updateRecord(uid, name, node.getId(), newSize, newMd5); } @@ -190,6 +203,7 @@ public class FileRecordService { * @param name 文件名列表 * @return 删除的文件个数 */ + @Transactional(rollbackFor = Exception.class) public List deleteRecords(int uid, String path, Collection name) throws NoSuchFileException { NodeInfo node = nodeService.getLastNodeInfoByPath(uid, path); List infos = fileDao.getFilesInfo(uid, name, node.getId()); @@ -224,6 +238,7 @@ public class FileRecordService { * @throws NoSuchFileException * 当父级目录不存在时抛出 */ + @Transactional(rollbackFor = Exception.class) public String mkdir(int uid, String name, String path) throws NoSuchFileException { log.debug("mkdir " + name + " at " + path); NodeInfo node = nodeService.getLastNodeInfoByPath(uid, path); @@ -236,6 +251,24 @@ public class FileRecordService { return nodeId; } + + @Transactional(rollbackFor = Exception.class) + public void mkdirs(int uid, String path) { + PathBuilder pb = new PathBuilder(); + pb.append(path); + String id = "root"; + for (String s : pb.getPath()) { + NodeInfo nodeInfo = nodeDao.getNodeByParentId(uid, id, s); + if (nodeInfo == null) { + String nid = nodeService.addNode(uid, s, id); + fileDao.addRecord(uid, s, -1L, nid, id); + id = nid; + } else { + id = nodeInfo.getId(); + } + } + } + /** * 对文件或文件夹进行重命名 * @param uid 用户ID @@ -243,6 +276,7 @@ public class FileRecordService { * @param oldName 旧文件名 * @param newName 新文件名 */ + @Transactional(rollbackFor = Exception.class) public void rename(int uid, String path, String oldName, String newName) throws NoSuchFileException { NodeInfo pathNodeInfo = nodeService.getLastNodeInfoByPath(uid, path); FileInfo fileInfo = fileDao.getFileInfo(uid, oldName, pathNodeInfo.getId()); diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/StoreService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/StoreService.java deleted file mode 100644 index 1c8f45c845cec24d35e9b896fd66cd6d1a08ac58..0000000000000000000000000000000000000000 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/file/StoreService.java +++ /dev/null @@ -1,339 +0,0 @@ -package com.xiaotao.saltedfishcloud.service.file; - -import com.xiaotao.saltedfishcloud.config.DiskConfig; -import com.xiaotao.saltedfishcloud.config.StoreType; -import com.xiaotao.saltedfishcloud.exception.JsonException; -import com.xiaotao.saltedfishcloud.exception.UnableOverwriteException; -import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; -import com.xiaotao.saltedfishcloud.po.file.DirCollection; -import com.xiaotao.saltedfishcloud.po.file.FileInfo; -import com.xiaotao.saltedfishcloud.service.file.exception.DirectoryAlreadyExistsException; -import com.xiaotao.saltedfishcloud.service.file.path.PathHandler; -import com.xiaotao.saltedfishcloud.utils.FileUtils; -import com.xiaotao.saltedfishcloud.utils.StringUtils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.DuplicateKeyException; -import org.springframework.stereotype.Service; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Collection; -import java.util.concurrent.atomic.AtomicLong; - -/** - * 本地文件存储服务,用于管理本地文件系统中的文件的创建,复制,删除,移动等操作 - */ -@Service -@Slf4j -public class StoreService { - /** - * 通过文件移动的方式存储文件到网盘系统,相对于{@link #store}方法,避免了文件的重复写入操作。对本地文件操作后,原路径文件不再存在

- * 如果是UNIQUE存储模式,则会先将文件移动到存储仓库(若仓库已存在文件则忽略该操作),随后再在目标网盘目录创建文件链接

- * 如果是RAW存储模式,则会直接移动到目标位置。若本地文件路径与网盘路径对应的本地路径相同,操作将忽略。 - * @param uid 用户ID - * @param nativePath 本地文件路径 - * @param diskPath 网盘路径 - * @param fileInfo 文件信息 - */ - public void moveToSave(int uid, Path nativePath, String diskPath, BasicFileInfo fileInfo) throws IOException { - Path sourcePath = nativePath; // 本地源文件 - Path targetPath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, diskPath, fileInfo)); // 被移动到的目标位置 - if (DiskConfig.STORE_TYPE == StoreType.UNIQUE) { - // 唯一文件仓库中的路径 - sourcePath = Paths.get(DiskConfig.uniquePathHandler.getStorePath(uid, diskPath, fileInfo)); // 文件仓库源文件路径 - if (Files.exists(sourcePath)) { - // 已存在相同文件时,直接删除本地文件 - log.debug("file md5 HIT: {}", fileInfo.getMd5()); - Files.delete(nativePath); - if (Files.exists(targetPath)) { - Files.delete(targetPath); - } - } else { - // 将本地文件移动到唯一仓库 - log.debug("file md5 NOT HIT: {}", fileInfo.getMd5()); - FileUtils.createParentDirectory(sourcePath); - Files.move(nativePath, sourcePath, StandardCopyOption.REPLACE_EXISTING); - } - // 在目标网盘位置创建文件仓库中的文件链接 - log.debug("Create file link: {} <==> {}", targetPath, sourcePath); - Files.createLink(targetPath, sourcePath); - } else { - // 非唯一模式,直接将文件移动到目标位置 - if (!sourcePath.equals(targetPath)) { - log.debug("File move {} => {}", sourcePath, targetPath); - Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } - } - - /** - * 在本地存储中复制用户网盘文件 - * @param uid 用户ID - * @param source 所在网盘路径 - * @param target 目的地网盘路径 - * @param sourceName 文件名 - * @param overwrite 是否覆盖,若非true,则跳过该文件 - */ - public void copy(int uid, String source, String target, int targetId, String sourceName, String targetName, Boolean overwrite) throws IOException { - BasicFileInfo fileInfo = new BasicFileInfo(sourceName, null); - String localSource = DiskConfig.getPathHandler().getStorePath(uid, source, fileInfo); - String localTarget = DiskConfig.getPathHandler().getStorePath(targetId, target, null); - - fileInfo = FileInfo.getLocal(localSource); - Path sourcePath = Paths.get(localSource); - - // 判断源与目标是否存在 - if (!Files.exists(sourcePath)) { - throw new NoSuchFileException("资源 \"" + source + "/" + sourceName + "\" 不存在"); - } - if (!Files.exists(Paths.get(localTarget))) { - throw new NoSuchFileException("目标目录 " + target + " 不存在"); - } - - CopyOption[] option; - if (overwrite) { - option = new CopyOption[]{StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING}; - } else { - option = new CopyOption[]{StandardCopyOption.COPY_ATTRIBUTES}; - } - - if (fileInfo.isFile()) { - Files.copy(sourcePath, Paths.get(localTarget + "/" + targetName), option); - } - - if (fileInfo.isDir()) { - DirCollection dirCollection = FileUtils.scanDir(Paths.get(localSource)); - Path targetDir = Paths.get(localTarget + "/" + targetName); - if (!Files.exists(targetDir)) { - Files.createDirectory(targetDir); - } - // 先创建文件夹 - for(File dir: dirCollection.getDirList()) { - String src = dir.getPath().substring(localSource.length()); - String dest = targetDir + "/" + src; - log.debug("local filesystem mkdir: " + dest); - try { Files.createDirectory(Paths.get(dest)); } catch (FileAlreadyExistsException ignored) {} - } - - // 复制文件 - for(File file: dirCollection.getFileList()) { - String src = file.getPath().substring(localSource.length()); - String dest = localTarget + "/" + targetName + src; - if (DiskConfig.STORE_TYPE == StoreType.UNIQUE) { - log.debug("create hard link: " + file + " ==> " + dest); - Files.createLink(Paths.get(dest), Paths.get(file.getPath())); - } else { - log.debug("local filesystem copy: " + file + " ==> " + dest); - try { Files.copy(Paths.get(file.getPath()), Paths.get(dest), option); } - catch (FileAlreadyExistsException ignored) {} - } - } - } - } - - /** - * 向用户网盘目录中保存一个文件 - * @param uid 用户ID 0表示公共 - * @param input 输入的文件 - * @param targetDir 保存到的目标网盘目录位置(注意:不是本地真是路径) - * @param fileInfo 文件信息 - * @throws JsonException 存储文件出错 - * @throws DuplicateKeyException UNIQUE模式下两个不相同的文件发生MD5碰撞 - * @throws UnableOverwriteException 保存位置存在同名的目录 - */ - public void store(int uid, InputStream input, String targetDir, FileInfo fileInfo) throws JsonException, IOException { - Path md5Target = Paths.get(DiskConfig.uniquePathHandler.getStorePath(uid, targetDir, fileInfo)); - if (DiskConfig.STORE_TYPE == StoreType.UNIQUE) { - if (Files.exists(md5Target)) { - log.debug("file md5 HIT:" + fileInfo.getMd5()); - if (Files.size(md5Target) != fileInfo.getSize()) { - throw new DuplicateKeyException("文件MD5冲突"); - } - } else { - log.debug("file md5 NOT HIT, saving:" + fileInfo.getMd5()); - FileUtils.createParentDirectory(md5Target); - Files.copy(input, md5Target, StandardCopyOption.REPLACE_EXISTING); - } - } - Path rawTarget = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, targetDir, fileInfo)); - if (Files.exists(rawTarget) && Files.isDirectory(rawTarget)) { - throw new UnableOverwriteException(409, "已存在同名目录: " + targetDir + "/" + fileInfo.getName()); - } - FileUtils.createParentDirectory(rawTarget); - if (DiskConfig.STORE_TYPE == StoreType.UNIQUE) { - log.info("create hard link:" + md5Target + " <==> " + rawTarget); - if (Files.exists(rawTarget)) Files.delete(rawTarget); - Files.createLink(rawTarget, md5Target); - } else { - log.info("save file:" + rawTarget); - Files.copy(input, rawTarget, StandardCopyOption.REPLACE_EXISTING); - } - } - - /** - * 在本地存储中移动用户网盘文件 - * @param uid 用户ID - * @param source 所在网盘路径 - * @param target 目的地网盘路径 - * @param name 文件名 - * @param overwrite 是否覆盖原文件 - */ - public void move(int uid, String source, String target, String name, boolean overwrite) throws IOException { - PathHandler pathHandler = DiskConfig.getPathHandler(); - BasicFileInfo fileInfo = new BasicFileInfo(name, null); - Path sourcePath = Paths.get(pathHandler.getStorePath(uid, source, fileInfo)); - Path targetPath = Paths.get(pathHandler.getStorePath(uid, target, fileInfo)); - if (Files.exists(targetPath)) { - if (Files.isDirectory(sourcePath) != Files.isDirectory(targetPath)) { - throw new UnsupportedOperationException("文件类型不一致,无法移动"); - } - - if (Files.isDirectory(sourcePath)) { - // 目录则合并 - FileUtils.mergeDir(sourcePath.toString(), targetPath.toString(), overwrite); - } else if (overwrite){ - // 文件则替换移动(仅当overwrite为true时) - Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); - } else { - // 为了与数据库记录保持一致,原文件还是要删滴 - Files.delete(sourcePath); - } - } else { - Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); - } - } - - /** - * 文件重命名 - * @param uid 用户ID 0表示公共 - * @param path 文件所在路径 - * @param oldName 旧文件名 - * @param newName 新文件名 - */ - public void rename(int uid, String path, String oldName, String newName) throws JsonException { - String base = DiskConfig.getRawFileStoreRootPath(uid); - File origin = new File(base + "/" + path + "/" + oldName); - File dist = new File(base + "/" + path + "/" + newName); - if (!origin.exists()) { - throw new JsonException("原文件不存在"); - } - if (dist.exists()) { - throw new JsonException("文件名冲突"); - } - if (!origin.renameTo(dist)) { - throw new JsonException("移动失败"); - } - - } - - /** - * 在本地文件系统中创建文件夹 - * @param uid 用户ID - * @param path 所在路径 - * @param name 文件夹名 - * @throws FileAlreadyExistsException 目标已存在时抛出 - * @return 是否创建成功 - */ - public boolean mkdir(int uid, String path, String name) throws FileAlreadyExistsException, DirectoryAlreadyExistsException { - String localFilePath = DiskConfig.getRawFileStoreRootPath(uid) + "/" + path + "/" + name; - File file = new File(localFilePath); - if (file.mkdir()) { - return true; - } else { - if (file.exists()) { - if (file.isDirectory()) { - throw new DirectoryAlreadyExistsException(file + "/" + name); - } else { - throw new FileAlreadyExistsException(file + "/" + name); - } - } - log.error("在本地路径\"" + localFilePath + "\"创建文件夹失败"); - return false; - } - } - - /** - * 删除一个唯一存储类型的文件 - * @param md5 文件MD5 - * @return 删除的文件和目录数 - */ - public int delete(String md5) throws IOException { - int res = 1; - Path filePath = Paths.get(DiskConfig.getUniqueStoreRoot() + "/" + StringUtils.getUniquePath(md5)); - Files.delete(filePath); - log.debug("删除本地文件:" + filePath); - DirectoryStream paths = Files.newDirectoryStream(filePath.getParent()); - // 最里层目录 - if ( !paths.iterator().hasNext() ) { - log.debug("删除本地目录:" + filePath.getParent()); - res++; - paths.close(); - Files.delete(filePath.getParent()); - paths = Files.newDirectoryStream(filePath.getParent().getParent()); - - // 外层目录 - if ( !paths.iterator().hasNext()) { - log.debug("删除本地目录:" + filePath.getParent().getParent()); - res++; - Files.delete(filePath.getParent().getParent()); - paths.close(); - } - paths.close(); - } else { - paths.close(); - } - return res; - } - - /** - * 删除本地文件(文件夹会连同所有子文件和目录) - * @param uid 用户ID - * @param path 文件所在网盘目录的路径 - * @param files 文件名 - * @return 删除的文件和文件夹总数 - */ - public long delete(int uid, String path, Collection files) { - AtomicLong cnt = new AtomicLong(); - // 本地物理基础路径 - String basePath = DiskConfig.getRawFileStoreRootPath(uid) + "/" + path; - files.forEach(fileName -> { - - // 本地完整路径 - String local = basePath + "/" + fileName; - File file = new File(local); - if (file.isDirectory()) { - Path path1 = Paths.get(local); - try { - Files.walkFileTree(path1, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - log.debug("删除文件 " + file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - log.debug("删除目录 " + dir); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new JsonException(500, e.getMessage()); - } - } else { - if (!file.delete()){ - log.error("文件删除失败:" + file.getPath()); - } else { - cnt.incrementAndGet(); - } - } - }); - return cnt.longValue(); - } - -} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystem.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystem.java new file mode 100644 index 0000000000000000000000000000000000000000..b6512c3a0580dcbaf705290696e5b28dd29be6eb --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystem.java @@ -0,0 +1,188 @@ +package com.xiaotao.saltedfishcloud.service.file.filesystem; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xiaotao.saltedfishcloud.config.DiskConfig; +import com.xiaotao.saltedfishcloud.exception.JsonException; +import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; +import com.xiaotao.saltedfishcloud.po.file.FileDCInfo; +import com.xiaotao.saltedfishcloud.po.file.FileInfo; +import com.xiaotao.saltedfishcloud.utils.JwtUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * @TODO 增加基于节点ID操作的方法以避免通过路径查询节点ID + */ +public interface DiskFileSystem { + /** + * 在网盘中连同所有父级目录,创建一个目录 + * @param uid 用户ID + * @param path 网盘目录完整路径 + * @throws JsonException 目录树中某个部分与文件名冲突时抛出 + */ + void mkdirs(int uid, String path) throws IOException; + + /** + * 通过文件MD5获取一个存储在系统中的文件
+ * @param md5 文件MD5值 + * @return 文件信息,path为本地文件系统中的实际存储文件路径,文件名将被重命名为md5+原文件拓展名 + * @throws NoSuchFileException 没有文件时抛出 + */ + FileInfo getFileByMD5(String md5) throws IOException; + + /** + * 复制指定用户的文件或目录到指定用户的某个目录下 + * @param uid + * 原资源的用户ID + * @param source + * 要操作的文件所在的网盘目录 + * @param target + * 复制到的目的地目录 + * @param targetUid + * 目标用户ID + * @param sourceName + * 源文件或目录名 + * @param targetName + * 目标文件或目录名, + * @param overwrite + * 是否覆盖 + */ + void copy(int uid, String source, String target, int targetUid, String sourceName, String targetName, Boolean overwrite) throws IOException; + + /** + * 移动网盘中的文件或目录到指定目录下 + * @param uid 用户ID + * @param source 要被移动的网盘文件或目录所在目录 + * @param target 要移动到的目标目录 + * @param name 文件名 + * @param overwrite 是否覆盖原文件 + */ + void move(int uid, String source, String target, String name, boolean overwrite) throws IOException; + + /** + * 获取某个用户网盘目录下的所有文件信息 + * 若路径不存在则抛出异常 + * 若路径指向一个文件则返回null + * 若路径指向一个目录则返回一个集合,数组下标0为目录,1为文件 + * @param uid 用户ID + * @param path 网盘路径 + * @return 一个List数组,数组下标0为目录,1为文件,或null + */ + List[] getUserFileList(int uid, String path) throws IOException; + + /** + * 获取用户所有文件信息
+ * 默认正序为根目录优先,倒序为最深级目录优先 + * @param uid 用户ID + * @param reverse 目录排序倒序 + * @return 文件信息集合,key为目录名,value为该目录下的文件信息列表 + */ + LinkedHashMap> collectFiles(int uid, boolean reverse); + + /** + * 通过节点ID获取节点下的文件信息 + * @param uid 用户ID + * @param nodeId 节点ID + * @return 一个List数组,数组下标0为目录,1为文件,或null + */ + List[] getUserFileListByNodeId(int uid, String nodeId); + + List search(int uid, String key); + + /** + * 通过移动本地文件的方式存储文件 + * @param uid 用户ID + * @param nativeFilePath 本地文件路径 + * @param path 网盘路径 + * @throws IOException 存储出错 + */ + void moveToSaveFile(int uid, Path nativeFilePath, String path, FileInfo fileInfo) throws IOException; + + /** + * 保存数据流的数据到网盘系统中 + * @param uid 用户ID 0表示公共 + * @param stream 要保存的数据流 + * @param path 文件要保存到的网盘目录 + * @param fileInfo 文件信息 + * @throws IOException 本地文件写入失败时抛出 + * @throws JsonException 文件夹同名时抛出 + */ + int saveFile(int uid, + InputStream stream, + String path, + FileInfo fileInfo) throws IOException; + + /** + * 保存上传的文件到网盘系统中 + * @param uid 用户ID 0表示公共 + * @param file 接收到的文件对象 + * @param requestPath 请求的文件路径 + * @param md5 请求时传入的文件md5 + * @return 1 + * @throws IOException 本地文件写入失败时抛出 + * @throws JsonException 文件夹同名时抛出 + */ + int saveFile(int uid, + MultipartFile file, + String requestPath, + String md5) throws IOException; + + /** + * 创建文件夹 + * @param uid 用户ID 0表示公共 + * @param path 请求的路径 + * @param name 文件夹名称 + * @throws NoSuchFileException 当目标目录不存在时抛出 + */ + void mkdir(int uid, String path, String name) throws IOException; + + /** + * 删除文件 + * @param uid 用户ID 0表示公共 + * @param path 请求路径 + * @param name 文件名列表 + * @throws NoSuchFileException 当目标路径不存在时抛出 + * @return 删除的数量 + */ + long deleteFile(int uid, String path, List name) throws IOException; + + /** + * 重命名文件或目录 + * @param uid 用户ID 0表示公共 + * @param path 文件所在路径(相对用户网盘目录) + * @param name 被操作的文件名或文件夹名 + * @throws NoSuchFileException 当目标路径不存在时抛出 + * @param newName 新文件名 + */ + void rename(int uid, String path, String name, String newName) throws IOException; + + /** + * 获取网盘中文件的下载码 + * @param uid 用户ID + * @param path 文件所在网盘目录 + * @param fileInfo 文件信息 + * @param expr 下载码有效时长(单位:天),若小于0,则无限制 + */ + @SuppressWarnings("all") + default String getFileDC(int uid, String path, BasicFileInfo fileInfo, int expr) throws IOException { + Path localPath = Paths.get(DiskConfig.getPathHandler().getStorePath(uid, path, fileInfo)); + if ( !Files.exists(localPath) ){ + throw new JsonException(404, "文件不存在"); + } + FileDCInfo info = new FileDCInfo(); + info.setDir(path); + info.setMd5(fileInfo.getMd5()); + info.setName(fileInfo.getName()); + info.setUid(uid); + String token = JwtUtils.generateToken(new ObjectMapper().writeValueAsString(info), expr < 0 ? expr : expr*60*60*24); + return token; + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystemFactory.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystemFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e5706f8f76859ef766a23dd08ff4c2b4da1a2312 --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystemFactory.java @@ -0,0 +1,5 @@ +package com.xiaotao.saltedfishcloud.service.file.filesystem; + +public interface DiskFileSystemFactory { + DiskFileSystem getFileSystem(); +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystemFactoryImpl.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystemFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..46a3ea83bd9d8e7ca4cc55337eba1c77532bb5e4 --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/DiskFileSystemFactoryImpl.java @@ -0,0 +1,14 @@ +package com.xiaotao.saltedfishcloud.service.file.filesystem; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DiskFileSystemFactoryImpl implements DiskFileSystemFactory { + private final LocalDiskFileSystem localDiskFileSystem; + @Override + public DiskFileSystem getFileSystem() { + return localDiskFileSystem; + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/FileService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/LocalDiskFileSystem.java similarity index 38% rename from src/main/java/com/xiaotao/saltedfishcloud/service/file/FileService.java rename to src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/LocalDiskFileSystem.java index 83417e71955b13e521ec9b07ebcd7593c6d6819b..44b36619e3065b1e29113dc0bd77d6eb50850bf9 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/file/FileService.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/filesystem/LocalDiskFileSystem.java @@ -1,74 +1,49 @@ -package com.xiaotao.saltedfishcloud.service.file; +package com.xiaotao.saltedfishcloud.service.file.filesystem; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.xiaotao.saltedfishcloud.config.DiskConfig; import com.xiaotao.saltedfishcloud.config.StoreType; import com.xiaotao.saltedfishcloud.dao.mybatis.FileDao; import com.xiaotao.saltedfishcloud.exception.JsonException; -import com.xiaotao.saltedfishcloud.helper.PathBuilder; import com.xiaotao.saltedfishcloud.po.NodeInfo; import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; -import com.xiaotao.saltedfishcloud.po.file.FileDCInfo; import com.xiaotao.saltedfishcloud.po.file.FileInfo; -import com.xiaotao.saltedfishcloud.service.file.exception.DirectoryAlreadyExistsException; +import com.xiaotao.saltedfishcloud.service.file.FileRecordService; +import com.xiaotao.saltedfishcloud.service.file.localstore.StoreServiceFactory; import com.xiaotao.saltedfishcloud.service.node.NodeService; import com.xiaotao.saltedfishcloud.utils.FileUtils; -import com.xiaotao.saltedfishcloud.utils.JwtUtils; import com.xiaotao.saltedfishcloud.utils.SetUtils; -import lombok.extern.slf4j.Slf4j; -import lombok.var; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.nio.file.*; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; -/** - * 对网盘文件的增删改查操作 - */ -@Service("fileService") -@Slf4j -@Transactional(rollbackFor = Exception.class) -public class FileService { - @javax.annotation.Resource - FileDao fileDao; - @javax.annotation.Resource - FileRecordService fileRecordService; - @javax.annotation.Resource - StoreService storeService; - @javax.annotation.Resource - NodeService nodeService; +@Component +@RequiredArgsConstructor +public class LocalDiskFileSystem implements DiskFileSystem { + private final StoreServiceFactory storeServiceFactory; + private final FileDao fileDao; + private final FileRecordService fileRecordService; + private final NodeService nodeService; - /** - * 在网盘中连同所有父级目录,创建一个目录 - * @param uid 用户ID - * @param path 网盘目录完整路径 - * @throws JsonException 目录树中某个部分与文件名冲突时抛出 - */ - public void mkdirs(int uid, String path) throws FileAlreadyExistsException, NoSuchFileException { - var pb = new PathBuilder(); - pb.append(path); - var p = new StringBuilder("/"); - for (String node : pb.getPath()) { - try { - mkdir(uid, p.toString(), node); - } catch (DirectoryAlreadyExistsException ignored) {} - p.append("/").append(node); - } + @Override + @Transactional(rollbackFor = Exception.class) + public void mkdirs(int uid, String path) throws IOException { + fileRecordService.mkdirs(uid, path); + storeServiceFactory.getService().mkdir(uid, path, ""); } - /** - * 通过文件MD5获取一个存储在系统中的文件
- * @param md5 文件MD5值 - * @return 文件信息,path为本地文件系统中的实际存储文件路径,文件名将被重命名为md5+原文件拓展名 - * @throws NoSuchFileException 没有文件时抛出 - */ + @Override public FileInfo getFileByMD5(String md5) throws NoSuchFileException { FileInfo fileInfo; List files = fileDao.getFilesByMD5(md5, 1); @@ -81,70 +56,29 @@ public class FileService { return fileInfo; } - /** - * 复制指定用户的文件或目录到指定用户的某个目录下 - * @param uid - * 原资源的用户ID - * @param source - * 要操作的文件所在的网盘目录 - * @param target - * 复制到的目的地目录 - * @param targetUid - * 目标用户ID - * @param sourceName - * 源文件或目录名 - * @param targetName - * 目标文件或目录名, - * @param overwrite - * 是否覆盖 - */ + @Override + @Transactional(rollbackFor = Exception.class) public void copy(int uid, String source, String target, int targetUid, String sourceName, String targetName, Boolean overwrite) throws IOException { - if (PathBuilder.formatPath(source).equals(PathBuilder.formatPath(target)) && sourceName.equals(targetName)) { - throw new IllegalArgumentException("无法原地复制"); - } - fileRecordService.copy(uid, source, target, targetUid, sourceName, targetName,overwrite); - storeService.copy(uid, source, target, targetUid, sourceName, targetName, overwrite); + fileRecordService.copy(uid, source, target, targetUid, sourceName, targetName, overwrite); + storeServiceFactory.getService().copy(uid, source, target, targetUid, sourceName, targetName, overwrite); } - /** - * 移动网盘中的文件或目录到指定目录下 - * @param uid 用户ID - * @param source 要被移动的网盘文件或目录所在目录 - * @param target 要移动到的目标目录 - * @param name 文件名 - * @param overwrite 是否覆盖原文件 - */ - public void move(int uid, String source, String target, String name, boolean overwrite) { + @Override + @Transactional(rollbackFor = Exception.class) + public void move(int uid, String source, String target, String name, boolean overwrite) throws IOException { try { target = URLDecoder.decode(target, "UTF-8"); - if (PathBuilder.formatPath(target).equals(PathBuilder.formatPath(source))) { - throw new IllegalArgumentException("无法原地移动"); - } fileRecordService.move(uid, source, target, name, overwrite); - storeService.move(uid, source, target, name, overwrite); + storeServiceFactory.getService().move(uid, source, target, name, overwrite); } catch (DuplicateKeyException e) { throw new JsonException(409, "目标目录下已存在 " + name + " 暂不支持目录合并或移动覆盖"); } catch (UnsupportedEncodingException e) { throw new JsonException(400, "不支持的编码(请使用UTF-8)"); - } catch (IllegalArgumentException e) { - throw new JsonException(422, e.getMessage()); - } catch (Exception e) { - e.printStackTrace(); - throw new JsonException(404, "资源不存在"); } } - /** - * 获取某个用户网盘目录下的所有文件信息 - * 若路径不存在则抛出异常 - * 若路径指向一个文件则返回null - * 若路径指向一个目录则返回一个集合,数组下标0为目录,1为文件 - * @param uid 用户ID - * @param path 网盘路径 - * @return 一个List数组,数组下标0为目录,1为文件,或null - */ + @Override public List[] getUserFileList(int uid, String path) throws IOException { - if (uid == 0 || DiskConfig.STORE_TYPE == StoreType.RAW) { // 初始化用户目录 String baseLocalPath = DiskConfig.getRawFileStoreRootPath(uid); @@ -160,13 +94,7 @@ public class FileService { return getUserFileListByNodeId(uid, nodeId.getId()); } - /** - * 获取用户所有文件信息
- * 默认正序为根目录优先,倒序为最深级目录优先 - * @param uid 用户ID - * @param reverse 目录排序倒序 - * @return 文件信息集合,key为目录名,value为该目录下的文件信息列表 - */ + @Override public LinkedHashMap> collectFiles(int uid, boolean reverse) { LinkedHashMap> res = new LinkedHashMap<>(); List nodes = new LinkedList<>(); @@ -182,12 +110,7 @@ public class FileService { return res; } - /** - * 通过节点ID获取节点下的文件信息 - * @param uid 用户ID - * @param nodeId 节点ID - * @return 一个List数组,数组下标0为目录,1为文件,或null - */ + @Override @SuppressWarnings("unchecked") public List[] getUserFileListByNodeId(int uid, String nodeId) { List fileList = fileDao.getFileListByNodeId(uid, nodeId); @@ -204,92 +127,30 @@ public class FileService { return new List[]{dirs, files}; } - /** - * 通过一个本地路径获取获取该路径下的所有文件列表并区分文件与目录 - * 若路径不存在则抛出异常 - * 若路径指向一个文件则返回null - * 若路径指向一个目录则返回一个集合,数组下标0为目录,1为文件 - * @param localPath 本地文件夹路径 - * @return 一个List数组,数组下标0为目录,1为文件,或null - * @throws FileNotFoundException 路径不存在 - */ - public Collection[] getFileList(String localPath) throws FileNotFoundException { - File file = new File(localPath); - return getFileList(file); - } - /** - * 通过一个本地路径获取获取该路径下的所有文件列表并区分文件与目录 - * 若路径不存在则抛出异常 - * 若路径指向一个文件则返回null - * 若路径指向一个目录则返回一个List数组,数组下标0为目录,1为文件 - * @throws FileNotFoundException 路径不存在 - * @param file 本地文件夹路径 - * @return 一个List数组,数组下标0为目录,1为文件,或null - */ - @SuppressWarnings("unchecked") - public Collection[] getFileList(File file) throws FileNotFoundException { - if (!file.exists()) { - throw new FileNotFoundException(); - } - if (file.isFile()) { - return null; - } - List dirs = new LinkedList<>(); - List files = new LinkedList<>(); - try { - for (File listFile : Objects.requireNonNull(file.listFiles())) { - if (listFile.isDirectory()) { - dirs.add(new FileInfo(listFile)); - } else { - files.add(new FileInfo(listFile)); - } - } - } catch (NullPointerException e) { - // do nothing - } - return new List[]{dirs, files}; - } - - - + @Override public List search(int uid, String key) { key = "%" + key.replaceAll("%", "\\%").replaceAll("/s+", "%") + "%"; return fileDao.search(uid, key); } - /** - * 通过移动本地文件的方式存储文件 - * @param uid 用户ID - * @param nativeFilePath 本地文件路径 - * @param path 网盘路径 - * @throws IOException 存储出错 - */ + @Override + @Transactional(rollbackFor = Exception.class) public void moveToSaveFile(int uid, Path nativeFilePath, String path, FileInfo fileInfo) throws IOException { - storeService.moveToSave(uid, nativeFilePath, path, fileInfo); + storeServiceFactory.getService().moveToSave(uid, nativeFilePath, path, fileInfo); int res = fileRecordService.addRecord(uid, fileInfo.getName(), fileInfo.getSize(), fileInfo.getMd5(), path); if ( res == 0) { fileRecordService.updateFileRecord(uid, fileInfo.getName(), path, fileInfo.getSize(), fileInfo.getMd5()); } } + @Override + @Transactional(rollbackFor = Exception.class) + public int saveFile(int uid, InputStream stream, String path, FileInfo fileInfo) throws IOException { - /** - * 保存数据流的数据到网盘系统中 - * @param uid 用户ID 0表示公共 - * @param stream 要保存的数据流 - * @param path 文件要保存到的网盘目录 - * @param fileInfo 文件信息 - * @throws IOException 本地文件写入失败时抛出 - * @throws JsonException 文件夹同名时抛出 - */ - public int saveFile(int uid, - InputStream stream, - String path, - FileInfo fileInfo) throws IOException { if (fileInfo.getMd5() == null) { fileInfo.updateMd5(); } - storeService.store(uid, stream, path, fileInfo); + storeServiceFactory.getService().store(uid, stream, path, fileInfo); int res = fileRecordService.addRecord(uid, fileInfo.getName(), fileInfo.getSize(), fileInfo.getMd5(), path); if ( res == 0) { @@ -299,21 +160,9 @@ public class FileService { } } - /** - * 保存上传的文件到网盘系统中 - * @param uid 用户ID 0表示公共 - * @param file 接收到的文件对象 - * @param requestPath 请求的文件路径 - * @param md5 请求时传入的文件md5 - * @return 1 - * @throws IOException 本地文件写入失败时抛出 - * @throws JsonException 文件夹同名时抛出 - */ - public int saveFile(int uid, - MultipartFile file, - String requestPath, - String md5) throws IOException, JsonException { - + @Override + @Transactional(rollbackFor = Exception.class) + public int saveFile(int uid, MultipartFile file, String requestPath, String md5) throws IOException { FileInfo fileInfo = new FileInfo(file); // 获取上传的文件信息 并看情况计算MD5 if (md5 != null) { @@ -322,8 +171,12 @@ public class FileService { fileInfo.updateMd5(); } - mkdirs(uid, requestPath); - storeService.store(uid, file.getInputStream(), requestPath, fileInfo); + try { + nodeService.getLastNodeInfoByPath(uid, requestPath); + } catch (NoSuchFileException e) { + mkdirs(uid, requestPath); + } + storeServiceFactory.getService().store(uid, file.getInputStream(), requestPath, fileInfo); int res = fileRecordService.addRecord(uid, file.getOriginalFilename(), fileInfo.getSize(), fileInfo.getMd5(), requestPath); if ( res == 0) { return fileRecordService.updateFileRecord(uid, file.getOriginalFilename(), requestPath, file.getSize(), fileInfo.getMd5()); @@ -332,79 +185,48 @@ public class FileService { } } - /** - * 创建文件夹 - * @param uid 用户ID 0表示公共 - * @param path 请求的路径 - * @param name 文件夹名称 - * @throws NoSuchFileException 当目标目录不存在时抛出 - */ - public void mkdir(int uid, String path, String name) throws JsonException, NoSuchFileException, FileAlreadyExistsException, DirectoryAlreadyExistsException { - if ( !storeService.mkdir(uid, path, name) ) { - throw new JsonException("在" + path + "创建文件夹失败"); + @Override + @Transactional(rollbackFor = Exception.class) + public void mkdir(int uid, String path, String name) throws IOException { + if ( !storeServiceFactory.getService().mkdir(uid, path, name) ) { + throw new IOException("在" + path + "创建文件夹失败"); } fileRecordService.mkdir(uid, name, path); } - /** - * 删除文件 - * @param uid 用户ID 0表示公共 - * @param path 请求路径 - * @param name 文件名列表 - * @throws NoSuchFileException 当目标路径不存在时抛出 - * @return 删除的数量 - */ + @Override + @Transactional(rollbackFor = Exception.class) public long deleteFile(int uid, String path, List name) throws IOException { // 计数删除数 long res = 0L; List fileInfos = fileRecordService.deleteRecords(uid, path, name); - res += storeService.delete(uid, path, name); + res += storeServiceFactory.getService().delete(uid, path, name); + + // 唯一存储模式下删除文件后若文件不再被引用,则在存储仓库中删除 if (DiskConfig.STORE_TYPE == StoreType.UNIQUE && fileInfos.size() > 0) { - Set all = fileInfos.stream().filter(BasicFileInfo::isFile).map(BasicFileInfo::getMd5).collect(Collectors.toSet()); + Set all = fileInfos + .stream() + .filter(BasicFileInfo::isFile) + .map(BasicFileInfo::getMd5) + .collect(Collectors.toSet()); + if (all.size() == 0) { return res; } Set valid = new HashSet<>(fileDao.getValidFileMD5s(all)); Set invalid = SetUtils.diff(all, valid); for (String md5 : invalid) { - storeService.delete(md5); + storeServiceFactory.getService().delete(md5); } } return res; } - /** - * 重命名文件或目录 - * @param uid 用户ID 0表示公共 - * @param path 文件所在路径(相对用户网盘目录) - * @param name 被操作的文件名或文件夹名 - * @throws NoSuchFileException 当目标路径不存在时抛出 - * @param newName 新文件名 - */ - public void rename(int uid, String path, String name, String newName) throws JsonException, NoSuchFileException { + @Override + @Transactional(rollbackFor = Exception.class) + public void rename(int uid, String path, String name, String newName) throws IOException { fileRecordService.rename(uid, path, name, newName); - storeService.rename(uid, path, name, newName); - } - - /** - * 获取网盘中文件的下载码 - * @param uid 用户ID - * @param path 文件所在网盘目录 - * @param fileInfo 文件信息 - * @param expr 下载码有效时长(单位:天),若小于0,则无限制 - */ - public String getFileDC(int uid, String path, BasicFileInfo fileInfo, int expr) throws JsonProcessingException { - Path localPath = Paths.get(DiskConfig.getPathHandler().getStorePath(uid, path, fileInfo)); - if ( !Files.exists(localPath) ){ - throw new JsonException(404, "文件不存在"); - } - FileDCInfo info = new FileDCInfo(); - info.setDir(path); - info.setMd5(fileInfo.getMd5()); - info.setName(fileInfo.getName()); - info.setUid(uid); - String token = JwtUtils.generateToken(new ObjectMapper().writeValueAsString(info), expr < 0 ? expr : expr*60*60*24); - return token; + storeServiceFactory.getService().rename(uid, path, name, newName); } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/HardLinkStoreService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/HardLinkStoreService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3fd21de4266542fb71d59c7958e993d127dc259 --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/HardLinkStoreService.java @@ -0,0 +1,122 @@ +package com.xiaotao.saltedfishcloud.service.file.localstore; + +import com.xiaotao.saltedfishcloud.config.DiskConfig; +import com.xiaotao.saltedfishcloud.exception.UnableOverwriteException; +import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; +import com.xiaotao.saltedfishcloud.po.file.FileInfo; +import com.xiaotao.saltedfishcloud.utils.FileUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Collection; + +@Slf4j +@Component +public class HardLinkStoreService implements StoreService { + @Override + public void moveToSave(int uid, Path nativePath, String diskPath, BasicFileInfo fileInfo) throws IOException { + Path targetPath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, diskPath, fileInfo)); // 被移动到的目标位置 + // 唯一文件仓库中的路径 + Path sourcePath = Paths.get(DiskConfig.uniquePathHandler.getStorePath(uid, diskPath, fileInfo)); // 文件仓库源文件路径 + if (Files.exists(sourcePath)) { + // 已存在相同文件时,直接删除本地文件 + log.debug("file md5 HIT: {}", fileInfo.getMd5()); + Files.delete(nativePath); + if (Files.exists(targetPath)) { + Files.delete(targetPath); + } + } else { + // 将本地文件移动到唯一仓库 + log.debug("file md5 NOT HIT: {}", fileInfo.getMd5()); + FileUtils.createParentDirectory(sourcePath); + Files.move(nativePath, sourcePath, StandardCopyOption.REPLACE_EXISTING); + } + // 在目标网盘位置创建文件仓库中的文件链接 + log.debug("Create file link: {} <==> {}", targetPath, sourcePath); + Files.createLink(targetPath, sourcePath); + } + + @Override + public void copy(int uid, String source, String target, int targetId, String sourceName, String targetName, Boolean overwrite) throws IOException { + String localSource = DiskConfig.getPathHandler().getStorePath(uid, source, null); + String localTarget = DiskConfig.getPathHandler().getStorePath(targetId, target, null); + FileUtils.copy(Paths.get(localSource),Paths.get(localTarget), sourceName, targetName, true); + } + + @Override + public void store(int uid, InputStream input, String targetDir, FileInfo fileInfo) throws IOException { + // MD5仓库路径 + Path md5Target = Paths.get(DiskConfig.uniquePathHandler.getStorePath(uid, targetDir, fileInfo)); + + + // 先操作总仓库 + if (Files.exists(md5Target)) { + // 重复文件命中 + log.debug("file md5 HIT:" + fileInfo.getMd5()); + if (Files.size(md5Target) != fileInfo.getSize()) { + throw new DuplicateKeyException("文件MD5冲突"); + } + } else { + // 新文件 + log.debug("file md5 NOT HIT, saving:" + fileInfo.getMd5()); + FileUtils.createParentDirectory(md5Target); + Files.copy(input, md5Target, StandardCopyOption.REPLACE_EXISTING); + } + + // 操作用户目录,建立仓库文件与用户文件的链接 + Path rawTarget = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, targetDir, fileInfo)); + if (Files.exists(rawTarget) && Files.isDirectory(rawTarget)) { + throw new UnableOverwriteException(409, "已存在同名目录: " + targetDir + "/" + fileInfo.getName()); + } + FileUtils.createParentDirectory(rawTarget); + log.info("create hard link:" + md5Target + " <==> " + rawTarget); + if (Files.exists(rawTarget)) Files.delete(rawTarget); + Files.createLink(rawTarget, md5Target); + } + + @Override + public void move(int uid, String source, String target, String name, boolean overwrite) throws IOException { + BasicFileInfo fileInfo = new BasicFileInfo(name, null); + Path sourcePath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, source, fileInfo)); + Path targetPath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, target, fileInfo)); + FileUtils.move(sourcePath, targetPath); + } + + @Override + public void rename(int uid, String path, String oldName, String newName) throws IOException { + String base = DiskConfig.rawPathHandler.getStorePath(uid, path, null); + FileUtils.rename(base, oldName, newName); + } + + @Override + public boolean mkdir(int uid, String path, String name) throws IOException { + Path localFilePath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, path, null) + "/" + name); + if (Files.exists(localFilePath)) { + return Files.isDirectory(localFilePath); + } + Files.createDirectories(localFilePath); + return true; + } + + @Override + public int delete(String md5) throws IOException { + return FileUtils.delete(md5); + } + + @Override + public long delete(int uid, String path, Collection files) throws IOException { + String basePath = DiskConfig.rawPathHandler.getStorePath(uid, path, null); + int res = 0; + for (String file : files) { + res += FileUtils.delete(Paths.get(basePath + "/" + file)); + } + return res; + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/RAWStoreService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/RAWStoreService.java new file mode 100644 index 0000000000000000000000000000000000000000..77bddfa9e102c34124882105e243ca6bc3d281e0 --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/RAWStoreService.java @@ -0,0 +1,88 @@ +package com.xiaotao.saltedfishcloud.service.file.localstore; + +import com.xiaotao.saltedfishcloud.config.DiskConfig; +import com.xiaotao.saltedfishcloud.exception.UnableOverwriteException; +import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; +import com.xiaotao.saltedfishcloud.po.file.FileInfo; +import com.xiaotao.saltedfishcloud.service.file.path.PathHandler; +import com.xiaotao.saltedfishcloud.utils.FileUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Collection; + +@Slf4j +@Component +public class RAWStoreService implements StoreService { + @Override + public void moveToSave(int uid, Path nativePath, String diskPath, BasicFileInfo fileInfo) throws IOException { + Path targetPath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, diskPath, fileInfo)); // 被移动到的目标位置 + // 非唯一模式,直接将文件移动到目标位置 + if (!nativePath.equals(targetPath)) { + log.debug("File move {} => {}", nativePath, targetPath); + Files.move(nativePath, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + } + + @Override + public void copy(int uid, String source, String target, int targetId, String sourceName, String targetName, Boolean overwrite) throws IOException { + String localSource = DiskConfig.getPathHandler().getStorePath(uid, source, null); + String localTarget = DiskConfig.getPathHandler().getStorePath(targetId, target, null); + FileUtils.copy(Paths.get(localSource),Paths.get(localTarget), sourceName, targetName, false); + } + + @Override + public void store(int uid, InputStream input, String targetDir, FileInfo fileInfo) throws IOException { + Path rawTarget = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, targetDir, fileInfo)); + if (Files.exists(rawTarget) && Files.isDirectory(rawTarget)) { + throw new UnableOverwriteException(409, "已存在同名目录: " + targetDir + "/" + fileInfo.getName()); + } + FileUtils.createParentDirectory(rawTarget); + log.info("save file:" + rawTarget); + Files.copy(input, rawTarget, StandardCopyOption.REPLACE_EXISTING); + } + + @Override + public void move(int uid, String source, String target, String name, boolean overwrite) throws IOException { + PathHandler pathHandler = DiskConfig.rawPathHandler; + BasicFileInfo fileInfo = new BasicFileInfo(name, null); + Path sourcePath = Paths.get(pathHandler.getStorePath(uid, source, fileInfo)); + Path targetPath = Paths.get(pathHandler.getStorePath(uid, target, fileInfo)); + FileUtils.move(sourcePath, targetPath); + } + + @Override + public void rename(int uid, String path, String oldName, String newName) throws IOException { + String base = DiskConfig.rawPathHandler.getStorePath(uid, path, null); + FileUtils.rename(base, oldName, newName); + } + + @Override + public boolean mkdir(int uid, String path, String name) throws IOException { + Path localFilePath = Paths.get(DiskConfig.rawPathHandler.getStorePath(uid, path, null) + "/" + name); + Files.createDirectory(localFilePath); + return true; + } + + @Override + public int delete(String md5) throws IOException { + return FileUtils.delete(md5); + } + + @Override + public long delete(int uid, String path, Collection files) throws IOException { + String base = DiskConfig.rawPathHandler.getStorePath(uid, path, null); + int cnt = 0; + for (String file : files) { + Path fullPath = Paths.get(base + "/" + file); + cnt += FileUtils.delete(fullPath); + } + return cnt; + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreService.java new file mode 100644 index 0000000000000000000000000000000000000000..3dbdec026d57f786362bd48875e6e21a6c2c1c0d --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreService.java @@ -0,0 +1,110 @@ +package com.xiaotao.saltedfishcloud.service.file.localstore; + +import com.xiaotao.saltedfishcloud.exception.JsonException; +import com.xiaotao.saltedfishcloud.exception.UnableOverwriteException; +import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; +import com.xiaotao.saltedfishcloud.po.file.FileInfo; +import com.xiaotao.saltedfishcloud.service.file.exception.DirectoryAlreadyExistsException; +import org.springframework.dao.DuplicateKeyException; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Path; +import java.util.Collection; + +public interface StoreService { + /** + * 通过文件移动的方式存储文件到网盘系统,相对于{@link #store}方法,避免了文件的重复写入操作。对本地文件操作后,原路径文件不再存在

+ * 如果是UNIQUE存储模式,则会先将文件移动到存储仓库(若仓库已存在文件则忽略该操作),随后再在目标网盘目录创建文件链接

+ * 如果是RAW存储模式,则会直接移动到目标位置。若本地文件路径与网盘路径对应的本地路径相同,操作将忽略。 + * @param uid 用户ID + * @param nativePath 本地文件路径 + * @param diskPath 网盘路径 + * @param fileInfo 文件信息 + */ + default void moveToSave(int uid, Path nativePath, String diskPath, BasicFileInfo fileInfo) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 在本地存储中复制用户网盘文件 + * @param uid 用户ID + * @param source 所在网盘路径 + * @param target 目的地网盘路径 + * @param sourceName 文件名 + * @param overwrite 是否覆盖,若非true,则跳过该文件 + */ + default void copy(int uid, String source, String target, int targetId, String sourceName, String targetName, Boolean overwrite) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 向用户网盘目录中保存一个文件 + * @param uid 用户ID 0表示公共 + * @param input 输入的文件 + * @param targetDir 保存到的目标网盘目录位置(注意:不是本地真是路径) + * @param fileInfo 文件信息 + * @throws JsonException 存储文件出错 + * @throws DuplicateKeyException UNIQUE模式下两个不相同的文件发生MD5碰撞 + * @throws UnableOverwriteException 保存位置存在同名的目录 + */ + default void store(int uid, InputStream input, String targetDir, FileInfo fileInfo) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 在本地存储中移动用户网盘文件 + * @param uid 用户ID + * @param source 所在网盘路径 + * @param target 目的地网盘路径 + * @param name 文件名 + * @param overwrite 是否覆盖原文件 + */ + default void move(int uid, String source, String target, String name, boolean overwrite) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 文件重命名 + * @param uid 用户ID 0表示公共 + * @param path 文件所在路径 + * @param oldName 旧文件名 + * @param newName 新文件名 + */ + default void rename(int uid, String path, String oldName, String newName) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 在本地文件系统中创建文件夹 + * @param uid 用户ID + * @param path 所在路径 + * @param name 文件夹名 + * @throws FileAlreadyExistsException 目标已存在时抛出 + * @return 是否创建成功 + */ + default boolean mkdir(int uid, String path, String name) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 删除一个唯一存储类型的文件 + * @param md5 文件MD5 + * @return 删除的文件和目录数 + */ + default int delete(String md5) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * 删除本地文件(文件夹会连同所有子文件和目录) + * @param uid 用户ID + * @param path 文件所在网盘目录的路径 + * @param files 文件名 + * @return 删除的文件和文件夹总数 + */ + default long delete(int uid, String path, Collection files) throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreServiceFactory.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreServiceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..50adc52de859c23835923a751c07a009e4afbb5d --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreServiceFactory.java @@ -0,0 +1,5 @@ +package com.xiaotao.saltedfishcloud.service.file.localstore; + +public interface StoreServiceFactory { + StoreService getService(); +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreServiceFactoryImpl.java b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreServiceFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e6610abd8cfce1f9403fb1beb9ad92f6dd842d8a --- /dev/null +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/file/localstore/StoreServiceFactoryImpl.java @@ -0,0 +1,27 @@ +package com.xiaotao.saltedfishcloud.service.file.localstore; + +import com.xiaotao.saltedfishcloud.config.DiskConfig; +import com.xiaotao.saltedfishcloud.config.StoreType; +import org.springframework.stereotype.Component; + +@Component +public class StoreServiceFactoryImpl implements StoreServiceFactory { + private final RAWStoreService rawLocalStoreService; + private final HardLinkStoreService hardLinkLocalStoreService; + + public StoreServiceFactoryImpl(RAWStoreService rawLocalStoreService, HardLinkStoreService hardLinkLocalStoreService) { + this.rawLocalStoreService = rawLocalStoreService; + this.hardLinkLocalStoreService = hardLinkLocalStoreService; + } + + @Override + public StoreService getService() { + if (DiskConfig.STORE_TYPE == StoreType.RAW) { + return rawLocalStoreService; + } else if (DiskConfig.STORE_TYPE == StoreType.UNIQUE) { + return hardLinkLocalStoreService; + } else { + throw new UnsupportedOperationException("不支持的存储类型:" + DiskConfig.STORE_TYPE); + } + } +} diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/DiskFtpFile.java b/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/DiskFtpFile.java index 92b7db1fd42043d9bf648e25d1e2082a3f521254..8ab91f398013d14a2061835c879bd56e02159622 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/DiskFtpFile.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/DiskFtpFile.java @@ -4,8 +4,7 @@ import com.xiaotao.saltedfishcloud.config.DiskConfig; import com.xiaotao.saltedfishcloud.enums.ReadOnlyLevel; import com.xiaotao.saltedfishcloud.helper.PathBuilder; import com.xiaotao.saltedfishcloud.po.User; -import com.xiaotao.saltedfishcloud.service.file.FileService; -import com.xiaotao.saltedfishcloud.service.file.exception.DirectoryAlreadyExistsException; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import com.xiaotao.saltedfishcloud.service.ftp.utils.FtpDiskType; import com.xiaotao.saltedfishcloud.service.ftp.utils.FtpPathInfo; import com.xiaotao.saltedfishcloud.utils.SecureUtils; @@ -14,8 +13,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.ftpserver.ftplet.FtpFile; import java.io.*; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.NoSuchFileException; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; @@ -27,7 +24,7 @@ public class DiskFtpFile implements FtpFile { private final FtpPathInfo pathInfo; private final DiskFtpUser user; private File nativeFile; - private final FileService fileService = SpringContextHolder.getContext().getBean(FileService.class); + private final DiskFileSystemFactory fileService = SpringContextHolder.getContext().getBean(DiskFileSystemFactory.class); /** * 构造一个网盘FTP文件 @@ -143,12 +140,12 @@ public class DiskFtpFile implements FtpFile { PathBuilder pb = new PathBuilder(); pb.append(pathInfo.getResourcePath()); try { - fileService.mkdir( + fileService.getFileSystem().mkdir( pathInfo.isPublicArea() ? 0 : user.getId(), new PathBuilder().append(pathInfo.getResourcePath()).range(-1), pathInfo.getName() ); - } catch (NoSuchFileException | FileAlreadyExistsException | DirectoryAlreadyExistsException e) { + } catch (IOException e) { e.printStackTrace(); return false; } @@ -157,9 +154,8 @@ public class DiskFtpFile implements FtpFile { @Override public boolean delete() { - FileService fileService = SpringContextHolder.getContext().getBean(FileService.class); try { - fileService.deleteFile( + fileService.getFileSystem().deleteFile( pathInfo.isPublicArea() ? 0 : user.getId(), (new PathBuilder()).append(pathInfo.getResourcePath()).range(-1), Collections.singletonList(pathInfo.getName()) @@ -188,13 +184,13 @@ public class DiskFtpFile implements FtpFile { try { // 资源路径相同表示重命名 if (pathInfo.getResourceParent().equals(this.pathInfo.getResourceParent()) ) { - fileService.rename(uid, pathInfo.getResourceParent(), this.pathInfo.getName(), destination.getName()); + fileService.getFileSystem().rename(uid, pathInfo.getResourceParent(), this.pathInfo.getName(), destination.getName()); return true; } else { - fileService.move(uid, this.pathInfo.getResourceParent(), pathInfo.getResourceParent(), destination.getName(), true); + fileService.getFileSystem().move(uid, this.pathInfo.getResourceParent(), pathInfo.getResourceParent(), destination.getName(), true); return true; } - } catch (NoSuchFileException e) { + } catch (IOException e) { e.printStackTrace(); return false; } @@ -228,7 +224,7 @@ public class DiskFtpFile implements FtpFile { log.debug("create output stream"); int uid = pathInfo.isPublicArea() ? 0 : user.getId(); if (doesExist()) { - fileService.deleteFile( + fileService.getFileSystem().deleteFile( uid, pathInfo.getResourceParent(), Collections.singletonList(pathInfo.getName()) diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/ftplet/FtpUploadHandler.java b/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/ftplet/FtpUploadHandler.java index d761ed76ffd6d704f714a42d0ec69fa895186132..0ef8e4c1277c23d99108817926746393f0346c49 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/ftplet/FtpUploadHandler.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/ftp/ftplet/FtpUploadHandler.java @@ -2,10 +2,11 @@ package com.xiaotao.saltedfishcloud.service.ftp.ftplet; import com.xiaotao.saltedfishcloud.dao.mybatis.UserDao; import com.xiaotao.saltedfishcloud.po.file.FileInfo; -import com.xiaotao.saltedfishcloud.service.file.FileService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import com.xiaotao.saltedfishcloud.service.ftp.DiskFtpUser; import com.xiaotao.saltedfishcloud.service.ftp.utils.FtpPathInfo; import com.xiaotao.saltedfishcloud.utils.SecureUtils; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.ftpserver.ftplet.*; import org.springframework.stereotype.Component; @@ -17,20 +18,16 @@ import java.nio.file.Paths; @Slf4j @Component +@RequiredArgsConstructor public class FtpUploadHandler extends DefaultFtplet { - private final FileService fileService; + private final DiskFileSystemFactory fileService; private final UserDao userDao; - public FtpUploadHandler(FileService fileService, UserDao userDao) { - this.fileService = fileService; - this.userDao = userDao; - } - /** * 开始文件上传时获取好用户id与路径信息 */ @Override - public FtpletResult onUploadStart(FtpSession session, FtpRequest request) throws FtpException, IOException { + public FtpletResult onUploadStart(FtpSession session, FtpRequest request) throws FtpException { FtpPathInfo pathInfo = new FtpPathInfo(session.getFileSystemView().getWorkingDirectory().getAbsolutePath() + "/" + request.getArgument()); User user = session.getUser(); int uid = 0; @@ -51,7 +48,7 @@ public class FtpUploadHandler extends DefaultFtplet { * 完成上传时更新文件表信息 */ @Override - public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws IOException { log.debug("upload end"); FtpPathInfo pathInfo = (FtpPathInfo) session.getAttribute("pathInfo"); @@ -66,7 +63,7 @@ public class FtpUploadHandler extends DefaultFtplet { FileInfo fileInfo = FileInfo.getLocal(nativePath.toString()); fileInfo.setName(pathInfo.getName()); - fileService.moveToSaveFile(uid, nativePath, pathInfo.getResourceParent(), fileInfo); + fileService.getFileSystem().moveToSaveFile(uid, nativePath, pathInfo.getResourceParent(), fileInfo); return FtpletResult.DEFAULT; } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/http/ResponseService.java b/src/main/java/com/xiaotao/saltedfishcloud/service/http/ResponseService.java index db5caa760d28bb32728b44a8085c212c02bfc6f0..380f2dc084971be53c0656d95a09f70ffe85cb37 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/http/ResponseService.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/http/ResponseService.java @@ -87,7 +87,7 @@ public class ResponseService { public ResponseEntity getResourceByDC(String dc, boolean directDownload) throws MalformedURLException, UnsupportedEncodingException { FileDCInfo info; try { - String data = (String) JwtUtils.parse(dc); + String data = JwtUtils.parse(dc); info = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false).readValue(data, FileDCInfo.class); } catch (JsonProcessingException e) { throw new JsonException(400, "下载码无效"); diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/sync/detector/SyncDiffDetectorImpl.java b/src/main/java/com/xiaotao/saltedfishcloud/service/sync/detector/SyncDiffDetectorImpl.java index 4d1594301f020715f64595226cea14485abc4f95..0c5cf4e8dc9911f59ba776fc5a64767b36bccd2f 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/sync/detector/SyncDiffDetectorImpl.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/sync/detector/SyncDiffDetectorImpl.java @@ -1,11 +1,11 @@ package com.xiaotao.saltedfishcloud.service.sync.detector; import com.xiaotao.saltedfishcloud.config.DiskConfig; -import com.xiaotao.saltedfishcloud.po.NodeInfo; import com.xiaotao.saltedfishcloud.po.User; import com.xiaotao.saltedfishcloud.po.file.DirCollection; import com.xiaotao.saltedfishcloud.po.file.FileInfo; -import com.xiaotao.saltedfishcloud.service.file.FileService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystem; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import com.xiaotao.saltedfishcloud.service.node.NodeService; import com.xiaotao.saltedfishcloud.service.sync.model.FileChangeInfo; import com.xiaotao.saltedfishcloud.service.sync.model.SyncDiffResultDefaultImpl; @@ -28,7 +28,7 @@ public class SyncDiffDetectorImpl implements SyncDiffDetector { @Resource private NodeService nodeService; @Resource - private FileService fileService; + private DiskFileSystemFactory fileService; /** @@ -38,8 +38,9 @@ public class SyncDiffDetectorImpl implements SyncDiffDetector { private Map> fetchDbFiles(int uid) { Map> dbFile = new HashMap<>(); var tree = nodeService.getFullTree(uid); + DiskFileSystem fileSystem = fileService.getFileSystem(); tree.forEach(n -> { - Collection[] fileList = fileService.getUserFileListByNodeId(uid, n.getId()); + Collection[] fileList = fileSystem.getUserFileListByNodeId(uid, n.getId()); String path = tree.getPath(n.getId()); dbFile.put(path, fileList[1]); }); diff --git a/src/main/java/com/xiaotao/saltedfishcloud/service/sync/handler/SyncDiffHandlerImpl.java b/src/main/java/com/xiaotao/saltedfishcloud/service/sync/handler/SyncDiffHandlerImpl.java index 36413508ed00e191ada01d117f677a94c1042757..a46d1bb5e2bc447540b2011a0fc9aebe1538276d 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/service/sync/handler/SyncDiffHandlerImpl.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/service/sync/handler/SyncDiffHandlerImpl.java @@ -6,8 +6,8 @@ import com.xiaotao.saltedfishcloud.dao.mybatis.FileDao; import com.xiaotao.saltedfishcloud.po.User; import com.xiaotao.saltedfishcloud.po.file.FileInfo; import com.xiaotao.saltedfishcloud.service.file.FileRecordService; -import com.xiaotao.saltedfishcloud.service.file.FileService; -import com.xiaotao.saltedfishcloud.service.file.StoreService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; +import com.xiaotao.saltedfishcloud.service.file.localstore.StoreServiceFactory; import com.xiaotao.saltedfishcloud.service.node.NodeService; import com.xiaotao.saltedfishcloud.service.sync.model.FileChangeInfo; import com.xiaotao.saltedfishcloud.utils.PathUtils; @@ -22,11 +22,11 @@ import java.util.*; @Slf4j public class SyncDiffHandlerImpl implements SyncDiffHandler{ @Resource - private FileService fileService; + private DiskFileSystemFactory fileService; @Resource private FileRecordService fileRecordService; @Resource - private StoreService storeService; + private StoreServiceFactory storeServiceFactory; @Resource private FileDao fileDao; @Resource @@ -37,7 +37,7 @@ public class SyncDiffHandlerImpl implements SyncDiffHandler{ int uid = user.getId(); for (FileInfo fileInfo : files) { fileInfo.updateMd5(); - storeService.moveToSave(uid, fileInfo.getOriginFile().toPath(), fileInfo.getPath(), fileInfo); + storeServiceFactory.getService().moveToSave(uid, fileInfo.getOriginFile().toPath(), fileInfo.getPath(), fileInfo); if (fileRecordService.addRecord(uid, fileInfo.getName(), fileInfo.getSize(), fileInfo.getMd5(), fileInfo.getPath()) <= 0) { log.error("信息添加失败:" + fileInfo.getPath() + "/" + fileInfo.getName() + " MD5:" + fileInfo.getMd5()); } @@ -45,7 +45,7 @@ public class SyncDiffHandlerImpl implements SyncDiffHandler{ } @Override - public void handleFileDel(User user, Collection files) throws Exception { + public void handleFileDel(User user, Collection files) { int uid = user.getId(); for (FileInfo file : files) { fileDao.deleteRecord(uid, file.getNode(), file.getName()); @@ -59,7 +59,7 @@ public class SyncDiffHandlerImpl implements SyncDiffHandler{ FileInfo newFile = changeInfo.newFile; newFile.updateMd5(); if (DiskConfig.STORE_TYPE == StoreType.UNIQUE) { - fileService.moveToSaveFile( + fileService.getFileSystem().moveToSaveFile( uid, newFile.getOriginFile().toPath(), newFile.getPath(), @@ -70,7 +70,7 @@ public class SyncDiffHandlerImpl implements SyncDiffHandler{ if (list.size() == 0) { log.debug("File no longer referenced: {}", oldFile.getMd5()); try { - storeService.delete(oldFile.getMd5()); + storeServiceFactory.getService().delete(oldFile.getMd5()); } catch (NoSuchFileException e) { log.warn("Not found md5 file : {}", e.getMessage()); } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/utils/FileUtils.java b/src/main/java/com/xiaotao/saltedfishcloud/utils/FileUtils.java index 5a49f9782819a3a344f86746b8b49301ae439369..c2c8fa092a94d0d515bdaba40573edeb44cc2f8b 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/utils/FileUtils.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/utils/FileUtils.java @@ -1,15 +1,19 @@ package com.xiaotao.saltedfishcloud.utils; +import com.xiaotao.saltedfishcloud.config.DiskConfig; +import com.xiaotao.saltedfishcloud.config.StoreType; import com.xiaotao.saltedfishcloud.helper.PathBuilder; +import com.xiaotao.saltedfishcloud.po.file.BasicFileInfo; import com.xiaotao.saltedfishcloud.po.file.DirCollection; +import com.xiaotao.saltedfishcloud.po.file.FileInfo; import lombok.extern.slf4j.Slf4j; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; -import java.util.Collections; -import java.util.HashMap; +import java.util.*; import java.util.function.Consumer; @Slf4j @@ -64,7 +68,10 @@ public class FileUtils { * @param path 路径 */ static public void createParentDirectory(Path path) throws IOException { - createParentDirectory(path.toString()); + Path parent = path.getParent(); + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } } /** @@ -82,13 +89,32 @@ public class FileUtils { * 删除一个文件或一个目录及其子目录与文件 * @param local 本地存储路径 */ - public static void delete(Path local) throws IOException { - log.info(local.toString()); + @SuppressWarnings("returnignore") + public static int delete(Path local) throws IOException { + log.debug("删除资源:" + local.toString()); DirCollection dirCollection = scanDir(local); Collections.reverse(dirCollection.getDirList()); - dirCollection.getFileList().forEach(File::delete); - dirCollection.getDirList().forEach(File::delete); - Files.delete(local); + int cnt = 0; + for (File file1 : dirCollection.getFileList()) { + log.debug("文件删除:" + file1.getAbsolutePath()); + if(file1.delete()) { + cnt++; + } else { + log.debug("删除失败:" + file1.getPath()); + } + } + for (File file : dirCollection.getDirList()) { + log.debug("目录删除:" + file.getAbsolutePath()); + if(file.delete()) { + cnt++; + } else { + log.debug("删除失败:" + file.getPath()); + } + } + if (Files.isDirectory(local)) { + Files.delete(local); + } + return cnt; } /** @@ -163,14 +189,229 @@ public class FileUtils { for (File file : sourceCollection.getFileList()) { Path p = Paths.get(target + "/" + StringUtils.removePrefix(source, file.getPath())); - if (overwrite) Files.move(Paths.get(file.getPath()), p, StandardCopyOption.REPLACE_EXISTING); - else file.delete(); + + // 若两个文件为同一份文件的链接,则原文件不会被删除。 + // 将目的地同名文件删除以避免该情况 + if (Files.exists(p)) Files.delete(p); + Files.move(Paths.get(file.getPath()), p, StandardCopyOption.REPLACE_EXISTING); log.debug("move " + file.getPath() + " -> " + p); } // 删除源文件夹 Collections.reverse(sourceCollection.getDirList()); - sourceCollection.getDirList().forEach(File::delete); + sourceCollection.getDirList().forEach(e -> { + if (!e.delete()) { + log.debug("删除失败:" + e.getPath()); + } + }); Files.delete(Paths.get(source)); } + + /** + * 复制文件或目录 + * @param source 被复制的目录/文件所在目录 + * @param target 目的地目录 + * @param sourceName 源文件名 + * @param targetName 目标文件名 + * @param useHardLink 文件是否使用硬链接 + */ + public static void copy(Path source, Path target, String sourceName, String targetName, boolean useHardLink) throws IOException { + // 判断源与目标是否存在 + if (!Files.exists(source)) { + throw new NoSuchFileException("资源 \"" + source + "/" + sourceName + "\" 不存在"); + } + if (!Files.exists(target) || !Files.isDirectory(target)) { + throw new NoSuchFileException("目标目录 " + target + " 不存在"); + } + Path sourceFile = Paths.get(source + "/" + sourceName); + Path targetFile = Paths.get(target + "/" + targetName); + if (sourceFile.equals(targetFile)) { + throw new IllegalStateException("不可原地复制"); + } + if (Files.isDirectory(sourceFile)) { + if (sourceName.equals(targetName) && PathUtils.isSubDir(source + "/" + sourceName, target + "/" + targetName)) { + throw new IllegalArgumentException("目标目录不能是源目录的子目录"); + } + + int sourceLen = sourceFile.toString().length(); + DirCollection dirCollection = FileUtils.scanDir(sourceFile); + if (!Files.exists(targetFile)) { + Files.createDirectory(targetFile); + } + // 先创建文件夹 + for(File dir: dirCollection.getDirList()) { + String src = dir.getPath().substring(sourceLen); + Path dest = Paths.get(target + "/" + targetName + "/" + src); + log.debug("local filesystem mkdir: " + dest); + try { Files.createDirectory(dest); } catch (FileAlreadyExistsException ignored) {} + } + + // 复制文件 + for(File file: dirCollection.getFileList()) { + String src = file.getPath().substring(sourceLen); + String dest = target + "/" + targetName + src; + if (useHardLink) { + log.debug("create hard link: " + file + " ==> " + dest); + linkFile(Paths.get(dest), Paths.get(file.getPath())); + } else { + log.debug("local filesystem copy: " + file + " ==> " + dest); + try { Files.copy(Paths.get(file.getPath()), Paths.get(dest), StandardCopyOption.REPLACE_EXISTING); } + catch (FileAlreadyExistsException ignored) {} + } + } + } else if (useHardLink) { + log.debug("create hard link: " + sourceFile + " ==> " + targetFile); + linkFile(targetFile, sourceFile); + } else { + log.debug("copy file: " + sourceFile + " ==> " + targetFile); + Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + + } + + /** + * 安全地创建文件硬链接,若目标链接已存在,则会先对其进行删除后再创建链接 + * @param link 要创建的链接 + * @param existing 被链接的源文件 + * @throws IOException 出错 + */ + public static void linkFile(Path link, Path existing) throws IOException { + if (link.equals(existing)) { + return; + } + if (Files.exists(link)) Files.delete(link); + Files.createLink(link, existing); + } + + /** + * 将本地文件系统指定目录下的文件或目录移动到另一个指定目录下,若对文件夹进行操作,且目标位置是已存在文件夹,将对文件夹进行合并 + * @param source 被移动的资源 + * @param target 移动后的目标资源路径 + * @throws UnsupportedOperationException source和target不是同为文件夹或文件 + */ + public static void move(Path source, Path target) throws IOException { + + if (PathUtils.isSubDir(source.toString(), target.toString())) { + throw new IllegalArgumentException("目标目录不能为源目录的子目录"); + } + if (Files.exists(target)) { + if (Files.isDirectory(source) != Files.isDirectory(target)) { + throw new UnsupportedOperationException("文件类型不一致,无法移动"); + } + + if (Files.isDirectory(source)) { + // 目录则合并 + FileUtils.mergeDir(source.toString(), target.toString(), true); + } else { + Files.delete(target); + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } else { + Files.move(source, target); + } + } + + /** + * 依据文件MD5,从存储仓库中删除对应的文件(仅Unique模式下有效) + * @param md5 文件MD5 + * @return 删除的文件数+目录数 + */ + public static int delete(String md5) throws IOException { + int res = 1; + Path filePath = Paths.get(DiskConfig.getUniqueStoreRoot() + "/" + StringUtils.getUniquePath(md5)); + Files.delete(filePath); + log.debug("删除本地文件:" + filePath); + DirectoryStream paths = Files.newDirectoryStream(filePath.getParent()); + // 最里层目录 + if ( !paths.iterator().hasNext() ) { + log.debug("删除本地目录:" + filePath.getParent()); + res++; + paths.close(); + Files.delete(filePath.getParent()); + paths = Files.newDirectoryStream(filePath.getParent().getParent()); + + // 外层目录 + if ( !paths.iterator().hasNext()) { + log.debug("删除本地目录:" + filePath.getParent().getParent()); + res++; + Files.delete(filePath.getParent().getParent()); + paths.close(); + } + paths.close(); + } else { + paths.close(); + } + return res; + } + + /** + * 对某个目录下的文件进行重命名 + * @param path 文件所在目录 + * @param oldName 原名称 + * @param newName 新名称 + * @throws IOException 文件不存在或冲突 + */ + public static void rename(String path, String oldName, String newName) throws IOException { + File origin = new File(path + "/" + oldName); + File dist = new File(path + "/" + newName); + if (!origin.exists()) { + throw new IOException("原文件不存在"); + } + if (dist.exists()) { + throw new IOException("文件名" + newName + "冲突"); + } + if (!origin.renameTo(dist)) { + throw new IOException("移动失败"); + } + } + + + + /** + * 通过一个本地路径获取获取该路径下的所有文件列表并区分文件与目录 + * 若路径不存在则抛出异常 + * 若路径指向一个文件则返回null + * 若路径指向一个目录则返回一个集合,数组下标0为目录,1为文件 + * @param localPath 本地文件夹路径 + * @return 一个List数组,数组下标0为目录,1为文件,或null + * @throws FileNotFoundException 路径不存在 + */ + public Collection[] getFileList(String localPath) throws FileNotFoundException { + File file = new File(localPath); + return getFileList(file); + } + /** + * 通过一个本地路径获取获取该路径下的所有文件列表并区分文件与目录 + * 若路径不存在则抛出异常 + * 若路径指向一个文件则返回null + * 若路径指向一个目录则返回一个List数组,数组下标0为目录,1为文件 + * @throws FileNotFoundException 路径不存在 + * @param file 本地文件夹路径 + * @return 一个List数组,数组下标0为目录,1为文件,或null + */ + @SuppressWarnings("unchecked") + public Collection[] getFileList(File file) throws FileNotFoundException { + if (!file.exists()) { + throw new FileNotFoundException(); + } + if (file.isFile()) { + return null; + } + List dirs = new LinkedList<>(); + List files = new LinkedList<>(); + try { + for (File listFile : Objects.requireNonNull(file.listFiles())) { + if (listFile.isDirectory()) { + dirs.add(new FileInfo(listFile)); + } else { + files.add(new FileInfo(listFile)); + } + } + } catch (NullPointerException e) { + // do nothing + } + return new List[]{dirs, files}; + } + + } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/utils/JwtUtils.java b/src/main/java/com/xiaotao/saltedfishcloud/utils/JwtUtils.java index 5fa01e3b85f45d4c4a252ae804f4ae0b906da9ed..d4e44a1f3b2a68cdfa734f6623a4205809652923 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/utils/JwtUtils.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/utils/JwtUtils.java @@ -9,10 +9,14 @@ import java.util.HashMap; import java.util.Map; public class JwtUtils { - private final static int EXPIRATION_TIME = 60*60*24; - private final static String SECRET = "1145141919810"; + private final static int EXPIRATION_TIME = 60*60*24*31; + private static String SECRET = "1145141919810"; public static final String AUTHORIZATION = "Token"; + public static void setSecret(String secret) { + JwtUtils.SECRET = secret; + } + /** * 生成一个包含了data作为负载信息的token * @param data 要附加的数据 @@ -31,13 +35,12 @@ public class JwtUtils { Map map = new HashMap<>(); map.put("data", data); - String res = Jwts.builder(). + return Jwts.builder(). setClaims(map) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); - return res; } /** @@ -50,17 +53,17 @@ public class JwtUtils { } /** - * 解析一个token中的负载数据 + * 解析一个token中的负载数据的json * @param token 输入的token - * @return + * @return json字符串 */ - public static Object parse(String token) { + public static String parse(String token) { try { Claims body = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); - return body.get("data"); + return (String)body.get("data"); } catch (ExpiredJwtException e) { throw new JsonException("token已过期"); } catch (Exception e) { diff --git a/src/main/java/com/xiaotao/saltedfishcloud/utils/PathUtils.java b/src/main/java/com/xiaotao/saltedfishcloud/utils/PathUtils.java index 1de2a4bd7b9d5af8aed8d388ff8f22b3446d470b..650956d7ba680a90da91009325642e31a593ea38 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/utils/PathUtils.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/utils/PathUtils.java @@ -67,4 +67,26 @@ public class PathUtils { res[res.length - 1] = path; return res; } + + + /** + * 判断b是否为a的子目录,如
+ * isSubDir("/a/b/c", "/a/b/c/d")为true
+ * isSubDir("/a/b/c/d", "/a/b/c")为false
+ * isSubDir("/a/b/c", "a/b/c/d")为true + * @param a 目录A + * @param b 目录B + * @return 是子目录则为true,否则为false + */ + public static boolean isSubDir(String a, String b) { + if (a.charAt(0) != '/' && a.charAt(0) != '\\') { + a = "/" + a; + } + if (b.charAt(0) != '/' && b.charAt(0) != '\\') { + b = "/" + b; + } + a = a.replaceAll("//+|\\\\+", "/"); + b = b.replaceAll("//+|\\\\+", "/"); + return b.startsWith(a); + } } diff --git a/src/main/java/com/xiaotao/saltedfishcloud/utils/StringUtils.java b/src/main/java/com/xiaotao/saltedfishcloud/utils/StringUtils.java index 8a64b0f14770df69fc1f18ddf963fac919232240..5676803bf7f6352b01a301ca73c78545f90a0997 100644 --- a/src/main/java/com/xiaotao/saltedfishcloud/utils/StringUtils.java +++ b/src/main/java/com/xiaotao/saltedfishcloud/utils/StringUtils.java @@ -2,8 +2,42 @@ package com.xiaotao.saltedfishcloud.utils; import java.net.MalformedURLException; import java.net.URL; +import java.util.Random; public class StringUtils { + private final static String PATTERN = "qwertyuiopasdfghjklzxcvbnm"; + private final static int PATTERN_LEN = PATTERN.length(); + + /** + * 生成一个纯字母随机字符串 + * @param len 生成的字符串长度 + * @param mixUpperCase 是否混入大写字母 + * @return 随机字符串 + */ + public static String getRandomString(int len, boolean mixUpperCase) { + StringBuilder sb = new StringBuilder(len); + Random r = new Random(); + if (mixUpperCase) { + for (int i = 0; i < len; i++) { + sb.append((char)(PATTERN.charAt(r.nextInt(PATTERN_LEN)) - (r.nextInt(2) == 0 ? 32 : 0))); + } + } else { + for (int i = 0; i < len; i++) { + sb.append(PATTERN.charAt(r.nextInt(PATTERN_LEN))); + } + } + return sb.toString(); + } + + /** + * 生成一个纯字母随机字符串 + * @param len 生成的字符串长度 + * @return 随机字符串 + */ + public static String getRandomString(int len) { + return getRandomString(len, true); + } + public static String getURLLastName(String url) throws MalformedURLException { return getURLLastName(new URL(url)); } diff --git a/src/test/java/com/xiaotao/saltedfishcloud/dao/redis/RedisDaoTest.java b/src/test/java/com/xiaotao/saltedfishcloud/dao/redis/RedisDaoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c4ba04e11db6bcc6d98583495be7a437f8867086 --- /dev/null +++ b/src/test/java/com/xiaotao/saltedfishcloud/dao/redis/RedisDaoTest.java @@ -0,0 +1,19 @@ +package com.xiaotao.saltedfishcloud.dao.redis; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class RedisDaoTest { + + @Autowired + private RedisDao redisDao; + + @Test + void scanKeys() { + redisDao.scanKeys("*").forEach(System.out::println); + } +} diff --git a/src/test/java/com/xiaotao/saltedfishcloud/service/file/FileServiceTest.java b/src/test/java/com/xiaotao/saltedfishcloud/service/file/FileServiceTest.java index d9a1e53bc40def5429e7f15c6b55da7616f11eef..29fae1fb1f00c5c62946b41dd5aa6c2d51a63f2c 100644 --- a/src/test/java/com/xiaotao/saltedfishcloud/service/file/FileServiceTest.java +++ b/src/test/java/com/xiaotao/saltedfishcloud/service/file/FileServiceTest.java @@ -4,6 +4,8 @@ import com.xiaotao.saltedfishcloud.config.StoreType; import com.xiaotao.saltedfishcloud.dao.mybatis.UserDao; import com.xiaotao.saltedfishcloud.po.file.FileInfo; import com.xiaotao.saltedfishcloud.service.config.ConfigService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystem; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -15,7 +17,7 @@ import java.nio.file.NoSuchFileException; @SpringBootTest public class FileServiceTest { @Resource - FileService fileService; + DiskFileSystemFactory fileService; @Resource ConfigService configService; @@ -25,6 +27,7 @@ public class FileServiceTest { @Test public void move() { try { + DiskFileSystem fileService = this.fileService.getFileSystem(); int uid = userDao.getUserByUser("xiaotao").getId(); fileService.mkdir(uid, "/", "test"); fileService.mkdir(uid, "/", "test2"); @@ -39,7 +42,7 @@ public class FileServiceTest { public void copy() { int uid = userDao.getUserByUser("xiaotao").getId(); try { - fileService.copy(uid, "/", "/", uid, "f1", "f2", true); + fileService.getFileSystem().copy(uid, "/", "/", uid, "f1", "f2", true); } catch (IOException e) { e.printStackTrace(); } @@ -48,6 +51,7 @@ public class FileServiceTest { @Test public void getLocalFilePathByMD5() throws IOException { configService.setStoreType(StoreType.RAW); + DiskFileSystem fileService = this.fileService.getFileSystem(); FileInfo f1 = fileService.getFileByMD5("b83294df4d6c5643853e3148132f2af5"); configService.setStoreType(StoreType.UNIQUE); FileInfo f2 = fileService.getFileByMD5("b83294df4d6c5643853e3148132f2af5"); @@ -68,7 +72,8 @@ public class FileServiceTest { } @Test - public void mkdirs() throws FileAlreadyExistsException, NoSuchFileException { + public void mkdirs() throws IOException { + DiskFileSystem fileService = this.fileService.getFileSystem(); fileService.mkdirs(1, "/a/b/c/d/e/f/g/h/j/k/l"); } } diff --git a/src/test/java/com/xiaotao/saltedfishcloud/service/file/StoreServiceTest.java b/src/test/java/com/xiaotao/saltedfishcloud/service/file/StoreServiceTest.java index aa0e41742868c1cad66b9174582df4fc3cd3adfd..9d72a482034e6575bff948eff81379291eee859c 100644 --- a/src/test/java/com/xiaotao/saltedfishcloud/service/file/StoreServiceTest.java +++ b/src/test/java/com/xiaotao/saltedfishcloud/service/file/StoreServiceTest.java @@ -1,6 +1,7 @@ package com.xiaotao.saltedfishcloud.service.file; import com.xiaotao.saltedfishcloud.dao.mybatis.UserDao; +import com.xiaotao.saltedfishcloud.service.file.localstore.StoreServiceFactory; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; @@ -15,7 +16,7 @@ import java.io.IOException; @RunWith(SpringRunner.class) class StoreServiceTest { @Resource - private StoreService storeService; + private StoreServiceFactory storeService; @Resource private UserDao userDao; @@ -23,6 +24,6 @@ class StoreServiceTest { @Test void copy() throws IOException { int uid = userDao.getUserByUser("xiaotao").getId(); - storeService.copy(uid, "/f1", "/", uid, "233", "f2", true); + storeService.getService().copy(uid, "/f1", "/", uid, "233", "f2", true); } } diff --git a/src/test/java/com/xiaotao/saltedfishcloud/service/node/NodeTreeTest.java b/src/test/java/com/xiaotao/saltedfishcloud/service/node/NodeTreeTest.java index aa308768282f0266054b481ac1f55fbf372aaea1..268d95620373946945544c5f3f221718921c7709 100644 --- a/src/test/java/com/xiaotao/saltedfishcloud/service/node/NodeTreeTest.java +++ b/src/test/java/com/xiaotao/saltedfishcloud/service/node/NodeTreeTest.java @@ -1,7 +1,8 @@ package com.xiaotao.saltedfishcloud.service.node; import com.xiaotao.saltedfishcloud.po.NodeInfo; -import com.xiaotao.saltedfishcloud.service.file.FileService; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystem; +import com.xiaotao.saltedfishcloud.service.file.filesystem.DiskFileSystemFactory; import lombok.var; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; @@ -20,12 +21,13 @@ class NodeTreeTest { @Resource private NodeService nodeService; @Resource - private FileService fileService; + private DiskFileSystemFactory fileService; @Test public void testGetNode() throws IOException { String targetPath = "/nodetest/folder2/deepfolder"; + DiskFileSystem fileService = this.fileService.getFileSystem(); // 初始化环境 fileService.mkdir(0, "/", "nodetest"); fileService.mkdir(0, "/nodetest", "folder2"); diff --git a/src/test/java/com/xiaotao/saltedfishcloud/utils/FileUtilsTest.java b/src/test/java/com/xiaotao/saltedfishcloud/utils/FileUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ab8792676806cb4104c2aedad071e5f5f64a38fc --- /dev/null +++ b/src/test/java/com/xiaotao/saltedfishcloud/utils/FileUtilsTest.java @@ -0,0 +1,17 @@ +package com.xiaotao.saltedfishcloud.utils; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FileUtilsTest { + @Test + public void dotest() { + assertTrue(PathUtils.isSubDir("/asd/asd/a", "/asd/asd/a")); + assertTrue(PathUtils.isSubDir("/asd/asd/a", "/asd/asd/a/123")); + assertTrue(PathUtils.isSubDir("/asd/asd/a", "asd/asd/a/123")); + assertFalse(PathUtils.isSubDir("/asd/asd/a", "/asd/asd")); + assertFalse(PathUtils.isSubDir("/asd/asd/a", "asd/asd")); + } +} diff --git a/src/test/java/com/xiaotao/saltedfishcloud/utils/StringUtilsTest.java b/src/test/java/com/xiaotao/saltedfishcloud/utils/StringUtilsTest.java index f7b30e96b24383cb7e16a64ac0adfb289e8d4d74..f385bfe63e022b95b4b68c3a8045b08a0a5a95ae 100644 --- a/src/test/java/com/xiaotao/saltedfishcloud/utils/StringUtilsTest.java +++ b/src/test/java/com/xiaotao/saltedfishcloud/utils/StringUtilsTest.java @@ -18,4 +18,10 @@ class StringUtilsTest { e.printStackTrace(); } } + + @Test + void getRandomString() { + System.out.println(StringUtils.getRandomString(32, false)); + System.out.println(StringUtils.getRandomString(32, true)); + } }