package de.is24.infrastructure.gridfs.http.gridfs;
import de.is24.infrastructure.gridfs.http.domain.YumEntry;
import de.is24.infrastructure.gridfs.http.domain.yum.YumPackage;
import de.is24.infrastructure.gridfs.http.domain.yum.YumPackageChecksum;
import de.is24.infrastructure.gridfs.http.exception.BadRequestException;
import de.is24.infrastructure.gridfs.http.exception.GridFSFileNotFoundException;
import de.is24.infrastructure.gridfs.http.exception.InvalidRpmHeaderException;
import de.is24.infrastructure.gridfs.http.exception.RepositoryIsUndeletableException;
import de.is24.infrastructure.gridfs.http.jaxb.Data;
import de.is24.infrastructure.gridfs.http.metadata.YumEntriesRepository;
import de.is24.infrastructure.gridfs.http.repos.RepoService;
import de.is24.infrastructure.gridfs.http.rpm.RpmHeaderToYumPackageConverter;
import de.is24.infrastructure.gridfs.http.rpm.RpmHeaderWrapper;
import de.is24.infrastructure.gridfs.http.rpm.version.YumPackageVersionComparator;
import de.is24.infrastructure.gridfs.http.storage.FileDescriptor;
import de.is24.infrastructure.gridfs.http.storage.FileStorageItem;
import de.is24.infrastructure.gridfs.http.storage.FileStorageService;
import de.is24.infrastructure.gridfs.http.storage.UploadResult;
import de.is24.util.monitoring.spring.TimeMeasurement;
import org.bson.types.ObjectId;
import org.redline_rpm.ReadableChannelWrapper;
import org.redline_rpm.Scanner;
import org.redline_rpm.header.Header;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import static de.is24.infrastructure.gridfs.http.domain.RepoType.SCHEDULED;
import static de.is24.infrastructure.gridfs.http.domain.RepoType.STATIC;
import static de.is24.infrastructure.gridfs.http.metadata.generation.DbGenerator.DB_VERSION;
import static de.is24.infrastructure.gridfs.http.mongo.DatabaseStructure.SHA256_KEY;
import static de.is24.infrastructure.gridfs.http.repos.RepositoryNameValidator.validateRepoName;
import static de.is24.infrastructure.gridfs.http.security.Permission.PROPAGATE_FILE;
import static de.is24.infrastructure.gridfs.http.security.Permission.PROPAGATE_REPO;
import static java.nio.channels.Channels.newChannel;
import static java.util.Collections.sort;
import static org.apache.commons.lang.StringUtils.countMatches;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.substringAfter;
@ManagedResource
@Service
public class StorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(StorageService.class);
private static final int BUFFER_SIZE = 16 * 1024 * 1024;
private final FileStorageService fileStorageService;
private final YumEntriesRepository yumEntriesRepository;
private final RepoService repoService;
private YumPackageVersionComparator comparator = new YumPackageVersionComparator();
//needed for cglib proxy
public StorageService() {
this.yumEntriesRepository = null;
this.fileStorageService = null;
this.repoService = null;
}
@Autowired
public StorageService(FileStorageService fileStorageService,
YumEntriesRepository yumEntriesRepository, RepoService repoService) {
this.fileStorageService = fileStorageService;
this.yumEntriesRepository = yumEntriesRepository;
this.repoService = repoService;
}
@TimeMeasurement
@PreAuthorize("hasPermission(#sourceFile, '" + PROPAGATE_FILE + "')")
public FileDescriptor propagateRpm(String sourceFile, String destinationRepo) {
validatePathToRpm(sourceFile);
FileDescriptor descriptor = new FileDescriptor(sourceFile);
validateDestinationRepo(descriptor.getRepo(), destinationRepo);
FileStorageItem storageItem = findRpmByPathDirectlyOrFindNewestRpmMatchingNameAndArch(descriptor);
if (storageItem == null) {
throw new GridFSFileNotFoundException("Could not find file.", sourceFile);
}
if (storageItem.isMarkedAsDeleted()) {
throw new GridFSFileNotFoundException("Could not propagate file. File is marked for deletion.", sourceFile);
}
String sourceRepo = storageItem.getRepo();
FileDescriptor fileDescriptor = move(storageItem, destinationRepo);
repoService.createOrUpdate(sourceRepo);
repoService.createOrUpdate(destinationRepo);
return fileDescriptor;
}
@TimeMeasurement
public void delete(FileDescriptor descriptor) {
FileStorageItem dbFile = fileStorageService.getFileBy(descriptor);
delete(dbFile);
String sourceRepo = dbFile.getRepo();
if (sourceRepo != null) {
repoService.createOrUpdate(sourceRepo);
}
}
@TimeMeasurement
public void delete(List<FileStorageItem> storageItems) {
storageItems.forEach(this::delete);
}
private FileStorageItem findRpmByPathDirectlyOrFindNewestRpmMatchingNameAndArch(FileDescriptor descriptor) {
FileStorageItem dbFile = fileStorageService.findBy(descriptor);
if (dbFile != null) {
if (!dbFile.getFilename().endsWith(".rpm")) {
throw new BadRequestException("Source rpm must end with .rpm!");
}
return dbFile;
}
return findNewestRpmByPath(descriptor);
}
private FileStorageItem findNewestRpmByPath(FileDescriptor descriptor) {
return findNewestRpmInRepoByNameAndArch(descriptor.getRepo(), descriptor.getArch(), descriptor.getFilename());
}
private FileStorageItem findNewestRpmInRepoByNameAndArch(String repo, String arch, String name) {
List<YumEntry> entries = yumEntriesRepository.findByRepoAndYumPackageArchAndYumPackageName(repo, arch, name);
if (entries.isEmpty()) {
return null;
}
sort(entries, (YumEntry entry1, YumEntry entry2) -> comparator.compare(
entry1.getYumPackage().getVersion(),
entry2.getYumPackage().getVersion()));
YumEntry lastEntry = entries.get(entries.size() - 1);
return fileStorageService.findById(lastEntry.getId());
}
private FileDescriptor move(FileStorageItem storageItem, String destinationRepo) {
YumEntry yumEntry = yumEntriesRepository.findOne((ObjectId) storageItem.getId());
if (yumEntry == null) {
try {
yumEntry = regenerateMetadataFor(storageItem);
} catch (InvalidRpmHeaderException e) {
throw new RuntimeException("Could not regenerate metadata for file " + storageItem.getFilename() + " because it is not a valid RPM. You should manually delete the file", e);
}
}
yumEntry.setRepo(null);
yumEntriesRepository.save(yumEntry);
FileDescriptor descriptor = new FileDescriptor(storageItem);
descriptor.setRepo(destinationRepo);
FileStorageItem storageItemToOverride = fileStorageService.findBy(descriptor);
fileStorageService.moveTo(storageItem, destinationRepo);
if (storageItemToOverride != null) {
delete(storageItemToOverride);
}
yumEntry.setRepo(destinationRepo);
yumEntriesRepository.save(yumEntry);
return descriptor;
}
@PreAuthorize("hasPermission(#sourceRepo, '" + PROPAGATE_REPO + "')")
public void propagateRepository(String sourceRepo, String destinationRepo) {
validateRepoName(sourceRepo);
validateDestinationRepo(sourceRepo, destinationRepo);
List<FileStorageItem> sourceStorageItems = fileStorageService.getAllRpms(sourceRepo);
sourceStorageItems.stream().filter(storageItem -> !storageItem.isMarkedAsDeleted()).
forEach(storageItem -> move(storageItem, destinationRepo));
repoService.createOrUpdate(sourceRepo);
repoService.createOrUpdate(destinationRepo);
}
@TimeMeasurement
public void storeRpm(String reponame, InputStream inputStream) throws InvalidRpmHeaderException, IOException {
validateRepoName(reponame);
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream, BUFFER_SIZE);
bufferedInputStream.mark(BUFFER_SIZE);
YumPackage yumPackage = convertHeader(bufferedInputStream);
bufferedInputStream.reset();
FileDescriptor descriptor = new FileDescriptor(reponame, yumPackage);
final FileStorageItem storageItem = fileStorageService.storeFile(bufferedInputStream, descriptor);
yumEntriesRepository.save(createYumEntry(yumPackage, storageItem));
repoService.createOrUpdate(reponame);
LOGGER.info("Stored RPM {}/{}", reponame, yumPackage.getLocation().getHref());
}
@TimeMeasurement
public Data storeRepodataDbBz2(String reponame, File metadataFile, String name) throws IOException {
validateRepoName(reponame);
UploadResult uploadResult = fileStorageService.storeSqliteFileCompressedWithChecksumName(reponame, metadataFile, name);
return createRepoMdData(uploadResult);
}
@TimeMeasurement
public void deleteRepository(String reponame) {
validateRepoName(reponame);
if (repoService.ensureEntry(reponame, STATIC, SCHEDULED).isUndeletable()) {
throw new RepositoryIsUndeletableException(reponame);
}
yumEntriesRepository.deleteByRepo(reponame);
fileStorageService.deleteRepo(reponame);
repoService.delete(reponame);
}
@ManagedOperation
public YumEntry regenerateMetadataFor(FileDescriptor descriptor) throws InvalidRpmHeaderException {
return regenerateMetadataFor(fileStorageService.getFileBy(descriptor));
}
@ManagedOperation
public void regenerateMetadataForAllFiles() throws InvalidRpmHeaderException {
for (FileStorageItem storageItem : fileStorageService.getAllRpms()) {
regenerateMetadataFor(storageItem);
}
}
private void validatePathToRpm(String path) {
if (isBlank(path)) {
throw new BadRequestException("Source rpm is not allowed to be blank!");
}
if (countMatches(path, "/") != 2) {
throw new BadRequestException("Rpm file has invalid depth!");
}
}
private void validateDestinationRepo(String sourceRepo, String destinationRepo) {
validateRepoName(destinationRepo);
if (destinationRepo.equals(sourceRepo)) {
throw new BadRequestException("Destination repo have not to be equals to source repo: " + sourceRepo);
}
}
private void delete(FileStorageItem storageItem) {
yumEntriesRepository.delete((ObjectId) storageItem.getId());
fileStorageService.delete(storageItem);
LOGGER.info("Deleted {}", storageItem.getFilename());
}
private YumPackage convertHeader(InputStream inputStream) throws InvalidRpmHeaderException {
RpmHeaderWrapper headerWrapper = new RpmHeaderWrapper(readHeader(inputStream));
RpmHeaderToYumPackageConverter converter = new RpmHeaderToYumPackageConverter(headerWrapper);
return converter.convert();
}
private YumEntry createYumEntry(YumPackage yumPackage, FileStorageItem storageItem) {
yumPackage.getSize().setPackaged((int) storageItem.getSize());
yumPackage.setChecksum(new YumPackageChecksum("sha256", storageItem.getChecksumSha256()));
yumPackage.getTime().setFile(yumTime(storageItem.getUploadDate()));
return new YumEntry((ObjectId) storageItem.getId(), storageItem.getRepo(), yumPackage);
}
private Data createRepoMdData(UploadResult uploadResult) {
Data data = new Data();
data.setChecksum(SHA256_KEY, uploadResult.getCompressedChecksum());
data.setSize(uploadResult.getCompressedSize());
data.setOpenSize(uploadResult.getUncompressedSize());
data.setOpenChecksum(SHA256_KEY, uploadResult.getUncompressedChecksum());
data.setLocation(substringAfter(uploadResult.getLocation(), "/"));
data.setTimestamp((int) (uploadResult.getUploadDate().getTime() / 1000));
data.setDatabaseVersion(DB_VERSION);
return data;
}
private static int yumTime(Date date) {
return (int) (date.getTime() / 1000);
}
private Header readHeader(InputStream inputStream) throws InvalidRpmHeaderException {
ReadableChannelWrapper channel = new ReadableChannelWrapper(newChannel(inputStream));
try {
return new Scanner().run(channel).getHeader();
} catch (Exception e) {
throw new InvalidRpmHeaderException("Could not read rpm header.", e);
}
}
private YumEntry regenerateMetadataFor(FileStorageItem storageItem) throws InvalidRpmHeaderException {
LOGGER.info("regenerating metadata for {}", storageItem.getFilename());
try {
YumPackage yumPackage = convertHeader(storageItem.getInputStream());
YumEntry yumEntry = createYumEntry(yumPackage, storageItem);
yumEntriesRepository.save(yumEntry);
return yumEntry;
} catch (InvalidRpmHeaderException e) {
LOGGER.error("Generating metadata for " + storageItem.getFilename() + " failed.", e);
throw e;
}
}
}