diff --git a/src/main/java/org/ycloud/aipan/controller/FileController.java b/src/main/java/org/ycloud/aipan/controller/FileController.java index 9932929..745bd6c 100644 --- a/src/main/java/org/ycloud/aipan/controller/FileController.java +++ b/src/main/java/org/ycloud/aipan/controller/FileController.java @@ -5,14 +5,11 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import org.ycloud.aipan.controller.req.FileBatchReq; -import org.ycloud.aipan.controller.req.FileUpdateReq; -import org.ycloud.aipan.controller.req.FileUploadReq; +import org.ycloud.aipan.controller.req.*; import org.ycloud.aipan.dto.AccountFileDTO; import org.ycloud.aipan.dto.FolderTreeNodeDTO; import org.ycloud.aipan.interceptor.LoginInterceptor; import org.ycloud.aipan.service.AccountFileService; -import org.ycloud.aipan.controller.req.FolderCreateReq; import org.ycloud.aipan.util.JsonData; import java.util.List; @@ -94,4 +91,37 @@ public class FileController { accountFileService.moveBatch(req); return JsonData.buildSuccess(); } + + /** + * 文件批量删除 + */ + @PostMapping("del_batch") + public JsonData delBatch(@RequestBody FileDelReq req) { + req.setAccountId(LoginInterceptor.threadLocal.get().getId()); + accountFileService.delBatch(req); + return JsonData.buildSuccess(); + } + + + /** + * 文件复制接口 + */ + @PostMapping("copy_batch") + @Operation(summary = "文件批量复制", description = "文件批量复制") + public JsonData copyBatch( + @Parameter(description = "文件批量复制请求对象", required = true) @RequestBody FileBatchReq req) { + req.setAccountId(LoginInterceptor.threadLocal.get().getId()); + accountFileService.copyBatch(req); + return JsonData.buildSuccess(); + } + + /** + * 文件秒传接口, true就是文件秒传成功,false失败,需要重新调用上传接口 + */ + @PostMapping("second_upload") + public JsonData secondUpload(@RequestBody FileSecondUploadReq req) { + req.setAccountId(LoginInterceptor.threadLocal.get().getId()); + Boolean flag = accountFileService.secondUpload(req); + return JsonData.buildSuccess(flag); + } } diff --git a/src/main/java/org/ycloud/aipan/controller/req/FileDelReq.java b/src/main/java/org/ycloud/aipan/controller/req/FileDelReq.java new file mode 100644 index 0000000..677fd54 --- /dev/null +++ b/src/main/java/org/ycloud/aipan/controller/req/FileDelReq.java @@ -0,0 +1,13 @@ +package org.ycloud.aipan.controller.req; + +import lombok.Data; +import java.util.List; + + +@Data +public class FileDelReq { + private List fileIds; + + private Long accountId; + +} \ No newline at end of file diff --git a/src/main/java/org/ycloud/aipan/controller/req/FileSecondUploadReq.java b/src/main/java/org/ycloud/aipan/controller/req/FileSecondUploadReq.java new file mode 100644 index 0000000..98857d9 --- /dev/null +++ b/src/main/java/org/ycloud/aipan/controller/req/FileSecondUploadReq.java @@ -0,0 +1,37 @@ +package org.ycloud.aipan.controller.req; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class FileSecondUploadReq { + + /** + * 文件名 + */ + private String filename; + + /** + * 文件唯一标识(md5) + */ + private String identifier; + + /** + * 用户id + */ + private Long accountId; + + /** + * 父级目录id + */ + private Long parentId; + + /** + * 文件大小 + */ + //private Long fileSize; + + + +} \ No newline at end of file diff --git a/src/main/java/org/ycloud/aipan/mapper/AccountFileMapper.java b/src/main/java/org/ycloud/aipan/mapper/AccountFileMapper.java index bf5f7a2..628a358 100644 --- a/src/main/java/org/ycloud/aipan/mapper/AccountFileMapper.java +++ b/src/main/java/org/ycloud/aipan/mapper/AccountFileMapper.java @@ -1,8 +1,11 @@ package org.ycloud.aipan.mapper; +import org.apache.ibatis.annotations.Param; import org.ycloud.aipan.model.AccountFileDO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import java.util.List; + /** *

* 用户文件表 Mapper 接口 @@ -13,4 +16,5 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; */ public interface AccountFileMapper extends BaseMapper { + void insertFileBatch(@Param("newAccountFileDOList") List newAccountFileDOList); } diff --git a/src/main/java/org/ycloud/aipan/service/AccountFileService.java b/src/main/java/org/ycloud/aipan/service/AccountFileService.java index cae9033..7a039e8 100644 --- a/src/main/java/org/ycloud/aipan/service/AccountFileService.java +++ b/src/main/java/org/ycloud/aipan/service/AccountFileService.java @@ -1,10 +1,7 @@ package org.ycloud.aipan.service; -import org.ycloud.aipan.controller.req.FileBatchReq; -import org.ycloud.aipan.controller.req.FileUpdateReq; -import org.ycloud.aipan.controller.req.FileUploadReq; -import org.ycloud.aipan.controller.req.FolderCreateReq; +import org.ycloud.aipan.controller.req.*; import org.ycloud.aipan.dto.AccountFileDTO; import org.ycloud.aipan.dto.FolderTreeNodeDTO; @@ -39,13 +36,40 @@ public interface AccountFileService { /** * 普通小文件上传 - * @param req */ void fileUpload(FileUploadReq req); /** * 批量移动目标文件夹 - * @param req */ void moveBatch(FileBatchReq req); + + /** + * 文件的批量删除 + * 步骤一:检查是否满足:1、文件ID数量是否合法,2、文件是否属于当前用户 + * 步骤二:判断文件是否是文件夹,文件夹的话需要递归获取里面子文件ID,然后进行批量删除 + * 步骤三:需要更新账号存储空间使用情况 + * 步骤四:批量删除账号映射文件,考虑回收站如何设计 + */ + void delBatch(FileDelReq req); + + + /** + * 文件复制 + * * 检查被转移的文件ID是否合法 + * * 检查目标文件夹ID是否合法 + * * 执行拷贝,递归查找【差异点,ID是全新的】 + * * 计算存储空间大小,检查是否足够【差异点,空间需要检查】 + * * 存储相关记录 + */ + void copyBatch(FileBatchReq req); + + + /** + * 文件秒传 + * 1、检查文件是否存在 + * 2、检查空间是否足够 + * 3、建立关系 + */ + Boolean secondUpload(FileSecondUploadReq req); } diff --git a/src/main/java/org/ycloud/aipan/service/impl/AccountFileServiceImpl.java b/src/main/java/org/ycloud/aipan/service/impl/AccountFileServiceImpl.java index 1329c36..2dd6882 100644 --- a/src/main/java/org/ycloud/aipan/service/impl/AccountFileServiceImpl.java +++ b/src/main/java/org/ycloud/aipan/service/impl/AccountFileServiceImpl.java @@ -1,5 +1,6 @@ package org.ycloud.aipan.service.impl; +import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import lombok.extern.slf4j.Slf4j; @@ -9,10 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.ycloud.aipan.component.StoreEngine; import org.ycloud.aipan.config.MinioConfig; -import org.ycloud.aipan.controller.req.FileBatchReq; -import org.ycloud.aipan.controller.req.FileUpdateReq; -import org.ycloud.aipan.controller.req.FileUploadReq; -import org.ycloud.aipan.controller.req.FolderCreateReq; +import org.ycloud.aipan.controller.req.*; import org.ycloud.aipan.dto.AccountFileDTO; import org.ycloud.aipan.dto.FolderTreeNodeDTO; import org.ycloud.aipan.enums.BizCodeEnum; @@ -21,8 +19,10 @@ import org.ycloud.aipan.enums.FolderFlagEnum; import org.ycloud.aipan.exception.BizException; import org.ycloud.aipan.mapper.AccountFileMapper; import org.ycloud.aipan.mapper.FileMapper; +import org.ycloud.aipan.mapper.StorageMapper; import org.ycloud.aipan.model.AccountFileDO; import org.ycloud.aipan.model.FileDO; +import org.ycloud.aipan.model.StorageDO; import org.ycloud.aipan.service.AccountFileService; import org.ycloud.aipan.util.CommonUtil; import org.ycloud.aipan.util.SpringBeanUtil; @@ -45,6 +45,8 @@ public class AccountFileServiceImpl implements AccountFileService { private AccountFileMapper accountFileMapper; @Autowired private FileMapper fileMapper; + @Autowired + private StorageMapper storageMapper; @Override public List listFile(Long accountId, Long parentId) { @@ -261,6 +263,80 @@ public class AccountFileServiceImpl implements AccountFileService { } + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatch(FileDelReq req) { + //步骤一:检查是否满足:1、文件ID数量是否合法,2、文件是否属于当前用户 + List accountFileDOList = checkFileIdLegal(req.getFileIds(), req.getAccountId()); + + //步骤二:判断文件是否是文件夹,文件夹的话需要递归获取里面子文件ID,然后进行批量删除 + List storeAccountFileDOList = new ArrayList<>(); + findAllAccountFileDOWithRecur(storeAccountFileDOList, accountFileDOList, false); + + //拿到全部文件ID列表 + List allFileIdList = storeAccountFileDOList.stream().map(AccountFileDO::getId).collect(Collectors.toList()); + + //步骤三:需要更新账号存储空间使用情况 可以加个分布式锁,redission 作业,提示可以用account_id锁粒度 + long allFileSize = storeAccountFileDOList.stream() + .filter(file -> file.getIsDir().equals(FolderFlagEnum.NO.getCode())) + .mapToLong(AccountFileDO::getFileSize).sum(); + StorageDO storageDO = storageMapper.selectOne(new QueryWrapper().eq("account_id", req.getAccountId())); + storageDO.setUsedSize(storageDO.getUsedSize() - allFileSize); + storageMapper.updateById(storageDO); + + // 步骤四:批量删除账号映射文件,考虑回收站如何设计 + accountFileMapper.deleteBatchIds(allFileIdList); + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void copyBatch(FileBatchReq req) { + //检查被转移的文件ID是否合法 + List accountFileDOList = checkFileIdLegal(req.getFileIds(), req.getAccountId()); + //检查目标文件夹ID是否合法 + checkTargetParentIdLegal(req); + //执行拷贝,递归查找【差异点,ID是全新的】 + List newAccountFileDOList = findBatchCopyFileWithRecur(accountFileDOList, req.getTargetParentId()); + //计算存储空间大小,检查是否足够【差异点,空间需要检查】 + long totalFileSize = newAccountFileDOList.stream().filter(file -> file.getIsDir().equals(FolderFlagEnum.NO.getCode())) + .mapToLong(AccountFileDO::getFileSize).sum(); + if (!checkAndUpdateCapacity(req.getAccountId(), totalFileSize)) { + throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH); + } + //存储 + accountFileMapper.insertFileBatch(newAccountFileDOList); + + + } + + + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean secondUpload(FileSecondUploadReq req) { + //检查文件是否存在 + FileDO fileDO = fileMapper.selectOne(new QueryWrapper().eq("identifier", req.getIdentifier())); + //检查空间是否足够 + if (fileDO != null && checkAndUpdateCapacity(req.getAccountId(), fileDO.getFileSize())) { + //处理文件秒传 + AccountFileDTO accountFileDTO = new AccountFileDTO(); + accountFileDTO.setAccountId(req.getAccountId()); + accountFileDTO.setFileId(fileDO.getId()); + accountFileDTO.setParentId(req.getParentId()); + accountFileDTO.setFileName(req.getFilename()); + accountFileDTO.setFileSize(fileDO.getFileSize()); + accountFileDTO.setDel(false); + accountFileDTO.setIsDir(FolderFlagEnum.NO.getCode()); + + //保存关联文件关系,里面有做相关检查 + saveAccountFile(accountFileDTO); + return true; + } + return false; + } + + /** * 检查目标文件夹ID是否合法,包括子文件夹 * 1、目标的文件ID不能是文件 @@ -414,8 +490,108 @@ public class AccountFileServiceImpl implements AccountFileService { accountFileDO.setFileName(split[0] + "_" + System.currentTimeMillis() + "." + split[1]); } } + } + /** + * 递归查找 + * + * @param allAccountFileDOList 容器存储查询到到全部文件或者文件夹 + * @param prepareAccountFileDOList 待查询的文件和文件夹 + * @param onlyFolder 控制是否只存储文件 + */ + public void findAllAccountFileDOWithRecur(List allAccountFileDOList, List prepareAccountFileDOList, boolean onlyFolder) { + for (AccountFileDO accountFileDO : prepareAccountFileDOList) { + if (Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) { + //递归查找 + List childAccountFileDOList = accountFileMapper.selectList(new QueryWrapper() + .eq("parent_id", accountFileDO.getId())); + findAllAccountFileDOWithRecur(allAccountFileDOList, childAccountFileDOList, onlyFolder); + } + //如果通过onlyFolder是true,只存储文件夹到allAccountFileDOList,否则都存储到allAccountFileDOList + if (!onlyFolder || Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) { + allAccountFileDOList.add(accountFileDO); + } + } + } + + /** + * 包括递归处理,生成新的ID + * + * @param accountFileDOList + * @param targetParentId + * @return + */ + public List findBatchCopyFileWithRecur(List accountFileDOList, Long targetParentId) { + List newAccountFileDOList = new ArrayList<>(); + accountFileDOList.forEach(accountFileDO -> doCopyChildRecord(newAccountFileDOList, accountFileDO, targetParentId)); + return newAccountFileDOList; + } + + /** + * 递归处理,包括子文件夹 + * + * @param newAccountFileDOList + * @param accountFileDO + * @param targetParentId + */ + private void doCopyChildRecord(List newAccountFileDOList, AccountFileDO accountFileDO, Long targetParentId) { + //保存旧的ID,方便查找子文件夹 + Long oldAccountFileId = accountFileDO.getId(); + //创建新记录 + accountFileDO.setId(IdUtil.getSnowflakeNextId()); + accountFileDO.setParentId(targetParentId); + accountFileDO.setGmtModified(null); + accountFileDO.setGmtCreate(null); + + //处理重复文件夹 + processFileNameDuplicate(accountFileDO); + + //纳入容器存储 + newAccountFileDOList.add(accountFileDO); + + //判断是文件还是文件夹,递归处理 + if (Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) { + //继续获取子文件夹列表 + List childAccountFileDOList = findChildAccountFile(accountFileDO.getAccountId(), oldAccountFileId); + if (CollectionUtils.isEmpty(childAccountFileDOList)) { + return; + } + //递归处理 + childAccountFileDOList + .forEach(childAccountFileDO -> doCopyChildRecord(newAccountFileDOList, childAccountFileDO, accountFileDO.getId())); + } } + /** + * 查找文件记录,只查询下一级,不递归 + * + * @param accountId + * @param parentId + * @return + */ + private List findChildAccountFile(Long accountId, Long parentId) { + return accountFileMapper.selectList(new QueryWrapper() + .eq("account_id", accountId).eq("parent_id", parentId)); + } + + + /** + * 检查存储空间和更新存储空间 + * + * @param accountId + * @param fileSize + * @return + */ + public boolean checkAndUpdateCapacity(Long accountId, Long fileSize) { + StorageDO storageDO = storageMapper.selectOne(new QueryWrapper().eq("account_id", accountId)); + Long totalSize = storageDO.getTotalSize(); + if (storageDO.getUsedSize() + fileSize <= totalSize) { + storageDO.setUsedSize(storageDO.getUsedSize() + fileSize); + storageMapper.updateById(storageDO); + return true; + } else { + return false; + } + } } \ No newline at end of file diff --git a/src/main/resources/mapper/AccountFileMapper.xml b/src/main/resources/mapper/AccountFileMapper.xml index 45c0b1d..7008b29 100644 --- a/src/main/resources/mapper/AccountFileMapper.xml +++ b/src/main/resources/mapper/AccountFileMapper.xml @@ -23,5 +23,13 @@ id, account_id, is_dir, parent_id, file_id, file_name, file_type, file_suffix, file_size, del, del_time, gmt_modified, gmt_create + + INSERT INTO account_file (id,account_id, is_dir, parent_id, file_id, file_name, file_type, file_suffix, file_size, del, del_time) + VALUES + + (#{item.id},#{item.accountId}, #{item.isDir}, #{item.parentId}, #{item.fileId}, #{item.fileName}, #{item.fileType}, + #{item.fileSuffix}, #{item.fileSize}, #{item.del}, #{item.delTime}) + +