diff --git a/src/main/java/cc/ryanc/halo/config/properties/HaloProperties.java b/src/main/java/cc/ryanc/halo/config/properties/HaloProperties.java index d357b21ae..cc0c26d41 100644 --- a/src/main/java/cc/ryanc/halo/config/properties/HaloProperties.java +++ b/src/main/java/cc/ryanc/halo/config/properties/HaloProperties.java @@ -12,6 +12,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("halo") public class HaloProperties { + private final static String USER_HOME = System.getProperty("user.home"); + /** * Doc api disabled. (Default is true) */ @@ -25,5 +27,5 @@ public class HaloProperties { /** * Work directory. */ - private String workDir = "${user.home}/halo/"; + private String workDir = USER_HOME + "/halo/"; } diff --git a/src/main/java/cc/ryanc/halo/model/entity/Attachment.java b/src/main/java/cc/ryanc/halo/model/entity/Attachment.java index c65df426a..4e0b61942 100644 --- a/src/main/java/cc/ryanc/halo/model/entity/Attachment.java +++ b/src/main/java/cc/ryanc/halo/model/entity/Attachment.java @@ -1,5 +1,6 @@ package cc.ryanc.halo.model.entity; +import cc.ryanc.halo.model.enums.AttachmentType; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -37,7 +38,7 @@ public class Attachment extends BaseEntity { /** * 附件路径 */ - @Column(name = "path", columnDefinition = "varchar(1023) default ''") + @Column(name = "path", columnDefinition = "varchar(1023) not null") private String path; /** @@ -49,7 +50,7 @@ public class Attachment extends BaseEntity { /** * 附件类型 */ - @Column(name = "media_type", columnDefinition = "varchar(50) default ''") + @Column(name = "media_type", columnDefinition = "varchar(50) not null") private String mediaType; /** @@ -59,27 +60,54 @@ public class Attachment extends BaseEntity { private String suffix; /** - * 附件尺寸 + * Attachment width. */ - @Column(name = "dimension", columnDefinition = "varchar(50) default ''") - private String dimension; + @Column(name = "width", columnDefinition = "int default 0") + private Integer width; + + /** + * Attachment height. + */ + @Column(name = "height", columnDefinition = "int default 0") + private Integer height; /** * 附件大小 */ - @Column(name = "size", columnDefinition = "varchar(50) default ''") - private String size; + @Column(name = "size", columnDefinition = "bigint not null") + private Long size; /** * 附件上传类型 */ @Column(name = "type", columnDefinition = "int default 0") - private Integer type; + private AttachmentType type; @Override public void prePersist() { super.prePersist(); id = null; + + if (thumbPath == null) { + thumbPath = ""; + } + + if (suffix == null) { + suffix = ""; + } + + if (width == null) { + width = 0; + } + + if (height == null) { + height = 0; + } + + if (type == null) { + type = AttachmentType.SERVER; + } + } } diff --git a/src/main/java/cc/ryanc/halo/model/enums/AttachOrigin.java b/src/main/java/cc/ryanc/halo/model/enums/AttachmentType.java similarity index 83% rename from src/main/java/cc/ryanc/halo/model/enums/AttachOrigin.java rename to src/main/java/cc/ryanc/halo/model/enums/AttachmentType.java index eba0a50b2..30c24a345 100644 --- a/src/main/java/cc/ryanc/halo/model/enums/AttachOrigin.java +++ b/src/main/java/cc/ryanc/halo/model/enums/AttachmentType.java @@ -6,7 +6,7 @@ package cc.ryanc.halo.model.enums; * @author : RYAN0UP * @date : 2019-03-12 */ -public enum AttachOrigin implements ValueEnum { +public enum AttachmentType implements ValueEnum { /** * 服务器 @@ -25,7 +25,7 @@ public enum AttachOrigin implements ValueEnum { private Integer value; - AttachOrigin(Integer value) { + AttachmentType(Integer value) { this.value = value; } diff --git a/src/main/java/cc/ryanc/halo/model/support/UploadResult.java b/src/main/java/cc/ryanc/halo/model/support/UploadResult.java new file mode 100644 index 000000000..87c528b4d --- /dev/null +++ b/src/main/java/cc/ryanc/halo/model/support/UploadResult.java @@ -0,0 +1,31 @@ +package cc.ryanc.halo.model.support; + +import lombok.Data; +import org.springframework.http.MediaType; + +/** + * Upload result dto. + * + * @author johnniang + * @date 3/26/19 + */ +@Data +public class UploadResult { + + private String filename; + + private String filePath; + + private String thumbPath; + + private String suffix; + + private MediaType mediaType; + + private Long size; + + private Integer width; + + private Integer height; + +} diff --git a/src/main/java/cc/ryanc/halo/service/FileService.java b/src/main/java/cc/ryanc/halo/service/FileService.java new file mode 100644 index 000000000..18f786012 --- /dev/null +++ b/src/main/java/cc/ryanc/halo/service/FileService.java @@ -0,0 +1,38 @@ +package cc.ryanc.halo.service; + +import cc.ryanc.halo.model.support.UploadResult; +import org.springframework.lang.NonNull; +import org.springframework.web.multipart.MultipartFile; + +/** + * File service interface. + * + * @author johnniang + * @date 3/26/19 + */ +public interface FileService { + + /** + * Upload sub directory. + */ + String UPLOAD_SUB_DIR = "upload"; + + /** + * Thumbnail width. + */ + int THUMB_WIDTH = 256; + + /** + * Thumbnail height. + */ + int THUMB_HEIGHT = 256; + + /** + * Uploads file to local storage. + * + * @param file multipart file must not be null + * @return upload result + */ + @NonNull + UploadResult uploadToLocal(@NonNull MultipartFile file); +} diff --git a/src/main/java/cc/ryanc/halo/service/OptionService.java b/src/main/java/cc/ryanc/halo/service/OptionService.java index e0807990b..793e8dcde 100755 --- a/src/main/java/cc/ryanc/halo/service/OptionService.java +++ b/src/main/java/cc/ryanc/halo/service/OptionService.java @@ -11,6 +11,7 @@ import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -200,4 +201,12 @@ public interface OptionService extends CrudService { @NonNull Zone getQiniuZone(); + /** + * Gets locale. + * + * @return locale user set or default locale + */ + @NonNull + Locale getLocale(); + } diff --git a/src/main/java/cc/ryanc/halo/service/impl/FileServiceImpl.java b/src/main/java/cc/ryanc/halo/service/impl/FileServiceImpl.java new file mode 100644 index 000000000..050ee0d2d --- /dev/null +++ b/src/main/java/cc/ryanc/halo/service/impl/FileServiceImpl.java @@ -0,0 +1,205 @@ +package cc.ryanc.halo.service.impl; + +import cc.ryanc.halo.config.properties.HaloProperties; +import cc.ryanc.halo.exception.ServiceException; +import cc.ryanc.halo.model.support.UploadResult; +import cc.ryanc.halo.service.FileService; +import cc.ryanc.halo.service.OptionService; +import cc.ryanc.halo.utils.FilenameUtils; +import cc.ryanc.halo.utils.HaloUtils; +import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Calendar; + +/** + * File service implementation. + * + * @author johnniang + * @date 3/26/19 + */ +@Slf4j +@Service +public class FileServiceImpl implements FileService { + + private final OptionService optionService; + + private final String workDir; + + private final MediaType imageType = MediaType.valueOf("image/*"); + + public FileServiceImpl(HaloProperties haloProperties, + OptionService optionService) throws URISyntaxException { + this.optionService = optionService; + + // Get work dir + workDir = normalizeDirectory(haloProperties.getWorkDir()); + + // Check directory + checkWorkDir(); + + log.info("Work directory: [{}]", workDir); + } + + /** + * Check work directory. + * + * @throws URISyntaxException throws when work directory is not a uri + */ + private void checkWorkDir() throws URISyntaxException { + // Get work path + Path workPath = Paths.get(workDir); + + // Check file type + Assert.isTrue(Files.isDirectory(workPath), workDir + " isn't a directory"); + + // Check readable + Assert.isTrue(Files.isReadable(workPath), workDir + " isn't readable"); + + // Check writable + Assert.isTrue(Files.isWritable(workPath), workDir + " isn't writable"); + } + + /** + * Normalize directory full name, ensure the end path separator. + * + * @param dir directory full name must not be blank + * @return normalized directory full name with end path separator + */ + @NonNull + private String normalizeDirectory(@NonNull String dir) { + Assert.hasText(dir, "Directory full name must not be blank"); + + return StringUtils.appendIfMissing(dir, File.separator); + } + + @Override + public UploadResult uploadToLocal(MultipartFile file) { + Assert.notNull(file, "Multipart file must not be null"); + + // Get current time + Calendar current = Calendar.getInstance(optionService.getLocale()); + // Get month and day of month + int year = current.get(Calendar.YEAR); + int month = current.get(Calendar.MONTH) + 1; + + // Build directory + String subDir = UPLOAD_SUB_DIR + File.separator + year + File.separator + month + File.separator; + + // Get basename + String basename = FilenameUtils.getBasename(file.getOriginalFilename()) + '-' + HaloUtils.randomUUIDWithoutDash(); + + // Get extension + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + + log.debug("Base name: [{}], extension: [{}] of original filename: [{}]", basename, extension, file.getOriginalFilename()); + + // Build sub file path + String subFilePath = subDir + basename + '.' + extension; + + // Get upload path + Path uploadPath = Paths.get(workDir + subFilePath); + + log.info("Uploading to directory: [{}]", uploadPath.getFileName()); + + try { + // TODO Synchronize here + // Create directory + Files.createDirectories(uploadPath.getParent()); + Files.createFile(uploadPath); + + // Upload this file + file.transferTo(uploadPath); + + // Build upload result + UploadResult uploadResult = new UploadResult(); + uploadResult.setFilename(basename); + uploadResult.setFilePath(subFilePath); + uploadResult.setSuffix(extension); + uploadResult.setMediaType(MediaType.valueOf(file.getContentType())); + uploadResult.setSize(file.getSize()); + + // Check file type + if (isImageType(file.getContentType())) { + // Upload a thumbnail + String thumbnailBasename = basename + '-' + "thumbnail"; + String thumbnailSubFilePath = subDir + thumbnailBasename + '.' + extension; + Path thumbnailPath = Paths.get(workDir + thumbnailSubFilePath); + + // Create the thumbnail + Files.createFile(thumbnailPath); + + // Generate thumbnail + generateThumbnail(uploadPath, thumbnailPath); + + // Set thumb path + uploadResult.setThumbPath(thumbnailSubFilePath); + + // Read as image + BufferedImage image = ImageIO.read(Files.newInputStream(uploadPath)); + + // Set width and height + uploadResult.setWidth(image.getWidth()); + uploadResult.setHeight(image.getHeight()); + } + + return uploadResult; + } catch (IOException e) { + log.error("Failed to upload file to local: " + uploadPath.getFileName(), e); + throw new ServiceException("Failed to upload file to local").setErrorData(uploadPath.getFileName()); + } + } + + /** + * Generates thumbnail image. + * + * @param imagePath image path must not be null + * @param thumbPath thumbnail path must not be null + * @throws IOException throws if image provided is not valid + */ + private void generateThumbnail(@NonNull Path imagePath, @NonNull Path thumbPath) throws IOException { + Assert.notNull(imagePath, "Image path must not be null"); + Assert.notNull(thumbPath, "Thumb path must not be null"); + + log.info("Generating thumbnail: [{}] for image: [{}]", thumbPath.getFileName(), imagePath.getFileName()); + + // Convert to thumbnail and copy the thumbnail + Thumbnails.of(imagePath.toFile()).size(THUMB_WIDTH, THUMB_HEIGHT).keepAspectRatio(true).toFile(thumbPath.toFile()); + } + + /** + * Check whether media type provided is an image type. + * + * @param mediaType media type provided + * @return true if it is an image type + */ + private boolean isImageType(@Nullable String mediaType) { + return mediaType != null && imageType.includes(MediaType.valueOf(mediaType)); + } + + + /** + * Check whether media type provided is an image type. + * + * @param mediaType media type provided + * @return true if it is an image type + */ + private boolean isImageType(@Nullable MediaType mediaType) { + return mediaType != null && imageType.includes(mediaType); + } +} diff --git a/src/main/java/cc/ryanc/halo/service/impl/OptionServiceImpl.java b/src/main/java/cc/ryanc/halo/service/impl/OptionServiceImpl.java index bc95aee10..ec61983ec 100644 --- a/src/main/java/cc/ryanc/halo/service/impl/OptionServiceImpl.java +++ b/src/main/java/cc/ryanc/halo/service/impl/OptionServiceImpl.java @@ -17,6 +17,7 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -249,5 +250,16 @@ public class OptionServiceImpl extends AbstractCrudService impl }).orElseGet(Zone::autoZone); } + @Override + public Locale getLocale() { + return getByProperty(BlogProperties.BLOG_LOCALE).map(localeStr -> { + try { + return Locale.forLanguageTag(localeStr); + } catch (Exception e) { + return Locale.getDefault(); + } + }).orElseGet(Locale::getDefault); + } + } diff --git a/src/main/java/cc/ryanc/halo/utils/FilenameUtils.java b/src/main/java/cc/ryanc/halo/utils/FilenameUtils.java new file mode 100644 index 000000000..455c54c6f --- /dev/null +++ b/src/main/java/cc/ryanc/halo/utils/FilenameUtils.java @@ -0,0 +1,69 @@ +package cc.ryanc.halo.utils; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.io.File; + +/** + * Filename utilities. + * + * @author johnniang + * @date 3/26/19 + */ +public class FilenameUtils { + + private FilenameUtils() { + } + + @NonNull + public static String getBasename(@NonNull String filename) { + Assert.hasText(filename, "Filename must not be blank"); + + // Find the last slash + int separatorLastIndex = StringUtils.lastIndexOf(filename, File.separatorChar); + + if (separatorLastIndex == filename.length() - 1) { + return ""; + } + + if (separatorLastIndex >= 0 && separatorLastIndex < filename.length() - 1) { + filename = filename.substring(separatorLastIndex + 1); + } + + // Find last dot + int dotLastIndex = StringUtils.lastIndexOf(filename, '.'); + + if (dotLastIndex < 0) { + return filename; + } + + return filename.substring(0, dotLastIndex); + } + + @NonNull + public static String getExtension(@NonNull String filename) { + Assert.hasText(filename, "Filename must not be blank"); + + // Find the last slash + int separatorLastIndex = StringUtils.lastIndexOf(filename, File.separatorChar); + + if (separatorLastIndex == filename.length() - 1) { + return ""; + } + + if (separatorLastIndex >= 0 && separatorLastIndex < filename.length() - 1) { + filename = filename.substring(separatorLastIndex + 1); + } + + // Find last dot + int dotLastIndex = StringUtils.lastIndexOf(filename, '.'); + + if (dotLastIndex < 0) { + return ""; + } + + return filename.substring(dotLastIndex + 1); + } +} diff --git a/src/main/java/cc/ryanc/halo/utils/HaloUtils.java b/src/main/java/cc/ryanc/halo/utils/HaloUtils.java index d30bfed9c..9f59d27ac 100755 --- a/src/main/java/cc/ryanc/halo/utils/HaloUtils.java +++ b/src/main/java/cc/ryanc/halo/utils/HaloUtils.java @@ -24,6 +24,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Calendar; import java.util.Date; import java.util.Properties; +import java.util.UUID; /** *
@@ -36,6 +37,16 @@ import java.util.Properties;
 @Slf4j
 public class HaloUtils {
 
+    /**
+     * Gets random uuid without dash.
+     *
+     * @return random uuid without dash
+     */
+    @NonNull
+    public static String randomUUIDWithoutDash() {
+        return StringUtils.remove(UUID.randomUUID().toString(), '-');
+    }
+
     /**
      * Initialize url if blank.
      *
diff --git a/src/main/java/cc/ryanc/halo/web/controller/admin/api/AttachmentController.java b/src/main/java/cc/ryanc/halo/web/controller/admin/api/AttachmentController.java
index dd15d2aab..34437d57d 100644
--- a/src/main/java/cc/ryanc/halo/web/controller/admin/api/AttachmentController.java
+++ b/src/main/java/cc/ryanc/halo/web/controller/admin/api/AttachmentController.java
@@ -1,12 +1,15 @@
 package cc.ryanc.halo.web.controller.admin.api;
 
 import cc.ryanc.halo.model.dto.AttachmentOutputDTO;
+import cc.ryanc.halo.model.support.UploadResult;
 import cc.ryanc.halo.service.AttachmentService;
+import cc.ryanc.halo.service.FileService;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.web.PageableDefault;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import static org.springframework.data.domain.Sort.Direction.DESC;
 
@@ -22,8 +25,12 @@ public class AttachmentController {
 
     private final AttachmentService attachmentService;
 
-    public AttachmentController(AttachmentService attachmentService) {
+    private final FileService fileService;
+
+    public AttachmentController(AttachmentService attachmentService,
+                                FileService fileService) {
         this.attachmentService = attachmentService;
+        this.fileService = fileService;
     }
 
     /**
@@ -59,4 +66,10 @@ public class AttachmentController {
     public void deletePermanently(@PathVariable("id") Integer id) {
         attachmentService.removeById(id);
     }
+
+    @PostMapping("upload")
+    public UploadResult uploadAttachment(@RequestParam("file") MultipartFile file) {
+        // TODO Just for test
+        return fileService.uploadToLocal(file);
+    }
 }
diff --git a/src/main/java/cc/ryanc/halo/web/controller/core/InstallController.java b/src/main/java/cc/ryanc/halo/web/controller/core/InstallController.java
index c1d19313c..5e3d6ee62 100644
--- a/src/main/java/cc/ryanc/halo/web/controller/core/InstallController.java
+++ b/src/main/java/cc/ryanc/halo/web/controller/core/InstallController.java
@@ -2,7 +2,7 @@ package cc.ryanc.halo.web.controller.core;
 
 import cc.ryanc.halo.exception.BadRequestException;
 import cc.ryanc.halo.model.entity.*;
-import cc.ryanc.halo.model.enums.AttachOrigin;
+import cc.ryanc.halo.model.enums.AttachmentType;
 import cc.ryanc.halo.model.enums.BlogProperties;
 import cc.ryanc.halo.model.params.InstallParam;
 import cc.ryanc.halo.model.support.BaseResponse;
@@ -173,7 +173,7 @@ public class InstallController {
         properties.put(BlogProperties.NEW_COMMENT_NOTICE, Boolean.FALSE.toString());
         properties.put(BlogProperties.COMMENT_PASS_NOTICE, Boolean.FALSE.toString());
         properties.put(BlogProperties.COMMENT_REPLY_NOTICE, Boolean.FALSE.toString());
-        properties.put(BlogProperties.ATTACH_LOC, AttachOrigin.SERVER.getValue().toString());
+        properties.put(BlogProperties.ATTACH_LOC, AttachmentType.SERVER.getValue().toString());
 
         // Create properties
         optionService.saveProperties(properties, "system");
diff --git a/src/test/java/cc/ryanc/halo/model/MediaTypeTest.java b/src/test/java/cc/ryanc/halo/model/MediaTypeTest.java
new file mode 100644
index 000000000..323d1224d
--- /dev/null
+++ b/src/test/java/cc/ryanc/halo/model/MediaTypeTest.java
@@ -0,0 +1,45 @@
+package cc.ryanc.halo.model;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.*;
+
+/**
+ * @author johnniang
+ * @date 3/26/19
+ */
+public class MediaTypeTest {
+
+    @Test
+    public void toStringTest() {
+        MediaType mediaType = MediaType.IMAGE_GIF;
+
+        assertThat(mediaType.toString(), equalTo("image/gif"));
+    }
+
+    @Test
+    public void parseTest() {
+        MediaType mediaType = MediaType.valueOf("image/gif");
+
+        assertNotNull(mediaType);
+        assertThat(mediaType, equalTo(MediaType.IMAGE_GIF));
+    }
+
+    @Test
+    public void includesTest() {
+        MediaType mediaType = MediaType.valueOf("image/*");
+        boolean isInclude = mediaType.includes(MediaType.IMAGE_GIF);
+        assertTrue(isInclude);
+
+        isInclude = mediaType.includes(MediaType.IMAGE_JPEG);
+        assertTrue(isInclude);
+
+        isInclude = mediaType.includes(MediaType.IMAGE_PNG);
+        assertTrue(isInclude);
+
+        isInclude = mediaType.includes(MediaType.TEXT_HTML);
+        assertFalse(isInclude);
+    }
+}
diff --git a/src/test/java/cc/ryanc/halo/model/enums/AttachOriginTest.java b/src/test/java/cc/ryanc/halo/model/enums/AttachmentTypeTest.java
similarity index 57%
rename from src/test/java/cc/ryanc/halo/model/enums/AttachOriginTest.java
rename to src/test/java/cc/ryanc/halo/model/enums/AttachmentTypeTest.java
index aa872a81c..465a8259c 100644
--- a/src/test/java/cc/ryanc/halo/model/enums/AttachOriginTest.java
+++ b/src/test/java/cc/ryanc/halo/model/enums/AttachmentTypeTest.java
@@ -17,15 +17,16 @@ import static org.junit.Assert.assertThat;
  */
 @RunWith(SpringRunner.class)
 @SpringBootTest
-public class AttachOriginTest {
+public class AttachmentTypeTest {
 
     @Autowired
     private ConversionService conversionService;
 
     @Test
     public void conversionTest() {
-        assertThat(conversionService.convert("SERVER", AttachOrigin.class), equalTo(AttachOrigin.SERVER));
-        assertThat(conversionService.convert("server", AttachOrigin.class), equalTo(AttachOrigin.SERVER));
-        assertThat(conversionService.convert("Server", AttachOrigin.class), equalTo(AttachOrigin.SERVER));
+        assertThat(conversionService.convert("SERVER", AttachmentType.class), equalTo(AttachmentType.SERVER));
+        assertThat(conversionService.convert("server", AttachmentType.class), equalTo(AttachmentType.SERVER));
+        assertThat(conversionService.convert("Server", AttachmentType.class), equalTo(AttachmentType.SERVER));
+        assertThat(conversionService.convert("SerVer", AttachmentType.class), equalTo(AttachmentType.SERVER));
     }
 }
\ No newline at end of file
diff --git a/src/test/java/cc/ryanc/halo/utils/FilenameUtilsTest.java b/src/test/java/cc/ryanc/halo/utils/FilenameUtilsTest.java
new file mode 100644
index 000000000..e64d026e5
--- /dev/null
+++ b/src/test/java/cc/ryanc/halo/utils/FilenameUtilsTest.java
@@ -0,0 +1,40 @@
+package cc.ryanc.halo.utils;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Filename utilities test.
+ *
+ * @author johnniang
+ * @date 3/26/19
+ */
+public class FilenameUtilsTest {
+
+    // a/b/c.txt --> c.txt
+    // a.txt     --> a.txt
+    // a/b/c     --> c
+    // a/b/c/    --> ""
+    @Test
+    public void getBasename() {
+        assertThat(FilenameUtils.getBasename("a/b/c.txt"), equalTo("c"));
+        assertThat(FilenameUtils.getBasename("a.txt"), equalTo("a"));
+        assertThat(FilenameUtils.getBasename("a/b/c"), equalTo("c"));
+        assertThat(FilenameUtils.getBasename("a/b/c/"), equalTo(""));
+    }
+
+    // foo.txt      --> "txt"
+    // a/b/c.jpg    --> "jpg"
+    // a/b.txt/c    --> ""
+    // a/b/c        --> ""
+    @Test
+    public void getExtension() {
+        assertThat(FilenameUtils.getExtension("foo.txt"), equalTo("txt"));
+        assertThat(FilenameUtils.getExtension("a/b/c.jpg"), equalTo("jpg"));
+        assertThat(FilenameUtils.getExtension("a/b.txt/c"), equalTo(""));
+        assertThat(FilenameUtils.getExtension("a/b/c"), equalTo(""));
+        assertThat(FilenameUtils.getExtension("a/b/c/"), equalTo(""));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/cc/ryanc/halo/utils/URITest.java b/src/test/java/cc/ryanc/halo/utils/URITest.java
new file mode 100644
index 000000000..90bc66890
--- /dev/null
+++ b/src/test/java/cc/ryanc/halo/utils/URITest.java
@@ -0,0 +1,22 @@
+package cc.ryanc.halo.utils;
+
+import org.junit.Test;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * URI test.
+ *
+ * @author johnniang
+ * @date 3/26/19
+ */
+public class URITest {
+
+    @Test
+    public void createURITest() throws URISyntaxException {
+        String homeDir = System.getProperty("user.home");
+        URI uri = new URI(homeDir);
+        System.out.println(uri);
+    }
+}