diff --git a/src/main/java/run/halo/app/listener/StartedListener.java b/src/main/java/run/halo/app/listener/StartedListener.java index d9d646642..08b4fbfbe 100644 --- a/src/main/java/run/halo/app/listener/StartedListener.java +++ b/src/main/java/run/halo/app/listener/StartedListener.java @@ -99,7 +99,7 @@ public class StartedListener implements ApplicationListener node; } diff --git a/src/main/java/run/halo/app/service/ThemeService.java b/src/main/java/run/halo/app/service/ThemeService.java index 39e781c79..250d6f153 100644 --- a/src/main/java/run/halo/app/service/ThemeService.java +++ b/src/main/java/run/halo/app/service/ThemeService.java @@ -47,24 +47,23 @@ public interface ThemeService { * @param absolutePath absolutePath * @return List */ - @Deprecated List listThemeFolder(@NonNull String absolutePath); /** * Lists theme folder by theme name. * - * @param theme theme + * @param themeId theme id * @return List */ - List listThemeFolderBy(@NonNull String theme); + List listThemeFolderBy(@NonNull String themeId); /** * Gets custom template, such as page_xxx.ftl, and xxx will be template name * - * @param theme theme name + * @param themeId theme id * @return List */ - List getCustomTpl(@NonNull String theme); + List getCustomTpl(@NonNull String themeId); /** * Judging whether template exists under the specified theme @@ -75,7 +74,7 @@ public interface ThemeService { boolean isTemplateExist(@NonNull String template); /** - * Judging whether theme exists under template path + * Checks whether theme exists under template path * * @param themeId theme name * @return boolean @@ -97,7 +96,7 @@ public interface ThemeService { Path getBasePath(); /** - * Get template content by template absolute path. + * Gets template content by template absolute path. * * @param absolutePath absolute path * @return template content @@ -105,32 +104,31 @@ public interface ThemeService { String getTemplateContent(@NonNull String absolutePath); /** - * Save template content by template absolute path. + * Saves template content by template absolute path. * * @param absolutePath absolute path * @param content new content */ - @Deprecated void saveTemplateContent(@NonNull String absolutePath, @NonNull String content); /** - * Delete a theme by key. + * Deletes a theme by key. * - * @param key theme key + * @param themeId theme id must not be blank */ - void deleteTheme(@NonNull String key); + void deleteTheme(@NonNull String themeId); /** - * Fetchs theme configuration. + * Fetches theme configuration. * - * @param themeName theme name must not be blank + * @param themeId must not be blank * @return theme configuration or null if not found */ @Nullable - Object fetchConfig(@NonNull String themeName); + Object fetchConfig(@NonNull String themeId); /** - * Render a theme page. + * Renders a theme page. * * @param pageName must not be blank * @return full path of the theme page @@ -144,12 +142,14 @@ public interface ThemeService { * @return current theme id */ @NonNull - String getActivatedTheme(); + String getActivatedThemeId(); /** * Actives a theme. * * @param themeId theme id must not be blank + * @return theme property */ - void activeTheme(@NonNull String themeId); + @NonNull + ThemeProperty activeTheme(@NonNull String themeId); } diff --git a/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java b/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java index 7654cabbd..bdb4c3d27 100644 --- a/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java @@ -1,14 +1,16 @@ package run.halo.app.service.impl; -import cn.hutool.core.io.FileUtil; 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 com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import freemarker.template.Configuration; +import freemarker.template.TemplateModelException; import lombok.extern.slf4j.Slf4j; 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; @@ -23,7 +25,6 @@ import run.halo.app.model.support.ThemeFile; import run.halo.app.model.support.ThemeProperty; import run.halo.app.service.OptionService; import run.halo.app.service.ThemeService; -import run.halo.app.utils.FilenameUtils; import run.halo.app.utils.JsonUtils; import java.io.File; @@ -78,6 +79,9 @@ public class ThemeServiceImpl implements ThemeService { */ private final static String RENDER_TEMPLATE = "themes/%s/%s"; + /** + * Theme cache key. + */ private final static String THEMES_CACHE_KEY = "themes"; /** @@ -91,11 +95,15 @@ public class ThemeServiceImpl implements ThemeService { private final StringCacheStore cacheStore; + private final Configuration configuration; + public ThemeServiceImpl(HaloProperties haloProperties, OptionService optionService, - StringCacheStore cacheStore) { + StringCacheStore cacheStore, + Configuration configuration) { this.optionService = optionService; this.cacheStore = cacheStore; + this.configuration = configuration; yamlMapper = new ObjectMapper(new YAMLFactory()); workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER); } @@ -117,11 +125,6 @@ public class ThemeServiceImpl implements ThemeService { return themes.stream().filter(themeProperty -> themeProperty.getId().equals(themeId)).findFirst(); } - /** - * Gets all themes - * - * @return list of themes - */ @Override public List getThemes() { // Fetch themes from cache @@ -155,80 +158,25 @@ public class ThemeServiceImpl implements ThemeService { }); } - /** - * Lists theme folder by absolute path. - * - * @param absolutePath absolutePath - * @return List - */ @Override public List listThemeFolder(String absolutePath) { - // Check this path - checkDirectory(absolutePath); - - final List templates = new ArrayList<>(); - try { - File absolutePathFile = new File(absolutePath); - File[] baseFiles = absolutePathFile.listFiles(); - if (null != baseFiles) { - baseFileFor: - for (File base : baseFiles) { - for (String filterFile : FILTER_FILES) { - if (filterFile.equals(base.getName())) { - continue baseFileFor; - } - } - ThemeFile file = new ThemeFile(); - if (base.isDirectory()) { - file.setName(base.getName()); - file.setPath(base.getAbsolutePath()); - file.setIsFile(false); - file.setNode(listThemeFolder(base.getAbsolutePath())); - } else { - file.setName(base.getName()); - file.setPath(base.getAbsolutePath()); - file.setIsFile(true); - for (String suffix : CAN_EDIT_SUFFIX) { - if (FilenameUtils.getExtension(file.getName()).equals(suffix)) { - file.setCanEdit(true); - break; - } else { - file.setCanEdit(false); - } - } - } - templates.add(file); - } - } - } catch (Exception e) { - throw new RuntimeException("Failed to get theme template", e); - } - templates.sort(Comparator.comparing(ThemeFile::getIsFile)); - return templates; + return listThemeFileTree(Paths.get(absolutePath)); } - /** - * Lists theme folder by theme name. - * - * @param theme theme - * @return List - */ @Override - public List listThemeFolderBy(String theme) { - File themePath = new File(getThemeBasePath(), theme); - return listThemeFolder(themePath.getAbsolutePath()); + public List listThemeFolderBy(String themeId) { + // Get the theme property + ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); + + // List theme file as tree view + return listThemeFolder(themeProperty.getThemePath()); } - /** - * Gets custom template, such as page_xxx.ftl, and xxx will be template name - * - * @param theme theme name - * @return List - */ @Override - public List getCustomTpl(String theme) { + public List getCustomTpl(String themeId) { + // TODO 这里的参数是有问题的,等待修复。 final List templates = new ArrayList<>(); - final File themePath = new File(getThemeBasePath(), theme); + final File themePath = new File(getThemeBasePath(), themeId); final File[] themeFiles = themePath.listFiles(); if (null != themeFiles && themeFiles.length > 0) { for (File file : themeFiles) { @@ -241,39 +189,20 @@ public class ThemeServiceImpl implements ThemeService { return templates; } - /** - * Judging whether template exists under the specified theme - * - * @param template template - * @return boolean - */ @Override public boolean isTemplateExist(String template) { - StrBuilder templatePath = new StrBuilder(getActivatedTheme()); + StrBuilder templatePath = new StrBuilder(getActivatedThemeId()); templatePath.append("/"); templatePath.append(template); File file = new File(getThemeBasePath(), templatePath.toString()); return file.exists(); } - /** - * Judging whether theme exists under template path - * - * @param themeId theme name - * @return boolean - */ @Override public boolean isThemeExist(String themeId) { - // TODO Get theme folder name by theme id - File file = new File(getThemeBasePath(), themeId); - return file.exists(); + return getThemeBy(themeId).isPresent(); } - /** - * Gets theme base path. - * - * @return File - */ @Override public File getThemeBasePath() { return getBasePath().toFile(); @@ -284,56 +213,56 @@ public class ThemeServiceImpl implements ThemeService { return workDir; } - /** - * Get template content by template absolute path. - * - * @param absolutePath absolute path - * @return template content - */ @Override public String getTemplateContent(String absolutePath) { // Check the path checkDirectory(absolutePath); + // Read file return new FileReader(absolutePath).readString(); } - /** - * Save template content by template absolute path. - * - * @param absolutePath absolute path - * @param content new content - */ @Override public void saveTemplateContent(String absolutePath, String content) { - final FileWriter fileWriter = new FileWriter(absolutePath); - fileWriter.write(content); + // Check the path + checkDirectory(absolutePath); + + // Write file + new FileWriter(absolutePath).write(content); } - /** - * Delete a theme by key. - * - * @param key theme key - */ @Override - public void deleteTheme(String key) { - if (!isThemeExist(key)) { - throw new NotFoundException("该主题不存在!").setErrorData(key); + public void deleteTheme(String themeId) { + // Get the theme property + ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); + + try { + // Delete the folder + Files.deleteIfExists(Paths.get(themeProperty.getThemePath())); + + // Delete theme cache + cacheStore.delete(THEMES_CACHE_KEY); + } catch (IOException e) { + throw new ServiceException("Failed to delete theme folder", e).setErrorData(themeId); } - File file = new File(this.getThemeBasePath(), key); - FileUtil.del(file); - - cacheStore.delete(THEMES_CACHE_KEY); } @Override - public Object fetchConfig(String themeName) { - Assert.hasText(themeName, "Theme name must not be blank"); + public Object fetchConfig(String themeId) { + Assert.hasText(themeId, "Theme name must not be blank"); + + // Get theme property + ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); + + if (!themeProperty.getHasOptions()) { + // If this theme dose not has an option, then return null + return null; + } try { for (String optionsName : OPTIONS_NAMES) { // Resolve the options path - Path optionsPath = workDir.resolve(themeName).resolve(optionsName); + Path optionsPath = Paths.get(themeProperty.getThemePath(), optionsName); log.debug("Finding options in: [{}]", optionsPath.toString()); @@ -355,17 +284,99 @@ public class ThemeServiceImpl implements ThemeService { @Override public String render(String pageName) { - return String.format(RENDER_TEMPLATE, getActivatedTheme(), pageName); + return String.format(RENDER_TEMPLATE, getActivatedThemeId(), pageName); } @Override - public String getActivatedTheme() { + public String getActivatedThemeId() { return optionService.getByProperty(PrimaryProperties.THEME).orElse(DEFAULT_THEME_NAME); } @Override - public void activeTheme(String themeId) { - // TODO Check existence of the theme + public ThemeProperty activeTheme(String themeId) { + // Check existence of the theme + ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); + + // Save the theme to database + optionService.saveProperty(PrimaryProperties.THEME, themeId); + + try { + // TODO Refactor here in the future + configuration.setSharedVariable("themeName", themeId); + configuration.setSharedVariable("options", optionService.listOptions()); + } catch (TemplateModelException e) { + throw new ServiceException("Failed to set shared variable", e).setErrorData(themeId); + } + + return themeProperty; + } + + /** + * Lists theme files as tree view. + * + * @param topPath must not be null + * @return theme file tree view or null only if the top path is not a directory + */ + @Nullable + private List listThemeFileTree(@NonNull Path topPath) { + Assert.notNull(topPath, "Top path must not be null"); + + // Check file type + if (!Files.isDirectory(topPath)) { + return null; + } + + try { + List themeFiles = new LinkedList<>(); + + Files.list(topPath).forEach(path -> { + // Build theme file + ThemeFile themeFile = new ThemeFile(); + themeFile.setName(path.getFileName().toString()); + themeFile.setPath(path.toString()); + themeFile.setIsFile(Files.isRegularFile(path)); + themeFile.setEditable(isEditable(path)); + + if (Files.isDirectory(path)) { + themeFile.setNode(listThemeFileTree(path)); + } + + // Add to theme files + themeFiles.add(themeFile); + }); + + // Sort with name + themeFiles.sort(Comparator.comparing(ThemeFile::getName)); + + return themeFiles; + } catch (IOException e) { + throw new ServiceException("Failed to list sub files", e); + } + } + + /** + * Check if the given path is editable. + * + * @param path must not be null + * @return true if the given path is editable; false otherwise + */ + private boolean isEditable(@NonNull Path path) { + Assert.notNull(path, "Path must not be null"); + + boolean isEditable = Files.isReadable(path) && Files.isWritable(path); + + if (!isEditable) { + return false; + } + + // Check suffix + for (String suffix : CAN_EDIT_SUFFIX) { + if (path.endsWith(suffix)) { + return true; + } + } + + return false; } /** @@ -377,7 +388,9 @@ public class ThemeServiceImpl implements ThemeService { private void checkDirectory(@NonNull String absoluteName) { Assert.hasText(absoluteName, "Absolute name must not be blank"); - boolean valid = Paths.get(absoluteName).startsWith(workDir); + ThemeProperty activeThemeProperty = getThemeOfNonNullBy(getActivatedThemeId()); + + boolean valid = Paths.get(absoluteName).startsWith(activeThemeProperty.getThemePath()); if (!valid) { throw new ForbiddenException("You cannot access " + absoluteName).setErrorData(absoluteName); diff --git a/src/main/java/run/halo/app/web/controller/admin/api/ThemeController.java b/src/main/java/run/halo/app/web/controller/admin/api/ThemeController.java index a31193bbc..d002897dd 100644 --- a/src/main/java/run/halo/app/web/controller/admin/api/ThemeController.java +++ b/src/main/java/run/halo/app/web/controller/admin/api/ThemeController.java @@ -1,14 +1,10 @@ package run.halo.app.web.controller.admin.api; -import freemarker.template.Configuration; -import freemarker.template.TemplateModelException; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.*; -import run.halo.app.model.properties.PrimaryProperties; import run.halo.app.model.support.BaseResponse; import run.halo.app.model.support.ThemeFile; import run.halo.app.model.support.ThemeProperty; -import run.halo.app.service.OptionService; import run.halo.app.service.ThemeService; import java.util.List; @@ -23,17 +19,9 @@ import java.util.List; @RequestMapping("/admin/api/themes") public class ThemeController { - private final OptionService optionService; - - private final Configuration configuration; - private final ThemeService themeService; - public ThemeController(OptionService optionService, - Configuration configuration, - ThemeService themeService) { - this.optionService = optionService; - this.configuration = configuration; + public ThemeController(ThemeService themeService) { this.themeService = themeService; } @@ -61,7 +49,7 @@ public class ThemeController { */ @GetMapping("files") public List listFiles() { - return themeService.listThemeFolderBy(themeService.getActivatedTheme()); + return themeService.listThemeFolderBy(themeService.getActivatedThemeId()); } @GetMapping("files/content") @@ -72,34 +60,38 @@ public class ThemeController { @PutMapping("files/content") public void updateContentBy(@RequestParam(name = "path") String path, @RequestParam(name = "content") String content) { + // TODO Refactor the params to body themeService.saveTemplateContent(path, content); } @GetMapping("files/custom") public List customTemplate() { - return themeService.getCustomTpl(themeService.getActivatedTheme()); + return themeService.getCustomTpl(themeService.getActivatedThemeId()); } @PostMapping("{themeId}/activate") - @ApiOperation("Active a theme") - public void active(@RequestParam("themeId") String themeId) throws TemplateModelException { - themeService.activeTheme(themeId); - - // TODO Check existence of the theme - optionService.saveProperty(PrimaryProperties.THEME, themeId); - configuration.setSharedVariable("themeName", themeId); - configuration.setSharedVariable("options", optionService.listOptions()); + @ApiOperation("Activates a theme") + public ThemeProperty active(@RequestParam("themeId") String themeId) { + return themeService.activeTheme(themeId); } - @DeleteMapping("key/{key}") - @ApiOperation("Deletes a theme") - public void deleteBy(@PathVariable("key") String key) { - themeService.deleteTheme(key); + @GetMapping("activate") + @ApiOperation("Gets activate theme") + public ThemeProperty getActivateTheme() { + return themeService.getThemeOfNonNullBy(themeService.getActivatedThemeId()); } - @GetMapping("configurations") + @GetMapping("activate/configurations") @ApiOperation("Fetches theme configuration") public BaseResponse fetchConfig() { - return BaseResponse.ok(themeService.fetchConfig(themeService.getActivatedTheme())); + return BaseResponse.ok(themeService.fetchConfig(themeService.getActivatedThemeId())); } + + @DeleteMapping("{themeId}") + @ApiOperation("Deletes a theme") + public void deleteBy(@PathVariable("themeId") String themeId) { + themeService.deleteTheme(themeId); + } + + } diff --git a/src/main/java/run/halo/app/web/controller/core/CommonController.java b/src/main/java/run/halo/app/web/controller/core/CommonController.java index 4773c697e..1fd3a80e7 100644 --- a/src/main/java/run/halo/app/web/controller/core/CommonController.java +++ b/src/main/java/run/halo/app/web/controller/core/CommonController.java @@ -114,7 +114,7 @@ public class CommonController implements ErrorController { return "common/error/404"; } StrBuilder path = new StrBuilder("themes/"); - path.append(themeService.getActivatedTheme()); + path.append(themeService.getActivatedThemeId()); path.append("/404"); return path.toString(); } @@ -130,7 +130,7 @@ public class CommonController implements ErrorController { return "common/error/500"; } StrBuilder path = new StrBuilder("themes/"); - path.append(themeService.getActivatedTheme()); + path.append(themeService.getActivatedThemeId()); path.append("/500"); return path.toString(); }