Skip to content

Commit

Permalink
NAS-130794 / 25.04 / Fix Cloud Backup Restore Include From Subfolder (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko authored Sep 11, 2024
1 parent f54a2ad commit 25da2b4
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 46 deletions.
8 changes: 8 additions & 0 deletions src/app/interfaces/cloud-backup.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { marker as T } from '@biesbjerg/ngx-translate-extract-marker';
import { ApiTimestamp } from 'app/interfaces/api-date.interface';
import { Job } from 'app/interfaces/job.interface';
import { BwLimit, BwLimitUpdate, CloudCredential } from './cloud-sync-task.interface';
Expand Down Expand Up @@ -51,6 +52,13 @@ export enum SnapshotIncludeExclude {
ExcludeByPattern = 'excludeByPattern',
}

export const snapshotIncludeExcludeOptions = new Map<SnapshotIncludeExclude, string>([
[SnapshotIncludeExclude.IncludeEverything, T('Include everything')],
[SnapshotIncludeExclude.IncludeFromSubFolder, T('Include from subfolder')],
[SnapshotIncludeExclude.ExcludePaths, T('Select paths to exclude')],
[SnapshotIncludeExclude.ExcludeByPattern, T('Exclude by pattern')],
]);

export type CloudBackupRestoreParams = [
id: number,
snapshot_id: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
@if (isIncludeFromSubfolderSelected) {
<ix-explorer
formControlName="includedPaths"
[root]="form.controls.subFolder.value || mntPath"
[root]="form.controls.subFolder.value || data.backup.path"
[label]="'Included Paths' | translate"
[nodeProvider]="snapshotNodeProvider"
[tooltip]="helptext.tooltips.included_paths"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectat
import { of } from 'rxjs';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockJob, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { SnapshotIncludeExclude } from 'app/interfaces/cloud-backup.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxSlideInRef } from 'app/modules/forms/ix-forms/components/ix-slide-in/ix-slide-in-ref';
import { SLIDE_IN_DATA } from 'app/modules/forms/ix-forms/components/ix-slide-in/ix-slide-in.token';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { CloudBackupRestoreFromSnapshotFormComponent } from 'app/pages/data-protection/cloud-backup/cloud-backup-details/cloud-backup-restore-form-snapshot-form/cloud-backup-restore-from-snapshot-form.component';
import { FilesystemService } from 'app/services/filesystem.service';
import { IxSlideInService } from 'app/services/ix-slide-in.service';
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('CloudBackupRestoreFromSnapshotFormComponent', () => {
provide: SLIDE_IN_DATA,
useValue: {
snapshot: { id: 1 },
backup: { id: 1, path: '/mnt/backup/path' },
backup: { id: 1, path: '/mnt/dozer' },
},
},
],
Expand All @@ -56,7 +56,7 @@ describe('CloudBackupRestoreFromSnapshotFormComponent', () => {

it('submits backup restore from snapshot with `Include Everything`', async () => {
spectator.component.form.patchValue({
target: '/mnt/my pool',
target: '/mnt/bulldozer',
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
Expand All @@ -65,20 +65,18 @@ describe('CloudBackupRestoreFromSnapshotFormComponent', () => {
expect(spectator.inject(WebSocketService).job).toHaveBeenCalledWith('cloud_backup.restore', [
1,
1,
'/mnt/backup/path',
'/mnt/my pool',
'/mnt/dozer',
'/mnt/bulldozer',
{},
]);
});

it('submits backup restore from snapshot with `Select paths to exclude`', async () => {
spectator.component.form.patchValue({
target: '/mnt/my pool',
includeExclude: SnapshotIncludeExclude.ExcludePaths,
});

spectator.component.form.patchValue({
excludedPaths: ['/mnt/another'],
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
Target: '/mnt/bulldozer',
'Include/Exclude': 'Select paths to exclude',
'Excluded Paths': '/mnt/dozer/another',
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
Expand All @@ -87,25 +85,48 @@ describe('CloudBackupRestoreFromSnapshotFormComponent', () => {
expect(spectator.inject(WebSocketService).job).toHaveBeenCalledWith('cloud_backup.restore', [
1,
1,
'/mnt/backup/path',
'/mnt/my pool',
'/mnt/dozer',
'/mnt/bulldozer',
{
exclude: [
'/mnt/another',
'/another',
],
},
]);
});

it('submits backup restore from snapshot with `Include from subfolder`', async () => {
spectator.component.form.patchValue({
target: '/mnt/my pool',
includeExclude: SnapshotIncludeExclude.IncludeFromSubFolder,
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
Target: '/mnt/bulldozer',
'Include/Exclude': 'Include from subfolder',
Subfolder: '/mnt/dozer',
'Included Paths': '/mnt/dozer/a',
});

spectator.component.form.patchValue({
subFolder: '/test',
includedPaths: ['/test/first'],
const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await saveButton.click();

expect(spectator.inject(WebSocketService).job).toHaveBeenCalledWith('cloud_backup.restore', [
1,
1,
'/mnt/dozer',
'/mnt/bulldozer',
{
include: [
'/a',
],
},
]);
});

it('submits backup restore from snapshot with `Include from subfolder` matches paths', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
Target: '/mnt/bulldozer',
'Include/Exclude': 'Include from subfolder',
Subfolder: '/mnt/dozer/a',
'Included Paths': '/mnt/dozer/a',
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
Expand All @@ -114,24 +135,22 @@ describe('CloudBackupRestoreFromSnapshotFormComponent', () => {
expect(spectator.inject(WebSocketService).job).toHaveBeenCalledWith('cloud_backup.restore', [
1,
1,
'/test',
'/mnt/my pool',
'/mnt/dozer/a',
'/mnt/bulldozer',
{
include: [
'/test/first',
'/',
],
},
]);
});

it('submits backup restore from snapshot with `Exclude by pattern`', async () => {
spectator.component.form.patchValue({
target: '/mnt/my pool',
includeExclude: SnapshotIncludeExclude.ExcludeByPattern,
});

spectator.component.form.patchValue({
excludePattern: 'pattern',
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
Target: '/mnt/bulldozer',
'Include/Exclude': 'Exclude by pattern',
Pattern: 'pattern',
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
Expand All @@ -140,8 +159,8 @@ describe('CloudBackupRestoreFromSnapshotFormComponent', () => {
expect(spectator.inject(WebSocketService).job).toHaveBeenCalledWith('cloud_backup.restore', [
1,
1,
'/mnt/backup/path',
'/mnt/my pool',
'/mnt/dozer',
'/mnt/bulldozer',
{
exclude: [
'pattern',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CloudBackupSnapshot,
CloudBackupSnapshotDirectoryFileType,
SnapshotIncludeExclude,
snapshotIncludeExcludeOptions,
} from 'app/interfaces/cloud-backup.interface';
import { DatasetCreate } from 'app/interfaces/dataset.interface';
import { ExplorerNodeData, TreeNode } from 'app/interfaces/tree-node.interface';
Expand All @@ -44,13 +45,7 @@ export class CloudBackupRestoreFromSnapshotFormComponent implements OnInit {
fileNodeProvider: TreeNodeProvider;
snapshotNodeProvider: TreeNodeProvider;

readonly includeExcludeOptions = new Map<SnapshotIncludeExclude, string>([
[SnapshotIncludeExclude.IncludeEverything, this.translate.instant('Include everything')],
[SnapshotIncludeExclude.IncludeFromSubFolder, this.translate.instant('Include from subfolder')],
[SnapshotIncludeExclude.ExcludePaths, this.translate.instant('Select paths to exclude')],
[SnapshotIncludeExclude.ExcludeByPattern, this.translate.instant('Exclude by pattern')],
]);
readonly includeExcludeOptions$ = of(mapToOptions(this.includeExcludeOptions, this.translate));
readonly includeExcludeOptions$ = of(mapToOptions(snapshotIncludeExcludeOptions, this.translate));

get title(): string {
return this.translate.instant('Restore from Snapshot');
Expand All @@ -61,7 +56,7 @@ export class CloudBackupRestoreFromSnapshotFormComponent implements OnInit {
includeExclude: [SnapshotIncludeExclude.IncludeEverything, Validators.required],
excludedPaths: [[] as string[], Validators.required],
excludePattern: [null as string | null, Validators.required],
subFolder: [mntPath],
subFolder: [this.data.backup.path],
includedPaths: [[] as string[]],
});

Expand Down Expand Up @@ -105,11 +100,15 @@ export class CloudBackupRestoreFromSnapshotFormComponent implements OnInit {
onSubmit(): void {
this.isLoading = true;

const subfolder = this.isIncludeFromSubfolderSelected ? this.form.controls.subFolder.value : this.data.backup.path;

const options = {
exclude: this.isExcludeByPatternSelected
? [this.form.controls.excludePattern.value]
: this.form.controls.excludedPaths.value,
include: this.isIncludeFromSubfolderSelected ? this.form.value.includedPaths : null,
: this.form.controls.excludedPaths.value.map((path) => path.replace(subfolder, '') || '/'),
include: this.isIncludeFromSubfolderSelected
? this.form.value.includedPaths.map((path) => path.replace(subfolder, '') || '/')
: null,
};

if (!options.exclude?.length) delete options.exclude;
Expand All @@ -118,11 +117,10 @@ export class CloudBackupRestoreFromSnapshotFormComponent implements OnInit {
const params: CloudBackupRestoreParams = [
this.data.backup.id,
this.data.snapshot.id,
this.isIncludeFromSubfolderSelected ? this.form.controls.subFolder.value : this.data.backup.path,
subfolder,
this.form.controls.target.value,
options,
];

this.ws.job('cloud_backup.restore', params)
.pipe(untilDestroyed(this))
.subscribe({
Expand Down

0 comments on commit 25da2b4

Please sign in to comment.