REQ-3540: 备份

This commit is contained in:
yanglin 2025-03-11 10:44:45 +08:00
parent 208fe9ab20
commit cb69eee281
16 changed files with 247 additions and 54 deletions

View File

@ -3,7 +3,6 @@ package cn.axzo.nanopart.doc.api.domain;
import com.alibaba.fastjson.JSON;
import cn.axzo.nanopart.doc.api.enums.FileFormat;
import lombok.Getter;
import lombok.Setter;
@ -24,12 +23,6 @@ public class IndexNodeAttributes {
return new IndexNodeAttributes();
}
public static IndexNodeAttributes fileAttributesForFileFormat(FileFormat fileFormat) {
IndexNodeAttributes attributes = IndexNodeAttributes.create();
attributes.getOrCreateFileAttributes().setFileFormat(fileFormat);
return attributes;
}
public DatabaseAttributes getOrCreateDatabaseAttributes() {
if (databaseAttributes == null)
databaseAttributes = new DatabaseAttributes();

View File

@ -3,7 +3,6 @@ package cn.axzo.nanopart.doc.api.filetemplate.request;
import javax.validation.constraints.NotNull;
import cn.axzo.nanopart.doc.api.domain.IndexNodeAttributes;
import cn.axzo.nanopart.doc.api.enums.FileFormat;
import cn.axzo.nanopart.doc.api.util.DefaultIcons;
import lombok.Getter;
@ -20,16 +19,11 @@ public class FileTemplateCreateFileRequest extends NodeCreateFileTemplate {
* 文件格式. EXCEL, WORD, PDF
*/
@NotNull(message = "文件格式不能为空")
private FileFormat fileFormat;
private FileFormat format;
@Override
public String icon() {
return DefaultIcons.defaultFileIcon(fileFormat);
}
@Override
public IndexNodeAttributes attributes() {
return IndexNodeAttributes.fileAttributesForFileFormat(fileFormat);
return DefaultIcons.defaultFileIcon(format);
}
}

View File

@ -4,7 +4,6 @@ package cn.axzo.nanopart.doc.api.filetemplate.request;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import cn.axzo.nanopart.doc.api.domain.IndexNodeAttributes;
import cn.axzo.nanopart.doc.api.enums.FileFormat;
import cn.axzo.nanopart.doc.api.util.DefaultIcons;
import lombok.Getter;
@ -27,16 +26,11 @@ public class FileTemplateUploadFileRequest extends NodeCreateFileTemplate {
* 文件格式. EXCEL, WORD, PDF
*/
@NotNull(message = "文件格式不能为空")
private FileFormat fileFormat;
private FileFormat format;
@Override
public String icon() {
return DefaultIcons.defaultFileIcon(fileFormat);
}
@Override
public IndexNodeAttributes attributes() {
return IndexNodeAttributes.fileAttributesForFileFormat(fileFormat);
return DefaultIcons.defaultFileIcon(format);
}
}

View File

@ -0,0 +1,49 @@
package cn.axzo.nanopart.doc.api.util;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import static java.util.stream.Collectors.joining;
/**
* @author yanglin
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class IdBuilder {
private final List<Object> buf = new ArrayList<>();
private final boolean appendAbsentValue;
public static IdBuilder idbuilder() {
return new IdBuilder(false);
}
public static IdBuilder idbuilder(boolean appendAbsentValue) {
return new IdBuilder(appendAbsentValue);
}
public IdBuilder append(Object value) {
if (isAbsentValue(value) && !appendAbsentValue)
return this;
buf.add(value);
return this;
}
private static boolean isAbsentValue(Object value) {
if (value == null)
return true;
if (value instanceof String)
return StringUtils.isBlank((String) value);
return false;
}
public String build() {
return buf.stream().map(String::valueOf).collect(joining(":"));
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.nanopart.doc.dao;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.doc.api.util.BizAssertions;
import cn.axzo.nanopart.doc.entity.DocLock;
import cn.axzo.nanopart.doc.mapper.DocLockMapper;
/**
* @author yanglin
*/
@Repository
public class DocLockDao extends ServiceImpl<DocLockMapper, DocLock> {
public void lockAndReleaseOnCommit(String key) {
BizAssertions.assertTrue(TransactionSynchronizationManager.isActualTransactionActive(), "必须和事务搭配使用");
DocLock lock = lambdaQuery() //
.eq(DocLock::getKey, key) //
.last("for update") //
.one();
if (lock == null) {
lock = new DocLock();
lock.setKey(key);
save(lock);
}
}
}

View File

@ -18,6 +18,19 @@ import cn.axzo.nanopart.doc.mapper.IndexNodeMapper;
@Repository
public class IndexNodeDao extends ServiceImpl<IndexNodeMapper, IndexNode> {
public IndexNode getNodeForUpdateOrThrow(String code) {
IndexNode node = findNodeForUpdateOrNull(code);
BizAssertions.assertNotNull(node, "节点不存在: {}", code);
return node;
}
public IndexNode findNodeForUpdateOrNull(String code) {
return lambdaQuery() //
.eq(IndexNode::getCode, code) //
.last("FOR UPDATE") //
.one();
}
public IndexNode getNodeOrThrow(String code) {
IndexNode node = findNodeOrNull(code);
BizAssertions.assertNotNull(node, "节点不存在: {}", code);
@ -64,4 +77,4 @@ public class IndexNodeDao extends ServiceImpl<IndexNodeMapper, IndexNode> {
.update();
}
}
}

View File

@ -0,0 +1,22 @@
package cn.axzo.nanopart.doc.entity;
import cn.axzo.pudge.core.persistence.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "doc_lock", autoResultMap = true)
public class DocLock extends BaseEntity<DocLock> {
/**
* 锁键
*/
private String key;
}

View File

@ -11,6 +11,7 @@ import cn.axzo.nanopart.doc.api.enums.FileScope;
import cn.axzo.nanopart.doc.api.enums.IndexNodeContext;
import cn.axzo.nanopart.doc.api.enums.IndexNodeState;
import cn.axzo.nanopart.doc.api.enums.IndexNodeType;
import cn.axzo.nanopart.doc.api.util.UUIDUtil;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import lombok.Getter;
import lombok.Setter;
@ -25,6 +26,10 @@ public class IndexNode extends BaseEntity<IndexNode> implements NodeCreate {
public static final String ROOT_CODE = "";
public static String newCode() {
return UUIDUtil.uuidString();
}
/**
* 父节点code
*/

View File

@ -2,6 +2,7 @@
package cn.axzo.nanopart.doc.file.filetemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import cn.axzo.nanopart.doc.api.enums.FileTemplateState;
import cn.axzo.nanopart.doc.api.filetemplate.request.FileTemplateCreateDirRequest;
@ -12,7 +13,7 @@ import cn.axzo.nanopart.doc.dao.FileTemplateDao;
import cn.axzo.nanopart.doc.entity.FileTemplate;
import cn.axzo.nanopart.doc.entity.IndexNode;
import cn.axzo.nanopart.doc.file.index.IndexManager;
import cn.axzo.nanopart.doc.utils.BizTransactional;
import cn.axzo.nanopart.doc.file.index.OssFile;
import lombok.RequiredArgsConstructor;
/**
@ -24,19 +25,20 @@ public class FileTemplateManager {
private final IndexManager indexManager;
private final FileTemplateDao fileTemplateDao;
private final TransactionTemplate transactionTemplate;
public String createDir(FileTemplateCreateDirRequest request) {
return indexManager.createDir(request).getCode();
}
@BizTransactional
public String createFile(FileTemplateCreateFileRequest request) {
return createFileTemplate(indexManager.createFile(request, request.getFileFormat()));
OssFile ossFile = indexManager.uploadEmptyFile(request, request.getFormat());
return transactionTemplate.execute(unused -> createFileTemplate(indexManager.createFile(request, ossFile)));
}
@BizTransactional
public String uploadFile(FileTemplateUploadFileRequest request) {
return createFileTemplate(indexManager.uploadFile(request, request.getFileFormat(), request.getFileBase64()));
OssFile ossFile = indexManager.uploadFile(request, request.getFormat(), request.getFileBase64());
return transactionTemplate.execute(unused -> createFileTemplate(indexManager.createFile(request, ossFile)));
}
private String createFileTemplate(IndexNode fileNode) {
@ -63,4 +65,4 @@ public class FileTemplateManager {
.set(FileTemplate::getState, FileTemplateState.PUBLISHED) //
.update();
}
}
}

View File

@ -4,6 +4,7 @@ package cn.axzo.nanopart.doc.file.index;
import java.util.List;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import cn.axzo.nanopart.doc.api.domain.NodeCreate;
import cn.axzo.nanopart.doc.api.enums.FileFormat;
@ -41,30 +42,43 @@ public class IndexManager {
return indexSupport.createNode(node, IndexNodeType.DIRECTORY, IndexNodeState.VALID);
}
@BizTransactional
public IndexNode createFile(NodeCreate node, FileFormat format) {
/**
* 不能在事务中使用
*/
public OssFile uploadEmptyFile(NodeCreate node, FileFormat format) {
BizAssertions.assertTrue(format.creatable(), "无法创建: {}", format.readableName());
BizAssertions.assertFalse(TransactionSynchronizationManager.isActualTransactionActive(), "不能在事务中使用");
indexSupport.ensureChildNameNotUsed(node);
IndexNode fileNode = createFileNode(node, format);
String emptyOssFileKey = format == FileFormat.WORD //
? docProps.getEmptyWordOssFileKey() //
: docProps.getEmptyExcelFileKey();
return updateFileKey(fileNode, ossClient.copy(emptyOssFileKey, fileNode.getCode(), format));
String code = IndexNode.newCode();
return new OssFile(format, code, ossClient.copy(emptyOssFileKey, code, format));
}
/**
* 不能在事务中使用
*/
public OssFile uploadFile(NodeCreate node, FileFormat format, String fileBase64) {
BizAssertions.assertFalse(TransactionSynchronizationManager.isActualTransactionActive(), "不能在事务中使用");
indexSupport.ensureChildNameNotUsed(node);
String code = IndexNode.newCode();
return new OssFile(format, code, ossClient.upload(fileBase64, code, format));
}
@BizTransactional
public IndexNode uploadFile(NodeCreate node, FileFormat format, String fileBase64) {
indexSupport.ensureChildNameNotUsed(node);
IndexNode fileNode = createFileNode(node, format);
return updateFileKey(fileNode, ossClient.upload(fileBase64, fileNode.getCode(), format));
}
private IndexNode createFileNode(NodeCreate node, FileFormat format) {
BizAssertions.assertTrue(format.creatable(), "无法创建: {}", format.readableName());
return indexSupport.createNode(node, IndexNodeType.FILE, IndexNodeState.VALID);
}
private IndexNode updateFileKey(IndexNode fileNode, String fileKey) {
fileNode.getOrCreateAttributes().getOrCreateFileAttributes().setOssFileKey(fileKey);
public IndexNode createFile(NodeCreate node, OssFile ossFile) {
indexSupport.lockParentAndReleaseOnCommit(node);
try {
indexSupport.ensureChildNameNotUsed(node);
}
catch (NameUsedException e) {
ossClient.delete(ossFile.ossFileKey());
throw e;
}
IndexNode fileNode = indexSupport.createNode(ossFile.code(), node, IndexNodeType.FILE, IndexNodeState.VALID);
fileNode.getOrCreateAttributes().getOrCreateFileAttributes().setOssFileKey(ossFile.ossFileKey());
fileNode.getOrCreateAttributes().getOrCreateFileAttributes().setFileFormat(ossFile.format());
indexNodeDao.updateAttributes(fileNode);
return indexNodeDao.findNodeOrNull(fileNode.getCode());
}
@ -72,15 +86,12 @@ public class IndexManager {
@BizTransactional
public void rename(String code, String newName) {
IndexNode indexNode = indexNodeDao.getNodeOrThrow(code);
indexSupport.lockParentAndReleaseOnCommit(indexNode);
indexSupport.ensureChildNameNotUsed(indexNode, indexNode.getParentCode(), newName);
indexNodeDao.rename(code, newName);
}
public IndexNode copy(String copyCode, String destParentCode) {
IndexNode copyNode = indexNodeDao.getNodeOrThrow(copyCode);
BizAssertions.assertEquals(IndexNodeType.FILE, copyNode.getNodeType(), "只能复制文件");
IndexNode parentNode = indexNodeDao.getNodeOrThrow(destParentCode);
new NodeCreateCopy(copyNode, destParentCode, null);
return null;
}

View File

@ -1,8 +1,11 @@
package cn.axzo.nanopart.doc.file.index;
import static cn.axzo.nanopart.doc.api.util.IdBuilder.idbuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import cn.axzo.nanopart.doc.api.domain.IndexNodeQueryContext;
import cn.axzo.nanopart.doc.api.domain.NodeCreate;
@ -10,6 +13,7 @@ import cn.axzo.nanopart.doc.api.enums.IndexNodeState;
import cn.axzo.nanopart.doc.api.enums.IndexNodeType;
import cn.axzo.nanopart.doc.api.util.BizAssertions;
import cn.axzo.nanopart.doc.api.util.UUIDUtil;
import cn.axzo.nanopart.doc.dao.DocLockDao;
import cn.axzo.nanopart.doc.dao.IndexNodeDao;
import cn.axzo.nanopart.doc.entity.IndexNode;
import lombok.RequiredArgsConstructor;
@ -22,6 +26,7 @@ import lombok.RequiredArgsConstructor;
class IndexSupport {
private final IndexNodeDao indexNodeDao;
private final DocLockDao docLockDao;
IndexNode createNode(NodeCreate node, IndexNodeType nodeType, IndexNodeState state) {
return createNode(UUIDUtil.uuidString(), node, nodeType, state);
@ -64,7 +69,28 @@ class IndexSupport {
void ensureChildNameNotUsed(IndexNodeQueryContext nodeContext, String parentCode, String childName) {
IndexNode child = indexNodeDao.findChildByName(nodeContext, parentCode, childName);
BizAssertions.assertFalse(child != null, "名称已被使用");
if (child != null)
throw new NameUsedException("名称已被使用");
}
void lockParentAndReleaseOnCommit(NodeCreate node) {
lockParentAndReleaseOnCommit(node, node.parentCode());
}
void lockParentAndReleaseOnCommit(IndexNodeQueryContext nodeContext, String parentCode) {
BizAssertions.assertTrue(TransactionSynchronizationManager.isActualTransactionActive(), "必须和事务搭配使用");
// 如果父节点存在就锁父节点
if (StringUtils.isNotBlank(parentCode)) {
indexNodeDao.getNodeForUpdateOrThrow(parentCode);
return;
}
// 如果父节点不存在就锁scope
docLockDao.lockAndReleaseOnCommit(idbuilder() //
.append("INDEX_NODE") //
.append(nodeContext.context()) //
.append(nodeContext.scope()) //
.append(nodeContext.scopeId()) //
.build());
}
}

View File

@ -0,0 +1,14 @@
package cn.axzo.nanopart.doc.file.index;
import cn.axzo.basics.common.exception.ServiceException;
/**
* @author yanglin
*/
public class NameUsedException extends ServiceException {
public NameUsedException(String msg) {
super(msg);
}
}

View File

@ -63,4 +63,4 @@ public class NodeCreateCopy implements NodeCreate {
return copyNode.getScopeId();
}
}
}

View File

@ -0,0 +1,20 @@
package cn.axzo.nanopart.doc.file.index;
import cn.axzo.nanopart.doc.api.enums.FileFormat;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author yanglin
*/
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
public class OssFile {
private final FileFormat format;
private final String code;
private final String ossFileKey;
}

View File

@ -13,7 +13,7 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class OssClient {
public String copy(String srcFileKey, String destFileName, FileFormat format) {
public String copy(String srcOssFileKey, String destFileName, FileFormat format) {
// TODO(yl): finish this
throw new UnsupportedOperationException();
}
@ -23,4 +23,7 @@ public class OssClient {
throw new UnsupportedOperationException();
}
public void delete(String ossFileKey) {
}
}

View File

@ -0,0 +1,15 @@
package cn.axzo.nanopart.doc.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.axzo.nanopart.doc.entity.DocLock;
/**
* @author yanglin
*/
@Mapper
public interface DocLockMapper extends BaseMapper<DocLock> {
}