51 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
vladp a7e94639d2 NC_BACK-36_1
Test Workflow / test (push) Successful in 3s
2026-02-06 19:27:25 +07:00
vladp f52ce6db3c dev add referral
Test Workflow / test (push) Successful in 3s
2026-02-06 01:01:34 +07:00
70 changed files with 1757 additions and 544 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
ENV BUILD_TIME_BACK="unknown"\
BUILD_TIME_FRONT="unknown"
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
+39
View File
@@ -122,3 +122,42 @@ SELECT 1
FROM protect_check pc
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";
+10 -2
View File
@@ -34,7 +34,7 @@ dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'commons-validator:commons-validator:1.7'
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.api-client', name: 'google-api-client', version: '2.7.2'
@@ -59,10 +59,18 @@ dependencies {
testImplementation 'org.mockito:mockito-core:5.3.1'
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 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') {
+1 -1
View File
@@ -181,7 +181,7 @@ services:
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d' # Удерживать 15 дней
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle'
healthcheck:
test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ]
@@ -39,6 +39,7 @@ pipeline {
stage('Deploy with docker-compose') {
steps {
script {
def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim()
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
@@ -85,6 +86,7 @@ pipeline {
-e POSTGRES_USER=${DB_USER} \\
-e POSTGRES_PASSWORD=${DB_PASSWORD} \\
-v pgdata_${params.PORT}:/var/lib/postgresql/data \\
-e BUILD_TIME_BACK='${buildTime}' \\
postgres:17.7
echo '4. Запускаем storage...'
+13 -14
View File
@@ -85,6 +85,8 @@ pipeline {
stage('Build on server') {
steps {
script {
def BUILD_TIME_FRONT = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim()
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
@@ -93,23 +95,20 @@ pipeline {
)
]) {
sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
cd /opt/deployments/frontend/${params.BRANCH}-${params.PORT}
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
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
# Меняем имя контейнера
sed -i 's/container_name: no-copy-frontend\$/container_name: no-copy-frontend-${params.PORT}/' docker-compose.yml
# 2. Меняем имя контейнера и порт
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 на нужный порт)
sed -i 's/\\\"2998:2999\\\"/\\\"${params.PORT}:2999\\\"/' docker-compose.yml
echo 'Проверяем изменения:'
grep -n 'container_name\\|ports' docker-compose.yml
echo '=== Строим образ ==='
docker-compose -f docker-compose.yml -p frontend-${params.PORT} build
"
# 3. Собираем образ
docker-compose -f docker-compose.yml -p frontend-${params.PORT} build
"
"""
}
}
+2
View File
@@ -34,6 +34,7 @@ pipeline {
stage('Deploy with docker-compose') {
steps {
script {
def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim()
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
@@ -92,6 +93,7 @@ pipeline {
-e POSTGRES_PASSWORD=$DB_PASSWORD \\
-e POSTGRES_PORT=5432 \\
-e POSTGRES_HOST=db \\
-e BUILD_TIME_BACK='${buildTime}' \\
--restart unless-stopped \\
app-backend:latest
+2 -2
View File
@@ -2,9 +2,9 @@ package ru.soune
interface Referral {
val userId: String
val userId: Long
val referralLink: String
val inviter: String?
val inviter: Long?
val currentLevel: String
val totalIncome: Int
val availableIncome: Int
+11 -11
View File
@@ -2,32 +2,32 @@ package ru.soune
interface ReferralRepo {
fun getUserReferralLink(userId: String): String
fun getUserReferralLink(userId: Long): String
fun getUserIdByLink(link: String): String
fun getUserIdByLink(link: String): Long?
fun getInviterIdForUser(userId: String): String?
fun getInviterIdForUser(userId: Long): Long?
fun getReferralLevelForUser(userId: String): String
fun getReferralLevelForUser(userId: Long): String
fun getReferralByUserId(userId: String): Referral
fun getReferralByUserId(userId: Long): Referral
fun getReferralInvitees(userId: String, pageSize: Int, pageNumber: Int): List<ReferralInvitee>
fun getReferralInvitees(userId: Long, pageSize: Int, pageNumber: Int): List<ReferralInvitee>
// создаем новую запись в БД
fun createReferralEntity(entity: Referral)
// прибавить к totalIncome и availableIncome значение transferAmount
fun increaseIncome(userId: String, transferAmount: Int)
fun increaseIncome(userId: Long, transferAmount: Int)
// установить active в true
fun activateUser(userId: String): Boolean
fun activateUser(userId: Long): Boolean
// посчитать записи, у которых inviter == userId и active == true
fun getActiveInviteeForUser(userId: String): Int
fun getActiveInviteeForUser(userId: Long): Int
// посчитать записи, у которых inviter == userId
fun getTotalInviteeForUser(userId: String): Int
fun getTotalInviteeForUser(userId: Long): Int
fun upgradeReferralLevelForUser(userId: String, newLevelId: String)
fun upgradeReferralLevelForUser(userId: Long, newLevelId: String)
}
+6 -5
View File
@@ -5,7 +5,7 @@ class ReferralService(
private val referralRepo: ReferralRepo,
) {
fun onRegister(userId: String, inviterReferralLink: String?) {
fun onRegister(userId: Long, inviterReferralLink: String?) {
val initialReferralLevel = referralLevelProvider.getInitialReferralLevelId()
val referralLink = referralRepo.getUserReferralLink(userId)
val inviter = inviterReferralLink?.let(referralRepo::getUserIdByLink)
@@ -24,7 +24,7 @@ class ReferralService(
referralRepo.createReferralEntity(referral)
}
fun onUserAccountRefill(userId: String, transferAmount: Int) {
fun onUserAccountRefill(userId: Long, transferAmount: Int) {
val wasActivated = referralRepo.activateUser(userId)
val inviterId = referralRepo.getInviterIdForUser(userId) ?: return
val currentInviterLevel = referralRepo.getReferralLevelForUser(inviterId)
@@ -47,11 +47,11 @@ class ReferralService(
referralLevelProvider.getAvailableReferralLevels()
fun getReferralLevelForUser(userId: String): ReferralLevel =
fun getReferralLevelForUser(userId: Long): ReferralLevel =
referralRepo.getReferralLevelForUser(userId)
.let { referralLevelProvider.getReferralLevelById(it) }
fun getReferralStatForUser(userId: String): ReferralStat {
fun getReferralStatForUser(userId: Long): ReferralStat {
var referral = referralRepo.getReferralByUserId(userId)
return ReferralStat(
totalInvitee = referralRepo.getTotalInviteeForUser(userId),
@@ -59,9 +59,10 @@ class ReferralService(
totalIncome = referral.totalIncome,
availableIncome = referral.availableIncome,
holdBalance = referral.holdBalance,
referralLink = referral.referralLink
)
}
fun getInviteeForUser(userId: String, pageSize: Int, pageNumber: Int): List<ReferralInvitee> =
fun getInviteeForUser(userId: Long, pageSize: Int, pageNumber: Int): List<ReferralInvitee> =
referralRepo.getReferralInvitees(userId, pageSize, pageNumber)
}
+1
View File
@@ -6,4 +6,5 @@ data class ReferralStat(
val totalIncome: Int,
val availableIncome: Int,
val holdBalance: Int,
val referralLink: String,
)
@@ -3,9 +3,9 @@ package ru.soune.nocopy.configuration;
import com.vrt.AudioFilePathProvider;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.audio.AudioLocalSearch;
import com.vrt.fileprotection.documents.DocumentLocalSearch;
import com.vrt.fileprotection.image.ImageLocalSearch;
import com.vrt.fileprotection.image.ImageUniqueCheck;
import com.vrt.fileprotection.image.phash.PerceptualHashHelper;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
@@ -33,7 +33,8 @@ public class ApplicationConfig {
ImageUniqueCheck imageUniqueCheck,
ImageLocalSearch imageLocalSearch,
AudioLocalSearch audioLocalSearch,
AudioFilePathProvider audioFilePathProvider) {
AudioFilePathProvider audioFilePathProvider,
DocumentLocalSearch documentLocalSearch) {
return new com.vrt.NoCopyFileService(
Collections.emptyList(),
@@ -42,7 +43,8 @@ public class ApplicationConfig {
imageUniqueCheck,
imageLocalSearch,
audioLocalSearch,
audioFilePathProvider
audioFilePathProvider,
documentLocalSearch
);
}
}
@@ -22,7 +22,9 @@ public class HandlerConfig {
AuthRequestHandler authRequestHandler,
CompanyHandler companyHandler,
TariffHandler tariffHandler,
TariffInfoHandler tariffInfoHandler
TariffInfoHandler tariffInfoHandler,
ReferralHandler referralHandler,
DaDataHandler daDataHandler
) {
Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login);
@@ -36,6 +38,8 @@ public class HandlerConfig {
map.put(30000, companyHandler);
map.put(30001, tariffHandler);
map.put(30002, tariffInfoHandler);
map.put(30003, referralHandler);
map.put(30004, daDataHandler);
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.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -20,7 +21,13 @@ public class JacksonConfig {
@Bean
@Primary
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();
@@ -0,0 +1,16 @@
package ru.soune.nocopy.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.soune.ReferralLevelProvider;
import ru.soune.ReferralRepo;
import ru.soune.ReferralService;
@Configuration
public class ReferralConfig {
@Bean
public ReferralService referralService(ReferralLevelProvider referralLevelProvider, ReferralRepo referralRepo) {
return new ReferralService(referralLevelProvider, referralRepo);
}
}
@@ -32,6 +32,7 @@ import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.FileSimilarityService;
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.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService;
@@ -73,6 +74,8 @@ public class ApiController {
private final AuthTokenRepository authTokenRepository;
private final CloudStorageService cloudStorageService;
@PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@PathVariable("version") int version) {
@@ -342,15 +345,14 @@ public class ApiController {
errorData));
}
Path filePath = Paths.get(entityResponse.getProtectedFilePath());
// Resource resource = new UrlResource(filePath.toUri());
FileSystemResource resource = new FileSystemResource(filePath);
File file = cloudStorageService.readFileFromStorageByPath(
entityResponse.getProtectedFilePath());
long fileSize = Files.size(filePath);
if (!resource.exists()) {
if (!file.exists()) {
Map<String, Object> errorData = new HashMap<>();
errorData.put("resource", resource.exists());
errorData.put("file", file.exists());
return ResponseEntity.ok().body(new BaseResponse(20004,
MessageCode.FILE_DOWNLOAD_ERROR.getCode(),
@@ -364,8 +366,8 @@ public class ApiController {
.contentLength(fileSize)
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + entityResponse.getOriginalFileName() + "\"")
.body(resource);
"attachment; filename=\"" + entityResponse.getFileName() + "\"")
.body(file);
} catch (FileEntityNotFoundException e) {
Map<String, Object> errorData = new HashMap<>();
errorData.put("fileId", fileId);
@@ -439,33 +441,6 @@ public class ApiController {
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,
String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
@@ -16,6 +16,8 @@ import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.file.FileStorageService;
import org.springframework.http.ContentDisposition;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
@@ -45,10 +47,13 @@ public class FileController {
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
ContentDisposition contentDisposition = ContentDisposition.inline()
.filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8)
.build();
return ResponseEntity.ok()
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + fileEntity.getOriginalFileName() + "\"")
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString())
.body(resource);
}
@@ -1,5 +1,6 @@
package ru.soune.nocopy.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@@ -15,6 +16,14 @@ public class HealtCheckController {
return HttpStatus.OK;
}
@Value("${BUILD_TIME_BACK:unknown}")
private String buildTimeBack;
@GetMapping("/build")
public BuildInfo getBuildInfo() {
return new BuildInfo(buildTimeBack);
}
@GetMapping("/api/debug/memory")
public Map<String, String> getMemoryInfo() {
Runtime runtime = Runtime.getRuntime();
@@ -32,4 +41,12 @@ public class HealtCheckController {
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) {
userDTO.setTariffs(tariffService.getAllTariffs());
userDTO.setTariffs(tariffService.getTariffByAccountType(user));
Long fileOnDisk = user.canManageCompanySettings() ?
fileStatsService.calculateCompanyFileSize(user.getId()):
@@ -116,6 +116,7 @@ public class UserController {
.maxFileOnDisk(tariff.getDiskSize())
.startTariff(personalTariffInfo.getStartTariff())
.endTariff(personalTariffInfo.getEndTariff())
.tokens(personalTariffInfo.getTokens() + personalTariffInfo.getBoughtTokens())
.build();
userDTO.setTariffInfo(infoDTO);
@@ -4,6 +4,7 @@ public enum MessageCode {
SUCCESS(0, "Operation successful"),
REG_EMAIL_EXISTS(1, "Email 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_TOKEN(2, "Invalid token"),
TOKEN_IS_ALIVE(2, "Token is alive"),
@@ -40,6 +41,7 @@ public enum MessageCode {
INTERNAL_ERROR(4, "Internal server error"),
COMPANY_NOT_FOUND(4, "Company not found"),
COMPANY_ALREADY_EXISTS(2, "Company already exists"),
USER_LIMIT_IS_OVER(2, "Over user limits"),
ERROR_TARIFF_INFO(2, "Erorr with tariff info");
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")
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")
private Long protectedFilesCount;
@@ -7,4 +7,10 @@ import lombok.Data;
public class ImageSearchRequest {
@JsonProperty("file_id")
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")
private List<ImageResult> images;
@JsonProperty("images")
public void setImages(List<ImageResult> images) {
if (images != null && images.size() > 5) {
this.images = images.subList(0, 5);
} else {
this.images = images;
}
}
// @JsonProperty("images")
// public void setImages(List<ImageResult> images) {
// this.images = images;
// }
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -42,4 +38,12 @@ public class YandexSearchResponse {
@JsonProperty("host")
private String host;
}
private int page;
private int pageSize;
private int totalResults;
private int totalPages;
}
@@ -0,0 +1,18 @@
package ru.soune.nocopy.dto.referral;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ReferralRequest {
private String token;
private Integer amount;
private Integer pageSize;
private Integer pageNumber;
private String action;
}
@@ -29,4 +29,6 @@ public class RegRequest {
private String password;
private String authToken;
private String referralLink;
}
@@ -46,4 +46,7 @@ public class TariffRequest {
@JsonProperty("action")
private String action;
@JsonProperty("user_token")
private String userToken;
}
@@ -7,11 +7,12 @@ import java.util.List;
@Getter
public enum FileType {
// IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "jfif")),
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")),
VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg",
"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 List<String> allowedExtensions;
@@ -0,0 +1,37 @@
package ru.soune.nocopy.entity.referral;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "referrals")
@Data
public class Referral {
@Id
private Long userId;
@Column(name = "referral_link", unique = true, nullable = false)
private String referralLink;
@Column(name = "inviter_id")
private Long inviterId = 0L;
@Column(name = "level_id")
private String levelId = "bronze";
@Column(name = "total_income")
private int totalIncome = 0;
@Column(name = "is_active")
private boolean isActive = false;
@CreatedDate
@Column(name = "created_at", updatable = false, nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "hold_balance")
private int holdBalance = 0;
}
@@ -0,0 +1,21 @@
package ru.soune.nocopy.entity.referral;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Table(name = "referral_levels")
@Data
public class ReferralLevelEntity {
@Id
private String id;
@Column(nullable = false)
private String name;
@Column(name = "min_invitees")
private int minInvitees;
@Column(name = "reward_percentage")
private int rewardPercentage;
}
@@ -37,4 +37,7 @@ public class Tariff {
@Column(name = "max_users")
private Long maxUsers;
@Column(name = "tariffAccountType")
private String tariffAccountType;
}
@@ -31,6 +31,9 @@ public class TariffInfo {
@Column(name = "tokens")
private Integer tokens;
@Column(name = "bought_tokens")
private Integer boughtTokens = 0;
@ManyToOne
@JoinColumn(name = "tariff_id")
private Tariff tariff;
@@ -2,5 +2,5 @@ package ru.soune.nocopy.entity.tarif;
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.NotFoundAuthToken;
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.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService;
@@ -35,6 +36,8 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
@Override
public BaseResponse handle(BaseRequest request) {
try {
@@ -302,7 +305,7 @@ public class FileEntityHandler implements RequestHandler {
int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete();
if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) {
fileEntityService.deleteFromDisk(fileEntity);
cloudStorageService.deleteFromStorageDisk(fileEntity);
response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId())
.message("File deleted from disk")
@@ -1,5 +1,6 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
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.MessageCode;
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.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.search.SearchApiToYandexResponseMapper;
import ru.soune.nocopy.service.search.YandexSearchService;
import ru.soune.nocopy.service.search.SearchImageService;
import ru.soune.nocopy.service.search.GoogleVisionSearchService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.util.Map;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeoutException;
@Slf4j
@Component
@@ -27,7 +30,7 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final YandexSearchService yandexSearchService;
private final SearchImageService searchImageService;
private final GoogleVisionSearchService googleVisionSearchService;
@@ -37,8 +40,6 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final TariffInfoService tariffInfoService;
private final SearchApiToYandexResponseMapper mapper = new SearchApiToYandexResponseMapper();
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
@@ -58,13 +59,54 @@ public class ImageFoundRequestHandler implements RequestHandler {
//TODO uncommited when add billing
// 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);
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(),
MessageCode.SUCCESS.getDescription(),mapper.mapToYandexResponse(yandexReverseSearchResponse));
MessageCode.SUCCESS.getDescription(), finalResponse);
}
}
@@ -0,0 +1,90 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.ReferralInvitee;
import ru.soune.ReferralLevel;
import ru.soune.ReferralService;
import ru.soune.ReferralStat;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.referral.ReferralRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import java.util.List;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class ReferralHandler implements RequestHandler {
private final ReferralService referralService;
private final AuthTokenRepository authTokenRepository;
private final ObjectMapper mapper = new ObjectMapper();
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ReferralRequest referralRequest = mapper.convertValue(request.getMessageBody(), ReferralRequest.class);
String action = referralRequest.getAction();
String token = referralRequest.getToken();
AuthToken authToken = authTokenRepository.findByToken(token).orElseThrow();
switch (action) {
case "refill":
return handleRefill(request, authToken.getUser().getId(), referralRequest.getAmount());
case "levels":
return handleReferralLevels(request);
case "level":
return handleUserLevel(request, authToken.getUser().getId());
case "userStats" :
return handleUserStats(request, authToken.getUser().getId());
case "invitees":
return handleInvitees(referralRequest, request, authToken.getUser().getId());
}
return null;
}
private BaseResponse handleRefill(BaseRequest request, Long userId, Integer amount) {
referralService.onUserAccountRefill(userId, amount);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), Map.of("account", "refill"));
}
private BaseResponse handleReferralLevels(BaseRequest request) {
List<ReferralLevel> referralLevels = referralService.getReferralLevels();
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), referralLevels);
}
private BaseResponse handleUserLevel(BaseRequest request, Long userId) {
ReferralLevel referralLevelForUser = referralService.getReferralLevelForUser(userId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), referralLevelForUser);
}
private BaseResponse handleUserStats(BaseRequest request, Long userId) {
ReferralStat referralStatForUser = referralService.getReferralStatForUser(userId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), referralStatForUser);
}
private BaseResponse handleInvitees(ReferralRequest referralRequest, BaseRequest request, Long userId) {
List<ReferralInvitee> inviteeForUser = referralService.getInviteeForUser(userId, referralRequest.getPageSize(),
referralRequest.getPageNumber());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), inviteeForUser);
}
}
@@ -6,13 +6,16 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import ru.soune.ReferralService;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.RegAnswer;
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.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.ReferralJpaRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.mail.EmailService;
@@ -34,6 +37,10 @@ public class RegRequestHandler implements RequestHandler {
private final EmailService emailService;
private final ReferralService referralService;
private final ReferralJpaRepository referralJpaRepository;
@Override
public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -49,6 +56,19 @@ public class RegRequestHandler implements RequestHandler {
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");
regRequestValidator.validate(regRequest, bindingResult);
@@ -76,12 +96,15 @@ public class RegRequestHandler implements RequestHandler {
RegAnswer regAnswer = new RegAnswer();
regAnswer.setUserId(authToken.getUser().getId());
regAnswer.setToken(token);
regAnswer.setVerified(true);
regAnswer.setActive(true);
if (regRequest.getAccountType().equals("b2c")) {
referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink());
}
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
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.TariffRequest;
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 java.util.List;
@@ -24,6 +27,8 @@ public class TariffHandler implements RequestHandler {
private final TariffService tariffService;
private final AuthTokenRepository authTokenRepository;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
try {
@@ -113,7 +118,9 @@ public class TariffHandler implements RequestHandler {
}
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(),
MessageCode.SUCCESS.getDescription(),
@@ -15,6 +15,8 @@ import java.util.Optional;
public interface FileEntityRepository extends JpaRepository<FileEntity, String> {
List<FileEntity> findByUserId(Long userId);
FileEntity findByIdAndUserId(String id, Long userId);
Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash);
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
@@ -0,0 +1,43 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.referral.Referral;
import java.util.List;
@Repository
public interface ReferralJpaRepository extends JpaRepository<Referral, Long> {
Referral findByReferralLink(String link);
Referral findByUserId(Long userId);
List<Referral> findByInviterId(Long inviterId);
@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);
@Modifying
@Query("UPDATE Referral r SET r.totalIncome = r.totalIncome + :amount WHERE r.userId = :userId")
void increaseIncome(@Param("userId") Long userId, @Param("amount") int amount);
@Modifying
@Query("UPDATE Referral r SET r.levelId = :newLevelId WHERE r.userId = :userId")
void updateLevel(@Param("userId") Long userId, @Param("newLevelId") String newLevelId);
@Modifying
@Query("UPDATE Referral r SET r.isActive = true WHERE r.userId = :userId")
void activateUser(@Param("userId") Long userId);
@Query("SELECT COUNT(r) FROM Referral r WHERE r.inviterId = :userId AND r.isActive = true")
int countActiveInvitees(@Param("userId") Long userId);
@Query("SELECT COUNT(r) FROM Referral r WHERE r.inviterId = :userId")
int countTotalInvitees(@Param("userId") Long userId);
}
@@ -0,0 +1,13 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.referral.ReferralLevelEntity;
import java.util.List;
@Repository
public interface ReferralLevelRepository extends JpaRepository<ReferralLevelEntity, Long> {
List<ReferralLevelEntity> findAllByOrderByMinInviteesAsc();
ReferralLevelEntity findById(String id);
}
@@ -12,5 +12,6 @@ import java.util.Optional;
public interface TariffRepository extends JpaRepository<Tariff, Long> {
Optional<Tariff> findByType(String type);
List<Tariff> findAllByOrderByPriceAsc();
List<Tariff> findByTariffAccountType(String typeAccount);
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.repository.ProtectedFileCheckRepository;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
@@ -15,7 +16,10 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@Component
@@ -35,6 +39,7 @@ public class FileCleanupService {
log.info("Starting cleanup of expired temporary files");
cleanupOldPeriods();
cleanupTempFilesNightly();
Path tempDir = Paths.get(basePath, "temp");
if (!Files.exists(tempDir)) {
@@ -98,4 +103,50 @@ public class FileCleanupService {
protectedFileCheckRepository.saveAll(checks);
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.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -42,6 +43,8 @@ public class FileEntityService {
private final UserRepository userRepository;
private final CloudStorageService cloudStorageService;
@Transactional(noRollbackFor = DuplicateImageException.class)
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
log.info("Creating FileEntity for upload session: {}", session.getUploadId());
@@ -55,7 +58,7 @@ public class FileEntityService {
Map<String, Long> imageHash = Map.of();
if (session.getFileType().startsWith("image")) {
if (!session.getFileType().startsWith("video")) {
imageHash = imageHashService.calculateHash(filePath);
List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash(
imageHash.get("hi"), imageHash.get("low"));
@@ -116,43 +119,22 @@ public class FileEntityService {
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)
public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
List<FileEntity> allFiles = fileEntityRepository.findByUserIdAndStatus(
userId, FileStatus.ACTIVE);
User user = userRepository.findById(userId).orElseThrow();
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 end = Math.min(start + pageSize, allFiles.size());
@@ -188,19 +170,6 @@ public class FileEntityService {
.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 {
if (fileEntity.getImageHash() != null) {
fileEntity.setImageHash(null);
@@ -217,16 +186,8 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity);
}
public void deleteFromDisk(FileEntity fileEntity) throws IOException {
Path path = Paths.get(fileEntity.getFilePath());
if (!Files.exists(path)) {
return;
}
Files.delete(path);
fileEntityRepository.delete(fileEntity);
public void deleteFromDisk(String deleteFilePath) throws IOException {
Files.deleteIfExists(Paths.get(deleteFilePath));
}
@Transactional(readOnly = true)
@@ -244,7 +205,7 @@ public class FileEntityService {
}
@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)
.orElseThrow(() -> new RuntimeException("File not found: " + id));
@@ -263,14 +224,14 @@ public class FileEntityService {
fileEntity.setFileExtension(extension);
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);
for (FileEntity fileEntity : fileByUserIdAndStatus) {
deleteFromDisk(fileEntity);
cloudStorageService.deleteFromStorageDisk(fileEntity);
}
}
@@ -282,24 +243,6 @@ public class FileEntityService {
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 {
Path originalPath = Paths.get(fileEntity.getFilePath());
@@ -336,6 +279,18 @@ public class FileEntityService {
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) {
try {
return Files.exists(Paths.get(filePath));
@@ -346,7 +301,7 @@ public class FileEntityService {
}
private String determineFileExtension(String fileExt, FileEntity fileEntity) {
if (fileExt != null && !fileExt.trim().isEmpty()) {
if (fileExt != null) {
return fileExt;
} else {
return fileEntity.getFileExtension();
@@ -376,7 +331,9 @@ public class FileEntityService {
.existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId())
.protectStatus(fileEntity.getProtectionStatus().toString())
.fileName(fileEntity.getOriginalFileName())
.fileName(fileEntity.getOriginalFileName().replace("." +
fileEntity.getFileExtension(), "") + "_nocopy_protected" +
"." + fileEntity.getFileExtension())
.ownerName(user.getFullName())
.ownerEmail(user.getEmail())
.ownerCompany(user.getCompanyName())
@@ -386,16 +343,4 @@ public class FileEntityService {
.protectedFilePath(fileEntity.getProtectedFilePath())
.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))
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
.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();
}
@@ -2,6 +2,7 @@ package ru.soune.nocopy.service.file;
import org.springframework.web.multipart.MultipartFile;
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.FileUploadSession;
@@ -19,5 +20,5 @@ public interface FileUploadService {
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.fileprotection.FileProtector;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.file.FileEntityService;
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.TariffInfoService;
import ru.soune.nocopy.util.FileUtil;
@@ -83,6 +85,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private final TariffInfoService tariffInfoService;
private final CloudStorageService cloudStorageService;
@PostConstruct
public void init() {
try {
@@ -177,7 +181,6 @@ public class FileUploadServiceImpl implements FileUploadService {
}
@Override
@Transactional
public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile, Integer findSimilar) {
FileUploadSession session = sessionRepository.findById(uploadId)
@@ -219,7 +222,8 @@ public class FileUploadServiceImpl implements FileUploadService {
@Async("fileUploadTaskExecutor")
@Transactional
@Override
public void completeFileProcessingAsync(FileUploadSession session, FileStatus status) {
public FileEntity completeFileProcessingAsync(FileUploadSession session, FileStatus status) {
FileEntity saved = null;
try {
Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath);
@@ -238,7 +242,7 @@ public class FileUploadServiceImpl implements FileUploadService {
.status(status)
.build();
FileEntity saved = fileEntityRepository.save(fileEntity);
saved = fileEntityRepository.save(fileEntity);
if (session.getFileType().equals("image")) {
Map<String, Long> hash = imageHashService.calculateHash(filePath);
@@ -248,16 +252,13 @@ public class FileUploadServiceImpl implements FileUploadService {
cleanupSessionFiles(session);
if (status != FileStatus.TEMP) {
noCopyFileService.addFile(fileUtil.createFileInfo(fileEntity));
}
log.info("File processing completed for session: {}", session.getUploadId());
} catch (Exception e) {
log.error("Failed to complete file processing for session {}: {}",
session.getUploadId(), e.getMessage(), e);
}
return saved;
}
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,
Integer findSimilar) {
String chunkPath = null;
@@ -329,8 +333,20 @@ public class FileUploadServiceImpl implements FileUploadService {
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) {
String fileType = session.getFileType();
@@ -377,13 +393,13 @@ public class FileUploadServiceImpl implements FileUploadService {
switch (fileType) {
case "video":
cost = TariffConstants.VIDEO_TOKEN_VALUE;
return TariffConstants.VIDEO_TOKEN_VALUE;
case "image":
cost = TariffConstants.IMAGE_TOKEN_VALUE;
return TariffConstants.IMAGE_TOKEN_VALUE;
case "audio":
cost = TariffConstants.AUDIO_TOKEN_VALUE;
case "pdf":
cost = TariffConstants.PDF_TOKEN_VALUE;
return TariffConstants.AUDIO_TOKEN_VALUE;
case "document":
return TariffConstants.PDF_TOKEN_VALUE;
}
return cost;
@@ -7,7 +7,10 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.cloud.CloudStorageService;
import java.io.File;
import java.io.IOException;
@@ -24,22 +27,55 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
private final FileEntityService fileEntityService;
private final CloudStorageService cloudStorageService;
private final FileEntityRepository fileEntityRepository;
@Nullable
@Override
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
@Override
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
@Override
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
@@ -74,7 +110,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override
public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
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);
@@ -86,7 +126,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override
public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
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);
@@ -98,7 +142,25 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override
public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
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();
} catch (Exception e) {
log.error("Failed to create protected file: {}", id, e);
@@ -0,0 +1,83 @@
package ru.soune.nocopy.service.referral;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.ReferralLevel;
import ru.soune.ReferralLevelProvider;
import ru.soune.nocopy.entity.referral.ReferralLevelEntity;
import ru.soune.nocopy.repository.ReferralLevelRepository;
import java.util.List;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class ReferralLevelProviderImpl implements ReferralLevelProvider {
private final ReferralLevelRepository referralLevelRepository;
@Override
public List<ReferralLevel> getAvailableReferralLevels() {
List<ReferralLevelEntity> entities = referralLevelRepository.findAll();
return entities.stream()
.map(this::createReferralLevel)
.collect(Collectors.toList());
}
@Override
public String getInitialReferralLevelId() {
return "bronze";
}
@Override
public ReferralLevel getReferralLevelById(String levelId) {
return createReferralLevel(referralLevelRepository.findById(levelId));
}
private ReferralLevel createReferralLevel(ReferralLevelEntity entity) {
return new ReferralLevel() {
@Override
public String getId() {
return entity.getId();
}
@Override
public String getName() {
return entity.getName();
}
@Override
public int getShare() {
return entity.getRewardPercentage();
}
@Override
public int getMinInvitee() {
return entity.getMinInvitees();
}
@Override
public int getMaxInvitee() {
switch (entity.getId()) {
case "bronze": return 5;
case "silver": return 15;
case "gold": return 50;
case "platinum": return Integer.MAX_VALUE;
default: return Integer.MAX_VALUE;
}
}
@Override
public String getNext() {
switch (entity.getId()) {
case "bronze": return "silver";
case "silver": return "gold";
case "gold": return "platinum";
default: return null;
}
}
};
}
}
@@ -0,0 +1,189 @@
package ru.soune.nocopy.service.referral;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.Referral;
import ru.soune.ReferralInvitee;
import ru.soune.ReferralRepo;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.ReferralJpaRepository;
import ru.soune.nocopy.repository.UserRepository;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Repository
@RequiredArgsConstructor
@Slf4j
@Transactional
public class ReferralRepoImpl implements ReferralRepo {
private final ReferralJpaRepository referralRepository;
private final UserRepository userRepository;
@Override
public @NotNull String getUserReferralLink(long userId) {
ru.soune.nocopy.entity.referral.Referral referral =
referralRepository.findByUserId(userId);
if (referral != null && referral.getReferralLink() != null &&
!referral.getReferralLink().isEmpty()) {
return referral.getReferralLink();
}
String newLink = "ref-" + userId + "-" + UUID.randomUUID().toString().substring(0, 8);
if (referral == null) {
referral = new ru.soune.nocopy.entity.referral.Referral();
referral.setUserId(userId);
referral.setLevelId("bronze");
referral.setActive(false);
}
referral.setReferralLink(newLink);
referralRepository.save(referral);
return newLink;
}
@Override
public @NotNull Long getUserIdByLink(@NotNull String link) {
ru.soune.nocopy.entity.referral.Referral referral = referralRepository.findByReferralLink(link);
return referral != null ? referral.getUserId() : 0L;
}
@Override
public @Nullable Long getInviterIdForUser(long userId) {
ru.soune.nocopy.entity.referral.Referral referral = referralRepository.findByUserId(userId);
return referral != null ? referral.getInviterId() : null;
}
@Override
public @NotNull String getReferralLevelForUser(long userId) {
ru.soune.nocopy.entity.referral.Referral referral = referralRepository.findByUserId(userId);
return referral != null ? referral.getLevelId() : "";
}
@Override
public @NotNull Referral getReferralByUserId(long userId) {
ru.soune.nocopy.entity.referral.Referral entity = referralRepository.findByUserId(userId);
return new Referral() {
@Override
public long getUserId() { return entity.getUserId(); }
@Override
public String getReferralLink() { return entity.getReferralLink(); }
@Override
public Long getInviter() {
return entity.getInviterId() != null ? entity.getInviterId() : 0L;
}
@Override
public String getCurrentLevel() { return entity.getLevelId(); }
@Override
public int getTotalIncome() { return entity.getTotalIncome(); }
@Override
public int getAvailableIncome() {
Integer holdBalance = entity.getHoldBalance();
return entity.getTotalIncome() - (holdBalance != null ? holdBalance : 0);
}
@Override
public int getHoldBalance() {
Integer holdBalance = entity.getHoldBalance();
return holdBalance != null ? holdBalance : 0;
}
@Override
public boolean getActive() { return entity.isActive(); }
};
}
@Override
public @NotNull List<ReferralInvitee> getReferralInvitees(long userId, int pageSize, int pageNumber) {
Pageable pageable = PageRequest.of(pageNumber, pageSize);
List<ru.soune.nocopy.entity.referral.Referral> referrals = referralRepository.findByInviterId(userId, pageable);
List<ReferralInvitee> result = new ArrayList<>();
for (ru.soune.nocopy.entity.referral.Referral referral : referrals) {
User user = userRepository.findById(referral.getUserId()).orElse(null);
String email = user != null ? user.getEmail() : "";
boolean isActive = referral.isActive();
String regDate = referral.getCreatedAt() != null ?
referral.getCreatedAt().toString() : "";
try {
Constructor<ReferralInvitee> constructor = ReferralInvitee.class.getDeclaredConstructor(
String.class, boolean.class, String.class
);
ReferralInvitee invitee = constructor.newInstance(email, isActive, regDate);
result.add(invitee);
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
@Override
public void createReferralEntity(@NotNull Referral entity) {
ru.soune.nocopy.entity.referral.Referral referral = new ru.soune.nocopy.entity.referral.Referral();
referral.setUserId(entity.getUserId());
referral.setReferralLink(entity.getReferralLink());
referral.setInviterId(entity.getInviter());
referral.setLevelId(entity.getCurrentLevel());
referral.setTotalIncome(entity.getTotalIncome());
referral.setHoldBalance(entity.getHoldBalance());
referral.setActive(entity.getActive());
referralRepository.save(referral);
}
@Override
public void increaseIncome(long userId, int transferAmount) {
referralRepository.increaseIncome(userId, transferAmount);
}
@Override
public boolean activateUser(long userId) {
try {
referralRepository.activateUser(userId);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public int getActiveInviteeForUser(long userId) {
return referralRepository.countActiveInvitees(userId);
}
@Override
public int getTotalInviteeForUser(long userId) {
return referralRepository.countTotalInvitees(userId);
}
@Override
public void upgradeReferralLevelForUser(long userId, @NotNull String newLevelId) {
referralRepository.updateLevel(userId, newLevelId);
}
}
@@ -1,5 +1,6 @@
package ru.soune.nocopy.service.register;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
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.RegRequest;
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.TariffType;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.Permission;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.MaxUserCountIsOver;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository;
@@ -176,20 +179,31 @@ public class AuthService {
private void createIndividualCompanyAccount(RegRequest request, User user) {
String authToken = request.getAuthToken();
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();
TariffInfo tariffInfo = company.getTariffInfo();
Long maxUsers = tariffInfo.getTariff().getMaxUsers();
long currentCountUsers = userRepository.countByCompanyId(company.getId());
//TODO add exception
if (currentCountUsers < maxUsers) {
throw new RuntimeException();
if (company == null) {
throw new IllegalStateException("Not have company");
}
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);
}
}
@@ -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.setEndTariff(endTariff);
tariffInfo.setTokens(tokens);
tariffInfo.setBoughtTokens(0);
return tariffInfoRepository.save(tariffInfo);
}
@@ -52,10 +53,15 @@ public class TariffInfoService {
}
Integer tokens = activeTariffInfo.getTokens();
Integer boughtTokens = activeTariffInfo.getBoughtTokens();
if (tokens - value >= 0) {
activeTariffInfo.setTokens(activeTariffInfo.getTokens() - value);
tariffInfoRepository.save(activeTariffInfo);
} else if (boughtTokens - value >= 0) {
activeTariffInfo.setTokens(activeTariffInfo.getBoughtTokens() - value);
tariffInfoRepository.save(activeTariffInfo);
} else {
throw new TariffNotFoundException("User not have tokens for protect");
@@ -118,7 +124,8 @@ public class TariffInfoService {
tariffInfo.getStartTariff(),
tariffInfo.getEndTariff(),
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().getMaxFilesCount());
}
@@ -101,40 +101,44 @@ public class TariffInitializationService {
tariff.setType(type.name());
switch (type) {
case DEMO:
tariff.setName("demo");
tariff.setPrice(demoPrice);
tariff.setTokens(demoTokens);
tariff.setMaxFilesCount(demoMaxFileCount);
case FREE:
tariff.setName("free");
tariff.setPrice(0);
tariff.setTokens(30);
tariff.setMaxFilesCount(10);
tariff.setDiskSize(1024L * 1024 * 100);
tariff.setMaxUsers(demoUserCount);
tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
break;
case START:
tariff.setName("start");
tariff.setPrice(startPrice);
tariff.setTokens(startTokens);
tariff.setMaxFilesCount(startMaxFileCount);
tariff.setPrice(1200);
tariff.setTokens(300);
tariff.setMaxFilesCount(80);
tariff.setDiskSize(1024L * 1024 * 500);
tariff.setMaxUsers(startUserCount);
tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
break;
case PRO:
tariff.setName("pro");
tariff.setPrice(proPrice);
tariff.setTokens(proTokens);
tariff.setMaxFilesCount(proMaxFileCount);
tariff.setPrice(4250);
tariff.setTokens(900);
tariff.setMaxFilesCount(300);
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(proUserCount);
tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
break;
case ENTERPRISE:
tariff.setName("premium");
tariff.setPrice(enterpricePrice);
tariff.setTokens(enterpriceTokens);
tariff.setMaxFilesCount(enterpriceMaxFileCount);
case STUDIO:
tariff.setName("studio");
tariff.setPrice(9750);
tariff.setTokens(3000);
tariff.setMaxFilesCount(1000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(enterpriceUserCount);
tariff.setMaxUsers(2L);
tariff.setTariffAccountType("b2c");
break;
case BASIC:
@@ -144,6 +148,37 @@ public class TariffInitializationService {
tariff.setMaxFilesCount(enterpriceMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
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;
}
@@ -6,6 +6,7 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.TariffRepository;
@@ -32,6 +33,15 @@ public class TariffService {
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() {
return tariffRepository.findAllByOrderByPriceAsc()
.stream()
@@ -41,6 +41,8 @@ public class FileUtil {
return FileProtector.Type.VIDEO;
} else if (mimeType.startsWith("audio")) {
return FileProtector.Type.AUDIO;
} else if (mimeType.startsWith("document")) {
return FileProtector.Type.DOC;
} else {
return FileProtector.Type.IMAGE;
}
+22 -1
View File
@@ -51,6 +51,10 @@ spring:
enabled: true
resolve-lazily: false
dadata:
api-key: 0109fcc9d5a0a68c08f3b12ba7e2a620ea438ef7
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party"
file:
storage:
base-path: ${FILE_STORAGE_PATH:/data/uploads}
@@ -72,6 +76,15 @@ security:
server:
port: ${SERVER_PORT:8080}
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:
level:
@@ -87,9 +100,16 @@ yandex:
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
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:
api-key: ${SEARCHAPI_API_KEY:2xBChafVmojL7G1ZXzzUuA6o}
api-key: ${SEARCHAPI_API_KEY:rdwNzCr2eybYVbU1KrGTJAgm}
reverse-image-url: "https://searchapi.io/api/v1/search"
app:
@@ -129,3 +149,4 @@ tarif:
enterprice-max-files-count: 50
enterprice-disk-size: 500
enterprice-user-count: 80