@duplicati/ngclient 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +13 -0
- package/.github/workflows/npm-publish.yml +59 -0
- package/.prettierignore +13 -0
- package/.prettierrc +12 -0
- package/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +13 -0
- package/.vscode/settings.json +8 -0
- package/.vscode/tasks.json +24 -0
- package/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/angular.json +98 -0
- package/bun.lockb +0 -0
- package/openapi-ts.config.ts +13 -0
- package/package.json +46 -0
- package/scripts/generate-package-json.ts +15 -0
- package/src/app/about/about.component.html +15 -0
- package/src/app/about/about.component.scss +19 -0
- package/src/app/about/about.component.ts +13 -0
- package/src/app/about/changelog/changelog.component.html +3 -0
- package/src/app/about/changelog/changelog.component.scss +7 -0
- package/src/app/about/changelog/changelog.component.ts +18 -0
- package/src/app/about/general/general.component.html +9 -0
- package/src/app/about/general/general.component.scss +23 -0
- package/src/app/about/general/general.component.ts +30 -0
- package/src/app/about/libraries/libraries.component.html +3 -0
- package/src/app/about/libraries/libraries.component.scss +7 -0
- package/src/app/about/libraries/libraries.component.ts +18 -0
- package/src/app/about/logs/logs.component.html +5 -0
- package/src/app/about/logs/logs.component.scss +7 -0
- package/src/app/about/logs/logs.component.ts +18 -0
- package/src/app/about/system-info/system-info.component.html +3 -0
- package/src/app/about/system-info/system-info.component.scss +7 -0
- package/src/app/about/system-info/system-info.component.ts +17 -0
- package/src/app/add-backup/add-backup.component.html +29 -0
- package/src/app/add-backup/add-backup.component.scss +38 -0
- package/src/app/add-backup/add-backup.component.ts +14 -0
- package/src/app/app.component.ts +35 -0
- package/src/app/app.config.ts +23 -0
- package/src/app/app.routes.ts +121 -0
- package/src/app/core/components/file-tree/file-tree.component.html +87 -0
- package/src/app/core/components/file-tree/file-tree.component.scss +80 -0
- package/src/app/core/components/file-tree/file-tree.component.ts +485 -0
- package/src/app/core/components/logo/logo.component.html +22 -0
- package/src/app/core/components/logo/logo.component.scss +12 -0
- package/src/app/core/components/logo/logo.component.ts +11 -0
- package/src/app/core/components/status-bar/status-bar.component.html +38 -0
- package/src/app/core/components/status-bar/status-bar.component.scss +27 -0
- package/src/app/core/components/status-bar/status-bar.component.ts +33 -0
- package/src/app/core/components/status-bar/status-bar.state.ts +182 -0
- package/src/app/core/components/toggle-card/toggle-card.component.html +11 -0
- package/src/app/core/components/toggle-card/toggle-card.component.scss +49 -0
- package/src/app/core/components/toggle-card/toggle-card.component.ts +28 -0
- package/src/app/core/interceptors/http.interceptor.ts +69 -0
- package/src/app/core/interceptors/websocket.interceptor.ts +66 -0
- package/src/app/core/openapi/core/ApiError.ts +21 -0
- package/src/app/core/openapi/core/ApiRequestOptions.ts +21 -0
- package/src/app/core/openapi/core/ApiResult.ts +7 -0
- package/src/app/core/openapi/core/OpenAPI.ts +55 -0
- package/src/app/core/openapi/core/request.ts +337 -0
- package/src/app/core/openapi/index.ts +6 -0
- package/src/app/core/openapi/schemas.gen.ts +3611 -0
- package/src/app/core/openapi/services.gen.ts +1460 -0
- package/src/app/core/openapi/types.gen.ts +1510 -0
- package/src/app/core/pipes/duration.pipe.ts +27 -0
- package/src/app/core/pipes/relative-time.pipe.ts +14 -0
- package/src/app/core/providers/dayjs.ts +16 -0
- package/src/app/core/services/dict.en.ts +0 -0
- package/src/app/core/services/dictionary.service.ts +12 -0
- package/src/app/core/services/localstorage.token.ts +82 -0
- package/src/app/core/services/password-generator.service.ts +39 -0
- package/src/app/core/services/relay-websocket.service.ts +268 -0
- package/src/app/core/services/timespan-literals.service.ts +25 -0
- package/src/app/core/states/app-auth.state.ts +87 -0
- package/src/app/core/states/backups.state.ts +71 -0
- package/src/app/core/states/relayconfig.state.ts +23 -0
- package/src/app/core/states/sysinfo.state.ts +31 -0
- package/src/app/core/states/tasks.state.ts +34 -0
- package/src/app/core/types/subscribed.ts +7 -0
- package/src/app/core/validators/custom.validators.ts +120 -0
- package/src/app/create-edit-backup/create-edit-backup.component.html +28 -0
- package/src/app/create-edit-backup/create-edit-backup.component.scss +15 -0
- package/src/app/create-edit-backup/create-edit-backup.component.ts +33 -0
- package/src/app/create-edit-backup/create-edit-backup.state.ts +582 -0
- package/src/app/create-edit-backup/destination/destination.component.html +367 -0
- package/src/app/create-edit-backup/destination/destination.component.scss +85 -0
- package/src/app/create-edit-backup/destination/destination.component.ts +249 -0
- package/src/app/create-edit-backup/destination/destination.config.ts +103 -0
- package/src/app/create-edit-backup/destination/destination.mapper.ts +177 -0
- package/src/app/create-edit-backup/general/general.component.html +108 -0
- package/src/app/create-edit-backup/general/general.component.scss +34 -0
- package/src/app/create-edit-backup/general/general.component.ts +136 -0
- package/src/app/create-edit-backup/options/options.component.html +95 -0
- package/src/app/create-edit-backup/options/options.component.scss +42 -0
- package/src/app/create-edit-backup/options/options.component.ts +113 -0
- package/src/app/create-edit-backup/schedule/schedule.component.html +102 -0
- package/src/app/create-edit-backup/schedule/schedule.component.scss +38 -0
- package/src/app/create-edit-backup/schedule/schedule.component.ts +137 -0
- package/src/app/create-edit-backup/source-data/source-data.component.html +93 -0
- package/src/app/create-edit-backup/source-data/source-data.component.scss +41 -0
- package/src/app/create-edit-backup/source-data/source-data.component.ts +114 -0
- package/src/app/home/backup.const.ts +167 -0
- package/src/app/home/home.component.html +98 -0
- package/src/app/home/home.component.scss +88 -0
- package/src/app/home/home.component.ts +54 -0
- package/src/app/layout/layout.component.html +64 -0
- package/src/app/layout/layout.component.scss +24 -0
- package/src/app/layout/layout.component.ts +51 -0
- package/src/app/layout/layout.state.ts +70 -0
- package/src/app/login/login.component.html +12 -0
- package/src/app/login/login.component.scss +25 -0
- package/src/app/login/login.component.ts +34 -0
- package/src/app/logout/logout.component.html +1 -0
- package/src/app/logout/logout.component.scss +3 -0
- package/src/app/logout/logout.component.ts +18 -0
- package/src/app/restore/restore.component.html +56 -0
- package/src/app/restore/restore.component.scss +37 -0
- package/src/app/restore/restore.component.ts +55 -0
- package/src/app/restore-flow/options/options.component.html +57 -0
- package/src/app/restore-flow/options/options.component.scss +16 -0
- package/src/app/restore-flow/options/options.component.ts +58 -0
- package/src/app/restore-flow/restore-flow.component.html +14 -0
- package/src/app/restore-flow/restore-flow.component.scss +15 -0
- package/src/app/restore-flow/restore-flow.component.ts +23 -0
- package/src/app/restore-flow/restore-flow.state.ts +120 -0
- package/src/app/restore-flow/select-files/select-files.component.html +42 -0
- package/src/app/restore-flow/select-files/select-files.component.scss +16 -0
- package/src/app/restore-flow/select-files/select-files.component.ts +129 -0
- package/src/app/settings/settings.component.html +256 -0
- package/src/app/settings/settings.component.scss +106 -0
- package/src/app/settings/settings.component.ts +475 -0
- package/src/assets/duplicati-logo.png +0 -0
- package/src/assets/duplicati-logo.svg +10 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/backup.png +0 -0
- package/src/assets/images/dark-theme.png +0 -0
- package/src/assets/images/folder.png +0 -0
- package/src/assets/images/light-theme.png +0 -0
- package/src/assets/images/new-backup.png +0 -0
- package/src/assets/images/restore.png +0 -0
- package/src/assets/images/system-theme.png +0 -0
- package/src/assets/spk.css +33 -0
- package/src/assets/spk.woff2 +0 -0
- package/src/environments/environment-token.ts +4 -0
- package/src/environments/environment.prod.ts +6 -0
- package/src/environments/environment.ts +6 -0
- package/src/index.html +15 -0
- package/src/main.ts +6 -0
- package/src/proxy.conf.mjs +6 -0
- package/src/styles.scss +41 -0
- package/tsconfig.app.json +15 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
2
|
+
import { AppAuthState } from '../core/states/app-auth.state';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'app-logout',
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [],
|
|
8
|
+
templateUrl: './logout.component.html',
|
|
9
|
+
styleUrl: './logout.component.scss',
|
|
10
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11
|
+
})
|
|
12
|
+
export default class LogoutComponent {
|
|
13
|
+
#auth = inject(AppAuthState);
|
|
14
|
+
|
|
15
|
+
ngOnInit() {
|
|
16
|
+
this.#auth.logout();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<app-status-bar />
|
|
2
|
+
|
|
3
|
+
<section>
|
|
4
|
+
<h2>Where do you want to restore from?</h2>
|
|
5
|
+
|
|
6
|
+
<!-- <spk-card class="type-a">
|
|
7
|
+
<img src="/assets/images/backup.png" alt="" width="48px" />
|
|
8
|
+
<div class="text">
|
|
9
|
+
<h3>Direct restore from backup files</h3>
|
|
10
|
+
<p>Point to your backup files and restore from there.</p>
|
|
11
|
+
</div>
|
|
12
|
+
<button spk-button [routerLink]="['/restore-flow']">
|
|
13
|
+
Start
|
|
14
|
+
<spk-icon>play-circle</spk-icon>
|
|
15
|
+
</button>
|
|
16
|
+
</spk-card> -->
|
|
17
|
+
|
|
18
|
+
<!-- <spk-card class="type-a">
|
|
19
|
+
<img src="/assets/images/restore.png" alt="" width="48px" />
|
|
20
|
+
<div class="text">
|
|
21
|
+
<h3>Restore from configuration</h3>
|
|
22
|
+
<p>Load destination from an exported job or a storage provider</p>
|
|
23
|
+
</div>
|
|
24
|
+
<button spk-button>
|
|
25
|
+
Start
|
|
26
|
+
<spk-icon>play-circle</spk-icon>
|
|
27
|
+
</button>
|
|
28
|
+
</spk-card>
|
|
29
|
+
|
|
30
|
+
<spk-divider>Or existing</spk-divider> -->
|
|
31
|
+
|
|
32
|
+
@if (backupsLoading()) {
|
|
33
|
+
<spk-progress-bar class="indeterminate primary"></spk-progress-bar>
|
|
34
|
+
} @else if (backups().length) {
|
|
35
|
+
@for (backup of backups(); track $index) {
|
|
36
|
+
<spk-card class="type-a">
|
|
37
|
+
<img src="/assets/images/folder.png" alt="" width="48px" />
|
|
38
|
+
|
|
39
|
+
<div class="text">
|
|
40
|
+
<h3>Restore {{ backup.Backup?.Name ?? 'Backup name missing' }}</h3>
|
|
41
|
+
<p>
|
|
42
|
+
{{ backup.Backup?.Metadata?.['SourceSizeString'] ?? '-' }} /
|
|
43
|
+
{{ backup.Backup?.Metadata?.['BackupListCount'] ?? '-' }}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<button spk-button [routerLink]="['/restore-flow', backup.Backup?.ID]">
|
|
48
|
+
Restore
|
|
49
|
+
<spk-icon>arrow-right</spk-icon>
|
|
50
|
+
</button>
|
|
51
|
+
</spk-card>
|
|
52
|
+
}
|
|
53
|
+
} @else {
|
|
54
|
+
No backups found
|
|
55
|
+
}
|
|
56
|
+
</section>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@use '@sparkle-ui/core/styles/helpers' as *;
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
h2 {
|
|
8
|
+
font: var(--title-30);
|
|
9
|
+
text-align: center;
|
|
10
|
+
padding: p2r(32);
|
|
11
|
+
}
|
|
12
|
+
section {
|
|
13
|
+
max-width: p2r(700);
|
|
14
|
+
margin: 0 auto;
|
|
15
|
+
padding: p2r(32 0);
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
gap: p2r(16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
spk-divider {
|
|
22
|
+
color: var(--base-level-60);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
spk-card {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: p2r(16);
|
|
29
|
+
--card-padding: #{p2r(32)};
|
|
30
|
+
|
|
31
|
+
.text {
|
|
32
|
+
flex: 1 0;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: p2r(4);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
2
|
+
import { RouterLink } from '@angular/router';
|
|
3
|
+
import {
|
|
4
|
+
SparkleButtonComponent,
|
|
5
|
+
SparkleCardComponent,
|
|
6
|
+
SparkleDividerComponent,
|
|
7
|
+
SparkleIconComponent,
|
|
8
|
+
SparkleProgressBarComponent,
|
|
9
|
+
} from '@sparkle-ui/core';
|
|
10
|
+
import StatusBarComponent from '../core/components/status-bar/status-bar.component';
|
|
11
|
+
import { BackupAndScheduleOutputDto, DuplicatiServerService } from '../core/openapi';
|
|
12
|
+
import { BackupsState } from '../core/states/backups.state';
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'app-restore',
|
|
16
|
+
standalone: true,
|
|
17
|
+
imports: [
|
|
18
|
+
StatusBarComponent,
|
|
19
|
+
SparkleCardComponent,
|
|
20
|
+
SparkleButtonComponent,
|
|
21
|
+
SparkleDividerComponent,
|
|
22
|
+
SparkleIconComponent,
|
|
23
|
+
SparkleProgressBarComponent,
|
|
24
|
+
RouterLink,
|
|
25
|
+
],
|
|
26
|
+
templateUrl: './restore.component.html',
|
|
27
|
+
styleUrl: './restore.component.scss',
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
})
|
|
30
|
+
export default class RestoreComponent {
|
|
31
|
+
#backupsState = inject(BackupsState);
|
|
32
|
+
#dupServer = inject(DuplicatiServerService);
|
|
33
|
+
|
|
34
|
+
backups = this.#backupsState.backups;
|
|
35
|
+
backupsLoading = this.#backupsState.backupsLoading;
|
|
36
|
+
|
|
37
|
+
ngOnInit() {
|
|
38
|
+
this.#backupsState.getBackups();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
restoreNow(backup: BackupAndScheduleOutputDto) {
|
|
42
|
+
this.#dupServer.postApiV1BackupByIdRestore({
|
|
43
|
+
id: backup.Backup?.ID!,
|
|
44
|
+
requestBody: {
|
|
45
|
+
// paths?: Array<(string)> | null;
|
|
46
|
+
// passphrase?: (string) | null;
|
|
47
|
+
// time?: (string) | null;
|
|
48
|
+
// restore_path?: (string) | null;
|
|
49
|
+
// overwrite?: (boolean) | null;
|
|
50
|
+
// permissions?: (boolean) | null;
|
|
51
|
+
// skip_metadata?: (boolean) | null;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<form [formGroup]="optionsForm" #formRef (submit)="submit()">
|
|
2
|
+
<h3 class="title-30">Restore options</h3>
|
|
3
|
+
|
|
4
|
+
<app-toggle-card [disallowToggle]="true">
|
|
5
|
+
<ng-container title>Where do you want to restore files from?</ng-container>
|
|
6
|
+
|
|
7
|
+
<spk-radio class="raised primary">
|
|
8
|
+
Original location
|
|
9
|
+
<input type="radio" name="restoreFrom" value="orignal" formControlName="restoreFrom" />
|
|
10
|
+
</spk-radio>
|
|
11
|
+
|
|
12
|
+
<spk-radio class="raised primary">
|
|
13
|
+
Pick location
|
|
14
|
+
<input type="radio" name="restoreFrom" value="pickLocation" formControlName="restoreFrom" />
|
|
15
|
+
</spk-radio>
|
|
16
|
+
|
|
17
|
+
<app-file-tree [disabled]="optionsFormSignal()?.restoreFrom !== 'pickLocation'">
|
|
18
|
+
<label for="path">Path</label>
|
|
19
|
+
<input type="text" id="path" formControlName="restoreFromPath" />
|
|
20
|
+
</app-file-tree>
|
|
21
|
+
</app-toggle-card>
|
|
22
|
+
|
|
23
|
+
<app-toggle-card [disallowToggle]="true">
|
|
24
|
+
<ng-container title>How do you want to handle existing files?</ng-container>
|
|
25
|
+
|
|
26
|
+
<spk-radio class="raised primary">
|
|
27
|
+
Overwrite
|
|
28
|
+
<input type="radio" name="handleExisting" value="overwrite" formControlName="handleExisting" />
|
|
29
|
+
</spk-radio>
|
|
30
|
+
|
|
31
|
+
<spk-radio class="raised primary">
|
|
32
|
+
Save different versions with timestamp in file name
|
|
33
|
+
<input type="radio" name="handleExisting" value="saveTimestamp" formControlName="handleExisting" />
|
|
34
|
+
</spk-radio>
|
|
35
|
+
</app-toggle-card>
|
|
36
|
+
|
|
37
|
+
<app-toggle-card>
|
|
38
|
+
<ng-container title>Permissions</ng-container>
|
|
39
|
+
|
|
40
|
+
<spk-toggle class="primary raised">
|
|
41
|
+
Restore read/write permissions
|
|
42
|
+
<input type="checkbox" formControlName="permissions" />
|
|
43
|
+
</spk-toggle>
|
|
44
|
+
</app-toggle-card>
|
|
45
|
+
|
|
46
|
+
<div class="form-actions">
|
|
47
|
+
<button spk-button type="button" (click)="goBack()">
|
|
48
|
+
<spk-icon>arrow-left</spk-icon>
|
|
49
|
+
Go back
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<button spk-button class="raised primary" type="submit">
|
|
53
|
+
<spk-icon>arrow-right</spk-icon>
|
|
54
|
+
Submit
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</form>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
2
|
+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
3
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
4
|
+
import {
|
|
5
|
+
SparkleButtonComponent,
|
|
6
|
+
SparkleIconComponent,
|
|
7
|
+
SparkleRadioComponent,
|
|
8
|
+
SparkleToggleComponent,
|
|
9
|
+
} from '@sparkle-ui/core';
|
|
10
|
+
import FileTreeComponent from '../../core/components/file-tree/file-tree.component';
|
|
11
|
+
import ToggleCardComponent from '../../core/components/toggle-card/toggle-card.component';
|
|
12
|
+
import { RestoreFlowState } from '../restore-flow.state';
|
|
13
|
+
|
|
14
|
+
const fb = new FormBuilder();
|
|
15
|
+
|
|
16
|
+
export const createRestoreOptionsForm = () => {
|
|
17
|
+
return fb.group({
|
|
18
|
+
restoreFrom: fb.control<'orignal' | 'pickLocation'>('orignal'),
|
|
19
|
+
restoreFromPath: fb.control<string>(''),
|
|
20
|
+
handleExisting: fb.control<'overwrite' | 'saveTimestamp'>('overwrite'),
|
|
21
|
+
permissions: fb.control<boolean>(false),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-options',
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [
|
|
29
|
+
ReactiveFormsModule,
|
|
30
|
+
|
|
31
|
+
ToggleCardComponent,
|
|
32
|
+
FileTreeComponent,
|
|
33
|
+
|
|
34
|
+
SparkleToggleComponent,
|
|
35
|
+
SparkleRadioComponent,
|
|
36
|
+
SparkleButtonComponent,
|
|
37
|
+
SparkleIconComponent,
|
|
38
|
+
],
|
|
39
|
+
templateUrl: './options.component.html',
|
|
40
|
+
styleUrl: './options.component.scss',
|
|
41
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
42
|
+
})
|
|
43
|
+
export default class OptionsComponent {
|
|
44
|
+
#restoreFlowState = inject(RestoreFlowState);
|
|
45
|
+
#router = inject(Router);
|
|
46
|
+
#route = inject(ActivatedRoute);
|
|
47
|
+
|
|
48
|
+
optionsForm = this.#restoreFlowState.optionsForm;
|
|
49
|
+
optionsFormSignal = this.#restoreFlowState.optionsFormSignal;
|
|
50
|
+
|
|
51
|
+
goBack() {
|
|
52
|
+
this.#router.navigate(['select-files'], { relativeTo: this.#route.parent });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
submit() {
|
|
56
|
+
this.#restoreFlowState.submit();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<spk-stepper>
|
|
2
|
+
<div spk-step routerLinkActive="active" [routerLink]="['/restore-flow', backupId(), 'select-files']">
|
|
3
|
+
<spk-radio />
|
|
4
|
+
Select files
|
|
5
|
+
</div>
|
|
6
|
+
<div spk-step routerLinkActive="active" [routerLink]="['/restore-flow', backupId(), 'options']">
|
|
7
|
+
<spk-radio />
|
|
8
|
+
Options
|
|
9
|
+
</div>
|
|
10
|
+
</spk-stepper>
|
|
11
|
+
|
|
12
|
+
<section>
|
|
13
|
+
<router-outlet />
|
|
14
|
+
</section>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
2
|
+
import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
3
|
+
import { SparkleRadioComponent, SparkleStepperComponent } from '@sparkle-ui/core';
|
|
4
|
+
import { RestoreFlowState } from './restore-flow.state';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-restore-flow',
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [SparkleStepperComponent, SparkleRadioComponent, RouterOutlet, RouterLink, RouterLinkActive],
|
|
10
|
+
templateUrl: './restore-flow.component.html',
|
|
11
|
+
styleUrl: './restore-flow.component.scss',
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13
|
+
})
|
|
14
|
+
export default class RestoreFlowComponent {
|
|
15
|
+
#route = inject(ActivatedRoute);
|
|
16
|
+
#restoreFlowState = inject(RestoreFlowState);
|
|
17
|
+
|
|
18
|
+
backupId = this.#restoreFlowState.backupId;
|
|
19
|
+
|
|
20
|
+
ngOnInit() {
|
|
21
|
+
this.#restoreFlowState.init(this.#route.snapshot.params['id']);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { inject, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { Router } from '@angular/router';
|
|
4
|
+
import { finalize, forkJoin, take } from 'rxjs';
|
|
5
|
+
import { DuplicatiServerService, GetBackupResultDto, IListResultFileset } from '../core/openapi';
|
|
6
|
+
import { createRestoreOptionsForm } from './options/options.component';
|
|
7
|
+
import { createRestoreSelectFilesForm } from './select-files/select-files.component';
|
|
8
|
+
|
|
9
|
+
@Injectable({
|
|
10
|
+
providedIn: 'root',
|
|
11
|
+
})
|
|
12
|
+
export class RestoreFlowState {
|
|
13
|
+
#router = inject(Router);
|
|
14
|
+
#dupServer = inject(DuplicatiServerService);
|
|
15
|
+
|
|
16
|
+
backupId = signal<'string' | null>(null);
|
|
17
|
+
backup = signal<GetBackupResultDto | null>(null);
|
|
18
|
+
selectFilesForm = createRestoreSelectFilesForm();
|
|
19
|
+
optionsForm = createRestoreOptionsForm();
|
|
20
|
+
optionsFormSignal = toSignal(this.optionsForm.valueChanges);
|
|
21
|
+
selectFilesFormSignal = toSignal(this.selectFilesForm.valueChanges);
|
|
22
|
+
versionOptionsLoading = signal(false);
|
|
23
|
+
versionOptions = signal<IListResultFileset[]>([]);
|
|
24
|
+
isSubmitting = signal(false);
|
|
25
|
+
|
|
26
|
+
init(id: 'string') {
|
|
27
|
+
this.backupId.set(id);
|
|
28
|
+
this.#getBackupData(id, true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// restoreFrom: fb.control<'orignal' | 'pickLocation'>('orignal'),
|
|
32
|
+
// restoreFromPath: fb.control<string>(''),
|
|
33
|
+
// handleExisting: fb.control<'overwrite' | 'saveTimestamp'>('overwrite'),
|
|
34
|
+
// permissions: fb.control<boolean>(false),
|
|
35
|
+
// {
|
|
36
|
+
// "paths": [
|
|
37
|
+
// "string"
|
|
38
|
+
// ],
|
|
39
|
+
// "passphrase": "string",
|
|
40
|
+
// "time": "string",
|
|
41
|
+
// "restore_path": "string",
|
|
42
|
+
// "overwrite": true,
|
|
43
|
+
// "permissions": true,
|
|
44
|
+
// "skip_metadata": true
|
|
45
|
+
// }
|
|
46
|
+
submit() {
|
|
47
|
+
this.isSubmitting.set(true);
|
|
48
|
+
|
|
49
|
+
const id = this.backupId();
|
|
50
|
+
const optionsValue = this.optionsForm.value;
|
|
51
|
+
const selectFilesFormValue = this.selectFilesForm.value;
|
|
52
|
+
const selectedOption = this.versionOptions()?.find((x) => x.Version === selectFilesFormValue.selectedOption);
|
|
53
|
+
|
|
54
|
+
this.#dupServer
|
|
55
|
+
.postApiV1BackupByIdRestore({
|
|
56
|
+
id: id ?? '',
|
|
57
|
+
requestBody: {
|
|
58
|
+
paths: selectFilesFormValue.filesToRestore?.split('\0').map((x) => `${x}*`) ?? [],
|
|
59
|
+
passphrase: selectFilesFormValue.passphrase ?? null,
|
|
60
|
+
time: selectedOption?.Time,
|
|
61
|
+
restore_path: optionsValue.restoreFromPath,
|
|
62
|
+
overwrite: optionsValue.handleExisting === 'overwrite',
|
|
63
|
+
permissions: optionsValue.permissions,
|
|
64
|
+
skip_metadata: true,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
.pipe(finalize(() => this.isSubmitting.set(false)))
|
|
68
|
+
.subscribe({
|
|
69
|
+
next: () => {
|
|
70
|
+
this.exit();
|
|
71
|
+
},
|
|
72
|
+
error: (err) => {
|
|
73
|
+
console.error('restore error', err);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
exit() {
|
|
79
|
+
// TODOS
|
|
80
|
+
// - Are you sure dialog
|
|
81
|
+
this.#resetAllForms();
|
|
82
|
+
this.#router.navigate(['/']);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getVersionOptions() {
|
|
86
|
+
this.versionOptions();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#getBackupData(id: string, setFirstToForm = false) {
|
|
90
|
+
this.versionOptionsLoading.set(true);
|
|
91
|
+
|
|
92
|
+
forkJoin([
|
|
93
|
+
this.#dupServer.getApiV1BackupById({
|
|
94
|
+
id,
|
|
95
|
+
}),
|
|
96
|
+
this.#dupServer.getApiV1BackupByIdFilesets({
|
|
97
|
+
id,
|
|
98
|
+
}),
|
|
99
|
+
])
|
|
100
|
+
.pipe(
|
|
101
|
+
take(1),
|
|
102
|
+
finalize(() => this.versionOptionsLoading.set(false))
|
|
103
|
+
)
|
|
104
|
+
.subscribe({
|
|
105
|
+
next: ([backup, filesets]) => {
|
|
106
|
+
this.versionOptions.set(filesets);
|
|
107
|
+
this.backup.set(backup);
|
|
108
|
+
|
|
109
|
+
if (setFirstToForm) {
|
|
110
|
+
this.selectFilesForm.controls.selectedOption.setValue(filesets[0].Version as number);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#resetAllForms() {
|
|
117
|
+
this.selectFilesForm.reset();
|
|
118
|
+
this.optionsForm.reset();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<form [formGroup]="selectFilesForm" #formRef (submit)="next()">
|
|
2
|
+
<h3 class="title-30">Select files</h3>
|
|
3
|
+
|
|
4
|
+
@if (versionOptionsLoading()) {
|
|
5
|
+
<spk-progress-bar class="indeterminate primary"></spk-progress-bar>
|
|
6
|
+
} @else {
|
|
7
|
+
<spk-select [displayFn]="displayFn()">
|
|
8
|
+
<input type="number" placeholder="Select an option..." formControlName="selectedOption" />
|
|
9
|
+
|
|
10
|
+
<ng-container options>
|
|
11
|
+
@for (option of versionOptions(); track $index) {
|
|
12
|
+
<option [value]="option.Version">{{ option.Version }}: {{ option.Time | date: 'd MMM y, h:mm' }}</option>
|
|
13
|
+
}
|
|
14
|
+
</ng-container>
|
|
15
|
+
</spk-select>
|
|
16
|
+
|
|
17
|
+
@if (rootPathLoaded()) {
|
|
18
|
+
<app-file-tree
|
|
19
|
+
[rootPath]="rootPath()"
|
|
20
|
+
[backupSettings]="backupSettings()"
|
|
21
|
+
[multiSelect]="true"
|
|
22
|
+
[showFiles]="true">
|
|
23
|
+
<label for="path">Path</label>
|
|
24
|
+
<input type="text" id="path" formControlName="filesToRestore" />
|
|
25
|
+
</app-file-tree>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
<!-- <pre>{{ selectFilesFormSignal() | json }}</pre> -->
|
|
30
|
+
|
|
31
|
+
<div class="form-actions">
|
|
32
|
+
<button spk-button type="button" (click)="exit()">
|
|
33
|
+
<spk-icon>arrow-left</spk-icon>
|
|
34
|
+
Exit
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
<button spk-button class="raised primary" type="submit">
|
|
38
|
+
<spk-icon>arrow-right</spk-icon>
|
|
39
|
+
Continue
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</form>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { DatePipe, JsonPipe } from '@angular/common';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
|
3
|
+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
5
|
+
import {
|
|
6
|
+
SparkleButtonComponent,
|
|
7
|
+
SparkleIconComponent,
|
|
8
|
+
SparkleProgressBarComponent,
|
|
9
|
+
SparkleSelectComponent,
|
|
10
|
+
} from '@sparkle-ui/core';
|
|
11
|
+
import { derivedFrom } from 'ngxtension/derived-from';
|
|
12
|
+
import { map, of, pipe, startWith, switchMap } from 'rxjs';
|
|
13
|
+
import FileTreeComponent from '../../core/components/file-tree/file-tree.component';
|
|
14
|
+
import { DuplicatiServerService } from '../../core/openapi';
|
|
15
|
+
import { RestoreFlowState } from '../restore-flow.state';
|
|
16
|
+
|
|
17
|
+
const fb = new FormBuilder();
|
|
18
|
+
|
|
19
|
+
export const createRestoreSelectFilesForm = () => {
|
|
20
|
+
return fb.group({
|
|
21
|
+
filesToRestore: fb.control<string>(''),
|
|
22
|
+
selectedOption: fb.control<number | null>(null),
|
|
23
|
+
passphrase: fb.control<string | null>(null),
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
@Component({
|
|
28
|
+
selector: 'app-select-files',
|
|
29
|
+
standalone: true,
|
|
30
|
+
imports: [
|
|
31
|
+
ReactiveFormsModule,
|
|
32
|
+
DatePipe,
|
|
33
|
+
JsonPipe,
|
|
34
|
+
|
|
35
|
+
FileTreeComponent,
|
|
36
|
+
|
|
37
|
+
SparkleButtonComponent,
|
|
38
|
+
SparkleIconComponent,
|
|
39
|
+
SparkleSelectComponent,
|
|
40
|
+
SparkleProgressBarComponent,
|
|
41
|
+
],
|
|
42
|
+
templateUrl: './select-files.component.html',
|
|
43
|
+
styleUrl: './select-files.component.scss',
|
|
44
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
45
|
+
providers: [DatePipe],
|
|
46
|
+
})
|
|
47
|
+
export default class SelectFilesComponent {
|
|
48
|
+
#dupServer = inject(DuplicatiServerService);
|
|
49
|
+
#restoreFlowState = inject(RestoreFlowState);
|
|
50
|
+
#router = inject(Router);
|
|
51
|
+
#route = inject(ActivatedRoute);
|
|
52
|
+
#datePipe = inject(DatePipe);
|
|
53
|
+
|
|
54
|
+
selectFilesForm = this.#restoreFlowState.selectFilesForm;
|
|
55
|
+
selectFilesFormSignal = this.#restoreFlowState.selectFilesFormSignal;
|
|
56
|
+
backupId = this.#restoreFlowState.backupId;
|
|
57
|
+
versionOptionsLoading = this.#restoreFlowState.versionOptionsLoading;
|
|
58
|
+
versionOptions = this.#restoreFlowState.versionOptions;
|
|
59
|
+
backupSettings = computed(() => {
|
|
60
|
+
const id = this.backupId();
|
|
61
|
+
const selectedOption = this.selectFilesFormSignal()?.selectedOption;
|
|
62
|
+
|
|
63
|
+
if (!(typeof selectedOption === 'number') || !id) return null;
|
|
64
|
+
|
|
65
|
+
const option = this.versionOptions()?.find((x) => x.Version === selectedOption);
|
|
66
|
+
const time = option?.Time ?? null;
|
|
67
|
+
|
|
68
|
+
if (!time) return null;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
time,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
rootPathLoaded = signal(false);
|
|
77
|
+
rootPath = derivedFrom(
|
|
78
|
+
[this.selectFilesFormSignal, this.backupId],
|
|
79
|
+
pipe(
|
|
80
|
+
startWith([null, null]),
|
|
81
|
+
switchMap(([selectFilesFormSignal, backupId]) => {
|
|
82
|
+
const selectedOption = selectFilesFormSignal?.selectedOption;
|
|
83
|
+
const id = backupId;
|
|
84
|
+
|
|
85
|
+
if (!(typeof selectedOption === 'number') || !id) return of('/');
|
|
86
|
+
|
|
87
|
+
const option = this.versionOptions()?.find((x) => x.Version === selectedOption);
|
|
88
|
+
|
|
89
|
+
return this.#dupServer
|
|
90
|
+
.getApiV1BackupByIdFiles({
|
|
91
|
+
id,
|
|
92
|
+
time: option?.Time ?? '',
|
|
93
|
+
prefixOnly: false,
|
|
94
|
+
folderContents: true,
|
|
95
|
+
})
|
|
96
|
+
.pipe(
|
|
97
|
+
map((x) => {
|
|
98
|
+
this.rootPathLoaded.set(true);
|
|
99
|
+
|
|
100
|
+
return (x['Files'] as any)[0].Path;
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
displayFn() {
|
|
108
|
+
const options = this.versionOptions();
|
|
109
|
+
|
|
110
|
+
return (val: string) => {
|
|
111
|
+
if (!val) return '';
|
|
112
|
+
|
|
113
|
+
const item = options?.find((x) => x.Version?.toString() === val);
|
|
114
|
+
|
|
115
|
+
if (!item) {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
return `${item.Version}: ${this.#datePipe.transform(item.Time, 'd MMM y, h:mm')}`;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
next() {
|
|
123
|
+
this.#router.navigate(['options'], { relativeTo: this.#route.parent });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
exit() {
|
|
127
|
+
this.#restoreFlowState.exit();
|
|
128
|
+
}
|
|
129
|
+
}
|