12 Commits
Author SHA1 Message Date
vladp 7b1ed5fe67 NCBACK-3
Test Workflow / test (push) Successful in 3s
2026-01-10 00:35:29 +07:00
vladp b3a3d57ee3 NCBACK-3
Test Workflow / test (push) Successful in 3s
2026-01-09 23:41:19 +07:00
vladp edd508e5d8 Merge branch 'dev' into NCBACK-3
Test Workflow / test (push) Successful in 5s
# Conflicts:
#	docker-compose.yaml
#	src/main/java/ru/soune/nocopy/dto/MessageCode.java
2026-01-09 12:34:24 +07:00
vladp 63baaf7c80 update types
Test Workflow / test (push) Successful in 4s
2026-01-08 12:59:30 +07:00
vladp 3a9a5b48a2 update types
Test Workflow / test (push) Successful in 4s
2026-01-07 14:10:35 +07:00
vladp 6fff5884d2 set chunk
Test Workflow / test (push) Successful in 3s
2026-01-07 13:16:53 +07:00
vladp f8673dd581 set chunk
Test Workflow / test (push) Successful in 3s
2026-01-07 12:50:40 +07:00
vladp b9a401f113 correct file_extensions
Test Workflow / test (push) Successful in 8s
2026-01-07 11:56:03 +07:00
vladp 918b4894fc add endpoint with files user info
Test Workflow / test (push) Successful in 6s
2026-01-06 17:27:38 +07:00
vladp bf4d1ca450 add endpoint with files user info
Test Workflow / test (push) Successful in 4s
2026-01-06 12:50:39 +07:00
vladp ffb147d716 NCBACK-3 Add found by yandex image
Test Workflow / test (push) Successful in 4s
2025-12-22 14:42:27 +07:00
vladp 647fd6e1a7 NCBACK-3 Add found by yandex image
Test Workflow / test (push) Successful in 3s
2025-12-22 14:07:44 +07:00
30 changed files with 529 additions and 318 deletions
+4
View File
@@ -31,6 +31,10 @@ dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'commons-validator:commons-validator:1.7' implementation 'commons-validator:commons-validator:1.7'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0'
implementation 'tools.jackson.core:jackson-core:3.0.3'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
+9 -4
View File
@@ -58,13 +58,18 @@ services:
environment: environment:
FILE_STORAGE_PATH: /data/uploads FILE_STORAGE_PATH: /data/uploads
MAX_FILE_SIZE: 10737418240 MAX_FILE_SIZE: 10737418240
FILE_CHUNK_SIZE: 1048576 # FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
POSTGRES_HOST: db POSTGRES_HOST: db
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
STORAGE_SERVICE_URL: http://storage:8081 STORAGE_SERVICE_URL: http://storage:8081
SPRING_PROFILES_ACTIVE: docker
depends_on: depends_on:
- db - db
ports: ports:
@@ -120,7 +125,7 @@ services:
prometheus: prometheus:
image: prom/prometheus:latest image: prom/prometheus:latest
container_name: prometheus container_name: prometheus
deploy: # ← ДОБАВЛЕНО deploy:
resources: resources:
limits: limits:
cpus: '0.5' cpus: '0.5'
@@ -148,7 +153,7 @@ services:
loki: loki:
image: grafana/loki:2.9.2 image: grafana/loki:2.9.2
container_name: loki container_name: loki
deploy: # ← ДОБАВЛЕНО deploy:
resources: resources:
limits: limits:
cpus: '1.0' cpus: '1.0'
@@ -173,7 +178,7 @@ services:
tempo: tempo:
image: grafana/tempo:2.4.1 image: grafana/tempo:2.4.1
container_name: tempo container_name: tempo
deploy: # ← ДОБАВЛЕНО deploy:
resources: resources:
limits: limits:
cpus: '0.5' cpus: '0.5'
+39 -141
View File
@@ -7,7 +7,6 @@ pipeline {
defaultValue: 'dev', defaultValue: 'dev',
description: 'Ветка для деплоя' description: 'Ветка для деплоя'
) )
string( string(
name: 'SERVER', name: 'SERVER',
defaultValue: '92.242.61.23', defaultValue: '92.242.61.23',
@@ -51,156 +50,57 @@ pipeline {
sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER " sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH} cd /opt/deployments/${params.BRANCH}
echo '========================================' echo '1. Остановка старого приложения...'
echo 'Step 1: Checking current state...' docker stop app-backend 2>/dev/null || echo 'Контейнер не найден'
echo '========================================' docker rm app-backend 2>/dev/null || echo 'Контейнер не найден'
docker ps --filter 'name=postgres|app-backend|storage|grafana|prometheus|loki|tempo|alloy' --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}' echo '2. Удаление старых образов...'
docker images --filter 'reference=*app*' -q | xargs -r docker rmi -f 2>/dev/null || echo 'Нет образов для удаления'
echo '========================================' echo '3. Создание сети если нужно...'
echo 'Step 2: Setup environment for compatibility...' docker network create app-network 2>/dev/null || echo 'Сеть уже существует'
echo '========================================'
docker network create app-network 2>/dev/null || echo 'Network app-network already exists' echo '4. Запуск инфраструктуры...'
docker-compose up -d db storage
echo 'COMPOSE_COMPATIBILITY=true' > .env echo '5. Ожидание PostgreSQL...'
sleep 10
echo '========================================' echo '6. Сборка нового образа приложения...'
echo 'Step 3: Force cleanup old application...' docker build --no-cache -t app-backend:latest .
echo '========================================'
docker-compose --compatibility down 2>/dev/null || echo 'No previous compose stack' echo '7. Запуск приложения...'
docker run -d \\
--name app-backend \\
--network app-network \\
--network-alias app \\
-p 80:8080 \\
-v uploads_data:/data/uploads:rw \\
-e POSTGRES_DB=no_copy_ \\
-e POSTGRES_USER=postgres \\
-e POSTGRES_PASSWORD=postgres \\
-e POSTGRES_PORT=5432 \\
-e POSTGRES_HOST=db \\
--restart unless-stopped \\
app-backend:latest
docker images --filter 'reference=*app-backend*' -q | xargs -r docker rmi -f 2>/dev/null || echo 'No images to remove' echo '8. Запуск мониторинга...'
docker-compose up -d grafana prometheus loki tempo alloy
echo '========================================' echo '9. Проверка...'
echo 'Step 4: Verify files and fix compose...' sleep 5
echo '========================================'
if [ ! -f docker-compose.yaml ]; then echo 'Статус контейнеров:'
echo 'ERROR: docker-compose.yaml not found!' docker ps --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}'
ls -la
exit 1
fi
echo 'Checking docker-compose.yaml syntax...' echo '10. Проверка health...'
docker-compose --compatibility config 2>&1 | head -20 || echo 'Config check output' if curl -s -f http://localhost:80/health > /dev/null 2>&1; then
echo 'Приложение работает'
echo '========================================' echo 'URL: http://${params.SERVER}:80'
echo 'Step 5: Start infrastructure services...'
echo '========================================'
echo 'Starting PostgreSQL with resource limits...'
docker-compose --compatibility up -d db
echo 'Starting storage service...'
docker-compose --compatibility up -d storage
echo 'Waiting for PostgreSQL to be ready...'
for i in {1..30}; do
if docker-compose --compatibility exec db pg_isready -U postgres 2>/dev/null; then
echo 'PostgreSQL is ready'
break
fi
echo 'Waiting... attempt ' \$i
sleep 2
done
echo '========================================'
echo 'Step 6: Build application image...'
echo '========================================'
if [ ! -f Dockerfile ]; then
echo 'ERROR: Dockerfile not found!'
exit 1
fi
echo 'Building Docker image...'
docker build --no-cache -t app-backend:latest . 2>&1 | tail -30
echo 'Image created successfully'
docker images app-backend:latest --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.Size}}\\t{{.CreatedAt}}'
echo '========================================'
echo 'Step 7: Start application with resource limits...'
echo '========================================'
docker volume create uploads_data 2>/dev/null || echo 'Volume uploads_data already exists'
echo 'Starting main application...'
docker-compose --compatibility up -d app
echo 'Waiting for application to start...'
sleep 15
echo '========================================'
echo 'Step 8: Start monitoring stack...'
echo '========================================'
docker-compose --compatibility up -d grafana prometheus loki tempo alloy
echo '========================================'
echo 'Step 9: Verify deployment...'
echo '========================================'
echo 'All containers status:'
docker-compose --compatibility ps
echo ''
echo 'Resource limits check:'
echo 'App backend limits:'
docker inspect app-backend --format='{{.HostConfig.Memory}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}' 2>/dev/null || echo 'Cannot inspect'
echo ''
echo 'PostgreSQL limits:'
docker inspect postgres --format='{{.HostConfig.Memory}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}' 2>/dev/null || echo 'Cannot inspect'
echo ''
echo 'Container resource usage:'
docker stats --no-stream --format 'table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}' postgres app-backend storage 2>/dev/null || echo 'Stats not available'
echo '========================================'
echo 'Step 10: Health checks...'
echo '========================================'
echo 'Application health check...'
for i in {1..10}; do
if curl -s -f http://localhost:80/health 2>/dev/null; then
echo '✓ Application health check PASSED'
break
elif [ \$i -eq 10 ]; then
echo '✗ Application health check FAILED'
echo 'Last logs:'
docker-compose --compatibility logs --tail=20 app
else
echo 'Waiting for application... attempt ' \$i
sleep 5
fi
done
echo 'Checking monitoring services...'
if curl -s -f http://localhost:3000/api/health 2>/dev/null; then
echo '✓ Grafana is healthy'
else else
echo '⚠ Grafana may be starting' echo 'Проверка health не удалась'
docker logs app-backend --tail=20
fi fi
if curl -s -f http://localhost:9090/-/healthy 2>/dev/null; then
echo '✓ Prometheus is healthy'
fi
echo '========================================'
echo 'Deployment Summary:'
echo '========================================'
echo 'Application URL: http://${params.SERVER}:80'
echo 'Grafana: http://${params.SERVER}:3000 (admin/admin)'
echo 'Prometheus: http://${params.SERVER}:9090'
echo 'Loki: http://${params.SERVER}:3100'
echo ''
echo 'To view logs: docker-compose logs -f app'
echo 'To view all containers: docker-compose ps'
echo 'To stop: docker-compose down'
echo '========================================'
" "
""" """
} }
@@ -213,8 +113,6 @@ pipeline {
success { success {
echo "Deployment successful" echo "Deployment successful"
echo "Application URL: http://${params.SERVER}:80" echo "Application URL: http://${params.SERVER}:80"
echo "Grafana: http://${params.SERVER}:3000"
echo "Prometheus: http://${params.SERVER}:9090"
} }
failure { failure {
echo "Deployment failed for branch ${params.BRANCH}" echo "Deployment failed for branch ${params.BRANCH}"
@@ -17,7 +17,7 @@ public class HandlerConfig {
FileUploadHandler upload, FileUploadHandler upload,
FileEntityHandler file, FileEntityHandler file,
LogoutRequestHandler logoutHandler, LogoutRequestHandler logoutHandler,
AuthRequestHandler authRequestHandler ImageFoundRequestHandler imageFoundRequestHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -25,7 +25,7 @@ public class HandlerConfig {
map.put(20004, upload); map.put(20004, upload);
map.put(20005, file); map.put(20005, file);
map.put(20006, logoutHandler); map.put(20006, logoutHandler);
map.put(20008, authRequestHandler); map.put(20007, imageFoundRequestHandler);
return map; return map;
} }
@@ -35,6 +35,8 @@ public class JacksonConfig {
mapper.registerModule(javaTimeModule); mapper.registerModule(javaTimeModule);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
return mapper; return mapper;
} }
@@ -19,11 +19,12 @@ import ru.soune.nocopy.dto.file.ChunkUploadResponse;
import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse;
import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.FileEntityResponse;
import ru.soune.nocopy.dto.file.UploadProgress; import ru.soune.nocopy.dto.file.UploadProgress;
import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.*; import ru.soune.nocopy.handler.*;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
@@ -47,7 +48,7 @@ public class ApiController {
private final FileEntityService fileEntityService; private final FileEntityService fileEntityService;
private final AuthService authService; private final AuthTokenRepository authTokenRepository;
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@@ -70,9 +71,6 @@ public class ApiController {
return ResponseEntity.ok().body(response); return ResponseEntity.ok().body(response);
} catch (ValidationException e) { } catch (ValidationException e) {
return createValidationErrorResponse(e.getBindingResult(), e.getMsgId()); return createValidationErrorResponse(e.getBindingResult(), e.getMsgId());
} catch (NotFoundAuthToken e) {
return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>()));
} catch (NotValidFieldException e) { } catch (NotValidFieldException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@@ -259,7 +257,7 @@ public class ApiController {
errorData)); errorData));
} }
Long userId = authService.useUserAuthToken(tokenHeader); Long userId = getUserIdFromToken(tokenHeader);
FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version);
if (!entityResponse.getUserId().equals(userId)) { if (!entityResponse.getUserId().equals(userId)) {
@@ -370,15 +368,10 @@ public class ApiController {
return contentType; return contentType;
} }
private boolean isPreviewSupported(String mimeType) { private Long getUserIdFromToken(String tokenHeader) {
if (mimeType == null) { String token = tokenHeader.replace("Bearer ", "");
return false; AuthToken authToken = authTokenRepository.findByToken(token)
} .orElseThrow(() -> new NotFoundAuthToken("Token not found"));
return authToken.getUser().getId();
return mimeType.startsWith("image") ||
mimeType.startsWith("text") ||
mimeType.equals("pdf") ||
mimeType.startsWith("video") ||
mimeType.startsWith("audio");
} }
} }
@@ -10,6 +10,7 @@ public enum MessageCode {
FILE_UPLOAD_ERROR(2, "File upload error"), FILE_UPLOAD_ERROR(2, "File upload error"),
FILE_DOWNLOAD_ERROR(2, "File download error"), FILE_DOWNLOAD_ERROR(2, "File download error"),
FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"),
IMAGE_FOUND_ERROR(2, "Image found error"),
INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
INCOMPLETE_UPLOAD(2, "Not load all chunks"), INCOMPLETE_UPLOAD(2, "Not load all chunks"),
MSG_ID_NOT_FOUND(4, "Message id not found"), MSG_ID_NOT_FOUND(4, "Message id not found"),
@@ -18,7 +19,6 @@ public enum MessageCode {
AUTH_EMAIL_NOT_FOUND(4, "Email not found"), AUTH_EMAIL_NOT_FOUND(4, "Email not found"),
AUTH_EMAIL_OR_TOKEN_NOT_FOUND(4, "Email or Token not found "), AUTH_EMAIL_OR_TOKEN_NOT_FOUND(4, "Email or Token not found "),
AUTH_TOKEN_MISMATCH(4, "Token mismatch"), AUTH_TOKEN_MISMATCH(4, "Token mismatch"),
AUTH_TOKEN_NOT_FOUND(4, "Token not found"),
FILE_NOT_FOUND(4, "File not found"), FILE_NOT_FOUND(4, "File not found"),
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"); AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match");
@@ -1,11 +0,0 @@
package ru.soune.nocopy.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class TokenProcessRequest {
@JsonProperty("token")
private String token;
}
@@ -0,0 +1,61 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileInfoUserResponse {
@JsonProperty("all_files_size")
private Long allFileSize;
@JsonProperty("all_files_quantity")
private Integer fileCount;
@JsonProperty("all_files_check")
private Integer filesCheck;
@JsonProperty("all_files_violation")
private Integer filesViolation;
@JsonProperty("images_size")
private Long imagesSize;
@JsonProperty("images_quantity")
private Integer imagesCount;
@JsonProperty("images_check")
private Integer imagesCheck;
@JsonProperty("images_violations")
private Integer imagesViolations;
@JsonProperty("videos_size")
private Long videosSize;
@JsonProperty("videos_quantity")
private Integer videosCount;
@JsonProperty("videos_check")
private Integer videosCheck;
@JsonProperty("videos_violations")
private Integer videosViolations;
@JsonProperty("audios_size")
private Long audiosSize;
@JsonProperty("audios_quantity")
private Integer audiosCount;
@JsonProperty("audios_check")
private Integer audiosCheck;
@JsonProperty("audios_violations")
private Integer audiosViolations;
}
@@ -0,0 +1,10 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ImageSearchRequest {
@JsonProperty("file_id")
private String fileId;
}
@@ -0,0 +1,36 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class YandexSearchResponse {
@JsonProperty("images")
private List<ImageResult> images;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ImageResult {
@JsonProperty("url")
private String url;
@JsonProperty("pageUrl")
private String pageUrl;
@JsonProperty("pageTitle")
private String pageTitle;
@JsonProperty("width")
private Integer width;
@JsonProperty("height")
private Integer height;
@JsonProperty("host")
private String host;
}
}
@@ -30,7 +30,7 @@ public class AuthToken {
private String token; private String token;
@Column(name = "expires_at", nullable = false) @Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); private LocalDateTime expiresAt = LocalDateTime.now().plusDays(30);
@CreatedDate @CreatedDate
@Column(name = "created_at", updatable = false, nullable = false) @Column(name = "created_at", updatable = false, nullable = false)
@@ -41,4 +41,8 @@ public class AuthToken {
@Column(name = "is_active") @Column(name = "is_active")
private Boolean isActive = true; private Boolean isActive = true;
public boolean isValid() {
return Boolean.TRUE.equals(isActive) && expiresAt.isAfter(LocalDateTime.now());
}
} }
@@ -4,5 +4,7 @@ public enum FileStatus {
ACTIVE, ACTIVE,
DELETED, DELETED,
PROCESSING, PROCESSING,
VIOLATION,
CHECKED,
ERROR ERROR
} }
@@ -7,21 +7,10 @@ import java.util.List;
@Getter @Getter
public enum FileType { public enum FileType {
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp")), IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "jfif")),
VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg", VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg",
"3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")), "3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")),
AUDIO("audio", Arrays.asList("mp3", "wav", "flac")), AUDIO("audio", Arrays.asList("mp3", "wav", "flac"));
DOCUMENT("document", Arrays.asList(
"pdf", "txt", "rtf",
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pps", "ppsx", "dot", "dotx", "xlt", "xltx", "pot", "potx",
"odt", "ods", "odp", "odg", "odf", "odb", "odc", "odi", "odm", "ott", "ots", "otp", "otg", "oth",
"sxw", "sxc", "sxi", "sxd", "sxg", "stc", "sti", "stw", "sxm",
"pages", "numbers", "key",
"csv", "tsv", "xml", "html", "htm", "tex", "md", "markdown",
"epub", "mobi", "azw", "azw3", "fb2",
"wps", "wpt", "et", "dps", "vsd", "vsdx",
"java", "py", "cpp", "c", "h", "js", "css", "php", "sql", "json", "yaml", "yml", "sh", "bat",
"one", "note"));
private final String displayName; private final String displayName;
private final List<String> allowedExtensions; private final List<String> allowedExtensions;
@@ -1,37 +0,0 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.TokenProcessRequest;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.service.auth.AuthService;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws NotFoundAuthToken {
TokenProcessRequest tokenProcessRequest = objectMapper.convertValue(request.getMessageBody(),
TokenProcessRequest.class);
authService.useUserAuthToken(tokenProcessRequest.getToken());
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Token processed successfully",
Map.of("token", tokenProcessRequest.getToken()));
}
}
@@ -6,14 +6,15 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.auth.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -22,9 +23,10 @@ import java.util.List;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class FileEntityHandler implements RequestHandler { public class FileEntityHandler implements RequestHandler {
private final FileEntityService fileEntityService; private final FileEntityService fileEntityService;
private final AuthService authService; private final FileStatsService fileStatsService;
private final AuthTokenRepository authTokenRepository; private final AuthTokenRepository authTokenRepository;
@@ -43,6 +45,8 @@ public class FileEntityHandler implements RequestHandler {
switch (action) { switch (action) {
case "file_info": case "file_info":
return handleGetFileInfo(request, fileRequest); return handleGetFileInfo(request, fileRequest);
case "user_files_info":
return handleGetFilesUserInfo(request, fileRequest);
case "file_by_session": case "file_by_session":
return handleGetFileBySession(request, fileRequest); return handleGetFileBySession(request, fileRequest);
case "user_files": case "user_files":
@@ -75,9 +79,19 @@ public class FileEntityHandler implements RequestHandler {
} }
} }
private BaseResponse handleGetFilesUserInfo(BaseRequest request, FileEntityRequest fileRequest) {
Long userId = getUserIdFromToken(fileRequest.getToken());
FileInfoUserResponse userFileStats = fileStatsService.getUserFileStats(userId);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
userFileStats);
}
private BaseResponse handleGetFileInfo(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleGetFileInfo(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = getUserIdFromToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId(), request.getVersion()); FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId(), request.getVersion());
if (!fileInfo.getUserId().equals(userId)) { if (!fileInfo.getUserId().equals(userId)) {
@@ -108,7 +122,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetFileBySession(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleGetFileBySession(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = getUserIdFromToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getByUploadSessionId(fileRequest.getUploadSessionId(), FileEntityResponse fileInfo = fileEntityService.getByUploadSessionId(fileRequest.getUploadSessionId(),
request.getVersion()); request.getVersion());
@@ -140,7 +154,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetUserFiles(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleGetUserFiles(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = getUserIdFromToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
@@ -167,7 +181,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = getUserIdFromToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
@@ -214,7 +228,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = getUserIdFromToken(fileRequest.getToken());
long usage = fileEntityService.getUserStorageUsed(userId); long usage = fileEntityService.getUserStorageUsed(userId);
StorageUsageResponse response = StorageUsageResponse.builder() StorageUsageResponse response = StorageUsageResponse.builder()
@@ -244,7 +258,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = getUserIdFromToken(fileRequest.getToken());
String fileId = fileRequest.getFileId(); String fileId = fileRequest.getFileId();
FileEntityResponse fileInfo = fileEntityService.getById(fileId, request.getVersion()); FileEntityResponse fileInfo = fileEntityService.getById(fileId, request.getVersion());
DeleteFileResponse response; DeleteFileResponse response;
@@ -291,6 +305,20 @@ public class FileEntityHandler implements RequestHandler {
} }
} }
private Long getUserIdFromToken(String token) {
if (token == null || token.isBlank()) {
throw new NotFoundAuthToken("Token is required");
}
if (token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
}
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
return authToken.getUser().getId();
}
private String formatFileSize(long size) { private String formatFileSize(long size) {
if (size < 1024) return size + " B"; if (size < 1024) return size + " B";
int exp = (int) (Math.log(size) / Math.log(1024)); int exp = (int) (Math.log(size) / Math.log(1024));
@@ -17,7 +17,6 @@ import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.validator.FileUploadRequestValidator; import ru.soune.nocopy.handler.validator.FileUploadRequestValidator;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.service.auth.AuthService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import java.util.*; import java.util.*;
@@ -29,8 +28,6 @@ import java.util.stream.Collectors;
public class FileUploadHandler implements RequestHandler { public class FileUploadHandler implements RequestHandler {
private final FileUploadService fileUploadService; private final FileUploadService fileUploadService;
private final AuthService authService;
private final AuthTokenRepository authTokenRepository; private final AuthTokenRepository authTokenRepository;
private final FileUploadSessionRepository fileUploadSessionRepository; private final FileUploadSessionRepository fileUploadSessionRepository;
@@ -89,9 +86,10 @@ public class FileUploadHandler implements RequestHandler {
MessageCode.INVALID_TOKEN.getDescription(), Map.of("token", token)); MessageCode.INVALID_TOKEN.getDescription(), Map.of("token", token));
} }
Long userId = authService.useUserAuthToken(token); AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest"); BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest");
fileUploadRequestValidator.validate(fileRequest, bindingResult); fileUploadRequestValidator.validate(fileRequest, bindingResult);
if (bindingResult.hasErrors()) { if (bindingResult.hasErrors()) {
Map<String, String> fieldErrors = bindingResult.getFieldErrors() Map<String, String> fieldErrors = bindingResult.getFieldErrors()
@@ -107,7 +105,7 @@ public class FileUploadHandler implements RequestHandler {
} }
FileUploadSession session = fileUploadService.initUpload( FileUploadSession session = fileUploadService.initUpload(
userId, authToken.getUser().getId(),
fileRequest.getFileName(), fileRequest.getFileName(),
fileRequest.getFileType(), fileRequest.getFileType(),
fileRequest.getExtension(), fileRequest.getExtension(),
@@ -0,0 +1,37 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.service.YandexSearchService;
@Slf4j
@Component
@RequiredArgsConstructor
public class ImageFoundRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper;
@Autowired
private final YandexSearchService yandexSearchService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
ImageSearchRequest.class);
String fileId = imageSearchRequest.getFileId();
YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), response);
}
}
@@ -7,7 +7,7 @@ import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.AuthService;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
@@ -17,9 +17,7 @@ import java.util.Map;
public class LoginRequestHandler implements RequestHandler { public class LoginRequestHandler implements RequestHandler {
private final UserRepository userRepository; private final UserRepository userRepository;
private final AuthService authService; private final AuthService authService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@Override @Override
@@ -36,12 +34,9 @@ public class LoginRequestHandler implements RequestHandler {
} }
AuthToken authToken = authService.login(loginRequest); AuthToken authToken = authService.login(loginRequest);
String token = authToken.getToken();
LoginAnswer loginAnswer = new LoginAnswer(); LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setToken(token); loginAnswer.setToken(authToken.getToken());
authService.useUserAuthToken(token);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), loginAnswer); MessageCode.SUCCESS.getDescription(), loginAnswer);
@@ -9,7 +9,7 @@ import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.AuthService;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@@ -12,7 +12,7 @@ import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator; import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.AuthService;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
@@ -52,11 +52,9 @@ public class RegRequestHandler implements RequestHandler {
} }
AuthToken authToken = authService.register(regRequest); AuthToken authToken = authService.register(regRequest);
String token = authToken.getToken();
authService.useUserAuthToken(token);
RegAnswer regAnswer = new RegAnswer(); RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(token); regAnswer.setToken(authToken.getToken());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer); MessageCode.SUCCESS.getDescription(), regAnswer);
@@ -4,12 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.AuthToken;
import java.time.LocalDateTime; import java.time.LocalDate;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> { public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
Optional<AuthToken> findByLastUsedAtAndExpiresAtBefore(LocalDateTime lastUsedAt, LocalDateTime expiresAtBefore); Optional<AuthToken> findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter);
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
Optional<AuthToken> findByToken(String token); Optional<AuthToken> findByToken(String token);
} }
@@ -14,6 +14,7 @@ import java.util.Optional;
public interface FileEntityRepository extends JpaRepository<FileEntity, String> { public interface FileEntityRepository extends JpaRepository<FileEntity, String> {
List<FileEntity> findByUserId(Long userId); List<FileEntity> findByUserId(Long userId);
Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash);
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status); List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
@@ -1,13 +1,13 @@
package ru.soune.nocopy.service.auth; package ru.soune.nocopy.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
@@ -29,6 +29,8 @@ public class AuthService {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final MessageSource messageSource;
private final SecureRandom secureRandom = new SecureRandom(); private final SecureRandom secureRandom = new SecureRandom();
@Transactional @Transactional
@@ -79,25 +81,6 @@ public class AuthService {
return authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
} }
@Transactional
public Long useUserAuthToken(String token) {
if (token == null || token.isBlank()) {
throw new NotFoundAuthToken("Token is required");
}
if (token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
}
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
authToken.setLastUsedAt(LocalDateTime.now());
authTokenRepository.save(authToken);
return authToken.getUser().getId();
}
@Transactional @Transactional
public void logout(String token) { public void logout(String token) {
authTokenRepository.findByToken(token) authTokenRepository.findByToken(token)
@@ -0,0 +1,162 @@
package ru.soune.nocopy.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class YandexSearchService {
private final FileEntityRepository fileEntityRepository;
private final ObjectMapper objectMapper;
@Value("${YANDEX_API_KEY}")
private String apiKey;
@Value("${YANDEX_FOLDER_ID}")
private String folderId;
@Value("${YANDEX_SEARCH_URL}")
private String searchUrl;
@PostConstruct
public void init() {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
public YandexSearchResponse searchByFileEntity(String fileId) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> {
throw new NotValidFieldException("File not found", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId",fileId)));
});
byte[] fileBytes;
if (!isImageFile(fileEntity)) {
log.error("File not image: {}", fileEntity.getMimeType());
throw new NotValidFieldException("File not image", new BaseResponse(20007,
MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(),
Map.of("file_type", fileEntity.getMimeType())));
}
try {
fileBytes = readFileFromDisk(fileEntity);
} catch (IOException e) {
throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId", fileId,
"filePath", fileEntity.getFilePath())));
}
return callYandexApi(fileBytes);
}
private boolean isImageFile(FileEntity fileEntity) {
String mimeType = fileEntity.getMimeType();
return mimeType != null && mimeType.startsWith("image");
}
private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException {
Path filePath = Path.of(fileEntity.getFilePath());
if (!Files.exists(filePath)) {
throw new IOException("File not found: " + fileEntity.getFilePath());
}
if (!Files.isReadable(filePath)) {
throw new IOException("Cannot read file: " + fileEntity.getFilePath());
}
return Files.readAllBytes(filePath);
}
private YandexSearchResponse callYandexApi(byte[] imageBytes) throws IOException {
String imageBase64 = Base64.getEncoder().encodeToString(imageBytes);
String jsonRequest = String.format("{\"folderId\":\"%s\",\"data\":\"%s\",\"page\":0,\"showSimilarImages\":true}",
folderId, imageBase64);
URL url = new URL(searchUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", "Api-Key " + apiKey);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.setConnectTimeout(30000);
connection.setReadTimeout(30000);
connection.setDoOutput(true);
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonRequest.getBytes("utf-8");
os.write(input, 0, input.length);
os.flush();
}
int responseCode = connection.getResponseCode();
String responseBody;
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(connection.getInputStream(), "utf-8"))) {
responseBody = readAll(br);
}
} else {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(connection.getErrorStream(), "utf-8"))) {
responseBody = readAll(br);
}
throw new IOException("Error Yandex API: " + responseCode + " - " + responseBody);
}
return parseJsonResponse(responseBody);
} finally {
connection.disconnect();
}
}
private String readAll(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
private YandexSearchResponse parseJsonResponse(String json) {
YandexSearchResponse response = null;
try {
response = objectMapper.readValue(json, YandexSearchResponse.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return response;
}
}
@@ -1,38 +0,0 @@
package ru.soune.nocopy.service.auth;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class CleanupTokenSessionsService {
@Autowired
private AuthTokenRepository authTokenRepository;
@Value("${file.storage.auth-token-life-hours}")
private int authTokenLife;
@Transactional
@Scheduled(fixedDelay = 30000)
public void cleanupExpiredFiles() {
LocalDateTime hourBefore = LocalDateTime.now().minusHours(authTokenLife);
List<AuthToken> tokensForDelete = authTokenRepository.findByLastUsedAtBefore(hourBefore).stream().toList();
List<AuthToken> tokensDeleteWithNullUpdate = authTokenRepository.findByLastUsedAtAndExpiresAtBefore(
null, hourBefore).stream().toList();
authTokenRepository.deleteAll(tokensForDelete);
authTokenRepository.deleteAll(tokensDeleteWithNullUpdate);
}
}
@@ -0,0 +1,93 @@
package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.FileInfoUserResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
public class FileStatsService {
private final FileEntityRepository fileEntityRepository;
public FileInfoUserResponse getUserFileStats(Long userId) {
List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId);
return calculateStats(userFiles);
}
public FileInfoUserResponse calculateStats(List<FileEntity> files) {
return FileInfoUserResponse.builder()
.allFileSize(calculateTotalSize(files))
.fileCount(calculateTotalCount(files))
.filesCheck(calculateByStatus(files, FileStatus.CHECKED))
.filesViolation(calculateByStatus(files, FileStatus.VIOLATION))
.imagesSize(calculateMediaSize(files, FileType.IMAGE))
.imagesCount(calculateMediaCount(files, FileType.IMAGE))
.imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED))
.imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION))
.videosSize(calculateMediaSize(files, FileType.VIDEO))
.videosCount(calculateMediaCount(files, FileType.VIDEO))
.videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED))
.videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION))
.audiosSize(calculateMediaSize(files, FileType.AUDIO))
.audiosCount(calculateMediaCount(files, FileType.AUDIO))
.audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED))
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
.build();
}
private Long calculateTotalSize(List<FileEntity> files) {
return files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED)
.mapToLong(FileEntity::getFileSize)
.sum();
}
private Integer calculateTotalCount(List<FileEntity> files) {
return (int) files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED)
.count();
}
private Integer calculateByStatus(List<FileEntity> files, FileStatus status) {
return (int) files.stream()
.filter(file -> file.getStatus() == status)
.count();
}
private Long calculateMediaSize(List<FileEntity> files, FileType fileType) {
return files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED)
.filter(file -> isFileType(file, fileType))
.mapToLong(FileEntity::getFileSize)
.sum();
}
private Integer calculateMediaCount(List<FileEntity> files, FileType fileType) {
return (int) files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED)
.filter(file -> isFileType(file, fileType))
.count();
}
private Integer calculateMediaByStatus(List<FileEntity> files, FileType fileType, FileStatus status) {
return (int) files.stream()
.filter(file -> isFileType(file, fileType))
.filter(file -> file.getStatus() == status)
.count();
}
private boolean isFileType(FileEntity file, FileType fileType) {
if (file.getFileExtension() == null) return false;
String extension = file.getFileExtension().toLowerCase().replace(".", "");
return fileType.supportsExtension(extension);
}
}
@@ -102,7 +102,7 @@ public class FileUploadServiceImpl implements FileUploadService {
int totalChunks = (int) Math.ceil((double) fileSize / chunkSize); int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
log.debug("File will be split into {} chunks (chunk size: {} bytes)", log.debug("File will be split into {} chunks (chunk size: {} bytes)",
totalChunks, chunkSize); totalChunks, chunkSize);
Long chunkSize = totalChunks == 1 ? fileSize : 1048576L; Long chunkSize = totalChunks == 1 ? fileSize : 1000000L;
FileUploadSession session = FileUploadSession.builder() FileUploadSession session = FileUploadSession.builder()
.userId(userId) .userId(userId)
+2 -2
View File
@@ -25,12 +25,12 @@ file:
storage: storage:
base-path: ${FILE_STORAGE_PATH:/data/uploads} base-path: ${FILE_STORAGE_PATH:/data/uploads}
# chunk-size: ${FILE_CHUNK_SIZE:5242880} # 5MB # chunk-size: ${FILE_CHUNK_SIZE:5242880} # 5MB
chunk-size: ${FILE_CHUNK_SIZE:1048576} # 15MB # chunk-size: ${FILE_CHUNK_SIZE:1048576} # 1MB
chunk-size: ${FILE_CHUNK_SIZE:1000000} # 1MB
max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB
max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3} max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3}
chunk-timeout-ms: ${CHUNK_TIMEOUT_MS:300000} # 5 минут chunk-timeout-ms: ${CHUNK_TIMEOUT_MS:300000} # 5 минут
temp-ttl-hours: ${TEMP_TTL_HOURS:72} # 3 дня temp-ttl-hours: ${TEMP_TTL_HOURS:72} # 3 дня
auth-token-life-hours: ${AUTH_TOKEN_LIFE:1} # 1 час
session-expiry-hours: ${SESSION_EXPIRY_HOURS:24} session-expiry-hours: ${SESSION_EXPIRY_HOURS:24}
security: security:
@@ -15,7 +15,6 @@ import ru.soune.nocopy.exception.NotValidationPasswordException;
import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService;
import java.util.Optional; import java.util.Optional;