diff --git a/bookmarks/pom.xml b/bookmarks/pom.xml new file mode 100644 index 0000000..5019f7d --- /dev/null +++ b/bookmarks/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + nbs-cloud-aggregator + de.77zzcx7.nbs-cloud + 1-SNAPSHOT + + + de.77zzcx7.nbs-cloud + bookmarks + jar + + + + de.77zzcx7.nbs-cloud + web-container-registry + + + de.77zzcx7.nbs-cloud + web-container-config + + + org.springframework.boot + spring-boot-starter-thymeleaf + provided + + + org.springframework.boot + spring-boot-starter-web + provided + + + + \ No newline at end of file diff --git a/bookmarks/src/main/java/de/nbscloud/bookmarks/BookmarksApp.java b/bookmarks/src/main/java/de/nbscloud/bookmarks/BookmarksApp.java new file mode 100644 index 0000000..5d96e63 --- /dev/null +++ b/bookmarks/src/main/java/de/nbscloud/bookmarks/BookmarksApp.java @@ -0,0 +1,38 @@ +package de.nbscloud.bookmarks; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class BookmarksApp implements App, InitializingBean { + @Autowired + private AppRegistry appRegistry; + + @Override + public String getId() { + return "bookmarks"; + } + + @Override + public String getIcon() { + return null; + } + + @Override + public String getStartPath() { + return this.getId(); + } + + @Override + public int getIndex() { + return 30; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.appRegistry.registerApp(this); + } +} diff --git a/dashboard/pom.xml b/dashboard/pom.xml new file mode 100644 index 0000000..476913a --- /dev/null +++ b/dashboard/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + nbs-cloud-aggregator + de.77zzcx7.nbs-cloud + 1-SNAPSHOT + + + de.77zzcx7.nbs-cloud + dashboard + jar + + + + de.77zzcx7.nbs-cloud + web-container-registry + + + de.77zzcx7.nbs-cloud + web-container-config + + + org.springframework.boot + spring-boot-starter-thymeleaf + provided + + + org.springframework.boot + spring-boot-starter-web + provided + + + + \ No newline at end of file diff --git a/dashboard/src/main/java/de/nbscloud/dashboard/DashboardApp.java b/dashboard/src/main/java/de/nbscloud/dashboard/DashboardApp.java new file mode 100644 index 0000000..2c44431 --- /dev/null +++ b/dashboard/src/main/java/de/nbscloud/dashboard/DashboardApp.java @@ -0,0 +1,38 @@ +package de.nbscloud.dashboard; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DashboardApp implements App, InitializingBean { + @Autowired + private AppRegistry appRegistry; + + @Override + public String getId() { + return "dashboard"; + } + + @Override + public String getIcon() { + return null; + } + + @Override + public String getStartPath() { + return this.getId(); + } + + @Override + public int getIndex() { + return 0; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.appRegistry.registerApp(this); + } +} diff --git a/dashboard/src/main/java/de/nbscloud/dashboard/controller/DashboardController.java b/dashboard/src/main/java/de/nbscloud/dashboard/controller/DashboardController.java new file mode 100644 index 0000000..a75362c --- /dev/null +++ b/dashboard/src/main/java/de/nbscloud/dashboard/controller/DashboardController.java @@ -0,0 +1,27 @@ +package de.nbscloud.dashboard.controller; + +import de.nbscloud.webcontainer.registry.AppRegistry; +import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import javax.servlet.http.HttpServletRequest; + +@Controller +public class DashboardController { + + @Autowired + private WebContainerSharedConfig webContainerSharedConfig; + @Autowired + private AppRegistry appRegistry; + + @GetMapping("/dashboard") + public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) { + model.addAttribute("apps", this.appRegistry.getAll()); + this.webContainerSharedConfig.addDefaults(model); + + return "dashboard/index"; + } +} diff --git a/dashboard/src/main/resources/static/css/dashboard_main.css b/dashboard/src/main/resources/static/css/dashboard_main.css new file mode 100644 index 0000000..c663e10 --- /dev/null +++ b/dashboard/src/main/resources/static/css/dashboard_main.css @@ -0,0 +1,3 @@ +#content-container { + padding-top: 2em; +} \ No newline at end of file diff --git a/dashboard/src/main/resources/templates/dashboard/index.html b/dashboard/src/main/resources/templates/dashboard/index.html new file mode 100644 index 0000000..192fcd1 --- /dev/null +++ b/dashboard/src/main/resources/templates/dashboard/index.html @@ -0,0 +1,22 @@ + + + + + + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}" /> + <link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}" /> + <link rel="stylesheet" th:href="@{/css/main.css}" /> + <link rel="stylesheet" th:href="@{/css/dashboard_main.css}" /> + <link rel="shortcut icon" th:href="@{/favicon.ico}" /> +</head> +<body> +<div id="main-container"> + <div th:replace="includes/header :: header"/> + <div id="content-container"> + Welcome to nbscloud + </div> + <div th:replace="includes/footer :: footer"/> +</div> +</body> +</html> \ No newline at end of file diff --git a/files/pom.xml b/files/pom.xml index 7be073b..47bec73 100644 --- a/files/pom.xml +++ b/files/pom.xml @@ -15,46 +15,61 @@ <packaging>jar</packaging> <dependencies> -<!-- <dependency>--> -<!-- <groupId>org.springframework.boot</groupId>--> -<!-- <artifactId>spring-boot-starter-data-jpa</artifactId>--> -<!-- </dependency>--> -<!-- <dependency>--> -<!-- <groupId>org.springframework.boot</groupId>--> -<!-- <artifactId>spring-boot-starter-security</artifactId>--> -<!-- </dependency>--> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-registry</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-config</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> + <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> - </dependency> -<!-- <dependency>--> -<!-- <groupId>org.flywaydb</groupId>--> -<!-- <artifactId>flyway-core</artifactId>--> -<!-- </dependency>--> -<!-- <dependency>--> -<!-- <groupId>org.thymeleaf.extras</groupId>--> -<!-- <artifactId>thymeleaf-extras-springsecurity5</artifactId>--> -<!-- </dependency>--> - - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> -<!-- <dependency>--> -<!-- <groupId>org.springframework.boot</groupId>--> -<!-- <artifactId>spring-boot-starter-test</artifactId>--> -<!-- <scope>test</scope>--> -<!-- </dependency>--> -<!-- <dependency>--> -<!-- <groupId>org.springframework.security</groupId>--> -<!-- <artifactId>spring-security-test</artifactId>--> -<!-- <scope>test</scope>--> -<!-- </dependency>--> + <dependency> + <groupId>org.overviewproject</groupId> + <artifactId>mime-types</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <!-- <dependency>--> + <!-- <groupId>org.flywaydb</groupId>--> + <!-- <artifactId>flyway-core</artifactId>--> + <!-- </dependency>--> + <!-- <dependency>--> + <!-- <groupId>org.thymeleaf.extras</groupId>--> + <!-- <artifactId>thymeleaf-extras-springsecurity5</artifactId>--> + <!-- </dependency>--> + + <!-- <dependency>--> + <!-- <groupId>org.springframework.boot</groupId>--> + <!-- <artifactId>spring-boot-starter-tomcat</artifactId>--> + <!-- <scope>provided</scope>--> + <!-- </dependency>--> + <!-- <dependency>--> + <!-- <groupId>org.springframework.boot</groupId>--> + <!-- <artifactId>spring-boot-starter-test</artifactId>--> + <!-- <scope>test</scope>--> + <!-- </dependency>--> + <!-- <dependency>--> + <!-- <groupId>org.springframework.security</groupId>--> + <!-- <artifactId>spring-security-test</artifactId>--> + <!-- <scope>test</scope>--> + <!-- </dependency>--> </dependencies> </project> \ No newline at end of file diff --git a/files/src/main/java/de/nbscloud/files/FileSystemService.java b/files/src/main/java/de/nbscloud/files/FileSystemService.java index 912f7eb..98677b9 100644 --- a/files/src/main/java/de/nbscloud/files/FileSystemService.java +++ b/files/src/main/java/de/nbscloud/files/FileSystemService.java @@ -1,20 +1,82 @@ package de.nbscloud.files; import de.nbscloud.files.config.FilesConfig; +import de.nbscloud.files.controller.FilesController; import de.nbscloud.files.exception.FileSystemServiceException; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.overviewproject.mime_types.GetBytesException; +import org.overviewproject.mime_types.MimeTypeDetector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @Service public class FileSystemService { - public record ContentContainer(boolean directory, String name, long size) { + private static final Logger logger = LoggerFactory.getLogger(FileSystemService.class); + + public static record ContentContainer(boolean directory, Path path, String name, long size, + LocalDateTime lastModified) { + } + + public enum SortOrder { + /** + * First by type (directories first), then by name ignoring case + */ + NATURAL(Comparator.comparing(ContentContainer::directory) + .reversed() + .thenComparing(con -> con.path().getFileName().toString().toLowerCase())), + + NATURAL_REVERSE(NATURAL.comparator.reversed()), + + SIZE_DESC(Comparator.comparing(ContentContainer::size).reversed()), + + SIZE_ASC(Comparator.comparing(ContentContainer::size)), + + LAST_MOD_DESC(Comparator.comparing(ContentContainer::lastModified).reversed()), + + LAST_MOD_ASC(Comparator.comparing(ContentContainer::lastModified)); + + private Comparator<ContentContainer> comparator; + + SortOrder(Comparator<ContentContainer> comparator) { + this.comparator = comparator; + } + + public static SortOrder ofQueryParam(String param) { + return Arrays.stream(values()).filter(so -> so.name().equalsIgnoreCase(param)).findFirst() + .orElse(SortOrder.NATURAL); + } + } + + public enum MimeTypeFilter { + IMAGES("image/jpeg", "image/png"), + VIDEOS("video/mpeg", "video/mp4", "video/quicktime", "video/x-msvideo"), + AUDIO("audio/mpeg", "audio/x-wav", "audio/x-flac", "audio/flac", "audio/MPA", "audio/mpa-robust"); + + private List<String> mimeTypes; + + MimeTypeFilter(String... mimeTypes) { + this.mimeTypes = Arrays.asList(mimeTypes); + } + + public boolean match(String mimeType) { + return this.mimeTypes.contains(mimeType); + } } @Autowired @@ -23,44 +85,211 @@ public class FileSystemService { @Autowired private LocationTracker locationTracker; + private MimeTypeDetector mimeTypeDetector = new MimeTypeDetector(); + public Path createDirectory(String name) { try { - return Files.createDirectory(this.locationTracker.getCurrentLocation().resolve(name)); + return Files.createDirectories(this.locationTracker.getCurrentLocation().resolve(name)); } catch (IOException e) { throw new FileSystemServiceException("Could not create directory", e); } } + public void createFile(String name, byte[] content) { + try { + Files.write(this.locationTracker.getCurrentLocation() + .resolve(name), content, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); + } catch (IOException e) { + throw new FileSystemServiceException("Could not create file", e); + } + } + + public void createFile(Path path, byte[] content) { + try { + Files.write(path, content, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); + } catch (IOException e) { + throw new FileSystemServiceException("Could not create file", e); + } + } + public boolean delete(String name) { try { + // TODO does only delete dirs if they are empty - but maybe we want that? return Files.deleteIfExists(this.locationTracker.getCurrentLocation().resolve(name)); } catch (IOException e) { throw new FileSystemServiceException("Could not delete file", e); } } - // TODO soft vs hard delete - first move into /trash then delete from there public Path move(Path originalPath, Path newPath) { try { - return Files.move(originalPath, newPath); + return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { throw new FileSystemServiceException("Could not move file", e); } } - public List<ContentContainer> list() { + public byte[] get(String name) { try { - return Files.list(this.locationTracker.getCurrentLocation()) - .map(path -> { - try { - return new ContentContainer(Files.isDirectory(path), - path.getFileName().toString(), - Files.size(path)); - } catch (IOException e) { - throw new FileSystemServiceException("Could not list files", e); - } - }) - .collect(Collectors.toList()); + return Files.readAllBytes(this.locationTracker.getCurrentLocation().resolve(name)); + } catch (IOException e) { + throw new FileSystemServiceException("Could not get file", e); + } + } + + public void stream(String name, OutputStream outputStream) { + try { + Files.copy(this.locationTracker.getCurrentLocation().resolve(name), outputStream); + } catch (IOException e) { + throw new FileSystemServiceException("Could not get file", e); + } + } + + public InputStream stream(String name) { + try { + return Files.newInputStream(this.locationTracker.getCurrentLocation().resolve(name)); + } catch (IOException e) { + throw new FileSystemServiceException("Could not get file", e); + } + } + + public long getSize(String name) { + try { + return Files.size(this.locationTracker.getCurrentLocation().resolve(name)); + } catch (IOException e) { + throw new FileSystemServiceException("Could not get file", e); + } + } + + public String getMimeType(String name) { + try { + final String detectedMimeType = this.mimeTypeDetector.detectMimeType(this.locationTracker.getCurrentLocation() + .resolve(name)); + + logger.debug("Detected mime type {} for file {}", detectedMimeType, name); + + return detectedMimeType; + } catch (GetBytesException e) { + throw new FileSystemServiceException("Could not get mime type", e); + } + } + + public List<String> getFilesWithMimeType(MimeTypeFilter mimeTypeFilter) { + try { + final Map<String, CompletableFuture<String>> fileFutureMap = new HashMap<>(); + + Files.list(this.locationTracker.getCurrentLocation()) + .filter(Files::isRegularFile) + .forEach(path -> { + fileFutureMap.put(path.getFileName().toString(), this.mimeTypeDetector.detectMimeTypeAsync(path) + .toCompletableFuture()); + }); + + CompletableFuture.allOf(fileFutureMap.values().toArray(new CompletableFuture[0])).join(); + + return fileFutureMap.entrySet().stream().filter(e -> { + try { + return mimeTypeFilter.match(e.getValue().get()); + } catch (InterruptedException | ExecutionException ex) { + throw new FileSystemServiceException("Could not determine mime types", ex); + } + }).map(Map.Entry::getKey).collect(Collectors.toList()); + } catch (IOException e) { + throw new FileSystemServiceException("Could not get file", e); + } + } + + public List<ContentContainer> list(SortOrder sortOrder) { + try { + List<ContentContainer> contentList = Files.list(this.locationTracker.getCurrentLocation()) + .filter(path -> { + boolean ok = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.isDirectory(path); + + try { + return ok && (this.filesConfig.isFilterHidden() ? !Files.isHidden(path) : true); + } catch (IOException e) { + throw new FileSystemServiceException("Could not filter files", e); + } + }) + .map(path -> { + try { + final boolean isDir = Files.isDirectory(path); + + return new ContentContainer(isDir, + this.locationTracker.getRelativeToBaseDir(path), + path.getFileName().toString(), + isDir ? FileUtils.sizeOfDirectory(path.toFile()) : Files.size(path), + LocalDateTime.ofInstant(Files.getLastModifiedTime(path) + .toInstant(), ZoneId.systemDefault())); + } catch (IOException e) { + throw new FileSystemServiceException("Could not list files", e); + } + }) + .collect(Collectors.toList()); + contentList.sort(sortOrder.comparator); + + return contentList; + } catch (IOException e) { + throw new FileSystemServiceException("Could not list files", e); + } + } + + public List<String> collectDirs(String sourceFile) { + try { + final List<String> resultList = new ArrayList<>(); + final Path sourcePath = this.locationTracker.getCurrentLocation().resolve(sourceFile); + final boolean sourceIsDir = Files.isDirectory(sourcePath); + + Files.walkFileTree(this.locationTracker.getBaseDirPath(), new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + super.visitFile(dir, attrs); + + if (sourceIsDir && sourcePath.equals(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + + if (filesConfig.isFilterHidden() ? Files.isHidden(dir) : false) { + return FileVisitResult.SKIP_SUBTREE; + } + + if (dir.toString().contains(filesConfig.getTrashBinName())) { + return FileVisitResult.SKIP_SUBTREE; + } + + if (dir.toString().contains(filesConfig.getSharesName())) { + return FileVisitResult.SKIP_SUBTREE; + } + + final String relPath = locationTracker.getRelativeToBaseDir(dir).toString(); + + resultList.add("/" + relPath); + + return FileVisitResult.CONTINUE; + } + + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + return FileVisitResult.SKIP_SUBTREE; + } + + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.SKIP_SUBTREE; + } + }); + + Collections.sort(resultList); + resultList.sort((s1, s2) -> { + if (StringUtils.equals(s1, "/")) { + return 1; + } + if (StringUtils.equals(s2, "/")) { + return 1; + } + + return 0; + }); + + return resultList; } catch (IOException e) { throw new FileSystemServiceException("Could not list files", e); } diff --git a/files/src/main/java/de/nbscloud/files/FilesApp.java b/files/src/main/java/de/nbscloud/files/FilesApp.java new file mode 100644 index 0000000..f49f557 --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/FilesApp.java @@ -0,0 +1,38 @@ +package de.nbscloud.files; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class FilesApp implements App, InitializingBean { + @Autowired + private AppRegistry appRegistry; + + @Override + public String getId() { + return "files"; + } + + @Override + public String getIcon() { + return null; + } + + @Override + public String getStartPath() { + return this.getId() + "/browse"; + } + + @Override + public int getIndex() { + return 10; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.appRegistry.registerApp(this); + } +} diff --git a/files/src/main/java/de/nbscloud/files/FilesFormatter.java b/files/src/main/java/de/nbscloud/files/FilesFormatter.java new file mode 100644 index 0000000..9749044 --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/FilesFormatter.java @@ -0,0 +1,110 @@ +package de.nbscloud.files; + +import de.nbscloud.files.config.FilesConfig; +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; + +@Component +public class FilesFormatter { + enum Units { + P(FileUtils.ONE_KB * FileUtils.ONE_KB * FileUtils.ONE_KB * FileUtils.ONE_KB * FileUtils.ONE_KB, null), + T(FileUtils.ONE_KB * FileUtils.ONE_KB * FileUtils.ONE_KB * FileUtils.ONE_KB, P), + G(FileUtils.ONE_KB * FileUtils.ONE_KB * FileUtils.ONE_KB, T), + M(FileUtils.ONE_KB * FileUtils.ONE_KB, G), + K(FileUtils.ONE_KB, M); + + final long minByteSize; + final Units immediatlyHigherUnit; + + private Units(long minByteSize, Units immediatlyHigherUnit) { + this.minByteSize = minByteSize; + this.immediatlyHigherUnit = immediatlyHigherUnit; + } + } + + private static final MathContext MC_BYTE_TO_HUMAN_LESS_100 = new MathContext(2, RoundingMode.UP); + private static final MathContext MC_BYTE_TO_HUMAN_LESS_1000 = new MathContext(3, RoundingMode.UP); + private static final MathContext MC_BYTE_TO_HUMAN_MORE_1000 = new MathContext(4, RoundingMode.UP); + + @Autowired + private FilesConfig filesConfig; + + public String truncateFilename(String name) { + String filename = name; + final int lastDotIndex = filename.lastIndexOf("."); + final int configMaxLength = this.filesConfig.getTruncateFileNameChars() - 5; // -5 for ellipses " ... " + final int filenameLength = filename.length(); + + if (lastDotIndex <= 0) { + if ((configMaxLength + 5) >= filenameLength) { + return filename; + } else { + return filename.substring(0, configMaxLength) + "..."; + } + } + + final String suffix = filename.substring(lastDotIndex, filenameLength); + + if ((configMaxLength + 5) >= filenameLength) { + return filename; + } else { + filename = filename.replace(suffix, ""); + + return filename.substring(0, configMaxLength - suffix.length()) + " ... " + suffix; + } + } + + public boolean needsTruncate(String name) { + return !name.equals(truncateFilename(name)); + } + + // Taken from: https://issues.apache.org/jira/secure/attachment/12670724/byteCountToHumanReadableGnu.patch + public String formatSize(final long size) { + String humanReadableOutput = null; + for (Units u : Units.values()) { + if (size >= u.minByteSize) { + double numOfUnits = (double) size / (double) u.minByteSize; + + // if there is no decimals and the number is less than ten + // then we want to format as X.0; numOfUnits.toString() does it for us + if (Math.ceil(numOfUnits) == numOfUnits && numOfUnits < 10d) { + humanReadableOutput = numOfUnits + u.toString(); + } else { + // we need to do some rounding + BigDecimal bdNumbOfUnits = new BigDecimal(numOfUnits); + + if (numOfUnits < 100d) { + bdNumbOfUnits = bdNumbOfUnits.round(MC_BYTE_TO_HUMAN_LESS_100); + } else if (numOfUnits < 1000d) { + bdNumbOfUnits = bdNumbOfUnits.round(MC_BYTE_TO_HUMAN_LESS_1000); + } else { + bdNumbOfUnits = bdNumbOfUnits.round(MC_BYTE_TO_HUMAN_MORE_1000); + // special case, if we get 1024, we should display one of the higher unit!! + if (bdNumbOfUnits.longValue() == FileUtils.ONE_KB && u.immediatlyHigherUnit != null) { + humanReadableOutput = "1.0" + u.immediatlyHigherUnit.toString(); + break; + } + } + + humanReadableOutput = bdNumbOfUnits.toPlainString() + u.toString(); + } + } + // exit the loop + if (humanReadableOutput != null) { + break; + } + } + + if (humanReadableOutput == null) { + humanReadableOutput = "" + size; + } + return humanReadableOutput; + } + +} diff --git a/files/src/main/java/de/nbscloud/files/LocationTracker.java b/files/src/main/java/de/nbscloud/files/LocationTracker.java index d527cfe..ded9ff4 100644 --- a/files/src/main/java/de/nbscloud/files/LocationTracker.java +++ b/files/src/main/java/de/nbscloud/files/LocationTracker.java @@ -1,6 +1,9 @@ package de.nbscloud.files; import de.nbscloud.files.config.FilesConfig; +import de.nbscloud.files.controller.FilesController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -9,6 +12,8 @@ import java.nio.file.Paths; @Component public class LocationTracker { + private static final Logger logger = LoggerFactory.getLogger(LocationTracker.class); + @Autowired private FilesConfig filesConfig; @@ -17,10 +22,14 @@ public class LocationTracker { public void init() { this.baseDirPath = Paths.get(this.filesConfig.getBaseDir()); - this.currentLocation = baseDirPath.resolve(""); + this.currentLocation = this.baseDirPath.resolve(""); + + logger.info("Initialized location to {}", this.currentLocation); } public void reset() { + logger.debug("Reset location"); + init(); } @@ -28,10 +37,34 @@ public class LocationTracker { return this.currentLocation; } + public Path getRelativeLocation() { + return this.baseDirPath.relativize(this.currentLocation); + } + + public Path getRelativeToBaseDir(Path other) { + return this.baseDirPath.relativize(other); + } + + public Path getTrashBin() { + return this.baseDirPath.resolve(this.filesConfig.getTrashBinName()); + } + + public boolean isTrashBin() { + return this.currentLocation.equals(this.getTrashBin()); + } + + public boolean isBasePath() { + return this.baseDirPath.equals(this.currentLocation); + } + + public Path getParent() { + return this.baseDirPath.relativize(this.currentLocation.getParent()); + } + public void setCurrentLocation(String navigateTo) { validate(navigateTo); - this.currentLocation = this.currentLocation.resolve(navigateTo); + this.currentLocation = this.baseDirPath.resolve(navigateTo); } private void validate(String navigateTo) { @@ -47,8 +80,12 @@ public class LocationTracker { throw new IllegalStateException("Absolute path: " + navigateTo); } - if(!this.currentLocation.resolve(navigateTo).startsWith(this.baseDirPath)) { + if(!this.baseDirPath.resolve(navigateTo).startsWith(this.baseDirPath)) { throw new IllegalStateException("Illegal path: " + navigateTo); } } + + public Path getBaseDirPath() { + return baseDirPath; + } } diff --git a/files/src/main/java/de/nbscloud/files/config/FilesConfig.java b/files/src/main/java/de/nbscloud/files/config/FilesConfig.java index 698d33d..d59806e 100644 --- a/files/src/main/java/de/nbscloud/files/config/FilesConfig.java +++ b/files/src/main/java/de/nbscloud/files/config/FilesConfig.java @@ -2,11 +2,18 @@ package de.nbscloud.files.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; @Configuration @ConfigurationProperties(prefix = "nbs-cloud.files") +@PropertySource("classpath:/config/files-application.properties") public class FilesConfig { private String baseDir; + private boolean filterHidden; + private boolean useTrashBin; + private String trashBinName; + private int truncateFileNameChars; + private String sharesName; public String getBaseDir() { return baseDir; @@ -15,4 +22,44 @@ public class FilesConfig { public void setBaseDir(String baseDir) { this.baseDir = baseDir; } + + public boolean isFilterHidden() { + return filterHidden; + } + + public void setFilterHidden(boolean filterHidden) { + this.filterHidden = filterHidden; + } + + public boolean isUseTrashBin() { + return useTrashBin; + } + + public void setUseTrashBin(boolean useTrashBin) { + this.useTrashBin = useTrashBin; + } + + public String getTrashBinName() { + return trashBinName; + } + + public void setTrashBinName(String trashBinName) { + this.trashBinName = trashBinName; + } + + public int getTruncateFileNameChars() { + return truncateFileNameChars; + } + + public void setTruncateFileNameChars(int truncateFileNameChars) { + this.truncateFileNameChars = truncateFileNameChars; + } + + public String getSharesName() { + return sharesName; + } + + public void setSharesName(String sharesName) { + this.sharesName = sharesName; + } } diff --git a/files/src/main/java/de/nbscloud/files/controller/FilesController.java b/files/src/main/java/de/nbscloud/files/controller/FilesController.java new file mode 100644 index 0000000..391b349 --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/controller/FilesController.java @@ -0,0 +1,336 @@ +package de.nbscloud.files.controller; + +import de.nbscloud.files.FileSystemService; +import de.nbscloud.files.LocationTracker; +import de.nbscloud.files.config.FilesConfig; +import de.nbscloud.files.exception.FileSystemServiceException; +import de.nbscloud.files.form.RenameForm; +import de.nbscloud.webcontainer.registry.AppRegistry; +import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.util.UriUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Path; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Controller +public class FilesController implements InitializingBean { + + private static final Logger logger = LoggerFactory.getLogger(FilesController.class); + + @Autowired + private FilesConfig filesConfig; + @Autowired + private LocationTracker locationTracker; + @Autowired + private WebContainerSharedConfig webContainerSharedConfig; + @Autowired + private FileSystemService fileSystemService; + @Autowired + private AppRegistry appRegistry; + + // We have to temporarily store messages as we redirect: in the manipulation methods + // so everything we add to the model will be gone + private final List<String> errors = new ArrayList<>(); + private final List<String> shareInfo = new ArrayList<>(); + private final List<String> infoMessages = new ArrayList<>(); + + @GetMapping("/files/browse/**") + public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) { + final FileSystemService.SortOrder order = FileSystemService.SortOrder.ofQueryParam(sortOrder); + + updateLocation(httpServletRequest); + + model.addAttribute("errors", getAndClear(this.errors)); + model.addAttribute("infoMessages", getAndClear(this.infoMessages)); + model.addAttribute("shareInfo", getAndClear(this.shareInfo)); + model.addAttribute("currentLocation", getCurrentLocationPrefixed()); + model.addAttribute("content", getContent(order)); + model.addAttribute("sortOrder", order); + model.addAttribute("apps", this.appRegistry.getAll()); + this.webContainerSharedConfig.addDefaults(model); + + return "files/filesIndex"; + } + + @GetMapping("/files/delete") + public String deleteFile(String filename) { + if (this.filesConfig.isUseTrashBin() && !this.locationTracker.isTrashBin()) { + try { + // Soft delete + this.fileSystemService.move( + this.locationTracker.getCurrentLocation().resolve(Path.of(filename)), + this.locationTracker.getTrashBin().resolve(filename)); + + this.infoMessages.add("nbscloud.files.delete.success"); + } catch (RuntimeException e) { + logger.error("Could not soft delete file", e); + + this.errors.add(e.getMessage()); + } + } else { + // Hard delete + this.fileSystemService.delete(filename); + + this.infoMessages.add("nbscloud.files.delete.success"); + } + + return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation(); + } + + @GetMapping("/files/rename") + public String rename(Model model, String filename) { + model.addAttribute("currentLocation", getCurrentLocationPrefixed()); + model.addAttribute("form", new RenameForm(filename, getCurrentLocation(), filename)); + model.addAttribute("targetDirs", this.fileSystemService.collectDirs(filename)); + model.addAttribute("apps", this.appRegistry.getAll()); + this.webContainerSharedConfig.addDefaults(model); + + return "files/rename"; + } + + @PostMapping("/files/doRename") + public String doRename(RenameForm form) { + final Path sourcePath = this.locationTracker.getCurrentLocation().resolve(Path.of(form.getOriginalFilename())); + + // We navigate to the target dir, so we display the content of that dir after the move + this.locationTracker.setCurrentLocation(form.getTargetDir().substring(1)); + + final Path targetPath = this.locationTracker.getCurrentLocation().resolve(Path.of(form.getFilename())); + + try { + this.fileSystemService.move(sourcePath, targetPath); + + this.infoMessages.add("nbscloud.files.rename.success"); + } catch (RuntimeException e) { + logger.error("Could not rename file", e); + + this.errors.add(e.getMessage()); + } + + return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation(); + } + + @GetMapping("/files/download") + public ResponseEntity<Resource> downloadFile(String filename) { + // TODO download of directories via .zip? Also needs to be streaming + + try { + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") + .header(HttpHeaders.CONTENT_TYPE, this.fileSystemService.getMimeType(filename)) + .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename))) + .body(new InputStreamResource(this.fileSystemService.stream(filename))); + } catch (RuntimeException e) { + logger.error("Could not get file", e); + + return ResponseEntity.internalServerError().body(null); + } + } + + @PostMapping("/files/upload") + public String uploadFiles(@RequestParam("files") MultipartFile[] files) { + for (MultipartFile file : files) { + try { + this.fileSystemService.createFile(file.getOriginalFilename(), file.getBytes()); + + this.infoMessages.add("nbscloud.files.created.file.success"); + } catch (IOException e) { + logger.error("Could not upload file", e); + + this.errors.add(e.getMessage()); + } + } + + return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation(); + } + + @PostMapping("/files/createDir") + public String createDir(@RequestParam("dirName") String dirName) { + try { + this.fileSystemService.createDirectory(dirName); + + this.infoMessages.add("nbscloud.files.created.dir.success"); + } catch (RuntimeException e) { + logger.error("Could not create dir", e); + + this.errors.add(e.getMessage()); + } + + return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation(); + } + + @GetMapping("/files/share") + public String share(String filename) { + final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.getCurrentLocation() + .resolve(filename)); + final String shareUuid = UUID.randomUUID().toString(); + + try { + this.fileSystemService.createFile(this.locationTracker.getBaseDirPath() + .resolve(this.filesConfig.getSharesName()) + .resolve(shareUuid), filePath.toString() + .getBytes(StandardCharsets.UTF_8)); + + this.shareInfo.add("/files/shares?shareUuid=" + shareUuid); + } catch (RuntimeException e) { + logger.error("Could not share file", e); + + this.errors.add(e.getMessage()); + } + + + return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation(); + } + + @GetMapping("files/shares") + public ResponseEntity shares(String shareUuid) { + try { + final String sharedFile = new String(this.fileSystemService.get(this.locationTracker.getBaseDirPath() + .resolve(this.filesConfig.getSharesName()) + .resolve(shareUuid) + .toString())); + final Path sharedFilePath = this.locationTracker.getBaseDirPath().resolve(sharedFile); + final String filename = sharedFilePath.getFileName().toString(); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") + .header(HttpHeaders.CONTENT_TYPE, this.fileSystemService.getMimeType(filename)) + .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename))) + .body(new InputStreamResource(this.fileSystemService.stream(filename))); + } catch (RuntimeException e) { + logger.error("Could not get shared file", e); + + return ResponseEntity.internalServerError().body(null); + } + } + + @GetMapping("files/preview") + public void preview(HttpServletResponse response, String filename) { + try { + response.setContentType(this.fileSystemService.getMimeType(filename)); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename))); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "filename=\"" + filename + "\""); + + this.fileSystemService.stream(filename, response.getOutputStream()); + } catch (FileSystemServiceException | IOException e) { + logger.error("Could not preview file", e); + } + } + + @GetMapping("files/gallery") + public String gallery(Model model) { + try { + final List<String> files = this.fileSystemService.getFilesWithMimeType(FileSystemService.MimeTypeFilter.IMAGES); + + model.addAttribute("files", files); + model.addAttribute("apps", this.appRegistry.getAll()); + this.webContainerSharedConfig.addDefaults(model); + + return "files/gallery"; + } catch (FileSystemServiceException e) { + logger.error("Could not generate gallery", e); + + this.errors.add(e.getMessage()); + + return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation(); + } + } + + private List<String> getAndClear(List<String> source) { + final List<String> retList = new ArrayList<>(source); + + source.clear(); + + return retList; + } + + private List<FileSystemService.ContentContainer> getContent(FileSystemService.SortOrder order) { + final List<FileSystemService.ContentContainer> contentList = this.fileSystemService.list(order); + + if (!this.locationTracker.isBasePath()) { + contentList.add(0, + new FileSystemService.ContentContainer(true, + this.locationTracker.getParent(), + "..", + 0L, + LocalDateTime.ofInstant(Instant.EPOCH, ZoneId.of("UTC")))); + } + + return contentList; + } + + private void updateLocation(HttpServletRequest httpServletRequest) { + final String requestUrl = httpServletRequest.getRequestURL().toString(); + final String[] split = requestUrl.split("/files/browse"); + + if (split.length > 1) { + this.locationTracker.setCurrentLocation(UriUtils.decode(split[1].substring(1), StandardCharsets.UTF_8)); + } else { + this.locationTracker.reset(); + } + } + + private String getCurrentLocation() { + if (this.locationTracker.getRelativeLocation().toString().isEmpty()) { + return "/"; + } + + return this.locationTracker.getRelativeLocation().toString(); + } + + private String getCurrentLocationPrefixed() { + if (this.locationTracker.getRelativeLocation().toString().isEmpty()) { + return "/"; + } + + return "/" + this.locationTracker.getRelativeLocation().toString(); + } + + @Override + public void afterPropertiesSet() throws Exception { + this.locationTracker.init(); + + if (this.filesConfig.isUseTrashBin()) { + try { + this.fileSystemService.createDirectory(this.filesConfig.getTrashBinName()); + } catch (FileSystemServiceException e) { + if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) { + throw e; + } + // else: do nothing + } + } + try { + this.fileSystemService.createDirectory(this.filesConfig.getSharesName()); + } catch (FileSystemServiceException e) { + if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) { + throw e; + } + // else: do nothing + } + } +} diff --git a/files/src/main/java/de/nbscloud/files/form/RenameForm.java b/files/src/main/java/de/nbscloud/files/form/RenameForm.java new file mode 100644 index 0000000..77c17b0 --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/form/RenameForm.java @@ -0,0 +1,37 @@ +package de.nbscloud.files.form; + +public class RenameForm { + private String filename; + private String targetDir; + private String originalFilename; + + public RenameForm(String filename, String targetDir, String originalFilename) { + this.filename = filename; + this.targetDir = targetDir; + this.originalFilename = originalFilename; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getTargetDir() { + return targetDir; + } + + public void setTargetDir(String targetDir) { + this.targetDir = targetDir; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } +} diff --git a/files/src/main/java/de/nbscloud/files/task/OrphanedThumbnailCleanup.java b/files/src/main/java/de/nbscloud/files/task/OrphanedThumbnailCleanup.java new file mode 100644 index 0000000..2d0dd86 --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/task/OrphanedThumbnailCleanup.java @@ -0,0 +1,4 @@ +package de.nbscloud.files.task; + +public class OrphanedThumbnailCleanup { +} diff --git a/files/src/main/resources/config/application.properties b/files/src/main/resources/config/application.properties deleted file mode 100644 index 9c3732e..0000000 --- a/files/src/main/resources/config/application.properties +++ /dev/null @@ -1,14 +0,0 @@ -### This is the main configuration file of the application. -### Filtering of the @...@ values happens via the maven-resource-plugin. The execution of the plugin is configured in -### the Spring Boot parent POM.// The same property exists on the server, look there for documentation -spring.profiles.active=@activeProfiles@ - -info.app.name=NoBullShit Cloud - Files app -info.app.description=A simple web file admin app -info.build.group=@project.groupId@ -info.build.artifact=@project.artifactId@ -info.build.version=@project.version@ - -nbs-cloud.files.baseDir=/home/marius - -spring.flyway.enabled=false \ No newline at end of file diff --git a/files/src/main/resources/config/files-application.properties b/files/src/main/resources/config/files-application.properties new file mode 100644 index 0000000..91a975b --- /dev/null +++ b/files/src/main/resources/config/files-application.properties @@ -0,0 +1,29 @@ +spring.profiles.active=@activeProfiles@ + +info.app.name=NoBullShit Cloud - Files app +info.app.description=A simple web file admin app + +# Knob to configure the base dir where files stores and reads files +# Make sure the permissions match +#nbs-cloud.files.baseDir=/home/marius/nbstest +nbs-cloud.files.baseDir=/home/marius + +# Knob to configure whether hidden files (e.g. starting with '.' on *NIX) +# will be filtered in the file view +# Possible values: true|false +nbs-cloud.files.filterHidden=true + +# Knob to configure whether a trash bin should be used +# or the files should be hard deleted +# Possible values: true|false +nbs-cloud.files.useTrashBin=true + +# Knob to configure the name of the trash bin directory +nbs-cloud.files.trashBinName=nbs.internal/nbs.trashbin + +# Knob to configure the amount of character after which the filename +# will be truncated +nbs-cloud.files.truncateFileNameChars=130 + +# Knob to configure the name of the shares directory +nbs-cloud.files.sharesName=nbs.internal/nbs.shares \ No newline at end of file diff --git a/files/src/main/resources/i18n/files_messages.properties b/files/src/main/resources/i18n/files_messages.properties new file mode 100644 index 0000000..9247cbf --- /dev/null +++ b/files/src/main/resources/i18n/files_messages.properties @@ -0,0 +1,26 @@ +nbscloud.files.index.title=nbscloud - files\:\u0020 + +nbscloud.files.files-content-table.table-header.filename=Filename +nbscloud.files.files-content-table.table-header.size=Size +nbscloud.files.files-content-table.table-header.lastmodified=Last modified +nbscloud.files.files-content-table.table-header.actions=Actions + +nbscloud.files-content-table.table.actions.delete=Delete +nbscloud.files-content-table.table.actions.rename=Rename/move +nbscloud.files-content-table.table.actions.download=Download +nbscloud.files-content-table.table.actions.share=Share +nbscloud.files-content-table.table.actions.preview=Preview + +nbscloud.files.rename.title=nbscloud - files\: rename/move \u0020 + +nbscloud.files.rename.label.filename=Filename\: +nbscloud.files.rename.label.targetDir=Target directory\: + +nbscloud.files.rename.submit=Rename/move + +nbscloud.files.share-message=Shared file\:\u0020 + +nbscloud.files.delete.success=File deleted +nbscloud.files.rename.success=File renamed +nbscloud.files.created.file.success=File created +nbscloud.files.created.dir.success=Directory created \ No newline at end of file diff --git a/files/src/main/resources/i18n/files_messages_de_DE.properties b/files/src/main/resources/i18n/files_messages_de_DE.properties new file mode 100644 index 0000000..a0f2eb6 --- /dev/null +++ b/files/src/main/resources/i18n/files_messages_de_DE.properties @@ -0,0 +1,26 @@ +nbscloud.files.index.title=nbscloud - Dateien\:\u0020 + +nbscloud.files.files-content-table.table-header.filename=Dateiname +nbscloud.files.files-content-table.table-header.size=Gr\u00F6\u00DFe +nbscloud.files.files-content-table.table-header.lastmodified=Zuletzt ge\u00E4ndert +nbscloud.files.files-content-table.table-header.actions=Aktionen + +nbscloud.files-content-table.table.actions.delete=L\u00F6schen +nbscloud.files-content-table.table.actions.rename=Umbenennen/verschieben +nbscloud.files-content-table.table.actions.download=Download +nbscloud.files-content-table.table.actions.share=Teilen +nbscloud.files-content-table.table.actions.preview=Vorschau + +nbscloud.files.rename.title=nbscloud - Dateien\: umbenennen/verschieben \u0020 + +nbscloud.files.rename.label.filename=Dateiname\: +nbscloud.files.rename.label.targetDir=Zielverzeichnis\: + +nbscloud.files.rename.submit=Umbenennen/verschieben + +nbscloud.files.share-message=Datei geteilt\:\u0020 + +nbscloud.files.delete.success=Datei gel\u00F6scht +nbscloud.files.rename.success=Datei verschoben/umbenannt +nbscloud.files.created.file.success=Datei erstellt +nbscloud.files.created.dir.success=Verzeichnis erstellt \ No newline at end of file diff --git a/files/src/main/resources/static/css/files_gallery.css b/files/src/main/resources/static/css/files_gallery.css new file mode 100644 index 0000000..38d5443 --- /dev/null +++ b/files/src/main/resources/static/css/files_gallery.css @@ -0,0 +1,39 @@ +#main-container { + padding-left: unset !important; + padding-right: unset !important; + padding-top: unset !important; + padding-bottom: unset !important; +} + +#gallery-container { + padding-top: 2em; +} + +#header-container { + padding-left: 3em; + padding-top: 3em; + padding-left: 3em; +} + +.galleryImage { + width: 15%; + padding-inline: 0.5em; + padding-bottom: 0.5em; + vertical-align: top; +} + +@media only screen and (max-width: 450px) { + .galleryImage { + width: 45%; + } + + #gallery-container { + padding-top: 1em; + } + + #header-container { + padding-left: 1em; + padding-top: 1em; + padding-left: 1em; + } +} \ No newline at end of file diff --git a/files/src/main/resources/static/css/files_main.css b/files/src/main/resources/static/css/files_main.css new file mode 100644 index 0000000..0c01f51 --- /dev/null +++ b/files/src/main/resources/static/css/files_main.css @@ -0,0 +1,170 @@ +#files-content-table { + width: 100%; +} + +#files-content-table th { + position: sticky; + background-color: var(--background-color); + top: 0px; +} + +#type-column { + width: 1%; +} + +#name-column { + width: auto; +} + +#size-column { + width: 1%; +} + +#lastmod-column { + width: 1%; +} + +#actions-column { + width: 1%; +} + +.files-table-padded-col { + padding-right: 2em; +} + +.files-table-text-align-right { + text-align: right; +} + +#files-content-table > tbody > tr > td > details > summary { + list-style-type: none; +} + +#files-content-table > tbody > tr > td > details[open] > summary { + list-style-type: none; +} + +#files-content-table > tbody > tr > td > details > summary:hover { + cursor: pointer; +} + +#rename-form { + margin-top: 3em; + margin-left: 3em; +} + +#rename-form * { + display: block; + margin-top: 1em; + box-sizing: border-box; +} +#rename-form > select > * { + margin-top: unset; +} + +#rename-form > select { + width: 70em; +} + +#rename-form > input[type=text] { + width: 70em; +} + +#menu-container { + padding-top: 2em; + margin-left: 3em; +} + +.files-menu-icon { + background: none !important; + border: none; + padding: 0 !important; + color: var(--text-color); + cursor: pointer; + font-size: 2em; +} + +.files-menu-icon:hover { + color: var(--link-color); +} + +#content-container { + padding-top: unset !important; +} + +#create-dir-container > details > summary { + list-style-type: none; +} + +#create-dir-container > details[open] > summary { + list-style-type: none; +} + +#menu-container > div { + display: inline-block !important; +} + +.create-dir-container-details-modal-content { + padding: 0.5em; + pointer-events: all; + display: inline; +} + +.create-dir-container-details-modal { + background: var(--background-color); + filter: brightness(var(--background-brightness)); + border-radius: 0.3em; + left: auto; + top: auto; + right: auto; + bottom: auto; + padding: 0; + pointer-events: none; + position: absolute; + display: flex; + flex-direction: column; + z-index: 100; +} + +.menu-spacer { + width: 2em; +} + +.messageContainer { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; +} + +.message { + margin-top: 2em; + border: 1px; + border-style: solid; + border-radius: 0.3em; + padding: 1em; + display: inline-block; +} + +.shareMessage { + border-color: var(--good-color) !important; +} + +.infoMessage { + border-color: var(--good-color) !important; +} + +.errorMessage { + border-color: var(--error-color) !important; +} + +@media only screen and (max-width: 450px) { + #menu-container { + padding-top: 1em; + margin-left: 1em; + } + + .menu-spacer { + width: 1em; + } +} \ No newline at end of file diff --git a/files/src/main/resources/templates/files/filesIndex.html b/files/src/main/resources/templates/files/filesIndex.html new file mode 100644 index 0000000..299217d --- /dev/null +++ b/files/src/main/resources/templates/files/filesIndex.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{nbscloud.files.index.title} + ${currentLocation}"/> + + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/> + <link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/> + <link rel="stylesheet" th:href="@{/css/main.css}"/> + <link rel="stylesheet" th:href="@{/css/files_main.css}"/> + <link rel="shortcut icon" th:href="@{/favicon.ico}"/> +</head> +<body> +<div id="main-container"> + <div th:replace="includes/header :: header"/> + <div class="messageContainer" th:each="shareInfo : ${shareInfo}"> + <div th:if="${!shareInfo.isEmpty()}" class="shareMessage message"> + <span th:text="#{'nbscloud.files.share-message'}"/> + <a th:href="@{${shareInfo}}" th:text="${shareInfo}"/> + </div> + </div> + <div class="messageContainer" th:each="error : ${errors}"> + <div th:if="${!errors.isEmpty()}" class="errorMessage message"> + <span th:text="${error}"/> + </div> + </div> + <div class="messageContainer" th:each="infoMessage : ${infoMessages}"> + <div th:if="${!infoMessages.isEmpty()}" class="infoMessage message"> + <span th:text="#{${infoMessage}}"/> + </div> + </div> + <div th:replace="files/includes/menu :: menu"/> + <div id="content-container"> + <table id="files-content-table"> + <tr> + <th id="type-column" class="no-mobile"/> + <th id="name-column"> + <a nowrap th:if="${sortOrder != T(de.nbscloud.files.FileSystemService.SortOrder).NATURAL}" + th:href="@{''(sortOrder=NATURAL)}" + th:text="#{nbscloud.files.files-content-table.table-header.filename}"/> + <a nowrap th:if="${sortOrder == T(de.nbscloud.files.FileSystemService.SortOrder).NATURAL}" + th:href="@{''(sortOrder=NATURAL_REVERSE)}" + th:text="#{nbscloud.files.files-content-table.table-header.filename}"/> + </th> + <th id="size-column" class="no-mobile"> + <a nowrap th:if="${sortOrder != T(de.nbscloud.files.FileSystemService.SortOrder).SIZE_DESC}" + th:href="@{''(sortOrder=SIZE_DESC)}" + th:text="#{nbscloud.files.files-content-table.table-header.size}"/> + <a nowrap th:if="${sortOrder == T(de.nbscloud.files.FileSystemService.SortOrder).SIZE_DESC}" + th:href="@{''(sortOrder=SIZE_ASC)}" + th:text="#{nbscloud.files.files-content-table.table-header.size}"/> + </th> + <th id="lastmod-column" class="no-mobile"> + <a nowrap th:if="${sortOrder != T(de.nbscloud.files.FileSystemService.SortOrder).LAST_MOD_DESC}" + th:href="@{''(sortOrder=LAST_MOD_DESC)}" + th:text="#{nbscloud.files.files-content-table.table-header.lastmodified}"/> + <a nowrap th:if="${sortOrder == T(de.nbscloud.files.FileSystemService.SortOrder).LAST_MOD_DESC}" + th:href="@{''(sortOrder=LAST_MOD_ASC)}" + th:text="#{nbscloud.files.files-content-table.table-header.lastmodified}"/> + </th> + <th nowrap id="actions-column" th:text="#{nbscloud.files.files-content-table.table-header.actions}"/> + </tr> + <tr th:each="fileEntry : ${content}"> + <td th:if="${fileEntry.directory}" class="no-mobile">d</td> + <td th:if="${!fileEntry.directory}" class="no-mobile">-</td> + <td th:if="${fileEntry.directory}"> + <a th:if="${fileEntry.name != '..'}" th:href="@{/files/browse/} + ${fileEntry.path}" + th:text="${fileEntry.name}"/> + <a th:if="${fileEntry.name == '..'}" th:href="@{/files/browse/} + ${fileEntry.path}" + th:text="${fileEntry.name}"/> + </td> + <td th:if="${!fileEntry.directory && @filesFormatter.needsTruncate(fileEntry.name)}" + th:text="${@filesFormatter.truncateFilename(fileEntry.name)}" + th:title="${fileEntry.name}"/> + <td th:if="${!fileEntry.directory && !@filesFormatter.needsTruncate(fileEntry.name)}" + th:text="${fileEntry.name}"/> + <td nowrap th:text="${@filesFormatter.formatSize(fileEntry.size)}" + class="files-table-padded-col files-table-text-align-right no-mobile"/> + <td nowrap th:text="${#temporals.format(fileEntry.lastModified, 'SHORT')}" + class="files-table-padded-col files-table-text-align-right no-mobile"/> + <td nowrap> + <details th:if="${fileEntry.name != '..'}" class="files-table-text-align-right"> + <summary th:text="#{nbscloud.show-actions}"/> + <div id="files-content-table-show-actions-detail-container"> + <div id="files-content-table-show-actions-detail-container-rename"> + <a th:href="@{/files/rename(filename=${fileEntry.name})}" + th:text="#{nbscloud.files-content-table.table.actions.rename}"/> + </div> + <div id="files-content-table-show-actions-detail-container-delete"> + <a th:href="@{/files/delete(filename=${fileEntry.name})}" + th:text="#{nbscloud.files-content-table.table.actions.delete}"/> + </div> + <div th:if="${!fileEntry.directory}" + id="files-content-table-show-actions-detail-container-download"> + <a th:href="@{/files/download(filename=${fileEntry.name})}" + th:text="#{nbscloud.files-content-table.table.actions.download}"/> + </div> + <div th:if="${!fileEntry.directory}" + id="files-content-table-show-actions-detail-container-preview"> + <a th:href="@{/files/preview(filename=${fileEntry.name})}" + th:text="#{nbscloud.files-content-table.table.actions.preview}"/> + </div> + <div th:if="${!fileEntry.directory}" + id="files-content-table-show-actions-detail-container-share"> + <a th:href="@{/files/share(filename=${fileEntry.name})}" + th:text="#{nbscloud.files-content-table.table.actions.share}"/> + </div> + </div> + </details> + <div th:if="${fileEntry.name == '..'}"/> + </td> + </tr> + </table> + </div> + <div th:replace="includes/footer :: footer"/> +</div> +</body> +</html> \ No newline at end of file diff --git a/files/src/main/resources/templates/files/gallery.html b/files/src/main/resources/templates/files/gallery.html new file mode 100644 index 0000000..3a6cedd --- /dev/null +++ b/files/src/main/resources/templates/files/gallery.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html xmlns:th="http://www.thymeleaf.org"> +<html lang="en"> +<head> + <title th:text="#{nbscloud.files.gallery.title}"/> + + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/> + <link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/> + <link rel="stylesheet" th:href="@{/css/main.css}"/> + <link rel="stylesheet" th:href="@{/css/files_main.css}"/> + <link rel="stylesheet" th:href="@{/css/files_gallery.css}"/> + <link rel="shortcut icon" th:href="@{/favicon.ico}"/> +</head> +<body> +<div id="main-container"> + <div th:replace="includes/header :: header"/> + <div id="gallery-container"> + <img class="galleryImage" th:each="file : ${files}" th:src="@{/files/preview(filename=${file})}"> + </div> + <div th:replace="includes/footer :: footer"/> +</div> +</body> +</html> \ No newline at end of file diff --git a/files/src/main/resources/templates/files/includes/menu.html b/files/src/main/resources/templates/files/includes/menu.html new file mode 100644 index 0000000..31afd0c --- /dev/null +++ b/files/src/main/resources/templates/files/includes/menu.html @@ -0,0 +1,35 @@ +<div id="menu-container" th:fragment="menu"> + <div class="menu-spacer"></div> + <div id="upload-container"> + <form method="POST" action="#" th:action="@{/files/upload}" enctype="multipart/form-data"> + <!-- JavaScript required unfortunately --> + <input id="upload-container-file-input" type="file" name="files" onchange="this.form.submit()" + style="display: none;" multiple> + <span id="upload-container-span-input" class="icon files-menu-icon" + onclick="document.getElementById('upload-container-file-input').click();"></span> + </form> + </div> + <div class="menu-spacer"></div> + <div id="create-dir-container"> + <details> + <summary> + <span id="create-dir-container-span-input" class="icon files-menu-icon"></span> + <div class="create-dir-container-details-modal-overlay"></div> + </summary> + <div class="create-dir-container-details-modal"> + <div class="create-dir-container-details-modal-content"> + <form method="POST" action="#" th:action="@{/files/createDir}" enctype="multipart/form-data"> + <input id="create-dir-container-dir-name" type="text" name="dirName" /> + <input type="submit" value="Create"/> + </form> + </div> + </div> + </details> + </div> + <div class="menu-spacer"></div> + <div> + <a th:href="@{/files/gallery}"> + <span class="icon files-menu-icon"></span> + </a> + </div> +</div> \ No newline at end of file diff --git a/files/src/main/resources/templates/files/rename.html b/files/src/main/resources/templates/files/rename.html new file mode 100644 index 0000000..e63da42 --- /dev/null +++ b/files/src/main/resources/templates/files/rename.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{nbscloud.files.rename.title} + ${filename}"/> + + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/> + <link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/> + <link rel="stylesheet" th:href="@{/css/main.css}"/> + <link rel="stylesheet" th:href="@{/css/files_main.css}"/> + <link rel="shortcut icon" th:href="@{/favicon.ico}"/> +</head> +<body> +<div id="main-container"> + <div th:replace="includes/header :: header"/> + <form id="rename-form" action="#" th:action="@{/files/doRename}" th:object="${form}" + method="post" enctype="multipart/form-data"> + <label for="filename" th:text="#{nbscloud.files.rename.label.filename}"/> + <input type="text" id="filename" th:field="*{filename}"/> + <label for="targetDir" th:text="#{nbscloud.files.rename.label.targetDir}"/> + <!-- We can't use th:field here because of th:selected, they don't work together --> + <select size="25" id="targetDir" name="targetDir"> + <option th:each="targetDir : ${targetDirs}" + th:value="${targetDir}" + th:text="${targetDir}" + th:selected="${targetDir.equals(currentLocation)}"/> + </select> + <input type="hidden" id="originalPath" th:field="*{originalFilename}"/> + <input type="submit" th:value="#{nbscloud.files.rename.submit}" /> + </form> + <div th:replace="includes/footer :: footer"/> +</div> +</body> +</html> \ No newline at end of file diff --git a/notes/pom.xml b/notes/pom.xml new file mode 100644 index 0000000..be1abb4 --- /dev/null +++ b/notes/pom.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>nbs-cloud-aggregator</artifactId> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <version>1-SNAPSHOT</version> + </parent> + + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>notes</artifactId> + <packaging>jar</packaging> + + <dependencies> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-registry</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-config</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> +</project> \ No newline at end of file diff --git a/notes/src/main/java/de/nbscloud/notes/NotesApp.java b/notes/src/main/java/de/nbscloud/notes/NotesApp.java new file mode 100644 index 0000000..fbf28b1 --- /dev/null +++ b/notes/src/main/java/de/nbscloud/notes/NotesApp.java @@ -0,0 +1,38 @@ +package de.nbscloud.notes; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class NotesApp implements App, InitializingBean { + @Autowired + private AppRegistry appRegistry; + + @Override + public String getId() { + return "notes"; + } + + @Override + public String getIcon() { + return null; + } + + @Override + public String getStartPath() { + return this.getId(); + } + + @Override + public int getIndex() { + return 20; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.appRegistry.registerApp(this); + } +} diff --git a/pastebin/pom.xml b/pastebin/pom.xml new file mode 100644 index 0000000..71f5dc4 --- /dev/null +++ b/pastebin/pom.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>nbs-cloud-aggregator</artifactId> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <version>1-SNAPSHOT</version> + </parent> + + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>pastebin</artifactId> + <packaging>jar</packaging> + + <dependencies> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-registry</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-config</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/pastebin/src/main/java/de/nbscloud/pastebin/PastebinApp.java b/pastebin/src/main/java/de/nbscloud/pastebin/PastebinApp.java new file mode 100644 index 0000000..a9729d3 --- /dev/null +++ b/pastebin/src/main/java/de/nbscloud/pastebin/PastebinApp.java @@ -0,0 +1,38 @@ +package de.nbscloud.pastebin; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class PastebinApp implements App, InitializingBean { + @Autowired + private AppRegistry appRegistry; + + @Override + public String getId() { + return "pastebin"; + } + + @Override + public String getIcon() { + return null; + } + + @Override + public String getStartPath() { + return this.getId(); + } + + @Override + public int getIndex() { + return 50; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.appRegistry.registerApp(this); + } +} diff --git a/pom.xml b/pom.xml index 91821d6..d333345 100644 --- a/pom.xml +++ b/pom.xml @@ -3,18 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <configuration> - <source>16</source> - <target>16</target> - </configuration> - </plugin> - </plugins> - </build> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> @@ -32,13 +20,18 @@ <modules> <module>files</module> <module>web-container</module> + <module>web-container-registry</module> + <module>notes</module> + <module>pastebin</module> + <module>bookmarks</module> + <module>web-container-config</module> + <module>todo</module> + <module>dashboard</module> </modules> <properties> - <maven.compiler.source>17</maven.compiler.source> - <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <java.version>17</java.version> + <java.version>18</java.version> </properties> <distributionManagement> @@ -55,16 +48,132 @@ <dependencyManagement> <dependencies> <dependency> - <groupId>de.77zzc7.nbs-cloud</groupId> + <groupId>de.77zzcx7.nbs-cloud</groupId> <artifactId>web-container</artifactId> <version>${project.version}</version> </dependency> <dependency> - <groupId>de.77zzc7.nbs-cloud</groupId> + <groupId>de.77zzcx7.nbs-cloud</groupId> <artifactId>files</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-registry</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>notes</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>pastebin</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>bookmarks</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-config</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>todo</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>dashboard</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>4.3</version> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>2.11.0</version> + </dependency> + <dependency> + <groupId>org.overviewproject</groupId> + <artifactId>mime-types</artifactId> + <version>1.0.2</version> + </dependency> </dependencies> </dependencyManagement> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>18</source> + <target>18</target> + </configuration> + </plugin> + <plugin> + <groupId>fr.jcgay.maven.plugins</groupId> + <artifactId>buildplan-maven-plugin</artifactId> + <version>1.5</version> + <executions> + <execution> + <id>build-plan</id> + <phase>initialize</phase> + <goals> + <goal>list</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <!-- As we had to rename the config file for modules (since Spring only supports one + application.properties), we also need to adjust the filtering --> + <artifactId>maven-resources-plugin</artifactId> + <executions> + <execution> + <id>process-module-configs</id> + <phase>process-resources</phase> + <goals> + <goal>resources</goal> + </goals> + <configuration> + <propertiesEncoding>UTF-8</propertiesEncoding> + <delimiters> + <delimiter>@</delimiter> + </delimiters> + <useDefaultDelimiters>false</useDefaultDelimiters> + <resources> + <resource> + <directory>${project.basedir}/src/main/resources</directory> + <includes> + <include>**/*-application.properties</include> + <include>**/*-application-*.properties</include> + </includes> + <filtering>true</filtering> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </pluginManagement> + + <plugins> + <plugin> + <groupId>fr.jcgay.maven.plugins</groupId> + <artifactId>buildplan-maven-plugin</artifactId> + </plugin> + </plugins> + </build> </project> \ No newline at end of file diff --git a/todo/pom.xml b/todo/pom.xml new file mode 100644 index 0000000..d91c52a --- /dev/null +++ b/todo/pom.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>nbs-cloud-aggregator</artifactId> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <version>1-SNAPSHOT</version> + </parent> + + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>todo</artifactId> + <packaging>jar</packaging> + + <dependencies> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-registry</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-config</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> +</project> \ No newline at end of file diff --git a/todo/src/main/java/de/nbscloud/todo/TodoApp.java b/todo/src/main/java/de/nbscloud/todo/TodoApp.java new file mode 100644 index 0000000..e4eedfb --- /dev/null +++ b/todo/src/main/java/de/nbscloud/todo/TodoApp.java @@ -0,0 +1,38 @@ +package de.nbscloud.todo; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TodoApp implements App, InitializingBean { + @Autowired + private AppRegistry appRegistry; + + @Override + public String getId() { + return "todo"; + } + + @Override + public String getIcon() { + return null; + } + + @Override + public String getStartPath() { + return this.getId(); + } + + @Override + public int getIndex() { + return 40; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.appRegistry.registerApp(this); + } +} diff --git a/web-container-config/pom.xml b/web-container-config/pom.xml new file mode 100644 index 0000000..9f56bc4 --- /dev/null +++ b/web-container-config/pom.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>nbs-cloud-aggregator</artifactId> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <version>1-SNAPSHOT</version> + </parent> + + <artifactId>web-container-config</artifactId> + + <dependencies> + <!-- Spring --> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + </dependencies> +</project> \ No newline at end of file diff --git a/web-container-config/src/main/java/de/nbscloud/webcontainer/shared/config/WebContainerSharedConfig.java b/web-container-config/src/main/java/de/nbscloud/webcontainer/shared/config/WebContainerSharedConfig.java new file mode 100644 index 0000000..684cc3d --- /dev/null +++ b/web-container-config/src/main/java/de/nbscloud/webcontainer/shared/config/WebContainerSharedConfig.java @@ -0,0 +1,43 @@ +package de.nbscloud.webcontainer.shared.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.ui.Model; + +@Configuration +@ConfigurationProperties(prefix = "nbs-cloud.web-container.shared") +@PropertySource("classpath:/config/shared-application.properties") +public class WebContainerSharedConfig { + private String version; + private boolean darkMode; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean isDarkMode() { + return darkMode; + } + + public void setDarkMode(boolean darkMode) { + this.darkMode = darkMode; + } + + public void addDefaults(Model model) { + addDarkMode(model); + addVersionAttribute(model); + } + + private final void addVersionAttribute(Model model) { + model.addAttribute("nbsVersion", this.version); + } + + private final void addDarkMode(Model model) { + model.addAttribute("darkMode", this.darkMode); + } +} diff --git a/web-container-config/src/main/resources/config/shared-application.properties b/web-container-config/src/main/resources/config/shared-application.properties new file mode 100644 index 0000000..f281398 --- /dev/null +++ b/web-container-config/src/main/resources/config/shared-application.properties @@ -0,0 +1,10 @@ +spring.profiles.active=@activeProfiles@ + +info.app.name=NoBullShit Cloud - web container config +info.app.description=Config for web projects + +nbs-cloud.web-container.shared.version=@project.version@ + +# Whether dark mode should be enabled by default +# Possible values: true|false +nbs-cloud.web-container.shared.darkMode=true \ No newline at end of file diff --git a/web-container-registry/pom.xml b/web-container-registry/pom.xml new file mode 100644 index 0000000..069e3af --- /dev/null +++ b/web-container-registry/pom.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>nbs-cloud-aggregator</artifactId> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <version>1-SNAPSHOT</version> + </parent> + + <artifactId>web-container-registry</artifactId> + + <dependencies> + <!-- Spring --> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + </dependency> + </dependencies> +</project> \ No newline at end of file diff --git a/web-container-registry/src/main/java/de/nbscloud/webcontainer/registry/App.java b/web-container-registry/src/main/java/de/nbscloud/webcontainer/registry/App.java new file mode 100644 index 0000000..0a2a9ce --- /dev/null +++ b/web-container-registry/src/main/java/de/nbscloud/webcontainer/registry/App.java @@ -0,0 +1,18 @@ +package de.nbscloud.webcontainer.registry; + +public interface App { + // New app: + // 1) Create module + // 2) Add dependencies (like other app modules) + // 3) Register module in web-container module + // 4) Add module to dependency management in aggregator pom + // 5) Add module package to web-container application @ComponentScan + + String getId(); + + String getIcon(); + + String getStartPath(); + + int getIndex(); +} diff --git a/web-container-registry/src/main/java/de/nbscloud/webcontainer/registry/AppRegistry.java b/web-container-registry/src/main/java/de/nbscloud/webcontainer/registry/AppRegistry.java new file mode 100644 index 0000000..9c94026 --- /dev/null +++ b/web-container-registry/src/main/java/de/nbscloud/webcontainer/registry/AppRegistry.java @@ -0,0 +1,23 @@ +package de.nbscloud.webcontainer.registry; + +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +@Component +public class AppRegistry { + private final List<App> apps = new CopyOnWriteArrayList<>(); + + public void registerApp(App app) { + this.apps.add(app); + + this.apps.sort(Comparator.comparing(App::getIndex)); + } + + public List<App> getAll() { + return Collections.unmodifiableList(this.apps); + } +} diff --git a/web-container/pom.xml b/web-container/pom.xml index 37c09ae..a3c130f 100644 --- a/web-container/pom.xml +++ b/web-container/pom.xml @@ -21,21 +21,44 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <configuration> - <source>17</source> - <target>17</target> - </configuration> - </plugin> </plugins> </build> <dependencies> - <!-- nbs-cloud modules --> + <!-- optional nbs-cloud modules --> + <!-- comment the ones you don't need --> <dependency> - <groupId>de.77zzc7.nbs-cloud</groupId> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>notes</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>pastebin</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>bookmarks</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>todo</artifactId> + </dependency> + + <!-- required nbs-cloud modules --> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-registry</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>web-container-config</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> + <artifactId>dashboard</artifactId> + </dependency> + <dependency> + <groupId>de.77zzcx7.nbs-cloud</groupId> <artifactId>files</artifactId> </dependency> @@ -44,6 +67,25 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-tomcat</artifactId> + <scope>provided</scope> + </dependency> + + <!-- Misc --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + </dependency> </dependencies> </project> \ No newline at end of file diff --git a/web-container/src/main/java/de/nbscloud/webcontainer/config/WebContainerConfig.java b/web-container/src/main/java/de/nbscloud/webcontainer/config/WebContainerConfig.java index 18f5299..55df885 100644 --- a/web-container/src/main/java/de/nbscloud/webcontainer/config/WebContainerConfig.java +++ b/web-container/src/main/java/de/nbscloud/webcontainer/config/WebContainerConfig.java @@ -6,6 +6,12 @@ import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "nbs-cloud.web-container") +@ComponentScan("de.nbscloud.webcontainer.shared") @ComponentScan("de.nbscloud.files") +@ComponentScan("de.nbscloud.notes") +@ComponentScan("de.nbscloud.pastebin") +@ComponentScan("de.nbscloud.bookmarks") +@ComponentScan("de.nbscloud.todo") +@ComponentScan("de.nbscloud.dashboard") public class WebContainerConfig { } diff --git a/web-container/src/main/java/de/nbscloud/webcontainer/controller/WebContainerController.java b/web-container/src/main/java/de/nbscloud/webcontainer/controller/WebContainerController.java new file mode 100644 index 0000000..fdd8964 --- /dev/null +++ b/web-container/src/main/java/de/nbscloud/webcontainer/controller/WebContainerController.java @@ -0,0 +1,58 @@ +package de.nbscloud.webcontainer.controller; + +import de.nbscloud.webcontainer.registry.App; +import de.nbscloud.webcontainer.registry.AppRegistry; +import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import javax.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.stream.Collectors; + +@Controller +public class WebContainerController { + + private static final Logger logger = LoggerFactory.getLogger(WebContainerController.class); + + @Autowired + private WebContainerSharedConfig webContainerSharedConfig; + + @Autowired + private AppRegistry appRegistry; + + @GetMapping("/") + public String landing(Model model) { + logger.info("NBSCloud started - apps available: {}", this.appRegistry.getAll().stream().map(App::getId) + .collect(Collectors.joining(", "))); + + return "redirect:dashboard"; + } + + @GetMapping("/toggleDarkMode") + public String toggleDarkMode(Model model, HttpServletRequest httpServletRequest) { + String navigateTo = "/"; + + try { + final String fullPath = new URL(httpServletRequest.getHeader("referer")).toURI().getPath(); + // FIXME - will break if the app is deployed on http://host/a/b/financer instead of http://host/financer + final String tmpPath = StringUtils.removeStart(fullPath, "/"); + navigateTo = tmpPath.substring(tmpPath.indexOf("/")); + } catch (MalformedURLException | URISyntaxException | StringIndexOutOfBoundsException e) { + // TODO + e.printStackTrace(); + } + + this.webContainerSharedConfig.setDarkMode(!this.webContainerSharedConfig.isDarkMode()); + + return "redirect:" + navigateTo; + } + +} diff --git a/web-container/src/main/resources/config/application.properties b/web-container/src/main/resources/config/application.properties new file mode 100644 index 0000000..c954ff6 --- /dev/null +++ b/web-container/src/main/resources/config/application.properties @@ -0,0 +1,20 @@ +### This is the main configuration file of the application. +### Filtering of the @...@ values happens via the maven-resource-plugin. The execution of the plugin is configured in +### the Spring Boot parent POM. +spring.profiles.active=@activeProfiles@ + +info.app.name=NoBullShit Cloud - web container +info.app.description=Aggregator for web projects +info.build.group=@project.groupId@ +info.build.artifact=@project.artifactId@ +info.build.version=@project.version@ + +spring.messages.basename=i18n/container_messages,i18n/files_messages + +spring.servlet.multipart.max-file-size=-1 +spring.servlet.multipart.max-request-size=-1 + +logging.level.de.nbscloud=DEBUG +logging.file=nbscloud.log +logging.file.max-history=7 +logging.file.max-size=50MB \ No newline at end of file diff --git a/web-container/src/main/resources/i18n/container_messages.properties b/web-container/src/main/resources/i18n/container_messages.properties new file mode 100644 index 0000000..c9f441c --- /dev/null +++ b/web-container/src/main/resources/i18n/container_messages.properties @@ -0,0 +1,3 @@ +nbscloud.show-actions=... +nbscloud.footer.changelog=Changelog +nbscloud.footer.toggleDarkMode=Toggle dark mode \ No newline at end of file diff --git a/web-container/src/main/resources/i18n/container_messages_de_DE.properties b/web-container/src/main/resources/i18n/container_messages_de_DE.properties new file mode 100644 index 0000000..6ffb6b3 --- /dev/null +++ b/web-container/src/main/resources/i18n/container_messages_de_DE.properties @@ -0,0 +1,3 @@ +nbscloud.show-actions=... +nbscloud.footer.changelog=\u00C4nderungshistorie +nbscloud.footer.toggleDarkMode=Dark Mode umschalten \ No newline at end of file diff --git a/web-container/src/main/resources/static/changelog.txt b/web-container/src/main/resources/static/changelog.txt new file mode 100644 index 0000000..759a035 --- /dev/null +++ b/web-container/src/main/resources/static/changelog.txt @@ -0,0 +1,2 @@ +v1: +- Initial \ No newline at end of file diff --git a/web-container/src/main/resources/static/css/darkModeColors.css b/web-container/src/main/resources/static/css/darkModeColors.css new file mode 100644 index 0000000..0ee5159 --- /dev/null +++ b/web-container/src/main/resources/static/css/darkModeColors.css @@ -0,0 +1,12 @@ +/* Color definitions for dark mode */ +:root { + --error-color: #D30000; + --text-color: #7f7f7f; + --background-color: #1d1f21; + --link-color: #87ab63; + --hover-color: #1f1f2f; + --border-color: #7f7f7f; + --background-brightness: 1.5; + --good-color: #479A37; +} +/* --------------------- */ \ No newline at end of file diff --git a/web-container/src/main/resources/static/css/lightModeColors.css b/web-container/src/main/resources/static/css/lightModeColors.css new file mode 100644 index 0000000..e028c57 --- /dev/null +++ b/web-container/src/main/resources/static/css/lightModeColors.css @@ -0,0 +1,12 @@ +/* Color definitions for light mode */ +:root { + --error-color: #D30000; + --text-color: #000000; + --background-color: #FFFFFF; + --link-color: #0000EE; + --hover-color: lightgrey; + --border-color: #ddd; + --background-brightness: 0.5; + --good-color: #479A37; +} +/* --------------------- */ \ No newline at end of file diff --git a/web-container/src/main/resources/static/css/main.css b/web-container/src/main/resources/static/css/main.css new file mode 100644 index 0000000..14afdba --- /dev/null +++ b/web-container/src/main/resources/static/css/main.css @@ -0,0 +1,95 @@ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../font/SourceCodePro_Regular_400.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../font/SourceCodePro_Bold_600.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(../font/Material_Icons.woff2) format('woff2'); +} + +.icon { + font-family: 'Material Icons'; + vertical-align: middle; +} + +body { + color: var(--text-color); + background-color: var(--background-color); + font-family: 'Source Code Pro', monospace; +} + +* { + font-family: 'Source Code Pro', monospace; +} + +a { + color: var(--link-color); +} + +tr:hover { + background-color: var(--hover-color); +} + +#footer-container { + margin-top: 1em; +} +#footer-container > hr, +#footer-container > div { + display: block; +} +#footer-container > hr { + width: 100%; +} +#footer-container > div { + text-align: center; + font-size: 0.9em; +} +.errorMessage { + color: var(--error-color); +} + +.menu-entry { + display: inline; + padding-right: 6em; +} + +#main-container { + display: flex; + flex-direction: column; + padding-left: 3em; + padding-right: 3em; + padding-top: 3em; + padding-bottom: 3em; +} + +@media only screen and (max-width: 450px) { + .no-mobile { + display: none; + } + + #main-container { + padding-left: 1em; + padding-right: 1em; + padding-top: 1em; + padding-bottom: 1em; + } + + .menu-entry { + display: inline; + padding-right: 1em; + } +} \ No newline at end of file diff --git a/web-container/src/main/resources/static/font/Material_Icons.woff2 b/web-container/src/main/resources/static/font/Material_Icons.woff2 new file mode 100644 index 0000000..6a23a55 Binary files /dev/null and b/web-container/src/main/resources/static/font/Material_Icons.woff2 differ diff --git a/web-container/src/main/resources/static/font/SourceCodePro_Bold_600.woff2 b/web-container/src/main/resources/static/font/SourceCodePro_Bold_600.woff2 new file mode 100644 index 0000000..3605950 Binary files /dev/null and b/web-container/src/main/resources/static/font/SourceCodePro_Bold_600.woff2 differ diff --git a/web-container/src/main/resources/static/font/SourceCodePro_Regular_400.woff2 b/web-container/src/main/resources/static/font/SourceCodePro_Regular_400.woff2 new file mode 100644 index 0000000..90d1a42 Binary files /dev/null and b/web-container/src/main/resources/static/font/SourceCodePro_Regular_400.woff2 differ diff --git a/web-container/src/main/resources/templates/includes/footer.html b/web-container/src/main/resources/templates/includes/footer.html new file mode 100644 index 0000000..9ed6d4c --- /dev/null +++ b/web-container/src/main/resources/templates/includes/footer.html @@ -0,0 +1,8 @@ +<div id="footer-container" th:fragment="footer"> + <hr> + <div> + <span th:text="'nbs-cloud v' + ${nbsVersion}"/> +  (<a th:href="@{/changelog.txt}" th:text="#{nbscloud.footer.changelog}" />, + <a th:href="@{/toggleDarkMode}" th:text="#{nbscloud.footer.toggleDarkMode}" />) + </div> +</div> \ No newline at end of file diff --git a/web-container/src/main/resources/templates/includes/header.html b/web-container/src/main/resources/templates/includes/header.html new file mode 100644 index 0000000..25f0972 --- /dev/null +++ b/web-container/src/main/resources/templates/includes/header.html @@ -0,0 +1,5 @@ +<div id="header-container" th:fragment="header"> + <div th:each="app : ${apps}" class="menu-entry"> + <a th:href="@{/} + ${app.startPath}" th:text="${app.id}"/> + </div> +</div> \ No newline at end of file