diff --git a/.gitignore b/.gitignore
index 72fc038c68e37795481943bec1f487767fa4d0a7..9a0dbf0d00a130dd9010fb23e0dbf6703bbfa201 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,7 @@
 
 # Node artifact files
 node_modules/
-dist/
+build/
 
 # Compiled Java class files
 *.class
diff --git a/package-lock.json b/package-lock.json
index 08e2fbb1ebb9eae559d2097f37b4779463c9840a..20679537934e19bff13eeb457af4d8c6aca20a0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1219,6 +1219,11 @@
       "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
       "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
     },
+    "@emotion/hash": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
+      "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
+    },
     "@eslint/eslintrc": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
@@ -1831,6 +1836,108 @@
         }
       }
     },
+    "@material-ui/core": {
+      "version": "4.11.3",
+      "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz",
+      "integrity": "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==",
+      "requires": {
+        "@babel/runtime": "^7.4.4",
+        "@material-ui/styles": "^4.11.3",
+        "@material-ui/system": "^4.11.3",
+        "@material-ui/types": "^5.1.0",
+        "@material-ui/utils": "^4.11.2",
+        "@types/react-transition-group": "^4.2.0",
+        "clsx": "^1.0.4",
+        "hoist-non-react-statics": "^3.3.2",
+        "popper.js": "1.16.1-lts",
+        "prop-types": "^15.7.2",
+        "react-is": "^16.8.0 || ^17.0.0",
+        "react-transition-group": "^4.4.0"
+      }
+    },
+    "@material-ui/icons": {
+      "version": "4.11.2",
+      "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
+      "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
+      "requires": {
+        "@babel/runtime": "^7.4.4"
+      }
+    },
+    "@material-ui/lab": {
+      "version": "4.0.0-alpha.57",
+      "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz",
+      "integrity": "sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==",
+      "requires": {
+        "@babel/runtime": "^7.4.4",
+        "@material-ui/utils": "^4.11.2",
+        "clsx": "^1.0.4",
+        "prop-types": "^15.7.2",
+        "react-is": "^16.8.0 || ^17.0.0"
+      }
+    },
+    "@material-ui/styles": {
+      "version": "4.11.3",
+      "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz",
+      "integrity": "sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==",
+      "requires": {
+        "@babel/runtime": "^7.4.4",
+        "@emotion/hash": "^0.8.0",
+        "@material-ui/types": "^5.1.0",
+        "@material-ui/utils": "^4.11.2",
+        "clsx": "^1.0.4",
+        "csstype": "^2.5.2",
+        "hoist-non-react-statics": "^3.3.2",
+        "jss": "^10.5.1",
+        "jss-plugin-camel-case": "^10.5.1",
+        "jss-plugin-default-unit": "^10.5.1",
+        "jss-plugin-global": "^10.5.1",
+        "jss-plugin-nested": "^10.5.1",
+        "jss-plugin-props-sort": "^10.5.1",
+        "jss-plugin-rule-value-function": "^10.5.1",
+        "jss-plugin-vendor-prefixer": "^10.5.1",
+        "prop-types": "^15.7.2"
+      },
+      "dependencies": {
+        "csstype": {
+          "version": "2.6.16",
+          "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz",
+          "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q=="
+        }
+      }
+    },
+    "@material-ui/system": {
+      "version": "4.11.3",
+      "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz",
+      "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==",
+      "requires": {
+        "@babel/runtime": "^7.4.4",
+        "@material-ui/utils": "^4.11.2",
+        "csstype": "^2.5.2",
+        "prop-types": "^15.7.2"
+      },
+      "dependencies": {
+        "csstype": {
+          "version": "2.6.16",
+          "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz",
+          "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q=="
+        }
+      }
+    },
+    "@material-ui/types": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
+      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A=="
+    },
+    "@material-ui/utils": {
+      "version": "4.11.2",
+      "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
+      "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
+      "requires": {
+        "@babel/runtime": "^7.4.4",
+        "prop-types": "^15.7.2",
+        "react-is": "^16.8.0 || ^17.0.0"
+      }
+    },
     "@nodelib/fs.scandir": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
@@ -4886,6 +4993,15 @@
         }
       }
     },
+    "css-vendor": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
+      "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
+      "requires": {
+        "@babel/runtime": "^7.8.3",
+        "is-in-browser": "^1.0.2"
+      }
+    },
     "css-what": {
       "version": "3.4.2",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz",
@@ -7905,6 +8021,11 @@
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
       "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="
     },
+    "hyphenate-style-name": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
+      "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
+    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -8010,6 +8131,14 @@
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
     },
+    "indefinite-observable": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz",
+      "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==",
+      "requires": {
+        "symbol-observable": "1.2.0"
+      }
+    },
     "indent-string": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@@ -8334,6 +8463,11 @@
         "is-extglob": "^2.1.1"
       }
     },
+    "is-in-browser": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
+      "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
+    },
     "is-module": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@@ -10344,6 +10478,85 @@
         "verror": "1.10.0"
       }
     },
+    "jss": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.1.tgz",
+      "integrity": "sha512-hbbO3+FOTqVdd7ZUoTiwpHzKXIo5vGpMNbuXH1a0wubRSWLWSBvwvaq4CiHH/U42CmjOnp6lVNNs/l+Z7ZdDmg==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "csstype": "^3.0.2",
+        "indefinite-observable": "^2.0.1",
+        "is-in-browser": "^1.1.3",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-camel-case": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.1.tgz",
+      "integrity": "sha512-9+oymA7wPtswm+zxVti1qiowC5q7bRdCJNORtns2JUj/QHp2QPXYwSNRD8+D2Cy3/CEMtdJzlNnt5aXmpS6NAg==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "hyphenate-style-name": "^1.0.3",
+        "jss": "10.5.1"
+      }
+    },
+    "jss-plugin-default-unit": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.1.tgz",
+      "integrity": "sha512-D48hJBc9Tj3PusvlillHW8Fz0y/QqA7MNmTYDQaSB/7mTrCZjt7AVRROExoOHEtd2qIYKOYJW3Jc2agnvsXRlQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.5.1"
+      }
+    },
+    "jss-plugin-global": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.1.tgz",
+      "integrity": "sha512-jX4XpNgoaB8yPWw/gA1aPXJEoX0LNpvsROPvxlnYe+SE0JOhuvF7mA6dCkgpXBxfTWKJsno7cDSCgzHTocRjCQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.5.1"
+      }
+    },
+    "jss-plugin-nested": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.1.tgz",
+      "integrity": "sha512-xXkWKOCljuwHNjSYcXrCxBnjd8eJp90KVFW1rlhvKKRXnEKVD6vdKXYezk2a89uKAHckSvBvBoDGsfZrldWqqQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.5.1",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-props-sort": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.1.tgz",
+      "integrity": "sha512-t+2vcevNmMg4U/jAuxlfjKt46D/jHzCPEjsjLRj/J56CvP7Iy03scsUP58Iw8mVnaV36xAUZH2CmAmAdo8994g==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.5.1"
+      }
+    },
+    "jss-plugin-rule-value-function": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.1.tgz",
+      "integrity": "sha512-3gjrSxsy4ka/lGQsTDY8oYYtkt2esBvQiceGBB4PykXxHoGRz14tbCK31Zc6DHEnIeqsjMUGbq+wEly5UViStQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.5.1",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-vendor-prefixer": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.1.tgz",
+      "integrity": "sha512-cLkH6RaPZWHa1TqSfd2vszNNgxT1W0omlSjAd6hCFHp3KIocSrW21gaHjlMU26JpTHwkc+tJTCQOmE/O1A4FKQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "css-vendor": "^2.0.8",
+        "jss": "10.5.1"
+      }
+    },
     "jsx-ast-utils": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz",
@@ -11959,6 +12172,11 @@
         "ts-pnp": "^1.1.6"
       }
     },
+    "popper.js": {
+      "version": "1.16.1-lts",
+      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
+      "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA=="
+    },
     "portfinder": {
       "version": "1.0.28",
       "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -15561,6 +15779,11 @@
         "util.promisify": "~1.0.0"
       }
     },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+    },
     "symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
diff --git a/package.json b/package.json
index 32c471e8f5c6c6afe3b9f0e59fff1aa6a240e8fe..5df80d9a0a8690ce63494409bd631d6e9f581209 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,9 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@material-ui/core": "4.11.3",
+    "@material-ui/icons": "4.11.2",
+    "@material-ui/lab": "4.0.0-alpha.57",
     "bootstrap": "4.5",
     "flexlayout-react": "0.5.5",
     "jszip": "3.2.0",
diff --git a/src/components/experiment-files-viewer/experiment-files-viewer.css b/src/components/experiment-files-viewer/experiment-files-viewer.css
new file mode 100644
index 0000000000000000000000000000000000000000..6469a06a9079ab846f365ea26fe8519b0bf045b4
--- /dev/null
+++ b/src/components/experiment-files-viewer/experiment-files-viewer.css
@@ -0,0 +1,136 @@
+.experiment-files-viewer-wrapper {
+    height: 100vh;
+    margin: 20px;
+    display: grid;
+    gap: 15px;
+    grid-template-rows: 110px auto;
+    grid-template-columns: 40% auto;
+    grid-template-areas:
+        "local-directory-picker selected-file-info"
+        "experiment-list experiment-files";
+}
+
+.grid-element {
+    border: 1px solid lightgray;
+}
+
+.grid-element-header {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    padding: 5px 5px 5px 15px;
+    margin-bottom: 5px;
+    width: 100%;
+    background-color: #048cb4;
+    color: white;
+    font-size: 1.1em;
+    font-weight: bold;
+}
+
+.grid-element-header-buttons {
+    font-size: 0.8em;
+    background-color: white;
+}
+
+.local-directory-picker {
+    grid-area: local-directory-picker;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+}
+
+.elements-local-directory {
+    display: flex;
+    flex-direction: column;
+    padding: 10px;
+}
+
+.local-directory-name {
+    padding: 0px 10px 0px 10px;
+    margin: 0px 10px 0px 10px;
+    border: 1px solid black;
+    min-width: 250px;
+}
+  
+.experiment-list {
+    grid-area: experiment-list;
+}
+
+.experiment-files-list {
+    padding-left: 5px;
+}
+
+.experiment-files-list li {
+    list-style-type: none;
+    margin: 2px 5px 2px 5px;
+    padding-left: 10px;
+    border-bottom: 1px solid lightgray;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+
+.experiment-files-list li:hover {
+    background-color: lightblue;
+}
+
+.experiments-li-selected {
+    border-left: 5px solid #048cb4;
+}
+
+.experiments-li-disabled {
+    color: gray;
+}
+
+.experiment-li-buttons {
+    justify-self: flex-end;
+    min-width: 76px;
+}
+  
+.experiment-files {
+    grid-area: experiment-files;
+}
+
+.selected-file-info {
+    grid-area: selected-file-info;
+    padding: 10px;
+    overflow: auto;
+}
+
+.fileinfo-group {
+    padding-bottom: 5px;
+}
+
+.fileinfo-entry {
+    padding-left: 5px;
+}
+
+.fileinfo-name {
+    font-weight: bold;
+}
+
+.treeview {
+    flex-grow: 1;
+    max-width: 400;
+    padding-left: 10px;
+}
+
+.experiment-file-local {
+    color: black;
+}
+
+.file-local-changes {
+    color: darkorange;
+}
+
+.file-dirty {
+    color: red;
+}
+
+.file-local-only {
+    color: green;
+}
+
+.file-server-only {
+    color: lightgray;
+}
\ No newline at end of file
diff --git a/src/components/experiment-files-viewer/experiment-files-viewer.js b/src/components/experiment-files-viewer/experiment-files-viewer.js
new file mode 100644
index 0000000000000000000000000000000000000000..222c1600500a988be4b9c8fbc92ad449d1c465bb
--- /dev/null
+++ b/src/components/experiment-files-viewer/experiment-files-viewer.js
@@ -0,0 +1,279 @@
+import React from 'react';
+import { FaDownload, FaUpload, FaFolderOpen, FaTrash } from 'react-icons/fa';
+import { IoSyncCircleOutline, IoSyncCircleSharp } from 'react-icons/io5';
+import TreeView from '@material-ui/lab/TreeView';
+import TreeItem from '@material-ui/lab/TreeItem';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+
+import RemoteExperimentFilesService from '../../services/experiments/files/remote-experiment-files-service';
+//import ExperimentFilesTree from './experiment-files-tree';
+
+import './experiment-files-viewer.css';
+
+export default class ExperimentFilesViewer extends React.Component {
+  constructor() {
+    super();
+
+    this.state = {
+      selectedExperiment: undefined,
+      selectedFilepaths: undefined,
+      autoSync: RemoteExperimentFilesService.instance.autoSync
+    };
+  }
+
+  /**
+   * Handles select events on the file tree.
+   * @param {Event} event - select event
+   * @param {Array} nodeIds - tree node IDs, in this case we use file relative paths (= server file UUID)
+   */
+  handleFileTreeSelect(event, nodeIds) {
+    this.setState({selectedFilepaths: nodeIds});
+  }
+
+  /**
+   * JSX for the file hierarchie of experiments.
+   * @param {Object} file - A file/folder with children to be displayed
+   * @returns {JSX} The JSX elements
+   */
+  renderFileTree(file) {
+    let className = '';
+    if (file.hasLocalChanges) {
+      className += ' file-local-changes';
+    }
+    if (file.isOutOfSync) {
+      className += ' file-dirty';
+    }
+    if (file.localOnly) {
+      className += ' file-local-only';
+    }
+    if (!RemoteExperimentFilesService.instance.mapLocalFiles.has(file.relativePath)) {
+      className += ' file-server-only';
+    }
+    className = className.trim();
+
+    return (
+      <TreeItem key={file.relativePath} nodeId={file.relativePath} label={file.name}
+        className={className}>
+        {Array.isArray(file.children) ? file.children.map((subfile) => this.renderFileTree(subfile)) : null}
+      </TreeItem>);
+  }
+
+  getExperimentsListItemClass(experiment) {
+    let className = '';
+    if (this.state.selectedExperiment && this.state.selectedExperiment.id === experiment.id) {
+      className += ' experiments-li-selected';
+    }
+    if (!RemoteExperimentFilesService.instance.mapFileInfos.has(experiment.uuid)) {
+      className += ' experiments-li-disabled';
+    }
+
+    return className.trim();
+  }
+
+  getInfoText() {
+    return (<div>
+      {this.state.selectedFilepaths && this.state.selectedFilepaths.map(filePath => {
+        let fileInfo = RemoteExperimentFilesService.instance.mapFileInfos.get(filePath);
+        if (fileInfo) {
+          return (<div key={fileInfo.relativePath} className="fileinfo-group">
+            <div className="fileinfo-name">{fileInfo.name}</div>
+            {fileInfo.msgWarning && <div className="fileinfo-entry">{'Warning: ' + fileInfo.msgWarning}</div>}
+            {fileInfo.msgError && <div className="fileinfo-entry">{'Error: ' + fileInfo.msgError}</div>}
+            {fileInfo.localOnly && <div className="fileinfo-entry">{'File exists only locally.'}</div>}
+            {fileInfo.serverOnly &&
+            <div className="fileinfo-entry">{'File exists only on server.'}</div>}
+            {fileInfo.hasLocalChanges &&
+            <div className="fileinfo-entry">{'File has local changes not synced with server.'}</div>}
+            {fileInfo.isOutOfSync &&
+            <div className="fileinfo-entry">{'File on server has newer changes.'}</div>}
+          </div>);
+        }
+        else {
+          return null;
+        }
+      })}
+    </div>);
+  }
+
+  render() {
+    let selectedExperimentFiles = this.state.selectedExperiment ?
+      RemoteExperimentFilesService.instance.mapFileInfos.get(this.state.selectedExperiment.uuid) : undefined;
+
+    return (
+      <div>
+        {RemoteExperimentFilesService.instance.isSupported() ?
+          <div className='experiment-files-viewer-wrapper'>
+            {/* choose the local parent directory for experiment files */}
+            <div className='grid-element local-directory-picker'>
+              <div className='grid-element-header'>
+                <div>Local working directory</div>
+                <div  className='grid-element-header-buttons'>
+                  <button className='nrp-btn'
+                    onClick={() => {
+                      RemoteExperimentFilesService.instance.chooseLocalSyncDirectory();
+                    }}
+                    title='This is your local directory to work in. All your experiment files will go here.
+                      Usually, you would want to always pick the same folder.
+                      If you have previously chosen a different folder, any changes made there will not be listed.'
+                  >
+                    <FaFolderOpen />
+                  </button>
+                </div>
+
+              </div>
+
+              <div className='elements-local-directory'>
+                <div></div>
+                <div className='local-directory-name'>
+                  {RemoteExperimentFilesService.instance.localSyncDirectoryHandle ?
+                    <span>
+                      {RemoteExperimentFilesService.instance.localSyncDirectoryHandle.name}
+                    </span>
+                    : <span style={{color: 'gray'}}>Please choose local folder you want to work in.</span>}
+                </div>
+
+              </div>
+            </div>
+
+            {/* list of experiments */}
+            <div className='grid-element experiment-list'>
+              <div className='grid-element-header'>
+                <div>Experiments</div>
+                <div className='grid-element-header-buttons'>
+                  <button className='nrp-btn'
+                    onClick={() => {
+                      RemoteExperimentFilesService.instance.toggleAutoSync();
+                      this.setState({autoSync: RemoteExperimentFilesService.instance.autoSync});
+                    }}
+                    title={this.state.autoSync ? 'Auto sync: ON' : 'Auto sync: OFF'}
+                  >
+                    {RemoteExperimentFilesService.instance.autoSync ?
+                      <IoSyncCircleSharp /> : <IoSyncCircleOutline />}
+                  </button>
+                </div>
+              </div>
+
+              <ol className='experiment-files-list'>
+                {this.props.experiments.map(experiment => {
+                  let experimentServerFiles = RemoteExperimentFilesService.instance
+                    .mapServerFiles.get(experiment.uuid);
+                  let experimentLocalFiles = RemoteExperimentFilesService.instance.mapLocalFiles.get(experiment.uuid);
+
+                  return (
+                    <li key={experiment.id || experiment.configuration.id}
+                      className={this.getExperimentsListItemClass(experiment)}
+                      onClick={() => {
+                        if (experimentLocalFiles) {
+                          this.setState({
+                            selectedExperiment: experiment,
+                            selectedFilepaths: undefined
+                          });
+                        }
+                      }}>
+                      {experiment.configuration.name}
+                      <div className='experiment-li-buttons'>
+                        <button className='nrp-btn'
+                          disabled={!RemoteExperimentFilesService.instance.localSyncDirectoryHandle
+                            || RemoteExperimentFilesService.instance.autoSync}
+                          onClick={() => {
+                            RemoteExperimentFilesService.instance.downloadExperimentToLocalFS(experiment);
+                          }}
+                          title='Download all experiment files (will OVERWRITE unsaved local changes)'
+                        >
+                          <FaDownload />
+                        </button>
+                        <button className='nrp-btn'
+                          disabled={!experimentServerFiles
+                            || !RemoteExperimentFilesService.instance.mapFileInfos.has(experiment.uuid)
+                            || RemoteExperimentFilesService.instance.autoSync}
+                          onClick={() => {
+                            RemoteExperimentFilesService.instance.uploadExperimentFromLocalFS(experiment);
+                          }}
+                          title='Upload all experiment files'
+                        >
+                          <FaUpload />
+                        </button>
+                      </div>
+                    </li>
+                  );
+                })}
+              </ol>
+            </div>
+
+            {/* file structure for selected experiment */}
+            <div className='grid-element experiment-files'>
+              <div className='grid-element-header'>
+                <div>Experiment Files</div>
+                <div className='grid-element-header-buttons'>
+                  <button className='nrp-btn' title='Download selected'
+                    disabled={!this.state.selectedFilepaths || this.state.selectedFilepaths.length === 0
+                      || RemoteExperimentFilesService.instance.autoSync}
+                    onClick={() =>
+                      RemoteExperimentFilesService.instance.downloadExperimentFileList(this.state.selectedFilepaths)}>
+                    <FaDownload />
+                  </button>
+                  <button className='nrp-btn' title='Upload selected'
+                    disabled={!this.state.selectedFilepaths || this.state.selectedFilepaths.length === 0
+                      || RemoteExperimentFilesService.instance.autoSync}
+                    onClick={() =>
+                      RemoteExperimentFilesService.instance.uploadExperimentFileList(this.state.selectedFilepaths)}>
+                    <FaUpload />
+                  </button>
+                  <button className='nrp-btn' title='Delete selected'
+                    disabled={!this.state.selectedFilepaths || this.state.selectedFilepaths.length === 0
+                      || RemoteExperimentFilesService.instance.autoSync}
+                    onClick={() =>
+                      RemoteExperimentFilesService.instance.deleteExperimentFileList(this.state.selectedFilepaths)}>
+                    <FaTrash />
+                  </button>
+                </div>
+              </div>
+
+              <div>
+                {selectedExperimentFiles ?
+                  <TreeView
+                    multiSelect
+                    className="treeview"
+                    defaultCollapseIcon={<ExpandMoreIcon />}
+                    defaultExpandIcon={<ChevronRightIcon />}
+                    defaultExpanded={this.props.experiments.map(experiment => experiment.uuid)}
+                    onNodeSelect={(event, nodeIds) => {
+                      this.handleFileTreeSelect(event, nodeIds);
+                    }}
+                  >
+                    {this.renderFileTree(selectedExperimentFiles)}
+                  </TreeView>
+                  : <span style={{margin: '20px'}}>Please select an experiment on the left first.</span>
+                }
+              </div>
+            </div>
+
+            {/* info for selected file */}
+            <div className='grid-element selected-file-info'>
+              {this.getInfoText()}
+            </div>
+          </div>
+
+          /* error notification for browser other than chrome */
+          : <div>
+            File System Access API is a working draft and not supported by this browser at the moment.
+            Please try one of the supporting browsers.
+            <br />
+            <a target='_blank' rel='noreferrer'
+              href='https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API'>
+              File System Access API
+            </a>
+            <br />
+            <a target='_blank' rel='noreferrer' href='https://wicg.github.io/file-system-access/'>
+              W3C Draft
+            </a>
+            <br />
+            <a target='_blank' rel='noreferrer' href='https://caniuse.com/?search=file%20system%20access'>
+              caniuse.com
+            </a>
+          </div>}
+      </div>
+    );
+  }
+}
diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js
index afa1f1becfd18912770efcf1a67559d89472b2a9..43bc7ff7a7580d9f6e21c9921b06952316e014c7 100644
--- a/src/components/experiment-overview/experiment-overview.js
+++ b/src/components/experiment-overview/experiment-overview.js
@@ -10,6 +10,7 @@ import ExperimentExecutionService from '../../services/experiments/execution/exp
 import ImportExperimentButtons from '../experiment-list/import-experiment-buttons.js';
 import ExperimentList from '../experiment-list/experiment-list.js';
 import NrpHeader from '../nrp-header/nrp-header.js';
+import ExperimentFilesViewer from '../experiment-files-viewer/experiment-files-viewer.js';
 
 import './experiment-overview.css';
 
@@ -144,7 +145,7 @@ export default class ExperimentOverview extends React.Component {
           </TabPanel>
           {/* Experiment Files */}
           <TabPanel>
-            <h2>"Experiment Files" tab coming soon ...</h2>
+            <ExperimentFilesViewer experiments={this.state.storageExperiments}/>
           </TabPanel>
           {/* Templates */}
           <TabPanel>
diff --git a/src/components/main.css b/src/components/main.css
index cb0a265943e91b921da9d548b419be8b956765b8..404747e79aae8232ef495bb6a300bceae38c1aa4 100644
--- a/src/components/main.css
+++ b/src/components/main.css
@@ -1,4 +1,9 @@
 .nrp-btn {
     padding: 5px 10px 5px 10px;
     border: 1px solid lightgray;
+}
+
+.nrp-btn-small {
+    width: 25px;
+    height: 25px;
 }
\ No newline at end of file
diff --git a/src/services/experiments/execution/__tests__/experiment-execution-service.test.js b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js
index f34a08dce1e9e5b5d9c118160e5c970872182e71..dbd8d11eb512ffe8701bf1a4cfd0a10d0b89c185 100644
--- a/src/services/experiments/execution/__tests__/experiment-execution-service.test.js
+++ b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js
@@ -78,7 +78,6 @@ test('should go through the list of available servers when trying to start an ex
     MockAvailableServers.forEach(server => {
       expect(ServerResourcesService.instance.getServerConfig).toHaveBeenCalledWith(server.id);
     });
-    expect(console.error).toHaveBeenCalled();
     done();
   });
 });
diff --git a/src/services/experiments/execution/__tests__/running-simulation-service.test.js b/src/services/experiments/execution/__tests__/running-simulation-service.test.js
index 9492c9fa6d1d1a2ea97af49449db67f08df23e7d..7af3b6e72eb1dbc0f5b53d53d0a9ffd960e99855 100644
--- a/src/services/experiments/execution/__tests__/running-simulation-service.test.js
+++ b/src/services/experiments/execution/__tests__/running-simulation-service.test.js
@@ -166,7 +166,7 @@ test('can retrieve the state of a simulation', async () => {
 
 test('can set the state of a simulation', async () => {
   let returnValuePUT = undefined;
-  jest.spyOn(ErrorHandlerService.instance, 'updateSimuationError').mockImplementation();
+  jest.spyOn(ErrorHandlerService.instance, 'updateSimulationError').mockImplementation();
   jest.spyOn(RunningSimulationService.instance, 'httpRequestPUT').mockImplementation(() => {
     if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) {
       returnValuePUT = {};
diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js
index a02538e76345527b61f35ea8e75d8b790b107553..0cf994dac08ab916701831c86cfe1e71e75fe1f8 100644
--- a/src/services/experiments/files/experiment-storage-service.js
+++ b/src/services/experiments/files/experiment-storage-service.js
@@ -147,25 +147,29 @@ class ExperimentStorageService extends HttpService {
 
   /**
    * Gets an experiment file from the storage.
-   * @param {string} experimentName - name of the experiment
+   * @param {string} experimentDirectoryPath - path of experiment folder + possibly subfolders
    * @param {string} filename - name of the file
    * @param {Boolean} byName - whether to check for the file by name or not
    *
    * @returns the file contents (as a request object)
    */
-  async getFile(experimentName, filename, byName = false) {
-    const url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${experimentName}/${filename}?byname=${byName}`;
+  async getFile(experimentDirectoryPath, filename, byName = false) {
+    let directory = experimentDirectoryPath.replaceAll('/', '%2F');
+    let file = filename.replaceAll('/', '%2F');
+    const url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}/${file}?byname=${byName}`;
     return this.httpRequestGET(url);
   }
 
   /**
    * Gets the list of the experiment files from the storage.
-   * @param {string} experimentName - name of the experiment
+   * @param {string} experimentDirectoryUUID - name of the experiment
+   * @param {string} subFolder - relative path to a subfolder from which to get files
    *
    * @returns {Array} the list of experiment files
    */
-  async getExperimentFiles(experimentName) {
-    const url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${experimentName}`;
+  async getExperimentFiles(directoryPath) {
+    let directory = directoryPath.replaceAll('/', '%2F');
+    let url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}`;
     const files = await (await this.httpRequestGET(url)).json();
     return files;
   }
@@ -246,24 +250,27 @@ class ExperimentStorageService extends HttpService {
    *
    * @returns the request object containing the status code
    */
-  async setFile(experimentName, filename, data, byname = true, contentType = 'text/plain') {
-    const url = new URL(`${config.api.proxy.url}${endpoints.proxy.storage.url}/${experimentName}/${filename}`);
+  async setFile(directoryPath, filename, data, byname = true, contentType = 'text/plain') {
+    let directory = directoryPath.replaceAll('/', '%2F');
+    const url = new URL(`${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}/${filename}`);
+    //console.info(url);
     url.searchParams.append('byname', byname);
 
     let requestOptions = {
       ...this.POSTOptions, ...{ headers: { 'Content-Type': contentType } }
     };
+    //console.info(requestOptions);
 
     if (contentType === 'text/plain') {
-      return this.httpRequestPOST(url, requestOptions, data);
+      return this.httpRequestPOST(url, data, requestOptions);
     }
     else if (contentType === 'application/json') {
-      return this.httpRequestPOST(url, requestOptions, JSON.stringify(data));
+      return this.httpRequestPOST(url, JSON.stringify(data), requestOptions);
     }
     else if (contentType === 'application/octet-stream') {
       // placeholder for blob files where the data has to be transormed,
       // possibly to Uint8Array
-      return this.httpRequestPOST(url, requestOptions,/* new Uint8Array(data) */data);
+      return this.httpRequestPOST(url,/* new Uint8Array(data) */data, requestOptions);
     }
     else {
       return new Error('Content-Type for setFile request not specified,' +
diff --git a/src/services/experiments/files/remote-experiment-files-service.js b/src/services/experiments/files/remote-experiment-files-service.js
new file mode 100644
index 0000000000000000000000000000000000000000..c9625dfd3e824321a419847c877322edb857708e
--- /dev/null
+++ b/src/services/experiments/files/remote-experiment-files-service.js
@@ -0,0 +1,467 @@
+import { HttpService } from '../../http-service.js';
+import ExperimentStorageService from './experiment-storage-service';
+import getMimeByExtension from '../../../utility/mime-type';
+
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+
+const LOCALSTORAGE_KEY_FILE_INFO = 'NRP-remote-experiment-files_local-files';
+const FS_TYPE_FILE = 'file';
+const FS_TYPE_DIRECTORY = 'directory';
+const SERVER_FILE_TYPE_DIRECTORY = 'folder';
+
+/**
+ * Provides functionality to mirror (up-/download) and manage experiment files locally.
+ */
+class RemoteExperimentFilesService extends HttpService {
+  constructor(enforcer) {
+    super();
+    if (enforcer !== SINGLETON_ENFORCER) {
+      throw new Error('Use ' + this.constructor.name + '.instance');
+    }
+
+    this.localSyncDirectoryHandle = undefined;
+    this.mapLocalFiles = new Map();
+    this.mapServerFiles = new Map();
+    this.mapFileInfos = new Map();
+    this.autoSync = false;
+  }
+
+  static get instance() {
+    if (_instance == null) {
+      _instance = new RemoteExperimentFilesService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  isSupported() {
+    return window.showDirectoryPicker !== undefined && window.showDirectoryPicker !== null;
+  }
+
+  toggleAutoSync() {
+    this.autoSync = !this.autoSync;
+  }
+
+  async chooseLocalSyncDirectory() {
+    this.localSyncDirectoryHandle = await window.showDirectoryPicker();
+
+    if (this.localSyncDirectoryHandle) {
+      this.initLocalFileInfoFromLocalStorage();
+      await this.updateFileLists();
+    }
+
+    this.intervalCheckLocalFiles = setInterval(async () => {
+      await this.updateFileLists();
+    }, RemoteExperimentFilesService.CONSTANTS.INTERVAL_UPDATE_FILES);
+  }
+
+  async updateFileLists() {
+    if (!this.localSyncDirectoryHandle) {
+      return;
+    }
+
+    await this.updateServerFiles();
+
+    await this.updateLocalFiles();
+
+    await this.updateFileInfos();
+
+    this.saveLocalFileInfoToLocalStorage();
+  }
+
+  async updateLocalFiles() {
+    let updatedFilePaths = [];
+
+    await this.traverseFilesystem(this.localSyncDirectoryHandle,
+      async (fileSystemHandle) => {
+        let fileRelativePath = await this.getRelativePathFromFSHandle(fileSystemHandle);
+        let file = await this.getOrCreateLocalFile(fileRelativePath, fileSystemHandle.kind, fileSystemHandle);
+        file && updatedFilePaths.push(file.relativePath);
+      }
+    );
+
+    // get rid of map entries that have been deleted in FS
+    for (let entry of this.mapLocalFiles) {
+      const relativePath = entry[0];
+      if (!updatedFilePaths.includes(relativePath)) {
+        this.mapLocalFiles.delete(relativePath);
+      }
+    }
+  }
+
+  /**
+   *
+   * @param {*} directoryHandle
+   * @param {*} callbackFile - callback function called with (fileRelativePath, fileSystemHandle)
+   * @returns
+   */
+  async traverseFilesystem(directoryHandle, callbackFile) {
+    if (!directoryHandle) {
+      return;
+    }
+
+    let traverseFolder = async (directoryHandle) => {
+      let iterator = directoryHandle.values();
+      let result = await iterator.next();
+      while (!result.done) {
+        let fileSystemHandle = result.value;
+
+        callbackFile && await callbackFile(fileSystemHandle);
+
+        if (fileSystemHandle.kind === 'directory') {
+          await traverseFolder(fileSystemHandle);
+        }
+
+        result = await iterator.next();
+      }
+    };
+    await traverseFolder(directoryHandle);
+  }
+
+  async getOrCreateLocalFile(relativePath, type, fileSystemHandle = undefined) {
+    if (!relativePath || relativePath.length === 0) {
+      return;
+    }
+
+    let fileName = this.getFileNameFromRelativePath(relativePath);
+    if (fileName.charAt(0) === '.' || fileName.includes('.crswap')) {
+      return;
+    }
+
+    this.getOrCreateFileInfo(relativePath, type);
+    let localFile = this.mapLocalFiles.get(relativePath);
+    if (!localFile) {
+      localFile = {
+        name: fileName,
+        relativePath: relativePath,
+        fileSystemHandle: fileSystemHandle
+      };
+      this.mapLocalFiles.set(relativePath, localFile);
+    }
+
+    localFile.fileSystemHandle = localFile.fileSystemHandle || fileSystemHandle;
+    if (!localFile.fileSystemHandle) {
+      let parentDirectory = undefined;
+      let lastIndexSlash = relativePath.lastIndexOf('/');
+      if (lastIndexSlash && lastIndexSlash !== -1) {
+        let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativePath);
+        parentDirectory = await this.getOrCreateLocalFile(parentDirectoryPath, FS_TYPE_DIRECTORY);
+      }
+      let parentDirectoryHandle = parentDirectory ? parentDirectory.fileSystemHandle : this.localSyncDirectoryHandle;
+
+      if (type === FS_TYPE_FILE) {
+        localFile.fileSystemHandle = await parentDirectoryHandle.getFileHandle(fileName, {create: true});
+      }
+      else if (type === FS_TYPE_DIRECTORY) {
+        localFile.fileSystemHandle = await parentDirectoryHandle.getDirectoryHandle(fileName, {create: true});
+      }
+    }
+
+    return localFile;
+  }
+
+  async updateServerFiles(forceUpdate = false) {
+    let newServerFilesMap = new Map();
+
+    let getServerDirectoryFiles = async (parentDirectory) => {
+      let serverFileList = await ExperimentStorageService.instance.getExperimentFiles(parentDirectory.uuid);
+      parentDirectory.children = serverFileList;
+
+      for (let serverFile of serverFileList) {
+        newServerFilesMap.set(serverFile.uuid, serverFile);
+        if (!this.mapLocalFiles.has(serverFile.uuid)) {
+          this.getOrCreateFileInfo(serverFile.uuid, serverFile.type);
+        }
+
+        try {
+          serverFile.parent = parentDirectory;
+          if (serverFile.type === SERVER_FILE_TYPE_DIRECTORY) {
+            await getServerDirectoryFiles(serverFile);
+          }
+        }
+        catch (error) {
+          console.error(error);
+        }
+      }
+    };
+
+    let experiments = await ExperimentStorageService.instance.getExperiments(forceUpdate);
+    for (let experiment of experiments) {
+      let serverExperiment = {
+        uuid: experiment.uuid,
+        name: experiment.configuration.name
+      };
+      await getServerDirectoryFiles(serverExperiment);
+
+      newServerFilesMap.set(experiment.uuid, serverExperiment);
+    }
+
+    this.mapServerFiles = newServerFilesMap;
+  }
+
+  async updateFileInfos() {
+    for (let keyValueEntry of this.mapFileInfos) {
+      const relativePath = keyValueEntry[0];
+      let fileInfo = keyValueEntry[1];
+      const localFile = this.mapLocalFiles.get(relativePath);
+      const serverFile = this.mapServerFiles.get(relativePath);
+
+      if (!localFile) {
+        if (!serverFile) {
+          // local file has been deleted and is not present on server, remove from list
+          this.removeFileInfo(relativePath);
+        }
+        else {
+          // local file has been deleted, but is part of the experiment files on server
+          if (this.autoSync && fileInfo.type === FS_TYPE_FILE) {
+            await this.downloadExperimentFile(relativePath);
+          }
+        }
+      }
+
+      if (localFile && localFile.fileSystemHandle && localFile.fileSystemHandle.kind === FS_TYPE_FILE) {
+        fileInfo.hasLocalChanges = await this.hasLocalChanges(relativePath);
+        fileInfo.isOutOfSync = this.isOutOfSync(relativePath);
+        fileInfo.localOnly = !this.mapServerFiles.has(relativePath);
+        fileInfo.serverOnly = !this.mapLocalFiles.has(relativePath);
+
+        if (this.autoSync) {
+          if (fileInfo.hasLocalChanges || fileInfo.localOnly) {
+            await this.uploadExperimentFile(relativePath);
+          }
+
+          if (fileInfo.isOutOfSync) {
+            await this.downloadExperimentFile(relativePath);
+          }
+        }
+      }
+    }
+  }
+
+  getOrCreateFileInfo(relativePath, type) {
+    let fileName = this.getFileNameFromRelativePath(relativePath);
+    if (fileName.charAt(0) === '.' || fileName.includes('.crswap')) {
+      return;
+    }
+
+    let fileInfo = this.mapFileInfos.get(relativePath);
+    if (!fileInfo) {
+      fileInfo = {};
+      this.mapFileInfos.set(relativePath, fileInfo);
+    }
+    fileInfo.name = fileInfo.name || fileName;
+    fileInfo.type = fileInfo.type || type;
+    fileInfo.relativePath = fileInfo.relativePath || relativePath;
+
+    let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativePath);
+    let parentDirectory = this.mapFileInfos.get(parentDirectoryPath);
+    if (!parentDirectory && parentDirectoryPath.length > 0) {
+      parentDirectory = this.getOrCreateFileInfo(parentDirectoryPath, FS_TYPE_DIRECTORY);
+    }
+    if (parentDirectory) {
+      fileInfo.parent = parentDirectory;
+      parentDirectory.children = parentDirectory.children || [];
+      if (!parentDirectory.children.includes(fileInfo)) {
+        parentDirectory.children.push(fileInfo);
+      }
+    }
+
+    return fileInfo;
+  }
+
+  removeFileInfo(relativePath) {
+    let file = this.mapFileInfos.get(relativePath);
+    if (!file) {
+      return;
+    }
+
+    if (file.parent) {
+      file.parent.children = file.parent.children.filter(child => child.relativePath !== relativePath);
+    }
+
+    this.mapFileInfos.delete(relativePath);
+  }
+
+  initLocalFileInfoFromLocalStorage() {
+    this.mapFileInfos = new Map(JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_FILE_INFO)));
+  }
+
+  saveLocalFileInfoToLocalStorage() {
+    let mapStorage = new Map();
+    for (let keyValuePair of this.mapFileInfos) {
+      let relativePath = keyValuePair[0];
+      let file = keyValuePair[1];
+      mapStorage.set(relativePath, {
+        name: file.name,
+        relativePath: file.relativePath,
+        type: file.type,
+        dateSync: file.dateSync
+      });
+    }
+
+    localStorage.setItem(LOCALSTORAGE_KEY_FILE_INFO, JSON.stringify(Array.from(mapStorage.entries())));
+  }
+
+  async hasLocalChanges(relativePath) {
+    let fileInfo = this.mapFileInfos.get(relativePath);
+    let localFile = this.mapLocalFiles.get(relativePath);
+    if (!localFile || !localFile.fileSystemHandle || !fileInfo || !fileInfo.dateSync) {
+      return undefined;
+    }
+    return (fileInfo.dateSync < (await localFile.fileSystemHandle.getFile()).lastModified);
+  }
+
+  isOutOfSync(relativePath) {
+    let fileInfo = this.mapFileInfos.get(relativePath);
+    let serverFile = this.mapServerFiles.get(relativePath);
+    return serverFile && fileInfo.dateSync && Date.parse(serverFile.modifiedOn) > fileInfo.dateSync;
+  }
+
+  async downloadExperimentFile(relativeFilepath) {
+    let localFile = await this.getOrCreateLocalFile(relativeFilepath, FS_TYPE_FILE);
+    let fileInfo = this.mapFileInfos.get(relativeFilepath);
+
+    let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativeFilepath);
+    let fileContent = await ExperimentStorageService.instance.getBlob(parentDirectoryPath, relativeFilepath, false);
+    let writable = await localFile.fileSystemHandle.createWritable();
+    await writable.write(fileContent);
+    await writable.close();
+    fileInfo.dateSync = (await localFile.fileSystemHandle.getFile()).lastModified;
+  }
+
+  async downloadExperimentFileList(fileList) {
+    for (const filepath of fileList) {
+      await this.downloadExperimentFile(filepath);
+    }
+  }
+
+  async downloadExperimentToLocalFS(experiment) {
+    if (!this.localSyncDirectoryHandle) {
+      return;
+    }
+
+    let experimentRootDirectory = await this.getOrCreateLocalFile(experiment.id, FS_TYPE_DIRECTORY);
+    if (!experimentRootDirectory) {
+      return;
+    }
+
+    let downloadFiles = async (parentDirectory) => {
+      let serverFileList = await ExperimentStorageService.instance.getExperimentFiles(parentDirectory.uuid);
+      parentDirectory.children = serverFileList;
+
+      for (let serverFile of serverFileList) {
+        serverFile.parent = parentDirectory;
+        if (serverFile.type === FS_TYPE_FILE) {
+          await this.downloadExperimentFile(serverFile.uuid);
+        }
+        else if (serverFile.type === SERVER_FILE_TYPE_DIRECTORY) {
+          await this.getOrCreateLocalFile(serverFile.uuid, FS_TYPE_DIRECTORY);
+          await downloadFiles(serverFile);
+        }
+      }
+    };
+
+    let serverExperiment = {
+      uuid: experiment.uuid,
+      name: experiment.configuration.name
+    };
+    await downloadFiles(serverExperiment);
+
+    this.mapServerFiles.set(experiment.id, serverExperiment);
+  }
+
+  async uploadExperimentFile(relativePath) {
+    let localFile = this.mapLocalFiles.get(relativePath);
+    let fileInfo = this.mapFileInfos.get(relativePath);
+    if (this.isOutOfSync(relativePath)) {
+      //TODO: error GUI
+      console.warn('WARNING! ' + fileInfo.name + ' has a newer version on the server, won\'t upload');
+      fileInfo.msgError = 'Won\'t upload - file version on server is newer!';
+    }
+    else {
+      let fileHandle = localFile && localFile.fileSystemHandle;
+      if (!fileHandle) {
+        console.warn('Could not upload ' + relativePath + ' - missing file handle.');
+        return;
+      }
+
+      let localFileData = await fileHandle.getFile();
+      let fileExtension = fileInfo.name.substring(fileHandle.name.lastIndexOf('.') + 1);
+      let contentType = getMimeByExtension(fileExtension);
+
+      let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativePath);
+      let response = await ExperimentStorageService.instance.setFile(
+        parentDirectoryPath, fileHandle.name, localFileData, true, contentType);
+      if (response.status === 200) {
+        fileInfo.dateSync = Date.now().valueOf();
+      }
+    }
+  }
+
+  async uploadExperimentFileList(fileList) {
+    for (const filepath of fileList) {
+      await this.uploadExperimentFile(filepath);
+    }
+  }
+
+  uploadExperimentFromLocalFS(experiment) {
+    let uploadFolder = async (folder) => {
+      for (let file of folder.children) {
+        /*let localFile = this.mapLocalFiles.get(file.relativePath);
+        if (localFile && localFile.fileSystemHandle && localFile.fileSystemHandle.kind === FS_TYPE_FILE) {
+          await this.uploadExperimentFile(this.getRelativePathFromFSHandle(localFile.fileSystemHandle));
+        }
+        else if (localFile && localFile.fileSystemHandle && localFile.type === FS_TYPE_DIRECTORY) {
+          uploadFolder(file);
+        }*/
+        if (file.type === FS_TYPE_FILE) {
+          await this.uploadExperimentFile(file.relativePath);
+        }
+        else if (file.type === FS_TYPE_DIRECTORY) {
+          uploadFolder(file);
+        }
+      }
+    };
+
+    let localExperimentFiles = this.mapFileInfos.get(experiment.uuid);
+    uploadFolder(localExperimentFiles);
+  }
+
+  deleteExperimentFile(relativePath) {
+    let experimentName = this.getExperimentNameFromRelativePath(relativePath);
+    let serverFile = this.mapServerFiles.get(relativePath);
+    ExperimentStorageService.instance.deleteEntity(experimentName, relativePath, true, serverFile.type);
+  }
+
+  deleteExperimentFileList(fileList) {
+    for (const relativePath of fileList) {
+      this.deleteExperimentFile(relativePath);
+    }
+  }
+
+  getExperimentNameFromRelativePath(relativePath) {
+    return relativePath.substring(0, relativePath.indexOf('/'));
+  }
+
+  getFileNameFromRelativePath(relativePath) {
+    return relativePath.substring(relativePath.lastIndexOf('/') + 1);
+  }
+
+  getParentDirectoryFromRelativePath(relativePath) {
+    return relativePath.substring(0, relativePath.lastIndexOf('/'));
+  }
+
+  async getRelativePathFromFSHandle(fileSystemHandle) {
+    let filePathArray = await this.localSyncDirectoryHandle.resolve(fileSystemHandle);
+    let fileRelativePath = filePathArray.join('/');
+
+    return fileRelativePath;
+  }
+}
+
+RemoteExperimentFilesService.CONSTANTS = Object.freeze({
+  INTERVAL_UPDATE_FILES: 1000
+});
+
+export default RemoteExperimentFilesService;
diff --git a/src/utility/mime-type.js b/src/utility/mime-type.js
new file mode 100644
index 0000000000000000000000000000000000000000..078f39cd639b57b95d3c8dbda024e8239d61a8fc
--- /dev/null
+++ b/src/utility/mime-type.js
@@ -0,0 +1,43 @@
+/**---LICENSE-BEGIN - DO NOT CHANGE OR MOVE THIS HEADER
+ * This file is part of the Neurorobotics Platform software
+ * Copyright (C) 2014,2015,2016,2017 Human Brain Project
+ * https://www.humanbrainproject.eu
+ *
+ * The Human Brain Project is a European Commission funded project
+ * in the frame of the Horizon2020 FET Flagship plan.
+ * http://ec.europa.eu/programmes/horizon2020/en/h2020-section/fet-flagships
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ * ---LICENSE-END**/
+
+const MAP_MIME_TYPES = {
+  'bibi': 'text/plain',
+  'exc': 'text/plain',
+  'ini': 'text/plain',
+  'jpg': 'image/jpeg',
+  'jpeg': 'image/jpeg',
+  'json': 'application/json',
+  'png': 'image/png',
+  'py': 'text/plain',
+  'sdf': 'text/plain',
+  'uis': 'text/plain'
+};
+
+export default function getMimeByExtension(fileExtension) {
+  if (MAP_MIME_TYPES.hasOwnProperty(fileExtension)) {
+    return MAP_MIME_TYPES[fileExtension];
+  }
+  return undefined;
+}
\ No newline at end of file