From 1c087fae322a1b07bb7bd554ee10ff473c47c727 Mon Sep 17 00:00:00 2001
From: Fin Reinhard <fin.reinhard@icloud.com>
Date: Tue, 22 Jan 2019 20:51:03 +0000
Subject: [PATCH] Merge branch 'master' into feature/15-archive-view-url
---
borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx | 24
borgbutler-webapp/src/components/views/jobs/Job.jsx | 36
borgbutler-core/build.gradle | 4
borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java | 8
borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java | 50 +
borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java | 12
borgbutler-server/src/test/java/de/micromata/borgbutler/server/BorgInstallationTest.java | 38 +
borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java | 14
borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java | 32
borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobQueue.java | 8
borgbutler-webapp/src/components/views/repos/ConfigureRepoPage.jsx | 252 +++++++
borgbutler-core/src/test/java/de/micromata/borgbutler/cache/ArchiveFilelistCacheTest.java | 2
borgbutler-server/build.gradle | 2
borgbutler-webapp/src/components/general/forms/FormRadioButton.jsx | 62 +
borgbutler-webapp/src/components/views/repos/RepoCard.jsx | 2
borgbutler-webapp/src/containers/WebApp.jsx | 23
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java | 6
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java | 34 +
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/queue/JsonJob.java | 12
borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgVersion.java | 54 +
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java | 44 +
borgbutler-core/src/main/java/de/micromata/borgbutler/BorgQueueExecutor.java | 5
borgbutler-webapp/src/components/views/repos/RepoListView.jsx | 21
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java | 84 ++
borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java | 4
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java | 6
borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractJob.java | 32
borgbutler-core/src/main/java/de/micromata/borgbutler/config/Configuration.java | 17
borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java | 9
borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgInstallation.java | 196 +++++
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java | 16
borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx | 97 +-
borgbutler-webapp/src/components/views/repos/RepoConfigPanel.jsx | 153 ++++
borgbutler-webapp/src/components/views/archives/ArchiveView.jsx | 4
borgbutler-webapp/src/components/general/forms/FormSelect.jsx | 9
borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx | 202 +++--
borgbutler-webapp/src/components/general/forms/FormComponents.jsx | 20
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java | 18
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java | 74 +-
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java | 25
borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java | 23
borgbutler-webapp/src/components/views/jobs/JobMonitorPanel.jsx | 102 ++
borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractCommandLineJob.java | 6
borgbutler-server/src/main/resources/log4j.properties | 2
borgbutler-core/src/main/java/de/micromata/borgbutler/cache/ButlerCache.java | 10
borgbutler-webapp/src/components/views/jobs/JobQueue.jsx | 58 +
borgbutler-webapp/src/components/views/jobs/Job.css | 8
borgbutler-webapp/src/css/my-style.css | 17
48 files changed, 1,641 insertions(+), 296 deletions(-)
diff --git a/borgbutler-core/build.gradle b/borgbutler-core/build.gradle
index a659115..6e10b34 100644
--- a/borgbutler-core/build.gradle
+++ b/borgbutler-core/build.gradle
@@ -16,8 +16,8 @@
compile group: 'com.esotericsoftware', name: 'kryo', version: '5.0.0-RC1'
// Serialization (faster than Java built-in)
- compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'
- compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6'
+ compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
+ compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.8'
compileOnly "org.projectlombok:lombok:1.18.4"
testCompileOnly "org.projectlombok:lombok:1.18.4"
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java
index f9ed76e..5923dc2 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java
@@ -10,6 +10,7 @@
import de.micromata.borgbutler.utils.DateUtils;
import de.micromata.borgbutler.utils.ReplaceUtils;
import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,16 +36,56 @@
BorgCommand command = new BorgCommand()
.setParams("--version")
.setDescription("Getting borg version.");
- JobResult<String> jobResult = getResult(command);
+
+ BorgJob<String> job = new BorgJob<>(command);
+ JobResult<String> jobResult = job.execute();
if (jobResult == null || jobResult.getStatus() != JobResult.Status.OK) {
return null;
}
- String version = jobResult.getResultObject();
+ String origVersion = jobResult.getResultObject();
+ String version = origVersion;
+ String[] strs = StringUtils.split(origVersion);
+ if (strs != null) {
+ if (!StringUtils.containsIgnoreCase(origVersion, "borg")) {
+ version = "";
+ } else {
+ version = strs[strs.length - 1]; // Work arround: borg returns "borg-macosx64 1.1.8" as version string (command is included).
+ }
+ }
+ if (version.length() == 0 || !Character.isDigit(version.charAt(0))) {
+ log.error("Version string returned by '" + job.getCommandLineAsString() + "' not as expected (not borg?): " + origVersion);
+ return null;
+ }
log.info("Borg version: " + version);
return version;
}
/**
+ * Executes borg init repository.
+ *
+ * @param repoConfig The configuration of the repo config (only repo is required).
+ * @param encryption The encryption value (repokey, repokey-blake2, none, ...).
+ * @return true, if no errors occured, otherwise false.
+ */
+ public static boolean init(BorgRepoConfig repoConfig, String encryption) {
+ BorgCommand command = new BorgCommand()
+ .setRepoConfig(repoConfig)
+ .setCommand("init")
+ //.setParams("--json") // --progress has no effect.
+ .setDescription("Init new repo '" + repoConfig.getDisplayName() + "'.");
+ JobResult<String> jobResult = getResult(command);
+ String result = jobResult != null ? jobResult.getResultObject() : null;
+ // If everything is ok, now String will be returned, result must be blank:
+ if (jobResult == null || jobResult.getStatus() != JobResult.Status.OK || StringUtils.isNotBlank(result)) {
+ log.error("Error while trying to intialize repo '" + repoConfig.getRepo() + "': " + result);
+ return false;
+ }
+ log.error("Error while trying to intialize repo '" + repoConfig.getRepo() + "': " + result);
+ return false;
+ }
+
+
+ /**
* Executes borg info repository.
*
* @param repoConfig
@@ -176,8 +217,9 @@
// The returned job might be an already queued or running one!
final ProgressInfo progressInfo = new ProgressInfo()
.setMessage("Getting file list...")
- .setCurrent(0)
- .setTotal(archive.getStats().getNfiles());
+ .setCurrent(0);
+ if (archive.getStats() != null) // Occurs only for demo repos.
+ progressInfo.setTotal(archive.getStats().getNfiles());
BorgJob<List<BorgFilesystemItem>> job = BorgQueueExecutor.getInstance().execute(new BorgJob<List<BorgFilesystemItem>>(command) {
@Override
public void processStdOutLine(String line, int level) {
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java
index 221d7f8..da71b43 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java
@@ -52,7 +52,12 @@
if (command == null) {
return null;
}
- CommandLine commandLine = new CommandLine(ConfigurationHandler.getConfiguration().getBorgCommand());
+ String borgCommand = ConfigurationHandler.getConfiguration().getBorgCommand();
+ if (StringUtils.isBlank(borgCommand)) {
+ log.error("Can't run empty borg command.");
+ return null;
+ }
+ CommandLine commandLine = new CommandLine(borgCommand);
commandLine.addArgument(command.getCommand());
if (command.getParams() != null) {
for (String param : command.getParams()) {
@@ -101,7 +106,7 @@
@Override
public JobResult<String> execute() {
- if (DemoRepos.isDemo(command.getRepoConfig().getRepo())) {
+ if (command.getRepoConfig() != null && DemoRepos.isDemo(command.getRepoConfig().getRepo())) {
return DemoRepos.execute(this);
}
return super.execute();
@@ -125,6 +130,9 @@
if (progressInfo != null) {
clone.setProgressInfo(progressInfo.clone());
}
+ clone.setCreateTime(getCreateTime());
+ clone.setStartTime(getStartTime());
+ clone.setStopTime(getStopTime());
return clone;
}
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgQueueExecutor.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgQueueExecutor.java
index 86f2743..ac0eb44 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgQueueExecutor.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgQueueExecutor.java
@@ -78,16 +78,17 @@
* For displaying purposes.
*
* @param repoConfig
+ * @param oldJobs If false, the running and queued jobs are returned, otherwise the done ones.
* @return A list of all jobs of the queue (as copies).
*/
- public List<BorgJob<?>> getJobListCopy(BorgRepoConfig repoConfig) {
+ public List<BorgJob<?>> getJobListCopy(BorgRepoConfig repoConfig, boolean oldJobs) {
JobQueue<String> origQueue = getQueue(repoConfig);
List<BorgJob<?>> jobList = new ArrayList<>();
if (origQueue == null) {
return jobList;
}
synchronized (origQueue) {
- Iterator<AbstractJob<String>> it = origQueue.getQueueIterator();
+ Iterator<AbstractJob<String>> it = oldJobs ? origQueue.getOldJobsIterator() : origQueue.getQueueIterator();
while (it.hasNext()) {
AbstractJob<String> origJob = it.next();
if (!(origJob instanceof BorgJob)) {
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/cache/ButlerCache.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/cache/ButlerCache.java
index 2c45d9c..a25e288 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/cache/ButlerCache.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/cache/ButlerCache.java
@@ -124,9 +124,15 @@
this.repoCacheAccess.clear();
}
+ public void clearRepoCacheAccess(String repo) {
+ if (this.repoCacheAccess.get(repo) != null) {
+ log.info("Clearing repository cache '" + repo + "'...");
+ this.repoCacheAccess.remove(repo);
+ }
+ }
+
public void clearRepoCacheAccess(Repository repository) {
- log.info("Clearing repository cache '" + repository.getName() + "'...");
- this.repoCacheAccess.remove(repository.getName());
+ clearRepoCacheAccess(repository.getName());
}
/**
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java
index 0ffeecd..52ac43a 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/BorgRepoConfig.java
@@ -1,6 +1,5 @@
package de.micromata.borgbutler.config;
-import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
@@ -29,7 +28,6 @@
private String passwordCommand;
@Getter
@Setter
- @JsonIgnore
private String id;
public String[] getEnvironmentVariables() {
@@ -51,4 +49,11 @@
if (StringUtils.isBlank(value)) return;
list.add(variable + "=" + value);
}
+
+ public void copyFrom(BorgRepoConfig other) {
+ this.displayName = other.displayName;
+ this.repo = other.repo;
+ this.passphrase = other.passphrase;
+ this.passwordCommand = other.passwordCommand;
+ }
}
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/Configuration.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/Configuration.java
index d06e30b..5ba55a6 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/Configuration.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/Configuration.java
@@ -24,9 +24,13 @@
@JsonIgnore
@Setter(AccessLevel.PACKAGE)
private File workingDir;
-
+ /**
+ * The path of the borg command to use.
+ */
@Getter
- private String borgCommand = "borg";
+ @Setter
+ private String borgCommand;
+
/**
* Default is 100 MB (approximately).
*/
@@ -85,13 +89,8 @@
}
public List<BorgRepoConfig> getRepoConfigs() {
- if (!ConfigurationHandler.getConfiguration().isShowDemoRepos()) {
- return repoConfigs;
- }
- List<BorgRepoConfig> result = new ArrayList<>();
- result.addAll(repoConfigs);
- DemoRepos.addDemoRepos(result);
- return result;
+ DemoRepos.handleDemoRepos(repoConfigs);
+ return repoConfigs;
}
List<BorgRepoConfig> _getRepoConfigs() {
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java
index cc0bd61..7ba4fa5 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java
@@ -18,9 +18,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Scanner;
+import java.util.*;
public class DemoRepos {
private enum Type {FAST, SLOW, VERY_SLOW}
@@ -36,13 +34,37 @@
*
* @param repositoryList
*/
- public static void addDemoRepos(List<BorgRepoConfig> repositoryList) {
+ public static void handleDemoRepos(List<BorgRepoConfig> repositoryList) {
if (!ConfigurationHandler.getConfiguration().isShowDemoRepos()) {
+ // Remove any demo repository if exist due to former settings:
+ Iterator<BorgRepoConfig> it = repositoryList.iterator();
+ while(it.hasNext()) {
+ BorgRepoConfig repoConfig = it.next();
+ if (!StringUtils.startsWith(repoConfig.getRepo(), DEMO_IDENTIFIER)) {
+ continue;
+ }
+ it.remove();
+ }
return;
}
init();
for (BorgRepoConfig repo : demoRepos) {
- repositoryList.add(repo);
+ if (!repositoryList.contains(repo))
+ repositoryList.add(repo);
+ }
+ // Remove duplicate entries (produced by former versions of BorgButler:
+ Set<String> set = new HashSet<>();
+ Iterator<BorgRepoConfig> it = repositoryList.iterator();
+ while(it.hasNext()) {
+ BorgRepoConfig repoConfig = it.next();
+ if (!StringUtils.startsWith(repoConfig.getRepo(), DEMO_IDENTIFIER)) {
+ continue;
+ }
+ if (set.contains(repoConfig.getRepo())) {
+ it.remove();
+ } else {
+ set.add(repoConfig.getRepo());
+ }
}
}
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractCommandLineJob.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractCommandLineJob.java
index 0c800e6..fa2c938 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractCommandLineJob.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractCommandLineJob.java
@@ -52,6 +52,9 @@
if (commandLine == null) {
commandLine = buildCommandLine();
}
+ if (commandLine == null) {
+ return null;
+ }
if (commandLineAsString == null) {
commandLineAsString = commandLine.getExecutable() + " " + StringUtils.join(commandLine.getArguments(), " ");
}
@@ -61,6 +64,9 @@
@Override
public JobResult<String> execute() {
getCommandLineAsString();
+ if (commandLine == null) {
+ return null;
+ }
DefaultExecutor executor = new DefaultExecutor();
if (workingDirectory != null) {
executor.setWorkingDirectory(workingDirectory);
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractJob.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractJob.java
index df97791..fc8d054 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractJob.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractJob.java
@@ -1,11 +1,13 @@
package de.micromata.borgbutler.jobs;
+import de.micromata.borgbutler.utils.DateUtils;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.time.LocalDateTime;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -18,7 +20,6 @@
@Setter
private boolean cancellationRequested;
@Getter
- @Setter(AccessLevel.PROTECTED)
private Status status;
@Getter
@Setter
@@ -29,17 +30,36 @@
@Getter
@Setter(AccessLevel.PROTECTED)
private long uniqueJobNumber = -1;
+ @Getter
+ @Setter
+ private String createTime;
+ @Getter
+ @Setter
+ private String startTime;
+ @Getter
+ @Setter
+ private String stopTime;
+
+ protected AbstractJob<T> setStatus(Status status) {
+ if (status == Status.RUNNING && this.status != Status.RUNNING) {
+ this.startTime = DateUtils.format(LocalDateTime.now());
+ } else if (status != Status.RUNNING && this.status == Status.RUNNING) {
+ this.stopTime = DateUtils.format(LocalDateTime.now());
+ }
+ this.status = status;
+ return this;
+ }
public void cancel() {
if (this.getStatus() == Status.QUEUED) {
- this.status = Status.CANCELLED;
+ setStatus(Status.CANCELLED);
}
this.cancellationRequested = true;
cancelRunningProcess();
}
protected void setCancelled() {
- this.status = Status.CANCELLED;
+ setStatus(Status.CANCELLED);
}
/**
@@ -74,7 +94,7 @@
if (this.status != Status.RUNNING) {
logger.error("Internal error, illegal state! You shouldn't set the job status to FAILED if not in status RUNNING: " + this.status);
}
- this.status = Status.FAILED;
+ setStatus(Status.FAILED);
}
/**
@@ -96,4 +116,8 @@
* @return
*/
public abstract Object getId();
+
+ protected AbstractJob() {
+ this.createTime = DateUtils.format(LocalDateTime.now());
+ }
}
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobQueue.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobQueue.java
index e040676..439acff 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobQueue.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobQueue.java
@@ -9,7 +9,7 @@
import java.util.concurrent.Executors;
public class JobQueue<T> {
- private static final int MAX_OLD_JOBS_SIZE = 50;
+ private static final int MAX_OLD_JOBS_SIZE = 10;
private static long jobSequence = 0;
private Logger log = LoggerFactory.getLogger(JobQueue.class);
private List<AbstractJob<T>> queue = new ArrayList<>();
@@ -43,6 +43,12 @@
}
}
+ public Iterator<AbstractJob<T>> getOldJobsIterator() {
+ synchronized (oldJobs) {
+ return Collections.unmodifiableList(oldJobs).iterator();
+ }
+ }
+
/**
* Searches only for queued jobs (not done jobs).
*
diff --git a/borgbutler-core/src/test/java/de/micromata/borgbutler/cache/ArchiveFilelistCacheTest.java b/borgbutler-core/src/test/java/de/micromata/borgbutler/cache/ArchiveFilelistCacheTest.java
index 7568e77..1397be9 100644
--- a/borgbutler-core/src/test/java/de/micromata/borgbutler/cache/ArchiveFilelistCacheTest.java
+++ b/borgbutler-core/src/test/java/de/micromata/borgbutler/cache/ArchiveFilelistCacheTest.java
@@ -63,7 +63,7 @@
@Test
void cleanUpMaximumSizeTest() throws Exception {
List<BorgFilesystemItem> list = createList(1000000);
- ArchiveFilelistCache cache = new ArchiveFilelistCache(new File("out"), 3);
+ ArchiveFilelistCache cache = new ArchiveFilelistCache(new File("out"), 5);
cache.removeAllCacheFiles();
BorgRepoConfig repoConfig = new BorgRepoConfig();
repoConfig.setRepo("repo");
diff --git a/borgbutler-server/build.gradle b/borgbutler-server/build.gradle
index 1cf9b43..99af8c8 100644
--- a/borgbutler-server/build.gradle
+++ b/borgbutler-server/build.gradle
@@ -24,8 +24,6 @@
compile group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.27'
compile group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.27'
compile group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.27'
- compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'
- compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6'
compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
compile group: 'javax.xml.ws', name: 'jaxws-api', version: '2.3.1'
compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgInstallation.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgInstallation.java
new file mode 100644
index 0000000..4f8196d
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgInstallation.java
@@ -0,0 +1,196 @@
+package de.micromata.borgbutler.server;
+
+import de.micromata.borgbutler.BorgCommands;
+import de.micromata.borgbutler.config.Configuration;
+import de.micromata.borgbutler.config.ConfigurationHandler;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.config.CookieSpecs;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+
+public class BorgInstallation {
+ private Logger log = LoggerFactory.getLogger(BorgInstallation.class);
+ private static final BorgInstallation instance = new BorgInstallation();
+
+ public static BorgInstallation getInstance() {
+ return instance;
+ }
+
+ public void initialize() {
+ Configuration configuration = ConfigurationHandler.getConfiguration();
+ if (StringUtils.isNotBlank(configuration.getBorgCommand())) {
+ if (version(configuration)) {
+ return;
+ }
+ }
+ initialize(getBinary(RunningMode.getOSType()));
+ if (version(configuration)) {
+ return;
+ }
+ BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion();
+ log.warn("No working borg version found. Please configure a borg version with minimal version '" + borgVersion.getMinimumRequiredBorgVersion() + "'.");
+ }
+
+ /**
+ * Configures a new borg configuration if modifications was done.
+ * @param oldVersion The old version before this current modification.
+ */
+ public void configure(BorgVersion oldVersion) {
+ Configuration configuration = ConfigurationHandler.getConfiguration();
+ BorgVersion newVersion = ServerConfiguration.get()._getBorgVersion();
+ boolean borgBinaryChanged = !StringUtils.equals(oldVersion.getBorgBinary(), newVersion.getBorgBinary());
+ boolean manualBorgCommand = "manual".equals(newVersion.getBorgBinary());
+ if (manualBorgCommand) {
+ boolean borgCommandChanged = !StringUtils.equals(oldVersion.getBorgCommand(), newVersion.getBorgCommand());
+ if (borgCommandChanged) {
+ configuration.setBorgCommand(newVersion.getBorgCommand());
+ version(configuration);
+ } else {
+ newVersion.copyFrom(oldVersion); // Restore all old settings.
+ }
+ } else {
+ if (borgBinaryChanged) {
+ newVersion.setBorgCommand(oldVersion.getBorgCommand()); // Don't modify borg command, if not manual.
+ initialize(getBinary(newVersion.getBorgBinary()));
+ } else {
+ newVersion.copyFrom(oldVersion); // Restore all old settings.
+ }
+ }
+ }
+
+ private boolean initialize(String[] binary) {
+ if (binary == null) {
+ return false;
+ }
+ Configuration configuration = ConfigurationHandler.getConfiguration();
+ File file = download(binary);
+ BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion();
+ if (file != null) {
+ configuration.setBorgCommand(file.getAbsolutePath());
+ borgVersion.setBorgCommand(file.getAbsolutePath());
+ borgVersion.setBorgBinary(binary[0]);
+ }
+ return version(configuration);
+ }
+
+ private boolean version(Configuration configuration) {
+ String versionString = BorgCommands.version();
+ boolean versionOK = false;
+ BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion();
+ String msg = null;
+ if (versionString != null) {
+ borgVersion.setVersion(versionString);
+ int cmp = versionString.compareTo(borgVersion.getMinimumRequiredBorgVersion());
+ if (cmp < 0) {
+ msg = "Found borg version '" + versionString + "' is less than minimum required version '" + borgVersion.getMinimumRequiredBorgVersion() + "'.";
+ log.info(msg);
+ } else {
+ versionOK = true;
+ msg = "Found borg '" + configuration.getBorgCommand() + "', version: " + versionString + " (equals to or newer than '" + borgVersion.getMinimumRequiredBorgVersion()
+ + "', OK).";
+ log.info(msg);
+ }
+ } else {
+ msg = "Couldn't execute borg command '" + configuration.getBorgCommand() + "'.";
+ }
+ borgVersion.setVersionOK(versionOK);
+ borgVersion.setStatusMessage(msg);
+ return versionOK;
+ }
+
+ private String[] getBinary(RunningMode.OSType osType) {
+ String os = null;
+ switch (osType) {
+ case MAC_OS:
+ os = "mac";
+ break;
+ case LINUX:
+ os = "linux64";
+ break;
+ case FREEBSD:
+ os = "freebsd64";
+ break;
+ }
+ return getBinary(os);
+ }
+
+ private String[] getBinary(String os) {
+ if (os == null) {
+ return null;
+ }
+ BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion();
+ for (String[] binary : borgVersion.getBorgBinaries()) {
+ if (binary[0].contains(os)) {
+ return binary;
+ }
+ }
+ return null;
+ }
+
+ File download(RunningMode.OSType osType) {
+ String[] binary = getBinary(osType);
+ if (binary == null) {
+ log.info("Can't download binary (no binary found for OS '" + osType + "'.");
+ return null;
+ }
+ return download(binary);
+ }
+
+ private File download(String[] binary) {
+ File file = getBinaryFile(binary);
+ if (file.exists()) {
+ // File already downloaded, nothing to do.
+ return file;
+ }
+ BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion();
+ String url = borgVersion.getBinariesDownloadUrl() + getDownloadFilename(binary);
+ log.info("Trying to download borg binary '" + binary[0] + "' (" + binary[1] + ") from url: " + url + "...");
+ HttpClientBuilder builder = HttpClients.custom()
+ .setDefaultRequestConfig(RequestConfig.custom()
+ .setCookieSpec(CookieSpecs.STANDARD).build());
+ try (CloseableHttpClient httpClient = builder.build()) {
+ HttpGet getRequest = new HttpGet(url);
+
+ HttpResponse response = httpClient.execute(getRequest);
+
+ if (response.getStatusLine().getStatusCode() != 200) {
+ throw new RuntimeException("Failed : HTTP error code : "
+ + response.getStatusLine().getStatusCode());
+ }
+ FileUtils.copyInputStreamToFile(response.getEntity().getContent(), file);
+ log.info("Downloaded: " + file.getAbsolutePath());
+ file.setExecutable(true, false);
+ return file;
+ } catch (IOException ex) {
+ log.error(ex.getMessage(), ex);
+ return null;
+ }
+ }
+
+ private File getBinaryFile(String[] binary) {
+ File dir = new File(ConfigurationHandler.getInstance().getWorkingDir(), "bin");
+ if (!dir.exists()) {
+ log.info("Creating binary directory: " + dir.getAbsolutePath());
+ dir.mkdirs();
+ }
+ BorgVersion borgVersion = ServerConfiguration.get()._getBorgVersion();
+ return new File(dir, getDownloadFilename(binary) + "-" + borgVersion.getBinariesDownloadVersion());
+ }
+
+ private String getDownloadFilename(String[] binary) {
+ return "borg-" + binary[0];
+ }
+
+ private BorgInstallation() {
+ }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgVersion.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgVersion.java
new file mode 100644
index 0000000..2f856d4
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/BorgVersion.java
@@ -0,0 +1,54 @@
+package de.micromata.borgbutler.server;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+
+public class BorgVersion {
+ @Getter
+ private String binariesDownloadVersion = "1.1.8";
+ @Getter
+ private String binariesDownloadUrl = "https://github.com/borgbackup/borg/releases/download/" + binariesDownloadVersion + "/";
+ @Getter
+ private String[][] borgBinaries = {
+ {"freebsd64", "FreeBSD 64"},
+ {"linux32", "Linux 32"},
+ {"linux64", "Linux 64"},
+ {"macosx64", "MacOS X 64"}};
+
+ @Getter
+ private String minimumRequiredBorgVersion = "1.1.8";
+
+ /**
+ * One of the values "macosx64", "linux64" etc. for using a binary provided by BorgButler or null / "manual" for
+ * using a manual installed borg version.
+ */
+ @Getter
+ @Setter(AccessLevel.PACKAGE)
+ private String borgBinary;
+ /**
+ * The path of the borg command to use.
+ */
+ @Getter
+ @Setter
+ private String borgCommand;
+
+ @Getter
+ @Setter(AccessLevel.PACKAGE)
+ private boolean versionOK = false;
+ @Getter
+ @Setter(AccessLevel.PACKAGE)
+ private String version;
+ @Getter
+ @Setter(AccessLevel.PACKAGE)
+ private String statusMessage;
+
+ public BorgVersion copyFrom(BorgVersion other) {
+ this.borgCommand = other.borgCommand;
+ this.borgBinary = other.borgBinary;
+ this.versionOK = other.versionOK;
+ this.version = other.version;
+ this.statusMessage = other.statusMessage;
+ return this;
+ }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
index 07f1b63..93f77ce 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
@@ -14,6 +14,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.awt.*;
import java.io.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -81,7 +82,11 @@
return;
}
}
- RunningMode.setServerType(RunningMode.ServerType.SERVER);
+ if (Desktop.isDesktopSupported()) {
+ RunningMode.setServerType(RunningMode.ServerType.DESKTOP);
+ } else {
+ RunningMode.setServerType(RunningMode.ServerType.SERVER);
+ }
RunningMode.logMode();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
@@ -91,6 +96,7 @@
});
JettyServer server = startUp();
+ BorgInstallation.getInstance().initialize();
if (!line.hasOption('q')) {
try {
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java
index a3940ef..3da4739 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java
@@ -16,7 +16,7 @@
public enum UserManagement {SINGLE}
- public enum OSType {MAC_OS, WINDOWS, LINUX, OTHER}
+ public enum OSType {MAC_OS, WINDOWS, LINUX, FREEBSD, OTHER}
private static boolean running;
private static File baseDir;
@@ -49,6 +49,8 @@
osType = OSType.WINDOWS;
} else if (osTypeString.toLowerCase().contains("linux")) {
osType = OSType.LINUX;
+ } else if (osTypeString.toLowerCase().contains("freebsd")) {
+ osType = OSType.FREEBSD;
} else {
osType = OSType.OTHER;
}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
index ce67240..8c91bf2 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
@@ -18,6 +18,7 @@
private int port = WEBSERVER_PORT_DEFAULT;
private boolean webDevelopmentMode = WEB_DEVELOPMENT_MODE_PREF_DEFAULT;
+ private BorgVersion borgVersion = new BorgVersion();
@JsonProperty
public String getCacheDir() {
return ButlerCache.getInstance().getCacheDir().getAbsolutePath();
@@ -31,6 +32,17 @@
return SUPPORTED_LANGUAGES;
}
+ /**
+ * @return a clone of this.borgVersion.
+ */
+ public BorgVersion getBorgVersion() {
+ return new BorgVersion().copyFrom(borgVersion);
+ }
+
+ BorgVersion _getBorgVersion() {
+ return this.borgVersion;
+ }
+
public static String getApplicationHome() {
if (applicationHome == null) {
applicationHome = System.getProperty("applicationHome");
@@ -65,5 +77,7 @@
super.copyFrom(other);
this.port = other.port;
this.webDevelopmentMode = other.webDevelopmentMode;
+ this.borgVersion.copyFrom(other.borgVersion);
+ this.setBorgCommand(this.borgVersion.getBorgCommand());
}
}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java
index 582715e..abf41ea 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java
@@ -33,16 +33,16 @@
public class ArchivesRest {
private static Logger log = LoggerFactory.getLogger(ArchivesRest.class);
- @GET
- @Produces(MediaType.APPLICATION_JSON)
/**
*
- * @param repo Name of repository ({@link Repository#getName()}.
+ * @param repoName Name of repository ({@link Repository#getName()}.
* @param archiveId Id or name of archive.
* @param prettyPrinter If true then the json output will be in pretty format.
* @return Repository (including list of archives) as json string.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
public String getArchive(@QueryParam("repo") String repoName,
@QueryParam("archiveId") String archiveId, @QueryParam("force") boolean force,
@QueryParam("prettyPrinter") boolean prettyPrinter) {
@@ -53,9 +53,6 @@
return JsonUtils.toJson(archive, prettyPrinter);
}
- @GET
- @Path("filelist")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param archiveId Id or name of archive.
@@ -69,6 +66,9 @@
* @return Repository (including list of archives) as json string.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("filelist")
+ @Produces(MediaType.APPLICATION_JSON)
public String getArchiveFileList(@QueryParam("archiveId") String archiveId,
@QueryParam("searchString") String searchString,
@QueryParam("mode") String mode,
@@ -105,13 +105,13 @@
return JsonUtils.toJson(items, prettyPrinter);
}
- @GET
- @Path("/restore")
- @Produces(MediaType.APPLICATION_OCTET_STREAM)
/**
* @param archiveId
* @param fileNumber The fileNumber of the file or directory in the archive served by BorgButler's
*/
+ @GET
+ @Path("/restore")
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response restore(@QueryParam("archiveId") String archiveId, @QueryParam("fileNumber") int fileNumber) {
log.info("Requesting file #" + fileNumber + " of archive '" + archiveId + "'.");
FileSystemFilter filter = new FileSystemFilter().setFileNumber(fileNumber);
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java
new file mode 100644
index 0000000..b2c0875
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/BorgRepoConfigsRest.java
@@ -0,0 +1,44 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.cache.ButlerCache;
+import de.micromata.borgbutler.config.BorgRepoConfig;
+import de.micromata.borgbutler.config.ConfigurationHandler;
+import de.micromata.borgbutler.json.JsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+
+@Path("/repoConfig")
+public class BorgRepoConfigsRest {
+ private static Logger log = LoggerFactory.getLogger(BorgRepoConfigsRest.class);
+
+ /**
+ * @param id id or name of repo.
+ * @param prettyPrinter If true then the json output will be in pretty format.
+ * @return {@link BorgRepoConfig} as json string.
+ * @see JsonUtils#toJson(Object, boolean)
+ */
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public String getRepoConfig(@QueryParam("id") String id, @QueryParam("prettyPrinter") boolean prettyPrinter) {
+ BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(id);
+ return JsonUtils.toJson(repoConfig, prettyPrinter);
+ }
+
+ @POST
+ @Produces(MediaType.TEXT_PLAIN)
+ public void setRepoConfig(String jsonConfig) {
+ BorgRepoConfig newRepoConfig = JsonUtils.fromJson(BorgRepoConfig.class, jsonConfig);
+ BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(newRepoConfig.getId());
+ if (repoConfig == null) {
+ log.error("Can't find repo config '" + newRepoConfig.getId() + "'. Can't save new settings.");
+ return;
+ }
+ ButlerCache.getInstance().clearRepoCacheAccess(repoConfig.getRepo());
+ ButlerCache.getInstance().clearRepoCacheAccess(newRepoConfig.getRepo());
+ repoConfig.copyFrom(newRepoConfig);
+ ConfigurationHandler.getInstance().save();
+ }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
index 06abb81..a0c5c08 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
@@ -3,6 +3,8 @@
import de.micromata.borgbutler.cache.ButlerCache;
import de.micromata.borgbutler.config.ConfigurationHandler;
import de.micromata.borgbutler.json.JsonUtils;
+import de.micromata.borgbutler.server.BorgInstallation;
+import de.micromata.borgbutler.server.BorgVersion;
import de.micromata.borgbutler.server.ServerConfiguration;
import de.micromata.borgbutler.server.user.UserData;
import de.micromata.borgbutler.server.user.UserManager;
@@ -17,14 +19,14 @@
public class ConfigurationRest {
private Logger log = LoggerFactory.getLogger(ConfigurationRest.class);
- @GET
- @Path("config")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param prettyPrinter If true then the json output will be in pretty format.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("config")
+ @Produces(MediaType.APPLICATION_JSON)
public String getConfig(@QueryParam("prettyPrinter") boolean prettyPrinter) {
String json = JsonUtils.toJson(ServerConfiguration.get(), prettyPrinter);
return json;
@@ -37,18 +39,20 @@
ConfigurationHandler configurationHandler = ConfigurationHandler.getInstance();
ServerConfiguration config = (ServerConfiguration)configurationHandler.getConfiguration();
ServerConfiguration srcConfig = JsonUtils.fromJson(ServerConfiguration.class, jsonConfig);
+ BorgVersion oldBorgVersion = config.getBorgVersion();
config.copyFrom(srcConfig);
+ BorgInstallation.getInstance().configure(oldBorgVersion);
configurationHandler.save();
}
- @GET
- @Path("user")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param prettyPrinter If true then the json output will be in pretty format.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("user")
+ @Produces(MediaType.APPLICATION_JSON)
public String getUser(@QueryParam("prettyPrinter") boolean prettyPrinter) {
UserData user = RestUtils.getUser();
String json = JsonUtils.toJson(user, prettyPrinter);
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java
new file mode 100644
index 0000000..150e913
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/FilesystemBrowserRest.java
@@ -0,0 +1,84 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.json.JsonUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.swing.*;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import java.io.File;
+
+@Path("/files")
+public class FilesystemBrowserRest {
+ private Logger log = LoggerFactory.getLogger(FilesystemBrowserRest.class);
+
+ /**
+ * Opens a directory browser or file browser on the desktop app and returns the chosen dir/file. Works only if Browser and Desktop app are running
+ * on the same host.
+ *
+ * @param current The current path of file. If not given the directory/file browser starts with the last used directory or user.home.
+ * @return The chosen directory path (absolute path).
+ */
+ @GET
+ @Path("/browse-local-filesystem")
+ @Produces(MediaType.APPLICATION_JSON)
+ public String browseLocalFilesystem(@Context HttpServletRequest requestContext, @QueryParam("current") String current) {
+ String msg = RestUtils.checkLocalDesktopAvailable(requestContext);
+ if (msg != null) {
+ log.info(msg);
+ return msg;
+ }
+ JFileChooser chooser;
+ if (StringUtils.isNotBlank(current)) {
+ chooser = new JFileChooser(current);
+ } else {
+ chooser = new JFileChooser();
+ }
+ chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ synchronized (FilesystemBrowserRest.class) {
+ if (frame == null) {
+ frame = new JFrame("BorgButler");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ JLabel label = new JLabel("Hello World");
+ frame.getContentPane().add(label);
+ frame.pack();
+ }
+ }
+ frame.setVisible(true);
+ frame.setAlwaysOnTop(true);
+ int returnCode = chooser.showDialog(frame, "Choose");
+ frame.setVisible(false);
+ frame.setAlwaysOnTop(false);
+ File file = null;
+ if (returnCode == JFileChooser.APPROVE_OPTION) {
+ file = chooser.getSelectedFile();
+ }
+ String filename = file != null ? JsonUtils.toJson(file.getAbsolutePath()) : "";
+ String result = "{\"directory\":" + filename + "}";
+ return result;
+ }
+
+ /**
+ * @return OK, if the local desktop services such as open file browser etc. are available.
+ */
+ @GET
+ @Path("/local-fileservices-available")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String browseLocalFilesystem(@Context HttpServletRequest requestContext) {
+ String msg = RestUtils.checkLocalDesktopAvailable(requestContext);
+ if (msg != null) {
+ log.info(msg);
+ return msg;
+ }
+ return "OK";
+ }
+
+ private static JFrame frame;
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
index 7e060d6..b17c855 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
@@ -20,9 +20,6 @@
public class I18nRest {
private Logger log = LoggerFactory.getLogger(I18nRest.class);
- @GET
- @Path("list")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param requestContext For detecting the user's client locale.
@@ -31,6 +28,9 @@
* @param prettyPrinter If true then the json output will be in pretty format.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("list")
+ @Produces(MediaType.APPLICATION_JSON)
public String getList(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter,
@QueryParam("keysOnly") boolean keysOnly, @QueryParam("locale") String locale) {
Locale localeObject;
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java
index 14f1525..be744f0 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/JobsRest.java
@@ -26,10 +26,8 @@
public class JobsRest {
private static Logger log = LoggerFactory.getLogger(JobsRest.class);
- private static List<JsonJobQueue> testList;
+ private static List<JsonJobQueue> testList, oldJobsTestList;
- @GET
- @Produces(MediaType.APPLICATION_JSON)
/**
* @param repo If given, only the job queue of the given repo will be returned.
* @param testMode If true, then a test job list is created.
@@ -37,12 +35,16 @@
* @return Job queues as json string.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
public String getJobs(@QueryParam("repo") String repo,
@QueryParam("testMode") boolean testMode,
+ @QueryParam("oldJobs") boolean oldJobs,
@QueryParam("prettyPrinter") boolean prettyPrinter) {
+ log.debug("getJobs repo=" + repo + ", oldJobs=" + oldJobs);
if (testMode) {
// Return dynamic test queue:
- return returnTestList(prettyPrinter);
+ return returnTestList(oldJobs, prettyPrinter);
}
boolean validRepo = false;
if (StringUtils.isNotBlank(repo) && !"null".equals(repo) && !"undefined".equals(repo)) {
@@ -51,13 +53,13 @@
BorgQueueExecutor borgQueueExecutor = BorgQueueExecutor.getInstance();
List<JsonJobQueue> queueList = new ArrayList<>();
if (validRepo) { // Get only the queue of the given repo:
- JsonJobQueue queue = getQueue(repo);
+ JsonJobQueue queue = getQueue(repo, oldJobs);
if (queue != null) {
queueList.add(queue);
}
} else { // Get all the queues (of all repos).
for (String rep : borgQueueExecutor.getRepos()) {
- JsonJobQueue queue = getQueue(rep);
+ JsonJobQueue queue = getQueue(rep, oldJobs);
if (queue != null) {
queueList.add(queue);
}
@@ -66,24 +68,13 @@
return JsonUtils.toJson(queueList, prettyPrinter);
}
- @GET
- @Produces(MediaType.APPLICATION_JSON)
- @Path("/statistics")
- /**
- * @return The total number of jobs queued or running (and other statistics): {@link de.micromata.borgbutler.BorgQueueStatistics}.
- * @see JsonUtils#toJson(Object, boolean)
- */
- public String getStatistics() {
- return JsonUtils.toJson(BorgQueueExecutor.getInstance().getStatistics());
- }
-
- private JsonJobQueue getQueue(String repo) {
+ private JsonJobQueue getQueue(String repo, boolean oldJobs) {
BorgQueueExecutor borgQueueExecutor = BorgQueueExecutor.getInstance();
BorgRepoConfig repoConfig = ConfigurationHandler.getConfiguration().getRepoConfig(repo);
if (repoConfig == null) {
return null;
}
- List<BorgJob<?>> borgJobList = borgQueueExecutor.getJobListCopy(repoConfig);
+ List<BorgJob<?>> borgJobList = borgQueueExecutor.getJobListCopy(repoConfig, oldJobs);
if (CollectionUtils.isEmpty(borgJobList))
return null;
JsonJobQueue queue = new JsonJobQueue().setRepo(repoConfig.getDisplayName());
@@ -95,11 +86,11 @@
return queue;
}
- @Path("/cancel")
- @GET
/**
* @param uniqueJobNumberString The id of the job to cancel.
*/
+ @Path("/cancel")
+ @GET
public void cancelJob(@QueryParam("uniqueJobNumber") String uniqueJobNumberString) {
Long uniqueJobNumber = null;
try {
@@ -114,28 +105,31 @@
/**
* Only for test purposes and development.
*
+ * @param oldJobs
* @param prettyPrinter
* @return
*/
- private String returnTestList(boolean prettyPrinter) {
- if (testList == null) {
- testList = new ArrayList<>();
+ private String returnTestList(boolean oldJobs, boolean prettyPrinter) {
+ List<JsonJobQueue> list = oldJobs ? oldJobsTestList : testList;
+ if (list == null) {
+ list = new ArrayList<>();
long uniqueJobNumber = 100000;
JsonJobQueue queue = new JsonJobQueue().setRepo("My Computer");
- addTestJob(queue, "info", "my-macbook", 0, 2342)
- .setUniqueJobNumber(uniqueJobNumber++);
- addTestJob(queue, "list", "my-macbook", -1, -1)
- .setUniqueJobNumber(uniqueJobNumber++);
- testList.add(queue);
+ addTestJob(queue, "info", "my-macbook", 0, 2342, uniqueJobNumber++, oldJobs);
+ addTestJob(queue, "list", "my-macbook", -1, -1, uniqueJobNumber++, oldJobs);
+ list.add(queue);
queue = new JsonJobQueue().setRepo("My Server");
- addTestJob(queue, "list", "my-server", 0, 1135821)
- .setUniqueJobNumber(uniqueJobNumber++);
- addTestJob(queue, "info", "my-server", -1, -1)
- .setUniqueJobNumber(uniqueJobNumber++);
- testList.add(queue);
- } else {
- for (JsonJobQueue jobQueue : testList) {
+ addTestJob(queue, "list", "my-server", 0, 1135821, uniqueJobNumber++, oldJobs);
+ addTestJob(queue, "info", "my-server", -1, -1, uniqueJobNumber++, oldJobs);
+ list.add(queue);
+ if (oldJobs) {
+ oldJobsTestList = list;
+ } else {
+ testList = list;
+ }
+ } else if (!oldJobs) {
+ for (JsonJobQueue jobQueue : list) {
for (JsonJob job : jobQueue.getJobs()) {
if (job.getStatus() != AbstractJob.Status.RUNNING) continue;
long current = job.getProgressInfo().getCurrent();
@@ -158,7 +152,7 @@
}
}
}
- return JsonUtils.toJson(testList, prettyPrinter);
+ return JsonUtils.toJson(list, prettyPrinter);
}
/**
@@ -171,7 +165,7 @@
* @param total
* @return
*/
- private JsonJob addTestJob(JsonJobQueue queue, String operation, String host, long current, long total) {
+ private JsonJob addTestJob(JsonJobQueue queue, String operation, String host, long current, long total, long uniqueNumber, boolean oldJobs) {
ProgressInfo progressInfo = new ProgressInfo()
.setCurrent(current)
.setTotal(total);
@@ -196,6 +190,10 @@
if (queue.getJobs() == null) {
queue.setJobs(new ArrayList<>());
}
+ job.setUniqueJobNumber(uniqueNumber);
+ if (oldJobs) {
+ job.setStatus(uniqueNumber % 2 == 0 ? AbstractJob.Status.CANCELLED : AbstractJob.Status.DONE);
+ }
queue.getJobs().add(job);
return job;
}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java
index 3152f1c..337dfb6 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ReposRest.java
@@ -19,15 +19,15 @@
public class ReposRest {
private static Logger log = LoggerFactory.getLogger(ReposRest.class);
- @GET
- @Path("list")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param prettyPrinter If true then the json output will be in pretty format.
* @return A list of repositories of type {@link BorgRepository}.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("list")
+ @Produces(MediaType.APPLICATION_JSON)
public String getList(@QueryParam("prettyPrinter") boolean prettyPrinter) {
List<Repository> repositories = ButlerCache.getInstance().getAllRepositories();
if (CollectionUtils.isEmpty(repositories)) {
@@ -36,32 +36,31 @@
return JsonUtils.toJson(repositories, prettyPrinter);
}
- @GET
- @Path("repo")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param id id or name of repo.
- * @param force If true, a reload of all repositories is forced.
* @param prettyPrinter If true then the json output will be in pretty format.
- * @return Repository (without list of archives) as json string.
+ * @return {@link Repository} (without list of archives) as json string.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("repo")
+ @Produces(MediaType.APPLICATION_JSON)
public String getRepo(@QueryParam("id") String id, @QueryParam("prettyPrinter") boolean prettyPrinter) {
Repository repository = ButlerCache.getInstance().getRepository(id);
return JsonUtils.toJson(repository, prettyPrinter);
}
- @GET
- @Path("repoArchiveList")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param id id or name of repo.
* @param prettyPrinter If true then the json output will be in pretty format.
- * @return Repository (including list of archives) as json string.
+ * @return {@link Repository} (including list of archives) as json string.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("repoArchiveList")
+ @Produces(MediaType.APPLICATION_JSON)
public String getRepoArchiveList(@QueryParam("id") String id, @QueryParam("force") boolean force,
@QueryParam("prettyPrinter") boolean prettyPrinter) {
if (force) {
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java
new file mode 100644
index 0000000..eaf6495
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfo.java
@@ -0,0 +1,25 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.BorgQueueStatistics;
+import de.micromata.borgbutler.server.BorgVersion;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Statistics of all the job queues, especially the number of total queued and running jobs.
+ * This is used e. g. by the client for showing a badge near to the menu entry "job monitor" with the number
+ * of Jobs in the queues.
+ */
+public class SystemInfo {
+ @Getter
+ @Setter
+ private BorgQueueStatistics queueStatistics;
+
+ @Getter
+ @Setter
+ private boolean configurationOK;
+
+ @Getter
+ @Setter
+ private BorgVersion borgVersion;
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java
new file mode 100644
index 0000000..ce585cf
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/SystemInfoRest.java
@@ -0,0 +1,34 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.BorgQueueExecutor;
+import de.micromata.borgbutler.json.JsonUtils;
+import de.micromata.borgbutler.server.BorgVersion;
+import de.micromata.borgbutler.server.ServerConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/system")
+public class SystemInfoRest {
+ private static Logger log = LoggerFactory.getLogger(SystemInfoRest.class);
+
+ /**
+ * @return The total number of jobs queued or running (and other statistics): {@link de.micromata.borgbutler.BorgQueueStatistics}.
+ * @see JsonUtils#toJson(Object, boolean)
+ */
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("info")
+ public String getStatistics() {
+ BorgVersion borgVersion = ServerConfiguration.get().getBorgVersion();
+ SystemInfo systemInfonfo = new SystemInfo()
+ .setQueueStatistics(BorgQueueExecutor.getInstance().getStatistics())
+ .setConfigurationOK(borgVersion.isVersionOK())
+ .setBorgVersion(borgVersion);
+ return JsonUtils.toJson(systemInfonfo);
+ }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
index efd277f..82f033f 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
@@ -22,15 +22,15 @@
public class VersionRest {
private Logger log = LoggerFactory.getLogger(VersionRest.class);
- @GET
- @Path("version")
- @Produces(MediaType.APPLICATION_JSON)
/**
*
* @param requestContext For detecting the user's client locale.
* @param prettyPrinter If true then the json output will be in pretty format.
* @see JsonUtils#toJson(Object, boolean)
*/
+ @GET
+ @Path("version")
+ @Produces(MediaType.APPLICATION_JSON)
public String getVersion(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter) {
UserData user = RestUtils.getUser();
String language = Languages.asString(user.getLocale());
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/queue/JsonJob.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/queue/JsonJob.java
index 119aa9a..0169269 100644
--- a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/queue/JsonJob.java
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/queue/JsonJob.java
@@ -38,6 +38,15 @@
@Getter
@Setter
private String[] environmentVariables;
+ @Getter
+ @Setter
+ private String createTime;
+ @Getter
+ @Setter
+ private String startTime;
+ @Getter
+ @Setter
+ private String stopTime;
public JsonJob() {
}
@@ -55,6 +64,9 @@
this.commandLineAsString = borgJob.getCommandLineAsString();
this.description = borgJob.getDescription();
environmentVariables = borgJob.getCommand().getRepoConfig().getEnvironmentVariables();
+ this.createTime = borgJob.getCreateTime();
+ this.startTime = borgJob.getStartTime();
+ this.stopTime = borgJob.getStopTime();
}
/**
diff --git a/borgbutler-server/src/main/resources/log4j.properties b/borgbutler-server/src/main/resources/log4j.properties
index 141c0c5..03a0e14 100644
--- a/borgbutler-server/src/main/resources/log4j.properties
+++ b/borgbutler-server/src/main/resources/log4j.properties
@@ -14,7 +14,7 @@
log4j.appender.file=org.apache.log4j.RollingFileAppender
-log4j.appender.file.File=merlin.log
+log4j.appender.file.File=borgbutler.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
diff --git a/borgbutler-server/src/test/java/de/micromata/borgbutler/server/BorgInstallationTest.java b/borgbutler-server/src/test/java/de/micromata/borgbutler/server/BorgInstallationTest.java
new file mode 100644
index 0000000..dc97e98
--- /dev/null
+++ b/borgbutler-server/src/test/java/de/micromata/borgbutler/server/BorgInstallationTest.java
@@ -0,0 +1,38 @@
+package de.micromata.borgbutler.server;
+
+import de.micromata.borgbutler.config.ConfigurationHandler;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class BorgInstallationTest {
+ private Logger log = LoggerFactory.getLogger(BorgInstallationTest.class);
+
+ @Test
+ void foo() throws Exception {
+ ConfigurationHandler.getConfiguration().setBorgCommand(null);
+ BorgInstallation borgInstallation = BorgInstallation.getInstance();
+ borgInstallation.initialize();
+ ConfigurationHandler.getConfiguration().setBorgCommand("borg");
+ borgInstallation.initialize();
+ }
+
+ @Test
+ void downloadTest() {
+ String version = new BorgVersion().getBinariesDownloadVersion();
+ checkDownload(RunningMode.OSType.LINUX, "borg-linux64-" + version);
+ checkDownload(RunningMode.OSType.MAC_OS, "borg-macosx64-" + version);
+ checkDownload(RunningMode.OSType.FREEBSD, "borg-freebsd64-" + version);
+ assertNull(BorgInstallation.getInstance().download(RunningMode.OSType.WINDOWS));
+ }
+
+ private void checkDownload(RunningMode.OSType osType, String expectedName) {
+ File file = BorgInstallation.getInstance().download(osType);
+ assertEquals(expectedName, file.getName());
+ assertTrue(file.canExecute());
+ }
+}
diff --git a/borgbutler-webapp/src/components/general/forms/FormComponents.jsx b/borgbutler-webapp/src/components/general/forms/FormComponents.jsx
index 4d8f4d0..32a84ff 100644
--- a/borgbutler-webapp/src/components/general/forms/FormComponents.jsx
+++ b/borgbutler-webapp/src/components/general/forms/FormComponents.jsx
@@ -3,8 +3,9 @@
import classNames from 'classnames';
import {FormFeedback, Input, UncontrolledTooltip} from 'reactstrap';
import {FormCheckbox} from "./FormCheckbox";
+import {FormRadioButton} from "./FormRadioButton";
import {FormButton} from "./FormButton";
-import {FormSelect, FormOption} from "./FormSelect";
+import {FormOption, FormSelect} from "./FormSelect";
import {revisedRandId} from "../../../utilities/global";
import I18n from "../translation/I18n";
@@ -80,10 +81,14 @@
</UncontrolledTooltip>;
}
const {fieldLength, className, hint, hintPlacement, id, ...other} = props;
+ let myClassName = className;
+ if (fieldLength > 0) {
+ myClassName = classNames(`col-sm-${props.fieldLength}`, className);
+ }
return (
<React.Fragment>
<Input id={targetId}
- className={classNames(`col-sm-${props.fieldLength}`, className)}
+ className={myClassName}
{...other}
/>
{tooltip}
@@ -113,7 +118,6 @@
name: '',
hint: null,
hintPlacement: 'top',
- fieldLength: 10,
value: '',
min: null,
max: null,
@@ -158,9 +162,9 @@
const FormLabelField = (props) => {
- const forId = props.children.props.id || props.htmlFor || revisedRandId();
+ const forId = props.children.props.id || props.children.props.name || props.htmlFor || revisedRandId();
return (
- <FormGroup>
+ <FormGroup className={props.className}>
<FormLabel length={props.labelLength} htmlFor={forId}>
{props.label}
</FormLabel>
@@ -173,6 +177,7 @@
FormLabelField.propTypes = {
id: PropTypes.string,
+ className: PropTypes.string,
htmlFor: PropTypes.string,
validationMessage: PropTypes.string,
labelLength: PropTypes.number,
@@ -184,6 +189,7 @@
FormLabelField.defaultProps = {
id: null,
+ className: null,
htmlFor: null,
validationMessage: null,
labelLength: 2,
@@ -202,6 +208,7 @@
label={props.label}
hint={props.hint}
validationState={props.validationState}
+ className={props.className}
>
<FormInput
id={props.id}
@@ -224,6 +231,7 @@
FormLabelInputField.propTypes = {
id: PropTypes.string,
label: PropTypes.node,
+ className: PropTypes.node,
labelLength: PropTypes.number,
fieldLength: PropTypes.number,
hint: PropTypes.string,
@@ -241,6 +249,7 @@
FormLabelInputField.defaultProps = {
id: null,
+ className: null,
label: '',
labelLength: 2,
fieldLength: 10,
@@ -291,6 +300,7 @@
FormSelect,
FormOption,
FormCheckbox,
+ FormRadioButton,
FormLabelInputField,
FormFieldset,
FormButton,
diff --git a/borgbutler-webapp/src/components/general/forms/FormRadioButton.jsx b/borgbutler-webapp/src/components/general/forms/FormRadioButton.jsx
new file mode 100644
index 0000000..e92f6e5
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/FormRadioButton.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {UncontrolledTooltip} from 'reactstrap';
+import {revisedRandId} from "../../../utilities/global";
+import classNames from 'classnames';
+import I18n from "../translation/I18n";
+
+class FormRadioButton extends React.Component {
+
+ _id = this.props.id || revisedRandId();
+
+ render() {
+ const {id, className, labelKey, label, hint, hintKey, ...other} = this.props;
+ let tooltip = null;
+ let hintId = null;
+ if (hint || hintKey) {
+ hintId = `hint-${this._id}`;
+ tooltip =
+ <UncontrolledTooltip placement="right" target={hintId}>
+ {hintKey ? <I18n name={hintKey}/> : hint}
+ </UncontrolledTooltip>;
+ }
+ let labelNode = <label
+ className={'custom-control-label'}
+ htmlFor={this._id}>
+ {labelKey ? <I18n name={labelKey}/> : this.props.label}
+ </label>;
+ return (
+ <React.Fragment>
+ <div className="custom-control custom-radio custom-control-inline" id={hintId}>
+ <input type="radio"
+ id={this._id}
+ className={classNames('custom-control-input', className)}
+ {...other}
+ />
+ {labelNode}
+ </div>
+ {tooltip}
+ </React.Fragment>
+ );
+ }
+}
+
+FormRadioButton.propTypes = {
+ id: PropTypes.string,
+ name: PropTypes.string,
+ checked: PropTypes.bool,
+ onChange: PropTypes.func,
+ hint: PropTypes.string,
+ label: PropTypes.node,
+ labelKey: PropTypes.string
+};
+
+FormRadioButton.defaultProps = {
+ checked: false,
+ onChange: null
+};
+
+
+export {
+ FormRadioButton
+};
diff --git a/borgbutler-webapp/src/components/general/forms/FormSelect.jsx b/borgbutler-webapp/src/components/general/forms/FormSelect.jsx
index 6b0bd21..906fcae 100644
--- a/borgbutler-webapp/src/components/general/forms/FormSelect.jsx
+++ b/borgbutler-webapp/src/components/general/forms/FormSelect.jsx
@@ -12,11 +12,15 @@
{props.hint}
</UncontrolledTooltip>;
}
- const {className, hint, hintPlacement, id, ...other} = props;
+ const {fieldLength, className, hint, hintPlacement, id, ...other} = props;
+ let myClassName = className;
+ if (fieldLength > 0) {
+ myClassName = classNames(`col-sm-${props.fieldLength}`, className);
+ }
return (
<React.Fragment>
<select id={targetId}
- className={classNames('custom-select form-control form-control-sm mr-1', className)}
+ className={classNames('custom-select form-control form-control-sm mr-1', myClassName)}
{...other}
>
{props.children}
@@ -31,6 +35,7 @@
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
name: PropTypes.string,
onChange: PropTypes.func,
+ fieldLength: PropTypes.number,
hint: PropTypes.string,
hintPlacement: PropTypes.oneOf(['right', 'top']),
children: PropTypes.node,
diff --git a/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx b/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
index 3a32ffc..8a2a060 100644
--- a/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
+++ b/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
@@ -72,7 +72,9 @@
/>;
} else if (this.state.archive) {
pageHeader = <React.Fragment>
- <Link to={`/repoArchives/${this.state.repoId}`}> {archive.repoDisplayName}</Link> - {archive.name}
+ <Link to={'/repos'}> Repositories</Link> -
+ <Link
+ to={`/repoArchives/${this.state.repoId}/${archive.repoDisplayName}`}> {archive.repoDisplayName}</Link> - {archive.name}
<div
className={'btn btn-outline-primary refresh-button-right'}
onClick={this.toggleModal}
diff --git a/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx b/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx
index 3a26b4d..715baf8 100644
--- a/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx
+++ b/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx
@@ -8,6 +8,7 @@
import ConfigAccountTab from "./ConfigurationAccountTab";
import ConfigServerTab from "./ConfigurationServerTab";
import LoadingOverlay from '../../general/loading/LoadingOverlay';
+import ConfirmModal from '../../general/modal/ConfirmModal';
class ConfigurationPage
extends React
@@ -20,11 +21,13 @@
this.state = {
activeTab: '1',
loading: false,
- reload: false
+ reload: false,
+ confirmModal: false
};
this.onSave = this.onSave.bind(this);
this.onCancel = this.onCancel.bind(this);
+ this.toggleModal = this.toggleModal.bind(this);
}
toggleTab = tab => () => {
@@ -58,6 +61,12 @@
this.setReload();
}
+ toggleModal() {
+ this.setState({
+ confirmModal: !this.state.confirmModal
+ })
+ }
+
render() {
// https://codepen.io/_arpy/pen/xYoyPW
if (this.state.reload) {
@@ -65,6 +74,17 @@
}
return (
<React.Fragment>
+ <ConfirmModal
+ onConfirm={ConfigServerTab.clearAllCaches}
+ title={'Are you sure?'}
+ toggle={this.toggleModal}
+ open={this.state.confirmModal}
+ >
+ Do you really want to clear all caches? All Archive file lists and caches for repo and archive
+ information will be cleared.
+ <br/>
+ This is a safe option but it may take some time to re-fill the caches (on demand) again.
+ </ConfirmModal>
<PageHeader><I18n name={'configuration'}/></PageHeader>
<Nav tabs>
<NavLink
@@ -92,6 +112,8 @@
</TabContent>
<FormGroup>
<FormField length={12}>
+ <FormButton id={'clearAllCaches'} onClick={this.toggleModal}> Clear all caches
+ </FormButton>
<FormButton onClick={this.onCancel}
hintKey="configuration.cancel.hint"><I18n name={'common.cancel'}/>
</FormButton>
diff --git a/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx b/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
index 84f0e2d..eff23c4 100644
--- a/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
+++ b/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
@@ -1,10 +1,20 @@
import React from 'react';
-import {FormButton, FormCheckbox, FormLabelField, FormLabelInputField} from '../../general/forms/FormComponents';
+import {Alert} from 'reactstrap';
+import {
+ FormCheckbox,
+ FormField,
+ FormGroup,
+ FormInput,
+ FormLabel,
+ FormLabelField,
+ FormLabelInputField,
+ FormOption,
+ FormSelect
+} from '../../general/forms/FormComponents';
import {getRestServiceUrl} from '../../../utilities/global';
import I18n from '../../general/translation/I18n';
import ErrorAlertGenericRestFailure from '../../general/ErrorAlertGenericRestFailure';
import Loading from '../../general/Loading';
-import ConfirmModal from '../../general/modal/ConfirmModal';
class ConfigServerTab extends React.Component {
loadConfig = () => {
@@ -25,6 +35,8 @@
.then((data) => {
this.setState({
loading: false,
+ borgBinary: data.borgVersion.borgBinary,
+ borgCommand: data.borgVersion.borgCommand,
...data
})
})
@@ -48,13 +60,13 @@
showDemoRepos: true,
maxArchiveContentCacheCapacityMb: 100,
redirect: false,
- confirmModal: false
+ borgCommand: null,
+ borgBinary: null
};
this.handleTextChange = this.handleTextChange.bind(this);
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
this.loadConfig = this.loadConfig.bind(this);
- this.toggleModal = this.toggleModal.bind(this);
}
componentDidMount() {
@@ -73,9 +85,13 @@
save() {
var config = {
port: this.state.port,
- maxArchiveContentCacheCapacityMb : this.state.maxArchiveContentCacheCapacityMb,
+ maxArchiveContentCacheCapacityMb: this.state.maxArchiveContentCacheCapacityMb,
webDevelopmentMode: this.state.webDevelopmentMode,
- showDemoRepos: this.state.showDemoRepos
+ showDemoRepos: this.state.showDemoRepos,
+ borgVersion: {
+ borgCommand: this.state.borgCommand,
+ borgBinary: this.state.borgBinary
+ }
};
return fetch(getRestServiceUrl("configuration/config"), {
method: 'POST',
@@ -96,12 +112,6 @@
})
}
- toggleModal() {
- this.setState({
- confirmModal: !this.state.confirmModal
- })
- }
-
render() {
if (this.state.loading) {
return <Loading/>;
@@ -110,30 +120,47 @@
if (this.state.failed) {
return <ErrorAlertGenericRestFailure handleClick={this.loadConfig}/>;
}
-
+ const borgVersion = this.state.borgVersion;
+ let borgInfoColor = 'success';
+ let borgInfoMessage = `Borg version '${borgVersion.version}' is OK.`;
+ if (!borgVersion.versionOK) {
+ borgInfoColor = 'danger';
+ borgInfoMessage = borgVersion.statusMessage;
+ }
return (
- <div>
- <ConfirmModal
- onConfirm={ConfigServerTab.clearAllCaches}
- title={'Are you sure?'}
- toggle={this.toggleModal}
- open={this.state.confirmModal}
- >
- Do you really want to clear all caches? All Archive file lists and caches for repo and archive
- information will be cleared.
- <br/>
- This is a safe option but it may take some time to re-fill the caches (on demand) again.
- </ConfirmModal>
+ <React.Fragment>
<form>
- <FormLabelField>
- <FormButton id={'clearAllCaches'} onClick={this.toggleModal}> Clear all caches
- </FormButton>
- </FormLabelField>
+ <FormGroup>
+ <FormLabel>{'Borg command'}</FormLabel>
+ <FormField length={2}>
+ <FormSelect
+ value={this.state.borgBinary}
+ name={'borgBinary'}
+ onChange={this.handleTextChange}
+ hint={`Choose your OS and BorgButler will download and use a ready to run borg binary from ${borgVersion.binariesDownloadUrl} or choose a manual installed version.`}
+ >
+ {borgVersion.borgBinaries
+ .map((binary, index) => <FormOption label={binary[1]} value={binary[0]}
+ key={index}/>)}
+ <FormOption label={'Manual'} value={'manual'}/>
+ </FormSelect>
+ </FormField>
+ <FormInput fieldLength={8} name={'borgCommand'} value={this.state.borgCommand}
+ onChange={this.handleTextChange}
+ placeholder="Enter path of borg command"
+ disabled={this.state.borgBinary !== "manual"}/>
+ </FormGroup>
+ <FormGroup>
+ <FormField length={4}/>
+ <Alert className={'col-sm-8'} color={borgInfoColor}>
+ {borgInfoMessage}
+ </Alert>
+ </FormGroup>
<FormLabelInputField label={'Port'} fieldLength={2} type="number" min={0} max={65535}
step={1}
name={'port'} value={this.state.port}
onChange={this.handleTextChange}
- placeholder="Enter port" />
+ placeholder="Enter port"/>
<FormLabelInputField label={'Maximum disc capacity (MB)'} fieldLength={2} type="number" min={50}
max={10000}
step={50}
@@ -141,21 +168,21 @@
value={this.state.maxArchiveContentCacheCapacityMb}
onChange={this.handleTextChange}
placeholder="Enter maximum Capacity"
- hint={`Limits the cache size of archive file lists in the local cache directory: ${this.state.cacheDir}`} />
+ hint={`Limits the cache size of archive file lists in the local cache directory: ${this.state.cacheDir}`}/>
<FormLabelField label={'Show demo repositories'} fieldLength={2}>
<FormCheckbox checked={this.state.showDemoRepos}
hint={'If true, some demo repositories are shown for testing the functionality of BorgButler without any further configuration and running borg backups.'}
name="showDemoRepos"
- onChange={this.handleCheckboxChange} />
+ onChange={this.handleCheckboxChange}/>
</FormLabelField>
- <FormLabelField label={<I18n name={'configuration.webDevelopmentMode'} />} fieldLength={2}>
+ <FormLabelField label={<I18n name={'configuration.webDevelopmentMode'}/>} fieldLength={2}>
<FormCheckbox checked={this.state.webDevelopmentMode}
hintKey={'configuration.webDevelopmentMode.hint'}
name="webDevelopmentMode"
- onChange={this.handleCheckboxChange} />
+ onChange={this.handleCheckboxChange}/>
</FormLabelField>
</form>
- </div>
+ </React.Fragment>
);
}
}
diff --git a/borgbutler-webapp/src/components/views/jobs/Job.css b/borgbutler-webapp/src/components/views/jobs/Job.css
index b57da22..7650622 100644
--- a/borgbutler-webapp/src/components/views/jobs/Job.css
+++ b/borgbutler-webapp/src/components/views/jobs/Job.css
@@ -7,4 +7,12 @@
.job-progress {
width: calc(100% - 3em);
margin-top: -10px;
+}
+
+.job-progress div.progress {
+ margin: 0 .75rem .375rem;
+}
+
+.onclick {
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/borgbutler-webapp/src/components/views/jobs/Job.jsx b/borgbutler-webapp/src/components/views/jobs/Job.jsx
index dcf01f4..e9b0a14 100644
--- a/borgbutler-webapp/src/components/views/jobs/Job.jsx
+++ b/borgbutler-webapp/src/components/views/jobs/Job.jsx
@@ -39,6 +39,22 @@
render() {
let content = undefined;
let job = this.props.job;
+ let cancelDisabled = undefined;
+ let times = ' (created: ' + job.createTime;
+ if (job.startTime) {
+ times += ', started: ' + job.startTime;
+ }
+ if (job.stopTime) {
+ times += ', ended: ' + job.stopTime;
+ }
+ times += ')';
+ if ((job.status !== 'RUNNING' && job.status !== 'QUEUED') || job.cancellationRequested) {
+ cancelDisabled = true;
+ }
+ let cancelButton = <div className="job-cancel"><Button color={'danger'}
+ onClick={() => this.cancelJob(job.uniqueJobNumber)}
+ disabled={cancelDisabled}><IconCancel/></Button>
+ </div>;
if (job.status === 'RUNNING') {
let progressPercent = 100;
let color = 'success';
@@ -49,13 +65,18 @@
progressPercent = job.progressPercent;
}
content = <Progress animated color={color} value={progressPercent}>{job.progressText}</Progress>;
+ } else if (job.status === 'CANCELLED') {
+ content = <Progress color={'warning'} value={100}>{job.status}</Progress>
+ cancelButton = '';
+ } else if (job.status === 'FAILED') {
+ content = <Progress color={'danger'} value={100}>{job.status}</Progress>
+ cancelButton = '';
+ } else if (job.status === 'DONE') {
+ content = <Progress color={'success'} value={100}>{job.status}</Progress>
+ cancelButton = '';
} else {
content = <Progress color={'info'} value={100}>{job.status}</Progress>
}
- let cancelDisabled = undefined;
- if ((job.status !== 'RUNNING' && job.status !== 'QUEUED') || job.cancellationRequested) {
- cancelDisabled = true;
- }
let environmentVariables = '';
if (job.environmentVariables && Array.isArray(job.environmentVariables)) {
environmentVariables = job.environmentVariables.map((variable, index) => <React.Fragment key={index}>
@@ -70,10 +91,7 @@
<Button color="link" onClick={this.toggle}>{job.description}</Button>
{content}
</div>
- <div className="job-cancel"><Button color={'danger'}
- onClick={() => this.cancelJob(job.uniqueJobNumber)}
- disabled={cancelDisabled}><IconCancel/></Button>
- </div>
+ {cancelButton}
</div>
<Collapse isOpen={this.state.collapse}>
<Card>
@@ -82,7 +100,7 @@
<tbody>
<tr>
<th>Status</th>
- <td>{job.status}</td>
+ <td>{job.status} {times}</td>
</tr>
<tr>
<th>Command line</th>
diff --git a/borgbutler-webapp/src/components/views/jobs/JobMonitorPanel.jsx b/borgbutler-webapp/src/components/views/jobs/JobMonitorPanel.jsx
index 759cab2..1dc5557 100644
--- a/borgbutler-webapp/src/components/views/jobs/JobMonitorPanel.jsx
+++ b/borgbutler-webapp/src/components/views/jobs/JobMonitorPanel.jsx
@@ -1,19 +1,21 @@
import React from 'react';
-import {Button} from 'reactstrap';
+import {Button, Collapse} from 'reactstrap';
import {getRestServiceUrl, isDevelopmentMode} from '../../../utilities/global';
import JobQueue from './JobQueue';
-import ErrorAlert from '../archives/ArchiveView';
+import ErrorAlert from '../../general/ErrorAlert';
import PropTypes from 'prop-types';
class JobMonitorPanel extends React.Component {
state = {
isFetching: false,
- testMode: false
+ isFetchingOldJobs: false,
+ testMode: false,
+ collapseOldJobs: false
};
componentDidMount = () => {
- this.fetchQueues();
- this.interval = setInterval(() => this.fetchQueues(), 2000);
+ this.fetchQueues(false);
+ this.interval = setInterval(() => this.fetchJobs(), 2000);
};
componentWillUnmount() {
@@ -26,14 +28,39 @@
});
}
- fetchQueues = () => {
+ toggleOldJobs() {
+ if (!this.state.collapseOldJobs) {
+ this.fetchQueues(true);
+ }
+ this.setState({collapseOldJobs: !this.state.collapseOldJobs});
+ }
+
+ fetchJobs() {
+ if (!this.state.isFetching) { // Don't run twice at the same time
+ this.fetchQueues(false);
+ }
+ if (this.state.collapseOldJobs && !this.state.isFetchingOldJobs) {
+ this.fetchQueues(true);
+ }
+ }
+
+ fetchQueues = (oldJobs) => {
+ let queuesVar = 'queues';
+ let isFetchingVar = 'isFetching';
+ let failedVar = 'failed';
+ if (oldJobs) {
+ queuesVar = 'oldJobsQueues';
+ isFetchingVar = 'isFetchingOldJobs';
+ failedVar = 'oldJobsFailed';
+ }
this.setState({
- isFetching: true,
- failed: false
+ [isFetchingVar]: true,
+ [failedVar]: false
});
fetch(getRestServiceUrl('jobs', {
repo: this.props.repo,
- testMode: this.state.testMode
+ testMode: this.state.testMode,
+ oldJobs: oldJobs
}), {
method: 'GET',
headers: {
@@ -46,11 +73,11 @@
return queue;
});
this.setState({
- isFetching: false,
- queues
+ [isFetchingVar]: false,
+ [queuesVar]: queues
});
})
- .catch(() => this.setState({isFetching: false, failed: true}));
+ .catch(() => this.setState({[isFetchingVar]: false, [failedVar]: true}));
};
render() {
@@ -60,7 +87,7 @@
content = <i>Loading...</i>;
} else if (this.state.failed) {
content = <ErrorAlert
- title={'Cannot load Repositories'}
+ title={'Cannot load job queues'}
description={'Something went wrong during contacting the rest api.'}
action={{
handleClick: this.fetchQueues,
@@ -77,16 +104,50 @@
key={queue.repo}
/>)}
</React.Fragment>;
- } else if (isDevelopmentMode() && !this.props.embedded) {
- content = <React.Fragment>No jobs are running or queued.<br/><br/>
- <Button color="primary" onClick={this.toggleTestMode}>Test mode</Button>
- </React.Fragment>
} else {
content = <React.Fragment>No jobs are running or queued.</React.Fragment>
}
}
+ let oldJobs = '';
+
+ if (this.state.isFetchingOldJobs && !this.state.oldJobsQueues) {
+ oldJobs = <i>Loading...</i>;
+ } else if (this.state.oldJobsFailed) {
+ oldJobs = <ErrorAlert
+ title={'Cannot load old job queues'}
+ description={'Something went wrong during contacting the rest api.'}
+ action={{
+ handleClick: this.fetchQueues,
+ title: 'Try again'
+ }}
+ />;
+ } else if (this.state.oldJobsQueues) {
+ if (this.state.oldJobsQueues.length > 0) {
+ oldJobs = <React.Fragment>
+ {this.state.oldJobsQueues
+ .map((queue) => <JobQueue
+ embedded={this.props.embedded}
+ queue={queue}
+ key={queue.repo}
+ />)}
+ </React.Fragment>
+ } else {
+ oldJobs = <React.Fragment>No old jobs available.</React.Fragment>
+ }
+ }
+ let testContent = '';
+ if (isDevelopmentMode() && !this.props.embedded) {
+ testContent = <React.Fragment><br/><br/><br/><Button className="btn-outline-info" onClick={this.toggleTestMode}>Test mode</Button></React.Fragment>;
+ }
+
return <React.Fragment>
{content}
+ <h5 className={'onclick'} onClick={this.toggleOldJobs}>Show old jobs
+ </h5>
+ <Collapse isOpen={this.state.collapseOldJobs}>
+ {oldJobs}
+ </Collapse>
+ {testContent}
</React.Fragment>;
}
@@ -95,15 +156,18 @@
this.fetchQueues = this.fetchQueues.bind(this);
this.toggleTestMode = this.toggleTestMode.bind(this);
+ this.toggleOldJobs = this.toggleOldJobs.bind(this);
}
}
-JobMonitorPanel.propTypes = {
+JobMonitorPanel
+ .propTypes = {
embedded: PropTypes.bool,
repo: PropTypes.string
};
-JobMonitorPanel.defaultProps = {
+JobMonitorPanel
+ .defaultProps = {
embedded: true,
repo: null
};
diff --git a/borgbutler-webapp/src/components/views/jobs/JobQueue.jsx b/borgbutler-webapp/src/components/views/jobs/JobQueue.jsx
index 20310ab..c04090e 100644
--- a/borgbutler-webapp/src/components/views/jobs/JobQueue.jsx
+++ b/borgbutler-webapp/src/components/views/jobs/JobQueue.jsx
@@ -1,25 +1,45 @@
import React from 'react';
-import {Card, CardBody, CardHeader, ListGroup} from 'reactstrap';
+import {Card, CardBody, CardHeader, Collapse, ListGroup} from 'reactstrap';
import Job from "./Job";
-function JobQueue({queue, embedded}) {
- return (
- <div>
- <Card>
- <CardHeader>Job queue: {queue.repo}</CardHeader>
- <CardBody>
- <ListGroup>
- {queue.jobs
- .map((job, index) => <Job
- embedded={embedded}
- job={job}
- key={job.uniqueJobNumber}
- />)}
- </ListGroup>
- </CardBody>
- </Card>
- </div>
- );
+class JobQueue extends React.Component {
+ state = {
+ collapsed: true
+ };
+
+ toggle() {
+ this.setState({collapsed: !this.state.collapsed});
+ }
+
+
+ render() {
+ const queue = this.props.queue;
+ return (
+ <div>
+ <Card>
+ <CardHeader className={'onclick'} onClick={this.toggle}>Job queue: {queue.repo}</CardHeader>
+ <Collapse isOpen={this.state.collapsed}>
+ <CardBody>
+ <ListGroup>
+ {queue.jobs
+ .map((job, index) => <Job
+ embedded={this.props.embedded}
+ job={job}
+ key={job.uniqueJobNumber}
+ />)}
+ </ListGroup>
+ </CardBody>
+ </Collapse>
+ </Card>
+ </div>
+ );
+ }
+
+ constructor(props) {
+ super(props);
+ this.toggle = this.toggle.bind(this);
+
+ }
}
export default JobQueue;
diff --git a/borgbutler-webapp/src/components/views/repos/ConfigureRepoPage.jsx b/borgbutler-webapp/src/components/views/repos/ConfigureRepoPage.jsx
new file mode 100644
index 0000000..9583c37
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/repos/ConfigureRepoPage.jsx
@@ -0,0 +1,252 @@
+import React from 'react';
+import {Link} from 'react-router-dom'
+import {
+ FormButton,
+ FormField,
+ FormGroup,
+ FormInput,
+ FormLabel,
+ FormLabelInputField,
+ FormOption,
+ FormRadioButton,
+ FormSelect
+} from '../../general/forms/FormComponents';
+import I18n from "../../general/translation/I18n";
+import {PageHeader} from "../../general/BootstrapComponents";
+
+class ConfigureRepoPage extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.handleTextChange = this.handleTextChange.bind(this);
+ this.handleRepoTextChange = this.handleRepoTextChange.bind(this);
+ this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
+ this.state = {
+ repoConfig: {},
+ mode: 'existingRepo',
+ localRemote: 'local',
+ encryption: 'repokey',
+ passwordMethod: 'passwordCommand',
+ passwordCreate: null
+ };
+ }
+
+ handleTextChange = event => {
+ event.preventDefault();
+ this.setState({[event.target.name]: event.target.value});
+ if (event.target.name === 'passwordMethod') {
+ const value = event.target.value;
+ var passwordCommand = null;
+ var passwordCreate = null;
+ if (value === 'passwordFile') {
+ passwordCommand = 'cat ~/.borg-passphrase';
+ passwordCreate = <React.Fragment>
+ Create a file with a password in it in your home directory and use permissions to keep anyone else
+ from
+ reading it:<br/>
+ <div className="command-line">head -c 1024 /dev/urandom | base64 > ~/.borg-passphrase<br/>
+ chmod 400 ~/.borg-passphrase
+ </div>
+ </React.Fragment>;
+ } else if (value === 'macos-keychain') {
+ passwordCommand = 'security find-generic-password -a $USER -s borg-passphrase';
+ passwordCreate = <React.Fragment>
+ Generate a passphrase and use security to save it to your login (default) keychain:<br/>
+ <div className="command-line">security add-generic-password -D secret -U -a $USER -s borg-passphrase
+ -w $(head -c 1024 /dev/urandom | base64)
+ </div>
+ </React.Fragment>;
+ } else if (value === 'gnome-keyring') {
+ passwordCommand = 'secret-tool lookup borg-repository repo-name';
+ passwordCreate = <React.Fragment>
+ First ensure libsecret-tools, gnome-keyring and libpam-gnome-keyring are installed. If
+ libpam-gnome-keyring wasn’t already installed, ensure it runs on login:<br/>
+ <div className="command-line">sudo sh -c "echo session optional pam_gnome_keyring.so auto_start >>
+ /etc/pam.d/login"<br/>
+ sudo sh -c "echo password optional pam_gnome_keyring.so >> /etc/pam.d/passwd"<br/>
+ # you may need to relogin afterwards to activate the login keyring
+ </div>
+ Then add a secret to the login keyring:
+ <div className="command-line">head -c 1024 /dev/urandom | base64 | secret-tool store borg-repository
+ repo-name --label="Borg Passphrase"</div>
+ </React.Fragment>;
+ } else if (value === 'kwallet') {
+ passwordCommand = 'kwalletcli -e borg-passphrase -f Passwords';
+ passwordCreate = <React.Fragment>
+ Ensure kwalletcli is installed, generate a passphrase, and store it in your “wallet”:<br/>
+ <div className="command-line">head -c 1024 /dev/urandom | base64 | kwalletcli -Pe borg-passphrase -f
+ Passwords
+ </div>
+ </React.Fragment>;
+ }
+ if (passwordCommand) {
+ this.setState({repoConfig: {...this.state.repoConfig, 'passwordCommand': passwordCommand}})
+ }
+ this.setState({'passwordCreate': passwordCreate});
+ }
+ }
+
+
+ handleRepoTextChange = event => {
+ event.preventDefault();
+ this.setState({repoConfig: {...this.state.repoConfig, [event.target.name]: event.target.value}});
+ }
+
+ handleCheckboxChange = event => {
+ this.setState({[event.target.name]: event.target.value});
+ /*if (event.target.name === 'mode' && event.target.value === 'existingRepo'
+ && this.state.passwordMethod !== 'passwordCommand' && this.state.passwordMethod !== 'passphrase') {
+ // Other options such as Mac OS key chain isn't available for existing repos:
+ this.setState({passwordMethod: 'passwordCommand'});
+ }*/
+ }
+
+ render() {
+ const repoConfig = this.state.repoConfig;
+ var passwordMethods = [['password-command', 'Password command'],
+ ['passwordFile', 'Password file'],
+ ['macos-keychain', 'Mac OS X keychain'],
+ ['gnome-keyring', 'GNOME keyring'],
+ ['kwallet', 'KWallet'],
+ ['passphrase', 'Passphrase (not recommended)']
+ ];
+ let repoPlaceHolder = 'Enter the repo used by Borg.';
+ if (this.state.mode === 'initNewRepo' && this.state.localRemote === 'remote') {
+ repoPlaceHolder = 'Enter the remote path of the repo, such as user@hostname:backup.';
+ }
+ let repoFieldLength = 10;
+ let browseButton = null;
+ if (this.state.localRemote === 'local') {
+ repoFieldLength = 9;
+ browseButton = <FormButton onClick={null}
+ hint={'Browse local backup directory.'}>Browse</FormButton>
+ repoPlaceHolder = 'Enter or browse the local path of the repo home dir used by Borg.';
+ }
+ return <React.Fragment>
+ <PageHeader>
+ Configure repository
+ </PageHeader>
+ <form>
+ <FormGroup>
+ <FormLabel length={2}>{'Mode'}</FormLabel>
+ <FormField length={10}>
+ <FormRadioButton name={'mode'} id={'mode1'} label={'Add existing repository'}
+ value={'existingRepo'}
+ checked={this.state.mode === 'existingRepo'}
+ onChange={this.handleCheckboxChange}
+ hint={'Do you want to add an already existing Borg repository?'}/>
+ <FormRadioButton name={'mode'} id={'mode2'} label={'Init new repository'} value={'initNewRepo'}
+ checked={this.state.mode === 'initNewRepo'}
+ onChange={this.handleCheckboxChange}
+ hint={'Do you want to create and init a new one?'}/>
+ </FormField>
+ </FormGroup>
+ <FormGroup>
+ <FormLabel length={2}>{'Local or remote'}</FormLabel>
+ <FormField length={10}>
+ <FormRadioButton name={'localRemote'} id={'localRemote1'} label={'Local repository'}
+ value={'local'}
+ checked={this.state.localRemote === 'local'}
+ onChange={this.handleCheckboxChange}/>
+ <FormRadioButton name={'localRemote'} id={'localRemote2'} label={'Remote repository'}
+ value={'remote'}
+ checked={this.state.localRemote === 'remote'}
+ onChange={this.handleCheckboxChange}/>
+ </FormField>
+ </FormGroup>
+ <FormLabelInputField label={'Display name'} fieldLength={12}
+ name={'displayName'} value={repoConfig.displayName}
+ onChange={this.handleRepoTextChange}
+ placeholder="Enter display name (only for displaying purposes)."/>
+ <FormGroup>
+ <FormLabel length={2}>{'Repo'}</FormLabel>
+ <FormField length={repoFieldLength}>
+ <FormInput
+ id={'repo'}
+ name={'repo'}
+ type={'text'}
+ value={repoConfig.repo}
+ onChange={this.handleRepoTextChange}
+ placeholder={repoPlaceHolder}
+ />
+ </FormField>
+ {browseButton}
+ </FormGroup>
+ <FormLabelInputField label={'RSH'} fieldLength={12}
+ name={'rsh'} value={repoConfig.rsh}
+ onChange={this.handleRepoTextChange}
+ placeholder="Enter the rsh value (ssh command) for remote repository."
+ className={this.state.localRemote === 'local' ? 'hidden' : null}/>
+ <FormGroup className={this.state.mode === 'existingRepo' ? 'hidden' : null}>
+ <FormLabel length={2}>{'Encryption'}</FormLabel>
+ <FormField length={2}>
+ <FormSelect
+ value={repoConfig.encryption}
+ name={'encryption'}
+ onChange={this.handleRepoTextChange}
+ hint={'Encryption for the new repository (use repokey if you don\'t know what to choose).'}
+ >
+ <FormOption label={'repokey (SHA256)'} value={'repokey'}/>
+ <FormOption label={'repokey-blake2'} value={'repokey-blake2'}/>
+ <FormOption label={'keyfile (SHA256)'} value={'keyfile'}/>
+ <FormOption label={'none (not recommended)'} value={'none'}/>
+ </FormSelect>
+ </FormField>
+ </FormGroup>
+ <FormGroup>
+ <FormLabel length={2}>{'Password method'}</FormLabel>
+ <FormField length={3}>
+ <FormSelect
+ value={this.state.passwordMethod}
+ name={'passwordMethod'}
+ onChange={this.handleTextChange}
+ >
+ {passwordMethods
+ .map((entry) => <FormOption label={entry[1]} value={entry[0]}
+ key={entry[0]}/>)}
+ </FormSelect>
+ </FormField>
+ </FormGroup>
+ <FormGroup className={!this.state.passwordCreate ? 'hidden' : null}>
+ <FormLabel length={2}>{'Passphrase creation info'}</FormLabel>
+ <FormField length={10}>
+ {this.state.passwordCreate}
+ </FormField>
+ </FormGroup>
+ <FormLabelInputField label={'Password command'} fieldLength={12}
+ name={'passwordCommand'} value={repoConfig.passwordCommand}
+ onChange={this.handleRepoTextChange}
+ placeholder="Enter the password command to get the command from or choose a method above."
+ className={this.state.passwordMethod === 'passphrase' ? 'hidden' : null}
+ />
+ <FormLabelInputField label={'Password'} fieldLength={6} type={'password'}
+ name={'passphrase'} value={repoConfig.passphrase}
+ onChange={this.handleRepoTextChange}
+ hint={"It's recommended to use password command instead."}
+ className={this.state.passwordMethod !== 'passphrase' ? 'hidden' : null}
+ />
+ <FormField length={12}>
+ <Link to={'/repos'} className={'btn btn-outline-primary'}><I18n name={'common.cancel'}/>
+ </Link>
+ <FormButton onClick={this.onSave} bsStyle="primary"
+ hintKey="configuration.save.hint"><I18n name={'common.save'}/>
+ </FormButton>
+ </FormField>
+ </form>
+ <code>
+ <h2>Todo:</h2>
+ <ul>
+ <li>Implement 'Save' button ;-)</li>
+ <li>Add own environment variables.</li>
+ <li>Implement browse button for local repos.</li>
+ <li>Note (for new backups): Save your password, otherwise your backup will be lost without!</li>
+ <li>Note (hide password fields): Your backup will not be encrypted!</li>
+ <li>Remove and clone repo.</li>
+ </ul>
+ </code>
+ </React.Fragment>;
+ }
+}
+
+export default ConfigureRepoPage;
+
diff --git a/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx b/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
index 4fe43bc..12995e8 100644
--- a/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
+++ b/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
@@ -1,19 +1,22 @@
import React from 'react'
-import {Nav, NavLink, TabContent, Table, TabPane} from 'reactstrap';
-import { Link } from "react-router-dom";
+import {Badge, Nav, NavLink, TabContent, Table, TabPane} from 'reactstrap';
+import {Link} from "react-router-dom";
import classNames from 'classnames';
import {PageHeader} from '../../general/BootstrapComponents';
import {getRestServiceUrl, humanFileSize} from '../../../utilities/global';
import ErrorAlert from '../../general/ErrorAlert';
import {IconCheck, IconRefresh} from '../../general/IconComponents';
import JobMonitorPanel from '../jobs/JobMonitorPanel';
+import RepoConfigPanel from "./RepoConfigPanel";
class RepoArchiveListView extends React.Component {
state = {
id: this.props.match.params.id,
+ displayName: this.props.match.params.displayName,
isFetching: false,
activeTab: '1',
+ redirectOnError: true
};
componentDidMount = () => {
@@ -42,7 +45,12 @@
repo: json
})
})
- .catch(() => this.setState({isFetching: false, failed: true}));
+ .catch(() => {
+ this.setState({isFetching: false, failed: true})
+ if (this.state.redirectOnError && this.state.activeTab !== '3') {
+ this.setState({activeTab: '3', redirectOnError: false});
+ }
+ });
};
toggleTab = tab => () => {
@@ -51,22 +59,46 @@
})
};
+ afterSave() {
+ if (!this.state.failed) {
+ this.setState({
+ activeTab: '1'
+ })
+ }
+ }
+
+ afterCancel() {
+ if (!this.state.failed) {
+ this.setState({
+ activeTab: '1'
+ })
+ }
+ }
+
render = () => {
- let content = undefined;
+ let errorBadge = '';
+ let content1 = undefined;
+ let content2 = undefined;
const repo = this.state.repo;
- let pageHeader = '';
+ const displayName = (this.state.displayName) ? this.state.displayName : `Error: id=${this.state.id}`;
+ let pageHeader = <React.Fragment>
+ {displayName}
+ </React.Fragment>;
if (this.state.isFetching) {
- content = <JobMonitorPanel repo={this.state.id} />;
+ content1 = <JobMonitorPanel repo={this.state.id}/>;
+ content2 = content1;
} else if (this.state.failed) {
- content = <ErrorAlert
+ content1 = <ErrorAlert
title={'Cannot load Repositories'}
- description={'Something went wrong during contacting the rest api.'}
+ description={'Something went wrong, may-be wrong configuration?'}
action={{
handleClick: this.fetchRepo,
title: 'Try again'
}}
/>;
+ content2 = content1;
+ errorBadge = <Badge color="danger" pill>!</Badge>;
} else if (this.state.repo) {
pageHeader = <React.Fragment>
{repo.displayName}
@@ -127,82 +159,92 @@
<td>{repo.cache.path}</td>
</tr>
}
- content = <React.Fragment>
- <Nav tabs>
- <NavLink
- className={classNames({active: this.state.activeTab === '1'})}
- onClick={this.toggleTab('1')}
- >
- Archives
- </NavLink>
- <NavLink
- className={classNames({active: this.state.activeTab === '2'})}
- onClick={this.toggleTab('2')}
- >
- Information
- </NavLink>
- </Nav>
- <TabContent activeTab={this.state.activeTab}>
- <TabPane tabId={'1'}>
- <Table hover>
- <tbody>
- <tr>
- <th>Archive</th>
- <th>Time</th>
- <th></th>
- <th>Id</th>
- </tr>
- {repo.archives.map((archive) => {
- // Return the element. Also pass key
- let loaded = '';
- if (archive.fileListAlreadyCached) {
- loaded = <IconCheck />;
- }
- return (
- <tr key={archive.id}>
- <td><Link to={`/archives/${repo.id}/${archive.id}/`}>{archive.name}</Link></td>
- <td>{archive.time}</td>
- <td>{loaded}</td>
- <td>{archive.id}</td>
- </tr>);
- })}
- </tbody>
- </Table>
- </TabPane>
- <TabPane tabId={'2'}>
- <Table striped bordered hover>
- <tbody>
- <tr>
- <td>Id</td>
- <td>{repo.id}</td>
- </tr>
- <tr>
- <td>Name</td>
- <td>{repo.name}</td>
- </tr>
- <tr>
- <td>Location</td>
- <td>{repo.location}</td>
- </tr>
- {stats}
- <tr>
- <td>Security dir</td>
- <td>{repo.securityDir}</td>
- </tr>
- {encryption}
- {cachePath}
- </tbody>
- </Table>
- </TabPane>
- </TabContent>
- </React.Fragment>;
+
+ content1 = <Table hover>
+ <tbody>
+ <tr>
+ <th>Archive</th>
+ <th>Time</th>
+ <th></th>
+ <th>Id</th>
+ </tr>
+ {repo.archives.map((archive) => {
+ // Return the element. Also pass key
+ let loaded = '';
+ if (archive.fileListAlreadyCached) {
+ loaded = <IconCheck/>;
+ }
+ return (
+ <tr key={archive.id}>
+ <td><Link to={`/archives/${repo.id}/${archive.id}/`}>{archive.name}</Link></td>
+ <td>{archive.time}</td>
+ <td>{loaded}</td>
+ <td>{archive.id}</td>
+ </tr>);
+ })}
+ </tbody>
+ </Table>;
+ content2 = <Table striped bordered hover>
+ <tbody>
+ <tr>
+ <td>Id</td>
+ <td>{repo.id}</td>
+ </tr>
+ <tr>
+ <td>Name</td>
+ <td>{repo.name}</td>
+ </tr>
+ <tr>
+ <td>Location</td>
+ <td>{repo.location}</td>
+ </tr>
+ {stats}
+ <tr>
+ <td>Security dir</td>
+ <td>{repo.securityDir}</td>
+ </tr>
+ {encryption}
+ {cachePath}
+ </tbody>
+ </Table>;
}
return <React.Fragment>
<PageHeader>
- {pageHeader}
+ <Link to={'/repos'}> Repositories</Link> - {pageHeader}
</PageHeader>
- {content}
+ <Nav tabs>
+ <NavLink
+ className={classNames({active: this.state.activeTab === '1'})}
+ onClick={this.toggleTab('1')}
+ >
+ Archives {errorBadge}
+ </NavLink>
+ <NavLink
+ className={classNames({active: this.state.activeTab === '2'})}
+ onClick={this.toggleTab('2')}
+ >
+ Information {errorBadge}
+ </NavLink>
+ <NavLink
+ className={classNames({active: this.state.activeTab === '3'})}
+ onClick={this.toggleTab('3')}
+ >
+ Configuration
+ </NavLink>
+ </Nav>
+ <TabContent activeTab={this.state.activeTab}>
+ <TabPane tabId={'1'}>
+ {content1}
+ </TabPane>
+ <TabPane tabId={'2'}>
+ {content2}
+ </TabPane>
+ <TabPane tabId={'3'}>
+ <RepoConfigPanel id={this.state.id} afterCancel={this.afterCancel} afterSave={this.afterSave}
+ repoError={this.state.failed}/>
+ </TabPane>
+ </TabContent>
</React.Fragment>;
};
@@ -211,6 +253,8 @@
this.fetchRepo = this.fetchRepo.bind(this);
this.toggleTab = this.toggleTab.bind(this);
+ this.afterCancel = this.afterCancel.bind(this);
+ this.afterSave = this.afterSave.bind(this);
}
}
diff --git a/borgbutler-webapp/src/components/views/repos/RepoCard.jsx b/borgbutler-webapp/src/components/views/repos/RepoCard.jsx
index b04cfad..b7ae760 100644
--- a/borgbutler-webapp/src/components/views/repos/RepoCard.jsx
+++ b/borgbutler-webapp/src/components/views/repos/RepoCard.jsx
@@ -18,7 +18,7 @@
let repoText = this.buildItem(null, content);
return <React.Fragment>
- <Card tag={Link} to={`/repoArchives/${repo.id}`} outline color="success" className={'repo'}
+ <Card tag={Link} to={`/repoArchives/${repo.id}/${repo.displayName}`} outline color="success" className={'repo'}
style={{backgroundColor: '#fff'}}>
<CardHeader>{repo.displayName}</CardHeader>
<CardBody>
diff --git a/borgbutler-webapp/src/components/views/repos/RepoConfigPanel.jsx b/borgbutler-webapp/src/components/views/repos/RepoConfigPanel.jsx
new file mode 100644
index 0000000..bb549df
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/repos/RepoConfigPanel.jsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import {FormGroup} from 'reactstrap';
+import {FormButton, FormField, FormLabelInputField} from '../../general/forms/FormComponents';
+import {getRestServiceUrl} from '../../../utilities/global';
+import I18n from "../../general/translation/I18n";
+import LoadingOverlay from '../../general/loading/LoadingOverlay';
+import PropTypes from "prop-types";
+import ErrorAlert from "../../general/ErrorAlert";
+
+class RepoConfigPanel extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: false,
+ repoConfig: undefined
+ };
+
+ this.fetch = this.fetch.bind(this);
+ this.handleTextChange = this.handleTextChange.bind(this);
+ this.onSave = this.onSave.bind(this);
+ this.onCancel = this.onCancel.bind(this);
+ }
+
+ componentDidMount = () => this.fetch();
+
+ fetch = () => {
+ this.setState({
+ isFetching: true,
+ failed: false,
+ repoConfig: undefined
+ });
+ fetch(getRestServiceUrl('repoConfig', {
+ id: this.props.id
+ }), {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ })
+ .then(response => response.json())
+ .then(json => {
+ this.setState({
+ isFetching: false,
+ repoConfig: json
+ })
+ })
+ .catch((error) => {
+ console.log("error", error);
+ this.setState({
+ isFetching: false,
+ failed: true
+ });
+ })
+ };
+
+ handleTextChange = event => {
+ event.preventDefault();
+ this.setState({repoConfig: {...this.state.repoConfig, [event.target.name]: event.target.value}});
+ }
+
+ async onSave(event) {
+ const response = fetch(getRestServiceUrl("repoConfig"), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(this.state.repoConfig)
+ });
+ if (response) await response;
+ if (this.props.afterSave) {
+ this.props.afterSave();
+ }
+ }
+
+ async onCancel() {
+ const response = this.fetch();
+ if (response) await response;
+ if (this.props.afterCancel) {
+ this.props.afterCancel();
+ }
+ }
+
+ render() {
+ let content;
+ let repoError = '';
+ if (this.props.repoError) {
+ repoError =<ErrorAlert
+ title={'Cannot access repository'}
+ description={'Repo not available or mis-configured (please refer the log files for more details).'}
+ />
+ }
+ if (this.state.isFetching) {
+ content = <React.Fragment>Loading...</React.Fragment>;
+ } else if (this.state.failed) {
+ content = <ErrorAlert
+ title={'Cannot load config of repository'}
+ description={'Something went wrong during contacting the rest api.'}
+ action={{
+ handleClick: this.fetchRepo,
+ title: 'Try again'
+ }}
+ />;
+ } else if (this.state.repoConfig) {
+ const repoConfig = this.state.repoConfig;
+ content = <React.Fragment>
+ <FormGroup>
+ <FormLabelInputField label={'Display name'} fieldLength={12}
+ name={'displayName'} value={repoConfig.displayName}
+ onChange={this.handleTextChange}
+ placeholder="Enter display name (only for displaying purposes)."/>
+ <FormLabelInputField label={'Repo'} fieldLength={12}
+ name={'repo'} value={repoConfig.repo}
+ onChange={this.handleTextChange}
+ placeholder="Enter the name of the repo, used by Borg."/>
+ <FormLabelInputField label={'RSH'} fieldLength={12}
+ name={'rsh'} value={repoConfig.rsh}
+ onChange={this.handleTextChange}
+ placeholder="Enter the rsh value (ssh command) for remote repository."/>
+ <FormLabelInputField label={'Password command'} fieldLength={12}
+ name={'passwordCommand'} value={repoConfig.passwordCommand}
+ onChange={this.handleTextChange}
+ placeholder="Enter the password command to get the command from."/>
+ <FormLabelInputField label={'Password'} fieldLength={6} type={'password'}
+ name={'passphrase'} value={repoConfig.passphrase}
+ onChange={this.handleTextChange}
+ hint={"It's recommended to use password command instead."}
+ />
+ <FormField length={12}>
+ <FormButton onClick={this.onCancel}
+ hintKey="configuration.cancel.hint"><I18n name={'common.cancel'}/>
+ </FormButton>
+ <FormButton onClick={this.onSave} bsStyle="primary"
+ hintKey="configuration.save.hint"><I18n name={'common.save'}/>
+ </FormButton>
+ </FormField>
+ </FormGroup>
+ <LoadingOverlay active={this.state.loading}/>
+ </React.Fragment>;
+ }
+ return <React.Fragment>{content}{repoError}</React.Fragment>;
+ }
+}
+
+RepoConfigPanel.propTypes = {
+ afterCancel: PropTypes.func.isRequired,
+ afterSave: PropTypes.func.isRequired,
+ id: PropTypes.string,
+ repoError: PropTypes.bool
+};
+
+export default RepoConfigPanel;
+
diff --git a/borgbutler-webapp/src/components/views/repos/RepoListView.jsx b/borgbutler-webapp/src/components/views/repos/RepoListView.jsx
index 7c03a4a..fde2450 100644
--- a/borgbutler-webapp/src/components/views/repos/RepoListView.jsx
+++ b/borgbutler-webapp/src/components/views/repos/RepoListView.jsx
@@ -1,11 +1,12 @@
import React from 'react'
+import {Link} from 'react-router-dom'
import './RepoListView.css';
import {CardDeck} from 'reactstrap';
import {PageHeader} from '../../general/BootstrapComponents';
import {getRestServiceUrl} from '../../../utilities/global';
import ErrorAlert from '../../general/ErrorAlert';
import RepoCard from './RepoCard';
-import {IconRefresh} from "../../general/IconComponents";
+import {IconAdd, IconRefresh} from "../../general/IconComponents";
class RepoListView extends React.Component {
@@ -73,12 +74,12 @@
content = <React.Fragment>
<CardDeck>
- {this.state.repos.map(repo => {
- return <RepoCard
- key={repo.id}
- repo={repo}
- />;
- })}
+ {this.state.repos.map(repo => {
+ return <RepoCard
+ key={repo.id}
+ repo={repo}
+ />;
+ })}
</CardDeck>
</React.Fragment>;
@@ -95,6 +96,12 @@
</div>
</PageHeader>
{content}
+ <br/>
+ <Link to={'/repo/configure'}
+ className={'btn btn-outline-primary'}
+ >
+ <IconAdd/>
+ </Link>
</React.Fragment>;
};
diff --git a/borgbutler-webapp/src/containers/WebApp.jsx b/borgbutler-webapp/src/containers/WebApp.jsx
index 73d1b82..1aeed96 100644
--- a/borgbutler-webapp/src/containers/WebApp.jsx
+++ b/borgbutler-webapp/src/containers/WebApp.jsx
@@ -17,6 +17,7 @@
import Footer from '../components/views/footer/Footer';
import {loadVersion} from '../actions';
import {getTranslation} from '../utilities/i18n';
+import ConfigureRepoPage from '../components/views/repos/ConfigureRepoPage';
class WebApp extends React.Component {
@@ -24,11 +25,11 @@
componentDidMount = () => {
this.props.loadVersion();
- this.interval = setInterval(() => this.fetchJobStatistics(), 5000);
+ this.interval = setInterval(() => this.fetchSystemInfo(), 5000);
};
- fetchJobStatistics = () => {
- fetch(getRestServiceUrl('jobs/statistics'), {
+ fetchSystemInfo = () => {
+ fetch(getRestServiceUrl('system/info'), {
method: 'GET',
headers: {
'Accept': 'application/json'
@@ -37,7 +38,7 @@
.then(response => response.json())
.then(json => {
this.setState({
- statistics: json
+ systemInfo: json
});
})
.catch();
@@ -45,15 +46,20 @@
render() {
let jobsBadge = '';
- if (this.state && this.state.statistics && this.state.statistics.numberOfRunningAndQueuedJobs > 0) {
- jobsBadge = <Badge color="danger" pill>{this.state.statistics.numberOfRunningAndQueuedJobs}</Badge>;
+ const statistics = (this.state && this.state.systemInfo) ? this.state.systemInfo.queueStatistics : null;
+ if (statistics && statistics.numberOfRunningAndQueuedJobs > 0) {
+ jobsBadge = <Badge color="danger" pill>{statistics.numberOfRunningAndQueuedJobs}</Badge>;
+ }
+ let configurationBadge = '';
+ if (this.state && this.state.systemInfo && !this.state.systemInfo.configurationOK) {
+ configurationBadge = <Badge color="danger" pill>!</Badge>;
}
let routes = [
['Start', '/', Start],
['Repositories', '/repos', RepoListView],
['Job monitor', '/jobmonitor', JobMonitorView, {badge: jobsBadge}],
[getTranslation('logviewer'), '/logging', LogPage],
- [getTranslation('configuration'), '/config', ConfigurationPage]
+ [getTranslation('configuration'), '/config', ConfigurationPage, {badge: configurationBadge}]
];
if (isDevelopmentMode()) {
@@ -76,8 +82,9 @@
/>
))
}
- <Route path={'/repoArchives/:id'} component={RepoArchiveListView}/>
+ <Route path={'/repoArchives/:id/:displayName'} component={RepoArchiveListView} />
<Route path={'/archives/:repoId/:archiveId/'} component={ArchiveView} />
+ <Route path={'/repo/configure'} component={ConfigureRepoPage} />
</Switch>
</div>
<Footer versionInfo={this.props.version}/>
diff --git a/borgbutler-webapp/src/css/my-style.css b/borgbutler-webapp/src/css/my-style.css
index 352ebd6..2b8e86a 100644
--- a/borgbutler-webapp/src/css/my-style.css
+++ b/borgbutler-webapp/src/css/my-style.css
@@ -2,6 +2,19 @@
color: #50555f;
}
+.hidden {
+ display: none;
+}
+
+.command-line {
+ font-family: monospace;
+ white-space: pre;
+ border-style: solid;
+ background-color: black;
+ color: white;
+ padding-left: 5pt;
+}
+
a:hover {
color: #47a7eb;
}
@@ -175,6 +188,10 @@
color: #47a7eb;
}
+ul.nav-tabs a.active {
+ font-weight: bold;
+}
+
.tab-pane.active {
padding: 20px;
}
--
Gitblit v1.10.0