mirror of https://github.com/halo-dev/halo
Refactor theme uploading and addition
parent
ef417a2c14
commit
01b57826f0
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package run.halo.app.model.support;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue