mirror of
https://github.com/halo-dev/halo.git
synced 2024-10-23 09:26:03 +08:00
Support backup and restore (#4206)
#### What type of PR is this?
/kind feature
/area core
#### What this PR does / why we need it:
See 9921deb076/docs/backup-and-restore.md
for more.
<img width="1906" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/41531186-d305-44fd-8bdc-30df9b71af43">
<img width="1909" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/3d7af1b9-37ad-4a40-9b81-f15ed0f1f6e8">
#### Which issue(s) this PR fixes:
Fixes https://github.com/halo-dev/halo/issues/4059
Fixes https://github.com/halo-dev/halo/issues/3274
#### Special notes for your reviewer:
#### Does this PR introduce a user-facing change?
```release-note
支持备份和恢复功能。
```
This commit is contained in:
parent
5ce47190fa
commit
bd912c36b9
@ -508,3 +508,6 @@ ij_html_text_wrap = normal
|
||||
indent_size = 2
|
||||
ij_yaml_keep_indents_on_empty_lines = false
|
||||
ij_yaml_keep_line_breaks = true
|
||||
|
||||
[*.md]
|
||||
indent_size = 2
|
@ -1,5 +1,6 @@
|
||||
package run.halo.app.extension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@ -11,21 +12,26 @@ public enum ExtensionUtil {
|
||||
&& extension.getMetadata().getDeletionTimestamp() != null;
|
||||
}
|
||||
|
||||
public static void addFinalizers(MetadataOperator metadata, Set<String> finalizers) {
|
||||
var existingFinalizers = metadata.getFinalizers();
|
||||
if (existingFinalizers == null) {
|
||||
existingFinalizers = new HashSet<>();
|
||||
public static boolean addFinalizers(MetadataOperator metadata, Set<String> finalizers) {
|
||||
var modifiableFinalizers = new HashSet<>(
|
||||
metadata.getFinalizers() == null ? Collections.emptySet() : metadata.getFinalizers());
|
||||
var added = modifiableFinalizers.addAll(finalizers);
|
||||
if (added) {
|
||||
metadata.setFinalizers(modifiableFinalizers);
|
||||
}
|
||||
existingFinalizers.addAll(finalizers);
|
||||
metadata.setFinalizers(existingFinalizers);
|
||||
return added;
|
||||
}
|
||||
|
||||
public static void removeFinalizers(MetadataOperator metadata, Set<String> finalizers) {
|
||||
var existingFinalizers = metadata.getFinalizers();
|
||||
if (existingFinalizers != null) {
|
||||
existingFinalizers.removeAll(finalizers);
|
||||
public static boolean removeFinalizers(MetadataOperator metadata, Set<String> finalizers) {
|
||||
if (metadata.getFinalizers() == null) {
|
||||
return false;
|
||||
}
|
||||
metadata.setFinalizers(existingFinalizers);
|
||||
var existingFinalizers = new HashSet<>(metadata.getFinalizers());
|
||||
var removed = existingFinalizers.removeAll(finalizers);
|
||||
if (removed) {
|
||||
metadata.setFinalizers(existingFinalizers);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,25 +34,25 @@ class ExtensionUtilTest {
|
||||
void addFinalizers() {
|
||||
var metadata = new Metadata();
|
||||
assertNull(metadata.getFinalizers());
|
||||
ExtensionUtil.addFinalizers(metadata, Set.of("fake"));
|
||||
assertTrue(ExtensionUtil.addFinalizers(metadata, Set.of("fake")));
|
||||
|
||||
assertEquals(Set.of("fake"), metadata.getFinalizers());
|
||||
|
||||
ExtensionUtil.addFinalizers(metadata, Set.of("fake"));
|
||||
assertFalse(ExtensionUtil.addFinalizers(metadata, Set.of("fake")));
|
||||
assertEquals(Set.of("fake"), metadata.getFinalizers());
|
||||
|
||||
ExtensionUtil.addFinalizers(metadata, Set.of("another-fake"));
|
||||
assertTrue(ExtensionUtil.addFinalizers(metadata, Set.of("another-fake")));
|
||||
assertEquals(Set.of("fake", "another-fake"), metadata.getFinalizers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeFinalizers() {
|
||||
var metadata = new Metadata();
|
||||
ExtensionUtil.removeFinalizers(metadata, Set.of("fake"));
|
||||
assertFalse(ExtensionUtil.removeFinalizers(metadata, Set.of("fake")));
|
||||
assertNull(metadata.getFinalizers());
|
||||
|
||||
metadata.setFinalizers(new HashSet<>(Set.of("fake")));
|
||||
ExtensionUtil.removeFinalizers(metadata, Set.of("fake"));
|
||||
assertTrue(ExtensionUtil.removeFinalizers(metadata, Set.of("fake")));
|
||||
assertEquals(Set.of(), metadata.getFinalizers());
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.Secret;
|
||||
import run.halo.app.migration.Backup;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionDefinition;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionPointDefinition;
|
||||
import run.halo.app.search.extension.SearchEngine;
|
||||
@ -89,6 +90,9 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
||||
schemeManager.register(AuthProvider.class);
|
||||
schemeManager.register(UserConnection.class);
|
||||
|
||||
// migration.halo.run
|
||||
schemeManager.register(Backup.class);
|
||||
|
||||
eventPublisher.publishEvent(new SchemeInitializedEvent(this));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package run.halo.app.infra.utils;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
import static org.springframework.util.FileSystemUtils.deleteRecursively;
|
||||
|
||||
import java.io.Closeable;
|
||||
@ -12,7 +13,9 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.stream.Stream;
|
||||
@ -21,6 +24,7 @@ import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.infra.exception.AccessDeniedException;
|
||||
|
||||
@ -246,4 +250,31 @@ public abstract class FileUtils {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyRecursively(Path src, Path target, Set<String> excludes)
|
||||
throws IOException {
|
||||
var pathMatcher = new AntPathMatcher();
|
||||
Predicate<Path> shouldExclude = path -> excludes.stream()
|
||||
.anyMatch(pattern -> pathMatcher.match(pattern, path.toString()));
|
||||
Files.walkFileTree(src, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
if (!shouldExclude.test(src.relativize(file))) {
|
||||
Files.copy(file, target.resolve(src.relativize(file)), REPLACE_EXISTING);
|
||||
}
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
if (shouldExclude.test(src.relativize(dir))) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
Files.createDirectories(target.resolve(src.relativize(dir)));
|
||||
return super.preVisitDirectory(dir, attrs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
65
application/src/main/java/run/halo/app/migration/Backup.java
Normal file
65
application/src/main/java/run/halo/app/migration/Backup.java
Normal file
@ -0,0 +1,65 @@
|
||||
package run.halo.app.migration;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "migration.halo.run", version = "v1alpha1", kind = "Backup",
|
||||
plural = "backups", singular = "backup")
|
||||
public class Backup extends AbstractExtension {
|
||||
|
||||
private Spec spec = new Spec();
|
||||
|
||||
private Status status = new Status();
|
||||
|
||||
@Data
|
||||
@Schema(name = "BackupSpec")
|
||||
public static class Spec {
|
||||
|
||||
@Schema(description = "Backup file format. Currently, only zip format is supported.")
|
||||
private String format;
|
||||
|
||||
private Instant expiresAt;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(name = "BackupStatus")
|
||||
public static class Status {
|
||||
|
||||
private Phase phase = Phase.PENDING;
|
||||
|
||||
private Instant startTimestamp;
|
||||
|
||||
private Instant completionTimestamp;
|
||||
|
||||
private String failureReason;
|
||||
|
||||
private String failureMessage;
|
||||
|
||||
/**
|
||||
* Size of backup file. Data unit: byte
|
||||
*/
|
||||
private Long size;
|
||||
|
||||
/**
|
||||
* Name of backup file.
|
||||
*/
|
||||
private String filename;
|
||||
}
|
||||
|
||||
public enum Phase {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
SUCCEEDED,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package run.halo.app.migration;
|
||||
|
||||
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
|
||||
import static run.halo.app.extension.ExtensionUtil.isDeleted;
|
||||
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
||||
import static run.halo.app.extension.controller.Reconciler.Result.doNotRetry;
|
||||
import static run.halo.app.migration.Constant.HOUSE_KEEPER_FINALIZER;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.Exceptions;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.Reconciler.Request;
|
||||
import run.halo.app.migration.Backup.Phase;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BackupReconciler implements Reconciler<Request> {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
private final MigrationService migrationService;
|
||||
|
||||
private Clock clock;
|
||||
|
||||
public BackupReconciler(ExtensionClient client, MigrationService migrationService) {
|
||||
this.client = client;
|
||||
this.migrationService = migrationService;
|
||||
clock = Clock.systemDefaultZone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clock. The method is only for unit test.
|
||||
*
|
||||
* @param clock is new clock
|
||||
*/
|
||||
void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return client.fetch(Backup.class, request.name())
|
||||
.map(backup -> {
|
||||
var metadata = backup.getMetadata();
|
||||
var status = backup.getStatus();
|
||||
var spec = backup.getSpec();
|
||||
if (isDeleted(backup)) {
|
||||
if (removeFinalizers(metadata, Set.of(HOUSE_KEEPER_FINALIZER))) {
|
||||
migrationService.cleanup(backup).block();
|
||||
client.update(backup);
|
||||
}
|
||||
return doNotRetry();
|
||||
}
|
||||
if (addFinalizers(metadata, Set.of(HOUSE_KEEPER_FINALIZER))) {
|
||||
client.update(backup);
|
||||
}
|
||||
|
||||
if (Phase.PENDING.equals(status.getPhase())) {
|
||||
// Do backup
|
||||
try {
|
||||
status.setPhase(Phase.RUNNING);
|
||||
status.setStartTimestamp(Instant.now(clock));
|
||||
updateStatus(request.name(), status);
|
||||
// Long period execution when backing up
|
||||
migrationService.backup(backup).block();
|
||||
status.setPhase(Phase.SUCCEEDED);
|
||||
status.setCompletionTimestamp(Instant.now(clock));
|
||||
updateStatus(request.name(), status);
|
||||
} catch (Throwable t) {
|
||||
var unwrapped = Exceptions.unwrap(t);
|
||||
log.error("Failed to backup", unwrapped);
|
||||
// Only happen when shutting down
|
||||
status.setPhase(Phase.FAILED);
|
||||
if (unwrapped instanceof InterruptedException) {
|
||||
status.setFailureReason("Interrupted");
|
||||
status.setFailureMessage("The backup process was interrupted.");
|
||||
} else {
|
||||
status.setFailureReason("SystemError");
|
||||
status.setFailureMessage(
|
||||
"Something went wrong! Error message: " + unwrapped.getMessage());
|
||||
}
|
||||
updateStatus(request.name(), status);
|
||||
}
|
||||
}
|
||||
// Only happen when failing to update status when interrupted
|
||||
if (Phase.RUNNING.equals(status.getPhase())) {
|
||||
status.setPhase(Phase.FAILED);
|
||||
status.setFailureReason("UnexpectedExit");
|
||||
status.setFailureMessage("The backup process may exit abnormally.");
|
||||
updateStatus(request.name(), status);
|
||||
}
|
||||
// Check the expires at and requeue if necessary
|
||||
if (isTerminal(status.getPhase())) {
|
||||
var expiresAt = spec.getExpiresAt();
|
||||
if (expiresAt != null) {
|
||||
var now = Instant.now(clock);
|
||||
if (now.isBefore(expiresAt)) {
|
||||
return new Result(true, Duration.between(now, expiresAt));
|
||||
}
|
||||
client.delete(backup);
|
||||
}
|
||||
}
|
||||
return doNotRetry();
|
||||
}).orElseGet(Result::doNotRetry);
|
||||
}
|
||||
|
||||
private void updateStatus(String name, Backup.Status status) {
|
||||
client.fetch(Backup.class, name)
|
||||
.ifPresent(backup -> {
|
||||
backup.setStatus(status);
|
||||
client.update(backup);
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isTerminal(Phase phase) {
|
||||
return Phase.FAILED.equals(phase) || Phase.SUCCEEDED.equals(phase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new Backup())
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package run.halo.app.migration;
|
||||
|
||||
public enum Constant {
|
||||
;
|
||||
|
||||
public static final String GROUP = "migration.halo.run";
|
||||
|
||||
public static final String VERSION = "v1alpha1";
|
||||
|
||||
public static final String HOUSE_KEEPER_FINALIZER = "housekeeper";
|
||||
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package run.halo.app.migration;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
@Component
|
||||
public class MigrationEndpoint implements CustomEndpoint {
|
||||
|
||||
private final MigrationService migrationService;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public MigrationEndpoint(MigrationService migrationService, ReactiveExtensionClient client) {
|
||||
this.migrationService = migrationService;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
var tag = groupVersion().toString() + "/Migration";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.GET("/backups/{name}/files/{filename}",
|
||||
request -> {
|
||||
var name = request.pathVariable("name");
|
||||
return client.get(Backup.class, name)
|
||||
.flatMap(migrationService::download)
|
||||
.flatMap(backupResource -> ServerResponse.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + backupResource.getFilename() + "\"")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.bodyValue(backupResource));
|
||||
},
|
||||
builder -> builder
|
||||
.tag(tag)
|
||||
.operationId("DownloadBackups")
|
||||
.parameter(parameterBuilder()
|
||||
.name("name")
|
||||
.description("Backup name.")
|
||||
.required(true)
|
||||
.in(ParameterIn.PATH))
|
||||
.parameter(parameterBuilder()
|
||||
.name("filename")
|
||||
.description("Backup filename.")
|
||||
.required(true)
|
||||
.in(ParameterIn.PATH))
|
||||
.build())
|
||||
.POST("/restorations", request -> request.multipartData()
|
||||
.map(RestoreRequest::new)
|
||||
.flatMap(restoreRequest -> migrationService.restore(
|
||||
restoreRequest.getFile().content()))
|
||||
.flatMap(v -> ServerResponse.ok().bodyValue("Restored successfully!")),
|
||||
builder -> builder
|
||||
.tag(tag)
|
||||
.operationId("RestoreBackup")
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
.schema(schemaBuilder().implementation(RestoreRequest.class))
|
||||
)
|
||||
)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static class RestoreRequest {
|
||||
private final MultiValueMap<String, Part> multipart;
|
||||
|
||||
public RestoreRequest(MultiValueMap<String, Part> multipart) {
|
||||
this.multipart = multipart;
|
||||
}
|
||||
|
||||
@Schema(requiredMode = REQUIRED, name = "file", description = "Backup file.")
|
||||
public FilePart getFile() {
|
||||
var part = multipart.getFirst("file");
|
||||
if (part instanceof FilePart filePart) {
|
||||
return filePart;
|
||||
}
|
||||
throw new ServerWebInputException("Invalid file part");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion(
|
||||
"api.console." + Constant.GROUP + "/" + Constant.VERSION);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package run.halo.app.migration;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface MigrationService {
|
||||
|
||||
Mono<Void> backup(Backup backup);
|
||||
|
||||
Mono<Resource> download(Backup backup);
|
||||
|
||||
Mono<Void> restore(Publisher<DataBuffer> content);
|
||||
|
||||
/**
|
||||
* Clean up backup file.
|
||||
*
|
||||
* @param backup backup detail.
|
||||
* @return void publisher.
|
||||
*/
|
||||
Mono<Void> cleanup(Backup backup);
|
||||
|
||||
}
|
@ -0,0 +1,275 @@
|
||||
package run.halo.app.migration.impl;
|
||||
|
||||
import static org.springframework.core.io.buffer.DataBufferUtils.releaseConsumer;
|
||||
import static run.halo.app.infra.utils.FileUtils.closeQuietly;
|
||||
import static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;
|
||||
|
||||
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
|
||||
import com.fasterxml.jackson.databind.MappingIterator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.context.Context;
|
||||
import run.halo.app.extension.store.ExtensionStore;
|
||||
import run.halo.app.extension.store.ExtensionStoreRepository;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.migration.Backup;
|
||||
import run.halo.app.migration.MigrationService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MigrationServiceImpl implements MigrationService {
|
||||
|
||||
private final ExtensionStoreRepository repository;
|
||||
|
||||
private final HaloProperties haloProperties;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final Set<String> excludes = Set.of(
|
||||
"**/.git/**",
|
||||
"**/node_modules/**",
|
||||
"backups/**",
|
||||
"db/**",
|
||||
"logs/**",
|
||||
"docker-compose.yaml",
|
||||
"docker-compose.yml",
|
||||
"mysql/**",
|
||||
"mysqlBackup/**",
|
||||
"**/.idea/**",
|
||||
"**/.vscode/**"
|
||||
);
|
||||
|
||||
private final DateTimeFormatter dateTimeFormatter;
|
||||
|
||||
public MigrationServiceImpl(ExtensionStoreRepository repository,
|
||||
HaloProperties haloProperties) {
|
||||
this.repository = repository;
|
||||
this.haloProperties = haloProperties;
|
||||
this.objectMapper = JsonMapper.builder()
|
||||
.defaultPrettyPrinter(new MinimalPrettyPrinter())
|
||||
.build();
|
||||
this.dateTimeFormatter = DateTimeFormatter
|
||||
.ofPattern("yyyyMMddHHmmss")
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
DateTimeFormatter getDateTimeFormatter() {
|
||||
return dateTimeFormatter;
|
||||
}
|
||||
|
||||
ObjectMapper getObjectMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
Path getBackupsRoot() {
|
||||
return haloProperties.getWorkDir().resolve("backups");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> backup(Backup backup) {
|
||||
try {
|
||||
// create temporary folder to store all backup files into single files.
|
||||
var tempDir = Files.createTempDirectory("halo-full-backup-");
|
||||
return backupExtensions(tempDir)
|
||||
.and(backupWorkDir(tempDir))
|
||||
.and(packageBackup(tempDir, backup))
|
||||
.doFinally(signalType -> deleteRecursivelyAndSilently(tempDir))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Resource> download(Backup backup) {
|
||||
var status = backup.getStatus();
|
||||
if (!Backup.Phase.SUCCEEDED.equals(status.getPhase()) || status.getFilename() == null) {
|
||||
return Mono.error(new ServerWebInputException("Current backup is not downloadable."));
|
||||
}
|
||||
|
||||
var backupFile = getBackupsRoot()
|
||||
.resolve(status.getFilename());
|
||||
|
||||
return Mono.just(new FileSystemResource(backupFile));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Mono<Void> restore(Publisher<DataBuffer> content) {
|
||||
return Mono.defer(() -> {
|
||||
try {
|
||||
var tempDir = Files.createTempDirectory("halo-restore-");
|
||||
return unpackBackup(content, tempDir)
|
||||
.and(restoreExtensions(tempDir))
|
||||
.and(restoreWorkdir(tempDir))
|
||||
.doFinally(signalType -> deleteRecursivelyAndSilently(tempDir))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> cleanup(Backup backup) {
|
||||
return Mono.<Void>fromRunnable(() -> {
|
||||
var status = backup.getStatus();
|
||||
if (status == null || status.getFilename() == null) {
|
||||
return;
|
||||
}
|
||||
var filename = status.getFilename();
|
||||
var backupsRoot = getBackupsRoot();
|
||||
var backupFile = backupsRoot.resolve(filename);
|
||||
try {
|
||||
FileUtils.checkDirectoryTraversal(backupsRoot, backupFile);
|
||||
Files.deleteIfExists(backupFile);
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private Mono<Void> restoreWorkdir(Path backupRoot) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
try {
|
||||
var workdir = backupRoot.resolve("workdir");
|
||||
if (Files.exists(workdir)) {
|
||||
FileSystemUtils.copyRecursively(workdir, haloProperties.getWorkDir());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> restoreExtensions(Path backupRoot) {
|
||||
var extensionsPath = backupRoot.resolve("extensions.data");
|
||||
var reader = objectMapper.readerFor(ExtensionStore.class);
|
||||
return Mono.<Void, MappingIterator<ExtensionStore>>using(
|
||||
() -> reader.readValues(extensionsPath.toFile()),
|
||||
itr -> Flux.<ExtensionStore>create(
|
||||
sink -> {
|
||||
while (itr.hasNext()) {
|
||||
sink.next(itr.next());
|
||||
}
|
||||
sink.complete();
|
||||
})
|
||||
// reset version
|
||||
.doOnNext(extensionStore -> extensionStore.setVersion(null))
|
||||
.buffer(100)
|
||||
// We might encounter OptimisticLockingFailureException when saving extension store,
|
||||
// So we have to delete all extension stores before saving.
|
||||
.flatMap(extensionStores -> repository.deleteAll(extensionStores)
|
||||
.thenMany(repository.saveAll(extensionStores)))
|
||||
.doOnNext(extensionStore ->
|
||||
log.info("Restored extension store: {}", extensionStore.getName()))
|
||||
.then(),
|
||||
itr -> {
|
||||
try {
|
||||
itr.close();
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> unpackBackup(Publisher<DataBuffer> content, Path target) {
|
||||
return Mono.create(sink -> {
|
||||
try (var pipedIs = new PipedInputStream();
|
||||
var pipedOs = new PipedOutputStream(pipedIs);
|
||||
var zipIs = new ZipInputStream(pipedIs)) {
|
||||
DataBufferUtils.write(content, pipedOs)
|
||||
.subscribe(
|
||||
releaseConsumer(),
|
||||
sink::error,
|
||||
() -> closeQuietly(pipedOs),
|
||||
Context.of(sink.contextView()));
|
||||
FileUtils.unzip(zipIs, target);
|
||||
sink.success();
|
||||
} catch (IOException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> packageBackup(Path baseDir, Backup backup) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
try {
|
||||
var backupsFolder = getBackupsRoot();
|
||||
Files.createDirectories(backupsFolder);
|
||||
var backupName = backup.getMetadata().getName();
|
||||
var startTimestamp = backup.getStatus().getStartTimestamp();
|
||||
var timePart = this.dateTimeFormatter.format(startTimestamp);
|
||||
var backupFile = backupsFolder.resolve(timePart + '-' + backupName + ".zip");
|
||||
FileUtils.zip(baseDir, backupFile);
|
||||
backup.getStatus().setFilename(backupFile.getFileName().toString());
|
||||
backup.getStatus().setSize(Files.size(backupFile));
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> backupWorkDir(Path baseDir) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
try {
|
||||
var workdirPath = Files.createDirectory(baseDir.resolve("workdir"));
|
||||
FileUtils.copyRecursively(haloProperties.getWorkDir(), workdirPath, excludes);
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> backupExtensions(Path baseDir) {
|
||||
try {
|
||||
var extensionsPath = Files.createFile(baseDir.resolve("extensions.data"));
|
||||
return Mono.using(() -> objectMapper.writerFor(ExtensionStore.class)
|
||||
.writeValuesAsArray(extensionsPath.toFile()),
|
||||
seqWriter -> repository.findAll()
|
||||
.doOnNext(extensionStore -> {
|
||||
try {
|
||||
seqWriter.write(extensionStore);
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
})
|
||||
.then(),
|
||||
seqWriter -> {
|
||||
try {
|
||||
seqWriter.close();
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -64,8 +64,10 @@ management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: ["health", "info", "startup", "globalinfo", "logfile"]
|
||||
include: ["health", "info", "startup", "globalinfo", "logfile", "shutdown"]
|
||||
endpoint:
|
||||
shutdown:
|
||||
enabled: true
|
||||
health:
|
||||
probes:
|
||||
enabled: true
|
||||
|
@ -0,0 +1,18 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
metadata:
|
||||
name: role-template-manage-migration
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Migration Management"
|
||||
rbac.authorization.halo.run/display-name: "Migration Manage"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["system:migrations:manage"]
|
||||
rules:
|
||||
- apiGroups: ["api.console.migration.halo.run"]
|
||||
resources: ["restorations"]
|
||||
verbs: ["create"]
|
||||
- apiGroups: ["migration.halo.run"]
|
||||
resources: ["backups"]
|
||||
verbs: ["list", "get", "create", "update", "delete"]
|
@ -0,0 +1,255 @@
|
||||
package run.halo.app.migration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BackupReconcilerTest {
|
||||
|
||||
@Mock
|
||||
MigrationService migrationService;
|
||||
|
||||
@Mock
|
||||
ExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
BackupReconciler reconciler;
|
||||
|
||||
@Test
|
||||
void whenFreshBackupIsComing() {
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
backup.getSpec().setFormat("zip");
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
doNothing().when(client).update(backup);
|
||||
when(migrationService.backup(backup)).thenReturn(Mono.fromRunnable(() -> {
|
||||
var status = backup.getStatus();
|
||||
status.setFilename("fake-backup-filename");
|
||||
status.setSize(1024L);
|
||||
}));
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
var status = backup.getStatus();
|
||||
assertEquals(Backup.Phase.SUCCEEDED, status.getPhase());
|
||||
assertNotNull(status.getStartTimestamp());
|
||||
assertNotNull(status.getCompletionTimestamp());
|
||||
assertEquals("fake-backup-filename", status.getFilename());
|
||||
assertEquals(1024L, status.getSize());
|
||||
|
||||
// 1. query
|
||||
// 2. pending -> running
|
||||
// 3. running -> succeeded
|
||||
verify(client, times(3)).fetch(Backup.class, name);
|
||||
verify(client, times(3)).update(backup);
|
||||
verify(migrationService).backup(backup);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBackupDeleted() {
|
||||
var name = "fake-deleted-backup";
|
||||
var backup = createPureBackup(name);
|
||||
backup.getMetadata().setDeletionTimestamp(Instant.now());
|
||||
addFinalizers(backup.getMetadata(), Set.of(Constant.HOUSE_KEEPER_FINALIZER));
|
||||
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
when(migrationService.cleanup(backup)).thenReturn(Mono.empty());
|
||||
doNothing().when(client).update(backup);
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
assertFalse(backup.getMetadata().getFinalizers().contains(Constant.HOUSE_KEEPER_FINALIZER));
|
||||
verify(client).fetch(Backup.class, name);
|
||||
verify(migrationService).cleanup(backup);
|
||||
verify(client).update(backup);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPhaseToFailedIfPhaseIsRunning() {
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
var status = backup.getStatus();
|
||||
status.setPhase(Backup.Phase.RUNNING);
|
||||
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
doNothing().when(client).update(backup);
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
assertEquals(Backup.Phase.FAILED, status.getPhase());
|
||||
assertEquals("UnexpectedExit", status.getFailureReason());
|
||||
// 1. add finalizer
|
||||
// 2. update status
|
||||
verify(client, times(2)).fetch(Backup.class, name);
|
||||
verify(client, times(2)).update(backup);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReQueueIfExpiresAtSetAndNotExpired() {
|
||||
var now = Instant.now();
|
||||
reconciler.setClock(Clock.fixed(now, ZoneId.systemDefault()));
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
addFinalizers(backup.getMetadata(), Set.of(Constant.HOUSE_KEEPER_FINALIZER));
|
||||
backup.getSpec().setExpiresAt(now.plus(Duration.ofSeconds(3)));
|
||||
var status = backup.getStatus();
|
||||
status.setPhase(Backup.Phase.SUCCEEDED);
|
||||
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
assertNotNull(result);
|
||||
assertTrue(result.reEnqueue());
|
||||
assertEquals(Duration.ofSeconds(3), result.retryAfter());
|
||||
|
||||
verify(client).fetch(Backup.class, name);
|
||||
verify(client, never()).update(backup);
|
||||
verify(client, never()).delete(backup);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteIfExpiresAtSetAndExpired() {
|
||||
var now = Instant.now();
|
||||
reconciler.setClock(Clock.fixed(now, ZoneId.systemDefault()));
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
addFinalizers(backup.getMetadata(), Set.of(Constant.HOUSE_KEEPER_FINALIZER));
|
||||
backup.getSpec().setExpiresAt(now.minus(Duration.ofSeconds(3)));
|
||||
var status = backup.getStatus();
|
||||
status.setPhase(Backup.Phase.SUCCEEDED);
|
||||
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
doNothing().when(client).delete(backup);
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
verify(client).fetch(Backup.class, name);
|
||||
verify(client, never()).update(backup);
|
||||
verify(client).delete(backup);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBackupInterrupted() {
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
backup.getSpec().setFormat("zip");
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
doNothing().when(client).update(backup);
|
||||
Mono<Void> mono = mock(Mono.class);
|
||||
when(mono.block()).thenThrow(Exceptions.propagate(new InterruptedException()));
|
||||
when(migrationService.backup(backup)).thenReturn(mono);
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
var status = backup.getStatus();
|
||||
assertEquals(Backup.Phase.FAILED, status.getPhase());
|
||||
assertNotNull(status.getStartTimestamp());
|
||||
assertNull(status.getCompletionTimestamp());
|
||||
assertEquals("Interrupted", status.getFailureReason());
|
||||
|
||||
// 1. query
|
||||
// 2. pending -> running
|
||||
// 3. running -> failed
|
||||
verify(client, times(3)).fetch(Backup.class, name);
|
||||
verify(client, times(3)).update(backup);
|
||||
verify(migrationService).backup(backup);
|
||||
verify(mono).block();
|
||||
}
|
||||
|
||||
@Test
|
||||
void somethingWentWrongWhenBackup() {
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
backup.getSpec().setFormat("zip");
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
doNothing().when(client).update(backup);
|
||||
Mono<Void> mono = mock(Mono.class);
|
||||
when(mono.block()).thenThrow(Exceptions.propagate(new IOException("File not found")));
|
||||
when(migrationService.backup(backup)).thenReturn(mono);
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
var status = backup.getStatus();
|
||||
assertEquals(Backup.Phase.FAILED, status.getPhase());
|
||||
assertNotNull(status.getStartTimestamp());
|
||||
assertNull(status.getCompletionTimestamp());
|
||||
assertEquals("SystemError", status.getFailureReason());
|
||||
|
||||
// 1. query
|
||||
// 2. pending -> running
|
||||
// 3. running -> failed
|
||||
verify(client, times(3)).fetch(Backup.class, name);
|
||||
verify(client, times(3)).update(backup);
|
||||
verify(migrationService).backup(backup);
|
||||
verify(mono).block();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBackupWasFailed() {
|
||||
var name = "fake-backup";
|
||||
var backup = createPureBackup(name);
|
||||
backup.getStatus().setPhase(Backup.Phase.FAILED);
|
||||
|
||||
when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));
|
||||
|
||||
var result = reconciler.reconcile(new Reconciler.Request(name));
|
||||
assertNotNull(result);
|
||||
assertFalse(result.reEnqueue());
|
||||
Mockito.verify(migrationService, never()).backup(any(Backup.class));
|
||||
}
|
||||
|
||||
Backup createPureBackup(String name) {
|
||||
var metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
var backup = new Backup();
|
||||
backup.setMetadata(metadata);
|
||||
return backup;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
package run.halo.app.migration.impl;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.store.ExtensionStore;
|
||||
import run.halo.app.extension.store.ExtensionStoreRepository;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.migration.Backup;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MigrationServiceImplTest {
|
||||
|
||||
@Mock
|
||||
ExtensionStoreRepository repository;
|
||||
|
||||
@Mock
|
||||
HaloProperties haloProperties;
|
||||
|
||||
@InjectMocks
|
||||
MigrationServiceImpl migrationService;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void backupTest() throws IOException {
|
||||
var startTimestamp = Instant.now();
|
||||
var backup = createRunningBackup("fake-backup", startTimestamp);
|
||||
Files.writeString(tempDir.resolve("fake-file"), "halo", StandardOpenOption.CREATE_NEW);
|
||||
var extensionStores = List.of(
|
||||
createExtensionStore("fake-extension-store", "fake-data")
|
||||
);
|
||||
when(repository.findAll()).thenReturn(Flux.fromIterable(extensionStores));
|
||||
when(haloProperties.getWorkDir()).thenReturn(tempDir);
|
||||
StepVerifier.create(migrationService.backup(backup))
|
||||
.verifyComplete();
|
||||
|
||||
verify(repository).findAll();
|
||||
// 1. backup workdir
|
||||
// 2. package backup
|
||||
verify(haloProperties, times(2)).getWorkDir();
|
||||
|
||||
var status = backup.getStatus();
|
||||
var datetimePart = migrationService.getDateTimeFormatter().format(startTimestamp);
|
||||
assertEquals(datetimePart + "-fake-backup.zip", status.getFilename());
|
||||
var backupFile = migrationService.getBackupsRoot()
|
||||
.resolve(status.getFilename());
|
||||
assertTrue(Files.exists(backupFile));
|
||||
assertEquals(Files.size(backupFile), status.getSize());
|
||||
|
||||
var target = tempDir.resolve("target");
|
||||
try (var zis = new ZipInputStream(
|
||||
Files.newInputStream(backupFile, StandardOpenOption.READ))) {
|
||||
FileUtils.unzip(zis, tempDir.resolve("target"));
|
||||
}
|
||||
|
||||
var extensionsFile = target.resolve("extensions.data");
|
||||
var workdir = target.resolve("workdir");
|
||||
assertTrue(Files.exists(extensionsFile));
|
||||
assertTrue(Files.exists(workdir));
|
||||
|
||||
var objectMapper = migrationService.getObjectMapper();
|
||||
var gotExtensionStores = objectMapper.readValue(extensionsFile.toFile(),
|
||||
new TypeReference<List<ExtensionStore>>() {
|
||||
});
|
||||
assertEquals(gotExtensionStores, extensionStores);
|
||||
assertEquals("halo", Files.readString(workdir.resolve("fake-file")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreTest() throws IOException, URISyntaxException {
|
||||
var unpackedBackup =
|
||||
getClass().getClassLoader().getResource("backups/backup-for-restoration");
|
||||
assertNotNull(unpackedBackup);
|
||||
var backupFile = tempDir.resolve("backups").resolve("fake-backup.zip");
|
||||
Files.createDirectories(backupFile.getParent());
|
||||
FileUtils.zip(Path.of(unpackedBackup.toURI()), backupFile);
|
||||
var workdir = tempDir.resolve("workdir-for-restoration");
|
||||
Files.createDirectory(workdir);
|
||||
|
||||
|
||||
var expectStore = createExtensionStore("fake-extension-store", "fake-data");
|
||||
expectStore.setVersion(null);
|
||||
|
||||
when(haloProperties.getWorkDir()).thenReturn(workdir);
|
||||
when(repository.deleteAll(List.of(expectStore))).thenReturn(Mono.empty());
|
||||
when(repository.saveAll(List.of(expectStore))).thenReturn(Flux.empty());
|
||||
|
||||
var content = DataBufferUtils.read(backupFile,
|
||||
DefaultDataBufferFactory.sharedInstance,
|
||||
2048,
|
||||
StandardOpenOption.READ);
|
||||
StepVerifier.create(migrationService.restore(content))
|
||||
.verifyComplete();
|
||||
|
||||
|
||||
verify(haloProperties).getWorkDir();
|
||||
verify(repository).deleteAll(List.of(expectStore));
|
||||
verify(repository).saveAll(List.of(expectStore));
|
||||
|
||||
// make sure the workdir is recovered.
|
||||
var fakeFile = workdir.resolve("fake-file");
|
||||
assertEquals("halo", Files.readString(fakeFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupBackupTest() throws IOException {
|
||||
var backup = createSucceededBackup("fake-backup", "backup.zip");
|
||||
|
||||
var backupFile = tempDir.resolve("workdir").resolve("backups").resolve("backup.zip");
|
||||
Files.createDirectories(backupFile.getParent());
|
||||
Files.createFile(backupFile);
|
||||
|
||||
when(haloProperties.getWorkDir()).thenReturn(tempDir.resolve("workdir"));
|
||||
StepVerifier.create(migrationService.cleanup(backup))
|
||||
.verifyComplete();
|
||||
verify(haloProperties).getWorkDir();
|
||||
assertTrue(Files.notExists(backupFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadBackupTest() throws IOException {
|
||||
var backup = createSucceededBackup("fake-backup", "backup.zip");
|
||||
var backupFile = tempDir.resolve("workdir").resolve("backups").resolve("backup.zip");
|
||||
Files.createDirectories(backupFile.getParent());
|
||||
Files.writeString(backupFile, "this is a backup file.", StandardOpenOption.CREATE_NEW);
|
||||
when(haloProperties.getWorkDir()).thenReturn(tempDir.resolve("workdir"));
|
||||
|
||||
StepVerifier.create(migrationService.download(backup))
|
||||
.assertNext(resource -> {
|
||||
assertEquals("backup.zip", resource.getFilename());
|
||||
try {
|
||||
var content = resource.getContentAsString(UTF_8);
|
||||
assertEquals("this is a backup file.", content);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
}
|
||||
|
||||
Backup createSucceededBackup(String name, String filename) {
|
||||
var metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
var backup = new Backup();
|
||||
backup.setMetadata(metadata);
|
||||
var status = backup.getStatus();
|
||||
status.setPhase(Backup.Phase.SUCCEEDED);
|
||||
status.setCompletionTimestamp(Instant.now());
|
||||
status.setFilename(filename);
|
||||
status.setSize(1024L);
|
||||
return backup;
|
||||
}
|
||||
|
||||
Backup createRunningBackup(String name, Instant startTimestamp) {
|
||||
var metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
var backup = new Backup();
|
||||
backup.setMetadata(metadata);
|
||||
var status = backup.getStatus();
|
||||
status.setPhase(Backup.Phase.RUNNING);
|
||||
status.setStartTimestamp(startTimestamp);
|
||||
return backup;
|
||||
}
|
||||
|
||||
ExtensionStore createExtensionStore(String name, String data) {
|
||||
var store = new ExtensionStore();
|
||||
store.setName(name);
|
||||
store.setData(data.getBytes(UTF_8));
|
||||
store.setVersion(1024L);
|
||||
return store;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
[{"name":"fake-extension-store","data":"ZmFrZS1kYXRh","version":1024}]
|
@ -0,0 +1 @@
|
||||
halo
|
43
console/docs/extension-points/backup.md
Normal file
43
console/docs/extension-points/backup.md
Normal file
@ -0,0 +1,43 @@
|
||||
# 备份页面选项卡扩展点
|
||||
|
||||
## 原由
|
||||
|
||||
在 Halo 2.8 中提供了基础备份和恢复的功能,此扩展点是为了提供给插件开发者针对备份扩展更多功能,比如定时备份设置、备份到第三方云存储等。
|
||||
|
||||
## 定义方式
|
||||
|
||||
```ts
|
||||
import { definePlugin } from "@halo-dev/console-shared";
|
||||
import BackupStorage from "@/views/BackupStorage.vue";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
components: {},
|
||||
routes: [],
|
||||
extensionPoints: {
|
||||
"backup:tabs:create": () => {
|
||||
return [
|
||||
{
|
||||
id: "storage",
|
||||
label: "备份位置",
|
||||
component: markRaw(BackupStorage),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
BackupTab 类型:
|
||||
|
||||
```ts
|
||||
import type { Component, Raw } from "vue";
|
||||
|
||||
export interface BackupTab {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Raw<Component>;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
```
|
@ -12,6 +12,7 @@ api/api-console-halo-run-v1alpha1-single-page-api.ts
|
||||
api/api-console-halo-run-v1alpha1-stats-api.ts
|
||||
api/api-console-halo-run-v1alpha1-theme-api.ts
|
||||
api/api-console-halo-run-v1alpha1-user-api.ts
|
||||
api/api-console-migration-halo-run-v1alpha1-migration-api.ts
|
||||
api/api-content-halo-run-v1alpha1-category-api.ts
|
||||
api/api-content-halo-run-v1alpha1-post-api.ts
|
||||
api/api-content-halo-run-v1alpha1-single-page-api.ts
|
||||
@ -34,6 +35,7 @@ api/content-halo-run-v1alpha1-snapshot-api.ts
|
||||
api/content-halo-run-v1alpha1-tag-api.ts
|
||||
api/login-api.ts
|
||||
api/metrics-halo-run-v1alpha1-counter-api.ts
|
||||
api/migration-halo-run-v1alpha1-backup-api.ts
|
||||
api/plugin-halo-run-v1alpha1-extension-definition-api.ts
|
||||
api/plugin-halo-run-v1alpha1-extension-point-definition-api.ts
|
||||
api/plugin-halo-run-v1alpha1-plugin-api.ts
|
||||
@ -71,6 +73,10 @@ models/auth-provider-list.ts
|
||||
models/auth-provider-spec.ts
|
||||
models/auth-provider.ts
|
||||
models/author.ts
|
||||
models/backup-list.ts
|
||||
models/backup-spec.ts
|
||||
models/backup-status.ts
|
||||
models/backup.ts
|
||||
models/category-list.ts
|
||||
models/category-spec.ts
|
||||
models/category-status.ts
|
||||
|
@ -23,6 +23,7 @@ export * from "./api/api-console-halo-run-v1alpha1-single-page-api";
|
||||
export * from "./api/api-console-halo-run-v1alpha1-stats-api";
|
||||
export * from "./api/api-console-halo-run-v1alpha1-theme-api";
|
||||
export * from "./api/api-console-halo-run-v1alpha1-user-api";
|
||||
export * from "./api/api-console-migration-halo-run-v1alpha1-migration-api";
|
||||
export * from "./api/api-content-halo-run-v1alpha1-category-api";
|
||||
export * from "./api/api-content-halo-run-v1alpha1-post-api";
|
||||
export * from "./api/api-content-halo-run-v1alpha1-single-page-api";
|
||||
@ -45,6 +46,7 @@ export * from "./api/content-halo-run-v1alpha1-snapshot-api";
|
||||
export * from "./api/content-halo-run-v1alpha1-tag-api";
|
||||
export * from "./api/login-api";
|
||||
export * from "./api/metrics-halo-run-v1alpha1-counter-api";
|
||||
export * from "./api/migration-halo-run-v1alpha1-backup-api";
|
||||
export * from "./api/plugin-halo-run-v1alpha1-extension-definition-api";
|
||||
export * from "./api/plugin-halo-run-v1alpha1-extension-point-definition-api";
|
||||
export * from "./api/plugin-halo-run-v1alpha1-plugin-api";
|
||||
|
@ -0,0 +1,210 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1RestorationApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleHaloRunV1alpha1RestorationApiAxiosParamCreator =
|
||||
function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
restoreBackup: async (
|
||||
file: File,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'file' is not null or undefined
|
||||
assertParamExists("restoreBackup", "file", file);
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/restorations`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
const localVarFormParams = new ((configuration &&
|
||||
configuration.formDataCtor) ||
|
||||
FormData)();
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (file !== undefined) {
|
||||
localVarFormParams.append("file", file as any);
|
||||
}
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "multipart/form-data";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = localVarFormParams;
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1RestorationApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleHaloRunV1alpha1RestorationApiFp = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const localVarAxiosParamCreator =
|
||||
ApiConsoleHaloRunV1alpha1RestorationApiAxiosParamCreator(configuration);
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async restoreBackup(
|
||||
file: File,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreBackup(
|
||||
file,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1RestorationApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleHaloRunV1alpha1RestorationApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = ApiConsoleHaloRunV1alpha1RestorationApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
restoreBackup(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<void> {
|
||||
return localVarFp
|
||||
.restoreBackup(requestParameters.file, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for restoreBackup operation in ApiConsoleHaloRunV1alpha1RestorationApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackupRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackupRequest {
|
||||
/**
|
||||
*
|
||||
* @type {File}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackup
|
||||
*/
|
||||
readonly file: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1RestorationApi - object-oriented interface
|
||||
* @export
|
||||
* @class ApiConsoleHaloRunV1alpha1RestorationApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiConsoleHaloRunV1alpha1RestorationApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1RestorationApi
|
||||
*/
|
||||
public restoreBackup(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1RestorationApiRestoreBackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleHaloRunV1alpha1RestorationApiFp(this.configuration)
|
||||
.restoreBackup(requestParameters.file, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
@ -0,0 +1,355 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
/**
|
||||
* ApiConsoleMigrationHaloRunV1alpha1MigrationApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleMigrationHaloRunV1alpha1MigrationApiAxiosParamCreator =
|
||||
function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} name Backup name.
|
||||
* @param {string} filename Backup filename.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
downloadBackups: async (
|
||||
name: string,
|
||||
filename: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("downloadBackups", "name", name);
|
||||
// verify required parameter 'filename' is not null or undefined
|
||||
assertParamExists("downloadBackups", "filename", filename);
|
||||
const localVarPath =
|
||||
`/apis/api.console.migration.halo.run/v1alpha1/backups/{name}/files/{filename}`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)))
|
||||
.replace(`{${"filename"}}`, encodeURIComponent(String(filename)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "GET",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
restoreBackup: async (
|
||||
file: File,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'file' is not null or undefined
|
||||
assertParamExists("restoreBackup", "file", file);
|
||||
const localVarPath = `/apis/api.console.migration.halo.run/v1alpha1/restorations`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
const localVarFormParams = new ((configuration &&
|
||||
configuration.formDataCtor) ||
|
||||
FormData)();
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (file !== undefined) {
|
||||
localVarFormParams.append("file", file as any);
|
||||
}
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "multipart/form-data";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = localVarFormParams;
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiConsoleMigrationHaloRunV1alpha1MigrationApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleMigrationHaloRunV1alpha1MigrationApiFp = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const localVarAxiosParamCreator =
|
||||
ApiConsoleMigrationHaloRunV1alpha1MigrationApiAxiosParamCreator(
|
||||
configuration
|
||||
);
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} name Backup name.
|
||||
* @param {string} filename Backup filename.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async downloadBackups(
|
||||
name: string,
|
||||
filename: string,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadBackups(
|
||||
name,
|
||||
filename,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async restoreBackup(
|
||||
file: File,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.restoreBackup(
|
||||
file,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiConsoleMigrationHaloRunV1alpha1MigrationApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleMigrationHaloRunV1alpha1MigrationApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp =
|
||||
ApiConsoleMigrationHaloRunV1alpha1MigrationApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackupsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
downloadBackups(
|
||||
requestParameters: ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackupsRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<void> {
|
||||
return localVarFp
|
||||
.downloadBackups(
|
||||
requestParameters.name,
|
||||
requestParameters.filename,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
restoreBackup(
|
||||
requestParameters: ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<void> {
|
||||
return localVarFp
|
||||
.restoreBackup(requestParameters.file, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for downloadBackups operation in ApiConsoleMigrationHaloRunV1alpha1MigrationApi.
|
||||
* @export
|
||||
* @interface ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackupsRequest
|
||||
*/
|
||||
export interface ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackupsRequest {
|
||||
/**
|
||||
* Backup name.
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackups
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Backup filename.
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackups
|
||||
*/
|
||||
readonly filename: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for restoreBackup operation in ApiConsoleMigrationHaloRunV1alpha1MigrationApi.
|
||||
* @export
|
||||
* @interface ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackupRequest
|
||||
*/
|
||||
export interface ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackupRequest {
|
||||
/**
|
||||
*
|
||||
* @type {File}
|
||||
* @memberof ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackup
|
||||
*/
|
||||
readonly file: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiConsoleMigrationHaloRunV1alpha1MigrationApi - object-oriented interface
|
||||
* @export
|
||||
* @class ApiConsoleMigrationHaloRunV1alpha1MigrationApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiConsoleMigrationHaloRunV1alpha1MigrationApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackupsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleMigrationHaloRunV1alpha1MigrationApi
|
||||
*/
|
||||
public downloadBackups(
|
||||
requestParameters: ApiConsoleMigrationHaloRunV1alpha1MigrationApiDownloadBackupsRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleMigrationHaloRunV1alpha1MigrationApiFp(this.configuration)
|
||||
.downloadBackups(
|
||||
requestParameters.name,
|
||||
requestParameters.filename,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleMigrationHaloRunV1alpha1MigrationApi
|
||||
*/
|
||||
public restoreBackup(
|
||||
requestParameters: ApiConsoleMigrationHaloRunV1alpha1MigrationApiRestoreBackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleMigrationHaloRunV1alpha1MigrationApiFp(this.configuration)
|
||||
.restoreBackup(requestParameters.file, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
@ -0,0 +1,802 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
// @ts-ignore
|
||||
import { Backup } from "../models";
|
||||
// @ts-ignore
|
||||
import { BackupList } from "../models";
|
||||
/**
|
||||
* MigrationHaloRunV1alpha1BackupApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const MigrationHaloRunV1alpha1BackupApiAxiosParamCreator = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
return {
|
||||
/**
|
||||
* Create migration.halo.run/v1alpha1/Backup
|
||||
* @param {Backup} [backup] Fresh backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createmigrationHaloRunV1alpha1Backup: async (
|
||||
backup?: Backup,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/migration.halo.run/v1alpha1/backups`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||
backup,
|
||||
localVarRequestOptions,
|
||||
configuration
|
||||
);
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Delete migration.halo.run/v1alpha1/Backup
|
||||
* @param {string} name Name of backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deletemigrationHaloRunV1alpha1Backup: async (
|
||||
name: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("deletemigrationHaloRunV1alpha1Backup", "name", name);
|
||||
const localVarPath =
|
||||
`/apis/migration.halo.run/v1alpha1/backups/{name}`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "DELETE",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Get migration.halo.run/v1alpha1/Backup
|
||||
* @param {string} name Name of backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getmigrationHaloRunV1alpha1Backup: async (
|
||||
name: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("getmigrationHaloRunV1alpha1Backup", "name", name);
|
||||
const localVarPath =
|
||||
`/apis/migration.halo.run/v1alpha1/backups/{name}`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "GET",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* List migration.halo.run/v1alpha1/Backup
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Support sorting based on attribute name path.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listmigrationHaloRunV1alpha1Backup: async (
|
||||
fieldSelector?: Array<string>,
|
||||
labelSelector?: Array<string>,
|
||||
page?: number,
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/migration.halo.run/v1alpha1/backups`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "GET",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter["fieldSelector"] = fieldSelector;
|
||||
}
|
||||
|
||||
if (labelSelector) {
|
||||
localVarQueryParameter["labelSelector"] = labelSelector;
|
||||
}
|
||||
|
||||
if (page !== undefined) {
|
||||
localVarQueryParameter["page"] = page;
|
||||
}
|
||||
|
||||
if (size !== undefined) {
|
||||
localVarQueryParameter["size"] = size;
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
localVarQueryParameter["sort"] = Array.from(sort);
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Update migration.halo.run/v1alpha1/Backup
|
||||
* @param {string} name Name of backup
|
||||
* @param {Backup} [backup] Updated backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updatemigrationHaloRunV1alpha1Backup: async (
|
||||
name: string,
|
||||
backup?: Backup,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("updatemigrationHaloRunV1alpha1Backup", "name", name);
|
||||
const localVarPath =
|
||||
`/apis/migration.halo.run/v1alpha1/backups/{name}`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "PUT",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||
backup,
|
||||
localVarRequestOptions,
|
||||
configuration
|
||||
);
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MigrationHaloRunV1alpha1BackupApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const MigrationHaloRunV1alpha1BackupApiFp = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const localVarAxiosParamCreator =
|
||||
MigrationHaloRunV1alpha1BackupApiAxiosParamCreator(configuration);
|
||||
return {
|
||||
/**
|
||||
* Create migration.halo.run/v1alpha1/Backup
|
||||
* @param {Backup} [backup] Fresh backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createmigrationHaloRunV1alpha1Backup(
|
||||
backup?: Backup,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.createmigrationHaloRunV1alpha1Backup(
|
||||
backup,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Delete migration.halo.run/v1alpha1/Backup
|
||||
* @param {string} name Name of backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deletemigrationHaloRunV1alpha1Backup(
|
||||
name: string,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.deletemigrationHaloRunV1alpha1Backup(
|
||||
name,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Get migration.halo.run/v1alpha1/Backup
|
||||
* @param {string} name Name of backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getmigrationHaloRunV1alpha1Backup(
|
||||
name: string,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.getmigrationHaloRunV1alpha1Backup(
|
||||
name,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* List migration.halo.run/v1alpha1/Backup
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Support sorting based on attribute name path.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listmigrationHaloRunV1alpha1Backup(
|
||||
fieldSelector?: Array<string>,
|
||||
labelSelector?: Array<string>,
|
||||
page?: number,
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<BackupList>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.listmigrationHaloRunV1alpha1Backup(
|
||||
fieldSelector,
|
||||
labelSelector,
|
||||
page,
|
||||
size,
|
||||
sort,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Update migration.halo.run/v1alpha1/Backup
|
||||
* @param {string} name Name of backup
|
||||
* @param {Backup} [backup] Updated backup
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async updatemigrationHaloRunV1alpha1Backup(
|
||||
name: string,
|
||||
backup?: Backup,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.updatemigrationHaloRunV1alpha1Backup(
|
||||
name,
|
||||
backup,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MigrationHaloRunV1alpha1BackupApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const MigrationHaloRunV1alpha1BackupApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = MigrationHaloRunV1alpha1BackupApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
* Create migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1BackupRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Backup> {
|
||||
return localVarFp
|
||||
.createmigrationHaloRunV1alpha1Backup(requestParameters.backup, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Delete migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deletemigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1BackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<void> {
|
||||
return localVarFp
|
||||
.deletemigrationHaloRunV1alpha1Backup(requestParameters.name, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Get migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1BackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Backup> {
|
||||
return localVarFp
|
||||
.getmigrationHaloRunV1alpha1Backup(requestParameters.name, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1BackupRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<BackupList> {
|
||||
return localVarFp
|
||||
.listmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.page,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Update migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updatemigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1BackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Backup> {
|
||||
return localVarFp
|
||||
.updatemigrationHaloRunV1alpha1Backup(
|
||||
requestParameters.name,
|
||||
requestParameters.backup,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createmigrationHaloRunV1alpha1Backup operation in MigrationHaloRunV1alpha1BackupApi.
|
||||
* @export
|
||||
* @interface MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1BackupRequest
|
||||
*/
|
||||
export interface MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1BackupRequest {
|
||||
/**
|
||||
* Fresh backup
|
||||
* @type {Backup}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly backup?: Backup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for deletemigrationHaloRunV1alpha1Backup operation in MigrationHaloRunV1alpha1BackupApi.
|
||||
* @export
|
||||
* @interface MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1BackupRequest
|
||||
*/
|
||||
export interface MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1BackupRequest {
|
||||
/**
|
||||
* Name of backup
|
||||
* @type {string}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getmigrationHaloRunV1alpha1Backup operation in MigrationHaloRunV1alpha1BackupApi.
|
||||
* @export
|
||||
* @interface MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1BackupRequest
|
||||
*/
|
||||
export interface MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1BackupRequest {
|
||||
/**
|
||||
* Name of backup
|
||||
* @type {string}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listmigrationHaloRunV1alpha1Backup operation in MigrationHaloRunV1alpha1BackupApi.
|
||||
* @export
|
||||
* @interface MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1BackupRequest
|
||||
*/
|
||||
export interface MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1BackupRequest {
|
||||
/**
|
||||
* Field selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly fieldSelector?: Array<string>;
|
||||
|
||||
/**
|
||||
* Label selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly labelSelector?: Array<string>;
|
||||
|
||||
/**
|
||||
* The page number. Zero indicates no page.
|
||||
* @type {number}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly page?: number;
|
||||
|
||||
/**
|
||||
* Size of one page. Zero indicates no limit.
|
||||
* @type {number}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly size?: number;
|
||||
|
||||
/**
|
||||
* Sort property and direction of the list result. Support sorting based on attribute name path.
|
||||
* @type {Array<string>}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly sort?: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatemigrationHaloRunV1alpha1Backup operation in MigrationHaloRunV1alpha1BackupApi.
|
||||
* @export
|
||||
* @interface MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1BackupRequest
|
||||
*/
|
||||
export interface MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1BackupRequest {
|
||||
/**
|
||||
* Name of backup
|
||||
* @type {string}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Updated backup
|
||||
* @type {Backup}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1Backup
|
||||
*/
|
||||
readonly backup?: Backup;
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrationHaloRunV1alpha1BackupApi - object-oriented interface
|
||||
* @export
|
||||
* @class MigrationHaloRunV1alpha1BackupApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class MigrationHaloRunV1alpha1BackupApi extends BaseAPI {
|
||||
/**
|
||||
* Create migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApi
|
||||
*/
|
||||
public createmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiCreatemigrationHaloRunV1alpha1BackupRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return MigrationHaloRunV1alpha1BackupApiFp(this.configuration)
|
||||
.createmigrationHaloRunV1alpha1Backup(requestParameters.backup, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApi
|
||||
*/
|
||||
public deletemigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiDeletemigrationHaloRunV1alpha1BackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return MigrationHaloRunV1alpha1BackupApiFp(this.configuration)
|
||||
.deletemigrationHaloRunV1alpha1Backup(requestParameters.name, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApi
|
||||
*/
|
||||
public getmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiGetmigrationHaloRunV1alpha1BackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return MigrationHaloRunV1alpha1BackupApiFp(this.configuration)
|
||||
.getmigrationHaloRunV1alpha1Backup(requestParameters.name, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApi
|
||||
*/
|
||||
public listmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiListmigrationHaloRunV1alpha1BackupRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return MigrationHaloRunV1alpha1BackupApiFp(this.configuration)
|
||||
.listmigrationHaloRunV1alpha1Backup(
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.page,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update migration.halo.run/v1alpha1/Backup
|
||||
* @param {MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1BackupRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MigrationHaloRunV1alpha1BackupApi
|
||||
*/
|
||||
public updatemigrationHaloRunV1alpha1Backup(
|
||||
requestParameters: MigrationHaloRunV1alpha1BackupApiUpdatemigrationHaloRunV1alpha1BackupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return MigrationHaloRunV1alpha1BackupApiFp(this.configuration)
|
||||
.updatemigrationHaloRunV1alpha1Backup(
|
||||
requestParameters.name,
|
||||
requestParameters.backup,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
79
console/packages/api-client/src/models/backup-list.ts
Normal file
79
console/packages/api-client/src/models/backup-list.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { Backup } from "./backup";
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface BackupList
|
||||
*/
|
||||
export interface BackupList {
|
||||
/**
|
||||
* Indicates whether current page is the first page.
|
||||
* @type {boolean}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
first: boolean;
|
||||
/**
|
||||
* Indicates whether current page has previous page.
|
||||
* @type {boolean}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
hasNext: boolean;
|
||||
/**
|
||||
* Indicates whether current page has previous page.
|
||||
* @type {boolean}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
hasPrevious: boolean;
|
||||
/**
|
||||
* A chunk of items.
|
||||
* @type {Array<Backup>}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
items: Array<Backup>;
|
||||
/**
|
||||
* Indicates whether current page is the last page.
|
||||
* @type {boolean}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
last: boolean;
|
||||
/**
|
||||
* Page number, starts from 1. If not set or equal to 0, it means no pagination.
|
||||
* @type {number}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
page: number;
|
||||
/**
|
||||
* Size of each page. If not set or equal to 0, it means no pagination.
|
||||
* @type {number}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* Total elements.
|
||||
* @type {number}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* Indicates total pages.
|
||||
* @type {number}
|
||||
* @memberof BackupList
|
||||
*/
|
||||
totalPages: number;
|
||||
}
|
33
console/packages/api-client/src/models/backup-spec.ts
Normal file
33
console/packages/api-client/src/models/backup-spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface BackupSpec
|
||||
*/
|
||||
export interface BackupSpec {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupSpec
|
||||
*/
|
||||
expiresAt?: string;
|
||||
/**
|
||||
* Backup file format. Currently, only zip format is supported.
|
||||
* @type {string}
|
||||
* @memberof BackupSpec
|
||||
*/
|
||||
format?: string;
|
||||
}
|
73
console/packages/api-client/src/models/backup-status.ts
Normal file
73
console/packages/api-client/src/models/backup-status.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface BackupStatus
|
||||
*/
|
||||
export interface BackupStatus {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
completionTimestamp?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
failureMessage?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
failureReason?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
phase?: BackupStatusPhaseEnum;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BackupStatus
|
||||
*/
|
||||
startTimestamp?: string;
|
||||
}
|
||||
|
||||
export const BackupStatusPhaseEnum = {
|
||||
Pending: "PENDING",
|
||||
Running: "RUNNING",
|
||||
Succeeded: "SUCCEEDED",
|
||||
Failed: "FAILED",
|
||||
} as const;
|
||||
|
||||
export type BackupStatusPhaseEnum =
|
||||
(typeof BackupStatusPhaseEnum)[keyof typeof BackupStatusPhaseEnum];
|
61
console/packages/api-client/src/models/backup.ts
Normal file
61
console/packages/api-client/src/models/backup.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { BackupSpec } from "./backup-spec";
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { BackupStatus } from "./backup-status";
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { Metadata } from "./metadata";
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Backup
|
||||
*/
|
||||
export interface Backup {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Backup
|
||||
*/
|
||||
apiVersion: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Backup
|
||||
*/
|
||||
kind: string;
|
||||
/**
|
||||
*
|
||||
* @type {Metadata}
|
||||
* @memberof Backup
|
||||
*/
|
||||
metadata: Metadata;
|
||||
/**
|
||||
*
|
||||
* @type {BackupSpec}
|
||||
* @memberof Backup
|
||||
*/
|
||||
spec?: BackupSpec;
|
||||
/**
|
||||
*
|
||||
* @type {BackupStatus}
|
||||
* @memberof Backup
|
||||
*/
|
||||
status?: BackupStatus;
|
||||
}
|
@ -9,6 +9,10 @@ export * from "./auth-provider";
|
||||
export * from "./auth-provider-list";
|
||||
export * from "./auth-provider-spec";
|
||||
export * from "./author";
|
||||
export * from "./backup";
|
||||
export * from "./backup-list";
|
||||
export * from "./backup-spec";
|
||||
export * from "./backup-status";
|
||||
export * from "./category";
|
||||
export * from "./category-list";
|
||||
export * from "./category-spec";
|
||||
|
33
console/packages/api-client/src/models/spec.ts
Normal file
33
console/packages/api-client/src/models/spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Spec
|
||||
*/
|
||||
export interface Spec {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Spec
|
||||
*/
|
||||
autoDeleteWhen?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Spec
|
||||
*/
|
||||
format?: string;
|
||||
}
|
73
console/packages/api-client/src/models/status.ts
Normal file
73
console/packages/api-client/src/models/status.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Status
|
||||
*/
|
||||
export interface Status {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Status
|
||||
*/
|
||||
completionTimestamp?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Status
|
||||
*/
|
||||
failureMessage?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Status
|
||||
*/
|
||||
failureReason?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Status
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Status
|
||||
*/
|
||||
phase?: StatusPhaseEnum;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Status
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Status
|
||||
*/
|
||||
startTimestamp?: string;
|
||||
}
|
||||
|
||||
export const StatusPhaseEnum = {
|
||||
Pending: "PENDING",
|
||||
Running: "RUNNING",
|
||||
Succeeded: "SUCCEEDED",
|
||||
Failed: "FAILED",
|
||||
} as const;
|
||||
|
||||
export type StatusPhaseEnum =
|
||||
(typeof StatusPhaseEnum)[keyof typeof StatusPhaseEnum];
|
@ -183,7 +183,7 @@ watch(
|
||||
justify-center
|
||||
top-0
|
||||
py-10;
|
||||
z-index: 999;
|
||||
z-index: 2000;
|
||||
|
||||
.modal-layer {
|
||||
@apply flex-none
|
||||
|
@ -59,6 +59,7 @@ import IconArrowDownCircleLine from "~icons/ri/arrow-down-circle-line";
|
||||
import IconTerminalBoxLine from "~icons/ri/terminal-box-line";
|
||||
import IconClipboardLine from "~icons/ri/clipboard-line";
|
||||
import IconLockPasswordLine from "~icons/ri/lock-password-line";
|
||||
import IconServerLine from "~icons/ri/server-line";
|
||||
import IconRiPencilFill from "~icons/ri/pencil-fill";
|
||||
import IconZoomInLine from "~icons/ri/zoom-in-line";
|
||||
import IconZoomOutLine from "~icons/ri/zoom-out-line";
|
||||
@ -128,6 +129,7 @@ export {
|
||||
IconTerminalBoxLine,
|
||||
IconClipboardLine,
|
||||
IconLockPasswordLine,
|
||||
IconServerLine,
|
||||
IconRiPencilFill,
|
||||
IconZoomInLine,
|
||||
IconZoomOutLine,
|
||||
|
@ -6,3 +6,4 @@ export * from "./states/attachment-selector";
|
||||
export * from "./states/editor";
|
||||
export * from "./states/plugin-tab";
|
||||
export * from "./states/comment-subject-ref";
|
||||
export * from "./states/backup";
|
||||
|
8
console/packages/shared/src/states/backup.ts
Normal file
8
console/packages/shared/src/states/backup.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { Component, Raw } from "vue";
|
||||
|
||||
export interface BackupTab {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Raw<Component>;
|
||||
permissions?: string[];
|
||||
}
|
@ -5,6 +5,7 @@ import type { AttachmentSelectProvider } from "../states/attachment-selector";
|
||||
import type { EditorProvider, PluginTab } from "..";
|
||||
import type { AnyExtension } from "@tiptap/vue-3";
|
||||
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
|
||||
import type { BackupTab } from "@/states/backup";
|
||||
|
||||
export interface RouteRecordAppend {
|
||||
parentName: RouteRecordName;
|
||||
@ -23,11 +24,13 @@ export interface ExtensionPoint {
|
||||
|
||||
"plugin:self:tabs:create"?: () => PluginTab[] | Promise<PluginTab[]>;
|
||||
|
||||
"default:editor:extension:create": () =>
|
||||
"default:editor:extension:create"?: () =>
|
||||
| AnyExtension[]
|
||||
| Promise<AnyExtension[]>;
|
||||
|
||||
"comment:subject-ref:create"?: () => CommentSubjectRefProvider[];
|
||||
|
||||
"backup:tabs:create"?: () => BackupTab[] | Promise<BackupTab[]>;
|
||||
}
|
||||
|
||||
export interface PluginModule {
|
||||
|
@ -24,6 +24,8 @@ const props = withDefaults(
|
||||
note?: string;
|
||||
method?: "GET" | "POST" | "PUT" | "HEAD" | "get" | "post" | "put" | "head";
|
||||
disabled?: boolean;
|
||||
width?: string;
|
||||
height?: string;
|
||||
doneButtonHandler?: () => void;
|
||||
}>(),
|
||||
{
|
||||
@ -35,6 +37,8 @@ const props = withDefaults(
|
||||
note: undefined,
|
||||
method: "post",
|
||||
disabled: false,
|
||||
width: "750px",
|
||||
height: "550px",
|
||||
doneButtonHandler: undefined,
|
||||
}
|
||||
);
|
||||
@ -97,11 +101,14 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<dashboard
|
||||
class="w-full"
|
||||
:uppy="uppy"
|
||||
:props="{
|
||||
theme: 'light',
|
||||
disabled: disabled,
|
||||
note: note,
|
||||
width,
|
||||
height,
|
||||
doneButtonHandler: doneButtonHandler,
|
||||
}"
|
||||
/>
|
||||
|
@ -69,6 +69,7 @@ core:
|
||||
users: Users
|
||||
settings: Settings
|
||||
actuator: Actuator
|
||||
backup: Backup
|
||||
operations:
|
||||
logout:
|
||||
button: Logout
|
||||
@ -975,6 +976,43 @@ core:
|
||||
os: "Operating system: {os}"
|
||||
alert:
|
||||
external_url_invalid: The external access url detected is inconsistent with the current access url, which may cause some links to fail to redirect properly. Please check the external access url settings.
|
||||
backup:
|
||||
title: Backup and Restore
|
||||
tabs:
|
||||
backup_list: Backups
|
||||
restore: Restore
|
||||
empty:
|
||||
title: No backups have been created yet
|
||||
message: You can click the button below to create a backup
|
||||
actions:
|
||||
create: Create backup
|
||||
operations:
|
||||
create:
|
||||
button: Create backup
|
||||
title: Create backup
|
||||
description: Are you sure you want to create a backup? This operation may last for a long time.
|
||||
toast_success: Requested to create a backup
|
||||
delete:
|
||||
title: Delete the backup
|
||||
description: Are you sure you want to delete the backup?
|
||||
restore:
|
||||
title: Restore successfully
|
||||
description: After successful restore, you need to restart Halo to load the system resources normally. After clicking OK, we will automatically stop running Halo.
|
||||
shutdown:
|
||||
toast_success: Requested to shutdown operation
|
||||
list:
|
||||
phases:
|
||||
pending: Pending
|
||||
running: Running
|
||||
succeeded: Succeeded
|
||||
failed: Failed
|
||||
fields:
|
||||
expiresAt: Expires {expiresAt}
|
||||
restore:
|
||||
tips:
|
||||
first: 1. The restore process may last for a long time, please do not refresh the page during this period.
|
||||
second: 2. During the restore process, although the existing data will not be cleaned up, if there is a conflict, the data will be overwritten.
|
||||
third: 3. After the restore is completed, you will be prompted to stop running Halo, and you may need to run it manually after stopping.
|
||||
exception:
|
||||
not_found:
|
||||
message: Page not found
|
||||
@ -1047,6 +1085,8 @@ core:
|
||||
Users Management: Users
|
||||
User manage: User Manage
|
||||
User View: User View
|
||||
Migration Management: Backup and Restore
|
||||
Migration Manage: Backup and Restore Manage
|
||||
role-template-view-users: User View
|
||||
role-template-change-password: Change Password
|
||||
components:
|
||||
|
@ -69,6 +69,7 @@ core:
|
||||
users: 用户
|
||||
settings: 设置
|
||||
actuator: 概览
|
||||
backup: 备份
|
||||
operations:
|
||||
logout:
|
||||
button: 退出登录
|
||||
@ -975,6 +976,43 @@ core:
|
||||
os: 操作系统:{os}
|
||||
alert:
|
||||
external_url_invalid: 检测到外部访问地址与当前访问地址不一致,可能会导致部分链接无法正常跳转,请检查外部访问地址设置。
|
||||
backup:
|
||||
title: 备份与恢复
|
||||
tabs:
|
||||
backup_list: 备份
|
||||
restore: 恢复
|
||||
empty:
|
||||
title: 没有备份
|
||||
message: 当前没有已创建的备份,你可以点击刷新或者创建新的备份
|
||||
actions:
|
||||
create: 创建备份
|
||||
operations:
|
||||
create:
|
||||
button: 创建备份
|
||||
title: 创建备份
|
||||
description: 确定要创建备份吗,此操作可能会持续较长时间。
|
||||
toast_success: 已请求创建备份
|
||||
delete:
|
||||
title: 删除备份
|
||||
description: 确定要删除该备份吗?
|
||||
restore:
|
||||
title: 恢复成功
|
||||
description: 恢复成功之后,需要重启一下 Halo 才能够正常加载系统资源,点击确定之后我们会自动停止运行 Halo
|
||||
shutdown:
|
||||
toast_success: 已请求停止运行
|
||||
list:
|
||||
phases:
|
||||
pending: 准备中
|
||||
running: 备份中
|
||||
succeeded: 备份完成
|
||||
failed: 备份失败
|
||||
fields:
|
||||
expiresAt: "{expiresAt}失效"
|
||||
restore:
|
||||
tips:
|
||||
first: 1. 恢复过程可能会持续较长时间,期间请勿刷新页面。
|
||||
second: 2. 在恢复的过程中,虽然已有的数据不会被清理掉,但如果有冲突的数据将被覆盖。
|
||||
third: 3. 恢复完成之后会提示停止运行 Halo,停止之后可能需要手动运行。
|
||||
exception:
|
||||
not_found:
|
||||
message: 没有找到该页面
|
||||
@ -1047,6 +1085,8 @@ core:
|
||||
Users Management: 用户
|
||||
User manage: 用户管理
|
||||
User View: 用户查看
|
||||
Migration Management: 备份与恢复
|
||||
Migration Manage: 备份与恢复管理
|
||||
role-template-view-users: 用户查看
|
||||
role-template-change-password: 修改密码
|
||||
components:
|
||||
|
@ -69,6 +69,7 @@ core:
|
||||
users: 用戶
|
||||
settings: 設置
|
||||
actuator: 概覽
|
||||
backup: 備份
|
||||
operations:
|
||||
logout:
|
||||
button: 登出
|
||||
@ -975,6 +976,43 @@ core:
|
||||
os: 操作系統:{os}
|
||||
alert:
|
||||
external_url_invalid: 檢測到外部訪問地址與當前訪問地址不一致,可能會導致部分連結無法正常跳轉,請檢查外部訪問地址設置。
|
||||
backup:
|
||||
title: 備份與還原
|
||||
tabs:
|
||||
backup_list: 備份
|
||||
restore: 還原
|
||||
empty:
|
||||
title: 沒有備份
|
||||
message: 目前沒有已建立的備份,您可以點擊重新整理或建立新的備份。
|
||||
actions:
|
||||
create: 建立備份
|
||||
operations:
|
||||
create:
|
||||
button: 建立備份
|
||||
title: 建立備份
|
||||
description: 確定要建立備份嗎?此操作可能需要較長時間。
|
||||
toast_success: 已請求建立備份
|
||||
delete:
|
||||
title: 刪除備份
|
||||
description: 確定要刪除此備份嗎?
|
||||
restore:
|
||||
title: 還原成功
|
||||
description: 還原成功後,需要重新啟動 Halo 才能正常載入系統資源,點擊確定後我們會自動停止運行 Halo。
|
||||
shutdown:
|
||||
toast_success: 已請求停止運行
|
||||
list:
|
||||
phases:
|
||||
pending: 準備中
|
||||
running: 備份中
|
||||
succeeded: 備份完成
|
||||
failed: 備份失敗
|
||||
fields:
|
||||
expiresAt: "{expiresAt}失效"
|
||||
restore:
|
||||
tips:
|
||||
first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。
|
||||
second: 2. 在還原過程中,雖然已有的資料不會被清除,但若有衝突的資料將被覆蓋。
|
||||
third: 3. 還原完成後會提示停止運行 Halo,停止後可能需要手動啟動。
|
||||
exception:
|
||||
not_found:
|
||||
message: 沒有找到該頁面
|
||||
@ -1047,6 +1085,8 @@ core:
|
||||
Users Management: 用戶
|
||||
User manage: 用戶管理
|
||||
User View: 用戶查看
|
||||
Migration Management: 備份與還原
|
||||
Migration Manage: 備份與還原管理
|
||||
role-template-view-users: 用戶查看
|
||||
role-template-change-password: 修改密碼
|
||||
components:
|
||||
|
@ -11,6 +11,7 @@ import roleModule from "./system/roles/module";
|
||||
import settingModule from "./system/settings/module";
|
||||
import actuatorModule from "./system/actuator/module";
|
||||
import authProviderModule from "./system/auth-providers/module";
|
||||
import backupModule from "./system/backup/module";
|
||||
|
||||
// const coreModules = [
|
||||
// dashboardModule,
|
||||
@ -40,6 +41,7 @@ const coreModules = [
|
||||
userModule,
|
||||
roleModule,
|
||||
authProviderModule,
|
||||
backupModule,
|
||||
];
|
||||
|
||||
export { coreModules };
|
||||
|
90
console/src/modules/system/backup/Backups.vue
Normal file
90
console/src/modules/system/backup/Backups.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
VPageHeader,
|
||||
VCard,
|
||||
VButton,
|
||||
VTabbar,
|
||||
IconAddCircle,
|
||||
IconServerLine,
|
||||
} from "@halo-dev/components";
|
||||
|
||||
import { onMounted, shallowRef } from "vue";
|
||||
import ListTab from "./tabs/List.vue";
|
||||
import RestoreTab from "./tabs/Restore.vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { markRaw } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useBackup } from "./composables/use-backup";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { BackupTab } from "@halo-dev/console-shared";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabs = shallowRef<BackupTab[]>([
|
||||
{
|
||||
id: "backups",
|
||||
label: t("core.backup.tabs.backup_list"),
|
||||
component: markRaw(ListTab),
|
||||
},
|
||||
{
|
||||
id: "restore",
|
||||
label: t("core.backup.tabs.restore"),
|
||||
component: markRaw(RestoreTab),
|
||||
},
|
||||
]);
|
||||
|
||||
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id);
|
||||
|
||||
const { handleCreate } = useBackup();
|
||||
|
||||
onMounted(() => {
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
pluginModules.forEach((pluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.["backup:tabs:create"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backupTabs = extensionPoints["backup:tabs:create"]() as BackupTab[];
|
||||
|
||||
if (backupTabs) {
|
||||
tabs.value = tabs.value.concat(backupTabs);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader :title="$t('core.backup.title')">
|
||||
<template #icon>
|
||||
<IconServerLine class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VButton type="secondary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.backup.operations.create.button") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<template #header>
|
||||
<VTabbar
|
||||
v-model:active-id="activeTab"
|
||||
:items="tabs.map((item) => ({ id: item.id, label: item.label }))"
|
||||
class="w-full !rounded-none"
|
||||
type="outline"
|
||||
></VTabbar>
|
||||
</template>
|
||||
<div class="bg-white">
|
||||
<template v-for="tab in tabs" :key="tab.id">
|
||||
<component :is="tab.component" v-if="activeTab === tab.id" />
|
||||
</template>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
171
console/src/modules/system/backup/components/BackupListItem.vue
Normal file
171
console/src/modules/system/backup/components/BackupListItem.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Dialog,
|
||||
Toast,
|
||||
VDropdownItem,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VSpace,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import type { Backup } from "@halo-dev/api-client";
|
||||
import { relativeTimeTo, formatDatetime } from "@/utils/date";
|
||||
import { computed } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
backup: Backup;
|
||||
}>();
|
||||
|
||||
type Phase = {
|
||||
text: string;
|
||||
state: "default" | "warning" | "success" | "error";
|
||||
animate: boolean;
|
||||
value: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED";
|
||||
};
|
||||
|
||||
const phases: Phase[] = [
|
||||
{
|
||||
text: t("core.backup.list.phases.pending"),
|
||||
state: "default",
|
||||
animate: false,
|
||||
value: "PENDING",
|
||||
},
|
||||
{
|
||||
text: t("core.backup.list.phases.running"),
|
||||
state: "warning",
|
||||
animate: true,
|
||||
value: "RUNNING",
|
||||
},
|
||||
{
|
||||
text: t("core.backup.list.phases.succeeded"),
|
||||
state: "success",
|
||||
animate: false,
|
||||
value: "SUCCEEDED",
|
||||
},
|
||||
{
|
||||
text: t("core.backup.list.phases.failed"),
|
||||
state: "error",
|
||||
animate: false,
|
||||
value: "FAILED",
|
||||
},
|
||||
];
|
||||
|
||||
const getPhase = computed(() => {
|
||||
if (!props.backup.status?.phase) {
|
||||
return undefined;
|
||||
}
|
||||
return phases.find((phase) => phase.value === props.backup.status?.phase);
|
||||
});
|
||||
|
||||
const getFailureMessage = computed(() => {
|
||||
const { phase, failureMessage } = props.backup.status || {};
|
||||
return phase === "FAILED" ? failureMessage : undefined;
|
||||
});
|
||||
|
||||
function handleDownload() {
|
||||
window.open(
|
||||
`/apis/api.console.migration.halo.run/v1alpha1/backups/${props.backup.metadata.name}/files/${props.backup.status?.filename}`,
|
||||
"_blank"
|
||||
);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
Dialog.warning({
|
||||
title: t("core.backup.operations.delete.title"),
|
||||
description: t("core.backup.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await apiClient.extension.backup.deletemigrationHaloRunV1alpha1Backup({
|
||||
name: props.backup.metadata.name,
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["backups"] });
|
||||
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VEntity>
|
||||
<template #start>
|
||||
<VEntityField
|
||||
:title="backup.metadata.name"
|
||||
:description="backup.status?.filename"
|
||||
>
|
||||
<template v-if="backup.status?.filename" #description>
|
||||
<VSpace class="flex-wrap">
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ backup.status?.filename }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ prettyBytes(backup.status?.size || 0) }}
|
||||
</span>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
<VEntityField v-if="getPhase">
|
||||
<template #description>
|
||||
<VStatusDot
|
||||
v-tooltip="{ content: getFailureMessage }"
|
||||
:state="getPhase.state"
|
||||
:text="getPhase.text"
|
||||
:animate="getPhase.animate"
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="backup.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField
|
||||
v-if="backup.spec?.expiresAt && backup.status?.phase === 'SUCCEEDED'"
|
||||
>
|
||||
<template #description>
|
||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||
{{
|
||||
$t("core.backup.list.fields.expiresAt", {
|
||||
expiresAt: relativeTimeTo(backup.spec?.expiresAt),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="backup.metadata.creationTimestamp">
|
||||
<template #description>
|
||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||
{{ formatDatetime(backup.metadata.creationTimestamp) }}
|
||||
</span>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<VDropdownItem
|
||||
v-if="backup.status?.phase === 'SUCCEEDED'"
|
||||
@click="handleDownload"
|
||||
>
|
||||
{{ $t("core.common.buttons.download") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem type="danger" @click="handleDelete">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VEntity>
|
||||
</template>
|
40
console/src/modules/system/backup/composables/use-backup.ts
Normal file
40
console/src/modules/system/backup/composables/use-backup.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export function useBackup() {
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleCreate = async () => {
|
||||
Dialog.info({
|
||||
title: t("core.backup.operations.create.title"),
|
||||
description: t("core.backup.operations.create.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await apiClient.extension.backup.createmigrationHaloRunV1alpha1Backup({
|
||||
backup: {
|
||||
apiVersion: "migration.halo.run/v1alpha1",
|
||||
kind: "Backup",
|
||||
metadata: {
|
||||
generateName: "backup-",
|
||||
name: "",
|
||||
},
|
||||
spec: {
|
||||
expiresAt: dayjs().add(7, "day").toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["backups"] });
|
||||
|
||||
Toast.success(t("core.backup.operations.create.toast_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { handleCreate };
|
||||
}
|
33
console/src/modules/system/backup/module.ts
Normal file
33
console/src/modules/system/backup/module.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { definePlugin } from "@halo-dev/console-shared";
|
||||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||
import Backups from "./Backups.vue";
|
||||
import { IconServerLine } from "@halo-dev/components";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
path: "/backup",
|
||||
component: BasicLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "Backup",
|
||||
component: Backups,
|
||||
meta: {
|
||||
title: "core.backup.title",
|
||||
searchable: true,
|
||||
permissions: ["system:migrations:manage"],
|
||||
menu: {
|
||||
name: "core.sidebar.menu.items.backup",
|
||||
group: "system",
|
||||
icon: markRaw(IconServerLine),
|
||||
priority: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
85
console/src/modules/system/backup/tabs/List.vue
Normal file
85
console/src/modules/system/backup/tabs/List.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { BackupStatusPhaseEnum } from "@halo-dev/api-client";
|
||||
import {
|
||||
IconAddCircle,
|
||||
VButton,
|
||||
VEmpty,
|
||||
VLoading,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import BackupListItem from "../components/BackupListItem.vue";
|
||||
import { useBackup } from "../composables/use-backup";
|
||||
|
||||
const {
|
||||
data: backups,
|
||||
isLoading,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["backups"],
|
||||
queryFn: async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.backup.listmigrationHaloRunV1alpha1Backup({
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
});
|
||||
return data;
|
||||
},
|
||||
refetchInterval(data) {
|
||||
const deletingBackups = data?.items.filter((backup) => {
|
||||
return !!backup.metadata.deletionTimestamp;
|
||||
});
|
||||
|
||||
if (deletingBackups?.length) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
const pendingBackups = data?.items.filter((backup) => {
|
||||
return (
|
||||
backup.status?.phase === BackupStatusPhaseEnum.Pending ||
|
||||
backup.status?.phase === BackupStatusPhaseEnum.Running
|
||||
);
|
||||
});
|
||||
|
||||
if (pendingBackups?.length) {
|
||||
return 3000;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const { handleCreate } = useBackup();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!backups?.items?.length" appear name="fade">
|
||||
<VEmpty
|
||||
:message="$t('core.backup.empty.message')"
|
||||
:title="$t('core.backup.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :loading="isFetching" @click="refetch()">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton type="secondary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.backup.empty.actions.create") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEmpty>
|
||||
</Transition>
|
||||
<Transition v-else appear name="fade">
|
||||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||
<li v-for="(backup, index) in backups?.items" :key="index">
|
||||
<BackupListItem :backup="backup" />
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
</template>
|
89
console/src/modules/system/backup/tabs/Restore.vue
Normal file
89
console/src/modules/system/backup/tabs/Restore.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||
import { Dialog, Toast, VAlert, VLoading } from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "axios";
|
||||
import { computed } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const complete = ref(false);
|
||||
|
||||
const onUploaded = () => {
|
||||
Dialog.success({
|
||||
title: t("core.backup.operations.restore.title"),
|
||||
description: t("core.backup.operations.restore.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await handleShutdown();
|
||||
},
|
||||
async onCancel() {
|
||||
await handleShutdown();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function handleShutdown() {
|
||||
await axios.post(`/actuator/shutdown`);
|
||||
Toast.success(t("core.backup.operations.shutdown.toast_success"));
|
||||
|
||||
setTimeout(() => {
|
||||
complete.value = true;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
useQuery({
|
||||
queryKey: ["check-health"],
|
||||
queryFn: async () => {
|
||||
const { data } = await axios.get("/actuator/health");
|
||||
return data;
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (data.status === "UP") {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
retry: true,
|
||||
retryDelay: 2000,
|
||||
enabled: computed(() => complete.value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!complete">
|
||||
<div class="px-4 py-3">
|
||||
<VAlert :title="$t('core.common.text.tip')" :closable="false">
|
||||
<template #description>
|
||||
<ul>
|
||||
<li>{{ $t("core.backup.restore.tips.first") }}</li>
|
||||
<li>
|
||||
{{ $t("core.backup.restore.tips.second") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t("core.backup.restore.tips.third") }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VAlert>
|
||||
</div>
|
||||
<div class="flex items-center justify-center px-4 py-3">
|
||||
<UppyUpload
|
||||
:restrictions="{
|
||||
maxNumberOfFiles: 1,
|
||||
allowedFileTypes: ['.zip'],
|
||||
}"
|
||||
endpoint="/apis/api.console.migration.halo.run/v1alpha1/restorations"
|
||||
width="100%"
|
||||
@uploaded="onUploaded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex h-72 flex-col items-center justify-center">
|
||||
<VLoading />
|
||||
<div class="text-xs text-gray-600">恢复完成,等待重启...</div>
|
||||
</div>
|
||||
</template>
|
@ -38,6 +38,8 @@ import {
|
||||
AuthHaloRunV1alpha1AuthProviderApi,
|
||||
AuthHaloRunV1alpha1UserConnectionApi,
|
||||
ApiHaloRunV1alpha1UserApi,
|
||||
MigrationHaloRunV1alpha1BackupApi,
|
||||
ApiConsoleMigrationHaloRunV1alpha1MigrationApi,
|
||||
} from "@halo-dev/api-client";
|
||||
import type { AxiosError, AxiosInstance } from "axios";
|
||||
import axios from "axios";
|
||||
@ -180,6 +182,7 @@ function setupApiClient(axios: AxiosInstance) {
|
||||
baseURL,
|
||||
axios
|
||||
),
|
||||
backup: new MigrationHaloRunV1alpha1BackupApi(undefined, baseURL, axios),
|
||||
},
|
||||
// custom endpoints
|
||||
user: new ApiConsoleHaloRunV1alpha1UserApi(undefined, baseURL, axios),
|
||||
@ -210,6 +213,11 @@ function setupApiClient(axios: AxiosInstance) {
|
||||
user: new ApiHaloRunV1alpha1UserApi(undefined, baseURL, axios),
|
||||
},
|
||||
cache: new V1alpha1CacheApi(undefined, baseURL, axios),
|
||||
migration: new ApiConsoleMigrationHaloRunV1alpha1MigrationApi(
|
||||
undefined,
|
||||
baseURL,
|
||||
axios
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,21 @@
|
||||
import { i18n } from "@/locales";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import "dayjs/locale/en";
|
||||
import "dayjs/locale/zh-tw";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(utc);
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
dayjs.extend(relativeTime);
|
||||
const dayjsLocales = {
|
||||
en: "en",
|
||||
zh: "zh-cn",
|
||||
"en-US": "en",
|
||||
"zh-CN": "zh-cn",
|
||||
"zh-TW": "zh-tw",
|
||||
};
|
||||
|
||||
export function formatDatetime(
|
||||
date: string | Date | undefined | null,
|
||||
@ -35,3 +44,24 @@ export function toDatetimeLocal(
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#the_y10k_problem_often_client-side
|
||||
return dayjs(date).tz(tz).format("YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative time to end date
|
||||
*
|
||||
* @param date end date
|
||||
* @returns relative time to end date
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* // now is 2020-12-01
|
||||
* RelativeTimeTo("2021-01-01") // in 1 month
|
||||
*/
|
||||
export function relativeTimeTo(date: string | Date | undefined | null) {
|
||||
dayjs.locale(dayjsLocales[i18n.global.locale.value] || dayjsLocales["zh-CN"]);
|
||||
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
return dayjs().to(dayjs(date));
|
||||
}
|
||||
|
240
docs/backup-and-restore.md
Normal file
240
docs/backup-and-restore.md
Normal file
@ -0,0 +1,240 @@
|
||||
# 备份和恢复 Proposal
|
||||
|
||||
## Motivation
|
||||
|
||||
目前,Halo 2.x 支持多种数据库:H2、MySQL、MariaDB、Microsoft SQL Server、Oracle 和
|
||||
PostgreSQL,虽然数据库有备份和恢复的功能,但是仍然缺少应用级别的备份和恢复功能。Halo
|
||||
的数据不仅限于数据库中的数据,还包含工作目录下的数据,例如主题、插件和日志等。
|
||||
|
||||
## Goals
|
||||
|
||||
- 全站备份,包括数据库中的数据和工作目录的数据。
|
||||
- 全站恢复,包括恢复数据库中的数据和工作目录的数据。
|
||||
- 用户可控制备份文件存储的时间。
|
||||
- 对于工作目录的数据,用户可选择性备份和恢复。
|
||||
- 用户可指定备份权限到任意用户。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 仅备份部分自定义资源。
|
||||
- 仅备份和恢复文章 Markdown。
|
||||
- 定时备份。
|
||||
- 加密备份文件。
|
||||
- 备份文件自动上传至对象存储。
|
||||
|
||||
## Use Cases
|
||||
|
||||
- 从某种数据库(例如:H2)迁移至另外的数据库(例如:MySQL),不会因为 SQL 的兼容性而影响迁移。
|
||||
- 定时完整备份 Halo,并存储至对象存储,一旦发生意外可随时恢复。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 仅支持 2.8.x 及以上的 Halo。
|
||||
- 恢复的数据的 creationTimestamp 可能会被当前时间覆盖。
|
||||
|
||||
## Draft
|
||||
|
||||
恢复数据之前需要完整备份当前 Halo,以便恢复过程中发生错误导致无法回滚。
|
||||
|
||||
备份文件将存储在 `${halo.work-dir}/backups/halo-full-backup-2023.07.03-17:52:59.zip`。
|
||||
|
||||
备份整站可能需要大量的时间,所以我们需要创建自定义模型(Backup)用于保存用户创建备份的请求,并异步执行备份操作,最终将结果反馈至自定义模型数据中。
|
||||
|
||||
Backup 模型样例如下:
|
||||
|
||||
- 备份成功样例
|
||||
|
||||
```yaml
|
||||
apiVersion: migration.halo.run/v1alpha1
|
||||
kind: Backup
|
||||
metadata:
|
||||
name: halo-full-backup-xyz
|
||||
creationTimestamp: 2023.07.04-10:25:30
|
||||
spec:
|
||||
format: zip
|
||||
autoDeleteWhen: 2023.07.10-00:00:00Z
|
||||
status:
|
||||
phase: Succeeded
|
||||
startTimestamp: 2023.07.04-10:25:31
|
||||
completionTimestamp: 2023.07.04-10:26:30
|
||||
filename: halo-full-backup-2023-07-04-10-25-30.zip
|
||||
size: 1024 # data unit: bytes
|
||||
```
|
||||
|
||||
- 备份失败样例
|
||||
|
||||
```yaml
|
||||
apiVersion: migration.halo.run/v1alpha1
|
||||
kind: Backup
|
||||
metadata:
|
||||
name: halo-full-backup-xyz
|
||||
creationTimestamp: 2023.07.04-10:25:30
|
||||
spec:
|
||||
compressionFormat: zip | 7z | tar | tar.gz # 压缩格式
|
||||
status:
|
||||
startTimestamp: 2023.07.04-10:25:31
|
||||
# Pending: 刚刚创建好 Backup 资源,等待 Reconciler reconcile。
|
||||
# Running: Reconciler 正在备份 Halo。
|
||||
# Succeeded: Reconciler 成功执行备份 Halo 操作。
|
||||
# Failed: 备份 Halo 失败。
|
||||
phase: Failed
|
||||
failureReason: DatabaseConnectionReset | UnsupportedCompression # 机器可识别的信息
|
||||
failureMessage: The database connection reset. # 人类可阅读的信息
|
||||
```
|
||||
|
||||
同时,BackupReconciler 将负责备份操作,并更新 Backup 数据。
|
||||
|
||||
请求示例如下:
|
||||
|
||||
```text
|
||||
POST /apis/migration.halo.run/v1alpha1/backups
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 备份
|
||||
|
||||
准备好所有的备份内容后,需要计算摘要并保存,以便后期恢复校验备份文件完整性使用。
|
||||
|
||||
#### 数据库备份和恢复
|
||||
|
||||
因为 Halo 的 [Extension 设计](https://github.com/halo-dev/rfcs/tree/main/extension),所以 Halo 的在数据库中的数据备份相对比较简单,只需要简单备份
|
||||
ExtensionStore 即可。恢复同理。
|
||||
|
||||
#### 工作目录备份和恢复
|
||||
|
||||
Halo 工作目录样例如下所示:
|
||||
|
||||
```text
|
||||
├── application.yaml
|
||||
├── attachments
|
||||
│ └── upload
|
||||
│ └── image_2023-06-09_16-24-41.png
|
||||
├── db
|
||||
│ └── halo-next.mv.db
|
||||
├── indices
|
||||
│ └── posts
|
||||
│ ├── _a.cfe
|
||||
│ ├── _a.cfs
|
||||
│ ├── _a.si
|
||||
│ ├── segments_h
|
||||
│ └── write.lock
|
||||
├── keys
|
||||
│ ├── id_rsa
|
||||
│ └── id_rsa.pub
|
||||
├── logs
|
||||
│ ├── halo.log
|
||||
│ ├── halo.log.2023-06-01.0.gz
|
||||
│ ├── halo.log.2023-06-02.0.gz
|
||||
│ ├── halo.log.2023-06-05.0.gz
|
||||
│ └── halo.log.2023-06-26.0.gz
|
||||
├── plugins
|
||||
│ ├── PluginCommentWidget-1.5.0.jar
|
||||
│ ├── PluginFeed-1.1.1.jar
|
||||
│ ├── PluginSearchWidget-1.0.0.jar
|
||||
│ ├── PluginSitemap-1.0.2.jar
|
||||
│ └── configs
|
||||
└── themes
|
||||
├── theme-earth
|
||||
│ ├── README.md
|
||||
│ ├── settings.yaml
|
||||
│ ├── templates
|
||||
│ │ ├── archives.html
|
||||
│ │ ├── assets
|
||||
│ │ │ ├── dist
|
||||
│ │ │ │ ├── main.iife.js
|
||||
│ │ │ │ └── style.css
|
||||
│ │ │ └── images
|
||||
│ │ │ ├── default-avatar.svg
|
||||
│ │ │ └── default-background.png
|
||||
│ │ ├── author.html
|
||||
│ │ ├── category.html
|
||||
│ │ ├── error
|
||||
│ │ │ └── error.html
|
||||
│ │ ├── index.html
|
||||
│ │ ├── links.html
|
||||
│ │ ├── modules
|
||||
│ │ │ ├── category-filter.html
|
||||
│ │ │ ├── category-tree.html
|
||||
│ │ │ ├── featured-post-card.html
|
||||
│ │ │ ├── footer.html
|
||||
│ │ │ ├── header.html
|
||||
│ │ │ ├── hero.html
|
||||
│ │ │ ├── layout.html
|
||||
│ │ │ ├── post-card.html
|
||||
│ │ │ ├── sidebar.html
|
||||
│ │ │ ├── tag-filter.html
|
||||
│ │ │ └── widgets
|
||||
│ │ │ ├── categories.html
|
||||
│ │ │ ├── latest-comments.html
|
||||
│ │ │ ├── popular-posts.html
|
||||
│ │ │ ├── profile.html
|
||||
│ │ │ └── tags.html
|
||||
│ │ ├── page.html
|
||||
│ │ ├── post.html
|
||||
│ │ ├── tag.html
|
||||
│ │ └── tags.html
|
||||
│ └── theme.yaml
|
||||
```
|
||||
|
||||
备份时需要过滤 `db`、backups` 和 `indices` 目录。
|
||||
|
||||
#### 备份文件结构
|
||||
|
||||
备份文件主要包含自定义资源(`extensions.data`)和工作目录(`workdir.data`)的数据。
|
||||
|
||||
- `extensions.data`
|
||||
|
||||
前期可考虑使用 JSON 来存储所有的 ExtensionStore 数据。
|
||||
|
||||
- `workdir.data`
|
||||
|
||||
对工作目录进行 `ZIP` 压缩。
|
||||
|
||||
- config.yaml(备份配置)
|
||||
|
||||
主要用于描述 `extensions.data` 和 `workdir.data` 压缩格式,后续可扩展备份与恢复相关的配置。例如:
|
||||
|
||||
```yaml
|
||||
compressions:
|
||||
extensions: json | others
|
||||
workdir: zip | others
|
||||
```
|
||||
|
||||
前期可不实现该功能。
|
||||
|
||||
### 恢复
|
||||
|
||||
用户通过上传备份文件的方式进行恢复。当且仅当博客未初始化阶段才能进行恢复操作,否则可能会造成数据不一致。
|
||||
|
||||
请求示例如下:
|
||||
|
||||
```text
|
||||
POST /apis/migration.halo.run/v1alpha1/restorations
|
||||
Content-Type: multipart/form-data; boundary="boundary"
|
||||
|
||||
'''
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="backupfile"; filename="halo-full-backup.zip"
|
||||
Content-Type: application/zip
|
||||
'''
|
||||
```
|
||||
|
||||
恢复步骤如下:
|
||||
|
||||
1. 解压缩备份文件。
|
||||
2. 校验备份文件的完整性。
|
||||
2. 恢复所有 ExtensionStore。
|
||||
3. 覆盖当前工作目录。
|
||||
4. 备份完成。
|
||||
|
||||
> 需要注意内存占用问题。
|
||||
|
||||
## TBDs
|
||||
|
||||
- 数据备份期间可能会存在数据的创建、更新和删除。
|
||||
|
||||
我们将忽略这些数据变化。
|
||||
|
||||
- 是否支持在初始化博客后恢复数据?
|
||||
|
||||
支持。不过可能会覆盖掉已有的数据。
|
Loading…
Reference in New Issue
Block a user