diff --git a/src/main/java/com/gw/utils/BaseTool.java b/src/main/java/com/gw/utils/BaseTool.java index 480f6222..bac5cec7 100644 --- a/src/main/java/com/gw/utils/BaseTool.java +++ b/src/main/java/com/gw/utils/BaseTool.java @@ -84,6 +84,9 @@ public class BaseTool { @Value("${geoweaver.upload_file_path}") String upload_file_path; + @Value("${geoweaver.result_file_path}") + String result_file_path; + @Value("${geoweaver.prefixurl}") String prefixurl; @@ -375,6 +378,21 @@ public String getFileTransferFolder() { return tempfolder; } + public String getResultsFolder() { + + String resultfolder = + this.normalizedPath(workspace) + + FileSystems.getDefault().getSeparator() + + this.result_file_path + + FileSystems.getDefault().getSeparator(); + + File tf = new File(resultfolder); + + if (!tf.exists()) tf.mkdirs(); + + return resultfolder; + } + /** * Judge whether an object is null * diff --git a/src/main/java/com/gw/web/ResultBrowserController.java b/src/main/java/com/gw/web/ResultBrowserController.java index 2240391a..81519d76 100644 --- a/src/main/java/com/gw/web/ResultBrowserController.java +++ b/src/main/java/com/gw/web/ResultBrowserController.java @@ -1,13 +1,16 @@ package com.gw.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.gw.utils.BaseTool; @@ -15,8 +18,15 @@ import java.io.IOException; import java.net.MalformedURLException; import java.nio.file.*; +import java.nio.file.attribute.FileTime; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.stream.Collectors; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Controller public class ResultBrowserController { @@ -28,33 +38,89 @@ public class ResultBrowserController { // Endpoint to list image files in the directory @GetMapping("/results") @ResponseBody - public List listImageFiles() throws IOException { - String resultfolder = bt.getFileTransferFolder(); - Path rootLocation = Paths.get(resultfolder); - return Files.walk(rootLocation, 1) // 1: only files in the current folder - .filter(Files::isRegularFile) - .map(path -> path.getFileName().toString()) // Return file names + public List> listFiles(@RequestParam(defaultValue = "") String subfolder) throws IOException { + String resultfolder = bt.getResultsFolder(); + + // Navigate into the subfolder if it's provided + Path rootLocation = Paths.get(resultfolder, subfolder); + + return Files.walk(rootLocation, 1) // 1: look at files in the current folder and subfolders + .map(path -> { + Map fileDetails = new HashMap<>(); + try { + Path relativePath = rootLocation.relativize(path); + String pathWithSubfolder = subfolder + "/" + relativePath.toString(); + pathWithSubfolder = pathWithSubfolder.replaceAll("^/+",""); + + fileDetails.put("name", rootLocation.relativize(path).toString()); // Relative path + fileDetails.put("path", pathWithSubfolder); // Relative path + fileDetails.put("isDirectory", Files.isDirectory(path)); // Check if it's a directory + if (!Files.isDirectory(path)) { + fileDetails.put("size", Files.size(path)); // File size for files + } + + // Get last modified time + FileTime fileTime = Files.getLastModifiedTime(path); + + // Convert FileTime to LocalDateTime + LocalDateTime dateTime = LocalDateTime.ofInstant(fileTime.toInstant(), ZoneId.systemDefault()); + + // Format date-time to remove nanoseconds + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + String formattedDateTime = dateTime.format(formatter); + + // Add formatted last modified time to file details + fileDetails.put("modified", formattedDateTime); + } catch (IOException e) { + e.printStackTrace(); + } + return fileDetails; + }) .collect(Collectors.toList()); } - // Endpoint to serve images by filename - @GetMapping("/results/{filename:.+}") - @ResponseBody - public ResponseEntity serveFile(@PathVariable String filename) { + + @GetMapping("/download") + public ResponseEntity downloadFile(@RequestParam String path) { try { - String resultfolder = bt.getFileTransferFolder(); - Path file = Paths.get(resultfolder).resolve(filename); - Resource resource = new UrlResource(file.toUri()); + String resultfolder = bt.getResultsFolder(); + Path filePath = Paths.get(resultfolder).resolve(path).normalize(); + System.out.println("File path: " + filePath.toAbsolutePath()); + + // Create a FileSystemResource instead of UrlResource + Resource resource = new FileSystemResource(filePath.toFile()); if (resource.exists() || resource.isReadable()) { return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, - "inline; filename=\"" + filename + "\"") + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + filePath.getFileName().toString() + "\"" + ) .body(resource); } else { - throw new RuntimeException("Could not read file: " + filename); + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } - } catch (MalformedURLException e) { - throw new RuntimeException("Could not read file: " + filename, e); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + // Endpoint to serve images by filename + @GetMapping("/results/{filename:.+}") + @ResponseBody + public ResponseEntity serveFile(@PathVariable String filename) { + String resultfolder = bt.getResultsFolder(); + Path filePath = Paths.get(resultfolder).resolve(filename).normalize(); + System.out.println("File path: " + filePath.toAbsolutePath()); + + // Create a FileSystemResource instead of UrlResource + Resource resource = new FileSystemResource(filePath.toFile()); + if (resource.exists() || resource.isReadable()) { + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "inline; filename=\"" + filename + "\"") + .body(resource); + } else { + throw new RuntimeException("Could not read file: " + filename); } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fcdf3e75..983d11de 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,10 +17,9 @@ logging.level.com.gw=INFO logging.level.org.hibernate=INFO logging.level.org.apache.catalina=FATAL -# import the external configuration file if exists +# import the external configuration file if exists - this will overwrite all the above configuration spring.config.import=optional:file:${user.home}/geoweaver/application.properties - ###### To use H2 Database spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.event.merge.entity_copy_observer=allow @@ -89,9 +88,12 @@ geoweaver.prefixurl=http://localhost:8080 #database_docker_url=jdbc:mysql://db:3306/Geoweaver geoweaver.upload_file_path=temp geoweaver.temp_file_path=temp +geoweaver.result_file_path=results geoweaver.workspace=~/gw-workspace # list the allowed ssh hosts. Input * if allowing all hosts. Input localhost if only allowing the local host. geoweaver.allowed_ssh_hosts=* # list the allowed ssh clients. Input * if allowing all client IPs. Input localhost if only allowing access from local host. geoweaver.allowed_ssh_clients=* geoweaver.secret_properties_path=cc_secret.properties + + diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index 90295a4a..335832df 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -432,44 +432,151 @@ form .progress { } -.result-full-height { - height: calc(100%-20px); +/* Result page style */ + +.search-box .form-control { + border-radius: 10px; + padding-left: 40px +} + +.search-box .search-icon { + position: absolute; + left: 13px; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + fill: #545965; + width: 16px; + height: 16px +} +.card { + margin-bottom: 24px; + -webkit-box-shadow: 0 2px 3px #e4e8f0; + box-shadow: 0 2px 3px #e4e8f0; +} +.card { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid #eff0f2; + border-radius: 1rem; +} +.me-3 { + margin-right: 1rem!important; +} + +.font-size-24 { + font-size: 24px!important; +} +.avatar-title { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #3b76e1; + color: #fff; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + font-weight: 500; + height: 100%; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; } -#result-left-panel { - height: 100%; /* Full height within the parent */ - overflow-y: auto; /* Scrollable when content overflows */ + +.bg-soft-info { + background-color: rgba(87,201,235,.25)!important; } -#result-file-list { - max-height: 100vh; /* Limit the height to viewport height */ - overflow-y: auto; /* Scrollable */ - padding: 0; + +.bg-soft-primary { + background-color: rgba(59,118,225,.25)!important; } -.result-list-group-item { - cursor: pointer; - transition: background-color 0.2s, color 0.2s; + +.avatar-xs { + height: 1rem; + width: 1rem } -.result-list-group-item:hover { - background-color: #007bff; - color: #fff; + +.avatar-sm { + height: 2rem; + width: 2rem +} + +.avatar { + height: 3rem; + width: 3rem +} + +.avatar-md { + height: 4rem; + width: 4rem +} + +.avatar-lg { + height: 5rem; + width: 5rem } -.result-card { - border-radius: 0.5rem; + +.avatar-xl { + height: 6rem; + width: 6rem +} + +.avatar-title { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #3b76e1; + color: #fff; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + font-weight: 500; + height: 100%; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 100% } -.result-card-body { - padding: 1.25rem; + +.avatar-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding-left: 8px } -.result-dark-theme { - background-color: #343a40; - color: #f8f9fa; + +.avatar-group .avatar-group-item { + margin-left: -8px; + border: 2px solid #fff; + border-radius: 50%; + -webkit-transition: all .2s; + transition: all .2s } -.result-dark-theme .card { - background-color: #495057; - border: 1px solid #6c757d; + +.avatar-group .avatar-group-item:hover { + position: relative; + -webkit-transform: translateY(-2px); + transform: translateY(-2px) } -.result-dark-theme .list-group-item { - background-color: #495057; - border-color: #6c757d; + +.fw-medium { + font-weight: 500; } -.result-dark-theme .list-group-item:hover { - background-color: #6c757d; + +a { + text-decoration: none!important; } + diff --git a/src/main/resources/static/js/gw.result.browser.js b/src/main/resources/static/js/gw.result.browser.js index 194dbad1..b8875ad6 100644 --- a/src/main/resources/static/js/gw.result.browser.js +++ b/src/main/resources/static/js/gw.result.browser.js @@ -3,10 +3,161 @@ GW.result.browser = { init: function () { - GW.result.browser.loadFileList(); + // GW.result.browser.loadFileList(); + GW.result.browser.render_file_list() }, + render_file_list: function(){ + var fileTable = $('#file-list-table').DataTable({ + columns: [ + { data: 'name', render: function (data, type, row) { + let icon = ''; // Default icon + if (row.isDirectory) { + icon = ''; // Folder icon + } else { + const ext = data.split('.').pop().toLowerCase(); // Get file extension + switch (ext) { + case 'pdf': + icon = ''; + break; + case 'doc': + case 'docx': + icon = ''; + break; + case 'xls': + case 'xlsx': + icon = ''; + break; + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + icon = ''; + break; + case 'txt': + icon = ''; + break; + default: + icon = ''; + } + } + return icon + ' ' + data; // Return icon + file name + }}, + { data: 'size', render: function (data, type, row) { + if (row.isDirectory) return ''; // Don't show size for directories + return GW.result.browser.formatFileSize(data); // Convert size to appropriate unit + }}, + { data: 'modified' }, + { + data: null, + render: function (data, type, row) { + if (row.isDirectory) { + return ''; // No actions for directories + } + + const ext = row.name.split('.').pop().toLowerCase(); + let actionButtons = ''; + + // "Download" button for all files + actionButtons += `Download `; + + // "Display" button for image files + if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) { + actionButtons += `Display`; + } + + return actionButtons; + } + } + ] + }); + + GW.result.browser.loadFolderContents("", fileTable); + + // Add click event to folder rows + $('#file-list-table tbody').on('click', 'tr td:first-child', function () { + var rowData = fileTable.row($(this).closest('tr')).data(); + if (rowData.isDirectory) { + // If the row is a folder, navigate into it + var path = rowData.path; + GW.result.browser.loadFolderContents(path, fileTable); + } + }); + + // Download button click event + $('#file-list-table tbody').on('click', '.btn-download', function (e) { + e.preventDefault(); // Prevent default action + console.log("download called once") + var filePath = $(this).data('path'); + window.open('/Geoweaver/download?path=' + encodeURIComponent(filePath), '_blank'); + }); + + // Display button click event for images + $('#file-list-table tbody').on('click', '.btn-display', function (e) { + e.preventDefault(); // Prevent default action + console.log("display called once") + var path = $(this).data('path'); // Get the value of the data-path attribute + console.log('Display path:', path); // Print the data-path value to the console + var filePath = $(this).data('path'); + var imgWindow = window.open('', '_blank'); + imgWindow.document.write(''); + }); + + }, + + formatFileSize: function(bytes){ + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + // Function to load folder contents + loadFolderContents: function(folderPath = '', fileTable) { + $('#file-list-table').before(` +
+ + Loading... +
+ `); + + // Clear existing table data + fileTable.clear().draw(); + + $.ajax({ + url: '/Geoweaver/results', // API endpoint to get file list + data: { subfolder: folderPath }, // Send the current folder path + method: 'GET', + success: function (data) { + // Remove loading message + $('#loading-message').remove(); + if(folderPath!=""){ + var newItem = { + "path": "..", + "size": 0, + "name": "..", + "modified": "", + "isDirectory": true + }; + data.unshift(newItem); + } + + // Add new data to the table + fileTable.rows.add(data); + fileTable.draw(); + }, + error: function (err) { + // Remove loading message + $('#loading-message').remove(); + + // Handle error (e.g., display an error message) + alert('Failed to load data: ' + error); + } + }); + }, + loadFileList: function() { $.ajax({ url: '/Geoweaver/results', diff --git a/src/main/resources/templates/fragments/content/workspace/result-browser.html b/src/main/resources/templates/fragments/content/workspace/result-browser.html index 3b832c90..c481e929 100644 --- a/src/main/resources/templates/fragments/content/workspace/result-browser.html +++ b/src/main/resources/templates/fragments/content/workspace/result-browser.html @@ -1,26 +1,23 @@
-
-
- -
-
    - - -
-
- - -
-
-
- - -
-
-
+
+
+ + + + + + + + + + + + +
File NameSizeModifiedAction
+
diff --git a/src/main/resources/templates/fragments/head.html b/src/main/resources/templates/fragments/head.html index a71c6a17..99ff00f1 100644 --- a/src/main/resources/templates/fragments/head.html +++ b/src/main/resources/templates/fragments/head.html @@ -58,6 +58,9 @@ + + + @@ -153,4 +156,5 @@ +