Compare commits
90 Commits
v8
...
27db87792d
| Author | SHA1 | Date | |
|---|---|---|---|
|
27db87792d
|
|||
|
0af399275d
|
|||
|
7b707e5bb5
|
|||
|
634ae4365f
|
|||
|
49008be24a
|
|||
|
eca68b7bbf
|
|||
|
d0b5b92fc7
|
|||
|
4386dc4449
|
|||
|
32c7719942
|
|||
|
950ab8c568
|
|||
|
73043da77c
|
|||
|
30d6252992
|
|||
|
66ccf4b263
|
|||
|
5cc6bd6305
|
|||
|
c42313e022
|
|||
|
ac439261f0
|
|||
|
be19d2881e
|
|||
|
71bc53488d
|
|||
|
89db72a316
|
|||
|
06e72a794f
|
|||
|
ea45ef73f4
|
|||
|
ef9c56f6c9
|
|||
|
718f3e4a51
|
|||
|
9cb1bab118
|
|||
|
90dbeecdca
|
|||
|
b6545e2f62
|
|||
|
c1ca25ea24
|
|||
|
046124464e
|
|||
|
2eab631796
|
|||
|
ed23426690
|
|||
|
b6e6a94a1e
|
|||
|
455cb63652
|
|||
|
4573df557f
|
|||
|
1cac696dde
|
|||
|
7be52466d2
|
|||
|
4a8bfe68c5
|
|||
|
b3ea85bdae
|
|||
|
7ba25aa455
|
|||
|
69cb3204b9
|
|||
|
b8251ac4e7
|
|||
|
9b30e06949
|
|||
|
1a50a196d4
|
|||
|
20ed65910a
|
|||
|
1f5b2c0a5d
|
|||
|
4188a86995
|
|||
|
2ab4497bd1
|
|||
|
a0239ecda6
|
|||
|
bd6f1e43d1
|
|||
|
1fcf2c3fbc
|
|||
|
4134160b97
|
|||
|
16fe2c4a93
|
|||
|
7a5cd8fe48
|
|||
|
76faace53d
|
|||
|
a1644c89da
|
|||
|
f4d9f4bab7
|
|||
|
e47af45211
|
|||
|
530b2f6198
|
|||
|
c081d69d29
|
|||
|
607a4c1c3f
|
|||
|
c3a60381c8
|
|||
|
5e4d9a05c9
|
|||
|
5921ba0bc7
|
|||
|
dbc9e74979
|
|||
|
739b8040a5
|
|||
|
ce27dd6b16
|
|||
|
b7efe6321c
|
|||
|
2aab11b518
|
|||
|
42b46d87c3
|
|||
|
78d1f59929
|
|||
|
62706fb214
|
|||
|
a67d524074
|
|||
|
7a06f56f94
|
|||
|
719227a57b
|
|||
|
941ce785f2
|
|||
|
283d4bc964
|
|||
|
48a0fd48ba
|
|||
|
66570e71c8
|
|||
|
ca881dbf86
|
|||
|
6fa96d110b
|
|||
|
98b1ecfe7a
|
|||
|
6b5c260bd2
|
|||
|
ea67c31867
|
|||
|
82e048d56e
|
|||
|
1b9ce54933
|
|||
|
f89ebaa44e
|
|||
|
21a61b8a4d
|
|||
|
8369822717
|
|||
|
54d3636275
|
|||
|
7ec613f0f4
|
|||
|
8a20c156d5
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
*/target/*
|
||||
.idea/*
|
||||
75
README.md
Normal file
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# NoBullShit-cloud
|
||||
A personal cloud without bullshit
|
||||
|
||||
## Prerequisites
|
||||
nbscloud requires `java>=18`
|
||||
|
||||
## Install
|
||||
### systemd service
|
||||
### `deploy.sh`
|
||||
|
||||
## Config
|
||||
To adjust the default configuration create the file `~/.config/nbscloud/application.properties`. It allows overwriting
|
||||
properties from every configuration file of nbscloud. To get a full list of existing properties look at the following
|
||||
files:
|
||||
|File|Description|
|
||||
|----|-----------|
|
||||
|[application.properties](./web-container/src/main/resources/config/application.properties)|Main config file providing general app properties|
|
||||
|[shared-application.properties](./web-container-config/src/main/resources/config/shared-application.properties)|Properties shared by all apps|
|
||||
|[files-application.properties](./files/src/main/resources/config/files-application.properties)|Config file for the files app|
|
||||
|[dashboard-application.properties](./dashboard/src/main/resources/config/dashboard-application.properties)|Config file for the dashboard app|
|
||||
|
||||
## Apache httpd config
|
||||
It is advised to not expose NoBullShit-cloud directly - instead a proxy server like Apache httpd should be used to shield access.
|
||||
The following config example can be used a blueprint:
|
||||
```
|
||||
# CSS, favicon and fonts need to be accesible without auth
|
||||
# for e.g. the password protected share feature
|
||||
# If the password protected share feature is not used
|
||||
# the locations can be omitted to prevent an information leak
|
||||
# In fact, they can also be ommitted if the password protected
|
||||
# share feature _is_ used, but then the password entry page
|
||||
# will not be styled. Pick your poison.
|
||||
<Location /nbscloud/css>
|
||||
ProxyPass http://localhost:PORT/nbscloud
|
||||
ProxyPassReverse /nbscloud
|
||||
allow from all
|
||||
satisfy any
|
||||
</Location>
|
||||
|
||||
|
||||
<Location /nbscloud/favicon.ico>
|
||||
ProxyPass http://localhost:PORT/nbscloud
|
||||
ProxyPassReverse /nbscloud
|
||||
allow from all
|
||||
satisfy any
|
||||
</Location>
|
||||
|
||||
<Location /nbscloud/font>
|
||||
ProxyPass http://localhost:PORT/nbscloud
|
||||
ProxyPassReverse /nbscloud
|
||||
allow from all
|
||||
satisfy any
|
||||
</Location>
|
||||
|
||||
# If shares should not be accessible to unknown clients
|
||||
# this (and the Location directives above) can be omitted
|
||||
<Location /nbscloud/files/shares>
|
||||
ProxyPass http://localhost:PORT/nbscloud
|
||||
ProxyPassReverse /nbscloud
|
||||
allow from all
|
||||
satisfy any
|
||||
</Location>
|
||||
|
||||
<Location /nbscloud>
|
||||
ProxyPass http://localhost:PORT/nbscloud
|
||||
ProxyPassReverse /nbscloud
|
||||
<RequireAll>
|
||||
Require all granted
|
||||
AuthName "YOUR AUTH"
|
||||
AuthType Basic
|
||||
AuthUserFile /var/www/html/.htpasswd
|
||||
Require valid-user
|
||||
</RequireAll>
|
||||
</Location>
|
||||
```
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
|
||||
13
build/Dockerfile
Normal file
13
build/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM eclipse-temurin:25-jre-alpine
|
||||
|
||||
# Create a non-root user for security
|
||||
RUN addgroup -S spring && adduser -S spring -G spring
|
||||
USER spring:spring
|
||||
|
||||
ARG JAR_FILE
|
||||
|
||||
COPY ${JAR_FILE} app.jar
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
||||
85
build/Jenkinsfile
vendored
Normal file
85
build/Jenkinsfile
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
pipeline {
|
||||
agent { label 'docker' }
|
||||
|
||||
parameters {
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: 'If checked, no code will be pushed to Gitea, Reposilite, or Docker.')
|
||||
}
|
||||
|
||||
environment {
|
||||
REPO_URL = credentials('reposilite-url')
|
||||
DOCKER_REGISTRY = credentials('docker-registry-url')
|
||||
GIT_URL_CLEAN = sh(script: "echo ${GIT_URL} | sed 's|https://||'", returnStdout: true).trim()
|
||||
IS_DRY_RUN = "${params.DRY_RUN}"
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Version & Tag') {
|
||||
steps {
|
||||
withCredentials([usernamePassword(credentialsId: 'Jenkins_Gitea',
|
||||
usernameVariable: 'GITEA_CREDS_USR',
|
||||
passwordVariable: 'GITEA_CREDS_PSW'),
|
||||
file(credentialsId: 'jenkins-gpg-key', variable: 'GPG_KEY_FILE')]) {
|
||||
sh '''
|
||||
gpg --batch --import "${GPG_KEY_FILE}"
|
||||
KEY_ID=$(gpg --list-keys --with-colons | awk -F: '/^pub:/ { print $5; exit }')
|
||||
|
||||
git config user.email "jenkins@77zzcx7.de"
|
||||
git config user.name "Jenkins"
|
||||
git config user.signingkey "$KEY_ID"
|
||||
git config commit.gpgsign true
|
||||
git config tag.gpgSign true
|
||||
|
||||
// We need to pass the repo url in -Darguments again because of insane maven lifecycle forking
|
||||
pkgx mvn release:prepare -B \
|
||||
-s build/settings.xml \
|
||||
-Dpassword="${GITEA_CREDS_PSW}" \
|
||||
-Dusername="${GITEA_CREDS_USR}" \
|
||||
-DdryRun=${IS_DRY_RUN} \
|
||||
-Drepository.url=${REPO_URL} \
|
||||
-DtagNameFormat="v@{project.version}" \
|
||||
-Darguments="-Drepository.url=${REPO_URL} -Dtag=v\\${project.version} -DskipTests"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Release') {
|
||||
steps {
|
||||
withCredentials([usernamePassword(credentialsId: 'reposilite-jenkins-cred',
|
||||
usernameVariable: 'REPO_USER',
|
||||
passwordVariable: 'REPO_TOKEN')]) {
|
||||
sh '''
|
||||
pkgx mvn release:perform -B \
|
||||
-s build/settings.xml \
|
||||
-DdryRun=${DRY_RUN} \
|
||||
-Drepository.url=${REPO_URL} \
|
||||
-Darguments="-Drepository.url=${REPO_URL} -DskipTests"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Docker Build & Push') {
|
||||
steps {
|
||||
script {
|
||||
sh "pwd"
|
||||
sh "ls -R"
|
||||
def jarPath = sh(script: "ls target/checkout/web-container/target/*.jar | head -n 1", returnStdout: true).trim()
|
||||
def releaseVer = sh(script: "pkgx mvn help:evaluate -Dexpression=project.version -q -DforceStdout -f target/checkout/web-container/pom.xml", returnStdout: true).trim()
|
||||
|
||||
docker.withRegistry("${env.DOCKER_REGISTRY}", '') {
|
||||
def customImage = docker.build("${env.DOCKER_REGISTRY}/my-app:${releaseVer}",
|
||||
"-f build/Dockerfile --build-arg JAR_FILE=${jarPath} .")
|
||||
|
||||
if (params.DRY_RUN) {
|
||||
echo "DRY_RUN - do not push image to registry"
|
||||
}
|
||||
else {
|
||||
customImage.push("latest")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
build/settings.xml
Normal file
20
build/settings.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<servers>
|
||||
<server>
|
||||
<id>77zzcx7-releases</id>
|
||||
<username>${env.REPO_USER}</username>
|
||||
<password>${env.REPO_TOKEN}</password>
|
||||
</server>
|
||||
</servers>
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>central</id>
|
||||
<mirrorOf>*</mirrorOf>
|
||||
<name>77zzcx7-central</name>
|
||||
<url>${env.REPO_URL}/releases</url>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
</settings>
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
@@ -31,6 +31,10 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -1,19 +1,26 @@
|
||||
package de.nbscloud.dashboard;
|
||||
|
||||
import de.nbscloud.dashboard.widget.MachineMetricsWidget;
|
||||
import de.nbscloud.webcontainer.registry.App;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class DashboardApp implements App, InitializingBean {
|
||||
public static final String ID = "dashboard";
|
||||
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "dashboard";
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +38,11 @@ public class DashboardApp implements App, InitializingBean {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Widget> getWidgets() {
|
||||
return List.of(new MachineMetricsWidget());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.appRegistry.registerApp(this);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.nbscloud.dashboard.config;
|
||||
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.UpdateScrapers;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "nbs-cloud.dashboard")
|
||||
@PropertySource("classpath:/config/dashboard-application.properties")
|
||||
public class DashboardConfig {
|
||||
@Configuration
|
||||
public static class Metrics {
|
||||
@Configuration
|
||||
public static class Updates {
|
||||
private List<UpdateScrapers> scrapers;
|
||||
|
||||
public List<UpdateScrapers> getScrapers() {
|
||||
return scrapers;
|
||||
}
|
||||
|
||||
public void setScrapers(List<UpdateScrapers> scrapers) {
|
||||
this.scrapers = scrapers;
|
||||
}
|
||||
}
|
||||
|
||||
private Updates updates;
|
||||
|
||||
public Updates getUpdates() {
|
||||
return updates;
|
||||
}
|
||||
|
||||
public void setUpdates(Updates updates) {
|
||||
this.updates = updates;
|
||||
}
|
||||
}
|
||||
|
||||
private Metrics metrics;
|
||||
|
||||
public Metrics getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
public void setMetrics(Metrics metrics) {
|
||||
this.metrics = metrics;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
@@ -20,6 +21,7 @@ public class DashboardController {
|
||||
@GetMapping("/dashboard")
|
||||
public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
model.addAttribute("widgets", this.appRegistry.getAll().stream().flatMap(a -> a.getWidgets().stream()).collect(Collectors.toList()));
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "dashboard/index";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.nbscloud.dashboard.controller;
|
||||
|
||||
import de.nbscloud.dashboard.config.DashboardConfig;
|
||||
import de.nbscloud.dashboard.widget.service.MetricService;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
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 javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Controller
|
||||
public class DashboardWidgetController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DashboardWidgetController.class);
|
||||
|
||||
@Autowired
|
||||
private WebContainerSharedConfig webContainerSharedConfig;
|
||||
@Autowired
|
||||
private MetricService metricService;
|
||||
@Autowired
|
||||
private DashboardConfig dashboardConfig;
|
||||
|
||||
@GetMapping("dashboard/widgets/machineMetrics")
|
||||
public String getDiskUsage(HttpServletRequest request, HttpServletResponse response, Model model) {
|
||||
|
||||
model.addAttribute("interfaces", metricService.getInterfaces());
|
||||
model.addAttribute("updates", metricService.getUpdates());
|
||||
|
||||
return "dashboard/widgets/machineMetrics :: dashboard-machine-metrics";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.nbscloud.dashboard.widget;
|
||||
|
||||
import de.nbscloud.dashboard.DashboardApp;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
|
||||
public class MachineMetricsWidget implements Widget {
|
||||
@Override
|
||||
public String getPath() {
|
||||
return DashboardApp.ID + "/widgets/machineMetrics";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nbscloud.dashboard.widget.metrics.updates.scraper;
|
||||
|
||||
import de.nbscloud.dashboard.widget.service.MetricService;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface UpdateScraper {
|
||||
public MetricService.UpdateContainer scrape();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.nbscloud.dashboard.widget.metrics.updates.scraper;
|
||||
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.impl.ArchAuracleScraperImpl;
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.impl.ArchCheckupdatesScraperImpl;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public enum UpdateScrapers {
|
||||
ARCH_CHECKUPDATES(ArchCheckupdatesScraperImpl.class),
|
||||
ARCH_AURACLE(ArchAuracleScraperImpl.class);
|
||||
|
||||
private Class<? extends UpdateScraper> scraperClass;
|
||||
|
||||
UpdateScrapers(Class<? extends UpdateScraper> scraperClass) {
|
||||
this.scraperClass = scraperClass;
|
||||
}
|
||||
|
||||
public UpdateScraper getUpdateScraper() {
|
||||
try {
|
||||
return this.scraperClass.getDeclaredConstructor().newInstance();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.nbscloud.dashboard.widget.metrics.updates.scraper.impl;
|
||||
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.UpdateScraper;
|
||||
import de.nbscloud.dashboard.widget.service.MetricService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class ArchAuracleScraperImpl implements UpdateScraper {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ArchAuracleScraperImpl.class);
|
||||
private static final String KEY = "AUR";
|
||||
|
||||
@Override
|
||||
public MetricService.UpdateContainer scrape() {
|
||||
try {
|
||||
final Process process = Runtime.getRuntime()
|
||||
.exec(new String[]{"sh", "-c", "auracle outdated | wc -l"});
|
||||
final String updateCount = new BufferedReader(new InputStreamReader(process.getInputStream())).readLine();
|
||||
|
||||
process.destroy();
|
||||
|
||||
if(updateCount != null && !updateCount.isBlank()) {
|
||||
return new MetricService.UpdateContainer(KEY, updateCount);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not get AUR update count", e);
|
||||
|
||||
return new MetricService.UpdateContainer(KEY, "error");
|
||||
}
|
||||
|
||||
return new MetricService.UpdateContainer(KEY, "0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.nbscloud.dashboard.widget.metrics.updates.scraper.impl;
|
||||
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.UpdateScraper;
|
||||
import de.nbscloud.dashboard.widget.service.MetricService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class ArchCheckupdatesScraperImpl implements UpdateScraper {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ArchCheckupdatesScraperImpl.class);
|
||||
private static final String KEY = "pacman";
|
||||
|
||||
@Override
|
||||
public MetricService.UpdateContainer scrape() {
|
||||
try {
|
||||
final Process process = Runtime.getRuntime()
|
||||
.exec(new String[]{"sh", "-c", "checkupdates | wc -l"});
|
||||
final String updateCount = new BufferedReader(new InputStreamReader(process.getInputStream())).readLine();
|
||||
|
||||
process.destroy();
|
||||
|
||||
if(updateCount != null && !updateCount.isBlank()) {
|
||||
return new MetricService.UpdateContainer(KEY, updateCount);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not get pacman update count", e);
|
||||
|
||||
return new MetricService.UpdateContainer(KEY, "error");
|
||||
}
|
||||
|
||||
return new MetricService.UpdateContainer(KEY, "0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.nbscloud.dashboard.widget.service;
|
||||
|
||||
import de.nbscloud.dashboard.config.DashboardConfig;
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.UpdateScraper;
|
||||
import de.nbscloud.dashboard.widget.metrics.updates.scraper.UpdateScrapers;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class MetricService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MetricService.class);
|
||||
|
||||
public static record InterfaceContainer(String name, String ip) {
|
||||
}
|
||||
|
||||
public static record UpdateContainer(String key, String count) {
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private DashboardConfig dashboardConfig;
|
||||
|
||||
public List<InterfaceContainer> getInterfaces() {
|
||||
try {
|
||||
final Iterator<NetworkInterface> networkInterfaceIterator = NetworkInterface.getNetworkInterfaces()
|
||||
.asIterator();
|
||||
final List<InterfaceContainer> retList = new ArrayList<>();
|
||||
|
||||
while (networkInterfaceIterator.hasNext()) {
|
||||
final NetworkInterface networkInterface = networkInterfaceIterator.next();
|
||||
|
||||
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
|
||||
retList.add(new InterfaceContainer(networkInterface.getDisplayName(), removeInterfaceName(removeSlash(interfaceAddress.getAddress()
|
||||
.toString()))));
|
||||
}
|
||||
}
|
||||
|
||||
retList.sort(Comparator.comparing(InterfaceContainer::name));
|
||||
|
||||
return retList;
|
||||
} catch (SocketException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String removeSlash(String address) {
|
||||
return address.startsWith("/") ? address.substring(1) : address;
|
||||
}
|
||||
|
||||
private static final String removeInterfaceName(String address) {
|
||||
// IPv6 addresses have the interface name in them like
|
||||
// abc:...:xyz%myInterface
|
||||
|
||||
return address.contains("%") ? address.substring(0, address.indexOf("%")) : address;
|
||||
}
|
||||
|
||||
public List<UpdateContainer> getUpdates() {
|
||||
return this.dashboardConfig.getMetrics()
|
||||
.getUpdates()
|
||||
.getScrapers()
|
||||
.stream()
|
||||
.map(UpdateScrapers::getUpdateScraper)
|
||||
.map(UpdateScraper::scrape)
|
||||
.sorted(Comparator.comparing(UpdateContainer::key))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
spring.profiles.active=@activeProfiles@
|
||||
|
||||
info.app.name=NoBullShit Cloud - Dashboard app
|
||||
info.app.description=A simple dashboard app with widget support
|
||||
|
||||
nbs-cloud.dashboard.metrics.updates.scrapers[0]=ARCH_CHECKUPDATES
|
||||
nbs-cloud.dashboard.metrics.updates.scrapers[1]=ARCH_AURACLE
|
||||
@@ -0,0 +1,7 @@
|
||||
nbscloud.dashboard.greeting=Welcome to nbscloud
|
||||
|
||||
nbscloud.dashboard.index.title=nbscloud\: dashboard
|
||||
|
||||
nbscloud.dashboard.dashboard-machine-metrics-widget.heading=dashboard machine metrics
|
||||
nbscloud.dashboard.dashboard-machine-metrics-widget-table.interfaces=Interfaces\:
|
||||
nbscloud.dashboard.dashboard-machine-metrics-widget-table.updates=Updates\:
|
||||
@@ -0,0 +1,7 @@
|
||||
nbscloud.dashboard.greeting=Willkommen bei nbscloud
|
||||
|
||||
nbscloud.dashboard.index.title=nbscloud\: \u00DCbersicht
|
||||
|
||||
nbscloud.dashboard.dashboard-machine-metrics-widget.heading=dashboard Metriken
|
||||
nbscloud.dashboard.dashboard-machine-metrics-widget-table.interfaces=Schnittstellen\:
|
||||
nbscloud.dashboard.dashboard-machine-metrics-widget-table.updates=Updates\:
|
||||
@@ -1,3 +1,36 @@
|
||||
#content-container {
|
||||
padding-top: 2em;
|
||||
#widgets-container {
|
||||
padding: 2em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-content: space-between;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
background-color:var(--background-color-highlight);
|
||||
padding: 1em;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.widget-heading {
|
||||
|
||||
}
|
||||
|
||||
.greeting {
|
||||
display: block;
|
||||
font-size: 1.5em;
|
||||
margin-top: 0.83em;
|
||||
margin-bottom: 0.83em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.widget > table > tbody > tr > td {
|
||||
padding-inline-end: 1em;
|
||||
}
|
||||
@@ -14,7 +14,14 @@
|
||||
<div id="main-container">
|
||||
<div th:replace="includes/header :: header"/>
|
||||
<div id="content-container">
|
||||
Welcome to nbscloud
|
||||
<div id="greetings-container">
|
||||
<p class="greeting" th:text="#{nbscloud.dashboard.greeting}" />
|
||||
</div>
|
||||
<div id="widgets-container">
|
||||
<th:block th:each="widget : ${widgets}">
|
||||
<th:block th:utext="${#servletContext.getRequestDispatcher('/' + widget.path).include(#request,#response)}"/>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div id="dashboard-machine-metrics-widget" class="widget" th:fragment="dashboard-machine-metrics">
|
||||
<p class="widget-heading" th:text="#{nbscloud.dashboard.dashboard-machine-metrics-widget.heading}"/>
|
||||
<table id="dashboard-machine-metrics-widget-table">
|
||||
<tr th:each="interface, i : ${interfaces}">
|
||||
<td th:if="${i.count == 1}"
|
||||
th:text="#{nbscloud.dashboard.dashboard-machine-metrics-widget-table.interfaces}" />
|
||||
<td th:if="${i.count > 1}"></td>
|
||||
<td th:text="${interface.name}" />
|
||||
<td th:text="${interface.ip}" />
|
||||
</tr>
|
||||
<tr th:each="update, i : ${updates}">
|
||||
<td th:if="${i.count == 1}"
|
||||
th:text="#{nbscloud.dashboard.dashboard-machine-metrics-widget-table.updates}" />
|
||||
<td th:if="${i.count > 1}"></td>
|
||||
<td th:text="${update.key}" />
|
||||
<td th:text="${update.count}" />
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
27
files-api/pom.xml
Normal file
27
files-api/pom.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>web-container-registry</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.nbscloud.files.api;
|
||||
|
||||
import de.nbscloud.webcontainer.registry.App;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FilesService {
|
||||
record ContentContainer(boolean directory, Path path, String name, long size,
|
||||
LocalDateTime lastModified) {
|
||||
}
|
||||
|
||||
class ContentTree {
|
||||
private String name;
|
||||
private boolean directory;
|
||||
private String path;
|
||||
private List<ContentTree> subTree = new ArrayList<>();
|
||||
private ContentTree parent;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(boolean directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public List<ContentTree> getSubTree() {
|
||||
return subTree;
|
||||
}
|
||||
|
||||
public void setSubTree(List<ContentTree> subTree) {
|
||||
this.subTree = subTree;
|
||||
}
|
||||
|
||||
public boolean getHasSubTree() {
|
||||
return !this.subTree.isEmpty();
|
||||
}
|
||||
|
||||
public ContentTree getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void setParent(ContentTree parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
void createAppDirectoryIfNotExists(App app);
|
||||
|
||||
void createDirectory(App app, Path path);
|
||||
|
||||
void createFile(App app, Path path, byte[] content);
|
||||
|
||||
void overwriteFile(App app, Path path, byte[] content);
|
||||
|
||||
void delete(App app, Path path);
|
||||
|
||||
byte[] get(App app, Path path);
|
||||
|
||||
/**
|
||||
* Paths in return list are always relative to the appDir. Non-recursive list.
|
||||
*
|
||||
* @param app to list the files for
|
||||
* @param path in case of an empty optional the appDir is used as start dir. If not empty, path has to be relative
|
||||
* to the appDir
|
||||
*/
|
||||
List<ContentContainer> list(App app, Optional<Path> path);
|
||||
|
||||
ContentTree getTree(App app, Optional<Path> path);
|
||||
|
||||
List<String> collectDirs(App app, Optional<Path> path);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
@@ -21,6 +21,10 @@
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>web-container-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -44,6 +48,11 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.flywaydb</groupId>-->
|
||||
<!-- <artifactId>flyway-core</artifactId>-->
|
||||
@@ -68,6 +77,11 @@
|
||||
<!-- <artifactId>spring-security-test</artifactId>-->
|
||||
<!-- <scope>test</scope>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import de.nbscloud.files.config.FilesConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Component
|
||||
@SessionScope
|
||||
public class AppLocationTracker implements InitializingBean {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AppLocationTracker.class);
|
||||
|
||||
@Autowired
|
||||
private FilesConfig filesConfig;
|
||||
@Autowired
|
||||
private LocationTracker locationTracker;
|
||||
|
||||
private Path baseDirPath;
|
||||
|
||||
public Path resolve(String appId, Path path) {
|
||||
validate(Path.of(appId));
|
||||
validate(path);
|
||||
|
||||
final Path appPath = this.baseDirPath.resolve(appId);
|
||||
|
||||
return appPath.resolve(path);
|
||||
}
|
||||
|
||||
private void validate(Path path) {
|
||||
if(path == null) {
|
||||
throw new IllegalStateException("Null");
|
||||
}
|
||||
|
||||
if(path.toString().contains("..")) {
|
||||
throw new IllegalStateException("Relative path");
|
||||
}
|
||||
|
||||
if (path.isAbsolute()) {
|
||||
throw new IllegalStateException("Absolute path: " + path);
|
||||
}
|
||||
|
||||
if(!this.baseDirPath.resolve(path).normalize().startsWith(this.baseDirPath)) {
|
||||
throw new IllegalStateException("Illegal path: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.baseDirPath = this.locationTracker.getAppDir();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import de.nbscloud.files.api.FilesService.ContentContainer;
|
||||
import de.nbscloud.files.api.FilesService.ContentTree;
|
||||
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;
|
||||
@@ -11,10 +12,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -22,17 +22,17 @@ import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Service
|
||||
public class FileSystemService {
|
||||
|
||||
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
|
||||
@@ -88,23 +88,31 @@ public class FileSystemService {
|
||||
private MimeTypeDetector mimeTypeDetector = new MimeTypeDetector();
|
||||
|
||||
public Path createDirectory(String name) {
|
||||
|
||||
try {
|
||||
return Files.createDirectories(this.locationTracker.getCurrentLocation().resolve(name));
|
||||
return Files.createDirectories(this.locationTracker.resolve(name));
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not create directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
void createDirectory(Path path) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
try {
|
||||
Files.createDirectories(path);
|
||||
} 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);
|
||||
}
|
||||
createFile(this.locationTracker.resolve(name), content);
|
||||
}
|
||||
|
||||
public void createFile(Path path, byte[] content) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
try {
|
||||
Files.write(path, content, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
|
||||
} catch (IOException e) {
|
||||
@@ -112,16 +120,20 @@ public class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean delete(String name) {
|
||||
public void overwriteFile(Path path, byte[] content) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
try {
|
||||
// TODO does only delete dirs if they are empty - but maybe we want that?
|
||||
return Files.deleteIfExists(this.locationTracker.getCurrentLocation().resolve(name));
|
||||
Files.write(path, content, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not delete file", e);
|
||||
throw new FileSystemServiceException("Could not overwrite file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path move(Path originalPath, Path newPath) {
|
||||
this.locationTracker.ensureValidPath(originalPath);
|
||||
this.locationTracker.ensureValidPath(newPath);
|
||||
|
||||
try {
|
||||
return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
@@ -129,9 +141,26 @@ public class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] get(String name) {
|
||||
public boolean delete(String name) {
|
||||
return delete(this.locationTracker.resolve(name));
|
||||
}
|
||||
|
||||
public boolean delete(Path path) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
try {
|
||||
return Files.readAllBytes(this.locationTracker.getCurrentLocation().resolve(name));
|
||||
// TODO does only delete dirs if they are empty - but maybe we want that?
|
||||
return Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not delete file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] get(Path path) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not get file", e);
|
||||
}
|
||||
@@ -139,34 +168,109 @@ public class FileSystemService {
|
||||
|
||||
public void stream(String name, OutputStream outputStream) {
|
||||
try {
|
||||
Files.copy(this.locationTracker.getCurrentLocation().resolve(name), outputStream);
|
||||
Files.copy(this.locationTracker.resolve(name), outputStream);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not get file", e);
|
||||
throw new FileSystemServiceException("Could not stream file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stream(Path targetPath, ZipOutputStream outputStream) {
|
||||
this.locationTracker.ensureValidPath(targetPath);
|
||||
|
||||
try {
|
||||
Files.walkFileTree(targetPath, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (Files.isDirectory(file)) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
final ZipEntry e = new ZipEntry(targetPath.relativize(file).toString());
|
||||
|
||||
e.setSize(getSize(file));
|
||||
e.setTime(System.currentTimeMillis());
|
||||
|
||||
outputStream.putNextEntry(e);
|
||||
|
||||
StreamUtils.copy(stream(file), outputStream);
|
||||
|
||||
outputStream.closeEntry();
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not stream file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(String name) {
|
||||
return stream(this.locationTracker.resolve(name));
|
||||
}
|
||||
|
||||
public InputStream stream(Path name) {
|
||||
this.locationTracker.ensureValidPath(name);
|
||||
|
||||
try {
|
||||
return Files.newInputStream(this.locationTracker.getCurrentLocation().resolve(name));
|
||||
return Files.newInputStream(name);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not get file", e);
|
||||
throw new FileSystemServiceException("Could not stream file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public long getSize(String name) {
|
||||
return getSize(this.locationTracker.resolve(name));
|
||||
}
|
||||
|
||||
public long getSize(Path name) {
|
||||
this.locationTracker.ensureValidPath(name);
|
||||
|
||||
try {
|
||||
return Files.size(this.locationTracker.getCurrentLocation().resolve(name));
|
||||
return Files.size(name);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not get file size", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDevice() {
|
||||
try {
|
||||
final Process process = Runtime.getRuntime()
|
||||
.exec(new String[]{"findmnt", "-n", "-o", "SOURCE", "--target", this.locationTracker.getBaseDirPath().toString()});
|
||||
final String device = new BufferedReader(new InputStreamReader(process.getInputStream())).readLine();
|
||||
|
||||
process.destroy();
|
||||
|
||||
return device;
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not get file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getMimeType(String name) {
|
||||
public long getDiskSize(String device) {
|
||||
try {
|
||||
final String detectedMimeType = this.mimeTypeDetector.detectMimeType(this.locationTracker.getCurrentLocation()
|
||||
.resolve(name));
|
||||
final Process process = Runtime.getRuntime()
|
||||
.exec(new String[]{"lsblk", "-b", "-o", "SIZE", "-n", device});
|
||||
final String size = new BufferedReader(new InputStreamReader(process.getInputStream())).readLine();
|
||||
|
||||
logger.debug("Detected mime type {} for file {}", detectedMimeType, name);
|
||||
process.destroy();
|
||||
|
||||
return Long.parseLong(size);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not get disk size", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getMimeType(String name) {
|
||||
return getMimeType(this.locationTracker.resolve(name));
|
||||
}
|
||||
|
||||
public String getMimeType(Path path) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
try {
|
||||
final String detectedMimeType = this.mimeTypeDetector.detectMimeType(path);
|
||||
|
||||
logger.debug("Detected mime type {} for file {}", detectedMimeType, path);
|
||||
|
||||
return detectedMimeType;
|
||||
} catch (GetBytesException e) {
|
||||
@@ -199,9 +303,11 @@ public class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public List<ContentContainer> list(SortOrder sortOrder) {
|
||||
List<ContentContainer> list(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
||||
this.locationTracker.ensureValidPath(startPath);
|
||||
|
||||
try {
|
||||
List<ContentContainer> contentList = Files.list(this.locationTracker.getCurrentLocation())
|
||||
List<ContentContainer> contentList = Files.list(startPath)
|
||||
.filter(path -> {
|
||||
boolean ok = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.isDirectory(path);
|
||||
|
||||
@@ -216,7 +322,7 @@ public class FileSystemService {
|
||||
final boolean isDir = Files.isDirectory(path);
|
||||
|
||||
return new ContentContainer(isDir,
|
||||
this.locationTracker.getRelativeToBaseDir(path),
|
||||
relativizer.apply(path),
|
||||
path.getFileName().toString(),
|
||||
isDir ? FileUtils.sizeOfDirectory(path.toFile()) : Files.size(path),
|
||||
LocalDateTime.ofInstant(Files.getLastModifiedTime(path)
|
||||
@@ -234,18 +340,97 @@ public class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
ContentTree getTree(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
||||
this.locationTracker.ensureValidPath(startPath);
|
||||
|
||||
try {
|
||||
if (!Files.isDirectory(startPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ContentTree root = new ContentTree();
|
||||
|
||||
root.setName(startPath.getFileName().toString());
|
||||
root.setPath(relativizer.apply(startPath).toString());
|
||||
root.setDirectory(true);
|
||||
|
||||
final AtomicReference<ContentTree> current = new AtomicReference<>(root);
|
||||
|
||||
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (startPath.equals(dir)) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
final ContentTree dirTree = new ContentTree();
|
||||
|
||||
dirTree.setName(dir.getFileName().toString());
|
||||
dirTree.setPath(relativizer.apply(dir).toString());
|
||||
dirTree.setDirectory(true);
|
||||
dirTree.setParent(current.get());
|
||||
|
||||
current.get().getSubTree().add(dirTree);
|
||||
current.set(dirTree);
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
final ContentTree fileTree = new ContentTree();
|
||||
|
||||
fileTree.setName(file.getFileName().toString());
|
||||
fileTree.setPath(relativizer.apply(file).toString());
|
||||
fileTree.setDirectory(false);
|
||||
fileTree.setParent(current.get());
|
||||
|
||||
current.get().getSubTree().add(fileTree);
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
current.set(current.get().getParent());
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
|
||||
return root;
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not list files", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDirectory(Path path) {
|
||||
this.locationTracker.ensureValidPath(path);
|
||||
|
||||
return Files.isDirectory(path);
|
||||
}
|
||||
|
||||
public List<ContentContainer> list(SortOrder sortOrder) {
|
||||
return list(this.locationTracker.getCurrentLocation(), sortOrder, path -> this.locationTracker.getRelativeToBaseDir(path));
|
||||
}
|
||||
|
||||
public List<String> collectDirs(String sourceFile) {
|
||||
return collectDirs(this.locationTracker.resolve(sourceFile), this.locationTracker.getBaseDirPath(), path -> this.locationTracker.getRelativeToBaseDir(path), false);
|
||||
}
|
||||
|
||||
List<String> collectDirs(Path sourcePath, Path baseDir, Function<Path, Path> relativizer, boolean includeSource) {
|
||||
this.locationTracker.ensureValidPath(sourcePath);
|
||||
this.locationTracker.ensureValidPath(baseDir);
|
||||
|
||||
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>() {
|
||||
Files.walkFileTree(baseDir, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
super.visitFile(dir, attrs);
|
||||
|
||||
if (sourceIsDir && sourcePath.equals(dir)) {
|
||||
if (!includeSource && sourceIsDir && sourcePath.equals(dir)) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
@@ -261,7 +446,7 @@ public class FileSystemService {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
final String relPath = locationTracker.getRelativeToBaseDir(dir).toString();
|
||||
final String relPath = relativizer.apply(dir).toString();
|
||||
|
||||
resultList.add("/" + relPath);
|
||||
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import de.nbscloud.files.widget.DiskUsageWidget;
|
||||
import de.nbscloud.webcontainer.registry.App;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class FilesApp implements App, InitializingBean {
|
||||
public static final String ID = "files";
|
||||
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "files";
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +39,11 @@ public class FilesApp implements App, InitializingBean {
|
||||
return 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Widget> getWidgets() {
|
||||
return List.of(new DiskUsageWidget());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.appRegistry.registerApp(this);
|
||||
|
||||
89
files/src/main/java/de/nbscloud/files/FilesServiceImpl.java
Normal file
89
files/src/main/java/de/nbscloud/files/FilesServiceImpl.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import de.nbscloud.files.api.FilesService;
|
||||
import de.nbscloud.files.exception.FileSystemServiceException;
|
||||
import de.nbscloud.webcontainer.registry.App;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@SessionScope
|
||||
public class FilesServiceImpl implements FilesService {
|
||||
@Autowired
|
||||
private FileSystemService fileSystemService;
|
||||
@Autowired
|
||||
private AppLocationTracker locationTracker;
|
||||
|
||||
private Path resolve(App app, Path path) {
|
||||
if(path.startsWith("/")) {
|
||||
path = path.subpath(0, path.getNameCount());
|
||||
}
|
||||
|
||||
return this.locationTracker.resolve(app.getId(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAppDirectoryIfNotExists(App app) {
|
||||
try {
|
||||
this.fileSystemService.createDirectory(resolve(app, Path.of("")));
|
||||
}
|
||||
catch(FileSystemServiceException e) {
|
||||
// Ignore, may happen if the dir already exists
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createDirectory(App app, Path path) {
|
||||
this.fileSystemService.createDirectory(resolve(app, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFile(App app, Path path, byte[] content) {
|
||||
this.fileSystemService.createFile(resolve(app, path), content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwriteFile(App app, Path path, byte[] content) {
|
||||
this.fileSystemService.overwriteFile(resolve(app, path), content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(App app, Path path) {
|
||||
this.fileSystemService.delete(resolve(app, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] get(App app, Path path) {
|
||||
return this.fileSystemService.get(resolve(app, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContentContainer> list(App app, Optional<Path> path) {
|
||||
final Path appPath = resolve(app, Path.of(""));
|
||||
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||
|
||||
return this.fileSystemService.list(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentTree getTree(App app, Optional<Path> path) {
|
||||
final Path appPath = resolve(app, Path.of(""));
|
||||
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||
|
||||
return this.fileSystemService.getTree(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> collectDirs(App app, Optional<Path> path) {
|
||||
final Path appPath = resolve(app, Path.of(""));
|
||||
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||
|
||||
|
||||
return this.fileSystemService.collectDirs(p, appPath, callbackPath -> appPath.relativize(callbackPath), true);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
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.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@Component
|
||||
public class LocationTracker {
|
||||
@SessionScope
|
||||
public class LocationTracker implements InitializingBean {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LocationTracker.class);
|
||||
|
||||
@Autowired
|
||||
@@ -20,32 +24,55 @@ public class LocationTracker {
|
||||
private Path baseDirPath;
|
||||
private Path currentLocation;
|
||||
|
||||
public void init() {
|
||||
// For testing
|
||||
void init_internal(String baseDir) {
|
||||
this.baseDirPath = Paths.get(baseDir);
|
||||
this.currentLocation = this.baseDirPath.resolve("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.baseDirPath = Paths.get(this.filesConfig.getBaseDir());
|
||||
this.currentLocation = this.baseDirPath.resolve("");
|
||||
|
||||
logger.info("Initialized location to {}", this.currentLocation);
|
||||
logger.info("{}: Initialized location to {}", id(), this.currentLocation);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
logger.debug("Reset location");
|
||||
logger.debug("{}: Reset location", id());
|
||||
|
||||
init();
|
||||
try {
|
||||
afterPropertiesSet();
|
||||
} catch (Exception e) {
|
||||
// Cannot happen
|
||||
}
|
||||
}
|
||||
|
||||
public Path getCurrentLocation() {
|
||||
public void resetCurrentLocation() {
|
||||
this.currentLocation = this.baseDirPath.resolve("");
|
||||
}
|
||||
|
||||
private long id() {
|
||||
return System.identityHashCode(this);
|
||||
}
|
||||
|
||||
Path getCurrentLocation() {
|
||||
return this.currentLocation;
|
||||
}
|
||||
|
||||
public Path getRelativeLocation() {
|
||||
return this.baseDirPath.relativize(this.currentLocation);
|
||||
public String getRelativeLocation() {
|
||||
return this.baseDirPath.relativize(this.currentLocation).toString();
|
||||
}
|
||||
|
||||
public Path getRelativeToBaseDir(Path other) {
|
||||
return this.baseDirPath.relativize(other);
|
||||
}
|
||||
|
||||
public Path getTrashBin() {
|
||||
public Path getAppDir() {
|
||||
return this.baseDirPath.resolve(this.filesConfig.getAppDir());
|
||||
}
|
||||
|
||||
private Path getTrashBin() {
|
||||
return this.baseDirPath.resolve(this.filesConfig.getTrashBinName());
|
||||
}
|
||||
|
||||
@@ -62,12 +89,24 @@ public class LocationTracker {
|
||||
}
|
||||
|
||||
public void setCurrentLocation(String navigateTo) {
|
||||
validate(navigateTo);
|
||||
validate_internal(navigateTo);
|
||||
|
||||
this.currentLocation = this.baseDirPath.resolve(navigateTo);
|
||||
final Path newLocation = this.baseDirPath.resolve(navigateTo);
|
||||
|
||||
logger.debug("{}: Change location from {} to {}", id(), this.currentLocation, newLocation);
|
||||
|
||||
this.currentLocation = newLocation;
|
||||
}
|
||||
|
||||
private void validate(String navigateTo) {
|
||||
public void setBaseDirPath(String path) {
|
||||
validate_internal(path);
|
||||
|
||||
this.baseDirPath = this.baseDirPath.resolve(path);
|
||||
|
||||
logger.debug("{}: Change base dir to {}", id(), baseDirPath);
|
||||
}
|
||||
|
||||
private void validate_internal(String navigateTo) {
|
||||
if(navigateTo == null) {
|
||||
throw new IllegalStateException("Null");
|
||||
}
|
||||
@@ -80,12 +119,61 @@ public class LocationTracker {
|
||||
throw new IllegalStateException("Absolute path: " + navigateTo);
|
||||
}
|
||||
|
||||
if(!this.baseDirPath.resolve(navigateTo).startsWith(this.baseDirPath)) {
|
||||
// Regardless of where we navigate to, we must never leave the base directory
|
||||
// The normalize() call is important, as it resolves (possible) relative paths
|
||||
if(!this.currentLocation.resolve(navigateTo).normalize().startsWith(this.baseDirPath)) {
|
||||
throw new IllegalStateException("Illegal path: " + navigateTo);
|
||||
}
|
||||
|
||||
if(!this.baseDirPath.resolve(navigateTo).normalize().startsWith(this.baseDirPath)) {
|
||||
throw new IllegalStateException("Illegal path: " + navigateTo);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getBaseDirPath() {
|
||||
return baseDirPath;
|
||||
public void ensureValidPath(Path path) {
|
||||
// The provided systemd unit file restricts access to the nbscloud/ directory
|
||||
// but better safe than sorry - user could roll their own service after all
|
||||
|
||||
if(path == null) {
|
||||
throw new IllegalStateException("Null");
|
||||
}
|
||||
|
||||
if(path.toString().contains("..")) {
|
||||
throw new IllegalStateException("Relative path");
|
||||
}
|
||||
|
||||
// Regardless of where we navigate to, we must never leave the base directory
|
||||
// The normalize() call is important, as it resolves (possible) relative paths
|
||||
if(!path.normalize().startsWith(this.baseDirPath)) {
|
||||
throw new IllegalStateException("Illegal path: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
public Path resolve(String name) {
|
||||
validate_internal(name);
|
||||
|
||||
return this.currentLocation.resolve(name).normalize();
|
||||
}
|
||||
|
||||
public Path resolveToBaseDir(String name) {
|
||||
validate_internal(name);
|
||||
|
||||
return this.baseDirPath.resolve(name).normalize();
|
||||
}
|
||||
|
||||
public Path resolveShare(String shareUuid) {
|
||||
validate_internal(shareUuid);
|
||||
|
||||
return this.baseDirPath.resolve(this.filesConfig.getSharesName()).resolve(shareUuid);
|
||||
}
|
||||
|
||||
public Path resolveTrash(String name) {
|
||||
validate_internal(name);
|
||||
|
||||
return this.baseDirPath.resolve(this.filesConfig.getTrashBinName()).resolve(name);
|
||||
}
|
||||
|
||||
Path getBaseDirPath() {
|
||||
return this.baseDirPath;
|
||||
}
|
||||
}
|
||||
|
||||
18
files/src/main/java/de/nbscloud/files/SessionInfo.java
Normal file
18
files/src/main/java/de/nbscloud/files/SessionInfo.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
@Component
|
||||
@SessionScope
|
||||
public class SessionInfo {
|
||||
private boolean restrictedSession = false;
|
||||
|
||||
public boolean isRestrictedSession() {
|
||||
return restrictedSession;
|
||||
}
|
||||
|
||||
public void setRestrictedSession(boolean restrictedSession) {
|
||||
this.restrictedSession = restrictedSession;
|
||||
}
|
||||
}
|
||||
51
files/src/main/java/de/nbscloud/files/Share.java
Normal file
51
files/src/main/java/de/nbscloud/files/Share.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class Share {
|
||||
private String uuid;
|
||||
private boolean oneTime;
|
||||
private LocalDate expiryDate;
|
||||
private String path;
|
||||
private String password; // clear text, as protecting it provides no additional security
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public boolean isOneTime() {
|
||||
return oneTime;
|
||||
}
|
||||
|
||||
public void setOneTime(boolean oneTime) {
|
||||
this.oneTime = oneTime;
|
||||
}
|
||||
|
||||
public LocalDate getExpiryDate() {
|
||||
return expiryDate;
|
||||
}
|
||||
|
||||
public void setExpiryDate(LocalDate expiryDate) {
|
||||
this.expiryDate = expiryDate;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ public class FilesConfig {
|
||||
private String trashBinName;
|
||||
private int truncateFileNameChars;
|
||||
private String sharesName;
|
||||
private String appDir;
|
||||
|
||||
public String getBaseDir() {
|
||||
return baseDir;
|
||||
@@ -62,4 +63,12 @@ public class FilesConfig {
|
||||
public void setSharesName(String sharesName) {
|
||||
this.sharesName = sharesName;
|
||||
}
|
||||
|
||||
public String getAppDir() {
|
||||
return appDir;
|
||||
}
|
||||
|
||||
public void setAppDir(String appDir) {
|
||||
this.appDir = appDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
package de.nbscloud.files.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.nbscloud.files.FileSystemService;
|
||||
import de.nbscloud.files.LocationTracker;
|
||||
import de.nbscloud.files.SessionInfo;
|
||||
import de.nbscloud.files.Share;
|
||||
import de.nbscloud.files.api.FilesService.ContentContainer;
|
||||
import de.nbscloud.files.config.FilesConfig;
|
||||
import de.nbscloud.files.exception.FileSystemServiceException;
|
||||
import de.nbscloud.files.form.PasswordForm;
|
||||
import de.nbscloud.files.form.RenameForm;
|
||||
import de.nbscloud.files.form.ShareForm;
|
||||
import de.nbscloud.webcontainer.MessageHelper;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
import de.nbscloud.webcontainer.shared.util.ControllerUtils;
|
||||
import org.apache.commons.io.input.ObservableInputStream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -27,14 +38,16 @@ 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.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Controller
|
||||
public class FilesController implements InitializingBean {
|
||||
@@ -51,12 +64,12 @@ public class FilesController implements InitializingBean {
|
||||
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<>();
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
@Autowired
|
||||
private MessageHelper messageHelper;
|
||||
@Autowired
|
||||
private SessionInfo sessionInfo;
|
||||
|
||||
@GetMapping("/files/browse/**")
|
||||
public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
||||
@@ -64,13 +77,13 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
updateLocation(httpServletRequest);
|
||||
|
||||
model.addAttribute("errors", getAndClear(this.errors));
|
||||
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
||||
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
||||
this.messageHelper.addAndClearAll(model);
|
||||
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
||||
model.addAttribute("content", getContent(order));
|
||||
model.addAttribute("sortOrder", order);
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
model.addAttribute("restricted", this.sessionInfo.isRestrictedSession());
|
||||
model.addAttribute("prefix", "");
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "files/filesIndex";
|
||||
@@ -82,20 +95,20 @@ public class FilesController implements InitializingBean {
|
||||
try {
|
||||
// Soft delete
|
||||
this.fileSystemService.move(
|
||||
this.locationTracker.getCurrentLocation().resolve(Path.of(filename)),
|
||||
this.locationTracker.getTrashBin().resolve(filename));
|
||||
this.locationTracker.resolve(filename),
|
||||
this.locationTracker.resolveTrash(filename));
|
||||
|
||||
this.infoMessages.add("nbscloud.files.delete.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.delete.success");
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Could not soft delete file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// Hard delete
|
||||
this.fileSystemService.delete(filename);
|
||||
|
||||
this.infoMessages.add("nbscloud.files.delete.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.delete.success");
|
||||
}
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
@@ -107,6 +120,9 @@ public class FilesController implements InitializingBean {
|
||||
model.addAttribute("form", new RenameForm(filename, getCurrentLocation(), filename));
|
||||
model.addAttribute("targetDirs", this.fileSystemService.collectDirs(filename));
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
model.addAttribute("filename", filename);
|
||||
model.addAttribute("restricted", this.sessionInfo.isRestrictedSession());
|
||||
model.addAttribute("prefix", "");
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "files/rename";
|
||||
@@ -114,40 +130,67 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
@PostMapping("/files/doRename")
|
||||
public String doRename(RenameForm form) {
|
||||
final Path sourcePath = this.locationTracker.getCurrentLocation().resolve(Path.of(form.getOriginalFilename()));
|
||||
final Path sourcePath = this.locationTracker.resolve(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()));
|
||||
final Path targetPath = this.locationTracker.resolve(form.getFilename());
|
||||
|
||||
try {
|
||||
this.fileSystemService.move(sourcePath, targetPath);
|
||||
|
||||
this.infoMessages.add("nbscloud.files.rename.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.rename.success");
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Could not rename file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
}
|
||||
|
||||
@GetMapping("/files/shares/files/download")
|
||||
public ResponseEntity sharesDownloadFile(HttpServletResponse response, String filename) {
|
||||
return downloadFile(response, filename);
|
||||
}
|
||||
|
||||
@GetMapping("/files/download")
|
||||
public ResponseEntity<Resource> downloadFile(String filename) {
|
||||
// TODO download of directories via .zip? Also needs to be streaming
|
||||
public ResponseEntity downloadFile(HttpServletResponse response, String filename) {
|
||||
final Path targetPath = this.locationTracker.resolve(filename);
|
||||
|
||||
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) {
|
||||
if (Files.isDirectory(targetPath)) {
|
||||
downloadDirectory(targetPath, filename, response);
|
||||
|
||||
return ResponseEntity.ok(":)");
|
||||
} else {
|
||||
return downloadSingleFile(filename);
|
||||
}
|
||||
} catch (RuntimeException | IOException e) {
|
||||
logger.error("Could not get file", e);
|
||||
|
||||
return ResponseEntity.internalServerError().body(null);
|
||||
return ResponseEntity.internalServerError().body(":(");
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity downloadSingleFile(String filename) {
|
||||
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)));
|
||||
}
|
||||
|
||||
private void downloadDirectory(Path targetPath, String filename, HttpServletResponse response) throws IOException {
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + ".zip");
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
|
||||
try (ZipOutputStream zippedOut = new ZipOutputStream(response.getOutputStream())) {
|
||||
fileSystemService.stream(targetPath, zippedOut);
|
||||
|
||||
zippedOut.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +200,11 @@ public class FilesController implements InitializingBean {
|
||||
try {
|
||||
this.fileSystemService.createFile(file.getOriginalFilename(), file.getBytes());
|
||||
|
||||
this.infoMessages.add("nbscloud.files.created.file.success");
|
||||
} catch (IOException e) {
|
||||
this.messageHelper.addInfo("nbscloud.files.created.file.success");
|
||||
} catch (IOException | RuntimeException e) {
|
||||
logger.error("Could not upload file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,67 +216,212 @@ public class FilesController implements InitializingBean {
|
||||
try {
|
||||
this.fileSystemService.createDirectory(dirName);
|
||||
|
||||
this.infoMessages.add("nbscloud.files.created.dir.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.created.dir.success");
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Could not create dir", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(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));
|
||||
@GetMapping("files/share")
|
||||
public String share(Model model, String filename) {
|
||||
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
||||
model.addAttribute("form", new ShareForm(filename, false, LocalDate.now().plusDays(1), null));
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
model.addAttribute("filename", filename);
|
||||
model.addAttribute("restricted", this.sessionInfo.isRestrictedSession());
|
||||
model.addAttribute("prefix", "");
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "files/share";
|
||||
}
|
||||
|
||||
@PostMapping("/files/doShare")
|
||||
public String doShare(@RequestParam("filename") String filename,
|
||||
@RequestParam(value = "oneTime", defaultValue = "false") boolean oneTime,
|
||||
@RequestParam(value = "expiryDate", required = false) String expiryDateString,
|
||||
@RequestParam(value = "password", required = false) String password
|
||||
) {
|
||||
final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename));
|
||||
final String shareUuid = UUID.randomUUID().toString();
|
||||
final Share share = new Share();
|
||||
|
||||
if(StringUtils.isEmpty(password)) {
|
||||
password = null; // if no real password has been provided assume no password
|
||||
}
|
||||
|
||||
share.setUuid(shareUuid);
|
||||
share.setPath(filePath.toString());
|
||||
share.setExpiryDate(ControllerUtils.parseDate(expiryDateString));
|
||||
share.setOneTime(oneTime);
|
||||
share.setPassword(password);
|
||||
|
||||
try {
|
||||
this.fileSystemService.createFile(this.locationTracker.getBaseDirPath()
|
||||
.resolve(this.filesConfig.getSharesName())
|
||||
.resolve(shareUuid), filePath.toString()
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
final String shareJson = mapper.writeValueAsString(share);
|
||||
|
||||
this.shareInfo.add("/files/shares?shareUuid=" + shareUuid);
|
||||
} catch (RuntimeException e) {
|
||||
this.fileSystemService.createFile(this.locationTracker.resolveShare(share.getUuid()), shareJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
this.messageHelper.addShare("/files/shares?shareUuid=" + share.getUuid());
|
||||
} catch (RuntimeException | JsonProcessingException e) {
|
||||
logger.error("Could not share file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(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();
|
||||
@GetMapping("/files/shares/files/browse/**")
|
||||
public String sharesStart(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
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) {
|
||||
final String start = start(model, httpServletRequest, sortOrder);
|
||||
|
||||
model.addAttribute("prefix", "/files/shares");
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
// We need to copy all functions for shares because they should be available without auth and that is
|
||||
// controlled by the URI via htaccess/htpasswd
|
||||
@GetMapping("files/shares")
|
||||
public Object shares(Model model, String shareUuid) {
|
||||
this.locationTracker.reset();
|
||||
|
||||
try {
|
||||
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
||||
final Share share = mapper.readValue(shareJson, Share.class);
|
||||
final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath());
|
||||
final Optional<LocalDate> optExpiryDate = Optional.ofNullable(share.getExpiryDate());
|
||||
final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false);
|
||||
|
||||
if(share.getPassword() != null) {
|
||||
model.addAttribute("form", new PasswordForm(shareUuid, null));
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "files/checkPassword";
|
||||
}
|
||||
else {
|
||||
if (expired) {
|
||||
fileSystemService.delete(locationTracker.resolveShare(shareUuid));
|
||||
|
||||
return ResponseEntity.status(410).body("Share expired!");
|
||||
}
|
||||
|
||||
if(this.fileSystemService.isDirectory(sharedFilePath)) {
|
||||
this.locationTracker.setBaseDirPath(share.getPath());
|
||||
this.locationTracker.resetCurrentLocation();
|
||||
|
||||
this.sessionInfo.setRestrictedSession(true);
|
||||
|
||||
return "redirect:/files/browse/";
|
||||
}
|
||||
else {
|
||||
return doShares(shareUuid);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException | JsonProcessingException e) {
|
||||
logger.error("Could not get shared file", e);
|
||||
|
||||
return ResponseEntity.internalServerError().body(null);
|
||||
return ResponseEntity.internalServerError().body(":(");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/files/shares/checkPassword")
|
||||
public Object checkPassword(Model model, @RequestParam(value = "shareUuid", required = true) String shareUuid,
|
||||
@RequestParam(value = "password", required = true) String password) {
|
||||
try {
|
||||
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
||||
final Share share = mapper.readValue(shareJson, Share.class);
|
||||
final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath());
|
||||
final Optional<LocalDate> optExpiryDate = Optional.ofNullable(share.getExpiryDate());
|
||||
final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false);
|
||||
|
||||
if(share.getPassword().equals(password)) {
|
||||
if (expired) {
|
||||
fileSystemService.delete(locationTracker.resolveShare(shareUuid));
|
||||
|
||||
return ResponseEntity.status(410).body("Share expired!");
|
||||
}
|
||||
|
||||
if(this.fileSystemService.isDirectory(sharedFilePath)) {
|
||||
this.locationTracker.setBaseDirPath(share.getPath());
|
||||
this.locationTracker.resetCurrentLocation();
|
||||
|
||||
this.sessionInfo.setRestrictedSession(true);
|
||||
|
||||
return "redirect:/files/browse/";
|
||||
}
|
||||
else {
|
||||
return doShares(shareUuid);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.messageHelper.addResolvableError("nbscloud.files.share.error.passwordWrong");
|
||||
this.messageHelper.addAndClearAll(model);
|
||||
|
||||
model.addAttribute("form", new PasswordForm(shareUuid, null));
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "files/checkPassword";
|
||||
}
|
||||
} catch (RuntimeException | JsonProcessingException e) {
|
||||
logger.error("Could not get shared file", e);
|
||||
|
||||
return ResponseEntity.internalServerError().body(":(");
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity doShares(String shareUuid) {
|
||||
try {
|
||||
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
||||
final Share share = mapper.readValue(shareJson, Share.class);
|
||||
final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath());
|
||||
final String filename = sharedFilePath.getFileName().toString();
|
||||
|
||||
if(this.fileSystemService.isDirectory(sharedFilePath)) {
|
||||
throw new IllegalStateException("Attempt to access folder via file share: " + sharedFilePath);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||
.header(HttpHeaders.CONTENT_TYPE, this.fileSystemService.getMimeType(sharedFilePath))
|
||||
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(sharedFilePath)))
|
||||
.body(new InputStreamResource(new ObservableInputStream(this.fileSystemService.stream(sharedFilePath), new ObservableInputStream.Observer() {
|
||||
@Override
|
||||
public void closed() throws IOException {
|
||||
if (share.isOneTime()) {
|
||||
fileSystemService.delete(locationTracker.resolveShare(shareUuid));
|
||||
}
|
||||
}
|
||||
})));
|
||||
} catch (RuntimeException | JsonProcessingException e) {
|
||||
logger.error("Could not get shared file", e);
|
||||
|
||||
return ResponseEntity.internalServerError().body(":(");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/files/shares/files/preview")
|
||||
public void sharesPreview(HttpServletResponse response, String filename) {
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
preview(response, filename);
|
||||
}
|
||||
|
||||
@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 + "\"");
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "filename=" + filename);
|
||||
|
||||
this.fileSystemService.stream(filename, response.getOutputStream());
|
||||
} catch (FileSystemServiceException | IOException e) {
|
||||
@@ -241,6 +429,19 @@ public class FilesController implements InitializingBean {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/files/shares/files/gallery")
|
||||
public String sharesGallery(Model model) {
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
final String gallery = gallery(model);
|
||||
|
||||
model.addAttribute("prefix", "/files/shares");
|
||||
|
||||
return gallery;
|
||||
}
|
||||
|
||||
@GetMapping("files/gallery")
|
||||
public String gallery(Model model) {
|
||||
try {
|
||||
@@ -248,32 +449,26 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
model.addAttribute("files", files);
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
model.addAttribute("restricted", this.sessionInfo.isRestrictedSession());
|
||||
model.addAttribute("prefix", "");
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "files/gallery";
|
||||
} catch (FileSystemServiceException e) {
|
||||
logger.error("Could not generate gallery", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(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);
|
||||
private List<ContentContainer> getContent(FileSystemService.SortOrder order) {
|
||||
final List<ContentContainer> contentList = this.fileSystemService.list(order);
|
||||
|
||||
if (!this.locationTracker.isBasePath()) {
|
||||
contentList.add(0,
|
||||
new FileSystemService.ContentContainer(true,
|
||||
new ContentContainer(true,
|
||||
this.locationTracker.getParent(),
|
||||
"..",
|
||||
0L,
|
||||
@@ -290,47 +485,50 @@ public class FilesController implements InitializingBean {
|
||||
if (split.length > 1) {
|
||||
this.locationTracker.setCurrentLocation(UriUtils.decode(split[1].substring(1), StandardCharsets.UTF_8));
|
||||
} else {
|
||||
this.locationTracker.reset();
|
||||
if(!this.sessionInfo.isRestrictedSession()) {
|
||||
this.locationTracker.reset();
|
||||
}
|
||||
else {
|
||||
this.locationTracker.resetCurrentLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getCurrentLocation() {
|
||||
if (this.locationTracker.getRelativeLocation().toString().isEmpty()) {
|
||||
if (this.locationTracker.getRelativeLocation().isEmpty()) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return this.locationTracker.getRelativeLocation().toString();
|
||||
return this.locationTracker.getRelativeLocation();
|
||||
}
|
||||
|
||||
private String getCurrentLocationPrefixed() {
|
||||
if (this.locationTracker.getRelativeLocation().toString().isEmpty()) {
|
||||
if (this.locationTracker.getRelativeLocation().isEmpty()) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return "/" + this.locationTracker.getRelativeLocation().toString();
|
||||
return "/" + this.locationTracker.getRelativeLocation();
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
// 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
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.nbscloud.files.controller;
|
||||
|
||||
import de.nbscloud.files.FileSystemService;
|
||||
import de.nbscloud.files.LocationTracker;
|
||||
import de.nbscloud.files.config.FilesConfig;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
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 javax.servlet.http.HttpServletResponse;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@Controller
|
||||
public class FilesWidgetController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(FilesWidgetController.class);
|
||||
|
||||
@Autowired
|
||||
private FilesConfig filesConfig;
|
||||
@Autowired
|
||||
private LocationTracker locationTracker;
|
||||
@Autowired
|
||||
private WebContainerSharedConfig webContainerSharedConfig;
|
||||
@Autowired
|
||||
private FileSystemService fileSystemService;
|
||||
|
||||
@GetMapping("files/widgets/diskUsage")
|
||||
public String getDiskUsage(HttpServletRequest request, HttpServletResponse response, Model model) {
|
||||
final String device = this.fileSystemService.getDevice();
|
||||
final long diskSize = this.fileSystemService.getDiskSize(device);
|
||||
final long used = FileUtils.sizeOfDirectory(Paths.get(this.filesConfig.getBaseDir()).toFile());
|
||||
final long available = diskSize - used;
|
||||
|
||||
model.addAttribute("baseDir", this.filesConfig.getBaseDir());
|
||||
model.addAttribute("device", device);
|
||||
model.addAttribute("available", available);
|
||||
model.addAttribute("used", used);
|
||||
model.addAttribute("diskSize", diskSize);
|
||||
|
||||
return "files/widgets/diskUsage :: file-disk-usage";
|
||||
}
|
||||
}
|
||||
27
files/src/main/java/de/nbscloud/files/form/PasswordForm.java
Normal file
27
files/src/main/java/de/nbscloud/files/form/PasswordForm.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package de.nbscloud.files.form;
|
||||
|
||||
public class PasswordForm {
|
||||
private String shareUuid;
|
||||
private String password;
|
||||
|
||||
public PasswordForm(String shareUuid, String password) {
|
||||
this.shareUuid = shareUuid;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getShareUuid() {
|
||||
return shareUuid;
|
||||
}
|
||||
|
||||
public void setShareUuid(String shareUuid) {
|
||||
this.shareUuid = shareUuid;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
50
files/src/main/java/de/nbscloud/files/form/ShareForm.java
Normal file
50
files/src/main/java/de/nbscloud/files/form/ShareForm.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package de.nbscloud.files.form;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class ShareForm {
|
||||
private String filename;
|
||||
private boolean oneTime;
|
||||
private LocalDate expiryDate;
|
||||
|
||||
private String password;
|
||||
|
||||
public ShareForm(String filename, boolean oneTime, LocalDate expiryDate, String password) {
|
||||
this.filename = filename;
|
||||
this.oneTime = oneTime;
|
||||
this.expiryDate = expiryDate;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public boolean isOneTime() {
|
||||
return oneTime;
|
||||
}
|
||||
|
||||
public void setOneTime(boolean oneTime) {
|
||||
this.oneTime = oneTime;
|
||||
}
|
||||
|
||||
public LocalDate getExpiryDate() {
|
||||
return expiryDate;
|
||||
}
|
||||
|
||||
public void setExpiryDate(LocalDate expiryDate) {
|
||||
this.expiryDate = expiryDate;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.nbscloud.files.widget;
|
||||
|
||||
import de.nbscloud.files.FilesApp;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
|
||||
public class DiskUsageWidget implements Widget {
|
||||
@Override
|
||||
public String getPath() {
|
||||
return FilesApp.ID + "/widgets/diskUsage";
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ 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=/opt/nextcloud_data/marius/files
|
||||
nbs-cloud.files.baseDir=/home/marius/nbstest
|
||||
|
||||
# Knob to configure whether hidden files (e.g. starting with '.' on *NIX)
|
||||
# will be filtered in the file view
|
||||
@@ -25,4 +25,7 @@ nbs-cloud.files.trashBinName=nbs.internal/nbs.trashbin
|
||||
nbs-cloud.files.truncateFileNameChars=130
|
||||
|
||||
# Knob to configure the name of the shares directory
|
||||
nbs-cloud.files.sharesName=nbs.internal/nbs.shares
|
||||
nbs-cloud.files.sharesName=nbs.internal/nbs.shares
|
||||
|
||||
# Knob to configure the name of the app directory
|
||||
nbs-cloud.files.appDir=nbs.internal/apps
|
||||
@@ -6,19 +6,36 @@ 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.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.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.title=nbscloud - files\: share \u0020
|
||||
nbscloud.files.share.label.filename=Filename\:
|
||||
nbscloud.files.share.label.onetime=One time\:
|
||||
nbscloud.files.share.label.expirydate=Expiry date\:
|
||||
nbscloud.files.share.label.password=Password\:
|
||||
nbscloud.files.share.submit=Share
|
||||
|
||||
nbscloud.files.share-message=Shared file\:\u0020
|
||||
nbscloud.files.share.error.passwordWrong=Wrong password!
|
||||
|
||||
nbscloud.files.share.password.title=Password check
|
||||
nbscloud.files.share.password.submit=Check password
|
||||
nbscloud.files.share.password.label.password=Password\:
|
||||
|
||||
nbscloud.files.file-disk-usage-widget.heading=files disk usage
|
||||
nbscloud.files.file-disk-usage-widget-table.basedir=Base dir\:
|
||||
nbscloud.files.file-disk-usage-widget-table.device=Host device\:
|
||||
nbscloud.files.file-disk-usage-widget-table.available=Available\:
|
||||
nbscloud.files.file-disk-usage-widget-table.used=Used\:
|
||||
nbscloud.files.file-disk-usage-widget-table.disksize=Disk size\:
|
||||
|
||||
nbscloud.files.delete.success=File deleted
|
||||
nbscloud.files.rename.success=File renamed
|
||||
|
||||
@@ -6,19 +6,36 @@ nbscloud.files.files-content-table.table-header.lastmodified=Zuletzt ge\u00E4nde
|
||||
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.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.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.title=nbscloud - Dateien\: teilen \u0020
|
||||
nbscloud.files.share.label.filename=Dateiname\:
|
||||
nbscloud.files.share.label.onetime=Einmaliger Abruf\:
|
||||
nbscloud.files.share.label.expirydate=Ablaufdatum\:
|
||||
nbscloud.files.share.label.password=Passwort\:
|
||||
nbscloud.files.share.submit=Teilen
|
||||
|
||||
nbscloud.files.share-message=Datei geteilt\:\u0020
|
||||
nbscloud.files.share.error.passwordWrong=Passwort falsch!
|
||||
|
||||
nbscloud.files.share.password.title=Passwort eingeben
|
||||
nbscloud.files.share.password.submit=Passwort pr\u00FCfen
|
||||
nbscloud.files.share.password.label.password=Passwort\:
|
||||
|
||||
nbscloud.files.file-disk-usage-widget.heading=Dateien Festplattennutzung
|
||||
nbscloud.files.file-disk-usage-widget-table.basedir=Verzeichnis\:
|
||||
nbscloud.files.file-disk-usage-widget-table.device=Ger\u00E4t\:
|
||||
nbscloud.files.file-disk-usage-widget-table.available=Verf\u00FCgbar\:
|
||||
nbscloud.files.file-disk-usage-widget-table.used=Belegt\:
|
||||
nbscloud.files.file-disk-usage-widget-table.disksize=Gr\u00F6\u00DFe\:
|
||||
|
||||
nbscloud.files.delete.success=Datei gel\u00F6scht
|
||||
nbscloud.files.rename.success=Datei verschoben/umbenannt
|
||||
|
||||
@@ -48,17 +48,21 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#rename-form {
|
||||
#rename-form,
|
||||
#share-form,
|
||||
#share-password-form {
|
||||
margin-top: 3em;
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
#rename-form * {
|
||||
#rename-form *,
|
||||
#share-form *,
|
||||
#share-password-form * {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#rename-form > select > * {
|
||||
#rename-form > select > *{
|
||||
margin-top: unset;
|
||||
}
|
||||
|
||||
@@ -66,109 +70,16 @@
|
||||
width: 70em;
|
||||
}
|
||||
|
||||
#rename-form > input[type=text] {
|
||||
#rename-form > input[type=text],
|
||||
#share-form > input[type=text],
|
||||
#share-password-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;
|
||||
}
|
||||
}
|
||||
|
||||
#gallery-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#file-disk-usage-widget {
|
||||
display: block;
|
||||
}
|
||||
25
files/src/main/resources/templates/files/checkPassword.html
Normal file
25
files/src/main/resources/templates/files/checkPassword.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{nbscloud.files.share.password.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="shortcut icon" th:href="@{/favicon.ico}"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div th:replace="includes/messages :: messages"/>
|
||||
<form id="share-password-form" action="#" th:action="@{/files/shares/checkPassword}" th:object="${form}" method="post" enctype="multipart/form-data">
|
||||
<label for="password" th:text="#{nbscloud.files.share.password.label.password}"/>
|
||||
<input type="password" id="password" th:field="*{password}" />
|
||||
<input type="hidden" id="shareUuid" th:field="*{shareUuid}" />
|
||||
<input type="submit" th:value="#{nbscloud.files.share.password.submit}" />
|
||||
</form>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,22 +13,7 @@
|
||||
<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="includes/messages :: messages"/>
|
||||
<div th:replace="files/includes/menu :: menu"/>
|
||||
<div id="content-container">
|
||||
<table id="files-content-table">
|
||||
@@ -64,9 +49,9 @@
|
||||
<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}"
|
||||
<a th:if="${fileEntry.name != '..'}" th:href="@{__${prefix}__/files/browse/__${fileEntry.path}__}"
|
||||
th:text="${fileEntry.name}"/>
|
||||
<a th:if="${fileEntry.name == '..'}" th:href="@{/files/browse/} + ${fileEntry.path}"
|
||||
<a th:if="${fileEntry.name == '..'}" th:href="@{__${prefix}__/files/browse/__${fileEntry.path}__}"
|
||||
th:text="${fileEntry.name}"/>
|
||||
</td>
|
||||
<td th:if="${!fileEntry.directory && @filesFormatter.needsTruncate(fileEntry.name)}"
|
||||
@@ -82,26 +67,27 @@
|
||||
<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">
|
||||
<div id="files-content-table-show-actions-detail-container-rename"
|
||||
th:if="${!restricted}">
|
||||
<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">
|
||||
<div id="files-content-table-show-actions-detail-container-delete"
|
||||
th:if="${!restricted}">
|
||||
<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})}"
|
||||
<div id="files-content-table-show-actions-detail-container-download">
|
||||
<a th:href="@{__${prefix}__/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})}"
|
||||
<a th:href="@{__${prefix}__/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">
|
||||
<div id="files-content-table-show-actions-detail-container-share"
|
||||
th:if="${!restricted}">
|
||||
<a th:href="@{/files/share(filename=${fileEntry.name})}"
|
||||
th:text="#{nbscloud.files-content-table.table.actions.share}"/>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<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})}">
|
||||
<img class="galleryImage" th:each="file : ${files}" th:src="@{__${prefix}__/files/preview(filename=${file})}">
|
||||
</div>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<div id="menu-container" th:fragment="menu">
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="upload-container">
|
||||
<div th:if="${!restricted}" class="menu-spacer"></div>
|
||||
<div th:if="${!restricted}" id="upload-container" class="menu-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"
|
||||
<span id="upload-container-span-input" class="icon menu-icon"
|
||||
onclick="document.getElementById('upload-container-file-input').click();"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="create-dir-container">
|
||||
<div th:if="${!restricted}" class="menu-spacer"></div>
|
||||
<div th:if="${!restricted}" id="create-dir-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="create-dir-container-span-input" class="icon files-menu-icon"></span>
|
||||
<span id="create-dir-container-span-input" class="icon 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">
|
||||
<div class="menu-modal">
|
||||
<div class="menu-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"/>
|
||||
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
<div class="menu-spacer"></div>
|
||||
<div>
|
||||
<a id="gallery-link" th:href="@{/files/gallery}">
|
||||
<span class="icon files-menu-icon"></span>
|
||||
<a id="gallery-link" th:href="@{__${prefix}__/files/gallery}">
|
||||
<span class="icon menu-icon"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
30
files/src/main/resources/templates/files/share.html
Normal file
30
files/src/main/resources/templates/files/share.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{nbscloud.files.share.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="share-form" action="#" th:action="@{/files/doShare}" th:object="${form}" method="post" enctype="multipart/form-data">
|
||||
<label for="filename" th:text="#{nbscloud.files.share.label.filename}"/>
|
||||
<input type="text" id="filename" th:field="*{filename}" readonly />
|
||||
<label for="oneTime" th:text="#{nbscloud.files.share.label.onetime}"/>
|
||||
<input type="checkbox" id="oneTime" th:field="*{oneTime}" />
|
||||
<label for="expiryDate" th:text="#{nbscloud.files.share.label.expirydate}"/>
|
||||
<input type="date" id="expiryDate" th:field="*{expiryDate}"/>
|
||||
<label for="password" th:text="#{nbscloud.files.share.label.password}"/>
|
||||
<input type="password" id="password" th:field="*{password}"/>
|
||||
<input type="submit" th:value="#{nbscloud.files.share.submit}" />
|
||||
</form>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<div id="file-disk-usage-widget" class="widget" th:fragment="file-disk-usage">
|
||||
<p class="widget-heading" th:text="#{nbscloud.files.file-disk-usage-widget.heading}"/>
|
||||
<table id="file-disk-usage-widget-table">
|
||||
<tr>
|
||||
<td th:text="#{nbscloud.files.file-disk-usage-widget-table.basedir}" />
|
||||
<td th:text="${baseDir}" />
|
||||
</tr>
|
||||
<tr>
|
||||
<td th:text="#{nbscloud.files.file-disk-usage-widget-table.device}" />
|
||||
<td th:text="${device}" />
|
||||
</tr>
|
||||
<tr>
|
||||
<td th:text="#{nbscloud.files.file-disk-usage-widget-table.disksize}" />
|
||||
<td th:text="${@filesFormatter.formatSize(diskSize)}" />
|
||||
</tr>
|
||||
<tr>
|
||||
<td th:text="#{nbscloud.files.file-disk-usage-widget-table.used}" />
|
||||
<td th:text="${@filesFormatter.formatSize(used)}" />
|
||||
</tr>
|
||||
<tr>
|
||||
<td th:text="#{nbscloud.files.file-disk-usage-widget-table.available}" />
|
||||
<td th:text="${@filesFormatter.formatSize(available)}" />
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class LocationTrackerTest {
|
||||
private static final String BASE_DIR = "/tmp";
|
||||
|
||||
private LocationTracker locationTracker = new LocationTracker();
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
this.locationTracker.init_internal(BASE_DIR);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_null() {
|
||||
this.locationTracker.resolve(null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_relative1() {
|
||||
this.locationTracker.resolve("../test");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_relative2() {
|
||||
this.locationTracker.resolve("/../test");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_relative3() {
|
||||
this.locationTracker.resolve("\\../test");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_relative4() {
|
||||
this.locationTracker.resolve("\\/../test");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_relative5() {
|
||||
this.locationTracker.resolve("..");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_relative6_ok() {
|
||||
// While 0x2E translates to the dot character and that might be used to
|
||||
// traverse the path, the Java Path API does not resolve those and just treats them as
|
||||
// any other file/dir name
|
||||
Assert.assertTrue(this.locationTracker.resolve("%2e%2e%2f").toString().equals(BASE_DIR + "/%2e%2e%2f"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_relative7() {
|
||||
this.locationTracker.resolve("...");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_relative8_ok() {
|
||||
Assert.assertTrue(this.locationTracker.resolve("././").toString().equals(BASE_DIR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_relative9_ok() {
|
||||
// The tilde is treated as regular file/dir name if it is resolved from a path
|
||||
Assert.assertTrue(this.locationTracker.resolve("~").toString().equals(BASE_DIR + "/~"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void test_absolut() {
|
||||
this.locationTracker.resolve("/bin");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
@@ -21,6 +21,10 @@
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>web-container-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package de.nbscloud.notes.controller;
|
||||
|
||||
import de.nbscloud.files.api.FilesService;
|
||||
import de.nbscloud.notes.NotesApp;
|
||||
import de.nbscloud.webcontainer.MessageHelper;
|
||||
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.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 java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
public class NotesController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotesController.class);
|
||||
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
@Autowired
|
||||
private FilesService filesService;
|
||||
@Autowired
|
||||
private WebContainerSharedConfig webContainerSharedConfig;
|
||||
@Autowired
|
||||
private NotesApp app;
|
||||
@Autowired
|
||||
private MessageHelper messageHelper;
|
||||
|
||||
public static Optional<Path> notePathToPath(String notePath) {
|
||||
final Optional<String> optNotePath = Optional.ofNullable(notePath);
|
||||
|
||||
return optNotePath.map(p -> Path.of(p));
|
||||
}
|
||||
|
||||
@GetMapping("/notes")
|
||||
public String start(Model model, @RequestParam(required = false) String notePath) {
|
||||
this.filesService.createAppDirectoryIfNotExists(this.app);
|
||||
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
final byte[] content = this.filesService.get(app, optNotePath.get());
|
||||
|
||||
model.addAttribute("content", new String(content));
|
||||
}
|
||||
|
||||
this.messageHelper.addAndClearAll(model);
|
||||
model.addAttribute("currentNote", optNotePath.orElse(Path.of("/")));
|
||||
model.addAttribute("tree", this.filesService.getTree(app, Optional.empty()));
|
||||
model.addAttribute("dirs", this.filesService.collectDirs(app, Optional.empty()));
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "notes/notesIndex";
|
||||
}
|
||||
|
||||
@PostMapping("/notes/save")
|
||||
public String save(String textContent, String notePath) {
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
this.filesService.overwriteFile(app, optNotePath.get(), textContent.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.save.success");
|
||||
}
|
||||
|
||||
return "redirect:?notePath=" + notePath;
|
||||
}
|
||||
|
||||
@PostMapping("/notes/delete")
|
||||
public String delete(String notePath) {
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
this.filesService.delete(app, optNotePath.get());
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.delete.success");
|
||||
}
|
||||
|
||||
return "redirect:";
|
||||
}
|
||||
|
||||
@PostMapping("/notes/createDir")
|
||||
public String createDirectory(String dirName, String parentPath, String currentPath) {
|
||||
final Optional<Path> optParentPath = notePathToPath(parentPath);
|
||||
|
||||
if(optParentPath.isPresent()) {
|
||||
final Path dirPath = optParentPath.get().resolve(dirName);
|
||||
|
||||
this.filesService.createDirectory(app, dirPath);
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.createDir.success");
|
||||
}
|
||||
|
||||
if(currentPath == null || currentPath.isBlank()) {
|
||||
return "redirect:";
|
||||
}
|
||||
|
||||
return "redirect:?notePath=" + currentPath;
|
||||
}
|
||||
|
||||
@PostMapping("/notes/createNote")
|
||||
public String createNote(String noteName, String parentPath, String currentPath) {
|
||||
final Optional<Path> optParentPath = notePathToPath(parentPath);
|
||||
|
||||
if(optParentPath.isPresent()) {
|
||||
final Path notePath = optParentPath.get().resolve(noteName);
|
||||
|
||||
this.filesService.createFile(app, notePath, new byte[] {});
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.createNote.success");
|
||||
|
||||
return "redirect:?notePath=" + notePath;
|
||||
}
|
||||
|
||||
return "redirect:?notePath=" + currentPath;
|
||||
}
|
||||
}
|
||||
9
notes/src/main/resources/i18n/notes_messages.properties
Normal file
9
notes/src/main/resources/i18n/notes_messages.properties
Normal file
@@ -0,0 +1,9 @@
|
||||
nbscloud.notes.action.save=Save
|
||||
nbscloud.notes.action.delete=Delete
|
||||
|
||||
nbscloud.notes.save.success=Saved
|
||||
nbscloud.notes.delete.success=Deleted
|
||||
nbscloud.notes.createDir.success=Directory created
|
||||
nbscloud.notes.createNote.success=Note created
|
||||
|
||||
nbscloud.notes.index.title=nbscloud - notes\:\u0020
|
||||
@@ -0,0 +1,9 @@
|
||||
nbscloud.notes.action.save=Speichern
|
||||
nbscloud.notes.action.delete=L\u00F6schen
|
||||
|
||||
nbscloud.notes.save.success=Gespeichert
|
||||
nbscloud.notes.delete.success=Gel\u00F6scht
|
||||
nbscloud.notes.createDir.success=Verzeichnis erstellt
|
||||
nbscloud.notes.createNote.success=Notiz erstellt
|
||||
|
||||
nbscloud.notes.index.title=nbscloud - Notizen\:\u0020
|
||||
59
notes/src/main/resources/static/css/notes_main.css
Normal file
59
notes/src/main/resources/static/css/notes_main.css
Normal file
@@ -0,0 +1,59 @@
|
||||
#content-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
}
|
||||
|
||||
#nav-tree {
|
||||
flex-grow: 1;
|
||||
background-color: var(--background-color-highlight);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#sub-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
flex-grow: 1;
|
||||
flex-basis: 70%;
|
||||
}
|
||||
|
||||
#nav-tree ul {
|
||||
list-style-type: none;
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
|
||||
#nav-tree p, a {
|
||||
margin: 0em;
|
||||
}
|
||||
|
||||
#edit-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
}
|
||||
|
||||
#edit-area textarea {
|
||||
flex: 1;
|
||||
background-color: var(--background-color-highlight);
|
||||
color: var(--text-color);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
#actions-sub {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
#dir-container > * {
|
||||
display: inline;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div th:fragment="tree-level">
|
||||
<ul>
|
||||
<li th:each="tmpSubTree : ${tree}">
|
||||
<div th:if="${tmpSubTree.directory}" id="dir-container">
|
||||
<span class="icon"></span>
|
||||
<p th:text="${tmpSubTree.name}" />
|
||||
</div>
|
||||
<p th:if="${!tmpSubTree.directory}"><a th:href="@{/notes(notePath=${tmpSubTree.path})}" th:text="${tmpSubTree.name}" /></p>
|
||||
<th:block th:include="@{notes/fragments/treeLevel} :: tree-level" th:with="tree=${tmpSubTree.subTree}" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
47
notes/src/main/resources/templates/notes/includes/menu.html
Normal file
47
notes/src/main/resources/templates/notes/includes/menu.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<div id="menu-container" th:fragment="menu">
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="create-dir-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="create-dir-container-span-input" class="icon menu-icon"></span>
|
||||
</summary>
|
||||
<div class="menu-modal">
|
||||
<div class="menu-modal-content">
|
||||
<form method="POST" action="#" th:action="@{/notes/createDir}" enctype="multipart/form-data">
|
||||
<input id="create-dir-container-dir-name" type="text" name="dirName" />
|
||||
<select size="1" id="parentPath" name="parentPath">
|
||||
<option th:each="dir : ${dirs}"
|
||||
th:value="${dir}"
|
||||
th:text="${dir}" />
|
||||
</select>
|
||||
<input type="hidden" name="notePath" th:value="${currentNote}" class="display-none" />
|
||||
<input type="submit" value="Create"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="create-note-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="create-note-container-span-input" class="icon menu-icon"></span>
|
||||
<div class="create-note-container-details-modal-overlay"></div>
|
||||
</summary>
|
||||
<div class="menu-modal">
|
||||
<div class="menu-modal-content">
|
||||
<form method="POST" action="#" th:action="@{/notes/createNote}" enctype="multipart/form-data">
|
||||
<input id="create-note-container-dir-name" type="text" name="noteName" />
|
||||
<select size="1" id="parentPathNote" name="parentPath">
|
||||
<option th:each="dir : ${dirs}"
|
||||
th:value="${dir}"
|
||||
th:text="${dir}" />
|
||||
</select>
|
||||
<input type="hidden" name="notePath" th:value="${currentNote}" class="display-none" />
|
||||
<input type="submit" value="Create"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
40
notes/src/main/resources/templates/notes/notesIndex.html
Normal file
40
notes/src/main/resources/templates/notes/notesIndex.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{nbscloud.notes.index.title} + ${currentNote}"/>
|
||||
|
||||
<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/notes_main.css}"/>
|
||||
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div th:replace="includes/header :: header"/>
|
||||
<div th:replace="includes/messages :: messages"/>
|
||||
<div th:replace="notes/includes/menu :: menu"/>
|
||||
<div id="content-container">
|
||||
<div id="nav-tree">
|
||||
<th:block th:include="@{notes/fragments/treeLevel} :: tree-level" th:with="tree=${tree}" />
|
||||
</div>
|
||||
<div id="sub-content">
|
||||
<form method="POST" action="#" enctype="multipart/form-data">
|
||||
<div id="edit-area">
|
||||
<textarea type="text" th:if="${content != null}" th:text="${content}" rows="25" name="textContent"/>
|
||||
</div>
|
||||
<div id="actions">
|
||||
<div th:if="${content != null}" id="actions-sub">
|
||||
<input type="submit" th:value="#{nbscloud.notes.action.save}" th:formaction="@{/notes/save}" />
|
||||
<input type="submit" th:value="#{nbscloud.notes.action.delete}" th:formaction="@{/notes/delete}" />
|
||||
<input type="hidden" name="notePath" th:value="${currentNote}" class="display-none" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
|
||||
31
pom.xml
31
pom.xml
@@ -1,7 +1,6 @@
|
||||
<?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>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
@@ -11,13 +10,14 @@
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<description>The umbrella for all No BullShit cloud projects</description>
|
||||
<name>nbs-cloud-aggregator</name>
|
||||
|
||||
<modules>
|
||||
<module>files</module>
|
||||
<module>files-api</module>
|
||||
<module>web-container</module>
|
||||
<module>web-container-registry</module>
|
||||
<module>notes</module>
|
||||
@@ -31,26 +31,26 @@
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>18</java.version>
|
||||
<scmDeveloperConnectionProp />
|
||||
<repository.url>http://reposilite.intern.77zzcx7.de</repository.url>
|
||||
</properties>
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>77zzcx7-snapshots</id>
|
||||
<url>http://192.168.10.4:8100/snapshots/</url>
|
||||
<url>${repository.url}/snapshots/</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>77zzcx7-releases</id>
|
||||
<url>http://192.168.10.4:8100/releases/</url>
|
||||
<url>${repository.url}/releases/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:https://77zzcx7.de/gitea/MK13/NoBullShit-cloud.git</connection>
|
||||
<developerConnection>${scmDeveloperConnectionProp}</developerConnection>
|
||||
<connection>scm:git:https://gitea.77zzcx7.de/MK13/NoBullShit-cloud.git</connection>
|
||||
<developerConnection>scm:git:https://gitea.77zzcx7.de/MK13/NoBullShit-cloud.git</developerConnection>
|
||||
<url>https://77zzcx7.de/gitea/MK13/NoBullShit-cloud</url>
|
||||
<tag>v8</tag>
|
||||
</scm>
|
||||
<tag>v22</tag>
|
||||
</scm>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -64,6 +64,11 @@
|
||||
<artifactId>files</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>web-container-registry</artifactId>
|
||||
@@ -114,6 +119,13 @@
|
||||
<artifactId>mime-types</artifactId>
|
||||
<version>1.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -193,5 +205,4 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
@@ -31,5 +31,9 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
5
todo/src/main/java/de/nbscloud/todo/Category.java
Normal file
5
todo/src/main/java/de/nbscloud/todo/Category.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package de.nbscloud.todo;
|
||||
|
||||
public enum Category {
|
||||
A, B, C;
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
package de.nbscloud.todo;
|
||||
|
||||
import de.nbscloud.todo.widget.TodoWidget;
|
||||
import de.nbscloud.webcontainer.registry.App;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class TodoApp implements App, InitializingBean {
|
||||
|
||||
public static final String ID = "todo";
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "todo";
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +38,11 @@ public class TodoApp implements App, InitializingBean {
|
||||
return 40;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Widget> getWidgets() {
|
||||
return List.of(new TodoWidget());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.appRegistry.registerApp(this);
|
||||
|
||||
74
todo/src/main/java/de/nbscloud/todo/TodoItem.java
Normal file
74
todo/src/main/java/de/nbscloud/todo/TodoItem.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package de.nbscloud.todo;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
public class TodoItem {
|
||||
private String id;
|
||||
private boolean done;
|
||||
private String text;
|
||||
private Category category;
|
||||
private LocalDate added;
|
||||
private LocalDate due;
|
||||
|
||||
public static final TodoItem create(boolean done, String text, Category category, LocalDate added, LocalDate due) {
|
||||
final TodoItem item = new TodoItem();
|
||||
|
||||
item.setDone(done);
|
||||
item.setText(text);
|
||||
item.setCategory(category);
|
||||
item.setAdded(added);
|
||||
item.setDue(due);
|
||||
item.setId(UUID.randomUUID().toString());
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public void setDone(boolean done) {
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public Category getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(Category category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public LocalDate getAdded() {
|
||||
return added;
|
||||
}
|
||||
|
||||
public void setAdded(LocalDate added) {
|
||||
this.added = added;
|
||||
}
|
||||
|
||||
public LocalDate getDue() {
|
||||
return due;
|
||||
}
|
||||
|
||||
public void setDue(LocalDate due) {
|
||||
this.due = due;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
16
todo/src/main/java/de/nbscloud/todo/TodoList.java
Normal file
16
todo/src/main/java/de/nbscloud/todo/TodoList.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package de.nbscloud.todo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TodoList {
|
||||
private List<TodoItem> todos = new ArrayList<>();
|
||||
|
||||
public List<TodoItem> getTodos() {
|
||||
return todos;
|
||||
}
|
||||
|
||||
public void setTodos(List<TodoItem> todos) {
|
||||
this.todos = todos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package de.nbscloud.todo.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.nbscloud.files.api.FilesService;
|
||||
import de.nbscloud.todo.Category;
|
||||
import de.nbscloud.todo.TodoApp;
|
||||
import de.nbscloud.todo.TodoItem;
|
||||
import de.nbscloud.todo.TodoList;
|
||||
import de.nbscloud.webcontainer.MessageHelper;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
import de.nbscloud.webcontainer.shared.util.ControllerUtils;
|
||||
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 org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
public class TodoController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TodoController.class);
|
||||
|
||||
public static final Comparator<TodoItem> TODO_ITEM_COMPARATOR = Comparator.comparing(TodoItem::isDone)
|
||||
.thenComparing(TodoItem::getDue,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(TodoItem::getAdded);
|
||||
static final Path FILENAME = Path.of("todo");
|
||||
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
@Autowired
|
||||
private FilesService filesService;
|
||||
@Autowired
|
||||
private WebContainerSharedConfig webContainerSharedConfig;
|
||||
@Autowired
|
||||
private TodoApp app;
|
||||
@Autowired
|
||||
private MessageHelper messageHelper;
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@GetMapping("/todo")
|
||||
public String start(Model model) {
|
||||
try {
|
||||
this.filesService.createAppDirectoryIfNotExists(this.app);
|
||||
|
||||
final String todoListJson = new String(this.filesService.get(app, FILENAME));
|
||||
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
|
||||
final Map<Category, List<TodoItem>> categoryMap = new TreeMap<>(Comparator.comparing(Category::name));
|
||||
|
||||
todos.getTodos().stream().collect(Collectors.groupingBy(TodoItem::getCategory, () -> categoryMap, Collectors.toList()));
|
||||
|
||||
if(categoryMap.containsKey(Category.A)) {
|
||||
Collections.sort(categoryMap.get(Category.A), TODO_ITEM_COMPARATOR);
|
||||
}
|
||||
|
||||
if(categoryMap.containsKey(Category.B)) {
|
||||
Collections.sort(categoryMap.get(Category.B), TODO_ITEM_COMPARATOR);
|
||||
}
|
||||
|
||||
if(categoryMap.containsKey(Category.C)) {
|
||||
Collections.sort(categoryMap.get(Category.C), TODO_ITEM_COMPARATOR);
|
||||
}
|
||||
|
||||
model.addAttribute("todosByCategory", categoryMap);
|
||||
}
|
||||
catch(JsonProcessingException e) {
|
||||
logger.error("Error while parsing todo file", e);
|
||||
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
catch(RuntimeException re) {
|
||||
if(re.getCause() instanceof NoSuchFileException) {
|
||||
logger.debug("Todo file does not exist, create it");
|
||||
|
||||
try {
|
||||
this.filesService.createFile(app, FILENAME, mapper.writeValueAsString(new TodoList()).getBytes(StandardCharsets.UTF_8));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.error("Error while reading todo file", re);
|
||||
|
||||
this.messageHelper.addError(re.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
model.addAttribute("categories", Category.values());
|
||||
this.messageHelper.addAndClearAll(model);
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "todo/todoIndex";
|
||||
}
|
||||
|
||||
@PostMapping("/todo/addItem")
|
||||
public String addItem(Model model, String text, Category category, String due) {
|
||||
try {
|
||||
final String todoListJson = new String(this.filesService.get(app, FILENAME));
|
||||
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
|
||||
|
||||
todos.getTodos().add(TodoItem.create(false, text, category, LocalDate.now(), ControllerUtils.parseDate(due)));
|
||||
|
||||
this.filesService.overwriteFile(app, FILENAME, mapper.writeValueAsBytes(todos));
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.todo.item.add");
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Error", e);
|
||||
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
|
||||
model.addAttribute("categories", Category.values());
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "redirect:";
|
||||
}
|
||||
|
||||
@PostMapping("/todo/toggle")
|
||||
public String toggle(Model model, String id, boolean done) {
|
||||
try {
|
||||
final String todoListJson = new String(this.filesService.get(app, FILENAME));
|
||||
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
|
||||
|
||||
todos.getTodos().stream().filter(item -> item.getId().equals(id)).findFirst().ifPresent(item -> item.setDone(done));
|
||||
|
||||
this.filesService.overwriteFile(app, FILENAME, mapper.writeValueAsBytes(todos));
|
||||
|
||||
if(done) {
|
||||
this.messageHelper.addInfo("nbscloud.todo.item.toggle.done");
|
||||
}
|
||||
else {
|
||||
this.messageHelper.addInfo("nbscloud.todo.item.toggle.inprogress");
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Error", e);
|
||||
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
|
||||
model.addAttribute("categories", Category.values());
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "redirect:";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.nbscloud.todo.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.nbscloud.files.api.FilesService;
|
||||
import de.nbscloud.todo.Category;
|
||||
import de.nbscloud.todo.TodoApp;
|
||||
import de.nbscloud.todo.TodoItem;
|
||||
import de.nbscloud.todo.TodoList;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
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 javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
public class TodoWidgetController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TodoWidgetController.class);
|
||||
|
||||
@Autowired
|
||||
private WebContainerSharedConfig webContainerSharedConfig;
|
||||
@Autowired
|
||||
private FilesService filesService;
|
||||
@Autowired
|
||||
private TodoApp app;
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@GetMapping("todo/widgets/todoOverview")
|
||||
public String getTodoOverview(HttpServletRequest request, HttpServletResponse response, Model model) {
|
||||
try {
|
||||
this.filesService.createAppDirectoryIfNotExists(this.app);
|
||||
|
||||
final String todoListJson = new String(this.filesService.get(app, TodoController.FILENAME));
|
||||
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
|
||||
final List<TodoItem> topFiveTodos = todos.getTodos().stream().filter(item -> !item.isDone()).sorted(
|
||||
Comparator.comparing(TodoItem::getDue,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(TodoItem::getCategory)
|
||||
.thenComparing(TodoItem::getAdded)).limit(5).collect(Collectors.toList());
|
||||
|
||||
model.addAttribute("todos", topFiveTodos);
|
||||
}
|
||||
catch(JsonProcessingException e) {
|
||||
logger.error("Error while parsing todo file", e);
|
||||
}
|
||||
|
||||
|
||||
return "todo/widgets/todoOverview :: todo-overview";
|
||||
}
|
||||
}
|
||||
11
todo/src/main/java/de/nbscloud/todo/widget/TodoWidget.java
Normal file
11
todo/src/main/java/de/nbscloud/todo/widget/TodoWidget.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package de.nbscloud.todo.widget;
|
||||
|
||||
import de.nbscloud.todo.TodoApp;
|
||||
import de.nbscloud.webcontainer.registry.Widget;
|
||||
|
||||
public class TodoWidget implements Widget {
|
||||
@Override
|
||||
public String getPath() {
|
||||
return TodoApp.ID + "/widgets/todoOverview";
|
||||
}
|
||||
}
|
||||
7
todo/src/main/resources/i18n/todo_messages.properties
Normal file
7
todo/src/main/resources/i18n/todo_messages.properties
Normal file
@@ -0,0 +1,7 @@
|
||||
nbscloud.todo.index.title=nbscloud - todo
|
||||
|
||||
nbscloud.todo.widget.heading=todo overview
|
||||
|
||||
nbscloud.todo.item.add=Todo added
|
||||
nbscloud.todo.item.toggle.done=Done
|
||||
nbscloud.todo.item.toggle.inprogress=In progress
|
||||
@@ -0,0 +1,7 @@
|
||||
nbscloud.todo.index.title=nbscloud - todo
|
||||
|
||||
nbscloud.todo.widget.heading=todo \u00FCbersicht
|
||||
|
||||
nbscloud.todo.item.add=Todo hinzugef\u00FCgt
|
||||
nbscloud.todo.item.toggle.done=Fertig
|
||||
nbscloud.todo.item.toggle.inprogress=Unfertig
|
||||
24
todo/src/main/resources/static/css/todo_main.css
Normal file
24
todo/src/main/resources/static/css/todo_main.css
Normal file
@@ -0,0 +1,24 @@
|
||||
#content-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
}
|
||||
|
||||
.todo-category {
|
||||
flex-grow: 1;
|
||||
background-color:var(--background-color-highlight);
|
||||
padding: 1em;
|
||||
align-self: stretch;
|
||||
word-break: break-all;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.todo-category-header {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.todo-item > td:nth-child(2) {
|
||||
padding-inline: 3em;
|
||||
}
|
||||
24
todo/src/main/resources/templates/todo/includes/menu.html
Normal file
24
todo/src/main/resources/templates/todo/includes/menu.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div id="menu-container" th:fragment="menu">
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="add-item-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="add-item-container-span-input" class="icon menu-icon"></span>
|
||||
</summary>
|
||||
<div class="menu-modal">
|
||||
<div class="menu-modal-content">
|
||||
<form method="POST" action="#" th:action="@{/todo/addItem}" enctype="multipart/form-data">
|
||||
<input id="add-item-container-text" type="text" name="text" />
|
||||
<select size="1" id="add-item-category" name="category">
|
||||
<option th:each="category : ${categories}"
|
||||
th:value="${category}"
|
||||
th:text="${category}" />
|
||||
</select>
|
||||
<input type="date" id="add-item-due" name="due"/>
|
||||
<input type="submit" value="Add"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
39
todo/src/main/resources/templates/todo/todoIndex.html
Normal file
39
todo/src/main/resources/templates/todo/todoIndex.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{nbscloud.todo.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/todo_main.css}"/>
|
||||
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div th:replace="includes/header :: header"/>
|
||||
<div th:replace="includes/messages :: messages"/>
|
||||
<div th:replace="todo/includes/menu :: menu"/>
|
||||
<div id="content-container">
|
||||
<div class="todo-category" th:each="entry : ${todosByCategory}">
|
||||
<p th:text="${#strings.prepend(#strings.append(entry.key, #strings.repeat(' ', 10)), ' ')}"
|
||||
class="todo-category-header"/>
|
||||
<table id="todo-item-table">
|
||||
<tr class="todo-item" th:each="todo : ${entry.value}">
|
||||
<td>
|
||||
<form method="POST" th:action="@{/todo/toggle}" enctype="multipart/form-data">
|
||||
<input type="checkbox" th:checked="${todo.done}" name="done" onClick="this.form.submit()" /> <!-- fucking javascript -->
|
||||
<input type="hidden" name="id" th:value="${todo.id}" class="display-none"/>
|
||||
</form>
|
||||
</td>
|
||||
<td th:text="${todo.text}"/>
|
||||
<td th:text="${todo.due}"/>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div id="todo-widget" class="widget" th:fragment="todo-overview">
|
||||
<p class="widget-heading" th:text="#{nbscloud.todo.widget.heading}"/>
|
||||
<table id="todo-widget-table">
|
||||
<tbody>
|
||||
<tr th:each="todo : ${todos}">
|
||||
<td th:text="${todo.category}"/>
|
||||
<td th:text="${todo.text}"/>
|
||||
<td th:text="${todo.due}"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,54 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
workspace_loc=$1
|
||||
echo "$workspace_loc"
|
||||
version=$1
|
||||
|
||||
pwd
|
||||
echo "Create required directories"
|
||||
mkdir -p ~/.local/share/java/nbscloud/
|
||||
mkdir -p ~/.config/nbscloud/
|
||||
mkdir -p ~/nbscloud
|
||||
|
||||
deploy_tmp="deploy_tmp"
|
||||
target_loc="/opt/nbscloud"
|
||||
echo "Download nbscloud version $version to ~/.local/share/java/nbscloud/nbscloud.jar"
|
||||
|
||||
app_props_loc="$workspace_loc/web-container/target/classes/config/application.properties"
|
||||
files_prop_loc="$workspace_loc/files/target/classes/config/files-application.properties"
|
||||
wget -qO ~/.local/share/java/nbscloud/nbscloud.jar http://192.168.10.4:8100/releases/de/77zzcx7/nbs-cloud/web-container/"$version"/web-container-"$version".jar
|
||||
|
||||
echo "$app_props_loc"
|
||||
echo "$files_prop_loc"
|
||||
|
||||
user=$(grep "nbscloud.deploy.service.user" "$app_props_loc" | cut -d'=' -f2)
|
||||
echo "$user"
|
||||
|
||||
deploy_path=$(grep "nbscloud.deploy.path" "$app_props_loc" | cut -d'=' -f2)
|
||||
echo "$deploy_path"
|
||||
|
||||
base_dir=$(grep "nbs-cloud.files.baseDir" "$files_prop_loc" | cut -d'=' -f2)
|
||||
echo "$base_dir"
|
||||
|
||||
cd tools
|
||||
mkdir $deploy_tmp
|
||||
|
||||
cp "template-nbscloud.service" $deploy_tmp"/nbscloud.service"
|
||||
|
||||
cd $deploy_tmp
|
||||
|
||||
sed -i "s|USER_R|$user|g" nbscloud.service
|
||||
sed -i "s|DEPLOY_PATH_R|$deploy_path|g" nbscloud.service
|
||||
sed -i "s|BASE_DIR_R|$base_dir|g" nbscloud.service
|
||||
|
||||
echo ""
|
||||
cat nbscloud.service
|
||||
echo ""
|
||||
|
||||
cp nbscloud.service $target_loc"/nbscloud.service"
|
||||
echo "Copied service file"
|
||||
|
||||
jar_path=$(find "$workspace_loc/web-container/target/" -name "*.jar")
|
||||
echo "$jar_path"
|
||||
|
||||
cp "$jar_path" "$target_loc"/nbscloud.jar
|
||||
echo "Copied jar"
|
||||
|
||||
echo "Finished deployment"
|
||||
echo ""
|
||||
echo "Manually reload service files via 'systemctl --user daemon-reload'"
|
||||
echo "Then, enable the service 'systemctl --user enable --now nbscloud.service'"
|
||||
echo "Or, restart the service 'systemctl --user restart nbscloud.service'"
|
||||
echo "Reload and restart service"
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart nbscloud
|
||||
@@ -1,30 +1,19 @@
|
||||
[unit]
|
||||
[Unit]
|
||||
Description=NoBullShit-cloud - A personal cloud without bullshit
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=true
|
||||
|
||||
# Values substituted from application.properties
|
||||
User=USER_R
|
||||
Group=USER_R
|
||||
ExecStart=java -jar DEPLOY_PATH_R/nbscloud.jar
|
||||
ReadWritePaths=BASE_DIR_R
|
||||
ExecStart=java -jar %h/.local/share/java/nbscloud/nbscloud.jar --spring.config.additional-location="file://%h/.config/nbscloud/application.properties"
|
||||
ReadWritePaths=%h/nbscloud
|
||||
ReadOnlyPaths=%h/.local/share/java/nbscloud/nbscloud.jar
|
||||
ReadOnlyPaths=%h/.config/nbscloud/application.properties
|
||||
|
||||
# Hardening
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
NoNewPrivileges=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
PrivateTmp=true
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>web-container-config</artifactId>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.nbscloud.webcontainer.shared.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ControllerUtils {
|
||||
|
||||
public static LocalDate parseDate(String date) {
|
||||
// The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
|
||||
return Optional.ofNullable(date)
|
||||
.map(ed -> ed.isBlank() ? null : ed)
|
||||
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>web-container-registry</artifactId>
|
||||
@@ -16,5 +16,10 @@
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.nbscloud.webcontainer;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@SessionScope
|
||||
public class MessageHelper {
|
||||
// We have to temporarily store messages as we redirect: in some methods
|
||||
// so everything we add to the model will be gone, that's why we store messages
|
||||
// temporarily in here
|
||||
private final List<String> errors = new ArrayList<>(); // not resolved against a bundle
|
||||
private final List<String> resolvableErrors = new ArrayList<>(); // messages that are resolved against a bundle
|
||||
private final List<String> shareInfo = new ArrayList<>();
|
||||
private final List<String> infoMessages = new ArrayList<>();
|
||||
|
||||
public void addAndClearAll(Model model) {
|
||||
model.addAttribute("errors", getAndClear(this.errors));
|
||||
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
||||
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
||||
model.addAttribute("resolvableErrors", getAndClear(this.resolvableErrors));
|
||||
}
|
||||
|
||||
private static List<String> getAndClear(List<String> source) {
|
||||
final List<String> retList = new ArrayList<>(source);
|
||||
|
||||
source.clear();
|
||||
|
||||
return retList;
|
||||
}
|
||||
|
||||
public void addError(String message) {
|
||||
this.errors.add(message);
|
||||
}
|
||||
|
||||
public void addResolvableError(String message) {
|
||||
this.resolvableErrors.add(message);
|
||||
}
|
||||
|
||||
public void addShare(String message) {
|
||||
this.shareInfo.add(message);
|
||||
}
|
||||
|
||||
public void addInfo(String message) {
|
||||
this.infoMessages.add(message);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.nbscloud.webcontainer.registry;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
public interface App {
|
||||
// New app:
|
||||
// 1) Create module
|
||||
@@ -15,4 +18,8 @@ public interface App {
|
||||
String getStartPath();
|
||||
|
||||
int getIndex();
|
||||
|
||||
default Collection<Widget> getWidgets() {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.nbscloud.webcontainer.registry;
|
||||
|
||||
public interface Widget {
|
||||
String getPath();
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>nbs-cloud-aggregator</artifactId>
|
||||
<version>8</version>
|
||||
<version>26-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
@@ -74,6 +74,10 @@
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Misc -->
|
||||
<dependency>
|
||||
|
||||
@@ -2,10 +2,21 @@ package de.nbscloud.webcontainer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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.messages.basename=i18n/container_messages,i18n/files_messages,i18n/dashboard_messages,i18n/notes_messages,i18n/todo_messages
|
||||
|
||||
spring.servlet.multipart.max-file-size=-1
|
||||
spring.servlet.multipart.max-request-size=-1
|
||||
@@ -20,6 +20,4 @@ logging.level.de.nbscloud=DEBUG
|
||||
|
||||
server.servlet.context-path=/nbscloud
|
||||
server.port=9966
|
||||
|
||||
nbscloud.deploy.service.user=marius
|
||||
nbscloud.deploy.path=/opt/nbscloud
|
||||
server.address=localhost
|
||||
BIN
web-container/src/main/resources/favicon.ico
Normal file
BIN
web-container/src/main/resources/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,2 +1,41 @@
|
||||
v19:
|
||||
- #22 Fix a bug with password protected shares
|
||||
- Add Apache httpd config example
|
||||
- #2 Add folder sharing
|
||||
|
||||
v18:
|
||||
- #22 Password protected shares
|
||||
- Basic Note app implementation
|
||||
- Files app now offers API for other apps to store files
|
||||
|
||||
v17:
|
||||
- Disk usage widget
|
||||
- Pending updates widget
|
||||
- Network interfaces widget
|
||||
|
||||
v14 -> v15:
|
||||
- Same as v14, but the release build of that failed
|
||||
|
||||
v13 -> v14:
|
||||
- #19 Release & deployment v2
|
||||
|
||||
v12 - v13:
|
||||
- #18 Add profiles
|
||||
|
||||
v11 - v12:
|
||||
- #18 Add profiles
|
||||
|
||||
v10 - v11:
|
||||
- #18 Add profiles
|
||||
|
||||
v9 -> v10:
|
||||
- #18 Add profiles
|
||||
|
||||
v2..v9:
|
||||
- Test releases
|
||||
- #8 Add Jenkins jobs
|
||||
- #9 Add systemd user service
|
||||
- #15 Make releasing easier
|
||||
|
||||
v1:
|
||||
- Initial
|
||||
@@ -3,6 +3,7 @@
|
||||
--error-color: #D30000;
|
||||
--text-color: #7f7f7f;
|
||||
--background-color: #1d1f21;
|
||||
--background-color-highlight: #1d2121;
|
||||
--link-color: #87ab63;
|
||||
--hover-color: #1f1f2f;
|
||||
--border-color: #7f7f7f;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
--error-color: #D30000;
|
||||
--text-color: #000000;
|
||||
--background-color: #FFFFFF;
|
||||
--background-color-highlight: darkgrey;
|
||||
--link-color: #0000EE;
|
||||
--hover-color: lightgrey;
|
||||
--border-color: #ddd;
|
||||
|
||||
@@ -92,4 +92,107 @@ tr:hover {
|
||||
display: inline;
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#menu-container {
|
||||
padding-top: 2em;
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
background: none !important;
|
||||
border: none;
|
||||
padding: 0 !important;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.menu-container > details > summary {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.menu-container > details[open] > summary {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.menu-icon:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.menu-spacer {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
#menu-container {
|
||||
padding-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.menu-spacer {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#menu-container > div {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
#content-container {
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.menu-modal-content {
|
||||
padding: 0.5em;
|
||||
pointer-events: all;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.menu-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;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<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 th:if="${restricted == null || restricted == false}">
|
||||
<div th:each="app : ${apps}" class="menu-entry">
|
||||
<a th:href="@{/} + ${app.startPath}" th:text="${app.id}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div id="messages-container" th:fragment="messages">
|
||||
<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="error : ${resolvableErrors}">
|
||||
<div th:if="${!resolvableErrors.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>
|
||||
Reference in New Issue
Block a user