@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.
Files changed (152) hide show
  1. package/.dockerignore +13 -0
  2. package/.github/workflows/npm-publish.yml +59 -0
  3. package/.prettierignore +13 -0
  4. package/.prettierrc +12 -0
  5. package/.vscode/extensions.json +4 -0
  6. package/.vscode/launch.json +13 -0
  7. package/.vscode/settings.json +8 -0
  8. package/.vscode/tasks.json +24 -0
  9. package/Dockerfile +38 -0
  10. package/LICENSE +21 -0
  11. package/README.md +27 -0
  12. package/angular.json +98 -0
  13. package/bun.lockb +0 -0
  14. package/openapi-ts.config.ts +13 -0
  15. package/package.json +46 -0
  16. package/scripts/generate-package-json.ts +15 -0
  17. package/src/app/about/about.component.html +15 -0
  18. package/src/app/about/about.component.scss +19 -0
  19. package/src/app/about/about.component.ts +13 -0
  20. package/src/app/about/changelog/changelog.component.html +3 -0
  21. package/src/app/about/changelog/changelog.component.scss +7 -0
  22. package/src/app/about/changelog/changelog.component.ts +18 -0
  23. package/src/app/about/general/general.component.html +9 -0
  24. package/src/app/about/general/general.component.scss +23 -0
  25. package/src/app/about/general/general.component.ts +30 -0
  26. package/src/app/about/libraries/libraries.component.html +3 -0
  27. package/src/app/about/libraries/libraries.component.scss +7 -0
  28. package/src/app/about/libraries/libraries.component.ts +18 -0
  29. package/src/app/about/logs/logs.component.html +5 -0
  30. package/src/app/about/logs/logs.component.scss +7 -0
  31. package/src/app/about/logs/logs.component.ts +18 -0
  32. package/src/app/about/system-info/system-info.component.html +3 -0
  33. package/src/app/about/system-info/system-info.component.scss +7 -0
  34. package/src/app/about/system-info/system-info.component.ts +17 -0
  35. package/src/app/add-backup/add-backup.component.html +29 -0
  36. package/src/app/add-backup/add-backup.component.scss +38 -0
  37. package/src/app/add-backup/add-backup.component.ts +14 -0
  38. package/src/app/app.component.ts +35 -0
  39. package/src/app/app.config.ts +23 -0
  40. package/src/app/app.routes.ts +121 -0
  41. package/src/app/core/components/file-tree/file-tree.component.html +87 -0
  42. package/src/app/core/components/file-tree/file-tree.component.scss +80 -0
  43. package/src/app/core/components/file-tree/file-tree.component.ts +485 -0
  44. package/src/app/core/components/logo/logo.component.html +22 -0
  45. package/src/app/core/components/logo/logo.component.scss +12 -0
  46. package/src/app/core/components/logo/logo.component.ts +11 -0
  47. package/src/app/core/components/status-bar/status-bar.component.html +38 -0
  48. package/src/app/core/components/status-bar/status-bar.component.scss +27 -0
  49. package/src/app/core/components/status-bar/status-bar.component.ts +33 -0
  50. package/src/app/core/components/status-bar/status-bar.state.ts +182 -0
  51. package/src/app/core/components/toggle-card/toggle-card.component.html +11 -0
  52. package/src/app/core/components/toggle-card/toggle-card.component.scss +49 -0
  53. package/src/app/core/components/toggle-card/toggle-card.component.ts +28 -0
  54. package/src/app/core/interceptors/http.interceptor.ts +69 -0
  55. package/src/app/core/interceptors/websocket.interceptor.ts +66 -0
  56. package/src/app/core/openapi/core/ApiError.ts +21 -0
  57. package/src/app/core/openapi/core/ApiRequestOptions.ts +21 -0
  58. package/src/app/core/openapi/core/ApiResult.ts +7 -0
  59. package/src/app/core/openapi/core/OpenAPI.ts +55 -0
  60. package/src/app/core/openapi/core/request.ts +337 -0
  61. package/src/app/core/openapi/index.ts +6 -0
  62. package/src/app/core/openapi/schemas.gen.ts +3611 -0
  63. package/src/app/core/openapi/services.gen.ts +1460 -0
  64. package/src/app/core/openapi/types.gen.ts +1510 -0
  65. package/src/app/core/pipes/duration.pipe.ts +27 -0
  66. package/src/app/core/pipes/relative-time.pipe.ts +14 -0
  67. package/src/app/core/providers/dayjs.ts +16 -0
  68. package/src/app/core/services/dict.en.ts +0 -0
  69. package/src/app/core/services/dictionary.service.ts +12 -0
  70. package/src/app/core/services/localstorage.token.ts +82 -0
  71. package/src/app/core/services/password-generator.service.ts +39 -0
  72. package/src/app/core/services/relay-websocket.service.ts +268 -0
  73. package/src/app/core/services/timespan-literals.service.ts +25 -0
  74. package/src/app/core/states/app-auth.state.ts +87 -0
  75. package/src/app/core/states/backups.state.ts +71 -0
  76. package/src/app/core/states/relayconfig.state.ts +23 -0
  77. package/src/app/core/states/sysinfo.state.ts +31 -0
  78. package/src/app/core/states/tasks.state.ts +34 -0
  79. package/src/app/core/types/subscribed.ts +7 -0
  80. package/src/app/core/validators/custom.validators.ts +120 -0
  81. package/src/app/create-edit-backup/create-edit-backup.component.html +28 -0
  82. package/src/app/create-edit-backup/create-edit-backup.component.scss +15 -0
  83. package/src/app/create-edit-backup/create-edit-backup.component.ts +33 -0
  84. package/src/app/create-edit-backup/create-edit-backup.state.ts +582 -0
  85. package/src/app/create-edit-backup/destination/destination.component.html +367 -0
  86. package/src/app/create-edit-backup/destination/destination.component.scss +85 -0
  87. package/src/app/create-edit-backup/destination/destination.component.ts +249 -0
  88. package/src/app/create-edit-backup/destination/destination.config.ts +103 -0
  89. package/src/app/create-edit-backup/destination/destination.mapper.ts +177 -0
  90. package/src/app/create-edit-backup/general/general.component.html +108 -0
  91. package/src/app/create-edit-backup/general/general.component.scss +34 -0
  92. package/src/app/create-edit-backup/general/general.component.ts +136 -0
  93. package/src/app/create-edit-backup/options/options.component.html +95 -0
  94. package/src/app/create-edit-backup/options/options.component.scss +42 -0
  95. package/src/app/create-edit-backup/options/options.component.ts +113 -0
  96. package/src/app/create-edit-backup/schedule/schedule.component.html +102 -0
  97. package/src/app/create-edit-backup/schedule/schedule.component.scss +38 -0
  98. package/src/app/create-edit-backup/schedule/schedule.component.ts +137 -0
  99. package/src/app/create-edit-backup/source-data/source-data.component.html +93 -0
  100. package/src/app/create-edit-backup/source-data/source-data.component.scss +41 -0
  101. package/src/app/create-edit-backup/source-data/source-data.component.ts +114 -0
  102. package/src/app/home/backup.const.ts +167 -0
  103. package/src/app/home/home.component.html +98 -0
  104. package/src/app/home/home.component.scss +88 -0
  105. package/src/app/home/home.component.ts +54 -0
  106. package/src/app/layout/layout.component.html +64 -0
  107. package/src/app/layout/layout.component.scss +24 -0
  108. package/src/app/layout/layout.component.ts +51 -0
  109. package/src/app/layout/layout.state.ts +70 -0
  110. package/src/app/login/login.component.html +12 -0
  111. package/src/app/login/login.component.scss +25 -0
  112. package/src/app/login/login.component.ts +34 -0
  113. package/src/app/logout/logout.component.html +1 -0
  114. package/src/app/logout/logout.component.scss +3 -0
  115. package/src/app/logout/logout.component.ts +18 -0
  116. package/src/app/restore/restore.component.html +56 -0
  117. package/src/app/restore/restore.component.scss +37 -0
  118. package/src/app/restore/restore.component.ts +55 -0
  119. package/src/app/restore-flow/options/options.component.html +57 -0
  120. package/src/app/restore-flow/options/options.component.scss +16 -0
  121. package/src/app/restore-flow/options/options.component.ts +58 -0
  122. package/src/app/restore-flow/restore-flow.component.html +14 -0
  123. package/src/app/restore-flow/restore-flow.component.scss +15 -0
  124. package/src/app/restore-flow/restore-flow.component.ts +23 -0
  125. package/src/app/restore-flow/restore-flow.state.ts +120 -0
  126. package/src/app/restore-flow/select-files/select-files.component.html +42 -0
  127. package/src/app/restore-flow/select-files/select-files.component.scss +16 -0
  128. package/src/app/restore-flow/select-files/select-files.component.ts +129 -0
  129. package/src/app/settings/settings.component.html +256 -0
  130. package/src/app/settings/settings.component.scss +106 -0
  131. package/src/app/settings/settings.component.ts +475 -0
  132. package/src/assets/duplicati-logo.png +0 -0
  133. package/src/assets/duplicati-logo.svg +10 -0
  134. package/src/assets/favicon.ico +0 -0
  135. package/src/assets/images/backup.png +0 -0
  136. package/src/assets/images/dark-theme.png +0 -0
  137. package/src/assets/images/folder.png +0 -0
  138. package/src/assets/images/light-theme.png +0 -0
  139. package/src/assets/images/new-backup.png +0 -0
  140. package/src/assets/images/restore.png +0 -0
  141. package/src/assets/images/system-theme.png +0 -0
  142. package/src/assets/spk.css +33 -0
  143. package/src/assets/spk.woff2 +0 -0
  144. package/src/environments/environment-token.ts +4 -0
  145. package/src/environments/environment.prod.ts +6 -0
  146. package/src/environments/environment.ts +6 -0
  147. package/src/index.html +15 -0
  148. package/src/main.ts +6 -0
  149. package/src/proxy.conf.mjs +6 -0
  150. package/src/styles.scss +41 -0
  151. package/tsconfig.app.json +15 -0
  152. package/tsconfig.json +30 -0
@@ -0,0 +1,27 @@
1
+ import { inject, Pipe, type PipeTransform } from '@angular/core';
2
+ import { DAYJS } from '../providers/dayjs';
3
+
4
+ @Pipe({
5
+ name: 'durationFormat',
6
+ standalone: true,
7
+ })
8
+ export class DurationFormatPipe implements PipeTransform {
9
+ #dayjs = inject(DAYJS);
10
+
11
+ transform(value: string | undefined | null, ...args: unknown[]): unknown {
12
+ if (!value) return '';
13
+
14
+ const _value = value.split('.')[0];
15
+ const hours = parseInt(_value.split(':')[0]) ?? 0;
16
+ const minutes = parseInt(_value.split(':')[1]) ?? 0;
17
+ const seconds = parseInt(_value.split(':')[2]) ?? 0;
18
+
19
+ return this.#dayjs
20
+ .duration({
21
+ hours,
22
+ minutes,
23
+ seconds,
24
+ })
25
+ .humanize();
26
+ }
27
+ }
@@ -0,0 +1,14 @@
1
+ import { inject, Pipe, type PipeTransform } from '@angular/core';
2
+ import { DAYJS } from '../providers/dayjs';
3
+
4
+ @Pipe({
5
+ name: 'appRelativeTime',
6
+ standalone: true,
7
+ })
8
+ export class RelativeTimePipe implements PipeTransform {
9
+ #dayjs = inject(DAYJS);
10
+
11
+ transform(value: string, ...args: unknown[]): unknown {
12
+ return this.#dayjs(value).fromNow();
13
+ }
14
+ }
@@ -0,0 +1,16 @@
1
+ import { InjectionToken, Provider } from '@angular/core';
2
+ import dayjs from 'dayjs';
3
+ import duration from 'dayjs/plugin/duration';
4
+ import relativeTime from 'dayjs/plugin/relativeTime';
5
+
6
+ export const DAYJS = new InjectionToken<typeof dayjs>('Dayjs');
7
+
8
+ export const DayJsProvider: Provider = {
9
+ provide: DAYJS,
10
+ useFactory: () => {
11
+ dayjs.extend(relativeTime);
12
+ dayjs.extend(duration);
13
+
14
+ return dayjs;
15
+ },
16
+ };
File without changes
@@ -0,0 +1,12 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+
3
+ type Dictionary = {};
4
+
5
+ @Injectable({
6
+ providedIn: 'root',
7
+ })
8
+ export class DictionaryService {
9
+ #activeDictionary = signal(null);
10
+
11
+ activeDictionary = this.#activeDictionary.asReadonly();
12
+ }
@@ -0,0 +1,82 @@
1
+ import { isPlatformBrowser, isPlatformServer } from '@angular/common';
2
+ import { Injectable, PLATFORM_ID, inject } from '@angular/core';
3
+
4
+ @Injectable()
5
+ export class LOCALSTORAGE implements Storage {
6
+ private platformId = inject(PLATFORM_ID);
7
+ private STORAGE_VERSION = 'v1';
8
+ private APP_NAME = 'duplicati';
9
+
10
+ getItemParsed<T>(key: string, presistedThroughClear = false): T | null {
11
+ if (isPlatformServer(this.platformId)) return null;
12
+
13
+ return JSON.parse(
14
+ localStorage.getItem(
15
+ `${this.STORAGE_VERSION}:${presistedThroughClear ? 'presist-duplicati' : this.APP_NAME}:${key}`
16
+ ) ?? 'null'
17
+ );
18
+ }
19
+
20
+ setItemParsed<T>(key: string, value: T, presistThroughClear = false): void {
21
+ if (isPlatformServer(this.platformId)) return;
22
+
23
+ return localStorage.setItem(
24
+ `${this.STORAGE_VERSION}:${presistThroughClear ? 'presist-duplicati' : this.APP_NAME}:${key}`,
25
+ JSON.stringify(value)
26
+ );
27
+ }
28
+
29
+ get length(): number {
30
+ return isPlatformBrowser(this.platformId) ? localStorage.length : 0;
31
+ }
32
+
33
+ getItem(key: string): string | null {
34
+ if (isPlatformServer(this.platformId)) return null;
35
+
36
+ return localStorage.getItem(`${this.STORAGE_VERSION}:${this.APP_NAME}:${key}`) ?? null;
37
+ }
38
+
39
+ setItem(key: string, value: string): void {
40
+ if (isPlatformServer(this.platformId)) return;
41
+
42
+ return localStorage.setItem(`${this.STORAGE_VERSION}:${this.APP_NAME}:${key}`, value);
43
+ }
44
+
45
+ removeItem(key: string): void {
46
+ if (isPlatformServer(this.platformId)) return;
47
+
48
+ localStorage.removeItem(`${this.STORAGE_VERSION}:${this.APP_NAME}:${key}`);
49
+ }
50
+
51
+ clearAllNotCurrentVersion(): void {
52
+ if (isPlatformServer(this.platformId)) return;
53
+
54
+ for (var key in localStorage) {
55
+ if (!key.startsWith(this.STORAGE_VERSION)) {
56
+ localStorage.removeItem(key);
57
+ }
58
+ }
59
+ }
60
+
61
+ clearAll(): void {
62
+ if (isPlatformServer(this.platformId)) return;
63
+
64
+ localStorage.clear();
65
+ }
66
+
67
+ clear(): void {
68
+ if (isPlatformServer(this.platformId)) return;
69
+
70
+ for (var key in localStorage) {
71
+ if (key.includes(this.APP_NAME) && !key.includes(`presist-${this.APP_NAME}`)) {
72
+ localStorage.removeItem(key);
73
+ }
74
+ }
75
+ }
76
+
77
+ key(index: number): string | null {
78
+ if (isPlatformServer(this.platformId)) return null;
79
+
80
+ return localStorage.key(index);
81
+ }
82
+ }
@@ -0,0 +1,39 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ const DEFAULT_PASSWORD_PATTERN = /[a-zA-Z0-9_\-\+\.]/;
4
+
5
+ @Injectable({
6
+ providedIn: 'root',
7
+ })
8
+ export class PasswordGeneratorService {
9
+ generate(length: number, pattern = DEFAULT_PASSWORD_PATTERN) {
10
+ const _self = this;
11
+
12
+ return Array.apply(null, { length: length } as any)
13
+ .map(() => {
14
+ let result;
15
+ while (true) {
16
+ result = String.fromCharCode(_self.#getRandomByte());
17
+
18
+ if (pattern.test(result)) {
19
+ return result;
20
+ }
21
+ }
22
+ }, this)
23
+ .join('');
24
+ }
25
+
26
+ #getRandomByte() {
27
+ if (window.crypto && window.crypto.getRandomValues) {
28
+ const result = new Uint8Array(1);
29
+ window.crypto.getRandomValues(result);
30
+ return result[0];
31
+ } else if ((window as any).msCrypto && (window as any).msCrypto.getRandomValues) {
32
+ const result = new Uint8Array(1);
33
+ (window as any).msCrypto.getRandomValues(result);
34
+ return result[0];
35
+ } else {
36
+ return Math.floor(Math.random() * 256);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,268 @@
1
+ import { effect, inject, Injectable, signal } from '@angular/core';
2
+ import { ENVIRONMENT_TOKEN } from '../../../environments/environment-token';
3
+
4
+ type SocketProtocolState = 'disconnected' | 'connect' | 'welcome' | 'authenticated' | 'error';
5
+
6
+ type MessageType = 'authportal' | 'list' | 'welcome' | 'command' | 'warning' | 'auth';
7
+
8
+ type MessageEnvelope = {
9
+ from: string;
10
+ to: string;
11
+ type: MessageType;
12
+ messageId: string;
13
+ payload: string | null;
14
+ };
15
+
16
+ type AuthRequestMessage = {
17
+ token: string;
18
+ publicKey: string;
19
+ clientVersion: string;
20
+ protocolVersion: number;
21
+ };
22
+
23
+ type AuthResponseMessage = {
24
+ accepted: boolean | null;
25
+ willReplaceToken: boolean | null;
26
+ newToken: string | null;
27
+ };
28
+
29
+ export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
30
+
31
+ type CommandRequest = {
32
+ method: RequestMethod;
33
+ path: string;
34
+ body: string | null;
35
+ headers: { [key: string]: string } | null;
36
+ };
37
+
38
+ type PromiseResolver = {
39
+ resolve: (value: CommandResponse | PromiseLike<CommandResponse>) => void;
40
+ reject: (reason?: any) => void;
41
+ timer: number;
42
+ };
43
+
44
+ export type CommandResponse = {
45
+ code: number;
46
+ body: string | null;
47
+ headers: { [key: string]: string } | null;
48
+ };
49
+
50
+ const ClientVersion = '1.0.0';
51
+ const ClientId = 'portal-proxy-client';
52
+ const ProtocolVersion = 1;
53
+
54
+ @Injectable({
55
+ providedIn: 'root',
56
+ })
57
+ export class RelayWebsocketService {
58
+ #MIN_POLL_INTERVAL = 1000;
59
+ #env = inject(ENVIRONMENT_TOKEN);
60
+ #wsState = signal<SocketProtocolState>('disconnected');
61
+ #reconnectInterval = signal<number | null>(this.#MIN_POLL_INTERVAL);
62
+ #isReconnecting = signal(false);
63
+ #isConnectedToMachineServer = signal(false);
64
+ #reconnectToken = signal<string | null>(null);
65
+
66
+ isConnectedToMachineServer = this.#isConnectedToMachineServer.asReadonly();
67
+ wsState = this.#wsState.asReadonly();
68
+
69
+ // TODO:
70
+ // - Auto reconnect
71
+
72
+ #queuedCommands: MessageEnvelope[] = [];
73
+ #pendingCommands: { [key: string]: PromiseResolver } = {};
74
+ #ws: WebSocket | null = null;
75
+ #textDecoder = new TextDecoder('utf-8');
76
+ #activeInterval: number | null = null;
77
+
78
+ #e = effect(() => {
79
+ const isReconnecting = this.#isReconnecting();
80
+ const reconnectInterval = this.#reconnectInterval();
81
+ const reconnectToken = this.#reconnectToken();
82
+
83
+ if (!reconnectToken || !isReconnecting || !reconnectInterval || reconnectInterval < this.#MIN_POLL_INTERVAL) {
84
+ this.#activeInterval && window.clearInterval(this.#activeInterval);
85
+ return;
86
+ }
87
+
88
+ this.#activeInterval = window.setInterval(() => {
89
+ this.connectToMachineServer(reconnectToken);
90
+ }, reconnectInterval);
91
+ });
92
+
93
+ utf8Atob(str: string) {
94
+ const decodedData = Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
95
+
96
+ const stringifiedJson = this.#textDecoder.decode(decodedData);
97
+ if (stringifiedJson)
98
+ return JSON.parse(stringifiedJson);
99
+ else
100
+ return null;
101
+ }
102
+
103
+ connectToMachineServer(token: string, options?: { reconnect?: boolean }) {
104
+ const reconnect = options?.reconnect ?? false;
105
+
106
+ if (this.#ws && this.wsState() === 'authenticated' && !reconnect) {
107
+ return;
108
+ }
109
+
110
+ if (this.#ws && this.wsState() !== 'authenticated') {
111
+ this.#ws.close();
112
+ }
113
+
114
+ this.#ws = new WebSocket(this.#env.machineServerUrl);
115
+
116
+ this.#ws.onopen = () => {
117
+ this.#wsState.set('connect');
118
+ this.#isConnectedToMachineServer.set(true);
119
+ this.#isReconnecting.set(false);
120
+ this.#reconnectToken.set(null);
121
+ };
122
+
123
+ this.#ws.onclose = () => {
124
+ // Should we reconnect automatically?
125
+ this.#wsState.set('disconnected');
126
+ this.#isConnectedToMachineServer.set(false);
127
+ this.#reconnectToken.set(token);
128
+ this.#isReconnecting.set(true);
129
+ };
130
+
131
+ this.#ws.onerror = () => {
132
+ this.#wsState.set('error');
133
+ };
134
+
135
+ this.#ws.onmessage = (event) => {
136
+ const data = JSON.parse(event.data) as MessageEnvelope;
137
+
138
+ if (this.wsState() === 'connect') {
139
+ if (data.type !== 'welcome') {
140
+ this.disconnectFromMachineServer(`Unexpected message type, expected 'welcome' but got '${data.type}'`);
141
+ return;
142
+ }
143
+
144
+ const authContent: AuthRequestMessage = {
145
+ token: token,
146
+ publicKey: '',
147
+ clientVersion: ClientVersion,
148
+ protocolVersion: ProtocolVersion,
149
+ };
150
+ const authRequest = this.createResponse(data, 'authportal', authContent);
151
+ this.#wsState.set('welcome');
152
+ this.#ws?.send(authRequest);
153
+ } else if (this.wsState() === 'welcome') {
154
+ if (data.type !== 'authportal') {
155
+ this.disconnectFromMachineServer(`Unexpected message type, expected 'authportal' but got '${data.type}'`);
156
+ return;
157
+ }
158
+
159
+ var payload = JSON.parse(data.payload ?? '') as AuthResponseMessage;
160
+ if (payload.accepted === false) {
161
+ this.disconnectFromMachineServer('Authentication failed');
162
+ return;
163
+ }
164
+
165
+ this.#wsState.set('authenticated');
166
+ this.#isConnectedToMachineServer.set(true);
167
+
168
+ // Send queued commands
169
+ while (this.#queuedCommands.length > 0) {
170
+ const message = this.#queuedCommands.pop();
171
+ // Only send if this request has not timed out yet
172
+ if (message && this.#pendingCommands[message.messageId]) this.activateCommand(message);
173
+ }
174
+ } else if (this.wsState() === 'authenticated') {
175
+ if (data.type !== 'command') {
176
+ console.warn(`Unexpected message type, expected 'command' but got '${data.type}'`);
177
+ return;
178
+ }
179
+
180
+ const payload = JSON.parse(data.payload ?? '') as CommandResponse;
181
+ payload.body = payload?.body == null ? null : this.utf8Atob(payload.body);
182
+ const f = this.#pendingCommands[data.messageId];
183
+ if (f) {
184
+ delete this.#pendingCommands[data.messageId];
185
+ window.clearTimeout(f.timer);
186
+ f.resolve(payload);
187
+ }
188
+ }
189
+ };
190
+ }
191
+
192
+ private disconnectFromMachineServer(errorMessage: string | null = null) {
193
+ if (errorMessage) console.error(errorMessage);
194
+
195
+ this.#ws?.close();
196
+ this.#ws = null;
197
+ this.#wsState.set('disconnected');
198
+ this.#isConnectedToMachineServer.set(false);
199
+ }
200
+
201
+ private createResponse(request: MessageEnvelope, type: MessageType, payload: any) {
202
+ const response: MessageEnvelope = {
203
+ from: ClientId,
204
+ to: request.from,
205
+ type: type,
206
+ messageId: request.messageId,
207
+ payload: JSON.stringify(payload),
208
+ };
209
+
210
+ return JSON.stringify(response);
211
+ }
212
+
213
+ sendCommand(
214
+ token: string,
215
+ clientId: string,
216
+ method: RequestMethod,
217
+ path: string,
218
+ body: string | null,
219
+ headers: { [key: string]: string } | null,
220
+ timeout: number = 5000
221
+ ) {
222
+ if (this.wsState() === 'disconnected' || this.wsState() === 'error') {
223
+ // Connect to the machine server if we are not connected
224
+ this.connectToMachineServer(token);
225
+ }
226
+
227
+ return new Promise<CommandResponse>((resolve, reject) => {
228
+ const messageId = crypto.randomUUID();
229
+
230
+ const request: CommandRequest = {
231
+ method,
232
+ path,
233
+ body,
234
+ headers,
235
+ };
236
+
237
+ const message: MessageEnvelope = {
238
+ from: ClientId,
239
+ to: clientId,
240
+ type: 'command',
241
+ messageId: messageId,
242
+ payload: JSON.stringify(request),
243
+ };
244
+
245
+ const f: PromiseResolver = {
246
+ resolve,
247
+ reject,
248
+ timer: window.setTimeout(() => {
249
+ const f = this.#pendingCommands[messageId];
250
+ if (f) {
251
+ delete this.#pendingCommands[messageId];
252
+ f.reject('Timeout');
253
+ }
254
+ }, timeout),
255
+ };
256
+
257
+ this.#pendingCommands[messageId] = f;
258
+
259
+ // If we are not yet connected to the machine server, queue the command
260
+ if (this.#isConnectedToMachineServer()) this.activateCommand(message);
261
+ else this.#queuedCommands.push(message);
262
+ });
263
+ }
264
+
265
+ private activateCommand(message: MessageEnvelope) {
266
+ this.#ws?.send(JSON.stringify(message));
267
+ }
268
+ }
@@ -0,0 +1,25 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ type Unit = 's' | 'm' | 'h' | 'D' | 'W' | 'M' | 'Y';
4
+
5
+ @Injectable({
6
+ providedIn: 'root',
7
+ })
8
+ export class TimespanLiteralsService {
9
+ fromString(str: string | null | undefined) {
10
+ if (!str) return null;
11
+
12
+ const res = str.match(/(\d+)([smhDWMY])/);
13
+
14
+ return (
15
+ res && {
16
+ value: parseInt(res[1]),
17
+ unit: res[2] as Unit,
18
+ }
19
+ );
20
+ }
21
+
22
+ toString(value: number, unit: Unit) {
23
+ return `${value}${unit}`;
24
+ }
25
+ }
@@ -0,0 +1,87 @@
1
+ import { effect, inject, Injectable, signal } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+ import { take, tap } from 'rxjs';
4
+ import { DuplicatiServerService } from '../openapi';
5
+ import { LOCALSTORAGE } from '../services/localstorage.token';
6
+
7
+ @Injectable({
8
+ providedIn: 'root',
9
+ })
10
+ export class AppAuthState {
11
+ #ls = inject(LOCALSTORAGE);
12
+ #router = inject(Router);
13
+ #dupServer = inject(DuplicatiServerService);
14
+ #token = signal<string | null>(this.#ls.getItemParsed<string>('token') ?? null);
15
+ #tokenSetTime = signal<number | null>(this.#ls.getItemParsed<number>('tokenSetTime') ?? null);
16
+
17
+ tokenSetTime = this.#tokenSetTime.asReadonly();
18
+ token = this.#token.asReadonly();
19
+
20
+ #tokenEffect = effect(
21
+ () => {
22
+ if (!this.#token()) {
23
+ this.#ls.clearAll();
24
+ this.#router.navigate(['/login']);
25
+ return;
26
+ }
27
+ const now = Date.now();
28
+ this.#ls.setItemParsed('tokenSetTime', now);
29
+ this.#ls.setItemParsed('token', this.#token());
30
+ this.#tokenSetTime.set(this.#token() ? now : null);
31
+ },
32
+ { allowSignalWrites: true }
33
+ );
34
+
35
+ login(pass = 'helloworld') {
36
+ return this.#dupServer
37
+ .postApiV1AuthLogin({
38
+ requestBody: {
39
+ Password: pass,
40
+ RememberMe: true,
41
+ },
42
+ })
43
+ .pipe(
44
+ take(1),
45
+ tap((res) => this.#token.set(res.AccessToken ?? null))
46
+ );
47
+ }
48
+
49
+ refreshToken() {
50
+ return this.#dupServer.postApiV1AuthRefresh().pipe(
51
+ take(1),
52
+ tap((res) => this.#token.set(res.AccessToken ?? null))
53
+ );
54
+ }
55
+
56
+ logout() {
57
+ this.#dupServer
58
+ .postApiV1AuthRefreshLogout()
59
+ .pipe(
60
+ take(1),
61
+ tap((_) => {
62
+ this.#token.set(null);
63
+ this.#ls.clearAll();
64
+ this.#router.navigate(['/login']);
65
+ })
66
+ )
67
+ .subscribe({
68
+ error: (_) => {
69
+ this.#token.set(null);
70
+ this.#ls.clearAll();
71
+ this.#router.navigate(['/login']);
72
+ },
73
+ });
74
+ }
75
+
76
+ // reLogin() {
77
+
78
+ // return this.#dupServer.postApiV1AuthRefreshLogout().pipe(
79
+ // take(1),
80
+ // tap((_) => {
81
+ // this.#token.set(null);
82
+ // this.#ls.clearAll();
83
+ // })
84
+ // .
85
+ // );
86
+ // }
87
+ }
@@ -0,0 +1,71 @@
1
+ import { inject, Injectable, signal } from '@angular/core';
2
+ import { finalize, take } from 'rxjs';
3
+ import { DuplicatiServerService } from '../openapi';
4
+ import { Subscribed } from '../types/subscribed';
5
+
6
+ export type BackupRes = Subscribed<ReturnType<DuplicatiServerService['getApiV1Backups']>>;
7
+ export type Backup = BackupRes[0];
8
+
9
+ @Injectable({
10
+ providedIn: 'root',
11
+ })
12
+ export class BackupsState {
13
+ #dupServer = inject(DuplicatiServerService);
14
+ #timestamp: number | null = null;
15
+
16
+ backups = signal<BackupRes>([]);
17
+ backupsLoading = signal(false);
18
+
19
+ getBackupById(id: string) {
20
+ return this.backups().find((x) => x.Backup?.ID === id) ?? null;
21
+ }
22
+
23
+ getBackups(refresh = false) {
24
+ if (this.#timestamp && Date.now() - this.#timestamp < 100) {
25
+ return;
26
+ }
27
+
28
+ this.#timestamp = Date.now();
29
+
30
+ this.backupsLoading.set(true);
31
+
32
+ this.#dupServer
33
+ .getApiV1Backups()
34
+ .pipe(
35
+ take(1),
36
+ finalize(() => this.backupsLoading.set(false))
37
+ )
38
+ .subscribe({
39
+ next: (res) => {
40
+ this.backups.set(res);
41
+ },
42
+ error: (err) => {
43
+ console.error(err);
44
+ },
45
+ });
46
+ }
47
+
48
+ startBackup(id: string) {
49
+ this.#dupServer
50
+ .postApiV1BackupByIdStart({
51
+ id,
52
+ })
53
+ .subscribe({
54
+ next: () => {
55
+ this.getBackups();
56
+ },
57
+ });
58
+ }
59
+
60
+ deleteBackup(id: string) {
61
+ this.#dupServer
62
+ .deleteApiV1BackupById({
63
+ id,
64
+ })
65
+ .subscribe({
66
+ next: () => {
67
+ this.getBackups();
68
+ },
69
+ });
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+
3
+ export type Relayconfig = {
4
+ accessToken: string;
5
+ clientId: string;
6
+ };
7
+
8
+ @Injectable({
9
+ providedIn: 'root',
10
+ })
11
+ export class RelayconfigState {
12
+ #config = signal<Relayconfig | null>(null);
13
+
14
+ config = this.#config.asReadonly();
15
+
16
+ setConfig(config: Relayconfig) {
17
+ this.#config.set(config);
18
+ }
19
+
20
+ clearConfig() {
21
+ this.#config.set(null);
22
+ }
23
+ }