BackupService.java

package jasper.service;

import io.micrometer.core.annotation.Timed;
import jasper.component.Backup;
import jasper.component.Ingest;
import jasper.domain.Ref;
import jasper.errors.NotFoundException;
import jasper.repository.RefRepository;
import jasper.security.Auth;
import jasper.service.dto.BackupDto;
import jasper.service.dto.BackupOptionsDto;
import jasper.service.dto.DtoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static jasper.repository.spec.RefSpec.isUrl;
import static org.apache.commons.lang3.StringUtils.isBlank;

@Service
public class BackupService {

	private static final BackupOptionsDto DEFAULT_OPTIONS = BackupOptionsDto.builder()
		.ref(true)
		.ext(true)
		.user(true)
		.plugin(false)
		.template(false)
		.cache(true)
		.build();

	@Autowired
	Backup backup;

	@Autowired
	RefRepository refRepository;

	@Autowired
	Ingest ingest;

	@Autowired
	Auth auth;

	@Autowired
	DtoMapper mapper;

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.hasRole('MOD')")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public String createBackup(String origin, BackupOptionsDto options) throws IOException {
		var id = Instant.now().toString();
		if (options == null) options = DEFAULT_OPTIONS;
		if (options.getNewerThan() != null) {
			id += "_-_" + options.getNewerThan();
		}
		backup.createBackup(origin, id, options);
		return id;
	}

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.hasRole('MOD')")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public void uploadBackup(String origin, String id, InputStream zipFile) throws IOException {
		backup.store(origin, id, zipFile);
	}

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.hasRole('MOD')")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public List<BackupDto> listBackups(String origin) {
		return backup.listBackups(origin).stream()
			.map(mapper::domainToDto)
			.toList();
	}

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.minReadBackupRole()")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public Backup.BackupStream getBackup(String origin, String id) {
		return backup.get(origin, id);
	}

	@PreAuthorize("@auth.minReadBackupRole()")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public String getKey(String key) {
		var ref = refRepository.findOneByUrlAndOrigin("system:backup-key", auth.getOrigin()).orElse(null);
		if (ref == null) {
			ref = new Ref();
			ref.setUrl("system:backup-key");
			ref.setOrigin(auth.getOrigin());
			ref.addTag("internal");
			ref.addTag("_plugin/system");
			ref.setTitle("Backup Key");
			ref.setComment(UUID.randomUUID().toString());
			ingest.create(auth.getOrigin(), ref);
			return ref.getComment();
		}
		if (isBlank(ref.getComment()) || !ref.getComment().equals(key)) {
			ref.setComment(UUID.randomUUID().toString());
			ingest.update(auth.getOrigin(), ref);
		}
		return ref.getComment();
	}

	@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
	public void clearBackupKey() {
		var list = refRepository.findAll(isUrl("system:backup-key"));
		for (var ref : list) {
			if (ref.getCreated().isBefore(Instant.now().minus(15, ChronoUnit.MINUTES))) {
				ingest.delete(ref.getOrigin(), "system:backup-key", ref.getOrigin());
			}
		}
	}

	public boolean unlock(String key) {
		if (isBlank(key)) return false;
		var ref = refRepository.findOneByUrlAndOrigin("system:backup-key", auth.getOrigin()).orElse(null);
		if (ref == null) return false;
		return key.equals(ref.getComment());
	}

	@PreAuthorize("@auth.subOrigin(#origin)")
	public Backup.BackupStream getBackupPreauth(String origin, String id) {
		return backup.get(origin, id);
	}

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.hasRole('MOD')")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public void restoreBackup(String origin, String id, BackupOptionsDto options) {
		if (!backup.exists(origin, id)) throw new NotFoundException("Backup " + id);
		backup.restore(origin, id, options);
	}

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.hasRole('MOD')")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public void regen(String origin) {
		backup.regen(origin);
	}

	@PreAuthorize("@auth.subOrigin(#origin) and @auth.hasRole('MOD')")
	@Timed(value = "jasper.service", extraTags = {"service", "backup"}, histogram = true)
	public void deleteBackup(String origin, String id) throws IOException {
		if (!backup.exists(origin, id)) return; // Delete is idempotent
		backup.delete(origin, id);
	}
}