Refactor theme uploading and addition

pull/146/head
johnniang 2019-04-19 10:15:28 +08:00
parent ef417a2c14
commit 01b57826f0
6 changed files with 234 additions and 35 deletions

View File

@ -0,0 +1,18 @@
package run.halo.app.exception;
/**
* Unsupported media type exception.
*
* @author johnniang
* @date 19-4-19
*/
public class UnsupportedMediaTypeException extends BadRequestException {
public UnsupportedMediaTypeException(String message) {
super(message);
}
public UnsupportedMediaTypeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,7 +1,6 @@
package run.halo.app.model.support;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**

View File

@ -5,7 +5,6 @@ import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import freemarker.template.Configuration;
import freemarker.template.TemplateModelException;
import lombok.extern.slf4j.Slf4j;
@ -28,6 +27,7 @@ import run.halo.app.model.support.HaloConst;
import run.halo.app.model.support.ThemeFile;
import run.halo.app.service.OptionService;
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.JsonUtils;
@ -39,6 +39,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipInputStream;
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
@ -222,7 +223,7 @@ public class ThemeServiceImpl implements ThemeService {
FileUtil.del(Paths.get(themeProperty.getThemePath()));
// Delete theme cache
cacheStore.delete(THEMES_CACHE_KEY);
clearThemeCache();
} catch (Exception e) {
throw new ServiceException("Failed to delete theme folder", e).setErrorData(themeId);
}
@ -296,7 +297,7 @@ public class ThemeServiceImpl implements ThemeService {
setActivatedThemeId(themeId);
// Clear the cache
cacheStore.delete(THEMES_CACHE_KEY);
clearThemeCache();
try {
// TODO Refactor here in the future
@ -313,31 +314,39 @@ public class ThemeServiceImpl implements ThemeService {
public ThemeProperty upload(MultipartFile file) {
Assert.notNull(file, "Multipart file must not be null");
// Get upload path
Path uploadPath = Paths.get(workDir.toString(), file.getOriginalFilename());
if (!HaloMediaType.isZipType(file.getContentType())) {
throw new UnsupportedMediaTypeException("Unsupported theme media type: " + file.getContentType()).setErrorData(file.getOriginalFilename());
}
final String originalBasename = FilenameUtils.getBasename(file.getOriginalFilename());
log.info("Uploading theme to directory: [{}]", uploadPath.toString());
ZipInputStream zis = null;
Path tempPath = null;
try {
// Create directory
Files.createDirectories(uploadPath.getParent());
Files.createFile(uploadPath);
file.transferTo(uploadPath);
// Create temp directory
tempPath = Files.createTempDirectory("halo");
String basename = FilenameUtils.getBasename(file.getOriginalFilename());
Path themeTempPath = tempPath.resolve(basename);
// Unzip theme package
ZipUtil.unzip(uploadPath.toFile(), uploadPath.getParent().toFile());
// Check directory traversal
FileUtils.checkDirectoryTraversal(tempPath, themeTempPath);
// Delete theme package
FileUtil.del(uploadPath.toFile());
// New zip input stream
zis = new ZipInputStream(file.getInputStream());
cacheStore.delete(THEMES_CACHE_KEY);
// Unzip to temp path
FileUtils.unzip(zis, themeTempPath);
return getProperty(Paths.get(workDir.toString(), originalBasename));
// Go to the base folder
// Add the theme to system
return add(FileUtils.skipZipParentFolder(themeTempPath));
} catch (IOException e) {
log.error("Failed to upload theme to local: " + uploadPath, e);
throw new ServiceException("Failed to upload theme to local").setErrorData(uploadPath);
throw new ServiceException("Failed to upload theme file: " + file.getOriginalFilename(), e);
} finally {
// Close zip input stream
FileUtils.closeQuietly(zis);
// Delete folder after testing
FileUtils.deleteFolderQuietly(tempPath);
}
}
@ -347,19 +356,35 @@ public class ThemeServiceImpl implements ThemeService {
Assert.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory");
// Check property config
Path configPath = getThemePropertyPathOfNullable(themeTmpPath).orElseThrow(() -> new ThemePropertyMissingException("Theme property file is dismiss").setErrorData(themeTmpPath));
ThemeProperty tmpThemeProperty = getProperty(themeTmpPath);
ThemeProperty tmpThemeProperty = getProperty(configPath);
// Check theme existence
boolean isExist = getThemes().stream()
.anyMatch(themeProperty -> themeProperty.getId().equalsIgnoreCase(tmpThemeProperty.getId()));
if (isExist) {
throw new AlreadyExistsException("The theme with id " + tmpThemeProperty.getId() + " has already existed");
}
// Copy the temporary path to current theme folder
Path targetThemePath = workDir.resolve(tmpThemeProperty.getId());
FileUtils.copyFolder(themeTmpPath, targetThemePath);
// Delete temp theme folder
FileUtils.deleteFolder(themeTmpPath);
// Get property again
return getProperty(targetThemePath);
ThemeProperty property = getProperty(targetThemePath);
// Clear theme cache
clearThemeCache();
// Delete cache
return property;
}
/**
* Clears theme cache.
*/
private void clearThemeCache() {
cacheStore.delete(THEMES_CACHE_KEY);
}
/**
@ -446,15 +471,8 @@ public class ThemeServiceImpl implements ThemeService {
* @throws ForbiddenException throws when the given absolute directory name is invalid
*/
private void checkDirectory(@NonNull String absoluteName) {
Assert.hasText(absoluteName, "Absolute name must not be blank");
ThemeProperty activeThemeProperty = getThemeOfNonNullBy(getActivatedThemeId());
boolean valid = Paths.get(absoluteName).startsWith(activeThemeProperty.getThemePath());
if (!valid) {
throw new ForbiddenException("You cannot access " + absoluteName).setErrorData(absoluteName);
}
FileUtils.checkDirectoryTraversal(activeThemeProperty.getThemePath(), absoluteName);
}
/**

View File

@ -0,0 +1,87 @@
package run.halo.app.service.support;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import java.nio.charset.Charset;
import java.util.Map;
/**
* Halo Media type.
*
* @author johnniang
* @date 19-4-18
*/
public class HaloMediaType extends MediaType {
/**
* Public constant media type of {@code application/zip}
*/
public static final MediaType APPLICATION_ZIP;
/**
* A String equivalent of {@link HaloMediaType#APPLICATION_ZIP}
*/
public static final String APPLICATION_ZIP_VALUE = "application/zip";
static {
APPLICATION_ZIP = valueOf(APPLICATION_ZIP_VALUE);
}
public HaloMediaType(String type) {
super(type);
}
public HaloMediaType(String type, String subtype) {
super(type, subtype);
}
public HaloMediaType(String type, String subtype, Charset charset) {
super(type, subtype, charset);
}
public HaloMediaType(String type, String subtype, double qualityValue) {
super(type, subtype, qualityValue);
}
public HaloMediaType(MediaType other, Charset charset) {
super(other, charset);
}
public HaloMediaType(MediaType other, Map<String, String> parameters) {
super(other, parameters);
}
public HaloMediaType(String type, String subtype, Map<String, String> parameters) {
super(type, subtype, parameters);
}
/**
* Checks whether the media type is zip type or not .
*
* @param mediaType media type
* @return true if the given media type is zip type; false otherwise
*/
public static boolean isZipType(MediaType mediaType) {
if (mediaType == null) {
return false;
}
return mediaType.includes(APPLICATION_ZIP);
}
/**
* Checks whether the media type is zip type or not .
*
* @param contentType content type
* @return true if the given content type is zip type; false otherwise
*/
public static boolean isZipType(String contentType) {
if (StringUtils.isBlank(contentType)) {
return false;
}
return isZipType(valueOf(contentType));
}
}

View File

@ -12,6 +12,8 @@ import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -108,6 +110,25 @@ public class FileUtils {
}
}
/**
* Skips zip parent folder. (Go into base folder)
*
* @param unzippedPath unzipped path must not be null
* @return path containing base files
* @throws IOException
*/
public static Path skipZipParentFolder(@NonNull Path unzippedPath) throws IOException {
Assert.notNull(unzippedPath, "Unzipped folder must not be null");
List<Path> childrenPath = Files.list(unzippedPath).collect(Collectors.toList());
if (childrenPath.size() == 1 && Files.isDirectory(childrenPath.get(0))) {
return childrenPath.get(0);
}
return unzippedPath;
}
/**
* Creates directories if absent.
*

View File

@ -0,0 +1,56 @@
package run.halo.app.utils;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.equalTo;
/**
* @author johnniang
* @date 19-4-19
*/
public class FileUtilsTest {
@Test
public void deleteFolder() throws IOException {
// Create a temp folder
Path tempDirectory = Files.createTempDirectory("halo-test");
Path testPath = tempDirectory.resolve("test/test/test");
// Create test folders
Files.createDirectories(testPath);
System.out.println("Walk path list");
List<Path> walkList = Files.walk(tempDirectory).collect(Collectors.toList());
walkList.forEach(System.out::println);
Assert.assertThat(walkList.size(), equalTo(4));
System.out.println("Walk 1 deep path list");
List<Path> walk1DeepList = Files.walk(tempDirectory, 1).collect(Collectors.toList());
walk1DeepList.forEach(System.out::println);
Assert.assertThat(walk1DeepList.size(), equalTo(2));
System.out.println("List path list");
List<Path> listList = Files.list(tempDirectory).collect(Collectors.toList());
listList.forEach(System.out::println);
Assert.assertThat(listList.size(), equalTo(1));
System.out.println("List test path list");
List<Path> testPathList = Files.list(testPath).collect(Collectors.toList());
testPathList.forEach(System.out::println);
Assert.assertThat(testPathList.size(), equalTo(0));
// Delete it
FileUtils.deleteFolder(tempDirectory);
Assert.assertTrue(Files.notExists(tempDirectory));
}
}