diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index ead8e40..8e90f20 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -1,7 +1,5 @@ package ru.soune.nocopy.controller; -import com.google.api.client.util.IOUtils; -import com.google.common.io.ByteStreams; import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; @@ -9,24 +7,20 @@ import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.InputStreamResource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.docviewer.DocViewerRequest; import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.entity.company.Company; @@ -41,6 +35,7 @@ import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; +import ru.soune.nocopy.service.dockview.DockViewService; import ru.soune.nocopy.service.file.*; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.ProtectionsLimitService; @@ -48,14 +43,8 @@ import ru.soune.nocopy.service.file.cloud.CloudStorageService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.moderation.UserVerificationService; import ru.soune.nocopy.util.FileUtil; -import software.amazon.awssdk.services.s3.model.NoSuchKeyException; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; @@ -92,7 +81,7 @@ public class ApiController { private final ZipService zipService; - private final UserVerificationService userVerificationService; + private final DockViewService dockViewService; @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @@ -511,27 +500,6 @@ public class ApiController { MessageCode.FILE_DELETE.getDescription(), errorData)); } -//TODO - -// if (!entityResponse.isExistsOnDisk()) { -// Map errorData = new HashMap<>(); -// errorData.put("onDisk", entityResponse.isExistsOnDisk()); -// -// return ResponseEntity.ok().body(new BaseResponse(20004, -// MessageCode.FILE_NOT_EXIST.getCode(), -// MessageCode.FILE_NOT_EXIST.getDescription(), -// errorData)); -// } - -// if (!file.exists()) { -// Map errorData = new HashMap<>(); -// errorData.put("file", file.exists()); -// -// return ResponseEntity.ok().body(new BaseResponse(20004, -// MessageCode.FILE_DOWNLOAD_ERROR.getCode(), -// MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), -// errorData)); -// } byte[] bytes = cloudStorageService.readFileFromStorageBytes(entityResponse.getProtectedFilePath()); @@ -556,16 +524,54 @@ public class ApiController { MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), errorData)); -// } catch (IOException e) { -// Map errorData = new HashMap<>(); -// errorData.put("token", tokenHeader); -// errorData.put("fileId", fileId); -// errorData.put("version", version); -// -// return ResponseEntity.ok().body(new BaseResponse(20004, -// MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(), -// MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(), -// errorData)); + } + } + + @GetMapping("/file/link") + public ResponseEntity downloadFile(@RequestBody DocViewerRequest viewerRequest) { + try { + String tokenHeader = viewerRequest.getToken(); + Long userId = tokenHeader == null ? 0L: authService.useUserAuthToken(tokenHeader); + + FileEntityResponse entityResponse = fileEntityService.getById(viewerRequest.getFileId(), 0); + FileStatus status = entityResponse.getStatus(); + + if (status.equals(FileStatus.BLOCKED) || status.equals(FileStatus.DELETED) || + status.equals(FileStatus.REMOVED)) { + Map errorData = new HashMap<>(); + errorData.put("fileId", entityResponse.getId()); + + return ResponseEntity.ok().body(new BaseResponse(0, + MessageCode.FILE_IS_BLOCKED.getCode(), + MessageCode.FILE_IS_BLOCKED.getDescription(), + errorData)); + } + + byte[] bytes = cloudStorageService.readFileFromStorageBytes(entityResponse.getProtectedFilePath()); + + dockViewService.createDockView(viewerRequest, userId); + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + entityResponse.getFileName() + "\"") + .body(bytes); + } catch (FileEntityNotFoundException e) { + Map errorData = new HashMap<>(); + errorData.put("fileId", viewerRequest.getFileId()); + + return ResponseEntity.ok().body(new BaseResponse(0, + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), + errorData)); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", viewerRequest.getFileId()); + + return ResponseEntity.ok().body(new BaseResponse(0, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + errorData)); } } diff --git a/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java b/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java new file mode 100644 index 0000000..e884473 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.dto.docviewer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DocViewerRequest { + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("token") + private String token; + + @JsonProperty("ip") + private String ip; + + @JsonProperty("user_agent") + private String userAgent; +} diff --git a/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java b/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java new file mode 100644 index 0000000..ae821d2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java @@ -0,0 +1,47 @@ +package ru.soune.nocopy.dto.docviewer; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DockViewStatsResponse { + private String fileId; + + private Long totalViews; + + private Long uniqueIps; + + private Long uniqueUsers; + + private LocalDateTime firstViewDate; + + private LocalDateTime lastViewDate; + + private List content; + + private Integer pageNumber; + + private Integer pageSize; + + private Long totalElements; + + private Integer totalPages; + + private Boolean isLast; + + private Boolean isFirst; + + private Boolean hasNext; + + private Boolean hasPrevious; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/docviewer/DockView.java b/src/main/java/ru/soune/nocopy/entity/docviewer/DockView.java new file mode 100644 index 0000000..fd4f22b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/docviewer/DockView.java @@ -0,0 +1,36 @@ +package ru.soune.nocopy.entity.docviewer; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Table(name = "dock_view") +@RequiredArgsConstructor +@AllArgsConstructor +@Entity +@Setter @Getter +public class DockView { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "viewer_id") + private Long viewerId; + + @Column(name = "viewer_ip") + private String viewerIp; + + @Column(name = "user_agent") + private String userAgent; + + @Column(name = "file_id") + private String fileId; + + @Column(name = "created_at",updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java b/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java new file mode 100644 index 0000000..e8fc429 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java @@ -0,0 +1,75 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.docviewer.DockView; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface DockViewRepository extends JpaRepository { + + List findAllByFileId(String fileId); + + List findAllByViewerId(Long viewerId); + + Optional findByFileIdAndViewerId(String fileId, Long viewerId); + + List findAllByViewerIp(String viewerIp); + + Page findAllByFileIdOrderByCreatedAtDesc(String fileId, Pageable pageable); + + Page findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable); + + long countByFileId(String fileId); + + @Query("SELECT COUNT(DISTINCT dv.viewerIp) FROM DockView dv WHERE dv.fileId = :fileId") + long countUniqueIpsByFileId(@Param("fileId") String fileId); + + @Query("SELECT COUNT(DISTINCT dv.viewerId) FROM DockView dv WHERE dv.fileId = :fileId") + long countUniqueViewersByFileId(@Param("fileId") String fileId); + + @Query("SELECT MAX(dv.createdAt) FROM DockView dv WHERE dv.fileId = :fileId") + Optional findLastViewDateByFileId(@Param("fileId") String fileId); + + + @Query("SELECT dv.fileId, " + + "COUNT(dv) as totalViews, " + + "COUNT(DISTINCT dv.viewerIp) as uniqueIps, " + + "MAX(dv.createdAt) as lastViewDate " + + "FROM DockView dv " + + "GROUP BY dv.fileId " + + "ORDER BY lastViewDate DESC") + Page findViewsStatsPaginated(Pageable pageable); + + @Query("SELECT dv.fileId, " + + "COUNT(dv) as totalViews, " + + "COUNT(DISTINCT dv.viewerIp) as uniqueIps, " + + "COUNT(DISTINCT dv.viewerId) as uniqueUsers, " + + "MAX(dv.createdAt) as lastViewDate, " + + "MIN(dv.createdAt) as firstViewDate " + + "FROM DockView dv " + + "WHERE dv.fileId = :fileId " + + "GROUP BY dv.fileId") + Optional findViewsStatsByFileId(@Param("fileId") String fileId); + + void deleteAllByFileId(String fileId); + + void deleteAllByCreatedAtBefore(LocalDateTime date); + + List findAllByFileIdAndCreatedAtBetween( + String fileId, + LocalDateTime startDate, + LocalDateTime endDate + ); + + boolean existsByFileIdAndViewerIp(String fileId, String viewerIp); + + List findTop10ByFileIdOrderByCreatedAtDesc(String fileId); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java b/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java new file mode 100644 index 0000000..c4f0ac2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java @@ -0,0 +1,124 @@ +package ru.soune.nocopy.service.dockview; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.docviewer.DocViewerRequest; +import ru.soune.nocopy.dto.docviewer.DockViewStatsResponse; +import ru.soune.nocopy.entity.docviewer.DockView; +import ru.soune.nocopy.repository.DockViewRepository; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DockViewService { + + private final DockViewRepository dockViewRepository; + + @Transactional + public DockView createDockView(DocViewerRequest request, Long userId) { + DockView dockView = new DockView(); + dockView.setViewerId(userId); + dockView.setViewerIp(request.getIp() != null ? request.getIp() : "-"); + dockView.setUserAgent(request.getUserAgent() != null ? request.getUserAgent() : "-"); + dockView.setFileId(request.getFileId()); + dockView.setCreatedAt(LocalDateTime.now()); + + DockView saved = dockViewRepository.save(dockView); + + log.info("Created dock view record: fileId={}, viewerId={}, ip={}", + saved.getFileId(), saved.getViewerId(), saved.getViewerIp()); + + return saved; + } + + + public DockViewStatsResponse getStatsByFileId(String fileId) { + Optional statsOpt = dockViewRepository.findViewsStatsByFileId(fileId); + + if (statsOpt.isEmpty()) { + return DockViewStatsResponse.builder() + .fileId(fileId) + .totalViews(0L) + .uniqueIps(0L) + .uniqueUsers(0L) + .firstViewDate(null) + .lastViewDate(null) + .build(); + } + + Object[] stats = statsOpt.get(); + return DockViewStatsResponse.builder() + .fileId((String) stats[0]) + .totalViews((Long) stats[1]) + .uniqueIps((Long) stats[2]) + .uniqueUsers((Long) stats[3]) + .lastViewDate((LocalDateTime) stats[4]) + .firstViewDate((LocalDateTime) stats[5]) + .build(); + } + + public DockViewStatsResponse getBasicStatsByFileId(String fileId) { + long totalViews = dockViewRepository.countByFileId(fileId); + long uniqueIps = dockViewRepository.countUniqueIpsByFileId(fileId); + + return DockViewStatsResponse.builder() + .fileId(fileId) + .totalViews(totalViews) + .uniqueIps(uniqueIps) + .build(); + } + + public DockViewStatsResponse getAllFilesStatsPaginated(Pageable pageable) { + Page statsPage = dockViewRepository.findViewsStatsPaginated(pageable); + + List statsList = statsPage.getContent().stream() + .map(row -> DockViewStatsResponse.builder() + .fileId((String) row[0]) + .totalViews((Long) row[1]) + .uniqueIps((Long) row[2]) + .lastViewDate((LocalDateTime) row[3]) + .build()) + .collect(Collectors.toList()); + + return DockViewStatsResponse.builder() + .content(statsList) + .pageNumber(statsPage.getNumber()) + .pageSize(statsPage.getSize()) + .totalElements(statsPage.getTotalElements()) + .totalPages(statsPage.getTotalPages()) + .isLast(statsPage.isLast()) + .isFirst(statsPage.isFirst()) + .hasNext(statsPage.hasNext()) + .hasPrevious(statsPage.hasPrevious()) + .build(); + } + + public boolean hasViewedFromIp(String fileId, String ip) { + return dockViewRepository.existsByFileIdAndViewerIp(fileId, ip); + } + + public Optional getLastViewDate(String fileId) { + return dockViewRepository.findLastViewDateByFileId(fileId); + } + + @Transactional + public void deleteAllViewsByFileId(String fileId) { + dockViewRepository.deleteAllByFileId(fileId); + log.info("Deleted all views for fileId: {}", fileId); + } + + @Transactional + public void cleanOldViews() { + LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); + dockViewRepository.deleteAllByCreatedAtBefore(thirtyDaysAgo); + log.info("Cleaned views older than: {}", thirtyDaysAgo); + } +}