Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/main/java/cloudpage/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
@EnableScheduling
public class Application implements WebMvcConfigurer {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import cloudpage.exceptions.FileNotFoundException;
import cloudpage.service.FileService;
import cloudpage.service.FolderService;
import cloudpage.service.TrashService;
import cloudpage.service.UserService;
import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -25,6 +26,7 @@ public class FileController {
private final FileService fileService;
private final UserService userService;
private final FolderService folderService;
private final TrashService trashService;

@PostMapping("/upload")
public void uploadFile(@RequestParam String folderPath, @RequestParam MultipartFile file)
Expand All @@ -50,7 +52,7 @@ public ResponseEntity<String> getFileContent(@RequestParam String path) throws I
@DeleteMapping
public void deleteFile(@RequestParam String filePath) throws IOException {
var user = userService.getCurrentUser();
fileService.deleteFile(user.getRootFolderPath(), filePath);
trashService.moveToTrash(user.getRootFolderPath(), user.getId(), filePath);
}

@PatchMapping("/move")
Expand Down
41 changes: 41 additions & 0 deletions backend/src/main/java/cloudpage/controller/TrashController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cloudpage.controller;

import cloudpage.dto.TrashEntryDto;
import cloudpage.service.TrashService;
import cloudpage.service.UserService;
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/** Endpoints for a user's trash: listing entries, restoring them, or permanently deleting them. */
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/files/trash")
public class TrashController {

private final TrashService trashService;
private final UserService userService;

@GetMapping
public List<TrashEntryDto> listTrash() {
return trashService.listTrash(userService.getCurrentUser().getId());
}

@PostMapping("/{id}/restore")
public void restore(@PathVariable String id) throws IOException {
var user = userService.getCurrentUser();
trashService.restore(user.getRootFolderPath(), user.getId(), id);
}

@DeleteMapping("/{id}")
public void purge(@PathVariable String id) throws IOException {
var user = userService.getCurrentUser();
trashService.purge(user.getRootFolderPath(), user.getId(), id);
}
Comment thread
DenizAltunkapan marked this conversation as resolved.
}
18 changes: 18 additions & 0 deletions backend/src/main/java/cloudpage/dto/TrashEntryDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cloudpage.dto;

import java.time.Instant;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
public class TrashEntryDto {

private String id;
private String name;
private String originalPath;
private Instant deletedAt;
private long size;
}
34 changes: 34 additions & 0 deletions backend/src/main/java/cloudpage/model/TrashEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cloudpage.model;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import lombok.Getter;
import lombok.Setter;

/**
* A file that has been soft-deleted into a user's trash. The {@link #id} also serves as the file
* name under the user's {@code .trash} directory. An entry is removed once its file is restored,
* permanently deleted, or purged after the retention period.
*/
@Entity
@Table(name = "trash_entries")
@Getter
@Setter
public class TrashEntry {

@Id private String id;

private String userId;

/** Path, relative to the user's root, the file was deleted from and should be restored to. */
private String originalPath;

/** Original file name, shown in the trash listing. */
private String displayName;

private Instant deletedAt;

private long sizeBytes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cloudpage.repository;

import cloudpage.model.TrashEntry;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TrashEntryRepository extends JpaRepository<TrashEntry, String> {

List<TrashEntry> findByUserId(String userId);

Optional<TrashEntry> findByIdAndUserId(String id, String userId);

List<TrashEntry> findByDeletedAtBefore(Instant cutoff);
}
48 changes: 28 additions & 20 deletions backend/src/main/java/cloudpage/service/FolderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ public List<SearchResult> searchInFolder(
// Locale.ROOT prevents using the system's local language for case conversion
String lowerQuery = query.toLowerCase(Locale.ROOT);

Path trashDir = Paths.get(rootPath, TrashService.TRASH_DIR).normalize();
try (var stream = Files.walk(folder)) {
return stream
.filter(p -> !p.equals(folder))
.filter(p -> !p.startsWith(trashDir))
.map(p -> createSearchResult(p, lowerQuery))
.filter(r -> r.getScore() >= minScore)
.sorted((a, b) -> Integer.compare(b.getScore(), a.getScore()))
Expand Down Expand Up @@ -183,8 +185,10 @@ public PageResponseDto<FolderContentItemDto> getFolderContentPage(
List<FolderContentItemDto> items = new ArrayList<>();

try (var stream = Files.list(folder)) {
Path trashDir = Paths.get(rootPath, TrashService.TRASH_DIR).normalize();
items =
stream
.filter(path -> !path.normalize().equals(trashDir))
.map(
path -> {
try {
Expand Down Expand Up @@ -315,27 +319,31 @@ private FolderDto readFolderShallow(String rootPath, Path path, boolean includeC

List<FolderListItemDto> subfolders = new ArrayList<>();
List<FileDto> files = new ArrayList<>();
Path trashDir = Paths.get(rootPath, TrashService.TRASH_DIR).normalize();
try (var stream = Files.list(path)) {
stream.forEach(
childPath -> {
try {
// Resolve and validate once per child to avoid repeated toRealPath() calls.
Path childPathReal = resolvePathWithinRoot(rootReal, childPath);

if (Files.isDirectory(childPath)) {
subfolders.add(
toFolderListItemDto(rootReal, childPath, childPathReal, includeChildCounts));
} else if (Files.isRegularFile(childPath)) {
files.add(toFileDto(rootReal, childPath, childPathReal));
}
} catch (IOException e) {
throw new InvalidPathException(
"Invalid path detected while reading folder: "
+ childPath
+ " - "
+ e.getMessage());
}
});
stream
.filter(childPath -> !childPath.normalize().equals(trashDir))
.forEach(
Comment thread
DenizAltunkapan marked this conversation as resolved.
childPath -> {
try {
// Resolve and validate once per child to avoid repeated toRealPath() calls.
Path childPathReal = resolvePathWithinRoot(rootReal, childPath);

if (Files.isDirectory(childPath)) {
subfolders.add(
toFolderListItemDto(
rootReal, childPath, childPathReal, includeChildCounts));
} else if (Files.isRegularFile(childPath)) {
files.add(toFileDto(rootReal, childPath, childPathReal));
}
} catch (IOException e) {
throw new InvalidPathException(
"Invalid path detected while reading folder: "
+ childPath
+ " - "
+ e.getMessage());
}
});
}

// Calculate relative path for the current folder
Expand Down
Loading