use different WebDAV urls

This commit is contained in:
gulnaz 2025-01-15 15:52:03 +03:00
parent c17be4426f
commit 58e5b4789e
8 changed files with 191 additions and 75 deletions

View file

@ -3,6 +3,7 @@ package ervu.client.fileupload;
import java.io.IOException;
import java.io.InputStream;
import java.net.Authenticator;
import java.net.ConnectException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URLEncoder;
@ -10,14 +11,26 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import javax.annotation.PostConstruct;
import com.github.sardine.DavResource;
import com.github.sardine.Sardine;
import com.github.sardine.SardineFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import ervu.model.webdav.Server;
import org.apache.http.client.ClientProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@ -38,21 +51,46 @@ import org.springframework.web.multipart.MultipartFile;
public class WebDavClient {
private static final Logger logger = LoggerFactory.getLogger(WebDavClient.class);
@Value("${file.webdav.upload.username}")
@Value("${webdav.urls}")
private String[] urls;
@Value("${webdav.username}")
private String username;
@Value("${file.webdav.upload.password}")
@Value("${webdav.password}")
private String password;
@Value("${webdav.bad_servers.cache.expire.seconds:120}")
private long cacheExpireSec;
private List<Server> servers;
private LoadingCache<String, String> badServersCache;
private final AtomicInteger counter = new AtomicInteger(0);
@PostConstruct
public void init() {
CacheLoader<String, String> loader = new CacheLoader<>() {
@Override
public String load(String key) {
return key;
}
};
badServersCache = CacheBuilder.newBuilder()
.expireAfterAccess(cacheExpireSec, TimeUnit.SECONDS)
.build(loader);
logger.info("WebDAV urls: {}", Arrays.asList(urls));
servers = Arrays.stream(urls)
.map(url -> new Server(url, ZonedDateTime.now().toInstant().toEpochMilli()))
.toList();
}
@Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
public boolean uploadFile(String url, MultipartFile multipartFile) {
public String uploadFile(MultipartFile multipartFile) {
String fileName = getNewFilename(multipartFile.getOriginalFilename());
Sardine sardine = initClient(username, password);
try {
sardine.put(url, multipartFile.getBytes());
return sardine.exists(url);
return putAndGetUrl(multipartFile.getBytes(), fileName, sardine);
}
catch (IOException e) {
throw new RuntimeException("Failed to put or check file in WebDAV", e);
throw new RuntimeException("Failed to put file into WebDAV", e);
}
finally {
try {
@ -64,6 +102,60 @@ public class WebDavClient {
}
}
private String getNewFilename(String filename) {
int lastIndexOf = filename.lastIndexOf(".");
String fileExtension = lastIndexOf == -1 ? "" : filename.substring(lastIndexOf);
return UUID.randomUUID() + fileExtension;
}
public String putAndGetUrl(byte[] fileBytes, String fileName, Sardine client) throws IOException {
if (badServersCache.size() == urls.length) {
return null;
}
Server server;
if (urls.length == 1) {
server = servers.get(0);
}
else {
Predicate<Server> isNotBad = s -> badServersCache.getIfPresent(s.getUrl()) == null;
server = servers.stream()
.filter(isNotBad.and(s -> servers.indexOf(s) == index()))
.findFirst()
.orElseGet(() -> servers.stream()
.filter(isNotBad)
.min(Comparator.comparing(Server::getLastCallTime))
.orElse(null));
}
if (server == null) {
return null;
}
boolean isBad = false;
String serverUrl = server.getUrl();
String fileUploadUrl = serverUrl + "/" + fileName;
try {
client.put(fileUploadUrl, fileBytes);
server.setLastCallTime(System.currentTimeMillis());
}
catch (ConnectException | ClientProtocolException ignore) {
isBad = true;
}
if (isBad) {
badServersCache.getUnchecked(serverUrl);
return putAndGetUrl(fileBytes, fileName, client);
}
return fileUploadUrl;
}
private int index() {
counter.compareAndSet(Integer.MAX_VALUE, 0);
return counter.getAndIncrement() % urls.length;
}
@Retryable(value = {IOException.class, InterruptedException.class},
backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
public ResponseEntity<Resource> webDavDownloadFile(String url) {
@ -109,8 +201,7 @@ public class WebDavClient {
}
@Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
public void deleteFilesOlderThan(long seconds, String url, String username, String password,
String... extensions) {
public void deleteFilesOlderThan(long seconds, String url, String... extensions) throws IOException {
Sardine sardine = initClient(username, password);
try {
@ -124,14 +215,11 @@ public class WebDavClient {
sardine.delete(url + davResource.getPath());
}
catch (IOException e) {
throw new RuntimeException("Failed to delete file " + davResource.getName(), e);
logger.error("Failed to delete file {}", davResource.getName(), e);
}
}
});
}
catch (IOException e) {
throw new RuntimeException("Failed to delete old files from WebDAV", e);
}
finally {
try {
sardine.shutdown();

View file

@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil;
/**
* @author Alexandr Shalaginov
@ -28,12 +30,15 @@ public class EmployeeInfoFileUploadController {
@RequestParam("file") MultipartFile multipartFile,
@RequestHeader("X-Employee-Info-File-Form-Type") String formType,
@RequestHeader("Client-Time-Zone") String clientTimeZone) {
UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair();
String offset = ZonedDateTime.now(TimeZone.getTimeZone(clientTimeZone).toZoneId())
.getOffset().getId();
if (userIdsPair != null) {
String offset = ZonedDateTime.now(TimeZone.getTimeZone(clientTimeZone).toZoneId())
.getOffset().getId();
if (this.fileUploadService.saveEmployeeInformationFile(multipartFile, formType, offset)) {
return ResponseEntity.ok("File successfully uploaded.");
if (this.fileUploadService.saveEmployeeInformationFile(multipartFile, formType, offset, userIdsPair)) {
return ResponseEntity.ok("File successfully uploaded.");
}
}
return ResponseEntity.internalServerError().body("An error occurred while uploading file.");

View file

@ -0,0 +1,27 @@
package ervu.model.webdav;
/**
* @author gulnaz
*/
public class Server {
private final String url;
private long lastCallTime;
public Server(String url, long lastCallTime) {
this.url = url;
this.lastCallTime = lastCallTime;
}
public String getUrl() {
return url;
}
public long getLastCallTime() {
return lastCallTime;
}
public void setLastCallTime(long lastCallTime) {
this.lastCallTime = lastCallTime;
}
}

View file

@ -36,7 +36,6 @@ import ru.micord.ervu.security.esia.model.PersonModel;
import ru.micord.ervu.security.esia.service.UlDataService;
import ru.micord.ervu.security.esia.token.EsiaTokensStore;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil;
import ru.micord.ervu.service.InteractionService;
import static ervu.enums.FileStatusCode.FILE_CLEAN;
@ -61,8 +60,6 @@ public class EmployeeInfoFileUploadService {
@Value("${av.kafka.message.topic.name}")
private String kafkaTopicName;
@Value("${file.webdav.upload.url:http://localhost:5757}")
private String url;
public EmployeeInfoFileUploadService(
WebDavClient webDavClient,
@ -78,29 +75,32 @@ public class EmployeeInfoFileUploadService {
}
public boolean saveEmployeeInformationFile(MultipartFile multipartFile, String formType,
String offset) {
String offset, UserIdsPair userIdsPair) {
if (!isValid(multipartFile)) {
return false;
}
String fileUploadUrl = this.url + "/" + getNewFilename(multipartFile.getOriginalFilename());
LocalDateTime now = LocalDateTime.now();
String fileId = UUID.randomUUID().toString();
String fileName = multipartFile.getOriginalFilename();
EmployeeInfoFileFormType employeeInfoFileFormType = EmployeeInfoFileFormType.valueOf(formType);
String esiaUserId = userIdsPair.getEsiaUserId();
String ervuId = userIdsPair.getErvuId();
String accessToken = EsiaTokensStore.getAccessToken(esiaUserId);
EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken);
PersonModel personModel = employeeModel.getPerson();
if (this.webDavClient.uploadFile(fileUploadUrl, multipartFile)) {
FileStatus fileStatus = new FileStatus();
fileStatus.setStatus("Загрузка");
String fileUploadUrl = this.webDavClient.uploadFile(multipartFile);
FileStatus fileStatus = new FileStatus();
fileStatus.setStatus(fileUploadUrl == null ? "Невозможно проверить файл ЛК РП" : "Загрузка");
LocalDateTime now = LocalDateTime.now();
interactionService.setStatus(fileId, fileStatus.getStatus(), fileName,
employeeInfoFileFormType.getFilePatternCode(), Timestamp.valueOf(now),
convertToFio(personModel.getFirstName(), personModel.getMiddleName(), personModel.getLastName()),
ervuId);
if (fileUploadUrl != null) {
fileStatus.setCode(FILE_UPLOADED.getCode());
fileStatus.setDescription("Файл принят до проверки на вирусы");
String fileId = UUID.randomUUID().toString();
String fileName = multipartFile.getOriginalFilename();
EmployeeInfoFileFormType employeeInfoFileFormType = EmployeeInfoFileFormType.valueOf(
formType);
UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair();
String esiaUserId = userIdsPair.getEsiaUserId();
String ervuId = userIdsPair.getErvuId();
String accessToken = EsiaTokensStore.getAccessToken(esiaUserId);
EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken);
PersonModel personModel = employeeModel.getPerson();
String departureDateTime = now.format(DateTimeFormatter.ofPattern(FORMAT));
String jsonMessage = getJsonKafkaMessage(
employeeInfoKafkaMessageService.getKafkaMessage(
@ -117,17 +117,10 @@ public class EmployeeInfoFileUploadService {
personModel
)
);
interactionService.setStatus(fileId, fileStatus.getStatus(), fileName,
employeeInfoFileFormType.getFilePatternCode(), Timestamp.valueOf(now),
convertToFio(personModel.getFirstName(), personModel.getMiddleName(),
personModel.getLastName()
),
ervuId
);
return sendMessage(jsonMessage);
}
else {
logger.error("Fail upload file: {}", multipartFile.getOriginalFilename());
logger.error("Failed to upload file: {}", fileName);
return false;
}
}
@ -172,27 +165,15 @@ public class EmployeeInfoFileUploadService {
try {
this.kafkaTemplate.send(record).get();
logger.debug("Success send record: {}", record);
logger.debug("Successfully sent record: {}", record);
return true;
}
catch (Exception exception) {
logger.error("Fail send message.", exception);
logger.error("Failed to send message", exception);
return false;
}
}
private String getNewFilename(String oldFilename) {
return UUID.randomUUID() + getFileExtension(oldFilename);
}
private String getFileExtension(String filename) {
int lastIndexOf = filename.lastIndexOf(".");
if (lastIndexOf == -1) {
return "";
}
return filename.substring(lastIndexOf);
}
private String getJsonKafkaMessage(EmployeeInfoKafkaMessage employeeInfoKafkaMessage) {
ObjectMapper mapper = new ObjectMapper();
try {

View file

@ -1,7 +1,12 @@
package ervu.service.scheduler;
import java.io.IOException;
import java.util.Arrays;
import ervu.client.fileupload.WebDavClient;
import net.javacrumbs.shedlock.core.SchedulerLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@ -11,15 +16,11 @@ import org.springframework.stereotype.Service;
*/
@Service
public class WebDavSchedulerService {
private static final Logger logger = LoggerFactory.getLogger(WebDavSchedulerService.class);
private final WebDavClient webDavClient;
@Value("${file.webdav.upload.url}")
private String url;
@Value("${file.webdav.upload.username}")
private String username;
@Value("${file.webdav.upload.password}")
private String password;
@Value("${webdav.urls}")
private String[] urls;
@Value("${file.webdav.lifetime.seconds:300}")
private long fileLifetime;
@Value("${file.webdav.extensions:}")
@ -30,8 +31,15 @@ public class WebDavSchedulerService {
}
@Scheduled(cron = "${webdav.cleanup.cron:0 0 0 * * *}")
@SchedulerLock
@SchedulerLock(name = "webDavOldFilesCleaning")
public void deleteOldFiles() {
webDavClient.deleteFilesOlderThan(fileLifetime, url, username, password, fileExtensions);
Arrays.stream(urls).forEach(url -> {
try {
webDavClient.deleteFilesOlderThan(fileLifetime, url, fileExtensions);
}
catch (IOException e) {
logger.error("Failed to clean up WebDAV on {}", url, e);
}
});
}
}

View file

@ -790,9 +790,14 @@ JBPM использует 3 корневых категории логирова
- `ERVU_FILE_UPLOAD_MAX_FILE_SIZE` - определяет максимальный размер загружаемого файла в байтах. Указывает предел размера для каждого индивидуального файла, который может быть загружен. Если файл превышает этот размер, загрузка будет прервана, и может быть вызвано исключение.
- `ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE` - устанавливает максимальный общий размер всех файлов в одном многозадачном запросе в байтах. Это ограничение на весь запрос, включающий данные и файлы. Если общий размер запроса превышает этот параметр, загрузка файлов будет остановлена.
- `ERVU_FILE_UPLOAD_FILE_SIZE_THRESHOLD` - указывает размер (в байтах), при достижении которого файл будет записан во временное хранилище на диск. Это позволяет улучшить производительность, исключая непосредственную запись мелких файлов на диск, если они не превышают указанного порога. Файлы, меньшие этого значения, могут быть сохранены в памяти.
- `FILE_WEBDAV_UPLOAD_URL` - url для подключения к WebDav
- `FILE_WEBDAV_UPLOAD_USERNAME` - логин пользователя для подключения к WebDav
- `FILE_WEBDAV_UPLOAD_PASSWORD` - пароль пользователя для подключения к WebDav
- `WEBDAV_URLS` - список url для подключения к WebDav
- `WEBDAV_USERNAME` - логин пользователя для подключения к WebDav
- `WEBDAV_PASSWORD` - пароль пользователя для подключения к WebDav
- `WEBDAV_BAD_SERVERS_CACHE_EXPIRE_SECONDS` - время жизни кэша адресов недоступных серверов WebDav (секунды)
- `WEBDAV_CLEANUP_CRON` - крон по очистке WebDav
- `WEBDAV_RETRY_DELAY` - количество попыток по операциям с файлами WebDav
- `FILE_WEBDAV_LIFETIME_SECONDS` - время жизни файла в WebDav (секунды)
- `FILE_WEBDAV_EXTENSIONS` - список расширений файлов, удаляемых с WebDav
- `AV_KAFKA_BOOTSTRAP_SERVERS` - список пар хост:порт, использующихся для установки первоначального соединения с кластером Kafka
- `AV_KAFKA_SECURITY_PROTOCOL` - протокол, используемый для взаимодействия с брокерами
- `AV_KAFKA_SASL_MECHANISM` - механизм SASL, используемый для клиентских подключений

View file

@ -6,9 +6,6 @@ DB_APP_HOST=10.10.31.119
DB_APP_PORT=5432
DB_APP_NAME=ervu_lkrp_ul
FILE_WEBDAV_UPLOAD_URL=https://ervu-webdav.k8s.micord.ru
FILE_WEBDAV_UPLOAD_USERNAME=test
FILE_WEBDAV_UPLOAD_PASSWORD=test
AV_KAFKA_MESSAGE_TOPIC_NAME=file-to-upload
AV_KAFKA_BOOTSTRAP_SERVERS=http://10.10.31.11:32609
AV_KAFKA_SECURITY_PROTOCOL=SASL_PLAINTEXT
@ -52,6 +49,10 @@ ERVU_FILE_UPLOAD_FILE_SIZE_THRESHOLD=0
ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * *
COOKIE_PATH=/ul
WEBDAV_URLS=https://ervu-webdav.k8s.micord.ru
WEBDAV_USERNAME=test
WEBDAV_PASSWORD=test
WEBDAV_BAD_SERVERS_CACHE_EXPIRE_SECONDS=120
WEBDAV_CLEANUP_CRON=0 0 0 * * *
WEBDAV_RETRY_DELAY=500
FILE_WEBDAV_LIFETIME_SECONDS=300

View file

@ -54,9 +54,9 @@
<property name="security.password.regex" value="^((?=(.*\d){1,})(?=.*[a-zа-яё])(?=.*[A-ZА-ЯЁ]).{8,})$"/>
<property name="fias.enable" value="false"/>
<property name="com.arjuna.ats.arjuna.allowMultipleLastResources" value="true"/>
<property name="file.webdav.upload.url" value="https://ervu-webdav.k8s.micord.ru"/>
<property name="file.webdav.upload.username" value="test"/>
<property name="file.webdav.upload.password" value="test"/>
<property name="webdav.urls" value="https://ervu-webdav.k8s.micord.ru"/>
<property name="webdav.username" value="test"/>
<property name="webdav.password" value="test"/>
<property name="av.kafka.download-request-topic" value="ervu.lkrp.download.request"/>
<property name="av.kafka.bootstrap.servers" value="localhost:9092"/>
<property name="av.kafka.security.protocol" value="SASL_PLAINTEXT"/>
@ -97,6 +97,7 @@
<property name="webdav.retry.delay" value="500"/>
<property name="file.webdav.lifetime.seconds" value="300"/>
<property name="file.webdav.extensions" value="csv,xlsx"/>
<property name="webdav.bad_servers.cache.expire.seconds" value="120"/>
</system-properties>
<management>
<audit-log>