This commit is contained in:
liosha84
2025-08-23 15:04:40 +03:00
parent fac4e2d4df
commit 9073f43b82
22 changed files with 416 additions and 114 deletions
@@ -9,6 +9,7 @@
<app-file-system-tree-component <app-file-system-tree-component
(folderSelected)="onFolderSelected($event)" (folderSelected)="onFolderSelected($event)"
[baseUrl]="baseUrl"
> >
</app-file-system-tree-component> </app-file-system-tree-component>
</mat-sidenav> </mat-sidenav>
@@ -1,4 +1,4 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild} from '@angular/core'; import {Component, CUSTOM_ELEMENTS_SCHEMA, Input, OnInit, ViewChild} from '@angular/core';
import {DatePipe, NgForOf, NgIf} from '@angular/common'; import {DatePipe, NgForOf, NgIf} from '@angular/common';
import { import {
MatCell, MatCell,
@@ -83,11 +83,18 @@ export class FileBrowserWithTreeComponent implements OnInit {
searchQuery = ''; searchQuery = '';
filteredFiles: FileItem[] = []; filteredFiles: FileItem[] = [];
@Input() baseUrl: string = '';
constructor( constructor(
private fileService: FileBrowserTreeService, private fileService: FileBrowserTreeService,
private dialog: MatDialog, private dialog: MatDialog,
private snackBar: MatSnackBar private snackBar: MatSnackBar
) {} ) {
if(this.baseUrl.length>0){
fileService.baseUrl = this.baseUrl;
}
}
ngOnInit(): void { ngOnInit(): void {
this.loadFiles(this.currentPath); this.loadFiles(this.currentPath);
@@ -7,8 +7,8 @@ import {
inject, inject,
signal, signal,
Output, Output,
EventEmitter EventEmitter, Input, OnInit
} from '@angular/core'; } from '@angular/core';
import {BehaviorSubject, merge, Observable} from 'rxjs'; import {BehaviorSubject, merge, Observable} from 'rxjs';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {MatProgressBarModule} from '@angular/material/progress-bar'; import {MatProgressBarModule} from '@angular/material/progress-bar';
@@ -38,19 +38,24 @@ class DynamicFlatNode {
@Injectable({providedIn: 'root'}) @Injectable({providedIn: 'root'})
export class DynamicDatabase { export class DynamicDatabase {
rootNodes:FileItem[] = []; rootNodes:FileItem[] = [];
constructor(private fileBrowserService: FileBrowserTreeService) { constructor(private fileBrowserService: FileBrowserTreeService) {
this.getFileItems('/').subscribe( // this.getFileItems('/').subscribe(
value => { // value => {
console.log('value', value); // console.log('value', value);
this.rootNodes = value; // this.rootNodes = value;
}, // },
error => { // error => {
console.error('Error loading files:', error); // console.error('Error loading files:', error);
} // }
) // )
//this.fileBrowserService.baseUrl
}
setBaseUrl(baseUrl: string) {
this.fileBrowserService.baseUrl = baseUrl;
} }
getFileItems(path:string):Observable<FileItem[]> { getFileItems(path:string):Observable<FileItem[]> {
@@ -182,7 +187,7 @@ export class DynamicDataSource implements DataSource<DynamicFlatNode> {
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FileSystemTreeComponent { export class FileSystemTreeComponent implements OnInit{
database = inject(DynamicDatabase); database = inject(DynamicDatabase);
treeControl: FlatTreeControl<DynamicFlatNode>; treeControl: FlatTreeControl<DynamicFlatNode>;
dataSource: DynamicDataSource; dataSource: DynamicDataSource;
@@ -190,12 +195,23 @@ export class FileSystemTreeComponent {
@Output() folderSelected = new EventEmitter<string>(); @Output() folderSelected = new EventEmitter<string>();
@Input() baseUrl: string = '';
constructor() { constructor() {
this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable); this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new DynamicDataSource(this.treeControl, this.database); this.dataSource = new DynamicDataSource(this.treeControl, this.database);
}
ngOnInit(): void {
if(this.baseUrl.length>0){
this.database.setBaseUrl(this.baseUrl);
}
// Initialize root data // Initialize root data
this.refresh(); this.refresh();
} }
@@ -26,7 +26,7 @@ export const ADMIN_ROUTES: Routes = [
{ {
path: 'users', path: 'users',
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:'manage_accounts', nav:true}
}, },
{ {
path: 'hard-drive-browser', path: 'hard-drive-browser',
@@ -1 +1,3 @@
<app-file-browser-with-tree-component></app-file-browser-with-tree-component> <app-file-browser-with-tree-component
[baseUrl]="baseUrl"
></app-file-browser-with-tree-component>
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { import {
FileBrowserWithTreeComponent FileBrowserWithTreeComponent
} from '../../../components/file-browser-with-tree-component/file-browser-with-tree-component'; } from '../../../components/file-browser-with-tree-component/file-browser-with-tree-component';
import {GlobalConstants} from '../../../global-constants';
@Component({ @Component({
selector: 'app-hard-drive-browser.component', selector: 'app-hard-drive-browser.component',
@@ -12,5 +13,6 @@ import {
styleUrl: './hard-drive-browser.component.scss' styleUrl: './hard-drive-browser.component.scss'
}) })
export class HardDriveBrowserComponent { export class HardDriveBrowserComponent {
baseUrl = `${GlobalConstants.API_URL}/admin/file-browser-tree`;
} }
@@ -0,0 +1,3 @@
<app-file-browser-with-tree-component
[baseUrl]="baseUrl"
></app-file-browser-with-tree-component>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilesBrowserComponent } from './files-browser.component';
describe('FilesBrowserComponent', () => {
let component: FilesBrowserComponent;
let fixture: ComponentFixture<FilesBrowserComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FilesBrowserComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FilesBrowserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import {Component, OnInit} from '@angular/core';
import {
FileBrowserWithTreeComponent
} from '../../../components/file-browser-with-tree-component/file-browser-with-tree-component';
import {GlobalConstants} from '../../../global-constants';
@Component({
selector: 'app-files-browser.component',
imports: [
FileBrowserWithTreeComponent
],
templateUrl: './files-browser.component.html',
styleUrl: './files-browser.component.scss'
})
export class FilesBrowserComponent implements OnInit{
baseUrl: string ='';
ngOnInit(): void {
this.baseUrl= `${GlobalConstants.API_URL}/user/file-browser-tree`;
}
}
@@ -31,6 +31,11 @@ export const USER_ROUTS: Routes = [
loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent), loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent),
data:{title: 'Edit Tutorial'} data:{title: 'Edit Tutorial'}
}, },
{
path: 'files-browser',
loadComponent: ()=>import('../user-module/files-browser.component/files-browser.component').then((c)=>c.FilesBrowserComponent),
data:{title: 'Files Browser', icon:'folder_data', nav:true}
},
{ {
path: 'ai-models', path: 'ai-models',
loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent), loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent),
@@ -16,23 +16,16 @@ export interface FileItem {
providedIn: 'root' providedIn: 'root'
}) })
export class FileBrowserTreeService { export class FileBrowserTreeService {
private baseUrl = `${GlobalConstants.API_URL}/file-browser-tree/files`; public baseUrl = `${GlobalConstants.API_URL}/user/file-browser-tree`;
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getFiles(path: string): Observable<FileItem[]> { getFiles(path: string): Observable<FileItem[]> {
const encodedPath = encodeURIComponent(path); const encodedPath = encodeURIComponent(path);
return this.http.get<FileItem[]>(`${this.baseUrl}?path=${encodedPath}`); return this.http.get<FileItem[]>(`${this.baseUrl}/files?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 // Alternative: dedicated backend endpoint for folders only
getFoldersOnly(path: string): Observable<FileItem[]> { getFoldersOnly(path: string): Observable<FileItem[]> {
@@ -1,9 +1,12 @@
package com.jambotronGroup.jambotron.controllers; package com.jambotronGroup.jambotron.controllers;
import com.jambotronGroup.jambotron.fileBrowserTree.CreateFolderRequest; import com.jambotronGroup.jambotron.fileBrowserTree.CreateFolderRequest;
import com.jambotronGroup.jambotron.fileBrowserTree.FileBrowserTreeExtendedService;
import com.jambotronGroup.jambotron.fileBrowserTree.FileItemDto; import com.jambotronGroup.jambotron.fileBrowserTree.FileItemDto;
import com.jambotronGroup.jambotron.fileBrowserTree.FileBrowserTreeService; import com.jambotronGroup.jambotron.fileBrowserTree.FileBrowserTreeService;
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage; import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -15,19 +18,24 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@RestController @RestController
@RequestMapping("/api/file-browser-tree/files") @RequestMapping("/api/user/file-browser-tree")
public class FileBrowserTreeController { public class FileBrowserTreeController {
private final Logger _logger = LoggerFactory.getLogger(FileBrowserTreeController.class);
@Autowired @Autowired
private FileBrowserTreeService fileBrowserTreeService; private FileBrowserTreeService fileBrowserTreeService;
@GetMapping @GetMapping
@RequestMapping("/files")
public ResponseEntity<List<FileItemDto>> getFiles(@RequestParam String path) { public ResponseEntity<List<FileItemDto>> getFiles(@RequestParam String path) {
try { try {
List<FileItemDto> files = fileBrowserTreeService.listFiles(path); List<FileItemDto> files = fileBrowserTreeService.listFiles(path);
return ResponseEntity.ok(files); return ResponseEntity.ok(files);
} catch (Exception e) { } catch (Exception e) {
_logger.error("Error retrieving files from path: {}", path, e.getMessage());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
} }
@@ -39,10 +47,12 @@ public class FileBrowserTreeController {
List<FileItemDto> folders = fileBrowserTreeService.listFolders(path); List<FileItemDto> folders = fileBrowserTreeService.listFolders(path);
return ResponseEntity.ok(folders); return ResponseEntity.ok(folders);
} catch (Exception e) { } catch (Exception e) {
_logger.error("Error retrieving folders from path: {}", path, e.getMessage());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
} }
// Search endpoint // Search endpoint
@GetMapping("/search") @GetMapping("/search")
public ResponseEntity<List<FileItemDto>> searchFiles( public ResponseEntity<List<FileItemDto>> searchFiles(
@@ -52,6 +62,7 @@ public class FileBrowserTreeController {
List<FileItemDto> files = fileBrowserTreeService.searchFiles(path, query); List<FileItemDto> files = fileBrowserTreeService.searchFiles(path, query);
return ResponseEntity.ok(files); return ResponseEntity.ok(files);
} catch (Exception e) { } catch (Exception e) {
_logger.error("Error searching files in path: {} with query: {}", path, query, e.getMessage());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
} }
@@ -68,6 +79,7 @@ public class FileBrowserTreeController {
"attachment; filename=\"" + filename + "\"") "attachment; filename=\"" + filename + "\"")
.body(resource); .body(resource);
} catch (Exception e) { } catch (Exception e) {
_logger.error("Error downloading file from path: {}", path, e.getMessage());
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
} }
@@ -80,6 +92,7 @@ public class FileBrowserTreeController {
fileBrowserTreeService.storeFile(file, path); fileBrowserTreeService.storeFile(file, path);
return ResponseEntity.ok(new ResponseMessage("File uploaded successfully: " + file.getOriginalFilename())); return ResponseEntity.ok(new ResponseMessage("File uploaded successfully: " + file.getOriginalFilename()));
} catch (IOException e) { } catch (IOException e) {
_logger.error("Error uploading file: {}", file.getOriginalFilename(), e.getMessage());
return ResponseEntity.badRequest() return ResponseEntity.badRequest()
.body(new ResponseMessage("Failed to upload file: " + e.getMessage())); .body(new ResponseMessage("Failed to upload file: " + e.getMessage()));
} }
@@ -0,0 +1,146 @@
package com.jambotronGroup.jambotron.controllers;
import com.jambotronGroup.jambotron.fileBrowserTree.CreateFolderRequest;
import com.jambotronGroup.jambotron.fileBrowserTree.FileBrowserTreeExtendedService;
import com.jambotronGroup.jambotron.fileBrowserTree.FileItemDto;
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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/admin/file-browser-tree")
public class FileBrowserTreeExtendedController {
private final Logger _logger = LoggerFactory.getLogger(FileBrowserTreeExtendedService.class);
@Autowired
private FileBrowserTreeExtendedService fileBrowserTreeService;
@GetMapping
@RequestMapping("/files")
public ResponseEntity<List<FileItemDto>> getFiles(@RequestParam String path) {
try {
List<FileItemDto> files = fileBrowserTreeService.listFiles(path);
return ResponseEntity.ok(files);
} catch (Exception e) {
_logger.error("Error retrieving files from path: {}", path, e.getMessage());
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) {
_logger.error("Error retrieving folders from path: {}", path, e.getMessage());
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) {
_logger.error("Error searching files in path: {} with query: {}", path, query, e.getMessage());
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) {
_logger.error("Error downloading file from path: {}", path, e.getMessage());
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 FileBrowserTreeController.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 FileBrowserTreeController.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()));
}
}
}
@@ -78,9 +78,9 @@ public class FilesController {
response.getBody() // File content response.getBody() // File content
); );
User user = authenticationFacade.getUser();
storageService.save(user.getId().toString(), multipartFile);
storageService.save(multipartFile);
message = "Uploaded the file successfully: " + fileName; message = "Uploaded the file successfully: " + fileName;
@@ -104,7 +104,7 @@ public class FilesController {
try { try {
User user = authenticationFacade.getUser(); User user = authenticationFacade.getUser();
String localFullFileName = storageService.save(user.getId().toString(), file); String localFullFileName = storageService.save(file);
FileInfo fileInfo = new FileInfo( FileInfo fileInfo = new FileInfo(
file.getOriginalFilename(), file.getOriginalFilename(),
@@ -122,9 +122,8 @@ public class FilesController {
@GetMapping("/api/user/getImages") @GetMapping("/api/user/getImages")
public ResponseEntity<List<FileInfo>> getImages() { public ResponseEntity<List<FileInfo>> getImages() {
User user = authenticationFacade.getUser();
List<FileInfo> fileInfos = storageService.loadUserImages(user.getId().toString()).map(path -> { List<FileInfo> fileInfos = storageService.loadUserImages().map(path -> {
String filename = path.getFileName().toString(); String filename = path.getFileName().toString();
String url = FilesRoutingHelper.getUserImageUrl(filename); String url = FilesRoutingHelper.getUserImageUrl(filename);
@@ -158,7 +157,7 @@ public class FilesController {
@ResponseBody @ResponseBody
public ResponseEntity<Resource> getUserImage(@PathVariable String filename) { public ResponseEntity<Resource> getUserImage(@PathVariable String filename) {
User user = authenticationFacade.getUser(); User user = authenticationFacade.getUser();
Resource file = storageService.loadUserImage(user.getId().toString(),filename); Resource file = storageService.loadUserImage(filename);
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file); .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
} }
@@ -169,7 +168,7 @@ public class FilesController {
String message = ""; String message = "";
try { try {
String filename = storageService.getFileNameFromUrl(imageUrl); String filename = storageService.getFileNameFromUrl(imageUrl);
Base64ImageResponse base64Image = storageService.getUserImageAsBase64(user.getId().toString(),filename); Base64ImageResponse base64Image = storageService.getUserImageAsBase64(filename);
return ResponseEntity.ok(base64Image); return ResponseEntity.ok(base64Image);
@@ -177,6 +176,10 @@ public class FilesController {
logger.error("Error retrieving base64 image for user {}: {}", user.getId(), e.getMessage()); logger.error("Error retrieving base64 image for user {}: {}", user.getId(), e.getMessage());
message = "Error retrieving base64 image for fileUrl" + imageUrl + ". \tError: " + e.getMessage(); message = "Error retrieving base64 image for fileUrl" + imageUrl + ". \tError: " + e.getMessage();
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new Base64ImageResponse(message)); return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new Base64ImageResponse(message));
} catch (IOException e) {
logger.error("IO Error retrieving base64 image for user {}: {}", user.getId(), e.getMessage());
message = "IO Error retrieving base64 image for fileUrl" + imageUrl + ". \tError: " + e.getMessage();
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new Base64ImageResponse(message));
} }
} }
@@ -190,7 +193,7 @@ public class FilesController {
try { try {
String fullFileName = storageService.saveUserBase64Image(user.getId().toString(),base64ImageRequest.getData(), base64ImageRequest.getMime()); String fullFileName = storageService.saveUserBase64Image(base64ImageRequest.getData(), base64ImageRequest.getMime());
String url = FilesRoutingHelper.getUserImageUrl(fullFileName); String url = FilesRoutingHelper.getUserImageUrl(fullFileName);
@@ -118,7 +118,6 @@ public class TutorialController {
String newFilename = filesStorageService.getFileNameFromUrl(tutorial.getTitleimage()); String newFilename = filesStorageService.getFileNameFromUrl(tutorial.getTitleimage());
Path path= filesStorageService.moveFile( Path path= filesStorageService.moveFile(
user.getId().toString(),
tutorial.getTitleimage(), tutorial.getTitleimage(),
String.format("Tutorial_%s",newFilename ) String.format("Tutorial_%s",newFilename )
); );
@@ -188,7 +187,6 @@ public class TutorialController {
filesStorageService.deletePublicFile(servImageFileName); filesStorageService.deletePublicFile(servImageFileName);
Path path= filesStorageService.moveFile( Path path= filesStorageService.moveFile(
user.getId().toString(),
tutorial.getTitleimage(), tutorial.getTitleimage(),
String.format("Tutorial_%s",imageFileName ) String.format("Tutorial_%s",imageFileName )
); );
@@ -0,0 +1,29 @@
package com.jambotronGroup.jambotron.fileBrowserTree;
import com.jambotronGroup.jambotron.globalConstants.GlobalConstants;
import com.jambotronGroup.jambotron.model.ERole;
import com.jambotronGroup.jambotron.security.AuthenticationFacade;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.file.Path;
@Service
public class FileBrowserTreeExtendedService extends FileBrowserTreeService {
@Override
protected Path getRootPath() {
boolean isAdmin = super.authenticationFacade.getUser()
.getRoles().stream()
.anyMatch(role -> role.getName().equals(ERole.ROLE_ADMIN));
if( isAdmin ) {
return GlobalConstants.PATH_ROOT.toAbsolutePath();
} else {
return super.getRootPath();
}
}
}
@@ -1,5 +1,6 @@
package com.jambotronGroup.jambotron.fileBrowserTree; package com.jambotronGroup.jambotron.fileBrowserTree;
import com.jambotronGroup.jambotron.globalConstants.GlobalConstants;
import com.jambotronGroup.jambotron.model.ERole; import com.jambotronGroup.jambotron.model.ERole;
import com.jambotronGroup.jambotron.security.AuthenticationFacade; import com.jambotronGroup.jambotron.security.AuthenticationFacade;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -20,24 +21,20 @@ import java.util.stream.Stream;
public class FileBrowserTreeService { public class FileBrowserTreeService {
@Autowired @Autowired
private AuthenticationFacade authenticationFacade; protected AuthenticationFacade authenticationFacade;
private final Path root = Paths.get("/jambotron_data"); //private final Path root = Paths.get("/jambotron_data");
private Path getRootPath() { //private final Path rootUsers = root.resolve("users");
boolean isAdmin = authenticationFacade.getUser() protected Path getRootPath() {
.getRoles().stream() GlobalConstants.PATH_USERS.toFile().mkdir();
.anyMatch(role -> role.getName().equals(ERole.ROLE_ADMIN)); Path userFolder = GlobalConstants.PATH_USERS.resolve(authenticationFacade.getUser().getUsername()).toAbsolutePath();
userFolder.toFile().mkdir();
if(isAdmin){ return userFolder;
// 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 { private Path resolvePath(String path) throws IOException {
Path root = getRootPath(); Path root = getRootPath();
Path resolvedPath = root.resolve(path.startsWith("/") ? path.substring(1) : path) Path resolvedPath = root.resolve(path.startsWith("/") ? path.substring(1) : path)
.normalize().toAbsolutePath(); .normalize();
// Security check: ensure the resolved path is within the root directory // Security check: ensure the resolved path is within the root directory
if (!resolvedPath.startsWith(root)) { if (!resolvedPath.startsWith(root)) {
@@ -47,6 +44,7 @@ public class FileBrowserTreeService {
return resolvedPath; return resolvedPath;
} }
public List<FileItemDto> listFiles(String path) throws IOException { public List<FileItemDto> listFiles(String path) throws IOException {
Path targetPath = resolvePath(path); Path targetPath = resolvePath(path);
@@ -5,6 +5,7 @@ import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -13,25 +14,24 @@ public interface FilesStorageService {
public String getFileNameFromUrl(String urlString); public String getFileNameFromUrl(String urlString);
public String save(String userID,MultipartFile file); public String save(MultipartFile file);
// public void saveZhipuAiImage(MultipartFile file);
public String saveUserBase64Image(String userID, String base64Image, String mimeType); public String saveUserBase64Image(String base64Image, String mimeType);
public void save(MultipartFile file);
public Resource load(String filename); public Resource load(String filename);
public Resource loadUserImage(String userID, String filename); public Resource loadUserImage(String filename);
public Base64ImageResponse getUserImageAsBase64(String userID, String filename); public Base64ImageResponse getUserImageAsBase64(String filename) throws IOException;
public void deleteAll(); public void deleteAll() throws IOException;
public Stream<Path> loadAll(); public Stream<Path> loadAll();
public Stream<Path> loadUserImages(String userID); public Stream<Path> loadUserImages();
public Path moveFile(String userID, String url, String newFilename) throws Exception; public Path moveFile(String url, String newFilename) throws Exception;
public void deletePublicFile(String filename); public void deletePublicFile(String filename);
} }
@@ -2,9 +2,12 @@ package com.jambotronGroup.jambotron.fileUpload;
import com.jambotronGroup.jambotron.controllers.AuthController; import com.jambotronGroup.jambotron.controllers.AuthController;
import com.jambotronGroup.jambotron.globalConstants.GlobalConstants;
import com.jambotronGroup.jambotron.payload.request.UploadImageResponse; import com.jambotronGroup.jambotron.payload.request.UploadImageResponse;
import com.jambotronGroup.jambotron.security.AuthenticationFacade;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource; import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -26,6 +29,9 @@ public class FilesStorageServiceImpl implements FilesStorageService {
private static final Logger _logger = LoggerFactory.getLogger(AuthController.class); private static final Logger _logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private AuthenticationFacade _authenticationFacade;
private static final Set<String> ALLOWED_MIME = Set.of( private static final Set<String> ALLOWED_MIME = Set.of(
"image/png", "image/jpeg", "image/webp", "image/gif" "image/png", "image/jpeg", "image/webp", "image/gif"
); );
@@ -38,22 +44,32 @@ public class FilesStorageServiceImpl implements FilesStorageService {
); );
// Update paths to use the Docker volume
private final Path root = Paths.get("/jambotron_data/uploads/user-images/");
private final Path rootPublic = Paths.get("/jambotron_data/uploads/public-images/");
// private final Path rootInitData = Pa private final Path _rootPublic = GlobalConstants.PATH_PUBLIC;
@Override @Override
public void init() { public void init() {
try { try {
Files.createDirectories(root); Files.createDirectories(GlobalConstants.PATH_USERS);
Files.createDirectories(rootPublic); Files.createDirectories(_rootPublic);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not initialize folder for upload!"); throw new RuntimeException("Could not initialize folder for upload!");
} }
} }
private Path getUserUploadsImagesPath() throws IOException {
String username = _authenticationFacade.getUser().getUsername();
Path rootUserUploadsImages = GlobalConstants.PATH_USERS
.resolve(username)
.resolve("uploads")
.resolve("images");
Files.createDirectories(rootUserUploadsImages);
return rootUserUploadsImages;
}
@Override @Override
public String getFileNameFromUrl(String urlString){ public String getFileNameFromUrl(String urlString){
try { try {
@@ -69,10 +85,10 @@ public class FilesStorageServiceImpl implements FilesStorageService {
} }
@Override @Override
public Base64ImageResponse getUserImageAsBase64(String userID,String filename){ public Base64ImageResponse getUserImageAsBase64(String filename) throws IOException {
Path userPath = this.root.resolve(userID);
Path file = userPath.resolve(filename); Path file = getUserUploadsImagesPath().resolve(filename);
try { try {
@@ -95,32 +111,32 @@ public class FilesStorageServiceImpl implements FilesStorageService {
* Moves a file from a user's directory to the public directory with a new name. * Moves a file from a user's directory to the public directory with a new name.
* StandardCopyOption.REPLACE_EXISTING * StandardCopyOption.REPLACE_EXISTING
* *
* @param userID The ID of the user owning the file.
* @param url The URL of the file to move. * @param url The URL of the file to move.
* @param newFilename The new name for the file in the public directory. * @param newFilename The new name for the file in the public directory.
* @return The path to the moved file in the public directory. * @return The path to the moved file in the public directory.
*/ */
@Override @Override
public Path moveFile(String userID, String url, String newFilename) { public Path moveFile(String url, String newFilename) {
String filename = this.getFileNameFromUrl(url); String filename = this.getFileNameFromUrl(url);
try { try {
Path sourcePath = this.root.resolve(userID).resolve(filename); Path sourcePath = getUserUploadsImagesPath().resolve(filename);
Path targetPath = this.rootPublic.resolve(newFilename); Path targetPath = _rootPublic.resolve(newFilename);
Files.copy(sourcePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); Files.copy(sourcePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not move the file: " + e.getMessage()); throw new RuntimeException("Could not move the file: " + e.getMessage());
} }
return this.rootPublic.resolve(newFilename); return _rootPublic.resolve(newFilename);
} }
@Override @Override
public void deletePublicFile(String filename) { public void deletePublicFile(String filename) {
try { try {
Path filePath = this.rootPublic.resolve(filename); Path filePath = _rootPublic.resolve(filename);
Files.deleteIfExists(filePath); Files.deleteIfExists(filePath);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not delete the file: " + e.getMessage()); throw new RuntimeException("Could not delete the file: " + e.getMessage());
@@ -131,17 +147,16 @@ public class FilesStorageServiceImpl implements FilesStorageService {
* Saves a file to a user's directory. * Saves a file to a user's directory.
* uploads/user-images/{userID}/{filename} * uploads/user-images/{userID}/{filename}
* *
* @param userID The ID of the user.
* @param file The file to save. * @param file The file to save.
*/ */
@Override @Override
public String save(String userID,MultipartFile file) { public String save(MultipartFile file) {
Path targetPath = null; Path targetPath = null;
try { try {
Path path = this.root.resolve(userID);
path.toFile().mkdirs(); // Ensure user directory exists
targetPath = path.resolve(file.getOriginalFilename()); String fixedFileName = getFixedFileName(file.getOriginalFilename());
targetPath = getUserUploadsImagesPath().resolve(fixedFileName);
Files.copy(file.getInputStream(), targetPath, Files.copy(file.getInputStream(), targetPath,
java.nio.file.StandardCopyOption.REPLACE_EXISTING); java.nio.file.StandardCopyOption.REPLACE_EXISTING);
@@ -157,24 +172,29 @@ public class FilesStorageServiceImpl implements FilesStorageService {
return targetPath.getFileName().toString(); return targetPath.getFileName().toString();
} }
@Override private String getFixedFileName(String filename) {
public void save(MultipartFile file) {
try {
Files.copy(file.getInputStream(), this.root.resolve(file.getOriginalFilename()),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
if (e instanceof FileAlreadyExistsException) {
throw new RuntimeException("A file of that name already exists.");
}
throw new RuntimeException(e.getMessage()); return filename.indexOf('?') != -1 ? filename.substring(0, filename.indexOf('?')) : filename;
}
} }
// @Override
// public void saveZhipuAiImage(MultipartFile file) {
// try {
// Files.copy(file.getInputStream(), getUserRootPath().resolve(file.getOriginalFilename()),
// java.nio.file.StandardCopyOption.REPLACE_EXISTING);
// } catch (Exception e) {
// if (e instanceof FileAlreadyExistsException) {
// throw new RuntimeException("A file of that name already exists.");
// }
//
// throw new RuntimeException(e.getMessage());
// }
// }
@Override @Override
public Resource load(String filename) { public Resource load(String filename) {
try { try {
Path file = rootPublic.resolve(filename); Path file = _rootPublic.resolve(filename);
Resource resource = new UrlResource(file.toUri()); Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) { if (resource.exists() || resource.isReadable()) {
@@ -188,11 +208,10 @@ public class FilesStorageServiceImpl implements FilesStorageService {
} }
@Override @Override
public Resource loadUserImage(String userID,String filename) { public Resource loadUserImage(String filename) {
try { try {
Path file = root.resolve(filename); Path file = getUserUploadsImagesPath().resolve(filename);
Path userPath = this.root.resolve(userID);
file = userPath.resolve(filename);
Resource resource = new UrlResource(file.toUri()); Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) { if (resource.exists() || resource.isReadable()) {
@@ -202,36 +221,49 @@ public class FilesStorageServiceImpl implements FilesStorageService {
} }
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
throw new RuntimeException("Error: " + e.getMessage()); throw new RuntimeException("Error: " + e.getMessage());
} catch (IOException e) {
throw new RuntimeException(e);
} }
} }
@Override @Override
public void deleteAll() { public void deleteAll() throws IOException {
FileSystemUtils.deleteRecursively(root.toFile()); FileSystemUtils.deleteRecursively(getUserUploadsImagesPath().toFile());
} }
@Override @Override
public Stream<Path> loadAll() { public Stream<Path> loadAll() {
try { try {
return Files.walk(this.root, 1).filter(path -> !path.equals(this.root)).map(this.root::relativize); return Files.walk(getUserUploadsImagesPath(), 1).filter(path -> {
try {
return !path.equals(getUserUploadsImagesPath());
} catch (IOException e) {
throw new RuntimeException(e);
}
}).map(getUserUploadsImagesPath()::relativize);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not load the files!"); throw new RuntimeException("Could not load the files!");
} }
} }
@Override @Override
public Stream<Path> loadUserImages(String userID) { public Stream<Path> loadUserImages() {
Path userPath = this.root.resolve(userID);
userPath.toFile().mkdirs();
try { try {
return Files.walk(userPath, 1).filter(path -> !path.equals(userPath)).map(userPath::relativize); return Files.walk(getUserUploadsImagesPath(), 1).filter(path -> {
try {
return !path.equals(getUserUploadsImagesPath());
} catch (IOException e) {
throw new RuntimeException(e);
}
}).map(getUserUploadsImagesPath()::relativize);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not load the files!"); throw new RuntimeException("Could not load the files!");
} }
} }
@Override @Override
public String saveUserBase64Image(String userID, String base64Image, String mimeType) { public String saveUserBase64Image(String base64Image, String mimeType) {
if (!ALLOWED_MIME.contains(mimeType)) { if (!ALLOWED_MIME.contains(mimeType)) {
_logger.error("Unsupported MIME type: " + mimeType); _logger.error("Unsupported MIME type: " + mimeType);
@@ -256,17 +288,14 @@ public class FilesStorageServiceImpl implements FilesStorageService {
String id = UUID.randomUUID().toString().replace("-", ""); String id = UUID.randomUUID().toString().replace("-", "");
String safeBaseName = "user-image-" + userID; String safeBaseName = "user-image-";
String storedName = safeBaseName + "-" + id + "." + ext; String storedName = safeBaseName + id + "." + ext;
try { try {
Path userPath = this.root.resolve(userID); Path targetPath = getUserUploadsImagesPath().resolve(storedName);
userPath.toFile().mkdirs(); // Ensure user directory exists
Path targetPath = userPath.resolve(storedName); Files.write(targetPath, bytes);
Files.write(targetPath, bytes);
return targetPath.getFileName().toString(); return targetPath.getFileName().toString();
@@ -0,0 +1,11 @@
package com.jambotronGroup.jambotron.globalConstants;
import java.nio.file.Path;
public class GlobalConstants {
public static Path PATH_ROOT = Path.of("/jambotron_data/");
public static Path PATH_PUBLIC = PATH_ROOT.resolve("public");
public static Path PATH_USERS = PATH_ROOT.resolve("users");
public static Path PATH_INIT_DATA = PATH_ROOT.resolve("init_data");
public static Path PATH_JSON_DUMP = PATH_ROOT.resolve("json_dump");
}
@@ -119,15 +119,17 @@ public class WebSecurityConfig implements WebMvcConfigurer {// extends WebSecuri
// Access permitted for specific roles // Access permitted for specific roles
.requestMatchers("/api/user/**").hasRole("USER") .requestMatchers("/api/user/**").hasRole("USER")
.requestMatchers("/api/user/file-browser-tree/**").hasRole("USER")
.requestMatchers("/api/moderator/**").hasRole("MODERATOR") .requestMatchers("/api/moderator/**").hasRole("MODERATOR")
//.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/admin/json-dump/**").hasRole("ADMIN") .requestMatchers("/api/admin/json-dump/**").hasRole("ADMIN")
.requestMatchers("/api/admin/json-dump/import/**").hasRole("ADMIN") .requestMatchers("/api/admin/json-dump/import/**").hasRole("ADMIN")
.requestMatchers("/api/admin/file-browser-tree/**").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()
); );