Skip to content

Commit

Permalink
feat: Support Rename and Edit Staitc Files (halo-dev#573) (halo-dev#819)
Browse files Browse the repository at this point in the history
* Add rename API

* Add save API

* Add unit test for rename API

* Fix an indentation

* Add a space before '{'

* Add a Param and some invalid checks

* Change comments

* Change test annotation

* Delete impl test unit

* Add directory traversal check for static file operations

Co-authored-by: Rokita <[email protected]>
  • Loading branch information
Arexh and Arexh authored May 11, 2020
1 parent d5a240e commit de81f7b
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import run.halo.app.model.params.StaticContentParam;
import run.halo.app.model.support.StaticFile;
import run.halo.app.service.StaticStorageService;

Expand Down Expand Up @@ -49,4 +50,17 @@ public void upload(String basePath,
@RequestPart("file") MultipartFile file) {
staticStorageService.upload(basePath, file);
}

@PostMapping("rename")
@ApiOperation("Renames static file")
public void rename(String basePath,
String newName) {
staticStorageService.rename(basePath, newName);
}

@PutMapping("files")
@ApiOperation("Save static file")
public void save(@RequestBody StaticContentParam param) {
staticStorageService.save(param.getPath(), param.getContent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import run.halo.app.handler.migrate.utils.PropertyMappingTo;
import run.halo.app.model.entity.BasePost;

import java.util.List;

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/run/halo/app/model/params/StaticContentParam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package run.halo.app.model.params;

import lombok.Data;

/**
* Static content param.
*
* @author Holldean
* @date 2020-05-04
*/
@Data
public class StaticContentParam {
private String path;
private String content;
}
16 changes: 16 additions & 0 deletions src/main/java/run/halo/app/service/StaticStorageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,20 @@ public interface StaticStorageService {
* @param file file must not be null.
*/
void upload(String basePath, @NonNull MultipartFile file);

/**
* Rename static file or folder.
*
* @param basePath base path must not be null
* @param newName new name must not be null
*/
void rename(@NonNull String basePath, @NonNull String newName);

/**
* Save static file.
*
* @param path path must not be null
* @param content saved content
*/
void save(@NonNull String path, String content);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@

import javax.activation.MimetypesFileTypeMap;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
Expand Down Expand Up @@ -97,6 +96,10 @@ public void delete(String relativePath) {
Assert.notNull(relativePath, "Relative path must not be null");

Path path = Paths.get(staticDir.toString(), relativePath);

// check if the path is valid (not outside staticDir)
FileUtils.checkDirectoryTraversal(staticDir.toString(), path.toString());

log.debug(path.toString());

try {
Expand Down Expand Up @@ -127,6 +130,9 @@ public void createFolder(String basePath, String folderName) {
path = Paths.get(staticDir.toString(), basePath, folderName);
}

// check if the path is valid (not outside staticDir)
FileUtils.checkDirectoryTraversal(staticDir.toString(), path.toString());

if (path.toFile().exists()) {
throw new FileOperationException("目录 " + path.toString() + " 已存在").setErrorData(path);
}
Expand Down Expand Up @@ -154,6 +160,9 @@ public void upload(String basePath, MultipartFile file) {
uploadPath = Paths.get(staticDir.toString(), basePath, file.getOriginalFilename());
}

// check if the path is valid (not outside staticDir)
FileUtils.checkDirectoryTraversal(staticDir.toString(), uploadPath.toString());

if (uploadPath.toFile().exists()) {
throw new FileOperationException("文件 " + file.getOriginalFilename() + " 已存在").setErrorData(uploadPath);
}
Expand All @@ -167,6 +176,53 @@ public void upload(String basePath, MultipartFile file) {
}
}

@Override
public void rename(String basePath, String newName) {
Assert.notNull(basePath, "Base path must not be null");
Assert.notNull(newName, "New name must not be null");

Path pathToRename;

if (StringUtils.startsWith(newName, API_FOLDER_NAME)) {
throw new FileOperationException("重命名名称 " + newName + " 不合法");
}

pathToRename = Paths.get(staticDir.toString(), basePath);

// check if the path is valid (not outside staticDir)
FileUtils.checkDirectoryTraversal(staticDir.toString(), pathToRename.toString());

try {
FileUtils.rename(pathToRename, newName);
onChange();
} catch (FileAlreadyExistsException e) {
throw new FileOperationException("该路径下名称 " + newName + " 已存在");
} catch (IOException e) {
throw new FileOperationException("重命名 " + pathToRename.toString() + " 失败");
}
}

@Override
public void save(String path, String content) {
Assert.notNull(path, "Path must not be null");

Path savePath = Paths.get(staticDir.toString(), path);

// check if the path is valid (not outside staticDir)
FileUtils.checkDirectoryTraversal(staticDir.toString(), savePath.toString());

// check if file exist
if (!Files.isRegularFile(savePath)) {
throw new FileOperationException("路径 " + path + " 不合法");
}

try {
Files.write(savePath, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new ServiceException("保存内容失败 " + path, e);
}
}

private void onChange() {
eventPublisher.publishEvent(new StaticStorageChangedEvent(this, staticDir));
}
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/run/halo/app/utils/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ public static void deleteFolder(@NonNull Path deletingPath) throws IOException {
log.info("Deleted [{}] successfully", deletingPath);
}

/**
* Renames file or folder.
*
* @param pathToRename file path to rename must not be null
* @param newName new name must not be null
*/
public static void rename(@NonNull Path pathToRename, @NonNull String newName) throws IOException {
Assert.notNull(pathToRename, "File path to rename must not be null");
Assert.notNull(newName, "New name must not be null");

Path newPath = pathToRename.resolveSibling(newName);
log.info("Rename [{}] to [{}]", pathToRename, newPath);

Files.move(pathToRename, newPath);

log.info("Rename [{}] successfully", pathToRename);
}

/**
* Unzips content to the target path.
*
Expand Down
87 changes: 87 additions & 0 deletions src/test/java/run/halo/app/utils/FileUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -108,4 +109,90 @@ public void dbFileReadTest() throws IOException {
log.debug("Buffer String: [{}]", bufString);
}
}

@Test
public void testRenameFile() throws IOException {
// Create a temp folder
Path tempDirectory = Files.createTempDirectory("halo-test");

Path testPath = tempDirectory.resolve("test/test");
Path filePath = tempDirectory.resolve("test/test/test.file");

// Create a temp file and folder
Files.createDirectories(testPath);
Files.createFile(filePath);

// Write content to the temp file
String content = "Test Content!\n";
Files.write(filePath, content.getBytes());

// Rename temp file
FileUtils.rename(filePath, "newName");
Path newPath = filePath.resolveSibling("newName");

Assert.assertFalse(Files.exists(filePath));
Assert.assertTrue(Files.isRegularFile(newPath));
Assert.assertEquals(new String(Files.readAllBytes(newPath)), content);

FileUtils.deleteFolder(tempDirectory);
}

@Test
public void testRenameFolder() throws IOException {
// Create a temp folder
Path tempDirectory = Files.createTempDirectory("halo-test");

Path testPath = tempDirectory.resolve("test/test");
Path filePath = tempDirectory.resolve("test/test.file");

// Create a temp file and folder
Files.createDirectories(testPath);
Files.createFile(filePath);

// Rename temp folder
FileUtils.rename(tempDirectory.resolve("test"), "newName");
Path newPath = tempDirectory.resolve("newName");

Assert.assertTrue(Files.isDirectory(newPath));
Assert.assertTrue(Files.isRegularFile(newPath.resolve("test.file")));

FileUtils.deleteFolder(tempDirectory);
}

@Test
public void testRenameRepeat() throws IOException {
// Create a temp folder
Path tempDirectory = Files.createTempDirectory("halo-test");

Path testPathOne = tempDirectory.resolve("test/testOne");
Path testPathTwo = tempDirectory.resolve("test/testTwo");
Path filePathOne = tempDirectory.resolve("test/testOne.file");
Path filePathTwo = tempDirectory.resolve("test/testTwo.file");

// Create temp files and folders
Files.createDirectories(testPathOne);
Files.createDirectories(testPathTwo);
Files.createFile(filePathOne);
Files.createFile(filePathTwo);

try {
FileUtils.rename(testPathOne, "testTwo");
} catch (Exception e) {
Assert.assertTrue(e instanceof FileAlreadyExistsException);
}

try {
FileUtils.rename(filePathOne, "testTwo.file");
} catch (Exception e) {
Assert.assertTrue(e instanceof FileAlreadyExistsException);
}

try {
FileUtils.rename(filePathOne, "testOne");
} catch (Exception e) {
Assert.assertTrue(e instanceof FileAlreadyExistsException);
}

FileUtils.deleteFolder(tempDirectory);
}
}

0 comments on commit de81f7b

Please sign in to comment.