Improved exercise submission flow.
This commit is contained in:
parent
50a6ece0d8
commit
6702fb564b
|
@ -0,0 +1,23 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
@Bean
|
||||||
|
public Executor taskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(3);
|
||||||
|
executor.setMaxPoolSize(5);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("gymboard-api-");
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,7 @@ public class GymController {
|
||||||
@PathVariable String cityCode,
|
@PathVariable String cityCode,
|
||||||
@PathVariable String gymName,
|
@PathVariable String gymName,
|
||||||
@RequestParam MultipartFile file
|
@RequestParam MultipartFile file
|
||||||
) throws IOException {
|
) {
|
||||||
return uploadService.handleUpload(new RawGymId(countryCode, cityCode, gymName), file);
|
return uploadService.handleSubmissionUpload(new RawGymId(countryCode, cityCode, gymName), file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,6 @@ public record ExerciseSubmissionPayload(
|
||||||
String name,
|
String name,
|
||||||
String exerciseShortName,
|
String exerciseShortName,
|
||||||
float weight,
|
float weight,
|
||||||
|
int reps,
|
||||||
long videoId
|
long videoId
|
||||||
) {}
|
) {}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.dao.exercise;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ExerciseSubmissionTempFileRepository extends JpaRepository<ExerciseSubmissionTempFile, Long> {
|
||||||
|
List<ExerciseSubmissionTempFile> findAllByCreatedAtBefore(LocalDateTime timestamp);
|
||||||
|
Optional<ExerciseSubmissionTempFile> findBySubmission(ExerciseSubmission submission);
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package nl.andrewlalis.gymboard_api.model.exercise;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||||
import nl.andrewlalis.gymboard_api.model.StoredFile;
|
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
@ -24,26 +23,30 @@ public class ExerciseSubmission {
|
||||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
private Exercise exercise;
|
private Exercise exercise;
|
||||||
|
|
||||||
@Column(nullable = false, updatable = false, length = 63)
|
@Column(nullable = false)
|
||||||
private String submitterName;
|
private String status;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean verified;
|
private boolean verified;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false, length = 63)
|
||||||
|
private String submitterName;
|
||||||
|
|
||||||
@Column(nullable = false, precision = 7, scale = 2)
|
@Column(nullable = false, precision = 7, scale = 2)
|
||||||
private BigDecimal weight;
|
private BigDecimal weight;
|
||||||
|
|
||||||
@OneToOne(optional = false, fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
@Column(nullable = false)
|
||||||
private StoredFile videoFile;
|
private int reps;
|
||||||
|
|
||||||
public ExerciseSubmission() {}
|
public ExerciseSubmission() {}
|
||||||
|
|
||||||
public ExerciseSubmission(Gym gym, Exercise exercise, String submitterName, BigDecimal weight, StoredFile videoFile) {
|
public ExerciseSubmission(Gym gym, Exercise exercise, String submitterName, BigDecimal weight, int reps) {
|
||||||
this.gym = gym;
|
this.gym = gym;
|
||||||
this.exercise = exercise;
|
this.exercise = exercise;
|
||||||
this.submitterName = submitterName;
|
this.submitterName = submitterName;
|
||||||
this.weight = weight;
|
this.weight = weight;
|
||||||
this.videoFile = videoFile;
|
this.reps = reps;
|
||||||
|
this.status = "PROCESSING";
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
|
@ -62,6 +65,14 @@ public class ExerciseSubmission {
|
||||||
return exercise;
|
return exercise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSubmitterName() {
|
public String getSubmitterName() {
|
||||||
return submitterName;
|
return submitterName;
|
||||||
}
|
}
|
||||||
|
@ -74,7 +85,7 @@ public class ExerciseSubmission {
|
||||||
return weight;
|
return weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoredFile getVideoFile() {
|
public int getReps() {
|
||||||
return videoFile;
|
return reps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.model.exercise;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the temporary file on disk that's stored while a user is preparing
|
||||||
|
* their submission. This file will be removed after the submission is
|
||||||
|
* processed.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "exercise_submission_temp_file")
|
||||||
|
public class ExerciseSubmissionTempFile {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false, length = 1024)
|
||||||
|
private String path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The submission that this temporary file is for. This will initially be
|
||||||
|
* null, but will be set as soon as the submission is finalized.
|
||||||
|
*/
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
private ExerciseSubmission submission;
|
||||||
|
|
||||||
|
public ExerciseSubmissionTempFile() {}
|
||||||
|
|
||||||
|
public ExerciseSubmissionTempFile(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExerciseSubmission getSubmission() {
|
||||||
|
return submission;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubmission(ExerciseSubmission submission) {
|
||||||
|
this.submission = submission;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,16 @@ import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||||
import nl.andrewlalis.gymboard_api.dao.StoredFileRepository;
|
import nl.andrewlalis.gymboard_api.dao.StoredFileRepository;
|
||||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
|
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionTempFileRepository;
|
||||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||||
import nl.andrewlalis.gymboard_api.model.StoredFile;
|
import nl.andrewlalis.gymboard_api.model.StoredFile;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||||
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
@ -21,19 +26,28 @@ import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GymService {
|
public class GymService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GymService.class);
|
||||||
|
|
||||||
private final GymRepository gymRepository;
|
private final GymRepository gymRepository;
|
||||||
private final StoredFileRepository fileRepository;
|
private final StoredFileRepository fileRepository;
|
||||||
private final ExerciseRepository exerciseRepository;
|
private final ExerciseRepository exerciseRepository;
|
||||||
private final ExerciseSubmissionRepository exerciseSubmissionRepository;
|
private final ExerciseSubmissionRepository exerciseSubmissionRepository;
|
||||||
|
private final ExerciseSubmissionTempFileRepository tempFileRepository;
|
||||||
|
|
||||||
public GymService(GymRepository gymRepository, StoredFileRepository fileRepository, ExerciseRepository exerciseRepository, ExerciseSubmissionRepository exerciseSubmissionRepository) {
|
public GymService(GymRepository gymRepository,
|
||||||
|
StoredFileRepository fileRepository,
|
||||||
|
ExerciseRepository exerciseRepository,
|
||||||
|
ExerciseSubmissionRepository exerciseSubmissionRepository,
|
||||||
|
ExerciseSubmissionTempFileRepository tempFileRepository) {
|
||||||
this.gymRepository = gymRepository;
|
this.gymRepository = gymRepository;
|
||||||
this.fileRepository = fileRepository;
|
this.fileRepository = fileRepository;
|
||||||
this.exerciseRepository = exerciseRepository;
|
this.exerciseRepository = exerciseRepository;
|
||||||
this.exerciseSubmissionRepository = exerciseSubmissionRepository;
|
this.exerciseSubmissionRepository = exerciseSubmissionRepository;
|
||||||
|
this.tempFileRepository = tempFileRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
@ -43,27 +57,69 @@ public class GymService {
|
||||||
return new GymResponse(gym);
|
return new GymResponse(gym);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the creation of a new exercise submission. This involves a few steps:
|
||||||
|
* <ol>
|
||||||
|
* <li>Pre-fetch all of the referenced data, like exercise and video file.</li>
|
||||||
|
* <li>Check that the submission is legitimate.</li>
|
||||||
|
* <li>Begin video processing.</li>
|
||||||
|
* <li>Save the submission with the PROCESSING status.</li>
|
||||||
|
* </ol>
|
||||||
|
* Once the asynchronous submission processing is complete, the submission
|
||||||
|
* status will change to COMPLETE.
|
||||||
|
* @param id The gym id.
|
||||||
|
* @param payload The submission data.
|
||||||
|
* @return The saved submission, which will be in the PROCESSING state at first.
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public ExerciseSubmissionResponse createSubmission(RawGymId id, ExerciseSubmissionPayload payload) throws IOException {
|
public ExerciseSubmissionResponse createSubmission(RawGymId id, ExerciseSubmissionPayload payload) throws IOException {
|
||||||
Gym gym = gymRepository.findByRawId(id.gymName(), id.cityCode(), id.countryCode())
|
Gym gym = gymRepository.findByRawId(id.gymName(), id.cityCode(), id.countryCode())
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
Exercise exercise = exerciseRepository.findById(payload.exerciseShortName())
|
Exercise exercise = exerciseRepository.findById(payload.exerciseShortName())
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid exercise."));
|
||||||
// TODO: Implement legitimate file storage.
|
ExerciseSubmissionTempFile tempFile = tempFileRepository.findById(payload.videoId())
|
||||||
Path path = Path.of("sample_data", "sample_curl_14kg.MP4");
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid video id."));
|
||||||
StoredFile file = fileRepository.save(new StoredFile(
|
|
||||||
"sample_curl_14kg.MP4",
|
// TODO: Validate the submission data.
|
||||||
"video/mp4",
|
|
||||||
Files.size(path),
|
|
||||||
Files.readAllBytes(path)
|
// Create the submission.
|
||||||
));
|
|
||||||
ExerciseSubmission submission = exerciseSubmissionRepository.save(new ExerciseSubmission(
|
ExerciseSubmission submission = exerciseSubmissionRepository.save(new ExerciseSubmission(
|
||||||
gym,
|
gym,
|
||||||
exercise,
|
exercise,
|
||||||
payload.name(),
|
payload.name(),
|
||||||
BigDecimal.valueOf(payload.weight()),
|
BigDecimal.valueOf(payload.weight()),
|
||||||
file
|
payload.reps()
|
||||||
));
|
));
|
||||||
|
// Then link it to the temporary video file so the async task can find it.
|
||||||
|
tempFile.setSubmission(submission);
|
||||||
|
tempFileRepository.save(tempFile);
|
||||||
|
|
||||||
return new ExerciseSubmissionResponse(submission);
|
return new ExerciseSubmissionResponse(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous task that's started after a submission is submitted, which
|
||||||
|
* handles video processing and anything else that might need to be done
|
||||||
|
* before the submission can be marked as COMPLETED.
|
||||||
|
* @param submissionId The submission's id.
|
||||||
|
*/
|
||||||
|
@Async @Transactional
|
||||||
|
public void processSubmission(long submissionId) {
|
||||||
|
Optional<ExerciseSubmission> optionalSubmission = exerciseSubmissionRepository.findById(submissionId);
|
||||||
|
if (optionalSubmission.isEmpty()) {
|
||||||
|
log.warn("Submission id {} is not associated with a submission.", submissionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExerciseSubmission submission = optionalSubmission.get();
|
||||||
|
Optional<ExerciseSubmissionTempFile> optionalTempFile = tempFileRepository.findBySubmission(submission);
|
||||||
|
if (optionalTempFile.isEmpty()) {
|
||||||
|
log.warn("Submission {} failed because the temporary video file couldn't be found.", submission.getId());
|
||||||
|
submission.setStatus("FAILED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExerciseSubmissionTempFile tempFile = optionalTempFile.get();
|
||||||
|
|
||||||
|
// TODO: Finish this!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,12 @@ package nl.andrewlalis.gymboard_api.service;
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.RawGymId;
|
import nl.andrewlalis.gymboard_api.controller.dto.RawGymId;
|
||||||
import nl.andrewlalis.gymboard_api.controller.dto.UploadedFileResponse;
|
import nl.andrewlalis.gymboard_api.controller.dto.UploadedFileResponse;
|
||||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||||
import nl.andrewlalis.gymboard_api.dao.StoredFileRepository;
|
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseSubmissionTempFileRepository;
|
||||||
import nl.andrewlalis.gymboard_api.model.Gym;
|
import nl.andrewlalis.gymboard_api.model.Gym;
|
||||||
import nl.andrewlalis.gymboard_api.model.StoredFile;
|
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmissionTempFile;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.FileSystemUtils;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@ -17,52 +16,85 @@ import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for handling large file uploads.
|
* Service for handling large file uploads.
|
||||||
* TODO: Use this instead of simple multipart form data.
|
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class UploadService {
|
public class UploadService {
|
||||||
private final StoredFileRepository fileRepository;
|
private static final String[] ALLOWED_VIDEO_TYPES = {
|
||||||
|
"video/mp4"
|
||||||
|
};
|
||||||
|
|
||||||
|
private final ExerciseSubmissionTempFileRepository tempFileRepository;
|
||||||
private final GymRepository gymRepository;
|
private final GymRepository gymRepository;
|
||||||
|
|
||||||
public UploadService(StoredFileRepository fileRepository, GymRepository gymRepository) {
|
public UploadService(ExerciseSubmissionTempFileRepository tempFileRepository, GymRepository gymRepository) {
|
||||||
this.fileRepository = fileRepository;
|
this.tempFileRepository = tempFileRepository;
|
||||||
this.gymRepository = gymRepository;
|
this.gymRepository = gymRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the upload of an exercise submission's video file by saving the
|
||||||
|
* file to a temporary location, and recording that location in the
|
||||||
|
* database for when the exercise submission is completed. We'll only do
|
||||||
|
* the computationally expensive video processing if a user successfully
|
||||||
|
* submits their submission; otherwise, the raw video is discarded after a
|
||||||
|
* while.
|
||||||
|
* @param gymId The gym's id.
|
||||||
|
* @param multipartFile The uploaded file.
|
||||||
|
* @return A response containing the uploaded file's id, to be included in
|
||||||
|
* the user's submission.
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public UploadedFileResponse handleUpload(RawGymId gymId, MultipartFile multipartFile) throws IOException {
|
public UploadedFileResponse handleSubmissionUpload(RawGymId gymId, MultipartFile multipartFile) {
|
||||||
Gym gym = gymRepository.findByRawId(gymId.gymName(), gymId.cityCode(), gymId.countryCode())
|
Gym gym = gymRepository.findByRawId(gymId.gymName(), gymId.cityCode(), gymId.countryCode())
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
// TODO: Check that user is allowed to upload.
|
// TODO: Check that user is allowed to upload.
|
||||||
// TODO: Robust file type check.
|
boolean fileTypeAcceptable = false;
|
||||||
if (!"video/mp4".equalsIgnoreCase(multipartFile.getContentType())) {
|
for (String allowedType : ALLOWED_VIDEO_TYPES) {
|
||||||
|
if (allowedType.equalsIgnoreCase(multipartFile.getContentType())) {
|
||||||
|
fileTypeAcceptable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fileTypeAcceptable) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid content type.");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid content type.");
|
||||||
}
|
}
|
||||||
Path tempDir = Files.createTempDirectory("gymboard-file-upload");
|
|
||||||
Path tempFile = tempDir.resolve("video-file");
|
|
||||||
multipartFile.transferTo(tempFile);
|
|
||||||
Process ffmpegProcess = new ProcessBuilder()
|
|
||||||
.command("ffmpeg", "-i", "video-file", "-vf", "scale=640x480:flags=lanczos", "-vcodec", "libx264", "-crf", "28", "output.mp4")
|
|
||||||
.inheritIO()
|
|
||||||
.directory(tempDir.toFile())
|
|
||||||
.start();
|
|
||||||
try {
|
try {
|
||||||
int result = ffmpegProcess.waitFor();
|
Path tempFileDir = Path.of("exercise_submission_temp_files");
|
||||||
if (result != 0) throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ffmpeg exited with code " + result);
|
if (!Files.exists(tempFileDir)) {
|
||||||
} catch (InterruptedException e) {
|
Files.createDirectory(tempFileDir);
|
||||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ffmpeg process interrupted", e);
|
|
||||||
}
|
}
|
||||||
Path compressedFile = tempDir.resolve("output.mp4");
|
Path tempFilePath = Files.createTempFile(tempFileDir, null, null);
|
||||||
StoredFile file = fileRepository.save(new StoredFile(
|
multipartFile.transferTo(tempFilePath);
|
||||||
"compressed.mp4",
|
ExerciseSubmissionTempFile tempFileEntity = tempFileRepository.save(new ExerciseSubmissionTempFile(tempFilePath.toString()));
|
||||||
"video/mp4",
|
return new UploadedFileResponse(tempFileEntity.getId());
|
||||||
Files.size(compressedFile),
|
} catch (IOException e) {
|
||||||
Files.readAllBytes(compressedFile)
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "File upload failed.", e);
|
||||||
));
|
}
|
||||||
FileSystemUtils.deleteRecursively(tempDir);
|
|
||||||
return new UploadedFileResponse(file.getId());
|
// Path tempDir = Files.createTempDirectory("gymboard-file-upload");
|
||||||
|
// Path tempFile = tempDir.resolve("video-file");
|
||||||
|
// multipartFile.transferTo(tempFile);
|
||||||
|
// Process ffmpegProcess = new ProcessBuilder()
|
||||||
|
// .command("ffmpeg", "-i", "video-file", "-vf", "scale=640x480:flags=lanczos", "-vcodec", "libx264", "-crf", "28", "output.mp4")
|
||||||
|
// .inheritIO()
|
||||||
|
// .directory(tempDir.toFile())
|
||||||
|
// .start();
|
||||||
|
// try {
|
||||||
|
// int result = ffmpegProcess.waitFor();
|
||||||
|
// if (result != 0) throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ffmpeg exited with code " + result);
|
||||||
|
// } catch (InterruptedException e) {
|
||||||
|
// throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ffmpeg process interrupted", e);
|
||||||
|
// }
|
||||||
|
// Path compressedFile = tempDir.resolve("output.mp4");
|
||||||
|
// StoredFile file = fileRepository.save(new StoredFile(
|
||||||
|
// "compressed.mp4",
|
||||||
|
// "video/mp4",
|
||||||
|
// Files.size(compressedFile),
|
||||||
|
// Files.readAllBytes(compressedFile)
|
||||||
|
// ));
|
||||||
|
// FileSystemUtils.deleteRecursively(tempDir);
|
||||||
|
// return new UploadedFileResponse(file.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue