fix
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
Manifest-Version: 1.0
|
||||||
|
Main-Class: org.springframework.boot.loader.launch.JarLauncher
|
||||||
|
Start-Class: com.jambotronGroup.jambotron.JambotronApplication
|
||||||
|
Spring-Boot-Version: 3.5.0
|
||||||
|
Spring-Boot-Classes: BOOT-INF/classes/
|
||||||
|
Spring-Boot-Lib: BOOT-INF/lib/
|
||||||
|
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
|
||||||
|
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
|
||||||
|
Build-Jdk-Spec: 24
|
||||||
|
Implementation-Title: jambotron
|
||||||
|
Implementation-Version: 0.0.1-SNAPSHOT
|
||||||
|
|
||||||
@@ -39,6 +39,9 @@ dependencies {
|
|||||||
implementation 'org.springframework.ai:spring-ai-zhipuai-spring-boot-starter:1.0.0-M6'
|
implementation 'org.springframework.ai:spring-ai-zhipuai-spring-boot-starter:1.0.0-M6'
|
||||||
implementation 'org.springframework:spring-mock:2.0.8'
|
implementation 'org.springframework:spring-mock:2.0.8'
|
||||||
|
|
||||||
|
// Apache Commons Exec for process execution
|
||||||
|
implementation 'org.apache.commons:commons-exec:1.3'
|
||||||
|
|
||||||
implementation 'com.google.guava:guava:33.4.8-jre'
|
implementation 'com.google.guava:guava:33.4.8-jre'
|
||||||
implementation 'io.micrometer:micrometer-core:1.12.0'
|
implementation 'io.micrometer:micrometer-core:1.12.0'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
|
|||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
<div class="file-browser-container">
|
||||||
|
<!-- Main Content with Sidenav -->
|
||||||
|
<mat-sidenav-container class="sidenav-container">
|
||||||
|
<!-- Folder Tree Sidenav -->
|
||||||
|
<mat-sidenav #sidenav
|
||||||
|
mode="side"
|
||||||
|
[opened]="showTreeView"
|
||||||
|
class="tree-sidenav">
|
||||||
|
|
||||||
|
<app-file-system-tree-component
|
||||||
|
(folderSelected)="onFolderSelected($event)"
|
||||||
|
>
|
||||||
|
</app-file-system-tree-component>
|
||||||
|
</mat-sidenav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<mat-sidenav-content class="main-content">
|
||||||
|
<mat-card class="content-card">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title class="card-title">
|
||||||
|
|
||||||
|
<div class="title-section">
|
||||||
|
<button mat-icon-button (click)="toggleView()"
|
||||||
|
[matTooltip]="showTreeView ? 'Hide Tree' : 'Show Tree'">
|
||||||
|
<mat-icon>{{ showTreeView ? 'view_list' : 'account_tree' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
<!-- Breadcrumb Navigation -->
|
||||||
|
<nav class="breadcrumb-nav" aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<button mat-button (click)="navigateToPath(-1)" class="path-button">
|
||||||
|
<mat-icon>home</mat-icon>
|
||||||
|
Root
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li *ngFor="let segment of pathSegments; let i = index" class="breadcrumb-item">
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
<button mat-button (click)="navigateToPath(i)" class="path-button">
|
||||||
|
{{ segment }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="header-actions">
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<!--<div class="search-container">
|
||||||
|
<mat-form-field appearance="outline" class="search-field">
|
||||||
|
<mat-label>Search files...</mat-label>
|
||||||
|
<input matInput
|
||||||
|
[(ngModel)]="searchQuery"
|
||||||
|
(input)="onSearchInput($event)"
|
||||||
|
placeholder="Enter filename or extension">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
<button *ngIf="searchQuery"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
(click)="clearSearch()"
|
||||||
|
matTooltip="Clear search">
|
||||||
|
<mat-icon>clear</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
</div>-->
|
||||||
|
|
||||||
|
<input #fileInput type="file" multiple style="display: none"
|
||||||
|
(change)="onFilesSelected($event)">
|
||||||
|
<button mat-icon-button
|
||||||
|
color="primary"
|
||||||
|
[matTooltip]="'Upload Files'"
|
||||||
|
(click)="fileInput.click()">
|
||||||
|
<mat-icon>upload</mat-icon>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button
|
||||||
|
color="accent"
|
||||||
|
[matTooltip]="'New Folder'"
|
||||||
|
(click)="openCreateFolderDialog()">
|
||||||
|
<mat-icon>create_new_folder</mat-icon>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||||
|
|
||||||
|
<!-- Upload Progress -->
|
||||||
|
<div *ngIf="uploading" class="upload-progress">
|
||||||
|
<mat-progress-bar mode="determinate" [value]="uploadProgress"></mat-progress-bar>
|
||||||
|
<span class="progress-text">Uploading... {{uploadProgress}}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Table -->
|
||||||
|
<div class="table-container">
|
||||||
|
<table mat-table [dataSource]="dataSource" class="file-table">
|
||||||
|
<!-- Selection Column -->
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<mat-checkbox (change)="masterToggle()"
|
||||||
|
[checked]="selection.size > 0 && isAllSelected()"
|
||||||
|
[indeterminate]="selection.size > 0 && !isAllSelected()">
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
<mat-checkbox (change)="toggleSelection(file)"
|
||||||
|
[checked]="isSelected(file)">
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Icon Column -->
|
||||||
|
<ng-container matColumnDef="icon">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell *matCellDef="let file" class="icon-cell">
|
||||||
|
<mat-icon [class.folder-icon]="file.type === 'directory'">
|
||||||
|
{{ getFileIcon(file) }}
|
||||||
|
</mat-icon>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||||
|
<td mat-cell *matCellDef="let file"
|
||||||
|
(dblclick)="onFileDoubleClick(file)"
|
||||||
|
class="name-cell"
|
||||||
|
[class.clickable]="file.type === 'directory'">
|
||||||
|
{{ file.name }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Size Column -->
|
||||||
|
<ng-container matColumnDef="size">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Size</th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
{{ file.type === 'directory' ? '-' : formatFileSize(file.size) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Last Modified Column -->
|
||||||
|
<ng-container matColumnDef="lastModified">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Last Modified</th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
{{ file.lastModified | date:'short' }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Actions Column -->
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<button *ngIf="selection.size > 0"
|
||||||
|
mat-icon-button
|
||||||
|
color="warn"
|
||||||
|
(click)="deleteSelected()"
|
||||||
|
matTooltip="Delete Selected">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
<button *ngIf="file.type === 'file'"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="downloadFile(file)"
|
||||||
|
matTooltip="Download">
|
||||||
|
<mat-icon>download</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="deleteFile(file)"
|
||||||
|
color="warn"
|
||||||
|
matTooltip="Delete">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div *ngIf="!loading && dataSource.data.length === 0" class="empty-state">
|
||||||
|
<mat-icon>{{ searchQuery ? 'search_off' : 'folder_open' }}</mat-icon>
|
||||||
|
<h3>{{ searchQuery ? 'No results found' : 'This folder is empty' }}</h3>
|
||||||
|
<p>{{ searchQuery ? 'Try adjusting your search terms' : 'Upload files or create folders to get started.' }}</p>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</mat-sidenav-content>
|
||||||
|
</mat-sidenav-container>
|
||||||
|
</div>
|
||||||
+407
@@ -0,0 +1,407 @@
|
|||||||
|
/* file-browser-with-tree.component.scss */
|
||||||
|
.file-browser-container {
|
||||||
|
height: calc(100vh - 172px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
//z-index: 1000;
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-icon:first-of-type {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header-actions{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.sidenav-container {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-sidenav {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
height: 100%;
|
||||||
|
overflow: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.mat-card-header {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.mat-raised-button {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-card-content {
|
||||||
|
padding: 16px 24px;
|
||||||
|
height: calc(100% - 80px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-nav {
|
||||||
|
//margin-bottom: 16px;
|
||||||
|
//padding: 8px 0;
|
||||||
|
//border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-button {
|
||||||
|
min-width: auto !important;
|
||||||
|
padding: 0 8px !important;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.search-field {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 16px;
|
||||||
|
height: calc(100vh - 95px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mat-header-cell {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-cell {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-row:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cell {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
color: #666;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&.folder-icon {
|
||||||
|
color: #2196F3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1976D2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(25, 118, 210, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
font-size: 72px;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-progress-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.tree-sidenav {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card .mat-card-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title .header-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.browser-toolbar {
|
||||||
|
.mat-toolbar-row {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:not(.toolbar-spacer) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-sidenav {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
.mat-card-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-table {
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.mat-header-cell,
|
||||||
|
.mat-cell {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.path-button {
|
||||||
|
padding: 0 4px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
.search-field {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.content-card .mat-card-content {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title .header-actions {
|
||||||
|
.mat-raised-button {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-table {
|
||||||
|
.icon-cell {
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme support
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.content-card {
|
||||||
|
background-color: #303030;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.mat-card-header {
|
||||||
|
border-bottom-color: #424242;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-nav {
|
||||||
|
border-bottom-color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-table {
|
||||||
|
.mat-cell {
|
||||||
|
border-bottom-color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-row:hover {
|
||||||
|
background-color: #424242;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: #aaa;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation for smooth transitions
|
||||||
|
.mat-sidenav-content {
|
||||||
|
transition: margin-left 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-sidenav {
|
||||||
|
transition: width 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom scrollbar for webkit browsers
|
||||||
|
.main-content::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FileBrowserWithTreeComponent } from './file-browser-with-tree-component';
|
||||||
|
|
||||||
|
describe('FileBrowserWithTreeComponent', () => {
|
||||||
|
let component: FileBrowserWithTreeComponent;
|
||||||
|
let fixture: ComponentFixture<FileBrowserWithTreeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FileBrowserWithTreeComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FileBrowserWithTreeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
+375
@@ -0,0 +1,375 @@
|
|||||||
|
import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {DatePipe, NgForOf, NgIf} from '@angular/common';
|
||||||
|
import {
|
||||||
|
MatCell,
|
||||||
|
MatCellDef, MatColumnDef,
|
||||||
|
MatHeaderCell, MatHeaderCellDef,
|
||||||
|
MatHeaderRow,
|
||||||
|
MatHeaderRowDef,
|
||||||
|
MatRow,
|
||||||
|
MatRowDef, MatTable, MatTableDataSource
|
||||||
|
} from '@angular/material/table';
|
||||||
|
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||||
|
import {MatCheckbox} from '@angular/material/checkbox';
|
||||||
|
import {MatProgressBar} from '@angular/material/progress-bar';
|
||||||
|
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardSubtitle, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {MatSidenav, MatSidenavContainer, MatSidenavContent} from '@angular/material/sidenav';
|
||||||
|
|
||||||
|
import {MatToolbar} from '@angular/material/toolbar';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {MatTooltip} from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
|
import {FileBrowserTreeService, FileItem} from '../../services/file-browser-tree.service';
|
||||||
|
import {FileSystemTreeComponent} from '../file-system-tree-component/file-system-tree-component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-file-browser-with-tree-component',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
MatIcon,
|
||||||
|
MatHeaderRowDef,
|
||||||
|
MatRowDef,
|
||||||
|
MatRow,
|
||||||
|
MatHeaderRow,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatCellDef,
|
||||||
|
MatCell,
|
||||||
|
MatHeaderCell,
|
||||||
|
MatColumnDef,
|
||||||
|
MatHeaderCellDef,
|
||||||
|
MatCheckbox,
|
||||||
|
MatTable,
|
||||||
|
MatProgressBar,
|
||||||
|
|
||||||
|
FormsModule,
|
||||||
|
|
||||||
|
NgForOf,
|
||||||
|
MatButton,
|
||||||
|
MatCardContent,
|
||||||
|
MatSidenavContent,
|
||||||
|
MatCard,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
MatSidenav,
|
||||||
|
MatSidenavContainer,
|
||||||
|
|
||||||
|
|
||||||
|
DatePipe,
|
||||||
|
FileSystemTreeComponent,
|
||||||
|
|
||||||
|
],
|
||||||
|
templateUrl: './file-browser-with-tree-component.html',
|
||||||
|
styleUrl: './file-browser-with-tree-component.scss',
|
||||||
|
schemas:[CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class FileBrowserWithTreeComponent implements OnInit {
|
||||||
|
@ViewChild('sidenav') sidenav!: MatSidenav;
|
||||||
|
@ViewChild(FileSystemTreeComponent) folderTree!: FileSystemTreeComponent;
|
||||||
|
|
||||||
|
displayedColumns: string[] = ['select', 'icon', 'name', 'size', 'lastModified', 'actions'];
|
||||||
|
dataSource = new MatTableDataSource<FileItem>();
|
||||||
|
currentPath: string = '/';
|
||||||
|
pathSegments: string[] = [];
|
||||||
|
loading = false;
|
||||||
|
uploading = false;
|
||||||
|
uploadProgress = 0;
|
||||||
|
selection = new Set<FileItem>();
|
||||||
|
showTreeView = true;
|
||||||
|
searchQuery = '';
|
||||||
|
filteredFiles: FileItem[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fileService: FileBrowserTreeService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private snackBar: MatSnackBar
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadFiles(this.currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleView(): void {
|
||||||
|
this.showTreeView = !this.showTreeView;
|
||||||
|
if (this.showTreeView) {
|
||||||
|
this.sidenav.open();
|
||||||
|
} else {
|
||||||
|
this.sidenav.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFolderSelected(path: string): void {
|
||||||
|
this.loadFiles(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiles(path: string): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.selection.clear();
|
||||||
|
this.clearSearch();
|
||||||
|
|
||||||
|
this.fileService.getFiles(path).subscribe({
|
||||||
|
next: (files) => {
|
||||||
|
this.filteredFiles = files;
|
||||||
|
this.dataSource.data = files;
|
||||||
|
this.currentPath = path;
|
||||||
|
this.updatePathSegments();
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
// Update tree selection
|
||||||
|
if (this.folderTree) {
|
||||||
|
//this.folderTree.expandToPath(path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.snackBar.open('Error loading files: ' + error.message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
panelClass: 'error-snackbar'
|
||||||
|
});
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePathSegments(): void {
|
||||||
|
this.pathSegments = this.currentPath === '/' ?
|
||||||
|
[] : this.currentPath.split('/').filter(segment => segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToPath(index: number): void {
|
||||||
|
if (index === -1) {
|
||||||
|
this.loadFiles('/');
|
||||||
|
} else {
|
||||||
|
const path = '/' + this.pathSegments.slice(0, index + 1).join('/');
|
||||||
|
this.loadFiles(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileDoubleClick(file: FileItem): void {
|
||||||
|
if (file.type === 'directory') {
|
||||||
|
const newPath = this.currentPath === '/' ?
|
||||||
|
`/${file.name}` : `${this.currentPath}/${file.name}`;
|
||||||
|
this.loadFiles(newPath);
|
||||||
|
} else {
|
||||||
|
this.downloadFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchInput(event: any): void {
|
||||||
|
const query = event.target.value.toLowerCase();
|
||||||
|
if (query) {
|
||||||
|
this.dataSource.data = this.filteredFiles.filter(file =>
|
||||||
|
file.name.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.dataSource.data = this.filteredFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch(): void {
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.dataSource.data = this.filteredFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilesSelected(event: any): void {
|
||||||
|
const files: FileList = event.target.files;
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
this.uploading = true;
|
||||||
|
this.uploadProgress = 0;
|
||||||
|
|
||||||
|
const uploadPromises = Array.from(files).map(file => {
|
||||||
|
return this.fileService.uploadFile(this.currentPath, file).toPromise();
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(uploadPromises).then(() => {
|
||||||
|
this.uploading = false;
|
||||||
|
this.uploadProgress = 0;
|
||||||
|
this.snackBar.open(`${files.length} file(s) uploaded successfully`, 'Close', {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
this.loadFiles(this.currentPath);
|
||||||
|
}).catch(error => {
|
||||||
|
this.uploading = false;
|
||||||
|
this.uploadProgress = 0;
|
||||||
|
this.snackBar.open('Upload failed: ' + error.message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
panelClass: 'error-snackbar'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate upload progress
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
this.uploadProgress += 10;
|
||||||
|
if (this.uploadProgress >= 100) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreateFolderDialog(): void {
|
||||||
|
const folderName = prompt('Enter folder name:');
|
||||||
|
if (folderName && folderName.trim()) {
|
||||||
|
this.fileService.createFolder(this.currentPath, folderName.trim()).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.snackBar.open('Folder created successfully', 'Close', {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
this.loadFiles(this.currentPath);
|
||||||
|
// Refresh tree view
|
||||||
|
if (this.folderTree) {
|
||||||
|
this.folderTree.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.snackBar.open('Failed to create folder: ' + error.message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
panelClass: 'error-snackbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(file: FileItem): void {
|
||||||
|
this.fileService.downloadFile(file.path).subscribe({
|
||||||
|
next: (blob) => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = file.name;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.snackBar.open('Download failed: ' + error.message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
panelClass: 'error-snackbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFile(file: FileItem): void {
|
||||||
|
if (confirm(`Are you sure you want to delete "${file.name}"?`)) {
|
||||||
|
this.fileService.deleteFile(file.path).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.snackBar.open('File deleted successfully', 'Close', {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
this.loadFiles(this.currentPath);
|
||||||
|
// Refresh tree view if folder was deleted
|
||||||
|
if (file.type === 'directory' && this.folderTree) {
|
||||||
|
this.folderTree.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.snackBar.open('Delete failed: ' + error.message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
panelClass: 'error-snackbar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelected(): void {
|
||||||
|
const selectedFiles = Array.from(this.selection);
|
||||||
|
if (selectedFiles.length === 0) return;
|
||||||
|
|
||||||
|
const message = `Are you sure you want to delete ${selectedFiles.length} selected item(s)?`;
|
||||||
|
if (confirm(message)) {
|
||||||
|
const deletePromises = selectedFiles.map(file =>
|
||||||
|
this.fileService.deleteFile(file.path).toPromise()
|
||||||
|
);
|
||||||
|
|
||||||
|
Promise.all(deletePromises).then(() => {
|
||||||
|
this.snackBar.open(`${selectedFiles.length} item(s) deleted successfully`, 'Close', {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
this.selection.clear();
|
||||||
|
this.loadFiles(this.currentPath);
|
||||||
|
// Refresh tree view if any folders were deleted
|
||||||
|
const hasFolder = selectedFiles.some(file => file.type === 'directory');
|
||||||
|
if (hasFolder && this.folderTree) {
|
||||||
|
this.folderTree.refresh();
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.snackBar.open('Some deletions failed: ' + error.message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
panelClass: 'error-snackbar'
|
||||||
|
});
|
||||||
|
this.loadFiles(this.currentPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSelection(file: FileItem): void {
|
||||||
|
if (this.selection.has(file)) {
|
||||||
|
this.selection.delete(file);
|
||||||
|
} else {
|
||||||
|
this.selection.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected(file: FileItem): boolean {
|
||||||
|
return this.selection.has(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
masterToggle(): void {
|
||||||
|
if (this.isAllSelected()) {
|
||||||
|
this.selection.clear();
|
||||||
|
} else {
|
||||||
|
this.dataSource.data.forEach(file => this.selection.add(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSelected(): boolean {
|
||||||
|
return this.dataSource.data.length > 0 &&
|
||||||
|
this.dataSource.data.every(file => this.selection.has(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileIcon(file: FileItem): string {
|
||||||
|
if (file.type === 'directory') return 'folder';
|
||||||
|
|
||||||
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
switch (extension) {
|
||||||
|
case 'pdf': return 'picture_as_pdf';
|
||||||
|
case 'doc':
|
||||||
|
case 'docx': return 'description';
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx': return 'grid_on';
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'png':
|
||||||
|
case 'gif': return 'image';
|
||||||
|
case 'mp4':
|
||||||
|
case 'avi':
|
||||||
|
case 'mov': return 'movie';
|
||||||
|
case 'mp3':
|
||||||
|
case 'wav': return 'audiotrack';
|
||||||
|
case 'zip':
|
||||||
|
case 'rar':
|
||||||
|
case '7z': return 'archive';
|
||||||
|
case 'txt':
|
||||||
|
case 'md': return 'text_fields';
|
||||||
|
case 'js':
|
||||||
|
case 'ts':
|
||||||
|
case 'html':
|
||||||
|
case 'css':
|
||||||
|
case 'json': return 'code';
|
||||||
|
default: return 'insert_drive_file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="file-tree-container">
|
||||||
|
<div class="tree-header">
|
||||||
|
<mat-icon>folder_open</mat-icon>
|
||||||
|
<span>Folders</span>
|
||||||
|
<button mat-icon-button (click)="refresh()" matTooltip="Refresh">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="file-tree">
|
||||||
|
<!-- Directory nodes -->
|
||||||
|
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[attr.aria-label]="'Toggle ' + node.item"
|
||||||
|
(click)="treeControl.toggle(node)"
|
||||||
|
[disabled]="node.isLoading()">
|
||||||
|
|
||||||
|
<mat-icon class="mat-icon-rtl-mirror">
|
||||||
|
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-icon class="folder-icon" [class.selected]="selectedNode === node">
|
||||||
|
{{ treeControl.isExpanded(node) ? 'folder_open' : 'folder' }}
|
||||||
|
</mat-icon>
|
||||||
|
|
||||||
|
<span class="node-label"
|
||||||
|
[class.selected]="selectedNode === node"
|
||||||
|
(click)="selectNode(node)">
|
||||||
|
{{ node.item }}
|
||||||
|
</span>
|
||||||
|
</mat-tree-node>
|
||||||
|
</mat-tree>
|
||||||
|
</div>
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
.file-tree-container {
|
||||||
|
//padding: 20px;
|
||||||
|
//max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
mat-icon:first-child {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree {
|
||||||
|
background: white;
|
||||||
|
// border: 1px solid #e0e0e0;
|
||||||
|
// border-radius: 4px;
|
||||||
|
//max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon.selected {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label.selected {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-tree-node {
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-tree-node:hover {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FileSystemTreeComponent } from './file-system-tree-component';
|
||||||
|
|
||||||
|
describe('FileSystemTreeComponent', () => {
|
||||||
|
let component: FileSystemTreeComponent;
|
||||||
|
let fixture: ComponentFixture<FileSystemTreeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FileSystemTreeComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FileSystemTreeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
import {CollectionViewer, SelectionChange, DataSource} from '@angular/cdk/collections';
|
||||||
|
import {FlatTreeControl} from '@angular/cdk/tree';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Injectable,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
Output,
|
||||||
|
EventEmitter
|
||||||
|
} from '@angular/core';
|
||||||
|
import {BehaviorSubject, merge, Observable} from 'rxjs';
|
||||||
|
import {map} from 'rxjs/operators';
|
||||||
|
import {MatProgressBarModule} from '@angular/material/progress-bar';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatTreeModule} from '@angular/material/tree';
|
||||||
|
import {NgIf} from '@angular/common';
|
||||||
|
|
||||||
|
import {FileBrowserTreeService, FileItem} from '../../services/file-browser-tree.service';
|
||||||
|
import {MatTooltip} from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
/** Flat node with expandable and level information */
|
||||||
|
class DynamicFlatNode {
|
||||||
|
constructor(
|
||||||
|
public item: string,
|
||||||
|
public level = 1,
|
||||||
|
public expandable = false,
|
||||||
|
public isLoading = signal(false),
|
||||||
|
public fileItem: FileItem,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database for dynamic data. When expanding a node in the tree, the data source will need to fetch
|
||||||
|
* the descendants data from the database.
|
||||||
|
*/
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
export class DynamicDatabase {
|
||||||
|
|
||||||
|
|
||||||
|
rootNodes:FileItem[] = [];
|
||||||
|
|
||||||
|
constructor(private fileBrowserService: FileBrowserTreeService) {
|
||||||
|
this.getFileItems('/').subscribe(
|
||||||
|
value => {
|
||||||
|
console.log('value', value);
|
||||||
|
this.rootNodes = value;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('Error loading files:', error);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileItems(path:string):Observable<FileItem[]> {
|
||||||
|
return this.fileBrowserService.getFoldersOnly(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initial data from database */
|
||||||
|
initialData(): DynamicFlatNode[] {
|
||||||
|
|
||||||
|
return this.rootNodes.map(fileItem => new DynamicFlatNode(fileItem.name, 0, true, signal(false), fileItem));
|
||||||
|
//return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 0, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
isExpandable(fileItem: FileItem): boolean {
|
||||||
|
return fileItem.type === "directory"; //this.dataMap.has(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* File database, it can build a tree structured Json object from string.
|
||||||
|
* Each node in Json object represents a file or a directory. For a file, it has filename and type.
|
||||||
|
* For a directory, it has filename and children (a list of files or directories).
|
||||||
|
* The input will be a json object string, and the output is a list of `FileNode` with nested
|
||||||
|
* structure.
|
||||||
|
*/
|
||||||
|
export class DynamicDataSource implements DataSource<DynamicFlatNode> {
|
||||||
|
dataChange = new BehaviorSubject<DynamicFlatNode[]>([]);
|
||||||
|
|
||||||
|
get data(): DynamicFlatNode[] {
|
||||||
|
return this.dataChange.value;
|
||||||
|
}
|
||||||
|
set data(value: DynamicFlatNode[]) {
|
||||||
|
this._treeControl.dataNodes = value;
|
||||||
|
this.dataChange.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _treeControl: FlatTreeControl<DynamicFlatNode>,
|
||||||
|
private _database: DynamicDatabase,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode[]> {
|
||||||
|
this._treeControl.expansionModel.changed.subscribe(change => {
|
||||||
|
if (
|
||||||
|
(change as SelectionChange<DynamicFlatNode>).added ||
|
||||||
|
(change as SelectionChange<DynamicFlatNode>).removed
|
||||||
|
) {
|
||||||
|
this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(collectionViewer: CollectionViewer): void {}
|
||||||
|
|
||||||
|
/** Handle expand/collapse behaviors */
|
||||||
|
handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
|
||||||
|
if (change.added) {
|
||||||
|
change.added.forEach(node => this.toggleNode(node, true));
|
||||||
|
}
|
||||||
|
if (change.removed) {
|
||||||
|
change.removed
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.forEach(node => this.toggleNode(node, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the node, remove from display list
|
||||||
|
*/
|
||||||
|
toggleNode(node: DynamicFlatNode, expand: boolean) {
|
||||||
|
|
||||||
|
const index = this.data.indexOf(node);
|
||||||
|
|
||||||
|
this._database.getFileItems(node.fileItem.path).subscribe(value => {
|
||||||
|
console.log('value', value);
|
||||||
|
node.isLoading.set(true);
|
||||||
|
const children = value;
|
||||||
|
if (!children || index < 0) {
|
||||||
|
// If no children, or cannot find the node, no op
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand) {
|
||||||
|
const nodes = children.map(
|
||||||
|
fileItem => new DynamicFlatNode(fileItem.name, node.level + 1, this._database.isExpandable(fileItem), signal(false), fileItem,),
|
||||||
|
);
|
||||||
|
this.data.splice(index + 1, 0, ...nodes);
|
||||||
|
} else {
|
||||||
|
let count = 0;
|
||||||
|
for (
|
||||||
|
let i = index + 1;
|
||||||
|
i < this.data.length && this.data[i].level > node.level;
|
||||||
|
i++, count++
|
||||||
|
) {}
|
||||||
|
this.data.splice(index + 1, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify the change
|
||||||
|
this.dataChange.next(this.data);
|
||||||
|
node.isLoading.set(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
//
|
||||||
|
// }, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title Tree with dynamic data
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-file-system-tree-component',
|
||||||
|
templateUrl: 'file-system-tree-component.html',
|
||||||
|
styleUrl: 'file-system-tree-component.scss',
|
||||||
|
imports: [
|
||||||
|
MatTreeModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatTooltip
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class FileSystemTreeComponent {
|
||||||
|
database = inject(DynamicDatabase);
|
||||||
|
treeControl: FlatTreeControl<DynamicFlatNode>;
|
||||||
|
dataSource: DynamicDataSource;
|
||||||
|
selectedNode: DynamicFlatNode | null = null;
|
||||||
|
|
||||||
|
@Output() folderSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
|
||||||
|
this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
|
||||||
|
this.dataSource = new DynamicDataSource(this.treeControl, this.database);
|
||||||
|
|
||||||
|
// Initialize root data
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLevel = (node: DynamicFlatNode) => node.level;
|
||||||
|
isExpandable = (node: DynamicFlatNode) => node.expandable;
|
||||||
|
hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable;
|
||||||
|
|
||||||
|
selectNode(node: DynamicFlatNode|undefined): void {
|
||||||
|
|
||||||
|
if(node !== undefined){
|
||||||
|
this.selectedNode = node;
|
||||||
|
this.folderSelected.emit(node.fileItem.path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.selectedNode = null;
|
||||||
|
this.folderSelected.emit('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
refresh(): void {
|
||||||
|
this.selectNode(undefined);
|
||||||
|
|
||||||
|
this.treeControl.collapseAll();
|
||||||
|
|
||||||
|
this.database.getFileItems('/').subscribe(value => {
|
||||||
|
this.dataSource.data = value.map(fileItem =>
|
||||||
|
new DynamicFlatNode(
|
||||||
|
fileItem.name,
|
||||||
|
0,
|
||||||
|
fileItem.type === "directory",
|
||||||
|
signal(false),
|
||||||
|
fileItem
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -1,6 +1,13 @@
|
|||||||
<p>admin-welcome.component works!</p>
|
<p>admin-welcome.component works!</p>
|
||||||
|
|
||||||
<p>admin-welcome.component works!</p>
|
<p>admin-welcome.component works!</p>
|
||||||
|
|
||||||
|
<p>admin-welcome.component works!</p>
|
||||||
|
|
||||||
<p>admin-welcome.component works!</p><p>admin-welcome.component works!</p>
|
<p>admin-welcome.component works!</p><p>admin-welcome.component works!</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>admin-welcome.component works!</p>
|
||||||
<p>admin-welcome.component works!</p>
|
<p>admin-welcome.component works!</p>
|
||||||
<p>admin-welcome.component works!</p>
|
<p>admin-welcome.component works!</p>
|
||||||
<p>admin-welcome.component works!</p>
|
<p>admin-welcome.component works!</p>
|
||||||
|
|||||||
@@ -2,12 +2,21 @@ import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
|
|||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {AdminComponent} from './admin.component/admin.component';
|
import {AdminComponent} from './admin.component/admin.component';
|
||||||
import {adminRouting} from './admin.routing';
|
import {adminRouting} from './admin.routing';
|
||||||
|
import {MatTreeModule} from '@angular/material/tree';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
||||||
],
|
],
|
||||||
imports: [adminRouting, AdminComponent],
|
imports: [adminRouting, AdminComponent,
|
||||||
|
MatTreeModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatProgressSpinnerModule
|
||||||
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
schemas:[CUSTOM_ELEMENTS_SCHEMA]
|
schemas:[CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export const ADMIN_ROUTES: Routes = [
|
|||||||
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent),
|
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent),
|
||||||
data:{title: 'Users', icon:'', nav:true}
|
data:{title: 'Users', icon:'', nav:true}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'hard-drive-browser',
|
||||||
|
loadComponent: () => import('../admin-module/hard-drive-browser.component/hard-drive-browser.component').then((c)=> c.HardDriveBrowserComponent),
|
||||||
|
data:{title: 'Hard Drive', icon:'hard_drive', nav:true}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path:"data-base",
|
path:"data-base",
|
||||||
loadComponent: () => import('../admin-module/data-base.component/data-base.component').then((c) => c.DataBaseComponent),
|
loadComponent: () => import('../admin-module/data-base.component/data-base.component').then((c) => c.DataBaseComponent),
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
<app-file-browser-with-tree-component></app-file-browser-with-tree-component>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HardDriveBrowserComponent } from './hard-drive-browser.component';
|
||||||
|
|
||||||
|
describe('HardDriveBrowserComponent', () => {
|
||||||
|
let component: HardDriveBrowserComponent;
|
||||||
|
let fixture: ComponentFixture<HardDriveBrowserComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [HardDriveBrowserComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HardDriveBrowserComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
FileBrowserWithTreeComponent
|
||||||
|
} from '../../../components/file-browser-with-tree-component/file-browser-with-tree-component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-hard-drive-browser.component',
|
||||||
|
imports: [
|
||||||
|
FileBrowserWithTreeComponent
|
||||||
|
],
|
||||||
|
templateUrl: './hard-drive-browser.component.html',
|
||||||
|
styleUrl: './hard-drive-browser.component.scss'
|
||||||
|
})
|
||||||
|
export class HardDriveBrowserComponent {
|
||||||
|
|
||||||
|
}
|
||||||
+2
-1
@@ -19,7 +19,7 @@ import {
|
|||||||
MatDialogRef,
|
MatDialogRef,
|
||||||
MatDialogTitle
|
MatDialogTitle
|
||||||
} from '@angular/material/dialog';
|
} from '@angular/material/dialog';
|
||||||
import {MatFormField, MatInput, MatLabel, MatSuffix} from '@angular/material/input';
|
import {MatError, MatFormField, MatInput, MatLabel, MatSuffix} from '@angular/material/input';
|
||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
import {DialogSignupData} from '../main.component';
|
import {DialogSignupData} from '../main.component';
|
||||||
@@ -41,6 +41,7 @@ import {ErrorStateMatcher} from '@angular/material/core';
|
|||||||
MatSuffix,
|
MatSuffix,
|
||||||
MatFormField,
|
MatFormField,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
MatError,
|
||||||
|
|
||||||
],
|
],
|
||||||
templateUrl: './dialog-signup.component.html',
|
templateUrl: './dialog-signup.component.html',
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
<div class="docker-management">
|
||||||
|
<h2>Docker Container Management</h2>
|
||||||
|
|
||||||
|
<!-- Container List -->
|
||||||
|
<div class="containers-section">
|
||||||
|
<h3>Running Containers</h3>
|
||||||
|
<button (click)="refreshContainers()" class="btn btn-secondary">Refresh</button>
|
||||||
|
|
||||||
|
<div class="container-list" *ngIf="containers.length > 0; else noContainers">
|
||||||
|
<div class="container-item" *ngFor="let container of containers">
|
||||||
|
<div class="container-info">
|
||||||
|
<strong>{{container.name}}</strong>
|
||||||
|
<span class="image">{{container.image}}</span>
|
||||||
|
<span class="status" [class.running]="container.status.includes('Up')">
|
||||||
|
{{container.status}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="container-actions">
|
||||||
|
<button (click)="stopContainer(container.name)"
|
||||||
|
[disabled]="!container.status.includes('Up')"
|
||||||
|
class="btn btn-warning">Stop</button>
|
||||||
|
<button (click)="startContainer(container.name)"
|
||||||
|
[disabled]="container.status.includes('Up')"
|
||||||
|
class="btn btn-success">Start</button>
|
||||||
|
<button (click)="removeContainer(container.name)"
|
||||||
|
class="btn btn-danger">Remove</button>
|
||||||
|
<button (click)="viewLogs(container.name)"
|
||||||
|
class="btn btn-info">Logs</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #noContainers>
|
||||||
|
<p>No containers found</p>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Run New Container -->
|
||||||
|
<div class="run-container-section">
|
||||||
|
<h3>Run New Container</h3>
|
||||||
|
<form [formGroup]="runContainerForm" (ngSubmit)="runContainer()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Image Name:</label>
|
||||||
|
<input type="text" formControlName="imageName"
|
||||||
|
placeholder="e.g., nginx:latest" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Container Name:</label>
|
||||||
|
<input type="text" formControlName="containerName"
|
||||||
|
placeholder="Optional container name" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ports:</label>
|
||||||
|
<input type="text" formControlName="ports"
|
||||||
|
placeholder="e.g., 8080:80" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Environment Variables:</label>
|
||||||
|
<input type="text" formControlName="environmentVars"
|
||||||
|
placeholder="e.g., -e NODE_ENV=production" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" [disabled]="runContainerForm.invalid || loading"
|
||||||
|
class="btn btn-primary">
|
||||||
|
{{loading ? 'Starting...' : 'Run Container'}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Execute Command -->
|
||||||
|
<div class="execute-command-section">
|
||||||
|
<h3>Execute Docker Command</h3>
|
||||||
|
<form [formGroup]="commandForm" (ngSubmit)="executeCommand()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Docker Command:</label>
|
||||||
|
<input type="text" formControlName="command"
|
||||||
|
placeholder="e.g., docker ps -a" class="form-control">
|
||||||
|
</div>
|
||||||
|
<button type="submit" [disabled]="commandForm.invalid || loading"
|
||||||
|
class="btn btn-primary">Execute</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Command Output -->
|
||||||
|
<div class="output-section" *ngIf="lastCommandResult">
|
||||||
|
<h3>Command Output</h3>
|
||||||
|
<div class="output-box"
|
||||||
|
[class.success]="lastCommandResult.success"
|
||||||
|
[class.error]="!lastCommandResult.success">
|
||||||
|
<pre>{{getOutputText(lastCommandResult.output)}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Container Logs Modal -->
|
||||||
|
<div class="logs-modal" *ngIf="selectedContainerLogs">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Container Logs: {{selectedContainer}}</h3>
|
||||||
|
<button (click)="closeLogs()" class="close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre class="logs-content">{{selectedContainerLogs.join('\n')}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DockerManagementComponent } from './docker-management-component';
|
||||||
|
|
||||||
|
describe('DockerManagementComponent', () => {
|
||||||
|
let component: DockerManagementComponent;
|
||||||
|
let fixture: ComponentFixture<DockerManagementComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DockerManagementComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DockerManagementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {NgForOf, NgIf} from '@angular/common';
|
||||||
|
import {Subject, takeUntil} from 'rxjs';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
|
import {ContainerInfo, DockerResult, DockerService} from '../docker.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-docker-management-component',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgForOf
|
||||||
|
],
|
||||||
|
templateUrl: './docker-management-component.html',
|
||||||
|
styleUrl: './docker-management-component.scss'
|
||||||
|
})
|
||||||
|
export class DockerManagementComponent implements OnInit, OnDestroy {
|
||||||
|
containers: ContainerInfo[] = [];
|
||||||
|
runContainerForm: FormGroup;
|
||||||
|
commandForm: FormGroup;
|
||||||
|
lastCommandResult: DockerResult | null = null;
|
||||||
|
selectedContainer: string = '';
|
||||||
|
selectedContainerLogs: string[] | null = null;
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dockerService: DockerService,
|
||||||
|
private fb: FormBuilder
|
||||||
|
) {
|
||||||
|
this.runContainerForm = this.fb.group({
|
||||||
|
imageName: ['', Validators.required],
|
||||||
|
containerName: [''],
|
||||||
|
ports: [''],
|
||||||
|
environmentVars: ['']
|
||||||
|
});
|
||||||
|
|
||||||
|
this.commandForm = this.fb.group({
|
||||||
|
command: ['docker ps', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Subscribe to containers updates
|
||||||
|
this.dockerService.containers$
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(containers => {
|
||||||
|
this.containers = containers;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
this.refreshContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshContainers() {
|
||||||
|
this.dockerService.refreshContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
runContainer() {
|
||||||
|
if (this.runContainerForm.valid) {
|
||||||
|
this.loading = true;
|
||||||
|
const formValue = this.runContainerForm.value;
|
||||||
|
|
||||||
|
this.dockerService.runContainer(formValue)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.lastCommandResult = result;
|
||||||
|
this.loading = false;
|
||||||
|
if (result.success) {
|
||||||
|
this.runContainerForm.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error running container:', error);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopContainer(containerName: string) {
|
||||||
|
this.dockerService.stopContainer(containerName)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.lastCommandResult = result;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error stopping container:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startContainer(containerName: string) {
|
||||||
|
this.dockerService.startContainer(containerName)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.lastCommandResult = result;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error starting container:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeContainer(containerName: string) {
|
||||||
|
if (confirm(`Are you sure you want to remove container "${containerName}"?`)) {
|
||||||
|
this.dockerService.removeContainer(containerName)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.lastCommandResult = result;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error removing container:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLogs(containerName: string) {
|
||||||
|
this.selectedContainer = containerName;
|
||||||
|
this.dockerService.getContainerLogs(containerName, 100)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (logs) => {
|
||||||
|
this.selectedContainerLogs = logs;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error getting logs:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeLogs() {
|
||||||
|
this.selectedContainerLogs = null;
|
||||||
|
this.selectedContainer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommand() {
|
||||||
|
if (this.commandForm.valid) {
|
||||||
|
this.loading = true;
|
||||||
|
const command = this.commandForm.get('command')?.value;
|
||||||
|
|
||||||
|
this.dockerService.executeCommand(command)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.lastCommandResult = result;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error executing command:', error);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutputText(output: string[] | string): string {
|
||||||
|
return Array.isArray(output) ? output.join('\n') : output;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DockerService } from './docker.service';
|
||||||
|
|
||||||
|
describe('DockerService', () => {
|
||||||
|
let service: DockerService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(DockerService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
// docker.service.ts
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
import { map, catchError } from 'rxjs/operators';
|
||||||
|
import { throwError } from 'rxjs';
|
||||||
|
import {GlobalConstants} from '../../global-constants';
|
||||||
|
|
||||||
|
// Interfaces for type safety
|
||||||
|
export interface DockerResult {
|
||||||
|
success: boolean;
|
||||||
|
exitCode?: number;
|
||||||
|
output: string[] | string;
|
||||||
|
command?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerRunRequest {
|
||||||
|
imageName: string;
|
||||||
|
containerName?: string;
|
||||||
|
ports?: string;
|
||||||
|
environmentVars?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerInfo {
|
||||||
|
containerId: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
status: string;
|
||||||
|
ports: string;
|
||||||
|
created: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandRequest {
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecCommandRequest {
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DockerService {
|
||||||
|
baseUrl = `${GlobalConstants.API_URL}/user/docker`;
|
||||||
|
|
||||||
|
private readonly httpOptions = {
|
||||||
|
headers: new HttpHeaders({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Observable for real-time container status updates
|
||||||
|
private containersSubject = new BehaviorSubject<ContainerInfo[]>([]);
|
||||||
|
public containers$ = this.containersSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
// Auto-refresh containers every 30 seconds
|
||||||
|
//this.startAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute arbitrary Docker command
|
||||||
|
*/
|
||||||
|
executeCommand(command: string): Observable<DockerResult> {
|
||||||
|
const request: CommandRequest = { command };
|
||||||
|
|
||||||
|
return this.http.post<DockerResult>(`${this.baseUrl}/execute`, request, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(response => this.normalizeDockerResult(response)),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute Docker command asynchronously
|
||||||
|
*/
|
||||||
|
executeCommandAsync(command: string): Observable<DockerResult> {
|
||||||
|
const request: CommandRequest = { command };
|
||||||
|
|
||||||
|
return this.http.post<DockerResult>(`${this.baseUrl}/execute/async`, request, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(response => this.normalizeDockerResult(response)),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a Docker container
|
||||||
|
*/
|
||||||
|
runContainer(request: ContainerRunRequest): Observable<DockerResult> {
|
||||||
|
return this.http.post<DockerResult>(`${this.baseUrl}/container/run`, request, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(response => {
|
||||||
|
// Refresh containers list after successful container creation
|
||||||
|
if (response.success) {
|
||||||
|
this.refreshContainers();
|
||||||
|
}
|
||||||
|
return this.normalizeDockerResult(response);
|
||||||
|
}),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a Docker container
|
||||||
|
*/
|
||||||
|
stopContainer(containerName: string): Observable<DockerResult> {
|
||||||
|
return this.http.post<DockerResult>(`${this.baseUrl}/container/${containerName}/stop`, {}, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(response => {
|
||||||
|
// Refresh containers list after stopping
|
||||||
|
if (response.success) {
|
||||||
|
this.refreshContainers();
|
||||||
|
}
|
||||||
|
return this.normalizeDockerResult(response);
|
||||||
|
}),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a Docker container
|
||||||
|
*/
|
||||||
|
startContainer(containerName: string): Observable<DockerResult> {
|
||||||
|
return this.executeCommand(`docker start ${containerName}`)
|
||||||
|
.pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.refreshContainers();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a Docker container
|
||||||
|
*/
|
||||||
|
removeContainer(containerName: string, force: boolean = false): Observable<DockerResult> {
|
||||||
|
const command = force ? `docker rm -f ${containerName}` : `docker rm ${containerName}`;
|
||||||
|
|
||||||
|
return this.executeCommand(command)
|
||||||
|
.pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.refreshContainers();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of all containers
|
||||||
|
*/
|
||||||
|
listContainers(): Observable<ContainerInfo[]> {
|
||||||
|
return this.http.get<{success: boolean, containers: string[]}>(`${this.baseUrl}/containers`)
|
||||||
|
.pipe(
|
||||||
|
map(response => {
|
||||||
|
if (response.success && response.containers) {
|
||||||
|
const containers = this.parseContainerList(response.containers);
|
||||||
|
this.containersSubject.next(containers);
|
||||||
|
return containers;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get running containers only
|
||||||
|
*/
|
||||||
|
listRunningContainers(): Observable<ContainerInfo[]> {
|
||||||
|
return this.listContainers().pipe(
|
||||||
|
map(containers => containers.filter(c => c.status.includes('Up')))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get container logs
|
||||||
|
*/
|
||||||
|
getContainerLogs(containerName: string, lines: number = 100): Observable<string[]> {
|
||||||
|
const params = new HttpParams().set('lines', lines.toString());
|
||||||
|
|
||||||
|
return this.http.get<{success: boolean, logs: string[]}>(`${this.baseUrl}/container/${containerName}/logs`, { params })
|
||||||
|
.pipe(
|
||||||
|
map(response => response.success ? response.logs : []),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute command inside container
|
||||||
|
*/
|
||||||
|
execInContainer(containerName: string, command: string): Observable<DockerResult> {
|
||||||
|
const request: ExecCommandRequest = { command };
|
||||||
|
|
||||||
|
return this.http.post<DockerResult>(`${this.baseUrl}/container/${containerName}/exec`, request, this.httpOptions)
|
||||||
|
.pipe(
|
||||||
|
map(response => this.normalizeDockerResult(response)),
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Docker image
|
||||||
|
*/
|
||||||
|
buildImage(imageName: string, dockerfilePath: string = '.', tag?: string): Observable<DockerResult> {
|
||||||
|
const fullTag = tag ? `${imageName}:${tag}` : imageName;
|
||||||
|
const command = `docker build -t ${fullTag} ${dockerfilePath}`;
|
||||||
|
|
||||||
|
return this.executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull Docker image
|
||||||
|
*/
|
||||||
|
pullImage(imageName: string, tag: string = 'latest'): Observable<DockerResult> {
|
||||||
|
const command = `docker pull ${imageName}:${tag}`;
|
||||||
|
return this.executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Docker images
|
||||||
|
*/
|
||||||
|
listImages(): Observable<DockerResult> {
|
||||||
|
return this.executeCommand('docker images');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove Docker image
|
||||||
|
*/
|
||||||
|
removeImage(imageName: string, force: boolean = false): Observable<DockerResult> {
|
||||||
|
const command = force ? `docker rmi -f ${imageName}` : `docker rmi ${imageName}`;
|
||||||
|
return this.executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker system info
|
||||||
|
*/
|
||||||
|
getSystemInfo(): Observable<DockerResult> {
|
||||||
|
return this.executeCommand('docker system info');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker version info
|
||||||
|
*/
|
||||||
|
getVersion(): Observable<DockerResult> {
|
||||||
|
return this.executeCommand('docker --version');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Docker is running
|
||||||
|
*/
|
||||||
|
isDockerRunning(): Observable<boolean> {
|
||||||
|
return this.executeCommand('docker ps').pipe(
|
||||||
|
map(result => result.success),
|
||||||
|
catchError(() => [false])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Docker Compose up
|
||||||
|
*/
|
||||||
|
dockerComposeUp(composePath: string = 'docker-compose.yml'): Observable<DockerResult> {
|
||||||
|
return this.executeCommand(`docker-compose -f ${composePath} up -d`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Docker Compose down
|
||||||
|
*/
|
||||||
|
dockerComposeDown(composePath: string = 'docker-compose.yml'): Observable<DockerResult> {
|
||||||
|
return this.executeCommand(`docker-compose -f ${composePath} down`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Docker Compose services status
|
||||||
|
*/
|
||||||
|
dockerComposePs(composePath: string = 'docker-compose.yml'): Observable<DockerResult> {
|
||||||
|
return this.executeCommand(`docker-compose -f ${composePath} ps`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh containers list
|
||||||
|
*/
|
||||||
|
refreshContainers(): void {
|
||||||
|
this.listContainers().subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start auto-refresh of containers
|
||||||
|
*/
|
||||||
|
private startAutoRefresh(): void {
|
||||||
|
setInterval(() => {
|
||||||
|
this.refreshContainers();
|
||||||
|
}, 30000); // Refresh every 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse container list output into structured data
|
||||||
|
*/
|
||||||
|
private parseContainerList(containerLines: string[]): ContainerInfo[] {
|
||||||
|
const containers: ContainerInfo[] = [];
|
||||||
|
|
||||||
|
// Skip header line
|
||||||
|
const dataLines = containerLines.slice(1);
|
||||||
|
|
||||||
|
dataLines.forEach(line => {
|
||||||
|
if (line.trim()) {
|
||||||
|
const parts = line.split(/\s{2,}/); // Split by 2 or more spaces
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
containers.push({
|
||||||
|
containerId: parts[0],
|
||||||
|
image: parts[1],
|
||||||
|
name: parts[6] || parts[parts.length - 1],
|
||||||
|
status: parts[4],
|
||||||
|
ports: parts[5] || '',
|
||||||
|
created: parts[3]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return containers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize Docker result response
|
||||||
|
*/
|
||||||
|
private normalizeDockerResult(response: any): DockerResult {
|
||||||
|
return {
|
||||||
|
success: response.success || false,
|
||||||
|
exitCode: response.exitCode,
|
||||||
|
output: response.output || response.logs || [],
|
||||||
|
command: response.command,
|
||||||
|
error: response.error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTTP errors
|
||||||
|
*/
|
||||||
|
private handleError = (error: any): Observable<never> => {
|
||||||
|
console.error('Docker Service Error:', error);
|
||||||
|
|
||||||
|
let errorMessage = 'An unknown error occurred';
|
||||||
|
if (error.error instanceof ErrorEvent) {
|
||||||
|
// Client-side error
|
||||||
|
errorMessage = error.error.message;
|
||||||
|
} else {
|
||||||
|
// Server-side error
|
||||||
|
errorMessage = error.error?.message || error.message || `Error Code: ${error.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// docker-models.ts - Additional models file
|
||||||
|
export interface DockerStats {
|
||||||
|
containerId: string;
|
||||||
|
name: string;
|
||||||
|
cpuPercentage: string;
|
||||||
|
memoryUsage: string;
|
||||||
|
memoryPercentage: string;
|
||||||
|
networkIO: string;
|
||||||
|
blockIO: string;
|
||||||
|
pids: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockerImage {
|
||||||
|
repository: string;
|
||||||
|
tag: string;
|
||||||
|
imageId: string;
|
||||||
|
created: string;
|
||||||
|
size: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockerVolume {
|
||||||
|
driver: string;
|
||||||
|
name: string;
|
||||||
|
mountpoint: string;
|
||||||
|
created: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended service methods
|
||||||
|
export class DockerExtendedService extends DockerService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get container statistics
|
||||||
|
*/
|
||||||
|
getContainerStats(containerName: string): Observable<DockerStats[]> {
|
||||||
|
return this.executeCommand(`docker stats ${containerName} --no-stream --format "table {{.Container}}\\t{{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}\\t{{.NetIO}}\\t{{.BlockIO}}\\t{{.PIDs}}"`)
|
||||||
|
.pipe(
|
||||||
|
map(result => {
|
||||||
|
if (result.success && Array.isArray(result.output)) {
|
||||||
|
return this.parseStatsOutput(result.output);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Docker volumes
|
||||||
|
*/
|
||||||
|
listVolumes(): Observable<DockerVolume[]> {
|
||||||
|
return this.executeCommand('docker volume ls --format "table {{.Driver}}\\t{{.Name}}"')
|
||||||
|
.pipe(
|
||||||
|
map(result => {
|
||||||
|
if (result.success && Array.isArray(result.output)) {
|
||||||
|
return this.parseVolumeOutput(result.output);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up Docker system
|
||||||
|
*/
|
||||||
|
systemPrune(force: boolean = false): Observable<DockerResult> {
|
||||||
|
const command = force ? 'docker system prune -f' : 'docker system prune';
|
||||||
|
return this.executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseStatsOutput(output: string[]): DockerStats[] {
|
||||||
|
const stats: DockerStats[] = [];
|
||||||
|
const dataLines = output.slice(1); // Skip header
|
||||||
|
|
||||||
|
dataLines.forEach(line => {
|
||||||
|
const parts = line.split('\t');
|
||||||
|
if (parts.length >= 8) {
|
||||||
|
stats.push({
|
||||||
|
containerId: parts[0],
|
||||||
|
name: parts[1],
|
||||||
|
cpuPercentage: parts[2],
|
||||||
|
memoryUsage: parts[3],
|
||||||
|
memoryPercentage: parts[4],
|
||||||
|
networkIO: parts[5],
|
||||||
|
blockIO: parts[6],
|
||||||
|
pids: parts[7]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseVolumeOutput(output: string[]): DockerVolume[] {
|
||||||
|
const volumes: DockerVolume[] = [];
|
||||||
|
const dataLines = output.slice(1); // Skip header
|
||||||
|
|
||||||
|
dataLines.forEach(line => {
|
||||||
|
const parts = line.split(/\s+/);
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
volumes.push({
|
||||||
|
driver: parts[0],
|
||||||
|
name: parts[1],
|
||||||
|
mountpoint: '',
|
||||||
|
created: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,11 @@ export const USER_ROUTS: Routes = [
|
|||||||
path: 'images',
|
path: 'images',
|
||||||
loadComponent: () => import('../user-module/images.component/images.component').then((c) => c.ImagesComponent),
|
loadComponent: () => import('../user-module/images.component/images.component').then((c) => c.ImagesComponent),
|
||||||
data:{title: 'Images', icon: 'imagesmode', nav:true}
|
data:{title: 'Images', icon: 'imagesmode', nav:true}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'docker',
|
||||||
|
loadComponent: () => import('../user-module/docker-management-component/docker-management-component').then((c)=>c.DockerManagementComponent),
|
||||||
|
data:{title: 'Docker', icon: 'docker', nav:true}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FileBrowserTreeService } from './file-browser-tree.service';
|
||||||
|
|
||||||
|
describe('FileBrowserTreeService', () => {
|
||||||
|
let service: FileBrowserTreeService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(FileBrowserTreeService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {GlobalConstants} from '../global-constants';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {map} from 'rxjs/operators';
|
||||||
|
|
||||||
|
export interface FileItem {
|
||||||
|
name: string;
|
||||||
|
type: 'file' | 'directory';
|
||||||
|
size: number;
|
||||||
|
lastModified: Date;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FileBrowserTreeService {
|
||||||
|
private baseUrl = `${GlobalConstants.API_URL}/file-browser-tree/files`;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getFiles(path: string): Observable<FileItem[]> {
|
||||||
|
const encodedPath = encodeURIComponent(path);
|
||||||
|
return this.http.get<FileItem[]>(`${this.baseUrl}?path=${encodedPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to get only folders for tree view
|
||||||
|
getFolders(path: string): Observable<FileItem[]> {
|
||||||
|
const encodedPath = encodeURIComponent(path);
|
||||||
|
return this.http.get<FileItem[]>(`${this.baseUrl}?path=${encodedPath}`)
|
||||||
|
.pipe(
|
||||||
|
map(files => files.filter(file => file.type === 'directory'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternative: dedicated backend endpoint for folders only
|
||||||
|
getFoldersOnly(path: string): Observable<FileItem[]> {
|
||||||
|
const encodedPath = encodeURIComponent(path);
|
||||||
|
return this.http.get<FileItem[]>(`${this.baseUrl}/folders?path=${encodedPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(filePath: string): Observable<Blob> {
|
||||||
|
const encodedPath = encodeURIComponent(filePath);
|
||||||
|
return this.http.get(`${this.baseUrl}/download?path=${encodedPath}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile(path: string, file: File): Observable<any> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('path', path);
|
||||||
|
|
||||||
|
return this.http.post(`${this.baseUrl}/upload`, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
createFolder(path: string, folderName: string): Observable<any> {
|
||||||
|
return this.http.post(`${this.baseUrl}/folder`, {
|
||||||
|
path: path,
|
||||||
|
name: folderName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFile(filePath: string): Observable<any> {
|
||||||
|
const encodedPath = encodeURIComponent(filePath);
|
||||||
|
return this.http.delete(`${this.baseUrl}?path=${encodedPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
searchFiles(path: string, query: string): Observable<FileItem[]> {
|
||||||
|
const encodedPath = encodeURIComponent(path);
|
||||||
|
const encodedQuery = encodeURIComponent(query);
|
||||||
|
return this.http.get<FileItem[]>(`${this.baseUrl}/search?path=${encodedPath}&query=${encodedQuery}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move/rename functionality
|
||||||
|
moveFile(sourcePath: string, targetPath: string): Observable<any> {
|
||||||
|
return this.http.post(`${this.baseUrl}/move`, {
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
targetPath: targetPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy functionality
|
||||||
|
copyFile(sourcePath: string, targetPath: string): Observable<any> {
|
||||||
|
return this.http.post(`${this.baseUrl}/copy`, {
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
targetPath: targetPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,10 @@
|
|||||||
#RUN ./gradlew build -x test
|
#RUN ./gradlew build -x test
|
||||||
|
|
||||||
FROM openjdk:24
|
FROM openjdk:24
|
||||||
|
|
||||||
|
ADD https://download.docker.com/linux/static/stable/x86_64/docker-24.0.5.tgz /tmp/
|
||||||
|
RUN tar xzvf /tmp/docker-24.0.5.tgz -C /usr/local/bin --strip-components=1 docker/docker
|
||||||
|
|
||||||
WORKDIR /jambotron/
|
WORKDIR /jambotron/
|
||||||
#VOLUME /jambotron_data/uploads
|
#VOLUME /jambotron_data/uploads
|
||||||
COPY './build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar'
|
COPY './build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar'
|
||||||
|
|||||||
@@ -13,8 +13,16 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- certs:/certs
|
- certs:/certs
|
||||||
- jambotron_data:/jambotron_data
|
- jambotron_data:/jambotron_data
|
||||||
|
# Mount host Docker socket
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /usr/bin/docker:/usr/bin/docker
|
||||||
# env_file: "webapp.env"
|
# env_file: "webapp.env"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
|
|
||||||
|
DOCKER_HOST: unix:///var/run/docker.sock
|
||||||
|
|
||||||
SSL_ENABLED: "true"
|
SSL_ENABLED: "true"
|
||||||
SERVER_PORT: 443
|
SERVER_PORT: 443
|
||||||
FULLCHAINPEM: /certs/live/jambotron.run.place/fullchain.pem
|
FULLCHAINPEM: /certs/live/jambotron.run.place/fullchain.pem
|
||||||
@@ -29,7 +37,7 @@ services:
|
|||||||
SPRING_FLYWAY_USER: admin
|
SPRING_FLYWAY_USER: admin
|
||||||
SPRING_FLYWAY_PASSWORD: postgrespw
|
SPRING_FLYWAY_PASSWORD: postgrespw
|
||||||
SPRING_FLYWAY_URL: jdbc:postgresql://postgres_jambotron:5432/jambotronDB
|
SPRING_FLYWAY_URL: jdbc:postgresql://postgres_jambotron:5432/jambotronDB
|
||||||
|
# Uncomment the following lines to use Koyeb secrets for database credentials
|
||||||
# SPRING_DATASOURCE_URL: jdbc:postgresql://ep-red-breeze-a2cxhq5f.eu-central-1.pg.koyeb.app:5432/jambotronDB
|
# SPRING_DATASOURCE_URL: jdbc:postgresql://ep-red-breeze-a2cxhq5f.eu-central-1.pg.koyeb.app:5432/jambotronDB
|
||||||
# SPRING_DATASOURCE_USERNAME: koyeb-adm
|
# SPRING_DATASOURCE_USERNAME: koyeb-adm
|
||||||
# SPRING_DATASOURCE_PASSWORD: npg_HfFEUA7bay1i
|
# SPRING_DATASOURCE_PASSWORD: npg_HfFEUA7bay1i
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package com.jambotronGroup.jambotron.controllers;
|
||||||
|
|
||||||
|
import com.jambotronGroup.jambotron.docker.ContainerRunRequest;
|
||||||
|
import com.jambotronGroup.jambotron.docker.DockerResult;
|
||||||
|
import com.jambotronGroup.jambotron.docker.DockerService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/user/docker")
|
||||||
|
public class DockerController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DockerService dockerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить произвольную Docker команду
|
||||||
|
*/
|
||||||
|
@PostMapping("/execute")
|
||||||
|
public ResponseEntity<?> executeCommand(@RequestBody Map<String, String> request) {
|
||||||
|
String command = request.get("command");
|
||||||
|
|
||||||
|
if (command == null || command.trim().isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("Команда не может быть пустой");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DockerResult result = dockerService.executeCommand(command);
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"exitCode", result.getExitCode(),
|
||||||
|
"output", result.getOutput(),
|
||||||
|
"command", result.getCommand()
|
||||||
|
));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить контейнер
|
||||||
|
*/
|
||||||
|
@PostMapping("/container/run")
|
||||||
|
public ResponseEntity<?> runContainer(@RequestBody ContainerRunRequest request) {
|
||||||
|
DockerResult result = dockerService.runContainer(
|
||||||
|
request.getImageName(),
|
||||||
|
request.getContainerName(),
|
||||||
|
request.getPorts(),
|
||||||
|
request.getEnvironmentVars()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"output", result.getOutputAsString(),
|
||||||
|
"containerId", result.isSuccess() && !result.getOutput().isEmpty()
|
||||||
|
? result.getOutput().get(0) : null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановить контейнер
|
||||||
|
*/
|
||||||
|
@PostMapping("/container/{containerName}/stop")
|
||||||
|
public ResponseEntity<?> stopContainer(@PathVariable String containerName) {
|
||||||
|
DockerResult result = dockerService.stopContainer(containerName);
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"output", result.getOutputAsString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список контейнеров
|
||||||
|
*/
|
||||||
|
@GetMapping("/containers")
|
||||||
|
public ResponseEntity<?> listContainers() {
|
||||||
|
DockerResult result = dockerService.listContainers();
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"containers", result.getOutput()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить логи контейнера
|
||||||
|
*/
|
||||||
|
@GetMapping("/container/{containerName}/logs")
|
||||||
|
public ResponseEntity<?> getContainerLogs(
|
||||||
|
@PathVariable String containerName,
|
||||||
|
@RequestParam(defaultValue = "100") int lines) {
|
||||||
|
|
||||||
|
DockerResult result = dockerService.getContainerLogs(containerName, lines);
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"logs", result.getOutput()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить команду в контейнере
|
||||||
|
*/
|
||||||
|
@PostMapping("/container/{containerName}/exec")
|
||||||
|
public ResponseEntity<?> execInContainer(
|
||||||
|
@PathVariable String containerName,
|
||||||
|
@RequestBody Map<String, String> request) {
|
||||||
|
|
||||||
|
String command = request.get("command");
|
||||||
|
DockerResult result = dockerService.execInContainer(containerName, command);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"output", result.getOutput()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Асинхронное выполнение команды
|
||||||
|
*/
|
||||||
|
@PostMapping("/execute/async")
|
||||||
|
public CompletableFuture<ResponseEntity<Map<String, Object>>> executeCommandAsync(
|
||||||
|
@RequestBody Map<String, String> request) {
|
||||||
|
|
||||||
|
String command = request.get("command");
|
||||||
|
|
||||||
|
return dockerService.executeCommandAsync(command)
|
||||||
|
.thenApply(result -> ResponseEntity.ok(Map.of(
|
||||||
|
"success", result.isSuccess(),
|
||||||
|
"output", result.getOutput(),
|
||||||
|
"exitCode", result.getExitCode()
|
||||||
|
)))
|
||||||
|
.exceptionally(ex -> ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", ex.getMessage())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
package com.jambotronGroup.jambotron.controllers;
|
||||||
|
|
||||||
|
import com.jambotronGroup.jambotron.fileBrowserTree.CreateFolderRequest;
|
||||||
|
import com.jambotronGroup.jambotron.fileBrowserTree.FileItemDto;
|
||||||
|
import com.jambotronGroup.jambotron.fileBrowserTree.FileBrowserTreeService;
|
||||||
|
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/file-browser-tree/files")
|
||||||
|
public class FileBrowserTreeController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileBrowserTreeService fileBrowserTreeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<FileItemDto>> getFiles(@RequestParam String path) {
|
||||||
|
try {
|
||||||
|
List<FileItemDto> files = fileBrowserTreeService.listFiles(path);
|
||||||
|
return ResponseEntity.ok(files);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New endpoint specifically for folders only (for tree view)
|
||||||
|
@GetMapping("/folders")
|
||||||
|
public ResponseEntity<List<FileItemDto>> getFolders(@RequestParam String path) {
|
||||||
|
try {
|
||||||
|
List<FileItemDto> folders = fileBrowserTreeService.listFolders(path);
|
||||||
|
return ResponseEntity.ok(folders);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search endpoint
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ResponseEntity<List<FileItemDto>> searchFiles(
|
||||||
|
@RequestParam String path,
|
||||||
|
@RequestParam String query) {
|
||||||
|
try {
|
||||||
|
List<FileItemDto> files = fileBrowserTreeService.searchFiles(path, query);
|
||||||
|
return ResponseEntity.ok(files);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download")
|
||||||
|
public ResponseEntity<Resource> downloadFile(@RequestParam String path) {
|
||||||
|
try {
|
||||||
|
Resource resource = fileBrowserTreeService.loadFileAsResource(path);
|
||||||
|
String filename = resource.getFilename();
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"" + filename + "\"")
|
||||||
|
.body(resource);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public ResponseEntity<ResponseMessage> uploadFile(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam("path") String path) {
|
||||||
|
try {
|
||||||
|
fileBrowserTreeService.storeFile(file, path);
|
||||||
|
return ResponseEntity.ok(new ResponseMessage("File uploaded successfully: " + file.getOriginalFilename()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseMessage("Failed to upload file: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/folder")
|
||||||
|
public ResponseEntity<ResponseMessage> createFolder(@RequestBody CreateFolderRequest request) {
|
||||||
|
try {
|
||||||
|
fileBrowserTreeService.createFolder(request.getPath(), request.getName());
|
||||||
|
return ResponseEntity.ok(new ResponseMessage("Folder created successfully: " + request.getName()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseMessage("Failed to create folder: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ResponseEntity<ResponseMessage> deleteFile(@RequestParam String path) {
|
||||||
|
try {
|
||||||
|
fileBrowserTreeService.deleteFile(path);
|
||||||
|
return ResponseEntity.ok(new ResponseMessage("File deleted successfully: " + path));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseMessage("Failed to delete file/folder: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move/rename endpoint
|
||||||
|
@PostMapping("/move")
|
||||||
|
public ResponseEntity<ResponseMessage> moveFile(@RequestBody MoveRequest request) {
|
||||||
|
try {
|
||||||
|
fileBrowserTreeService.moveFile(request.getSourcePath(), request.getTargetPath());
|
||||||
|
return ResponseEntity.ok(new ResponseMessage("File moved successfully from " + request.getSourcePath() + " to " + request.getTargetPath()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseMessage("Failed to move file: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy endpoint
|
||||||
|
@PostMapping("/copy")
|
||||||
|
public ResponseEntity<ResponseMessage> copyFile(@RequestBody CopyRequest request) {
|
||||||
|
try {
|
||||||
|
fileBrowserTreeService.copyFile(request.getSourcePath(), request.getTargetPath());
|
||||||
|
return ResponseEntity.ok(new ResponseMessage("File copied successfully from " + request.getSourcePath() + " to " + request.getTargetPath()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseMessage("Failed to copy file: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner classes for request DTOs
|
||||||
|
public static class MoveRequest {
|
||||||
|
private String sourcePath;
|
||||||
|
private String targetPath;
|
||||||
|
|
||||||
|
public String getSourcePath() { return sourcePath; }
|
||||||
|
public void setSourcePath(String sourcePath) { this.sourcePath = sourcePath; }
|
||||||
|
public String getTargetPath() { return targetPath; }
|
||||||
|
public void setTargetPath(String targetPath) { this.targetPath = targetPath; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CopyRequest {
|
||||||
|
private String sourcePath;
|
||||||
|
private String targetPath;
|
||||||
|
|
||||||
|
public String getSourcePath() { return sourcePath; }
|
||||||
|
public void setSourcePath(String sourcePath) { this.sourcePath = sourcePath; }
|
||||||
|
public String getTargetPath() { return targetPath; }
|
||||||
|
public void setTargetPath(String targetPath) { this.targetPath = targetPath; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.jambotronGroup.jambotron.docker;
|
||||||
|
|
||||||
|
public class ContainerRunRequest {
|
||||||
|
private String imageName;
|
||||||
|
private String containerName;
|
||||||
|
private String ports;
|
||||||
|
private String environmentVars;
|
||||||
|
|
||||||
|
// Getters and setters
|
||||||
|
public String getImageName() { return imageName; }
|
||||||
|
public void setImageName(String imageName) { this.imageName = imageName; }
|
||||||
|
|
||||||
|
public String getContainerName() { return containerName; }
|
||||||
|
public void setContainerName(String containerName) { this.containerName = containerName; }
|
||||||
|
|
||||||
|
public String getPorts() { return ports; }
|
||||||
|
public void setPorts(String ports) { this.ports = ports; }
|
||||||
|
|
||||||
|
public String getEnvironmentVars() { return environmentVars; }
|
||||||
|
public void setEnvironmentVars(String environmentVars) { this.environmentVars = environmentVars; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.jambotronGroup.jambotron.docker;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DockerResult {
|
||||||
|
private final boolean success;
|
||||||
|
private final int exitCode;
|
||||||
|
private final List<String> output;
|
||||||
|
private final String command;
|
||||||
|
|
||||||
|
public DockerResult(boolean success, int exitCode, List<String> output, String command) {
|
||||||
|
this.success = success;
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
this.output = output;
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() { return success; }
|
||||||
|
public int getExitCode() { return exitCode; }
|
||||||
|
public List<String> getOutput() { return output; }
|
||||||
|
public String getCommand() { return command; }
|
||||||
|
public String getOutputAsString() { return String.join("\n", output); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("DockerResult{success=%s, exitCode=%d, command='%s', output='%s'}",
|
||||||
|
success, exitCode, command, getOutputAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package com.jambotronGroup.jambotron.docker;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DockerService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DockerService.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить Docker команду синхронно
|
||||||
|
*/
|
||||||
|
public DockerResult executeCommand(String command) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
||||||
|
|
||||||
|
if (os.contains("win")) {
|
||||||
|
processBuilder.command("cmd.exe", "/c", command);
|
||||||
|
} else {
|
||||||
|
processBuilder.command("bash", "-c", command);
|
||||||
|
}
|
||||||
|
processBuilder.redirectErrorStream(true);
|
||||||
|
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
|
||||||
|
List<String> output = new ArrayList<>();
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.add(line);
|
||||||
|
logger.info("Docker output: {}", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
|
||||||
|
return new DockerResult(exitCode == 0, exitCode, output, command);
|
||||||
|
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
logger.error("Ошибка выполнения Docker команды: {}", command, e);
|
||||||
|
return new DockerResult(false, -1, List.of("Error: " + e.getMessage()), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить Docker команду асинхронно
|
||||||
|
*/
|
||||||
|
public CompletableFuture<DockerResult> executeCommandAsync(String command) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> executeCommand(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить Docker контейнер
|
||||||
|
*/
|
||||||
|
public DockerResult runContainer(String imageName, String containerName,
|
||||||
|
String ports, String environmentVars) {
|
||||||
|
StringBuilder cmd = new StringBuilder("docker run -d");
|
||||||
|
|
||||||
|
if (containerName != null && !containerName.isEmpty()) {
|
||||||
|
cmd.append(" --name ").append(containerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ports != null && !ports.isEmpty()) {
|
||||||
|
cmd.append(" -p ").append(ports);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environmentVars != null && !environmentVars.isEmpty()) {
|
||||||
|
cmd.append(" ").append(environmentVars);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.append(" ").append(imageName);
|
||||||
|
|
||||||
|
logger.info("Запуск контейнера: {}", cmd.toString());
|
||||||
|
return executeCommand(cmd.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановить Docker контейнер
|
||||||
|
*/
|
||||||
|
public DockerResult stopContainer(String containerName) {
|
||||||
|
String command = "docker stop " + containerName;
|
||||||
|
logger.info("Остановка контейнера: {}", command);
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить Docker контейнер
|
||||||
|
*/
|
||||||
|
public DockerResult removeContainer(String containerName) {
|
||||||
|
String command = "docker rm " + containerName;
|
||||||
|
logger.info("Удаление контейнера: {}", command);
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список запущенных контейнеров
|
||||||
|
*/
|
||||||
|
public DockerResult listContainers() {
|
||||||
|
String command = "docker ps";
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить логи контейнера
|
||||||
|
*/
|
||||||
|
public DockerResult getContainerLogs(String containerName, int lines) {
|
||||||
|
String command = String.format("docker logs --tail %d %s", lines, containerName);
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Построить Docker образ
|
||||||
|
*/
|
||||||
|
public DockerResult buildImage(String dockerfilePath, String imageName, String tag) {
|
||||||
|
String fullTag = tag != null ? imageName + ":" + tag : imageName;
|
||||||
|
String command = String.format("docker build -t %s %s", fullTag, dockerfilePath);
|
||||||
|
logger.info("Сборка образа: {}", command);
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить команду внутри контейнера
|
||||||
|
*/
|
||||||
|
public DockerResult execInContainer(String containerName, String command) {
|
||||||
|
String dockerCommand = String.format("docker exec %s %s", containerName, command);
|
||||||
|
logger.info("Выполнение команды в контейнере: {}", dockerCommand);
|
||||||
|
return executeCommand(dockerCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить Docker Compose
|
||||||
|
*/
|
||||||
|
public DockerResult dockerComposeUp(String composePath) {
|
||||||
|
String command = String.format("docker-compose -f %s up -d", composePath);
|
||||||
|
logger.info("Запуск Docker Compose: {}", command);
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановить Docker Compose
|
||||||
|
*/
|
||||||
|
public DockerResult dockerComposeDown(String composePath) {
|
||||||
|
String command = String.format("docker-compose -f %s down", composePath);
|
||||||
|
logger.info("Остановка Docker Compose: {}", command);
|
||||||
|
return executeCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.jambotronGroup.jambotron.fileBrowserTree;
|
||||||
|
|
||||||
|
public class CreateFolderRequest {
|
||||||
|
private String path;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
public CreateFolderRequest() {}
|
||||||
|
|
||||||
|
public CreateFolderRequest(String path, String name) {
|
||||||
|
this.path = path;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
+410
@@ -0,0 +1,410 @@
|
|||||||
|
package com.jambotronGroup.jambotron.fileBrowserTree;
|
||||||
|
|
||||||
|
import com.jambotronGroup.jambotron.model.ERole;
|
||||||
|
import com.jambotronGroup.jambotron.security.AuthenticationFacade;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class FileBrowserTreeService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationFacade authenticationFacade;
|
||||||
|
|
||||||
|
private final Path root = Paths.get("/jambotron_data");
|
||||||
|
private Path getRootPath() {
|
||||||
|
boolean isAdmin = authenticationFacade.getUser()
|
||||||
|
.getRoles().stream()
|
||||||
|
.anyMatch(role -> role.getName().equals(ERole.ROLE_ADMIN));
|
||||||
|
|
||||||
|
if(isAdmin){
|
||||||
|
// Admins can access the root directory
|
||||||
|
return root.normalize().toAbsolutePath();
|
||||||
|
}
|
||||||
|
return root.resolve(authenticationFacade.getUser().getUsername()).normalize().toAbsolutePath();
|
||||||
|
}
|
||||||
|
private Path resolvePath(String path) throws IOException {
|
||||||
|
Path root = getRootPath();
|
||||||
|
Path resolvedPath = root.resolve(path.startsWith("/") ? path.substring(1) : path)
|
||||||
|
.normalize().toAbsolutePath();
|
||||||
|
|
||||||
|
// Security check: ensure the resolved path is within the root directory
|
||||||
|
if (!resolvedPath.startsWith(root)) {
|
||||||
|
throw new SecurityException("Access denied: Path is outside root directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileItemDto> listFiles(String path) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath)) {
|
||||||
|
throw new IOException("Directory does not exist: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(targetPath)) {
|
||||||
|
throw new IOException("Path is not a directory: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileItemDto> files = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.list(targetPath)) {
|
||||||
|
stream.forEach(filePath -> {
|
||||||
|
try {
|
||||||
|
FileItemDto dto = createFileItemDto(filePath);
|
||||||
|
files.add(dto);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Log and skip files that can't be read
|
||||||
|
System.err.println("Error reading file: " + filePath + " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to list only folders
|
||||||
|
public List<FileItemDto> listFolders(String path) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath)) {
|
||||||
|
throw new IOException("Directory does not exist: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(targetPath)) {
|
||||||
|
throw new IOException("Path is not a directory: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileItemDto> folders = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.list(targetPath)) {
|
||||||
|
stream.filter(Files::isDirectory)
|
||||||
|
.forEach(folderPath -> {
|
||||||
|
try {
|
||||||
|
FileItemDto dto = createFileItemDto(folderPath);
|
||||||
|
folders.add(dto);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error reading folder: " + folderPath + " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort folders alphabetically
|
||||||
|
folders.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName()));
|
||||||
|
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
public List<FileItemDto> searchFiles(String path, String query) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath)) {
|
||||||
|
throw new IOException("Directory does not exist: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileItemDto> matchingFiles = new ArrayList<>();
|
||||||
|
String lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.walk(targetPath, 10)) { // Max depth of 10
|
||||||
|
stream.filter(filePath -> !filePath.equals(targetPath))
|
||||||
|
.filter(filePath -> filePath.getFileName().toString().toLowerCase().contains(lowerQuery))
|
||||||
|
.forEach(filePath -> {
|
||||||
|
try {
|
||||||
|
FileItemDto dto = createFileItemDto(filePath);
|
||||||
|
matchingFiles.add(dto);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Error reading file during search: " + filePath + " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortFiles(matchingFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to create FileItemDto
|
||||||
|
private FileItemDto createFileItemDto(Path filePath) throws IOException {
|
||||||
|
FileItemDto dto = new FileItemDto();
|
||||||
|
dto.setName(filePath.getFileName().toString());
|
||||||
|
dto.setType(Files.isDirectory(filePath) ? "directory" : "file");
|
||||||
|
|
||||||
|
// Calculate relative path from root
|
||||||
|
Path relativePath = getRootPath().relativize(filePath);
|
||||||
|
String pathString = "/" + relativePath.toString().replace("\\", "/");
|
||||||
|
dto.setPath(pathString);
|
||||||
|
|
||||||
|
if (Files.isRegularFile(filePath)) {
|
||||||
|
dto.setSize(Files.size(filePath));
|
||||||
|
} else {
|
||||||
|
dto.setSize(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
dto.setLastModified(new Date(Files.getLastModifiedTime(filePath).toMillis()));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to sort files (directories first, then alphabetically)
|
||||||
|
private List<FileItemDto> sortFiles(List<FileItemDto> files) {
|
||||||
|
files.sort((a, b) -> {
|
||||||
|
if (a.getType().equals(b.getType())) {
|
||||||
|
return a.getName().compareToIgnoreCase(b.getName());
|
||||||
|
}
|
||||||
|
return "directory".equals(a.getType()) ? -1 : 1;
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource loadFileAsResource(String path) throws IOException {
|
||||||
|
Path filePath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(filePath) || !Files.isReadable(filePath)) {
|
||||||
|
throw new IOException("File not found or not readable: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = new UrlResource(filePath.toUri());
|
||||||
|
|
||||||
|
if (resource.exists() && resource.isReadable()) {
|
||||||
|
return resource;
|
||||||
|
} else {
|
||||||
|
throw new IOException("File not found or not readable: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void storeFile(MultipartFile file, String targetPath) throws IOException {
|
||||||
|
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
|
||||||
|
|
||||||
|
if (fileName.contains("..")) {
|
||||||
|
throw new IOException("Invalid file path: " + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path targetDir = resolvePath(targetPath);
|
||||||
|
|
||||||
|
// Create directories if they don't exist
|
||||||
|
if (!Files.exists(targetDir)) {
|
||||||
|
Files.createDirectories(targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path targetFilePath = targetDir.resolve(fileName);
|
||||||
|
Files.copy(file.getInputStream(), targetFilePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createFolder(String path, String folderName) throws IOException {
|
||||||
|
if (folderName.contains("..") || folderName.contains("/") || folderName.contains("\\")) {
|
||||||
|
throw new IOException("Invalid folder name: " + folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
Path newFolderPath = targetPath.resolve(folderName);
|
||||||
|
|
||||||
|
if (Files.exists(newFolderPath)) {
|
||||||
|
throw new IOException("Folder already exists: " + folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createDirectories(newFolderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteFile(String path) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath)) {
|
||||||
|
throw new IOException("File or directory does not exist: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isDirectory(targetPath)) {
|
||||||
|
// Delete directory and all its contents
|
||||||
|
deleteDirectoryRecursively(targetPath);
|
||||||
|
} else {
|
||||||
|
Files.delete(targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move/rename functionality
|
||||||
|
public void moveFile(String sourcePath, String targetPath) throws IOException {
|
||||||
|
Path source = resolvePath(sourcePath);
|
||||||
|
Path target = resolvePath(targetPath);
|
||||||
|
|
||||||
|
if (!Files.exists(source)) {
|
||||||
|
throw new IOException("Source file does not exist: " + sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create target directory if it doesn't exist
|
||||||
|
Path targetParent = target.getParent();
|
||||||
|
if (targetParent != null && !Files.exists(targetParent)) {
|
||||||
|
Files.createDirectories(targetParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy functionality
|
||||||
|
public void copyFile(String sourcePath, String targetPath) throws IOException {
|
||||||
|
Path source = resolvePath(sourcePath);
|
||||||
|
Path target = resolvePath(targetPath);
|
||||||
|
|
||||||
|
if (!Files.exists(source)) {
|
||||||
|
throw new IOException("Source file does not exist: " + sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create target directory if it doesn't exist
|
||||||
|
Path targetParent = target.getParent();
|
||||||
|
if (targetParent != null && !Files.exists(targetParent)) {
|
||||||
|
Files.createDirectories(targetParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isDirectory(source)) {
|
||||||
|
copyDirectoryRecursively(source, target);
|
||||||
|
} else {
|
||||||
|
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get directory size (recursive)
|
||||||
|
public long getDirectorySize(String path) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath) || !Files.isDirectory(targetPath)) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.walk(targetPath)) {
|
||||||
|
return stream
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
|
.mapToLong(filePath -> {
|
||||||
|
try {
|
||||||
|
return Files.size(filePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file count in directory
|
||||||
|
public long getFileCount(String path) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath) || !Files.isDirectory(targetPath)) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.list(targetPath)) {
|
||||||
|
return stream.count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if path exists
|
||||||
|
public boolean pathExists(String path) throws IOException {
|
||||||
|
try {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
return Files.exists(targetPath);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file/directory info
|
||||||
|
public FileItemDto getFileInfo(String path) throws IOException {
|
||||||
|
Path targetPath = resolvePath(path);
|
||||||
|
|
||||||
|
if (!Files.exists(targetPath)) {
|
||||||
|
throw new IOException("File or directory does not exist: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createFileItemDto(targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteDirectoryRecursively(Path directory) throws IOException {
|
||||||
|
try (Stream<Path> stream = Files.walk(directory)) {
|
||||||
|
stream.sorted((a, b) -> b.getNameCount() - a.getNameCount())
|
||||||
|
.forEach(path -> {
|
||||||
|
try {
|
||||||
|
Files.delete(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Failed to delete: " + path + " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyDirectoryRecursively(Path source, Path target) throws IOException {
|
||||||
|
Files.createDirectories(target);
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.walk(source)) {
|
||||||
|
stream.forEach(sourcePath -> {
|
||||||
|
try {
|
||||||
|
Path targetPath = target.resolve(source.relativize(sourcePath));
|
||||||
|
if (Files.isDirectory(sourcePath)) {
|
||||||
|
Files.createDirectories(targetPath);
|
||||||
|
} else {
|
||||||
|
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Failed to copy: " + sourcePath + " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
public void deleteMultipleFiles(List<String> paths) throws IOException {
|
||||||
|
List<String> failedDeletions = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String path : paths) {
|
||||||
|
try {
|
||||||
|
deleteFile(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
failedDeletions.add(path);
|
||||||
|
System.err.println("Failed to delete: " + path + " - " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!failedDeletions.isEmpty()) {
|
||||||
|
throw new IOException("Failed to delete " + failedDeletions.size() + " files");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get disk usage information
|
||||||
|
public DiskUsageInfo getDiskUsage() throws IOException {
|
||||||
|
Path rootPath = getRootPath();
|
||||||
|
FileStore store = Files.getFileStore(rootPath);
|
||||||
|
|
||||||
|
long totalSpace = store.getTotalSpace();
|
||||||
|
long usableSpace = store.getUsableSpace();
|
||||||
|
long usedSpace = totalSpace - usableSpace;
|
||||||
|
|
||||||
|
return new DiskUsageInfo(totalSpace, usedSpace, usableSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner class for disk usage info
|
||||||
|
public static class DiskUsageInfo {
|
||||||
|
private final long totalSpace;
|
||||||
|
private final long usedSpace;
|
||||||
|
private final long availableSpace;
|
||||||
|
|
||||||
|
public DiskUsageInfo(long totalSpace, long usedSpace, long availableSpace) {
|
||||||
|
this.totalSpace = totalSpace;
|
||||||
|
this.usedSpace = usedSpace;
|
||||||
|
this.availableSpace = availableSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalSpace() { return totalSpace; }
|
||||||
|
public long getUsedSpace() { return usedSpace; }
|
||||||
|
public long getAvailableSpace() { return availableSpace; }
|
||||||
|
public double getUsagePercentage() {
|
||||||
|
return totalSpace > 0 ? (double) usedSpace / totalSpace * 100 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.jambotronGroup.jambotron.fileBrowserTree;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class FileItemDto {
|
||||||
|
private String name;
|
||||||
|
private String type; // "file" or "directory"
|
||||||
|
private long size;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
|
||||||
|
private Date lastModified;
|
||||||
|
private String path;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
public FileItemDto() {}
|
||||||
|
|
||||||
|
public FileItemDto(String name, String type, long size, Date lastModified, String path) {
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
this.size = size;
|
||||||
|
this.lastModified = lastModified;
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(long size) {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getLastModified() {
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastModified(Date lastModified) {
|
||||||
|
this.lastModified = lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class InitDataService {
|
|||||||
private UserRepository _userRepository;
|
private UserRepository _userRepository;
|
||||||
|
|
||||||
|
|
||||||
private final Path _initDataPath = Path.of("/jambotron-data/initData/");
|
private final Path _initDataPath = Path.of("/jambotron_data/initData/");
|
||||||
|
|
||||||
public InitDataService(){
|
public InitDataService(){
|
||||||
|
|
||||||
|
|||||||
@@ -122,9 +122,12 @@ public class WebSecurityConfig implements WebMvcConfigurer {// extends WebSecuri
|
|||||||
.requestMatchers("/api/moderator/**").hasRole("MODERATOR")
|
.requestMatchers("/api/moderator/**").hasRole("MODERATOR")
|
||||||
|
|
||||||
//.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
//.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
.requestMatchers("/api/admin/**").permitAll()
|
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
.requestMatchers("/api/admin/json-dump/**").permitAll()
|
.requestMatchers("/api/admin/json-dump/**").hasRole("ADMIN")
|
||||||
.requestMatchers("/api/admin/json-dump/import/**").permitAll()
|
.requestMatchers("/api/admin/json-dump/import/**").hasRole("ADMIN")
|
||||||
|
|
||||||
|
.requestMatchers("/api/file-browser/files/**").hasAnyRole("USER", "MODERATOR", "ADMIN")
|
||||||
|
.requestMatchers("/api/file-browser-tree/files/**").hasAnyRole("USER", "MODERATOR", "ADMIN")
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ logging:
|
|||||||
root: INFO
|
root: INFO
|
||||||
com.jambotronGroup.jambotron: DEBUG
|
com.jambotronGroup.jambotron: DEBUG
|
||||||
org.springframework: WARN
|
org.springframework: WARN
|
||||||
|
com.example.service.DockerService: DEBUG
|
||||||
pattern:
|
pattern:
|
||||||
console: "%d{yyyy-MM-dd HH:mm:ss} %-5level - %msg%n"
|
console: "%d{yyyy-MM-dd HH:mm:ss} %-5level - %msg%n"
|
||||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||||
@@ -11,3 +12,7 @@ logging:
|
|||||||
max-size: 10MB
|
max-size: 10MB
|
||||||
max-history: 30
|
max-history: 30
|
||||||
|
|
||||||
|
docker:
|
||||||
|
socket-path: /var/run/docker.sock
|
||||||
|
enable-security: true
|
||||||
|
command-timeout: 30000
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ logging:
|
|||||||
root: INFO
|
root: INFO
|
||||||
com.jambotronGroup.jambotron: DEBUG
|
com.jambotronGroup.jambotron: DEBUG
|
||||||
org.springframework: WARN
|
org.springframework: WARN
|
||||||
|
com.example.service.DockerService: DEBUG
|
||||||
pattern:
|
pattern:
|
||||||
console: "%d{yyyy-MM-dd HH:mm:ss} %-5level - %msg%n"
|
console: "%d{yyyy-MM-dd HH:mm:ss} %-5level - %msg%n"
|
||||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||||
@@ -17,3 +18,8 @@ server:
|
|||||||
certificate: ${FULLCHAINPEM:""} # Default to empty string if FULLCHAINPEM is not set
|
certificate: ${FULLCHAINPEM:""} # Default to empty string if FULLCHAINPEM is not set
|
||||||
certificate-private-key: ${PRIVKEYPEM:""} # Default to empty string if PRIVKEYPEM is not set
|
certificate-private-key: ${PRIVKEYPEM:""} # Default to empty string if PRIVKEYPEM is not set
|
||||||
# port: ${SERVER_PORT:443} # Default to 443 if SERVER_PORT is not set
|
# port: ${SERVER_PORT:443} # Default to 443 if SERVER_PORT is not set
|
||||||
|
|
||||||
|
docker:
|
||||||
|
socket-path: /var/run/docker.sock
|
||||||
|
enable-security: true
|
||||||
|
command-timeout: 30000
|
||||||
|
|||||||
Reference in New Issue
Block a user