Complete theme fetching feature

pull/146/head
johnniang 2019-04-19 13:57:28 +08:00
parent ef9b6eb9f8
commit 5b59a8ab50
7 changed files with 172 additions and 10 deletions

View File

@ -50,6 +50,7 @@ dependencies {
implementation 'org.apache.commons:commons-lang3:3.8.1' implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'org.apache.httpcomponents:httpclient:4.5.7' implementation 'org.apache.httpcomponents:httpclient:4.5.7'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.2' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.2'
implementation 'org.eclipse.jgit:org.eclipse.jgit:5.3.0.201903130848-r'
runtimeOnly 'com.h2database:h2' runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java' runtimeOnly 'mysql:mysql-connector-java'

View File

@ -221,4 +221,13 @@ public interface ThemeService {
*/ */
@NonNull @NonNull
ThemeProperty add(@NonNull Path themeTmpPath) throws IOException; ThemeProperty add(@NonNull Path themeTmpPath) throws IOException;
/**
* Fetches a new theme.
*
* @param uri theme remote uri must not be null
* @return theme property
*/
@NonNull
ThemeProperty fetch(@NonNull String uri);
} }

View File

@ -9,11 +9,15 @@ import freemarker.template.Configuration;
import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import run.halo.app.cache.StringCacheStore; import run.halo.app.cache.StringCacheStore;
import run.halo.app.config.properties.HaloProperties; import run.halo.app.config.properties.HaloProperties;
@ -30,8 +34,10 @@ import run.halo.app.service.ThemeService;
import run.halo.app.service.support.HaloMediaType; import run.halo.app.service.support.HaloMediaType;
import run.halo.app.utils.FileUtils; import run.halo.app.utils.FileUtils;
import run.halo.app.utils.FilenameUtils; import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.HaloUtils;
import run.halo.app.utils.JsonUtils; import run.halo.app.utils.JsonUtils;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -65,6 +71,9 @@ public class ThemeServiceImpl implements ThemeService {
private final ThemeConfigResolver themeConfigResolver; private final ThemeConfigResolver themeConfigResolver;
private final ThemePropertyResolver themePropertyResolver; private final ThemePropertyResolver themePropertyResolver;
private final RestTemplate restTemplate;
/** /**
* Activated theme id. * Activated theme id.
*/ */
@ -75,13 +84,16 @@ public class ThemeServiceImpl implements ThemeService {
StringCacheStore cacheStore, StringCacheStore cacheStore,
Configuration configuration, Configuration configuration,
ThemeConfigResolver themeConfigResolver, ThemeConfigResolver themeConfigResolver,
ThemePropertyResolver themePropertyResolver) { ThemePropertyResolver themePropertyResolver,
RestTemplate restTemplate) {
this.optionService = optionService; this.optionService = optionService;
this.cacheStore = cacheStore; this.cacheStore = cacheStore;
this.configuration = configuration; this.configuration = configuration;
this.themeConfigResolver = themeConfigResolver; this.themeConfigResolver = themeConfigResolver;
workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
this.themePropertyResolver = themePropertyResolver; this.themePropertyResolver = themePropertyResolver;
this.restTemplate = restTemplate;
workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
} }
@Override @Override
@ -318,7 +330,7 @@ public class ThemeServiceImpl implements ThemeService {
try { try {
// Create temp directory // Create temp directory
tempPath = Files.createTempDirectory("halo"); tempPath = createTempPath();
String basename = FilenameUtils.getBasename(file.getOriginalFilename()); String basename = FilenameUtils.getBasename(file.getOriginalFilename());
Path themeTempPath = tempPath.resolve(basename); Path themeTempPath = tempPath.resolve(basename);
@ -331,9 +343,7 @@ public class ThemeServiceImpl implements ThemeService {
// Unzip to temp path // Unzip to temp path
FileUtils.unzip(zis, themeTempPath); FileUtils.unzip(zis, themeTempPath);
// Go to the base folder // Go to the base folder and add the theme into system
// Add the theme to system
return add(FileUtils.skipZipParentFolder(themeTempPath)); return add(FileUtils.skipZipParentFolder(themeTempPath));
} catch (IOException e) { } catch (IOException e) {
throw new ServiceException("Failed to upload theme file: " + file.getOriginalFilename(), e); throw new ServiceException("Failed to upload theme file: " + file.getOriginalFilename(), e);
@ -350,6 +360,9 @@ public class ThemeServiceImpl implements ThemeService {
Assert.notNull(themeTmpPath, "Theme temporary path must not be null"); Assert.notNull(themeTmpPath, "Theme temporary path must not be null");
Assert.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory"); Assert.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory");
log.debug("Children path of [{}]:", themeTmpPath);
Files.list(themeTmpPath).forEach(path -> log.debug(path.toString()));
// Check property config // Check property config
ThemeProperty tmpThemeProperty = getProperty(themeTmpPath); ThemeProperty tmpThemeProperty = getProperty(themeTmpPath);
@ -375,6 +388,96 @@ public class ThemeServiceImpl implements ThemeService {
return property; return property;
} }
@Override
public ThemeProperty fetch(String uri) {
Assert.hasText(uri, "Theme remote uri must not be blank");
Path tmpPath = null;
try {
// Create temp path
tmpPath = createTempPath();
// Create temp path
Path themeTmpPath = tmpPath.resolve(HaloUtils.randomUUIDWithoutDash());
if (StringUtils.endsWithIgnoreCase(uri, ".git")) {
cloneFromGit(uri, themeTmpPath);
} else {
downloadZipAndUnzip(uri, themeTmpPath);
// } else {
// throw new UnsupportedMediaTypeException("Unsupported download type: " + uri);
}
return add(themeTmpPath);
} catch (IOException | GitAPIException e) {
throw new ServiceException("Failed to fetch theme from remote " + uri, e);
} finally {
FileUtils.deleteFolderQuietly(tmpPath);
}
}
/**
* Clones theme from git.
*
* @param gitUrl git url must not be blank
* @param targetPath target path must not be null
* @throws GitAPIException
*/
private void cloneFromGit(@NonNull String gitUrl, @NonNull Path targetPath) throws GitAPIException {
Assert.hasText(gitUrl, "Git url must not be blank");
Assert.notNull(targetPath, "Target path must not be null");
log.debug("Cloning git repo [{}] to [{}]", gitUrl, targetPath);
// Clone it
Git.cloneRepository()
.setURI(gitUrl)
.setDirectory(targetPath.toFile())
.call();
log.debug("Cloned git repo [{}]", gitUrl);
}
/**
* Downloads zip file and unzip it into specified path.
*
* @param zipUrl zip url must not be null
* @param targetPath target path must not be null
* @throws IOException
*/
private void downloadZipAndUnzip(@NonNull String zipUrl, @NonNull Path targetPath) throws IOException {
Assert.hasText(zipUrl, "Zip url must not be blank");
log.debug("Downloading [{}]", zipUrl);
// Download it
ResponseEntity<byte[]> downloadResponse = restTemplate.getForEntity(zipUrl, byte[].class);
log.debug("Download response: [{}]", downloadResponse.getStatusCode());
if (downloadResponse.getStatusCode().isError() || downloadResponse.getBody() == null) {
throw new ServiceException("Failed to download " + zipUrl + ", status: " + downloadResponse.getStatusCode());
}
log.debug("Downloaded [{}]", zipUrl);
// New zip input stream
ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(downloadResponse.getBody()));
// Unzip it
FileUtils.unzip(zis, targetPath);
}
/**
* Creates temporary path.
*
* @return temporary path
* @throws IOException
*/
@NonNull
private Path createTempPath() throws IOException {
return Files.createTempDirectory("halo");
}
/** /**
* Clears theme cache. * Clears theme cache.
*/ */
@ -490,6 +593,8 @@ public class ThemeServiceImpl implements ThemeService {
} }
} }
log.warn("Property file was not found in [{}]", themePath);
return Optional.empty(); return Optional.empty();
} }

View File

@ -1,5 +1,6 @@
package run.halo.app.service.support; package run.halo.app.service.support;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -12,6 +13,7 @@ import java.util.Map;
* @author johnniang * @author johnniang
* @date 19-4-18 * @date 19-4-18
*/ */
@Slf4j
public class HaloMediaType extends MediaType { public class HaloMediaType extends MediaType {
/** /**
@ -24,9 +26,14 @@ public class HaloMediaType extends MediaType {
*/ */
public static final String APPLICATION_ZIP_VALUE = "application/zip"; public static final String APPLICATION_ZIP_VALUE = "application/zip";
public static final MediaType APPLICATION_GIT;
public static final String APPLICATION_GIT_VALUE = "application/git";
static { static {
APPLICATION_ZIP = valueOf(APPLICATION_ZIP_VALUE); APPLICATION_ZIP = valueOf(APPLICATION_ZIP_VALUE);
APPLICATION_GIT = valueOf(APPLICATION_GIT_VALUE);
} }
public HaloMediaType(String type) { public HaloMediaType(String type) {

View File

@ -66,10 +66,14 @@ public class FileUtils {
public static void deleteFolder(Path deletingPath) throws IOException { public static void deleteFolder(Path deletingPath) throws IOException {
Assert.notNull(deletingPath, "Deleting path must not be null"); Assert.notNull(deletingPath, "Deleting path must not be null");
log.debug("Deleting [{}]", deletingPath);
Files.walk(deletingPath) Files.walk(deletingPath)
.sorted(Comparator.reverseOrder()) .sorted(Comparator.reverseOrder())
.map(Path::toFile) .map(Path::toFile)
.forEach(File::delete); .forEach(File::delete);
log.debug("Deleted [{}] successfully", deletingPath);
} }
/** /**

View File

@ -4,9 +4,9 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import run.halo.app.handler.theme.config.support.Group; import run.halo.app.handler.theme.config.support.Group;
import run.halo.app.handler.theme.config.support.ThemeProperty;
import run.halo.app.model.support.BaseResponse; import run.halo.app.model.support.BaseResponse;
import run.halo.app.model.support.ThemeFile; import run.halo.app.model.support.ThemeFile;
import run.halo.app.handler.theme.config.support.ThemeProperty;
import run.halo.app.service.ThemeService; import run.halo.app.service.ThemeService;
import run.halo.app.service.ThemeSettingService; import run.halo.app.service.ThemeSettingService;
@ -122,4 +122,10 @@ public class ThemeController {
public ThemeProperty uploadTheme(@RequestPart("file") MultipartFile file) { public ThemeProperty uploadTheme(@RequestPart("file") MultipartFile file) {
return themeService.upload(file); return themeService.upload(file);
} }
@PostMapping("fetching")
@ApiOperation("Fetches a new theme")
public ThemeProperty fetchTheme(@RequestParam("uri") String uri) {
return themeService.fetch(uri);
}
} }

View File

@ -0,0 +1,30 @@
package run.halo.app.service.support;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.net.URI;
import java.net.URISyntaxException;
/**
* @author johnniang
* @date 19-4-19
*/
@Slf4j
public class HaloMediaTypeTest {
@Test
public void gitUrlCheckTest() throws URISyntaxException {
String git = "https://github.com/halo-dev/halo.git";
URI uri = new URI(git);
log.debug(uri.toString());
git = "ssh://git@github.com:halo-dev/halo.git";
uri = new URI(git);
log.debug(uri.toString());
}
}