TorrentService.java
package api.services;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import api.entities.MimeType;
import api.entities.S3Object;
import api.entities.Torrent;
import api.entities.User;
import api.exceptions.EntityNotFoundException;
import api.repositories.TorrentRepository;
/**
* {@link TorrentService}.
*/
@Slf4j
@Service
public class TorrentService {
@Autowired
private S3ObjectService s3ObjectService;
@Autowired
private TorrentRepository torrentRepository;
@Autowired
private MimeTypeService mimeTypeService;
public static final String TORRENT_DIR = "torrent/";
private static final String TORRENT_MIME_TYPE = "application/x-bittorrent";
@Value("${api.s3.torrent.max:10000000}")
private long maxTorrentSize;
/**
* Find torrent by id.
*
* @param id Torrent id
* @return {@link Optional} {@link Torrent}
*/
@Transactional(readOnly = true)
public Optional<Torrent> find(Long id) {
return torrentRepository.findById(id);
}
/**
* Find torrent by name.
*
* @param name Torrent name
* @return {@link Optional} {@link Torrent}
*/
@Transactional(readOnly = true)
public Optional<Torrent> findByName(String name) {
return torrentRepository.findByName(name);
}
/**
* Find torrent by root commit hash.
*
* @param repoId 40-character hex repository root commit hash
* @return {@link Optional} {@link Torrent}
*/
@Transactional(readOnly = true)
public Optional<Torrent> findByRepoId(String repoId) {
return torrentRepository.findByRepoId(repoId);
}
/**
* Find torrent by id with file eager loaded.
*
* @param id Torrent id
* @return {@link Optional} {@link Torrent}
*/
@Transactional(readOnly = true)
public Optional<Torrent> findWithFile(Long id) {
Optional<Torrent> torrent = find(id);
torrent.ifPresent(t -> Hibernate.initialize(t.getFile()));
return torrent;
}
/**
* Get torrent by id.
*
* @param id Torrent id
* @return {@link Torrent}
* @throws EntityNotFoundException if torrent not found
*/
@Transactional(readOnly = true)
public Torrent get(Long id) {
return find(id).orElseThrow(() -> EntityNotFoundException.fromTorrent(id));
}
/**
* Get torrent by name.
*
* @param name Torrent name
* @return {@link Torrent}
* @throws EntityNotFoundException if torrent not found
*/
@Transactional(readOnly = true)
public Torrent getByName(String name) {
return findByName(name).orElseThrow(() -> EntityNotFoundException.fromTorrent(name));
}
/**
* Get torrent by repository id (first commit hash).
*
* @param repoId 40-character hex repository root commit hash
* @return {@link Torrent}
* @throws EntityNotFoundException if torrent not found
*/
@Transactional(readOnly = true)
public Torrent getByRepoId(String repoId) {
return findByRepoId(repoId).orElseThrow(() -> EntityNotFoundException.fromTorrentByrepoId(repoId));
}
/**
* Get torrent by id with file eager loaded.
*
* @param id Torrent id
* @return {@link Torrent}
* @throws EntityNotFoundException if torrent not found
*/
@Transactional(readOnly = true)
public Torrent getWithFile(Long id) {
return findWithFile(id).orElseThrow(() -> EntityNotFoundException.fromTorrent(id));
}
/**
* Get all torrents.
*
* @param pageable {@link Pageable}
* @return {@link Page} of {@link Torrent}
*/
@Transactional(readOnly = true)
public Page<Torrent> getAll(Pageable pageable) {
return torrentRepository.findAll(pageable);
}
/**
* Get all torrents by uploader.
*
* @param uploader Uploader
* @param pageable {@link Pageable}
* @return {@link Page} of {@link Torrent}
*/
@Transactional(readOnly = true)
public Page<Torrent> getAllByUploader(User uploader, Pageable pageable) {
return torrentRepository.findAllByUploader(uploader, pageable);
}
/**
* Check if torrent exists.
*
* @param id Torrent id
* @return true if exists
*/
@Transactional(readOnly = true)
public boolean exists(Long id) {
return torrentRepository.existsById(id);
}
/**
* Save torrent.
*
* @param torrent Torrent
* @return {@link Torrent}
*/
@Transactional
public Torrent save(Torrent torrent) {
Torrent saved = torrentRepository.save(torrent);
log.info("Torrent saved: " + torrent);
return saved;
}
/**
* Create new torrent.
*
* @param name Torrent name
* @param description Torrent description
* @param uploader Uploader
* @param file Torrent file
* @param repoId Repository root commit hash
* @return {@link Torrent}
* @throws IOException if file cannot be read
*/
@Transactional
public Torrent create(String name, String description, User uploader, MultipartFile file, String repoId)
throws IOException {
validateTorrentFile(file);
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name must not be empty.");
}
MimeType mimeType = mimeTypeService.getOrCreateByName(TORRENT_MIME_TYPE);
S3Object s3Object =
S3Object.builder().key(TORRENT_DIR + UUID.randomUUID()).size(file.getSize()).mimeType(mimeType).build();
s3ObjectService.save(s3Object, file.getInputStream());
Torrent torrent = Torrent.builder().name(name).description(description).repoId(repoId).file(s3Object)
.uploader(uploader).build();
return save(torrent);
}
/**
* Update torrent metadata.
*
* @param id Torrent id
* @param name New name (optional)
* @param description New description (optional)
* @return {@link Torrent}
*/
@Transactional
public Torrent updateMetadata(Long id, String name, String description) {
Torrent torrent = get(id);
if (name != null && !name.isBlank()) {
torrent.setName(name);
}
if (description != null) {
torrent.setDescription(description);
}
return save(torrent);
}
/**
* Update torrent file.
*
* @param id Torrent id
* @param file New torrent file
* @throws IOException if file cannot be read
*/
@Transactional
public void updateTorrentFile(Long id, MultipartFile file) throws IOException {
Torrent torrent = get(id);
validateTorrentFile(file);
final S3Object oldFile = torrent.getFile();
MimeType mimeType = mimeTypeService.getOrCreateByName(TORRENT_MIME_TYPE);
S3Object newFile =
S3Object.builder().key(TORRENT_DIR + UUID.randomUUID()).size(file.getSize()).mimeType(mimeType).build();
s3ObjectService.save(newFile, file.getInputStream());
torrent.setFile(newFile);
save(torrent);
// Delete old file after successfully saving new one
if (oldFile != null) {
s3ObjectService.delete(oldFile);
}
}
/**
* Download torrent file.
*
* @param id Torrent id
* @return {@link InputStream} of torrent file
*/
@Transactional(readOnly = true)
public InputStream downloadTorrentFile(Long id) {
Torrent torrent = get(id);
S3Object file = torrent.getFile();
if (file == null) {
throw new IllegalStateException("Torrent " + id + " has no file.");
}
return s3ObjectService.download(file);
}
/**
* Delete torrent.
*
* @param id Torrent id
*/
@Transactional
public void delete(Long id) {
Torrent torrent = get(id);
torrentRepository.delete(torrent);
S3Object file = torrent.getFile();
if (file != null) {
s3ObjectService.delete(file);
}
log.info("Torrent deleted: " + id);
}
/**
* Validate torrent file.
*
* @param file File to validate
* @throws IllegalArgumentException if file is invalid
*/
private void validateTorrentFile(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("File must not be empty.");
}
if (file.getSize() > maxTorrentSize) {
throw new IllegalArgumentException("File size exceeds limit (" + maxTorrentSize + " bytes).");
}
String contentType = file.getContentType();
if (!TORRENT_MIME_TYPE.equals(contentType)) {
throw new IllegalArgumentException("Only torrent files (application/x-bittorrent) are allowed.");
}
}
}