未验证 提交 f8f37066 编写于 作者: W wxq 提交者: GitHub

Multilple configs export

上级 0341e5c8
package com.ctrip.framework.apollo.portal.controller;
import com.ctrip.framework.apollo.common.dto.NamespaceDTO;
import com.ctrip.framework.apollo.common.exception.BadRequestException;
import com.ctrip.framework.apollo.common.exception.ServiceException;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO;
import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel;
import com.ctrip.framework.apollo.portal.service.ItemService;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.ctrip.framework.apollo.portal.service.ConfigsExportService;
import com.ctrip.framework.apollo.portal.service.NamespaceService;
import com.ctrip.framework.apollo.portal.util.ConfigToFileUtils;
import com.ctrip.framework.apollo.portal.util.NamespaceBOUtils;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* jian.tan
......@@ -35,57 +31,31 @@ import java.util.stream.Collectors;
@RestController
public class ConfigsExportController {
private final ItemService configService;
private static final Logger logger = LoggerFactory.getLogger(ConfigsExportController.class);
private final ConfigsExportService configsExportService;
private final NamespaceService namespaceService;
public ConfigsExportController(
final ItemService configService,
final @Lazy NamespaceService namespaceService) {
this.configService = configService;
final ConfigsExportService configsExportService,
final @Lazy NamespaceService namespaceService
) {
this.configsExportService = configsExportService;
this.namespaceService = namespaceService;
}
@PreAuthorize(value = "@permissionValidator.hasModifyNamespacePermission(#appId, #namespaceName, #env)")
@PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/import")
public void importConfigFile(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName, @PathVariable String namespaceName,
@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BadRequestException("The file is empty.");
}
NamespaceDTO namespaceDTO = namespaceService
.loadNamespaceBaseInfo(appId, Env.fromString(env), clusterName, namespaceName);
if (Objects.isNull(namespaceDTO)) {
throw new BadRequestException(String.format("Namespace: %s not exist.", namespaceName));
}
NamespaceTextModel model = new NamespaceTextModel();
List<String> fileNameSplit = Splitter.on(".").splitToList(file.getOriginalFilename());
if (fileNameSplit.size() <= 1) {
throw new BadRequestException("The file format is invalid.");
}
String format = fileNameSplit.get(fileNameSplit.size() - 1);
model.setFormat(format);
model.setAppId(appId);
model.setEnv(env);
model.setClusterName(clusterName);
model.setNamespaceName(namespaceName);
model.setNamespaceId(namespaceDTO.getId());
String configText;
try(InputStream in = file.getInputStream()){
configText = ConfigToFileUtils.fileToString(in);
}catch (IOException e) {
throw new ServiceException("Read config file errors:{}", e);
}
model.setConfigText(configText);
configService.updateConfigItemByText(model);
}
/**
* export one config as file.
* keep compatibility.
* file name examples:
* <pre>
* application.properties
* application.yml
* application.json
* </pre>
*/
@PreAuthorize(value = "!@permissionValidator.shouldHideConfigToCurrentUser(#appId, #env, #namespaceName)")
@GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/export")
public void exportItems(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName, @PathVariable String namespaceName,
......@@ -94,30 +64,38 @@ public class ConfigsExportController {
String fileName = fileNameSplit.size() <= 1 ? Joiner.on(".")
.join(namespaceName, ConfigFileFormat.Properties.getValue()) : namespaceName;
NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(appId, Env.fromString
NamespaceBO namespaceBO = namespaceService.loadNamespaceBO(appId, Env.valueOf
(env), clusterName, namespaceName);
//generate a file.
res.setHeader("Content-Disposition", "attachment;filename=" + fileName);
List<String> fileItems = namespaceBO.getItems().stream().map(itemBO -> {
String key = itemBO.getItem().getKey();
String value = itemBO.getItem().getValue();
if (ConfigConsts.CONFIG_FILE_CONTENT_KEY.equals(key)) {
return value;
}
if ("".equals(key)) {
return Joiner.on("").join(itemBO.getItem().getKey(), itemBO.getItem().getValue());
}
return Joiner.on(" = ").join(itemBO.getItem().getKey(), itemBO.getItem().getValue());
}).collect(Collectors.toList());
res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName);
// file content
final String configFileContent = NamespaceBOUtils.convert2configFileContent(namespaceBO);
try {
ConfigToFileUtils.itemsToFile(res.getOutputStream(), fileItems);
// write content to net
res.getOutputStream().write(configFileContent.getBytes());
} catch (Exception e) {
throw new ServiceException("export items failed:{}", e);
}
}
/**
* Export all configs in a compressed file.
* Just export namespace which current exists read permission.
* The permission check in service.
*/
@GetMapping("/export")
public void exportAll(HttpServletRequest request, HttpServletResponse response) throws IOException {
// filename must contain the information of time
final String filename = "apollo_config_export_" + DateFormatUtils.format(new Date(), "yyyy_MMdd_HH_mm_ss") + ".zip";
// log who download the configs
logger.info("Download configs, remote addr [{}], remote host [{}]. Filename is [{}]", request.getRemoteAddr(), request.getRemoteHost(), filename);
// set downloaded filename
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
try (OutputStream outputStream = response.getOutputStream()) {
configsExportService.exportAllTo(outputStream);
}
}
}
package com.ctrip.framework.apollo.portal.controller;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.portal.service.ConfigsImportService;
import com.ctrip.framework.apollo.portal.util.ConfigFileUtils;
import java.io.IOException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* Import the configs from file.
* First version: move code from {@link ConfigsExportController}
* @author wxq
*/
@RestController
public class ConfigsImportController {
private final ConfigsImportService configsImportService;
public ConfigsImportController(
final ConfigsImportService configsImportService
) {
this.configsImportService = configsImportService;
}
/**
* copy from old {@link ConfigsExportController}.
* @param file Yml file's name must ends with {@code .yml}.
* Properties file's name must ends with {@code .properties}.
* etc.
* @throws IOException
*/
@PreAuthorize(value = "@permissionValidator.hasModifyNamespacePermission(#appId, #namespaceName, #env)")
@PostMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/import")
public void importConfigFile(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName, @PathVariable String namespaceName,
@RequestParam("file") MultipartFile file) throws IOException {
// check file
ConfigFileUtils.check(file);
final String format = ConfigFileUtils.getFormat(file.getOriginalFilename());
final String standardFilename = ConfigFileUtils.toFilename(appId, clusterName, namespaceName, ConfigFileFormat.fromString(format));
configsImportService.importOneConfigFromFile(env, standardFilename, file.getInputStream());
}
}
package com.ctrip.framework.apollo.portal.entity.bo;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.ctrip.framework.apollo.portal.util.NamespaceBOUtils;
/**
* a namespace represent.
* @author wxq
*/
public class ConfigBO {
private final Env env;
private final String ownerName;
private final String appId;
private final String clusterName;
private final String namespace;
private final String configFileContent;
private final ConfigFileFormat format;
public ConfigBO(Env env, String ownerName, String appId, String clusterName,
String namespace, String configFileContent, ConfigFileFormat format) {
this.env = env;
this.ownerName = ownerName;
this.appId = appId;
this.clusterName = clusterName;
this.namespace = namespace;
this.configFileContent = configFileContent;
this.format = format;
}
public ConfigBO(Env env, String ownerName, String appId, String clusterName, NamespaceBO namespaceBO) {
this(env, ownerName, appId, clusterName,
namespaceBO.getBaseInfo().getNamespaceName(),
NamespaceBOUtils.convert2configFileContent(namespaceBO),
ConfigFileFormat.fromString(namespaceBO.getFormat())
);
}
@Override
public String toString() {
return "ConfigBO{" +
"env=" + env +
", ownerName='" + ownerName + '\'' +
", appId='" + appId + '\'' +
", clusterName='" + clusterName + '\'' +
", namespace='" + namespace + '\'' +
", configFileContent='" + configFileContent + '\'' +
", format=" + format +
'}';
}
public Env getEnv() {
return env;
}
public String getOwnerName() {
return ownerName;
}
public String getAppId() {
return appId;
}
public String getClusterName() {
return clusterName;
}
public String getNamespace() {
return namespace;
}
public String getConfigFileContent() {
return configFileContent;
}
public ConfigFileFormat getFormat() {
return format;
}
}
package com.ctrip.framework.apollo.portal.service;
import com.ctrip.framework.apollo.common.dto.ClusterDTO;
import com.ctrip.framework.apollo.common.entity.App;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.portal.component.PermissionValidator;
import com.ctrip.framework.apollo.portal.component.PortalSettings;
import com.ctrip.framework.apollo.portal.entity.bo.ConfigBO;
import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.ctrip.framework.apollo.portal.util.ConfigFileUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
public class ConfigsExportService {
private static final Logger logger = LoggerFactory.getLogger(ConfigsExportService.class);
private final AppService appService;
private final ClusterService clusterService;
private final NamespaceService namespaceService;
private final PortalSettings portalSettings;
private final PermissionValidator permissionValidator;
public ConfigsExportService(
AppService appService,
ClusterService clusterService,
final @Lazy NamespaceService namespaceService,
PortalSettings portalSettings,
PermissionValidator permissionValidator) {
this.appService = appService;
this.clusterService = clusterService;
this.namespaceService = namespaceService;
this.portalSettings = portalSettings;
this.permissionValidator = permissionValidator;
}
/**
* write multiple namespace to a zip. use {@link Stream#reduce(Object, BiFunction,
* BinaryOperator)} to forbid concurrent write.
*
* @param configBOStream namespace's stream
* @param outputStream receive zip file output stream
* @throws IOException if happen write problem
*/
private static void writeAsZipOutputStream(
Stream<ConfigBO> configBOStream, OutputStream outputStream) throws IOException {
try (final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
final Consumer<ConfigBO> configBOConsumer =
configBO -> {
try {
// TODO, Stream.reduce will cause some problems. Is There other way to speed up the
// downloading?
synchronized (zipOutputStream) {
write2ZipOutputStream(zipOutputStream, configBO);
}
} catch (IOException e) {
logger.error("Write error. {}", configBO);
throw new IllegalStateException(e);
}
};
configBOStream.forEach(configBOConsumer);
}
}
/**
* write {@link ConfigBO} as file to {@link ZipOutputStream}. Watch out the concurrent problem!
* zip output stream is same like cannot write concurrently! the name of file is determined by
* {@link ConfigFileUtils#toFilename(String, String, String, ConfigFileFormat)}. the path of file
* is determined by {@link ConfigFileUtils#toFilePath(String, String, Env, String)}.
*
* @param zipOutputStream zip file output stream
* @param configBO a namespace represent
* @return zip file output stream same as parameter zipOutputStream
*/
private static ZipOutputStream write2ZipOutputStream(
final ZipOutputStream zipOutputStream, final ConfigBO configBO) throws IOException {
final Env env = configBO.getEnv();
final String ownerName = configBO.getOwnerName();
final String appId = configBO.getAppId();
final String clusterName = configBO.getClusterName();
final String namespace = configBO.getNamespace();
final String configFileContent = configBO.getConfigFileContent();
final ConfigFileFormat configFileFormat = configBO.getFormat();
final String configFilename =
ConfigFileUtils.toFilename(appId, clusterName, namespace, configFileFormat);
final String filePath = ConfigFileUtils.toFilePath(ownerName, appId, env, configFilename);
final ZipEntry zipEntry = new ZipEntry(filePath);
try {
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(configFileContent.getBytes());
zipOutputStream.closeEntry();
} catch (IOException e) {
logger.error("config export failed. {}", configBO);
throw new IOException("config export failed", e);
}
return zipOutputStream;
}
/** @return the namespaces current user exists */
private Stream<ConfigBO> makeStreamBy(
final Env env, final String ownerName, final String appId, final String clusterName) {
final List<NamespaceBO> namespaceBOS =
namespaceService.findNamespaceBOs(appId, env, clusterName);
final Function<NamespaceBO, ConfigBO> function =
namespaceBO -> new ConfigBO(env, ownerName, appId, clusterName, namespaceBO);
return namespaceBOS.parallelStream().map(function);
}
private Stream<ConfigBO> makeStreamBy(final Env env, final String ownerName, final String appId) {
final List<ClusterDTO> clusterDTOS = clusterService.findClusters(env, appId);
final Function<ClusterDTO, Stream<ConfigBO>> function =
clusterDTO -> this.makeStreamBy(env, ownerName, appId, clusterDTO.getName());
return clusterDTOS.parallelStream().flatMap(function);
}
private Stream<ConfigBO> makeStreamBy(final Env env, final List<App> apps) {
final Function<App, Stream<ConfigBO>> function =
app -> this.makeStreamBy(env, app.getOwnerName(), app.getAppId());
return apps.parallelStream().flatMap(function);
}
private Stream<ConfigBO> makeStreamBy(final Collection<Env> envs) {
// get all apps
final List<App> apps = appService.findAll();
// permission check
final Predicate<App> isAppAdmin =
app -> {
try {
return permissionValidator.isAppAdmin(app.getAppId());
} catch (Exception e) {
logger.error("app = {}", app);
logger.error(app.getAppId());
}
return false;
};
// app admin permission filter
final List<App> appsExistPermission =
apps.stream().filter(isAppAdmin).collect(Collectors.toList());
return envs.parallelStream().flatMap(env -> this.makeStreamBy(env, appsExistPermission));
}
/**
* Export all projects which current user own them. Permission check by {@link
* PermissionValidator#isAppAdmin(java.lang.String)}
*
* @param outputStream network file download stream to user
* @throws IOException if happen write problem
*/
public void exportAllTo(OutputStream outputStream) throws IOException {
final List<Env> activeEnvs = portalSettings.getActiveEnvs();
final Stream<ConfigBO> configBOStream = this.makeStreamBy(activeEnvs);
writeAsZipOutputStream(configBOStream, outputStream);
}
}
package com.ctrip.framework.apollo.portal.service;
import com.ctrip.framework.apollo.common.dto.NamespaceDTO;
import com.ctrip.framework.apollo.common.exception.ServiceException;
import com.ctrip.framework.apollo.portal.component.PermissionValidator;
import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.ctrip.framework.apollo.portal.util.ConfigFileUtils;
import com.ctrip.framework.apollo.portal.util.ConfigToFileUtils;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessControlException;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
/**
* @author wxq
*/
@Service
public class ConfigsImportService {
private final ItemService itemService;
private final NamespaceService namespaceService;
private final PermissionValidator permissionValidator;
public ConfigsImportService(
final ItemService itemService,
final @Lazy NamespaceService namespaceService,
PermissionValidator permissionValidator) {
this.itemService = itemService;
this.namespaceService = namespaceService;
this.permissionValidator = permissionValidator;
}
/**
* move from {@link com.ctrip.framework.apollo.portal.controller.ConfigsImportController}
*/
private void importConfig(
final String appId,
final String env,
final String clusterName,
final String namespaceName,
final long namespaceId,
final String format,
final String configText
) {
final NamespaceTextModel model = new NamespaceTextModel();
model.setAppId(appId);
model.setEnv(env);
model.setClusterName(clusterName);
model.setNamespaceName(namespaceName);
model.setNamespaceId(namespaceId);
model.setFormat(format);
model.setConfigText(configText);
itemService.updateConfigItemByText(model);
}
/**
* import one config from file
*/
private void importOneConfigFromFile(
final String appId,
final String env,
final String clusterName,
final String namespaceName,
final String configText,
final String format
) {
final NamespaceDTO namespaceDTO = namespaceService
.loadNamespaceBaseInfo(appId, Env.valueOf(env), clusterName, namespaceName);
this.importConfig(appId, env, clusterName, namespaceName, namespaceDTO.getId(), format, configText);
}
/**
* import a config file.
* the name of config file must be special like
* appId+cluster+namespace.format
* Example:
* <pre>
* 123456+default+application.properties (appId is 123456, cluster is default, namespace is application, format is properties)
* 654321+north+password.yml (appId is 654321, cluster is north, namespace is password, format is yml)
* </pre>
* so we can get the information of appId, cluster, namespace, format from the file name.
* @param env environment
* @param standardFilename appId+cluster+namespace.format
* @param configText config content
*/
private void importOneConfigFromText(
final String env,
final String standardFilename,
final String configText
) {
final String appId = ConfigFileUtils.getAppId(standardFilename);
final String clusterName = ConfigFileUtils.getClusterName(standardFilename);
final String namespace = ConfigFileUtils.getNamespace(standardFilename);
final String format = ConfigFileUtils.getFormat(standardFilename);
this.importOneConfigFromFile(appId, env, clusterName, namespace, configText, format);
}
/**
* @see ConfigsImportService#importOneConfigFromText(java.lang.String, java.lang.String, java.lang.String)
* @throws AccessControlException if has no modify namespace permission
*/
public void importOneConfigFromFile(
final String env,
final String standardFilename,
final InputStream inputStream
) {
final String configText;
try(InputStream in = inputStream) {
configText = ConfigToFileUtils.fileToString(in);
} catch (IOException e) {
throw new ServiceException("Read config file errors:{}", e);
}
this.importOneConfigFromText(env, standardFilename, configText);
}
}
......@@ -122,7 +122,7 @@ public class NamespaceService {
String namespaceName) {
NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName);
if (namespace == null) {
throw new BadRequestException("namespaces not exist");
throw new BadRequestException(String.format("Namespace: %s not exist.", namespaceName));
}
return namespace;
}
......@@ -256,20 +256,24 @@ public class NamespaceService {
private void fillAppNamespaceProperties(NamespaceBO namespace) {
NamespaceDTO namespaceDTO = namespace.getBaseInfo();
final NamespaceDTO namespaceDTO = namespace.getBaseInfo();
final String appId = namespaceDTO.getAppId();
final String clusterName = namespaceDTO.getClusterName();
final String namespaceName = namespaceDTO.getNamespaceName();
//先从当前appId下面找,包含私有的和公共的
AppNamespace appNamespace =
appNamespaceService
.findByAppIdAndName(namespaceDTO.getAppId(), namespaceDTO.getNamespaceName());
.findByAppIdAndName(appId, namespaceName);
//再从公共的app namespace里面找
if (appNamespace == null) {
appNamespace = appNamespaceService.findPublicAppNamespace(namespaceDTO.getNamespaceName());
appNamespace = appNamespaceService.findPublicAppNamespace(namespaceName);
}
String format;
boolean isPublic;
final String format;
final boolean isPublic;
if (appNamespace == null) {
//dirty data
logger.warn("Dirty data, cannot find appNamespace by namespaceName [{}], appId = {}, cluster = {}, set it format to {}, make public", namespaceName, appId, clusterName, ConfigFileFormat.Properties.getValue());
format = ConfigFileFormat.Properties.getValue();
isPublic = true; // set to true, because public namespace allowed to delete by user
} else {
......
package com.ctrip.framework.apollo.portal.util;
import com.ctrip.framework.apollo.common.exception.BadRequestException;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.portal.controller.ConfigsImportController;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.google.common.base.Splitter;
import java.io.File;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
/**
* First version: move from {@link ConfigsImportController#importConfigFile(java.lang.String, java.lang.String, java.lang.String, java.lang.String, org.springframework.web.multipart.MultipartFile)}
* @author wxq
*/
public class ConfigFileUtils {
public static void check(MultipartFile file) {
checkEmpty(file);
final String originalFilename = file.getOriginalFilename();
checkFormat(originalFilename);
}
/**
* @throws BadRequestException if file is empty
*/
static void checkEmpty(MultipartFile file) {
if (file.isEmpty()) {
throw new BadRequestException("The file is empty. " + file.getOriginalFilename());
}
}
/**
* @throws BadRequestException if file's format is invalid
*/
static void checkFormat(final String originalFilename) {
final List<String> fileNameSplit = Splitter.on(".").splitToList(originalFilename);
if (fileNameSplit.size() <= 1) {
throw new BadRequestException("The file format is invalid.");
}
for (String s : fileNameSplit) {
if (StringUtils.isEmpty(s)) {
throw new BadRequestException("The file format is invalid.");
}
}
}
static String[] getThreePart(final String originalFilename) {
return originalFilename.split("[+]");
}
/**
* @throws BadRequestException if file's name cannot divide to 3 parts by "+" symbol
*/
static void checkThreePart(final String originalFilename) {
String[] parts = getThreePart(originalFilename);
if (3 != parts.length) {
throw new BadRequestException("file name [" + originalFilename + "] not valid");
}
}
/**
* <pre>
* "application+default+application.properties" -> "properties"
* "application+default+application.yml" -> "yml"
* </pre>
* @throws BadRequestException if file's format is invalid
*/
public static String getFormat(final String originalFilename) {
final List<String> fileNameSplit = Splitter.on(".").splitToList(originalFilename);
if (fileNameSplit.size() <= 1) {
throw new BadRequestException("The file format is invalid.");
}
return fileNameSplit.get(fileNameSplit.size() - 1);
}
/**
* <pre>
* "123+default+application.properties" -> "123"
* "abc+default+application.yml" -> "abc"
* "666+default+application.json" -> "666"
* </pre>
* @throws BadRequestException if file's name is invalid
*/
public static String getAppId(final String originalFilename) {
checkThreePart(originalFilename);
return getThreePart(originalFilename)[0];
}
public static String getClusterName(final String originalFilename) {
checkThreePart(originalFilename);
return getThreePart(originalFilename)[1];
}
/**
* <pre>
* "application+default+application.properties" -> "application"
* "application+default+application.yml" -> "application.yml"
* "application+default+application.json" -> "application.json"
* "application+default+application.333.yml" -> "application.333.yml"
* </pre>
* @throws BadRequestException if file's name is invalid
*/
public static String getNamespace(final String originalFilename) {
checkThreePart(originalFilename);
final String[] threeParts = getThreePart(originalFilename);
final String suffix = threeParts[2];
if (!suffix.contains(".")) {
throw new BadRequestException(originalFilename + " namespace and format is invalid!");
}
final int lastDotIndex = suffix.lastIndexOf(".");
final String namespace = suffix.substring(0, lastDotIndex);
// format after last character '.'
final String format = suffix.substring(lastDotIndex + 1);
if (!ConfigFileFormat.isValidFormat(format)) {
throw new BadRequestException(originalFilename + " format is invalid!");
}
ConfigFileFormat configFileFormat = ConfigFileFormat.fromString(format);
if (configFileFormat.equals(ConfigFileFormat.Properties)) {
return namespace;
} else {
// compatibility of other format
return namespace + "." + format;
}
}
/**
* <pre>
* appId cluster namespace return
* 666 default application 666+default+application.properties
* 123 none action.yml 123+none+action.yml
* </pre>
*/
public static String toFilename(
final String appId,
final String clusterName,
final String namespace,
final ConfigFileFormat configFileFormat
) {
final String suffix;
if (ConfigFileFormat.Properties.equals(configFileFormat)) {
suffix = "." + ConfigFileFormat.Properties.getValue();
} else {
suffix = "";
}
return appId + "+" + clusterName + "+" + namespace + suffix;
}
/**
* file path = ownerName/appId/env/configFilename
* @return file path in compressed file
*/
public static String toFilePath(
final String ownerName,
final String appId,
final Env env,
final String configFilename
) {
return String.join(File.separator, ownerName, appId, env.getName(), configFilename);
}
}
......@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
*/
public class ConfigToFileUtils {
@Deprecated
public static void itemsToFile(OutputStream os, List<String> items) {
try {
PrintWriter printWriter = new PrintWriter(os);
......
package com.ctrip.framework.apollo.portal.util;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.core.utils.PropertiesUtil;
import com.ctrip.framework.apollo.portal.controller.ConfigsExportController;
import com.ctrip.framework.apollo.portal.entity.bo.ItemBO;
import com.ctrip.framework.apollo.portal.entity.bo.NamespaceBO;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
/**
* @author wxq
*/
public class NamespaceBOUtils {
/**
* namespace must not be {@link ConfigFileFormat#Properties}.
* the content of namespace in item's value which item's key is {@link ConfigConsts#CONFIG_FILE_CONTENT_KEY}.
* @param namespaceBO namespace
* @return content of non-properties's namespace
*/
static String convertNonProperties2configFileContent(NamespaceBO namespaceBO) {
List<ItemBO> itemBOS = namespaceBO.getItems();
for (ItemBO itemBO : itemBOS) {
String key = itemBO.getItem().getKey();
// special namespace format(not properties)
if (ConfigConsts.CONFIG_FILE_CONTENT_KEY.equals(key)) {
return itemBO.getItem().getValue();
}
}
// If there is no items?
// return empty string ""
return "";
}
/**
* copy from old {@link ConfigsExportController}.
* convert {@link NamespaceBO} to a file content.
* @return content of config file
* @throws IllegalStateException if convert properties to string fail
*/
public static String convert2configFileContent(NamespaceBO namespaceBO) {
// early return if it is not a properties format namespace
if (!ConfigFileFormat.Properties.equals(ConfigFileFormat.fromString(namespaceBO.getFormat()))) {
// it is not a properties namespace
return convertNonProperties2configFileContent(namespaceBO);
}
// it must be a properties format namespace
List<ItemBO> itemBOS = namespaceBO.getItems();
// save the kev value pair
Properties properties = new Properties();
for (ItemBO itemBO : itemBOS) {
String key = itemBO.getItem().getKey();
String value = itemBO.getItem().getValue();
// ignore comment, so the comment will lack
properties.put(key, value);
}
// use a special method convert properties to string
final String configFileContent;
try {
configFileContent = PropertiesUtil.toString(properties);
} catch (IOException e) {
throw new IllegalStateException("convert properties to string fail.", e);
}
return configFileContent;
}
}
<!doctype html>
<html ng-app="config_export">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="img/config.png">
<!-- styles -->
<link rel="stylesheet" type="text/css" href="vendor/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="vendor/angular/angular-toastr-1.4.1.min.css">
<link rel="stylesheet" type="text/css" href="vendor/select2/select2.min.css">
<link rel="stylesheet" type="text/css" media='all' href="vendor/angular/loading-bar.min.css">
<link rel="stylesheet" type="text/css" href="styles/common-style.css">
<title>{{'ConfigExport.Title' | translate }}</title>
</head>
<body>
<apollonav></apollonav>
<div class="container-fluid">
<div class="col-md-8 col-md-offset-2 panel">
<section class="panel-body">
<div class="row">
<header class="panel-heading">
{{'ConfigExport.Title' | translate }}
<small>
{{'ConfigExport.TitleTips' | translate}}
</small>
</header>
<div class="col-sm-offset-2 col-sm-9">
<a href="export" target="_blank">
<button class="btn btn-block btn-lg btn-primary">
{{'ConfigExport.Download' | translate }}
</button>
</a>
</div>
</div>
</section>
</div>
</div>
<div ng-include="'views/common/footer.html'"></div>
<!-- jquery.js -->
<script src="vendor/jquery.min.js" type="text/javascript"></script>
<!--angular-->
<script src="vendor/angular/angular.min.js"></script>
<script src="vendor/angular/angular-route.min.js"></script>
<script src="vendor/angular/angular-resource.min.js"></script>
<script src="vendor/angular/angular-toastr-1.4.1.tpls.min.js"></script>
<script src="vendor/angular/loading-bar.min.js"></script>
<script src="vendor/angular/angular-cookies.min.js"></script>
<script src="vendor/angular/angular-translate.2.18.1/angular-translate.min.js"></script>
<script src="vendor/angular/angular-translate.2.18.1/angular-translate-loader-static-files.min.js"></script>
<script src="vendor/angular/angular-translate.2.18.1/angular-translate-storage-cookie.min.js"></script>
<!--valdr-->
<script src="vendor/valdr/valdr.min.js" type="text/javascript"></script>
<script src="vendor/valdr/valdr-message.min.js" type="text/javascript"></script>
<!-- bootstrap.js -->
<script src="vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="vendor/lodash.min.js"></script>
<script src="vendor/select2/select2.min.js" type="text/javascript"></script>
<!--biz-->
<!--must import-->
<script type="application/javascript" src="scripts/app.js"></script>
<script type="application/javascript" src="scripts/services/AppService.js"></script>
<script type="application/javascript" src="scripts/services/EnvService.js"></script>
<script type="application/javascript" src="scripts/services/UserService.js"></script>
<script type="application/javascript" src="scripts/services/CommonService.js"></script>
<script type="application/javascript" src="scripts/services/PermissionService.js"></script>
<script type="application/javascript" src="scripts/services/ClusterService.js"></script>
<script type="application/javascript" src="scripts/services/NamespaceService.js"></script>
<script type="application/javascript" src="scripts/services/SystemInfoService.js"></script>
<script type="application/javascript" src="scripts/AppUtils.js"></script>
<script type="application/javascript" src="scripts/PageCommon.js"></script>
<script type="application/javascript" src="scripts/directive/directive.js"></script>
<script type="application/javascript" src="scripts/valdr.js"></script>
<script type="application/javascript" src="scripts/AppUtils.js"></script>
<script type="application/javascript" src="scripts/services/OrganizationService.js"></script>
</body>
</html>
\ No newline at end of file
......@@ -6,12 +6,14 @@
"Common.Nav.HideNavBar": "Hide navigation bar",
"Common.Nav.Help": "Help",
"Common.Nav.AdminTools": "Admin Tools",
"Common.Nav.NonAdminTools": "Tools",
"Common.Nav.UserManage": "User Management",
"Common.Nav.SystemRoleManage": "System Permission Management",
"Common.Nav.OpenMange": "Open Platform Authorization Management",
"Common.Nav.SystemConfig": "System Configuration",
"Common.Nav.DeleteApp-Cluster-Namespace": "Delete Apps, Clusters, AppNamespace",
"Common.Nav.SystemInfo": "System Information",
"Common.Nav.ConfigExport": "Config Export",
"Common.Nav.Logout": "Logout",
"Common.Department": "Department",
"Common.Cluster": "Cluster",
......@@ -681,6 +683,9 @@
"Config.Diff.DiffCluster": "Clusters to be compared",
"Config.Diff.HasDiffComment": "Whether to compare comments or not",
"Config.Diff.PleaseChooseTwoCluster": "Please select at least two clusters",
"ConfigExport.Title": "Config Export",
"ConfigExport.TitleTips" : "Super administrators will download the configuration of all projects, normal users will only download the configuration of their own projects",
"ConfigExport.Download": "Download",
"App.CreateProject": "Create Project",
"App.AppIdTips": "(Application's unique identifiers)",
"App.AppNameTips": "(Suggested format xx-yy-zz e.g. apollo-server)",
......
......@@ -6,12 +6,14 @@
"Common.Nav.HideNavBar": "隐藏导航栏",
"Common.Nav.Help": "帮助",
"Common.Nav.AdminTools": "管理员工具",
"Common.Nav.NonAdminTools": "工具",
"Common.Nav.UserManage": "用户管理",
"Common.Nav.SystemRoleManage": "系统权限管理",
"Common.Nav.OpenMange": "开放平台授权管理",
"Common.Nav.SystemConfig": "系统参数",
"Common.Nav.DeleteApp-Cluster-Namespace": "删除应用、集群、AppNamespace",
"Common.Nav.SystemInfo": "系统信息",
"Common.Nav.ConfigExport": "配置导出",
"Common.Nav.Logout": "退出",
"Common.Department": "部门",
"Common.Cluster": "集群",
......@@ -681,6 +683,9 @@
"Config.Diff.DiffCluster": "要比较的集群",
"Config.Diff.HasDiffComment": "是否比较注释",
"Config.Diff.PleaseChooseTwoCluster": "请至少选择两个集群",
"ConfigExport.Title": "配置导出",
"ConfigExport.TitleTips" : "超级管理员会下载所有项目的配置,普通用户只会下载自己项目的配置",
"ConfigExport.Download": "下载",
"App.CreateProject": "创建项目",
"App.AppIdTips": "(应用唯一标识)",
"App.AppNameTips": "(建议格式 xx-yy-zz 例:apollo-server)",
......
......@@ -68,3 +68,5 @@ var delete_app_cluster_namespace_module = angular.module('delete_app_cluster_nam
var system_info_module = angular.module('system_info', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
//access secretKey
var access_key_module = angular.module('access_key', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
//config export
var config_export_module = angular.module('config_export', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
\ No newline at end of file
......@@ -34,19 +34,33 @@
<li value="zh-CN"><a href="javascript:void(0)" ng-click="changeLanguage('zh-CN')">简体中文</a></li>
</ul>
</li>
<li class="dropdown" ng-if="hasRootPermission">
<!-- admin tool -->
<li class="dropdown" ng-if="hasRootPermission == true">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-cog"></span>&nbsp;{{'Common.Nav.AdminTools' | translate }}
<span class="caret"></span></a>
<ul class="dropdown-menu">
<ul class="dropdown-menu" >
<li><a ng-href="{{ '/user-manage.html' | prefixPath }}" target="_blank">{{'Common.Nav.UserManage' | translate }}</a></li>
<li><a href="{{ '/system-role-manage.html' | prefixPath }}" target="_blank">{{'Common.Nav.SystemRoleManage' | translate }}</a></li>
<li><a href="{{ '/open/manage.html' | prefixPath }}" target="_blank">{{'Common.Nav.OpenMange' | translate }}</a></li>
<li><a href="{{ '/server_config.html' | prefixPath }}" target="_blank">{{'Common.Nav.SystemConfig' | translate }}</a></li>
<li><a href="{{ '/delete_app_cluster_namespace.html' | prefixPath }}" target="_blank">{{'Common.Nav.DeleteApp-Cluster-Namespace' | translate }}</a></li>
<li><a href="{{ '/system_info.html' | prefixPath }}" target="_blank">{{'Common.Nav.SystemInfo' | translate }}</a></li>
<li><a href="{{ '/config_export.html' | prefixPath }}" target="_blank">{{'Common.Nav.ConfigExport' | translate }}</a></li>
</ul>
</li>
<!-- normal user tool (not admin)-->
<li class="dropdown" ng-if="hasRootPermission == false">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-cog"></span>&nbsp;{{'Common.Nav.NonAdminTools' | translate }}
<span class="caret"></span></a>
<ul class="dropdown-menu" >
<li><a href="{{ '/config_export.html' | prefixPath }}" target="_blank">{{'Common.Nav.ConfigExport' | translate }}</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-user"></span>&nbsp;{{userName}}
......
package com.ctrip.framework.apollo.portal.util;
import static org.junit.Assert.*;
import com.ctrip.framework.apollo.common.exception.BadRequestException;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConfigFileUtilsTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Test
public void checkFormat() {
ConfigFileUtils.checkFormat("1234+default+app.properties");
ConfigFileUtils.checkFormat("1234+default+app.yml");
ConfigFileUtils.checkFormat("1234+default+app.json");
}
@Test(expected = BadRequestException.class)
public void checkFormatWithException0() {
ConfigFileUtils.checkFormat("1234+defaultes");
}
@Test(expected = BadRequestException.class)
public void checkFormatWithException1() {
ConfigFileUtils.checkFormat(".json");
}
@Test(expected = BadRequestException.class)
public void checkFormatWithException2() {
ConfigFileUtils.checkFormat("application.");
}
@Test
public void getFormat() {
final String properties = ConfigFileUtils.getFormat("application+default+application.properties");
assertEquals("properties", properties);
final String yml = ConfigFileUtils.getFormat("application+default+application.yml");
assertEquals("yml", yml);
}
@Test
public void getAppId() {
final String application = ConfigFileUtils.getAppId("application+default+application.properties");
assertEquals("application", application);
final String abc = ConfigFileUtils.getAppId("abc+default+application.yml");
assertEquals("abc", abc);
}
@Test
public void getClusterName() {
final String cluster = ConfigFileUtils.getClusterName("application+default+application.properties");
assertEquals("default", cluster);
final String Beijing = ConfigFileUtils.getClusterName("abc+Beijing+application.yml");
assertEquals("Beijing", Beijing);
}
@Test
public void getNamespace() {
final String application = ConfigFileUtils.getNamespace("234+default+application.properties");
assertEquals("application", application);
final String applicationYml = ConfigFileUtils.getNamespace("abc+default+application.yml");
assertEquals("application.yml", applicationYml);
}
@Test
public void toFilename() {
final String propertiesFilename0 = ConfigFileUtils.toFilename("123", "default", "application", ConfigFileFormat.Properties);
logger.info("propertiesFilename0 {}", propertiesFilename0);
assertEquals("123+default+application.properties", propertiesFilename0);
final String ymlFilename0 = ConfigFileUtils.toFilename("666", "none", "cc.yml", ConfigFileFormat.YML);
logger.info("ymlFilename0 {}", ymlFilename0);
assertEquals("666+none+cc.yml", ymlFilename0);
}
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册