Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d8f61e1d | ||
|
|
8fe4d854a6 | ||
|
|
4c607f5bf9 | ||
|
|
ce879fea18 | ||
|
|
33479ea9af | ||
|
|
76e8f4ab1d | ||
|
|
c03ca2566d |
@@ -12,6 +12,8 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY --from=build /app/build/libs/*.jar app.jar
|
COPY --from=build /app/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
RUN mkdir -p /data/uploads && chmod 755 /data/uploads
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["java", "-jar", "app.jar"]
|
CMD ["java", "-jar", "app.jar"]
|
||||||
|
|||||||
+13
-2
@@ -1,6 +1,16 @@
|
|||||||
version: '3.9'
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
storage:
|
||||||
|
image: alpine:latest
|
||||||
|
container_name: file-storage
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
volumes:
|
||||||
|
- uploads_data:/storage:rw
|
||||||
|
command: tail -f /dev/null
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:17
|
image: postgres:17
|
||||||
restart: always
|
restart: always
|
||||||
@@ -30,6 +40,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_PORT: 5432
|
POSTGRES_PORT: 5432
|
||||||
POSTGRES_HOST: db
|
POSTGRES_HOST: db
|
||||||
|
STORAGE_SERVICE_URL: http://storage:8081
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
@@ -41,7 +52,7 @@ services:
|
|||||||
- backend
|
- backend
|
||||||
- api
|
- api
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/data/uploads:rw
|
- uploads_data:/data/uploads:rw
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:10.3.1
|
image: grafana/grafana:10.3.1
|
||||||
@@ -146,7 +157,7 @@ volumes:
|
|||||||
loki_chunks:
|
loki_chunks:
|
||||||
loki_index:
|
loki_index:
|
||||||
loki_rules:
|
loki_rules:
|
||||||
# uploads_volume:
|
uploads_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ EOF
|
|||||||
--network app-network \\
|
--network app-network \\
|
||||||
--network-alias app \\
|
--network-alias app \\
|
||||||
-p 80:8080 \\
|
-p 80:8080 \\
|
||||||
|
-v /opt/uploads:/data/uploads:rw \\
|
||||||
-e POSTGRES_DB=no_copy_ \\
|
-e POSTGRES_DB=no_copy_ \\
|
||||||
-e POSTGRES_USER=postgres \\
|
-e POSTGRES_USER=postgres \\
|
||||||
-e POSTGRES_PASSWORD=postgres \\
|
-e POSTGRES_PASSWORD=postgres \\
|
||||||
|
|||||||
@@ -387,11 +387,11 @@ public class ApiController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mimeType.startsWith("image/") ||
|
return mimeType.startsWith("image") ||
|
||||||
mimeType.startsWith("text/") ||
|
mimeType.startsWith("text") ||
|
||||||
mimeType.equals("application/pdf") ||
|
mimeType.equals("pdf") ||
|
||||||
mimeType.startsWith("video/") ||
|
mimeType.startsWith("video") ||
|
||||||
mimeType.startsWith("audio/");
|
mimeType.startsWith("audio");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getUserIdFromToken(String tokenHeader) {
|
private Long getUserIdFromToken(String tokenHeader) {
|
||||||
|
|||||||
@@ -2,20 +2,47 @@ package ru.soune.nocopy.entity.file;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public enum FileType {
|
public enum FileType {
|
||||||
PHOTO("photo"),
|
PHOTO("photo", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")),
|
||||||
VIDEO("video"),
|
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "svg", "tiff", "tif", "ico",
|
||||||
AUDIO("audio"),
|
"psd", "ai", "eps", "raw", "heic", "heif")),
|
||||||
DOCUMENT("document");
|
VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg",
|
||||||
|
"3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")),
|
||||||
|
AUDIO("audio", Arrays.asList("mp3", "wav", "ogg", "aac", "flac", "m4a", "wma", "aiff", "aif", "amr",
|
||||||
|
"opus", "mka", "ac3", "alac")),
|
||||||
|
DOCUMENT("document", Arrays.asList(
|
||||||
|
"pdf", "txt", "rtf",
|
||||||
|
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pps", "ppsx", "dot", "dotx", "xlt", "xltx", "pot", "potx",
|
||||||
|
"odt", "ods", "odp", "odg", "odf", "odb", "odc", "odi", "odm", "ott", "ots", "otp", "otg", "oth",
|
||||||
|
"sxw", "sxc", "sxi", "sxd", "sxg", "stc", "sti", "stw", "sxm",
|
||||||
|
"pages", "numbers", "key",
|
||||||
|
"csv", "tsv", "xml", "html", "htm", "tex", "md", "markdown",
|
||||||
|
"epub", "mobi", "azw", "azw3", "fb2",
|
||||||
|
"wps", "wpt", "et", "dps", "vsd", "vsdx",
|
||||||
|
"java", "py", "cpp", "c", "h", "js", "css", "php", "sql", "json", "yaml", "yml", "sh", "bat",
|
||||||
|
"one", "note"));
|
||||||
|
|
||||||
private final String code;
|
private final String displayName;
|
||||||
|
private final List<String> allowedExtensions;
|
||||||
|
|
||||||
FileType(String code) {
|
FileType(String displayName, List<String> allowedExtensions) {
|
||||||
this.code = code;
|
this.displayName = displayName;
|
||||||
|
this.allowedExtensions = allowedExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCode() {
|
public String getDisplayName() {
|
||||||
return code;
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedExtensions() {
|
||||||
|
return allowedExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsExtension(String extension) {
|
||||||
|
return allowedExtensions.contains(extension.toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.BeanPropertyBindingResult;
|
import org.springframework.validation.BeanPropertyBindingResult;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
import ru.soune.nocopy.dto.*;
|
import ru.soune.nocopy.dto.*;
|
||||||
import ru.soune.nocopy.dto.file.*;
|
import ru.soune.nocopy.dto.file.*;
|
||||||
import ru.soune.nocopy.entity.AuthToken;
|
import ru.soune.nocopy.entity.AuthToken;
|
||||||
@@ -17,10 +18,8 @@ import ru.soune.nocopy.repository.AuthTokenRepository;
|
|||||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||||
import ru.soune.nocopy.service.file.FileUploadService;
|
import ru.soune.nocopy.service.file.FileUploadService;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -70,18 +69,31 @@ public class FileUploadHandler implements RequestHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private BaseResponse handleInitUpload(BaseRequest request, FileUploadRequest fileRequest) {
|
private BaseResponse handleInitUpload(BaseRequest request, FileUploadRequest fileRequest) {
|
||||||
try {
|
|
||||||
String token = fileRequest.getToken();
|
String token = fileRequest.getToken();
|
||||||
|
|
||||||
Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
|
Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
|
||||||
|
|
||||||
|
if (tokenOptional.isEmpty()) {
|
||||||
|
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
|
||||||
|
MessageCode.INVALID_TOKEN.getDescription(), Map.of("token", token));
|
||||||
|
}
|
||||||
|
|
||||||
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
|
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
|
||||||
|
|
||||||
BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest");
|
BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest");
|
||||||
fileUploadRequestValidator.validate(fileRequest, bindingResult);
|
fileUploadRequestValidator.validate(fileRequest, bindingResult);
|
||||||
|
|
||||||
if (bindingResult.hasErrors()) {
|
if (bindingResult.hasErrors()) {
|
||||||
throw new ValidationException(bindingResult, request.getMsgId());
|
Map<String, String> fieldErrors = bindingResult.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
FieldError::getField,
|
||||||
|
fieldError -> fieldError.getDefaultMessage() != null
|
||||||
|
? fieldError.getDefaultMessage()
|
||||||
|
: "Validation error"));
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_FIELD.getCode(),
|
||||||
|
MessageCode.INVALID_FIELD.getDescription(), fieldErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileUploadSession session = fileUploadService.initUpload(
|
FileUploadSession session = fileUploadService.initUpload(
|
||||||
@@ -101,15 +113,6 @@ public class FileUploadHandler implements RequestHandler {
|
|||||||
|
|
||||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||||
MessageCode.SUCCESS.getDescription(), fileResponse);
|
MessageCode.SUCCESS.getDescription(), fileResponse);
|
||||||
|
|
||||||
} catch (NotFoundAuthToken e) {
|
|
||||||
InitFileResponse initFileResponse = InitFileResponse.builder()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
throw new NotValidFieldException("Invalid or expired token: " + fileRequest.getToken(),
|
|
||||||
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
|
|
||||||
MessageCode.INVALID_TOKEN.getDescription(), initFileResponse));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseResponse handleGetProgress(BaseRequest request, FileUploadRequest fileRequest) {
|
private BaseResponse handleGetProgress(BaseRequest request, FileUploadRequest fileRequest) {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import ru.soune.nocopy.dto.file.FileUploadRequest;
|
|||||||
import ru.soune.nocopy.entity.file.FileType;
|
import ru.soune.nocopy.entity.file.FileType;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class FileUploadRequestValidator implements Validator {
|
public class FileUploadRequestValidator implements Validator {
|
||||||
|
private final List<FileType> supportedFileTypes = Arrays.asList(FileType.values());
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(Class<?> clazz) {
|
public boolean supports(Class<?> clazz) {
|
||||||
return FileUploadRequest.class.isAssignableFrom(clazz);
|
return FileUploadRequest.class.isAssignableFrom(clazz);
|
||||||
@@ -21,7 +24,7 @@ public class FileUploadRequestValidator implements Validator {
|
|||||||
|
|
||||||
validateType(request.getFileType(), errors);
|
validateType(request.getFileType(), errors);
|
||||||
validateFileName(request.getFileName(), errors);
|
validateFileName(request.getFileName(), errors);
|
||||||
validateExtension(request.getExtension(), errors);
|
validateExtension(request.getExtension(), request.getFileType(), errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateType(String fileType, Errors errors) {
|
private void validateType(String fileType, Errors errors) {
|
||||||
@@ -31,7 +34,12 @@ public class FileUploadRequestValidator implements Validator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FileType.valueOf(fileType.toUpperCase());
|
FileType parsedType = FileType.valueOf(fileType.toUpperCase());
|
||||||
|
|
||||||
|
if (!supportedFileTypes.contains(parsedType)) {
|
||||||
|
errors.rejectValue("fileType", "fileType.unsupported",
|
||||||
|
"Unsupported file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||||
|
}
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
errors.rejectValue("fileType", "fileType.invalid",
|
errors.rejectValue("fileType", "fileType.invalid",
|
||||||
"Invalid file type. Valid types: " + Arrays.toString(FileType.values()));
|
"Invalid file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||||
@@ -50,10 +58,34 @@ public class FileUploadRequestValidator implements Validator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateExtension(String extension, Errors errors) {
|
private void validateExtension(String extension, String fileType, Errors errors) {
|
||||||
if (extension == null || extension.isBlank()) {
|
if (extension == null || extension.isBlank()) {
|
||||||
errors.rejectValue("extension", "extension.required", "Extension is required");
|
errors.rejectValue("fileType", "fileType.required", "File type is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extension.contains(".")) {
|
||||||
|
errors.rejectValue("extension", "extension.required", "Extension contains comma");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileType parsedType = FileType.valueOf(fileType.toUpperCase());
|
||||||
|
|
||||||
|
if (!parsedType.supportsExtension(extension)) {
|
||||||
|
errors.rejectValue("fileName", "fileType.extension.mismatch",
|
||||||
|
String.format("File extension '%s' does not match file type '%s'. Allowed extensions for %s: %s",
|
||||||
|
extension, parsedType.getDisplayName(), parsedType.getDisplayName(),
|
||||||
|
parsedType.getAllowedExtensions()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
errors.rejectValue("fileType", "fileType.invalid",
|
||||||
|
"Invalid file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileExtension(String fileName) {
|
||||||
|
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user