feat 构建项目发布支持配置发布到二级目录

This commit is contained in:
bwcx_jzy 2022-12-19 13:48:59 +08:00
parent 3d37b26cac
commit 15aaf2b7c5
No known key found for this signature in database
GPG Key ID: 5E48E9372088B9E5
23 changed files with 215 additions and 122 deletions

View File

@ -5,6 +5,8 @@
1. 【all】外置 `logback` 配置文件 1. 【all】外置 `logback` 配置文件
2. 【server】服务端管理相关功能独立页面菜单 2. 【server】服务端管理相关功能独立页面菜单
3. 【server】新增项目触发器用于管理项目状态 3. 【server】新增项目触发器用于管理项目状态
4. 【all】新增 构建项目发布支持配置发布到二级目录
5. 【server】新增 节点分发发布支持配置发布到二级目录
### 🐞 解决BUG、优化功能 ### 🐞 解决BUG、优化功能

View File

@ -26,6 +26,7 @@ import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.BooleanUtil;
@ -130,7 +131,7 @@ public class ProjectFileControl extends BaseAgentController {
List<DiffFileVo.DiffItem> data = diffFileVo.getData(); List<DiffFileVo.DiffItem> data = diffFileVo.getData();
Assert.notEmpty(data, "没有要对比的数据"); Assert.notEmpty(data, "没有要对比的数据");
// 扫描项目目录下面的所有文件 // 扫描项目目录下面的所有文件
String path = projectInfoModel.allLib(); String path = FileUtil.file(projectInfoModel.allLib(), Opt.ofBlankAble(diffFileVo.getDir()).orElse(StrUtil.SLASH)).getAbsolutePath();
List<File> files = FileUtil.loopFiles(path); List<File> files = FileUtil.loopFiles(path);
// 将所有的文件信息组装并签名 // 将所有的文件信息组装并签名
List<JSONObject> collect = files.stream().map(file -> { List<JSONObject> collect = files.stream().map(file -> {
@ -336,6 +337,7 @@ public class ProjectFileControl extends BaseAgentController {
@RequestMapping(value = "batch_delete", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "batch_delete", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public JsonMessage<String> batchDelete(@RequestBody DiffFileVo diffFileVo) { public JsonMessage<String> batchDelete(@RequestBody DiffFileVo diffFileVo) {
String id = diffFileVo.getId(); String id = diffFileVo.getId();
String dir = diffFileVo.getDir();
NodeProjectInfoModel projectInfoModel = super.getProjectInfoModel(id); NodeProjectInfoModel projectInfoModel = super.getProjectInfoModel(id);
// 备份文件 // 备份文件
String backupId = ProjectFileBackupUtil.backup(projectInfoModel.getId(), projectInfoModel.allLib()); String backupId = ProjectFileBackupUtil.backup(projectInfoModel.getId(), projectInfoModel.allLib());
@ -344,13 +346,13 @@ public class ProjectFileControl extends BaseAgentController {
List<DiffFileVo.DiffItem> data = diffFileVo.getData(); List<DiffFileVo.DiffItem> data = diffFileVo.getData();
Assert.notEmpty(data, "没有要对比的数据"); Assert.notEmpty(data, "没有要对比的数据");
// //
String path = projectInfoModel.allLib(); File path = FileUtil.file(projectInfoModel.allLib(), Opt.ofBlankAble(dir).orElse(StrUtil.SLASH));
for (DiffFileVo.DiffItem datum : data) { for (DiffFileVo.DiffItem datum : data) {
File file = FileUtil.file(path, datum.getName()); File file = FileUtil.file(path, datum.getName());
if (FileUtil.del(file)) { if (FileUtil.del(file)) {
continue; continue;
} }
return new JsonMessage<>(500, "删除失败"); return new JsonMessage<>(500, "删除失败" + file.getAbsolutePath());
} }
return JsonMessage.success("删除成功"); return JsonMessage.success("删除成功");
} finally { } finally {

View File

@ -22,57 +22,39 @@
*/ */
package io.jpom.controller.manage.vo; package io.jpom.controller.manage.vo;
import lombok.Data;
import java.util.List; import java.util.List;
/** /**
* @author bwcx_jzy * @author bwcx_jzy
* @since 2021/12/16 * @since 2021/12/16
*/ */
@Data
public class DiffFileVo { public class DiffFileVo {
private String id; /**
private List<DiffItem> data; * 项目id
*/
private String id;
/**
* 需要对比的数据
*/
private List<DiffItem> data;
/**
* 需要对比的目录
*/
private String dir;
public String getId() { @Data
return id; public static class DiffItem {
} /**
* 名称
public void setId(String id) { */
this.id = id; private String name;
} /**
* 文件签名
public List<DiffItem> getData() { */
return data; private String sha1;
} }
public void setData(List<DiffItem> data) {
this.data = data;
}
public static class DiffItem {
/**
* 名称
*/
private String name;
/**
* 文件签名
*/
private String sha1;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSha1() {
return sha1;
}
public void setSha1(String sha1) {
this.sha1 = sha1;
}
}
} }

View File

@ -39,6 +39,7 @@ import io.jpom.system.AgentConfig;
import io.jpom.system.ConfigBean; import io.jpom.system.ConfigBean;
import io.jpom.util.CommandUtil; import io.jpom.util.CommandUtil;
import io.jpom.util.StringUtil; import io.jpom.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.io.File; import java.io.File;
@ -51,6 +52,7 @@ import java.util.stream.Collectors;
* @author bwcx_jzy * @author bwcx_jzy
* @since 2022/5/10 * @since 2022/5/10
*/ */
@Slf4j
public class ProjectFileBackupUtil { public class ProjectFileBackupUtil {
/** /**
@ -152,47 +154,51 @@ public class ProjectFileBackupUtil {
} }
// 考虑到大文件对比比较耗时需要异步对比文件 // 考虑到大文件对比比较耗时需要异步对比文件
ThreadUtil.execute(() -> { ThreadUtil.execute(() -> {
File backupItemPath = ProjectFileBackupUtil.path(pathId, backupId); try {
File backupPath = ProjectFileBackupUtil.path(pathId); File backupItemPath = ProjectFileBackupUtil.path(pathId, backupId);
// 获取文件列表 File backupPath = ProjectFileBackupUtil.path(pathId);
Map<String, File> backupFiles = ProjectFileBackupUtil.listFiles(backupItemPath.getAbsolutePath()); // 获取文件列表
Map<String, File> nowFiles = ProjectFileBackupUtil.listFiles(projectPath); Map<String, File> backupFiles = ProjectFileBackupUtil.listFiles(backupItemPath.getAbsolutePath());
nowFiles.forEach((fileSha1, file) -> { Map<String, File> nowFiles = ProjectFileBackupUtil.listFiles(projectPath);
// 当前目录存在的但是备份目录也存在的相同文件则删除 nowFiles.forEach((fileSha1, file) -> {
File backupFile = backupFiles.get(fileSha1); // 当前目录存在的但是备份目录也存在的相同文件则删除
if (backupFile != null) { File backupFile = backupFiles.get(fileSha1);
CommandUtil.systemFastDel(backupFile); if (backupFile != null) {
backupFiles.remove(fileSha1); CommandUtil.systemFastDel(backupFile);
} backupFiles.remove(fileSha1);
}); }
// 判断保存指定后缀
String[] backupSuffix = Optional.ofNullable(dslYmlDto)
.map(DslYmlDto::getFile)
.map(DslYmlDto.FileConfig::getBackupSuffix)
.orElseGet(() -> {
AgentConfig agentConfig = SpringUtil.getBean(AgentConfig.class);
AgentConfig.ProjectConfig project = agentConfig.getProject();
return project.getFileBackupSuffix();
}); });
if (ArrayUtil.isNotEmpty(backupSuffix)) { // 判断保存指定后缀
backupFiles.values() String[] backupSuffix = Optional.ofNullable(dslYmlDto)
.stream() .map(DslYmlDto::getFile)
.filter(file -> { .map(DslYmlDto.FileConfig::getBackupSuffix)
String name = FileUtil.getName(file); .orElseGet(() -> {
for (String reg : backupSuffix) { AgentConfig agentConfig = SpringUtil.getBean(AgentConfig.class);
if (ReUtil.isMatch(reg, name)) { AgentConfig.ProjectConfig project = agentConfig.getProject();
// 满足正则条件 return project.getFileBackupSuffix();
return false; });
if (ArrayUtil.isNotEmpty(backupSuffix)) {
backupFiles.values()
.stream()
.filter(file -> {
String name = FileUtil.getName(file);
for (String reg : backupSuffix) {
if (ReUtil.isMatch(reg, name)) {
// 满足正则条件
return false;
}
} }
} return !StrUtil.endWithAny(name, backupSuffix);
return !StrUtil.endWithAny(name, backupSuffix); })
}) .forEach(CommandUtil::systemFastDel);
.forEach(CommandUtil::systemFastDel); }
// 删除空文件夹
loopClean(backupItemPath);
// 检查备份保留个数
clearOldBackup(backupPath, dslYmlDto);
} catch (Exception e) {
log.warn("对比清空项目文件备份失败", e);
} }
// 删除空文件夹
loopClean(backupItemPath);
// 检查备份保留个数
clearOldBackup(backupPath, dslYmlDto);
}); });
} }

View File

@ -61,6 +61,14 @@ public class AgentAuthorize implements InitializingBean {
*/ */
private String authorize; private String authorize;
public void setAuthorize(String authorize) {
// 不能外部 set
}
public String getAuthorize() {
throw new UnsupportedOperationException("不能调用此方法");
}
private final ConfigBean configBean; private final ConfigBean configBean;
/** /**
* 注入控制加载顺序必须先加载数据目录才能初始化 * 注入控制加载顺序必须先加载数据目录才能初始化
@ -123,9 +131,13 @@ public class AgentAuthorize implements InitializingBean {
if (StrUtil.isEmpty(this.agentName)) { if (StrUtil.isEmpty(this.agentName)) {
throw new JpomRuntimeException("The agent login name cannot be empty"); throw new JpomRuntimeException("The agent login name cannot be empty");
} }
this.checkPwd(); if (StrUtil.isEmpty(this.authorize)) {
// 生成密码授权字符串 this.checkPwd();
this.authorize = SecureUtil.sha1(this.agentName + "@" + this.agentPwd); // 生成密码授权字符串
this.authorize = SecureUtil.sha1(this.agentName + "@" + this.agentPwd);
} else {
log.warn("authorized 不能重复加载");
}
// //
JvmUtil.checkJpsNormal(); JvmUtil.checkJpsNormal();
} }

View File

@ -13,9 +13,9 @@ jpom:
# 停止、启动项目(项目状态检测)等待的时长 单位秒 # 停止、启动项目(项目状态检测)等待的时长 单位秒
status-wait-time: 10 status-wait-time: 10
# 项目文件备份保留个数,大于 0 才会备份 # 项目文件备份保留个数,大于 0 才会备份
file-backup-count: 0 file-backup-count: 1
# 限制备份指定文件后缀(支持正则) # 限制备份指定文件后缀(支持正则)
#file-backup-suffix: [ '.jar','.html','^.+\\.(?i)(txt)$' ] file-backup-suffix: [ '.jar','.html','^.+\\.(?i)(txt)$' ]
# 项目状态检测间隔时间 单位毫秒最小为1毫秒 # 项目状态检测间隔时间 单位毫秒最小为1毫秒
status-detection-interval: 500 status-detection-interval: 500
log: log:

View File

@ -64,13 +64,15 @@ public class WebAopLog {
Object proceed; Object proceed;
Object logResult = null; Object logResult = null;
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
String requestUri = requestAttributes.getRequest().getRequestURI();
try { try {
aopLogInterface.forEach(aopLogInterface -> aopLogInterface.before(joinPoint)); aopLogInterface.forEach(aopLogInterface -> aopLogInterface.before(joinPoint));
proceed = joinPoint.proceed(); proceed = joinPoint.proceed();
logResult = proceed; logResult = proceed;
log.debug("{} {}", requestAttributes.getRequest().getRequestURI(), Optional.ofNullable(proceed).orElse(StrUtil.EMPTY)); log.debug("{} {}", requestUri, Optional.ofNullable(proceed).orElse(StrUtil.EMPTY));
} catch (Throwable e) { } catch (Throwable e) {
log.debug("{}", requestAttributes.getRequest().getRequestURI(), e); // 不用记录异常日志全局异常拦截里面会记录此处不用重复记录
// log.debug("发生异常 {}", requestUri, e);
logResult = e; logResult = e;
throw e; throw e;
} finally { } finally {

View File

@ -32,6 +32,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import lombok.Lombok;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import java.io.File; import java.io.File;
@ -41,6 +42,7 @@ import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor; import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -260,4 +262,19 @@ public class FileUtils {
}); });
return paths; return paths;
} }
/**
* 判断目录是否有越级问题
*
* @param dir 目录
* @param function 异常
*/
public static void checkSlip(String dir, Function<Exception, Exception> function) {
try {
File userHomeDir = FileUtil.getUserHomeDir();
FileUtil.checkSlip(userHomeDir, FileUtil.file(userHomeDir, dir));
} catch (IllegalArgumentException e) {
throw Lombok.sneakyThrow(function.apply(e));
}
}
} }

View File

@ -151,6 +151,10 @@ public class BuildExtraModule extends BaseModel {
* 镜像标签 * 镜像标签
*/ */
private String dockerImagesLabels; private String dockerImagesLabels;
/**
* 项目二级目录
*/
private String projectSecondaryDirectory;
public String getResultDirFile() { public String getResultDirFile() {
if (resultDirFile == null) { if (resultDirFile == null) {

View File

@ -30,6 +30,7 @@ import cn.hutool.core.date.SystemClock;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.text.CharPool; import cn.hutool.core.text.CharPool;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -494,6 +495,9 @@ public class ReleaseManage implements Runnable {
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("id", projectId); jsonObject.put("id", projectId);
jsonObject.put("data", collect); jsonObject.put("data", collect);
String directory = this.buildExtraModule.getProjectSecondaryDirectory();
directory = Opt.ofBlankAble(directory).orElse(StrUtil.SLASH);
jsonObject.put("dir", directory);
JsonMessage<JSONObject> requestBody = NodeForward.requestBody(nodeModel, NodeUrl.MANAGE_FILE_DIFF_FILE, this.userModel, jsonObject); JsonMessage<JSONObject> requestBody = NodeForward.requestBody(nodeModel, NodeUrl.MANAGE_FILE_DIFF_FILE, this.userModel, jsonObject);
if (requestBody.getCode() != HttpStatus.HTTP_OK) { if (requestBody.getCode() != HttpStatus.HTTP_OK) {
throw new JpomRuntimeException("对比项目文件失败:" + requestBody); throw new JpomRuntimeException("对比项目文件失败:" + requestBody);
@ -523,6 +527,7 @@ public class ReleaseManage implements Runnable {
File file = FileUtil.file(resultFileParent, name); File file = FileUtil.file(resultFileParent, name);
// //
String startPath = StringUtil.delStartPath(file, resultFileParent, false); String startPath = StringUtil.delStartPath(file, resultFileParent, false);
startPath = FileUtil.normalize(startPath + StrUtil.SLASH + directory);
// //
JsonMessage<String> jsonMessage = OutGivingRun.fileUpload(file, startPath, JsonMessage<String> jsonMessage = OutGivingRun.fileUpload(file, startPath,
projectId, false, last ? afterOpt : AfterOpt.No, nodeModel, this.userModel, false); projectId, false, last ? afterOpt : AfterOpt.No, nodeModel, this.userModel, false);
@ -540,7 +545,7 @@ public class ReleaseManage implements Runnable {
* 发布项目 * 发布项目
*/ */
private void doProject() { private void doProject() {
// AfterOpt afterOpt, boolean clearOld, boolean diffSync //AfterOpt afterOpt, boolean clearOld, boolean diffSync
AfterOpt afterOpt = BaseEnum.getEnum(AfterOpt.class, this.buildExtraModule.getAfterOpt(), AfterOpt.No); AfterOpt afterOpt = BaseEnum.getEnum(AfterOpt.class, this.buildExtraModule.getAfterOpt(), AfterOpt.No);
boolean clearOld = this.buildExtraModule.isClearOld(); boolean clearOld = this.buildExtraModule.isClearOld();
boolean diffSync = this.buildExtraModule.isDiffSync(); boolean diffSync = this.buildExtraModule.isDiffSync();
@ -563,7 +568,7 @@ public class ReleaseManage implements Runnable {
zipFile = this.resultFile; zipFile = this.resultFile;
unZip = false; unZip = false;
} }
JsonMessage<String> jsonMessage = OutGivingRun.fileUpload(zipFile, null, JsonMessage<String> jsonMessage = OutGivingRun.fileUpload(zipFile, this.buildExtraModule.getProjectSecondaryDirectory(),
projectId, projectId,
unZip, unZip,
afterOpt, afterOpt,

View File

@ -24,6 +24,7 @@ package io.jpom.controller.build;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.RegexPool; import cn.hutool.core.lang.RegexPool;
import cn.hutool.core.lang.Tuple; import cn.hutool.core.lang.Tuple;
import cn.hutool.core.lang.Validator; import cn.hutool.core.lang.Validator;
@ -60,6 +61,7 @@ import io.jpom.service.node.ssh.SshService;
import io.jpom.service.script.ScriptServer; import io.jpom.service.script.ScriptServer;
import io.jpom.system.extconf.BuildExtConfig; import io.jpom.system.extconf.BuildExtConfig;
import io.jpom.util.CommandUtil; import io.jpom.util.CommandUtil;
import io.jpom.util.FileUtils;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -210,12 +212,7 @@ public class BuildInfoController extends BaseServerController {
if (StrUtil.isNotEmpty(webhook)) { if (StrUtil.isNotEmpty(webhook)) {
Validator.validateMatchRegex(RegexPool.URL_HTTP, webhook, "WebHooks 地址不合法"); Validator.validateMatchRegex(RegexPool.URL_HTTP, webhook, "WebHooks 地址不合法");
} }
try { FileUtils.checkSlip(resultDirFile, e -> new IllegalArgumentException("产物目录不能越级:" + e.getMessage()));
File userHomeDir = FileUtil.getUserHomeDir();
FileUtil.checkSlip(userHomeDir, FileUtil.file(userHomeDir, resultDirFile));
} catch (Exception e) {
return new JsonMessage<>(405, "产物目录不能越级:" + e.getMessage());
}
buildInfoModel.setAutoBuildCron(this.checkCron(autoBuildCron)); buildInfoModel.setAutoBuildCron(this.checkCron(autoBuildCron));
buildInfoModel.setWebhook(webhook); buildInfoModel.setWebhook(webhook);
buildInfoModel.setRepositoryId(repositoryId); buildInfoModel.setRepositoryId(repositoryId);
@ -230,7 +227,7 @@ public class BuildInfoController extends BaseServerController {
BuildReleaseMethod releaseMethod1 = BaseEnum.getEnum(BuildReleaseMethod.class, releaseMethod); BuildReleaseMethod releaseMethod1 = BaseEnum.getEnum(BuildReleaseMethod.class, releaseMethod);
Assert.notNull(releaseMethod1, "发布方法不正确"); Assert.notNull(releaseMethod1, "发布方法不正确");
buildInfoModel.setReleaseMethod(releaseMethod1.getCode()); buildInfoModel.setReleaseMethod(releaseMethod1.getCode());
// extraData 信息转换成 JSON 字符串 // extraData 信息转换成 JSON 字符串 ,不能直接使用 io.jpom.build.BuildExtraModule
JSONObject jsonObject = JSON.parseObject(extraData); JSONObject jsonObject = JSON.parseObject(extraData);
// 验证发布方式 extraData 信息 // 验证发布方式 extraData 信息
@ -389,6 +386,11 @@ public class BuildInfoController extends BaseServerController {
String clearOld = jsonObject.getString("clearOld"); String clearOld = jsonObject.getString("clearOld");
jsonObject.put("afterOpt", afterOpt1.getCode()); jsonObject.put("afterOpt", afterOpt1.getCode());
jsonObject.put("clearOld", Convert.toBool(clearOld, false)); jsonObject.put("clearOld", Convert.toBool(clearOld, false));
//
String projectSecondaryDirectory = jsonObject.getString("projectSecondaryDirectory");
Opt.ofBlankAble(projectSecondaryDirectory).ifPresent(s -> {
FileUtils.checkSlip(s, e -> new IllegalArgumentException("二级目录不能越级:" + e.getMessage()));
});
} }
/** /**

View File

@ -93,7 +93,8 @@ public class BuildInfoManageController extends BaseServerController {
String resultDirFile, String resultDirFile,
String branchName, String branchName,
String branchTagName, String branchTagName,
String checkRepositoryDiff) { String checkRepositoryDiff,
String projectSecondaryDirectory) {
BuildInfoModel item = buildInfoService.getByKey(id, getRequest()); BuildInfoModel item = buildInfoService.getByKey(id, getRequest());
Assert.notNull(item, "没有对应数据"); Assert.notNull(item, "没有对应数据");
// 更新数据 // 更新数据
@ -101,8 +102,16 @@ public class BuildInfoManageController extends BaseServerController {
Opt.ofBlankAble(resultDirFile).ifPresent(update::setResultDirFile); Opt.ofBlankAble(resultDirFile).ifPresent(update::setResultDirFile);
Opt.ofBlankAble(branchName).ifPresent(update::setBranchName); Opt.ofBlankAble(branchName).ifPresent(update::setBranchName);
Opt.ofBlankAble(branchTagName).ifPresent(update::setBranchTagName); Opt.ofBlankAble(branchTagName).ifPresent(update::setBranchTagName);
Opt.ofBlankAble(projectSecondaryDirectory).ifPresent(s -> {
FileUtils.checkSlip(s, e -> new IllegalArgumentException("二级目录不能越级:" + e.getMessage()));
//
String extraData = item.getExtraData();
JSONObject jsonObject = JSONObject.parseObject(extraData);
jsonObject.put("projectSecondaryDirectory", s);
update.setExtraData(jsonObject.toString());
});
if (!StrUtil.isAllBlank(resultDirFile, branchName, branchTagName)) { if (!StrUtil.isAllBlank(resultDirFile, branchName, branchTagName, projectSecondaryDirectory)) {
update.setId(id); update.setId(id);
buildInfoService.update(update); buildInfoService.update(update);
} }

View File

@ -24,6 +24,7 @@ package io.jpom.controller.outgiving;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.Validator; import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Entity; import cn.hutool.db.Entity;
@ -50,6 +51,7 @@ import io.jpom.service.dblog.BuildInfoService;
import io.jpom.service.node.ProjectInfoCacheService; import io.jpom.service.node.ProjectInfoCacheService;
import io.jpom.service.outgiving.DbOutGivingLogService; import io.jpom.service.outgiving.DbOutGivingLogService;
import io.jpom.service.outgiving.OutGivingServer; import io.jpom.service.outgiving.OutGivingServer;
import io.jpom.util.FileUtils;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -207,7 +209,12 @@ public class OutGivingController extends BaseServerController {
outGivingModel.setIntervalTime(intervalTime); outGivingModel.setIntervalTime(intervalTime);
// //
outGivingModel.setClearOld(Convert.toBool(getParameter("clearOld"), false)); outGivingModel.setClearOld(Convert.toBool(getParameter("clearOld"), false));
//
String secondaryDirectory = getParameter("secondaryDirectory");
Opt.ofBlankAble(secondaryDirectory).ifPresent(s -> {
FileUtils.checkSlip(s, e -> new IllegalArgumentException("二级目录不能越级:" + e.getMessage()));
outGivingModel.setSecondaryDirectory(secondaryDirectory);
});
} }
/** /**

View File

@ -181,7 +181,7 @@ public class OutGivingProjectController extends BaseServerController {
*/ */
@RequestMapping(value = "upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.UPLOAD) @Feature(method = MethodFeature.UPLOAD)
public JsonMessage<Object> upload(String id, String afterOpt, String clearOld, String autoUnzip) throws IOException { public JsonMessage<Object> upload(String id, String afterOpt, String clearOld, String autoUnzip, String secondaryDirectory) throws IOException {
OutGivingModel outGivingModel = this.check(id); OutGivingModel outGivingModel = this.check(id);
AfterOpt afterOpt1 = BaseEnum.getEnum(AfterOpt.class, Convert.toInt(afterOpt, 0)); AfterOpt afterOpt1 = BaseEnum.getEnum(AfterOpt.class, Convert.toInt(afterOpt, 0));
Assert.notNull(afterOpt1, "请选择分发后的操作"); Assert.notNull(afterOpt1, "请选择分发后的操作");
@ -201,6 +201,7 @@ public class OutGivingProjectController extends BaseServerController {
//outGivingModel = outGivingServer.getItem(id); //outGivingModel = outGivingServer.getItem(id);
outGivingModel.setClearOld(Convert.toBool(clearOld, false)); outGivingModel.setClearOld(Convert.toBool(clearOld, false));
outGivingModel.setAfterOpt(afterOpt1.getCode()); outGivingModel.setAfterOpt(afterOpt1.getCode());
outGivingModel.setSecondaryDirectory(secondaryDirectory);
outGivingServer.update(outGivingModel); outGivingServer.update(outGivingModel);
// 开启 // 开启
@ -228,7 +229,7 @@ public class OutGivingProjectController extends BaseServerController {
*/ */
@RequestMapping(value = "remote_download", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "remote_download", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.REMOTE_DOWNLOAD) @Feature(method = MethodFeature.REMOTE_DOWNLOAD)
public JsonMessage<String> remoteDownload(String id, String afterOpt, String clearOld, String url, String autoUnzip) { public JsonMessage<String> remoteDownload(String id, String afterOpt, String clearOld, String url, String autoUnzip, String secondaryDirectory) {
OutGivingModel outGivingModel = this.check(id); OutGivingModel outGivingModel = this.check(id);
AfterOpt afterOpt1 = BaseEnum.getEnum(AfterOpt.class, Convert.toInt(afterOpt, 0)); AfterOpt afterOpt1 = BaseEnum.getEnum(AfterOpt.class, Convert.toInt(afterOpt, 0));
Assert.notNull(afterOpt1, "请选择分发后的操作"); Assert.notNull(afterOpt1, "请选择分发后的操作");
@ -242,6 +243,7 @@ public class OutGivingProjectController extends BaseServerController {
//outGivingModel = outGivingServer.getItem(id); //outGivingModel = outGivingServer.getItem(id);
outGivingModel.setClearOld(Convert.toBool(clearOld, false)); outGivingModel.setClearOld(Convert.toBool(clearOld, false));
outGivingModel.setAfterOpt(afterOpt1.getCode()); outGivingModel.setAfterOpt(afterOpt1.getCode());
outGivingModel.setSecondaryDirectory(secondaryDirectory);
outGivingServer.update(outGivingModel); outGivingServer.update(outGivingModel);
//下载 //下载
File file = FileUtil.file(serverConfig.getUserTempPath(), ServerConst.OUTGIVING_FILE, id); File file = FileUtil.file(serverConfig.getUserTempPath(), ServerConst.OUTGIVING_FILE, id);

View File

@ -24,6 +24,7 @@ package io.jpom.controller.outgiving;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.Tuple; import cn.hutool.core.lang.Tuple;
import cn.hutool.core.lang.Validator; import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -51,6 +52,7 @@ import io.jpom.permission.MethodFeature;
import io.jpom.service.dblog.BuildInfoService; import io.jpom.service.dblog.BuildInfoService;
import io.jpom.service.node.ProjectInfoCacheService; import io.jpom.service.node.ProjectInfoCacheService;
import io.jpom.service.outgiving.OutGivingServer; import io.jpom.service.outgiving.OutGivingServer;
import io.jpom.util.FileUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -404,7 +406,12 @@ public class OutGivingProjectEditController extends BaseServerController {
deleteProject(outGivingModel, outGivingNodeProjects, userModel); deleteProject(outGivingModel, outGivingNodeProjects, userModel);
outGivingModel.outGivingNodeProjectList(outGivingNodeProjects); outGivingModel.outGivingNodeProjectList(outGivingNodeProjects);
//
String secondaryDirectory = getParameter("secondaryDirectory");
Opt.ofBlankAble(secondaryDirectory).ifPresent(s -> {
FileUtils.checkSlip(s, e -> new IllegalArgumentException("二级目录不能越级:" + e.getMessage()));
outGivingModel.setSecondaryDirectory(secondaryDirectory);
});
return tuples; return tuples;
} }

View File

@ -75,6 +75,11 @@ public class OutGivingModel extends BaseWorkspaceModel {
*/ */
private Integer status; private Integer status;
/**
* 二级目录
*/
private String secondaryDirectory;
public boolean clearOld() { public boolean clearOld() {
return clearOld != null && clearOld; return clearOld != null && clearOld;

View File

@ -69,7 +69,8 @@ public class OutGivingItemRun implements Callable<OutGivingNodeProject.Status> {
private final UserModel userModel; private final UserModel userModel;
private final boolean unzip; private final boolean unzip;
private final boolean clearOld; private final boolean clearOld;
private Integer sleepTime; private final Integer sleepTime;
private final String secondaryDirectory;
/** /**
* 数据库记录id * 数据库记录id
*/ */
@ -82,8 +83,9 @@ public class OutGivingItemRun implements Callable<OutGivingNodeProject.Status> {
boolean unzip, boolean unzip,
Integer sleepTime) { Integer sleepTime) {
this.outGivingId = item.getId(); this.outGivingId = item.getId();
this.unzip = unzip; this.secondaryDirectory = item.getSecondaryDirectory();
this.clearOld = item.clearOld(); this.clearOld = item.clearOld();
this.unzip = unzip;
this.outGivingNodeProject = outGivingNodeProject; this.outGivingNodeProject = outGivingNodeProject;
this.file = file; this.file = file;
this.afterOpt = ObjectUtil.defaultIfNull(EnumUtil.likeValueOf(AfterOpt.class, item.getAfterOpt()), AfterOpt.No); this.afterOpt = ObjectUtil.defaultIfNull(EnumUtil.likeValueOf(AfterOpt.class, item.getAfterOpt()), AfterOpt.No);
@ -105,7 +107,7 @@ public class OutGivingItemRun implements Callable<OutGivingNodeProject.Status> {
this.updateStatus(this.outGivingId, this.outGivingNodeProject, this.updateStatus(this.outGivingId, this.outGivingNodeProject,
OutGivingNodeProject.Status.Ing, "开始分发"); OutGivingNodeProject.Status.Ing, "开始分发");
// //
JsonMessage<String> jsonMessage = OutGivingRun.fileUpload(file, null, JsonMessage<String> jsonMessage = OutGivingRun.fileUpload(file, this.secondaryDirectory,
this.outGivingNodeProject.getProjectId(), this.outGivingNodeProject.getProjectId(),
unzip, unzip,
afterOpt, afterOpt,

View File

@ -61,7 +61,7 @@ public class DbExtConfig {
/** /**
* 缓存大小 * 缓存大小
* <p> * <p>
* http://www.h2database.com/html/features.html#cache_settings * <a href="http://www.h2database.com/html/features.html#cache_settings">http://www.h2database.com/html/features.html#cache_settings</a>
*/ */
private DataSize cacheSize = DataSize.ofMegabytes(10); private DataSize cacheSize = DataSize.ofMegabytes(10);

View File

@ -73,7 +73,7 @@ public class OperateLogController implements AopLogInterface {
private final DbUserOperateLogService dbUserOperateLogService; private final DbUserOperateLogService dbUserOperateLogService;
private String[] logFilterPar = new String[]{"pwd", "pass", "password"}; private final String[] logFilterPar = new String[]{"pwd", "pass", "password"};
public OperateLogController(DbUserOperateLogService dbUserOperateLogService) { public OperateLogController(DbUserOperateLogService dbUserOperateLogService) {

View File

@ -25,3 +25,6 @@ ALTER TABLE NODE_INFO DROP COLUMN IF EXISTS `cycle`;
ALTER TABLE PROJECT_INFO ALTER TABLE PROJECT_INFO
ADD IF NOT EXISTS triggerToken VARCHAR (100) comment '触发器token'; ADD IF NOT EXISTS triggerToken VARCHAR (100) comment '触发器token';
ALTER TABLE OUT_GIVING
ADD IF NOT EXISTS secondaryDirectory VARCHAR (200) comment '二级目录';

View File

@ -361,14 +361,19 @@
</a-select> </a-select>
</a-form-model-item> </a-form-model-item>
<!-- 项目 --> <!-- 项目 -->
<a-form-model-item v-if="temp.releaseMethod === 2" label="发布项目" prop="releaseMethodDataIdList"> <template v-if="temp.releaseMethod === 2">
<a-cascader v-model="temp.releaseMethodDataIdList" :options="cascaderList" placeholder="请选择节点项目" /> <a-form-model-item label="发布项目" prop="releaseMethodDataIdList">
</a-form-model-item> <a-cascader v-model="temp.releaseMethodDataIdList" :options="cascaderList" placeholder="请选择节点项目" />
<a-form-model-item v-if="temp.releaseMethod === 2" label="发布后操作" prop="afterOpt"> </a-form-model-item>
<a-select show-search allowClear v-model="tempExtraData.afterOpt" placeholder="请选择发布后操作"> <a-form-model-item label="发布后操作" prop="afterOpt">
<a-select-option v-for="opt in afterOptListSimple" :key="opt.value">{{ opt.title }}</a-select-option> <a-select show-search allowClear v-model="tempExtraData.afterOpt" placeholder="请选择发布后操作">
</a-select> <a-select-option v-for="opt in afterOptListSimple" :key="opt.value">{{ opt.title }}</a-select-option>
</a-form-model-item> </a-select>
</a-form-model-item>
<a-form-model-item prop="projectSecondaryDirectory" label="二级目录">
<a-input v-model="tempExtraData.projectSecondaryDirectory" placeholder="不填写则发布至项目的根目录" />
</a-form-model-item>
</template>
<!-- SSH --> <!-- SSH -->
<template v-if="temp.releaseMethod === 3"> <template v-if="temp.releaseMethod === 3">
<a-form-model-item prop="releaseMethodDataId"> <a-form-model-item prop="releaseMethodDataId">
@ -872,6 +877,10 @@
</a-space> </a-space>
</a-form-model-item> </a-form-model-item>
<a-form-model-item v-if="temp.releaseMethod === 2" prop="projectSecondaryDirectory" label="二级目录">
<a-input v-model="temp.projectSecondaryDirectory" placeholder="不填写则发布至项目的根目录" />
</a-form-model-item>
<a-form-model-item label="构建备注" prop="buildRemark"> <a-form-model-item label="构建备注" prop="buildRemark">
<a-textarea v-model="temp.buildRemark" :maxLength="240" placeholder="请输入构建备注,长度小于 240" :auto-size="{ minRows: 3, maxRows: 5 }" /> <a-textarea v-model="temp.buildRemark" :maxLength="240" placeholder="请输入构建备注,长度小于 240" :auto-size="{ minRows: 3, maxRows: 5 }" />
</a-form-model-item> </a-form-model-item>
@ -1555,8 +1564,10 @@ export default {
this.buildConfirmVisible = true; this.buildConfirmVisible = true;
this.branchList = []; this.branchList = [];
this.branchTagList = []; this.branchTagList = [];
//
try { try {
this.temp = { ...this.temp, checkRepositoryDiff: (JSON.parse(record.extraData) || {}).checkRepositoryDiff }; const extraData = JSON.parse(record.extraData) || {};
this.temp = { ...this.temp, checkRepositoryDiff: extraData.checkRepositoryDiff, projectSecondaryDirectory: extraData.projectSecondaryDirectory };
} catch (e) { } catch (e) {
// //
} }
@ -1570,6 +1581,7 @@ export default {
branchTagName: this.temp.branchTagName, branchTagName: this.temp.branchTagName,
branchName: this.temp.branchName, branchName: this.temp.branchName,
checkRepositoryDiff: this.temp.checkRepositoryDiff, checkRepositoryDiff: this.temp.checkRepositoryDiff,
projectSecondaryDirectory: this.temp.projectSecondaryDirectory,
}, },
true true
).then(() => { ).then(() => {

View File

@ -235,6 +235,9 @@
</template> </template>
<a-input-number :min="0" v-model="temp.intervalTime" placeholder="分发间隔时间 (顺序重启、完整顺序重启)方式才生效" style="width: 100%" /> <a-input-number :min="0" v-model="temp.intervalTime" placeholder="分发间隔时间 (顺序重启、完整顺序重启)方式才生效" style="width: 100%" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item prop="secondaryDirectory" label="二级目录">
<a-input v-model="temp.secondaryDirectory" placeholder="不填写则发布至项目的根目录" />
</a-form-model-item>
<a-form-model-item prop="clearOld"> <a-form-model-item prop="clearOld">
<template slot="label"> <template slot="label">
清空发布 清空发布
@ -392,6 +395,9 @@
</template> </template>
<a-input-number :min="0" v-model="temp.intervalTime" placeholder="分发间隔时间 (顺序重启、完整顺序重启)方式才生效" style="width: 100%" /> <a-input-number :min="0" v-model="temp.intervalTime" placeholder="分发间隔时间 (顺序重启、完整顺序重启)方式才生效" style="width: 100%" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item prop="secondaryDirectory" label="二级目录">
<a-input v-model="temp.secondaryDirectory" placeholder="不填写则发布至项目的根目录" />
</a-form-model-item>
<a-form-model-item prop="clearOld"> <a-form-model-item prop="clearOld">
<template slot="label"> <template slot="label">
清空发布 清空发布
@ -540,6 +546,9 @@
<a-select-option v-for="item in afterOptList" :key="item.value">{{ item.title }}</a-select-option> <a-select-option v-for="item in afterOptList" :key="item.value">{{ item.title }}</a-select-option>
</a-select> </a-select>
</a-form-model-item> </a-form-model-item>
<a-form-model-item prop="secondaryDirectory" label="二级目录">
<a-input v-model="temp.secondaryDirectory" placeholder="不填写则发布至项目的根目录" />
</a-form-model-item>
</a-form-model> </a-form-model>
</a-modal> </a-modal>
<!-- 项目文件组件 --> <!-- 项目文件组件 -->
@ -800,6 +809,7 @@ export default {
id: record.id, id: record.id,
intervalTime: record.intervalTime, intervalTime: record.intervalTime,
clearOld: record.clearOld, clearOld: record.clearOld,
secondaryDirectory: record.secondaryDirectory,
}; };
// console.log(this.temp); // console.log(this.temp);
this.linkDispatchVisible = true; this.linkDispatchVisible = true;
@ -927,6 +937,7 @@ export default {
nodeIdList: [], nodeIdList: [],
intervalTime: record.intervalTime, intervalTime: record.intervalTime,
clearOld: record.clearOld, clearOld: record.clearOld,
secondaryDirectory: record.secondaryDirectory,
}; };
} }
// nodeIdList // nodeIdList
@ -1094,6 +1105,7 @@ export default {
formData.append("afterOpt", this.temp.afterOpt); formData.append("afterOpt", this.temp.afterOpt);
formData.append("clearOld", this.temp.clearOld); formData.append("clearOld", this.temp.clearOld);
formData.append("autoUnzip", this.temp.autoUnzip); formData.append("autoUnzip", this.temp.autoUnzip);
formData.append("secondaryDirectory", this.temp.secondaryDirectory);
uploadDispatchFile(formData).then((res) => { uploadDispatchFile(formData).then((res) => {
if (res.code === 200) { if (res.code === 200) {
this.$notification.success({ this.$notification.success({

View File

@ -120,7 +120,7 @@
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="SSH节点" required> <a-form-model-item label="SSH节点" required>
<a-select show-search option-filter-prop="children" mode="multiple" v-model="chooseSsh"> <a-select show-search option-filter-prop="children" mode="multiple" v-model="chooseSsh" placeholder="请选择 SSH节点">
<a-select-option v-for="item in sshList" :key="item.id" :value="item.id"> <a-select-option v-for="item in sshList" :key="item.id" :value="item.id">
{{ item.name }} {{ item.name }}
</a-select-option> </a-select-option>