Merge branch 'static-page' into dev

pull/471/head
ruibaby 2020-01-06 20:30:48 +08:00
commit f975a20fea
38 changed files with 1801 additions and 10 deletions

View File

@ -33,6 +33,7 @@ public class Application extends SpringBootServletInitializer {
// Run application
context = SpringApplication.run(Application.class, args);
}
/**

View File

@ -0,0 +1,50 @@
package run.halo.app.controller.admin.api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import run.halo.app.service.DataProcessService;
import run.halo.app.service.ThemeSettingService;
/**
* @author ryanwang
* @date 2019-12-29
*/
@RestController
@RequestMapping("/api/admin/data/process")
public class DataProcessController {
private final DataProcessService dataProcessService;
private final ThemeSettingService themeSettingService;
public DataProcessController(DataProcessService dataProcessService,
ThemeSettingService themeSettingService) {
this.dataProcessService = dataProcessService;
this.themeSettingService = themeSettingService;
}
@PutMapping("url/replace")
@ApiOperation("Replace url in all table.")
public void replaceUrl(@RequestParam("oldUrl") String oldUrl,
@RequestParam("newUrl") String newUrl) {
dataProcessService.replaceAllUrl(oldUrl, newUrl);
}
@DeleteMapping("themes/settings/inactivated")
@ApiOperation("Delete inactivated theme settings.")
public void deleteInactivatedThemeSettings() {
themeSettingService.deleteInactivated();
}
@DeleteMapping("tags/unused")
@ApiOperation("Delete unused tags")
public void deleteUnusedTags() {
// TODO
}
@DeleteMapping("categories/unused")
@ApiOperation("Delete unused categories")
public void deleteUnusedCategories() {
// TODO
}
}

View File

@ -0,0 +1,92 @@
package run.halo.app.controller.admin.api;
import cn.hutool.core.io.FileUtil;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import run.halo.app.model.properties.NetlifyStaticDeployProperties;
import run.halo.app.model.support.StaticPageFile;
import run.halo.app.service.OptionService;
import run.halo.app.service.StaticPageService;
import java.io.FileNotFoundException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
/**
* Static page controller.
*
* @author ryanwang
* @date 2019-12-25
*/
@RestController
@RequestMapping("/api/admin/static_page")
public class StaticPageController {
private final static String DEPLOY_API = "https://api.netlify.com/api/v1/sites/%s/deploys";
private final OptionService optionService;
private final RestTemplate httpsRestTemplate;
private final StaticPageService staticPageService;
public StaticPageController(StaticPageService staticPageService,
OptionService optionService,
RestTemplate httpsRestTemplate) {
this.staticPageService = staticPageService;
this.optionService = optionService;
this.httpsRestTemplate = httpsRestTemplate;
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
this.httpsRestTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter);
}
@GetMapping
@ApiOperation("List static page files.")
public List<StaticPageFile> list() {
return staticPageService.listFile();
}
@GetMapping("generate")
@ApiOperation("Generate static page files.")
public void generate() {
staticPageService.generate();
}
@PostMapping("deploy")
@ApiOperation("Deploy static page to remove platform")
public void deploy() {
staticPageService.deploy();
}
@GetMapping("netlify")
public void testNetlify() throws FileNotFoundException {
String domain = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_DOMAIN).toString();
String siteId = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_SITE_ID).toString();
String token = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_TOKEN).toString();
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/zip");
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
Path path = staticPageService.zipStaticPagesDirectory();
byte[] bytes = FileUtil.readBytes(path.toFile());
HttpEntity<byte[]> httpEntity = new HttpEntity<>(bytes, headers);
ResponseEntity<Object> responseEntity = httpsRestTemplate.postForEntity(String.format(DEPLOY_API, siteId), httpEntity, Object.class);
}
}

View File

@ -1,5 +1,7 @@
package run.halo.app.handler.migrate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import run.halo.app.model.enums.MigrateType;
@ -9,6 +11,8 @@ import run.halo.app.model.enums.MigrateType;
* @author ryanwang
* @date 2019-10-30
*/
@Slf4j
@Component
public class CnBlogsMigrateHandler implements MigrateHandler {
@Override

View File

@ -31,7 +31,7 @@ public class MigrateHandlers {
public MigrateHandlers(ApplicationContext applicationContext) {
// Add all migrate handler
addFileHandlers(applicationContext.getBeansOfType(MigrateHandler.class).values());
addMigrateHandlers(applicationContext.getBeansOfType(MigrateHandler.class).values());
}
@NonNull
@ -56,7 +56,7 @@ public class MigrateHandlers {
* @return current migrate handlers
*/
@NonNull
private MigrateHandlers addFileHandlers(@Nullable Collection<MigrateHandler> migrateHandlers) {
private MigrateHandlers addMigrateHandlers(@Nullable Collection<MigrateHandler> migrateHandlers) {
if (!CollectionUtils.isEmpty(migrateHandlers)) {
this.migrateHandlers.addAll(migrateHandlers);
}

View File

@ -0,0 +1,33 @@
package run.halo.app.handler.staticdeploy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import run.halo.app.model.enums.StaticDeployType;
import run.halo.app.service.OptionService;
/**
* Git deploy handler.
*
* @author ryanwang
* @date 2019-12-26
*/
@Slf4j
@Component
public class GitStaticDeployHandler implements StaticDeployHandler {
private final OptionService optionService;
public GitStaticDeployHandler(OptionService optionService) {
this.optionService = optionService;
}
@Override
public void deploy() {
}
@Override
public boolean supportType(StaticDeployType type) {
return StaticDeployType.GIT.equals(type);
}
}

View File

@ -0,0 +1,67 @@
package run.halo.app.handler.staticdeploy;
import cn.hutool.core.io.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import run.halo.app.model.enums.StaticDeployType;
import run.halo.app.model.properties.NetlifyStaticDeployProperties;
import run.halo.app.service.OptionService;
import run.halo.app.service.StaticPageService;
import java.nio.file.Path;
/**
* Netlify deploy handler.
*
* @author ryanwang
* @date 2019-12-26
*/
@Slf4j
@Component
public class NetlifyStaticDeployHandler implements StaticDeployHandler {
private final static String DEPLOY_API = "https://api.netlify.com/api/v1/sites/%s/deploys";
private final OptionService optionService;
private final RestTemplate httpsRestTemplate;
private final StaticPageService staticPageService;
public NetlifyStaticDeployHandler(OptionService optionService,
RestTemplate httpsRestTemplate,
StaticPageService staticPageService) {
this.optionService = optionService;
this.httpsRestTemplate = httpsRestTemplate;
this.staticPageService = staticPageService;
}
@Override
public void deploy() {
String domain = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_DOMAIN).toString();
String siteId = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_SITE_ID).toString();
String token = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_TOKEN).toString();
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/zip");
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
Path path = staticPageService.zipStaticPagesDirectory();
byte[] bytes = FileUtil.readBytes(path.toFile());
HttpEntity<byte[]> httpEntity = new HttpEntity<>(bytes, headers);
ResponseEntity<Object> responseEntity = httpsRestTemplate.postForEntity(String.format(DEPLOY_API, siteId), httpEntity, Object.class);
}
@Override
public boolean supportType(StaticDeployType type) {
return StaticDeployType.NETLIFY.equals(type);
}
}

View File

@ -0,0 +1,26 @@
package run.halo.app.handler.staticdeploy;
import org.springframework.lang.Nullable;
import run.halo.app.model.enums.StaticDeployType;
/**
* Static deploy handler interface class.
*
* @author ryanwang
* @date 2019-12-26
*/
public interface StaticDeployHandler {
/**
* do deploy.
*/
void deploy();
/**
* Checks if the given type is supported.
*
* @param type deploy type
* @return true if supported; false or else
*/
boolean supportType(@Nullable StaticDeployType type);
}

View File

@ -0,0 +1,65 @@
package run.halo.app.handler.staticdeploy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.StaticDeployType;
import java.util.Collection;
import java.util.LinkedList;
/**
* Static deploy handlers.
*
* @author ryanwang
* @date 2019-12-26
*/
@Slf4j
@Component
public class StaticDeployHandlers {
private final Collection<StaticDeployHandler> staticDeployHandlers = new LinkedList<>();
public StaticDeployHandlers(ApplicationContext applicationContext) {
// Add all static deploy handler
addStaticDeployHandlers(applicationContext.getBeansOfType(StaticDeployHandler.class).values());
}
/**
* do deploy.
*
* @param staticDeployType static deploy type
*/
public void deploy(@NonNull StaticDeployType staticDeployType) {
Assert.notNull(staticDeployType, "Static deploy type must not be null");
for (StaticDeployHandler staticDeployHandler : staticDeployHandlers) {
if (staticDeployHandler.supportType(staticDeployType)) {
staticDeployHandler.deploy();
return;
}
}
throw new FileOperationException("No available static deploy handler to deploy static pages").setErrorData(staticDeployType);
}
/**
* Adds static deploy handlers.
*
* @param staticDeployHandlers static deploy handler collection
* @return current file handlers
*/
@NonNull
public StaticDeployHandlers addStaticDeployHandlers(@Nullable Collection<StaticDeployHandler> staticDeployHandlers) {
if (!CollectionUtils.isEmpty(staticDeployHandlers)) {
this.staticDeployHandlers.addAll(staticDeployHandlers);
}
return this;
}
}

View File

@ -0,0 +1,36 @@
package run.halo.app.model.enums;
/**
* Static deploy type.
*
* @author ryanwang
* @date 2019-12-26
*/
public enum StaticDeployType implements ValueEnum<Integer> {
/**
* Deploy static pages in remote git repository, such as github pages,gitee pages,coding pages.etc.
*/
GIT(0),
/**
* Deploy static pages in netlify.
*/
NETLIFY(1);
private Integer value;
StaticDeployType(Integer value) {
this.value = value;
}
/**
* Get enum value.
*
* @return enum value
*/
@Override
public Integer getValue() {
return value;
}
}

View File

@ -0,0 +1,76 @@
package run.halo.app.model.properties;
/**
* Git static deploy properties.
*
* @author ryanwang
* @date 2019-12-26
*/
public enum GitStaticDeployProperties implements PropertyEnum {
/**
* Git static deploy domain.
*/
GIT_DOMAIN("git_static_deploy_domain", String.class, ""),
/**
* Git static deploy repository.
*/
GIT_REPOSITORY("git_static_deploy_repository", String.class, ""),
/**
* Git static deploy branch.
*/
GIT_BRANCH("git_static_deploy_branch", String.class, "master"),
/**
* Git static deploy username.
*/
GIT_USERNAME("git_static_deploy_username", String.class, ""),
/**
* Git static deploy email.
*/
GIT_EMAIL("git_static_deploy_email", String.class, ""),
/**
* Git static deploy token.
*/
GIT_TOKEN("git_static_deploy_token", String.class, ""),
/**
* Git static deploy cname.
*/
GIT_CNAME("git_static_deploy_cname", String.class, "");
private final String value;
private final Class<?> type;
private final String defaultValue;
GitStaticDeployProperties(String value, Class<?> type, String defaultValue) {
this.defaultValue = defaultValue;
if (!PropertyEnum.isSupportedType(type)) {
throw new IllegalArgumentException("Unsupported blog property type: " + type);
}
this.value = value;
this.type = type;
}
@Override
public Class<?> getType() {
return type;
}
@Override
public String defaultValue() {
return defaultValue;
}
@Override
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,56 @@
package run.halo.app.model.properties;
/**
* Netlify static deploy properties.
*
* @author ryanwang
* @date 2019-12-26
*/
public enum NetlifyStaticDeployProperties implements PropertyEnum {
/**
* Netlify static deploy domain.
*/
NETLIFY_DOMAIN("netlify_static_deploy_domain", String.class, ""),
/**
* Netlify static deploy site id.
*/
NETLIFY_SITE_ID("netlify_static_deploy_site_id", String.class, ""),
/**
* Netlify static deploy token.
*/
NETLIFY_TOKEN("netlify_static_deploy_token", String.class, "");
private final String value;
private final Class<?> type;
private final String defaultValue;
NetlifyStaticDeployProperties(String value, Class<?> type, String defaultValue) {
this.defaultValue = defaultValue;
if (!PropertyEnum.isSupportedType(type)) {
throw new IllegalArgumentException("Unsupported blog property type: " + type);
}
this.value = value;
this.type = type;
}
@Override
public Class<?> getType() {
return type;
}
@Override
public String defaultValue() {
return defaultValue;
}
@Override
public String getValue() {
return value;
}
}

View File

@ -155,6 +155,9 @@ public interface PropertyEnum extends ValueEnum<String> {
propertyEnumClasses.add(SeoProperties.class);
propertyEnumClasses.add(UpOssProperties.class);
propertyEnumClasses.add(ApiProperties.class);
propertyEnumClasses.add(StaticDeployProperties.class);
propertyEnumClasses.add(GitStaticDeployProperties.class);
propertyEnumClasses.add(NetlifyStaticDeployProperties.class);
Map<String, PropertyEnum> result = new HashMap<>();

View File

@ -0,0 +1,45 @@
package run.halo.app.model.properties;
import run.halo.app.model.enums.StaticDeployType;
/**
* Static deploy properties.
*
* @author ryanwang
* @date 2019-12-26
*/
public enum StaticDeployProperties implements PropertyEnum {
/**
* static deploy type
*/
DEPLOY_TYPE("static_deploy_type", StaticDeployType.class, StaticDeployType.GIT.name());
private final String value;
private final Class<?> type;
private final String defaultValue;
StaticDeployProperties(String value, Class<?> type, String defaultValue) {
this.value = value;
this.type = type;
this.defaultValue = defaultValue;
}
@Override
public Class<?> getType() {
return type;
}
@Override
public String defaultValue() {
return defaultValue;
}
@Override
public String getValue() {
return value;
}
}

View File

@ -30,6 +30,11 @@ public class HaloConst {
*/
public final static String HALO_BACKUP_PREFIX = "halo-backup-";
/**
* Static pages pack prefix.
*/
public final static String STATIC_PAGE_PACK_PREFIX = "static-pages-";
/**
* Default theme name.
*/

View File

@ -16,6 +16,8 @@ import java.util.List;
@ToString
public class StaticFile implements Comparator<StaticFile> {
private String id;
private String name;
private String path;

View File

@ -0,0 +1,37 @@
package run.halo.app.model.support;
import lombok.Data;
import java.util.Comparator;
import java.util.List;
/**
* Static page dto.
*
* @author ryanwang
* @date 2019-12-26
*/
@Data
public class StaticPageFile implements Comparator<StaticPageFile> {
private String id;
private String name;
private Boolean isFile;
private List<StaticPageFile> children;
@Override
public int compare(StaticPageFile leftFile, StaticPageFile rightFile) {
if (leftFile.isFile && !rightFile.isFile) {
return 1;
}
if (!leftFile.isFile && rightFile.isFile) {
return -1;
}
return leftFile.getName().compareTo(rightFile.getName());
}
}

View File

@ -42,4 +42,11 @@ public interface ThemeSettingRepository extends BaseRepository<ThemeSetting, Int
*/
@NonNull
Optional<ThemeSetting> findByThemeIdAndKey(@NonNull String themeId, @NonNull String key);
/**
* Deletes inactivated theme settings.
*
* @param activatedThemeId activated theme id.
*/
void deleteByThemeIdIsNot(@NonNull String activatedThemeId);
}

View File

@ -83,4 +83,13 @@ public interface AttachmentService extends CrudService<Attachment, Integer> {
* @return list of type.
*/
List<AttachmentType> listAllType();
/**
* Replace attachment url in batch.
*
* @param oldUrl old blog url.
* @param newUrl new blog url.
* @return replaced attachments.
*/
List<Attachment> replaceUrl(@NonNull String oldUrl, @NonNull String newUrl);
}

View File

@ -0,0 +1,20 @@
package run.halo.app.service;
import org.springframework.lang.NonNull;
/**
* Data process service interface.
*
* @author ryanwang
* @date 2019-12-29
*/
public interface DataProcessService {
/**
* Replace all url.
*
* @param oldUrl old url must not be null.
* @param newUrl new url must not be null.
*/
void replaceAllUrl(@NonNull String oldUrl, @NonNull String newUrl);
}

View File

@ -338,6 +338,15 @@ public interface OptionService extends CrudService<Option, Integer> {
*/
long getBirthday();
/**
* Replace option url in batch.
*
* @param oldUrl old blog url.
* @param newUrl new blog url.
* @return replaced options.
*/
List<OptionDTO> replaceUrl(@NonNull String oldUrl, @NonNull String newUrl);
/**
* Converts to option output dto.
*

View File

@ -80,4 +80,13 @@ public interface PhotoService extends CrudService<Photo, Integer> {
* @return list of teams
*/
List<String> listAllTeams();
/**
* Replace photo url in batch.
*
* @param oldUrl old blog url.
* @param newUrl new blog url.
* @return replaced photos.
*/
List<PhotoDTO> replaceUrl(@NonNull String oldUrl, @NonNull String newUrl);
}

View File

@ -0,0 +1,53 @@
package run.halo.app.service;
import run.halo.app.model.support.StaticPageFile;
import java.nio.file.Path;
import java.util.List;
import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR;
import static run.halo.app.model.support.HaloConst.TEMP_DIR;
import static run.halo.app.utils.HaloUtils.ensureSuffix;
/**
* Static Page service interface.
*
* @author ryanwang
* @date 2019-12-25
*/
public interface StaticPageService {
/**
* Static page folder location.
*/
String PAGES_FOLDER = "static_pages";
String STATIC_PAGE_PACK_DIR = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "static-pages-pack" + FILE_SEPARATOR;
String[] USELESS_FILE_SUFFIX = {"ftl", "md", "yaml", "yml", "gitignore"};
/**
* Generate pages.
*/
void generate();
/**
* Deploy static pages.
*/
void deploy();
/**
* Zip static pages directory.
*
* @return zip path
*/
Path zipStaticPagesDirectory();
/**
* List file of generated static page.
*
* @return a list of generated static page.
*/
List<StaticPageFile> listFile();
}

View File

@ -64,6 +64,11 @@ public interface ThemeService {
*/
String RENDER_TEMPLATE = "themes/%s/%s";
/**
* Render template with suffix.
*/
String RENDER_TEMPLATE_SUFFIX = "themes/%s/%s.ftl";
/**
* Theme cache key.
*/
@ -231,6 +236,15 @@ public interface ThemeService {
@NonNull
String render(@NonNull String pageName);
/**
* Renders a theme page.
*
* @param pageName must not be blank
* @return full path of the theme page
*/
@NonNull
String renderWithSuffix(@NonNull String pageName);
/**
* Gets current theme id.
*

View File

@ -55,4 +55,18 @@ public interface ThemeSettingService {
*/
@NonNull
Map<String, Object> listAsMapBy(@NonNull String themeId);
/**
* Replace theme setting url in batch.
*
* @param oldUrl old blog url.
* @param newUrl new blog url.
* @return replaced theme settings.
*/
List<ThemeSetting> replaceUrl(@NonNull String oldUrl, @NonNull String newUrl);
/**
* Delete unused theme setting.
*/
void deleteInactivated();
}

View File

@ -304,4 +304,13 @@ public interface BaseCommentService<COMMENT extends BaseComment> extends CrudSer
*/
<T extends BaseCommentDTO> Page<T> filterIpAddress(@NonNull Page<T> commentPage);
/**
* Replace comment url in batch.
*
* @param oldUrl old blog url.
* @param newUrl new blog url.
* @return replaced comments.
*/
List<BaseCommentDTO> replaceUrl(@NonNull String oldUrl, @NonNull String newUrl);
}

View File

@ -287,4 +287,14 @@ public interface BasePostService<POST extends BasePost> extends CrudService<POST
*/
@NonNull
List<POST> updateStatusByIds(@NonNull List<Integer> ids, @NonNull PostStatus status);
/**
* Replace post blog url in batch.
*
* @param oldUrl old blog url.
* @param newUrl new blog url.
* @return replaced posts.
*/
@NonNull
List<BasePostDetailDTO> replaceUrl(@NonNull String oldUrl, @NonNull String newUrl);
}

View File

@ -183,6 +183,22 @@ public class AttachmentServiceImpl extends AbstractCrudService<Attachment, Integ
return attachmentRepository.findAllType();
}
@Override
public List<Attachment> replaceUrl(String oldUrl, String newUrl) {
List<Attachment> attachments = listAll();
List<Attachment> replaced = new ArrayList<>();
attachments.forEach(attachment -> {
if (StringUtils.isNotEmpty(attachment.getPath())) {
attachment.setPath(attachment.getPath().replaceAll(oldUrl, newUrl));
}
if (StringUtils.isNotEmpty(attachment.getThumbPath())) {
attachment.setThumbPath(attachment.getThumbPath().replaceAll(oldUrl, newUrl));
}
replaced.add(attachment);
});
return updateInBatch(replaced);
}
@Override
public Attachment create(Attachment attachment) {
Assert.notNull(attachment, "Attachment must not be null");

View File

@ -610,6 +610,20 @@ public abstract class BaseCommentServiceImpl<COMMENT extends BaseComment> extend
return commentPage;
}
@Override
public List<BaseCommentDTO> replaceUrl(String oldUrl, String newUrl) {
List<COMMENT> comments = listAll();
List<COMMENT> replaced = new ArrayList<>();
comments.forEach(comment -> {
if (StringUtils.isNotEmpty(comment.getAuthorUrl())) {
comment.setAuthorUrl(comment.getAuthorUrl().replaceAll(oldUrl, newUrl));
}
replaced.add(comment);
});
List<COMMENT> updated = updateInBatch(replaced);
return convertTo(updated);
}
/**
* Get children comments recursively.
*

View File

@ -29,10 +29,7 @@ import run.halo.app.utils.HaloUtils;
import run.halo.app.utils.MarkdownUtils;
import run.halo.app.utils.ServiceUtils;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -395,6 +392,26 @@ public abstract class BasePostServiceImpl<POST extends BasePost> extends Abstrac
}).collect(Collectors.toList());
}
@Override
public List<BasePostDetailDTO> replaceUrl(String oldUrl, String newUrl) {
List<POST> posts = listAll();
List<POST> replaced = new ArrayList<>();
posts.forEach(post -> {
if (StringUtils.isNotEmpty(post.getThumbnail())) {
post.setThumbnail(post.getThumbnail().replaceAll(oldUrl, newUrl));
}
if (StringUtils.isNotEmpty(post.getOriginalContent())) {
post.setOriginalContent(post.getOriginalContent().replaceAll(oldUrl, newUrl));
}
if (StringUtils.isNotEmpty(post.getFormatContent())) {
post.setFormatContent(post.getFormatContent().replaceAll(oldUrl, newUrl));
}
replaced.add(post);
});
List<POST> updated = updateInBatch(replaced);
return updated.stream().map(this::convertToDetail).collect(Collectors.toList());
}
@Override
public POST create(POST post) {
// Check title

View File

@ -0,0 +1,65 @@
package run.halo.app.service.impl;
import org.springframework.stereotype.Service;
import run.halo.app.service.*;
/**
* DataProcessService implementation.
*
* @author ryanwang
* @date 2019-12-29
*/
@Service
public class DataProcessServiceImpl implements DataProcessService {
private final PostService postService;
private final SheetService sheetService;
private final PostCommentService postCommentService;
private final SheetCommentService sheetCommentService;
private final JournalCommentService journalCommentService;
private final AttachmentService attachmentService;
private final OptionService optionService;
private final PhotoService photoService;
private final ThemeSettingService themeSettingService;
public DataProcessServiceImpl(PostService postService,
SheetService sheetService,
PostCommentService postCommentService,
SheetCommentService sheetCommentService,
JournalCommentService journalCommentService,
AttachmentService attachmentService,
OptionService optionService,
PhotoService photoService,
ThemeSettingService themeSettingService) {
this.postService = postService;
this.sheetService = sheetService;
this.postCommentService = postCommentService;
this.sheetCommentService = sheetCommentService;
this.journalCommentService = journalCommentService;
this.attachmentService = attachmentService;
this.optionService = optionService;
this.photoService = photoService;
this.themeSettingService = themeSettingService;
}
@Override
public void replaceAllUrl(String oldUrl, String newUrl) {
postService.replaceUrl(oldUrl, newUrl);
sheetService.replaceUrl(oldUrl, newUrl);
postCommentService.replaceUrl(oldUrl, newUrl);
sheetCommentService.replaceUrl(oldUrl, newUrl);
journalCommentService.replaceUrl(oldUrl, newUrl);
attachmentService.replaceUrl(oldUrl, newUrl);
optionService.replaceUrl(oldUrl, newUrl);
photoService.replaceUrl(oldUrl, newUrl);
themeSettingService.replaceUrl(oldUrl, newUrl);
}
}

View File

@ -36,6 +36,7 @@ import run.halo.app.utils.ValidationUtils;
import javax.persistence.criteria.Predicate;
import java.util.*;
import java.util.stream.Collectors;
/**
* OptionService implementation class
@ -460,6 +461,21 @@ public class OptionServiceImpl extends AbstractCrudService<Option, Integer> impl
});
}
@Override
public List<OptionDTO> replaceUrl(String oldUrl, String newUrl) {
List<Option> options = listAll();
List<Option> replaced = new ArrayList<>();
options.forEach(option -> {
if (StringUtils.isNotEmpty(option.getValue())) {
option.setValue(option.getValue().replaceAll(oldUrl, newUrl));
}
replaced.add(option);
});
List<Option> updated = updateInBatch(replaced);
publishOptionUpdatedEvent();
return updated.stream().map(this::convertToDto).collect(Collectors.toList());
}
@Override
public OptionSimpleDTO convertToDto(Option option) {
Assert.notNull(option, "Option must not be null");

View File

@ -19,10 +19,7 @@ import run.halo.app.service.base.AbstractCrudService;
import run.halo.app.utils.ServiceUtils;
import javax.persistence.criteria.Predicate;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
/**
@ -114,6 +111,23 @@ public class PhotoServiceImpl extends AbstractCrudService<Photo, Integer> implem
return photoRepository.findAllTeams();
}
@Override
public List<PhotoDTO> replaceUrl(String oldUrl, String newUrl) {
List<Photo> photos = listAll();
List<Photo> replaced = new ArrayList<>();
photos.forEach(photo -> {
if (StringUtils.isNotEmpty(photo.getThumbnail())) {
photo.setThumbnail(photo.getThumbnail().replace(oldUrl, newUrl));
}
if (StringUtils.isNotEmpty(photo.getUrl())) {
photo.setUrl(photo.getUrl().replaceAll(oldUrl, newUrl));
}
replaced.add(photo);
});
List<Photo> updated = updateInBatch(replaced);
return updated.stream().map(photo -> (PhotoDTO) new PhotoDTO().convertFrom(photo)).collect(Collectors.toList());
}
@NonNull
private Specification<Photo> buildSpecByQuery(@NonNull PhotoQuery photoQuery) {
Assert.notNull(photoQuery, "Photo query must not be null");

View File

@ -0,0 +1,835 @@
package run.halo.app.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.PageUtil;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.ui.ModelMap;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.util.Assert;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.ServiceException;
import run.halo.app.handler.staticdeploy.StaticDeployHandlers;
import run.halo.app.handler.theme.config.support.ThemeProperty;
import run.halo.app.model.dto.PhotoDTO;
import run.halo.app.model.entity.*;
import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.enums.StaticDeployType;
import run.halo.app.model.properties.PostProperties;
import run.halo.app.model.properties.StaticDeployProperties;
import run.halo.app.model.support.HaloConst;
import run.halo.app.model.support.StaticPageFile;
import run.halo.app.model.vo.PostDetailVO;
import run.halo.app.model.vo.PostListVO;
import run.halo.app.model.vo.SheetDetailVO;
import run.halo.app.service.*;
import run.halo.app.utils.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
import static org.springframework.data.domain.Sort.Direction.DESC;
/**
* Static Page service implementation.
*
* @author ryanwang
* @date 2019-12-25
*/
@Slf4j
@Service
public class StaticPageServiceImpl implements StaticPageService {
private final Path pagesDir;
private final PostService postService;
private final PostCategoryService postCategoryService;
private final PostTagService postTagService;
private final PostMetaService postMetaService;
private final SheetService sheetService;
private final CategoryService categoryService;
private final TagService tagService;
private final LinkService linkService;
private final PhotoService photoService;
private final JournalService journalService;
private final ThemeService themeService;
private final HaloProperties haloProperties;
private final OptionService optionService;
private final FreeMarkerConfigurer freeMarkerConfigurer;
private final StaticDeployHandlers staticDeployHandlers;
public StaticPageServiceImpl(PostService postService,
PostCategoryService postCategoryService,
PostTagService postTagService,
PostMetaService postMetaService,
SheetService sheetService,
CategoryService categoryService,
TagService tagService,
LinkService linkService,
PhotoService photoService,
JournalService journalService,
ThemeService themeService,
HaloProperties haloProperties,
OptionService optionService,
FreeMarkerConfigurer freeMarkerConfigurer,
StaticDeployHandlers staticDeployHandlers) throws IOException {
this.postService = postService;
this.postCategoryService = postCategoryService;
this.postTagService = postTagService;
this.postMetaService = postMetaService;
this.sheetService = sheetService;
this.categoryService = categoryService;
this.tagService = tagService;
this.linkService = linkService;
this.photoService = photoService;
this.journalService = journalService;
this.themeService = themeService;
this.haloProperties = haloProperties;
this.optionService = optionService;
this.freeMarkerConfigurer = freeMarkerConfigurer;
this.staticDeployHandlers = staticDeployHandlers;
pagesDir = Paths.get(haloProperties.getWorkDir(), PAGES_FOLDER);
FileUtils.createIfAbsent(pagesDir);
Files.createDirectories(Paths.get(STATIC_PAGE_PACK_DIR));
}
@Override
public void generate() {
try {
this.cleanFolder();
this.generateIndex(1);
this.generatePost();
this.generateArchives(1);
this.generateSheet();
this.generateLink();
this.generatePhoto(1);
this.generateCategories();
this.generateTags();
this.generateRss();
this.generateAtom();
this.generateSiteMapHtml();
this.generateSiteMapXml();
this.generateRobots();
this.generateReadme();
this.copyThemeFolder();
this.copyUpload();
this.copyStatic();
} catch (Exception e) {
throw new ServiceException("生成静态页面失败!", e);
}
}
@Override
public void deploy() {
StaticDeployType type = getStaticDeployType();
staticDeployHandlers.deploy(type);
}
@Override
public Path zipStaticPagesDirectory() {
try {
String staticPagePackName = HaloConst.STATIC_PAGE_PACK_PREFIX +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-")) +
IdUtil.simpleUUID().hashCode() + ".zip";
Path staticPageZipPath = Files.createFile(Paths.get(STATIC_PAGE_PACK_DIR, staticPagePackName));
FileUtils.zip(pagesDir, staticPageZipPath);
return staticPageZipPath;
} catch (IOException e) {
throw new ServiceException("Failed to zip static pages directory", e);
}
}
@Override
public List<StaticPageFile> listFile() {
return listStaticPageFileTree(pagesDir);
}
@Nullable
private List<StaticPageFile> listStaticPageFileTree(@NonNull Path topPath) {
Assert.notNull(topPath, "Top path must not be null");
if (!Files.isDirectory(topPath)) {
return null;
}
try (Stream<Path> pathStream = Files.list(topPath)) {
List<StaticPageFile> staticPageFiles = new LinkedList<>();
pathStream.forEach(path -> {
StaticPageFile staticPageFile = new StaticPageFile();
staticPageFile.setId(IdUtil.fastSimpleUUID());
staticPageFile.setName(path.getFileName().toString());
staticPageFile.setIsFile(Files.isRegularFile(path));
if (Files.isDirectory(path)) {
staticPageFile.setChildren(listStaticPageFileTree(path));
}
staticPageFiles.add(staticPageFile);
});
staticPageFiles.sort(new StaticPageFile());
return staticPageFiles;
} catch (IOException e) {
throw new ServiceException("Failed to list sub files", e);
}
}
/**
* Clean static pages folder
*/
private void cleanFolder() {
FileUtils.deleteFolderQuietly(pagesDir);
}
/**
* Generate index.html and page/{page}/index.html.
*
* @param page current page.
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateIndex(int page) throws IOException, TemplateException {
if (!themeService.templateExists("index.ftl")) {
log.warn("index.ftl not found,skip!");
return;
}
ModelMap model = new ModelMap();
String indexSort = optionService.getByPropertyOfNonNull(PostProperties.INDEX_SORT).toString();
int pageSize = optionService.getPostPageSize();
Pageable pageable = PageRequest.of(page >= 1 ? page - 1 : page, pageSize, Sort.by(DESC, "topPriority").and(Sort.by(DESC, indexSort)));
Page<Post> postPage = postService.pageBy(PostStatus.PUBLISHED, pageable);
Page<PostListVO> posts = postService.convertToListVo(postPage);
int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3);
model.addAttribute("is_index", true);
model.addAttribute("posts", posts);
model.addAttribute("rainbow", rainbow);
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("index"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter;
if (page == 1) {
fileWriter = new FileWriter(getPageFile("index.html"), "UTF-8");
} else {
fileWriter = new FileWriter(getPageFile("page/" + page + "/index.html"), "UTF-8");
}
fileWriter.write(html);
if (postPage.hasNext()) {
generateIndex(postPage.getNumber() + 2);
log.info("Generate page/{}/index.html", postPage.getNumber() + 2);
}
}
/**
* Generate archives/index.html and archives/page/{page}/index.html.
*
* @param page current page
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateArchives(int page) throws IOException, TemplateException {
if (!themeService.templateExists("archives.ftl")) {
log.warn("archives.ftl not found,skip!");
return;
}
ModelMap model = new ModelMap();
Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), Sort.by(DESC, "topPriority"));
Page<Post> postPage = postService.pageBy(PostStatus.PUBLISHED, pageable);
Page<PostListVO> postListVos = postService.convertToListVo(postPage);
int[] pageRainbow = PageUtil.rainbow(page, postListVos.getTotalPages(), 3);
model.addAttribute("is_archives", true);
model.addAttribute("pageRainbow", pageRainbow);
model.addAttribute("posts", postListVos);
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("archives"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter;
if (page == 1) {
fileWriter = new FileWriter(getPageFile("archives/index.html"), "UTF-8");
} else {
fileWriter = new FileWriter(getPageFile("archives/page/" + page + "/index.html"), "UTF-8");
}
fileWriter.write(html);
if (postPage.hasNext()) {
generateArchives(postPage.getNumber() + 2);
log.info("Generate page/{}/index.html", postPage.getNumber() + 2);
}
}
/**
* Generate archives/{url}/index.html.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generatePost() throws IOException, TemplateException {
if (!themeService.templateExists("post.ftl")) {
log.warn("post.ftl not found,skip!");
return;
}
List<Post> posts = postService.listAllBy(PostStatus.PUBLISHED);
for (Post post : posts) {
log.info("Generate archives/{}/index.html", post.getUrl());
ModelMap model = new ModelMap();
postService.getNextPost(post.getCreateTime()).ifPresent(nextPost -> model.addAttribute("nextPost", nextPost));
postService.getPrePost(post.getCreateTime()).ifPresent(prePost -> model.addAttribute("prePost", prePost));
List<Category> categories = postCategoryService.listCategoriesBy(post.getId());
List<Tag> tags = postTagService.listTagsBy(post.getId());
List<PostMeta> metas = postMetaService.listBy(post.getId());
model.addAttribute("is_post", true);
model.addAttribute("post", postService.convertToDetailVo(post));
model.addAttribute("categories", categories);
model.addAttribute("tags", tags);
model.addAttribute("metas", postMetaService.convertToMap(metas));
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("post"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("archives/" + post.getUrl() + "/index.html"), "UTF-8");
fileWriter.write(html);
log.info("Generate archives/{}/index.html succeed.", post.getUrl());
}
}
/**
* Generate s/{url}/index.html.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateSheet() throws IOException, TemplateException {
if (!themeService.templateExists("sheet.ftl")) {
log.warn("sheet.ftl not found,skip!");
return;
}
List<Sheet> sheets = sheetService.listAllBy(PostStatus.PUBLISHED);
for (Sheet sheet : sheets) {
log.info("Generate s/{}/index.html", sheet.getUrl());
ModelMap model = new ModelMap();
SheetDetailVO sheetDetailVO = sheetService.convertToDetailVo(sheet);
model.addAttribute("sheet", sheetDetailVO);
model.addAttribute("post", sheetDetailVO);
model.addAttribute("is_sheet", true);
String templateName = "sheet";
if (themeService.templateExists(ThemeService.CUSTOM_SHEET_PREFIX + sheet.getTemplate() + HaloConst.SUFFIX_FTL)) {
templateName = ThemeService.CUSTOM_SHEET_PREFIX + sheet.getTemplate();
}
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix(templateName));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("s/" + sheet.getUrl() + "/index.html"), "UTF-8");
fileWriter.write(html);
log.info("Generate s/{}/index.html succeed.", sheet.getUrl());
}
}
/**
* Generate links/index.html.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateLink() throws IOException, TemplateException {
log.info("Generate links.html");
if (!themeService.templateExists("links.ftl")) {
log.warn("links.ftl not found,skip!");
return;
}
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("links"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, null);
FileWriter fileWriter = new FileWriter(getPageFile("links/index.html"), "UTF-8");
fileWriter.write(html);
log.info("Generate links.html succeed.");
}
/**
* Generate photos/index.html and photos/page/{page}/index.html.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generatePhoto(int page) throws IOException, TemplateException {
log.info("Generate photos.html");
if (!themeService.templateExists("photos.ftl")) {
log.warn("photos.ftl not found,skip!");
return;
}
ModelMap model = new ModelMap();
Pageable pageable = PageRequest.of(page >= 1 ? page - 1 : page, 10, Sort.by(DESC, "createTime"));
Page<PhotoDTO> photos = photoService.pageBy(pageable);
model.addAttribute("photos", photos);
FileWriter fileWriter;
if (page == 1) {
fileWriter = new FileWriter(getPageFile("photos/index.html"), "UTF-8");
} else {
fileWriter = new FileWriter(getPageFile("photos/page/" + page + "/photos.html"), "UTF-8");
}
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("photos"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
fileWriter.write(html);
log.info("Generate photos.html succeed.");
if (photos.hasNext()) {
generatePhoto(photos.getNumber() + 2);
log.info("Generate page/{}/index.html", photos.getNumber() + 2);
}
}
/**
* Generate categories/index.html.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateCategories() throws IOException, TemplateException {
log.info("Generate categories.html");
ModelMap model = new ModelMap();
if (!themeService.templateExists("categories.ftl")) {
log.warn("categories.ftl not found,skip!");
return;
}
model.addAttribute("is_categories", true);
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("categories"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("categories/index.html"), "UTF-8");
fileWriter.write(html);
List<Category> categories = categoryService.listAll();
for (Category category : categories) {
generateCategory(1, category);
}
log.info("Generate categories.html succeed.");
}
/**
* Generate categories/{slugName}/index.html and categories/{slugName}/{page}/index.html.
*
* @param page current page
* @param category current category
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateCategory(int page, Category category) throws IOException, TemplateException {
if (!themeService.templateExists("category.ftl")) {
log.warn("category.ftl not found,skip!");
return;
}
ModelMap model = new ModelMap();
final Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), Sort.by(DESC, "createTime"));
Page<Post> postPage = postCategoryService.pagePostBy(category.getId(), PostStatus.PUBLISHED, pageable);
Page<PostListVO> posts = postService.convertToListVo(postPage);
final int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3);
model.addAttribute("is_category", true);
model.addAttribute("posts", posts);
model.addAttribute("rainbow", rainbow);
model.addAttribute("category", category);
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("category"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter;
if (page == 1) {
fileWriter = new FileWriter(getPageFile("categories/" + category.getSlugName() + "/index.html"), "UTF-8");
} else {
fileWriter = new FileWriter(getPageFile("categories/" + category.getSlugName() + "/page/" + page + "/index.html"), "UTF-8");
}
fileWriter.write(html);
if (postPage.hasNext()) {
generateCategory(postPage.getNumber() + 2, category);
log.info("Generate categories/{}/page/{}/index.html", category.getSlugName(), postPage.getNumber() + 2);
}
}
/**
* Generate tags/{slugName}/index.html and tags/{slugName}/{page}/index.html.
*
* @param page current page
* @param category current category
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateTag(int page, Tag tag) throws IOException, TemplateException {
if (!themeService.templateExists("tag.ftl")) {
log.warn("tag.ftl not found,skip!");
return;
}
ModelMap model = new ModelMap();
final Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), Sort.by(DESC, "createTime"));
Page<Post> postPage = postTagService.pagePostsBy(tag.getId(), PostStatus.PUBLISHED, pageable);
Page<PostListVO> posts = postService.convertToListVo(postPage);
final int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3);
model.addAttribute("is_tag", true);
model.addAttribute("posts", posts);
model.addAttribute("rainbow", rainbow);
model.addAttribute("tag", tag);
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("tag"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter;
if (page == 1) {
fileWriter = new FileWriter(getPageFile("tags/" + tag.getSlugName() + "/index.html"), "UTF-8");
} else {
fileWriter = new FileWriter(getPageFile("tags/" + tag.getSlugName() + "/page/" + page + "/index.html"), "UTF-8");
}
fileWriter.write(html);
if (postPage.hasNext()) {
generateTag(postPage.getNumber() + 2, tag);
log.info("Generate tags/{}/page/{}/index.html", tag.getSlugName(), postPage.getNumber() + 2);
}
}
/**
* Generate tags/index.html.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateTags() throws IOException, TemplateException {
log.info("Generate tags.html");
ModelMap model = new ModelMap();
if (!themeService.templateExists("tags.ftl")) {
log.warn("tags.ftl not found,skip!");
return;
}
model.addAttribute("is_tags", true);
Template template = freeMarkerConfigurer.getConfiguration().getTemplate(themeService.renderWithSuffix("tags"));
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("tags/index.html"), "UTF-8");
fileWriter.write(html);
log.info("Generate tags.html succeed.");
List<Tag> tags = tagService.listAll();
for (Tag tag : tags) {
generateTag(1, tag);
}
}
/**
* Generate rss.xml and feed.xml
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateRss() throws IOException, TemplateException {
log.info("Generate rss.xml/feed.xml");
ModelMap model = new ModelMap();
model.addAttribute("posts", buildPosts(buildPostPageable(optionService.getRssPageSize())));
Template template = freeMarkerConfigurer.getConfiguration().getTemplate("common/web/rss.ftl");
String xml = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter rssWriter = new FileWriter(getPageFile("rss.xml"), "UTF-8");
rssWriter.write(xml);
FileWriter feedWriter = new FileWriter(getPageFile("feed.xml"), "UTF-8");
feedWriter.write(xml);
log.info("Generate rss.xml/feed.xml succeed.");
}
/**
* Generate atom.xml
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateAtom() throws IOException, TemplateException {
log.info("Generate atom.xml");
ModelMap model = new ModelMap();
model.addAttribute("posts", buildPosts(buildPostPageable(optionService.getRssPageSize())));
Template template = freeMarkerConfigurer.getConfiguration().getTemplate("common/web/atom.ftl");
String xml = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("atom.xml"), "UTF-8");
fileWriter.write(xml);
log.info("Generate atom.xml succeed.");
}
/**
* Generate sitemap.html
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateSiteMapHtml() throws IOException, TemplateException {
log.info("Generate sitemap.html");
ModelMap model = new ModelMap();
model.addAttribute("posts", buildPosts(buildPostPageable(optionService.getRssPageSize())));
Template template = freeMarkerConfigurer.getConfiguration().getTemplate("common/web/sitemap_html.ftl");
String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("sitemap.html"), "UTF-8");
fileWriter.write(html);
log.info("Generate sitemap.html succeed.");
}
/**
* Generate sitemap.xml
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateSiteMapXml() throws IOException, TemplateException {
log.info("Generate sitemap.xml");
ModelMap model = new ModelMap();
model.addAttribute("posts", buildPosts(buildPostPageable(optionService.getRssPageSize())));
Template template = freeMarkerConfigurer.getConfiguration().getTemplate("common/web/sitemap_xml.ftl");
String xml = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
FileWriter fileWriter = new FileWriter(getPageFile("sitemap.xml"), "UTF-8");
fileWriter.write(xml);
log.info("Generate sitemap.xml succeed.");
}
/**
* Generate robots.txt
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateRobots() throws IOException, TemplateException {
log.info("Generate robots.txt");
Template template = freeMarkerConfigurer.getConfiguration().getTemplate("common/web/robots.ftl");
String txt = FreeMarkerTemplateUtils.processTemplateIntoString(template, null);
FileWriter fileWriter = new FileWriter(getPageFile("robots.txt"), "UTF-8");
fileWriter.write(txt);
log.info("Generate robots.txt succeed.");
}
/**
* Generate README.md.
*
* @throws IOException IOException
* @throws TemplateException TemplateException
*/
private void generateReadme() throws IOException, TemplateException {
log.info("Generate readme.md");
Template template = freeMarkerConfigurer.getConfiguration().getTemplate("common/web/readme.ftl");
String txt = FreeMarkerTemplateUtils.processTemplateIntoString(template, null);
FileWriter fileWriter = new FileWriter(getPageFile("README.md"), "UTF-8");
fileWriter.write(txt);
log.info("Generate readme.md succeed.");
}
/**
* Copy current theme folder.
*
* @throws IOException IOException
*/
private void copyThemeFolder() throws IOException {
ThemeProperty activatedTheme = themeService.getActivatedTheme();
Path path = Paths.get(pagesDir.toString(), activatedTheme.getFolderName());
FileUtils.createIfAbsent(path);
FileUtils.copyFolder(Paths.get(activatedTheme.getThemePath()), path);
cleanThemeFolder(Paths.get(pagesDir.toString(), activatedTheme.getFolderName()));
}
private void cleanThemeFolder(Path themePath) {
if (!Files.isDirectory(themePath)) {
return;
}
try (Stream<Path> pathStream = Files.list(themePath)) {
pathStream.forEach(path -> {
if (!Files.isDirectory(path)) {
for (String suffix : USELESS_FILE_SUFFIX) {
if (suffix.contains(FileUtil.extName(path.toFile()))) {
try {
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
}
}
} else {
if (path.getFileName().toString().contains(".git")) {
FileUtils.deleteFolderQuietly(path);
} else {
cleanThemeFolder(path);
}
}
});
} catch (IOException e) {
throw new ServiceException("Failed to list sub files", e);
}
}
/**
* Copy upload folder.
*
* @throws IOException IOException
*/
private void copyUpload() throws IOException {
Path path = Paths.get(pagesDir.toString(), "upload");
FileUtils.createIfAbsent(path);
FileUtils.copyFolder(Paths.get(haloProperties.getWorkDir(), "upload"), path);
}
/**
* Copy static folder.
*
* @throws IOException IOException
*/
private void copyStatic() throws IOException {
FileUtils.copyFolder(Paths.get(haloProperties.getWorkDir(), "static"), pagesDir);
}
/**
* Build posts for feed
*
* @param pageable pageable
* @return List<Post>
*/
private List<PostDetailVO> buildPosts(@NonNull Pageable pageable) {
Page<Post> postPage = postService.pageBy(PostStatus.PUBLISHED, pageable);
Page<PostDetailVO> posts = postService.convertToDetailVo(postPage);
posts.getContent().forEach(postListVO -> {
try {
// Encode post url
postListVO.setUrl(URLEncoder.encode(postListVO.getUrl(), StandardCharsets.UTF_8.name()));
} catch (UnsupportedEncodingException e) {
log.warn("Failed to encode url: " + postListVO.getUrl(), e);
}
});
return posts.getContent();
}
/**
* Builds page info for post.
*
* @param size page size
* @return page info
*/
@NonNull
private Pageable buildPostPageable(int size) {
return PageRequest.of(0, size, Sort.by(DESC, "createTime"));
}
private File getPageFile(String subPath) {
Path path = Paths.get(pagesDir.toString(), subPath);
return path.toFile();
}
/**
* Get static deploy type from options.
*
* @return static deploy type
*/
@NonNull
private StaticDeployType getStaticDeployType() {
return optionService.getEnumByPropertyOrDefault(StaticDeployProperties.DEPLOY_TYPE, StaticDeployType.class, StaticDeployType.GIT);
}
}

View File

@ -1,5 +1,6 @@
package run.halo.app.service.impl;
import cn.hutool.core.util.IdUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
@ -59,6 +60,7 @@ public class StaticStorageServiceImpl implements StaticStorageService {
pathStream.forEach(path -> {
StaticFile staticFile = new StaticFile();
staticFile.setId(IdUtil.fastSimpleUUID());
staticFile.setName(path.getFileName().toString());
staticFile.setPath(path.toString());
staticFile.setRelativePath(StringUtils.removeStart(path.toString(), staticDir.toString()));

View File

@ -352,6 +352,14 @@ public class ThemeServiceImpl implements ThemeService {
return String.format(RENDER_TEMPLATE, activatedTheme.getFolderName(), pageName);
}
@Override
public String renderWithSuffix(String pageName) {
// Get activated theme
ThemeProperty activatedTheme = getActivatedTheme();
// Build render url
return String.format(RENDER_TEMPLATE_SUFFIX, activatedTheme.getFolderName(), pageName);
}
@Override
public String getActivatedThemeId() {
if (activatedThemeId == null) {

View File

@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.exception.ServiceException;
@ -160,6 +161,25 @@ public class ThemeSettingServiceImpl extends AbstractCrudService<ThemeSetting, I
return result;
}
@Override
public List<ThemeSetting> replaceUrl(String oldUrl, String newUrl) {
List<ThemeSetting> themeSettings = listAll();
List<ThemeSetting> replaced = new ArrayList<>();
themeSettings.forEach(themeSetting -> {
if (StringUtils.isNotEmpty(themeSetting.getValue())) {
themeSetting.setValue(themeSetting.getValue().replaceAll(oldUrl, newUrl));
}
replaced.add(themeSetting);
});
return updateInBatch(replaced);
}
@Override
@Transactional
public void deleteInactivated() {
themeSettingRepository.deleteByThemeIdIsNot(themeService.getActivatedThemeId());
}
/**
* Gets config item map. (key: item name, value: item)
*

View File

@ -0,0 +1,32 @@
<div style="text-align:center">
<img src="${user.avatar!}" width="100" height="100" alt="${user.nickname!}">
<h3>${options.blog_title!}</h3>
<h4>
<a href="${context!}" target="_blank">${context!}</a>
</h4>
</div>
---
<@postTag method="archiveYear">
<#list archives as archive>
## ${archive.year?c}
<#list archive.posts?sort_by("createTime")?reverse as post>
- <a href="${context!}/archives/${post.url!}" title="${post.title!}" target="_blank">${post.title!}</a>
</#list>
</#list>
</@postTag>
## 分类目录
<@categoryTag method="list">
<#list categories as category>
- <a href="${context!}/categories/${category.slugName!}" target="_blank">${category.name!}</a>
</#list>
</@categoryTag>
## 标签
<@tagTag method="list">
<#list tags as tag>
- <a href="${context!}/tags/${tag.slugName!}" target="_blank">${tag.name!}</a>
</#list>
</@tagTag>