mxd 4 éve
szülő
commit
6243073dfa

+ 10 - 2
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java

@@ -24,6 +24,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -471,7 +472,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 												 GroupServiceProvider groupServiceProvider,
 												 MappingHandlerMapping mappingHandlerMapping,
 												 FunctionServiceProvider functionServiceProvider,
-												 MagicFunctionManager magicFunctionManager) {
+												 MagicFunctionManager magicFunctionManager) throws NoSuchMethodException {
 		logger.info("magic-api工作目录:{}", magicResource);
 		setupSpringSecurity();
 		AsyncCall.setThreadPoolExecutorSize(properties.getThreadPoolExecutorSize());
@@ -506,17 +507,24 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		String base = properties.getWeb();
 		mappingHandlerMapping.setRequestMappingHandlerMapping(requestMappingHandlerMapping);
 		MagicDataSourceController dataSourceController = new MagicDataSourceController(configuration);
+		MagicWorkbenchController magicWorkbenchController = new MagicWorkbenchController(configuration, properties.getSecretKey());
 		if (base != null) {
 			configuration.setEnableWeb(true);
 			List<MagicController> controllers = new ArrayList<>(Arrays.asList(
 					new MagicAPIController(configuration),
 					dataSourceController,
-					new MagicWorkbenchController(configuration),
+					magicWorkbenchController,
 					new MagicGroupController(configuration),
 					new MagicFunctionController(configuration)
 			));
 			controllers.forEach(item -> mappingHandlerMapping.registerController(item, base));
 		}
+		// 注册接收推送的接口
+		if(StringUtils.isNotBlank(properties.getSecretKey())){
+			RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(properties.getPushPath()).build();
+			Method method = MagicWorkbenchController.class.getDeclaredMethod("receivePush", MultipartFile.class, String.class, Long.class, String.class);
+			Mapping.create(requestMappingHandlerMapping).register(requestMappingInfo,magicWorkbenchController, method);
+		}
 		magicAPIService.registerAllDataSource();
 		// 设置拦截器信息
 		this.requestInterceptors.forEach(interceptor -> {

+ 30 - 0
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIProperties.java

@@ -83,6 +83,20 @@ public class MagicAPIProperties {
 	 */
 	private String response;
 
+	/**
+	 * 远程推送时的秘钥,未配置则不开启
+	 *
+	 * @since 1.2.1
+	 */
+	private String secretKey;
+
+	/**
+	 * 远程推送的路径,默认为/_magic-api-sync
+	 *
+	 * @since 1.2.1
+	 */
+	private String pushPath = "/_magic-api-sync";
+
 
 	@NestedConfigurationProperty
 	private SecurityConfig securityConfig = new SecurityConfig();
@@ -295,4 +309,20 @@ public class MagicAPIProperties {
 	public void setClusterConfig(ClusterConfig clusterConfig) {
 		this.clusterConfig = clusterConfig;
 	}
+
+	public String getSecretKey() {
+		return secretKey;
+	}
+
+	public void setSecretKey(String secretKey) {
+		this.secretKey = secretKey;
+	}
+
+	public String getPushPath() {
+		return pushPath;
+	}
+
+	public void setPushPath(String pushPath) {
+		this.pushPath = pushPath;
+	}
 }

+ 32 - 4
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -24,6 +24,8 @@ import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
 import org.ssssssss.magicapi.modules.SQLModule;
 import org.ssssssss.magicapi.provider.MagicAPIService;
+import org.ssssssss.magicapi.utils.IoUtils;
+import org.ssssssss.magicapi.utils.SignUtils;
 import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScriptEngine;
 import org.ssssssss.script.ScriptClass;
@@ -31,6 +33,7 @@ import org.ssssssss.script.parsing.Span;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
@@ -44,8 +47,14 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	private static final Logger logger = LoggerFactory.getLogger(MagicWorkbenchController.class);
 
-	public MagicWorkbenchController(MagicConfiguration configuration) {
+	private final MagicAPIService magicApiService;
+
+	private final String secretKey;
+
+	public MagicWorkbenchController(MagicConfiguration configuration, String secretKey) {
 		super(configuration);
+		magicApiService = configuration.getMagicAPIService();
+		this.secretKey = secretKey;
 		// 给前端添加代码提示
 		MagicScriptEngine.addScriptClass(SQLModule.class);
 		MagicScriptEngine.addScriptClass(MagicAPIService.class);
@@ -103,7 +112,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	@RequestMapping("/user")
 	@ResponseBody
-	public JsonBean<MagicUser> user(HttpServletRequest request){
+	public JsonBean<MagicUser> user(HttpServletRequest request) {
 		if (configuration.getAuthorizationInterceptor().requireLogin()) {
 			try {
 				return new JsonBean<>(configuration.getAuthorizationInterceptor().getUserByToken(request.getHeader(Constants.MAGIC_TOKEN_HEADER)));
@@ -116,7 +125,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	@RequestMapping("/logout")
 	@ResponseBody
-	public JsonBean<Void> logout(HttpServletRequest request){
+	public JsonBean<Void> logout(HttpServletRequest request) {
 		configuration.getAuthorizationInterceptor().logout(request.getHeader(Constants.MAGIC_TOKEN_HEADER));
 		return new JsonBean<>();
 	}
@@ -210,13 +219,32 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	@ResponseBody
 	public JsonBean<Boolean> upload(MultipartFile file, String mode) throws IOException {
 		notNull(file, FILE_IS_REQUIRED);
-		configuration.getMagicAPIService().upload(file.getInputStream(), mode);
+		magicApiService.upload(file.getInputStream(), mode);
 		return new JsonBean<>(SUCCESS, true);
 	}
 
+	@RequestMapping("/push")
+	@ResponseBody
+	public JsonBean<?> push(String target, String secretKey, String mode) {
+		return magicApiService.push(target, secretKey, mode);
+	}
+
+	@ResponseBody
+	public JsonBean<Void> receivePush(MultipartFile file, String mode, Long timestamp, String sign) throws IOException {
+		notNull(timestamp, SIGN_IS_INVALID);
+		notBlank(mode, SIGN_IS_INVALID);
+		notBlank(sign, SIGN_IS_INVALID);
+		notNull(file, SIGN_IS_INVALID);
+		byte[] bytes = IoUtils.bytes(file.getInputStream());
+		isTrue(sign.equals(SignUtils.sign(timestamp, secretKey, mode, bytes)), SIGN_IS_INVALID);
+		magicApiService.upload(new ByteArrayInputStream(bytes), mode);
+		return new JsonBean<>();
+	}
+
 	private ResponseEntity<?> download(Resource resource, String filename) throws IOException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
 		resource.export(os, Constants.PATH_BACKUPS);
 		return ResponseModule.download(os.toByteArray(), filename);
 	}
+
 }

+ 6 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/JsonCodeConstants.java

@@ -13,6 +13,10 @@ public interface JsonCodeConstants {
 
 	JsonCode GROUP_NOT_FOUND = new JsonCode(0, "找不到分组信息");
 
+	JsonCode TARGET_IS_REQUIRED = new JsonCode(0, "目标网址不能为空");
+
+	JsonCode SECRET_KEY_IS_REQUIRED = new JsonCode(0, "secretKey不能为空");
+
 	JsonCode NAME_CONFLICT = new JsonCode(0, "移动后名称会重复,请修改名称后在试。");
 
 	JsonCode REQUEST_PATH_CONFLICT = new JsonCode(0, "该路径已被映射,请换一个请求方法或路径");
@@ -59,6 +63,8 @@ public interface JsonCodeConstants {
 
 	JsonCode FILE_IS_REQUIRED = new JsonCode(0, "请上传文件");
 
+	JsonCode SIGN_IS_INVALID = new JsonCode(0, "签名验证失败");
+
 	JsonCode UPLOAD_PATH_CONFLICT = new JsonCode(0, "上传后%s路径会有冲突,请检查");
 
 	JsonCode DEBUG_SESSION_NOT_FOUND = new JsonCode(0, "debug session not found!");

+ 16 - 11
magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicAPIService.java

@@ -1,10 +1,7 @@
 package org.ssssssss.magicapi.provider;
 
 import org.ssssssss.magicapi.config.MagicModule;
-import org.ssssssss.magicapi.model.ApiInfo;
-import org.ssssssss.magicapi.model.FunctionInfo;
-import org.ssssssss.magicapi.model.Group;
-import org.ssssssss.magicapi.model.MagicNotify;
+import org.ssssssss.magicapi.model.*;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -43,7 +40,8 @@ public interface MagicAPIService extends MagicModule {
 
 	/**
 	 * 获取接口详情
-	 * @param id	接口id
+	 *
+	 * @param id 接口id
 	 */
 	ApiInfo getApiInfo(String id);
 
@@ -76,7 +74,8 @@ public interface MagicAPIService extends MagicModule {
 
 	/**
 	 * 获取函数详情
-	 * @param id	函数id
+	 *
+	 * @param id 函数id
 	 */
 	FunctionInfo getFunctionInfo(String id);
 
@@ -137,7 +136,8 @@ public interface MagicAPIService extends MagicModule {
 
 	/**
 	 * 获取数据源详情
-	 * @param id	数据源id
+	 *
+	 * @param id 数据源id
 	 */
 	Map<String, String> getDataSource(String id);
 
@@ -148,21 +148,24 @@ public interface MagicAPIService extends MagicModule {
 
 	/**
 	 * 测试数据源
-	 * @param properties	数据源属性
-	 * @return	返回错误说明,连接正常返回 null
+	 *
+	 * @param properties 数据源属性
+	 * @return 返回错误说明,连接正常返回 null
 	 */
 	String testDataSource(Map<String, String> properties);
 
 	/**
 	 * 保存数据源
-	 * @param properties	数据源属性
+	 *
+	 * @param properties 数据源属性
 	 * @return 返回数据源ID
 	 */
 	String saveDataSource(Map<String, String> properties);
 
 	/**
 	 * 删除数据源
-	 * @param id	数据源id
+	 *
+	 * @param id 数据源id
 	 */
 	boolean deleteDataSource(String id);
 
@@ -172,6 +175,8 @@ public interface MagicAPIService extends MagicModule {
 	 */
 	void upload(InputStream inputStream, String mode) throws IOException;
 
+	JsonBean<?> push(String target, String secretKey, String mode);
+
 	/**
 	 * 处理刷新通知
 	 */

+ 52 - 23
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -13,8 +13,15 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyN
 import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
 import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
 import org.springframework.boot.jdbc.DatabaseDriver;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
 import org.springframework.jdbc.datasource.DataSourceUtils;
 import org.springframework.util.ClassUtils;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
 import org.ssssssss.magicapi.adapter.Resource;
 import org.ssssssss.magicapi.adapter.resource.ZipResource;
 import org.ssssssss.magicapi.config.MagicDynamicDataSource;
@@ -29,6 +36,7 @@ import org.ssssssss.magicapi.script.ScriptManager;
 import org.ssssssss.magicapi.utils.IoUtils;
 import org.ssssssss.magicapi.utils.JsonUtils;
 import org.ssssssss.magicapi.utils.PathUtils;
+import org.ssssssss.magicapi.utils.SignUtils;
 import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScript;
 import org.ssssssss.script.MagicScriptContext;
@@ -40,6 +48,8 @@ import org.ssssssss.script.parsing.ast.Expression;
 import javax.script.ScriptContext;
 import javax.script.SimpleScriptContext;
 import javax.sql.DataSource;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.sql.Connection;
@@ -49,40 +59,26 @@ import java.util.stream.Stream;
 
 public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstants {
 
+	private final static Logger logger = LoggerFactory.getLogger(DefaultMagicAPIService.class);
+	private static final ClassLoader classLoader = MagicDataSourceController.class.getClassLoader();
+	// copy from DataSourceBuilder
+	private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]{
+			"com.zaxxer.hikari.HikariDataSource",
+			"org.apache.tomcat.jdbc.pool.DataSource",
+			"org.apache.commons.dbcp2.BasicDataSource"};
 	private final MappingHandlerMapping mappingHandlerMapping;
-
 	private final boolean throwException;
-
 	private final ResultProvider resultProvider;
-
 	private final ApiServiceProvider apiServiceProvider;
-
 	private final FunctionServiceProvider functionServiceProvider;
-
 	private final GroupServiceProvider groupServiceProvider;
-
 	private final MagicDynamicDataSource magicDynamicDataSource;
-
 	private final MagicFunctionManager magicFunctionManager;
-
 	private final MagicNotifyService magicNotifyService;
-
 	private final String instanceId;
-
 	private final Resource workspace;
-
 	private final Resource datasourceResource;
 
-	private final static Logger logger = LoggerFactory.getLogger(DefaultMagicAPIService.class);
-
-	private static final ClassLoader classLoader = MagicDataSourceController.class.getClassLoader();
-
-	// copy from DataSourceBuilder
-	private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]{
-			"com.zaxxer.hikari.HikariDataSource",
-			"org.apache.tomcat.jdbc.pool.DataSource",
-			"org.apache.commons.dbcp2.BasicDataSource"};
-
 	public DefaultMagicAPIService(MappingHandlerMapping mappingHandlerMapping,
 								  ApiServiceProvider apiServiceProvider,
 								  FunctionServiceProvider functionServiceProvider,
@@ -226,8 +222,8 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return false;
 	}
 
-	private boolean deleteApiWithoutNotify(String id){
-		if(apiServiceProvider.delete(id)){	//删除成功时在取消注册
+	private boolean deleteApiWithoutNotify(String id) {
+		if (apiServiceProvider.delete(id)) {    //删除成功时在取消注册
 			mappingHandlerMapping.unregisterMapping(id, true);
 			return true;
 		}
@@ -570,6 +566,39 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		magicNotifyService.sendNotify(new MagicNotify(instanceId));
 	}
 
+	@Override
+	public JsonBean<?> push(String target, String secretKey, String mode) {
+		notBlank(target, TARGET_IS_REQUIRED);
+		notBlank(secretKey, SECRET_KEY_IS_REQUIRED);
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		try {
+			workspace.export(baos, Constants.PATH_BACKUPS);
+		} catch (IOException e) {
+			return new JsonBean<>(-1, e.getMessage());
+		}
+		byte[] bytes = baos.toByteArray();
+		long timestamp = System.currentTimeMillis();
+		RestTemplate restTemplate = new RestTemplate();
+		MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
+		param.add("timestamp", timestamp);
+		param.add("mode", mode);
+		param.add("sign", SignUtils.sign(timestamp, secretKey, mode, bytes));
+		param.add("file", new InputStreamResource(new ByteArrayInputStream(bytes)) {
+			@Override
+			public String getFilename() {
+				return "magic-api.zip";
+			}
+
+			@Override
+			public long contentLength() {
+				return bytes.length;
+			}
+		});
+		HttpHeaders headers = new HttpHeaders();
+		headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+		return restTemplate.postForObject(target, new HttpEntity<>(param, headers), JsonBean.class);
+	}
+
 	@Override
 	public boolean processNotify(MagicNotify magicNotify) {
 		if (magicNotify == null || instanceId.equals(magicNotify.getFrom())) {

+ 8 - 0
magic-api/src/main/java/org/ssssssss/magicapi/utils/SignUtils.java

@@ -0,0 +1,8 @@
+package org.ssssssss.magicapi.utils;
+
+public class SignUtils {
+
+	public static String sign(Long timestamp, String secretKey, String mode, byte[] bytes) {
+		return MD5Utils.encrypt(String.format("%s|%s|%s|%s", timestamp, mode, MD5Utils.encrypt(bytes), secretKey));
+	}
+}

+ 1 - 0
magic-editor/src/console/src/components/common/magic-contextmenu/Submenu.vue

@@ -255,6 +255,7 @@ export default {
 }
 .magic-contextmenu-item .magic-contextmenu-item-icon i{
     font-size: 12px;
+    color: var(--icon-color);
 }
 
 .magic-contextmenu-item .magic-contextmenu-item-label {

+ 44 - 1
magic-editor/src/console/src/components/layout/magic-header.vue

@@ -23,6 +23,9 @@
     <span title="导出接口" @click="download">
       <i class="ma-icon ma-icon-download"></i>
     </span>
+    <span title="远程推送" @click="showPushDialog = true">
+      <i class="ma-icon ma-icon-push"></i>
+    </span>
     <span v-if="config.header.skin !== false" title="换肤" @click.stop="skinVisible = true">
       <i class="ma-icon ma-icon-skin"></i>
     </span>
@@ -58,6 +61,22 @@
         <button class="ma-button" @click="() => doUpload('full')">全量上传</button>
       </template>
     </magic-dialog>
+    <magic-dialog title="远程推送" :value="showPushDialog" align="right" @onClose="showPushDialog = false" class="ma-remote-push-container" width="400px">
+        <template #content>
+            <div>
+                <label>远程地址:</label>
+                <magic-input placeholder="请输入远程地址" v-model="target" width="300px"/>
+            </div>
+            <div>
+                <label>秘钥:</label>
+                <magic-input placeholder="请输入秘钥" v-model="secretKey" width="300px"/>
+            </div>
+        </template>
+        <template #buttons>
+            <button class="ma-button active" @click="() => doPush('increment')">增量推送</button>
+            <button class="ma-button" @click="() => doPush('full')">全量推送</button>
+        </template>
+    </magic-dialog>
     <magic-search ref="search" style="flex: none"></magic-search>
   </div>
 </template>
@@ -91,7 +110,10 @@ export default {
       Themes,
       skinVisible: false,
       showUploadDialog: false,
+      showPushDialog: false,
       filename: null,
+      target: 'http://host:port/_magic-api-sync',
+      secretKey: '123456789',
       skinRight: 15 + ((this.config.header.repo ? 2 : 0) + (this.config.header.qqGroup ? 1 : 0) + (this.config.header.document ? 1 : 0)) * 25 + 'px',
     }
   },
@@ -126,6 +148,18 @@ export default {
     upload() {
       this.showUploadDialog = true;
     },
+    doPush(mode){
+        request.send('/push',{
+            target: this.target,
+            secretKey: this.secretKey,
+            mode: mode
+        }).success(() => {
+            this.$magicAlert({
+                content: '推送成功!'
+            })
+            this.showPushDialog = false;
+        })
+    },
     doUpload(mode) {
       let file = this.$refs.file.files[0];
       if (file) {
@@ -247,6 +281,11 @@ export default {
   color: var(--button-run-color);
 }
 
+.ma-header .ma-icon-push {
+    color: var(--button-run-color);
+    font-weight: bold;
+}
+
 .ma-header > span:hover:not(.disabled) {
   background: var(--button-hover-background);
 }
@@ -269,7 +308,11 @@ export default {
   border-bottom: 1px solid var(--border-color);
   padding: 2px 5px;
 }
-
+.ma-remote-push-container label{
+  width: 80px;
+  text-align: right;
+  display: inline-block;
+}
 ul li {
   height: 24px;
   line-height: 24px;