mirror of https://github.com/halo-dev/halo
Fix theme updation error (#1217)
* Make rest controller loggable * Refactor pull from git process * Replace Callback interface with Consumer * Tag theme fetch apis and services deprecated * Add getAllBranchesTest * Refactor theme fetcher partially * Refactor theme property scanner * Add ThemeFetcherComposite * Add InputStreamThemeFetcher * Accomplish multipart zip file theme fetcher * Reformat ThemeServiceImpl * Reformat codes * Provide ThemeRepository * Complete MultipartFileThemeUpdater * Make CommonsMultipartResolver support put request method * Replace some methods with ThemeRepository * Add GitThemeUpdater * Add merge two local repo test * Refine merge process with two repos * Add more test entry point in GitTest * Add shutdown hook after creating temporary directory * Add test: find commit by tag * Refactor git clone process in GitThemeFetcher * Refine merge process of two repo * Make sure that RevWalk closed * Fix FileUtils#findRootPath bug * Add clean task before gradle check * Add fallback theme fetcher * Disable logback-test.xml * Set testLogging.showStandardStreams with true * Fix test error while missing halo-test folder * Enhance git theme fetcher * Add copy hidden folder test * Refine GitThemeFetcherTest * Accomplish GitThemeUpdater * Accomplish theme update * Fix checkstyle error * Add more deprecated detailspull/1215/head^2
parent
eaa3a80358
commit
30c9baf92b
|
@ -172,4 +172,5 @@ dependencies {
|
|||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging.showStandardStreams = true
|
||||
}
|
||||
|
|
|
@ -14,7 +14,10 @@ import java.util.List;
|
|||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.fileupload.FileUploadBase;
|
||||
import org.apache.commons.fileupload.servlet.ServletRequestContext;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
|
||||
|
@ -32,6 +35,7 @@ import org.springframework.http.CacheControl;
|
|||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.multipart.MultipartResolver;
|
||||
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
|
||||
|
@ -118,7 +122,16 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
|
|||
@Bean(name = "multipartResolver")
|
||||
MultipartResolver multipartResolver(MultipartProperties multipartProperties) {
|
||||
MultipartConfigElement multipartConfigElement = multipartProperties.createMultipartConfig();
|
||||
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
|
||||
CommonsMultipartResolver resolver = new CommonsMultipartResolver() {
|
||||
@Override
|
||||
public boolean isMultipart(@NonNull HttpServletRequest request) {
|
||||
final var method = request.getMethod();
|
||||
if (!"POST".equalsIgnoreCase(method) && !"PUT".equalsIgnoreCase(method)) {
|
||||
return false;
|
||||
}
|
||||
return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
|
||||
}
|
||||
};
|
||||
resolver.setDefaultEncoding("UTF-8");
|
||||
resolver.setMaxUploadSize(multipartConfigElement.getMaxRequestSize());
|
||||
resolver.setMaxUploadSizePerFile(multipartConfigElement.getMaxFileSize());
|
||||
|
|
|
@ -177,6 +177,7 @@ public class ThemeController {
|
|||
return themeService.upload(file);
|
||||
}
|
||||
|
||||
@PutMapping("upload/{themeId}")
|
||||
@PostMapping("upload/{themeId}")
|
||||
@ApiOperation("Upgrades theme by file")
|
||||
public ThemeProperty updateThemeByUpload(@PathVariable("themeId") String themeId,
|
||||
|
@ -190,20 +191,23 @@ public class ThemeController {
|
|||
return themeService.fetch(uri);
|
||||
}
|
||||
|
||||
@PostMapping("fetchingBranches")
|
||||
@PostMapping(value = {"fetchingBranches", "/fetching/git/branches"})
|
||||
@ApiOperation("Fetches all branches")
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
public List<ThemeProperty> fetchBranches(@RequestParam("uri") String uri) {
|
||||
return themeService.fetchBranches(uri);
|
||||
}
|
||||
|
||||
@PostMapping("fetchingReleases")
|
||||
@ApiOperation("Fetches all releases")
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
public List<ThemeProperty> fetchReleases(@RequestParam("uri") String uri) {
|
||||
return themeService.fetchReleases(uri);
|
||||
}
|
||||
|
||||
@GetMapping("fetchingRelease")
|
||||
@ApiOperation("Fetches a specific release")
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
public ThemeProperty fetchRelease(@RequestParam("uri") String uri,
|
||||
@RequestParam("tag") String tagName) {
|
||||
return themeService.fetchRelease(uri, tagName);
|
||||
|
@ -211,6 +215,7 @@ public class ThemeController {
|
|||
|
||||
@GetMapping("fetchBranch")
|
||||
@ApiOperation("Fetch specific branch")
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
public ThemeProperty fetchBranch(@RequestParam("uri") String uri,
|
||||
@RequestParam("branch") String branchName) {
|
||||
return themeService.fetchBranch(uri, branchName);
|
||||
|
@ -218,12 +223,13 @@ public class ThemeController {
|
|||
|
||||
@GetMapping("fetchLatestRelease")
|
||||
@ApiOperation("Fetch latest release")
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
public ThemeProperty fetchLatestRelease(@RequestParam("uri") String uri) {
|
||||
return themeService.fetchLatestRelease(uri);
|
||||
}
|
||||
|
||||
@PutMapping("fetching/{themeId}")
|
||||
@ApiOperation("Upgrades theme by remote")
|
||||
@ApiOperation("Upgrades theme from remote")
|
||||
public ThemeProperty updateThemeByFetching(@PathVariable("themeId") String themeId) {
|
||||
return themeService.update(themeId);
|
||||
}
|
||||
|
|
|
@ -31,11 +31,15 @@ import run.halo.app.utils.JsonUtils;
|
|||
@Slf4j
|
||||
public class ControllerLogAop {
|
||||
|
||||
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
|
||||
public void restController() {
|
||||
}
|
||||
|
||||
@Pointcut("@within(org.springframework.stereotype.Controller)")
|
||||
public void controller() {
|
||||
}
|
||||
|
||||
@Around("controller()")
|
||||
@Around("controller() || restController()")
|
||||
public Object controller(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
final Method method = signature.getMethod();
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package run.halo.app.exception;
|
||||
|
||||
/**
|
||||
* Theme up to date exception.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public class ThemeUpToDateException extends BadRequestException {
|
||||
|
||||
public ThemeUpToDateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import java.nio.file.Path;
|
|||
import java.nio.file.Paths;
|
||||
import java.util.Calendar;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -58,8 +57,6 @@ public class LocalFileHandler implements FileHandler {
|
|||
|
||||
private final String workDir;
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public LocalFileHandler(OptionService optionService,
|
||||
HaloProperties haloProperties) {
|
||||
this.optionService = optionService;
|
||||
|
@ -79,13 +76,11 @@ public class LocalFileHandler implements FileHandler {
|
|||
Path workPath = Paths.get(workDir);
|
||||
|
||||
// Check file type
|
||||
Assert.isTrue(Files.isDirectory(workPath), workDir + " isn't a directory");
|
||||
|
||||
// Check readable
|
||||
Assert.isTrue(Files.isReadable(workPath), workDir + " isn't readable");
|
||||
|
||||
// Check writable
|
||||
Assert.isTrue(Files.isWritable(workPath), workDir + " isn't writable");
|
||||
if (!Files.isDirectory(workPath)
|
||||
|| !Files.isReadable(workPath)
|
||||
|| !Files.isWritable(workPath)) {
|
||||
log.warn("Please make sure that {} is a directory, readable and writable!", workDir);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -8,6 +8,7 @@ import lombok.Data;
|
|||
* Theme property.
|
||||
*
|
||||
* @author ryanwang
|
||||
* @author johnniang
|
||||
* @date 2019-03-22
|
||||
*/
|
||||
@Data
|
||||
|
@ -31,13 +32,18 @@ public class ThemeProperty {
|
|||
/**
|
||||
* Theme remote branch.(default is master)
|
||||
*/
|
||||
private String branch;
|
||||
private String branch = "master";
|
||||
|
||||
/**
|
||||
* Theme repo url.
|
||||
* Theme git repo url.
|
||||
*/
|
||||
private String repo;
|
||||
|
||||
/**
|
||||
* Theme update strategy. Default is branch.
|
||||
*/
|
||||
private UpdateStrategy updateStrategy = UpdateStrategy.RELEASE;
|
||||
|
||||
/**
|
||||
* Theme description.
|
||||
*/
|
||||
|
@ -115,8 +121,13 @@ public class ThemeProperty {
|
|||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme author info.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Data
|
||||
private static class Author {
|
||||
public static class Author {
|
||||
|
||||
/**
|
||||
* Author name.
|
||||
|
@ -133,4 +144,22 @@ public class ThemeProperty {
|
|||
*/
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme update strategy.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public enum UpdateStrategy {
|
||||
|
||||
/**
|
||||
* Update from specific branch
|
||||
*/
|
||||
BRANCH,
|
||||
|
||||
/**
|
||||
* Update from latest release, only available if the repo is a github repo
|
||||
*/
|
||||
RELEASE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import java.sql.DatabaseMetaData;
|
|||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.eclipse.jgit.storage.file.WindowCacheConfig;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.internal.jdbc.JdbcUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -21,10 +23,10 @@ import org.springframework.boot.ansi.AnsiColor;
|
|||
import org.springframework.boot.ansi.AnsiOutput;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import run.halo.app.config.properties.HaloProperties;
|
||||
|
@ -42,7 +44,7 @@ import run.halo.app.utils.FileUtils;
|
|||
* @date 2018-12-05
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class StartedListener implements ApplicationListener<ApplicationStartedEvent> {
|
||||
|
||||
|
@ -71,9 +73,19 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
|
|||
} catch (SQLException e) {
|
||||
log.error("Failed to migrate database!", e);
|
||||
}
|
||||
this.initThemes();
|
||||
this.initDirectory();
|
||||
this.initThemes();
|
||||
this.printStartInfo();
|
||||
this.configGit();
|
||||
}
|
||||
|
||||
private void configGit() {
|
||||
// Config packed git MMAP
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
WindowCacheConfig config = new WindowCacheConfig();
|
||||
config.setPackedGitMMAP(false);
|
||||
config.install();
|
||||
}
|
||||
}
|
||||
|
||||
private void printStartInfo() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import java.io.UnsupportedEncodingException;
|
|||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Consumer;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
|
@ -28,10 +29,15 @@ import run.halo.app.service.OptionService;
|
|||
public abstract class AbstractMailService implements MailService {
|
||||
|
||||
private static final int DEFAULT_POOL_SIZE = 5;
|
||||
|
||||
protected final OptionService optionService;
|
||||
|
||||
private JavaMailSender cachedMailSender;
|
||||
|
||||
private MailProperties cachedMailProperties;
|
||||
|
||||
private String cachedFromName;
|
||||
|
||||
@Nullable
|
||||
private ExecutorService executorService;
|
||||
|
||||
|
@ -47,7 +53,7 @@ public abstract class AbstractMailService implements MailService {
|
|||
return executorService;
|
||||
}
|
||||
|
||||
public void setExecutorService(ExecutorService executorService) {
|
||||
public void setExecutorService(@Nullable ExecutorService executorService) {
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
|
@ -72,7 +78,7 @@ public abstract class AbstractMailService implements MailService {
|
|||
*
|
||||
* @param callback mime message callback.
|
||||
*/
|
||||
protected void sendMailTemplate(@Nullable Callback callback) {
|
||||
protected void sendMailTemplate(@Nullable Consumer<MimeMessageHelper> callback) {
|
||||
if (callback == null) {
|
||||
log.info("Callback is null, skip to send email");
|
||||
return;
|
||||
|
@ -101,7 +107,7 @@ public abstract class AbstractMailService implements MailService {
|
|||
// set from-name
|
||||
messageHelper.setFrom(getFromAddress(mailSender));
|
||||
// handle message set separately
|
||||
callback.handle(messageHelper);
|
||||
callback.accept(messageHelper);
|
||||
|
||||
// get mime message
|
||||
MimeMessage mimeMessage = messageHelper.getMimeMessage();
|
||||
|
@ -123,9 +129,10 @@ public abstract class AbstractMailService implements MailService {
|
|||
* @param callback callback message handler
|
||||
* @param tryToAsync if the send procedure should try to asynchronous
|
||||
*/
|
||||
protected void sendMailTemplate(boolean tryToAsync, @Nullable Callback callback) {
|
||||
protected void sendMailTemplate(boolean tryToAsync,
|
||||
@Nullable Consumer<MimeMessageHelper> callback) {
|
||||
ExecutorService executorService = getExecutorService();
|
||||
if (tryToAsync && executorService != null) {
|
||||
if (tryToAsync) {
|
||||
// send mail asynchronously
|
||||
executorService.execute(() -> sendMailTemplate(callback));
|
||||
} else {
|
||||
|
@ -233,16 +240,4 @@ public abstract class AbstractMailService implements MailService {
|
|||
log.debug("Cleared all mail caches");
|
||||
}
|
||||
|
||||
/**
|
||||
* Message callback.
|
||||
*/
|
||||
protected interface Callback {
|
||||
/**
|
||||
* Handle message set.
|
||||
*
|
||||
* @param messageHelper mime message helper
|
||||
* @throws Exception if something goes wrong
|
||||
*/
|
||||
void handle(@NonNull MimeMessageHelper messageHelper) throws Exception;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package run.halo.app.mail;
|
||||
|
||||
import freemarker.template.Template;
|
||||
import freemarker.template.TemplateException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import javax.mail.MessagingException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
||||
|
@ -35,9 +39,13 @@ public class MailServiceImpl extends AbstractMailService
|
|||
@Override
|
||||
public void sendTextMail(String to, String subject, String content) {
|
||||
sendMailTemplate(true, messageHelper -> {
|
||||
messageHelper.setSubject(subject);
|
||||
messageHelper.setTo(to);
|
||||
messageHelper.setText(content);
|
||||
try {
|
||||
messageHelper.setSubject(subject);
|
||||
messageHelper.setTo(to);
|
||||
messageHelper.setText(content);
|
||||
} catch (MessagingException e) {
|
||||
throw new RuntimeException("Failed to set message subject, to or test!", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -46,13 +54,19 @@ public class MailServiceImpl extends AbstractMailService
|
|||
String templateName) {
|
||||
sendMailTemplate(true, messageHelper -> {
|
||||
// build message content with freemarker
|
||||
Template template = freeMarker.getConfiguration().getTemplate(templateName);
|
||||
String contentResult =
|
||||
FreeMarkerTemplateUtils.processTemplateIntoString(template, content);
|
||||
try {
|
||||
Template template = freeMarker.getConfiguration().getTemplate(templateName);
|
||||
String contentResult = FreeMarkerTemplateUtils.processTemplateIntoString(template,
|
||||
content);
|
||||
messageHelper.setSubject(subject);
|
||||
messageHelper.setTo(to);
|
||||
messageHelper.setText(contentResult, true);
|
||||
} catch (IOException | TemplateException e) {
|
||||
throw new RuntimeException("Failed to convert template to html!", e);
|
||||
} catch (MessagingException e) {
|
||||
throw new RuntimeException("Failed to set message subject, to or test", e);
|
||||
}
|
||||
|
||||
messageHelper.setSubject(subject);
|
||||
messageHelper.setTo(to);
|
||||
messageHelper.setText(contentResult, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -60,11 +74,15 @@ public class MailServiceImpl extends AbstractMailService
|
|||
public void sendAttachMail(String to, String subject, Map<String, Object> content,
|
||||
String templateName, String attachFilePath) {
|
||||
sendMailTemplate(true, messageHelper -> {
|
||||
messageHelper.setSubject(subject);
|
||||
messageHelper.setTo(to);
|
||||
Path attachmentPath = Paths.get(attachFilePath);
|
||||
messageHelper
|
||||
.addAttachment(attachmentPath.getFileName().toString(), attachmentPath.toFile());
|
||||
try {
|
||||
messageHelper.setSubject(subject);
|
||||
messageHelper.setTo(to);
|
||||
Path attachmentPath = Paths.get(attachFilePath);
|
||||
messageHelper.addAttachment(attachmentPath.getFileName().toString(),
|
||||
attachmentPath.toFile());
|
||||
} catch (MessagingException e) {
|
||||
throw new RuntimeException("Failed to set message subject, to or test", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -73,9 +91,8 @@ public class MailServiceImpl extends AbstractMailService
|
|||
super.testConnection();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(OptionUpdatedEvent event) {
|
||||
public void onApplicationEvent(@NonNull OptionUpdatedEvent event) {
|
||||
// clear the cached java mail sender
|
||||
clearCache();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package run.halo.app.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
|
||||
/**
|
||||
* Theme repository.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public interface ThemeRepository {
|
||||
|
||||
/**
|
||||
* Get activated theme id.
|
||||
*
|
||||
* @return activated theme id
|
||||
*/
|
||||
String getActivatedThemeId();
|
||||
|
||||
/**
|
||||
* Get activated theme property.
|
||||
*
|
||||
* @return activated theme property
|
||||
*/
|
||||
ThemeProperty getActivatedThemeProperty();
|
||||
|
||||
/**
|
||||
* Fetch theme property by theme id.
|
||||
*
|
||||
* @param themeId theme id
|
||||
* @return an optional theme property
|
||||
*/
|
||||
Optional<ThemeProperty> fetchThemePropertyByThemeId(String themeId);
|
||||
|
||||
/**
|
||||
* List all themes
|
||||
*
|
||||
* @return theme list
|
||||
*/
|
||||
List<ThemeProperty> listAll();
|
||||
|
||||
/**
|
||||
* Set activated theme.
|
||||
*
|
||||
* @param themeId theme id
|
||||
*/
|
||||
void setActivatedTheme(String themeId);
|
||||
|
||||
/**
|
||||
* Attempt to add new theme.
|
||||
*
|
||||
* @param newThemeProperty new theme property
|
||||
* @return theme property
|
||||
*/
|
||||
ThemeProperty attemptToAdd(ThemeProperty newThemeProperty);
|
||||
|
||||
/**
|
||||
* Delete theme by theme id.
|
||||
*
|
||||
* @param themeId theme id
|
||||
*/
|
||||
void deleteTheme(String themeId);
|
||||
|
||||
/**
|
||||
* Delete theme by theme property.
|
||||
*
|
||||
* @param themeProperty theme property
|
||||
*/
|
||||
void deleteTheme(ThemeProperty themeProperty);
|
||||
|
||||
/**
|
||||
* Check theme property compatibility
|
||||
*
|
||||
* @param themeProperty theme property
|
||||
*/
|
||||
boolean checkThemePropertyCompatibility(ThemeProperty themeProperty);
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package run.halo.app.repository;
|
||||
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
import static run.halo.app.utils.FileUtils.copyFolder;
|
||||
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
||||
import static run.halo.app.utils.VersionUtil.compareVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.config.properties.HaloProperties;
|
||||
import run.halo.app.exception.AlreadyExistsException;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.exception.ServiceException;
|
||||
import run.halo.app.exception.ThemeNotSupportException;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.model.entity.Option;
|
||||
import run.halo.app.model.properties.PrimaryProperties;
|
||||
import run.halo.app.model.support.HaloConst;
|
||||
import run.halo.app.theme.ThemePropertyScanner;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Theme repository implementation.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Repository
|
||||
@Slf4j
|
||||
public class ThemeRepositoryImpl implements ThemeRepository {
|
||||
|
||||
private final OptionRepository optionRepository;
|
||||
|
||||
private final HaloProperties properties;
|
||||
|
||||
public ThemeRepositoryImpl(OptionRepository optionRepository,
|
||||
HaloProperties properties) {
|
||||
this.optionRepository = optionRepository;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivatedThemeId() {
|
||||
return optionRepository.findByKey(PrimaryProperties.THEME.getValue())
|
||||
.map(Option::getValue)
|
||||
.orElse(HaloConst.DEFAULT_THEME_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty getActivatedThemeProperty() {
|
||||
return fetchThemePropertyByThemeId(getActivatedThemeId()).orElseThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ThemeProperty> fetchThemePropertyByThemeId(String themeId) {
|
||||
return listAll().stream()
|
||||
.filter(property -> Objects.equals(themeId, property.getId()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ThemeProperty> listAll() {
|
||||
return ThemePropertyScanner.INSTANCE.scan(getThemeRootPath(), getActivatedThemeId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActivatedTheme(@NonNull String themeId) {
|
||||
Assert.hasText(themeId, "Theme id must not be blank");
|
||||
|
||||
final var newThemeOption = optionRepository.findByKey(PrimaryProperties.THEME.getValue())
|
||||
.map(themeOption -> {
|
||||
// set theme id
|
||||
themeOption.setValue(themeId);
|
||||
return themeOption;
|
||||
})
|
||||
.orElseGet(() -> new Option(PrimaryProperties.THEME.getValue(), themeId));
|
||||
optionRepository.save(newThemeOption);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty attemptToAdd(ThemeProperty newProperty) {
|
||||
// 1. check existence
|
||||
final var alreadyExist = fetchThemePropertyByThemeId(newProperty.getId()).isPresent();
|
||||
if (alreadyExist) {
|
||||
throw new AlreadyExistsException("当前安装的主题已存在");
|
||||
}
|
||||
|
||||
// 2. check version compatibility
|
||||
// Not support current halo version.
|
||||
if (checkThemePropertyCompatibility(newProperty)) {
|
||||
throw new ThemeNotSupportException(
|
||||
"当前主题仅支持 Halo " + newProperty.getRequire() + " 及以上的版本");
|
||||
}
|
||||
|
||||
// 3. move the temp folder into templates/themes/{theme_id}
|
||||
final var sourceThemePath = Paths.get(newProperty.getThemePath());
|
||||
final var targetThemePath =
|
||||
getThemeRootPath().resolve(newProperty.getId() + "-" + randomAlphabetic(5));
|
||||
|
||||
// 4. clear target theme folder firstly
|
||||
deleteFolderQuietly(targetThemePath);
|
||||
|
||||
log.info("Copying new theme({}) from {} to {}",
|
||||
newProperty.getId(),
|
||||
sourceThemePath,
|
||||
targetThemePath);
|
||||
|
||||
try {
|
||||
copyFolder(sourceThemePath, targetThemePath);
|
||||
} catch (IOException e) {
|
||||
// clear data
|
||||
deleteFolderQuietly(targetThemePath);
|
||||
throw new ServiceException("复制主题文件失败!", e);
|
||||
} finally {
|
||||
log.info("Clean temporary theme folder {}", sourceThemePath);
|
||||
deleteFolderQuietly(sourceThemePath);
|
||||
}
|
||||
|
||||
// or else throw should never happen
|
||||
return ThemePropertyScanner.INSTANCE.fetchThemeProperty(targetThemePath).orElseThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTheme(String themeId) {
|
||||
final var themeProperty = fetchThemePropertyByThemeId(themeId)
|
||||
.orElseThrow(() -> new NotFoundException("主题 ID 为 " + themeId + " 不存在或已删除!"));
|
||||
deleteTheme(themeProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTheme(ThemeProperty themeProperty) {
|
||||
final var themePath = Paths.get(themeProperty.getThemePath());
|
||||
try {
|
||||
FileUtils.deleteFolder(themePath);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to delete theme path: " + themePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkThemePropertyCompatibility(ThemeProperty themeProperty) {
|
||||
// check version compatibility
|
||||
// Not support current halo version.
|
||||
return StringUtils.isNotEmpty(themeProperty.getRequire())
|
||||
&& !compareVersion(HaloConst.HALO_VERSION, themeProperty.getRequire());
|
||||
}
|
||||
|
||||
private Path getThemeRootPath() {
|
||||
return Paths.get(properties.getWorkDir()).resolve("templates/themes");
|
||||
}
|
||||
}
|
|
@ -112,7 +112,6 @@ public interface ThemeService {
|
|||
* @return theme property
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated
|
||||
ThemeProperty getThemeOfNonNullBy(@NonNull String themeId);
|
||||
|
||||
/**
|
||||
|
@ -305,6 +304,7 @@ public interface ThemeService {
|
|||
* @throws IOException IOException
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated
|
||||
ThemeProperty add(@NonNull Path themeTmpPath) throws IOException;
|
||||
|
||||
/**
|
||||
|
@ -323,6 +323,7 @@ public interface ThemeService {
|
|||
* @return theme property
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
ThemeProperty fetchLatestRelease(@NonNull String uri);
|
||||
|
||||
/**
|
||||
|
@ -332,6 +333,7 @@ public interface ThemeService {
|
|||
* @return list of theme properties
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
List<ThemeProperty> fetchBranches(@NonNull String uri);
|
||||
|
||||
/**
|
||||
|
@ -341,6 +343,7 @@ public interface ThemeService {
|
|||
* @return list of theme properties
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
List<ThemeProperty> fetchReleases(@NonNull String uri);
|
||||
|
||||
/**
|
||||
|
@ -351,6 +354,7 @@ public interface ThemeService {
|
|||
* @return theme property
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
ThemeProperty fetchRelease(@NonNull String uri, @NonNull String tagName);
|
||||
|
||||
/**
|
||||
|
@ -361,6 +365,7 @@ public interface ThemeService {
|
|||
* @return theme property
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
ThemeProperty fetchBranch(@NonNull String uri, @NonNull String branchName);
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
//package run.halo.app.service;
|
||||
//
|
||||
//import org.springframework.boot.actuate.trace.http.HttpTrace;
|
||||
//import org.springframework.lang.NonNull;
|
||||
//
|
||||
//import java.util.List;
|
||||
//
|
||||
///**
|
||||
// * Trace service interface.
|
||||
// *
|
||||
// * @author johnniang
|
||||
// * @date 2019-06-18
|
||||
// */
|
||||
//public interface TraceService {
|
||||
//
|
||||
// /**
|
||||
// * Gets all http traces.
|
||||
// *
|
||||
// * @return
|
||||
// */
|
||||
// @NonNull
|
||||
// List<HttpTrace> listHttpTraces();
|
||||
//
|
||||
//}
|
|
@ -1,47 +1,33 @@
|
|||
package run.halo.app.service.impl;
|
||||
|
||||
import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH;
|
||||
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
|
||||
import static run.halo.app.utils.FileUtils.copyFolder;
|
||||
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
||||
import static run.halo.app.utils.VersionUtil.compareVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.PullResult;
|
||||
import org.eclipse.jgit.api.ResetCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.transport.RemoteConfig;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import run.halo.app.cache.AbstractStringCacheStore;
|
||||
import run.halo.app.config.properties.HaloProperties;
|
||||
import run.halo.app.event.theme.ThemeActivatedEvent;
|
||||
import run.halo.app.event.theme.ThemeUpdatedEvent;
|
||||
|
@ -53,24 +39,26 @@ import run.halo.app.exception.ServiceException;
|
|||
import run.halo.app.exception.ThemeNotSupportException;
|
||||
import run.halo.app.exception.ThemePropertyMissingException;
|
||||
import run.halo.app.exception.ThemeUpdateException;
|
||||
import run.halo.app.exception.UnsupportedMediaTypeException;
|
||||
import run.halo.app.handler.theme.config.ThemeConfigResolver;
|
||||
import run.halo.app.handler.theme.config.support.Group;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.model.properties.PrimaryProperties;
|
||||
import run.halo.app.model.support.HaloConst;
|
||||
import run.halo.app.model.support.ThemeFile;
|
||||
import run.halo.app.repository.ThemeRepository;
|
||||
import run.halo.app.repository.ThemeSettingRepository;
|
||||
import run.halo.app.service.OptionService;
|
||||
import run.halo.app.service.ThemeService;
|
||||
import run.halo.app.theme.GitThemeFetcher;
|
||||
import run.halo.app.theme.GitThemeUpdater;
|
||||
import run.halo.app.theme.MultipartFileThemeUpdater;
|
||||
import run.halo.app.theme.MultipartZipFileThemeFetcher;
|
||||
import run.halo.app.theme.ThemeFetcherComposite;
|
||||
import run.halo.app.theme.ThemeFileScanner;
|
||||
import run.halo.app.theme.ThemePropertyScanner;
|
||||
import run.halo.app.theme.ZipThemeFetcher;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
import run.halo.app.utils.FilenameUtils;
|
||||
import run.halo.app.utils.GitUtils;
|
||||
import run.halo.app.utils.GithubUtils;
|
||||
import run.halo.app.utils.HaloUtils;
|
||||
import run.halo.app.utils.VersionUtil;
|
||||
|
||||
/**
|
||||
* Theme service implementation.
|
||||
|
@ -87,10 +75,6 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
*/
|
||||
private final Path themeWorkDir;
|
||||
|
||||
private final OptionService optionService;
|
||||
|
||||
private final AbstractStringCacheStore cacheStore;
|
||||
|
||||
private final ThemeConfigResolver themeConfigResolver;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
@ -99,69 +83,48 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
|
||||
private final ThemeSettingRepository themeSettingRepository;
|
||||
|
||||
/**
|
||||
* Activated theme id.
|
||||
*/
|
||||
@Nullable
|
||||
private volatile String activatedThemeId;
|
||||
private final ThemeFetcherComposite fetcherComposite;
|
||||
|
||||
/**
|
||||
* Activated theme property.
|
||||
*/
|
||||
private volatile ThemeProperty activatedTheme;
|
||||
private final ThemeRepository themeRepository;
|
||||
|
||||
public ThemeServiceImpl(HaloProperties haloProperties,
|
||||
OptionService optionService,
|
||||
AbstractStringCacheStore cacheStore,
|
||||
ThemeConfigResolver themeConfigResolver,
|
||||
RestTemplate restTemplate,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
ThemeSettingRepository themeSettingRepository) {
|
||||
this.optionService = optionService;
|
||||
this.cacheStore = cacheStore;
|
||||
ThemeSettingRepository themeSettingRepository,
|
||||
ThemeRepository themeRepository) {
|
||||
this.themeConfigResolver = themeConfigResolver;
|
||||
this.restTemplate = restTemplate;
|
||||
|
||||
themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
|
||||
this.themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.themeSettingRepository = themeSettingRepository;
|
||||
this.themeRepository = themeRepository;
|
||||
|
||||
this.fetcherComposite = new ThemeFetcherComposite();
|
||||
this.fetcherComposite.addFetcher(new ZipThemeFetcher());
|
||||
this.fetcherComposite.addFetcher(new GitThemeFetcher());
|
||||
this.fetcherComposite.addFetcher(new MultipartZipFileThemeFetcher());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ThemeProperty getThemeOfNonNullBy(@NonNull String themeId) {
|
||||
return fetchThemePropertyBy(themeId).orElseThrow(
|
||||
() -> new NotFoundException(themeId + " 主题不存在或已删除!").setErrorData(themeId));
|
||||
return fetchThemePropertyBy(themeId)
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException(themeId + " 主题不存在或已删除!").setErrorData(themeId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Optional<ThemeProperty> fetchThemePropertyBy(String themeId) {
|
||||
if (StringUtils.isBlank(themeId)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Get all themes
|
||||
List<ThemeProperty> themes = getThemes();
|
||||
|
||||
// filter and find first
|
||||
return themes.stream()
|
||||
.filter(themeProperty -> StringUtils.equals(themeProperty.getId(), themeId))
|
||||
.findFirst();
|
||||
return themeRepository.fetchThemePropertyByThemeId(themeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<ThemeProperty> getThemes() {
|
||||
ThemeProperty[] themeProperties =
|
||||
cacheStore.getAny(THEMES_CACHE_KEY, ThemeProperty[].class).orElseGet(() -> {
|
||||
List<ThemeProperty> properties =
|
||||
ThemePropertyScanner.INSTANCE.scan(getBasePath(), getActivatedThemeId());
|
||||
// Cache the themes
|
||||
cacheStore.putAny(THEMES_CACHE_KEY, properties);
|
||||
return properties.toArray(new ThemeProperty[0]);
|
||||
});
|
||||
return Arrays.asList(themeProperties);
|
||||
return themeRepository.listAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -185,13 +148,11 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
// Get the theme path
|
||||
Path themePath = Paths.get(themeProperty.getThemePath());
|
||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||
return pathStream.filter(
|
||||
path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), prefix))
|
||||
.filter(path -> !(HaloConst.POST_PASSWORD_TEMPLATE + HaloConst.SUFFIX_FTL)
|
||||
.equals(path.getFileName().toString()))
|
||||
return pathStream.filter(path ->
|
||||
StringUtils.startsWithIgnoreCase(path.getFileName().toString(), prefix))
|
||||
.map(path -> {
|
||||
// Remove prefix
|
||||
String customTemplate = StringUtils
|
||||
final var customTemplate = StringUtils
|
||||
.removeStartIgnoreCase(path.getFileName().toString(), prefix);
|
||||
// Remove suffix
|
||||
return StringUtils
|
||||
|
@ -239,7 +200,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
// Read file
|
||||
Path path = Paths.get(absolutePath);
|
||||
try {
|
||||
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
|
||||
return Files.readString(path);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("读取模板内容失败 " + absolutePath, e);
|
||||
}
|
||||
|
@ -253,7 +214,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
// Read file
|
||||
Path path = Paths.get(absolutePath);
|
||||
try {
|
||||
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
|
||||
return Files.readString(path);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("读取模板内容失败 " + absolutePath, e);
|
||||
}
|
||||
|
@ -296,7 +257,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
|
||||
if (themeId.equals(getActivatedThemeId())) {
|
||||
// Prevent to delete the activated theme
|
||||
throw new BadRequestException("不能删除正在使用的主题").setErrorData(themeId);
|
||||
throw new BadRequestException("无法删除正在使用的主题!").setErrorData(themeId);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -339,8 +300,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
}
|
||||
|
||||
// Read the yaml file
|
||||
String optionContent =
|
||||
new String(Files.readAllBytes(optionsPath), StandardCharsets.UTF_8);
|
||||
String optionContent = Files.readString(optionsPath);
|
||||
|
||||
// Resolve it
|
||||
return themeConfigResolver.resolve(optionContent);
|
||||
|
@ -355,8 +315,8 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
@Override
|
||||
public String render(String pageName) {
|
||||
return fetchActivatedTheme()
|
||||
.map(themeProperty -> String
|
||||
.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName))
|
||||
.map(themeProperty ->
|
||||
String.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName))
|
||||
.orElse(DEFAULT_ERROR_PATH);
|
||||
}
|
||||
|
||||
|
@ -371,41 +331,13 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
@Override
|
||||
@NonNull
|
||||
public String getActivatedThemeId() {
|
||||
if (activatedThemeId == null) {
|
||||
synchronized (this) {
|
||||
if (activatedThemeId == null) {
|
||||
activatedThemeId = optionService
|
||||
.getByPropertyOrDefault(PrimaryProperties.THEME, String.class,
|
||||
DEFAULT_THEME_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
return activatedThemeId;
|
||||
return themeRepository.getActivatedThemeId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ThemeProperty getActivatedTheme() {
|
||||
if (activatedTheme == null) {
|
||||
synchronized (this) {
|
||||
if (activatedTheme == null) {
|
||||
// Get theme property
|
||||
activatedTheme = getThemeOfNonNullBy(getActivatedThemeId());
|
||||
}
|
||||
}
|
||||
}
|
||||
return activatedTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets activated theme.
|
||||
*
|
||||
* @param activatedTheme activated theme
|
||||
*/
|
||||
private void setActivatedTheme(@Nullable ThemeProperty activatedTheme) {
|
||||
this.activatedTheme = activatedTheme;
|
||||
this.activatedThemeId =
|
||||
Optional.ofNullable(activatedTheme).map(ThemeProperty::getId).orElse(null);
|
||||
return themeRepository.getActivatedThemeProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -417,14 +349,8 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
@Override
|
||||
@NonNull
|
||||
public ThemeProperty activateTheme(@NonNull String themeId) {
|
||||
// Check existence of the theme
|
||||
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
|
||||
|
||||
// Save the theme to database
|
||||
optionService.saveProperty(PrimaryProperties.THEME, themeId);
|
||||
|
||||
// Set activated theme
|
||||
setActivatedTheme(themeProperty);
|
||||
// set activated theme
|
||||
themeRepository.setActivatedTheme(themeId);
|
||||
|
||||
// Clear the cache
|
||||
eventPublisher.publishEvent(new ThemeUpdatedEvent(this));
|
||||
|
@ -432,7 +358,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
// Publish a theme activated event
|
||||
eventPublisher.publishEvent(new ThemeActivatedEvent(this));
|
||||
|
||||
return themeProperty;
|
||||
return themeRepository.getActivatedThemeProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -440,54 +366,23 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
public ThemeProperty upload(@NonNull MultipartFile file) {
|
||||
Assert.notNull(file, "Multipart file must not be null");
|
||||
|
||||
if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".zip")) {
|
||||
throw new UnsupportedMediaTypeException("不支持的文件类型: " + file.getContentType())
|
||||
.setErrorData(file.getOriginalFilename());
|
||||
}
|
||||
|
||||
ZipInputStream zis = null;
|
||||
Path tempPath = null;
|
||||
|
||||
try {
|
||||
// Create temp directory
|
||||
tempPath = FileUtils.createTempDirectory();
|
||||
String basename =
|
||||
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename()));
|
||||
Path themeTempPath = tempPath.resolve(basename);
|
||||
|
||||
// Check directory traversal
|
||||
FileUtils.checkDirectoryTraversal(tempPath, themeTempPath);
|
||||
|
||||
// New zip input stream
|
||||
zis = new ZipInputStream(file.getInputStream());
|
||||
|
||||
// Unzip to temp path
|
||||
FileUtils.unzip(zis, themeTempPath);
|
||||
|
||||
Path themePath = getThemeRootPath(themeTempPath);
|
||||
|
||||
// Go to the base folder and add the theme into system
|
||||
return add(themePath);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("主题上传失败: " + file.getOriginalFilename(), e);
|
||||
} finally {
|
||||
// Close zip input stream
|
||||
FileUtils.closeQuietly(zis);
|
||||
// Delete folder after testing
|
||||
FileUtils.deleteFolderQuietly(tempPath);
|
||||
}
|
||||
final var newThemeProperty = this.fetcherComposite.fetch(file);
|
||||
return this.themeRepository.attemptToAdd(newThemeProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public ThemeProperty add(@NonNull Path themeTmpPath) throws IOException {
|
||||
Assert.notNull(themeTmpPath, "Theme temporary path must not be null");
|
||||
Assert.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory");
|
||||
|
||||
log.debug("Children path of [{}]:", themeTmpPath);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Children path of [{}]:", themeTmpPath);
|
||||
|
||||
try (Stream<Path> pathStream = Files.list(themeTmpPath)) {
|
||||
pathStream.forEach(path -> log.debug(path.toString()));
|
||||
try (Stream<Path> pathStream = Files.list(themeTmpPath)) {
|
||||
pathStream.forEach(path -> log.trace(path.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// Check property config
|
||||
|
@ -495,8 +390,8 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
|
||||
// Check theme existence
|
||||
boolean isExist = getThemes().stream()
|
||||
.anyMatch(
|
||||
themeProperty -> themeProperty.getId().equalsIgnoreCase(tmpThemeProperty.getId()));
|
||||
.anyMatch(themeProperty -> themeProperty.getId()
|
||||
.equalsIgnoreCase(tmpThemeProperty.getId()));
|
||||
|
||||
if (isExist) {
|
||||
throw new AlreadyExistsException("当前安装的主题已存在");
|
||||
|
@ -504,14 +399,14 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
|
||||
// Not support current halo version.
|
||||
if (StringUtils.isNotEmpty(tmpThemeProperty.getRequire())
|
||||
&& !VersionUtil.compareVersion(HaloConst.HALO_VERSION, tmpThemeProperty.getRequire())) {
|
||||
&& !compareVersion(HaloConst.HALO_VERSION, tmpThemeProperty.getRequire())) {
|
||||
throw new ThemeNotSupportException(
|
||||
"当前主题仅支持 Halo " + tmpThemeProperty.getRequire() + " 以上的版本");
|
||||
}
|
||||
|
||||
// Copy the temporary path to current theme folder
|
||||
Path targetThemePath = themeWorkDir.resolve(tmpThemeProperty.getId());
|
||||
FileUtils.copyFolder(themeTmpPath, targetThemePath);
|
||||
copyFolder(themeTmpPath, targetThemePath);
|
||||
|
||||
// Get property again
|
||||
ThemeProperty property = getProperty(targetThemePath);
|
||||
|
@ -527,28 +422,8 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
public ThemeProperty fetch(@NonNull String uri) {
|
||||
Assert.hasText(uri, "Theme remote uri must not be blank");
|
||||
|
||||
Path tmpPath = null;
|
||||
|
||||
try {
|
||||
// Create temp path
|
||||
tmpPath = FileUtils.createTempDirectory();
|
||||
// Create temp path
|
||||
Path themeTmpPath = tmpPath.resolve(HaloUtils.randomUUIDWithoutDash());
|
||||
|
||||
if (StringUtils.endsWithIgnoreCase(uri, ".zip")) {
|
||||
downloadZipAndUnzip(uri, themeTmpPath);
|
||||
} else {
|
||||
String repoUrl = StringUtils.appendIfMissingIgnoreCase(uri, ".git", ".git");
|
||||
// Clone from git
|
||||
GitUtils.cloneFromGit(repoUrl, themeTmpPath);
|
||||
}
|
||||
|
||||
return add(themeTmpPath);
|
||||
} catch (IOException | GitAPIException e) {
|
||||
throw new ServiceException("主题拉取失败 " + uri, e);
|
||||
} finally {
|
||||
FileUtils.deleteFolderQuietly(tmpPath);
|
||||
}
|
||||
final var themeProperty = fetcherComposite.fetch(uri);
|
||||
return this.themeRepository.attemptToAdd(themeProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -570,7 +445,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
} catch (IOException | GitAPIException e) {
|
||||
throw new ServiceException("主题拉取失败 " + uri + "。" + e.getMessage(), e);
|
||||
} finally {
|
||||
FileUtils.deleteFolderQuietly(tmpPath);
|
||||
deleteFolderQuietly(tmpPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,7 +477,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
} catch (IOException e) {
|
||||
throw new ServiceException("主题拉取失败 " + uri, e);
|
||||
} finally {
|
||||
FileUtils.deleteFolderQuietly(tmpPath);
|
||||
deleteFolderQuietly(tmpPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -610,28 +485,14 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
public ThemeProperty fetchLatestRelease(@NonNull String uri) {
|
||||
Assert.hasText(uri, "Theme remote uri must not be blank");
|
||||
|
||||
Path tmpPath = null;
|
||||
try {
|
||||
tmpPath = FileUtils.createTempDirectory();
|
||||
|
||||
Path themeTmpPath = tmpPath.resolve(HaloUtils.randomUUIDWithoutDash());
|
||||
|
||||
Map<String, Object> releaseInfo = GithubUtils.getLatestRelease(uri);
|
||||
|
||||
if (releaseInfo == null) {
|
||||
throw new ServiceException("主题拉取失败" + uri);
|
||||
}
|
||||
|
||||
String zipUrl = (String) releaseInfo.get(ZIP_FILE_KEY);
|
||||
|
||||
downloadZipAndUnzip(zipUrl, themeTmpPath);
|
||||
|
||||
return add(themeTmpPath);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("主题拉取失败 " + uri, e);
|
||||
} finally {
|
||||
FileUtils.deleteFolderQuietly(tmpPath);
|
||||
Map<String, Object> releaseInfo = GithubUtils.getLatestRelease(uri);
|
||||
if (releaseInfo == null) {
|
||||
throw new ServiceException("主题拉取失败" + uri);
|
||||
}
|
||||
String zipUrl = (String) releaseInfo.get(ZIP_FILE_KEY);
|
||||
|
||||
final var themeProperty = this.fetcherComposite.fetch(zipUrl);
|
||||
return this.themeRepository.attemptToAdd(themeProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -639,7 +500,7 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
Assert.hasText(uri, "Theme remote uri must not be blank");
|
||||
|
||||
String repoUrl = StringUtils.appendIfMissingIgnoreCase(uri, ".git", ".git");
|
||||
List<String> branches = GitUtils.getAllBranches(repoUrl);
|
||||
List<String> branches = GitUtils.getAllBranchesFromRemote(repoUrl);
|
||||
|
||||
List<ThemeProperty> themeProperties = new ArrayList<>();
|
||||
|
||||
|
@ -680,22 +541,16 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
|
||||
@Override
|
||||
public ThemeProperty update(String themeId) {
|
||||
final var themeUpdater = new GitThemeUpdater(themeRepository, fetcherComposite);
|
||||
Assert.hasText(themeId, "Theme id must not be blank");
|
||||
|
||||
ThemeProperty updatingTheme = getThemeOfNonNullBy(themeId);
|
||||
|
||||
try {
|
||||
pullFromGit(updatingTheme);
|
||||
|
||||
final var themeProperty = themeUpdater.update(themeId);
|
||||
} catch (Exception e) {
|
||||
if (e instanceof ThemeNotSupportException) {
|
||||
throw (ThemeNotSupportException) e;
|
||||
}
|
||||
if (e instanceof GitAPIException) {
|
||||
throw new ThemeUpdateException("主题更新失败!" + e.getMessage(), e);
|
||||
}
|
||||
throw new ThemeUpdateException("主题更新失败!您与主题作者可能同时更改了同一个文件,您也可以尝试删除主题并重新拉取最新的主题", e)
|
||||
.setErrorData(themeId);
|
||||
throw new ThemeUpdateException("主题更新失败!", e).setErrorData(themeId);
|
||||
}
|
||||
|
||||
eventPublisher.publishEvent(new ThemeUpdatedEvent(this));
|
||||
|
@ -706,157 +561,17 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
@Override
|
||||
public ThemeProperty update(String themeId, MultipartFile file) {
|
||||
Assert.hasText(themeId, "Theme id must not be blank");
|
||||
Assert.notNull(themeId, "Theme file must not be blank");
|
||||
|
||||
if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".zip")) {
|
||||
throw new UnsupportedMediaTypeException("不支持的文件类型: " + file.getContentType())
|
||||
.setErrorData(file.getOriginalFilename());
|
||||
}
|
||||
|
||||
ThemeProperty updatingTheme = getThemeOfNonNullBy(themeId);
|
||||
|
||||
ZipInputStream zis = null;
|
||||
Path tempPath = null;
|
||||
Assert.notNull(file, "Theme file must not be null");
|
||||
|
||||
final var themeUpdater =
|
||||
new MultipartFileThemeUpdater(file, fetcherComposite, themeRepository);
|
||||
try {
|
||||
// Create temp directory
|
||||
tempPath = FileUtils.createTempDirectory();
|
||||
|
||||
String basename = FilenameUtils.getBasename(file.getOriginalFilename());
|
||||
Path themeTempPath = tempPath.resolve(basename);
|
||||
|
||||
// Check directory traversal
|
||||
FileUtils.checkDirectoryTraversal(tempPath, themeTempPath);
|
||||
|
||||
// New zip input stream
|
||||
zis = new ZipInputStream(file.getInputStream());
|
||||
|
||||
// Unzip to temp path
|
||||
FileUtils.unzip(zis, themeTempPath);
|
||||
|
||||
Path preparePath = getThemeRootPath(themeTempPath);
|
||||
|
||||
ThemeProperty prepareThemeProperty = getProperty(preparePath);
|
||||
|
||||
if (!prepareThemeProperty.getId().equals(updatingTheme.getId())) {
|
||||
throw new ServiceException("上传的主题包不是该主题的更新包: " + file.getOriginalFilename());
|
||||
}
|
||||
|
||||
// Not support current halo version.
|
||||
if (StringUtils.isNotEmpty(prepareThemeProperty.getRequire()) && !VersionUtil
|
||||
.compareVersion(HaloConst.HALO_VERSION, prepareThemeProperty.getRequire())) {
|
||||
throw new ThemeNotSupportException(
|
||||
"新版本主题仅支持 Halo " + prepareThemeProperty.getRequire() + " 以上的版本");
|
||||
}
|
||||
|
||||
// Coping new theme files to old theme folder.
|
||||
FileUtils.copyFolder(preparePath, Paths.get(updatingTheme.getThemePath()));
|
||||
|
||||
eventPublisher.publishEvent(new ThemeUpdatedEvent(this));
|
||||
|
||||
// Gets theme property again.
|
||||
return getProperty(Paths.get(updatingTheme.getThemePath()));
|
||||
return themeUpdater.update(themeId);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("更新主题失败: " + file.getOriginalFilename(), e);
|
||||
} finally {
|
||||
// Close zip input stream
|
||||
FileUtils.closeQuietly(zis);
|
||||
// Delete folder after testing
|
||||
FileUtils.deleteFolderQuietly(tempPath);
|
||||
throw new ServiceException("更新主题失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void pullFromGit(@NonNull ThemeProperty themeProperty) throws
|
||||
IOException, GitAPIException, URISyntaxException {
|
||||
Assert.notNull(themeProperty, "Theme property must not be null");
|
||||
|
||||
// Get branch
|
||||
String branch = StringUtils.isBlank(themeProperty.getBranch())
|
||||
? DEFAULT_REMOTE_BRANCH : themeProperty.getBranch();
|
||||
|
||||
Git git = null;
|
||||
|
||||
try {
|
||||
git = GitUtils.openOrInit(Paths.get(themeProperty.getThemePath()));
|
||||
|
||||
Repository repository = git.getRepository();
|
||||
|
||||
// Add all changes
|
||||
git.add()
|
||||
.addFilepattern(".")
|
||||
.call();
|
||||
// Commit the changes
|
||||
git.commit().setMessage("Commit by halo automatically").call();
|
||||
|
||||
RevWalk revWalk = new RevWalk(repository);
|
||||
|
||||
Ref ref = repository.findRef(Constants.HEAD);
|
||||
|
||||
Assert.notNull(ref, Constants.HEAD + " ref was not found!");
|
||||
|
||||
RevCommit lastCommit = revWalk.parseCommit(ref.getObjectId());
|
||||
|
||||
// Force to set remote name
|
||||
git.remoteRemove().setRemoteName(THEME_PROVIDER_REMOTE_NAME).call();
|
||||
RemoteConfig remoteConfig = git.remoteAdd()
|
||||
.setName(THEME_PROVIDER_REMOTE_NAME)
|
||||
.setUri(new URIish(themeProperty.getRepo()))
|
||||
.call();
|
||||
|
||||
// Check out to specified branch
|
||||
if (!StringUtils.equalsIgnoreCase(branch, git.getRepository().getBranch())) {
|
||||
boolean present = git.branchList()
|
||||
.call()
|
||||
.stream()
|
||||
.map(Ref::getName)
|
||||
.anyMatch(name -> StringUtils.equalsIgnoreCase(name, branch));
|
||||
|
||||
git.checkout()
|
||||
.setCreateBranch(true)
|
||||
.setForced(!present)
|
||||
.setName(branch)
|
||||
.call();
|
||||
}
|
||||
|
||||
// Pull with rebasing
|
||||
PullResult pullResult = git.pull()
|
||||
.setRemote(remoteConfig.getName())
|
||||
.setRemoteBranchName(branch)
|
||||
.setRebase(true)
|
||||
.call();
|
||||
|
||||
if (!pullResult.isSuccessful()) {
|
||||
log.debug("Rebase result: [{}]", pullResult.getRebaseResult());
|
||||
log.debug("Merge result: [{}]", pullResult.getMergeResult());
|
||||
|
||||
throw new ThemeUpdateException("拉取失败!您与主题作者可能同时更改了同一个文件");
|
||||
}
|
||||
|
||||
String latestTagName =
|
||||
(String) GithubUtils.getLatestRelease(themeProperty.getRepo()).get(TAG_KEY);
|
||||
git.checkout().setName(latestTagName).call();
|
||||
|
||||
// updated successfully.
|
||||
ThemeProperty updatedThemeProperty =
|
||||
getProperty(Paths.get(themeProperty.getThemePath()));
|
||||
|
||||
// Not support current halo version.
|
||||
if (StringUtils.isNotEmpty(updatedThemeProperty.getRequire()) && !VersionUtil
|
||||
.compareVersion(HaloConst.HALO_VERSION, updatedThemeProperty.getRequire())) {
|
||||
// reset theme version
|
||||
git.reset()
|
||||
.setMode(ResetCommand.ResetType.HARD)
|
||||
.setRef(lastCommit.getName())
|
||||
.call();
|
||||
throw new ThemeNotSupportException(
|
||||
"新版本主题仅支持 Halo " + updatedThemeProperty.getRequire() + " 以上的版本");
|
||||
}
|
||||
} finally {
|
||||
GitUtils.closeQuietly(git);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads zip file and unzip it into specified path.
|
||||
*
|
||||
|
@ -875,8 +590,10 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
log.debug("Download response: [{}]", downloadResponse.getStatusCode());
|
||||
|
||||
if (downloadResponse.getStatusCode().isError() || downloadResponse.getBody() == null) {
|
||||
throw new ServiceException(
|
||||
"下载失败 " + zipUrl + ", 状态码: " + downloadResponse.getStatusCode());
|
||||
throw new ServiceException("下载失败 "
|
||||
+ zipUrl
|
||||
+ ", 状态码: "
|
||||
+ downloadResponse.getStatusCode());
|
||||
}
|
||||
|
||||
log.debug("Downloaded [{}]", zipUrl);
|
||||
|
@ -927,10 +644,12 @@ public class ThemeServiceImpl implements ThemeService {
|
|||
* @throws IOException IO exception
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
private Path getThemeRootPath(@NonNull Path themePath) throws IOException {
|
||||
return FileUtils.findRootPath(themePath,
|
||||
path -> StringUtils.equalsAny(path.getFileName().toString(), "theme.yaml", "theme.yml"))
|
||||
.orElseThrow(
|
||||
() -> new BadRequestException("无法准确定位到主题根目录,请确认主题目录中包含 theme.yml(theme.yaml)。"));
|
||||
path -> StringUtils.equalsAny(path.getFileName().toString(),
|
||||
"theme.yaml", "theme.yml"))
|
||||
.orElseThrow(() ->
|
||||
new BadRequestException("无法准确定位到主题根目录,请确认主题目录中包含 theme.yml(theme.yaml)。"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import java.io.IOException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.transport.TagOpt;
|
||||
import run.halo.app.exception.ThemePropertyMissingException;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
import run.halo.app.utils.GitUtils;
|
||||
|
||||
/**
|
||||
* Git theme fetcher.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public class GitThemeFetcher implements ThemeFetcher {
|
||||
|
||||
@Override
|
||||
public boolean support(Object source) {
|
||||
if (source instanceof String) {
|
||||
return ((String) source).endsWith(".git");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty fetch(Object source) {
|
||||
final var repoUrl = source.toString();
|
||||
|
||||
try {
|
||||
// create temp folder
|
||||
final var tempDirectory = FileUtils.createTempDirectory();
|
||||
|
||||
// clone from git
|
||||
log.info("Cloning git repo {} to {}", repoUrl, tempDirectory);
|
||||
try (final var git = Git.cloneRepository()
|
||||
.setTagOption(TagOpt.FETCH_TAGS)
|
||||
.setNoCheckout(false)
|
||||
.setDirectory(tempDirectory.toFile())
|
||||
.setCloneSubmodules(false)
|
||||
.setURI(repoUrl)
|
||||
.setRemote("upstream")
|
||||
.call()) {
|
||||
log.info("Cloned git repo {} to {} successfully", repoUrl, tempDirectory);
|
||||
|
||||
// find latest tag
|
||||
final var latestTag = GitUtils.getLatestTag(git);
|
||||
final var checkoutCommand = git.checkout()
|
||||
.setName("halo")
|
||||
.setCreateBranch(true);
|
||||
if (latestTag != null) {
|
||||
// checkout latest tag
|
||||
checkoutCommand.setStartPoint(latestTag.getValue());
|
||||
}
|
||||
Ref haloBranch = checkoutCommand.call();
|
||||
log.info("Checkout branch: {}", haloBranch.getName());
|
||||
}
|
||||
|
||||
// locate theme property location
|
||||
var themePropertyPath = ThemeMetaLocator.INSTANCE.locateProperty(tempDirectory)
|
||||
.orElseThrow(() -> new ThemePropertyMissingException("主题配置文件缺失,请确认后重试!"));
|
||||
|
||||
// fetch property
|
||||
return ThemePropertyScanner.INSTANCE.fetchThemeProperty(themePropertyPath.getParent())
|
||||
.orElseThrow();
|
||||
} catch (IOException | GitAPIException e) {
|
||||
throw new RuntimeException("主题拉取失败!(" + e.getMessage() + ")", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static run.halo.app.theme.ThemeUpdater.backup;
|
||||
import static run.halo.app.theme.ThemeUpdater.restore;
|
||||
import static run.halo.app.utils.GitUtils.commitAutomatically;
|
||||
import static run.halo.app.utils.GitUtils.logCommit;
|
||||
import static run.halo.app.utils.GitUtils.removeRemoteIfExists;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Paths;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.RebaseCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.RepositoryState;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.exception.ServiceException;
|
||||
import run.halo.app.exception.ThemeUpdateException;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.repository.ThemeRepository;
|
||||
|
||||
/**
|
||||
* Update from theme property config.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public class GitThemeUpdater implements ThemeUpdater {
|
||||
|
||||
private final ThemeRepository themeRepository;
|
||||
|
||||
private final ThemeFetcherComposite fetcherComposite;
|
||||
|
||||
public GitThemeUpdater(ThemeRepository themeRepository,
|
||||
ThemeFetcherComposite fetcherComposite) {
|
||||
this.themeRepository = themeRepository;
|
||||
this.fetcherComposite = fetcherComposite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty update(String themeId) throws IOException {
|
||||
// get theme property
|
||||
final var oldThemeProperty = themeRepository.fetchThemePropertyByThemeId(themeId)
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("主题 " + themeId + " 不存在或以删除!").setErrorData(themeId));
|
||||
|
||||
// get update config
|
||||
final var gitRepo = oldThemeProperty.getRepo();
|
||||
|
||||
// fetch latest theme
|
||||
final var newThemeProperty = fetcherComposite.fetch(gitRepo);
|
||||
|
||||
// merge old theme and new theme
|
||||
final var mergedThemeProperty = merge(oldThemeProperty, newThemeProperty);
|
||||
|
||||
// backup old theme
|
||||
final var backupPath = backup(oldThemeProperty);
|
||||
|
||||
try {
|
||||
// delete old theme
|
||||
themeRepository.deleteTheme(oldThemeProperty);
|
||||
|
||||
// copy new theme to old theme folder
|
||||
return themeRepository.attemptToAdd(mergedThemeProperty);
|
||||
} catch (Throwable t) {
|
||||
log.error("Failed to add new theme, and restoring old theme from " + backupPath, t);
|
||||
// restore old theme
|
||||
restore(backupPath, oldThemeProperty);
|
||||
log.info("Restored old theme from path: {}", backupPath);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
public ThemeProperty merge(ThemeProperty oldThemeProperty, ThemeProperty newThemeProperty)
|
||||
throws IOException {
|
||||
|
||||
final var oldThemePath = Paths.get(oldThemeProperty.getThemePath());
|
||||
// open old git repo
|
||||
try (final var oldGit = Git.open(oldThemePath.toFile())) {
|
||||
// 0. commit old repo
|
||||
commitAutomatically(oldGit);
|
||||
|
||||
final var newThemePath = Paths.get(newThemeProperty.getThemePath());
|
||||
// trying to open new git repo
|
||||
try (final var ignored = Git.open(newThemePath.toFile())) {
|
||||
// remove remote
|
||||
removeRemoteIfExists(oldGit, "newTheme");
|
||||
// add this new git to remote for old repo
|
||||
final var addedRemoteConfig = oldGit.remoteAdd()
|
||||
.setName("newTheme")
|
||||
.setUri(new URIish(newThemePath.toString()))
|
||||
.call();
|
||||
log.info("git remote add newTheme {} {}",
|
||||
addedRemoteConfig.getName(),
|
||||
addedRemoteConfig.getURIs());
|
||||
|
||||
// fetch remote data
|
||||
final var remote = "newTheme/halo";
|
||||
log.info("git fetch newTheme/halo");
|
||||
final var fetchResult = oldGit.fetch()
|
||||
.setRemote("newTheme")
|
||||
.call();
|
||||
log.info("Fetch result: {}", fetchResult.getMessages());
|
||||
|
||||
// rebase upstream
|
||||
log.info("git rebase newTheme");
|
||||
final var rebaseResult = oldGit.rebase()
|
||||
.setUpstream(remote)
|
||||
.call();
|
||||
log.info("Rebase result: {}", rebaseResult.getStatus());
|
||||
logCommit(rebaseResult.getCurrentCommit());
|
||||
|
||||
// check rebase result
|
||||
if (!rebaseResult.getStatus().isSuccessful()) {
|
||||
if (oldGit.getRepository().getRepositoryState() != RepositoryState.SAFE) {
|
||||
// if rebasing stopped or failed, you can get back to the original state by
|
||||
// running it
|
||||
// with setOperation(RebaseCommand.Operation.ABORT)
|
||||
final var abortRebaseResult = oldGit.rebase()
|
||||
.setUpstream(remote)
|
||||
.setOperation(RebaseCommand.Operation.ABORT)
|
||||
.call();
|
||||
log.error("Aborted rebase with state: {} : {}",
|
||||
abortRebaseResult.getStatus(),
|
||||
abortRebaseResult.getConflicts());
|
||||
}
|
||||
throw new ThemeUpdateException("无法自动合并最新文件!请尝试删除主题并重新拉取。");
|
||||
}
|
||||
}
|
||||
} catch (URISyntaxException | GitAPIException e) {
|
||||
throw new ServiceException("合并主题失败!请确认该主题支持在线更新。", e);
|
||||
}
|
||||
|
||||
return newThemeProperty;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import run.halo.app.exception.BadRequestException;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.repository.ThemeRepository;
|
||||
|
||||
/**
|
||||
* Multipart file theme updater.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public class MultipartFileThemeUpdater implements ThemeUpdater {
|
||||
|
||||
private final MultipartFile file;
|
||||
|
||||
private final ThemeFetcherComposite fetcherComposite;
|
||||
|
||||
private final ThemeRepository themeRepository;
|
||||
|
||||
public MultipartFileThemeUpdater(MultipartFile file,
|
||||
ThemeFetcherComposite fetcherComposite,
|
||||
ThemeRepository themeRepository) {
|
||||
this.file = file;
|
||||
this.fetcherComposite = fetcherComposite;
|
||||
this.themeRepository = themeRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty update(String themeId) throws IOException {
|
||||
// check old theme id
|
||||
final var oldThemeProperty = this.themeRepository.fetchThemePropertyByThemeId(themeId)
|
||||
.orElseThrow(() -> new NotFoundException("主题 ID 为 " + themeId + " 不存在或已删除!"));
|
||||
|
||||
// fetch new theme
|
||||
final var newThemeProperty = this.fetcherComposite.fetch(this.file);
|
||||
|
||||
if (!Objects.equals(oldThemeProperty.getId(), newThemeProperty.getId())) {
|
||||
log.error("Expected theme: {}, but provided theme: {}",
|
||||
oldThemeProperty.getId(),
|
||||
newThemeProperty.getId());
|
||||
// clear new theme folder
|
||||
this.themeRepository.deleteTheme(newThemeProperty);
|
||||
throw new BadRequestException("上传的主题 "
|
||||
+ newThemeProperty.getId()
|
||||
+ " 和当前主题的 "
|
||||
+ oldThemeProperty.getId()
|
||||
+ " 不一致,无法进行更新操作!");
|
||||
}
|
||||
|
||||
// backup old theme
|
||||
final var backupPath = ThemeUpdater.backup(oldThemeProperty);
|
||||
|
||||
try {
|
||||
// delete old theme
|
||||
themeRepository.deleteTheme(oldThemeProperty);
|
||||
|
||||
// add new theme
|
||||
return themeRepository.attemptToAdd(newThemeProperty);
|
||||
} catch (Throwable t) {
|
||||
log.error("Failed to add new theme, and restoring old theme from " + backupPath, t);
|
||||
ThemeUpdater.restore(backupPath, oldThemeProperty);
|
||||
log.info("Restored old theme from path: {}", backupPath);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static run.halo.app.utils.FileUtils.unzip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import run.halo.app.exception.ServiceException;
|
||||
import run.halo.app.exception.ThemePropertyMissingException;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Multipart zip file theme fetcher.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public class MultipartZipFileThemeFetcher implements ThemeFetcher {
|
||||
|
||||
@Override
|
||||
public boolean support(Object source) {
|
||||
if (source instanceof MultipartFile) {
|
||||
final var filename = ((MultipartFile) source).getOriginalFilename();
|
||||
return filename != null && filename.endsWith(".zip");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty fetch(Object source) {
|
||||
final var file = (MultipartFile) source;
|
||||
|
||||
try (var zis = new ZipInputStream(file.getInputStream())) {
|
||||
final var tempDirectory = FileUtils.createTempDirectory();
|
||||
log.info("Unzipping {} to path {}", file.getOriginalFilename(), tempDirectory);
|
||||
unzip(zis, tempDirectory);
|
||||
return ThemePropertyScanner.INSTANCE.fetchThemeProperty(tempDirectory)
|
||||
.orElseThrow(() -> new ThemePropertyMissingException("主题配置文件缺失!请确认后重试。"));
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("主题上传失败!", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
|
||||
/**
|
||||
* Remote theme fetcher interface.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public interface ThemeFetcher {
|
||||
|
||||
/**
|
||||
* Check whether source is supported or not.
|
||||
*
|
||||
* @param source input stream or remote git uri
|
||||
* @return true if supported, false otherwise
|
||||
*/
|
||||
boolean support(Object source);
|
||||
|
||||
/**
|
||||
* Fetch theme from source.
|
||||
*
|
||||
* @param source input stream or remote git uri
|
||||
* @return theme property
|
||||
*/
|
||||
ThemeProperty fetch(Object source);
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
|
||||
/**
|
||||
* Theme fetcher composite.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public class ThemeFetcherComposite implements ThemeFetcher {
|
||||
|
||||
/**
|
||||
* Theme fetcher container.
|
||||
*/
|
||||
private final List<ThemeFetcher> themeFetchers = new ArrayList<>(4);
|
||||
|
||||
/**
|
||||
* Fallback theme fetcher.
|
||||
*/
|
||||
private final ThemeFetcher fallbackFetcher = new GitThemeFetcher();
|
||||
|
||||
public ThemeFetcherComposite addFetcher(ThemeFetcher fetcher) {
|
||||
this.themeFetchers.add(fetcher);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ThemeFetcherComposite addFetcher(ThemeFetcher... fetchers) {
|
||||
if (fetchers != null) {
|
||||
Collections.addAll(this.themeFetchers, fetchers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<ThemeFetcher> getFetchers() {
|
||||
return Collections.unmodifiableList(this.themeFetchers);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.themeFetchers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(Object source) {
|
||||
return getThemeFetcher(source).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty fetch(Object source) {
|
||||
final var themeFetcher = getThemeFetcher(source).orElse(fallbackFetcher);
|
||||
return themeFetcher.fetch(source);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Optional<ThemeFetcher> getThemeFetcher(Object source) {
|
||||
return themeFetchers.stream()
|
||||
.filter(fetcher -> fetcher.support(source))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.equalsAnyIgnoreCase;
|
||||
import static run.halo.app.utils.FileUtils.findPath;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* Theme meta data locator.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public enum ThemeMetaLocator {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
/**
|
||||
* Theme property filenames.
|
||||
*/
|
||||
private static final String[] THEME_PROPERTY_FILENAMES = new String[] {
|
||||
"theme.yaml",
|
||||
"theme.yml",
|
||||
};
|
||||
|
||||
private static final String[] THEME_SETTING_FILENAMES = new String[] {
|
||||
"settings.yaml",
|
||||
"settings.yml",
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme screenshots name.
|
||||
*/
|
||||
private static final String THEME_SCREENSHOTS_NAME = "screenshot";
|
||||
|
||||
/**
|
||||
* Locate theme root folder.
|
||||
*
|
||||
* @param path the given path must not be null
|
||||
* @return root path or empty
|
||||
*/
|
||||
@NonNull
|
||||
public Optional<Path> locateThemeRoot(@NonNull Path path) {
|
||||
return locateProperty(path).map(Path::getParent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate theme property path.
|
||||
*
|
||||
* @return theme property path or empty
|
||||
*/
|
||||
@NonNull
|
||||
public Optional<Path> locateProperty(@NonNull Path path) {
|
||||
try {
|
||||
var predicate = ((Predicate<Path>)
|
||||
Files::isRegularFile)
|
||||
.and(Files::isReadable)
|
||||
.and(
|
||||
p -> equalsAnyIgnoreCase(p.getFileName().toString(), THEME_PROPERTY_FILENAMES));
|
||||
|
||||
log.debug("Locating property in path: {}", path);
|
||||
return findPath(path, 3, predicate);
|
||||
} catch (IOException e) {
|
||||
log.warn("Error occurred while finding theme root path", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate theme setting path.
|
||||
*
|
||||
* @return theme setting path or empty
|
||||
*/
|
||||
@NonNull
|
||||
public Optional<Path> locateSetting(@NonNull Path path) {
|
||||
return locateThemeRoot(path).flatMap(root -> {
|
||||
try {
|
||||
var predicate = ((Predicate<Path>)
|
||||
Files::isRegularFile)
|
||||
.and(Files::isReadable)
|
||||
.and(p -> equalsAnyIgnoreCase(p.getFileName().toString(),
|
||||
THEME_SETTING_FILENAMES));
|
||||
log.debug("Locating setting from {}", path);
|
||||
return findPath(path, 3, predicate);
|
||||
} catch (IOException e) {
|
||||
log.warn("Error occurred while finding theme root path", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate screenshot.
|
||||
*
|
||||
* @param path root path
|
||||
* @return screenshot path or empty
|
||||
*/
|
||||
@NonNull
|
||||
public Optional<Path> locateScreenshot(@NonNull Path path) {
|
||||
return locateThemeRoot(path).flatMap(root -> {
|
||||
try (var pathStream = Files.list(root)) {
|
||||
var predicate = ((Predicate<Path>) Files::isRegularFile)
|
||||
.and(Files::isReadable)
|
||||
.and(p -> p.getFileName().toString().startsWith(THEME_SCREENSHOTS_NAME));
|
||||
log.debug("Locating screenshot from path: {}", path);
|
||||
return pathStream.filter(predicate).findFirst();
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to list path: " + path, e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static run.halo.app.service.ThemeService.SETTINGS_NAMES;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
|
@ -21,7 +18,6 @@ import org.springframework.util.CollectionUtils;
|
|||
import run.halo.app.handler.theme.config.ThemePropertyResolver;
|
||||
import run.halo.app.handler.theme.config.impl.YamlThemePropertyResolver;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.utils.FilenameUtils;
|
||||
|
||||
/**
|
||||
* Theme property scanner.
|
||||
|
@ -33,14 +29,6 @@ public enum ThemePropertyScanner {
|
|||
|
||||
INSTANCE;
|
||||
|
||||
/**
|
||||
* Theme property file name.
|
||||
*/
|
||||
private static final String[] THEME_PROPERTY_FILE_NAMES = {"theme.yaml", "theme.yml"};
|
||||
/**
|
||||
* Theme screenshots name.
|
||||
*/
|
||||
private static final String THEME_SCREENSHOTS_NAME = "screenshot";
|
||||
private final ThemePropertyResolver propertyResolver = new YamlThemePropertyResolver();
|
||||
|
||||
/**
|
||||
|
@ -91,93 +79,42 @@ public enum ThemePropertyScanner {
|
|||
/**
|
||||
* Fetch theme property
|
||||
*
|
||||
* @param themePath theme path must not be null
|
||||
* @param themeRootPath theme root path must not be null
|
||||
* @return an optional theme property
|
||||
*/
|
||||
@NonNull
|
||||
public Optional<ThemeProperty> fetchThemeProperty(@NonNull Path themePath) {
|
||||
Assert.notNull(themePath, "Theme path must not be null");
|
||||
public Optional<ThemeProperty> fetchThemeProperty(@NonNull Path themeRootPath) {
|
||||
Assert.notNull(themeRootPath, "Theme path must not be null");
|
||||
|
||||
Optional<Path> optionalPath = fetchPropertyPath(themePath);
|
||||
return ThemeMetaLocator.INSTANCE.locateProperty(themeRootPath).map(propertyPath -> {
|
||||
final var rootPath = propertyPath.getParent();
|
||||
try {
|
||||
// Get property content
|
||||
final var propertyContent = Files.readString(propertyPath);
|
||||
|
||||
if (!optionalPath.isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// Resolve the base properties
|
||||
final var themeProperty = propertyResolver.resolve(propertyContent);
|
||||
|
||||
Path propertyPath = optionalPath.get();
|
||||
// Resolve additional properties
|
||||
themeProperty.setThemePath(rootPath.toString());
|
||||
themeProperty.setFolderName(rootPath.getFileName().toString());
|
||||
themeProperty.setHasOptions(hasOptions(rootPath));
|
||||
themeProperty.setActivated(false);
|
||||
|
||||
try {
|
||||
// Get property content
|
||||
String propertyContent =
|
||||
new String(Files.readAllBytes(propertyPath), StandardCharsets.UTF_8);
|
||||
|
||||
// Resolve the base properties
|
||||
ThemeProperty themeProperty = propertyResolver.resolve(propertyContent);
|
||||
|
||||
// Resolve additional properties
|
||||
themeProperty.setThemePath(themePath.toString());
|
||||
themeProperty.setFolderName(themePath.getFileName().toString());
|
||||
themeProperty.setHasOptions(hasOptions(themePath));
|
||||
themeProperty.setActivated(false);
|
||||
|
||||
// Set screenshots
|
||||
getScreenshotsFileName(themePath).ifPresent(
|
||||
screenshotsName -> themeProperty.setScreenshots(StringUtils.join("/themes/",
|
||||
FilenameUtils.getBasename(themeProperty.getThemePath()),
|
||||
"/",
|
||||
screenshotsName)));
|
||||
|
||||
return Optional.of(themeProperty);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to load theme property file", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets screenshots file name.
|
||||
*
|
||||
* @param themePath theme path must not be null
|
||||
* @return screenshots file name or null if the given theme path has not screenshots
|
||||
* @throws IOException throws when listing files
|
||||
*/
|
||||
@NonNull
|
||||
private Optional<String> getScreenshotsFileName(@NonNull Path themePath) throws IOException {
|
||||
Assert.notNull(themePath, "Theme path must not be null");
|
||||
|
||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||
return pathStream.filter(path -> Files.isRegularFile(path)
|
||||
&& Files.isReadable(path)
|
||||
&&
|
||||
FilenameUtils.getBasename(path.toString()).equalsIgnoreCase(THEME_SCREENSHOTS_NAME))
|
||||
.findFirst()
|
||||
.map(path -> path.getFileName().toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets property path of nullable.
|
||||
*
|
||||
* @param themePath theme path.
|
||||
* @return an optional property path
|
||||
*/
|
||||
@NonNull
|
||||
private Optional<Path> fetchPropertyPath(@NonNull Path themePath) {
|
||||
Assert.notNull(themePath, "Theme path must not be null");
|
||||
|
||||
for (String propertyPathName : THEME_PROPERTY_FILE_NAMES) {
|
||||
Path propertyPath = themePath.resolve(propertyPathName);
|
||||
|
||||
log.debug("Attempting to find property file: [{}]", propertyPath);
|
||||
if (Files.exists(propertyPath) && Files.isReadable(propertyPath)) {
|
||||
log.debug("Found property file: [{}]", propertyPath);
|
||||
return Optional.of(propertyPath);
|
||||
// resolve screenshot
|
||||
ThemeMetaLocator.INSTANCE.locateScreenshot(rootPath).ifPresent(screenshotPath -> {
|
||||
final var screenshotRelPath = StringUtils.join("/themes/",
|
||||
themeProperty.getFolderName(),
|
||||
"/",
|
||||
screenshotPath.getFileName().toString());
|
||||
themeProperty.setScreenshots(screenshotRelPath);
|
||||
});
|
||||
return themeProperty;
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to load theme property file", e);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Property file was not found in [{}]", themePath);
|
||||
|
||||
return Optional.empty();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,16 +126,6 @@ public enum ThemePropertyScanner {
|
|||
private boolean hasOptions(@NonNull Path themePath) {
|
||||
Assert.notNull(themePath, "Path must not be null");
|
||||
|
||||
for (String optionsName : SETTINGS_NAMES) {
|
||||
// Resolve the options path
|
||||
Path optionsPath = themePath.resolve(optionsName);
|
||||
|
||||
log.debug("Check options file for path: [{}]", optionsPath);
|
||||
|
||||
if (Files.exists(optionsPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return ThemeMetaLocator.INSTANCE.locateSetting(themePath).isPresent();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static run.halo.app.utils.FileUtils.copyFolder;
|
||||
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Theme updater.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public interface ThemeUpdater {
|
||||
|
||||
Logger log = LoggerFactory.getLogger(ThemeUpdater.class);
|
||||
|
||||
/**
|
||||
* Update theme property.
|
||||
*
|
||||
* @param themeId theme id
|
||||
* @return updated theme property
|
||||
*/
|
||||
ThemeProperty update(String themeId) throws IOException;
|
||||
|
||||
/**
|
||||
* Backup old theme.
|
||||
*
|
||||
* @param themeProperty theme property
|
||||
* @return theme backup path
|
||||
* @throws IOException throws io exception
|
||||
*/
|
||||
static Path backup(final ThemeProperty themeProperty) throws IOException {
|
||||
final var themePath = Paths.get(themeProperty.getThemePath());
|
||||
Path tempDirectory = null;
|
||||
try {
|
||||
tempDirectory = FileUtils.createTempDirectory();
|
||||
copyFolder(themePath, tempDirectory);
|
||||
log.info("Backup theme: {} to {} successfully!", themeProperty.getId(), tempDirectory);
|
||||
return tempDirectory;
|
||||
} catch (IOException e) {
|
||||
// clear temp directory
|
||||
deleteFolderQuietly(tempDirectory);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
static void restore(final Path backupPath, final ThemeProperty oldThemeProperty)
|
||||
throws IOException {
|
||||
final var targetPath = Paths.get(oldThemeProperty.getThemePath());
|
||||
log.info("Restoring backup path: {} to target path: {}", backupPath, targetPath);
|
||||
// copy backup to target path
|
||||
FileUtils.copyFolder(backupPath, targetPath);
|
||||
log.debug("Copied backup path: {} to target path: {} successfully!", backupPath,
|
||||
targetPath);
|
||||
// delete backup
|
||||
FileUtils.deleteFolderQuietly(backupPath);
|
||||
log.debug("Deleted backup path: {} successfully!", backupPath);
|
||||
log.info("Restored backup path: {} to target path: {} successfully!", backupPath,
|
||||
targetPath);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static run.halo.app.utils.FileUtils.unzip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import run.halo.app.exception.ThemePropertyMissingException;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Zip theme fetcher.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public class ZipThemeFetcher implements ThemeFetcher {
|
||||
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public ZipThemeFetcher() {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.connectTimeout(Duration.ofMinutes(5))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(Object source) {
|
||||
if (source instanceof String) {
|
||||
return ((String) source).endsWith(".zip");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeProperty fetch(Object source) {
|
||||
final var themeZipLink = source.toString();
|
||||
|
||||
// build http request
|
||||
final var request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(themeZipLink))
|
||||
.timeout(Duration.ofMinutes(2))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
try {
|
||||
// request from remote
|
||||
log.info("Fetching theme from {}", themeZipLink);
|
||||
var inputStreamResponse =
|
||||
httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
var inputStream = inputStreamResponse.body();
|
||||
|
||||
// unzip zip archive
|
||||
try (var zipInputStream = new ZipInputStream(inputStream)) {
|
||||
var tempDirectory = FileUtils.createTempDirectory();
|
||||
log.info("Unzipping theme {} to {}", themeZipLink, tempDirectory);
|
||||
unzip(zipInputStream, tempDirectory);
|
||||
|
||||
// resolve theme property
|
||||
return ThemePropertyScanner.INSTANCE.fetchThemeProperty(tempDirectory)
|
||||
.orElseThrow(() -> new ThemePropertyMissingException("主题配置文件缺失!请确认后重试。"));
|
||||
}
|
||||
} catch (InterruptedException | IOException e) {
|
||||
throw new RuntimeException("主题拉取失败!(" + e.getMessage() + ")", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,6 @@ import java.nio.file.Paths;
|
|||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
@ -39,11 +38,6 @@ import run.halo.app.exception.ForbiddenException;
|
|||
@Slf4j
|
||||
public class FileUtils {
|
||||
|
||||
/**
|
||||
* Ignored folders while finding root path.
|
||||
*/
|
||||
private static final List<String> IGNORED_FOLDERS = Arrays.asList(".git");
|
||||
|
||||
private FileUtils() {
|
||||
}
|
||||
|
||||
|
@ -57,12 +51,12 @@ public class FileUtils {
|
|||
Assert.notNull(source, "Source path must not be null");
|
||||
Assert.notNull(target, "Target path must not be null");
|
||||
|
||||
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
|
||||
Files.walkFileTree(source, new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
Path current = target.resolve(source.relativize(dir).toString());
|
||||
Path current = target.resolve(source.relativize(dir));
|
||||
Files.createDirectories(current);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
@ -70,7 +64,7 @@ public class FileUtils {
|
|||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
Files.copy(file, target.resolve(source.relativize(file).toString()),
|
||||
Files.copy(file, target.resolve(source.relativize(file)),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
@ -255,44 +249,117 @@ public class FileUtils {
|
|||
*/
|
||||
@NonNull
|
||||
public static Optional<Path> findRootPath(@NonNull final Path path,
|
||||
@Nullable final Predicate<Path> pathPredicate) throws IOException {
|
||||
@Nullable final Predicate<Path> pathPredicate)
|
||||
throws IOException {
|
||||
return findRootPath(path, Integer.MAX_VALUE, pathPredicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find root path.
|
||||
*
|
||||
* @param path super root path starter
|
||||
* @param maxDepth max loop depth
|
||||
* @param pathPredicate path predicate
|
||||
* @return empty if path is not a directory or the given path predicate is null
|
||||
* @throws IOException IO exception
|
||||
*/
|
||||
@NonNull
|
||||
public static Optional<Path> findRootPath(@NonNull final Path path,
|
||||
int maxDepth,
|
||||
@Nullable final Predicate<Path> pathPredicate)
|
||||
throws IOException {
|
||||
return findPath(path, maxDepth, pathPredicate).map(Path::getParent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find path.
|
||||
*
|
||||
* @param path super root path starter
|
||||
* @param pathPredicate path predicate
|
||||
* @return empty if path is not a directory or the given path predicate is null
|
||||
* @throws IOException IO exception
|
||||
*/
|
||||
@NonNull
|
||||
public static Optional<Path> findPath(@NonNull final Path path,
|
||||
@Nullable final Predicate<Path> pathPredicate)
|
||||
throws IOException {
|
||||
return findPath(path, Integer.MAX_VALUE, pathPredicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find path.
|
||||
*
|
||||
* @param path super root path starter
|
||||
* @param pathPredicate path predicate
|
||||
* @return empty if path is not a directory or the given path predicate is null
|
||||
* @throws IOException IO exception
|
||||
*/
|
||||
@NonNull
|
||||
public static Optional<Path> findPath(@NonNull final Path path,
|
||||
int maxDepth,
|
||||
@Nullable final Predicate<Path> pathPredicate)
|
||||
throws IOException {
|
||||
Assert.isTrue(maxDepth > 0, "Max depth must not be less than 1");
|
||||
if (!Files.isDirectory(path) || pathPredicate == null) {
|
||||
// if the path is not a directory or the given path predicate is null, then return an
|
||||
// empty optional
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
log.debug("Trying to find root path from [{}]", path);
|
||||
log.debug("Trying to find path from [{}]", path);
|
||||
|
||||
// the queue holds folders which may be root
|
||||
final LinkedList<Path> queue = new LinkedList<>();
|
||||
final var queue = new LinkedList<Path>();
|
||||
// depth container
|
||||
final var depthQueue = new LinkedList<Integer>();
|
||||
|
||||
// init queue
|
||||
queue.push(path);
|
||||
while (!queue.isEmpty()) {
|
||||
depthQueue.push(1);
|
||||
|
||||
boolean found = false;
|
||||
Path result = null;
|
||||
while (!found && !queue.isEmpty()) {
|
||||
// pop the first path as candidate root path
|
||||
final Path rootPath = queue.pop();
|
||||
final var rootPath = queue.pop();
|
||||
final int depth = depthQueue.pop();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Peek({}) into {}", depth, rootPath);
|
||||
}
|
||||
try (final Stream<Path> childrenPaths = Files.list(rootPath)) {
|
||||
List<Path> subFolders = new LinkedList<>();
|
||||
Optional<Path> matchedPath = childrenPaths.peek(child -> {
|
||||
if (Files.isDirectory(child)) {
|
||||
// collect directory
|
||||
subFolders.add(child);
|
||||
final var subFolders = new LinkedList<Path>();
|
||||
var resultPath = childrenPaths
|
||||
.peek(p -> {
|
||||
if (Files.isDirectory(p)) {
|
||||
subFolders.add(p);
|
||||
}
|
||||
})
|
||||
.filter(pathPredicate)
|
||||
.findFirst();
|
||||
if (resultPath.isPresent()) {
|
||||
queue.clear();
|
||||
depthQueue.clear();
|
||||
// return current result path
|
||||
found = true;
|
||||
result = resultPath.get();
|
||||
} else {
|
||||
// put all directory into queue
|
||||
if (depth < maxDepth) {
|
||||
for (Path subFolder : subFolders) {
|
||||
if (!Files.isHidden(subFolder)) {
|
||||
// skip hidden folder
|
||||
queue.push(subFolder);
|
||||
depthQueue.push(depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter(pathPredicate).findAny();
|
||||
if (matchedPath.isPresent()) {
|
||||
log.debug("Found root path: [{}]", rootPath);
|
||||
return Optional.of(rootPath);
|
||||
}
|
||||
// add all folder into queue
|
||||
subFolders.forEach(e -> {
|
||||
// if
|
||||
if (!IGNORED_FOLDERS.contains(e.getFileName().toString())) {
|
||||
queue.push(e);
|
||||
}
|
||||
});
|
||||
subFolders.clear();
|
||||
}
|
||||
}
|
||||
// if tests are failed completely
|
||||
return Optional.empty();
|
||||
|
||||
log.debug("Found path: [{}]", result);
|
||||
return Optional.ofNullable(result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -427,7 +494,7 @@ public class FileUtils {
|
|||
FileUtils.deleteFolder(deletingPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete " + deletingPath);
|
||||
log.warn("Failed to delete {}", deletingPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,7 +507,9 @@ public class FileUtils {
|
|||
*/
|
||||
@NonNull
|
||||
public static Path createTempDirectory() throws IOException {
|
||||
return Files.createTempDirectory("halo");
|
||||
final var tempDirectory = Files.createTempDirectory("halo");
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> deleteFolderQuietly(tempDirectory)));
|
||||
return tempDirectory;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,17 +5,26 @@ import java.nio.file.Path;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.api.errors.InvalidRemoteException;
|
||||
import org.eclipse.jgit.api.errors.TransportException;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.storage.file.WindowCacheConfig;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevSort;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.transport.RemoteConfig;
|
||||
import org.eclipse.jgit.treewalk.filter.TreeFilter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Git utilities.
|
||||
|
@ -27,66 +36,27 @@ import org.springframework.util.Assert;
|
|||
public class GitUtils {
|
||||
|
||||
private GitUtils() {
|
||||
// Config packed git MMAP
|
||||
WindowCacheConfig config = new WindowCacheConfig();
|
||||
config.setPackedGitMMAP(false);
|
||||
config.install();
|
||||
}
|
||||
|
||||
public static void cloneFromGit(@NonNull String repoUrl, @NonNull Path targetPath)
|
||||
throws GitAPIException {
|
||||
Assert.hasText(repoUrl, "Repository remote url must not be blank");
|
||||
Assert.notNull(targetPath, "Target path must not be null");
|
||||
|
||||
log.debug("Trying to clone git repo [{}] to [{}]", repoUrl, targetPath);
|
||||
|
||||
// Use try-with-resource-statement
|
||||
Git git = null;
|
||||
try {
|
||||
git = Git.cloneRepository()
|
||||
.setURI(repoUrl)
|
||||
.setDirectory(targetPath.toFile())
|
||||
.call();
|
||||
log.debug("Cloned git repo [{}] successfully", repoUrl);
|
||||
} finally {
|
||||
closeQuietly(git);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(since = "1.4.2", forRemoval = true)
|
||||
public static void cloneFromGit(@NonNull String repoUrl, @NonNull Path targetPath,
|
||||
@NonNull String branchName) throws GitAPIException {
|
||||
Assert.hasText(repoUrl, "Repository remote url must not be blank");
|
||||
Assert.notNull(targetPath, "Target path must not be null");
|
||||
|
||||
Git git = null;
|
||||
try {
|
||||
git = Git.cloneRepository()
|
||||
try (
|
||||
Git ignored = Git.cloneRepository()
|
||||
.setURI(repoUrl)
|
||||
.setDirectory(targetPath.toFile())
|
||||
.setBranchesToClone(Collections.singletonList("refs/heads/" + branchName))
|
||||
.setCloneSubmodules(true)
|
||||
.setBranch("refs/heads/" + branchName)
|
||||
.call();
|
||||
} finally {
|
||||
closeQuietly(git);
|
||||
.call()) {
|
||||
// empty block placeholder
|
||||
}
|
||||
}
|
||||
|
||||
public static Git openOrInit(Path repoPath) throws IOException, GitAPIException {
|
||||
Git git;
|
||||
|
||||
try {
|
||||
git = Git.open(repoPath.toFile());
|
||||
} catch (RepositoryNotFoundException e) {
|
||||
log.warn(
|
||||
"Git repository may not exist, we will try to initialize an empty repository: [{}]",
|
||||
e.getMessage());
|
||||
git = Git.init().setDirectory(repoPath.toFile()).call();
|
||||
}
|
||||
|
||||
return git;
|
||||
}
|
||||
|
||||
public static List<String> getAllBranches(@NonNull String repoUrl) {
|
||||
public static List<String> getAllBranchesFromRemote(@NonNull String repoUrl) {
|
||||
List<String> branches = new ArrayList<>();
|
||||
try {
|
||||
Collection<Ref> refs = Git.lsRemoteRepository()
|
||||
|
@ -106,11 +76,84 @@ public class GitUtils {
|
|||
return branches;
|
||||
}
|
||||
|
||||
public static void closeQuietly(Git git) {
|
||||
if (git != null) {
|
||||
git.getRepository().close();
|
||||
git.close();
|
||||
@Nullable
|
||||
public static Pair<Ref, RevCommit> getLatestTag(final Git git)
|
||||
throws GitAPIException, IOException {
|
||||
final var tags = git.tagList().call();
|
||||
if (CollectionUtils.isEmpty(tags)) {
|
||||
return null;
|
||||
}
|
||||
try (final var revWalk = new RevWalk(git.getRepository())) {
|
||||
revWalk.reset();
|
||||
revWalk.setTreeFilter(TreeFilter.ANY_DIFF);
|
||||
revWalk.sort(RevSort.TOPO, true);
|
||||
revWalk.sort(RevSort.COMMIT_TIME_DESC, true);
|
||||
|
||||
final var commitTagMap = new HashMap<RevCommit, Ref>(tags.size());
|
||||
|
||||
for (final var tag : tags) {
|
||||
final var commit = revWalk.parseCommit(tag.getObjectId());
|
||||
commitTagMap.put(commit, tag);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("tag: {} with commit: {} {}", tag.getName(),
|
||||
commit.getFullMessage(), new Date(commit.getCommitTime() * 1000L));
|
||||
}
|
||||
}
|
||||
|
||||
return commitTagMap.keySet()
|
||||
.stream()
|
||||
.max(Comparator.comparing(RevCommit::getCommitTime))
|
||||
.map(latestCommit -> Pair.of(commitTagMap.get(latestCommit), latestCommit))
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeRemoteIfExists(final Git git, String remote) throws GitAPIException {
|
||||
final var remoteExists = git.remoteList()
|
||||
.call()
|
||||
.stream().map(RemoteConfig::getName)
|
||||
.anyMatch(name -> name.equals(remote));
|
||||
if (remoteExists) {
|
||||
// remove newRepo remote
|
||||
final var removedRemoteConfig = git.remoteRemove()
|
||||
.setRemoteName(remote)
|
||||
.call();
|
||||
log.info("git remote remove {} {}", removedRemoteConfig.getName(),
|
||||
removedRemoteConfig.getURIs());
|
||||
}
|
||||
}
|
||||
|
||||
public static void logCommit(final RevCommit commit) {
|
||||
if (commit == null) {
|
||||
return;
|
||||
}
|
||||
log.info("Commit result: {} {} {}",
|
||||
commit.getName(),
|
||||
commit.getFullMessage(),
|
||||
new Date(commit.getCommitTime() * 1000L));
|
||||
}
|
||||
|
||||
public static void commitAutomatically(final Git git) throws GitAPIException, IOException {
|
||||
// git status
|
||||
if (git.status().call().isClean()) {
|
||||
final var branch = git.getRepository().getBranch();
|
||||
final var fullBranch = git.getRepository().getFullBranch();
|
||||
log.info("Current branch {}", branch);
|
||||
log.info("Your branch is up to date with {}.", fullBranch);
|
||||
log.info("");
|
||||
log.info("nothing to commit, working tree clean");
|
||||
return;
|
||||
}
|
||||
// git add .
|
||||
git.add().addFilepattern(".").call();
|
||||
log.info("git add .");
|
||||
// git commit -m "Committed by halo automatically."
|
||||
final var commit = git.commit()
|
||||
.setSign(false)
|
||||
.setAuthor("halo", "hi@halo.run")
|
||||
.setMessage("Committed by halo automatically.")
|
||||
.call();
|
||||
log.info("git commit -m \"Committed by halo automatically.\"");
|
||||
logCommit(commit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package run.halo.app.utils;
|
||||
|
||||
/**
|
||||
* Remote git helper.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public class RemoteGitHelper {
|
||||
|
||||
/**
|
||||
* Remote git url. like: https://github.com/halo/halo-dev.git
|
||||
*/
|
||||
private final String remoteGitUrl;
|
||||
|
||||
public RemoteGitHelper(String remoteGitUrl) {
|
||||
this.remoteGitUrl = remoteGitUrl;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Git theme fetcher test.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
class GitThemeFetcherTest {
|
||||
|
||||
GitThemeFetcher gitThemeFetcher;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.gitThemeFetcher = new GitThemeFetcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Time consumption")
|
||||
void fetchTest() throws IOException, GitAPIException {
|
||||
final var repo = "https://gitee.com/xzhuz/halo-theme-xue";
|
||||
final var property = this.gitThemeFetcher.fetch(repo);
|
||||
final var themePath = Paths.get(property.getThemePath());
|
||||
try (final var git = Git.open(themePath.toFile())) {
|
||||
final var remoteConfigs = git.remoteList().call();
|
||||
assertEquals(1, remoteConfigs.size());
|
||||
assertEquals("upstream", remoteConfigs.get(0).getName());
|
||||
|
||||
List<Ref> refs = git.branchList().call();
|
||||
assertEquals(2, refs.size());
|
||||
assertEquals("refs/heads/halo", refs.get(0).getName());
|
||||
}
|
||||
|
||||
Path tempDirectory = FileUtils.createTempDirectory();
|
||||
// copy repo to temp folder
|
||||
FileUtils.copyFolder(themePath, tempDirectory);
|
||||
try (final var git = Git.open(tempDirectory.toFile())) {
|
||||
final var remoteConfigs = git.remoteList().call();
|
||||
assertEquals(1, remoteConfigs.size());
|
||||
assertEquals("upstream", remoteConfigs.get(0).getName());
|
||||
|
||||
List<Ref> refs = git.branchList().call();
|
||||
assertEquals(2, refs.size());
|
||||
assertEquals("refs/heads/halo", refs.get(0).getName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Zip remote theme fetcher test.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
class ZipThemeFetcherTest {
|
||||
|
||||
@Test
|
||||
@Disabled("Disabled due to time consumed")
|
||||
void fetch() {
|
||||
var themeFetcher = new ZipThemeFetcher();
|
||||
var themeProperty = themeFetcher.fetch("https://github.com/halo-dev/halo-theme-hshan/archive/master.zip");
|
||||
log.debug("{}", themeProperty);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ import java.util.stream.Collectors;
|
|||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.model.support.HaloConst;
|
||||
|
@ -30,19 +30,14 @@ class FileUtilsTest {
|
|||
|
||||
Path tempDirectory = null;
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() throws IOException {
|
||||
if (tempDirectory != null) {
|
||||
FileUtils.deleteFolder(tempDirectory);
|
||||
assertTrue(Files.notExists(tempDirectory));
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
tempDirectory = FileUtils.createTempDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteFolder() throws IOException {
|
||||
// Create a temp folder
|
||||
tempDirectory = Files.createTempDirectory("halo-test");
|
||||
|
||||
Path testPath = tempDirectory.resolve("test/test/test");
|
||||
|
||||
// Create test folders
|
||||
|
@ -75,8 +70,6 @@ class FileUtilsTest {
|
|||
|
||||
@Test
|
||||
void zipFolderTest() throws IOException {
|
||||
// Create some temporary files
|
||||
tempDirectory = Files.createTempDirectory("zip-root-");
|
||||
log.debug("Folder name: [{}]", tempDirectory.getFileName());
|
||||
Files.createTempFile(tempDirectory, "zip-file1-", ".txt");
|
||||
Files.createTempFile(tempDirectory, "zip-file2-", ".txt");
|
||||
|
@ -118,9 +111,6 @@ class FileUtilsTest {
|
|||
|
||||
@Test
|
||||
void testRenameFile() throws IOException {
|
||||
// Create a temp folder
|
||||
tempDirectory = Files.createTempDirectory("halo-test");
|
||||
|
||||
Path testPath = tempDirectory.resolve("test/test");
|
||||
Path filePath = tempDirectory.resolve("test/test/test.file");
|
||||
|
||||
|
@ -143,9 +133,6 @@ class FileUtilsTest {
|
|||
|
||||
@Test
|
||||
void testRenameFolder() throws IOException {
|
||||
// Create a temp folder
|
||||
tempDirectory = Files.createTempDirectory("halo-test");
|
||||
|
||||
Path testPath = tempDirectory.resolve("test/test");
|
||||
Path filePath = tempDirectory.resolve("test/test.file");
|
||||
|
||||
|
@ -163,9 +150,6 @@ class FileUtilsTest {
|
|||
|
||||
@Test
|
||||
void testRenameRepeat() throws IOException {
|
||||
// Create a temp folder
|
||||
tempDirectory = Files.createTempDirectory("halo-test");
|
||||
|
||||
Path testPathOne = tempDirectory.resolve("test/testOne");
|
||||
Path testPathTwo = tempDirectory.resolve("test/testTwo");
|
||||
Path filePathOne = tempDirectory.resolve("test/testOne.file");
|
||||
|
@ -205,9 +189,6 @@ class FileUtilsTest {
|
|||
// file2
|
||||
// folder3
|
||||
// expected_file
|
||||
// expected: folder2
|
||||
tempDirectory = Files.createTempDirectory("halo-test");
|
||||
|
||||
log.info("Preparing test folder structure");
|
||||
Path folder1 = tempDirectory.resolve("folder1");
|
||||
Files.createDirectory(folder1);
|
||||
|
@ -223,11 +204,12 @@ class FileUtilsTest {
|
|||
Files.createFile(expectedFile);
|
||||
log.info("Prepared test folder structure");
|
||||
|
||||
// find the root folder where expected file locates, and we expect folder3
|
||||
Optional<Path> rootPath = FileUtils.findRootPath(tempDirectory,
|
||||
// find expected_file
|
||||
final var resultPath = FileUtils.findRootPath(tempDirectory,
|
||||
path -> path.getFileName().toString().equals("expected_file"));
|
||||
assertTrue(rootPath.isPresent());
|
||||
assertEquals(folder3.toString(), rootPath.get().toString());
|
||||
assertTrue(resultPath.isPresent());
|
||||
log.debug("Got result path: {}", resultPath.get());
|
||||
assertEquals(folder3.toString(), resultPath.get().toString());
|
||||
}
|
||||
|
||||
|
||||
|
@ -242,9 +224,6 @@ class FileUtilsTest {
|
|||
// file2
|
||||
// folder3
|
||||
// file3
|
||||
// expected: folder2
|
||||
tempDirectory = Files.createTempDirectory("halo-test");
|
||||
|
||||
log.info("Preparing test folder structure");
|
||||
Path folder1 = tempDirectory.resolve("folder1");
|
||||
Files.createDirectory(folder1);
|
||||
|
@ -269,4 +248,62 @@ class FileUtilsTest {
|
|||
path -> path.getFileName().toString().equals("expected_file"));
|
||||
assertFalse(rootPath.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findRootPathWithLowerMaxDepth() throws IOException {
|
||||
// build folder structure
|
||||
// folder1
|
||||
// file1
|
||||
// folder2
|
||||
// file2
|
||||
// folder3
|
||||
// expected_file
|
||||
log.info("Preparing test folder structure");
|
||||
Path folder1 = tempDirectory.resolve("folder1");
|
||||
Files.createDirectory(folder1);
|
||||
Path file1 = tempDirectory.resolve("file1");
|
||||
Files.createFile(file1);
|
||||
Path folder2 = tempDirectory.resolve("folder2");
|
||||
Files.createDirectory(folder2);
|
||||
Path file2 = folder2.resolve("file2");
|
||||
Files.createFile(file2);
|
||||
Path folder3 = folder2.resolve("folder3");
|
||||
Files.createDirectory(folder3);
|
||||
Path expectedFile = folder3.resolve("expected_file");
|
||||
Files.createFile(expectedFile);
|
||||
log.info("Prepared test folder structure");
|
||||
|
||||
// find the root folder where expected file locates, and we expect folder3
|
||||
var filePathResult = FileUtils.findPath(tempDirectory, 1,
|
||||
path -> path.getFileName().toString().equals("expected_file"));
|
||||
assertFalse(filePathResult.isPresent());
|
||||
|
||||
filePathResult = FileUtils.findPath(tempDirectory, 2,
|
||||
path -> path.getFileName().toString().equals("expected_file"));
|
||||
assertFalse(filePathResult.isPresent());
|
||||
|
||||
filePathResult = FileUtils.findPath(tempDirectory, 3,
|
||||
path -> path.getFileName().toString().equals("expected_file"));
|
||||
assertTrue(filePathResult.isPresent());
|
||||
assertEquals(expectedFile.toString(), filePathResult.get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyHiddenFolder() throws IOException {
|
||||
// source
|
||||
// .hidden
|
||||
// test.txt # contain: test
|
||||
final var source = tempDirectory.resolve("source");
|
||||
Files.createDirectories(source);
|
||||
var hidden = source.resolve(".git");
|
||||
Files.createDirectories(hidden);
|
||||
var testTxt = hidden.resolve("test.txt");
|
||||
Files.writeString(testTxt, "test");
|
||||
|
||||
final var target = tempDirectory.resolve("target");
|
||||
FileUtils.copyFolder(source, target);
|
||||
|
||||
assertTrue(Files.exists(target.resolve(".git")));
|
||||
assertEquals("test", Files.readString(target.resolve(".git").resolve("test.txt")));
|
||||
}
|
||||
}
|
|
@ -2,20 +2,30 @@ package run.halo.app.utils;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.ListBranchCommand;
|
||||
import org.eclipse.jgit.api.Status;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.merge.MergeStrategy;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.transport.RemoteConfig;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
|
@ -25,7 +35,7 @@ import org.junit.jupiter.api.Test;
|
|||
* Git test.
|
||||
*
|
||||
* @author johnniang
|
||||
* @date 19-5-21
|
||||
* @date 2020.01.21
|
||||
*/
|
||||
@Slf4j
|
||||
class GitTest {
|
||||
|
@ -35,11 +45,11 @@ class GitTest {
|
|||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
tempPath = Files.createTempDirectory("git-test");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void destroy() throws IOException {
|
||||
FileUtils.deleteFolder(tempPath);
|
||||
final var thread = new Thread(() -> {
|
||||
log.info("Clear temporary folder.");
|
||||
FileUtils.deleteFolderQuietly(tempPath);
|
||||
});
|
||||
Runtime.getRuntime().addShutdownHook(thread);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -88,9 +98,9 @@ class GitTest {
|
|||
|
||||
@Test
|
||||
@Disabled("Due to time-consumption fetching")
|
||||
void getAllBranchesTest() {
|
||||
void getAllBranchesFromRemote() {
|
||||
List<String> branches =
|
||||
GitUtils.getAllBranches("https://github.com/halo-dev/halo-theme-hux.git");
|
||||
GitUtils.getAllBranchesFromRemote("https://github.com/halo-dev/halo-theme-hux.git");
|
||||
assertNotNull(branches);
|
||||
}
|
||||
|
||||
|
@ -98,14 +108,165 @@ class GitTest {
|
|||
@Disabled("Due to time-consumption fetching")
|
||||
void getAllBranchesWithInvalidURL() {
|
||||
List<String> branches =
|
||||
GitUtils.getAllBranches("https://github.com/halo-dev/halo-theme.git");
|
||||
GitUtils.getAllBranchesFromRemote("https://github.com/halo-dev/halo-theme.git");
|
||||
assertNotNull(branches);
|
||||
assertEquals(0, branches.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllBranchesTest() throws GitAPIException {
|
||||
try (Git git = Git.init().setDirectory(tempPath.toFile()).call()) {
|
||||
git.add().addFilepattern(".").call();
|
||||
git.commit().setAllowEmpty(true).setSign(false).setMessage("Empty commit").call();
|
||||
|
||||
git.branchCreate().setName("main").call();
|
||||
git.branchCreate().setName("dev").call();
|
||||
Set<String> branches = git.branchList()
|
||||
.call()
|
||||
.stream()
|
||||
.map(ref -> {
|
||||
String refName = ref.getName();
|
||||
return refName.substring(refName.lastIndexOf('/') + 1);
|
||||
}).collect(Collectors.toSet());
|
||||
assertTrue(branches.containsAll(Arrays.asList("main", "dev")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBranchesFromRemote() throws GitAPIException {
|
||||
Map<String, Ref> refMap = Git.lsRemoteRepository()
|
||||
.setRemote("https://github.com/halo-dev/halo.git")
|
||||
.setHeads(true)
|
||||
.setTags(true)
|
||||
.callAsMap();
|
||||
refMap.forEach((name, ref) -> {
|
||||
log.debug("name: [{}], ref: [{}]", name, ref);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeTwoLocalRepo() throws GitAPIException, IOException, URISyntaxException {
|
||||
final var oldRepoPath = tempPath.resolve("old-repo");
|
||||
final var newRepoPath = tempPath.resolve("new-repo");
|
||||
|
||||
// prepare one local repo
|
||||
try (final var oldGit = Git.init()
|
||||
.setDirectory(oldRepoPath.toFile())
|
||||
.call()) {
|
||||
|
||||
final var testTextInOldRepoPath = oldRepoPath.resolve("test.txt");
|
||||
Files.writeString(testTextInOldRepoPath, "hello old git");
|
||||
oldGit.add().addFilepattern(".").call();
|
||||
oldGit.commit()
|
||||
.setSign(false)
|
||||
.setMessage("commit test.txt at old repo")
|
||||
.call();
|
||||
|
||||
printAllLog(oldGit);
|
||||
|
||||
// copy old repo path to new repo path
|
||||
FileUtils.copyFolder(oldRepoPath, newRepoPath);
|
||||
|
||||
try (final var newGit = Git.init()
|
||||
.setDirectory(newRepoPath.toFile())
|
||||
.call()) {
|
||||
|
||||
Files.writeString(newRepoPath.resolve("test.txt"), "hello old git\nhello new git");
|
||||
newGit.add().addFilepattern(".").call();
|
||||
newGit.commit()
|
||||
.setSign(false)
|
||||
.setMessage("commit test.txt at new repo")
|
||||
.call();
|
||||
|
||||
final var refs = newGit.branchList()
|
||||
.setListMode(ListBranchCommand.ListMode.ALL)
|
||||
.call();
|
||||
refs.forEach(ref -> {
|
||||
log.debug("Ref in new repo: {}", ref);
|
||||
});
|
||||
|
||||
printAllLog(newGit);
|
||||
}
|
||||
|
||||
// add new repo as old repo remote
|
||||
oldGit.remoteAdd().setName("newRepo")
|
||||
.setUri(new URIish(newRepoPath.toString()))
|
||||
.call();
|
||||
|
||||
oldGit.fetch()
|
||||
.setRemote("newRepo")
|
||||
.call();
|
||||
|
||||
final var refs = oldGit.branchList()
|
||||
.setListMode(ListBranchCommand.ListMode.ALL)
|
||||
.call();
|
||||
|
||||
refs.forEach(ref -> log.debug("Ref in old repo: {}", ref));
|
||||
|
||||
final var testTextInOldRepo = Files.readString(testTextInOldRepoPath);
|
||||
Assertions.assertEquals("hello old git", testTextInOldRepo);
|
||||
|
||||
final var rebaseResult = oldGit.rebase()
|
||||
.setUpstream("newRepo/master")
|
||||
.setStrategy(MergeStrategy.THEIRS)
|
||||
.call();
|
||||
|
||||
log.debug("{} | {} | {}", rebaseResult.getCurrentCommit(), rebaseResult.getConflicts(),
|
||||
rebaseResult.getStatus());
|
||||
assertTrue(rebaseResult.getStatus().isSuccessful());
|
||||
|
||||
final var testTextAfterRebase = Files.readString(testTextInOldRepoPath);
|
||||
Assertions.assertEquals("hello old git\nhello new git", testTextAfterRebase);
|
||||
|
||||
printAllLog(oldGit);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Time consume")
|
||||
void findTags() throws GitAPIException, IOException {
|
||||
Git.lsRemoteRepository()
|
||||
.setRemote("https://gitee.com/xzhuz/halo-theme-xue.git")
|
||||
.setTags(true)
|
||||
.setHeads(false)
|
||||
.call()
|
||||
.forEach(ref -> {
|
||||
log.info("ref: {}, object id: {}", ref.getName(), ref.getObjectId());
|
||||
});
|
||||
|
||||
try (final var git = cloneRepository("https://gitee.com/xzhuz/halo-theme-xue.git")) {
|
||||
git.branchList()
|
||||
.setListMode(ListBranchCommand.ListMode.ALL)
|
||||
.call()
|
||||
.forEach(
|
||||
ref -> log.debug("ref: {}, object id: {}", ref.getName(), ref.getObjectId()));
|
||||
|
||||
Pair<Ref, RevCommit> latestTagPair = GitUtils.getLatestTag(git);
|
||||
assertNotNull(latestTagPair);
|
||||
Ref latestTag = latestTagPair.getKey();
|
||||
RevCommit tagCommit = latestTagPair.getValue();
|
||||
|
||||
log.debug("Latest tag: {} with commit: {} {}",
|
||||
latestTag.getName(),
|
||||
tagCommit.getFullMessage(),
|
||||
new Date(tagCommit.getCommitTime() * 1000L));
|
||||
}
|
||||
}
|
||||
|
||||
void printAllLog(Git git) throws IOException, GitAPIException {
|
||||
var commits = git.log().all().call();
|
||||
for (var commit : commits) {
|
||||
log.debug("{}: {} {}", git.toString(), commit, commit.getFullMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Git cloneRepository() throws GitAPIException {
|
||||
return cloneRepository("https://github.com/halo-dev/halo-theme-pinghsu.git");
|
||||
}
|
||||
|
||||
Git cloneRepository(String url) throws GitAPIException {
|
||||
return Git.cloneRepository()
|
||||
.setURI("https://github.com/halo-dev/halo-theme-pinghsu.git")
|
||||
.setURI(url)
|
||||
.setDirectory(tempPath.toFile())
|
||||
.call();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue