Przeglądaj źródła

1、备份初步重构。2、修复import + exit 未正确退出的问题

mxd 4 lat temu
rodzic
commit
7d009a96e9
20 zmienionych plików z 679 dodań i 117 usunięć
  1. 74 0
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/BackupConfig.java
  2. 47 6
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java
  3. 12 2
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIProperties.java
  4. 10 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/MagicConfiguration.java
  5. 7 1
      magic-api/src/main/java/org/ssssssss/magicapi/config/MagicFunctionManager.java
  6. 6 10
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicAPIController.java
  7. 8 0
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicController.java
  8. 0 3
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDataSourceController.java
  9. 8 11
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicFunctionController.java
  10. 0 3
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicGroupController.java
  11. 5 8
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java
  12. 113 0
      magic-api/src/main/java/org/ssssssss/magicapi/model/Backup.java
  13. 74 0
      magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicBackupService.java
  14. 0 53
      magic-api/src/main/java/org/ssssssss/magicapi/provider/StoreServiceProvider.java
  15. 21 18
      magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java
  16. 92 0
      magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/MagicDatabaseBackupService.java
  17. 167 0
      magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/MagicFileBackupService.java
  18. 32 0
      magic-api/src/main/java/org/ssssssss/magicapi/utils/WebUtils.java
  19. 2 1
      magic-editor/src/console/src/components/editor/magic-history.vue
  20. 1 1
      magic-editor/src/console/src/components/editor/magic-script-editor.vue

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

@@ -0,0 +1,74 @@
+package org.ssssssss.magicapi.spring.boot.starter;
+
+/**
+ * 备份配置
+ *
+ * @since 1.3.5
+ */
+public class BackupConfig extends ResourceConfig {
+
+	/**
+	 * 存储类型,可选 file, database
+	 */
+	private String resourceType = "file";
+
+	/**
+	 * 存储位置,选择存储为文件时专用
+	 */
+	private String location = "/data/magic-api/backup";
+
+	/**
+	 * 保留天数,<=0 为不限制
+	 */
+	private int maxHistory = -1;
+
+	/**
+	 * 使用数据库存储时的表名
+	 */
+	private String tableName;
+
+	/**
+	 * 使用数据库存储时使用的数据源
+	 */
+	private String database;
+
+	public String getResourceType() {
+		return resourceType;
+	}
+
+	public void setResourceType(String resourceType) {
+		this.resourceType = resourceType;
+	}
+
+	public String getLocation() {
+		return location;
+	}
+
+	public void setLocation(String location) {
+		this.location = location;
+	}
+
+	public int getMaxHistory() {
+		return maxHistory;
+	}
+
+	public void setMaxHistory(int maxHistory) {
+		this.maxHistory = maxHistory;
+	}
+
+	public String getTableName() {
+		return tableName;
+	}
+
+	public void setTableName(String tableName) {
+		this.tableName = tableName;
+	}
+
+	public String getDatabase() {
+		return database;
+	}
+
+	public void setDatabase(String database) {
+		this.database = database;
+	}
+}

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

@@ -67,10 +67,13 @@ import org.ssssssss.script.reflection.JavaReflection;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.function.BiFunction;
 
 @Configuration
@@ -230,6 +233,18 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 		return ResourceAdapter.getResource(resourceConfig.getLocation(), resourceConfig.isReadonly());
 	}
 
+	@Bean
+	@ConditionalOnMissingBean(MagicBackupService.class)
+	@ConditionalOnProperty(prefix = "magic-api", name = "backup-config.resource-type", havingValue = "database")
+	public MagicBackupService magicDatabaseBackupService(MagicDynamicDataSource magicDynamicDataSource) {
+		BackupConfig backupConfig = properties.getBackupConfig();
+		MagicDynamicDataSource.DataSourceNode dataSourceNode = magicDynamicDataSource.getDataSource(backupConfig.getDatasource());
+		if (dataSourceNode == null) {
+			throw new IllegalArgumentException(String.format("找不到数据源:%s", backupConfig.getDatasource()));
+		}
+		return new MagicDatabaseBackupService(new JdbcTemplate(dataSourceNode.getDataSource()), backupConfig.getTableName());
+	}
+
 
 	@Override
 	public void addResourceHandlers(ResourceHandlerRegistry registry) {
@@ -339,6 +354,13 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 		};
 	}
 
+	@Bean
+	@ConditionalOnMissingBean(MagicBackupService.class)
+	@ConditionalOnProperty(prefix = "magic-api", name = "backup-config.resource-type", havingValue = "file", matchIfMissing = true)
+	public MagicBackupService magicFileBackupService() {
+		return new MagicFileBackupService(new File(properties.getBackupConfig().getLocation()));
+	}
+
 	@Bean
 	public MagicFunctionManager magicFunctionManager(GroupServiceProvider groupServiceProvider, FunctionServiceProvider functionServiceProvider) {
 		return new MagicFunctionManager(groupServiceProvider, functionServiceProvider);
@@ -356,8 +378,9 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 										   ResultProvider resultProvider,
 										   MagicDynamicDataSource magicDynamicDataSource,
 										   MagicFunctionManager magicFunctionManager,
-										   Resource workspace) {
-		return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicDynamicDataSource, magicFunctionManager, magicNotifyServiceProvider.getObject(), properties.getClusterConfig().getInstanceId(), workspace, properties.isThrowException());
+										   Resource workspace,
+										   MagicBackupService magicBackupService) {
+		return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicDynamicDataSource, magicFunctionManager, magicNotifyServiceProvider.getObject(), properties.getClusterConfig().getInstanceId(), workspace, magicBackupService, properties.isThrowException());
 	}
 
 	private void setupSpringSecurity() {
@@ -402,7 +425,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 		dialectsProvider.getIfAvailable(Collections::emptyList).forEach(dialectAdapter::add);
 		sqlModule.setDialectAdapter(dialectAdapter);
 		sqlModule.setLogicDeleteColumn(properties.getCrudConfig().getLogicDeleteColumn());
-        sqlModule.setLogicDeleteValue(properties.getCrudConfig().getLogicDeleteValue());
+		sqlModule.setLogicDeleteValue(properties.getCrudConfig().getLogicDeleteValue());
 		return sqlModule;
 	}
 
@@ -486,7 +509,8 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 												 MappingHandlerMapping mappingHandlerMapping,
 												 FunctionServiceProvider functionServiceProvider,
 												 MagicNotifyService magicNotifyService,
-												 MagicFunctionManager magicFunctionManager) throws NoSuchMethodException {
+												 MagicFunctionManager magicFunctionManager,
+												 MagicBackupService magicBackupService) throws NoSuchMethodException {
 		logger.info("magic-api工作目录:{}", magicResource);
 		setupSpringSecurity();
 		AsyncCall.setThreadPoolExecutorSize(properties.getThreadPoolExecutorSize());
@@ -505,6 +529,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 		configuration.setGroupServiceProvider(groupServiceProvider);
 		configuration.setMappingHandlerMapping(mappingHandlerMapping);
 		configuration.setFunctionServiceProvider(functionServiceProvider);
+		configuration.setMagicBackupService(magicBackupService);
 		SecurityConfig securityConfig = properties.getSecurityConfig();
 		configuration.setDebugTimeout(properties.getDebugConfig().getTimeout());
 		configuration.setHttpMessageConverters(httpMessageConvertersProvider.getIfAvailable(Collections::emptyList));
@@ -541,13 +566,14 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 			Method method = MagicWorkbenchController.class.getDeclaredMethod("receivePush", MultipartFile.class, String.class, Long.class, String.class);
 			Mapping.create(requestMappingHandlerMapping).register(requestMappingInfo, magicWorkbenchController, method);
 		}
+		// 注册数据源
 		magicAPIService.registerAllDataSource();
 		// 设置拦截器信息
 		this.requestInterceptorsProvider.getIfAvailable(Collections::emptyList).forEach(interceptor -> {
 			logger.info("注册请求拦截器:{}", interceptor.getClass());
 			configuration.addRequestInterceptor(interceptor);
 		});
-
+		// 打印banner
 		if (this.properties.isBanner()) {
 			configuration.printBanner();
 		}
@@ -561,6 +587,21 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 		mappingHandlerMapping.setGroupServiceProvider(groupServiceProvider);
 		// 注册所有映射
 		mappingHandlerMapping.registerAllMapping();
+		// 备份清理
+		if (properties.getBackupConfig().getMaxHistory() > 0) {
+			long interval = properties.getBackupConfig().getMaxHistory() * 86400000L;
+			// 1小时执行1次
+			Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
+				try {
+					long count = magicBackupService.removeBackupByTimestamp(System.currentTimeMillis() - interval);
+					if(count > 0){
+						logger.info("已删除备份记录{}条", count);
+					}
+				} catch (Exception e) {
+					logger.error("删除备份记录时出错", e);
+				}
+			}, 1, 1, TimeUnit.HOURS);
+		}
 		return configuration;
 	}
 
@@ -588,7 +629,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
 	public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
 		String web = properties.getWeb();
 		if (web != null) {
-			MagicWebSocketDispatcher dispatcher = new MagicWebSocketDispatcher(properties.getClusterConfig().getInstanceId(),magicNotifyServiceProvider.getObject(), Arrays.asList(
+			MagicWebSocketDispatcher dispatcher = new MagicWebSocketDispatcher(properties.getClusterConfig().getInstanceId(), magicNotifyServiceProvider.getObject(), Arrays.asList(
 					new MagicDebugHandler()
 			));
 			WebSocketHandlerRegistration registration = webSocketHandlerRegistry.addHandler(dispatcher, web + "/console");

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

@@ -129,6 +129,9 @@ public class MagicAPIProperties {
 	@NestedConfigurationProperty
 	private CrudConfig crudConfig = new CrudConfig();
 
+	@NestedConfigurationProperty
+	private BackupConfig backupConfig = new BackupConfig();
+
 	public CrudConfig getCrudConfig() {
 		return crudConfig;
 	}
@@ -345,8 +348,15 @@ public class MagicAPIProperties {
 		return showUrl;
 	}
 
-	public MagicAPIProperties setShowUrl(boolean showUrl) {
+	public void setShowUrl(boolean showUrl) {
 		this.showUrl = showUrl;
-		return this;
+	}
+
+	public BackupConfig getBackupConfig() {
+		return backupConfig;
+	}
+
+	public void setBackupConfig(BackupConfig backupConfig) {
+		this.backupConfig = backupConfig;
 	}
 }

+ 10 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/MagicConfiguration.java

@@ -65,6 +65,8 @@ public class MagicConfiguration {
 
 	private AuthorizationInterceptor authorizationInterceptor;
 
+	private MagicBackupService magicBackupService;
+
 	/**
 	 * debug 超时时间
 	 */
@@ -208,6 +210,14 @@ public class MagicConfiguration {
 		this.instanceId = instanceId;
 	}
 
+	public MagicBackupService getMagicBackupService() {
+		return magicBackupService;
+	}
+
+	public void setMagicBackupService(MagicBackupService magicBackupService) {
+		this.magicBackupService = magicBackupService;
+	}
+
 	/**
 	 * 打印banner
 	 */

+ 7 - 1
magic-api/src/main/java/org/ssssssss/magicapi/config/MagicFunctionManager.java

@@ -12,6 +12,8 @@ import org.ssssssss.magicapi.script.ScriptManager;
 import org.ssssssss.magicapi.utils.PathUtils;
 import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScriptContext;
+import org.ssssssss.script.exception.MagicExitException;
+import org.ssssssss.script.parsing.ast.statement.Exit;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
@@ -46,7 +48,11 @@ public class MagicFunctionManager {
 								functionContext.set(parameters.get(i).getName(), objects[i]);
 							}
 						}
-						return ScriptManager.executeScript(info.getScript(), functionContext);
+						Object value =  ScriptManager.executeScript(info.getScript(), functionContext);
+						if(value instanceof Exit.Value){
+							throw new MagicExitException((Exit.Value) value);
+						}
+						return value;
 					} finally {
 						MagicScriptContext.set(context);
 					}

+ 6 - 10
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicAPIController.java

@@ -7,9 +7,11 @@ import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.Valid;
 import org.ssssssss.magicapi.interceptor.Authorization;
 import org.ssssssss.magicapi.model.ApiInfo;
+import org.ssssssss.magicapi.model.Backup;
 import org.ssssssss.magicapi.model.JsonBean;
 import org.ssssssss.magicapi.provider.ApiServiceProvider;
 import org.ssssssss.magicapi.provider.MagicAPIService;
+import org.ssssssss.magicapi.provider.MagicBackupService;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.List;
@@ -20,14 +22,8 @@ import java.util.stream.Collectors;
  */
 public class MagicAPIController extends MagicController implements MagicExceptionHandler {
 
-	private final ApiServiceProvider apiServiceProvider;
-
-	private final MagicAPIService magicAPIService;
-
 	public MagicAPIController(MagicConfiguration configuration) {
 		super(configuration);
-		this.apiServiceProvider = configuration.getApiServiceProvider();
-		this.magicAPIService = configuration.getMagicAPIService();
 	}
 
 	/**
@@ -76,9 +72,9 @@ public class MagicAPIController extends MagicController implements MagicExceptio
 	 */
 	@RequestMapping("/backups")
 	@ResponseBody
-	public JsonBean<List<Long>> backups(HttpServletRequest request, String id) {
+	public JsonBean<List<Backup>> backupList(HttpServletRequest request, String id) {
 		isTrue(allowVisit(request, Authorization.VIEW, getApiInfo(id)), PERMISSION_INVALID);
-		return new JsonBean<>(apiServiceProvider.backupList(id));
+		return new JsonBean<>(magicBackupService.backupById(id));
 	}
 
 	/**
@@ -89,9 +85,9 @@ public class MagicAPIController extends MagicController implements MagicExceptio
 	 */
 	@RequestMapping("/backup/get")
 	@ResponseBody
-	public JsonBean<ApiInfo> backups(HttpServletRequest request, String id, Long timestamp) {
+	public JsonBean<Backup> backups(HttpServletRequest request, String id, Long timestamp) {
 		isTrue(allowVisit(request, Authorization.VIEW, getApiInfo(id)), PERMISSION_INVALID);
-		return new JsonBean<>(apiServiceProvider.backupInfo(id, timestamp));
+		return new JsonBean<>(magicBackupService.backupInfo(id, timestamp));
 	}
 
 	/**

+ 8 - 0
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicController.java

@@ -9,6 +9,8 @@ import org.ssssssss.magicapi.exception.MagicLoginException;
 import org.ssssssss.magicapi.interceptor.Authorization;
 import org.ssssssss.magicapi.interceptor.MagicUser;
 import org.ssssssss.magicapi.model.*;
+import org.ssssssss.magicapi.provider.MagicAPIService;
+import org.ssssssss.magicapi.provider.MagicBackupService;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -16,8 +18,14 @@ public class MagicController implements JsonCodeConstants {
 
 	MagicConfiguration configuration;
 
+	final MagicAPIService magicAPIService;
+
+	final MagicBackupService magicBackupService;
+
 	MagicController(MagicConfiguration configuration) {
 		this.configuration = configuration;
+		this.magicAPIService = configuration.getMagicAPIService();
+		this.magicBackupService = configuration.getMagicBackupService();
 	}
 
 	public void doValid(HttpServletRequest request, Valid valid) {

+ 0 - 3
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDataSourceController.java

@@ -16,11 +16,8 @@ import java.util.stream.Collectors;
 
 public class MagicDataSourceController extends MagicController implements MagicExceptionHandler {
 
-	private final MagicAPIService magicAPIService;
-
 	public MagicDataSourceController(MagicConfiguration configuration) {
 		super(configuration);
-		this.magicAPIService = configuration.getMagicAPIService();
 	}
 
 	/**

+ 8 - 11
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicFunctionController.java

@@ -6,10 +6,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
 import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.Valid;
 import org.ssssssss.magicapi.interceptor.Authorization;
+import org.ssssssss.magicapi.model.Backup;
 import org.ssssssss.magicapi.model.FunctionInfo;
 import org.ssssssss.magicapi.model.JsonBean;
 import org.ssssssss.magicapi.provider.FunctionServiceProvider;
 import org.ssssssss.magicapi.provider.MagicAPIService;
+import org.ssssssss.magicapi.provider.MagicBackupService;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.List;
@@ -17,14 +19,9 @@ import java.util.stream.Collectors;
 
 public class MagicFunctionController extends MagicController implements MagicExceptionHandler {
 
-	private final FunctionServiceProvider functionService;
-
-	private final MagicAPIService magicAPIService;
 
 	public MagicFunctionController(MagicConfiguration configuration) {
 		super(configuration);
-		this.functionService = configuration.getFunctionServiceProvider();
-		this.magicAPIService = configuration.getMagicAPIService();
 	}
 
 	@RequestMapping("/function/list")
@@ -46,16 +43,16 @@ public class MagicFunctionController extends MagicController implements MagicExc
 
 	@RequestMapping("/function/backup/get")
 	@ResponseBody
-	public JsonBean<FunctionInfo> backups(HttpServletRequest request, String id, Long timestamp) {
+	public JsonBean<Backup> backups(HttpServletRequest request, String id, Long timestamp) {
 		isTrue(allowVisit(request, Authorization.VIEW, getFunctionInfo(id)), PERMISSION_INVALID);
-		return new JsonBean<>(functionService.backupInfo(id, timestamp));
+		return new JsonBean<>(magicBackupService.backupInfo(id, timestamp));
 	}
 
 	@RequestMapping("/function/backups")
 	@ResponseBody
-	public JsonBean<List<Long>> backups(HttpServletRequest request, String id) {
+	public JsonBean<List<Backup>> backupList(HttpServletRequest request, String id) {
 		isTrue(allowVisit(request, Authorization.VIEW, getFunctionInfo(id)), PERMISSION_INVALID);
-		return new JsonBean<>(functionService.backupList(id));
+		return new JsonBean<>(magicBackupService.backupById(id));
 	}
 
 	@RequestMapping("/function/move")
@@ -82,8 +79,8 @@ public class MagicFunctionController extends MagicController implements MagicExc
 		isTrue(allowVisit(request, Authorization.DELETE, getFunctionInfo(id)), PERMISSION_INVALID);
 		return new JsonBean<>(magicAPIService.deleteFunction(id));
 	}
-	
-	public FunctionInfo getFunctionInfo(String id){
+
+	public FunctionInfo getFunctionInfo(String id) {
 		FunctionInfo functionInfo = magicAPIService.getFunctionInfo(id);
 		notNull(functionInfo, FUNCTION_NOT_FOUND);
 		return functionInfo;

+ 0 - 3
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicGroupController.java

@@ -16,11 +16,8 @@ import java.util.stream.Collectors;
 
 public class MagicGroupController extends MagicController implements MagicExceptionHandler {
 
-	private final MagicAPIService magicAPIService;
-
 	public MagicGroupController(MagicConfiguration configuration) {
 		super(configuration);
-		this.magicAPIService = configuration.getMagicAPIService();
 	}
 
 	/**

+ 5 - 8
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -49,8 +49,6 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	private static final Logger logger = LoggerFactory.getLogger(MagicWorkbenchController.class);
 
-	private final MagicAPIService magicApiService;
-
 	private final String secretKey;
 
 	private static final Pattern SINGLE_LINE_COMMENT_TODO = Pattern.compile("((TODO)|(todo)|(fixme)|(FIXME))[ \t]+[^\n]+");
@@ -59,7 +57,6 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	public MagicWorkbenchController(MagicConfiguration configuration, String secretKey) {
 		super(configuration);
-		magicApiService = configuration.getMagicAPIService();
 		this.secretKey = secretKey;
 		// 给前端添加代码提示
 		MagicScriptEngine.addScriptClass(SQLModule.class);
@@ -145,7 +142,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 		// 重新注册函数
 		configuration.getMagicFunctionManager().registerAllFunction();;
 		// 重新注册数据源
-		magicApiService.registerAllDataSource();
+		magicAPIService.registerAllDataSource();
 		// 发送更新通知
 		configuration.getMagicNotifyService().sendNotify(new MagicNotify(configuration.getInstanceId()));
 		return new JsonBean<>();
@@ -254,7 +251,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	@ResponseBody
 	public ResponseEntity<?> download(String groupId, @RequestBody(required = false) List<SelectedResource> resources) throws IOException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
-		magicApiService.download(groupId, resources, os);
+		magicAPIService.download(groupId, resources, os);
 		if (StringUtils.isBlank(groupId)) {
 			return ResponseModule.download(os.toByteArray(), "magic-api-group.zip");
 		} else {
@@ -267,7 +264,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	@ResponseBody
 	public JsonBean<Boolean> upload(MultipartFile file, String mode) throws IOException {
 		notNull(file, FILE_IS_REQUIRED);
-		magicApiService.upload(file.getInputStream(), mode);
+		magicAPIService.upload(file.getInputStream(), mode);
 		return new JsonBean<>(SUCCESS, true);
 	}
 
@@ -276,7 +273,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	@Valid(authorization = Authorization.PUSH)
 	public JsonBean<?> push(@RequestHeader("magic-push-target") String target, @RequestHeader("magic-push-secret-key")String secretKey,
 							@RequestHeader("magic-push-mode")String mode, @RequestBody List<SelectedResource> resources) {
-		return magicApiService.push(target, secretKey, mode, resources);
+		return magicAPIService.push(target, secretKey, mode, resources);
 	}
 
 	@ResponseBody
@@ -288,7 +285,7 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 		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);
+		magicAPIService.upload(new ByteArrayInputStream(bytes), mode);
 		return new JsonBean<>();
 	}
 }

+ 113 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/Backup.java

@@ -0,0 +1,113 @@
+package org.ssssssss.magicapi.model;
+
+/**
+ * 备份记录
+ */
+public class Backup {
+
+	/**
+	 * 记录ID
+	 */
+	private String id;
+	/**
+	 * 备份时间
+	 */
+	private long createDate;
+
+	/**
+	 * 标签,只允许有一个
+	 */
+	private String tag;
+
+	/**
+	 * 备份类型,api function datasource
+	 */
+	private String type;
+
+	/**
+	 * 原名称
+	 */
+	private String name;
+
+	/**
+	 * 备份内容
+	 */
+	private String content;
+
+	/**
+	 * 操作人,取用户名,空为系统记录
+	 */
+	private String createBy;
+
+
+	public Backup() {
+	}
+
+	public Backup(String id, String type, String name, String content) {
+		this.id = id;
+		this.type = type;
+		this.name = name;
+		this.content = content;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public long getCreateDate() {
+		return createDate;
+	}
+
+	public void setCreateDate(long createDate) {
+		this.createDate = createDate;
+	}
+
+	public String getTag() {
+		return tag;
+	}
+
+	public void setTag(String tag) {
+		this.tag = tag;
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public String getCreateBy() {
+		return createBy;
+	}
+
+	public void setCreateBy(String createBy) {
+		this.createBy = createBy;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Backup small(){
+		setContent(null);
+		return this;
+	}
+}

+ 74 - 0
magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicBackupService.java

@@ -0,0 +1,74 @@
+package org.ssssssss.magicapi.provider;
+
+import org.ssssssss.magicapi.model.*;
+import org.ssssssss.magicapi.utils.JsonUtils;
+
+import java.util.List;
+
+/**
+ * 数据备份接口
+ */
+public interface MagicBackupService {
+
+	int FETCH_SIZE = 100;
+
+	default void backup(ApiInfo apiInfo) {
+		doBackup(new Backup(apiInfo.getId(), Constants.PATH_API, apiInfo.getName(), JsonUtils.toJsonString(apiInfo)));
+	}
+
+	default void backup(FunctionInfo functionInfo) {
+		doBackup(new Backup(functionInfo.getId(), Constants.PATH_FUNCTION, functionInfo.getName(), JsonUtils.toJsonString(functionInfo)));
+	}
+
+	default void backup(DataSourceInfo dataSourceInfo) {
+		doBackup(new Backup(dataSourceInfo.getId(), Constants.PATH_DATASOURCE, dataSourceInfo.get("name"), JsonUtils.toJsonString(dataSourceInfo)));
+	}
+
+	/**
+	 * 执行备份动作
+	 */
+	void doBackup(Backup backup);
+
+	/**
+	 * 根据时间戳查询最近的 FETCH_SIZE 条记录
+	 */
+	List<Backup> backupList(long timestamp);
+
+	/**
+	 * 根据对象ID查询备份记录
+	 */
+	List<Backup> backupById(String id);
+
+	/**
+	 * 根据对象ID和备份时间查询
+	 */
+	Backup backupInfo(String id, long timestamp);
+
+	/**
+	 * 根据标签查询备份记录
+	 */
+	List<Backup> backupByTag(String tag);
+
+	/**
+	 * 删除备份
+	 *
+	 * @return 返回删除的记录数
+	 */
+	long removeBackup(String id);
+
+	/**
+	 * 删除一组备份信息
+	 *
+	 * @return 返回删除的记录数
+	 */
+	long removeBackup(List<String> idList);
+
+	/**
+	 * 根据13位时间戳删除备份记录(清除小于该值的记录)
+	 *
+	 * @return 返回删除的记录数
+	 */
+	long removeBackupByTimestamp(long timestamp);
+
+
+}

+ 0 - 53
magic-api/src/main/java/org/ssssssss/magicapi/provider/StoreServiceProvider.java

@@ -15,7 +15,6 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 
 	private static final Logger logger = LoggerFactory.getLogger(StoreServiceProvider.class);
 	protected Resource workspace;
-	protected Resource backupResource;
 	protected Map<String, Resource> mappings = new HashMap<>();
 	protected Map<String, T> infos = new HashMap<>();
 	protected GroupServiceProvider groupServiceProvider;
@@ -30,10 +29,6 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 		if (!this.workspace.exists()) {
 			this.workspace.mkdir();
 		}
-		this.backupResource = this.workspace.parent().getDirectory(Constants.PATH_BACKUPS);
-		if (!this.backupResource.exists()) {
-			this.backupResource.mkdir();
-		}
 	}
 
 
@@ -53,54 +48,6 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 		return false;
 	}
 
-	/**
-	 * 备份历史记录
-	 */
-	public boolean backup(T info) {
-		Resource directory = this.backupResource.getDirectory(info.getId());
-		if (!directory.readonly() && (directory.exists() || directory.mkdir())) {
-			Resource resource = directory.getResource(String.format("%s.ms", System.currentTimeMillis()));
-			try {
-				return resource.write(serialize(info));
-			} catch (Exception e) {
-				logger.warn("保存历史记录失败,{}", e.getMessage());
-			}
-		}
-		return false;
-	}
-
-
-	/**
-	 * 查询历史记录
-	 *
-	 * @return 时间戳列表
-	 */
-	public List<Long> backupList(String id) {
-		Resource directory = this.backupResource.getDirectory(id);
-		List<Resource> resources = directory.files(".ms");
-		return resources.stream()
-				.map(it -> Long.valueOf(it.name().replace(".ms", "")))
-				.sorted(Comparator.reverseOrder())
-				.collect(Collectors.toList());
-	}
-
-	/**
-	 * 查询历史记录详情
-	 *
-	 * @param id        ID
-	 * @param timestamp 时间戳
-	 */
-	public T backupInfo(String id, Long timestamp) {
-		Resource directory = this.backupResource.getDirectory(id);
-		if (directory.exists()) {
-			Resource resource = directory.getResource(String.format("%s.ms", timestamp));
-			if (resource.exists()) {
-				return deserialize(resource.read());
-			}
-		}
-		return null;
-	}
-
 	/**
 	 * 修改
 	 */

+ 21 - 18
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -21,6 +21,7 @@ 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.KeyValueResource;
 import org.ssssssss.magicapi.adapter.resource.ZipResource;
 import org.ssssssss.magicapi.config.MagicDynamicDataSource;
 import org.ssssssss.magicapi.config.MagicFunctionManager;
@@ -40,10 +41,12 @@ import org.ssssssss.magicapi.utils.SignUtils;
 import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScript;
 import org.ssssssss.script.MagicScriptContext;
+import org.ssssssss.script.exception.MagicExitException;
 import org.ssssssss.script.functions.ObjectConvertExtension;
 import org.ssssssss.script.parsing.Scope;
 import org.ssssssss.script.parsing.Span;
 import org.ssssssss.script.parsing.ast.Expression;
+import org.ssssssss.script.parsing.ast.statement.Exit;
 
 import javax.script.ScriptContext;
 import javax.script.SimpleScriptContext;
@@ -79,6 +82,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	private final String instanceId;
 	private final Resource workspace;
 	private final Resource datasourceResource;
+	private final MagicBackupService backupService;
 
 	public DefaultMagicAPIService(MappingHandlerMapping mappingHandlerMapping,
 								  ApiServiceProvider apiServiceProvider,
@@ -90,6 +94,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 								  MagicNotifyService magicNotifyService,
 								  String instanceId,
 								  Resource workspace,
+								  MagicBackupService backupService,
 								  boolean throwException) {
 		this.mappingHandlerMapping = mappingHandlerMapping;
 		this.apiServiceProvider = apiServiceProvider;
@@ -102,6 +107,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		this.workspace = workspace;
 		this.throwException = throwException;
 		this.instanceId = instanceId;
+		this.backupService = backupService;
 		this.datasourceResource = workspace.getDirectory(PATH_DATASOURCE);
 		if (!this.datasourceResource.exists()) {
 			this.datasourceResource.mkdir();
@@ -116,7 +122,11 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 					return new Expression(new Span("unknown source")) {
 						@Override
 						public Object evaluate(MagicScriptContext context, Scope scope) {
-							return execute(info, scope.getVariables());
+							Object value = execute(info, scope.getVariables());
+							if(value instanceof Exit.Value){
+								throw new MagicExitException((Exit.Value) value);
+							}
+							return value;
 						}
 					};
 				}
@@ -193,7 +203,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 					.findFirst();
 			if (optional.isPresent() && !optional.get().equals(info)) {
 				isTrue(apiServiceProvider.update(info), API_SAVE_FAILURE);
-				apiServiceProvider.backup(info);
+				backupService.backup(info);
 			}
 		}
 		// 注册接口
@@ -262,7 +272,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		} else {
 			isTrue(!functionServiceProvider.existsWithoutId(functionInfo), FUNCTION_ALREADY_EXISTS.format(functionInfo.getPath()));
 			isTrue(functionServiceProvider.update(functionInfo), FUNCTION_SAVE_FAILURE);
-			functionServiceProvider.backup(functionInfo);
+			backupService.backup(functionInfo);
 		}
 		magicFunctionManager.register(functionInfo);
 		magicNotifyService.sendNotify(new MagicNotify(instanceId, functionInfo.getId(), action, NOTIFY_ACTION_FUNCTION));
@@ -488,6 +498,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		magicDynamicDataSource.put(dsId, key, name, createDataSource(properties), maxRows);
 		properties.put("id", dsId);
 		datasourceResource.getResource(dsId + ".json").write(JsonUtils.toJsonString(properties));
+		backupService.backup(properties);
 		magicNotifyService.sendNotify(new MagicNotify(instanceId, dsId, action, NOTIFY_ACTION_DATASOURCE));
 		return dsId;
 	}
@@ -558,10 +569,12 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 				groupServiceProvider.insert(group);
 			}
 		}
-		Resource backups = workspace.getDirectory(PATH_BACKUPS);
 		// 保存
-		write(apiServiceProvider, backups, apiInfos);
-		write(functionServiceProvider, backups, functionInfos);
+		write(apiServiceProvider, apiInfos);
+		write(functionServiceProvider, functionInfos);
+		// 备份
+		apiInfos.forEach(backupService::backup);
+		functionInfos.forEach(backupService::backup);
 		// 重新注册
 		mappingHandlerMapping.registerAllMapping();
 		magicFunctionManager.registerAllFunction();
@@ -571,6 +584,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 				byte[] content = it.read();
 				// 保存数据源
 				this.datasourceResource.getResource(it.name()).write(content);
+				// TODO 备份数据源
 			});
 		}
 		// TODO 会造成闪断,需要上锁处理。
@@ -812,25 +826,14 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_SET);
 	}
 
-	private <T extends MagicEntity> void write(StoreServiceProvider<T> provider, Resource backups, Set<T> infos) {
+	private <T extends MagicEntity> void write(StoreServiceProvider<T> provider, Set<T> infos) {
 		for (T info : infos) {
-			Resource resource = groupServiceProvider.getGroupResource(info.getGroupId());
-			resource = resource.getResource(info.getName() + ".ms");
-			byte[] content = null;
 			T oldInfo = provider.get(info.getId());
 			if (oldInfo != null) {
-				content = provider.serialize(oldInfo);
 				provider.update(info);
 			} else {
 				provider.insert(info);
 			}
-			if (content != null) {
-				Resource directory = backups.getDirectory(info.getId());
-				if (!directory.exists()) {
-					directory.mkdir();
-				}
-				directory.getResource(System.currentTimeMillis() + ".ms").write(content);
-			}
 		}
 	}
 

+ 92 - 0
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/MagicDatabaseBackupService.java

@@ -0,0 +1,92 @@
+package org.ssssssss.magicapi.provider.impl;
+
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.ssssssss.magicapi.model.Backup;
+import org.ssssssss.magicapi.provider.MagicBackupService;
+import org.ssssssss.magicapi.utils.WebUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class MagicDatabaseBackupService implements MagicBackupService {
+
+	private final static String DEFAULT_COLUMNS = "id,create_date,tag,type,name,create_by";
+
+	private final JdbcTemplate template;
+
+	private final String INSERT_SQL;
+
+	private final String FIND_BY_ID;
+
+	private final String FIND_BY_TAG;
+
+	private final String FIND_BY_TIMESTAMP;
+
+	private final String FIND_BY_ID_AND_TIMESTAMP;
+
+	private final String DELETE_BY_ID;
+
+	private final String DELETE_BY_TIMESTAMP;
+
+	private final BeanPropertyRowMapper<Backup> rowMapper = new BeanPropertyRowMapper<>(Backup.class);
+
+	public MagicDatabaseBackupService(JdbcTemplate template, String tableName) {
+		this.template = template;
+		this.INSERT_SQL = String.format("insert into %s(%s,content) values(?,?,?,?,?,?,?)", tableName, DEFAULT_COLUMNS);
+		this.FIND_BY_ID = String.format("select %s from %s where id = ?", DEFAULT_COLUMNS, tableName);
+		this.DELETE_BY_ID = String.format("delete from %s where id = ?", tableName);
+		this.FIND_BY_TAG = String.format("select %s from %s where tag = ?", DEFAULT_COLUMNS, tableName);
+		this.FIND_BY_TIMESTAMP = String.format("select %s from %s where create_date < ?", DEFAULT_COLUMNS, tableName);
+		this.DELETE_BY_TIMESTAMP = String.format("delete from %s where create_date < ?", tableName);
+		this.FIND_BY_ID_AND_TIMESTAMP = String.format("select * from %s where id = ? and create_date = ?", tableName);
+	}
+
+	@Override
+	public void doBackup(Backup backup) {
+		if (backup.getCreateDate() == 0) {
+			backup.setCreateDate(System.currentTimeMillis());
+		}
+		if (backup.getCreateBy() == null) {
+			backup.setCreateBy(WebUtils.currentUserName());
+		}
+		template.update(INSERT_SQL, backup.getId(), backup.getCreateDate(), backup.getTag(), backup.getType(), backup.getName(), backup.getCreateBy(), backup.getContent());
+	}
+
+	@Override
+	public List<Backup> backupList(long timestamp) {
+		Stream<Backup> stream = template.queryForStream(FIND_BY_TIMESTAMP, rowMapper, timestamp);
+		return stream.limit(20).collect(Collectors.toList());
+	}
+
+	@Override
+	public List<Backup> backupById(String id) {
+		return template.query(FIND_BY_ID, rowMapper, id);
+	}
+
+	@Override
+	public Backup backupInfo(String id, long timestamp) {
+		return template.queryForObject(FIND_BY_ID_AND_TIMESTAMP, rowMapper, id, timestamp);
+	}
+
+	@Override
+	public List<Backup> backupByTag(String tag) {
+		return template.query(FIND_BY_TAG, rowMapper, tag);
+	}
+
+	@Override
+	public long removeBackup(String id) {
+		return template.update(DELETE_BY_ID, id);
+	}
+
+	@Override
+	public long removeBackup(List<String> idList) {
+		return idList.stream().mapToLong(this::removeBackup).sum();
+	}
+
+	@Override
+	public long removeBackupByTimestamp(long timestamp) {
+		return template.update(DELETE_BY_TIMESTAMP, timestamp);
+	}
+}

+ 167 - 0
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/MagicFileBackupService.java

@@ -0,0 +1,167 @@
+package org.ssssssss.magicapi.provider.impl;
+
+import org.ssssssss.magicapi.model.Backup;
+import org.ssssssss.magicapi.provider.MagicBackupService;
+import org.ssssssss.magicapi.utils.IoUtils;
+import org.ssssssss.magicapi.utils.JsonUtils;
+import org.ssssssss.magicapi.utils.MD5Utils;
+import org.ssssssss.magicapi.utils.WebUtils;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class MagicFileBackupService implements MagicBackupService {
+
+	private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+	private static final String suffix = ".json";
+
+	/**
+	 * 保存路径
+	 */
+	private final File backupDirectory;
+
+	public MagicFileBackupService(File backupDirectory) {
+		this.backupDirectory = backupDirectory;
+	}
+
+	@Override
+	public void doBackup(Backup backup) {
+		if (backup.getCreateDate() == 0) {
+			backup.setCreateDate(System.currentTimeMillis());
+		}
+		if (backup.getCreateBy() == null) {
+			backup.setCreateBy(WebUtils.currentUserName());
+		}
+		File directory = new File(backupDirectory, Instant.ofEpochMilli(backup.getCreateDate()).atZone(ZoneOffset.ofHours(8)).toLocalDate().format(FORMATTER));
+		if (!directory.exists()) {
+			directory.mkdirs();
+		}
+		IoUtils.write(new File(directory, getFilename(backup)), JsonUtils.toJsonString(backup));
+	}
+
+	@Override
+	public List<Backup> backupList(long timestamp) {
+		File[] fileArray = backupDirectory.listFiles();
+		List<Backup> records = new ArrayList<>(FETCH_SIZE);
+		if (fileArray != null) {
+			List<File> dirs = Stream.of(fileArray).sorted(Comparator.comparing(File::getName)).collect(Collectors.toList());
+			outer:
+			for (File dir : dirs) {
+				fileArray = dir.listFiles((_dir, name) -> getTimestampFromFilename(name) < timestamp);
+				if (fileArray != null) {
+					for (File file : fileArray) {
+						records.add(JsonUtils.readValue(IoUtils.string(file), Backup.class));
+						if (records.size() >= FETCH_SIZE) {
+							break outer;
+						}
+					}
+				}
+			}
+		}
+		return records.stream()
+				.sorted(Comparator.comparing(Backup::getCreateDate).reversed())
+				.collect(Collectors.toList());
+	}
+
+	@Override
+	public List<Backup> backupById(String id) {
+		return backupByFilenameFilter((dir, name) -> name.endsWith(id + suffix))
+				.stream()
+				.sorted(Comparator.comparing(Backup::getCreateDate).reversed())
+				.collect(Collectors.toList());
+	}
+
+	@Override
+	public Backup backupInfo(String id, long timestamp) {
+		File directory = new File(backupDirectory, Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.ofHours(8)).toLocalDate().format(FORMATTER));
+		if (directory.exists()) {
+			File[] files = directory.listFiles((_dir, name) -> name.startsWith("" + timestamp) && name.endsWith(id + suffix));
+			if (files != null && files.length > 0) {
+				return JsonUtils.readValue(IoUtils.string(files[0]), Backup.class);
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public List<Backup> backupByTag(String tag) {
+		String tagId = MD5Utils.encrypt(tag);
+		return backupByFilenameFilter((dir, name) -> name.endsWith(suffix) && name.contains("-" + tagId + "-"));
+	}
+
+
+	@Override
+	public long removeBackup(String id) {
+		return getFilesByFilenameFilter((dir, name) -> name.endsWith(id + suffix))
+				.stream()
+				.filter(File::delete)
+				.count();
+	}
+
+	@Override
+	public long removeBackup(List<String> idList) {
+		List<String> filenames = idList.stream().map(it -> it + suffix).collect(Collectors.toList());
+		return getFilesByFilenameFilter((dir, name) -> filenames.stream().anyMatch(name::endsWith))
+				.stream()
+				.filter(File::delete)
+				.count();
+	}
+
+	@Override
+	public long removeBackupByTimestamp(long timestamp) {
+		long count = getFilesByFilenameFilter((dir, name) -> name.contains("-") && name.endsWith(".json") && name.length() == 32 + 32 + 13 + 2 + 5 && getTimestampFromFilename(name) < timestamp)
+				.stream()
+				.filter(File::delete)
+				.count();
+		// 删除空目录
+		File[] files = backupDirectory.listFiles(File::isDirectory);
+		if(files != null){
+			for (File file : files) {
+				String[] list = file.list();
+				if(list == null || list.length == 0){
+					file.delete();
+				}
+			}
+		}
+		return count;
+	}
+
+	private String getFilename(Backup backup) {
+		return String.format("%s-%s-%s.json", backup.getCreateDate(), MD5Utils.encrypt(Objects.toString(backup.getTag(), "")), backup.getId());
+	}
+
+	private Long getTimestampFromFilename(String filename) {
+		return Long.valueOf(filename.substring(0, 13));
+	}
+
+	private List<File> getFilesByFilenameFilter(FilenameFilter filenameFilter) {
+		List<File> list = new ArrayList<>();
+		File[] dirs = backupDirectory.listFiles();
+		if (dirs != null) {
+			for (File dir : dirs) {
+				if (dir.isDirectory()) {
+					File[] files = dir.listFiles(filenameFilter);
+					if (files != null) {
+						list.addAll(Arrays.asList(files));
+					}
+
+				}
+			}
+		}
+		return list;
+	}
+
+	private List<Backup> backupByFilenameFilter(FilenameFilter filenameFilter) {
+		return getFilesByFilenameFilter(filenameFilter).stream()
+				.map(IoUtils::string)
+				.map(it -> JsonUtils.readValue(it, Backup.class).small())
+				.collect(Collectors.toList());
+	}
+}

+ 32 - 0
magic-api/src/main/java/org/ssssssss/magicapi/utils/WebUtils.java

@@ -0,0 +1,32 @@
+package org.ssssssss.magicapi.utils;
+
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.ssssssss.magicapi.interceptor.MagicUser;
+import org.ssssssss.magicapi.model.Constants;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.Principal;
+import java.util.Optional;
+
+public class WebUtils {
+
+	public static Optional<HttpServletRequest> getRequest(){
+		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+		if (requestAttributes instanceof ServletRequestAttributes) {
+			return Optional.of(((ServletRequestAttributes) requestAttributes).getRequest());
+		}
+		return Optional.empty();
+	}
+
+	public static String currentUserName(){
+		Optional<HttpServletRequest> request = getRequest();
+		return request.map(r -> (MagicUser)r.getAttribute(Constants.ATTRIBUTE_MAGIC_USER))
+				.map(MagicUser::getUsername)
+				.orElseGet(() -> request.map(HttpServletRequest::getUserPrincipal)
+						.map(Principal::getName)
+						.orElse(null)
+				);
+	}
+}

+ 2 - 1
magic-editor/src/console/src/components/editor/magic-history.vue

@@ -55,6 +55,7 @@ export default {
             timestamp: item.timestamp,
           })
           .success((info) => {
+            info = JSON.parse(info.content)
             this.originalModel = monaco.editor.createModel(info.script, 'magicscript');
             this.diffEditor.setModel({
               original: this.originalModel,
@@ -70,7 +71,7 @@ export default {
       this.scriptModel = monaco.editor.createModel(this.scriptEditor.getValue(), 'magicscript')
       this.originalModel = this.scriptModel;
       this.timestampes = timestampes.map((t) => {
-        return {id: item.id, timestamp: t, dateTime: formatDate(Number(t))}
+        return {id: item.id, timestamp: t.createDate, dateTime: formatDate(t.createDate)}
       })
       if (this.timestampes.length > 0) {
         this.open(this.timestampes[0])

+ 1 - 1
magic-editor/src/console/src/components/editor/magic-script-editor.vue

@@ -36,7 +36,7 @@
       </div>
     </div>
 
-    <magic-dialog :moveable="false" :title="'历史记录:' + (info && info.name)" :value="showHsitoryDialog"
+    <magic-dialog :title="'历史记录:' + (info && info.name)" :value="showHsitoryDialog"
                   align="right" height="750px" maxWidth="inherit" padding="none" width="80%"
                   @onClose="showHsitoryDialog = false">
       <template #content>