Complete halo admin updation feature

pull/199/head
johnniang 2019-06-13 00:07:10 +08:00
parent af42a5fdac
commit 000d1ff1f2
9 changed files with 183 additions and 31 deletions

View File

@ -31,6 +31,8 @@ import java.io.IOException;
import java.util.List;
import java.util.Properties;
import static run.halo.app.model.support.HaloConst.HALO_ADMIN_RELATIVE_PATH;
/**
* Mvc configuration.
*
@ -93,7 +95,7 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer {
registry.addResourceHandler("/backup/**")
.addResourceLocations(workDir + "backup/");
registry.addResourceHandler("/admin/**")
.addResourceLocations(workDir + "templates/admin/")
.addResourceLocations(workDir + HALO_ADMIN_RELATIVE_PATH)
.addResourceLocations("classpath:/admin/");
if (!haloProperties.isDocDisabled()) {
@ -122,7 +124,7 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer {
configurer.setDefaultEncoding("UTF-8");
Properties properties = new Properties();
properties.setProperty("auto_import","/common/macro/common_macro.ftl as common");
properties.setProperty("auto_import", "/common/macro/common_macro.ftl as common");
configurer.setFreemarkerSettings(properties);

View File

@ -67,4 +67,10 @@ public class AdminController {
return adminService.refreshToken(refreshToken);
}
@PutMapping("halo-admin")
@ApiOperation("Updates halo-admin manually")
public void updateAdmin() {
adminService.updateAdminAssets();
}
}

View File

@ -82,6 +82,20 @@ public class HaloConst {
*/
public static String USER_SESSION_KEY = "user_session";
/**
* Github Api url for halo-admin release.
*/
public final static String HALO_ADMIN_RELEASES_LATEST = "https://api.github.com/repos/halo-dev/halo-admin/releases/latest";
/**
* Halo admin version regex.
*/
public final static String HALO_ADMIN_VERSION_REGEX = "halo-admin-\\d+\\.\\d+(\\.\\d+)?(-\\S*)?\\.zip";
public final static String HALO_ADMIN_RELATIVE_PATH = "templates/admin/";
public final static String HALO_ADMIN_RELATIVE_BACKUP_PATH = "templates/admin-backup/";
static {
// Set version
HALO_VERSION = HaloConst.class.getPackage().getImplementationVersion();

View File

@ -64,4 +64,9 @@ public interface AdminService {
*/
@NonNull
AuthToken refreshToken(@NonNull String refreshToken);
/**
* Updates halo admin assets.
*/
void updateAdminAssets();
}

View File

@ -2,15 +2,17 @@ package run.halo.app.service.impl;
import cn.hutool.core.lang.Validator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.BadRequestException;
import run.halo.app.exception.NotFoundException;
import run.halo.app.exception.ServiceException;
import run.halo.app.model.dto.EnvironmentDTO;
import run.halo.app.model.dto.StatisticDTO;
import run.halo.app.model.entity.User;
@ -24,11 +26,20 @@ import run.halo.app.security.context.SecurityContextHolder;
import run.halo.app.security.token.AuthToken;
import run.halo.app.security.util.SecurityUtils;
import run.halo.app.service.*;
import run.halo.app.utils.FileUtils;
import run.halo.app.utils.HaloUtils;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import static run.halo.app.model.support.HaloConst.*;
/**
* Admin service implementation.
@ -61,7 +72,9 @@ public class AdminServiceImpl implements AdminService {
private final StringCacheStore cacheStore;
private final ApplicationEventPublisher eventPublisher;
private final RestTemplate restTemplate;
private final HaloProperties haloProperties;
private final String driverClassName;
@ -77,7 +90,8 @@ public class AdminServiceImpl implements AdminService {
UserService userService,
LinkService linkService,
StringCacheStore cacheStore,
ApplicationEventPublisher eventPublisher,
RestTemplate restTemplate,
HaloProperties haloProperties,
@Value("${spring.datasource.driver-class-name}") String driverClassName,
@Value("${spring.profiles.active:prod}") String mode) {
this.postService = postService;
@ -90,7 +104,8 @@ public class AdminServiceImpl implements AdminService {
this.userService = userService;
this.linkService = linkService;
this.cacheStore = cacheStore;
this.eventPublisher = eventPublisher;
this.restTemplate = restTemplate;
this.haloProperties = haloProperties;
this.driverClassName = driverClassName;
this.mode = mode;
}
@ -220,6 +235,99 @@ public class AdminServiceImpl implements AdminService {
return buildAuthToken(user);
}
@Override
@SuppressWarnings("unchecked")
public void updateAdminAssets() {
// Request github api
ResponseEntity<Map> responseEntity = restTemplate.getForEntity(HaloConst.HALO_ADMIN_RELEASES_LATEST, Map.class);
if (responseEntity == null ||
responseEntity.getStatusCode().isError() ||
responseEntity.getBody() == null) {
throw new ServiceException("Failed to request remote url: " + HALO_ADMIN_RELEASES_LATEST).setErrorData(HALO_ADMIN_RELEASES_LATEST);
}
Object assetsObject = responseEntity.getBody().get("assets");
if (assetsObject instanceof List) {
try {
List assets = (List) assetsObject;
Map assetMap = (Map) assets.stream()
.filter(assetPredicate())
.findFirst()
.orElseThrow(() -> new ServiceException("Halo admin 最新版暂无资源文件,请稍后再试"));
Object browserDownloadUrl = assetMap.getOrDefault("browser_download_url", "");
// Download the assets
ResponseEntity<byte[]> downloadResponseEntity = restTemplate.getForEntity(browserDownloadUrl.toString(), byte[].class);
if (downloadResponseEntity == null ||
downloadResponseEntity.getStatusCode().isError() ||
downloadResponseEntity.getBody() == null) {
throw new ServiceException("Failed to request remote url: " + browserDownloadUrl.toString()).setErrorData(browserDownloadUrl.toString());
}
String adminTargetName = haloProperties.getWorkDir() + HALO_ADMIN_RELATIVE_PATH;
Path adminPath = Paths.get(adminTargetName);
Path adminBackupPath = Paths.get(haloProperties.getWorkDir(), HALO_ADMIN_RELATIVE_BACKUP_PATH);
backupAndClearAdminAssetsIfPresent(adminPath, adminBackupPath);
// Create temp folder
Path assetTempPath = FileUtils.createTempDirectory()
.resolve(assetMap.getOrDefault("name", "halo-admin-latest.zip").toString());
// Unzip
FileUtils.unzip(downloadResponseEntity.getBody(), assetTempPath);
// Copy it to template/admin folder
FileUtils.copyFolder(FileUtils.tryToSkipZipParentFolder(assetTempPath), adminPath);
} catch (Throwable t) {
throw new ServiceException(t.getMessage(), t);
}
} else {
throw new ServiceException("Github response error").setErrorData(assetsObject);
}
}
@NonNull
@SuppressWarnings("unchecked")
private Predicate<Object> assetPredicate() {
return asset -> {
if (!(asset instanceof Map)) {
return false;
}
Map aAssetMap = (Map) asset;
// Get content-type
String contentType = aAssetMap.getOrDefault("content_type", "").toString();
Object name = aAssetMap.getOrDefault("name", "");
return name.toString().matches(HALO_ADMIN_VERSION_REGEX) && contentType.equalsIgnoreCase("application/zip");
};
}
private void backupAndClearAdminAssetsIfPresent(@NonNull Path sourcePath, @NonNull Path backupPath) throws IOException {
Assert.notNull(sourcePath, "Source path must not be null");
Assert.notNull(backupPath, "Backup path must not be null");
if (!FileUtils.isEmpty(sourcePath)) {
// Clone this assets
Path adminPathBackup = Paths.get(haloProperties.getWorkDir(), HALO_ADMIN_RELATIVE_BACKUP_PATH);
// Delete backup
FileUtils.deleteFolder(backupPath);
// Copy older assets into backup
FileUtils.copyFolder(sourcePath, backupPath);
// Delete older assets
FileUtils.deleteFolder(sourcePath);
} else {
FileUtils.createIfAbsent(sourcePath);
}
}
/**
* Builds authentication token.
*

View File

@ -36,8 +36,6 @@ import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.GitUtils;
import run.halo.app.utils.HaloUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
@ -378,7 +376,7 @@ public class ThemeServiceImpl implements ThemeService {
try {
// Create temp directory
tempPath = createTempPath();
tempPath = FileUtils.createTempDirectory();
String basename = FilenameUtils.getBasename(file.getOriginalFilename());
Path themeTempPath = tempPath.resolve(basename);
@ -392,7 +390,7 @@ public class ThemeServiceImpl implements ThemeService {
FileUtils.unzip(zis, themeTempPath);
// Go to the base folder and add the theme into system
return add(FileUtils.skipZipParentFolder(themeTempPath));
return add(FileUtils.tryToSkipZipParentFolder(themeTempPath));
} catch (IOException e) {
throw new ServiceException("上传主题失败: " + file.getOriginalFilename(), e);
} finally {
@ -447,7 +445,7 @@ public class ThemeServiceImpl implements ThemeService {
try {
// Create temp path
tmpPath = createTempPath();
tmpPath = FileUtils.createTempDirectory();
// Create temp path
Path themeTmpPath = tmpPath.resolve(HaloUtils.randomUUIDWithoutDash());
@ -570,22 +568,8 @@ public class ThemeServiceImpl implements ThemeService {
log.debug("Downloaded [{}]", zipUrl);
// New zip input stream
ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(downloadResponse.getBody()));
// Unzip it
FileUtils.unzip(zis, targetPath);
}
/**
* Creates temporary path.
*
* @return temporary path
* @throws IOException if an I/O error occurs or the temporary-file directory does not exist
*/
@NonNull
private Path createTempPath() throws IOException {
return Files.createTempDirectory("halo");
FileUtils.unzip(downloadResponse.getBody(), targetPath);
}
/**

View File

@ -6,6 +6,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import run.halo.app.exception.ForbiddenException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
@ -88,7 +89,7 @@ public class FileUtils {
}
/**
* Unzip content to the target path.
* Unzips content to the target path.
*
* @param zis zip input stream must not be null
* @param targetPath target path must not be null and not empty
@ -125,14 +126,29 @@ public class FileUtils {
}
}
/**
* Skips zip parent folder. (Go into base folder)
* Unzips content to the target path.
*
* @param bytes zip bytes array must not be null
* @param targetPath target path must not be null and not empty
* @throws IOException
*/
public static void unzip(@NonNull byte[] bytes, @NonNull Path targetPath) throws IOException {
Assert.notNull(bytes, "Zip bytes must not be null");
ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(bytes));
unzip(zis, targetPath);
}
/**
* Try to skip zip parent folder. (Go into base folder)
*
* @param unzippedPath unzipped path must not be null
* @return path containing base files
* @throws IOException
*/
public static Path skipZipParentFolder(@NonNull Path unzippedPath) throws IOException {
public static Path tryToSkipZipParentFolder(@NonNull Path unzippedPath) throws IOException {
Assert.notNull(unzippedPath, "Unzipped folder must not be null");
// TODO May cause a latent problem.
@ -173,6 +189,10 @@ public class FileUtils {
public static boolean isEmpty(@NonNull Path path) throws IOException {
Assert.notNull(path, "Path must not be null");
if (!Files.isDirectory(path) || Files.notExists(path)) {
return true;
}
try (Stream<Path> pathStream = Files.list(path)) {
return pathStream.count() == 0;
}
@ -275,4 +295,15 @@ public class FileUtils {
}
}
/**
* Creates temp directory.
*
* @return temp directory path
* @throws IOException if an I/O error occurs or the temporary-file directory does not exist
*/
@NonNull
public static Path createTempDirectory() throws IOException {
return Files.createTempDirectory("halo");
}
}

View File

@ -8,6 +8,7 @@ import org.springframework.util.Assert;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.UUID;
import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR;

View File

@ -41,9 +41,10 @@ public class GithubTest {
}
@Test
@SuppressWarnings("unchecked")
public void getLatestReleaseTest() throws Throwable {
ResponseEntity<Map> responseEntity = restTemplate.getForEntity(API_URL, Map.class);
System.out.println("Reponse: " + responseEntity);
System.out.println("Response: " + responseEntity);
Object assetsObject = responseEntity.getBody().get("assets");
System.out.println("Assets class: " + assetsObject.getClass());
System.out.println("Assets: " + assetsObject);