diff --git a/src/app/enums/explorer-type.enum.ts b/src/app/enums/explorer-type.enum.ts
index 23d14f6881b..00d28c46c8d 100644
--- a/src/app/enums/explorer-type.enum.ts
+++ b/src/app/enums/explorer-type.enum.ts
@@ -1,4 +1,5 @@
export enum ExplorerNodeType {
Directory = 'directory',
File = 'file',
+ Symlink = 'symlink',
}
diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html
index ce06d7cceed..e480bdf5067 100644
--- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html
+++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html
@@ -60,6 +60,8 @@
>
@if (node.data.type === ExplorerNodeType.File) {
+ } @else if (node.data.type === ExplorerNodeType.Symlink) {
+
} @else {
@if (node.data.isLock) {
diff --git a/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.html b/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.html
index bc6dcc81d94..b48964ea304 100644
--- a/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.html
+++ b/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.html
@@ -13,6 +13,7 @@
[tooltip]="helptext.source_path_tooltip | translate"
[required]="true"
[multiple]="false"
+ [root]="'/'"
[nodeProvider]="fileNodeProvider"
>
diff --git a/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.ts b/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.ts
index 39d53afe19b..59f0a8767df 100644
--- a/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.ts
+++ b/src/app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component.ts
@@ -331,7 +331,9 @@ export class CloudBackupFormComponent implements OnInit {
}
private setFileNodeProvider(): void {
- this.fileNodeProvider = this.filesystemService.getFilesystemNodeProvider({ directoriesOnly: true });
+ this.fileNodeProvider = this.filesystemService.getFilesystemNodeProvider({
+ datasetsAndZvols: true,
+ });
}
private setBucketNodeProvider(): void {
diff --git a/src/app/services/filesystem.service.spec.ts b/src/app/services/filesystem.service.spec.ts
index b17c4e1dc6c..5cf9cd5d4c6 100644
--- a/src/app/services/filesystem.service.spec.ts
+++ b/src/app/services/filesystem.service.spec.ts
@@ -28,6 +28,12 @@ describe('FilesystemService', () => {
type: FileType.File,
attributes: [FileAttribute.Immutable],
},
+ {
+ path: '/mnt/parent/zvol',
+ name: 'zvol',
+ type: FileType.Symlink,
+ attributes: [FileAttribute.Immutable],
+ },
] as FileRecord[]),
]),
],
@@ -37,7 +43,7 @@ describe('FilesystemService', () => {
describe('getFilesystemNodeProvider', () => {
it('returns a TreeNodeProvider that calls filesystem.listdir to list files and directories', async () => {
- const treeNodeProvider = spectator.service.getFilesystemNodeProvider();
+ const treeNodeProvider = spectator.service.getFilesystemNodeProvider({ datasetsAndZvols: true });
const childNodes = await lastValueFrom(
treeNodeProvider({
@@ -72,6 +78,14 @@ describe('FilesystemService', () => {
isMountpoint: false,
isLock: true,
},
+ {
+ hasChildren: false,
+ name: 'zvol',
+ path: '/mnt/parent/zvol',
+ type: ExplorerNodeType.Symlink,
+ isMountpoint: false,
+ isLock: true,
+ },
]);
});
});
diff --git a/src/app/services/filesystem.service.ts b/src/app/services/filesystem.service.ts
index 806e17672a7..3f7c899e93d 100644
--- a/src/app/services/filesystem.service.ts
+++ b/src/app/services/filesystem.service.ts
@@ -1,14 +1,23 @@
import { Injectable } from '@angular/core';
-import { map } from 'rxjs';
+import {
+ catchError, map, of, throwError,
+} from 'rxjs';
import { ExplorerNodeType } from 'app/enums/explorer-type.enum';
import { FileAttribute } from 'app/enums/file-attribute.enum';
import { FileType } from 'app/enums/file-type.enum';
+import { ApiError } from 'app/interfaces/api-error.interface';
import { FileRecord } from 'app/interfaces/file-record.interface';
import { QueryFilter, QueryOptions } from 'app/interfaces/query-api.interface';
import { ExplorerNodeData, TreeNode } from 'app/interfaces/tree-node.interface';
import { TreeNodeProvider } from 'app/modules/forms/ix-forms/components/ix-explorer/tree-node-provider.interface';
import { ApiService } from 'app/services/websocket/api.service';
+export interface ProviderOptions {
+ directoriesOnly?: boolean;
+ showHiddenFiles?: boolean;
+ includeSnapshots?: boolean;
+ datasetsAndZvols?: boolean;
+}
@Injectable({ providedIn: 'root' })
export class FilesystemService {
constructor(
@@ -18,19 +27,34 @@ export class FilesystemService {
/**
* Returns a pre-configured node provider for files and directories.
*/
- getFilesystemNodeProvider(providerOptions?: {
- directoriesOnly?: boolean;
- showHiddenFiles?: boolean;
- includeSnapshots?: boolean;
- }): TreeNodeProvider {
- const options = {
+ getFilesystemNodeProvider(providerOptions?: ProviderOptions): TreeNodeProvider {
+ const options: ProviderOptions = {
directoriesOnly: false,
showHiddenFiles: false,
includeSnapshots: true,
+ datasetsAndZvols: false,
...providerOptions,
};
return (node: TreeNode) => {
+ if (options.datasetsAndZvols) {
+ if (node.data.path.trim() === '/') {
+ return of([
+ {
+ path: '/mnt',
+ name: '/mnt',
+ hasChildren: true,
+ type: ExplorerNodeType.Directory,
+ },
+ {
+ path: '/dev/zvol',
+ name: '/dev/zvol',
+ hasChildren: true,
+ type: ExplorerNodeType.Directory,
+ },
+ ] as ExplorerNodeData[]);
+ }
+ }
const typeFilter: [QueryFilter?] = [];
if (options.directoriesOnly) {
typeFilter.push(['type', '=', FileType.Directory]);
@@ -47,10 +71,11 @@ export class FilesystemService {
};
return this.api.call('filesystem.listdir', [node.data.path, typeFilter, queryOptions]).pipe(
+
map((files) => {
const children: ExplorerNodeData[] = [];
files.forEach((file) => {
- if (file.type === FileType.Symlink || !file.hasOwnProperty('name')) {
+ if ((!options.datasetsAndZvols && file.type === FileType.Symlink) || !file.hasOwnProperty('name')) {
return;
}
@@ -58,18 +83,36 @@ export class FilesystemService {
return;
}
+ let fileType: ExplorerNodeType;
+ switch (file.type) {
+ case FileType.Directory:
+ fileType = ExplorerNodeType.Directory;
+ break;
+ case FileType.Symlink:
+ fileType = ExplorerNodeType.Symlink;
+ break;
+ default:
+ fileType = ExplorerNodeType.File;
+ break;
+ }
children.push({
path: file.path,
name: file.name,
isMountpoint: file.attributes.includes(FileAttribute.MountRoot),
isLock: file.attributes.includes(FileAttribute.Immutable),
- type: file.type === FileType.Directory ? ExplorerNodeType.Directory : ExplorerNodeType.File,
+ type: fileType,
hasChildren: file.type === FileType.Directory,
});
});
return children;
}),
+ catchError((error: ApiError) => {
+ if (error.reason === '[ENOENT] Directory /dev/zvol does not exist') {
+ return of([]);
+ }
+ return throwError(() => (error));
+ }),
);
};
}
diff --git a/src/assets/icons/custom/file-link.svg b/src/assets/icons/custom/file-link.svg
new file mode 100644
index 00000000000..354e1faee13
--- /dev/null
+++ b/src/assets/icons/custom/file-link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/sprite-config.json b/src/assets/icons/sprite-config.json
index ea5ff843078..5c47544f78a 100644
--- a/src/assets/icons/sprite-config.json
+++ b/src/assets/icons/sprite-config.json
@@ -1,3 +1,3 @@
{
- "iconUrl": "assets/icons/sprite.svg?v=0a208a877e"
+ "iconUrl": "assets/icons/sprite.svg?v=29ff3e7fb0"
}
\ No newline at end of file
diff --git a/src/assets/icons/sprite.svg b/src/assets/icons/sprite.svg
index b7f0748fbb1..341d86b69b8 100644
--- a/src/assets/icons/sprite.svg
+++ b/src/assets/icons/sprite.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file