Now logs file lifetime renewals and displays them in the file history

This commit is contained in:
Rostislav Raykov
2025-01-11 18:05:19 +02:00
parent adda1a77b3
commit 4504d90a0c
10 changed files with 276 additions and 79 deletions

View File

@@ -100,8 +100,8 @@ public class AdminViewController {
}
@PostMapping("/keep-indefinitely/{uuid}")
public String updateKeepIndefinitely(@PathVariable String uuid, @RequestParam(required = false, defaultValue = "false") boolean keepIndefinitely) {
fileService.updateKeepIndefinitely(uuid, keepIndefinitely);
public String updateKeepIndefinitely(@PathVariable String uuid, @RequestParam(required = false, defaultValue = "false") boolean keepIndefinitely, HttpServletRequest request) {
fileService.updateKeepIndefinitely(uuid, keepIndefinitely, request);
return "redirect:/admin/dashboard";
}

View File

@@ -4,8 +4,9 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.rostislav.quickdrop.entity.DownloadLog;
import org.rostislav.quickdrop.entity.FileEntity;
import org.rostislav.quickdrop.entity.FileRenewalLog;
import org.rostislav.quickdrop.model.FileActionLogDTO;
import org.rostislav.quickdrop.model.FileEntityView;
import org.rostislav.quickdrop.repository.DownloadLogRepository;
import org.rostislav.quickdrop.service.AnalyticsService;
import org.rostislav.quickdrop.service.ApplicationSettingsService;
import org.rostislav.quickdrop.service.FileService;
@@ -16,6 +17,8 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
@@ -26,14 +29,12 @@ import static org.rostislav.quickdrop.util.FileUtils.populateModelAttributes;
public class FileViewController {
private final FileService fileService;
private final ApplicationSettingsService applicationSettingsService;
private final DownloadLogRepository downloadLogRepository;
private final AnalyticsService analyticsService;
private final SessionService sessionService;
public FileViewController(FileService fileService, ApplicationSettingsService applicationSettingsService, DownloadLogRepository downloadLogRepository, AnalyticsService analyticsService, SessionService sessionService) {
public FileViewController(FileService fileService, ApplicationSettingsService applicationSettingsService, AnalyticsService analyticsService, SessionService sessionService) {
this.fileService = fileService;
this.applicationSettingsService = applicationSettingsService;
this.downloadLogRepository = downloadLogRepository;
this.analyticsService = analyticsService;
this.sessionService = sessionService;
}
@@ -63,15 +64,24 @@ public class FileViewController {
}
@GetMapping("/history/{uuid}")
public String viewDownloadHistory(@PathVariable String uuid, Model model) {
FileEntity file = fileService.getFile(uuid);
List<DownloadLog> downloadHistory = downloadLogRepository.findByFileUuid(uuid);
public String viewFileHistory(@PathVariable String uuid, Model model) {
FileEntity fileEntity = fileService.getFile(uuid);
long totalDownloads = analyticsService.getTotalDownloadsByFile(uuid);
FileEntityView fileEntityView = new FileEntityView(fileEntity, totalDownloads);
model.addAttribute("file", new FileEntityView(file, totalDownloads));
model.addAttribute("downloadHistory", downloadHistory);
List<FileActionLogDTO> actionLogs = new ArrayList<>();
return "download-history";
List<DownloadLog> downloadLogs = analyticsService.getDownloadsByFile(uuid);
List<FileRenewalLog> renewalLogs = analyticsService.getRenewalLogsByFile(uuid);
downloadLogs.forEach(log -> actionLogs.add(new FileActionLogDTO(log)));
renewalLogs.forEach(log -> actionLogs.add(new FileActionLogDTO(log)));
actionLogs.sort(Comparator.comparing(FileActionLogDTO::getActionDate).reversed());
model.addAttribute("file", fileEntityView);
model.addAttribute("actionLogs", actionLogs);
return "file-history";
}
@@ -101,7 +111,7 @@ public class FileViewController {
@PostMapping("/extend/{uuid}")
public String extendFile(@PathVariable String uuid, Model model, HttpServletRequest request) {
fileService.extendFile(uuid);
fileService.extendFile(uuid, request);
FileEntity fileEntity = fileService.getFile(uuid);
populateModelAttributes(fileEntity, model, request);
@@ -126,8 +136,8 @@ public class FileViewController {
}
@PostMapping("/keep-indefinitely/{uuid}")
public String updateKeepIndefinitely(@PathVariable String uuid, @RequestParam(required = false, defaultValue = "false") boolean keepIndefinitely) {
FileEntity fileEntity = fileService.updateKeepIndefinitely(uuid, keepIndefinitely);
public String updateKeepIndefinitely(@PathVariable String uuid, @RequestParam(required = false, defaultValue = "false") boolean keepIndefinitely, HttpServletRequest request) {
FileEntity fileEntity = fileService.updateKeepIndefinitely(uuid, keepIndefinitely, request);
if (fileEntity != null) {
return "redirect:/file/" + fileEntity.uuid;
}

View File

@@ -19,4 +19,9 @@ public class IndexViewController {
model.addAttribute("maxFileLifeTime", applicationSettingsService.getMaxFileLifeTime());
return "upload";
}
@GetMapping("/error")
public String getErrorPage() {
return "error";
}
}

View File

@@ -74,4 +74,4 @@ public class DownloadLog {
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
}
}

View File

@@ -0,0 +1,77 @@
package org.rostislav.quickdrop.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
public class FileRenewalLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "file_id", nullable = false)
private FileEntity file;
@Column(name = "action_date", nullable = false)
private LocalDateTime actionDate;
@Column(name = "ip_address", nullable = false)
private String ipAddress;
@Column(name = "user_agent", nullable = true)
private String userAgent;
public FileRenewalLog() {
this.actionDate = LocalDateTime.now();
}
public FileRenewalLog(FileEntity file, String ipAddress, String userAgent) {
this.file = file;
this.ipAddress = ipAddress;
this.userAgent = userAgent;
this.actionDate = LocalDateTime.now();
}
public FileEntity getFile() {
return file;
}
public void setFile(FileEntity file) {
this.file = file;
}
public LocalDateTime getActionDate() {
return actionDate;
}
public void setActionDate(LocalDateTime actionDate) {
this.actionDate = actionDate;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,67 @@
package org.rostislav.quickdrop.model;
import org.rostislav.quickdrop.entity.DownloadLog;
import org.rostislav.quickdrop.entity.FileRenewalLog;
import java.time.LocalDateTime;
public class FileActionLogDTO {
private String actionType; // "Download" or "Lifetime Renewed"
private LocalDateTime actionDate;
private String ipAddress;
private String userAgent;
public FileActionLogDTO(String actionType, LocalDateTime actionDate, String ipAddress, String userAgent) {
this.actionType = actionType;
this.actionDate = actionDate;
this.ipAddress = ipAddress;
this.userAgent = userAgent;
}
public FileActionLogDTO(DownloadLog downloadLog) {
this.actionType = "Download";
this.actionDate = downloadLog.getDownloadDate();
this.ipAddress = downloadLog.getDownloaderIp();
this.userAgent = downloadLog.getUserAgent();
}
public FileActionLogDTO(FileRenewalLog renewalLog) {
this.actionType = "Lifetime Renewed";
this.actionDate = renewalLog.getActionDate();
this.ipAddress = renewalLog.getIpAddress();
this.userAgent = renewalLog.getUserAgent();
}
// Getters and setters
public String getActionType() {
return actionType;
}
public void setActionType(String actionType) {
this.actionType = actionType;
}
public LocalDateTime getActionDate() {
return actionDate;
}
public void setActionDate(LocalDateTime actionDate) {
this.actionDate = actionDate;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
}

View File

@@ -0,0 +1,13 @@
package org.rostislav.quickdrop.repository;
import org.rostislav.quickdrop.entity.FileRenewalLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface RenewalLogRepository extends JpaRepository<FileRenewalLog, Long> {
@Query("SELECT f FROM FileRenewalLog f WHERE f.file.uuid = :uuid")
List<FileRenewalLog> findByFileUuid(String uuid);
}

View File

@@ -1,20 +1,27 @@
package org.rostislav.quickdrop.service;
import org.rostislav.quickdrop.entity.DownloadLog;
import org.rostislav.quickdrop.entity.FileRenewalLog;
import org.rostislav.quickdrop.model.AnalyticsDataView;
import org.rostislav.quickdrop.repository.DownloadLogRepository;
import org.rostislav.quickdrop.repository.RenewalLogRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import static org.rostislav.quickdrop.util.FileUtils.formatFileSize;
@Service
public class AnalyticsService {
private final FileService fileService;
private final DownloadLogRepository downloadLogRepository;
private final RenewalLogRepository renewalLogRepository;
public AnalyticsService(FileService fileService, DownloadLogRepository downloadLogRepository) {
public AnalyticsService(FileService fileService, DownloadLogRepository downloadLogRepository, RenewalLogRepository renewalLogRepository) {
this.fileService = fileService;
this.downloadLogRepository = downloadLogRepository;
this.renewalLogRepository = renewalLogRepository;
}
public AnalyticsDataView getAnalytics() {
@@ -34,4 +41,12 @@ public class AnalyticsService {
public long getTotalDownloadsByFile(String uuid) {
return downloadLogRepository.countDownloadsByFileId(uuid);
}
public List<DownloadLog> getDownloadsByFile(String fileUUID) {
return downloadLogRepository.findByFileUuid(fileUUID);
}
public List<FileRenewalLog> getRenewalLogsByFile(String fileUUID) {
return renewalLogRepository.findByFileUuid(fileUUID);
}
}

View File

@@ -4,10 +4,12 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.transaction.Transactional;
import org.rostislav.quickdrop.entity.DownloadLog;
import org.rostislav.quickdrop.entity.FileEntity;
import org.rostislav.quickdrop.entity.FileRenewalLog;
import org.rostislav.quickdrop.model.FileEntityView;
import org.rostislav.quickdrop.model.FileUploadRequest;
import org.rostislav.quickdrop.repository.DownloadLogRepository;
import org.rostislav.quickdrop.repository.FileRepository;
import org.rostislav.quickdrop.repository.RenewalLogRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
@@ -50,14 +52,16 @@ public class FileService {
private final DownloadLogRepository downloadLogRepository;
private final File tempDir = Paths.get(System.getProperty("java.io.tmpdir")).toFile();
private final SessionService sessionService;
private final RenewalLogRepository renewalLogRepository;
@Lazy
public FileService(FileRepository fileRepository, PasswordEncoder passwordEncoder, ApplicationSettingsService applicationSettingsService, DownloadLogRepository downloadLogRepository, SessionService sessionService) {
public FileService(FileRepository fileRepository, PasswordEncoder passwordEncoder, ApplicationSettingsService applicationSettingsService, DownloadLogRepository downloadLogRepository, SessionService sessionService, RenewalLogRepository renewalLogRepository) {
this.fileRepository = fileRepository;
this.passwordEncoder = passwordEncoder;
this.applicationSettingsService = applicationSettingsService;
this.downloadLogRepository = downloadLogRepository;
this.sessionService = sessionService;
this.renewalLogRepository = renewalLogRepository;
}
private static StreamingResponseBody getStreamingResponseBody(Path outputFile, FileEntity fileEntity) {
@@ -199,16 +203,22 @@ public class FileService {
return fileRepository.findByUUID(uuid).orElse(null);
}
public void extendFile(String uuid) {
Optional<FileEntity> referenceById = fileRepository.findByUUID(uuid);
if (referenceById.isEmpty()) {
return;
private static RequesterInfo getRequesterInfo(HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
String realIp = request.getHeader("X-Real-IP");
String ipAddress;
if (forwardedFor != null && !forwardedFor.isEmpty()) {
// The X-Forwarded-For header can contain multiple IPs, pick the first one
ipAddress = forwardedFor.split(",")[0].trim();
} else if (realIp != null && !realIp.isEmpty()) {
ipAddress = realIp;
} else {
ipAddress = request.getRemoteAddr();
}
FileEntity fileEntity = referenceById.get();
fileEntity.uploadDate = LocalDate.now();
logger.info("File extended: {}", fileEntity);
fileRepository.save(fileEntity);
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
return new RequesterInfo(ipAddress, userAgent);
}
public boolean deleteFileFromFileSystem(String uuid) {
@@ -267,10 +277,6 @@ public class FileService {
return sessionService.getPasswordForFileSessionToken(sessionToken.toString()).getPassword();
}
public List<FileEntity> searchFiles(String query) {
return fileRepository.searchFiles(query);
}
public List<FileEntity> searchNotHiddenFiles(String query) {
return fileRepository.searchNotHiddenFiles(query);
}
@@ -279,22 +285,17 @@ public class FileService {
return nullToZero(fileRepository.totalFileSizeForAllFiles());
}
public FileEntity updateKeepIndefinitely(String uuid, boolean keepIndefinitely) {
public void extendFile(String uuid, HttpServletRequest request) {
Optional<FileEntity> referenceById = fileRepository.findByUUID(uuid);
if (referenceById.isEmpty()) {
logger.info("File not found for 'update keep indefinitely': {}", uuid);
return null;
}
if (!keepIndefinitely) {
extendFile(uuid);
return;
}
FileEntity fileEntity = referenceById.get();
fileEntity.keepIndefinitely = keepIndefinitely;
logger.info("File keepIndefinitely updated: {}", fileEntity);
fileEntity.uploadDate = LocalDate.now();
logger.info("File extended: {}", fileEntity);
fileRepository.save(fileEntity);
return fileEntity;
logFileRenewal(fileEntity, request);
}
public FileEntity toggleHidden(String uuid) {
@@ -478,25 +479,39 @@ public class FileService {
return false;
}
private void logDownload(FileEntity fileEntity, HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
String realIp = request.getHeader("X-Real-IP");
String downloaderIp;
if (forwardedFor != null && !forwardedFor.isEmpty()) {
// The X-Forwarded-For header can contain multiple IPs, pick the first one
downloaderIp = forwardedFor.split(",")[0].trim();
} else if (realIp != null && !realIp.isEmpty()) {
downloaderIp = realIp;
} else {
downloaderIp = request.getRemoteAddr();
public FileEntity updateKeepIndefinitely(String uuid, boolean keepIndefinitely, HttpServletRequest request) {
Optional<FileEntity> referenceById = fileRepository.findByUUID(uuid);
if (referenceById.isEmpty()) {
logger.info("File not found for 'update keep indefinitely': {}", uuid);
return null;
}
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
DownloadLog downloadLog = new DownloadLog(fileEntity, downloaderIp, userAgent);
if (!keepIndefinitely) {
extendFile(uuid, request);
}
FileEntity fileEntity = referenceById.get();
fileEntity.keepIndefinitely = keepIndefinitely;
logger.info("File keepIndefinitely updated: {}", fileEntity);
fileRepository.save(fileEntity);
return fileEntity;
}
private void logDownload(FileEntity fileEntity, HttpServletRequest request) {
RequesterInfo info = getRequesterInfo(request);
DownloadLog downloadLog = new DownloadLog(fileEntity, info.ipAddress(), info.userAgent());
downloadLogRepository.save(downloadLog);
}
private void logFileRenewal(FileEntity fileEntity, HttpServletRequest request) {
RequesterInfo info = getRequesterInfo(request);
FileRenewalLog fileRenewalLog = new FileRenewalLog(fileEntity, info.ipAddress(), info.userAgent());
renewalLogRepository.save(fileRenewalLog);
}
private record RequesterInfo(String ipAddress, String userAgent) {
}
private String getFileChunkName(String fileName, int chunkNumber) {
return fileName + "_chunk_" + chunkNumber;
}

View File

@@ -58,7 +58,7 @@
<!-- Main Content -->
<div class="container mt-5">
<h1 class="text-center mb-4">Download History</h1>
<h1 class="text-center mb-4">History</h1>
<!-- File Name and Description -->
<div class="text-center">
@@ -89,30 +89,25 @@
</div>
</div>
<!-- Download History Table -->
<div class="card">
<div class="card-header bg-primary text-white">
<h2 class="mb-0">Download History</h2>
</div>
<div class="card-body">
<table class="table table-striped text-center">
<thead>
<tr>
<th>Downloader IP</th>
<th>Date</th>
<th>User Agent</th>
</tr>
</thead>
<tbody>
<tr th:each="log : ${downloadHistory}">
<td th:text="${log.downloaderIp}">127.0.0.1</td>
<td th:text="${#temporals.format(log.downloadDate, 'dd.MM.yyyy HH:mm:ss')}">01.12.2024 20:12:22</td>
<td th:text="${log.userAgent ?: 'N/A'}">Mozilla/5.0 (Windows NT 10.0; Win64; x64)</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- History Table -->
<table class="table table-striped text-center">
<thead>
<tr>
<th>Date</th>
<th>Action</th>
<th>IP Address</th>
<th>User Agent</th>
</tr>
</thead>
<tbody>
<tr th:each="log : ${actionLogs}">
<td th:text="${#temporals.format(log.actionDate, 'dd.MM.yyyy HH:mm:ss')}">01.12.2024 20:12:22</td>
<td th:text="${log.actionType}">Action</td>
<td th:text="${log.ipAddress}">127.0.0.1</td>
<td th:text="${log.userAgent}">Mozilla/5.0 (Windows NT 10.0; Win64; x64)</td>
</tr>
</tbody>
</table>
</div>
<!-- Bootstrap Bundle -->