TorrentController.java
package api.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import api.dtos.ErrorDto;
import api.dtos.TorrentDto;
import api.entities.Torrent;
import api.entities.User;
import api.mapper.TorrentMapper;
import api.services.TorrentService;
/**
* {@link TorrentController}.
*/
@Validated
@RestController
@RequestMapping("/torrents")
@Tag(name = "Torrents", description = "Torrents represent a repository stored in the system.")
public class TorrentController {
@Autowired
private TorrentService torrentService;
@Autowired
private TorrentMapper torrentMapper;
@Value("${pagination.default-page-size:10}")
private int defaultPageSize;
@Value("${pagination.max-page-size:100}")
private int maxPageSize;
public static final String TORRENT_MIME_TYPE = "application/x-bittorrent";
/**
* Upload new torrent. Client sends multipart/form-data with: - "metadata" part (application/json) -
* "file" part (application/x-bittorrent)
*
* @param user Current user
* @param metadata Torrent metadata
* @param file Torrent file
* @return {@link TorrentDto}
* @throws IOException if file cannot be read
*/
// region
@Operation(summary = "Upload Torrent", description = "Upload a new torrent file with metadata.")
@ApiResponses({
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = TorrentDto.class), mediaType = "application/json")),
@ApiResponse(responseCode = "400",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TorrentDto uploadTorrent(@AuthenticationPrincipal User user,
@RequestPart("metadata") @Valid TorrentDto metadata, @RequestPart("file") MultipartFile file)
throws IOException {
Torrent torrent =
torrentService.create(metadata.getName(), metadata.getDescription(), user, file, metadata.getRepoId());
return torrentMapper.toDto(torrent);
}
/**
* Get all torrents with pagination.
*
* @param page Page number (0-indexed)
* @param size Page size
* @return List of {@link TorrentDto}
*/
// region
@Operation(summary = "List Torrents", description = "Get all torrents with pagination.")
@ApiResponses({@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = TorrentDto.class), mediaType = "application/json"))})
// endregion
@GetMapping("")
public List<TorrentDto> getAllTorrents(@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) Integer size) {
int requestedSize = size != null ? size : defaultPageSize;
int safeSize = Math.min(requestedSize, maxPageSize);
Pageable pageable = PageRequest.of(page, safeSize, Sort.by("createdAt").descending());
return torrentService.getAll(pageable).map(torrentMapper::toDto).getContent();
}
/**
* Get torrent by id.
*
* @param id Torrent id
* @return {@link TorrentDto}
*/
// region
@Operation(summary = "Get Torrent", description = "Get torrent metadata by ID.")
@ApiResponses({
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = TorrentDto.class), mediaType = "application/json")),
@ApiResponse(responseCode = "404",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@GetMapping("/{id}")
public TorrentDto getTorrent(@PathVariable Long id) {
Torrent torrent = torrentService.get(id);
return torrentMapper.toDto(torrent);
}
/**
* Get torrent by repository ID (root commit hash).
*
* @param repoId 40-character hex repository root commit hash
* @return {@link TorrentDto}
*/
// region
@Operation(summary = "Get Torrent by Repository ID",
description = "Get torrent metadata by the repository's root commit hash.")
@ApiResponses({
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = TorrentDto.class), mediaType = "application/json")),
@ApiResponse(responseCode = "400",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json")),
@ApiResponse(responseCode = "404",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@GetMapping("/repository/{repoId}")
public TorrentDto getTorrentByRepoId(@PathVariable @Pattern(regexp = "[0-9a-fA-F]{40}") String repoId) {
Torrent torrent = torrentService.getByRepoId(repoId);
return torrentMapper.toDto(torrent);
}
/**
* Update torrent metadata.
*
* @param id Torrent id
* @param updateDto Update data
* @return {@link TorrentDto}
*/
// region
@Operation(summary = "Update Torrent Metadata", description = "Update torrent name and/or description.")
@ApiResponses({
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = TorrentDto.class), mediaType = "application/json")),
@ApiResponse(responseCode = "404",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@PutMapping("/{id}")
public TorrentDto updateTorrent(@PathVariable Long id, @RequestBody TorrentDto updateDto) {
Torrent torrent = torrentService.updateMetadata(id, updateDto.getName(), updateDto.getDescription());
return torrentMapper.toDto(torrent);
}
/**
* Delete torrent.
*
* @param id Torrent id
*/
// region
@Operation(summary = "Delete Torrent", description = "Delete a torrent and its file.")
@ApiResponses({
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = Void.class), mediaType = "application/json")),
@ApiResponse(responseCode = "404",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@DeleteMapping("/{id}")
public void deleteTorrent(@PathVariable Long id) {
torrentService.delete(id);
}
/**
* Download torrent file.
*
* @param id Torrent id
* @return Torrent file
* @throws IOException if file cannot be read
*/
// region
@Operation(summary = "Download Torrent File", description = "Download the actual .torrent file.")
@ApiResponses({@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/x-bittorrent")),
@ApiResponse(responseCode = "404",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@GetMapping("/{id}/file")
public ResponseEntity<Resource> getTorrentFile(@PathVariable Long id) throws IOException {
Torrent torrent = torrentService.getWithFile(id);
byte[] bytes;
try (InputStream in = torrentService.downloadTorrentFile(id)) {
bytes = in.readAllBytes();
}
ByteArrayResource resource = new ByteArrayResource(bytes);
// Suggest filename for download
String filename = torrent.getName().replaceAll("[^a-zA-Z0-9.-]", "_") + ".torrent";
return ResponseEntity.ok().contentType(MediaType.parseMediaType(TORRENT_MIME_TYPE))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentLength(bytes.length).body(resource);
}
/**
* Update torrent file.
*
* @param id Torrent id
* @param file New torrent file
* @throws IOException if file cannot be read
*/
// region
@Operation(summary = "Update Torrent File", description = "Replace the torrent file.")
@ApiResponses({
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(implementation = Void.class), mediaType = "application/json")),
@ApiResponse(responseCode = "400",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json")),
@ApiResponse(responseCode = "404",
content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
// endregion
@PutMapping("/{id}/file")
public void updateTorrentFile(@PathVariable Long id, @RequestParam("file") MultipartFile file) throws IOException {
torrentService.updateTorrentFile(id, file);
}
}