mirror of https://github.com/halo-dev/halo
Complete halo admin updation feature
parent
af42a5fdac
commit
000d1ff1f2
|
@ -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);
|
||||
|
||||
|
|
|
@ -67,4 +67,10 @@ public class AdminController {
|
|||
return adminService.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
@PutMapping("halo-admin")
|
||||
@ApiOperation("Updates halo-admin manually")
|
||||
public void updateAdmin() {
|
||||
adminService.updateAdminAssets();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -64,4 +64,9 @@ public interface AdminService {
|
|||
*/
|
||||
@NonNull
|
||||
AuthToken refreshToken(@NonNull String refreshToken);
|
||||
|
||||
/**
|
||||
* Updates halo admin assets.
|
||||
*/
|
||||
void updateAdminAssets();
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue