From 8290a1feaf4bec966be00919cbe5963d4b6e3867 Mon Sep 17 00:00:00 2001
From: Kai Reinhard <k.reinhard@micromata.de>
Date: Mon, 14 Jan 2019 16:23:37 +0000
Subject: [PATCH] Merge pull request #18 from kreinhard/master

---
 borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-12_01-00.json.gz |    0 
 borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java                          |    6 
 README.adoc                                                                                      |    4 
 borgbutler-core/README.adoc                                                                      |   16 +
 borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java                               |   12 
 borgbutler-core/src/main/java/de/micromata/borgbutler/data/DiffFileSystemFilter.java             |   19 
 borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobResult.java                        |    2 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java          |    9 
 borgbutler-webapp/src/components/views/archives/FileListEntry.jsx                                |   56 +++
 borgbutler-webapp/src/components/views/archives/FileListPanel.jsx                                |    6 
 borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java                        |  149 +++++++++++
 borgbutler-core/src/main/java/de/micromata/borgbutler/config/ConfigurationHandler.java           |    4 
 borgbutler-core/src/main/java/de/micromata/borgbutler/json/borg/BorgFilesystemItem.java          |    3 
 borgbutler-core/src/main/java/de/micromata/borgbutler/data/FileSystemFilter.java                 |  169 +++++++-----
 borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-13_01-00.json.gz |    0 
 borgbutler-core/src/main/resources/demodata/repo-list.json.gz                                    |    0 
 borgbutler-webapp/src/components/views/archives/FileListTable.jsx                                |    5 
 borgbutler-core/src/main/java/de/micromata/borgbutler/config/Configuration.java                  |   23 +
 borgbutler-core/src/test/java/de/micromata/borgbutler/DiffFileSystemFilterTest.java              |   28 +-
 borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx                         |   10 
 borgbutler-webapp/src/components/views/archives/ArchiveView.jsx                                  |    2 
 borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-13_01-00.json.gz |    0 
 borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx                             |   94 ++++---
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ArchivesRest.java            |   31 +-
 borgbutler-webapp/src/components/general/IconComponents.jsx                                      |   14 
 borgbutler-core/demo/createFiles.sh                                                              |   70 +++++
 borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommand.java                           |    2 
 borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/AbstractCommandLineJob.java           |    4 
 borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-12_01-00.json.gz |    0 
 borgbutler-core/src/main/java/de/micromata/borgbutler/cache/ButlerCache.java                     |    3 
 borgbutler-core/src/main/resources/demodata/repo-info.json.gz                                    |    0 
 31 files changed, 546 insertions(+), 195 deletions(-)

diff --git a/README.adoc b/README.adoc
index fe4ffb5..0d20c2a 100644
--- a/README.adoc
+++ b/README.adoc
@@ -77,6 +77,10 @@
 3. `npm start` (opens the web browser on port 3000)
 4. Start `de.micromata.borgbutler.server.Main` (ignore the opened browser window for port 9042)
 
+=== Profiling heap, cpu and everything using JProfiler
+JProfiler is an excellent tool for analysing your software. BorgButler was optimized regarding heap memory and CPU usage by
+using https://www.ej-technologies.com/products/jprofiler/overview.html[JProfiler from EJ Technologies^]
+
 == Ideas
 === 2 factor authentication
 https://github.com/j256/two-factor-auth
diff --git a/borgbutler-core/README.adoc b/borgbutler-core/README.adoc
new file mode 100644
index 0000000..8589582
--- /dev/null
+++ b/borgbutler-core/README.adoc
@@ -0,0 +1,16 @@
+Micromata BorgBackup-Butler
+===========================
+Micromata GmbH, Kai Reinhard
+:toc:
+:toclevels: 4
+
+Copyright (C) 2019
+
+ifdef::env-github,env-browser[:outfilesuffix: .adoc]
+
+== Development
+=== Creating test data
+1. Install virtual debian system
+2. `apt install net-tools curl`
+3. Execute script `./createFiles.sh` on debian host (borg is installed automatically)
+4. See the result files in `out.tar`.
\ No newline at end of file
diff --git a/borgbutler-core/demo/createFiles.sh b/borgbutler-core/demo/createFiles.sh
new file mode 100755
index 0000000..56232ee
--- /dev/null
+++ b/borgbutler-core/demo/createFiles.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+export BORG_PASSPHRASE='borgbutler123'
+export BORG_COMMAND='/root/bin/borg-linux64'
+export TEST_DIR='/root/borgbutler-demo'
+
+if [ -f $BORG_COMMAND ]; then
+  echo Borg command already exists...
+else
+  echo Downloading borg;
+  mkdir /root/bin
+  cd /root/bin
+  curl -LJO https://github.com/borgbackup/borg/releases/download/1.1.8/borg-linux64
+  chmod 700 $BORG_COMMAND
+fi;
+
+echo Creating backup dir /backup-test...
+rm -rf /backup-test
+mkdir /backup-test
+
+echo Initializing borg backup...
+$BORG_COMMAND init --encryption=repokey /backup-test
+
+function backup() {
+echo Creating backup...
+$BORG_COMMAND create --filter AME                    \
+                     --stats                         \
+                     --progress                      \
+                     --show-rc                       \
+                     --compression lz4               \
+                     --exclude-caches                \
+                     /backup-test::borgbutlerdemo-$1 \
+                     /home /root /etc /usr/bin /usr/sbin /opt
+}
+
+rm -rf $TEST_DIR
+mkdir $TEST_DIR
+cd $TEST_DIR
+touch README.txt
+chmod 700 README.txt
+echo `ls /usr` > filelist
+touch oldfile
+
+backup 2019-01-12_01-00
+
+rm oldfile
+mkdir newDir
+touch newDir/newfile
+chown borgbutler.users README.txt
+chmod 755 README.txt
+echo `ls /` >> filelist
+
+backup 2019-01-13_01-00
+
+cd /root
+rm -rf out
+mkdir out
+cd out
+$BORG_COMMAND info --json /backup-test >repo-info.json
+$BORG_COMMAND list --json /backup-test >repo-list.json
+
+$BORG_COMMAND info --json /backup-test::borgbutlerdemo-2019-01-12_01-00 >archive-info-borgbuterldemo-2019-01-12_01-00.json
+$BORG_COMMAND info --json /backup-test::borgbutlerdemo-2019-01-13_01-00 >archive-info-borgbuterldemo-2019-01-13_01-00.json
+
+$BORG_COMMAND list --json-lines /backup-test::borgbutlerdemo-2019-01-12_01-00 >archive-list-borgbuterldemo-2019-01-12_01-00.json
+$BORG_COMMAND list --json-lines /backup-test::borgbutlerdemo-2019-01-13_01-00 >archive-list-borgbuterldemo-2019-01-13_01-00.json
+
+gzip -9 *
+cd /root
+tar cvf out.tar out
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommand.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommand.java
index 7198ef7..4ddc54d 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommand.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommand.java
@@ -55,7 +55,7 @@
         return this;
     }
 
-    String getRepoArchive() {
+    public String getRepoArchive() {
         if (archive == null) {
             if (repoConfig == null) {
                 return null;
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 a5c6355..f9ed76e 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgCommands.java
@@ -3,6 +3,7 @@
 import de.micromata.borgbutler.config.BorgRepoConfig;
 import de.micromata.borgbutler.data.Archive;
 import de.micromata.borgbutler.data.Repository;
+import de.micromata.borgbutler.demo.DemoRepos;
 import de.micromata.borgbutler.jobs.JobResult;
 import de.micromata.borgbutler.json.JsonUtils;
 import de.micromata.borgbutler.json.borg.*;
@@ -72,6 +73,7 @@
                 .setEncryption(repoInfo.getEncryption())
                 .setSecurityDir(repoInfo.getSecurityDir())
                 .setLastCacheRefresh(DateUtils.format(LocalDateTime.now()));
+        DemoRepos.repoWasRead(repoConfig, repository);
         return repository;
     }
 
@@ -178,7 +180,7 @@
                 .setTotal(archive.getStats().getNfiles());
         BorgJob<List<BorgFilesystemItem>> job = BorgQueueExecutor.getInstance().execute(new BorgJob<List<BorgFilesystemItem>>(command) {
             @Override
-            protected void processStdOutLine(String line, int level) {
+            public void processStdOutLine(String line, int level) {
                 BorgFilesystemItem item = JsonUtils.fromJson(BorgFilesystemItem.class, line);
                 item.setMtime(DateUtils.format(item.getMtime()));
                 payload.add(item);
@@ -190,7 +192,7 @@
         });
         job.payload = new ArrayList<>();
         JobResult<String> jobResult = job.getResult();
-        if (jobResult == null ||jobResult.getStatus() != JobResult.Status.OK) {
+        if (jobResult == null || jobResult.getStatus() != JobResult.Status.OK) {
             return null;
         }
         List<BorgFilesystemItem> items = job.payload;
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 38d496a..707cc8a 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/BorgJob.java
@@ -3,7 +3,9 @@
 import de.micromata.borgbutler.config.BorgRepoConfig;
 import de.micromata.borgbutler.config.ConfigurationHandler;
 import de.micromata.borgbutler.data.Archive;
+import de.micromata.borgbutler.demo.DemoRepos;
 import de.micromata.borgbutler.jobs.AbstractCommandLineJob;
+import de.micromata.borgbutler.jobs.JobResult;
 import de.micromata.borgbutler.json.JsonUtils;
 import de.micromata.borgbutler.json.borg.ProgressInfo;
 import lombok.AccessLevel;
@@ -70,7 +72,7 @@
         return commandLine;
     }
 
-    protected void processStdErrLine(String line, int level) {
+    public void processStdErrLine(String line, int level) {
         try {
             if (StringUtils.startsWith(line, "{\"message")) {
                 ProgressInfo message = JsonUtils.fromJson(ProgressInfo.class, line);
@@ -103,6 +105,14 @@
     }
 
     @Override
+    public JobResult<String> execute() {
+        if (DemoRepos.isDemo(command.getRepoConfig().getRepo())) {
+            return DemoRepos.execute(this);
+        }
+        return super.execute();
+    }
+
+    @Override
     public BorgJob<?> clone() {
         BorgJob<?> clone = new BorgJob<>();
         if (command != null) {
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 716e360..2c45d9c 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
@@ -278,12 +278,9 @@
                 if (CollectionUtils.isNotEmpty(list)) {
                     archiveFilelistCache.save(repoConfig, archive, list);
                     items = new ArrayList<>();
-                    int fileNumber = -1;
                     Iterator<BorgFilesystemItem> it = list.iterator(); // Don't use for-each (ConcurrentModificationException)
                     while (it.hasNext()) {
                         BorgFilesystemItem item = it.next();
-                        ++fileNumber;
-                        item.setFileNumber(fileNumber);
                         if (filter == null || filter.matches(item)) {
                             items.add(item);
                             if (filter != null && filter.isFinished()) break;
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 28af84c..d06e30b 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
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import de.micromata.borgbutler.demo.DemoRepos;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Setter;
@@ -32,6 +33,10 @@
     @Getter
     private int maxArchiveContentCacheCapacityMb = 100;
 
+    @Getter
+    @Setter
+    private boolean showDemoRepos = true;
+
     /**
      * Default is restore inside BorgButler's home dir (~/.borgbutler/restore).
      */
@@ -41,7 +46,6 @@
     @JsonIgnore
     private File restoreHomeDir;
 
-    @Getter
     private List<BorgRepoConfig> repoConfigs = new ArrayList<>();
 
     public void add(BorgRepoConfig repoConfig) {
@@ -52,7 +56,7 @@
         if (idOrName == null) {
             return null;
         }
-        for (BorgRepoConfig repoConfig : repoConfigs) {
+        for (BorgRepoConfig repoConfig : getRepoConfigs()) {
             if (StringUtils.equals(idOrName, repoConfig.getRepo()) || StringUtils.equals(idOrName, repoConfig.getId())) {
                 return repoConfig;
             }
@@ -77,5 +81,20 @@
     public void copyFrom(Configuration other) {
         this.borgCommand = other.borgCommand;
         this.maxArchiveContentCacheCapacityMb = other.maxArchiveContentCacheCapacityMb;
+        this.showDemoRepos = other.showDemoRepos;
+    }
+
+    public List<BorgRepoConfig> getRepoConfigs() {
+        if (!ConfigurationHandler.getConfiguration().isShowDemoRepos()) {
+            return repoConfigs;
+        }
+        List<BorgRepoConfig> result = new ArrayList<>();
+        result.addAll(repoConfigs);
+        DemoRepos.addDemoRepos(result);
+        return result;
+    }
+
+    List<BorgRepoConfig> _getRepoConfigs() {
+        return repoConfigs;
     }
 }
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/ConfigurationHandler.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/ConfigurationHandler.java
index 41bb53c..baad873 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/config/ConfigurationHandler.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/config/ConfigurationHandler.java
@@ -48,8 +48,8 @@
                 }
             }
             this.configuration = JsonUtils.fromJson(configClazz, json);
-            if (this.configuration.getRepoConfigs() != null) {
-                for (BorgRepoConfig repoConfig : this.configuration.getRepoConfigs()) {
+            if (this.configuration._getRepoConfigs() != null) {
+                for (BorgRepoConfig repoConfig : this.configuration._getRepoConfigs()) {
                     if (StringUtils.isBlank(repoConfig.getDisplayName())) {
                         repoConfig.setDisplayName(repoConfig.getRepo());
                     }
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/DiffTool.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/data/DiffFileSystemFilter.java
similarity index 75%
rename from borgbutler-core/src/main/java/de/micromata/borgbutler/DiffTool.java
rename to borgbutler-core/src/main/java/de/micromata/borgbutler/data/DiffFileSystemFilter.java
index a0a16a1..a9b6423 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/DiffTool.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/data/DiffFileSystemFilter.java
@@ -1,4 +1,4 @@
-package de.micromata.borgbutler;
+package de.micromata.borgbutler.data;
 
 import de.micromata.borgbutler.json.borg.BorgFilesystemItem;
 import org.slf4j.Logger;
@@ -11,15 +11,15 @@
 /**
  * Extracts the differences between two archives of one repo.
  */
-public class DiffTool {
-    private static Logger log = LoggerFactory.getLogger(DiffTool.class);
+public class DiffFileSystemFilter extends FileSystemFilter {
+    private Logger log = LoggerFactory.getLogger(DiffFileSystemFilter.class);
 
     /**
      * @param items      Sorted list of items from the current archive.
      * @param otherItems Sorted list of items of the archive to extract differences.
      * @return A list of differing items (new, removed and modified ones).
      */
-    public static List<BorgFilesystemItem> extractDifferences(List<BorgFilesystemItem> items, List<BorgFilesystemItem> otherItems) {
+    public List<BorgFilesystemItem> extractDifferences(List<BorgFilesystemItem> items, List<BorgFilesystemItem> otherItems) {
         List<BorgFilesystemItem> currentList = items != null ? items : new ArrayList<>();
         List<BorgFilesystemItem> otherList = otherItems != null ? otherItems : new ArrayList<>();
         List<BorgFilesystemItem> result = new ArrayList<>();
@@ -37,7 +37,8 @@
             }
             int cmp = current.compareTo(other);
             if (cmp == 0) { // Items represents both the same file system item.
-                if (current.equals(other)) {
+                if (!checkDirectoryMatchAndRegisterSubDirectories(current) ||
+                        current.equals(other)) {
                     current = other = null; // increment both iterators.
                     continue;
                 }
@@ -48,10 +49,14 @@
                 result.add(current);
                 current = other = null; // increment both iterators.
             } else if (cmp < 0) {
-                result.add(current.setDiffStatus(BorgFilesystemItem.DiffStatus.NEW));
+                if (checkDirectoryMatchAndRegisterSubDirectories(current)) {
+                    result.add(current.setDiffStatus(BorgFilesystemItem.DiffStatus.NEW));
+                }
                 current = currentIt.hasNext() ? currentIt.next() : null;
             } else {
-                result.add(other.setDiffStatus(BorgFilesystemItem.DiffStatus.REMOVED));
+                if (checkDirectoryMatchAndRegisterSubDirectories(other)) {
+                    result.add(other.setDiffStatus(BorgFilesystemItem.DiffStatus.REMOVED));
+                }
                 other = otherIt.hasNext() ? otherIt.next() : null;
             }
         }
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/data/FileSystemFilter.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/data/FileSystemFilter.java
index cd971c3..6f8e431 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/data/FileSystemFilter.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/data/FileSystemFilter.java
@@ -3,8 +3,6 @@
 import de.micromata.borgbutler.json.borg.BorgFilesystemItem;
 import lombok.Getter;
 import lombok.Setter;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -57,16 +55,8 @@
             }
             return false;
         }
-        if (mode == Mode.TREE) {
-            // In this run only register all direct childs of currentDirectory.
-            String topLevelDir = getTopLevel(item.getPath());
-            if (topLevelDir == null) {
-                // item is not inside the current directory.
-                return false;
-            }
-            if (!subDirectories.containsKey(topLevelDir)) {
-                subDirectories.put(topLevelDir, item);
-            }
+        if (!checkDirectoryMatchAndRegisterSubDirectories(item)) {
+            return false;
         }
         if (searchKeyWords == null && blackListSearchKeyWords == null) {
             processFinishedFlag();
@@ -103,79 +93,47 @@
      * @return The original list for mode {@link Mode#FLAT} or the reduced list for the tree view.
      */
     public List<BorgFilesystemItem> reduce(List<BorgFilesystemItem> list) {
-        if (mode == FileSystemFilter.Mode.TREE) {
-            if (MapUtils.isEmpty(subDirectories)) {
-                // If matches was not called before, do this now for getting all subdirectories.
-                subDirectories = new HashMap<>();
-                for (BorgFilesystemItem item : list) {
-                    // Needed for building subdirectories...
-                    this.matches(item);
-                }
+        if (mode != FileSystemFilter.Mode.TREE) {
+            return list;
+        }
+        Set<String> set = new HashSet<>();
+        List<BorgFilesystemItem> list2 = list;
+        list = new ArrayList<>();
+        for (BorgFilesystemItem item : list2) {
+            String topLevel = getTopLevel(item.getPath());
+            if (topLevel == null) {
+                continue;
             }
-            Set<String> set = new HashSet<>();
-            List<BorgFilesystemItem> list2 = list;
-            list = new ArrayList<>();
-            for (BorgFilesystemItem item : list2) {
-                String topLevel = getTopLevel(item.getPath());
-                if (topLevel == null) {
-                    continue;
-                }
-                if (set.contains(topLevel) == false) {
-                    set.add(topLevel);
-                    BorgFilesystemItem topItem = subDirectories.get(topLevel);
+            if (set.contains(topLevel) == false) {
+                set.add(topLevel);
+                BorgFilesystemItem topItem = subDirectories.get(topLevel);
+                if (topItem == null) {
+                    log.error("Internal error, can't find subDirectory: " + topLevel);
+                } else {
                     topItem.setDisplayPath(StringUtils.removeStart(topItem.getPath(), currentDirectory));
                     list.add(topItem);
                 }
             }
-            list2 = list;
-            // Re-ordering (show dot files at last)
-            list = new ArrayList<>();
-            // First add normal files:
-            for (BorgFilesystemItem item : list2) {
-                if (!item.getDisplayPath().startsWith(".")) {
-                    list.add(item);
-                }
+        }
+        list2 = list;
+        // Re-ordering (show dot files at last)
+        list = new ArrayList<>();
+        // First add normal files:
+        for (BorgFilesystemItem item : list2) {
+            if (!item.getDisplayPath().startsWith(".")) {
+                list.add(item);
             }
-            // Now add dot files:
-            for (BorgFilesystemItem item : list2) {
-                if (item.getDisplayPath().startsWith(".")) {
-                    list.add(item);
-                }
+        }
+        // Now add dot files:
+        for (BorgFilesystemItem item : list2) {
+            if (item.getDisplayPath().startsWith(".")) {
+                list.add(item);
             }
         }
         return list;
     }
 
     /**
-     * @param path The path of the current item.
-     * @return null if the item is not a child of the current directory otherwise the top level sub directory name of
-     * the current directory.
-     */
-    String getTopLevel(String path) {
-        if (StringUtils.isEmpty(currentDirectory)) {
-            int pos = path.indexOf('/');
-            if (pos < 0) {
-                return path;
-            }
-            return path.substring(0, pos);
-        }
-        if (!path.startsWith(currentDirectory)) {
-            // item is not a child of currentDirectory.
-            return null;
-        }
-        if (path.length() <= currentDirectory.length() + 1) {
-            // Don't show the current directory itself.
-            return null;
-        }
-        path = StringUtils.removeStart(path, currentDirectory);
-        int pos = path.indexOf('/');
-        if (pos < 0) {
-            return path;
-        }
-        return path.substring(0, pos);
-    }
-
-    /**
      * @param searchString The search string. If this string contains several key words separated by white chars,
      *                     all key words must be found.
      * @return this for chaining.
@@ -211,6 +169,71 @@
     }
 
     /**
+     * This method has only effect in FLAT view. This method has to be called for every item of the list before
+     * {@link #reduce(List)} may work, because this method registers sub directories of the current directory needed
+     * by {@link #reduce(List)}.
+     *
+     * @param item
+     * @return false, if the given item is not a sub item of the current directory. You may skip further checkings for this
+     * item. If true, this item might be part of the result.
+     */
+    protected boolean checkDirectoryMatchAndRegisterSubDirectories(BorgFilesystemItem item) {
+        if (mode != Mode.TREE) {
+            return true;
+        }
+        if (StringUtils.isNotEmpty(currentDirectory) && !item.getPath().startsWith(currentDirectory)) {
+            // item is not inside the current directory.
+            return false;
+        }
+        // In this run only register all direct childs of currentDirectory.
+        String topLevelDir = getTopLevel(item.getPath());
+        if (topLevelDir == null) {
+            // item is not inside the current directory.
+            return false;
+        }
+        if (!subDirectories.containsKey(topLevelDir)) {
+            subDirectories.put(topLevelDir, item);
+        }
+        return true;
+    }
+
+
+    /**
+     * currentDirectory '': <tt>home</tt> -&gt; <tt>home</tt><br>
+     * currentDirectory '': <tt>home/kai</tt> -&gt; <tt>home</tt><br>
+     * currentDirectory 'home': <tt>home</tt> -&gt; <tt>null</tt><br>
+     * currentDirectory 'home': <tt>home/kai</tt> -&gt; <tt>kai</tt><br>
+     * currentDirectory 'home': <tt>home/kai/test.java</tt> -&gt; <tt>kai</tt><br>
+     *
+     * @param path The path of the current item.
+     * @return null if the item is not a child of the current directory otherwise the top level sub directory name of
+     * the current directory.
+     */
+    String getTopLevel(String path) {
+        if (StringUtils.isEmpty(currentDirectory)) {
+            int pos = path.indexOf('/');
+            if (pos < 0) {
+                return path;
+            }
+            return path.substring(0, pos);
+        }
+        if (!path.startsWith(currentDirectory)) {
+            // item is not a child of currentDirectory.
+            return null;
+        }
+        if (path.length() <= currentDirectory.length() + 1) {
+            // Don't show the current directory itself.
+            return null;
+        }
+        path = StringUtils.removeStart(path, currentDirectory);
+        int pos = path.indexOf('/');
+        if (pos < 0) {
+            return path;
+        }
+        return path.substring(0, pos);
+    }
+
+    /**
      * @param mode
      * @return this for chaining.
      */
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
new file mode 100644
index 0000000..cc0bd61
--- /dev/null
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/demo/DemoRepos.java
@@ -0,0 +1,149 @@
+package de.micromata.borgbutler.demo;
+
+import de.micromata.borgbutler.BorgCommand;
+import de.micromata.borgbutler.BorgJob;
+import de.micromata.borgbutler.config.BorgRepoConfig;
+import de.micromata.borgbutler.config.ConfigurationHandler;
+import de.micromata.borgbutler.config.Definitions;
+import de.micromata.borgbutler.data.Repository;
+import de.micromata.borgbutler.jobs.JobResult;
+import de.micromata.borgbutler.json.JsonUtils;
+import de.micromata.borgbutler.json.borg.ProgressInfo;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+public class DemoRepos {
+    private enum Type {FAST, SLOW, VERY_SLOW}
+
+    private static Logger log = LoggerFactory.getLogger(DemoRepos.class);
+    private static final String DEMO_IDENTIFIER = "borgbutler-demo";
+
+    private static final String[] REPOS = {"fast", "slow", "very-slow"};
+    private static List<BorgRepoConfig> demoRepos;
+
+    /**
+     * If configured by the user, demo repositories are added to the given list. If not configured this method does nothing.
+     *
+     * @param repositoryList
+     */
+    public static void addDemoRepos(List<BorgRepoConfig> repositoryList) {
+        if (!ConfigurationHandler.getConfiguration().isShowDemoRepos()) {
+            return;
+        }
+        init();
+        for (BorgRepoConfig repo : demoRepos) {
+            repositoryList.add(repo);
+        }
+    }
+
+    public static boolean isDemo(String name) {
+        return StringUtils.startsWith(name, DEMO_IDENTIFIER);
+    }
+
+    public static void repoWasRead(BorgRepoConfig repoConfig, Repository repository) {
+        if (!isDemo(repository.getName())) {
+            return;
+        }
+        repository.setId(repository.getId() + "-" + REPOS[getType(repoConfig).ordinal()]);
+    }
+
+    public static JobResult<String> execute(BorgJob job) {
+        BorgCommand command = job.getCommand();
+        if (!StringUtils.equalsAny(command.getCommand(), "list", "info")) {
+            log.info("Commmand '" + command.getCommand() + "' not supported for demo repositories.");
+            return new JobResult<String>().setStatus(JobResult.Status.ERROR);
+        }
+        StringBuilder sb = new StringBuilder();
+        boolean archive = command.getArchive() != null;
+        if (archive) {
+            sb.append("archive-");
+        } else {
+            sb.append("repo-");
+        }
+        sb.append(command.getCommand());
+        if (archive) {
+            sb.append("-").append(command.getArchive());
+        }
+        sb.append(".json.gz");
+        int wait = 0;
+        Type type = getType(command.getRepoConfig());
+        if (type == Type.VERY_SLOW) {
+            wait = 10;
+        } else if (type == Type.SLOW) {
+            wait = 1;
+        }
+        String file = sb.toString();
+        log.info("Loading demo archive from '" + file + "'...");
+        try (InputStream inputStream = new GzipCompressorInputStream(DemoRepos.class.getResourceAsStream("/demodata/" + file))) {
+            if (wait > 0) {
+                ProgressInfo progress = new ProgressInfo()
+                        .setMessage("Faked demo progress")
+                        .setTotal(10 * wait);
+                for (int i = 0; i < 10 * wait; i++) {
+                    if (job.isCancellationRequested()) {
+                        break;
+                    }
+                    try {
+                        Thread.sleep(1000);
+                    } catch (InterruptedException ex) {
+                        // Do nothing.
+                    }
+                    job.processStdErrLine(JsonUtils.toJson(progress.setCurrent(i)), 0);
+                }
+            }
+            if (archive && "list".equals(command.getCommand())) {
+                try (Scanner scanner = new Scanner(inputStream)) {
+                    while (scanner.hasNextLine()) {
+                        String line = scanner.nextLine();
+                        job.processStdOutLine(line, 0);
+                    }
+                    return new JobResult<String>().setStatus(JobResult.Status.OK);
+                }
+            } else {
+                StringWriter writer = new StringWriter();
+                IOUtils.copy(inputStream, writer, Definitions.STD_CHARSET);
+                return new JobResult<String>().setResultObject(writer.toString()).setStatus(JobResult.Status.OK);
+            }
+        } catch (IOException ex) {
+            log.error("Error while reading demo file '" + file + "': " + ex.getMessage() + ".");
+            return null;
+        }
+    }
+
+    private static Type getType(BorgRepoConfig repoConfig) {
+        if (repoConfig.getRepo().endsWith("very-slow")) {
+            return Type.VERY_SLOW;
+        } else if (repoConfig.getRepo().endsWith("slow")) {
+            return Type.SLOW;
+        }
+        return Type.FAST;
+    }
+
+    private static void init() {
+        synchronized (DEMO_IDENTIFIER) {
+            if (demoRepos != null) {
+                return;
+            }
+            demoRepos = new ArrayList<>();
+            demoRepos.add(new BorgRepoConfig()
+                    .setRepo(DEMO_IDENTIFIER + "-fast")
+                    .setDisplayName("Demo repository fast"));
+            demoRepos.add(new BorgRepoConfig()
+                    .setRepo(DEMO_IDENTIFIER + "-slow")
+                    .setDisplayName("Demo repository slow"));
+            demoRepos.add(new BorgRepoConfig()
+                    .setRepo(DEMO_IDENTIFIER + "-very-slow")
+                    .setDisplayName("Demo repository very-slow"));
+        }
+    }
+}
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 bccf7ea..4daf718 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
@@ -101,7 +101,7 @@
         return result;
     }
 
-    protected void processStdOutLine(String line, int level) {
+    public void processStdOutLine(String line, int level) {
         //log.info(line);
         try {
             outputStream.write(line.getBytes());
@@ -111,7 +111,7 @@
         }
     }
 
-    protected void processStdErrLine(String line, int level) {
+    public void processStdErrLine(String line, int level) {
         //log.info(line);
         try {
             errorOutputStream.write(line.getBytes());
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobResult.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobResult.java
index f57bd9b..809e52f 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobResult.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/jobs/JobResult.java
@@ -7,7 +7,7 @@
 public class JobResult<T> {
     public enum Status {OK, ERROR}
     @Getter
-    @Setter(AccessLevel.PACKAGE)
+    @Setter
     private Status status;
     @Getter
     @Setter
diff --git a/borgbutler-core/src/main/java/de/micromata/borgbutler/json/borg/BorgFilesystemItem.java b/borgbutler-core/src/main/java/de/micromata/borgbutler/json/borg/BorgFilesystemItem.java
index 61f6381..8d03c47 100644
--- a/borgbutler-core/src/main/java/de/micromata/borgbutler/json/borg/BorgFilesystemItem.java
+++ b/borgbutler-core/src/main/java/de/micromata/borgbutler/json/borg/BorgFilesystemItem.java
@@ -66,7 +66,8 @@
      */
     @Getter
     @Setter
-    private int fileNumber;
+    private int fileNumber = -1;
+
     /**
      * If created by diff tool, this flag represents the type of difference.
      */
diff --git a/borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-12_01-00.json.gz b/borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-12_01-00.json.gz
new file mode 100644
index 0000000..769e79d
--- /dev/null
+++ b/borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-12_01-00.json.gz
Binary files differ
diff --git a/borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-13_01-00.json.gz b/borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-13_01-00.json.gz
new file mode 100644
index 0000000..69dd0f5
--- /dev/null
+++ b/borgbutler-core/src/main/resources/demodata/archive-info-borgbutlerdemo-2019-01-13_01-00.json.gz
Binary files differ
diff --git a/borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-12_01-00.json.gz b/borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-12_01-00.json.gz
new file mode 100644
index 0000000..00cad35
--- /dev/null
+++ b/borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-12_01-00.json.gz
Binary files differ
diff --git a/borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-13_01-00.json.gz b/borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-13_01-00.json.gz
new file mode 100644
index 0000000..c9321b9
--- /dev/null
+++ b/borgbutler-core/src/main/resources/demodata/archive-list-borgbutlerdemo-2019-01-13_01-00.json.gz
Binary files differ
diff --git a/borgbutler-core/src/main/resources/demodata/repo-info.json.gz b/borgbutler-core/src/main/resources/demodata/repo-info.json.gz
new file mode 100644
index 0000000..c8bba90
--- /dev/null
+++ b/borgbutler-core/src/main/resources/demodata/repo-info.json.gz
Binary files differ
diff --git a/borgbutler-core/src/main/resources/demodata/repo-list.json.gz b/borgbutler-core/src/main/resources/demodata/repo-list.json.gz
new file mode 100644
index 0000000..c60b152
--- /dev/null
+++ b/borgbutler-core/src/main/resources/demodata/repo-list.json.gz
Binary files differ
diff --git a/borgbutler-core/src/test/java/de/micromata/borgbutler/DiffToolTest.java b/borgbutler-core/src/test/java/de/micromata/borgbutler/DiffFileSystemFilterTest.java
similarity index 88%
rename from borgbutler-core/src/test/java/de/micromata/borgbutler/DiffToolTest.java
rename to borgbutler-core/src/test/java/de/micromata/borgbutler/DiffFileSystemFilterTest.java
index 5fd2940..23ff366 100644
--- a/borgbutler-core/src/test/java/de/micromata/borgbutler/DiffToolTest.java
+++ b/borgbutler-core/src/test/java/de/micromata/borgbutler/DiffFileSystemFilterTest.java
@@ -1,5 +1,6 @@
 package de.micromata.borgbutler;
 
+import de.micromata.borgbutler.data.DiffFileSystemFilter;
 import de.micromata.borgbutler.json.borg.BorgFilesystemItem;
 import org.junit.jupiter.api.Test;
 
@@ -9,7 +10,7 @@
 
 import static org.junit.jupiter.api.Assertions.*;
 
-public class DiffToolTest {
+public class DiffFileSystemFilterTest {
     @Test
     void differencesTest() {
         BorgFilesystemItem i1 = create("etc", true, "drwx------", 0, "2018-11-21");
@@ -28,33 +29,34 @@
         List<BorgFilesystemItem> l1 = null;
         List<BorgFilesystemItem> l2 = null;
         List<BorgFilesystemItem> result;
-        assertEquals(0, DiffTool.extractDifferences(l1, l2).size());
+        DiffFileSystemFilter filter = new DiffFileSystemFilter();
+        assertEquals(0, filter.extractDifferences(l1, l2).size());
         l1 = create();
-        result = DiffTool.extractDifferences(l1, l2);
+        result = filter.extractDifferences(l1, l2);
         assertEquals(7, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(1).getDiffStatus());
-        result = DiffTool.extractDifferences(l2, l1);
+        result = filter.extractDifferences(l2, l1);
         assertEquals(7, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(1).getDiffStatus());
 
         l1 = create();
         l2 = create();
-        result = DiffTool.extractDifferences(l2, l1);
+        result = filter.extractDifferences(l2, l1);
         assertEquals(0, result.size());
         remove(l2, "etc"); // 0
         remove(l2, "etc/passwd"); // 1
         remove(l1, "home/kai/.borgbutler/borgbutler-config-bak.json"); // 2
         get(l1, "home/kai/.borgbutler/borgbutler-config.json").setSize(712).setMtime("2018-11-22"); // 3
-        result = DiffTool.extractDifferences(l1, l2);
+        result = filter.extractDifferences(l1, l2);
         assertEquals(4, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(1).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(2).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.MODIFIED, result.get(3).getDiffStatus());
 
-        result = DiffTool.extractDifferences(l2, l1);
+        result = filter.extractDifferences(l2, l1);
         assertEquals(4, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(1).getDiffStatus());
@@ -66,12 +68,12 @@
         remove(l2, "etc"); // 0
         remove(l2, "etc/passwd"); // 1
         remove(l1, "home/kai/.borgbutler/borgbutler-config.json"); // 2
-        result = DiffTool.extractDifferences(l1, l2);
+        result = filter.extractDifferences(l1, l2);
         assertEquals(3, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(1).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(2).getDiffStatus());
-        result = DiffTool.extractDifferences(l2, l1);
+        result = filter.extractDifferences(l2, l1);
         assertEquals(3, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(1).getDiffStatus());
@@ -82,11 +84,11 @@
         l2 = create();
         remove(l1, "home/kai/.borgbutler/borgbutler-config-bak.json");
         remove(l2, "home/kai/.borgbutler/borgbutler-config.json");
-        result = DiffTool.extractDifferences(l1, l2);
+        result = filter.extractDifferences(l1, l2);
         assertEquals(2, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(1).getDiffStatus());
-        result = DiffTool.extractDifferences(l2, l1);
+        result = filter.extractDifferences(l2, l1);
         assertEquals(2, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(1).getDiffStatus());
@@ -96,12 +98,12 @@
         remove(l1, "home/kai");
         remove(l1, "home/kai/.borgbutler");
         remove(l2, "home/kai/.borgbutler/borgbutler-config-bak.json");
-        result = DiffTool.extractDifferences(l1, l2);
+        result = filter.extractDifferences(l1, l2);
         assertEquals(3, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.REMOVED, result.get(1).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(2).getDiffStatus());
-        result = DiffTool.extractDifferences(l2, l1);
+        result = filter.extractDifferences(l2, l1);
         assertEquals(3, result.size());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(0).getDiffStatus());
         assertEquals(BorgFilesystemItem.DiffStatus.NEW, result.get(1).getDiffStatus());
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 d3f0ad8..ce67240 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
@@ -1,12 +1,9 @@
 package de.micromata.borgbutler.server;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import de.micromata.borgbutler.cache.ButlerCache;
 import de.micromata.borgbutler.config.Configuration;
 import de.micromata.borgbutler.config.ConfigurationHandler;
-import lombok.Getter;
-import lombok.Setter;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -15,16 +12,11 @@
     private static Logger log = LoggerFactory.getLogger(ServerConfiguration.class);
     private final static String[] SUPPORTED_LANGUAGES = {"en", "de"};
     public static final int WEBSERVER_PORT_DEFAULT = 9042;
-    private static final boolean SHOW_TEST_DATA_PREF_DEFAULT = false;
     private static final boolean WEB_DEVELOPMENT_MODE_PREF_DEFAULT = false;
 
     private static String applicationHome;
 
     private int port = WEBSERVER_PORT_DEFAULT;
-    @Getter
-    @Setter
-    @JsonIgnore
-    private boolean showTestData = SHOW_TEST_DATA_PREF_DEFAULT;
     private boolean webDevelopmentMode = WEB_DEVELOPMENT_MODE_PREF_DEFAULT;
     @JsonProperty
     public String getCacheDir() {
@@ -72,7 +64,6 @@
     public void copyFrom(ServerConfiguration other) {
         super.copyFrom(other);
         this.port = other.port;
-        this.showTestData = other.showTestData;
         this.webDevelopmentMode = other.webDevelopmentMode;
     }
 }
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 b24774b..582715e 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
@@ -1,11 +1,11 @@
 package de.micromata.borgbutler.server.rest;
 
 import de.micromata.borgbutler.BorgCommands;
-import de.micromata.borgbutler.DiffTool;
 import de.micromata.borgbutler.cache.ButlerCache;
 import de.micromata.borgbutler.config.BorgRepoConfig;
 import de.micromata.borgbutler.config.ConfigurationHandler;
 import de.micromata.borgbutler.data.Archive;
+import de.micromata.borgbutler.data.DiffFileSystemFilter;
 import de.micromata.borgbutler.data.FileSystemFilter;
 import de.micromata.borgbutler.data.Repository;
 import de.micromata.borgbutler.json.JsonUtils;
@@ -77,29 +77,30 @@
                                      @QueryParam("diffArchiveId") String diffArchiveId,
                                      @QueryParam("force") boolean force,
                                      @QueryParam("prettyPrinter") boolean prettyPrinter) {
+        boolean diffMode = StringUtils.isNotBlank(diffArchiveId);
         int maxSize = NumberUtils.toInt(maxResultSize, 50);
-        FileSystemFilter filter = new FileSystemFilter()
-                .setSearchString(searchString)
-                .setMaxResultSize(maxSize)
-                .setMode(mode)
+        FileSystemFilter filter = diffMode ? new DiffFileSystemFilter() : new FileSystemFilter();
+        filter.setSearchString(searchString)
                 .setCurrentDirectory(currentDirectory);
         List<BorgFilesystemItem> items = null;
-        if (StringUtils.isBlank(diffArchiveId)) {
+        if (diffMode) {
+            filter.setMode(FileSystemFilter.Mode.FLAT);
+            items = ButlerCache.getInstance().getArchiveContent(archiveId, true, filter);
+            List<BorgFilesystemItem> diffItems = ButlerCache.getInstance().getArchiveContent(diffArchiveId, true,
+                    filter);
+            filter.setMaxResultSize(maxSize)
+                    .setMode(mode);
+            items = ((DiffFileSystemFilter) filter).extractDifferences(items, diffItems);
+            items = filter.reduce(items);
+        } else {
+            filter.setMode(mode)
+                    .setMaxResultSize(maxSize);
             // Get file list (without running diff).
             items = ButlerCache.getInstance().getArchiveContent(archiveId, force,
                     filter);
             if (items == null) {
                 return "[{\"mode\": \"notLoaded\"}]";
             }
-        } else {
-            filter.setMode(FileSystemFilter.Mode.FLAT).setMaxResultSize(-1);
-            items = ButlerCache.getInstance().getArchiveContent(archiveId, true, filter);
-            List<BorgFilesystemItem> diffItems = ButlerCache.getInstance().getArchiveContent(diffArchiveId, true,
-                    filter);
-            items = DiffTool.extractDifferences(items, diffItems);
-            filter.setMaxResultSize(maxSize)
-                    .setMode(mode);
-            items = filter.reduce(items);
         }
         return JsonUtils.toJson(items, prettyPrinter);
     }
diff --git a/borgbutler-webapp/src/components/general/IconComponents.jsx b/borgbutler-webapp/src/components/general/IconComponents.jsx
index 1bb6474..f8707cf 100644
--- a/borgbutler-webapp/src/components/general/IconComponents.jsx
+++ b/borgbutler-webapp/src/components/general/IconComponents.jsx
@@ -1,5 +1,6 @@
 import React from 'react';
 import {
+    faBan,
     faCaretDown,
     faCaretUp,
     faCheck,
@@ -8,12 +9,12 @@
     faExclamationTriangle,
     faInfoCircle,
     faPlus,
+    faSkullCrossbones,
     faSortDown,
     faSortUp,
     faSync,
-    faTrash,
     faTimes,
-    faSkullCrossbones,
+    faTrash,
     faUpload
 } from '@fortawesome/free-solid-svg-icons'
 import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@@ -24,6 +25,12 @@
     );
 }
 
+function IconBan() {
+    return (
+        <FontAwesomeIcon icon={faBan}/>
+    );
+}
+
 function IconCancel() {
     return (
         <FontAwesomeIcon icon={faTimes}/>
@@ -80,7 +87,7 @@
 
 function IconSpinner() {
     return (
-        <FontAwesomeIcon icon={faCircleNotch} spin={true} size={'3x'} color={'#aaaaaa'} />
+        <FontAwesomeIcon icon={faCircleNotch} spin={true} size={'3x'} color={'#aaaaaa'}/>
     );
 }
 
@@ -110,6 +117,7 @@
 
 export {
     IconAdd,
+    IconBan,
     IconCancel,
     IconCheck,
     IconCollapseClose,
diff --git a/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx b/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
index 437a84c..3a32ffc 100644
--- a/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
+++ b/borgbutler-webapp/src/components/views/archives/ArchiveView.jsx
@@ -99,7 +99,7 @@
                     <TabPane tabId={'1'}>
                         <FileListPanel
                             repoId={this.state.repoId}
-                            archiveId={archive.id}
+                            archive={archive}
                             archiveShortInfoList={archive.archiveShortInfoList}
                         />
                     </TabPane>
diff --git a/borgbutler-webapp/src/components/views/archives/FileListEntry.jsx b/borgbutler-webapp/src/components/views/archives/FileListEntry.jsx
index 5f7f9cd..38dab55 100644
--- a/borgbutler-webapp/src/components/views/archives/FileListEntry.jsx
+++ b/borgbutler-webapp/src/components/views/archives/FileListEntry.jsx
@@ -2,19 +2,24 @@
 import PropTypes from 'prop-types';
 import Highlight from 'react-highlighter';
 import {Button, UncontrolledTooltip} from 'reactstrap';
-import {IconCheck, IconDownload} from '../../general/IconComponents';
+import {IconBan, IconCheck, IconDownload} from '../../general/IconComponents';
 import {getResponseHeaderFilename, getRestServiceUrl, humanFileSize} from '../../../utilities/global';
 import fileDownload from 'js-file-download';
 
 class FileListEntry extends React.Component {
 
     state = {
-        downloaded: false
+        thisDownloaded: false,
+        otherDownloaded: false
     };
 
-    download(archiveId, fileNumber) {
+    download(archiveId, fileNumber, thisDownload) {
         let filename;
-        this.setState({downloaded: true});
+        if (thisDownload) {
+            this.setState({thisDownloaded: true});
+        } else {
+            this.setState({otherDownloaded: true});
+        }
         fetch(getRestServiceUrl('archives/restore', {
             archiveId: archiveId,
             fileNumber: fileNumber
@@ -42,7 +47,6 @@
 
     render = () => {
         const entry = this.props.entry;
-        let downloadArchiveId = this.props.archiveId;
         let displayPath = entry.displayPath;
         let pathCss = 'tt';
 
@@ -61,14 +65,46 @@
         let mtimeCss = 'tt';
         let mtimeTooltip = undefined;
         let mtimeId = undefined;
+        let iconBan = <div className={'btn'}><IconBan/></div>;
+        let iconCheck = <div className={'btn'}><IconCheck/></div>;
+
+        let icon1 = iconCheck;
+        let icon1Tooltip = '';
+        if (!this.state.thisDownloaded) {
+            const icon1Id = `icon1-${entry.fileNumber}`;
+            icon1 = <div id={icon1Id} className={'btn'}
+                         onClick={() => this.download(this.props.archive.id, entry.fileNumber, true)}>
+                <IconDownload/></div>;
+            icon1Tooltip = <UncontrolledTooltip target={icon1Id}>
+                {this.props.archive.time}
+            </UncontrolledTooltip>;
+        }
+        let icon2 = '';
+        let icon2Tooltip = '';
+        if (this.props.diffArchiveId) {
+            icon2 = iconCheck;
+            if (!this.state.otherDownloaded) {
+                const icon2Id = `icon2-${entry.fileNumber}`;
+                icon2 =
+                    <div id={icon2Id} className={'btn'}
+                         onClick={() => this.download(this.props.diffArchiveId, entry.fileNumber, false)}>
+                        <IconDownload/></div>;
+                icon2Tooltip = <UncontrolledTooltip target={icon2Id}>
+                    other
+                </UncontrolledTooltip>;
+            }
+        }
         if (entry.diffStatus === 'NEW') {
             pathCss = 'tt file-new';
             pathtooltipText = 'NEW';
+            icon2 = iconBan;
+            icon2Tooltip = '';
         } else if (entry.diffStatus === 'REMOVED') {
             pathCss = 'tt file-removed';
             // Download removed files from other archive.
-            downloadArchiveId = this.props.diffArchiveId;
             pathtooltipText = 'REMOVED';
+            icon1 = iconBan;
+            icon1Tooltip = '';
         } else if (entry.diffStatus === 'MODIFIED') {
             if (entry.differences) {
                 pathCss = 'tt file-modified';
@@ -100,7 +136,7 @@
             }
         }
         if (pathtooltipText) {
-            pathId = `path-${entry.fileNumber}`;
+            pathId = `path-${entry.fileNumber}-${entry.diffStatus}`;
             pathTooltip =
                 <UncontrolledTooltip target={pathId}>
                     {pathtooltipText}
@@ -113,16 +149,13 @@
         } else {
             path = <Highlight search={this.props.search} id={pathId}>{displayPath}</Highlight>;
         }
-        let icon = this.state.downloaded ? <IconCheck/> :
-            <div className={'btn'} onClick={() => this.download(downloadArchiveId, entry.fileNumber)}>
-                <IconDownload/></div>;
         return (
             <tr>
                 <td className={pathCss}>
                     {path}{pathTooltip}
                 </td>
                 <td className={'tt'} style={{textAlign: 'center'}}>
-                    {icon}
+                    {icon1}{icon1Tooltip} {icon2}{icon2Tooltip}
                 </td>
                 <td className={sizeCss} style={{textAlign: 'center'}}>
                     <span id={sizeId}>{humanFileSize(entry.size, true, true)}</span>{sizeTooltip}
@@ -149,6 +182,7 @@
     entry: PropTypes.shape({}).isRequired,
     search: PropTypes.string,
     mode: PropTypes.string,
+    diffArchiveId: PropTypes.string,
     changeCurrentDirectory: PropTypes.func.isRequired
 };
 
diff --git a/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx b/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx
index af47cc7..3f8b3b7 100644
--- a/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx
+++ b/borgbutler-webapp/src/components/views/archives/FileListPanel.jsx
@@ -51,7 +51,7 @@
             failed: false
         });
         fetch(getRestServiceUrl('archives/filelist', {
-            archiveId: this.props.archiveId,
+            archiveId: this.props.archive.id,
             diffArchiveId: this.state.filter.diffArchiveId,
             force: force,
             searchString: this.state.filter.search,
@@ -128,12 +128,12 @@
                             event.preventDefault();
                             this.fetchArchiveFileList();
                         }}
-                        currentArchiveId={this.props.archiveId}
+                        currentArchiveId={this.props.archive.id}
                         archiveShortInfoList={this.props.archiveShortInfoList}
                     />
                     {breadcrumb}
                     <FileListTable
-                        archiveId={this.props.archiveId}
+                        archive={this.props.archive}
                         diffArchiveId={this.state.filter.diffArchiveId}
                         entries={this.state.fileList}
                         search={this.state.filter.search}
diff --git a/borgbutler-webapp/src/components/views/archives/FileListTable.jsx b/borgbutler-webapp/src/components/views/archives/FileListTable.jsx
index 8b03adc..710ffa6 100644
--- a/borgbutler-webapp/src/components/views/archives/FileListTable.jsx
+++ b/borgbutler-webapp/src/components/views/archives/FileListTable.jsx
@@ -3,7 +3,7 @@
 import {Table} from 'reactstrap';
 import FileListEntry from './FileListEntry';
 
-function FileListTable({archiveId, diffArchiveId, entries, search, mode, changeCurrentDirectory}) {
+function FileListTable({archive, diffArchiveId, entries, search, mode, changeCurrentDirectory}) {
     const lowercaseSearch = search.split(' ')[0].toLowerCase();
     return (
         <Table striped bordered hover size={'sm'} responsive>
@@ -19,7 +19,7 @@
             <tbody>
             {entries
                 .map((entry, index) => <FileListEntry
-                    archiveId={archiveId}
+                    archive={archive}
                     diffArchiveId={diffArchiveId}
                     entry={entry}
                     search={lowercaseSearch}
@@ -33,7 +33,6 @@
 }
 
 FileListTable.propTypes = {
-    archiveId: PropTypes.string,
     diffArchiveId: PropTypes.string,
     entries: PropTypes.array,
     search: PropTypes.string,
diff --git a/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx b/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
index 827a47c..84f0e2d 100644
--- a/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
+++ b/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
@@ -45,6 +45,7 @@
             failed: false,
             port: 9042,
             webdevelopmentMode: false,
+            showDemoRepos: true,
             maxArchiveContentCacheCapacityMb: 100,
             redirect: false,
             confirmModal: false
@@ -73,7 +74,8 @@
         var config = {
             port: this.state.port,
             maxArchiveContentCacheCapacityMb : this.state.maxArchiveContentCacheCapacityMb,
-            webDevelopmentMode: this.state.webDevelopmentMode
+            webDevelopmentMode: this.state.webDevelopmentMode,
+            showDemoRepos: this.state.showDemoRepos
         };
         return fetch(getRestServiceUrl("configuration/config"), {
             method: 'POST',
@@ -140,6 +142,12 @@
                                          onChange={this.handleTextChange}
                                          placeholder="Enter maximum Capacity"
                                          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} />
+                    </FormLabelField>
                     <FormLabelField label={<I18n name={'configuration.webDevelopmentMode'} />} fieldLength={2}>
                         <FormCheckbox checked={this.state.webDevelopmentMode}
                                       hintKey={'configuration.webDevelopmentMode.hint'}
diff --git a/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx b/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
index 81cbe43..32b1b91 100644
--- a/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
+++ b/borgbutler-webapp/src/components/views/repos/RepoArchiveListView.jsx
@@ -77,6 +77,56 @@
                     <IconRefresh/>
                 </div>
             </React.Fragment>;
+            let stats = '';
+            if (repo.cache && repo.cache.stats) {
+                stats = <tr>
+                    <td>Stats</td>
+                    <td>
+                        <table className="inline">
+                            <tbody>
+                            <tr>
+                                <td>Total chunks</td>
+                                <td>{Number(repo.cache.stats.total_chunks).toLocaleString()}</td>
+                            </tr>
+                            <tr>
+                                <td>Total csize</td>
+                                <td>{humanFileSize(repo.cache.stats.total_csize)}</td>
+                            </tr>
+                            <tr>
+                                <td>Total size</td>
+                                <td>{humanFileSize(repo.cache.stats.total_size)}</td>
+                            </tr>
+                            <tr>
+                                <td>Total unique chunks</td>
+                                <td>{Number(repo.cache.stats.total_unique_chunks).toLocaleString()}</td>
+                            </tr>
+                            <tr>
+                                <td>Unique csize</td>
+                                <td>{humanFileSize(repo.cache.stats.unique_csize)}</td>
+                            </tr>
+                            <tr>
+                                <td>Unique size</td>
+                                <td>{humanFileSize(repo.cache.stats.unique_size)}</td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </td>
+                </tr>
+            }
+            let encryption = '';
+            if (repo.encryption) {
+                encryption = <tr>
+                    <td>Encryption</td>
+                    <td>{repo.encryption.mode}</td>
+                </tr>
+            }
+            let cachePath = '';
+            if (repo.cache) {
+                cachePath = <tr>
+                    <td>Cache</td>
+                    <td>{repo.cache.path}</td>
+                </tr>
+            }
             content = <React.Fragment>
                 <Nav tabs>
                     <NavLink
@@ -134,51 +184,13 @@
                                 <td>Location</td>
                                 <td>{repo.location}</td>
                             </tr>
-                            <tr>
-                                <td>Stats</td>
-                                <td>
-                                    <table className="inline">
-                                        <tbody>
-                                        <tr>
-                                            <td>Total chunks</td>
-                                            <td>{Number(repo.cache.stats.total_chunks).toLocaleString()}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>Total csize</td>
-                                            <td>{humanFileSize(repo.cache.stats.total_csize)}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>Total size</td>
-                                            <td>{humanFileSize(repo.cache.stats.total_size)}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>Total unique chunks</td>
-                                            <td>{Number(repo.cache.stats.total_unique_chunks).toLocaleString()}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>Unique csize</td>
-                                            <td>{humanFileSize(repo.cache.stats.unique_csize)}</td>
-                                        </tr>
-                                        <tr>
-                                            <td>Unique size</td>
-                                            <td>{humanFileSize(repo.cache.stats.unique_size)}</td>
-                                        </tr>
-                                        </tbody>
-                                    </table>
-                                </td>
-                            </tr>
+                            {stats}
                             <tr>
                                 <td>Security dir</td>
                                 <td>{repo.securityDir}</td>
                             </tr>
-                            <tr>
-                                <td>Encryption</td>
-                                <td>{repo.encryption.mode}</td>
-                            </tr>
-                            <tr>
-                                <td>Cache</td>
-                                <td>{repo.cache.path}</td>
-                            </tr>
+                            {encryption}
+                            {cachePath}
                             </tbody>
                         </Table>
                     </TabPane>

--
Gitblit v1.10.0