WIP: Submit transcoding jobs via a HTTP API #6
@ -1,9 +1,15 @@
|
||||
package se.su.dsv.whisperapi;
|
||||
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import se.su.dsv.whisperapi.core.OutputFormat;
|
||||
import se.su.dsv.whisperapi.core.TransactionRepository;
|
||||
import se.su.dsv.whisperapi.core.Transcription;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class JDBCTransactionRepository implements TransactionRepository {
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
@ -23,4 +29,34 @@ public class JDBCTransactionRepository implements TransactionRepository {
|
||||
.param("output_format", transcription.outputFormat().name())
|
||||
.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Transcription> findByOwnerAndId(Principal owner, UUID uuid) {
|
||||
return jdbcClient.sql("""
|
||||
SELECT id, owner, callback_uri, output_format
|
||||
FROM transcriptions
|
||||
WHERE id = :id AND owner = :owner
|
||||
""")
|
||||
.param("id", uuid)
|
||||
.param("owner", owner.getName())
|
||||
.query((rs, rowNum) -> {
|
||||
UUID id = UUID.fromString(rs.getString("id"));
|
||||
URI callbackUri = URI.create(rs.getString("callback_uri"));
|
||||
OutputFormat outputFormat = OutputFormat.valueOf(rs.getString("output_format"));
|
||||
return new Transcription(id, owner, callbackUri, outputFormat);
|
||||
})
|
||||
.optional();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFileToTranscription(Transcription transcription, String filename) {
|
||||
jdbcClient.sql("""
|
||||
INSERT INTO transcriptions_files (transcription_id, filename)
|
||||
VALUES (:transcription_id, :filename)
|
||||
ON DUPLICATE KEY UPDATE filename = :filename
|
||||
""")
|
||||
.param("transcription_id", transcription.id())
|
||||
.param("filename", filename)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package se.su.dsv.whisperapi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
@ -12,6 +13,7 @@ import se.su.dsv.whisperapi.core.TransactionRepository;
|
||||
import se.su.dsv.whisperapi.core.TranscriptionService;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(WhisperFrontendConfiguration.class)
|
||||
public class WhisperApiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@ -39,8 +41,11 @@ public class WhisperApiApplication {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TranscriptionService transcriptionService(TransactionRepository transcriptionRepository) {
|
||||
return new TranscriptionService(transcriptionRepository);
|
||||
public TranscriptionService transcriptionService(
|
||||
TransactionRepository transcriptionRepository,
|
||||
WhisperFrontendConfiguration config)
|
||||
{
|
||||
return new TranscriptionService(transcriptionRepository, config.transcriptionFilesDirectory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -0,0 +1,11 @@
|
||||
package se.su.dsv.whisperapi;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@ConfigurationProperties(prefix = "whisper.frontend")
|
||||
public record WhisperFrontendConfiguration(
|
||||
Path transcriptionFilesDirectory)
|
||||
{
|
||||
}
|
@ -1,17 +1,25 @@
|
||||
package se.su.dsv.whisperapi.api;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import se.su.dsv.whisperapi.core.CreateTranscription;
|
||||
import se.su.dsv.whisperapi.core.OutputFormat;
|
||||
import se.su.dsv.whisperapi.core.Transcription;
|
||||
import se.su.dsv.whisperapi.core.TranscriptionFile;
|
||||
import se.su.dsv.whisperapi.core.TranscriptionService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* For submitting jobs programmatically via a JSON HTTP API.
|
||||
@ -21,6 +29,7 @@ import java.security.Principal;
|
||||
@RestController
|
||||
@RequestMapping(consumes = "application/json", produces = "application/json")
|
||||
public class ApiController {
|
||||
private static final System.Logger LOG = System.getLogger(ApiController.class.getName());
|
||||
|
||||
private final TranscriptionService transcriptionService;
|
||||
|
||||
@ -54,4 +63,33 @@ public class ApiController {
|
||||
default -> throw new InvalidOutputFormat(outputFormat);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a file to be transcribed in the specified job. Multiple files can be attached to a single job.
|
||||
* They must be unique based on filename, same filename will overwrite the existing file.
|
||||
*/
|
||||
@PutMapping(value = "/api/transcriptions/{id}/file", consumes = "*/*")
|
||||
public ResponseEntity<Void> uploadFileToBeTranscribed(
|
||||
Principal owner,
|
||||
@PathVariable("id") String id,
|
||||
@RequestHeader("X-Filename") String filename,
|
||||
InputStream fileData)
|
||||
{
|
||||
try {
|
||||
if (filename == null || filename.isBlank()) {
|
||||
throw new MissingFilename();
|
||||
}
|
||||
UUID uuid = UUID.fromString(id);
|
||||
Transcription transcription = transcriptionService.getTranscription(owner, uuid)
|
||||
.orElseThrow(() -> new TranscriptionNotFound(id));
|
||||
transcriptionService.addFileToBeTranscribed(transcription, new TranscriptionFile(filename, fileData));
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Invalid UUID
|
||||
throw new TranscriptionNotFound(id);
|
||||
} catch (IOException e) {
|
||||
LOG.log(System.Logger.Level.ERROR, "Failed to save file", e);
|
||||
throw new FileUploadFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
src/main/java/se/su/dsv/whisperapi/api/FileUploadFailed.java
Normal file
14
src/main/java/se/su/dsv/whisperapi/api/FileUploadFailed.java
Normal file
@ -0,0 +1,14 @@
|
||||
package se.su.dsv.whisperapi.api;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class FileUploadFailed extends ErrorResponseException {
|
||||
public FileUploadFailed() {
|
||||
super(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#file-upload-failed"));
|
||||
setTitle("File upload failed");
|
||||
}
|
||||
}
|
15
src/main/java/se/su/dsv/whisperapi/api/MissingFilename.java
Normal file
15
src/main/java/se/su/dsv/whisperapi/api/MissingFilename.java
Normal file
@ -0,0 +1,15 @@
|
||||
package se.su.dsv.whisperapi.api;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class MissingFilename extends ErrorResponseException {
|
||||
public MissingFilename() {
|
||||
super(HttpStatus.BAD_REQUEST);
|
||||
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#missing-filename"));
|
||||
setTitle("Missing filename");
|
||||
setDetail("A filename must be specified in the X-Filename header");
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package se.su.dsv.whisperapi.api;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class TranscriptionNotFound extends ErrorResponseException {
|
||||
public TranscriptionNotFound(String id) {
|
||||
super(HttpStatus.BAD_REQUEST);
|
||||
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#transcription-file-not-found"));
|
||||
setTitle("Transcription not found");
|
||||
setDetail("Transcription with id '" + id + "' not found");
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
package se.su.dsv.whisperapi.core;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface TransactionRepository {
|
||||
void save(Transcription transcription);
|
||||
|
||||
Optional<Transcription> findByOwnerAndId(Principal owner, UUID uuid);
|
||||
|
||||
void addFileToTranscription(Transcription transcription, String filename);
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package se.su.dsv.whisperapi.core;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public record TranscriptionFile(String filename, InputStream data) {
|
||||
}
|
@ -1,12 +1,20 @@
|
||||
package se.su.dsv.whisperapi.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class TranscriptionService {
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final Path fileDirectory;
|
||||
|
||||
public TranscriptionService(TransactionRepository transactionRepository) {
|
||||
public TranscriptionService(TransactionRepository transactionRepository, Path fileDirectory) {
|
||||
this.transactionRepository = transactionRepository;
|
||||
this.fileDirectory = fileDirectory;
|
||||
}
|
||||
|
||||
|
||||
@ -20,4 +28,21 @@ public class TranscriptionService {
|
||||
transactionRepository.save(transcription);
|
||||
return transcription;
|
||||
}
|
||||
|
||||
public Optional<Transcription> getTranscription(Principal owner, UUID uuid) {
|
||||
return transactionRepository.findByOwnerAndId(owner, uuid);
|
||||
}
|
||||
|
||||
public void addFileToBeTranscribed(Transcription transcription, TranscriptionFile file)
|
||||
throws IOException
|
||||
{
|
||||
Path fileToBeTranscribed = fileDirectory.resolve(transcription.id().toString()).resolve(file.filename());
|
||||
Files.createDirectories(fileToBeTranscribed.getParent());
|
||||
try (var out = Files.newOutputStream(fileToBeTranscribed, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
var in = file.data())
|
||||
{
|
||||
in.transferTo(out);
|
||||
transactionRepository.addFileToTranscription(transcription, file.filename());
|
||||
}
|
||||
}
|
||||
}
|
||||
ansv7779 marked this conversation as resolved
|
||||
|
@ -16,3 +16,4 @@ spring.mvc.problemdetails.enabled=true
|
||||
# Disable logging of resolved exceptions or every ErrorResponseException (ProblemDetails)
|
||||
# that is correctly handled will be logged as a warning
|
||||
spring.mvc.log-resolved-exception=false
|
||||
whisper.frontend.transcription-files-directory=/tmp
|
||||
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE transcriptions_files (
|
||||
transcription_id UUID NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (transcription_id, filename),
|
||||
CONSTRAINT FK_transcriptions_files_transcription
|
||||
FOREIGN KEY (transcription_id) REFERENCES transcriptions (id) ON DELETE CASCADE
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user
Avoid using the filename provided by the user as a security measure.
Fixed in
515b2aa642