BackupController.java

package jasper.web.rest;

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.constraints.Pattern;
import jasper.aop.ClearIdle;
import jasper.component.Backup;
import jasper.domain.proj.HasOrigin;
import jasper.errors.NotFoundException;
import jasper.service.BackupService;
import jasper.service.dto.BackupDto;
import jasper.service.dto.BackupOptionsDto;
import org.hibernate.validator.constraints.Length;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static jasper.domain.proj.HasOrigin.ORIGIN_LEN;
import static jasper.service.dto.BackupOptionsDto.ID_LEN;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.springframework.http.MediaType.parseMediaType;

@ClearIdle
@RestController
@RequestMapping("api/v1/backup")
@Tag(name = "Backup")
@ApiResponses({
	@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(ref = "https://opensource.zalando.com/problem/schema.yaml#/Problem"))),
})
public class BackupController {

	@Autowired
	BackupService backupService;

	@ApiResponses({
		@ApiResponse(responseCode = "201"),
	})
	@PostMapping
	String createBackup(
		@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin,
		@RequestBody(required = false) BackupOptionsDto options
	) throws IOException {
		return backupService.createBackup(origin, options);
	}

	@ApiResponses({
		@ApiResponse(responseCode = "200"),
	})
	@GetMapping
	List<BackupDto> listBackups(@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin) {
		return backupService.listBackups(origin);
	}

	@ApiResponses({
		@ApiResponse(responseCode = "200"),
		@ApiResponse(responseCode = "304", content = @Content()),
		@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(ref = "https://opensource.zalando.com/problem/schema.yaml#/Problem"))),
		@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(ref = "https://opensource.zalando.com/problem/schema.yaml#/Problem"))),
	})
	@GetMapping("{id}")
	ResponseEntity<StreamingResponseBody> downloadBackup(
		@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin,
		@PathVariable @Length(max = ID_LEN) String id,
		@RequestParam(defaultValue = "") String p
	) {
		if (id.endsWith(".zip")) {
			id = id.substring(0, id.length() - 4);
		}
		Backup.BackupStream b;
		if (isBlank(p)) {
			b = backupService.getBackup(origin, id);
		} else {
			if (!backupService.unlock(p)) throw new NotFoundException(id);
			b = backupService.getBackupPreauth(origin, id);
		}
		if (b == null) throw new NotFoundException("Storage unavailable!");
		return ResponseEntity.ok()
			.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + URLEncoder.encode(id + ".zip", StandardCharsets.UTF_8).replace("+", "%20"))
			.contentLength(b.size())
			.contentType(parseMediaType("application/zip"))
			.cacheControl(CacheControl.maxAge(100, TimeUnit.DAYS).cachePrivate())
			.body(outputStream -> {
				try (var is = b.inputStream()) {
					byte[] buffer = new byte[64 * 1024];
					int bytesRead;
					while ((bytesRead = is.read(buffer)) != -1) {
						outputStream.write(buffer, 0, bytesRead);
					}
				}
			});
	}

	@ApiResponses({
		@ApiResponse(responseCode = "200"),
		@ApiResponse(responseCode = "304", content = @Content()),
		@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(ref = "https://opensource.zalando.com/problem/schema.yaml#/Problem"))),
		@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(ref = "https://opensource.zalando.com/problem/schema.yaml#/Problem"))),
	})
	@PostMapping("key")
	String getBackupKey(
		@RequestParam(defaultValue = "") String key
	) {
		return backupService.getKey(key);
	}

	@ApiResponses({
		@ApiResponse(responseCode = "201"),
	})
	@PostMapping("upload/{id}")
	@ResponseStatus(HttpStatus.CREATED)
	void uploadBackup(
		@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin,
		@PathVariable @Length(max = ID_LEN) String id,
		InputStream zipFile
	) throws IOException {
		if (id.endsWith(".zip")) {
			id = id.substring(0, id.length() - 4);
		}
		backupService.uploadBackup(origin, id, zipFile);
	}

	@ApiResponses({
		@ApiResponse(responseCode = "204"),
		@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(ref = "https://opensource.zalando.com/problem/schema.yaml#/Problem"))),
	})
	@PostMapping("restore/{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void restoreBackup(
		@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin,
		@PathVariable @Length(max = ID_LEN) String id,
		@RequestBody(required = false) BackupOptionsDto options
	) {
		if (id.endsWith(".zip")) {
			id = id.substring(0, id.length() - 4);
		}
		backupService.restoreBackup(origin, id, options);
	}

	@ApiResponses({
		@ApiResponse(responseCode = "204"),
	})
	@PostMapping("regen")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void regen(@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin) {
		backupService.regen(origin);
	}

	@ApiResponses({
		@ApiResponse(responseCode = "204"),
	})
	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteBackup(
		@RequestParam(defaultValue = "") @Length(max = ORIGIN_LEN) @Pattern(regexp = HasOrigin.REGEX) String origin,
		@PathVariable @Length(max = ID_LEN) String id
	) throws IOException {
		if (id.endsWith(".zip")) {
			id = id.substring(0, id.length() - 4);
		}
		backupService.deleteBackup(origin, id);
	}
}