49 Commits
Author SHA1 Message Date
vladp daef30ec11 NCBACK-35 add worked logic for save protected files
Test Workflow / test (push) Successful in 4s
2026-02-16 16:05:15 +07:00
vladp a25b33fe13 NCBACK-35 add worked logic for save protected files
Test Workflow / test (push) Successful in 4s
2026-02-16 15:04:58 +07:00
vladp 2985d84b04 NCBACK-35 add cloud for save when download last chunk and after protected
Test Workflow / test (push) Successful in 3s
2026-02-13 21:11:30 +07:00
vladp 03a330b570 NCBACK-46 fix file name
Test Workflow / test (push) Successful in 3s
2026-02-12 16:03:39 +07:00
vladp b372180f83 NCBACK-42 add dadata handler
Test Workflow / test (push) Successful in 3s
2026-02-12 15:36:45 +07:00
vladp 920fc67962 dev only unige result
Test Workflow / test (push) Successful in 3s
2026-02-12 12:07:41 +07:00
vladp fb2ac0cf03 dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-12 11:58:05 +07:00
vladp 8a2be0d310 dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-11 19:50:07 +07:00
vladp 0743000c10 dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-11 19:15:16 +07:00
vladp 640deef98d dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-11 18:47:58 +07:00
vladp 28b5a64a33 dev change to visual_matches engine
Test Workflow / test (push) Successful in 4s
2026-02-11 18:28:57 +07:00
vladp 6818b74307 dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-11 18:12:44 +07:00
vladp 6b73d01aeb dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-11 17:59:41 +07:00
vladp 1cb9edd5bf dev change to visual_matches engine
Test Workflow / test (push) Successful in 4s
2026-02-11 17:53:02 +07:00
vladp 081ca05f88 dev change to visual_matches engine
Test Workflow / test (push) Successful in 3s
2026-02-11 17:41:48 +07:00
vladp 970e0fa615 dev change to visual_matches engine
Test Workflow / test (push) Successful in 4s
2026-02-11 16:24:19 +07:00
vladp 52e1a7dd02 dev add pdf docs protect
Test Workflow / test (push) Successful in 3s
2026-02-10 20:50:06 +07:00
vladp 19330d853d dev add pdf docs protect
Test Workflow / test (push) Successful in 3s
2026-02-10 20:40:55 +07:00
vladp 1472c342f5 dev add pdf docs protect
Test Workflow / test (push) Successful in 3s
2026-02-10 19:14:16 +07:00
vladp 1ced86ff8c dev add pdf docs protect
Test Workflow / test (push) Successful in 3s
2026-02-10 18:55:21 +07:00
vladp 8149c450b2 dev add pdf docs protect
Test Workflow / test (push) Successful in 4s
2026-02-10 18:38:46 +07:00
vladp fea2d5ad89 dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 16:23:49 +07:00
vladp 2ef1619ef9 dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 16:10:05 +07:00
vladp 266bcaefcd dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 15:55:20 +07:00
vladp bf48725244 dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 13:47:08 +07:00
vladp 86f7fc593a dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 13:26:44 +07:00
vladp 90be1d9d2b dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 13:03:19 +07:00
vladp 9692fe48b7 dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 12:52:36 +07:00
vladp d50c565405 dev add utf encoding
Test Workflow / test (push) Successful in 4s
2026-02-10 12:46:45 +07:00
vladp 98fc145f1b dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 12:36:52 +07:00
vladp e7e870ee30 dev add utf encoding
Test Workflow / test (push) Successful in 3s
2026-02-10 12:33:07 +07:00
vladp 8103960b45 dev add pagination for search in internet images
Test Workflow / test (push) Successful in 3s
2026-02-10 12:16:31 +07:00
vladp 792e0048cc dev add pagination for search in internet images
Test Workflow / test (push) Successful in 4s
2026-02-10 11:58:32 +07:00
vladp 33883faa98 dev split tokens
Test Workflow / test (push) Successful in 2s
2026-02-09 17:25:06 +07:00
vladp 7cc744829b dev split tariff by account types
Test Workflow / test (push) Successful in 3s
2026-02-09 17:16:08 +07:00
vladp 5a70c091f8 dev
Test Workflow / test (push) Successful in 5s
2026-02-09 15:29:55 +07:00
vladp 69aabe3619 dev
Test Workflow / test (push) Successful in 3s
2026-02-09 15:20:42 +07:00
vladp 27a8766171 dev fix name for parameters
Test Workflow / test (push) Successful in 2s
2026-02-09 14:15:29 +07:00
vladp 2bba70e583 dev fix name for parameters
Test Workflow / test (push) Successful in 3s
2026-02-09 13:50:42 +07:00
vladp 8e8193cda6 dev company users saw all company files
Test Workflow / test (push) Successful in 6s
2026-02-09 12:20:19 +07:00
vladp b96b50c01f add referal only for b2c
Test Workflow / test (push) Successful in 3s
2026-02-09 11:28:23 +07:00
vladp e926934473 add add sql scripts
Test Workflow / test (push) Successful in 3s
2026-02-09 11:24:34 +07:00
vladp a5003958a8 add set user limits
Test Workflow / test (push) Successful in 3s
2026-02-09 11:06:50 +07:00
vladp 5b73fd2c39 add document
Test Workflow / test (push) Has been cancelled
2026-02-07 11:54:34 +07:00
vladp daadc82bbb add document
Test Workflow / test (push) Successful in 3s
2026-02-06 22:21:51 +07:00
vladp 682c21ba73 fix value
Test Workflow / test (push) Successful in 2s
2026-02-06 21:22:47 +07:00
vladp 1c7a33be32 fix value
Test Workflow / test (push) Successful in 4s
2026-02-06 20:43:27 +07:00
vladp aceb70484f fix value
Test Workflow / test (push) Successful in 3s
2026-02-06 20:18:43 +07:00
backdev 3fc83e5458 Merge pull request 'NCBACK-36_1' (#19) from NCBACK-36_1 into dev
Test Workflow / test (push) Successful in 3s
Reviewed-on: #19
2026-02-06 20:27:56 +08:00
60 changed files with 1226 additions and 565 deletions
+3
View File
@@ -14,6 +14,9 @@ COPY --from=build /app/build/libs/*.jar app.jar
RUN mkdir -p /data/uploads && chmod 755 /data/uploads RUN mkdir -p /data/uploads && chmod 755 /data/uploads
ENV BUILD_TIME_BACK="unknown"\
BUILD_TIME_FRONT="unknown"
EXPOSE 8080 EXPOSE 8080
CMD ["java", "-jar", "app.jar"] CMD ["java", "-jar", "app.jar"]
+39
View File
@@ -122,3 +122,42 @@ SELECT 1
FROM protect_check pc FROM protect_check pc
WHERE pc.user_id = u.id WHERE pc.user_id = u.id
); );
------
Скрипты рефералки
INSERT INTO referrals (user_id, referral_link, inviter_id, level_id, total_income, is_active, created_at, hold_balance)
SELECT
u.id as user_id,
CONCAT('ref-', u.id, '-', LOWER(SUBSTRING(MD5(RANDOM()::text) FROM 1 FOR 8))) as referral_link,
NULL as inviter_id,
'bronze' as level_id,
0 as total_income,
false as is_active,
NOW() as created_at,
0 as hold_balance
FROM users u
LEFT JOIN referrals r ON u.id = r.user_id
WHERE
u.company_id IS NULL
AND r.user_id IS NULL
ORDER BY u.id;
-------
INSERT INTO referral_levels (id, name, min_invitees, reward_percentage) VALUES
('bronze', 'BRONZE', 0, 15),
('silver', 'SILVER', 6, 18),
('gold', 'GOLD', 16, 22),
('platinum', 'PLATINUM', 50, 25);
------
Настройка Яндекс Клауд
1. YCAJEpWAtaVkVGX0sH6_EupEg - индетификатор ключа
2. YCMmVykfXrZ_nfU13Vo4yoCVGa70DnTlBgF1pUzO - секретный ключ
private static final String KEY_ID = "YCAJEmHZcWxzf4oGWvETG3mms";
private static final String SECRET_KEY = "YCPAPZ32XC-LWb8xE0b_xTcBD8NZUCHpSOfu8LUG";
+8 -2
View File
@@ -34,7 +34,7 @@ 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 group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '4.0.1' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '4.0.1'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.2'
implementation group: 'com.google.cloud', name: 'google-cloud-vision', version: '3.55.0' implementation group: 'com.google.cloud', name: 'google-cloud-vision', version: '3.55.0'
implementation group: 'com.google.api-client', name: 'google-api-client', version: '2.7.2' implementation group: 'com.google.api-client', name: 'google-api-client', version: '2.7.2'
@@ -59,12 +59,18 @@ dependencies {
testImplementation 'org.mockito:mockito-core:5.3.1' testImplementation 'org.mockito:mockito-core:5.3.1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation name: 'testlib-fat-0.2.1-all' implementation name: 'testlib-fat-0.3.0-all'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0' implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0'
implementation project(':referral') implementation project(':referral')
//cloud
implementation("software.amazon.awssdk:aws-sdk-java:2.29.33")
implementation("software.amazon.awssdk:apache-client:2.29.33")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("org.springframework.boot:spring-boot-starter-web")
} }
tasks.named('test') { tasks.named('test') {
+1 -1
View File
@@ -181,7 +181,7 @@ services:
command: command:
- '--config.file=/etc/prometheus/prometheus.yml' - '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus' - '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d' # Удерживать 15 дней - '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle' - '--web.enable-lifecycle'
healthcheck: healthcheck:
test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ] test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ]
@@ -39,6 +39,7 @@ pipeline {
stage('Deploy with docker-compose') { stage('Deploy with docker-compose') {
steps { steps {
script { script {
def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim()
withCredentials([ withCredentials([
usernamePassword( usernamePassword(
credentialsId: 'server-root-password', credentialsId: 'server-root-password',
@@ -85,6 +86,7 @@ pipeline {
-e POSTGRES_USER=${DB_USER} \\ -e POSTGRES_USER=${DB_USER} \\
-e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -e POSTGRES_PASSWORD=${DB_PASSWORD} \\
-v pgdata_${params.PORT}:/var/lib/postgresql/data \\ -v pgdata_${params.PORT}:/var/lib/postgresql/data \\
-e BUILD_TIME_BACK='${buildTime}' \\
postgres:17.7 postgres:17.7
echo '4. Запускаем storage...' echo '4. Запускаем storage...'
+13 -14
View File
@@ -85,6 +85,8 @@ pipeline {
stage('Build on server') { stage('Build on server') {
steps { steps {
script { script {
def BUILD_TIME_FRONT = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim()
withCredentials([ withCredentials([
usernamePassword( usernamePassword(
credentialsId: 'server-root-password', credentialsId: 'server-root-password',
@@ -93,23 +95,20 @@ pipeline {
) )
]) { ]) {
sh """ sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
cd /opt/deployments/frontend/${params.BRANCH}-${params.PORT} cd /opt/deployments/frontend/${params.BRANCH}-${params.PORT}
echo '=== Редактируем docker-compose.yml ===' # 1. СОЗДАЕМ JSON ФАЙЛ (ЭТО РАБОТАЕТ 100%)
mkdir -p public
echo '{\\"buildTime\\": \\"${BUILD_TIME_FRONT}\\"}' > public/build-info.json
# Меняем имя контейнера # 2. Меняем имя контейнера и порт
sed -i 's/container_name: no-copy-frontend\$/container_name: no-copy-frontend-${params.PORT}/' docker-compose.yml sed -i 's/container_name: no-copy-frontend\$/container_name: no-copy-frontend-${params.PORT}/' docker-compose.yml
sed -i 's/\\\"2998:2999\\\"/\\\"${params.PORT}:2999\\\"/' docker-compose.yml
# Меняем порт (2998 на нужный порт) # 3. Собираем образ
sed -i 's/\\\"2998:2999\\\"/\\\"${params.PORT}:2999\\\"/' docker-compose.yml docker-compose -f docker-compose.yml -p frontend-${params.PORT} build
"
echo 'Проверяем изменения:'
grep -n 'container_name\\|ports' docker-compose.yml
echo '=== Строим образ ==='
docker-compose -f docker-compose.yml -p frontend-${params.PORT} build
"
""" """
} }
} }
+2
View File
@@ -34,6 +34,7 @@ pipeline {
stage('Deploy with docker-compose') { stage('Deploy with docker-compose') {
steps { steps {
script { script {
def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim()
withCredentials([ withCredentials([
usernamePassword( usernamePassword(
credentialsId: 'server-root-password', credentialsId: 'server-root-password',
@@ -92,6 +93,7 @@ pipeline {
-e POSTGRES_PASSWORD=$DB_PASSWORD \\ -e POSTGRES_PASSWORD=$DB_PASSWORD \\
-e POSTGRES_PORT=5432 \\ -e POSTGRES_PORT=5432 \\
-e POSTGRES_HOST=db \\ -e POSTGRES_HOST=db \\
-e BUILD_TIME_BACK='${buildTime}' \\
--restart unless-stopped \\ --restart unless-stopped \\
app-backend:latest app-backend:latest
@@ -3,9 +3,9 @@ package ru.soune.nocopy.configuration;
import com.vrt.AudioFilePathProvider; import com.vrt.AudioFilePathProvider;
import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.audio.AudioLocalSearch; import com.vrt.fileprotection.audio.AudioLocalSearch;
import com.vrt.fileprotection.documents.DocumentLocalSearch;
import com.vrt.fileprotection.image.ImageLocalSearch; import com.vrt.fileprotection.image.ImageLocalSearch;
import com.vrt.fileprotection.image.ImageUniqueCheck; import com.vrt.fileprotection.image.ImageUniqueCheck;
import com.vrt.fileprotection.image.phash.PerceptualHashHelper;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -33,7 +33,8 @@ public class ApplicationConfig {
ImageUniqueCheck imageUniqueCheck, ImageUniqueCheck imageUniqueCheck,
ImageLocalSearch imageLocalSearch, ImageLocalSearch imageLocalSearch,
AudioLocalSearch audioLocalSearch, AudioLocalSearch audioLocalSearch,
AudioFilePathProvider audioFilePathProvider) { AudioFilePathProvider audioFilePathProvider,
DocumentLocalSearch documentLocalSearch) {
return new com.vrt.NoCopyFileService( return new com.vrt.NoCopyFileService(
Collections.emptyList(), Collections.emptyList(),
@@ -42,7 +43,8 @@ public class ApplicationConfig {
imageUniqueCheck, imageUniqueCheck,
imageLocalSearch, imageLocalSearch,
audioLocalSearch, audioLocalSearch,
audioFilePathProvider audioFilePathProvider,
documentLocalSearch
); );
} }
} }
@@ -23,7 +23,8 @@ public class HandlerConfig {
CompanyHandler companyHandler, CompanyHandler companyHandler,
TariffHandler tariffHandler, TariffHandler tariffHandler,
TariffInfoHandler tariffInfoHandler, TariffInfoHandler tariffInfoHandler,
ReferralHandler referralHandler ReferralHandler referralHandler,
DaDataHandler daDataHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -38,6 +39,7 @@ public class HandlerConfig {
map.put(30001, tariffHandler); map.put(30001, tariffHandler);
map.put(30002, tariffInfoHandler); map.put(30002, tariffInfoHandler);
map.put(30003, referralHandler); map.put(30003, referralHandler);
map.put(30004, daDataHandler);
return map; return map;
} }
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -20,7 +21,13 @@ public class JacksonConfig {
@Bean @Bean
@Primary @Primary
public ObjectMapper objectMapper() { public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = Jackson2ObjectMapperBuilder.json()
.defaultViewInclusion(true)
.autoDetectFields(true)
.failOnUnknownProperties(false)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
.build();
JavaTimeModule javaTimeModule = new JavaTimeModule(); JavaTimeModule javaTimeModule = new JavaTimeModule();
@@ -32,6 +32,7 @@ import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.file.ProtectionsLimitService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
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;
@@ -73,6 +74,8 @@ public class ApiController {
private final AuthTokenRepository authTokenRepository; private final AuthTokenRepository authTokenRepository;
private final CloudStorageService cloudStorageService;
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@PathVariable("version") int version) { @PathVariable("version") int version) {
@@ -342,15 +345,14 @@ public class ApiController {
errorData)); errorData));
} }
Path filePath = Paths.get(entityResponse.getProtectedFilePath()); Path filePath = Paths.get(entityResponse.getProtectedFilePath());
// Resource resource = new UrlResource(filePath.toUri()); File file = cloudStorageService.readFileFromStorageByPath(
FileSystemResource resource = new FileSystemResource(filePath); entityResponse.getProtectedFilePath());
long fileSize = Files.size(filePath); long fileSize = Files.size(filePath);
if (!resource.exists()) { if (!file.exists()) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
errorData.put("resource", resource.exists()); errorData.put("file", file.exists());
return ResponseEntity.ok().body(new BaseResponse(20004, return ResponseEntity.ok().body(new BaseResponse(20004,
MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getCode(),
@@ -364,8 +366,8 @@ public class ApiController {
.contentLength(fileSize) .contentLength(fileSize)
.contentType(MediaType.parseMediaType(contentType)) .contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + entityResponse.getOriginalFileName() + "\"") "attachment; filename=\"" + entityResponse.getFileName() + "\"")
.body(resource); .body(file);
} catch (FileEntityNotFoundException e) { } catch (FileEntityNotFoundException e) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
errorData.put("fileId", fileId); errorData.put("fileId", fileId);
@@ -439,33 +441,6 @@ public class ApiController {
return ResponseEntity.ok().body(fileTypeStats); return ResponseEntity.ok().body(fileTypeStats);
} }
private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) {
return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5);
}
private ResponseEntity<BaseResponse> handleDuplicate(FileEntity fileEntity, List<SimilarFileDTO> similarFiles)
throws IOException {
fileEntityService.deleteFromDisk(fileEntity);
fileEntityService.softDeleteFileWithHash(fileEntity);
Optional<FileEntity> originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId());
if (originalFile.isPresent()) {
Map<String, String> duplicateInfo = Map.of(
"duplicate_file_id", originalFile.get().getId(),
"owner_user_id", String.valueOf(originalFile.get().getUserId()));
return ResponseEntity.ok().body(new BaseResponse(
20004,
MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
"Failed to upload chunk, duplicate",
duplicateInfo));
}
return null;
}
private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk, private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk,
String fileId) { String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder() ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
@@ -16,6 +16,8 @@ import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.file.FileStorageService; import ru.soune.nocopy.service.file.FileStorageService;
import org.springframework.http.ContentDisposition;
import java.nio.charset.StandardCharsets;
import java.io.IOException; import java.io.IOException;
@@ -45,10 +47,13 @@ public class FileController {
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
ContentDisposition contentDisposition = ContentDisposition.inline()
.filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8)
.build();
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString())
"inline; filename=\"" + fileEntity.getOriginalFileName() + "\"")
.body(resource); .body(resource);
} }
@@ -1,5 +1,6 @@
package ru.soune.nocopy.controller; package ru.soune.nocopy.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -15,6 +16,14 @@ public class HealtCheckController {
return HttpStatus.OK; return HttpStatus.OK;
} }
@Value("${BUILD_TIME_BACK:unknown}")
private String buildTimeBack;
@GetMapping("/build")
public BuildInfo getBuildInfo() {
return new BuildInfo(buildTimeBack);
}
@GetMapping("/api/debug/memory") @GetMapping("/api/debug/memory")
public Map<String, String> getMemoryInfo() { public Map<String, String> getMemoryInfo() {
Runtime runtime = Runtime.getRuntime(); Runtime runtime = Runtime.getRuntime();
@@ -32,4 +41,12 @@ public class HealtCheckController {
return info; return info;
} }
static class BuildInfo {
public String buildTimeBack;
public BuildInfo(String buildTimeBack) {
this.buildTimeBack = buildTimeBack;
}
}
} }
@@ -85,7 +85,7 @@ public class UserController {
} }
if (user.canManageCompanySettings() || user.canLogin() && user.getCompany() == null) { if (user.canManageCompanySettings() || user.canLogin() && user.getCompany() == null) {
userDTO.setTariffs(tariffService.getAllTariffs()); userDTO.setTariffs(tariffService.getTariffByAccountType(user));
Long fileOnDisk = user.canManageCompanySettings() ? Long fileOnDisk = user.canManageCompanySettings() ?
fileStatsService.calculateCompanyFileSize(user.getId()): fileStatsService.calculateCompanyFileSize(user.getId()):
@@ -116,6 +116,7 @@ public class UserController {
.maxFileOnDisk(tariff.getDiskSize()) .maxFileOnDisk(tariff.getDiskSize())
.startTariff(personalTariffInfo.getStartTariff()) .startTariff(personalTariffInfo.getStartTariff())
.endTariff(personalTariffInfo.getEndTariff()) .endTariff(personalTariffInfo.getEndTariff())
.tokens(personalTariffInfo.getTokens() + personalTariffInfo.getBoughtTokens())
.build(); .build();
userDTO.setTariffInfo(infoDTO); userDTO.setTariffInfo(infoDTO);
@@ -4,6 +4,7 @@ public enum MessageCode {
SUCCESS(0, "Operation successful"), SUCCESS(0, "Operation successful"),
REG_EMAIL_EXISTS(1, "Email already registered"), REG_EMAIL_EXISTS(1, "Email already registered"),
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"),
INVALID_FIELD(2, "Invalid field"), INVALID_FIELD(2, "Invalid field"),
INVALID_TOKEN(2, "Invalid token"), INVALID_TOKEN(2, "Invalid token"),
TOKEN_IS_ALIVE(2, "Token is alive"), TOKEN_IS_ALIVE(2, "Token is alive"),
@@ -40,6 +41,7 @@ public enum MessageCode {
INTERNAL_ERROR(4, "Internal server error"), INTERNAL_ERROR(4, "Internal server error"),
COMPANY_NOT_FOUND(4, "Company not found"), COMPANY_NOT_FOUND(4, "Company not found"),
COMPANY_ALREADY_EXISTS(2, "Company already exists"), COMPANY_ALREADY_EXISTS(2, "Company already exists"),
USER_LIMIT_IS_OVER(2, "Over user limits"),
ERROR_TARIFF_INFO(2, "Erorr with tariff info"); ERROR_TARIFF_INFO(2, "Erorr with tariff info");
private final Integer code; private final Integer code;
@@ -0,0 +1,10 @@
package ru.soune.nocopy.dto.dadata;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DaDataAddress {
private String value;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.dadata;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DaDataData {
private String inn;
private DaDataAddress address;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.dadata;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class DaDataRequest {
@JsonProperty("inn")
private String inn;
}
@@ -0,0 +1,16 @@
package ru.soune.nocopy.dto.dadata;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class DaDataResponse {
@JsonProperty("companyName")
private String companyName;
@JsonProperty("inn")
private String inn;
@JsonProperty("address")
private String address;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.dadata;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DaDataSuggestion {
private String value;
private DaDataData data;
}
@@ -0,0 +1,12 @@
package ru.soune.nocopy.dto.dadata;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DaDataWrapper {
private List<DaDataSuggestion> suggestions;
}
@@ -59,6 +59,21 @@ public class FileInfoUserResponse {
@JsonProperty("audios_violations") @JsonProperty("audios_violations")
private Integer audiosViolations; private Integer audiosViolations;
@JsonProperty("document_size")
private Long documentSize;
@JsonProperty("document_quantity")
private Integer documentCount;
@JsonProperty("document_check")
private Integer documentCheck;
@JsonProperty("document_violations")
private Integer documentViolations;
@JsonProperty("protected_document_files_count")
private Long protectedDocumentFilesCount;
@JsonProperty("protected_files_count") @JsonProperty("protected_files_count")
private Long protectedFilesCount; private Long protectedFilesCount;
@@ -7,4 +7,10 @@ import lombok.Data;
public class ImageSearchRequest { public class ImageSearchRequest {
@JsonProperty("file_id") @JsonProperty("file_id")
private String fileId; private String fileId;
@JsonProperty("page")
private Integer page = 1;
@JsonProperty("page_size")
private Integer pageSize = 10;
} }
@@ -12,14 +12,10 @@ public class YandexSearchResponse {
@JsonProperty("images") @JsonProperty("images")
private List<ImageResult> images; private List<ImageResult> images;
@JsonProperty("images") // @JsonProperty("images")
public void setImages(List<ImageResult> images) { // public void setImages(List<ImageResult> images) {
if (images != null && images.size() > 5) { // this.images = images;
this.images = images.subList(0, 5); // }
} else {
this.images = images;
}
}
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@@ -42,4 +38,12 @@ public class YandexSearchResponse {
@JsonProperty("host") @JsonProperty("host")
private String host; private String host;
} }
private int page;
private int pageSize;
private int totalResults;
private int totalPages;
} }
@@ -46,4 +46,7 @@ public class TariffRequest {
@JsonProperty("action") @JsonProperty("action")
private String action; private String action;
@JsonProperty("user_token")
private String userToken;
} }
@@ -7,11 +7,12 @@ import java.util.List;
@Getter @Getter
public enum FileType { public enum FileType {
// IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "jfif")),
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")), IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")),
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", List.of("wav")); AUDIO("audio", List.of("wav")),
DOCUMENT("document", Arrays.asList("pdf"));
private final String displayName; private final String displayName;
private final List<String> allowedExtensions; private final List<String> allowedExtensions;
@@ -17,7 +17,7 @@ public class Referral {
private String referralLink; private String referralLink;
@Column(name = "inviter_id") @Column(name = "inviter_id")
private Long inviterId; private Long inviterId = 0L;
@Column(name = "level_id") @Column(name = "level_id")
private String levelId = "bronze"; private String levelId = "bronze";
@@ -37,4 +37,7 @@ public class Tariff {
@Column(name = "max_users") @Column(name = "max_users")
private Long maxUsers; private Long maxUsers;
@Column(name = "tariffAccountType")
private String tariffAccountType;
} }
@@ -31,6 +31,9 @@ public class TariffInfo {
@Column(name = "tokens") @Column(name = "tokens")
private Integer tokens; private Integer tokens;
@Column(name = "bought_tokens")
private Integer boughtTokens = 0;
@ManyToOne @ManyToOne
@JoinColumn(name = "tariff_id") @JoinColumn(name = "tariff_id")
private Tariff tariff; private Tariff tariff;
@@ -2,5 +2,5 @@ package ru.soune.nocopy.entity.tarif;
public enum TariffType { public enum TariffType {
START, BASIC, PRO, ENTERPRISE, DEMO; START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE;
} }
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class MaxUserCountIsOver extends RuntimeException {
public MaxUserCountIsOver(String message) {
super(message);
}
}
@@ -0,0 +1,32 @@
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.dadata.DaDataRequest;
import ru.soune.nocopy.dto.dadata.DaDataResponse;
import ru.soune.nocopy.service.dadata.DaDataService;
@Component
@RequiredArgsConstructor
@Slf4j
public class DaDataHandler implements RequestHandler {
private final DaDataService daDataService;
private ObjectMapper mapper = new ObjectMapper();
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
DaDataRequest daDataRequest = mapper.convertValue(request.getMessageBody(), DaDataRequest.class);
String inn = daDataRequest.getInn();
DaDataResponse daData = daDataService.callDaData(inn);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), daData);
}
}
@@ -12,6 +12,7 @@ import ru.soune.nocopy.entity.file.ProtectionStatus;
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.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService; import ru.soune.nocopy.service.file.FileStatsService;
@@ -35,6 +36,8 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
@Override @Override
public BaseResponse handle(BaseRequest request) { public BaseResponse handle(BaseRequest request) {
try { try {
@@ -302,7 +305,7 @@ public class FileEntityHandler implements RequestHandler {
int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete(); int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete();
if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) { if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) {
fileEntityService.deleteFromDisk(fileEntity); cloudStorageService.deleteFromStorageDisk(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
.message("File deleted from disk") .message("File deleted from disk")
@@ -1,5 +1,6 @@
package ru.soune.nocopy.handler; package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -8,17 +9,19 @@ import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.search.SearchApiToYandexResponseMapper; import ru.soune.nocopy.service.search.SearchImageService;
import ru.soune.nocopy.service.search.YandexSearchService;
import ru.soune.nocopy.service.search.GoogleVisionSearchService; import ru.soune.nocopy.service.search.GoogleVisionSearchService;
import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.util.Map; import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeoutException;
@Slf4j @Slf4j
@Component @Component
@@ -27,7 +30,7 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final YandexSearchService yandexSearchService; private final SearchImageService searchImageService;
private final GoogleVisionSearchService googleVisionSearchService; private final GoogleVisionSearchService googleVisionSearchService;
@@ -37,8 +40,6 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final TariffInfoService tariffInfoService; private final TariffInfoService tariffInfoService;
private final SearchApiToYandexResponseMapper mapper = new SearchApiToYandexResponseMapper();
@Override @Override
public BaseResponse handle(BaseRequest request) throws Exception { public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
@@ -58,13 +59,54 @@ public class ImageFoundRequestHandler implements RequestHandler {
//TODO uncommited when add billing //TODO uncommited when add billing
// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); // GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId);
String yandexReverseSearchResponse = yandexSearchService.searchReverseByPublicUrl(fileEntity); String searchResponseYandex;
String searchResponseGoogle;
try {
searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image",
"visual_matches");
searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens",
"exact_matches");
} catch (TimeoutException e) {
log.warn("Search timeout for file {}, returning empty results", fileId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
"Search completed (partial)", Map.of(
"results", List.of(),
"total", 0,
"searchStatus", "timeout"
));
} catch (IOException e) {
log.error("Search failed for file {}", fileId, e);
return new BaseResponse(request.getMsgId(), 20007,
"Search service temporarily unavailable",
Map.of("fileId", fileId));
}
tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH);
checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType());
List<YandexSearchResponse.ImageResult> allYandexImages = searchImageService.getAllImagesWithoutPagination(
searchResponseYandex, "visual_matches");
List<YandexSearchResponse.ImageResult> allGoogleImages = searchImageService.getAllImagesWithoutPagination(
searchResponseGoogle, "exact_matches");
List<YandexSearchResponse.ImageResult> allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages,
allGoogleImages);
int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1;
int pageSize = 5;
List<YandexSearchResponse.ImageResult> pagedResults = searchImageService.paginateResults(allUniqueImages, page,
pageSize * 2);
YandexSearchResponse finalResponse = new YandexSearchResponse();
finalResponse.setImages(pagedResults);
finalResponse.setPage(page);
finalResponse.setPageSize(pageSize * 2);
finalResponse.setTotalResults(allUniqueImages.size());
finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2)));
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),mapper.mapToYandexResponse(yandexReverseSearchResponse)); MessageCode.SUCCESS.getDescription(), finalResponse);
} }
} }
@@ -10,10 +10,12 @@ import ru.soune.ReferralService;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.referral.Referral;
import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.exception.NotValidFieldException; 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.ReferralJpaRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.mail.EmailService;
@@ -37,6 +39,8 @@ public class RegRequestHandler implements RequestHandler {
private final ReferralService referralService; private final ReferralService referralService;
private final ReferralJpaRepository referralJpaRepository;
@Override @Override
public BaseResponse handle(BaseRequest request) throws ValidationException { public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -52,6 +56,19 @@ public class RegRequestHandler implements RequestHandler {
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer)); MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer));
} }
String referralLink = regRequest.getReferralLink();
if (referralLink != null) {
Referral referral = referralJpaRepository.findByReferralLink(referralLink);
if (referral == null) {
throw new NotValidFieldException("Referral link is not found:" + referralLink +
referralLink, new BaseResponse(request.getMsgId(),
MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(),
MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of(
"referralLink", referralLink)));
}
}
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
regRequestValidator.validate(regRequest, bindingResult); regRequestValidator.validate(regRequest, bindingResult);
@@ -79,13 +96,14 @@ public class RegRequestHandler implements RequestHandler {
RegAnswer regAnswer = new RegAnswer(); RegAnswer regAnswer = new RegAnswer();
regAnswer.setUserId(authToken.getUser().getId()); regAnswer.setUserId(authToken.getUser().getId());
regAnswer.setToken(token); regAnswer.setToken(token);
regAnswer.setVerified(true); regAnswer.setVerified(true);
regAnswer.setActive(true); regAnswer.setActive(true);
referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); if (regRequest.getAccountType().equals("b2c")) {
referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink());
}
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer); MessageCode.SUCCESS.getDescription(), regAnswer);
@@ -10,6 +10,9 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.dto.tarriff.TariffRequest; import ru.soune.nocopy.dto.tarriff.TariffRequest;
import ru.soune.nocopy.dto.tarriff.TariffResponse; import ru.soune.nocopy.dto.tarriff.TariffResponse;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.tariff.TariffService; import ru.soune.nocopy.service.tariff.TariffService;
import java.util.List; import java.util.List;
@@ -24,6 +27,8 @@ public class TariffHandler implements RequestHandler {
private final TariffService tariffService; private final TariffService tariffService;
private final AuthTokenRepository authTokenRepository;
@Override @Override
public BaseResponse handle(BaseRequest request) throws Exception { public BaseResponse handle(BaseRequest request) throws Exception {
try { try {
@@ -113,7 +118,9 @@ public class TariffHandler implements RequestHandler {
} }
private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) { private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) {
List<TariffDTO> allTariffs = tariffService.getAllTariffs(); AuthToken authToken = authTokenRepository.findByToken(tariffRequest.getUserToken()).orElseThrow();
User user = authToken.getUser();
List<TariffDTO> allTariffs = tariffService.getTariffByAccountType(user);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), MessageCode.SUCCESS.getDescription(),
@@ -15,6 +15,8 @@ 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);
FileEntity findByIdAndUserId(String id, Long userId);
Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash); Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash);
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status); List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
@@ -17,7 +17,10 @@ public interface ReferralJpaRepository extends JpaRepository<Referral, Long> {
List<Referral> findByInviterId(Long inviterId); List<Referral> findByInviterId(Long inviterId);
@Query("SELECT r FROM Referral r WHERE r.inviterId = :userId AND r.isActive = true") @Query("SELECT r FROM Referral r WHERE r.inviterId = :inviterId AND r.isActive = true")
List<Referral> findByInviterIdWhenUserActivated(Long inviterId, Pageable pageable);
@Query("SELECT r FROM Referral r WHERE r.inviterId = :inviterId")
List<Referral> findByInviterId(Long inviterId, Pageable pageable); List<Referral> findByInviterId(Long inviterId, Pageable pageable);
@Modifying @Modifying
@@ -12,5 +12,6 @@ import java.util.Optional;
public interface TariffRepository extends JpaRepository<Tariff, Long> { public interface TariffRepository extends JpaRepository<Tariff, Long> {
Optional<Tariff> findByType(String type); Optional<Tariff> findByType(String type);
List<Tariff> findAllByOrderByPriceAsc(); List<Tariff> findAllByOrderByPriceAsc();
List<Tariff> findByTariffAccountType(String typeAccount);
Tariff findByName(String name); Tariff findByName(String name);
} }
@@ -0,0 +1,83 @@
package ru.soune.nocopy.service.dadata;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.dadata.DaDataResponse;
import ru.soune.nocopy.dto.dadata.DaDataSuggestion;
import ru.soune.nocopy.dto.dadata.DaDataWrapper;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class DaDataService {
@Value("${dadata.api-key}")
private String apiKey ;
@Value("${dadata.url}")
private String url ;
private final OkHttpClient httpClient;
public DaDataService() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.callTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
}
public DaDataResponse callDaData(String inn) throws IOException {
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("SearchAPI key not configured");
}
String jsonBody = String.format("{\"query\": \"%s\"}", inn);
Request request = new Request.Builder()
.url("https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party")
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonBody))
.header("Accept", "application/json")
.header("Authorization", "Token " + apiKey)
.build();
try (Response response = httpClient.newCall(request).execute()) {
ResponseBody body = response.body();
if (body == null) {
throw new IOException("Empty response body");
}
String jsonResponse = body.string();
ObjectMapper mapper = new ObjectMapper();
DaDataWrapper wrapper = mapper.readValue(jsonResponse, DaDataWrapper.class);
DaDataResponse daDataResponse = new DaDataResponse();
if (wrapper.getSuggestions() != null && !wrapper.getSuggestions().isEmpty()) {
DaDataSuggestion suggestion = wrapper.getSuggestions().get(0);
daDataResponse.setCompanyName(suggestion.getValue());
if (suggestion.getData() != null) {
daDataResponse.setInn(suggestion.getData().getInn());
if (suggestion.getData().getAddress() != null) {
daDataResponse.setAddress(suggestion.getData().getAddress().getValue());
}
}
}
return daDataResponse;
}
}
}
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import ru.soune.nocopy.entity.user.ProtectedFileCheck; import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.repository.ProtectedFileCheckRepository; import ru.soune.nocopy.repository.ProtectedFileCheckRepository;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
@@ -15,7 +16,10 @@ import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j @Slf4j
@Component @Component
@@ -35,6 +39,7 @@ public class FileCleanupService {
log.info("Starting cleanup of expired temporary files"); log.info("Starting cleanup of expired temporary files");
cleanupOldPeriods(); cleanupOldPeriods();
cleanupTempFilesNightly();
Path tempDir = Paths.get(basePath, "temp"); Path tempDir = Paths.get(basePath, "temp");
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
@@ -98,4 +103,50 @@ public class FileCleanupService {
protectedFileCheckRepository.saveAll(checks); protectedFileCheckRepository.saveAll(checks);
log.info("Cleaned up {} old protection usage records", checks.size()); log.info("Cleaned up {} old protection usage records", checks.size());
} }
private void cleanupTempFilesNightly() {
log.info("Starting nightly cleanup of temporary files");
String tempDir = System.getProperty("java.io.tmpdir");
Path tempPath = Paths.get(tempDir);
if (!Files.exists(tempPath)) {
return;
}
Instant cutoffTime = Instant.now().minus(24, ChronoUnit.HOURS); // 24 часа
int deletedCount = 0;
try (Stream<Path> files = Files.list(tempPath)) {
List<Path> tempFiles = files
.filter(path -> {
String filename = path.getFileName().toString();
return filename.contains("_") &&
!filename.endsWith(".tmp") &&
Files.isRegularFile(path);
})
.collect(Collectors.toList());
for (Path filePath : tempFiles) {
try {
File file = filePath.toFile();
Instant lastModified = Instant.ofEpochMilli(file.lastModified());
if (lastModified.isBefore(cutoffTime)) {
boolean deleted = Files.deleteIfExists(filePath);
if (deleted) {
deletedCount++;
}
}
} catch (IOException e) {
log.error("Error deleting file: {}", filePath, e);
}
}
log.info("Nightly cleanup deleted {} temporary files", deletedCount);
} catch (IOException e) {
log.error("Failed to list temp directory", e);
}
}
} }
@@ -18,13 +18,14 @@ import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -42,6 +43,8 @@ public class FileEntityService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final CloudStorageService cloudStorageService;
@Transactional(noRollbackFor = DuplicateImageException.class) @Transactional(noRollbackFor = DuplicateImageException.class)
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
log.info("Creating FileEntity for upload session: {}", session.getUploadId()); log.info("Creating FileEntity for upload session: {}", session.getUploadId());
@@ -55,7 +58,7 @@ public class FileEntityService {
Map<String, Long> imageHash = Map.of(); Map<String, Long> imageHash = Map.of();
if (session.getFileType().startsWith("image")) { if (!session.getFileType().startsWith("video")) {
imageHash = imageHashService.calculateHash(filePath); imageHash = imageHashService.calculateHash(filePath);
List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash( List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash(
imageHash.get("hi"), imageHash.get("low")); imageHash.get("hi"), imageHash.get("low"));
@@ -116,43 +119,22 @@ public class FileEntityService {
return convertToResponse(fileEntity, version); return convertToResponse(fileEntity, version);
} }
@Transactional(readOnly = true)
public FileEntityResponse getByFilePath(String filePath, int version) {
FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath)
.orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath));
return convertToResponse(fileEntity, version);
}
@Transactional(readOnly = true)
public FileResponse getAllUserFiles(Long userId, int version) {
List<FileEntity> fileEntities = fileEntityRepository.findByUserIdAndStatus(
userId, FileStatus.ACTIVE);
List<FileEntityResponse> files = fileEntities.stream()
.map(file -> convertToResponse(file, version))
.collect(Collectors.toList());
long totalSize = fileEntities.stream()
.mapToLong(FileEntity::getFileSize)
.sum();
return FileResponse.builder()
.files(files)
.totalCount(files.size())
.totalSize(totalSize)
.formattedTotalSize(formatFileSize(totalSize))
.page(1)
.pageSize(files.size())
.build();
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) { public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
List<FileEntity> allFiles = fileEntityRepository.findByUserIdAndStatus( User user = userRepository.findById(userId).orElseThrow();
userId, FileStatus.ACTIVE); List<FileEntity> allFiles = new ArrayList<>();
if (user.getCompany() != null) {
for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
.map(User::getId)
.toList()) {
allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE));
}
} else {
allFiles = fileEntityRepository.findByUserIdAndStatus(
userId, FileStatus.ACTIVE);
}
int start = (page - 1) * pageSize; int start = (page - 1) * pageSize;
int end = Math.min(start + pageSize, allFiles.size()); int end = Math.min(start + pageSize, allFiles.size());
@@ -188,19 +170,6 @@ public class FileEntityService {
.build(); .build();
} }
@Transactional
public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException {
fileEntity.setStatus(FileStatus.DELETED);
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
Path path = Paths.get(fileEntity.getProtectedFilePath());
Files.deleteIfExists(path);
fileEntity.setProtectedFilePath("");
return fileEntityRepository.save(fileEntity);
}
public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException { public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException {
if (fileEntity.getImageHash() != null) { if (fileEntity.getImageHash() != null) {
fileEntity.setImageHash(null); fileEntity.setImageHash(null);
@@ -217,16 +186,8 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public void deleteFromDisk(FileEntity fileEntity) throws IOException { public void deleteFromDisk(String deleteFilePath) throws IOException {
Path path = Paths.get(fileEntity.getFilePath()); Files.deleteIfExists(Paths.get(deleteFilePath));
if (!Files.exists(path)) {
return;
}
Files.delete(path);
fileEntityRepository.delete(fileEntity);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -244,7 +205,7 @@ public class FileEntityService {
} }
@Transactional @Transactional
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { public FileEntity writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(id) FileEntity fileEntity = fileEntityRepository.findById(id)
.orElseThrow(() -> new RuntimeException("File not found: " + id)); .orElseThrow(() -> new RuntimeException("File not found: " + id));
@@ -263,14 +224,14 @@ public class FileEntityService {
fileEntity.setFileExtension(extension); fileEntity.setFileExtension(extension);
fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
fileEntityRepository.save(fileEntity); return fileEntityRepository.save(fileEntity);
} }
public void clearTempFiles(long userId) throws IOException { public void clearTempFiles(long userId) {
List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP); List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP);
for (FileEntity fileEntity : fileByUserIdAndStatus) { for (FileEntity fileEntity : fileByUserIdAndStatus) {
deleteFromDisk(fileEntity); cloudStorageService.deleteFromStorageDisk(fileEntity);
} }
} }
@@ -282,24 +243,6 @@ public class FileEntityService {
return fileEntityRepository.findFileIdByFilePath(filePath); return fileEntityRepository.findFileIdByFilePath(filePath);
} }
public File getFileById(String id) {
try {
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
File file = new File(fileEntity.getFilePath());
if (!file.exists()) {
throw new RuntimeException("File not found on disk: " + fileEntity.getFilePath());
}
return file;
} catch (Exception e) {
log.error("Error getting file: {}", id, e);
return null;
}
}
public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException { public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException {
Path originalPath = Paths.get(fileEntity.getFilePath()); Path originalPath = Paths.get(fileEntity.getFilePath());
@@ -336,6 +279,18 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
private boolean checkFileExistsOnDisk(String filePath) { private boolean checkFileExistsOnDisk(String filePath) {
try { try {
return Files.exists(Paths.get(filePath)); return Files.exists(Paths.get(filePath));
@@ -346,7 +301,7 @@ public class FileEntityService {
} }
private String determineFileExtension(String fileExt, FileEntity fileEntity) { private String determineFileExtension(String fileExt, FileEntity fileEntity) {
if (fileExt != null && !fileExt.trim().isEmpty()) { if (fileExt != null) {
return fileExt; return fileExt;
} else { } else {
return fileEntity.getFileExtension(); return fileEntity.getFileExtension();
@@ -376,7 +331,9 @@ public class FileEntityService {
.existsOnDisk(existsOnDisk) .existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId()) .supportId(fileEntity.getSupportId())
.protectStatus(fileEntity.getProtectionStatus().toString()) .protectStatus(fileEntity.getProtectionStatus().toString())
.fileName(fileEntity.getOriginalFileName()) .fileName(fileEntity.getOriginalFileName().replace("." +
fileEntity.getFileExtension(), "") + "_nocopy_protected" +
"." + fileEntity.getFileExtension())
.ownerName(user.getFullName()) .ownerName(user.getFullName())
.ownerEmail(user.getEmail()) .ownerEmail(user.getEmail())
.ownerCompany(user.getCompanyName()) .ownerCompany(user.getCompanyName())
@@ -386,16 +343,4 @@ public class FileEntityService {
.protectedFilePath(fileEntity.getProtectedFilePath()) .protectedFilePath(fileEntity.getProtectedFilePath())
.build(); .build();
} }
public String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
} }
@@ -55,6 +55,12 @@ public class FileStatsService {
.audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED)) .audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED))
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION)) .audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
.protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO)) .protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO))
.documentSize(calculateMediaSize(files, FileType.DOCUMENT))
.documentCount(calculateMediaCount(files, FileType.DOCUMENT))
.documentCheck(calculateMediaByStatus(files, FileType.DOCUMENT, FileStatus.CHECKED))
.documentViolations(calculateMediaByStatus(files, FileType.DOCUMENT, FileStatus.VIOLATION))
.protectedDocumentFilesCount(protectedUserFiles(files, FileType.DOCUMENT))
.build(); .build();
} }
@@ -2,6 +2,7 @@ package ru.soune.nocopy.service.file;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
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.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
@@ -19,5 +20,5 @@ public interface FileUploadService {
UploadProgressResponse getUploadProgress(String uploadId); UploadProgressResponse getUploadProgress(String uploadId);
void completeFileProcessingAsync(FileUploadSession session, FileStatus status); FileEntity completeFileProcessingAsync(FileUploadSession session, FileStatus status);
} }
@@ -0,0 +1,143 @@
package ru.soune.nocopy.service.file.cloud;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
@Service
@Slf4j
@RequiredArgsConstructor
public class CloudStorageService {
private final FileEntityRepository fileEntityRepository;
@Value("${yandex.cloud.key}")
private String key;
@Value("${yandex.cloud.secret-key}")
private String secretKey;
@Value("${yandex.cloud.region}")
private String region;
@Value("${yandex.cloud.s3-endpoint}")
private String s3endpoint;
@Value("${yandex.cloud.bucket}")
private String bucket;
public void saveFilesToStorage(String mimeType, String extension, String sendToCloudFilePath) {
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String mimeTypeFromFile = mimeType.equals("document") ? "application" : mimeType;
String contentType = mimeTypeFromFile + "/" + extension;
String filePath = "files" + sendToCloudFilePath;
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(filePath)
.contentType(contentType)
.build();
try {
s3Client.putObject(putObjectRequest,
RequestBody.fromBytes(Files.readAllBytes(Paths.get(sendToCloudFilePath))));
Files.deleteIfExists(Paths.get(sendToCloudFilePath));
} catch (IOException e) {
log.error("Failed to read file: {}", filePath, e);
}
}
public File readFileFromStorageByPath(String filePath) throws IOException {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key("files" + filePath)
.build();
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String tempDir = System.getProperty("java.io.tmpdir");
String fileName = filePath.replace("/", "_");
File tempFile = new File(tempDir, fileName);
s3Client.getObject(objectRequest, tempFile.toPath());
return tempFile;
}
public void deleteFileFromStorage(String filePath) {
if (filePath == null || filePath.isEmpty()) {
log.warn("Attempted to delete file with empty path");
return;
}
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String cloudKey = "files" + filePath;
try {
try {
s3Client.headObject(builder -> builder.bucket(bucket).key(cloudKey).build());
} catch (Exception e) {
log.warn("File does not exist in cloud storage: {}", cloudKey);
return;
}
s3Client.deleteObject(builder -> builder.bucket(bucket).key(cloudKey).build());
log.info("File successfully deleted from cloud storage: {}", cloudKey);
} catch (Exception e) {
log.error("Failed to delete file from cloud storage: {}", cloudKey, e);
throw new RuntimeException("Failed to delete file from cloud storage: " + cloudKey, e);
}
}
public void deleteFromStorageDisk(FileEntity fileEntity) {
deleteFileFromStorage(fileEntity.getFilePath());
if (fileEntity.getProtectedFilePath() != null) {
deleteFileFromStorage(fileEntity.getProtectedFilePath());
}
fileEntityRepository.delete(fileEntity);
}
}
@@ -0,0 +1,31 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.documents.DocumentLocalSearch;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.util.FileUtil;
@Slf4j
@Service
@RequiredArgsConstructor
public class DocumentLocalSearchImpl implements DocumentLocalSearch {
private final FileUtil fileUtil;
private final FileEntityRepository fileEntityRepository;
@Override
public @Nullable FileProtector.FileInfo findByIds(@NotNull String ownerId, @NotNull String fileId) {
FileEntity fileEntity = fileEntityRepository.findByIdAndUserId(fileId, Long.valueOf(ownerId));
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
log.info("fileInfo: {}", fileInfo);
return fileInfo;
}
}
@@ -3,6 +3,7 @@ package ru.soune.nocopy.service.file.impl;
import com.vrt.NoCopyFileService; import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.FileProtector;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +25,7 @@ import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
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;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
@@ -83,6 +85,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private final TariffInfoService tariffInfoService; private final TariffInfoService tariffInfoService;
private final CloudStorageService cloudStorageService;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -177,7 +181,6 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
@Override @Override
@Transactional
public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile, Integer findSimilar) { MultipartFile chunkFile, Integer findSimilar) {
FileUploadSession session = sessionRepository.findById(uploadId) FileUploadSession session = sessionRepository.findById(uploadId)
@@ -219,7 +222,8 @@ public class FileUploadServiceImpl implements FileUploadService {
@Async("fileUploadTaskExecutor") @Async("fileUploadTaskExecutor")
@Transactional @Transactional
@Override @Override
public void completeFileProcessingAsync(FileUploadSession session, FileStatus status) { public FileEntity completeFileProcessingAsync(FileUploadSession session, FileStatus status) {
FileEntity saved = null;
try { try {
Path filePath = Paths.get(session.getFilePath()); Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath); String checksum = calculateChecksum(filePath);
@@ -238,7 +242,7 @@ public class FileUploadServiceImpl implements FileUploadService {
.status(status) .status(status)
.build(); .build();
FileEntity saved = fileEntityRepository.save(fileEntity); saved = fileEntityRepository.save(fileEntity);
if (session.getFileType().equals("image")) { if (session.getFileType().equals("image")) {
Map<String, Long> hash = imageHashService.calculateHash(filePath); Map<String, Long> hash = imageHashService.calculateHash(filePath);
@@ -248,16 +252,13 @@ public class FileUploadServiceImpl implements FileUploadService {
cleanupSessionFiles(session); cleanupSessionFiles(session);
if (status != FileStatus.TEMP) {
noCopyFileService.addFile(fileUtil.createFileInfo(fileEntity));
}
log.info("File processing completed for session: {}", session.getUploadId()); log.info("File processing completed for session: {}", session.getUploadId());
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to complete file processing for session {}: {}", log.error("Failed to complete file processing for session {}: {}",
session.getUploadId(), e.getMessage(), e); session.getUploadId(), e.getMessage(), e);
} }
return saved;
} }
private void validateSession(FileUploadSession session) { private void validateSession(FileUploadSession session) {
@@ -289,6 +290,9 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
} }
@Autowired
private EntityManager entityManager;
private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile, private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile,
Integer findSimilar) { Integer findSimilar) {
String chunkPath = null; String chunkPath = null;
@@ -329,8 +333,20 @@ public class FileUploadServiceImpl implements FileUploadService {
fileEntityService.clearTempFiles(session.getUserId()); fileEntityService.clearTempFiles(session.getUserId());
} }
completeFileProcessingAsync(session, status); FileEntity fileEntity = completeFileProcessingAsync(session, status);
fileEntityRepository.flush();
entityManager.clear();
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getFilePath());
if (status != FileStatus.TEMP) {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
noCopyFileService.addFile(fileInfo);
}
if (status != FileStatus.TEMP) { if (status != FileStatus.TEMP) {
String fileType = session.getFileType(); String fileType = session.getFileType();
@@ -377,13 +393,13 @@ public class FileUploadServiceImpl implements FileUploadService {
switch (fileType) { switch (fileType) {
case "video": case "video":
cost = TariffConstants.VIDEO_TOKEN_VALUE; return TariffConstants.VIDEO_TOKEN_VALUE;
case "image": case "image":
cost = TariffConstants.IMAGE_TOKEN_VALUE; return TariffConstants.IMAGE_TOKEN_VALUE;
case "audio": case "audio":
cost = TariffConstants.AUDIO_TOKEN_VALUE; return TariffConstants.AUDIO_TOKEN_VALUE;
case "pdf": case "document":
cost = TariffConstants.PDF_TOKEN_VALUE; return TariffConstants.PDF_TOKEN_VALUE;
} }
return cost; return cost;
@@ -7,7 +7,10 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -24,22 +27,55 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
private final FileEntityService fileEntityService; private final FileEntityService fileEntityService;
private final CloudStorageService cloudStorageService;
private final FileEntityRepository fileEntityRepository;
@Nullable @Nullable
@Override @Override
public File getImageFile(@NotNull String id) { public File getImageFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Nullable @Nullable
@Override @Override
public File getVideoFile(@NotNull String id) { public File getVideoFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Nullable @Nullable
@Override @Override
public File getAudioFile(@NotNull String id) { public File getAudioFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public @Nullable File getDocument(@NotNull String id) {
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
@@ -74,7 +110,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -86,7 +126,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -98,7 +142,25 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success();
} catch (Exception e) {
log.error("Failed to create protected file: {}", id, e);
return OperationResult.Companion.failure("Failed to create protected file");
}
}
@Override
public @NotNull OperationResult writeDocumentFile(@NotNull String id, @NotNull byte[] data) {
try {
FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, null);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -61,9 +61,9 @@ public class ReferralLevelProviderImpl implements ReferralLevelProvider {
@Override @Override
public int getMaxInvitee() { public int getMaxInvitee() {
switch (entity.getId()) { switch (entity.getId()) {
case "bronze": return 9; case "bronze": return 5;
case "silver": return 49; case "silver": return 15;
case "gold": return 99; case "gold": return 50;
case "platinum": return Integer.MAX_VALUE; case "platinum": return Integer.MAX_VALUE;
default: return Integer.MAX_VALUE; default: return Integer.MAX_VALUE;
} }
@@ -66,7 +66,7 @@ public class ReferralRepoImpl implements ReferralRepo {
@Override @Override
public @Nullable Long getInviterIdForUser(long userId) { public @Nullable Long getInviterIdForUser(long userId) {
ru.soune.nocopy.entity.referral.Referral referral = referralRepository.findByUserId(userId); ru.soune.nocopy.entity.referral.Referral referral = referralRepository.findByUserId(userId);
return referral != null ? referral.getInviterId() : 0L; return referral != null ? referral.getInviterId() : null;
} }
@Override @Override
@@ -79,10 +79,6 @@ public class ReferralRepoImpl implements ReferralRepo {
public @NotNull Referral getReferralByUserId(long userId) { public @NotNull Referral getReferralByUserId(long userId) {
ru.soune.nocopy.entity.referral.Referral entity = referralRepository.findByUserId(userId); ru.soune.nocopy.entity.referral.Referral entity = referralRepository.findByUserId(userId);
if (entity == null) {
return createDefaultReferral(userId);
}
return new Referral() { return new Referral() {
@Override @Override
public long getUserId() { return entity.getUserId(); } public long getUserId() { return entity.getUserId(); }
@@ -118,34 +114,6 @@ public class ReferralRepoImpl implements ReferralRepo {
}; };
} }
private Referral createDefaultReferral(long userId) {
return new Referral() {
@Override
public long getUserId() { return userId; }
@Override
public String getReferralLink() { return ""; }
@Override
public Long getInviter() { return 0L; }
@Override
public String getCurrentLevel() { return "bronze"; }
@Override
public int getTotalIncome() { return 0; }
@Override
public int getAvailableIncome() { return 0; }
@Override
public int getHoldBalance() { return 0; }
@Override
public boolean getActive() { return false; }
};
}
@Override @Override
public @NotNull List<ReferralInvitee> getReferralInvitees(long userId, int pageSize, int pageNumber) { public @NotNull List<ReferralInvitee> getReferralInvitees(long userId, int pageSize, int pageNumber) {
Pageable pageable = PageRequest.of(pageNumber, pageSize); Pageable pageable = PageRequest.of(pageNumber, pageSize);
@@ -1,5 +1,6 @@
package ru.soune.nocopy.service.register; package ru.soune.nocopy.service.register;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -10,11 +11,13 @@ import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.dto.register.LoginRequest; import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.company.Company; import ru.soune.nocopy.entity.company.Company;
import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.Permission; import ru.soune.nocopy.entity.user.Permission;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.MaxUserCountIsOver;
import ru.soune.nocopy.exception.NotFoundAuthToken; 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;
@@ -176,20 +179,31 @@ public class AuthService {
private void createIndividualCompanyAccount(RegRequest request, User user) { private void createIndividualCompanyAccount(RegRequest request, User user) {
String authToken = request.getAuthToken(); String authToken = request.getAuthToken();
Long userIdByToken = authTokenRepository.findUserIdByToken(authToken); Long userIdByToken = authTokenRepository.findUserIdByToken(authToken);
User companyUser = userRepository.findById(userIdByToken).orElseThrow(); User companyUser = userRepository.findById(userIdByToken)
.orElseThrow(() -> new EntityNotFoundException("Not found token: " + authToken));
Company company = companyUser.getCompany(); Company company = companyUser.getCompany();
TariffInfo tariffInfo = company.getTariffInfo(); if (company == null) {
throw new IllegalStateException("Not have company");
Long maxUsers = tariffInfo.getTariff().getMaxUsers();
long currentCountUsers = userRepository.countByCompanyId(company.getId());
//TODO add exception
if (currentCountUsers < maxUsers) {
throw new RuntimeException();
} }
user.setCompany(companyUser.getCompany()); TariffInfo tariffInfo = company.getTariffInfo();
if (tariffInfo == null) {
throw new IllegalStateException("Company not have tariff");
}
Tariff tariff = tariffInfo.getTariff();
Long maxUsers = tariff.getMaxUsers();
long currentCountUsers = userRepository.countByCompanyId(company.getId());
if (currentCountUsers >= maxUsers) {
throw new NotValidFieldException("Max user is over.", new BaseResponse(20002,
MessageCode.USER_LIMIT_IS_OVER.getCode(),
MessageCode.USER_LIMIT_IS_OVER.getDescription(), Map.of(
"limit", maxUsers, "countUser", currentCountUsers)));
}
user.setCompany(company);
user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS); user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS);
} }
} }
@@ -1,62 +0,0 @@
package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import java.util.ArrayList;
import java.util.List;
public class SearchApiToYandexResponseMapper {
private final ObjectMapper objectMapper = new ObjectMapper();
public YandexSearchResponse mapToYandexResponse(String searchApiJson) throws Exception {
JsonNode root = objectMapper.readTree(searchApiJson);
YandexSearchResponse response = new YandexSearchResponse();
List<YandexSearchResponse.ImageResult> imageResults = new ArrayList<>();
JsonNode visualMatches = root.path("visual_matches");
if (visualMatches.isArray()) {
for (JsonNode match : visualMatches) {
YandexSearchResponse.ImageResult result = new YandexSearchResponse.ImageResult();
JsonNode imageNode = match.path("image");
if (!imageNode.isMissingNode()) {
result.setUrl(imageNode.path("link").asText());
result.setWidth(imageNode.path("width").asInt());
result.setHeight(imageNode.path("height").asInt());
}
result.setPageUrl(match.path("link").asText());
result.setPageTitle(match.path("title").asText());
String source = match.path("source").asText();
result.setHost(extractHostFromSource(source));
if (result.getUrl() != null && !result.getUrl().isEmpty()) {
imageResults.add(result);
}
}
}
response.setImages(imageResults);
return response;
}
private String extractHostFromSource(String source) {
if (source == null || source.isEmpty()) {
return "";
}
source = source.replaceFirst("^(https?://)?(www\\.)?", "");
int slashIndex = source.indexOf('/');
if (slashIndex > 0) {
return source.substring(0, slashIndex);
}
return source;
}
}
@@ -0,0 +1,275 @@
package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
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.NotValidFieldException;
import java.io.*;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
@Slf4j
@Service
public class SearchImageService {
private final ObjectMapper objectMapper;
private final OkHttpClient httpClient;
public SearchImageService() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.callTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
this.objectMapper = new ObjectMapper();
}
@Value("${yandex.api-key}")
private String apiKey;
@Value("${yandex.folder-id}")
private String folderId;
@Value("${yandex.search-url}")
private String searchUrl;
@Value("${searchapi.api-key:}")
private String searchApiKey;
@Value("${server.baseurl}")
private String appBaseUrl;
@PostConstruct
public void init() {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
public String searchReverseByPublicUrl(FileEntity fileEntity, String engine, String searchType)
throws IOException, TimeoutException {
if (!isImageFile(fileEntity)) {
throw new NotValidFieldException(
"File not image",
new BaseResponse(
20007,
MessageCode.INVALID_FIELD.getCode(),
MessageCode.INVALID_FIELD.getDescription(),
Map.of("file_type", fileEntity.getMimeType())
)
);
}
String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId());
log.info("Searching reverse for image: {}", publicUrl);
try {
return callReverseImageApiByUrl(publicUrl, engine, searchType);
} catch (SocketTimeoutException e) {
log.error("Yandex search timeout after {}", fileEntity.getId());
throw new TimeoutException("Search timeout");
}
}
private boolean isImageFile(FileEntity fileEntity) {
String mimeType = fileEntity.getMimeType();
return mimeType != null && mimeType.startsWith("image");
}
private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException {
if (searchApiKey == null || searchApiKey.isBlank()) {
throw new IllegalStateException("SearchAPI key not configured");
}
HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search")
.newBuilder()
.addQueryParameter("engine", engine)
.addQueryParameter("api_key", searchApiKey)
.addQueryParameter("url", imageUrl)
.addQueryParameter("search_type", searchType)
.addQueryParameter("wait", "true")
.build();
Request request = new Request.Builder()
.url(url)
.header("Accept", "application/json")
.header("User-Agent", "Mozilla/5.0")
.build();
long start = System.currentTimeMillis();
try (Response response = httpClient.newCall(request).execute()) {
long duration = System.currentTimeMillis() - start;
log.info("Yandex response code={}, duration={}ms", response.code(), duration);
if (!response.isSuccessful()) {
throw new IOException("API error: " + response.code());
}
ResponseBody body = response.body();
if (body == null) {
throw new IOException("Empty response body");
}
return body.string();
}
}
public List<YandexSearchResponse.ImageResult> getAllImagesWithoutPagination(String searchApiJson, String findType)
throws IOException {
JsonNode root = objectMapper.readTree(searchApiJson);
JsonNode matches = root.path(findType);
List<YandexSearchResponse.ImageResult> allImages = new ArrayList<>();
if (matches.isArray()) {
for (JsonNode match : matches) {
YandexSearchResponse.ImageResult result = mapImageResult(match);
if ("exact_matches".equals(findType)) {
JsonNode thumbnail = match.path("thumbnail");
if (!thumbnail.isMissingNode() && thumbnail.asText().startsWith("data:image")) {
result.setUrl(thumbnail.asText());
}
JsonNode imageNode = match.path("image");
if (!imageNode.isMissingNode()) {
String directUrl = imageNode.path("link").asText();
if (directUrl != null && !directUrl.isBlank()) {
result.setUrl(directUrl);
}
}
}
if (result.getUrl() != null && !result.getUrl().isBlank()) {
allImages.add(result);
}
}
}
return allImages;
}
public List<YandexSearchResponse.ImageResult> removeDuplicateUrls(
List<YandexSearchResponse.ImageResult> yandexImages,
List<YandexSearchResponse.ImageResult> googleImages) {
Map<String, YandexSearchResponse.ImageResult> uniqueByUrl = new LinkedHashMap<>();
for (YandexSearchResponse.ImageResult image : googleImages) {
String normalizedUrl = normalizeUrl(image.getUrl());
uniqueByUrl.putIfAbsent(normalizedUrl, image);
}
for (YandexSearchResponse.ImageResult image : yandexImages) {
String normalizedUrl = normalizeUrl(image.getUrl());
uniqueByUrl.putIfAbsent(normalizedUrl, image);
}
return new ArrayList<>(uniqueByUrl.values());
}
public List<YandexSearchResponse.ImageResult> paginateResults(
List<YandexSearchResponse.ImageResult> allResults,
int page,
int pageSize) {
if (allResults.isEmpty()) {
return new ArrayList<>();
}
int totalPages = (int) Math.ceil((double) allResults.size() / pageSize);
if (page > totalPages) {
return new ArrayList<>();
}
int startIndex = (page - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, allResults.size());
return new ArrayList<>(allResults.subList(startIndex, endIndex));
}
private String normalizeUrl(String url) {
if (url == null) return null;
try {
if (url.startsWith("data:image")) {
return url.length() > 100 ? url.substring(0, 100) : url;
}
java.net.URI uri = new java.net.URI(url);
String host = uri.getHost();
if (host != null) {
host = host.toLowerCase().replaceFirst("^www\\.", "");
}
String path = uri.getPath();
if (path != null && path.length() > 1 && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return (host != null ? host : "") + (path != null ? path : "");
} catch (Exception e) {
return url.toLowerCase()
.replaceFirst("^(https?://)?(www\\.)?", "")
.replaceFirst("/$", "");
}
}
private YandexSearchResponse.ImageResult mapImageResult(JsonNode match) {
YandexSearchResponse.ImageResult result =
new YandexSearchResponse.ImageResult();
JsonNode imageNode = match.path("image");
if (imageNode.isObject()) {
result.setUrl(imageNode.path("link").asText());
result.setWidth(imageNode.path("width").asInt(0));
result.setHeight(imageNode.path("height").asInt(0));
}
result.setPageUrl(match.path("link").asText());
result.setPageTitle(match.path("title").asText());
String source = match.path("source").asText();
result.setHost(extractHostFromSource(source));
return result;
}
private String extractHostFromSource(String source) {
if (source == null || source.isEmpty()) {
return "";
}
source = source.replaceFirst("^(https?://)?(www\\.)?", "");
int slashIndex = source.indexOf('/');
if (slashIndex > 0) {
return source.substring(0, slashIndex);
}
return source;
}
}
@@ -1,230 +0,0 @@
package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
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.NotValidFieldException;
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;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class YandexSearchService {
private final ObjectMapper objectMapper;
private final OkHttpClient httpClient;
public YandexSearchService() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.callTimeout(180, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
this.objectMapper = new ObjectMapper();
}
@Value("${yandex.api-key}")
private String apiKey;
@Value("${yandex.folder-id}")
private String folderId;
@Value("${yandex.search-url}")
private String searchUrl;
@Value("${searchapi.api-key:}")
private String searchApiKey;
@Value("${server.baseurl}")
private String appBaseUrl;
@PostConstruct
public void init() {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
public YandexSearchResponse searchByFileEntity(FileEntity fileEntity) throws IOException {
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", fileEntity.getId(),
"filePath", fileEntity.getFilePath())));
}
return callYandexApi(fileBytes);
}
public String searchReverseByPublicUrl(FileEntity fileEntity) throws IOException {
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())));
}
String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId());
log.info("Searching reverse for image: {}", publicUrl);
return callReverseImageApiByUrl(publicUrl);
}
private String callReverseImageApiByUrl(String imageUrl) throws IOException {
if (searchApiKey == null || searchApiKey.isEmpty()) {
throw new IllegalStateException("SearchAPI key not configured. Set searchapi.api-key property");
}
HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search")
.newBuilder()
.addQueryParameter("engine", "yandex_reverse_image")
.addQueryParameter("api_key", searchApiKey)
.addQueryParameter("url", imageUrl)
.addQueryParameter("wait", "true")
.addQueryParameter("timeout", "30000")
.build();
log.info("Calling Yandex Reverse Image API: {}", url);
Request request = new Request.Builder()
.url(url)
.header("Accept", "application/json")
.header("User-Agent", "Mozilla/5.0")
.build();
try (Response response = httpClient.newCall(request).execute()) {
log.info("Response code: {}", response.code());
log.info("Response headers: {}", response.headers());
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "No error body";
log.error("API request failed: {} - {}", response.code(), errorBody);
throw new IOException("API request failed: " + response.code() + " - " + response.message());
}
String responseBody = response.body().string();
log.info("Response body length: {} characters", responseBody.length());
return responseBody;
}
}
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;
}
}
@@ -35,6 +35,7 @@ public class TariffInfoService {
tariffInfo.setStatus(tariffStatus); tariffInfo.setStatus(tariffStatus);
tariffInfo.setEndTariff(endTariff); tariffInfo.setEndTariff(endTariff);
tariffInfo.setTokens(tokens); tariffInfo.setTokens(tokens);
tariffInfo.setBoughtTokens(0);
return tariffInfoRepository.save(tariffInfo); return tariffInfoRepository.save(tariffInfo);
} }
@@ -52,10 +53,15 @@ public class TariffInfoService {
} }
Integer tokens = activeTariffInfo.getTokens(); Integer tokens = activeTariffInfo.getTokens();
Integer boughtTokens = activeTariffInfo.getBoughtTokens();
if (tokens - value >= 0) { if (tokens - value >= 0) {
activeTariffInfo.setTokens(activeTariffInfo.getTokens() - value); activeTariffInfo.setTokens(activeTariffInfo.getTokens() - value);
tariffInfoRepository.save(activeTariffInfo);
} else if (boughtTokens - value >= 0) {
activeTariffInfo.setTokens(activeTariffInfo.getBoughtTokens() - value);
tariffInfoRepository.save(activeTariffInfo); tariffInfoRepository.save(activeTariffInfo);
} else { } else {
throw new TariffNotFoundException("User not have tokens for protect"); throw new TariffNotFoundException("User not have tokens for protect");
@@ -118,7 +124,8 @@ public class TariffInfoService {
tariffInfo.getStartTariff(), tariffInfo.getStartTariff(),
tariffInfo.getEndTariff(), tariffInfo.getEndTariff(),
tariffInfo.getTariff() != null ? tariffInfo.getTariff().getId() : null, tariffInfo.getTariff() != null ? tariffInfo.getTariff().getId() : null,
tariffInfo.getTariff() != null ? tariffInfo.getTariff().getName() : null, tariffInfo.getTokens(), tariffInfo.getTariff() != null ? tariffInfo.getTariff().getName() : null,
tariffInfo.getTokens() + tariffInfo.getBoughtTokens(),
tariffInfo.getTariff().getDiskSize(), 0L, 0, tariffInfo.getTariff().getDiskSize(), 0L, 0,
tariffInfo.getTariff().getMaxFilesCount()); tariffInfo.getTariff().getMaxFilesCount());
} }
@@ -101,40 +101,44 @@ public class TariffInitializationService {
tariff.setType(type.name()); tariff.setType(type.name());
switch (type) { switch (type) {
case DEMO: case FREE:
tariff.setName("demo"); tariff.setName("free");
tariff.setPrice(demoPrice); tariff.setPrice(0);
tariff.setTokens(demoTokens); tariff.setTokens(30);
tariff.setMaxFilesCount(demoMaxFileCount); tariff.setMaxFilesCount(10);
tariff.setDiskSize(1024L * 1024 * 100); tariff.setDiskSize(1024L * 1024 * 100);
tariff.setMaxUsers(demoUserCount); tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
break; break;
case START: case START:
tariff.setName("start"); tariff.setName("start");
tariff.setPrice(startPrice); tariff.setPrice(1200);
tariff.setTokens(startTokens); tariff.setTokens(300);
tariff.setMaxFilesCount(startMaxFileCount); tariff.setMaxFilesCount(80);
tariff.setDiskSize(1024L * 1024 * 500); tariff.setDiskSize(1024L * 1024 * 500);
tariff.setMaxUsers(startUserCount); tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
break; break;
case PRO: case PRO:
tariff.setName("pro"); tariff.setName("pro");
tariff.setPrice(proPrice); tariff.setPrice(4250);
tariff.setTokens(proTokens); tariff.setTokens(900);
tariff.setMaxFilesCount(proMaxFileCount); tariff.setMaxFilesCount(300);
tariff.setDiskSize(1024L * 1024 * 1024 * 10L); tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(proUserCount); tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
break; break;
case ENTERPRISE: case STUDIO:
tariff.setName("premium"); tariff.setName("studio");
tariff.setPrice(enterpricePrice); tariff.setPrice(9750);
tariff.setTokens(enterpriceTokens); tariff.setTokens(3000);
tariff.setMaxFilesCount(enterpriceMaxFileCount); tariff.setMaxFilesCount(1000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(enterpriceUserCount); tariff.setMaxUsers(2L);
tariff.setTariffAccountType("b2c");
break; break;
case BASIC: case BASIC:
@@ -144,6 +148,37 @@ public class TariffInitializationService {
tariff.setMaxFilesCount(enterpriceMaxFileCount); tariff.setMaxFilesCount(enterpriceMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(basicUserCount); tariff.setMaxUsers(basicUserCount);
tariff.setTariffAccountType("b2c");
break;
case ENTERPRISE:
tariff.setName("enterprise");
tariff.setPrice(200000);
tariff.setTokens(30000);
tariff.setMaxFilesCount(100000);
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(10L);
tariff.setTariffAccountType("b2b");
break;
case BUISNES:
tariff.setName("buisnes");
tariff.setPrice(29000);
tariff.setTokens(5000);
tariff.setMaxFilesCount(5000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(5L);
tariff.setTariffAccountType("b2b");
break;
case CORPORAT:
tariff.setName("corporat");
tariff.setPrice(99000);
tariff.setTokens(20000);
tariff.setMaxFilesCount(50000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(10L);
tariff.setTariffAccountType("b2b");
break; break;
} }
@@ -6,6 +6,7 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.TariffRepository;
@@ -32,6 +33,15 @@ public class TariffService {
tariffRepository.save(tariff); tariffRepository.save(tariff);
} }
public List<TariffDTO> getTariffByAccountType(User user) {
String typeAccount = user.canManageCompanySettings() ? "b2b": "b2c";
return tariffRepository.findByTariffAccountType(typeAccount)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
public List<TariffDTO> getAllTariffs() { public List<TariffDTO> getAllTariffs() {
return tariffRepository.findAllByOrderByPriceAsc() return tariffRepository.findAllByOrderByPriceAsc()
.stream() .stream()
@@ -41,6 +41,8 @@ public class FileUtil {
return FileProtector.Type.VIDEO; return FileProtector.Type.VIDEO;
} else if (mimeType.startsWith("audio")) { } else if (mimeType.startsWith("audio")) {
return FileProtector.Type.AUDIO; return FileProtector.Type.AUDIO;
} else if (mimeType.startsWith("document")) {
return FileProtector.Type.DOC;
} else { } else {
return FileProtector.Type.IMAGE; return FileProtector.Type.IMAGE;
} }
+22 -1
View File
@@ -51,6 +51,10 @@ spring:
enabled: true enabled: true
resolve-lazily: false resolve-lazily: false
dadata:
api-key: 0109fcc9d5a0a68c08f3b12ba7e2a620ea438ef7
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party"
file: file:
storage: storage:
base-path: ${FILE_STORAGE_PATH:/data/uploads} base-path: ${FILE_STORAGE_PATH:/data/uploads}
@@ -72,6 +76,15 @@ security:
server: server:
port: ${SERVER_PORT:8080} port: ${SERVER_PORT:8080}
baseurl: "http://92.242.61.23" baseurl: "http://92.242.61.23"
tomcat:
uri-encoding: UTF-8
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
force-request: true
force-response: true
logging: logging:
level: level:
@@ -87,9 +100,16 @@ yandex:
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
cloud:
key: "YCAJEmHZcWxzf4oGWvETG3mms"
secret-key: "YCPAPZ32XC-LWb8xE0b_xTcBD8NZUCHpSOfu8LUG"
region: "ru-central1"
s3-endpoint: "https://storage.yandexcloud.net"
bucket: "no-copy-storage"
searchapi: searchapi:
api-key: ${SEARCHAPI_API_KEY:2xBChafVmojL7G1ZXzzUuA6o} api-key: ${SEARCHAPI_API_KEY:rdwNzCr2eybYVbU1KrGTJAgm}
reverse-image-url: "https://searchapi.io/api/v1/search" reverse-image-url: "https://searchapi.io/api/v1/search"
app: app:
@@ -129,3 +149,4 @@ tarif:
enterprice-max-files-count: 50 enterprice-max-files-count: 50
enterprice-disk-size: 500 enterprice-disk-size: 500
enterprice-user-count: 80 enterprice-user-count: 80