mirror of https://github.com/halo-dev/halo
				
				
				
			Complete theme fetching feature
							parent
							
								
									ef9b6eb9f8
								
							
						
					
					
						commit
						5b59a8ab50
					
				| 
						 | 
				
			
			@ -22,9 +22,9 @@ configurations {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    developmentOnly
 | 
			
		||||
	runtimeClasspath {
 | 
			
		||||
		extendsFrom developmentOnly
 | 
			
		||||
	}
 | 
			
		||||
    runtimeClasspath {
 | 
			
		||||
        extendsFrom developmentOnly
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ dependencies {
 | 
			
		|||
    implementation 'org.apache.commons:commons-lang3:3.8.1'
 | 
			
		||||
    implementation 'org.apache.httpcomponents:httpclient:4.5.7'
 | 
			
		||||
    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 'mysql:mysql-connector-java'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -221,4 +221,13 @@ public interface ThemeService {
 | 
			
		|||
     */
 | 
			
		||||
    @NonNull
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,11 +9,15 @@ import freemarker.template.Configuration;
 | 
			
		|||
import freemarker.template.TemplateModelException;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
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.Nullable;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.CollectionUtils;
 | 
			
		||||
import org.springframework.web.client.RestTemplate;
 | 
			
		||||
import org.springframework.web.multipart.MultipartFile;
 | 
			
		||||
import run.halo.app.cache.StringCacheStore;
 | 
			
		||||
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.utils.FileUtils;
 | 
			
		||||
import run.halo.app.utils.FilenameUtils;
 | 
			
		||||
import run.halo.app.utils.HaloUtils;
 | 
			
		||||
import run.halo.app.utils.JsonUtils;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +71,9 @@ public class ThemeServiceImpl implements ThemeService {
 | 
			
		|||
    private final ThemeConfigResolver themeConfigResolver;
 | 
			
		||||
 | 
			
		||||
    private final ThemePropertyResolver themePropertyResolver;
 | 
			
		||||
 | 
			
		||||
    private final RestTemplate restTemplate;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Activated theme id.
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -75,13 +84,16 @@ public class ThemeServiceImpl implements ThemeService {
 | 
			
		|||
                            StringCacheStore cacheStore,
 | 
			
		||||
                            Configuration configuration,
 | 
			
		||||
                            ThemeConfigResolver themeConfigResolver,
 | 
			
		||||
                            ThemePropertyResolver themePropertyResolver) {
 | 
			
		||||
                            ThemePropertyResolver themePropertyResolver,
 | 
			
		||||
                            RestTemplate restTemplate) {
 | 
			
		||||
        this.optionService = optionService;
 | 
			
		||||
        this.cacheStore = cacheStore;
 | 
			
		||||
        this.configuration = configuration;
 | 
			
		||||
        this.themeConfigResolver = themeConfigResolver;
 | 
			
		||||
        workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
 | 
			
		||||
        this.themePropertyResolver = themePropertyResolver;
 | 
			
		||||
        this.restTemplate = restTemplate;
 | 
			
		||||
 | 
			
		||||
        workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
| 
						 | 
				
			
			@ -318,7 +330,7 @@ public class ThemeServiceImpl implements ThemeService {
 | 
			
		|||
 | 
			
		||||
        try {
 | 
			
		||||
            // Create temp directory
 | 
			
		||||
            tempPath = Files.createTempDirectory("halo");
 | 
			
		||||
            tempPath = createTempPath();
 | 
			
		||||
            String basename = FilenameUtils.getBasename(file.getOriginalFilename());
 | 
			
		||||
            Path themeTempPath = tempPath.resolve(basename);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -331,9 +343,7 @@ public class ThemeServiceImpl implements ThemeService {
 | 
			
		|||
            // Unzip to temp path
 | 
			
		||||
            FileUtils.unzip(zis, themeTempPath);
 | 
			
		||||
 | 
			
		||||
            // Go to the base folder
 | 
			
		||||
 | 
			
		||||
            // Add the theme to system
 | 
			
		||||
            // Go to the base folder and add the theme into system
 | 
			
		||||
            return add(FileUtils.skipZipParentFolder(themeTempPath));
 | 
			
		||||
        } catch (IOException 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.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
 | 
			
		||||
        ThemeProperty tmpThemeProperty = getProperty(themeTmpPath);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -375,6 +388,96 @@ public class ThemeServiceImpl implements ThemeService {
 | 
			
		|||
        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.
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -490,6 +593,8 @@ public class ThemeServiceImpl implements ThemeService {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.warn("Property file was not found in [{}]", themePath);
 | 
			
		||||
 | 
			
		||||
        return Optional.empty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
package run.halo.app.service.support;
 | 
			
		||||
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +13,7 @@ import java.util.Map;
 | 
			
		|||
 * @author johnniang
 | 
			
		||||
 * @date 19-4-18
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
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 MediaType APPLICATION_GIT;
 | 
			
		||||
 | 
			
		||||
    public static final String APPLICATION_GIT_VALUE = "application/git";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        APPLICATION_ZIP = valueOf(APPLICATION_ZIP_VALUE);
 | 
			
		||||
        APPLICATION_GIT = valueOf(APPLICATION_GIT_VALUE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public HaloMediaType(String type) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,10 +66,14 @@ public class FileUtils {
 | 
			
		|||
    public static void deleteFolder(Path deletingPath) throws IOException {
 | 
			
		||||
        Assert.notNull(deletingPath, "Deleting path must not be null");
 | 
			
		||||
 | 
			
		||||
        log.debug("Deleting [{}]", deletingPath);
 | 
			
		||||
 | 
			
		||||
        Files.walk(deletingPath)
 | 
			
		||||
                .sorted(Comparator.reverseOrder())
 | 
			
		||||
                .map(Path::toFile)
 | 
			
		||||
                .forEach(File::delete);
 | 
			
		||||
 | 
			
		||||
        log.debug("Deleted [{}] successfully", deletingPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,9 @@ import io.swagger.annotations.ApiOperation;
 | 
			
		|||
import org.springframework.web.bind.annotation.*;
 | 
			
		||||
import org.springframework.web.multipart.MultipartFile;
 | 
			
		||||
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.ThemeFile;
 | 
			
		||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
 | 
			
		||||
import run.halo.app.service.ThemeService;
 | 
			
		||||
import run.halo.app.service.ThemeSettingService;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,4 +122,10 @@ public class ThemeController {
 | 
			
		|||
    public ThemeProperty uploadTheme(@RequestPart("file") MultipartFile file) {
 | 
			
		||||
        return themeService.upload(file);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PostMapping("fetching")
 | 
			
		||||
    @ApiOperation("Fetches a new theme")
 | 
			
		||||
    public ThemeProperty fetchTheme(@RequestParam("uri") String uri) {
 | 
			
		||||
        return themeService.fetch(uri);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue