1
0

Basic implementation for files app

This commit is contained in:
2022-05-06 23:01:23 +02:00
parent 4e12f4805e
commit 7d6c6c6321
54 changed files with 2381 additions and 94 deletions

38
bookmarks/pom.xml Normal file
View File

@@ -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>bookmarks</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>

View File

@@ -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);
}
}

38
dashboard/pom.xml Normal file
View File

@@ -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>dashboard</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>

View File

@@ -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);
}
}

View File

@@ -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";
}
}

View File

@@ -0,0 +1,3 @@
#content-container {
padding-top: 2em;
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{nbscloud.dashboard.index.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/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>

View File

@@ -15,21 +15,36 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
<!-- <dependency>--> <dependency>
<!-- <groupId>org.springframework.boot</groupId>--> <groupId>de.77zzcx7.nbs-cloud</groupId>
<!-- <artifactId>spring-boot-starter-data-jpa</artifactId>--> <artifactId>web-container-registry</artifactId>
<!-- </dependency>--> </dependency>
<!-- <dependency>--> <dependency>
<!-- <groupId>org.springframework.boot</groupId>--> <groupId>de.77zzcx7.nbs-cloud</groupId>
<!-- <artifactId>spring-boot-starter-security</artifactId>--> <artifactId>web-container-config</artifactId>
<!-- </dependency>--> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
<scope>provided</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>
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>org.flywaydb</groupId>--> <!-- <groupId>org.flywaydb</groupId>-->
@@ -40,11 +55,11 @@
<!-- <artifactId>thymeleaf-extras-springsecurity5</artifactId>--> <!-- <artifactId>thymeleaf-extras-springsecurity5</artifactId>-->
<!-- </dependency>--> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>org.springframework.boot</groupId> <!-- <groupId>org.springframework.boot</groupId>-->
<artifactId>spring-boot-starter-tomcat</artifactId> <!-- <artifactId>spring-boot-starter-tomcat</artifactId>-->
<scope>provided</scope> <!-- <scope>provided</scope>-->
</dependency> <!-- </dependency>-->
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>--> <!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-test</artifactId>--> <!-- <artifactId>spring-boot-starter-test</artifactId>-->

View File

@@ -1,20 +1,82 @@
package de.nbscloud.files; package de.nbscloud.files;
import de.nbscloud.files.config.FilesConfig; import de.nbscloud.files.config.FilesConfig;
import de.nbscloud.files.controller.FilesController;
import de.nbscloud.files.exception.FileSystemServiceException; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.io.InputStream;
import java.nio.file.Path; import java.io.OutputStream;
import java.util.List; 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; import java.util.stream.Collectors;
@Service @Service
public class FileSystemService { 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 @Autowired
@@ -23,44 +85,211 @@ public class FileSystemService {
@Autowired @Autowired
private LocationTracker locationTracker; private LocationTracker locationTracker;
private MimeTypeDetector mimeTypeDetector = new MimeTypeDetector();
public Path createDirectory(String name) { public Path createDirectory(String name) {
try { try {
return Files.createDirectory(this.locationTracker.getCurrentLocation().resolve(name)); return Files.createDirectories(this.locationTracker.getCurrentLocation().resolve(name));
} catch (IOException e) { } catch (IOException e) {
throw new FileSystemServiceException("Could not create directory", 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) { public boolean delete(String name) {
try { try {
// TODO does only delete dirs if they are empty - but maybe we want that?
return Files.deleteIfExists(this.locationTracker.getCurrentLocation().resolve(name)); return Files.deleteIfExists(this.locationTracker.getCurrentLocation().resolve(name));
} catch (IOException e) { } catch (IOException e) {
throw new FileSystemServiceException("Could not delete file", 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) { public Path move(Path originalPath, Path newPath) {
try { try {
return Files.move(originalPath, newPath); return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) { } catch (IOException e) {
throw new FileSystemServiceException("Could not move file", e); throw new FileSystemServiceException("Could not move file", e);
} }
} }
public List<ContentContainer> list() { public byte[] get(String name) {
try { try {
return Files.list(this.locationTracker.getCurrentLocation()) 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 -> { .map(path -> {
try { try {
return new ContentContainer(Files.isDirectory(path), final boolean isDir = Files.isDirectory(path);
return new ContentContainer(isDir,
this.locationTracker.getRelativeToBaseDir(path),
path.getFileName().toString(), path.getFileName().toString(),
Files.size(path)); isDir ? FileUtils.sizeOfDirectory(path.toFile()) : Files.size(path),
LocalDateTime.ofInstant(Files.getLastModifiedTime(path)
.toInstant(), ZoneId.systemDefault()));
} catch (IOException e) { } catch (IOException e) {
throw new FileSystemServiceException("Could not list files", e); throw new FileSystemServiceException("Could not list files", e);
} }
}) })
.collect(Collectors.toList()); .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) { } catch (IOException e) {
throw new FileSystemServiceException("Could not list files", e); throw new FileSystemServiceException("Could not list files", e);
} }

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,9 @@
package de.nbscloud.files; package de.nbscloud.files;
import de.nbscloud.files.config.FilesConfig; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -9,6 +12,8 @@ import java.nio.file.Paths;
@Component @Component
public class LocationTracker { public class LocationTracker {
private static final Logger logger = LoggerFactory.getLogger(LocationTracker.class);
@Autowired @Autowired
private FilesConfig filesConfig; private FilesConfig filesConfig;
@@ -17,10 +22,14 @@ public class LocationTracker {
public void init() { public void init() {
this.baseDirPath = Paths.get(this.filesConfig.getBaseDir()); 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() { public void reset() {
logger.debug("Reset location");
init(); init();
} }
@@ -28,10 +37,34 @@ public class LocationTracker {
return this.currentLocation; 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) { public void setCurrentLocation(String navigateTo) {
validate(navigateTo); validate(navigateTo);
this.currentLocation = this.currentLocation.resolve(navigateTo); this.currentLocation = this.baseDirPath.resolve(navigateTo);
} }
private void validate(String navigateTo) { private void validate(String navigateTo) {
@@ -47,8 +80,12 @@ public class LocationTracker {
throw new IllegalStateException("Absolute path: " + navigateTo); 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); throw new IllegalStateException("Illegal path: " + navigateTo);
} }
} }
public Path getBaseDirPath() {
return baseDirPath;
}
} }

View File

@@ -2,11 +2,18 @@ package de.nbscloud.files.config;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration @Configuration
@ConfigurationProperties(prefix = "nbs-cloud.files") @ConfigurationProperties(prefix = "nbs-cloud.files")
@PropertySource("classpath:/config/files-application.properties")
public class FilesConfig { public class FilesConfig {
private String baseDir; private String baseDir;
private boolean filterHidden;
private boolean useTrashBin;
private String trashBinName;
private int truncateFileNameChars;
private String sharesName;
public String getBaseDir() { public String getBaseDir() {
return baseDir; return baseDir;
@@ -15,4 +22,44 @@ public class FilesConfig {
public void setBaseDir(String baseDir) { public void setBaseDir(String baseDir) {
this.baseDir = 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;
}
} }

View File

@@ -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
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,4 @@
package de.nbscloud.files.task;
public class OrphanedThumbnailCleanup {
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();">&#xe9fc;</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">&#xe2cc;</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">&#xe3b6;</span>
</a>
</div>
</div>

View File

@@ -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>

37
notes/pom.xml Normal file
View File

@@ -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>

View File

@@ -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);
}
}

38
pastebin/pom.xml Normal file
View File

@@ -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>

View File

@@ -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);
}
}

143
pom.xml
View File

@@ -3,18 +3,6 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> 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> <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> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
@@ -32,13 +20,18 @@
<modules> <modules>
<module>files</module> <module>files</module>
<module>web-container</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> </modules>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version> <java.version>18</java.version>
</properties> </properties>
<distributionManagement> <distributionManagement>
@@ -55,16 +48,132 @@
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>de.77zzc7.nbs-cloud</groupId> <groupId>de.77zzcx7.nbs-cloud</groupId>
<artifactId>web-container</artifactId> <artifactId>web-container</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>de.77zzc7.nbs-cloud</groupId> <groupId>de.77zzcx7.nbs-cloud</groupId>
<artifactId>files</artifactId> <artifactId>files</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </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> </dependencies>
</dependencyManagement> </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> </project>

37
todo/pom.xml Normal file
View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -21,21 +21,44 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
<dependencies> <dependencies>
<!-- nbs-cloud modules --> <!-- optional nbs-cloud modules -->
<!-- comment the ones you don't need -->
<dependency> <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> <artifactId>files</artifactId>
</dependency> </dependency>
@@ -44,6 +67,25 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </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> </dependencies>
</project> </project>

View File

@@ -6,6 +6,12 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@ConfigurationProperties(prefix = "nbs-cloud.web-container") @ConfigurationProperties(prefix = "nbs-cloud.web-container")
@ComponentScan("de.nbscloud.webcontainer.shared")
@ComponentScan("de.nbscloud.files") @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 { public class WebContainerConfig {
} }

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -0,0 +1,3 @@
nbscloud.show-actions=...
nbscloud.footer.changelog=Changelog
nbscloud.footer.toggleDarkMode=Toggle dark mode

View File

@@ -0,0 +1,3 @@
nbscloud.show-actions=...
nbscloud.footer.changelog=\u00C4nderungshistorie
nbscloud.footer.toggleDarkMode=Dark Mode umschalten

View File

@@ -0,0 +1,2 @@
v1:
- Initial

View File

@@ -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;
}
/* --------------------- */

View File

@@ -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;
}
/* --------------------- */

View File

@@ -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;
}
}

View File

@@ -0,0 +1,8 @@
<div id="footer-container" th:fragment="footer">
<hr>
<div>
<span th:text="'nbs-cloud v' + ${nbsVersion}"/>
&nbsp;(<a th:href="@{/changelog.txt}" th:text="#{nbscloud.footer.changelog}" />,
<a th:href="@{/toggleDarkMode}" th:text="#{nbscloud.footer.toggleDarkMode}" />)
</div>
</div>

View File

@@ -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>