Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ target/
!**/src/main/**/target/
!**/src/test/**/target/
logs/*

uploads/*.*
uploads/apk/*.*
### STS ###
.apt_generated
.classpath
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ services:
ports:
- "8080:8080"
restart: always
volumes:
- ./uploads:/app/uploads
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
db:
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/org/pkwmtt/files/FileController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.pkwmtt.files;

import lombok.RequiredArgsConstructor;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileNotFoundException;
import java.io.IOException;

@RestController
@RequestMapping("/admin/files")
@RequiredArgsConstructor
public class FileController {
private final FileService service;

/**
* @param file provided file
* @return 200 if request ok
* @throws IOException when file or directory malformed
*/
@PostMapping(value = "/upload", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Void> upload (@RequestParam("file") MultipartFile file) throws IOException {
service.upload(file);
return ResponseEntity.ok().build();
}

/**
* @param fileName name of requested file
* @return file
* @throws IOException problem with accessing selected file
*/
@GetMapping(value = "/download/{fileName}")
public ResponseEntity<UrlResource> download (@PathVariable String fileName) throws IOException {
try {
UrlResource resource = service.getResourceByFileName(fileName);
return ResponseEntity
.ok()
.contentType(service.getContentNameByFileName(fileName))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} catch (FileNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}
73 changes: 73 additions & 0 deletions src/main/java/org/pkwmtt/files/FileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.pkwmtt.files;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.UrlResource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;

@Service
@RequiredArgsConstructor
public class FileService {
@Value("${app.upload.dir:uploads}")
private String UPLOADS_DIR;

/**
* Upload files - admin only
*
* @param file - file to upload
* @throws IOException - when location is malformed
*/
public void upload (MultipartFile file) throws IOException {
Path projectRoot = Paths.get("").toAbsolutePath();
Path uploadPath = projectRoot.resolve(UPLOADS_DIR);

//Create directory if not exists
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}

//Create file
Path filePath = uploadPath.resolve(Objects.requireNonNull(file.getOriginalFilename()));

//Move content from provided file to recently created one
file.transferTo(filePath.toFile());
}

public UrlResource getResourceByFileName (String fileName) throws IOException {
//Dir: ProjectRoot/uploads/fileName
Path filePath = getFilePathByName(fileName);
UrlResource resource = new UrlResource(filePath.toUri());

if (!resource.exists()) {
throw new FileNotFoundException();
}
return resource;
}

public MediaType getContentNameByFileName (String fileName) throws IOException {
Path filePath = getFilePathByName(fileName);

//Get file content
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
//Default value
contentType = "application/octet-stream";
}
//Parse to Media type
return MediaType.parseMediaType(contentType);
}

private Path getFilePathByName (String fileName) {
//Location of provided file
return Paths.get("").toAbsolutePath().resolve(UPLOADS_DIR).resolve(fileName).normalize();
}
}
27 changes: 27 additions & 0 deletions src/main/java/org/pkwmtt/files/FileUploadsExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.pkwmtt.files;

import org.pkwmtt.exceptions.dto.ErrorResponseDTO;
import org.pkwmtt.files.apk.ApkController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.IOException;

@RestControllerAdvice(assignableTypes = {FileController.class, ApkController.class})
public class FileUploadsExceptionHandler {

@ExceptionHandler(IOException.class)
public ResponseEntity<ErrorResponseDTO> handleIOException () {
return new ResponseEntity<>(
new ErrorResponseDTO("File or directory not found or is malformed."),
HttpStatus.NOT_FOUND
);
}

@ExceptionHandler({IllegalAccessException.class, RuntimeException.class})
public ResponseEntity<ErrorResponseDTO> handleIllegalArgumentException (Exception e) {
return new ResponseEntity<>(new ErrorResponseDTO(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
34 changes: 34 additions & 0 deletions src/main/java/org/pkwmtt/files/apk/ApkController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.pkwmtt.files.apk;

import lombok.RequiredArgsConstructor;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RequestMapping("${apiPrefix}/apk")
@RestController
@RequiredArgsConstructor
public class ApkController {

private final ApkService apkService;

@GetMapping("/download")
public ResponseEntity<UrlResource> download () throws IOException {
return ResponseEntity
.ok()
.contentType(MediaType.parseMediaType("application/vnd.android.package-archive"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=PKWM_App.apk")
.body(apkService.getApkResource());
}

@GetMapping("/version")
public String getApkVersion () throws IOException {
return apkService.getApkVersion();
}
}
66 changes: 66 additions & 0 deletions src/main/java/org/pkwmtt/files/apk/ApkService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.pkwmtt.files.apk;

import lombok.RequiredArgsConstructor;
import org.pkwmtt.files.FileService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

@Service
@RequiredArgsConstructor
public class ApkService {
@Value("${app.upload.dir:uploads}")
private String FILES_DIR;

private final FileService fileService;

public UrlResource getApkResource () throws IOException, IllegalArgumentException {
Path filePath = findNewestApkByExtensionInUploads().orElseThrow(FileNotFoundException::new);
return fileService.getResourceByFileName(filePath.getFileName().toString());
}

public String getApkVersion () throws IOException {
Path filePath = findNewestApkByExtensionInUploads().orElseThrow(IOException::new);
String fileName = filePath.getFileName().toString();
Pattern pattern = Pattern.compile("\\d+(?:\\.\\d+){1,2}");
Matcher matcher = pattern.matcher(fileName);
if (!matcher.find()) {
return null;
}
return matcher.group();
}

private Optional<Path> findNewestApkByExtensionInUploads () throws IOException, IllegalArgumentException {
Path dirPath = Paths.get(FILES_DIR);

if (!Files.exists(dirPath) || !Files.isDirectory(dirPath)) {
throw new IllegalArgumentException("Invalid directory: " + dirPath);
}

Stream<Path> stream = Files.list(dirPath);

try (stream) {
return stream
.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().toLowerCase().endsWith(".apk"))
.max(Comparator.comparingLong(file -> {
try {
return Files.getLastModifiedTime(file).toMillis();
} catch (IOException e) {
throw new RuntimeException("Couldn't locate last modified file");
}
}));
}
}
}
1 change: 0 additions & 1 deletion src/main/java/org/pkwmtt/global/RequestInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ public class RequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle (@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {


String headerName = "X-API-KEY";
try {
String providedApiKey = request.getHeader(headerName);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/pkwmtt/global/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public void addInterceptors (@NonNull InterceptorRegistry registry) {
String apiPrefix = environment.getProperty("apiPrefix", "");
requestInterceptor.ifPresent(interceptor -> registry
.addInterceptor(interceptor)
.addPathPatterns(apiPrefix + "/**"));
.addPathPatterns(apiPrefix + "/**")
.excludePathPatterns(apiPrefix + "/apk/download"));
registry.addInterceptor(adminRequestInterceptor).addPathPatterns("/admin");
}
}
7 changes: 6 additions & 1 deletion src/main/resources/application-prod.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ spring.mail.properties.mail.smtp.starttls.enable=true
#Path
apiPrefix=/pkwmtt/api/v1
#Swagger https protocol
swagger.url=https://backend.pkwmapp.pl
swagger.url=https://backend.pkwmapp.pl
#Uploads
app.upload.dir="uploads"
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
spring.servlet.multipart.enabled=true
7 changes: 6 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ spring.mail.password=${EMAIL_PASSWORD:}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
#Path
apiPrefix=/pkwmtt/api/v1
apiPrefix=/pkwmtt/api/v1
#Uploads
app.upload.dir=uploads
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
spring.servlet.multipart.enabled=true