[
  {
    "path": ".gitignore",
    "content": ".gradle\n*.sw?\n.#*\n*#\n*~\n/build\n/code\n.classpath\n.project\n.settings\n.metadata\n.factorypath\n.recommenders\nbin\nbuild\nlib/\ntarget\n.factorypath\n.springBeans\ninterpolated*.xml\ndependency-reduced-pom.xml\nbuild.log\n_site/\n.*.md.html\nmanifest.yml\nMANIFEST.MF\nsettings.xml\nactivemq-data\noverridedb.*\n*.iml\n*.ipr\n*.iws\n.idea\n.DS_Store\n.factorypath\ndavos.log\nsrc/main/resources/application.properties\nsrc/main/resources/log4j2.xml\n.vscode/"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 LinuxServer.io\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# davos\n\n[![Build Status](http://ci.linuxserver.io/buildStatus/icon?job=Software/Davos/davos_10_Unit_Tests)](http://ci.linuxserver.io/job/Software/job/Davos/job/davos_10_Unit_Tests/) [![Documentation Status](https://readthedocs.org/projects/davos/badge/?version=latest)](http://davos.readthedocs.io/en/latest)\n\ndavos is an FTP download automation tool that allows you to scan various FTP servers for files you are interested in. This can be used to configure file feeds as part of a wider workflow.\n\n# Why use davos?\n\nA fair number of services still rely on \"file-drops\" to transport data from place to place. A common practice is to configure a cron job to periodically trigger FTP/SFTP programs to download those files. _davos_ is relatively similar, only it also adds a web UI to the whole process, making the management of these schedules easier.\n\n# How it works\n\n## Hosts\n\nAll periodic scans (Schedules) require a host to connect to. These can be added individually:\n\n![https://raw.githubusercontent.com/linuxserver/davos/master/docs/host.png](https://raw.githubusercontent.com/linuxserver/davos/master/docs/host.png)\n\n## Schedules\n\nEach schedule contains all of the required information pertaining to the files it is interested in. This includes the host it needs to connect to, where to look for the files, where to download them, and how often:\n\n![https://raw.githubusercontent.com/linuxserver/davos/master/docs/schedule1.png](https://raw.githubusercontent.com/linuxserver/davos/master/docs/schedule1.png)\n\nIt is also possible to limit what the schedule downloads by applying filters to each scan. _davos_ will only download files that match its list of given filters. If no filters are applied to a schedule, all files will be downloaded. Each schedule also keeps an internal record of what it scanned in the previous run, so it won't download the same file twice.\n\n![https://raw.githubusercontent.com/linuxserver/davos/master/docs/schedule2.png](https://raw.githubusercontent.com/linuxserver/davos/master/docs/schedule2.png)\n\nOnce each file has been downloaded, _davos_ can also notify you via Pushbullet, as well as sending downstream requests to other services. This is particularly useful if another service makes use of the file _davos_ has just downloaded.\n\n![https://raw.githubusercontent.com/linuxserver/davos/master/docs/schedule3.png](https://raw.githubusercontent.com/linuxserver/davos/master/docs/schedule3.png)\n\n## Running\n\nFinally, schedules can be started or stopped at any point, using the schedules listing UI:\n\n\n![https://raw.githubusercontent.com/linuxserver/davos/master/docs/list.PNG](https://raw.githubusercontent.com/linuxserver/davos/master/docs/list.PNG)\n\n# Changelog\n- **2.2.2**\n  - Updated log4j dependency to 2.16.0, accounting for CVE-2021-44228\n\n- **2.2.1**\n  - Fixed bug where lastRunTime got reset whenever a change was made to a schedule.\n  - General refactoring of code, plus added unit tests.\n  - Allow $filename resolution in URLs of API calls.\n\n- **2.2.0**\n  - The filter pattern matcher now resolves '*' to zero or more characters, rather than one or more.\n  - The scanned items list can now be cleared.\n  - Added a Last Run field to the scanned items modal.\n  - Included [readthedocs](https://davos.readthedocs.io) documentation!\n  - Added SNS capability to notifications area\n  - Updated FTPS connections to run over Explicit TLS, rather than Implicit SSL\n    - This may or may not break existing schedules that use FTPS prior to 2.2.0.\n  - Improved some areas of DEBUG logging\n  - Schedules page now automatically updates when files are downloading\n  - Added identity file authentication for SFTP connections\n  - Included a version checker to help prompt users when a new version is available\n    - **Full disclosure**: This makes a GET request to GitHub to ascertain the latest release version.\n\n- **2.1.2**\n  - Fixed NaN bug caused by empty files (Div/0)\n  - Fixed recursive delete issue for directories in FTP and SFTP connections.\n\n- **2.1.1**\n  - Fixed primitive issue on Schedule model for new fields\n\n- **2.1.0**\n  - Mandatory filtering allows schedules to only download files when at least one filter has been set.\n  - Form validation on Hosts and Schedule pages\n  - New theme\n  - Inverse filtering allows schedules to download files that DO NOT match provided filters.\n  - \"Test Connection\" button added to Hosts page\n  - Schedules can now delete the remote copy of each file once the download has completed. This is separate to the Post-download actions.\n  - New intervals: \"Every minute\" and \"Every 5 minutes\"\n"
  },
  {
    "path": "build.gradle",
    "content": "import java.util.regex.Matcher;\n\nbuildscript {\n\n    ext {\n        springBootVersion = '1.4.2.RELEASE'\n    }\n    repositories {\n        mavenCentral()\n    }\n    dependencies {\n        classpath(\"org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}\") \n        classpath('io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE')\n    }\n}\n\nplugins {\n\tid \"com.github.samueltbrown.cucumber\" version \"0.9\"\n}\n\napply plugin: 'java'\napply plugin: 'eclipse'\napply plugin: 'idea'\napply plugin: 'org.springframework.boot' \napply plugin: 'io.spring.dependency-management' \n\njar {\n    baseName = 'davos'\n    version = file('version.txt').text.trim()\n}\n\nsourceCompatibility = 1.8\ntargetCompatibility = 1.8\n\nrepositories {\n    mavenCentral()\n}\n\nconfigurations {\n\n    all*.exclude module : 'spring-boot-starter-logging'\n    \n    bddTestCompile\n    testCompile.extendsFrom bddTestCompile\n\n}\n\nsourceSets {\n\n\tbbdTest {\n\t\tjava { srcDir 'src/cucumber/java' }\n\t\tresources { srcDir 'src/cucumber/resources' }\n\t\tcompileClasspath += test.output\n\t}\n}\n\ndependencies {\n\n    compile 'org.springframework.boot:spring-boot-starter-web'\n    compile 'org.springframework.boot:spring-boot-starter-thymeleaf'\n    compile 'org.springframework.boot:spring-boot-starter-data-jpa'\n    compile 'org.springframework.boot:spring-boot-starter-jdbc'\n    \n    compile 'org.apache.logging.log4j:log4j-api:2.16.0'\n    compile 'org.apache.logging.log4j:log4j-core:2.16.0'\n    compile 'org.apache.logging.log4j:log4j-slf4j-impl:2.16.0'\n\t\n\tcompile 'com.jcraft:jsch:0.1.50'\n    compile 'joda-time:joda-time:2.3'\n    compile 'commons-net:commons-net:3.3'\n\tcompile 'commons-io:commons-io:2.4'\n\tcompile 'org.apache.commons:commons-lang3:3.4'\n\t\n\tcompile 'com.amazonaws:aws-java-sdk-sns:1.11.167'\n\t\n    runtime 'com.h2database:h2'\n\n    testCompile 'org.springframework.boot:spring-boot-starter-test'\n    testCompile 'org.assertj:assertj-core:3.2.0'\n    testCompile 'org.mockito:mockito-all:1.9.5'\n    testCompile 'junit:junit:4.11'\n    \n    bddTestCompile 'org.mockftpserver:MockFtpServer:2.6'\n    bddTestCompile 'org.apache.sshd:sshd-core:1.4.0'\n    bddTestCompile 'info.cukes:cucumber-java:1.2.4'\n    cucumberCompile 'info.cukes:cucumber-java:1.2.4'\n}\n\neclipse {\n\n    classpath {\n         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')\n         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'\n    }\n}\n\ntask wrapper(type: Wrapper) {\n    gradleVersion = '2.14'\n}\n\ntask updateBuildVersion << {\n\n\tMatcher matcher = file('version.txt').text.trim() =~ /(.+)\\.(.+)\\.(.+)$/\n\t\n\tif (matcher.find()) {\n\t\n\t\tString major = matcher.group(1)\n\t\tString minor = matcher.group(2)\n\t\tString patch = matcher.group(3).trim()\n\t\tint nextBuild = Integer.valueOf(patch) + 1\n\t\n\t\tString updatedVersion =  \"${major}.${minor}.${nextBuild}\"\n\t\t\n\t\tfile('version.txt').text = \"${updatedVersion}\\n\"\n\t}\n}\n\ntask copyConfig(type: Copy) {\n\t\n\tdef location = project.hasProperty('env') ? \"$env\" : 'local'\n\t\n\tfrom \"conf/${location}\"\n\tinto \"src/main/resources/\"\n}\n\ncucumber {\n\n    jvmOptions {\n      maxHeapSize = '512m'\n      environment 'ENV', 'staging'\n    }\n}\n\nbuild.dependsOn copyConfig\nbuild.mustRunAfter copyConfig\n"
  },
  {
    "path": "conf/local/application.properties",
    "content": "davos.version=2.2.2\n"
  },
  {
    "path": "conf/local/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Configuration>\n\n\t<Appenders>\n\n\t\t<Console name=\"Console\" target=\"SYSTEM_OUT\" follow=\"true\">\n\t\t\t<PatternLayout pattern=\"%d{yyyy-MM-dd HH:mm:ss.SSS} - %5p - [%c{1}] - %msg%n\" />\n\t\t</Console>\n\n\t</Appenders>\n\n\t<Loggers>\n\n\t\t<Logger name=\"io.linuxserver\" level=\"debug\" additivity=\"false\">\n\t\t\t<AppenderRef ref=\"Console\" />\n\t\t</Logger>\n\t\t\n\t\t<Logger name=\"org.thymeleaf\" level=\"warn\" additivity=\"false\">\n\t\t\t<AppenderRef ref=\"Console\" />\n\t\t</Logger>\n\t\t\n\t\t<Logger name=\"org.springframework\" level=\"info\" additivity=\"false\">\n\t\t\t<AppenderRef ref=\"Console\" />\n\t\t</Logger>\n\t\t\n\t\t<Root level=\"info\">\n\t\t\t<AppenderRef ref=\"Console\" />\n\t\t</Root>\n\n\t</Loggers>\n\n</Configuration>"
  },
  {
    "path": "conf/release/application.properties",
    "content": "spring.datasource.url=jdbc:h2:file:/config/db/davos2\nspring.datasource.username=sa\nspring.datasource.password=sa\nspring.datasource.driverClassName=org.h2.Driver\nspring.jpa.hibernate.ddl-auto=update\n\ndavos.version=2.2.2\n"
  },
  {
    "path": "conf/release/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Configuration>\n\n\t<Appenders>\n\n\t\t<RollingFile name=\"File\" fileName=\"/config/logs/davos.log\" filePattern=\"/config/logs/${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log\">\n\t\t\t<PatternLayout pattern=\"%d{yyyy-MM-dd HH:mm:ss.SSS} - %5p - [%c{1}] - %msg%n\" />\n\t\t\t<Policies>\n\t\t\t\t<SizeBasedTriggeringPolicy size=\"10 MB\" />\n\t\t\t</Policies>\n\t\t</RollingFile>\n\n\t</Appenders>\n\n\t<Loggers>\n\n\t\t<Logger name=\"io.linuxserver\" level=\"info\" additivity=\"false\">\n\t\t\t<AppenderRef ref=\"File\" />\n\t\t</Logger>\n\t\t\n\t\t<Logger name=\"org.thymeleaf\" level=\"warn\" additivity=\"false\">\n\t\t\t<AppenderRef ref=\"File\" />\n\t\t</Logger>\n\t\t\n\t\t<Logger name=\"org.springframework\" level=\"error\" additivity=\"false\">\n\t\t\t<AppenderRef ref=\"File\" />\n\t\t</Logger>\n\t\t\n\t\t<Root level=\"warn\">\n\t\t\t<AppenderRef ref=\"File\" />\n\t\t</Root>\n\t\t\n\t</Loggers>\n\n</Configuration>\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = python -msphinx\nSPHINXPROJ    = davos\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=python -msphinx\n)\nset SOURCEDIR=source\nset BUILDDIR=build\nset SPHINXPROJ=davos\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The Sphinx module was not found. Make sure you have Sphinx installed,\n\techo.then set the SPHINXBUILD environment variable to point to the full\n\techo.path of the 'sphinx-build' executable. Alternatively you may add the\n\techo.Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\n\n:end\npopd\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# davos documentation build configuration file, created by\n# sphinx-quickstart on Sat Jul 29 08:01:32 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = []\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'davos'\ncopyright = u'2017, Josh Stark'\nauthor = u'Josh Stark'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'2.2'\n# The full version, including alpha/beta/rc tags.\nrelease = u'2.2.0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = []\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n#html_theme = 'alabaster'\nimport sphinx_rtd_theme\nhtml_theme = \"sphinx_rtd_theme\"\nhtml_theme_path = [sphinx_rtd_theme.get_html_theme_path()]\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# This is required for the alabaster theme\n# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars\nhtml_sidebars = {\n    '**': [\n        'about.html',\n        'navigation.html',\n        'relations.html',  # needs 'show_related': True theme option to display\n        'searchbox.html',\n        'donate.html',\n    ]\n}\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'davosdoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'davos.tex', u'davos Documentation',\n     u'Josh Stark', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'davos', u'davos Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'davos', u'davos Documentation',\n     author, 'davos', 'One line description of project.',\n     'Miscellaneous'),\n]\n"
  },
  {
    "path": "docs/source/developers/index.rst",
    "content": "##########\nDevelopers\n##########\n\nIf you wish to contribute to davos (and help me tidy up some of its rather messy code!), you\nwill need to be able to build and run it locally. davos is written almost completely in\nJava using the Spring Framework, utilising the Thymeleaf rendering engine. The project is\nunit and integration tested using jUnit and Cucumber JVM, respectively.\n\n*****\nSetup\n*****\n\nDownload and install the `Java 8 JDK <http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html>`_.\nI'd also recommend using `Spring Tool Suite (STS) <https://spring.io/tools/sts/all>`_ as it is a prebuilt version of Eclipse IDE with all of the necessary\nplugins installed for working with a Spring application.\n\n********\nBuilding\n********\n\n.. note:: You do not need to pre-install Gradle for this application as it comes with Gradle Wrapper, which does all the work for you.\n\nTo build the application, use Gradle:\n\n.. code-block:: text\n\n    ./gradlew clean build -Penv={release|local}\n\nThis will download all necessary dependencies, run tests, then package up the application.\nThe resulting .jar file will be in ``build/libs``. If you pass through a ``-Penv=release`` when\nrunning this command, the packaged application will use the config under ``conf/release``, which\ntells davos to use a file-based database. By default (i.e. if you do not pass this switch\nthrough), it will use the ``conf/local`` configuration, which makes use of an in-memory database.\n\n***************\nRunning the app\n***************\n\nIt is recommended to build the app first before running, so you know your latest\nchanges are built:\n\n.. code-block:: text\n\n    ./gradlew clean build && java -jar build/libs/davos-2.2.0.jar\n\n***********\nDevelopment\n***********\n\nClasspath\n---------\n\nWhen using Eclipse (or STS), a separate Gradle command is required in order to update\nthe project's classpath files so Eclipse is aware of the downloaded dependencies:\n\n.. code-block:: text\n\n    ./gradlew cleanEcipse eclipse\n\nCode Structure\n--------------\n\nThe code of davos is split in to four main sections:\n\n    ``src/main/java``\n        The core functional code. This contains all logic for the workflow, API,\n        connectivity, and object persistence (database).\n\n    ``src/main/resources``\n        The front-end code, including all JavaScript, CSS, images, and Thymeleaf\n        templates.\n\n    ``src/test/java``\n        All unit tests for the core code\n\n    ``src/cucumber/java``\n        Integration test code. This is separate to the main project code and\n        does not get packaged in to the released application.\n\nRunning Tests\n-------------\n\nTo run all unit tests, use Gradle:\n\n.. code-block:: text\n\n    ./gradlew test\n\nTo run all integration tests:\n\n.. code-block:: text\n\n    ./gradlew cucumber\n\nManaging the version\n--------------------\n\nThe version of the application is referenced in three files:\n\n* ``version.txt`` in the project root directory\n* ``conf/local/application.properties`` as a property called ``davos.version``\n* ``conf/release/application.properties`` as a property called ``davos.version``\n\nAll three of these need to be updated if you are changing the version number.\n"
  },
  {
    "path": "docs/source/faq/index.rst",
    "content": "###\nFAQ\n###\n\n**********************************\nCan davos be used to upload files?\n**********************************\n\nNo, davos only downloads files. There are currently no plans on implementing the ability\nto upload files as this will require a rework of the schedule workflow engine.\n\n******************************\nHow many schedules can I have?\n******************************\n\nThere is no theoretical limit to the number of schedules you can have. davos creates\nan initial thread pool of 10 worker threads, but this gets extended if more than 10\nschedules are created.\n\n**************************\nHow many hosts can I have?\n**************************\n\nUnlimited.\n\n********************************************\nAre host credentials hashed in the database?\n********************************************\n\nNo, all host usernames and passwords are stored in plain text in the H2 database. This\nis because the application needs to query the hosts table every time a schedule runs,\nand would have no way to compare a hash with a valid password.\n\n***************************************************\nHow do I use an identity file for SFTP connections?\n***************************************************\n\nOn the Host configuration page for your Host, make sure **Use Identity File** is checked. Then\nenter the absolute path of the identity file. If you're running davos in a Docker container (recommended),\nthe value of this should be some thing like \"/config/id_rsa\", assuming you are using an SSH private key called\n\"id_rsa\" and have placed it in your mapped host directory on your machine.\n\nAny form of private identity is applicable, for example if your host server uses .pem files\nfor authentication, use \"/config/my_identity.pem\".\n\n.. note:: Remember, davos can't see files outside of its ``/download`` and ``/config`` directories when running in a Docker container. So remember to place your identity file(s) in the mapped directory on the host (e.g. ``/home/user/davos``).\n\n***************************************************************\nI've just updated davos. The application is behaving strangely.\n***************************************************************\n\nSome version updates include changes to the JavaScript sources for the website side\nof the application. Modern browsers like Chrome tend to cache these types of sources for\nthe sake of performance. It is likely your browser has not re-cached the latest version of\nthe JavaScript code.\n\nTo remedy this, hard-refresh the app: ``CTRL`` + ``F5``.\n\n****************************************\nHow can I use SNS to notify me by email?\n****************************************\n\nTo use SNS, you'll need an Amazon AWS Account. Once set up, you should go to **Services -> Simple Notification Service**,\nthen **Create topic**. For Topic name, enter something like \"davos-notifications\", and click **Create topic**. The first\nthing you'll notice is that it has generated a **Topic ARN**. You'll need this for the notification configuration later.\n\nNow create a subscription to your topic by clicking on **Create subscription**, chosing \"Email\" as the Protocol, and your\npreferred email address as the Endpoint. Click **Create subscription**. You'll receive an email asking you to confirm\nthe subscription request.\n\nOnce your topic has been configured, you should create an IAM User that can publish messages to it. It is this user's\ncredentials that davos needs to perform the publish.\n\nGo to **Services -> IAM**, then **Users**. Click **Add user**. For User name, enter something sensible, then select \"Programmatic access\"\nas the Access Type. Click **Next: Permissions**. This user should only have permission to publish to this topic,\nnothing more. So, under \"Add user to group\", click **Create group**, and then **Create policy**.\n\n.. note:: A user can be in many groups. Groups can have many policies. A policy is a set of permissions for access to various things in AWS.\n\nYou should be directed to the policy creation tool. Select the Policy Generator and set the following:\n\n    Effect\n        Allow\n\n    AWS Service\n        Amazon SNS\n\n    Actions\n        Publish\n\n    Amazon Resource Name (ARN)\n        {YOUR_TOPIC_ARN}\n\nThen click **Add Statement**. You should see it added underneath. Click **Next Step**. The generated policy will be shown\nto you on screen (it's formatted as JSON, and contains a ``Statement`` array). Update the Policy Name to something\nsensible (e.g. \"DavosTopicPublishAccess\") then click **Create Policy**. You'll be redirected back to the IAM\nconsole, but you can close this.\n\nGo back to the previous tab and under the Filter, type in the name of the policy you just created. Select it. Now, for the\nGroup name, give it a sensible name (e.g. DavosNotifications), and click **Create group**. The group should now be selected under\nthe IAM user console. Click **Next: Review**, make sure you're happy, and then click **Create user**.\n\nYou should see a table showing the user's Access key ID and Secret access key. You'll need these for the SNS configuration\nin davos, so keep them safe somewhere (you can download a .csv with the credentials in).\n\n.. warning:: The Secret access key will only be shown once in the console, so make sure you store it somewhere safe.\n"
  },
  {
    "path": "docs/source/guides/appsettings.rst",
    "content": "############\nApp Settings\n############\n\nUnder **Settings -> App Settings**, you can configure the log level that davos\nwill output to its log file.\n\n*******\nLogging\n*******\n\nAll logs are written to ``davos.log``, located in the ``/config/logs`` directory.\nWhen mapping the ``/config`` directory in the container to a host directory, logs\nwill be made available in that host directory.\n\nThe log level can be changed at any time while the application is running. The available\nlevels are:\n\n* DEBUG\n* INFO\n* WARN\n* ERROR\n\nThe higher the level (``DEBUG`` is lowest, ``ERROR`` is highest), the fewer logs will be\nwritten. By default, davos logs at ``INFO`` level. If you are experiencing issues\nwith davos and wish to understand the area of failure, change the level to ``DEBUG``.\nUnder this setting, the most logs will be written.\n\n.. warning:: When setting the log level to ``DEBUG``, any secure credentials used in connections to the FTP host, or notification systems **will** be logged.\n"
  },
  {
    "path": "docs/source/guides/gettingstarted/hosts.rst",
    "content": "#####\nHosts\n#####\n\nA Host configuration provides one or more Schedules with information pertaining\nto the FTP server to connect to when scanning for files. They are separate to the\nSchedule configuration to allow multiple Schedules to use the same Host configuration\nwithout the need for having to input the same data multiple times.\n\nUnder **Settings -> New Host**, you will be prompted to enter all of the relevant\ninformation.\n\n    Name *[REQUIRED]*\n        The friendly name for this Host. This is what will be visible when creating a\n        schedule, so make it indicative of the Host you're making.\n\n    Protocol\n        Which type of connection to be made. This has no bearing on how you configure\n        the host, but will direct davos to build the specific client when connecting.\n\n    Host Address *[REQUIRED]*\n        The IP address (or hostname) of the server.\n\n    Port\n        FTP and FTPS are usually on ``21``, while SFTP is usually on ``22``. If your\n        server has been configured to run on a separate port, this is where you\n        reference it.\n\n    Username *[REQUIRED]*\n        Name of the user to connect as.\n\n    Password\n        Password of the user to connect as.\n\n    Use Identity File\n        Only available when ``SFTP`` is selected. Choose this if the SFTP server\n        requires an identity file to authenticate the user.\n\n    Identity File\n        Displayed when ``Use Identity File`` is checked, replacing the ``Password`` field. Enter\n        the location of the file.\n\n.. note:: The location of the identity file will be relative to the container's filesystem, so should ideally be under ``/config`` as this is the directory exposed by the Docker volume mapping.\n\nIt is also possible to create, manage, and delete a Host via the HTTP API. See :doc:`../../reference/api` for more details.\n"
  },
  {
    "path": "docs/source/guides/gettingstarted/index.rst",
    "content": "###############\nGetting Started\n###############\n\nThis section aims to help you understand how davos is pieced together, and shows\nyou how it can be configured to meet your needs. It is recommended that you follow\nthe below guides.\n\n.. toctree::\n   :maxdepth: 1\n\n   hosts\n   schedules\n\n************\nHow it works\n************\n\nThe Schedules in davos are powered by a basic workflow engine that runs a series\nof steps to ensure each run processes files properly. The order of this\nworkflow is as follows:\n\n1. Connect to the host.\n2. List all files in the provided remote directory.\n3. Filter all files in the remote directory so only the relevant ones remain.\n4. Remove any files that have been previously scanned.\n5. For each matched file, download it. Once downloaded, run any actions required by the schedule.\n6. Store the list of scanned files against the Schedule.\n7. Disconnect.\n\nThere is no theoretical limit to the number of schedules you can have running at\nthe same time, however it is advised you keep it below 10, as memory usage can\nbecome quite high.\n"
  },
  {
    "path": "docs/source/guides/gettingstarted/schedules.rst",
    "content": "#########\nSchedules\n#########\n\nA Schedule is the configuration that tells davos when to run, where to connect, what\nto look for, and what to do once it has finished downloading. Schedules are the heart\nof davos and are powered by its workflow engine.\n\nTo create a new Schedule, go to **Settings -> New Schedule**. Schedules are split into\nmultiple sections, each with their own part to play in the process.\n\n*******\nGeneral\n*******\n\nThis defines the metadata and connection information of the Schedule. The **General** section\nallows you to name the Schedule, as well as define how often it should run, and where files\nshould be managed.\n\n    Name *[REQUIRED]*\n        The name of the Schedule. This should be relevant to the task this schedule\n        is performing. E.g. \"Nightly Feed\"\n\n    Interval\n        How often the schedule should run. The rate at which the schedule runs begins\n        when the schedule is started for the first time. So, if it is started at 14:05,\n        with an interval of \"Every 30 minutes\", it will run again at 14:35, then 15:05, and\n        so on.\n\n.. note:: If you change the interval for an already running Schedule, you'll need to restart it before the change takes effect.\n..\n\n    Host\n        The Host configuration to use for this Schedule. It will default to the first\n        Host in the list. You cannot create a Schedule if no Hosts have been created.\n\n    Host Directory *[REQUIRED]*\n        This is the directory on the host (relative to the connection entry point) that\n        the Schedule should use for file scanning. Absolute paths are also compliant.\n\n    Local Directory *[REQUIRED]*\n        The directory where this schedule should place file downloads.\n\n.. note :: The local directory must be relative to the container's filesystem, so should be under ``/download``.\n..\n\n    Transfer Type\n        This setting will inform the Schedule whether or not it should only download\n        matching files (``FILE``), or if it should also scan matching directories (``RECURSIVE``). This can be useful\n        if the server contains sub-directories that may match in a scan, but should not be\n        downloaded.\n\n    Start Automatically\n        If checked, the Schedule will automatically start when davos is started. Useful if\n        you have a restart policy enabled in Docker and your machine requires a restart.\n\n*********\nFiltering\n*********\n\nThis is a process that allows you to narrow down file scanning so only relevant\nfiles are processed. Filters can be exceptionally useful for host directories that\nare used by multiple processes or contain large numbers of files.\n\n    Mandatory\n        If checked, the Schedule will only consider scanning files if at least one filter has been\n        defined. If checked and no filters are defined, nothing will be scanned, so nothing\n        will be downloaded.\n\n    Invert\n        The default behaviour is to match all files on the host with the defined filters. Checking\n        this option will invert that behaviour, so all files *not* matching the defined filters\n        will be downloaded.\n\n    Filters\n        A list of strings that will be used to scan the host directory. Each file on the host is compared to\n        this list - if it matches at least one filter, it will be downloaded. Filters can also be wildcarded\n        using ``?`` (single character) and ``*`` (multiple characters).\n\n        For example, for a file called \"my_file_name.txt\":\n\n        .. code-block:: text\n\n            my?file?name.txt = MATCH\n            my*name.txt      = MATCH\n            my_file.name.txt = NO MATCH\n            *file_name*      = MATCH\n            *file_name       = NO MATCH\n\n***************\nFile Management\n***************\n\ndavos also provides a way to tidy processed files upon completion. You can choose to\neither delete the file remotely once downloaded (effectively making it a *move* operation),\nand you can also move the file locally.\n\n    Delete from Host\n        If checked, all matched and downloaded files will be deleted from the Host. This\n        logic will run after each individual download has completed.\n\n.. warning :: If the FTP user does not have permission to delete files on the Host, this step will fail and the Schedule will cancel the current run. A future run of the Schedule will skip all files previously scanned.\n..\n\n    Move Downloaded File\n        The location to move each successfully downloaded file. This will occur after each individual\n        download has completed. A common use-case for this feature is to separate in-progress files with\n        completed files (i.e. ``/download/doing`` and ``/download/done``).\n\n.. note :: The \"move to\" directory must be relative to the container's filesystem, so should be under ``/download``. Advanced users may create additional volume mappings if need be.\n\n.. note :: If davos is unable to move the file, it will remain in its originating directory, and will continue on to the Schedule's next step without failure.\n\n******************\nDownstream Actions\n******************\n\nOne of the unique aspects of davos in respect to FTP management is its ability to create hooks in to other\napplications that may be interested in the downloaded files. This may be useful when\nthe download action is part of a wider workflow that must be continued outside of the scope\nof davos.\n\nActions defined against a Schedule will run for each individually downloaded file *after*\nthe File Management step previously mentioned has run.\n\nThere are two types of Downstream Action: *Notifications* and *API Calls*.\n\nNotifications\n=============\n\nNotifications are useful if you'd like to know whenever davos has successfully downloaded\na file. Generally speaking, no further action is taken after a notification is sent,\nbut SNS may be configured to include a subscriber to a topic that performs a further action.\n\n.. note:: There is no limit to the number of notifications you can have.\n\nPushbullet\n----------\n\nYou will need an account with `Pushbullet <https://www.pushbullet.com/>`_ in order to use this feature.\nIn your Pushbullet account, create an Access Token.\n\n    Access Token\n        Your Pushbullet account's access token. This will be used to authenticate\n        notification push requests to the Pushbullet API.\n\nAmazon SNS\n-------------------------\n\nYou will need an `Amazon AWS <https://aws.amazon.com/>`_ account to use this feature.\n\n    Topic Arn\n        The Amazon Resource Name for an SNS Topic created under your AWS account. This\n        will be the topic that notifications are sent to.\n\n    Region\n        The region that the topic was created under. While regions are not mandatory for\n        Topic Arns, this will be used to authenticate your account and create an SNS\n        client in the correct region.\n\n    Access Key\n        The access key for an IAM User under your AWS account.\n\n    Secret Access Key\n        The second half of authentication with AWS. This is the secret key for the same\n        IAM User.\n\n.. warning:: Be careful with IAM User permissions! You should create a new IAM User with permissions only to publish messages to your notification topic, nothing more! See :doc:`../../faq/index` for more details on best practice regarding IAM Users.\n\nAPI Calls\n=========\n\nAPI Calls are a great way to create hooks in to other applications via their own HTTP API.\n\n    URL\n        The URL of the API you wish to call\n\n    Method\n        Available options are *GET*, *POST*, *PUT* and *DELETE*\n\n    Content-Type\n        Informs the target API what type of body you're sending (if any), e.g. \"application/json\"\n\n    Message Body\n        The request payload being sent to the target API\n\n.. note:: If you need to reference the downloaded file in an HTTP request, use **$filename**. This will resolve to the file or folder that was matched and subsequently downloaded.\n"
  },
  {
    "path": "docs/source/guides/index.rst",
    "content": "######\nGuides\n######\n\nThis section will run you through the aspects of the application itself, including installation,\nfirst time use, and the concept of schedules (what they consist of), hosts, and how they tie together.\n\n.. toctree::\n   :maxdepth: 1\n\n   installation\n   gettingstarted/index\n   appsettings\n"
  },
  {
    "path": "docs/source/guides/installation.rst",
    "content": "############\nInstallation\n############\n\n.. note :: davos has been written with `Docker <https://www.docker.com/>`_ at the forefront regarding installation and deployment. This means that you should consider using the pre-built Docker image that `LinuxServer have provided <https://github.com/linuxserver/docker-davos>`_ for this application.\n\n***********\nWith Docker\n***********\n\nThis is the recommended method of installation and deployment.\n\nInstall Docker\n--------------\n\nFirstly, you'll need to install `Docker <https://www.docker.com/>`_, a container engine that is used to\nfire up user-space virtual containers. I recommend using `Docker's official guide <https://docs.docker.com/engine/installation/>`_ on installing the latest version of Docker CE on your machine,\nas the steps differ depending on your platform.\n\nBuild the container\n===================\n\nCreate a new container from LinuxServer's image.\n\n.. code-block:: text\n\n    docker create \\\n    --name=davos \\\n    -v <path to config>:/config \\\n    -v <path to downloads folder>:/download\n    -e PGID=<gid> -e PUID=<uid> \\\n    -p 8080:8080 \\\n    linuxserver/davos\n\nParams\n------\n\n* ``<path to config>``\n    The folder on your machine where davos will place its configuration and log files.\n    Typically this will be somewhere like ``/home/me/davos``, but it can be anywhere.\n* ``<path to downloads folder>``\n    The folder on your machine that davos can download files to. This is the volume mount\n    point that davos is aware of for all file downloads.\n* ``<uid>``\n    The id of the user you'd like davos to run as. All files downloaded by davos will be owned by this user.\n* ``<gid>``\n    The id of the group you'd like to attribute to the user davos runs as. All files downloaded by davos will be owned by this group.\n\n.. warning:: Docker will run all containers as ``root`` by default. Omitting ``PUID`` and ``PGID`` is not recommended.\n\nRun the container\n=================\n\nOnce the container has been created, you can run it.\n\n.. code-block:: text\n\n    docker start davos\n\nAfter about 30 seconds, the application will be running and will be accessible on ``http://localhost:8080``. If you are running\ndavos on a remote server, substitute ``localhost`` with the server's IP address.\n\n**************\nWithout Docker\n**************\n\nThis is not the recommended method of installation and deployment, but has the potential for being the most configurable and flexible.\nDavos does not have any prebuilt binaries, so you'll need to get the source and build it yourself (another reason to use Docker instead).\n\nGet the source\n--------------\n\n.. code-block:: text\n\n    wget https://github.com/linuxserver/davos/archive/LatestRelease.zip\n    unzip LatestRelease.zip -d davos\n\nConfigure the application\n-------------------------\n\nBy default, davos is configured to place all of its configuration in ``/config``, which may\nnot be preferable if you're running the application on bare metal. Firstly, reconfigure davos\nto use your own defined directory for its database.\n\nIn ``conf/release/application.properties``, change ``spring.datasource.url``, e.g.:\n\n.. code-block:: text\n\n    spring.datasource.url=jdbc:h2:file:/home/me/davos\n\nYou'll also need to do the same in ``conf/release/log4j2.xml``, this time for the appender:\n\n.. code-block:: xml\n\n    <RollingFile name=\"File\" fileName=\"/home/me/davos/logs/davos.log\" filePattern=\"/config/logs/${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log\">\n\nBuild davos\n-----------\n\n.. note:: davos requires the `Java 8 SDK <http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html>`_ to build.\n\nOnce you've updated the configuration locations, you can build the binary.\n\n.. code-block:: text\n\n    ./gradlew build -Penv=release\n\nThis will create \"davos-|release|.jar\" in ``build/libs``. You should move this somewhere more fitting for an executable (``/var/lib``, for example).\nIt may also be worth renaming the .jar to \"davos.jar\", although this is not necessary.\n\nRun davos\n---------\n\n.. note:: davos requires the `Java 8 JRE <http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html>`_ to build. This is not required if you already have the SDK installed.\n\n\nTo run the application, run the following command:\n\n.. code-block:: text\n\n    java -jar davos.jar\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": "##############################\ndavos: FTP Download Automation\n##############################\n\nThis is the documentation for `davos <https://github.com/linuxserver/davos>`_, a web-based tool for\nautomating and managing file downloads over FTP, FTPS and SFTP. Davos was born from the idea that\neven today, FTP still has relevance in many different markets, but there weren't many web-based\nsolutions that provided an easy way to manage the movement of files (outside of a command line cron job)\nfrom one place to another.\n\nFor those new to davos, look through the :doc:`guides/installation` and :doc:`guides/gettingstarted/index` guides.\nThey will run you though how to get and set up davos for the first time.\n\ndavos also provides a basic HTTP API that can be used to hook in to the application to manage things like\nschedules, hosts, filters, and even to stop or start individual schedules.\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents\n\n   guides/index\n   reference/index\n   faq/index\n   developers/index\n"
  },
  {
    "path": "docs/source/reference/api.rst",
    "content": "###\nAPI\n###\n\ndavos provides an HTTP API that exposes Schedules and Hosts so they can be managed\noutside the scope of the web application. This API is also used by the web application's\nAJAX calls.\n\n.. warning:: This API is completely unauthenticated, so anyone on your network can use this\n\n*********\n/schedule\n*********\n\n\nPOST\n----\n\nCreates a single Schedule.\n\n.. code-block:: text\n\n    POST /api/v2/schedule HTTP 1.0\n    Host: localhost:8080\n    Content-Type: application/json\n    Accept: application/json\n\n    {\n        \"name\": String,\n        \"interval\": Integer,\n        \"host\": Integer,\n        \"hostDirectory\": String,\n        \"localDirectory\": String,\n        \"transferType\": String [ FILE | RECURSIVE ],\n        \"automatic\": Boolean,\n        \"moveFileTo\": String,\n        \"filtersMandatory\": Boolean,\n        \"invertFilters\": Boolean,\n        \"deleteHostFile\": Boolean,\n        \"filters\": [\n            {\n                \"value\": String\n            }\n        ],\n        \"notifications\": {\n            \"pushbullet\": [\n                {\n                    \"apiKey\": String\n                }\n            ],\n            \"sns\": [\n                {\n                    \"topicArn\": String,\n                    \"region\": String,\n                    \"accessKey\": String,\n                    \"secretAccessKey\": String\n                }\n            ]\n        },\n        \"apis\": [\n            {\n                \"url\": String,\n                \"method\": String [ POST | GET | PUT | DELETE ],\n                \"contentType\": String,\n                \"body\": String\n            }\n        ]\n    }\n\nFor more information regarding what each field represents, see the :doc:`../guides/gettingstarted/schedules` documentation\nin :doc:`../guides/gettingstarted/index`.\n\nResponse\n========\n\nSee: :ref:`Schedule Response Syntax <schedule-response>`.\n\n**************\n/schedule/{id}\n**************\n\nGET\n---\n\nRetrieves a single Schedule based on the supplied ``{id}``.\n\n.. code-block:: text\n\n    GET /api/v2/schedule/{id} HTTP 1.0\n    Host: localhost:8080\n    Accept: application/json\n\nResponse\n========\n\nSee: :ref:`Schedule Response Syntax <schedule-response>`.\n\nPUT\n---\n\nUpdates a single Schedule based on the given ``{id}``. All fields must be supplied, even if only a subset is\nbeing updated. Use a GET to first obtain the most up-to-date payload before performing\na PUT.\n\n.. code-block:: text\n\n    PUT /api/v2/schedule/{id} HTTP 1.0\n    Host: localhost:8080\n    Content-Type: application/json\n    Accept: application/json\n\n    {\n        \"name\": String,\n        \"interval\": Integer,\n        \"host\": Integer,\n        \"hostDirectory\": String,\n        \"localDirectory\": String,\n        \"transferType\": String [ FILE | RECURSIVE ],\n        \"automatic\": Boolean,\n        \"moveFileTo\": String,\n        \"filtersMandatory\": Boolean,\n        \"invertFilters\": Boolean,\n        \"deleteHostFile\": Boolean,\n        \"filters\": [\n            {\n                \"id\": Integer,\n                \"value\": String\n            }\n        ],\n        \"notifications\": {\n            \"pushbullet\": [\n                {\n                    \"id\": Integer,\n                    \"apiKey\": String\n                }\n            ],\n            \"sns\": [\n                {\n                    \"id\": Integer,\n                    \"topicArn\": String,\n                    \"region\": String,\n                    \"accessKey\": String,\n                    \"secretAccessKey\": String\n                }\n            ]\n        },\n        \"apis\": [\n            {\n                \"url\": String,\n                \"method\": String [ POST | GET | PUT | DELETE ],\n                \"contentType\": String,\n                \"body\": String\n            }\n        ]\n    }\n\n.. note:: If you are updating a listed object, you must provide the object's ``id``. If you do not, the API will remove the old reference and create a new one. To add a new item to the list, provide the new item (without an ``id``) alongside the existing one.\n\nResponse\n========\n\nSee: :ref:`Schedule Response Syntax <schedule-response>`.\n\nDELETE\n------\n\nDeletes a single Schedule with the given ``{id}``.\n\n.. code-block:: text\n\n    DELETE /api/v2/schedule/{id} HTTP 1.0\n    Host: localhost:8080\n    Accept: application/json\n\nResponse\n========\n\n.. code-block:: javascript\n\n    {\n        \"status\":  String [ OK | Failed ],\n        \"body\": String\n    }\n\n***************************\n/schedule/{id}/scannedFiles\n***************************\n\nDELETE\n------\n\nClears all items in the given Schedule's ``lastScannedFiles``.\n\n.. code-block:: text\n\n    DELETE /api/v2/schedule/{id}/scannedFiles HTTP 1.0\n    Host: localhost:8080\n    Accept: application/json\n\nResponse\n========\n\n.. code-block:: javascript\n\n    {\n        \"status\":  String [ OK | Failed ],\n        \"body\": String\n    }\n\n**********************\n/schedule/{id}/execute\n**********************\n\nPOST\n----\n\nStarts/Stops an existing Schedule.\n\n.. code-block:: text\n\n    POST /api/v2/schedule/{id}/execute\n    Host: localhost:8080\n    Content-Type: application/json\n    Accept: application/json\n\n    {\n        \"command\": String [ START | STOP ]\n    }\n\nResponse\n========\n\n.. code-block:: javascript\n\n    {\n        \"status\":  String [ OK | Failed ],\n        \"body\": String\n    }\n\n*****\n/host\n*****\n\nPOST\n----\n\nCreates a new Host.\n\n.. code-block:: text\n\n    POST /api/v2/host\n    Host: localhost:8080\n    Content-Type: application/json\n    Accept: application/json\n\n    {\n        \"name\": String,\n        \"address\": String,\n        \"port\": Integer,\n        \"protocol\": String [ FTP | FTPS | SFTP ],\n        \"username\": String,\n        \"password\": String,\n        \"identityFile\": String,\n        \"identityFileEnabled\": Boolean\n    }\n\n.. note:: If ``identityFileEnabled`` is set to TRUE, you must also provide ``identityFile``, otherwise provide ``password``.\n\n**********\n/host/{id}\n**********\n\nGET\n---\n\nRetrieves a single Host based on the given ``{id}``.\n\n.. code-block:: text\n\n    GET /api/v2/host/{id}\n    Host: localhost:8080\n    Accept: application/json\n\nResponse\n========\n\nSee: :ref:`Host Response Syntax <host-response>`.\n\nPUT\n---\n\nUpdates a Host with the given ``{id}``.\n\n.. code-block:: text\n\n    POST /api/v2/host/{id}\n    Host: localhost:8080\n    Content-Type: application/json\n    Accept: application/json\n\n    {\n        \"name\": String,\n        \"address\": String,\n        \"port\": Integer,\n        \"protocol\": String [ FTP | FTPS | SFTP ],\n        \"username\": String,\n        \"password\": String,\n        \"identityFile\": String,\n        \"identityFileEnabled\": Boolean\n    }\n\n.. note:: If ``identityFileEnabled`` is set to TRUE, you must also provide ``identityFile``, otherwise provide ``password``.\n\nResponse\n========\n\nSee: :ref:`Host Response Syntax <host-response>`.\n\nDELETE\n------\n\nDeletes a single Host with the given ``{id}``.\n\n.. code-block:: text\n\n    DELETE /api/v2/host/{id} HTTP 1.0\n    Host: localhost:8080\n    Accept: application/json\n\nResponse\n========\n\n.. code-block:: javascript\n\n    {\n        \"status\":  String [ OK | Failure ],\n        \"body\": String\n    }\n\n.. warning:: If the Host you are attempting to delete is being used by an active Schedule, the DELETE call will fail.\n\n***************\n/testConnection\n***************\n\nPOST\n----\n\nAllows you to assert whether or not the provided payload contains valid Host information.\n\n.. code-block:: text\n\n    POST /api/v2/testConnection\n    Host: localhost:8080\n    Content-Type: application/json\n\n    {\n        \"id\": Integer,\n        \"name\": String,\n        \"address\": String,\n        \"port\": Integer,\n        \"protocol\": String [ FTP | FTPS | SFTP ],\n        \"username\": String,\n        \"password\": String,\n        \"identityFile\": String,\n        \"identityFileEnabled\": Boolean\n    }\n\nResponse\n========\n\n.. code-block:: javascript\n\n    {\n        \"status\":  String [ OK | Failed ],\n        \"body\": String\n    }\n\n*************\n/settings/log\n*************\n\nPOST\n----\n\nChanges the logging level of the application's core code. Unlike other POST calls,\nthere is no payload body. The level is passed in as a request parameter.\n\n    level\n        The level to change the logging to. Available options are DEBUG, INFO, WARN, ERROR, FATAL\n\n.. code-block:: text\n\n    POST /api/v2/settings/log?level={LEVEL}\n    Host: localhost:8080\n    Accept: application/json\n\nResponse\n========\n\n.. code-block:: javascript\n\n    {\n        \"status\":  String [ OK | Failed ],\n        \"body\": String\n    }\n\n\n*********\nResponses\n*********\n\n.. _schedule-response:\n\nSchedule Response Syntax\n------------------------\n\n.. code-block:: javascript\n\n    {\n        \"status\": String [ OK ],\n        \"body\": {\n            \"id\": Integer,\n            \"name\": String,\n            \"interval\": Integer,\n            \"host\": Integer,\n            \"hostDirectory\": String,\n            \"localDirectory\": String,\n            \"transferType\": String [ FILE | RECURSIVE ],\n            \"automatic\": Boolean,\n            \"moveFileTo\": String,\n            \"running\": Boolean,\n            \"filtersMandatory\": Boolean,\n            \"invertFilters\": Boolean,\n            \"lastRunTime\": String,\n            \"deleteHostFile\": Boolean,\n            \"lastScannedFiles\": [\n                String\n            ],\n            \"filters\": [\n                {\n                    \"id\": Integer,\n                    \"value\": String\n                }\n            ],\n            \"notifications\": {\n                \"pushbullet\": [\n                    {\n                        \"id\": Integer,\n                        \"apiKey\": String\n                    }\n                ],\n                \"sns\": [\n                    {\n                        \"id\": Integer,\n                        \"topicArn\": String,\n                        \"region\": String,\n                        \"accessKey\": String,\n                        \"secretAccessKey\": String\n                    }\n                ]\n            },\n            \"transfers\": [\n                {\n                    \"fileName\": String,\n                    \"fileSize\": Integer,\n                    \"directory\": Boolean,\n                    \"progress\": {\n                        \"percentageComplete\": Double,\n                        \"transferSpeed\": Double\n                    },\n                    \"status\": String [ DOWNLOADING | SKIPPED | PENDING | FINISHED ]\n                }\n            ],\n            \"apis\": [\n                {\n                    \"id\": Integer,\n                    \"url\": String,\n                    \"method\": String [ POST | GET | PUT | DELETE ],\n                    \"contentType\": String,\n                    \"body\": String\n                }\n            ]\n        }\n    }\n\n.. note:: ``running``, ``lastScannedFiles``, ``lastRunTime`` and ``transfers`` are immutable metadata fields and can't be used in PUT or POST requests. If supplied, they will be ignored.\n..\n\n    host\n        References the ``id`` of the linked host.\n\n    running\n        Descibes whether or not the Schedule is running.\n\n    lastRunTime\n        The time recorded when the Schedule last *finished* running.\n\n    lastScannedFiles\n        A list of Strings that represent the files/folders found in the last run of the\n        schedule.\n\n    transfers\n        A list of transfer objects that describe all files being actioned. This list\n        will only be populated when the Schedule is running and is actively downloading.\n\n.. _host-response:\n\nHost Response Syntax\n--------------------\n\nSuccess\n=======\n\n.. code-block:: javascript\n\n    {\n        \"status\": String [ OK ],\n        \"body\": {\n            \"id\": Integer,\n            \"name\": String,\n            \"address\": String,\n            \"port\": Integer,\n            \"protocol\": String [ FTP | FTPS | SFTP ],\n            \"username\": String,\n            \"password\": String,\n            \"identityFile\": String,\n            \"identityFileEnabled\": Boolean\n        }\n    }\n\nFailure\n=======\n\n.. code-block:: javascript\n\n    {\n        \"status\": String [ Failed ],\n        \"body\": String\n    }\n"
  },
  {
    "path": "docs/source/reference/index.rst",
    "content": "#########\nReference\n#########\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n"
  },
  {
    "path": "docs/source/requirements.txt",
    "content": "sphinx_rtd_theme"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Nov 11 19:22:20 GMT 2016\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-2.14-bin.zip\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "src/cucumber/java/io/linuxserver/davos/bdd/ClientStepDefs.java",
    "content": "package io.linuxserver.davos.bdd;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.File;\nimport java.util.List;\n\nimport org.apache.commons.io.FileUtils;\n\nimport cucumber.api.java.After;\nimport cucumber.api.java.en.Then;\nimport cucumber.api.java.en.When;\nimport io.linuxserver.davos.bdd.helpers.FakeFTPServerFactory;\nimport io.linuxserver.davos.bdd.helpers.FakeSFTPServerFactory;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.client.Client;\nimport io.linuxserver.davos.transfer.ftp.client.FTPClient;\nimport io.linuxserver.davos.transfer.ftp.client.SFTPClient;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\n\npublic class ClientStepDefs {\n\n    private static final String TMP = FileUtils.getTempDirectoryPath();\n    \n    private Connection connection;\n    private Client client;\n    private ProgressListener progressListener;\n\n    @After(\"@Client\")\n    public void after() {\n        client.disconnect();\n    }\n    \n    @When(\"^davos connects to the server$\")\n    public void davos_connects_to_the_server() throws Throwable {\n\n        client = new FTPClient();\n        client.setCredentials(new UserCredentials(\"user\", \"password\"));\n        client.setHost(\"localhost\");\n        client.setPort(FakeFTPServerFactory.getPort());\n\n        connection = client.connect();\n    }\n    \n    @When(\"^davos connects to the SFTP server$\")\n    public void davos_connects_to_the_SFTP_server() throws Throwable {\n\n        client = new SFTPClient();\n        client.setCredentials(new UserCredentials(\"user\", \"password\"));\n        client.setHost(\"localhost\");\n        client.setPort(FakeSFTPServerFactory.getPort());\n\n        connection = client.connect();\n    }\n\n    @When(\"^deletes an SFTP directory$\")\n    public void deletes_an_SFTP_directory() throws Throwable {\n        connection.deleteRemoteFile(new FTPFile(\"toDelete\", 0, \"/\", 0, true));\n    }\n\n    @Then(\"^the SFTP directory is deleted on the server$\")\n    public void the_SFTP_directory_is_deleted_on_the_server() throws Throwable {\n        assertThat(new File(TMP + \"/toDelete\").exists()).isFalse();\n    }\n\n    @Then(\"^listing the files will show the correct files$\")\n    public void listing_the_files_will_show_the_correct_files() throws Throwable {\n\n        List<FTPFile> files = connection.listFiles(\"/tmp\");\n\n        assertThat(files).hasSize(3);\n        assertThat(files.get(0).getName()).isEqualTo(\"file3.txt\");\n        assertThat(files.get(1).getName()).isEqualTo(\"file2.txt\");\n        assertThat(files.get(2).getName()).isEqualTo(\"file1.txt\");\n    }\n\n    @When(\"^downloads a file$\")\n    public void downloads_a_file() throws Throwable {\n        connection.download(new FTPFile(\"file2.txt\", \"hello world\".getBytes().length, \"/tmp/\", 0, false), TMP);\n    }\n\n    @Then(\"^the file is located in the specified local directory$\")\n    public void the_file_is_located_in_the_specified_local_directory() throws Throwable {\n        \n        File file = new File(TMP + \"/file2.txt\");\n        assertThat(file.exists()).isTrue();\n        file.delete();\n    }\n    \n    @When(\"^initialises a Progress Listener for that connection$\")\n    public void initialises_a_Progress_Listener_for_that_connection() throws Throwable {\n        \n        progressListener = new CountingFTPProgressListener();\n        connection.setProgressListener(progressListener);\n    }\n\n    @Then(\"^the Progress Listener will have its values updated$\")\n    public void the_Progress_Listener_will_have_its_values_updated() throws Throwable {\n        \n        assertThat(progressListener.getProgress()).isEqualTo(100);\n        assertThat(((CountingFTPProgressListener) progressListener).getTimesCalled()).isEqualTo(11);\n    }\n    \n    @When(\"^deletes a directory$\")\n    public void deletes_a_directory() throws Throwable {\n        connection.deleteRemoteFile(new FTPFile(\"toDelete\", 0, \"/tmp\", 0, true));\n    }\n\n    @Then(\"^the directory is deleted on the server$\")\n    public void the_directory_is_deleted_on_the_server() throws Throwable {\n        assertThat(FakeFTPServerFactory.checkFileExists(\"/tmp/toDelete\")).isFalse();\n    }\n    \n    class CountingFTPProgressListener extends ProgressListener {\n        \n        int timesCalled;\n        \n        @Override \n        public void setBytesWritten(long byteCount) {\n            super.setBytesWritten(byteCount);\n            timesCalled++;\n        }\n        \n        public int getTimesCalled() {\n            return timesCalled;\n        }\n    }\n}\n"
  },
  {
    "path": "src/cucumber/java/io/linuxserver/davos/bdd/ScheduleStepDefs.java",
    "content": "package io.linuxserver.davos.bdd;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.File;\nimport java.util.List;\n\nimport org.apache.commons.io.FileUtils;\n\nimport cucumber.api.java.en.Given;\nimport cucumber.api.java.en.Then;\nimport cucumber.api.java.en.When;\nimport io.linuxserver.davos.bdd.helpers.FakeFTPServerFactory;\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.FilterModel;\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.schedule.RunnableSchedule;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ScheduleStepDefs {\n\n    private static final String TMP = FileUtils.getTempDirectoryPath();\n\n    private ScheduleModel scheduleModel;\n\n    @Given(\"^a schedule exists for that server, with filters$\")\n    public void a_schedule_exists_for_that_server_with_filters() throws Throwable {\n\n        createBasicSchedule();\n\n        FilterModel filter1 = new FilterModel();\n        filter1.value = \"file2*\";\n        scheduleModel.filters.add(filter1);\n\n        FilterModel filter2 = new FilterModel();\n        filter2.value = \"file3*\";\n        scheduleModel.filters.add(filter2);\n    }\n    \n    @Given(\"^the schedule is set to delete host files$\")\n    public void the_schedule_is_set_to_delete_host_files() throws Throwable {\n        scheduleModel.setDeleteHostFile(true);\n    }\n    \n    @Given(\"^the schedule is set to invert filters$\")\n    public void the_schedule_is_set_to_invert_filters() throws Throwable {\n        scheduleModel.setInvertFilters(true);\n    }\n    \n    @Given(\"^the schedule is set to have mandatory filters$\")\n    public void the_schedule_is_set_to_have_mandatory_filters() throws Throwable {\n        scheduleModel.setFiltersMandatory(true);\n    }\n    \n    @Given(\"^a schedule exists for that server, without filters$\")\n    public void a_schedule_exists_for_that_server() throws Throwable {\n        createBasicSchedule();\n    }\n\n    @When(\"^that schedule is run$\")\n    public void that_schedule_is_run() throws Throwable {\n        new RunnableSchedule(1L, new CucumberScheduleConfigurationDAO()).run();\n    }\n    \n    @Then(\"^no files are downloaded$\")\n    public void no_files_are_downloaded() throws Throwable {\n\n        File file1 = new File(TMP + \"/file1.txt\");\n        File file2 = new File(TMP + \"/file2.txt\");\n        File file3 = new File(TMP + \"/file3.txt\");\n\n        assertThat(file1.exists()).isFalse();\n        assertThat(file2.exists()).isFalse();\n        assertThat(file3.exists()).isFalse();\n    }\n\n    @Then(\"^all files not matching the filters are downloaded$\")\n    public void all_files_not_matching_the_filters_are_downloaded() throws Throwable {\n        \n        File file1 = new File(TMP + \"/file1.txt\");\n        File file2 = new File(TMP + \"/file2.txt\");\n        File file3 = new File(TMP + \"/file3.txt\");\n\n        assertThat(file1.exists()).isTrue();\n        assertThat(file2.exists()).isFalse();\n        assertThat(file3.exists()).isFalse();\n\n        file1.delete();\n    }\n    \n    @Then(\"^those files are deleted on the host$\")\n    public void those_files_are_deleted_on_the_host() throws Throwable {\n\n        assertThat(FakeFTPServerFactory.checkFileExists(\"/tmp/file1.txt\")).isTrue();\n        assertThat(FakeFTPServerFactory.checkFileExists(\"/tmp/file2.txt\")).isFalse();\n        assertThat(FakeFTPServerFactory.checkFileExists(\"/tmp/file3.txt\")).isFalse();\n    }\n\n    @Then(\"^only the filtered files are downloaded$\")\n    public void only_the_filtered_files_are_downloaded() throws Throwable {\n\n        File file1 = new File(TMP + \"/file1.txt\");\n        File file2 = new File(TMP + \"/file2.txt\");\n        File file3 = new File(TMP + \"/file3.txt\");\n\n        assertThat(file1.exists()).isFalse();\n        assertThat(file2.exists()).isTrue();\n        assertThat(file3.exists()).isTrue();\n\n        file2.delete();\n        file3.delete();\n    }\n\n    private void createBasicSchedule() {\n        \n        scheduleModel = new ScheduleModel();\n        scheduleModel.host = new HostModel();\n\n        scheduleModel.host.address = \"localhost\";\n        scheduleModel.host.port = FakeFTPServerFactory.getPort();\n        scheduleModel.host.username = \"user\";\n        scheduleModel.host.password = \"password\";\n        scheduleModel.host.protocol = TransferProtocol.FTP;\n        scheduleModel.remoteFilePath = \"/tmp\";\n        scheduleModel.localFilePath = TMP;\n    }\n\n    class CucumberScheduleConfigurationDAO implements ScheduleDAO {\n\n        @Override\n        public List<ScheduleModel> getAll() {\n            return null;\n        }\n\n        @Override\n        public ScheduleModel fetchSchedule(Long id) {\n            return scheduleModel;\n        }\n\n        @Override\n        public ScheduleModel updateConfig(ScheduleModel model) {\n            return null;\n        }\n\n        @Override\n        public List<ScheduleModel> fetchSchedulesUsingHost(Long hostId) {\n            // TODO Auto-generated method stub\n            return null;\n        }\n\n        @Override\n        public void updateScannedFilesOnSchedule(Long id, List<String> newlyScannedFiles) {\n            // TODO Auto-generated method stub\n\n        }\n\n        @Override\n        public void deleteSchedule(Long id) {\n            // TODO Auto-generated method stub\n\n        }\n    }\n}\n"
  },
  {
    "path": "src/cucumber/java/io/linuxserver/davos/bdd/ServerStepDefs.java",
    "content": "package io.linuxserver.davos.bdd;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport org.apache.commons.io.FileUtils;\n\nimport cucumber.api.java.After;\nimport cucumber.api.java.en.Given;\nimport io.linuxserver.davos.bdd.helpers.FakeFTPServerFactory;\nimport io.linuxserver.davos.bdd.helpers.FakeSFTPServerFactory;\n\npublic class ServerStepDefs {\n\n    private static final String TMP = FileUtils.getTempDirectoryPath();\n    \n    @Given(\"^there is an FTP server running$\")\n    public void there_is_an_FTP_server_running() throws Throwable {\n        FakeFTPServerFactory.setup();\n    }\n    \n    @Given(\"^the FTP server has a directory with contents$\")\n    public void the_FTP_server_has_a_directory_with_contents() throws Throwable {\n        FakeFTPServerFactory.addDirectoryWithNameAndNumberOfFiles(\"toDelete\", 3);\n    }\n    \n    @Given(\"^the FTP server has a directory without contents$\")\n    public void the_FTP_server_has_a_directory_without_contents() throws Throwable {\n        FakeFTPServerFactory.addDirectoryWithNameAndNumberOfFiles(\"toDelete\", 0);\n    }\n    \n    @Given(\"^there is an SFTP server running$\")\n    public void there_is_an_SFTP_server_running() throws Throwable {\n        FakeSFTPServerFactory.setup();\n    }\n    \n    @Given(\"^the SFTP server has a directory with contents$\")\n    public void the_SFTP_server_has_a_directory_with_contents() throws Throwable {\n        \n        FakeSFTPServerFactory.addDirectoryWithNameAndNumberOfFiles(\"toDelete\", 3);\n        \n        assertThat(new File(TMP + \"/toDelete\").exists()).isTrue();\n        assertThat(new File(TMP + \"/toDelete/file0\").exists()).isTrue();\n        assertThat(new File(TMP + \"/toDelete/file1\").exists()).isTrue();\n        assertThat(new File(TMP + \"/toDelete/file2\").exists()).isTrue();\n    }\n    \n    @Given(\"^the SFTP server has a directory without contents$\")\n    public void the_SFTP_server_has_a_directory_without_contents() throws Throwable {\n        FakeSFTPServerFactory.addDirectoryWithNameAndNumberOfFiles(\"toDelete\", 0);\n    }\n    \n    @After(\"@Server\")\n    public void after() {\n        FakeFTPServerFactory.stop();\n    }\n    \n    @After(\"@SFTPServer\")\n    public void afterSFTP() throws IOException {\n        FakeSFTPServerFactory.stop();\n    }\n}\n"
  },
  {
    "path": "src/cucumber/java/io/linuxserver/davos/bdd/helpers/FakeFTPServerFactory.java",
    "content": "package io.linuxserver.davos.bdd.helpers;\n\nimport org.mockftpserver.fake.FakeFtpServer;\nimport org.mockftpserver.fake.UserAccount;\nimport org.mockftpserver.fake.filesystem.DirectoryEntry;\nimport org.mockftpserver.fake.filesystem.FileEntry;\nimport org.mockftpserver.fake.filesystem.FileSystem;\nimport org.mockftpserver.fake.filesystem.UnixFakeFileSystem;\n\npublic class FakeFTPServerFactory {\n\n    private static FakeFtpServer server;\n\n    public static int getPort() {\n        return server.getServerControlPort();\n    }\n\n    public static FakeFtpServer setup() {\n\n        server = new FakeFtpServer();\n        server.addUserAccount(new UserAccount(\"user\", \"password\", \"/tmp\"));\n        server.setServerControlPort(0);\n\n        FileSystem fileSystem = new UnixFakeFileSystem();\n        fileSystem.add(new DirectoryEntry(\"/tmp\"));\n        fileSystem.add(new FileEntry(\"/tmp/file1.txt\", \"hello world\"));\n        fileSystem.add(new FileEntry(\"/tmp/file2.txt\", \"hello world\"));\n        fileSystem.add(new FileEntry(\"/tmp/file3.txt\", \"hello world\"));\n\n        server.setFileSystem(fileSystem);\n        server.start();\n\n        return server;\n    }\n\n    public static boolean checkFileExists(String filePath) {\n        return server.getFileSystem().exists(filePath);\n    }\n\n    public static void addDirectoryWithNameAndNumberOfFiles(String name, int numberOfFiles) {\n\n        server.getFileSystem().add(new DirectoryEntry(\"/tmp/\" + name));\n\n        int i;\n        for (i = 0; i < numberOfFiles; i++)\n            server.getFileSystem().add(new FileEntry(\"/tmp/\" + name + \"/file\" + i));\n    }\n\n    public static void stop() {\n        server.stop();\n    }\n}\n"
  },
  {
    "path": "src/cucumber/java/io/linuxserver/davos/bdd/helpers/FakeSFTPServerFactory.java",
    "content": "package io.linuxserver.davos.bdd.helpers;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\n\nimport org.apache.commons.io.FileUtils;\nimport org.apache.sshd.common.NamedFactory;\nimport org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;\nimport org.apache.sshd.common.session.Session;\nimport org.apache.sshd.server.Command;\nimport org.apache.sshd.server.SshServer;\nimport org.apache.sshd.server.auth.password.PasswordAuthenticator;\nimport org.apache.sshd.server.auth.password.PasswordChangeRequiredException;\nimport org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;\nimport org.apache.sshd.server.scp.ScpCommandFactory;\nimport org.apache.sshd.server.session.ServerSession;\nimport org.apache.sshd.server.shell.ProcessShellFactory;\nimport org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;\n\npublic class FakeSFTPServerFactory {\n\n    private static final String TMP = FileUtils.getTempDirectoryPath();\n    private static final String USERNAME = \"user\";\n    private static final String PASSWORD = \"password\";\n\n    private static SshServer sshd;\n\n    public static void setup() throws IOException {\n\n        SftpSubsystemFactory factory = new SftpSubsystemFactory.Builder().build();\n\n        sshd = SshServer.setUpDefaultServer();\n        sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());\n        sshd.setShellFactory(new ProcessShellFactory(new String[] { \"/bin/sh\", \"-i\", \"-l\" }));\n        sshd.setCommandFactory(new ScpCommandFactory());\n        sshd.setSubsystemFactories(Collections.<NamedFactory<Command>> singletonList(factory));\n        sshd.setPasswordAuthenticator(new PasswordAuthenticator() {\n\n            @Override\n            public boolean authenticate(String username, String password, ServerSession session)\n                    throws PasswordChangeRequiredException {\n                return USERNAME.equals(username) && PASSWORD.equals(password);\n            }\n        });\n        \n        sshd.setFileSystemFactory(new VirtualFileSystemFactory() {\n            @Override\n            protected Path computeRootDir(Session session) throws IOException  {\n                return Paths.get(TMP);\n            }\n        });\n\n        sshd.start();\n    }\n\n    public static void stop() throws IOException {\n        sshd.stop();\n    }\n\n    public static void addDirectoryWithNameAndNumberOfFiles(String name, int numberOfFiles) throws IOException {\n\n        File directory = new File(TMP + \"/\" + name);\n        directory.mkdirs();\n\n        int i;\n        for (i = 0; i < numberOfFiles; i++)\n            new File(TMP + \"/\" + name + \"/file\" + i).createNewFile();\n    }\n\n    public static int getPort() {\n        return sshd.getPort();\n    }\n}\n"
  },
  {
    "path": "src/cucumber/java/io/linuxserver/davos/bdd/helpers/Logging.java",
    "content": "package io.linuxserver.davos.bdd.helpers;\n\nimport org.apache.logging.log4j.Level;\nimport org.apache.logging.log4j.core.config.Configurator;\n\nimport cucumber.api.java.Before;\n\npublic class Logging {\n\n    @Before\n    public void before() {\n        Configurator.setRootLevel(Level.ERROR);\n        Configurator.setLevel(\"io.linuxserver\", Level.ERROR);\n    }\n}\n"
  },
  {
    "path": "src/cucumber/resources/Client.feature",
    "content": "@Client\nFeature: General client tests\n\n    @Server\n\tScenario: Connecting to the FTP server\n\t\n\t\tGiven there is an FTP server running\n\t\tWhen davos connects to the server\n\t\tThen listing the files will show the correct files\n\t\n\t@Server\n\tScenario: Downloading a file from the server\n\t\n\t\tGiven there is an FTP server running\n\t\tWhen davos connects to the server\n\t\tAnd downloads a file\n\t\tThen the file is located in the specified local directory\n\t\t\n    @Listener @Server\n\tScenario: Download with FTP Progress Listener\n\t\n\t\tGiven there is an FTP server running\n\t\tWhen davos connects to the server\n\t\tAnd initialises a Progress Listener for that connection\n\t\tAnd downloads a file\n\t\tThen the Progress Listener will have its values updated\n\t\t\n    @Server\n\tScenario: Deleting directories on the remote FTP server\n\t\n\t\tGiven there is an FTP server running\n\t\tAnd the FTP server has a directory with contents\n\t\tWhen davos connects to the server\n\t\tAnd deletes a directory\n\t\tThen the directory is deleted on the server\n\t\t\n\t@Server\n\tScenario: Deleting directories on the remote FTP server (empty)\n\t\n\t\tGiven there is an FTP server running\n\t\tAnd the FTP server has a directory without contents\n\t\tWhen davos connects to the server\n\t\tAnd deletes a directory\n\t\tThen the directory is deleted on the server\n\t\t\n\t@SFTPServer\n\tScenario: Deleting directories on the remote SFTP server\n\t\n\t\tGiven there is an SFTP server running\n\t\tAnd the SFTP server has a directory with contents\n\t\tWhen davos connects to the SFTP server\n\t\tAnd deletes an SFTP directory\n\t\tThen the SFTP directory is deleted on the server\n\t\t\n\t@SFTPServer\n\tScenario: Deleting directories on the remote SFTP server (empty)\n\t\n\t\tGiven there is an SFTP server running\n\t\tAnd the SFTP server has a directory without contents\n\t\tWhen davos connects to the SFTP server\n\t\tAnd deletes an SFTP directory\n\t\tThen the SFTP directory is deleted on the server\n\t\t\n\t\t"
  },
  {
    "path": "src/cucumber/resources/Schedule.feature",
    "content": "@Schedule @Server\nFeature: Scheduling\n\n\tScenario: Finding files that match filters\n\t\n\t\tGiven there is an FTP server running\n\t\tAnd a schedule exists for that server, with filters\n\t\tWhen that schedule is run\n\t\tThen only the filtered files are downloaded\n\t\t\n\tScenario: Should delete files once matched and downloaded\n\t\n\t\tGiven there is an FTP server running\n\t\tAnd a schedule exists for that server, with filters\n\t\tAnd the schedule is set to delete host files\n\t\tWhen that schedule is run\n\t\tThen only the filtered files are downloaded\n\t\tAnd those files are deleted on the host\n\t\t\n\tScenario: Should download all files not matching filters if inverted\n\t\n\t\tGiven there is an FTP server running\n\t\tAnd a schedule exists for that server, with filters\n\t\tAnd the schedule is set to invert filters\n\t\tWhen that schedule is run\n\t\tThen all files not matching the filters are downloaded\n\t\t\n\tScenario: Should not download any files if filters are mandatory but not set\n\t\n\t\tGiven there is an FTP server running\n\t\tAnd a schedule exists for that server, without filters\n\t\tAnd the schedule is set to have mandatory filters\n\t\tWhen that schedule is run\n\t\tThen no files are downloaded\n\t\t\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/DavosApplication.java",
    "content": "package io.linuxserver.davos;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class DavosApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(DavosApplication.class, args);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/Version.java",
    "content": "package io.linuxserver.davos;\n\npublic class Version {\n\n    private int major;\n    private int minor;\n    private int patch;\n\n    public Version(int major, int minor, int patch) {\n\n        this.major = major;\n        this.minor = minor;\n        this.patch = patch;\n    }\n\n    public Version(String version) {\n\n        String[] bits = version.split(\"\\\\.\");\n\n        this.major = Integer.parseInt(bits[0]);\n        this.minor = Integer.parseInt(bits[1]);\n        this.patch = Integer.parseInt(bits[2]);\n    }\n\n    public int getMajor() {\n        return major;\n    }\n\n    public int getMinor() {\n        return minor;\n    }\n\n    public int getPatch() {\n        return patch;\n    }\n\n    public boolean isNewerThan(Version version) {\n\n        if (this.major > version.major)\n            return true;\n\n        if (this.minor > version.minor)\n            return true;\n        else if (this.minor < version.minor)\n            return false; \n        \n        if (this.patch > version.patch)\n            return true;\n\n        return false;\n    }\n\n    @Override\n    public String toString() {\n        return new StringBuilder().append(major).append(\".\").append(minor).append(\".\").append(patch).toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/converters/Converter.java",
    "content": "package io.linuxserver.davos.converters;\n\npublic interface Converter<S, T> {\n\n    T convertTo(S source);\n    \n    S convertFrom(T source);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/converters/HostConverter.java",
    "content": "package io.linuxserver.davos.converters;\n\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\nimport io.linuxserver.davos.web.Host;\nimport io.linuxserver.davos.web.selectors.ProtocolSelector;\n\n@Component\npublic class HostConverter implements Converter<HostModel, Host> {\n\n    @Override\n    public Host convertTo(HostModel source) {\n\n        Host host = new Host();\n        \n        host.setId(source.id);\n        host.setName(source.name);\n        host.setAddress(source.address);\n        host.setPort(source.port);\n        host.setUsername(source.username);\n        host.setPassword(source.password);\n        host.setProtocol(ProtocolSelector.valueOf(source.protocol.toString()));\n        host.setIdentityFileEnabled(source.isIdentityFileEnabled());\n        host.setIdentityFile(source.identityFile);\n        \n        return host;\n    }\n\n    @Override\n    public HostModel convertFrom(Host source) {\n\n        HostModel model = new HostModel();\n        \n        model.id = source.getId();\n        model.name = source.getName();\n        model.address = source.getAddress();\n        model.port = source.getPort();\n        model.username = source.getUsername();\n        model.password = source.getPassword();\n        model.protocol = TransferProtocol.valueOf(source.getProtocol().toString());\n        model.setIdentityFileEnabled(source.isIdentityFileEnabled());\n        model.identityFile = source.getIdentityFile();\n        \n        return model;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/converters/ScheduleConverter.java",
    "content": "package io.linuxserver.davos.converters;\n\nimport static java.util.stream.Collectors.toList;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.persistence.model.ActionModel;\nimport io.linuxserver.davos.persistence.model.FilterModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.web.API;\nimport io.linuxserver.davos.web.Filter;\nimport io.linuxserver.davos.web.Pushbullet;\nimport io.linuxserver.davos.web.SNS;\nimport io.linuxserver.davos.web.Schedule;\nimport io.linuxserver.davos.web.selectors.MethodSelector;\nimport io.linuxserver.davos.web.selectors.TransferSelector;\n\n@Component\npublic class ScheduleConverter implements Converter<ScheduleModel, Schedule> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleConverter.class);\n\n    @Override\n    public Schedule convertTo(ScheduleModel source) {\n\n        Schedule schedule = new Schedule();\n\n        schedule.setId(source.id);\n        schedule.setInterval(source.interval);\n        schedule.setLocalDirectory(source.localFilePath);\n        schedule.setName(source.name);\n        schedule.setHostDirectory(source.remoteFilePath);\n        schedule.setAutomatic(source.getStartAutomatically());\n        schedule.setHost(source.host.id);\n        schedule.setTransferType(TransferSelector.valueOf(source.transferType.toString()));\n        schedule.setMoveFileTo(source.moveFileTo);\n        schedule.getLastScannedFiles().addAll(source.scannedFiles.stream().map(f -> f.file).collect(toList()));\n        schedule.setFiltersMandatory(source.getFiltersMandatory());\n        schedule.setDeleteHostFile(source.getDeleteHostFile());\n        schedule.setInvertFilters(source.getInvertFilters());\n\n        if (source.getLastRunTime() > 0)\n            schedule.setLastRunTime(new DateTime(source.getLastRunTime()).toString(\"yyyy-MM-dd HH:mm:ss\"));\n\n        for (ActionModel action : source.actions) {\n\n            if (\"api\".equals(action.actionType)) {\n\n                API api = new API();\n                api.setId(action.id);\n                api.setUrl(action.f1);\n                api.setMethod(MethodSelector.valueOf(action.f2));\n                api.setContentType(action.f3);\n                api.setBody(action.f4);\n\n                schedule.getApis().add(api);\n\n            } else if (\"pushbullet\".equals(action.actionType)) {\n\n                Pushbullet notification = new Pushbullet();\n                notification.setId(action.id);\n                notification.setApiKey(action.f1);\n\n                schedule.getNotifications().getPushbullet().add(notification);\n\n            } else if (\"sns\".equals(action.actionType)) {\n\n                SNS sns = new SNS();\n                sns.setId(action.id);\n                sns.setTopicArn(action.f1);\n                sns.setRegion(action.f2);\n                sns.setAccessKey(action.f3);\n                sns.setSecretAccessKey(action.f4);\n\n                schedule.getNotifications().getSns().add(sns);\n            }\n        }\n\n        for (FilterModel filter : source.filters) {\n\n            Filter filterDto = new Filter();\n\n            filterDto.setId(filter.id);\n            filterDto.setValue(filter.value);\n\n            schedule.getFilters().add(filterDto);\n        }\n\n        return schedule;\n    }\n\n    @Override\n    public ScheduleModel convertFrom(Schedule source) {\n\n        ScheduleModel model = new ScheduleModel();\n\n        model.id = source.getId();\n        model.name = source.getName();\n        model.interval = source.getInterval();\n        model.localFilePath = source.getLocalDirectory();\n        model.remoteFilePath = source.getHostDirectory();\n        model.setStartAutomatically(source.isAutomatic());\n        model.transferType = FileTransferType.valueOf(source.getTransferType().toString());\n        model.moveFileTo = source.getMoveFileTo();\n        model.setFiltersMandatory(source.isFiltersMandatory());\n        model.setInvertFilters(source.isInvertFilters());\n        model.setDeleteHostFile(source.isDeleteHostFile());\n\n        if (StringUtils.isNotBlank(source.getMoveFileTo())) {\n\n            LOGGER.debug(\"Converting MoveTo to internal action: {}\", source.getMoveFileTo());\n\n            ActionModel moveTo = new ActionModel();\n            moveTo.actionType = \"move\";\n            moveTo.f1 = source.getMoveFileTo();\n            moveTo.schedule = model;\n\n            model.actions.add(moveTo);\n        }\n\n        for (Pushbullet action : source.getNotifications().getPushbullet()) {\n\n            LOGGER.debug(\"Converting Pushbullet to internal action: {}\", action.getApiKey());\n\n            ActionModel actionModel = new ActionModel();\n            actionModel.id = action.getId();\n            actionModel.actionType = \"pushbullet\";\n            actionModel.f1 = action.getApiKey();\n            actionModel.schedule = model;\n\n            model.actions.add(actionModel);\n        }\n\n        for (SNS action : source.getNotifications().getSns()) {\n\n            LOGGER.debug(\"Converting SNS to internal action: {}\", action.getTopicArn());\n\n            ActionModel actionModel = new ActionModel();\n            actionModel.id = action.getId();\n            actionModel.actionType = \"sns\";\n            actionModel.f1 = action.getTopicArn();\n            actionModel.f2 = action.getRegion();\n            actionModel.f3 = action.getAccessKey();\n            actionModel.f4 = action.getSecretAccessKey();\n            actionModel.schedule = model;\n\n            model.actions.add(actionModel);\n        }\n\n        for (API action : source.getApis()) {\n\n            LOGGER.debug(\"Converting API to internal action: {}\", action.getUrl());\n\n            ActionModel actionModel = new ActionModel();\n            actionModel.id = action.getId();\n            actionModel.actionType = \"api\";\n            actionModel.f1 = action.getUrl();\n            actionModel.f2 = action.getMethod().toString();\n            actionModel.f3 = action.getContentType();\n            actionModel.f4 = action.getBody();\n            actionModel.schedule = model;\n\n            model.actions.add(actionModel);\n        }\n\n        for (Filter filter : source.getFilters()) {\n\n            FilterModel filterModel = new FilterModel();\n\n            filterModel.id = filter.getId();\n            filterModel.value = filter.getValue();\n            filterModel.schedule = model;\n\n            model.filters.add(filterModel);\n        }\n\n        return model;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/delegation/services/HostService.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport java.util.List;\n\nimport io.linuxserver.davos.web.Host;\n\npublic interface HostService {\n\n    List<Host> fetchAllHosts();\n    \n    Host fetchHost(Long id);\n    \n    Host saveHost(Host host);\n    \n    void deleteHost(Long id);\n    \n    List<Long> fetchSchedulesUsingHost(Long id);\n    \n    void testConnection(Host host);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/delegation/services/HostServiceImpl.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport javax.annotation.Resource;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.converters.HostConverter;\nimport io.linuxserver.davos.exception.HostInUseException;\nimport io.linuxserver.davos.persistence.dao.HostDAO;\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.transfer.ftp.client.Client;\nimport io.linuxserver.davos.transfer.ftp.client.ClientFactory;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials.Identity;\nimport io.linuxserver.davos.web.Host;\n\n@Component\npublic class HostServiceImpl implements HostService {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(HostServiceImpl.class);\n\n    @Resource\n    private HostDAO hostDAO;\n\n    @Resource\n    private ScheduleDAO scheduleDAO;\n\n    @Resource\n    private HostConverter hostConverter;\n\n    @Override\n    public Host fetchHost(Long id) {\n        return toHost(hostDAO.fetchHost(id));\n    }\n\n    @Override\n    public Host saveHost(Host host) {\n\n        HostModel model = hostConverter.convertFrom(host);\n        return hostConverter.convertTo(hostDAO.saveHost(model));\n    }\n\n    @Override\n    public void deleteHost(Long id) {\n        \n        List<Long> schedulesUsingHost = fetchSchedulesUsingHost(id);\n        \n        if (schedulesUsingHost.isEmpty()) {\n            hostDAO.deleteHost(id);\n        } else {\n            throw new HostInUseException(\"Host is being used by Schedules: \" + schedulesUsingHost);\n        }\n    }\n\n    @Override\n    public List<Host> fetchAllHosts() {\n        return hostDAO.fetchAllHosts().stream().map(this::toHost).collect(Collectors.toList());\n    }\n\n    private Host toHost(HostModel model) {\n        return hostConverter.convertTo(model);\n    }\n\n    @Override\n    public List<Long> fetchSchedulesUsingHost(Long id) {\n        return scheduleDAO.fetchSchedulesUsingHost(id).stream().map(s -> s.id).collect(Collectors.toList());\n    }\n\n    @Override\n    public void testConnection(Host host) {\n\n        HostModel model = hostConverter.convertFrom(host);\n\n        LOGGER.info(\"Attempting to test connection to host\", model.address);\n\n        Client client = new ClientFactory().getClient(model.protocol);\n\n        LOGGER.debug(\"Credentials: {} : {}\", model.username, model.password);\n        \n        UserCredentials userCredentials;\n        \n        if (model.isIdentityFileEnabled())\n            userCredentials = new UserCredentials(model.username, new Identity(model.identityFile));\n        else\n            userCredentials = new UserCredentials(model.username, model.password);\n        \n        client.setCredentials(userCredentials);\n        client.setHost(model.address);\n        client.setPort(model.port);\n\n        LOGGER.debug(\"Making connection on port {}\", model.port);\n        client.connect();\n        LOGGER.info(\"Connection successful.\");\n        client.disconnect();\n        LOGGER.debug(\"Disconnected\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/delegation/services/ScheduleService.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport java.util.List;\n\nimport io.linuxserver.davos.web.Schedule;\n\npublic interface ScheduleService {\n\n    void startSchedule(Long id);\n\n    void stopSchedule(Long id);\n\n    void deleteSchedule(Long id);\n\n    List<Schedule> fetchAllSchedules();\n\n    Schedule fetchSchedule(Long id);\n    \n    Schedule createSchedule(Schedule schedule);\n    \n    Schedule updateSchedule(Schedule schedule);\n\n    void clearScannedFilesFromSchedule(Long id);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/delegation/services/ScheduleServiceImpl.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.List;\n\nimport javax.annotation.Resource;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.converters.HostConverter;\nimport io.linuxserver.davos.converters.ScheduleConverter;\nimport io.linuxserver.davos.persistence.dao.HostDAO;\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.schedule.ScheduleExecutor;\nimport io.linuxserver.davos.schedule.workflow.transfer.FTPTransfer;\nimport io.linuxserver.davos.web.Schedule;\nimport io.linuxserver.davos.web.Transfer;\nimport io.linuxserver.davos.web.Transfer.Progress;\n\n@Component\npublic class ScheduleServiceImpl implements ScheduleService {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleServiceImpl.class);\n\n    @Resource\n    private ScheduleConverter scheduleConverter;\n\n    @Resource\n    private ScheduleExecutor scheduleExecutor;\n\n    @Resource\n    private HostConverter hostConverter;\n\n    @Resource\n    private ScheduleDAO scheduleDAO;\n\n    @Resource\n    private HostDAO hostDAO;\n\n    @Override\n    public void startSchedule(Long id) {\n\n        LOGGER.info(\"Starting schedule\");\n        scheduleExecutor.startSchedule(id);\n        LOGGER.info(\"Schedule started\");\n    }\n\n    @Override\n    public void stopSchedule(Long id) {\n\n        LOGGER.info(\"Stopping schedule\");\n        scheduleExecutor.stopSchedule(id);\n        LOGGER.info(\"Schedule stopped\");\n    }\n\n    @Override\n    public void deleteSchedule(Long id) {\n\n        if (scheduleExecutor.isScheduleRunning(id)) {\n\n            LOGGER.debug(\"Schedule is running, so will stop it before deleting\");\n            stopSchedule(id);\n        }\n\n        scheduleDAO.deleteSchedule(id);\n    }\n\n    @Override\n    public List<Schedule> fetchAllSchedules() {\n        return scheduleDAO.getAll().stream().map(this::toSchedule).collect(toList());\n    }\n\n    @Override\n    public Schedule fetchSchedule(Long id) {\n        return toSchedule(scheduleDAO.fetchSchedule(id));\n    }\n\n    @Override\n    public Schedule createSchedule(Schedule schedule) {\n\n        ScheduleModel model = scheduleConverter.convertFrom(schedule);\n        model.host = getHostForSchedule(schedule.getHost());\n        return scheduleConverter.convertTo(scheduleDAO.updateConfig(model));\n    }\n\n    @Override\n    public Schedule updateSchedule(Schedule schedule) {\n\n        if (null == schedule.getId())\n            throw new IllegalArgumentException(\"Schdule has no ID\");\n        \n        ScheduleModel existingModel = scheduleDAO.fetchSchedule(schedule.getId());\n        ScheduleModel model = scheduleConverter.convertFrom(schedule);\n        \n        model.host = getHostForSchedule(schedule.getHost());\n        model.setLastRunTime(existingModel.getLastRunTime());\n        \n        return scheduleConverter.convertTo(scheduleDAO.updateConfig(model));\n    }\n\n    @Override\n    public void clearScannedFilesFromSchedule(Long id) {\n\n        ScheduleModel model = scheduleDAO.fetchSchedule(id);\n        model.scannedFiles.clear();\n        scheduleDAO.updateConfig(model);\n    }\n\n    private HostModel getHostForSchedule(Long id) {\n\n        HostModel hostModel = hostDAO.fetchHost(id);\n\n        if (null == hostModel) {\n\n            LOGGER.info(\"Schedule is referencing a host that does not exist\");\n            throw new IllegalArgumentException(\"Host with id \" + id + \" does not exist.\");\n        }\n        \n        return hostModel;\n    }\n\n    private Schedule toSchedule(ScheduleModel model) {\n\n        Schedule convertTo = scheduleConverter.convertTo(model);\n\n        if (scheduleExecutor.isScheduleRunning(convertTo.getId())) {\n\n            convertTo.setRunning(true);\n\n            List<FTPTransfer> transfers = scheduleExecutor.getRunningSchedule(convertTo.getId()).getSchedule().getTransfers();\n            convertTo.getTransfers().addAll(transfers.stream().map(this::toTransfer).collect(toList()));\n        }\n\n        return convertTo;\n    }\n\n    private Transfer toTransfer(FTPTransfer ftpTransfer) {\n\n        Transfer transfer = new Transfer();\n\n        transfer.setFileName(ftpTransfer.getFile().getName());\n        transfer.setFileSize(ftpTransfer.getFile().getSize());\n        transfer.setDirectory(ftpTransfer.getFile().isDirectory());\n        transfer.setStatus(ftpTransfer.getState().toString());\n\n        if (null != ftpTransfer.getListener()) {\n\n            Progress progress = new Progress();\n            progress.setPercentageComplete(ftpTransfer.getListener().getProgress());\n            progress.setTransferSpeed(ftpTransfer.getListener().getTransferSpeed());\n            transfer.setProgress(progress);\n        }\n\n        return transfer;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/delegation/services/SettingsService.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport io.linuxserver.davos.Version;\nimport io.linuxserver.davos.web.selectors.LogLevelSelector;\n\npublic interface SettingsService {\n\n    void setLoggingLevel(LogLevelSelector level);\n    \n    LogLevelSelector getCurrentLoggingLevel();\n\n    Version retrieveRemoteVersion();\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/delegation/services/SettingsServiceImpl.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport org.apache.logging.log4j.Level;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.http.converter.HttpMessageConversionException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.client.RestTemplate;\n\nimport io.linuxserver.davos.Version;\nimport io.linuxserver.davos.logging.LoggingManager;\nimport io.linuxserver.davos.web.selectors.LogLevelSelector;\n\n@Component\npublic class SettingsServiceImpl implements SettingsService {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(SettingsServiceImpl.class);\n\n    private LogLevelSelector currentLevel = LogLevelSelector.INFO;\n    private RestTemplate restTemplate = new RestTemplate();\n\n    @Override\n    public void setLoggingLevel(LogLevelSelector level) {\n        currentLevel = level;\n        LoggingManager.setLogLevel(Level.valueOf(level.toString()));\n    }\n\n    @Override\n    public LogLevelSelector getCurrentLoggingLevel() {\n        return currentLevel;\n    }\n\n    @Override\n    public Version retrieveRemoteVersion() {\n\n        try {\n\n            String gitHubURL = \"https://raw.githubusercontent.com/linuxserver/davos/LatestRelease/version.txt\";\n\n            LOGGER.debug(\"Calling out to GitHub to check for new version ({})\", gitHubURL);\n            ResponseEntity<String> response = restTemplate.exchange(gitHubURL, HttpMethod.GET,\n                    new HttpEntity<String>(new HttpHeaders()), String.class);\n\n            String body = response.getBody();\n            LOGGER.debug(\"GitHub responded with a {}, and body of {}\", response.getStatusCode(), body);\n            return new Version(body);\n\n        } catch (RestClientException | HttpMessageConversionException e) {\n            \n            LOGGER.error(\"Unable to get version from GitHub: {}\", e.getMessage(), e);\n            LOGGER.debug(\"Defaulting remote version to zero\");\n            return new Version(0, 0, 0);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/dto/ActionDTO.java",
    "content": "package io.linuxserver.davos.dto;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\npublic class ActionDTO {\n\n    public Long id;\n\n    public String actionType;\n    public String f1;\n    public String f2;\n    public String f3;\n    public String f4;\n    \n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/dto/FTPFileDTO.java",
    "content": "package io.linuxserver.davos.dto;\n\npublic class FTPFileDTO {\n\n    public String name;\n    public String extension;\n    public String modified;\n    public String path;\n    public long size;\n    public boolean directory;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/dto/FilterDTO.java",
    "content": "package io.linuxserver.davos.dto;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\npublic class FilterDTO {\n\n    public Long id;\n    public String value;\n    \n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/dto/HostDTO.java",
    "content": "package io.linuxserver.davos.dto;\n\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class HostDTO {\n\n    public String name;\n    public String address;\n    public int port;\n    public String username;\n    public String password;\n    public TransferProtocol protocol = TransferProtocol.FTP;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/dto/ScheduleDTO.java",
    "content": "package io.linuxserver.davos.dto;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ScheduleDTO {\n\n    public Long id;\n    public String name;\n    public boolean startAutomatically;\n    public int interval;\n    public TransferProtocol connectionType;\n    public String hostName;\n    public int port;\n    public String username;\n    public String password;\n    public String remoteFilePath;\n    public String localFilePath;\n    public long lastRun;\n    public boolean running;\n    public FileTransferType transferType;\n    public List<FilterDTO> filters = new ArrayList<FilterDTO>();\n    public List<ActionDTO> actions = new ArrayList<ActionDTO>();\n    \n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/dto/ScheduleProcessResponse.java",
    "content": "package io.linuxserver.davos.dto;\n\npublic class ScheduleProcessResponse {\n\n    public String message = \"OK\";\n    public Long id;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/exception/HostInUseException.java",
    "content": "package io.linuxserver.davos.exception;\n\npublic class HostInUseException extends RuntimeException {\n\n    private static final long serialVersionUID = 618892455818185964L;\n\n    public HostInUseException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/exception/ScheduleAlreadyRunningException.java",
    "content": "package io.linuxserver.davos.exception;\n\npublic class ScheduleAlreadyRunningException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public ScheduleAlreadyRunningException() {\n        super(\"The schedule is already running\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/exception/ScheduleNotRunningException.java",
    "content": "package io.linuxserver.davos.exception;\n\npublic class ScheduleNotRunningException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public ScheduleNotRunningException() {\n        super(\"The schedule was not running\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/logging/LoggingManager.java",
    "content": "package io.linuxserver.davos.logging;\n\nimport org.apache.logging.log4j.Level;\nimport org.apache.logging.log4j.core.config.Configurator;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class LoggingManager {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingManager.class);\n    \n    public static void enableDebug() {\n        Configurator.setLevel(\"io.linuxserver\", Level.DEBUG);\n        LOGGER.debug(\"DEBUG has been enabled\");\n    }\n    \n    public static void disableDebug() {\n        Configurator.setLevel(\"io.linuxserver\", Level.INFO);\n        LOGGER.info(\"DEBUG has been disabled. Back at INFO.\");\n    }\n    \n    public static void setLogLevel(Level level) {\n        LOGGER.info(\"Logging level now set at {}\", level);\n        Configurator.setLevel(\"io.linuxserver\", level);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/dao/DefaultHostDAO.java",
    "content": "package io.linuxserver.davos.persistence.dao;\n\nimport java.util.List;\n\nimport javax.annotation.Resource;\n\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.persistence.repository.HostRepository;\n\n@Component\npublic class DefaultHostDAO implements HostDAO {\n\n    @Resource\n    private HostRepository hostRepository;\n    \n    @Override\n    public HostModel saveHost(HostModel host) {\n        return hostRepository.save(host);\n    }\n\n    @Override\n    public HostModel fetchHost(Long id) {\n        return hostRepository.findOne(id);\n    }\n\n    @Override\n    public List<HostModel> fetchAllHosts() {\n        return hostRepository.findAll();\n    }\n\n    @Override\n    public void deleteHost(Long id) {\n        hostRepository.delete(id);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/dao/DefaultScheduleDAO.java",
    "content": "package io.linuxserver.davos.persistence.dao;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.List;\n\nimport javax.annotation.Resource;\n\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.persistence.model.ScannedFileModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.persistence.repository.ScheduleRepository;\n\n@Component\npublic class DefaultScheduleDAO implements ScheduleDAO {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultScheduleDAO.class);\n\n    @Resource\n    private ScheduleRepository configRepository;\n\n    @Override\n    public List<ScheduleModel> getAll() {\n        return configRepository.findAll();\n    }\n\n    @Override\n    public ScheduleModel fetchSchedule(Long id) {\n        return configRepository.findOne(id);\n    }\n\n    @Override\n    public ScheduleModel updateConfig(ScheduleModel model) {\n\n        if (null != model.id) {\n\n            LOGGER.debug(\"Getting original view of schedule to overlay scannedFiles. \"\n                    + \"These should only be updated by 'updateScannedFilesOnSchedule'\");\n            ScheduleModel current = fetchSchedule(model.id);\n            model.scannedFiles = current.scannedFiles;\n        }\n\n        LOGGER.debug(\"Saving model: {}, filters {}\", model, model.filters);\n        ScheduleModel savedModel = configRepository.save(model);\n        LOGGER.debug(\"Schedule model has been saved. Returned values from DB are: {}\", model);\n        return savedModel;\n    }\n\n    @Override\n    public List<ScheduleModel> fetchSchedulesUsingHost(Long hostId) {\n\n        List<ScheduleModel> models = configRepository.findByHost_Id(hostId);\n        LOGGER.debug(\"Found {} schedules using host {}\", models.size(), hostId);\n        return models;\n    }\n\n    @Override\n    public void updateScannedFilesOnSchedule(Long id, List<String> newlyScannedFiles) {\n\n        ScheduleModel model = configRepository.findOne(id);\n\n        model.scannedFiles.clear();\n        model.scannedFiles.addAll(newlyScannedFiles.stream().map(f -> toScannedFileModel(f, model)).collect(toList()));\n        model.setLastRunTime(DateTime.now().getMillis());\n\n        configRepository.save(model);\n    }\n\n    private ScannedFileModel toScannedFileModel(String fileName, ScheduleModel model) {\n\n        ScannedFileModel scannedFileModel = new ScannedFileModel();\n        scannedFileModel.file = fileName;\n        scannedFileModel.schedule = model;\n\n        return scannedFileModel;\n    }\n\n    @Override\n    public void deleteSchedule(Long id) {\n        configRepository.delete(id);\n        LOGGER.info(\"Schedule has been deleted\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/dao/HostDAO.java",
    "content": "package io.linuxserver.davos.persistence.dao;\n\nimport java.util.List;\n\nimport io.linuxserver.davos.persistence.model.HostModel;\n\npublic interface HostDAO {\n\n    HostModel saveHost(HostModel host);\n    \n    HostModel fetchHost(Long id);\n    \n    List<HostModel> fetchAllHosts();\n    \n    void deleteHost(Long id);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/dao/ScheduleDAO.java",
    "content": "package io.linuxserver.davos.persistence.dao;\n\nimport java.util.List;\n\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\n\npublic interface ScheduleDAO {\n\n    List<ScheduleModel> getAll();\n\n    List<ScheduleModel> fetchSchedulesUsingHost(Long hostId);\n    \n    ScheduleModel fetchSchedule(Long id);\n\n    ScheduleModel updateConfig(ScheduleModel model);\n    \n    void updateScannedFilesOnSchedule(Long id, List<String> newlyScannedFiles);\n\n    void deleteSchedule(Long id);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/model/ActionModel.java",
    "content": "package io.linuxserver.davos.persistence.model;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.Id;\nimport javax.persistence.JoinColumn;\nimport javax.persistence.ManyToOne;\n\n@Entity\npublic class ActionModel {\n\n    @Id\n    @GeneratedValue\n    public Long id;\n\n    @Column\n    public String actionType;\n\n    @Column\n    public String f1;\n\n    @Column\n    public String f2;\n\n    @Column\n    public String f3;\n\n    @Column\n    public String f4;\n\n    @ManyToOne\n    @JoinColumn(name = \"action_schedule_id\")\n    public ScheduleModel schedule;\n\n    @Override\n    public String toString() {\n        return \"ActionModel [id=\" + id + \", actionType=\" + actionType + \", f1=\" + f1 + \", f2=\" + f2 + \", f3=\" + f3 + \", f4=\" + f4\n                + \"]\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/model/FilterModel.java",
    "content": "package io.linuxserver.davos.persistence.model;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.Id;\nimport javax.persistence.JoinColumn;\nimport javax.persistence.ManyToOne;\n\n@Entity\npublic class FilterModel {\n\n    @Id\n    @GeneratedValue\n    public Long id;\n    \n    @Column\n    public String value;\n    \n    @ManyToOne\n    @JoinColumn(name = \"filter_schedule_id\")\n    public ScheduleModel schedule;\n\n    @Override\n    public String toString() {\n        return \"FilterModel [id=\" + id + \", value=\" + value + \"]\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/model/HostModel.java",
    "content": "package io.linuxserver.davos.persistence.model;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.Id;\nimport javax.persistence.OneToMany;\n\nimport org.hibernate.annotations.LazyCollection;\nimport org.hibernate.annotations.LazyCollectionOption;\n\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\n@Entity\npublic class HostModel {\n\n    @Id\n    @GeneratedValue\n    public Long id;\n\n    @Column\n    public String name;\n\n    @Column\n    public String address;\n\n    @Column\n    public int port;\n\n    @Column\n    public TransferProtocol protocol = TransferProtocol.FTP;\n\n    @Column\n    public String username;\n\n    @Column\n    public String password;\n\n    @Column\n    public String identityFile;\n\n    @Column\n    private Boolean identityFileEnabled;\n\n    public boolean isIdentityFileEnabled() {\n\n        if (null == identityFileEnabled)\n            return false;\n\n        return identityFileEnabled;\n    }\n\n    public void setIdentityFileEnabled(boolean identityFileEnabled) {\n        this.identityFileEnabled = identityFileEnabled;\n    }\n\n    @OneToMany(mappedBy = \"host\", orphanRemoval = false)\n    @LazyCollection(LazyCollectionOption.TRUE)\n    public List<ScheduleModel> schedules = new ArrayList<ScheduleModel>();\n\n    @Override\n    public String toString() {\n        return \"HostModel [id=\" + id + \", name=\" + name + \", address=\" + address + \", port=\" + port + \", protocol=\"\n                + protocol + \", username=\" + username + \", password=\" + password + \", identityFile=\" + identityFile\n                + \", identityFileEnabled=\" + identityFileEnabled + \", schedules=\" + schedules + \"]\";\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/model/ScannedFileModel.java",
    "content": "package io.linuxserver.davos.persistence.model;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.Id;\nimport javax.persistence.JoinColumn;\nimport javax.persistence.ManyToOne;\n\n@Entity\npublic class ScannedFileModel {\n\n    @Id\n    @GeneratedValue\n    public Long id;\n    \n    @Column\n    public String file;\n    \n    @ManyToOne\n    @JoinColumn(name = \"scanned_file_schedule_id\")\n    public ScheduleModel schedule;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/model/ScheduleModel.java",
    "content": "package io.linuxserver.davos.persistence.model;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.persistence.CascadeType;\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.FetchType;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.Id;\nimport javax.persistence.JoinColumn;\nimport javax.persistence.ManyToOne;\nimport javax.persistence.OneToMany;\n\nimport org.hibernate.annotations.LazyCollection;\nimport org.hibernate.annotations.LazyCollectionOption;\n\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\n\n@Entity\npublic class ScheduleModel {\n\n    @Id\n    @GeneratedValue\n    public Long id;\n\n    @Column\n    public String name;\n\n    @Column\n    private Boolean startAutomatically;\n\n    @Column\n    public int interval;\n\n    @Column\n    public String remoteFilePath;\n\n    @Column\n    public String localFilePath;\n\n    @Column\n    public String moveFileTo;\n\n    @Column\n    private Boolean filtersMandatory;\n\n    @Column\n    private Boolean deleteHostFile;\n\n    @Column\n    private Boolean invertFilters;\n    \n    @Column\n    private Long lastRunTime;\n\n    public long getLastRunTime() {\n        \n        if (null != lastRunTime)\n            return lastRunTime;\n        \n        return 0;\n    }\n    \n    public Boolean getFiltersMandatory() {\n\n        if (null != filtersMandatory)\n            return filtersMandatory;\n\n        return false;\n    }\n    \n    public void setLastRunTime(long millis) {\n        lastRunTime = millis;\n    }\n\n    public void setFiltersMandatory(boolean filtersMandatory) {\n        this.filtersMandatory = filtersMandatory;\n    }\n\n    public Boolean getDeleteHostFile() {\n\n        if (null != deleteHostFile)\n            return deleteHostFile;\n\n        return false;\n    }\n\n    public void setDeleteHostFile(boolean deleteHostFile) {\n        this.deleteHostFile = deleteHostFile;\n    }\n\n    public Boolean getStartAutomatically() {\n\n        if (null != startAutomatically)\n            return startAutomatically;\n\n        return false;\n    }\n\n    public void setStartAutomatically(boolean startAutomatically) {\n        this.startAutomatically = startAutomatically;\n    }\n\n    public Boolean getInvertFilters() {\n\n        if (null != invertFilters)\n            return invertFilters;\n\n        return false;\n    }\n\n    public void setInvertFilters(boolean invertFilters) {\n        this.invertFilters = invertFilters;\n    }\n\n    @Column\n    public FileTransferType transferType = FileTransferType.FILE;\n\n    @ManyToOne(fetch = FetchType.EAGER)\n    @JoinColumn(name = \"schedule_host_id\")\n    public HostModel host;\n\n    @OneToMany(orphanRemoval = true, mappedBy = \"schedule\", cascade = CascadeType.ALL)\n    @LazyCollection(LazyCollectionOption.FALSE)\n    public List<FilterModel> filters = new ArrayList<FilterModel>();\n\n    @OneToMany(orphanRemoval = true, mappedBy = \"schedule\", cascade = CascadeType.ALL)\n    @LazyCollection(LazyCollectionOption.FALSE)\n    public List<ActionModel> actions = new ArrayList<ActionModel>();\n\n    @OneToMany(orphanRemoval = true, mappedBy = \"schedule\", cascade = CascadeType.ALL)\n    @LazyCollection(LazyCollectionOption.FALSE)\n    public List<ScannedFileModel> scannedFiles = new ArrayList<ScannedFileModel>();\n\n    @Override\n    public String toString() {\n        return \"ScheduleModel [id=\" + id + \", name=\" + name + \", startAutomatically=\" + startAutomatically + \", interval=\"\n                + interval + \", remoteFilePath=\" + remoteFilePath + \", localFilePath=\" + localFilePath + \", transferType=\"\n                + transferType + \", filters=\" + filters + \", actions=\" + actions + \"]\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/repository/HostRepository.java",
    "content": "package io.linuxserver.davos.persistence.repository;\n\nimport java.util.List;\n\nimport org.springframework.data.repository.CrudRepository;\n\nimport io.linuxserver.davos.persistence.model.HostModel;\n\npublic interface HostRepository extends CrudRepository<HostModel, Long> {\n\n    List<HostModel> findAll();\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/persistence/repository/ScheduleRepository.java",
    "content": "package io.linuxserver.davos.persistence.repository;\n\nimport java.util.List;\n\nimport org.springframework.data.repository.CrudRepository;\n\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\n\npublic interface ScheduleRepository extends CrudRepository<ScheduleModel, Long> {\n\n    List<ScheduleModel> findAll();\n    \n    List<ScheduleModel> findByHost_Id(Long hostId);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/RunnableSchedule.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.schedule.workflow.ScheduleWorkflow;\nimport io.linuxserver.davos.schedule.workflow.transfer.FTPTransfer;\n\npublic class RunnableSchedule implements Runnable {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(RunnableSchedule.class);\n    \n    private ScheduleDAO configurationDAO;\n    private Long scheduleId;\n    private ScheduleWorkflow scheduleWorkflow;\n\n    public RunnableSchedule(Long scheduleId, ScheduleDAO configurationDAO) {\n\n        this.scheduleId = scheduleId;\n        this.configurationDAO = configurationDAO;\n    }\n\n    @Override\n    public void run() {\n\n        LOGGER.info(\"Starting schedule {}\", scheduleId);\n        \n        ScheduleModel model = configurationDAO.fetchSchedule(scheduleId);\n        \n        ScheduleConfiguration config = ScheduleConfigurationFactory.createConfig(model);\n        scheduleWorkflow = new ScheduleWorkflow(config);\n\n        LOGGER.debug(\"Setting last scanned files on workflow before starting.\");\n        scheduleWorkflow.getFilesFromLastScan().addAll(model.scannedFiles.stream().map(sf -> sf.file).collect(toList()));\n        \n        LOGGER.debug(\"Starting workflow\");\n        scheduleWorkflow.start();\n        LOGGER.debug(\"Workflow finished\");\n        \n        LOGGER.debug(\"Saving newly scanned files against schedule\");\n        configurationDAO.updateScannedFilesOnSchedule(scheduleId, scheduleWorkflow.getFilesFromLastScan());\n    }\n    \n    public List<FTPTransfer> getTransfers() {\n        return scheduleWorkflow.getFilesToDownload();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/RunningSchedule.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport java.util.concurrent.ScheduledFuture;\n\npublic class RunningSchedule {\n\n    private final ScheduledFuture<?> future;\n    private final RunnableSchedule schedule;\n    \n    public RunningSchedule(ScheduledFuture<?> future, RunnableSchedule schedule) {\n        this.future = future;\n        this.schedule = schedule;\n    }\n    \n    public ScheduledFuture<?> getFuture() {\n        return future;\n    }\n    \n    public RunnableSchedule getSchedule() {\n        return schedule;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/ScheduleConfiguration.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.linuxserver.davos.schedule.workflow.actions.PostDownloadAction;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\n\npublic class ScheduleConfiguration {\n\n    private TransferProtocol connectionType;\n    private String hostname;\n    private int port;\n    private UserCredentials credentials;\n    private String remoteFilePath;\n    private String localFilePath;\n    private String scheduleName;\n    private List<String> filters = new ArrayList<String>();\n    private List<PostDownloadAction> actions = new ArrayList<PostDownloadAction>();\n    private FileTransferType transferType;\n    private boolean filtersMandatory;\n    private boolean invertFilters;\n    private boolean deleteHostFile;\n\n    public ScheduleConfiguration(final String scheduleName, final TransferProtocol protocol, final String hostname,\n            final int port, final UserCredentials credentials, final String remoteFilePath, final String localFilePath,\n            FileTransferType transferType, boolean filtersMandatory, boolean invertFilters, boolean deleteHostFile) {\n\n        this.scheduleName = scheduleName;\n        this.connectionType = protocol;\n        this.hostname = hostname;\n        this.port = port;\n        this.credentials = credentials;\n        this.localFilePath = localFilePath;\n        this.remoteFilePath = remoteFilePath;\n        this.transferType = transferType;\n        this.filtersMandatory = filtersMandatory;\n        this.invertFilters = invertFilters;\n        this.deleteHostFile = deleteHostFile;\n    }\n\n    public TransferProtocol getConnectionType() {\n        return connectionType;\n    }\n\n    public String getHostName() {\n        return hostname;\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    public UserCredentials getCredentials() {\n        return credentials;\n    }\n\n    public String getRemoteFilePath() {\n        return remoteFilePath;\n    }\n\n    public String getLocalFilePath() {\n        return localFilePath;\n    }\n\n    public List<String> getFilters() {\n        return filters;\n    }\n\n    public void setFilters(List<String> filters) {\n        this.filters = filters;\n    }\n\n    public String getScheduleName() {\n        return scheduleName;\n    }\n\n    public List<PostDownloadAction> getActions() {\n        return actions;\n    }\n\n    public void setActions(List<PostDownloadAction> actions) {\n        this.actions = actions;\n    }\n\n    public FileTransferType getTransferType() {\n        return transferType;\n    }\n    \n    public boolean isFiltersMandatory() {\n        return filtersMandatory;\n    }\n    \n    public boolean isInvertFilters() {\n        return invertFilters;\n    }\n    \n    public boolean isDeleteHostFile() {\n        return deleteHostFile;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/ScheduleConfigurationFactory.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport org.apache.commons.lang3.StringUtils;\n\nimport io.linuxserver.davos.persistence.model.ActionModel;\nimport io.linuxserver.davos.persistence.model.FilterModel;\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.schedule.workflow.actions.HttpAPICallAction;\nimport io.linuxserver.davos.schedule.workflow.actions.MoveFileAction;\nimport io.linuxserver.davos.schedule.workflow.actions.PushbulletNotifyAction;\nimport io.linuxserver.davos.schedule.workflow.actions.SNSNotifyAction;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials.Identity;\n\npublic class ScheduleConfigurationFactory {\n\n    public static ScheduleConfiguration createConfig(ScheduleModel model) {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(model.name, model.host.protocol, model.host.address,\n                model.host.port, buildCredentials(model.host), model.remoteFilePath, model.localFilePath, model.transferType,\n                model.getFiltersMandatory(), model.getInvertFilters(), model.getDeleteHostFile());\n\n        if (StringUtils.isNotBlank(model.moveFileTo))\n            config.getActions().add(new MoveFileAction(config.getLocalFilePath(), model.moveFileTo));\n\n        if (null != model.filters)\n            addFilters(model, config);\n\n        if (null != model.actions)\n            addActions(model, config);\n\n        return config;\n    }\n\n    private static UserCredentials buildCredentials(HostModel host) {\n\n        if (host.isIdentityFileEnabled())\n            return new UserCredentials(host.username, new Identity(host.identityFile));\n\n        return new UserCredentials(host.username, host.password);\n    }\n\n    private static void addActions(ScheduleModel model, ScheduleConfiguration config) {\n\n        for (ActionModel action : model.actions) {\n\n            if (\"pushbullet\".equals(action.actionType))\n                config.getActions().add(new PushbulletNotifyAction(action.f1));\n            \n            if (\"sns\".equals(action.actionType))\n                config.getActions().add(new SNSNotifyAction(action.f2, action.f1, action.f3, action.f4));\n\n            if (\"api\".equals(action.actionType))\n                config.getActions().add(new HttpAPICallAction(action.f1, action.f2, action.f3, action.f4));\n        }\n    }\n\n    private static void addFilters(ScheduleModel model, ScheduleConfiguration config) {\n\n        for (FilterModel filter : model.filters)\n            config.getFilters().add(filter.value);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/ScheduleExecutor.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.TimeUnit;\n\nimport javax.annotation.PostConstruct;\nimport javax.annotation.Resource;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport io.linuxserver.davos.exception.ScheduleAlreadyRunningException;\nimport io.linuxserver.davos.exception.ScheduleNotRunningException;\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\n\n@Component\npublic class ScheduleExecutor {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleExecutor.class);\n\n    private Map<Long, RunningSchedule> runningSchedules = new HashMap<>();\n\n    @Resource\n    private ScheduleDAO scheduleConfigurationDAO;\n\n    private ScheduledExecutorService scheduledExecutorService;\n\n    public ScheduleExecutor() {\n        this.scheduledExecutorService = Executors.newScheduledThreadPool(10);\n    }\n\n    public boolean isScheduleRunning(Long id) {\n        return runningSchedules.containsKey(id);\n    }\n    \n    public RunningSchedule getRunningSchedule(Long id) {\n        return runningSchedules.get(id);\n    }\n\n    @PostConstruct\n    public void runAutomaticStartupSchedules() {\n\n        LOGGER.info(\"Initialising automatic startup schedules\");\n\n        for (ScheduleModel model : scheduleConfigurationDAO.getAll()) {\n\n            if (model.getStartAutomatically()) {\n\n                RunnableSchedule runnable = new RunnableSchedule(model.id, scheduleConfigurationDAO);\n                ScheduledFuture<?> runningSchedule = scheduledExecutorService.scheduleAtFixedRate(runnable, 0, model.interval,\n                        TimeUnit.MINUTES);\n\n                runningSchedules.put(model.id, new RunningSchedule(runningSchedule, runnable));\n            }\n        }\n\n        LOGGER.info(\"Automatic startup schedules should now be running\");\n    }\n\n    public void startSchedule(Long id) throws ScheduleAlreadyRunningException {\n\n        if (!runningSchedules.containsKey(id)) {\n\n            ScheduleModel model = scheduleConfigurationDAO.fetchSchedule(id);\n            RunnableSchedule runnable = new RunnableSchedule(model.id, scheduleConfigurationDAO);\n\n            LOGGER.info(\"Starting schedule {}\", id);\n            ScheduledFuture<?> runningSchedule = scheduledExecutorService.scheduleAtFixedRate(runnable, 0, model.interval,\n                    TimeUnit.MINUTES);\n\n            runningSchedules.put(model.id, new RunningSchedule(runningSchedule, runnable));\n\n        } else {\n            throw new ScheduleAlreadyRunningException();\n        }\n    }\n\n    public void stopSchedule(Long id) throws ScheduleNotRunningException {\n\n        if (runningSchedules.containsKey(id)) {\n\n            LOGGER.info(\"Stopping schedule {}\", id);\n\n            ScheduledFuture<?> future = runningSchedules.get(id).getFuture();\n            \n            if (!future.isCancelled()) {\n\n                future.cancel(true);\n                runningSchedules.remove(id);\n                LOGGER.info(\"Schedule should now be stopped\");\n            }\n\n        } else {\n            throw new ScheduleNotRunningException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/ConnectWorkflowStep.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.client.Client;\nimport io.linuxserver.davos.transfer.ftp.client.ClientFactory;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\n\npublic class ConnectWorkflowStep extends WorkflowStep {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectWorkflowStep.class);\n\n    private ClientFactory clientFactory = new ClientFactory();\n\n    public ConnectWorkflowStep() {\n        this.nextStep = new FilterFilesWorkflowStep();\n    }\n\n    @Override\n    public void runStep(ScheduleWorkflow schedule) {\n\n        Client client = clientFactory.getClient(schedule.getConfig().getConnectionType());\n\n        client.setCredentials(schedule.getConfig().getCredentials());\n        client.setHost(schedule.getConfig().getHostName());\n        client.setPort(schedule.getConfig().getPort());\n\n        try {\n\n            LOGGER.info(\"Connecting to host {} on port {}\", schedule.getConfig().getHostName(), schedule.getConfig().getPort());\n            schedule.setConnection(client.connect());\n            schedule.setClient(client);\n\n            LOGGER.info(\"Connection success. Moving onto next step\");\n            nextStep.runStep(schedule);\n\n        } catch (FTPException e) {\n\n            LOGGER.error(\"Unable to create connection to {} on port {}. Falling back. Will try again next time.\",\n                    schedule.getConfig().getHostName(), schedule.getConfig().getPort());\n            LOGGER.error(\"Error was: {}\", e.getMessage());\n            LOGGER.debug(\"Stacktrace\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/DisconnectWorkflowStep.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\n\npublic class DisconnectWorkflowStep extends WorkflowStep {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(DisconnectWorkflowStep.class);\n    \n    @Override\n    public void runStep(ScheduleWorkflow schedule) {\n\n        try {\n            schedule.getClient().disconnect();\n        } catch (FTPException e) {\n            LOGGER.error(\"Unable to disconnect from host. Error was: {}\", e.getMessage());\n            LOGGER.debug(\"Stacktrace\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/DownloadFilesWorkflowStep.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.schedule.ScheduleConfiguration;\nimport io.linuxserver.davos.schedule.workflow.transfer.FTPTransfer;\nimport io.linuxserver.davos.schedule.workflow.transfer.TransferStrategy;\nimport io.linuxserver.davos.schedule.workflow.transfer.TransferStrategyFactory;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ListenerFactory;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\n\npublic class DownloadFilesWorkflowStep extends WorkflowStep {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(DownloadFilesWorkflowStep.class);\n\n    private TransferStrategyFactory transferStrategyFactory = new TransferStrategyFactory();\n\n    public DownloadFilesWorkflowStep() {\n        this.nextStep = new DisconnectWorkflowStep();\n    }\n\n    @Override\n    public void runStep(ScheduleWorkflow schedule) {\n\n        ScheduleConfiguration config = schedule.getConfig();\n\n        TransferStrategy strategyToUse = transferStrategyFactory.getStrategy(config.getTransferType(), schedule.getConnection());\n        LOGGER.debug(\"Strategy chosen for downloads is {}, selected {}\", config.getTransferType(), strategyToUse);\n        strategyToUse.setPostDownloadActions(schedule.getConfig().getActions());\n        LOGGER.debug(\"PostDownloadActions: {} have been set against chosen strategy\", schedule.getConfig().getActions());\n\n        try {\n\n            if (schedule.getFilesToDownload().isEmpty())\n                LOGGER.info(\"There are no files to download in this run\");\n            \n            for (FTPTransfer transfer : schedule.getFilesToDownload()) {\n\n                LOGGER.debug(\"Generating listener for transfer\");\n\n                FTPFile file = transfer.getFile();\n                \n                ProgressListener listener = new ListenerFactory().createListener(config.getConnectionType());\n                schedule.getConnection().setProgressListener(listener);\n                transfer.setListener(listener);\n                \n                strategyToUse.transferFile(transfer, config.getLocalFilePath());\n                \n                if (config.isDeleteHostFile())\n                    schedule.getConnection().deleteRemoteFile(file);\n            }\n\n            LOGGER.info(\"Download step complete. Moving onto next step\");\n            schedule.getFilesToDownload().clear();\n\n        } catch (FTPException e) {\n\n            LOGGER.error(\"Unable to complete download. Error was: {}\", e.getMessage());\n            LOGGER.debug(\"Stacktrace\", e);\n            LOGGER.info(\"Clearing current queue and will still continue to next step\");\n            schedule.getFilesToDownload().clear();\n        }\n\n        nextStep.runStep(schedule);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/FilterFilesWorkflowStep.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.schedule.workflow.filter.ReferentialFileFilter;\nimport io.linuxserver.davos.schedule.workflow.transfer.FTPTransfer;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\nimport io.linuxserver.davos.util.PatternBuilder;\n\npublic class FilterFilesWorkflowStep extends WorkflowStep {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FilterFilesWorkflowStep.class);\n\n    public FilterFilesWorkflowStep() {\n\n        this.nextStep = new DownloadFilesWorkflowStep();\n        this.backoutStep = new DisconnectWorkflowStep();\n    }\n\n    @Override\n    public void runStep(ScheduleWorkflow schedule) {\n\n        try {\n\n            List<String> filters = schedule.getConfig().getFilters();\n\n            List<FTPFile> allFiles = schedule.getConnection().listFiles(schedule.getConfig().getRemoteFilePath()).stream()\n                    .filter(removeCurrentAndParentDirs()).collect(toList());\n            List<FTPFile> filesToFilter = new ReferentialFileFilter(schedule.getFilesFromLastScan()).filter(allFiles);\n            List<FTPFile> filteredFiles = new ArrayList<FTPFile>();\n\n            LOGGER.debug(\"Clearing pending download list\");\n            schedule.getFilesToDownload().clear();\n\n            if (noFilteringRequired(schedule, filters)) {\n\n                LOGGER.info(\"Filter list was empty. Adding all found files to list\");\n                LOGGER.debug(\"All files: {}\", filesToFilter.stream().map(f -> f.getName()).collect(Collectors.toList()));\n                schedule.getFilesToDownload().addAll(filesToFilter.stream().map(f -> new FTPTransfer(f)).collect(toList()));\n\n            } else {\n\n                LOGGER.debug(\"Filters used {}\", filters);\n                LOGGER.debug(\"Files to filter against {}\", filesToFilter.stream().map(f -> f.getName()).collect(toList()));\n\n                boolean invertFilters = schedule.getConfig().isInvertFilters();\n\n                for (FTPFile file : filesToFilter)\n                    filterFilesByName(invertFilters, filters, filteredFiles, file);\n\n                schedule.getFilesToDownload().addAll(filteredFiles.stream().map(f -> new FTPTransfer(f)).collect(toList()));\n            }\n\n            LOGGER.debug(\"Resetting files from scan to files in this scan\");\n            schedule.getFilesFromLastScan().clear();\n            schedule.getFilesFromLastScan().addAll(allFiles.stream().map(f -> f.getName()).collect(toList()));\n            LOGGER.debug(\"Files from last scan set to {}\", schedule.getFilesFromLastScan());\n\n            LOGGER.info(\"Filtered files. Moving onto next step\");\n            nextStep.runStep(schedule);\n\n        } catch (FTPException e) {\n\n            LOGGER.error(\"Unable to filter files. Error message was: {}\", e.getMessage());\n            LOGGER.debug(\"Stacktrace\", e);\n\n            LOGGER.info(\"Backing out of this run.\");\n            backoutStep.runStep(schedule);\n        }\n    }\n\n    private boolean noFilteringRequired(ScheduleWorkflow schedule, List<String> filters) {\n        return filters.isEmpty() && !schedule.getConfig().isFiltersMandatory();\n    }\n\n    private void filterFilesByName(boolean invertFilters, List<String> filters, List<FTPFile> filteredFiles, FTPFile file) {\n\n        if (invertFilters) {\n\n            boolean filterForFileFound = false;\n\n            for (String filter : filters) {\n\n                String expression = PatternBuilder.buildFromFilterString(filter);\n\n                if (file.getName().toLowerCase().matches(expression.toLowerCase()))\n                    filterForFileFound = true;\n            }\n\n            if (!filterForFileFound) {\n\n                LOGGER.debug(\"Inverting enabled - no matching filter found for file {}, so adding to download list.\", file.getName());\n                filteredFiles.add(file);\n            }\n\n        } else {\n\n            for (String filter : filters) {\n\n                String expression = PatternBuilder.buildFromFilterString(filter);\n\n                if (file.getName().toLowerCase().matches(expression.toLowerCase())) {\n\n                    LOGGER.debug(\"Matched {} to {}. Adding to final filter list.\", file.getName().toLowerCase(),\n                            expression.toLowerCase());\n\n                    filteredFiles.add(file);\n\n                    return;\n                }\n            }\n        }\n    }\n\n    private Predicate<? super FTPFile> removeCurrentAndParentDirs() {\n        return file -> !file.getName().equals(\".\") && !file.getName().equals(\"..\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/ScheduleWorkflow.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.schedule.ScheduleConfiguration;\nimport io.linuxserver.davos.schedule.workflow.transfer.FTPTransfer;\nimport io.linuxserver.davos.transfer.ftp.client.Client;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\n\npublic class ScheduleWorkflow {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleWorkflow.class);\n    \n    private ScheduleConfiguration config;\n    \n    private Client client;\n    private Connection connection;\n    private List<String> filesFromLastScan = new ArrayList<>();\n    private List<FTPTransfer> filesToDownload = new ArrayList<>();\n    \n    public ScheduleWorkflow(ScheduleConfiguration config) {\n        this.config = config;\n    }\n    \n    protected Client getClient() {\n        return client;\n    }\n\n    protected ScheduleConfiguration getConfig() {\n        return config;\n    }\n\n    protected Connection getConnection() {\n        return connection;\n    }\n    \n    public void start() {\n        \n        LOGGER.info(\"Running schedule: {}\", config.getScheduleName());\n        new ConnectWorkflowStep().runStep(this);\n        LOGGER.info(\"Finished schedule run: {}\", config.getScheduleName());\n    }\n\n    protected void setConnection(Connection connection) {\n        this.connection = connection;\n    }\n\n    protected void setClient(Client client) {\n        this.client = client;\n    }\n\n    public List<String> getFilesFromLastScan() {\n        return filesFromLastScan;\n    }\n\n    public List<FTPTransfer> getFilesToDownload() {\n        return filesToDownload;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/WorkflowStep.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\npublic abstract class WorkflowStep {\n\n    protected WorkflowStep nextStep;\n    protected WorkflowStep backoutStep;\n\n    abstract public void runStep(ScheduleWorkflow schedule);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/actions/HttpAPICallAction.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.converter.HttpMessageConversionException;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.client.RestTemplate;\n\npublic class HttpAPICallAction implements PostDownloadAction {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(HttpAPICallAction.class);\n\n    private RestTemplate restTemplate = new RestTemplate();\n\n    private String url;\n    private HttpMethod method;\n    private String contentType;\n    private String body;\n\n    public HttpAPICallAction(String url, String method, String contentType, String body) {\n\n        this.url = url;\n        this.method = HttpMethod.valueOf(method);\n        this.contentType = contentType;\n        this.body = body;\n    }\n\n    @Override\n    public void execute(PostDownloadExecution execution) {\n\n        try {\n            \n            HttpHeaders headers = new HttpHeaders();\n            headers.add(\"Content-Type\", contentType);\n\n            LOGGER.info(\"Sending message to generic API for {}\", execution.fileName);\n            HttpEntity<String> httpEntity = new HttpEntity<String>(resolveFilename(body, execution.fileName), headers);\n            LOGGER.debug(\"Sending {} message {} to generic API: {}\", method, httpEntity, url);\n            restTemplate.exchange(resolveFilename(url, execution.fileName), method, httpEntity, Object.class);\n            \n        } catch (RestClientException | HttpMessageConversionException e) {\n\n            LOGGER.debug(\"Full stacktrace\", e);\n            LOGGER.error(\"Unable to complete message to generic API. Given error: {}\", e.getMessage());\n        }\n    }\n    \n    @Override\n    public String toString() {\n        return getClass().getSimpleName();\n    }\n    \n    private String resolveFilename(String value, String filename) {\n        return value.replace(\"$filename\", filename);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/actions/MoveFileAction.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport java.io.IOException;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class MoveFileAction implements PostDownloadAction {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(MoveFileAction.class);\n\n    private String currentFilePath;\n    private String newFilePath;\n\n    private FileUtils fileUtils = new FileUtils();\n\n    public MoveFileAction(String currentFilePath, String newFilePath) {\n        \n        this.currentFilePath = FileUtils.ensureTrailingSlash(currentFilePath);\n        this.newFilePath = FileUtils.ensureTrailingSlash(newFilePath);\n    }\n\n    @Override\n    public void execute(PostDownloadExecution execution) {\n\n        try {\n\n            LOGGER.info(\"Executing move action: Moving {} to {}\", execution.fileName, newFilePath);\n            fileUtils.moveFileToDirectory(currentFilePath + execution.fileName, newFilePath);\n            LOGGER.info(\"File successfully moved!\");\n\n        } catch (IOException e) {\n\n            LOGGER.error(\"Unable to move {} to {}. Reason given: {}\", execution.fileName, newFilePath, e.getMessage());\n            LOGGER.debug(\"Full stack trace on error\", e);\n        }\n    }\n    \n    @Override\n    public String toString() {\n        return getClass().getSimpleName();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/actions/PostDownloadAction.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\npublic interface PostDownloadAction {\n\n    void execute(PostDownloadExecution execution);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/actions/PostDownloadExecution.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\npublic class PostDownloadExecution {\n\n    public String fileName;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/actions/PushbulletNotifyAction.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.converter.HttpMessageConversionException;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.client.RestTemplate;\n\npublic class PushbulletNotifyAction implements PostDownloadAction {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(PushbulletNotifyAction.class);\n    private RestTemplate restTemplate = new RestTemplate();\n    private String apiKey;\n\n    public PushbulletNotifyAction(String apiKey) {\n        this.apiKey = apiKey;\n    }\n\n    @Override\n    public void execute(PostDownloadExecution execution) {\n\n        PushbulletRequest body = new PushbulletRequest();\n        body.body = execution.fileName;\n        body.title = \"A new file has been downloaded\";\n        body.type = \"note\";\n\n        HttpHeaders headers = new HttpHeaders();\n        headers.setContentType(MediaType.APPLICATION_JSON);\n        headers.add(\"Authorization\", \"Bearer \" + apiKey);\n\n        try {\n\n            LOGGER.info(\"Sending notification to Pushbullet for {}\", execution.fileName);\n            LOGGER.debug(\"API Key: {}\", apiKey);\n            HttpEntity<PushbulletRequest> httpEntity = new HttpEntity<PushbulletRequest>(body, headers);\n            LOGGER.debug(\"Sending message to Pushbullet: {}\", httpEntity);\n            restTemplate.exchange(\"https://api.pushbullet.com/v2/pushes\", HttpMethod.POST, httpEntity, Object.class);\n\n        } catch (RestClientException | HttpMessageConversionException e ) {\n            \n            LOGGER.debug(\"Full stacktrace\", e);\n            LOGGER.error(\"Unable to complete notification to Pushbullet. Given error: {}\", e.getMessage());\n        }\n    }\n    \n    @Override\n    public String toString() {\n        return getClass().getSimpleName();\n    }\n\n    class PushbulletRequest {\n\n        public String type;\n        public String title;\n        public String body;\n\n        @Override\n        public String toString() {\n            return \"PushbulletRequest [type=\" + type + \", title=\" + title + \", body=\" + body + \"]\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/actions/SNSNotifyAction.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.auth.AWSCredentials;\nimport com.amazonaws.auth.AWSStaticCredentialsProvider;\nimport com.amazonaws.auth.BasicAWSCredentials;\nimport com.amazonaws.services.sns.AmazonSNS;\nimport com.amazonaws.services.sns.AmazonSNSClient;\nimport com.amazonaws.services.sns.AmazonSNSClientBuilder;\nimport com.amazonaws.services.sns.model.PublishRequest;\nimport com.amazonaws.services.sns.model.PublishResult;\n\npublic class SNSNotifyAction implements PostDownloadAction {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(SNSNotifyAction.class);\n\n    private AmazonSNSClientBuilder snsClientBuilder = AmazonSNSClient.builder();\n\n    private String region;\n    private String arn;\n    private String accessKey;\n    private String secretAccessKey;\n\n    public SNSNotifyAction(String region, String arn, String accessKey, String secretAccessKey) {\n\n        this.region = region;\n        this.arn = arn;\n        this.accessKey = accessKey;\n        this.secretAccessKey = secretAccessKey;\n    }\n\n    @Override\n    public void execute(PostDownloadExecution execution) {\n\n        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretAccessKey);\n\n        AmazonSNS sns = snsClientBuilder.withRegion(region)\n                .withCredentials(new AWSStaticCredentialsProvider(credentials)).build();\n\n        LOGGER.debug(\"SNS: Topic Arn               : {}\", arn);\n        LOGGER.debug(\"SNS: Topic Region            : {}\", region);\n        LOGGER.debug(\"SNS: Topic Access Key        : {}\", accessKey);\n        LOGGER.debug(\"SNS: Topic Secret Access Key : {}\", secretAccessKey);\n\n        PublishRequest request = new PublishRequest();\n        request.setTopicArn(arn);\n        request.setMessageStructure(\"json\");\n        request.setMessage(formatJsonMessage(execution.fileName));\n        request.setSubject(\"A new file has been downloaded\");\n\n        LOGGER.info(\"Publishing message to SNS\");\n        PublishResult result = sns.publish(request);\n        LOGGER.info(\"Publish successful!\");\n        LOGGER.debug(\"{}\", result.getMessageId());\n    }\n    \n    private String formatJsonMessage(String message) {\n        return String.format(\"{\\\"default\\\": \\\"%s\\\"}\", message);\n    }\n    \n    @Override\n    public String toString() {\n        return getClass().getSimpleName();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/filter/FileFilter.java",
    "content": "package io.linuxserver.davos.schedule.workflow.filter;\n\nimport java.util.List;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\n\npublic interface FileFilter {\n\n    List<FTPFile> filter(List<FTPFile> allFiles);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/filter/ReferentialFileFilter.java",
    "content": "package io.linuxserver.davos.schedule.workflow.filter;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\n\npublic class ReferentialFileFilter implements FileFilter {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ReferentialFileFilter.class);\n\n    private List<String> filesToCompareWith = new ArrayList<>();\n\n    public ReferentialFileFilter(List<String> files) {\n        filesToCompareWith = files;\n    }\n\n    @Override\n    public List<FTPFile> filter(List<FTPFile> allFiles) {\n\n        if (filesToCompareWith.isEmpty()) {\n            LOGGER.debug(\"No files in last scan. Using all files in this scan for filtering\");\n            return allFiles;\n        }\n\n        LOGGER.debug(\"Files in last scan {}\", filesToCompareWith);\n        LOGGER.debug(\"Files in this scan {}\", allFiles.stream().map(f -> f.getName()).collect(toList()));\n        \n        LOGGER.debug(\"Checking this scan for new files - comparing with files from last scan\");\n        List<FTPFile> collectedFiles = allFiles.stream().filter(f -> !filesToCompareWith.contains(f.getName())).collect(toList());\n        LOGGER.debug(\"New files {}\", collectedFiles.stream().map(f -> f.getName()).collect(toList()));\n        \n        return collectedFiles;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/filter/TemporalFileFilter.java",
    "content": "package io.linuxserver.davos.schedule.workflow.filter;\n\nimport static java.util.stream.Collectors.toList;\n\nimport java.util.List;\nimport java.util.function.Predicate;\n\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\n\npublic class TemporalFileFilter implements FileFilter {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(TemporalFileFilter.class);\n\n    private DateTime lastRun;\n    \n    public TemporalFileFilter(DateTime lastRun) {\n        this.lastRun = lastRun;\n    }\n    \n    @Override\n    public List<FTPFile> filter(List<FTPFile> allFiles) {\n        return allFiles.stream().filter(after(lastRun)).collect(toList());\n    }\n\n    private Predicate<? super FTPFile> after(DateTime lastRun) {\n\n        LOGGER.debug(\"Filtering initial set of files by lastRun. Last run was {}\", lastRun);\n        return f -> f.getLastModified().isAfter(lastRun);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/transfer/FTPTransfer.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\n\npublic class FTPTransfer {\n\n    private State state = State.PENDING;\n    private final FTPFile file;\n    private ProgressListener listener;\n\n    public FTPTransfer(FTPFile file) {\n        this.file = file;\n    }\n\n    public FTPFile getFile() {\n        return file;\n    }\n\n    public ProgressListener getListener() {\n        return listener;\n    }\n\n    public void setListener(ProgressListener listener) {\n        this.listener = listener;\n    }\n    \n    public State getState() {\n        return state;\n    }\n\n    public void setState(State state) {\n        this.state = state;\n    }\n\n    public enum State {\n        PENDING, DOWNLOADING, SKIPPED, FINISHED\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/transfer/FilesAndFoldersTranferStrategy.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class FilesAndFoldersTranferStrategy extends TransferStrategy {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FilesAndFoldersTranferStrategy.class);\n    \n    public FilesAndFoldersTranferStrategy(Connection connection) {\n        super(connection);\n    }\n\n    @Override\n    public void transferFile(FTPTransfer transfer, String destination) {\n\n        FTPFile file = transfer.getFile();\n        String filename = file.getName();\n        String cleanFilePath = FileUtils.ensureTrailingSlash(file.getPath());\n        String cleanDestination = FileUtils.ensureTrailingSlash(destination);\n\n        LOGGER.info(\"Downloading {} to {}\", cleanFilePath + filename, cleanDestination);\n        transfer.setState(FTPTransfer.State.DOWNLOADING);\n        connection.download(file, cleanDestination);\n        transfer.setState(FTPTransfer.State.FINISHED);\n        LOGGER.info(\"Successfully downloaded file.\");\n\n        LOGGER.info(\"Running post download actions on {}\", filename);\n        runPostDownloadAction(file);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/transfer/FilesOnlyTransferStrategy.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class FilesOnlyTransferStrategy extends TransferStrategy {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FilesOnlyTransferStrategy.class);\n    \n    public FilesOnlyTransferStrategy(Connection connection) {\n        super(connection);\n    }\n\n    @Override\n    public void transferFile(FTPTransfer transfer, String destination) {\n\n        FTPFile file = transfer.getFile();\n        String filename = file.getName();\n        String cleanFilePath = FileUtils.ensureTrailingSlash(file.getPath());\n        String cleanDestination = FileUtils.ensureTrailingSlash(destination);\n\n        if (!file.isDirectory()) {\n         \n            LOGGER.info(\"Downloading {} to {}\", cleanFilePath + filename, cleanDestination);\n            transfer.setState(FTPTransfer.State.DOWNLOADING);\n            connection.download(file, cleanDestination);\n            transfer.setState(FTPTransfer.State.FINISHED);\n            LOGGER.info(\"Successfully downloaded file.\");\n            \n            LOGGER.info(\"Running post download actions on {}\", filename);\n            runPostDownloadAction(file);\n        } else {\n            \n            LOGGER.debug(\"Nullifying listener as it will never get used\");\n            transfer.setState(FTPTransfer.State.SKIPPED);\n            transfer.setListener(null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/transfer/TransferStrategy.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.schedule.workflow.actions.PostDownloadAction;\nimport io.linuxserver.davos.schedule.workflow.actions.PostDownloadExecution;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\n\npublic abstract class TransferStrategy {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(TransferStrategy.class);\n\n    protected Connection connection;\n    private List<PostDownloadAction> postDownloadActions = new ArrayList<PostDownloadAction>();\n\n    public TransferStrategy(Connection connection) {\n        this.connection = connection;\n    }\n\n    public void setPostDownloadActions(List<PostDownloadAction> postDownloadActions) {\n        this.postDownloadActions = postDownloadActions;\n    }\n\n    @Override\n    public String toString() {\n        return getClass().getSimpleName();\n    }\n\n    public abstract void transferFile(FTPTransfer fileToTransfer, String destination);\n\n    protected void runPostDownloadAction(FTPFile file) {\n\n        if (null == postDownloadActions) {\n\n            LOGGER.warn(\"Post download actions have been nulled! This should not happen. Will not attempt run of actions\");\n            return;\n        }\n\n        LOGGER.debug(\"Running actions...\");\n        for (PostDownloadAction action : postDownloadActions) {\n\n            PostDownloadExecution execution = new PostDownloadExecution();\n            execution.fileName = file.getName();\n\n            action.execute(execution);\n        }\n        LOGGER.debug(\"Finished running actions...\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/schedule/workflow/transfer/TransferStrategyFactory.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\n\npublic class TransferStrategyFactory {\n\n    public TransferStrategy getStrategy(FileTransferType type, Connection connection) {\n\n        if (FileTransferType.FILE.equals(type))\n            return new FilesOnlyTransferStrategy(connection);\n\n        return new FilesAndFoldersTranferStrategy(connection);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/FTPFile.java",
    "content": "package io.linuxserver.davos.transfer.ftp;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\nimport org.joda.time.DateTime;\n\npublic class FTPFile {\n\n    private String name;\n    private long size;\n    private String path;\n    private DateTime lastModified;\n    private boolean directory;\n\n    public FTPFile(String name, long size, String path, long mTime, boolean directory) {\n        \n        this.name = name;\n        this.size = size;\n        this.path = path;\n        this.lastModified = new DateTime(mTime);\n        this.directory = directory;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public long getSize() {\n        return size;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public DateTime getLastModified() {\n        return lastModified;\n    }\n\n    public boolean isDirectory() {\n        return directory;\n    }\n    \n    @Override\n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/FileTransferType.java",
    "content": "package io.linuxserver.davos.transfer.ftp;\n\npublic enum FileTransferType {\n    FILE, RECURSIVE;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/TransferProtocol.java",
    "content": "package io.linuxserver.davos.transfer.ftp;\n\npublic enum TransferProtocol {\n    FTP, FTPS, SFTP;\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/client/Client.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\n\npublic abstract class Client {\n\n    protected String host;\n    protected int port;\n    \n    protected UserCredentials userCredentials = UserCredentials.ANONYMOUS;\n    \n    public void setCredentials(UserCredentials userCredentials) {\n        this.userCredentials = userCredentials;\n    }\n\n    public void setHost(String host) {\n        this.host = host;\n    }\n\n    public void setPort(int port) {\n        this.port = port;\n    }\n    \n    public abstract Connection connect();\n    \n    public abstract void disconnect();\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/client/ClientFactory.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ClientFactory {\n\n    public Client getClient(TransferProtocol protocol) {\n        \n        if (protocol.equals(TransferProtocol.SFTP))\n            return new SFTPClient();\n        \n        if (protocol.equals(TransferProtocol.FTPS))\n            return new FTPSClient();\n        \n        return new FTPClient();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/client/FTPClient.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport java.io.IOException;\nimport java.net.SocketException;\n\nimport org.apache.commons.net.ftp.FTPReply;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.connection.ConnectionFactory;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientConnectionException;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientDisconnectException;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\n\npublic class FTPClient extends Client {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FTPClient.class);\n    \n    private ConnectionFactory connectionFactory = new ConnectionFactory();\n\n    protected org.apache.commons.net.ftp.FTPClient ftpClient;\n\n    public FTPClient() {\n        \n        LOGGER.debug(\"Initialising FTP Client\");\n        ftpClient = new org.apache.commons.net.ftp.FTPClient();\n    }\n\n    public Connection connect() {\n\n        try {\n\n            connectClientAndCheckStatus();\n            setSpecificModesOnClient();\n            login();\n\n        } catch (IOException e) {\n            throw new ClientConnectionException(String.format(\"Unable to connect to host %s on port %d\", host, port), e);\n        }\n\n        return connectionFactory.createFTPConnection(ftpClient);\n    }\n\n    public void disconnect() {\n\n        try {\n\n            if (null == ftpClient)\n                throw new ClientDisconnectException(\"The underlying client was null.\");\n\n            if (ftpClient.isConnected()) {\n                LOGGER.debug(\"Disconnecting...\");\n                ftpClient.disconnect();\n                LOGGER.debug(\"Disconnected\");\n            }\n\n        } catch (IOException e) {\n            throw new ClientDisconnectException(\"There was an unexpected error while trying to disconnect.\", e);\n        }\n    }\n\n    private void connectClientAndCheckStatus() throws SocketException, IOException, FTPException {\n\n        LOGGER.debug(\"Connecting to {}:{}\", host, port);\n        ftpClient.connect(host, port);\n\n        int replyCode = ftpClient.getReplyCode();\n        if (!FTPReply.isPositiveCompletion(replyCode)) {\n            \n            LOGGER.debug(\"Connection not made.\");\n            LOGGER.debug(\"Response status: {}\", replyCode);\n            LOGGER.debug(\"Disconnecting\");\n            ftpClient.disconnect();\n            LOGGER.debug(\"Disconnected\");\n            \n            throw new ClientConnectionException(String.format(\"The host %s on port %d returned a bad status code.\", host, port));\n        }\n    }\n\n    private void login() throws IOException, FTPException {\n\n        String username = userCredentials.getUsername();\n        String password = userCredentials.getPassword();\n\n        LOGGER.debug(\"Username: {}\", username);\n        boolean hasLoggedIn = ftpClient.login(username, password);\n\n        if (!hasLoggedIn)\n            throw new ClientConnectionException(String.format(\"Unable to login for user %s\", username));\n\n        ftpClient.setFileType(org.apache.commons.net.ftp.FTPClient.BINARY_FILE_TYPE);\n    }\n\n    private void setSpecificModesOnClient() throws IOException {\n\n        ftpClient.enterLocalPassiveMode();\n        ftpClient.setControlKeepAliveTimeout(300);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/client/FTPSClient.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class FTPSClient extends FTPClient {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FTPSClient.class);\n    \n    public FTPSClient() {\n        \n        LOGGER.debug(\"Initialising FTPS Client\");\n        ftpClient = new org.apache.commons.net.ftp.FTPSClient();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/client/SFTPClient.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.jcraft.jsch.Channel;\nimport com.jcraft.jsch.JSch;\nimport com.jcraft.jsch.JSchException;\nimport com.jcraft.jsch.Session;\n\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials.Identity;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.connection.ConnectionFactory;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientConnectionException;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientDisconnectException;\n\npublic class SFTPClient extends Client {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(SFTPClient.class);\n\n    private JSch jsch;\n    private ConnectionFactory connectionFactory;\n\n    private Session session;\n    private Channel channel;\n\n    public SFTPClient() {\n\n        this.jsch = new JSch();\n        this.connectionFactory = new ConnectionFactory();\n    }\n\n    @Override\n    public Connection connect() {\n\n        session = null;\n        channel = null;\n\n        try {\n\n            configureSessionAndConnect();\n            openChannelFromSession();\n\n        } catch (JSchException e) {\n            throw new ClientConnectionException(String.format(\"Unable to connect to host %s on port %d\", host, port), e);\n        }\n\n        return connectionFactory.createSFTPConnection(channel);\n    }\n\n    @Override\n    public void disconnect() {\n\n        if (null == channel || null == session)\n            throw new ClientDisconnectException(\"The underlying connection was never initially made.\");\n\n        LOGGER.debug(\"Disconnecting from channel\");\n        channel.disconnect();\n        LOGGER.debug(\"Disconnecting from session\");\n        session.disconnect();\n    }\n\n    private void configureSessionAndConnect() throws JSchException {\n\n        LOGGER.debug(\"Configuring connection credentials and options on session\");\n\n        Identity identity = userCredentials.getIdentity();\n        if (null != identity) {\n            \n            String identityFile = identity.getIdentityFile();\n            LOGGER.debug(\"SSH identity found ({}). Setting against session\", identityFile);\n            jsch.addIdentity(identityFile);\n        }\n        \n        String username = userCredentials.getUsername();\n        String password = userCredentials.getPassword();\n        \n        LOGGER.debug(\"Username: {}\", username);\n        session = jsch.getSession(username, host, port);\n        session.setConfig(\"StrictHostKeyChecking\", \"no\");\n\n        // I'm going to have to think of a nicer way of doing this...\n        if (null == userCredentials.getIdentity())\n            session.setPassword(password);\n\n        session.connect();\n\n        LOGGER.debug(\"Connected to session\");\n    }\n\n    private void openChannelFromSession() throws JSchException {\n\n        LOGGER.debug(\"Opening SFTP channel from session\");\n\n        channel = session.openChannel(\"sftp\");\n        channel.connect();\n\n        LOGGER.debug(\"Connected to channel\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/client/UserCredentials.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\npublic class UserCredentials {\n\n    public static final UserCredentials ANONYMOUS = new UserCredentials(\"anonymous\", \"stark@linuxserver.io\");\n\n    private String username;\n    private String password;\n    private Identity identity;\n\n    public UserCredentials(final String username, final String password) {\n\n        this.username = username;\n        this.password = password;\n    }\n\n    public UserCredentials(final String username, final Identity identity) {\n        \n        this.username = username;\n        this.identity = identity;\n    }\n    \n    public Identity getIdentity() {\n        return identity;\n    }\n    \n    public String getUsername() {\n        return username;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n    \n    public static class Identity {\n        \n        private final String identityFile;\n        \n        public Identity(String identityFile) {\n            this.identityFile = identityFile;\n        }\n        \n        public String getIdentityFile() {\n            return identityFile;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/Connection.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection;\n\nimport java.util.List;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\n\npublic interface Connection {\n    \n    String currentDirectory() throws FTPException;\n    \n    void download(FTPFile remoteFilePath, String localFilePath) throws FTPException;\n    \n    void deleteRemoteFile(FTPFile file) throws FTPException;\n    \n    List<FTPFile> listFiles() throws FTPException;\n    \n    List<FTPFile> listFiles(String remoteDirectory) throws FTPException;\n    \n    void setProgressListener(ProgressListener progressListener);\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/ConnectionFactory.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.jcraft.jsch.Channel;\nimport com.jcraft.jsch.ChannelSftp;\n\npublic class ConnectionFactory {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionFactory.class);\n    \n    public SFTPConnection createSFTPConnection(Channel channel) {\n        LOGGER.debug(\"Creating SFTP connection for channel {}\", channel);\n        return new SFTPConnection((ChannelSftp) channel);\n    }\n    \n    public FTPConnection createFTPConnection(org.apache.commons.net.ftp.FTPClient client) {\n        LOGGER.debug(\"Creating FTP connection for client {}\", client);\n        return new FTPConnection(client);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/FTPConnection.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\nimport org.apache.commons.io.output.CountingOutputStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\nimport io.linuxserver.davos.transfer.ftp.exception.DeleteFileException;\nimport io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\nimport io.linuxserver.davos.transfer.ftp.exception.FileListingException;\nimport io.linuxserver.davos.util.FileStreamFactory;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class FTPConnection implements Connection {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FTPConnection.class);\n\n    private org.apache.commons.net.ftp.FTPClient client;\n    private FileStreamFactory fileStreamFactory = new FileStreamFactory();\n    private FileUtils fileUtils = new FileUtils();\n    private ProgressListener progressListener;\n\n    public FTPConnection(org.apache.commons.net.ftp.FTPClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public String currentDirectory() {\n\n        try {\n            String workingDirectory = client.printWorkingDirectory();\n            LOGGER.debug(\"{}\", workingDirectory);\n            return workingDirectory;\n        } catch (IOException e) {\n            throw new FileListingException(\"Unable to print the working directory\", e);\n        }\n    }\n\n    @Override\n    public void download(FTPFile file, String localFilePath) {\n\n        String cleanRemotePath = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();\n        String cleanLocalPath = FileUtils.ensureTrailingSlash(localFilePath);\n\n        LOGGER.debug(\"Remote path: {}\", cleanRemotePath);\n        LOGGER.debug(\"Local path: {}\", cleanLocalPath);\n        \n        try {\n\n            if (file.isDirectory())\n                downloadDirectoryAndContents(file, cleanLocalPath, cleanRemotePath);\n\n            else\n                doDownload(file, cleanRemotePath, cleanLocalPath);\n\n        } catch (FileNotFoundException e) {\n            throw new DownloadFailedException(\n                    String.format(\"Unable to write to local directory %s\", cleanLocalPath + file.getName()), e);\n        } catch (IOException e) {\n            throw new DownloadFailedException(String.format(\"Unable to download file %s\", cleanRemotePath), e);\n        }\n    }\n\n    @Override\n    public List<FTPFile> listFiles() {\n        return listFiles(currentDirectory());\n    }\n\n    @Override\n    public List<FTPFile> listFiles(String remoteDirectory) {\n\n        List<FTPFile> files = new ArrayList<FTPFile>();\n\n        try {\n\n            String cleanRemoteDirectory = FileUtils.ensureTrailingSlash(remoteDirectory);\n            LOGGER.debug(\"Listing all files in {}\", cleanRemoteDirectory);\n            org.apache.commons.net.ftp.FTPFile[] ftpFiles = client.listFiles(cleanRemoteDirectory);\n\n            for (org.apache.commons.net.ftp.FTPFile file : ftpFiles)\n                files.add(toFtpFile(file, cleanRemoteDirectory));\n            \n            LOGGER.debug(\"{}\", files);\n\n        } catch (IOException e) {\n            throw new FileListingException(String.format(\"Unable to list files in directory %s\", remoteDirectory), e);\n        }\n\n        return files.stream().filter(removeCurrentAndParentDirs()).collect(Collectors.toList());\n    }\n\n    @Override\n    public void setProgressListener(ProgressListener progressListener) {\n        this.progressListener = progressListener;\n    }\n\n    private CountingOutputStream listenOn(OutputStream outputStream) {\n\n        LOGGER.debug(\"Creating wrapping output stream for progress listener\");\n\n        CountingOutputStream countingStream = new CountingOutputStream(outputStream) {\n\n            @Override\n            protected void beforeWrite(int n) {\n\n                super.beforeWrite(n);\n                progressListener.setBytesWritten(getByteCount());\n            }\n        };\n\n        return countingStream;\n    }\n\n    private void doDownload(FTPFile file, String cleanRemotePath, String cleanLocalPath)\n            throws FileNotFoundException, IOException {\n\n        LOGGER.info(\"Downloading {} to {}\", cleanRemotePath, cleanLocalPath);\n        LOGGER.debug(\"Creating output stream for file {}\", cleanLocalPath + file.getName());\n\n        OutputStream outputStream = fileStreamFactory.createOutputStream(cleanLocalPath + file.getName());\n\n        boolean hasDownloaded;\n\n        if (null != progressListener) {\n\n            LOGGER.debug(\"ProgressListener has been set. Initialising...\");\n            LOGGER.debug(\"Total file size is {}\", file.getSize());\n            progressListener.reset();\n            progressListener.setTotalBytes(file.getSize());\n            progressListener.start();\n\n            hasDownloaded = client.retrieveFile(cleanRemotePath, listenOn(outputStream));\n        } else\n            hasDownloaded = client.retrieveFile(cleanRemotePath, outputStream);\n\n        outputStream.close();\n\n        if (!hasDownloaded)\n            throw new DownloadFailedException(\"Server returned failure while downloading.\");\n    }\n\n    private void downloadDirectoryAndContents(FTPFile file, String localDownloadFolder, String path) throws IOException {\n\n        LOGGER.info(\"Item {} is a directory. Will now check sub-items\", file.getName());\n        List<FTPFile> subItems = listFiles(path).stream().filter(removeCurrentAndParentDirs()).collect(Collectors.toList());\n        LOGGER.debug(\"Counted {} sub items.\", subItems.size());\n            \n        String fullLocalDownloadPath = FileUtils.ensureTrailingSlash(localDownloadFolder + file.getName());\n\n        LOGGER.debug(\"Creating new local directory {}\", fullLocalDownloadPath);\n        fileUtils.createLocalDirectory(fullLocalDownloadPath);\n\n        for (FTPFile subItem : subItems) {\n\n            String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();\n\n            LOGGER.debug(\"Download. Sub item path: {}\", subItemPath);\n\n            if (subItem.isDirectory()) {\n\n                String subLocalFilePath = FileUtils.ensureTrailingSlash(fullLocalDownloadPath);\n                downloadDirectoryAndContents(subItem, subLocalFilePath, FileUtils.ensureTrailingSlash(subItemPath));\n            }\n\n            else\n                doDownload(subItem, subItemPath, fullLocalDownloadPath);\n        }\n    }\n\n    private Predicate<? super FTPFile> removeCurrentAndParentDirs() {\n        return file -> !file.getName().equals(\".\") && !file.getName().equals(\"..\");\n    }\n\n    private FTPFile toFtpFile(org.apache.commons.net.ftp.FTPFile ftpFile, String filePath) throws IOException {\n\n        String name = ftpFile.getName();\n        long fileSize = ftpFile.getSize();\n        long mTime = ftpFile.getTimestamp().getTime().getTime();\n        boolean isDirectory = ftpFile.isDirectory();\n\n        return new FTPFile(name, fileSize, filePath, mTime, isDirectory);\n    }\n\n    @Override\n    public void deleteRemoteFile(FTPFile file) throws FTPException {\n\n        String cleanRemotePath = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();\n        LOGGER.debug(\"Deleting remote file {}\", cleanRemotePath);\n        \n        try {\n\n            if (file.isDirectory()) {\n                deleteDirectoryAndContents(file, cleanRemotePath);\n            } else\n                doDelete(cleanRemotePath);\n\n        } catch (IOException e) {\n\n            LOGGER.debug(\"client#deleteFile() threw exception. Assuming file not deleted\");\n            throw new DeleteFileException(\"Unable to delete file on remote server\", e);\n        }\n    }\n\n    private void deleteDirectoryAndContents(FTPFile file, String remoteDirectoryPath) throws IOException {\n\n        LOGGER.info(\"Item {} is a directory. Will now check sub-items\", file.getName());\n        List<FTPFile> subItems = listFiles(remoteDirectoryPath).stream().filter(removeCurrentAndParentDirs())\n                .collect(Collectors.toList());\n        \n        for (FTPFile subItem : subItems) {\n            \n            String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();\n            \n            LOGGER.debug(\"Delete. Sub item path: {}\", subItemPath);\n            \n            if (subItem.isDirectory())\n                deleteDirectoryAndContents(subItem, subItemPath);\n            else\n                doDelete(subItemPath);\n        }\n        \n        LOGGER.debug(\"Removing empty directory {}\", remoteDirectoryPath);\n        client.removeDirectory(remoteDirectoryPath);\n    }\n\n    private void doDelete(String subItemPath) throws IOException {\n        \n        LOGGER.debug(\"Deleting file: {}\", subItemPath);\n        boolean deleted = client.deleteFile(subItemPath);\n        LOGGER.debug(\"File deleted\");\n\n        if (!deleted)\n            throw new DeleteFileException(\"Unable to delete file on remote server. Unknown reason\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/SFTPConnection.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Vector;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.jcraft.jsch.ChannelSftp;\nimport com.jcraft.jsch.ChannelSftp.LsEntry;\nimport com.jcraft.jsch.SftpException;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.SFTPProgressListener;\nimport io.linuxserver.davos.transfer.ftp.exception.DeleteFileException;\nimport io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\nimport io.linuxserver.davos.transfer.ftp.exception.FileListingException;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class SFTPConnection implements Connection {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(SFTPConnection.class);\n\n    private ChannelSftp channel;\n    private FileUtils fileUtils = new FileUtils();\n    private SFTPProgressListener progressListener;\n\n    public SFTPConnection(ChannelSftp channel) {\n        this.channel = channel;\n    }\n\n    @Override\n    public String currentDirectory() {\n\n        try {\n            String pwd = channel.pwd();\n            LOGGER.debug(\"{}\", pwd);\n            return pwd;\n        } catch (SftpException e) {\n            throw new FileListingException(\"Unable to print the working directory\", e);\n        }\n    }\n\n    @Override\n    public void download(FTPFile file, String localFilePath) {\n\n        String path = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();\n        String cleanLocalPath = FileUtils.ensureTrailingSlash(localFilePath);\n        \n        LOGGER.debug(\"Download. Remote path: {}\", path);\n        LOGGER.debug(\"Download. Local path: {}\", cleanLocalPath);\n\n        try {\n\n            if (file.isDirectory())\n                downloadDirectoryAndContents(file, cleanLocalPath, path);\n            else\n                doGet(path, cleanLocalPath);\n\n        } catch (SftpException e) {\n            throw new DownloadFailedException(\"Unable to download file \" + path, e);\n        }\n    }\n\n    @Override\n    public List<FTPFile> listFiles() {\n        return listFiles(currentDirectory());\n    }\n\n    @Override\n    public List<FTPFile> listFiles(String remoteDirectory) {\n\n        try {\n\n            String cleanRemoteDirectory = FileUtils.ensureTrailingSlash(remoteDirectory);\n\n            List<FTPFile> files = new ArrayList<FTPFile>();\n\n            LOGGER.debug(\"Listing files in {}\", cleanRemoteDirectory);\n\n            @SuppressWarnings(\"unchecked\")\n            Vector<LsEntry> lsEntries = channel.ls(cleanRemoteDirectory);\n\n            for (LsEntry entry : lsEntries)\n                files.add(toFtpFile(entry, cleanRemoteDirectory));\n\n            LOGGER.debug(\"{}\", files);\n            LOGGER.debug(\"Listed {} items from remote directory {}\", files.size(), cleanRemoteDirectory);\n\n            return files;\n\n        } catch (SftpException e) {\n            throw new FileListingException(String.format(\"Unable to list files in directory %s\", remoteDirectory), e);\n        }\n    }\n\n    @Override\n    public void setProgressListener(ProgressListener progressListener) {\n        this.progressListener = (SFTPProgressListener) progressListener;\n    }\n\n    private void doGet(String fullRemotePath, String fullLocalDownloadPath) throws SftpException {\n\n        LOGGER.debug(\"Performing channel.get from {} to {}\", fullRemotePath, fullLocalDownloadPath);\n\n        if (null != progressListener) {\n\n            LOGGER.debug(\"Progress listener has been enabled\");\n            channel.get(fullRemotePath, fullLocalDownloadPath, progressListener);\n\n        } else\n            channel.get(fullRemotePath, fullLocalDownloadPath);\n    }\n\n    private void downloadDirectoryAndContents(FTPFile file, String localDownloadFolder, String path) throws SftpException {\n\n        LOGGER.info(\"Item {} is a directory. Will now check sub-items\", file.getName());\n        List<FTPFile> subItems = listFiles(path).stream().filter(removeCurrentAndParentDirs()).collect(Collectors.toList());\n\n        String fullLocalDownloadPath = FileUtils.ensureTrailingSlash(localDownloadFolder + file.getName());\n\n        LOGGER.debug(\"Creating new local directory {}\", fullLocalDownloadPath);\n        fileUtils.createLocalDirectory(fullLocalDownloadPath);\n\n        for (FTPFile subItem : subItems) {\n\n            LOGGER.debug(\"{}\", subItem);\n            \n            String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();\n            \n            if (subItem.isDirectory()) {\n\n                String subLocalFilePath = FileUtils.ensureTrailingSlash(fullLocalDownloadPath);\n                downloadDirectoryAndContents(subItem, subLocalFilePath, FileUtils.ensureTrailingSlash(subItemPath));\n            }\n\n            else {\n\n                LOGGER.info(\"Downloading {} to {}\", subItemPath, fullLocalDownloadPath);\n                doGet(subItemPath, fullLocalDownloadPath);\n            }\n        }\n    }\n\n    private Predicate<? super FTPFile> removeCurrentAndParentDirs() {\n        return file -> !file.getName().equals(\".\") && !file.getName().equals(\"..\");\n    }\n\n    private FTPFile toFtpFile(LsEntry lsEntry, String filePath) throws SftpException {\n\n        String name = lsEntry.getFilename();\n        long fileSize = lsEntry.getAttrs().getSize();\n        int mTime = lsEntry.getAttrs().getMTime();\n        boolean directory = lsEntry.getAttrs().isDir();\n\n        return new FTPFile(name, fileSize, filePath, (long) mTime * 1000, directory);\n    }\n\n    @Override\n    public void deleteRemoteFile(FTPFile file) throws FTPException {\n\n        String cleanRemotePath = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();\n        LOGGER.debug(\"Deleting remote file {}\", cleanRemotePath);\n\n        try {\n\n            if (file.isDirectory()) {\n                deleteDirectoryAndContents(file, cleanRemotePath);\n            } else\n                doDelete(cleanRemotePath);\n\n        } catch (SftpException e) {\n\n            LOGGER.debug(\"channel threw exception. Assuming file not deleted\");\n            throw new DeleteFileException(\"Unable to delete file on remote server\", e);\n        }\n    }\n    \n    private void deleteDirectoryAndContents(FTPFile file, String remoteDirectoryPath) throws SftpException {\n\n        LOGGER.info(\"Item {} is a directory. Will now check sub-items\", file.getName());\n        List<FTPFile> subItems = listFiles(remoteDirectoryPath).stream().filter(removeCurrentAndParentDirs())\n                .collect(Collectors.toList());\n        \n        for (FTPFile subItem : subItems) {\n            \n            LOGGER.debug(\"{}\", subItem);\n            \n            String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();\n            \n            if (subItem.isDirectory())\n                deleteDirectoryAndContents(subItem, subItemPath);\n            else\n                doDelete(subItemPath);\n        }\n        \n        LOGGER.debug(\"Removing empty directory {}\", remoteDirectoryPath);\n        channel.rmdir(remoteDirectoryPath);\n    }\n\n    private void doDelete(String subItemPath) throws SftpException {\n        \n        LOGGER.debug(\"Deleting file: {}\", subItemPath);\n        channel.rm(subItemPath);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/progress/ListenerFactory.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection.progress;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ListenerFactory {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ListenerFactory.class);\n    \n    public ProgressListener createListener(TransferProtocol protocol) {\n        \n        if (TransferProtocol.SFTP.equals(protocol)) {\n            LOGGER.debug(\"Chosen listener is SFTPProgressListener, for {}\", protocol);\n            return new SFTPProgressListener();\n        }\n        \n        LOGGER.debug(\"Chosen listener is ProgressListener, for {}\", protocol);\n        return new ProgressListener();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/progress/ProgressListener.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection.progress;\n\npublic class ProgressListener {\n\n    private long lastWriteTime;\n    private long totalBytesWritten;\n    private long bytesInWrite;\n    private long totalBytes;\n    private double currentTransferSpeed;\n\n    public double getProgress() {\n        \n        if (totalBytes > 0)\n            return ((double) totalBytesWritten / (double) totalBytes) * 100;\n\n        return 100;\n    }\n\n    public double getTransferSpeed() {\n        return currentTransferSpeed;\n    }\n\n    public void reset() {\n        totalBytes = 0;\n    }\n\n    public void updateBytesWritten(long bytes) {\n        setBytesWritten(totalBytesWritten + bytes);\n    }\n\n    public void setBytesWritten(long bytesWritten) {\n\n        long currentTimeMillis = System.currentTimeMillis();\n        long timeSinceLastWrite = currentTimeMillis - this.lastWriteTime;\n\n        this.lastWriteTime = currentTimeMillis;\n        this.bytesInWrite = bytesWritten - this.totalBytesWritten;\n        this.totalBytesWritten = bytesWritten;\n\n        this.currentTransferSpeed = (double) this.bytesInWrite / (double) timeSinceLastWrite / 1000;\n    }\n\n    public void setTotalBytes(long totalBytes) {\n        this.totalBytes = totalBytes;\n    }\n\n    public void start() {\n\n        lastWriteTime = System.currentTimeMillis();\n        totalBytesWritten = 0;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/connection/progress/SFTPProgressListener.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection.progress;\n\nimport com.jcraft.jsch.SftpProgressMonitor;\n\npublic class SFTPProgressListener extends ProgressListener implements SftpProgressMonitor {\n  \n    @Override\n    public void init(int op, String src, String dest, long max) {\n        \n        setTotalBytes(max);\n        start();\n    }\n\n    @Override\n    public boolean count(long count) {\n        updateBytesWritten(count);\n        return true;\n    }\n\n    @Override\n    public void end() {\n        // reset();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/exception/ClientConnectionException.java",
    "content": "package io.linuxserver.davos.transfer.ftp.exception;\n\npublic class ClientConnectionException extends FTPException {\n\n    private static final long serialVersionUID = 7733358928451506618L;\n\n    public ClientConnectionException() {\n        super();\n    }\n    \n    public ClientConnectionException(String message) {\n        super(message);\n    }\n    \n    public ClientConnectionException(String message, Exception cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/exception/ClientDisconnectException.java",
    "content": "package io.linuxserver.davos.transfer.ftp.exception;\n\npublic class ClientDisconnectException extends FTPException {\n\n    private static final long serialVersionUID = 7733358928451506618L;\n\n    public ClientDisconnectException() {\n        super();\n    }\n    \n    public ClientDisconnectException(String message) {\n        super(message);\n    }\n    \n    public ClientDisconnectException(String message, Exception cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/exception/DeleteFileException.java",
    "content": "package io.linuxserver.davos.transfer.ftp.exception;\n\npublic class DeleteFileException extends FTPException {\n\n    private static final long serialVersionUID = 6191478212036531333L;\n\n    public DeleteFileException() {\n        super();\n    }\n    \n    public DeleteFileException(String message) {\n        super(message);\n    }\n    \n    public DeleteFileException(String message, Exception cause) {\n        super(message, cause);\n    } \n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/exception/DownloadFailedException.java",
    "content": "package io.linuxserver.davos.transfer.ftp.exception;\n\npublic class DownloadFailedException extends FTPException {\n\n    private static final long serialVersionUID = 7733358928451506618L;\n\n    public DownloadFailedException() {\n        super();\n    }\n    \n    public DownloadFailedException(String message) {\n        super(message);\n    }\n    \n    public DownloadFailedException(String message, Exception cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/exception/FTPException.java",
    "content": "package io.linuxserver.davos.transfer.ftp.exception;\n\npublic abstract class FTPException extends RuntimeException {\n\n    private static final long serialVersionUID = 7733358928451506618L;\n\n    public FTPException() {\n        super();\n    }\n    \n    public FTPException(String message) {\n        super(message);\n    }\n    \n    public FTPException(String message, Exception cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/transfer/ftp/exception/FileListingException.java",
    "content": "package io.linuxserver.davos.transfer.ftp.exception;\n\npublic class FileListingException extends FTPException {\n\n    private static final long serialVersionUID = 7733358928451506618L;\n\n    public FileListingException() {\n        super();\n    }\n    \n    public FileListingException(String message) {\n        super(message);\n    }\n    \n    public FileListingException(String message, Exception cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/util/FileStreamFactory.java",
    "content": "package io.linuxserver.davos.util;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\n\npublic class FileStreamFactory {\n\n    public FileInputStream createInputStream(String filePath) throws FileNotFoundException {\n        return new FileInputStream(new File(filePath));\n    }\n\n    public FileOutputStream createOutputStream(String filePath) throws FileNotFoundException {\n        return new FileOutputStream(new File(filePath));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/util/FileUtils.java",
    "content": "package io.linuxserver.davos.util;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class FileUtils {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(FileUtils.class);\n\n    public File getFile(String filePath) {\n        return new File(filePath);\n    }\n\n    public void moveFileToDirectory(String oldPath, String newPath) throws IOException {\n        org.apache.commons.io.FileUtils.moveToDirectory(getFile(oldPath), getFile(newPath), true);\n    }\n\n    public void createLocalDirectory(String directoryPath) {\n        boolean directoryCreated = new File(directoryPath).mkdirs();\n\n        if (!directoryCreated)\n            LOGGER.debug(\"Directory was not created!\");\n    }\n\n    public static String ensureTrailingSlash(String path) {\n\n        if (!path.endsWith(\"/\"))\n            return path + \"/\";\n\n        return path;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/util/PatternBuilder.java",
    "content": "package io.linuxserver.davos.util;\n\npublic class PatternBuilder {\n\n    public static String buildFromFilterString(String filter) {\n        return filter.replace(\".\", \"\\\\.\").replaceAll(\"\\\\?\", \".{1}\").replaceAll(\"\\\\*\", \".*\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/API.java",
    "content": "package io.linuxserver.davos.web;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\nimport io.linuxserver.davos.web.selectors.MethodSelector;\n\npublic class API {\n\n    private Long id;\n    private String url;\n    private MethodSelector method = MethodSelector.POST;\n    private String contentType;\n    private String body;\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public MethodSelector getMethod() {\n        return method;\n    }\n\n    public void setMethod(MethodSelector method) {\n        this.method = method;\n    }\n\n    public String getContentType() {\n        return contentType;\n    }\n\n    public void setContentType(String contentType) {\n        this.contentType = contentType;\n    }\n\n    public String getBody() {\n        return body;\n    }\n\n    public void setBody(String body) {\n        this.body = body;\n    }\n    \n    @Override\n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Filter.java",
    "content": "package io.linuxserver.davos.web;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\npublic class Filter {\n\n    private Long id;\n    private String value;\n\n    public String getValue() {\n        return value;\n    }\n\n    public void setValue(String value) {\n        this.value = value;\n    }\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n    \n    @Override\n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Host.java",
    "content": "package io.linuxserver.davos.web;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\nimport io.linuxserver.davos.web.selectors.ProtocolSelector;\n\npublic class Host {\n\n    private Long id;\n    private String name;\n    private String address;\n    private int port;\n    private ProtocolSelector protocol = ProtocolSelector.SFTP;\n    private String username;\n    private String password;\n    private String identityFile;\n    private boolean identityFileEnabled;\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getAddress() {\n        return address;\n    }\n\n    public void setAddress(String address) {\n        this.address = address;\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    public void setPort(int port) {\n        this.port = port;\n    }\n\n    public ProtocolSelector getProtocol() {\n        return protocol;\n    }\n\n    public void setProtocol(ProtocolSelector protocol) {\n        this.protocol = protocol;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public void setUsername(String username) {\n        this.username = username;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    @Override\n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n\n    public String getIdentityFile() {\n        return identityFile;\n    }\n\n    public void setIdentityFile(String identityFile) {\n        this.identityFile = identityFile;\n    }\n\n    public boolean isIdentityFileEnabled() {\n        return identityFileEnabled;\n    }\n\n    public void setIdentityFileEnabled(boolean identityFileEnabled) {\n        this.identityFileEnabled = identityFileEnabled;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Notifications.java",
    "content": "package io.linuxserver.davos.web;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Notifications {\n\n    private List<Pushbullet> pushbullet = new ArrayList<Pushbullet>();\n    private List<SNS> sns = new ArrayList<SNS>();\n\n    public List<Pushbullet> getPushbullet() {\n        return pushbullet;\n    }\n\n    public List<SNS> getSns() {\n        return sns;\n    }\n\n    public void setPushbullet(List<Pushbullet> pushbullet) {\n        this.pushbullet = pushbullet;\n    }\n\n    public void setSns(List<SNS> sns) {\n        this.sns = sns;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Pushbullet.java",
    "content": "package io.linuxserver.davos.web;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\npublic class Pushbullet {\n\n    private Long id;\n    private String apiKey;\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getApiKey() {\n        return apiKey;\n    }\n\n    public void setApiKey(String apiKey) {\n        this.apiKey = apiKey;\n    }\n    \n    @Override\n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/SNS.java",
    "content": "package io.linuxserver.davos.web;\n\npublic class SNS {\n\n    private Long id;\n    private String topicArn;\n    private String region;\n    private String accessKey;\n    private String secretAccessKey;\n\n    public String getTopicArn() {\n        return topicArn;\n    }\n\n    public void setTopicArn(String topicArn) {\n        this.topicArn = topicArn;\n    }\n\n    public String getRegion() {\n        return region;\n    }\n\n    public void setRegion(String region) {\n        this.region = region;\n    }\n\n    public String getAccessKey() {\n        return accessKey;\n    }\n\n    public void setAccessKey(String accessKey) {\n        this.accessKey = accessKey;\n    }\n\n    public String getSecretAccessKey() {\n        return secretAccessKey;\n    }\n\n    public void setSecretAccessKey(String secretAccessKey) {\n        this.secretAccessKey = secretAccessKey;\n    }\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Schedule.java",
    "content": "package io.linuxserver.davos.web;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\nimport org.apache.commons.lang3.builder.ToStringStyle;\n\nimport io.linuxserver.davos.web.selectors.TransferSelector;\n\npublic class Schedule {\n\n    private Long id;\n    private String name;\n    private int interval = 60;\n    private Long host;\n    private String hostDirectory;\n    private String localDirectory;\n    private TransferSelector transferType = TransferSelector.FILE;\n    private boolean automatic;\n    private String moveFileTo;\n    private boolean running;\n    private boolean filtersMandatory;\n    private boolean invertFilters;\n    private boolean deleteHostFile;\n    private String lastRunTime;\n\n    private Notifications notifications = new Notifications();\n    private List<String> lastScannedFiles = new ArrayList<>();\n    private List<Filter> filters = new ArrayList<>();\n    private List<Transfer> transfers = new ArrayList<>();\n    private List<API> apis = new ArrayList<>();\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public int getInterval() {\n        return interval;\n    }\n\n    public void setInterval(int interval) {\n        this.interval = interval;\n    }\n\n    public Long getHost() {\n        return host;\n    }\n\n    public void setHost(Long host) {\n        this.host = host;\n    }\n\n    public String getHostDirectory() {\n        return hostDirectory;\n    }\n\n    public void setHostDirectory(String hostDirectory) {\n        this.hostDirectory = hostDirectory;\n    }\n\n    public String getLocalDirectory() {\n        return localDirectory;\n    }\n\n    public void setLocalDirectory(String localDirectory) {\n        this.localDirectory = localDirectory;\n    }\n\n    public TransferSelector getTransferType() {\n        return transferType;\n    }\n\n    public void setTransferType(TransferSelector transferType) {\n        this.transferType = transferType;\n    }\n\n    public boolean isAutomatic() {\n        return automatic;\n    }\n\n    public void setAutomatic(boolean automatic) {\n        this.automatic = automatic;\n    }\n\n    public List<Filter> getFilters() {\n        return filters;\n    }\n\n    public String getMoveFileTo() {\n        return moveFileTo;\n    }\n\n    public void setMoveFileTo(String moveFileTo) {\n        this.moveFileTo = moveFileTo;\n    }\n\n    public List<API> getApis() {\n        return apis;\n    }\n\n    public boolean isRunning() {\n        return running;\n    }\n\n    public void setRunning(boolean running) {\n        this.running = running;\n    }\n\n    @Override\n    public String toString() {\n        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);\n    }\n\n    public List<String> getLastScannedFiles() {\n        return lastScannedFiles;\n    }\n\n    public List<Transfer> getTransfers() {\n        return transfers;\n    }\n\n    public boolean isFiltersMandatory() {\n        return filtersMandatory;\n    }\n\n    public void setFiltersMandatory(boolean filtersMandatory) {\n        this.filtersMandatory = filtersMandatory;\n    }\n\n    public boolean isInvertFilters() {\n        return invertFilters;\n    }\n\n    public void setInvertFilters(boolean invertFilters) {\n        this.invertFilters = invertFilters;\n    }\n\n    public boolean isDeleteHostFile() {\n        return deleteHostFile;\n    }\n\n    public void setDeleteHostFile(boolean deleteHostFile) {\n        this.deleteHostFile = deleteHostFile;\n    }\n\n    public Notifications getNotifications() {\n        return notifications;\n    }\n\n    public void setNotifications(Notifications notifications) {\n        this.notifications = notifications;\n    }\n\n    public void setLastRunTime(String lastRunTime) {\n        this.lastRunTime = lastRunTime;        \n    }\n    \n    public String getLastRunTime() {\n        return lastRunTime;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/ScheduleCommand.java",
    "content": "package io.linuxserver.davos.web;\n\npublic class ScheduleCommand {\n\n    public Command command;\n    \n    public enum Command {\n        START, STOP\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Settings.java",
    "content": "package io.linuxserver.davos.web;\n\nimport io.linuxserver.davos.web.selectors.LogLevelSelector;\n\npublic class Settings {\n\n    private LogLevelSelector logLevel;\n\n    public LogLevelSelector getLogLevel() {\n        return logLevel;\n    }\n\n    public void setLogLevel(LogLevelSelector logLevel) {\n        this.logLevel = logLevel;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/Transfer.java",
    "content": "package io.linuxserver.davos.web;\n\npublic class Transfer {\n\n    private String fileName;\n    private long fileSize;\n    private boolean directory;\n    private Progress progress;\n    private String status;\n\n    public String getFileName() {\n        return fileName;\n    }\n\n    public void setFileName(String fileName) {\n        this.fileName = fileName;\n    }\n\n    public long getFileSize() {\n        return fileSize;\n    }\n\n    public void setFileSize(long fileSize) {\n        this.fileSize = fileSize;\n    }\n\n    public boolean isDirectory() {\n        return directory;\n    }\n\n    public void setDirectory(boolean directory) {\n        this.directory = directory;\n    }\n\n    public Progress getProgress() {\n        return progress;\n    }\n\n    public void setProgress(Progress progress) {\n        this.progress = progress;\n    }\n\n    public String getStatus() {\n        return status;\n    }\n\n    public void setStatus(String status) {\n        this.status = status;\n    }\n\n    public static class Progress {\n\n        private double percentageComplete;\n        private double transferSpeed;\n\n        public double getPercentageComplete() {\n            return percentageComplete;\n        }\n\n        public void setPercentageComplete(double percentageComplete) {\n            this.percentageComplete = percentageComplete;\n        }\n\n        public double getTransferSpeed() {\n            return transferSpeed;\n        }\n\n        public void setTransferSpeed(double transferSpeed) {\n            this.transferSpeed = transferSpeed;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/VersionChecker.java",
    "content": "package io.linuxserver.davos.web;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport io.linuxserver.davos.Version;\n\npublic class VersionChecker {\n\n    private static Logger LOGGER = LoggerFactory.getLogger(VersionChecker.class);\n    \n    private boolean newVersionAvailable;\n    private String newVersion;\n\n    public VersionChecker(Version currentVersion, Version remoteVersion) {\n\n        LOGGER.debug(\"Current version: {}, Remote version: {}\", currentVersion, remoteVersion);\n        newVersionAvailable = remoteVersion.isNewerThan(currentVersion);\n        LOGGER.debug(\"Remote version is {}newer\", newVersionAvailable ? \"\" : \"not \");\n        newVersion = remoteVersion.toString();\n    }\n\n    public boolean isNewVersionAvailable() {\n        return newVersionAvailable;\n    }\n\n    public String getNewVersion() {\n        return newVersion;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/controller/APIController.java",
    "content": "package io.linuxserver.davos.web.controller;\n\nimport javax.annotation.Resource;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport io.linuxserver.davos.delegation.services.HostService;\nimport io.linuxserver.davos.delegation.services.ScheduleService;\nimport io.linuxserver.davos.delegation.services.SettingsService;\nimport io.linuxserver.davos.exception.HostInUseException;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\nimport io.linuxserver.davos.web.Host;\nimport io.linuxserver.davos.web.Schedule;\nimport io.linuxserver.davos.web.ScheduleCommand;\nimport io.linuxserver.davos.web.controller.response.APIResponse;\nimport io.linuxserver.davos.web.controller.response.APIResponseBuilder;\nimport io.linuxserver.davos.web.selectors.LogLevelSelector;\n\n@RestController\n@RequestMapping(\"/api/v2\")\npublic class APIController {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(APIController.class);\n\n    @Resource\n    private ScheduleService scheduleService;\n\n    @Resource\n    private HostService hostService;\n\n    @Resource\n    private SettingsService settingsService;\n\n    @RequestMapping(value = \"/schedule\", method = RequestMethod.POST)\n    public ResponseEntity<APIResponse> createSchedule(@RequestBody Schedule schedule) {\n\n        LOGGER.info(\"Creating new schedule\");\n        LOGGER.debug(\"Schedule values are {}\", schedule);\n\n        if (!isSchedulePostPayloadValid(schedule)) {\n\n            LOGGER.error(\"Unable to create schedule: An id was supplied in the payload\");\n            return ResponseEntity.status(HttpStatus.BAD_REQUEST)\n                    .body(APIResponseBuilder.create().withBody(\"Payload contains ids\"));\n        }\n\n        try {\n\n            Schedule createdSchedule = scheduleService.createSchedule(schedule);\n            LOGGER.info(\"New schedule has been created\");\n\n            return ResponseEntity.status(HttpStatus.CREATED)\n                    .body(APIResponseBuilder.create().withStatus(\"Failure\").withBody(createdSchedule));\n\n        } catch (IllegalArgumentException e) {\n\n            LOGGER.error(\"Unable to create schedule: {}\", e.getMessage());\n            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(APIResponseBuilder.create().withBody(e.getMessage()));\n        }\n    }\n\n    private boolean isSchedulePostPayloadValid(Schedule schedule) {\n\n        boolean hasPushbulletIds = schedule.getNotifications().getPushbullet().stream().anyMatch(pb -> pb.getId() != null);\n        boolean hasSnsIds = schedule.getNotifications().getSns().stream().anyMatch(pb -> pb.getId() != null);\n        boolean hasFilterIds = schedule.getFilters().stream().anyMatch(f -> f.getId() != null);\n        boolean hasApiIds = schedule.getApis().stream().anyMatch(a -> a.getId() != null);\n\n        if (null != schedule.getId() || hasPushbulletIds || hasSnsIds || hasFilterIds || hasApiIds)\n            return false;\n\n        return true;\n    }\n\n    @RequestMapping(value = \"/schedule/{id}\", method = RequestMethod.GET)\n    public ResponseEntity<APIResponse> fetchSchedule(@PathVariable(\"id\") Long id) {\n\n        Schedule schedule = scheduleService.fetchSchedule(id);\n        LOGGER.debug(\"Fetched schedule: {}\", schedule);\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create().withBody(schedule));\n    }\n\n    @RequestMapping(value = \"/schedule/{id}\", method = RequestMethod.PUT)\n    public ResponseEntity<APIResponse> updateSchedule(@PathVariable(\"id\") Long id, @RequestBody Schedule schedule) {\n\n        LOGGER.info(\"Updating schedule with id {} and name {}\", id, schedule.getName());\n        LOGGER.debug(\"Schedule values are {}\", schedule);\n        LOGGER.debug(\"Imposing id from URL into body\");\n        schedule.setId(id);\n\n        Schedule updatedSchedule = scheduleService.updateSchedule(schedule);\n        LOGGER.debug(\"Schedule has been updated\");\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create().withBody(updatedSchedule));\n    }\n\n    @RequestMapping(value = \"/schedule/{id}\", method = RequestMethod.DELETE)\n    public ResponseEntity<APIResponse> deleteSchedule(@PathVariable(\"id\") Long id) {\n\n        LOGGER.info(\"Deleting schedule with id {}\", id);\n        scheduleService.deleteSchedule(id);\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create());\n    }\n    \n    @RequestMapping(value = \"/schedule/{id}/scannedFiles\", method = RequestMethod.DELETE)\n    public ResponseEntity<APIResponse> deleteScheduleScannedFiles(@PathVariable(\"id\") Long id) {\n        \n        LOGGER.info(\"Clearing last scanned file list for schedule {}\", id);\n        scheduleService.clearScannedFilesFromSchedule(id);        \n        \n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create());\n    }\n\n    @RequestMapping(value = \"/schedule/{id}/execute\", method = RequestMethod.POST)\n    public ResponseEntity<APIResponse> executeSchedule(@PathVariable(\"id\") Long id, @RequestBody ScheduleCommand command) {\n\n        if (command.command == ScheduleCommand.Command.START)\n            scheduleService.startSchedule(id);\n\n        if (command.command == ScheduleCommand.Command.STOP)\n            scheduleService.stopSchedule(id);\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create());\n    }\n\n    @RequestMapping(value = \"/host/{id}\", method = RequestMethod.GET)\n    public ResponseEntity<APIResponse> getHost(@PathVariable(\"id\") Long id) {\n\n        LOGGER.info(\"Getting host with id: {}\", id);\n        Host host = hostService.fetchHost(id);\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create().withBody(host));\n    }\n\n    @RequestMapping(value = \"/host\", method = RequestMethod.POST)\n    public ResponseEntity<APIResponse> createHost(@RequestBody Host host) {\n\n        LOGGER.info(\"Saving new host\");\n        LOGGER.debug(\"Host values are {}\", host);\n        Host createdHost = hostService.saveHost(host);\n        LOGGER.info(\"Host has been created\");\n\n        return ResponseEntity.status(HttpStatus.CREATED).body(APIResponseBuilder.create().withBody(createdHost));\n    }\n\n    @RequestMapping(value = \"/host/{id}\", method = RequestMethod.PUT)\n    public ResponseEntity<APIResponse> updateHost(@PathVariable(\"id\") Long id, @RequestBody Host host) {\n\n        LOGGER.info(\"Updating host with id {} and name {}\", id, host.getName());\n        LOGGER.debug(\"Host values are {}\", host);\n        LOGGER.debug(\"Imposing id from URL into body\");\n        host.setId(id);\n\n        Host updatedHost = hostService.saveHost(host);\n        LOGGER.debug(\"Host has been updated\");\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create().withBody(updatedHost));\n    }\n\n    @RequestMapping(value = \"/host/{id}\", method = RequestMethod.DELETE)\n    public ResponseEntity<APIResponse> deleteHost(@PathVariable(\"id\") Long id) {\n\n        LOGGER.info(\"Deleting host with id {}\", id);\n        try {\n            hostService.deleteHost(id);\n        } catch (HostInUseException e) {\n            return ResponseEntity.status(HttpStatus.BAD_REQUEST)\n                    .body(APIResponseBuilder.create().withStatus(\"Failed\").withBody(e.getMessage()));\n        }\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create());\n    }\n\n    @RequestMapping(value = \"/testConnection\", method = RequestMethod.POST)\n    public ResponseEntity<APIResponse> testConnection(@RequestBody Host host) {\n\n        APIResponse response = APIResponseBuilder.create();\n        HttpStatus status = HttpStatus.OK;\n\n        try {\n            hostService.testConnection(host);\n        } catch (FTPException e) {\n\n            LOGGER.error(\"Failed to connect to host\");\n            LOGGER.debug(\"Exception: \", e);\n\n            response.withBody(e.getCause().getMessage()).withStatus(\"Failed\");\n            status = HttpStatus.BAD_REQUEST;\n        }\n\n        return ResponseEntity.status(status).body(response);\n    }\n\n    @RequestMapping(value = \"/settings/log\", method = RequestMethod.POST)\n    public ResponseEntity<APIResponse> setLogLevel(@RequestParam(\"level\") LogLevelSelector level) {\n\n        settingsService.setLoggingLevel(level);\n\n        return ResponseEntity.status(HttpStatus.OK).body(APIResponseBuilder.create());\n    }\n\n    @ExceptionHandler(Exception.class)\n    public ResponseEntity<APIResponse> handleException(Exception e) {\n\n        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)\n                .body(APIResponseBuilder.create().withBody(e.getMessage()).withStatus(\"Failed\"));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/controller/FragmentController.java",
    "content": "package io.linuxserver.davos.web.controller;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport javax.annotation.Resource;\n\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.ModelAttribute;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\n\nimport io.linuxserver.davos.delegation.services.ScheduleService;\nimport io.linuxserver.davos.web.selectors.MethodSelector;\n\n@Controller\n@RequestMapping(\"/fragments\")\npublic class FragmentController {\n\n    @Resource\n    private ScheduleService scheduleService;\n    \n    @ModelAttribute(\"allMethods\")\n    public List<MethodSelector> populateMethods() {\n        return Arrays.asList(MethodSelector.ALL);\n    }\n    \n    @RequestMapping(\"/filter\")\n    public String filter(@RequestParam(\"value\") String value, Model model) {\n        \n        model.addAttribute(\"value\", value);\n        \n        return \"fragments/filter\";\n    }\n    \n    @RequestMapping(\"/notification/pushbullet\")\n    public String notificationPushbullet() {\n        return \"fragments/pushbullet\";\n    }\n    \n    @RequestMapping(\"/notification/sns\")\n    public String notificationSns() {\n        return \"fragments/sns\";\n    }\n    \n    @RequestMapping(\"/api\")\n    public String api() {\n        return \"fragments/api\";\n    }\n    \n    @RequestMapping(\"/schedule/{id}/transfers\")\n    public String transfers(@PathVariable Long id, Model model) {\n        \n        model.addAttribute(\"schedule\", scheduleService.fetchSchedule(id));\n        \n        return \"fragments/transfers\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/controller/ViewController.java",
    "content": "package io.linuxserver.davos.web.controller;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport javax.annotation.Resource;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.ModelAttribute;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\nimport io.linuxserver.davos.Version;\nimport io.linuxserver.davos.delegation.services.HostService;\nimport io.linuxserver.davos.delegation.services.ScheduleService;\nimport io.linuxserver.davos.delegation.services.SettingsService;\nimport io.linuxserver.davos.web.Host;\nimport io.linuxserver.davos.web.Schedule;\nimport io.linuxserver.davos.web.Settings;\nimport io.linuxserver.davos.web.VersionChecker;\nimport io.linuxserver.davos.web.selectors.IntervalSelector;\nimport io.linuxserver.davos.web.selectors.LogLevelSelector;\nimport io.linuxserver.davos.web.selectors.MethodSelector;\nimport io.linuxserver.davos.web.selectors.ProtocolSelector;\nimport io.linuxserver.davos.web.selectors.TransferSelector;\n\n@Controller\npublic class ViewController {\n\n    @Resource\n    private ScheduleService scheduleService;\n\n    @Resource\n    private HostService hostService;\n    \n    @Resource\n    private SettingsService settingsService;\n    \n    @Value(\"${davos.version}\")\n    private String currentVersion;\n\n    @ModelAttribute(\"currentVersion\")\n    public String currentVersion() {\n        return currentVersion;\n    }\n    \n    @ModelAttribute(\"allIntervals\")\n    public List<IntervalSelector> populateIntervals() {\n        return Arrays.asList(IntervalSelector.ALL);\n    }\n    \n    @ModelAttribute(\"versionChecker\")\n    public VersionChecker versionChecker() {\n        return new VersionChecker(new Version(currentVersion()), settingsService.retrieveRemoteVersion());\n    }\n\n    @ModelAttribute(\"allProtocols\")\n    public List<ProtocolSelector> populateProtocols() {\n        return Arrays.asList(ProtocolSelector.ALL);\n    }\n\n    @ModelAttribute(\"allTransferTypes\")\n    public List<TransferSelector> populateTypes() {\n        return Arrays.asList(TransferSelector.ALL);\n    }\n    \n    @ModelAttribute(\"allMethods\")\n    public List<MethodSelector> populateMethods() {\n        return Arrays.asList(MethodSelector.ALL);\n    }\n\n    @ModelAttribute(\"allHosts\")\n    public List<Host> allHosts() {\n        return hostService.fetchAllHosts();\n    }\n    \n    @ModelAttribute(\"allLogLevels\")\n    public List<LogLevelSelector> allLogLevels() {\n        return Arrays.asList(LogLevelSelector.ALL);\n    }\n\n    @RequestMapping(\"/\")\n    public String index() {\n        return \"redirect:/schedules\";\n    }\n\n    @RequestMapping(\"/settings\")\n    public String settings(Model model) {\n        \n        Settings settings = new Settings();\n        settings.setLogLevel(settingsService.getCurrentLoggingLevel());\n        \n        model.addAttribute(\"settings\", settings);\n        \n        return \"v2/settings\";\n    }\n\n    @RequestMapping(\"/schedules\")\n    public String schedules(Model model) {\n\n        model.addAttribute(\"schedules\", scheduleService.fetchAllSchedules());\n        return \"v2/schedules\";\n    }\n\n    @RequestMapping(\"/schedules/new\")\n    public String newSchedule(Model model) {\n        \n        model.addAttribute(\"schedule\", new Schedule());\n        return \"v2/edit-schedule\";\n    }\n\n    @RequestMapping(\"/schedules/{id}\")\n    public String schedules(@PathVariable Long id, Model model) {\n\n        model.addAttribute(\"schedule\", scheduleService.fetchSchedule(id));\n        return \"v2/edit-schedule\";\n    }\n\n    @RequestMapping(\"/hosts\")\n    public String hosts() {\n        return \"v2/hosts\";\n    }\n\n    @RequestMapping(\"/hosts/new\")\n    public String newHost(Model model) {\n\n        model.addAttribute(\"host\", new Host());\n        return \"v2/edit-host\";\n    }\n\n    @RequestMapping(\"/hosts/{id}\")\n    public String hosts(@PathVariable Long id, Model model) {\n\n        model.addAttribute(\"host\", hostService.fetchHost(id));\n        model.addAttribute(\"usedBy\", hostService.fetchSchedulesUsingHost(id));\n        \n        return \"v2/edit-host\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/controller/response/APIResponse.java",
    "content": "package io.linuxserver.davos.web.controller.response;\n\npublic class APIResponse {\n\n    public String status = \"OK\";\n    public Object body;\n\n    public APIResponse withBody(Object body) {\n\n        this.body = body;\n        return this;\n    }\n\n    public APIResponse withStatus(String status) {\n        this.status = status;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/controller/response/APIResponseBuilder.java",
    "content": "package io.linuxserver.davos.web.controller.response;\n\npublic class APIResponseBuilder {\n\n    public static APIResponse create() {\n        return new APIResponse();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/selectors/IntervalSelector.java",
    "content": "package io.linuxserver.davos.web.selectors;\n\npublic enum IntervalSelector {\n\n    MINS_1(1, \"Every minute\"),\n    MINS_5(5, \"Every 5 minutes\"),\n    MINS_15(15, \"Every 15 minutes\"), \n    MINS_30(30, \"Every 30 minutes\"), \n    EVERY_HOUR(60, \"Every hour\"),\n    EVERY_2_HOURS(120, \"Every two hours\"), \n    TWICE_A_DAY(720, \"Twice a day\"), \n    EVERY_DAY(1440, \"Once a day\");\n    \n    public static final IntervalSelector[] ALL = { MINS_1, MINS_5, MINS_15, MINS_30, EVERY_HOUR, \n            EVERY_2_HOURS, TWICE_A_DAY, EVERY_DAY };\n    \n    private IntervalSelector(int minutes, String text) {\n        this.minutes = minutes;\n        this.text = text;\n    }\n    \n    private final int minutes;\n    private final String text;\n    \n    public int getMinutes() {\n        return minutes;\n    }\n    \n    public String getText() {\n        return text;\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/selectors/LogLevelSelector.java",
    "content": "package io.linuxserver.davos.web.selectors;\n\npublic enum LogLevelSelector {\n    \n    DEBUG, INFO, WARN, ERROR;\n\n    public static final LogLevelSelector[] ALL = { DEBUG, INFO, WARN, ERROR };\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/selectors/MethodSelector.java",
    "content": "package io.linuxserver.davos.web.selectors;\n\npublic enum MethodSelector {\n\n    GET, POST, PUT, DELETE;\n    \n    public static final MethodSelector[] ALL = { GET, POST, PUT, DELETE };\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/selectors/ProtocolSelector.java",
    "content": "package io.linuxserver.davos.web.selectors;\n\npublic enum ProtocolSelector {\n\n    FTP, FTPS, SFTP;\n    \n    public static final ProtocolSelector[] ALL = { FTP, FTPS, SFTP };\n}\n"
  },
  {
    "path": "src/main/java/io/linuxserver/davos/web/selectors/TransferSelector.java",
    "content": "package io.linuxserver.davos.web.selectors;\n\npublic enum TransferSelector {\n\n    FILE, RECURSIVE;\n    \n    public static final TransferSelector[] ALL = { FILE, RECURSIVE };\n}\n"
  },
  {
    "path": "src/main/resources/static/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/mstile-150x150.png\"/>\n            <TileColor>#2b5797</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "src/main/resources/static/css/davos.css",
    "content": "body {\n    padding-top: 40px;\n}\n\n.filter-label {\n    padding: 10px;\n    font-weight: normal;\n    font-size: 12px;\n    margin-bottom: 3px;\n    display: inline-block;\n}\n\n.filter-close {\n    margin-left: 5px;\n    cursor: pointer;\n}\n\n.form-group.last {\n    margin-top: 40px;\n}\n\n.form-group.filters {\n    margin-bottom: 50px;\n}\n\n.glyphicon.help {\n    cursor: pointer;\n}\n\n.align-right {\n    text-align: right;\n}\n\ntd .progress {\n    margin-bottom: 0;\n    height: 15px;\n    border-radius: 3px;\n}\n\n.downloads {\n    font-size: 12px;\n}\n\n.panel .panel-heading, .panel .panel-title {\n    font-size: 13px;\n    line-height: 26px;\n}\n\n.panel .panel-heading .glyphicon, .edit-host {\n    cursor: pointer;\n}\n\n.label-title {\n    background-color: #333333;\n}\n\n.dropdown-menu li a {\n    cursor: pointer;\n}\n\na.link {\n    color: #333333\n}\n\n.popover {\n    max-width: 600px;\n}\n\n.hide {\n    display: none;\n}\n\n.navbar-brand small {\n    color: #cdcdcd;\n    font-size: 14px;\n}\n\n#announcement {\n\tmargin-top: 25px;\n}\n\n.transfer-speed {\n\twidth: 80px;\n}"
  },
  {
    "path": "src/main/resources/static/js/davos.js",
    "content": "/*global $, jQuery, base, Materialize */\nvar settings = (function($) {\n\n    'use strict';\n\n    var initialise, makeNotify, validate;\n\n    makeNotify = function(notificationType, messageText, icon) {\n\n        $.notify({\n            icon: 'glyphicon ' + icon,\n            message: messageText\n        }, {\n            // settings\n            type: notificationType,\n            placement: {\n                from: \"top\",\n                align: \"right\"\n            },\n            delay: 3000\n        });\n    };\n\n    validate = function() {\n\n        var validationPassed = true;\n\n        $('input[type=\"text\"].validate, input[type=\"number\"].validate').each(function() {\n\n            if ($.trim($(this).val()).length === 0) {\n                $(this).parents('.form-group').addClass('has-error');\n                validationPassed = false;\n            } else {\n                $(this).parents('.form-group').removeClass('has-error');\n            }\n        });\n\n        return validationPassed;\n    };\n\n    initialise = function() {\n\n        $('#logLevel').on('change', function() {\n\n            var logLevel = $(this).find('option:selected').val();\n\n            makeNotify('info', 'Changing logging level to ' + logLevel, 'glyphicon-info-sign');\n\n            $.ajax({\n                method: 'POST',\n                url: '/api/v2/settings/log?level=' + logLevel\n            }).done(function(msg) {\n                makeNotify('success', 'Settings saved!', 'glyphicon-ok-sign');\n            }).fail(function(msg) {\n                makeNotify('danger', 'There was an error: ' + msg.responseJSON.status, 'glyphicon-warning-sign');\n            });\n        });\n    };\n\n    return {\n        init: initialise,\n        notify: makeNotify,\n        validate: validate\n    }\n\n}(jQuery));\n\nvar fragments = (function($) {\n\n    'use strict';\n\n    var initialise, clicks, removes, keypresses;\n\n    initialise = function() {\n\n        clicks();\n        removes();\n        keypresses();\n    };\n\n    clicks = function() {\n\n        $('#newAPI').on('click', function() {\n            $('#apis').append($(\"<div />\").load(\"/fragments/api\"));\n        });\n\n        $('#newPushbullet').on('click', function() {\n            $('#notifications').append($(\"<div />\").load(\"/fragments/notification/pushbullet\"));\n        });\n        \n        $('#newSns').on('click', function() {\n            $('#notifications').append($(\"<div />\").load(\"/fragments/notification/sns\"));\n        });\n\n        $('#addFilter').on('click', function() {\n\n            if ($.trim($('#newFilter').val()).length > 0) {\n                $('#filters').append($(\"<span />\").load(\"/fragments/filter?value=\" + $('#newFilter').val()));\n                $('#newFilter').val('');\n            }\n        });\n    };\n\n    keypresses = function() {\n\n        $('#newFilter').on('keypress', function(e) {\n\n            if (e.keyCode == 13) {\n\n                if ($.trim($('#newFilter').val()).length > 0) {\n                    $('#filters').append($(\"<span />\").load(\"/fragments/filter?value=\" + $('#newFilter').val()));\n                    $('#newFilter').val('');\n                }\n            }\n        });\n    };\n\n    removes = function() {\n\n        $('#notifications').on('click', '.remove-notification', function() {\n            $(this).parents('.notification').remove();\n        });\n\n        $('#apis').on('click', '.remove-api', function() {\n            $(this).parents('.api').remove();\n        });\n\n        $('#filters').on('click', '.filter-close', function() {\n            $(this).parents('.filter-label').remove();\n        });\n    }\n\n    return {\n        init: initialise\n    };\n\n}(jQuery));\n\nvar schedule = (function($, settings) {\n\n    'use strict';\n\n    var initialise, cleanId, success, error;\n\n    initialise = function() {\n\n        $('#schedule-form').on('submit', function(e) {\n            e.preventDefault();\n            e.stopPropagation();\n        });\n\n        $('#saveSchedule').on('click', function() {\n\n            settings.notify('info', 'Saving...', 'glyphicon-info-sign');\n\n            if (settings.validate()) {\n\n                var postData = {\n\n                    id: cleanId($('#id').val()),\n                    name: $('#name').val(),\n                    interval: parseInt($('#interval option:checked').attr('value'), 10),\n                    host: parseInt($('#host option:checked').attr('value'), 10),\n                    hostDirectory: $('#hostDirectory').val(),\n                    localDirectory: $('#localDirectory').val(),\n                    transferType: $('input[name=\"transferType\"]:checked').val(),\n                    automatic: $('input[name=\"automatic\"]').prop('checked'),\n                    filtersMandatory: $('input[name=\"filtersMandatory\"]').prop('checked'),\n                    invertFilters: $('input[name=\"invertFilters\"]').prop('checked'),\n                    deleteHostFile: $('input[name=\"deleteHostFile\"]').prop('checked'),\n                    moveFileTo: $('#moveFileTo').val(),\n                    filters: [],\n                    notifications: {\n                    \tpushbullet: [],\n                    \tsns: []\n                    },\n                    apis: []\n                };\n\n                $('.filter-label').each(function() {\n\n                    postData.filters.push({\n                        \"id\": cleanId($(this).attr('data-filter-id')),\n                        \"value\": $(this).attr('data-filter-value')\n                    });\n                });\n\n                $('#notifications .notification.pushbullet').each(function() {\n\n                    postData.notifications.pushbullet.push({\n                        \"id\": cleanId($(this).attr('data-notification-id')),\n                        \"apiKey\": $(this).find('.apiKey').val()\n                    });\n                });\n                \n                $('#notifications .notification.sns').each(function() {\n                \t\n                    postData.notifications.sns.push({\n                        \"id\": cleanId($(this).attr('data-notification-id')),\n                        \"topicArn\": $(this).find('.topicArn').val(),\n                        \"region\": $(this).find('.region').val(),\n                        \"accessKey\": $(this).find('.accessKey').val(),\n                        \"secretAccessKey\": $(this).find('.secretAccessKey').val()\n                    });\n                });\n\n                $('#apis .api').each(function() {\n\n                    postData.apis.push({\n                        \"id\": cleanId($(this).attr('data-api-id')),\n                        \"url\": $(this).find('.url').val(),\n                        \"method\": $(this).find('.method option:checked').attr('value'),\n                        \"contentType\": $(this).find('.contentType').val(),\n                        \"body\": $(this).find('.body').val()\n                    });\n                });\n\n                var url = \"/api/v2/schedule\";\n                var method = \"POST\";\n\n                if (null !== cleanId($('#id').val())) {\n\n                    url += \"/\" + cleanId($('#id').val());\n                    method = \"PUT\";\n                }\n\n                $.ajax({\n\n                    method: method,\n                    url: url,\n                    dataType: \"json\",\n                    contentType: 'application/json',\n                    data: JSON.stringify(postData)\n\n                }).done(success).fail(error);\n\n            } else {\n                settings.notify('danger', 'Required fields are missing.', 'glyphicon-warning-sign');\n            }\n        });\n\n        $('#deleteSchedule').on('click', function() {\n\n            $.ajax({\n                method: 'DELETE',\n                url: '/api/v2/schedule/' + $('#id').val()\n            }).done(function(msg) {\n                window.location.replace('/schedules');\n            }).fail(error);\n        });\n\n        $('.start-schedule').on('click', function() {\n\n            var id = $(this).attr('data-schedule-id'),\n                name = $(this).attr('data-schedule-name');\n\n            settings.notify('info', 'Starting schedule \"' + name + '\"', 'glyphicon-info-sign');\n\n            $.ajax({\n\n                method: 'POST',\n                url: '/api/v2/schedule/' + id + '/execute',\n                dataType: \"json\",\n                contentType: 'application/json',\n                data: JSON.stringify({\n                    command: 'START'\n                })\n\n            }).done(function(msg) {\n\n                settings.notify('success', 'Schedule Started', 'glyphicon-ok-sign');\n\n                $('span[data-schedule-id=\"' + id + '\"].start-schedule').toggleClass('hide');\n                $('span[data-schedule-id=\"' + id + '\"].stop-schedule').parents('span').toggleClass('hide');\n\n            }).fail(error);\n\n        });\n\n        $('.stop-schedule').on('click', function() {\n\n            var id = $(this).attr('data-schedule-id'),\n                name = $(this).attr('data-schedule-name');\n\n            settings.notify('info', 'Stopping schedule \"' + name + '\"', 'glyphicon-info-sign');\n\n            $.ajax({\n\n                method: 'POST',\n                url: '/api/v2/schedule/' + id + '/execute',\n                dataType: \"json\",\n                contentType: 'application/json',\n                data: JSON.stringify({\n                    command: 'STOP'\n                })\n\n            }).done(function(msg) {\n\n                settings.notify('success', 'Schedule Stopped', 'glyphicon-ok-sign');\n\n                $('span[data-schedule-id=\"' + id + '\"].start-schedule').toggleClass('hide');\n                $('span[data-schedule-id=\"' + id + '\"].stop-schedule').parents('span').toggleClass('hide');\n\n            }).fail(error);\n\n        });\n        \n        $('.clearLastScanned').on('click', function() {\n        \t\n        \tvar id = $(this).attr('data-schedule-id');\n        \t\n        \t$.ajax({\n\n                method: 'DELETE',\n                url: '/api/v2/schedule/' + id + '/scannedFiles',\n                dataType: \"json\"\n                \n            }).done(function(msg) {\n                $('#lastScanned' + id + ' table tbody').empty();\n            }).fail(error);\n        });\n\n    };\n\n    cleanId = function(id) {\n\n        if (id && $.trim(id).length > 0) {\n            return parseInt(id, 10);\n        }\n\n        return null;\n    };\n\n    success = function(msg) {\n\n        settings.notify('success', 'Schedule Saved', 'glyphicon-ok-sign');\n\n        if (window.location.pathname === '/schedules/new') {\n            window.location.replace('/schedules/' + msg.body.id);\n        }\n    };\n\n    error = function(msg) {\n        settings.notify('danger', 'There was an error: ' + msg.responseJSON.body, 'glyphicon-warning-sign');\n    };\n\n    return {\n        init: initialise\n    }\n\n}(jQuery, settings));\n\nvar host = (function($, settings) {\n\n    'use strict';\n\n    var initialise, cleanId, makeRequest, success, error, validate;\n\n    initialise = function() {\n\n\t\tif ($('input[name=\"identityFileEnabled\"]').prop('checked')) {\n\t\t\t\n\t\t\t$('#password-group').hide();\n\t\t\t$('#identityFile-group').show();\n\t\t\t$('#identityFile').addClass('validate');\n\t\t}\n\t\t\n\t\t$('input[name=\"identityFileEnabled\"]').on('change', function() {\n\t\t\n\t\t\t$('#password-group').toggle();\n\t\t\t$('#identityFile-group').toggle();\n\t\t\t$('#identityFile').toggleClass('validate');\n\t\t});\n    \t\n\t\t$('input[name=\"protocol\"]').on('change', function() {\n\t\t\t\n\t\t\tif ($('input[name=\"protocol\"]:checked').val() !== 'SFTP') {\n\t\t\t\t\n\t\t\t\t$('input[name=\"identityFileEnabled\"]').prop('checked', false);\t\t\t\t\n\t\t\t\t$('#identityFile-group').hide();\n\t\t\t\t$('#toggleIdentity-group').hide();\n\t\t\t\t$('#password-group').show();\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t$('#toggleIdentity-group').show();\n\t\t\t}\n\t\t});\n\t\t\n        $('#testConnection').on('click', function() {\n\n        \tif (settings.validate()) {\n        \t\t\n\t            settings.notify('info', 'Testing connection...', 'glyphicon-info-sign');\n\t\n\t            var postData = {\n\t                address: $('#address').val(),\n\t                port: parseInt($('#port').val(), 10),\n\t                protocol: $('input[name=\"protocol\"]:checked').val(),\n\t                username: $('#username').val(),\n\t                password: $('#password').val(),\n\t                identityFileEnabled: $('input[name=\"identityFileEnabled\"]').prop('checked'),\n\t                identityFile: $('#identityFile').val()\n\t            };\n\t\n\t            var url = \"/api/v2/testConnection\";\n\t            var method = \"POST\";\n\t\n\t            makeRequest(url, method, postData, function(msg) {\n\t                settings.notify('success', 'Connection successful!', 'glyphicon-ok-sign');\n\t            }, error);\n        \t}\n\n        });\n\n        $('#saveHost').on('click', function() {\n\n            settings.notify('info', 'Saving...', 'glyphicon-info-sign');\n\n            if (settings.validate()) {\n\n                var postData = {\n                    id: cleanId($('#id').val()),\n                    name: $('#name').val(),\n                    address: $('#address').val(),\n                    port: parseInt($('#port').val(), 10),\n                    protocol: $('input[name=\"protocol\"]:checked').val(),\n                    username: $('#username').val(),\n                    password: $('#password').val(),\n                    identityFileEnabled: $('input[name=\"identityFileEnabled\"]').prop('checked'),\n                    identityFile: $('#identityFile').val()\n                };\n\n                var url = \"/api/v2/host\";\n                var method = \"POST\";\n\n                if (null !== cleanId($('#id').val())) {\n\n                    url += \"/\" + cleanId($('#id').val());\n                    method = \"PUT\";\n                }\n\n                makeRequest(url, method, postData, success, error);\n            } else {\n                settings.notify('danger', 'Required fields are missing', 'glyphicon-warning-sign');\n            }\n        });\n\n        $('#deleteHost').on('click', function() {\n\n            $.ajax({\n                method: 'DELETE',\n                url: '/api/v2/host/' + $('#id').val()\n            }).done(function(msg) {\n                window.location.replace('/hosts');\n            }).fail(error);\n        });\n    };\n\n    makeRequest = function(url, method, postData, successCallback, errorCallback) {\n\n        $.ajax({\n\n            method: method,\n            url: url,\n            dataType: \"json\",\n            contentType: 'application/json',\n            data: JSON.stringify(postData)\n\n        }).done(successCallback).fail(errorCallback);\n\n    };\n\n    success = function(msg) {\n\n        settings.notify('success', 'Host Saved!', 'glyphicon-ok-sign');\n\n        if (window.location.pathname === '/hosts/new') {\n            window.location.replace('/hosts/' + msg.body.id);\n        }\n    };\n\n    error = function(msg) {\n        settings.notify('danger', 'There was an error: ' + msg.responseJSON.body, 'glyphicon-warning-sign');\n    };\n\n    cleanId = function(id) {\n\n        if (id && $.trim(id).length > 0) {\n            return parseInt(id, 10);\n        }\n\n        return null;\n    };\n\n    return {\n        init: initialise\n    }\n\n}(jQuery, settings))\n\nvar interval = (function($) {\n\t\n\tvar init;\n\t\n\tinit = function() {\n\t\t\n\t\tsetInterval(function() {\n\n\t\t\t$(\".downloads\").each(function() {\n\t\t\t\t\t\t\t\t\n\t\t\t\tvar $this = $(this);\n\t\t\t\tvar scheduleId = $this.attr('data-schedule-id');\n\t\t\t\t$this.load('/fragments/schedule/' + scheduleId + '/transfers')\n\t\t\t});\n\t\t}, 2000);\n\t};\n\t\n\treturn {\n\t\tinit: init\n\t};\n\n}(jQuery));\n\njQuery(document).ready(host.init);\njQuery(document).ready(schedule.init);\njQuery(document).ready(fragments.init);\njQuery(document).ready(settings.init);\njQuery(document).ready(interval.init);\n"
  },
  {
    "path": "src/main/resources/static/manifest.json",
    "content": "{\n    \"name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}"
  },
  {
    "path": "src/main/resources/templates/fragments/api.html",
    "content": "<div class=\"well api\">\n\n\t<h4>API Call</h4>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">URL</label>\n        <div class=\"col-lg-10\">\n            <input type=\"text\" class=\"form-control url\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Method</label>\n        <div class=\"col-lg-10\">\n            <select class=\"form-control method\">\n                <option th:each=\"method : ${allMethods}\" th:value=\"${method}\" th:text=\"${method}\"></option>\n            </select>\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Content-Type</label>\n        <div class=\"col-lg-10\">\n            <input type=\"text\" class=\"form-control contentType\" placeholder=\"e.g. application/json\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <label for=\"sch-api-body\" class=\"col-lg-2 control-label\">Message Body <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Use $filename to reference the downloaded file\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n        <div class=\"col-lg-10\">\n            <textarea class=\"form-control body\" rows=\"3\"></textarea>\n            <span class=\"help-block\"></span>\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <div class=\"col-lg-12 align-right\">\n            <button type=\"button\" class=\"btn btn-danger btn-sm remove-api\">Remove</button>\n        </div>\n    </div>\n\n</div>\n"
  },
  {
    "path": "src/main/resources/templates/fragments/filter.html",
    "content": "<span class=\"label label-default filter-label\" th:attr=\"data-filter-value=${value}\" th:inline=\"text\">[[${value}]] <span class=\"filter-close\">&times;</span></span>\n"
  },
  {
    "path": "src/main/resources/templates/fragments/header.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\t<body>\n\t\n\t    <div class=\"navbar navbar-default navbar-fixed-top\" th:fragment=\"header\">\n\t        <div class=\"container\">\n\t            <div class=\"navbar-header\">\n\t                <a th:href=\"@{/}\" class=\"navbar-brand\">davos<small>v2</small></a>\n\t                <button class=\"navbar-toggle\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbar-main\">\n\t            <span class=\"icon-bar\"></span>\n\t            <span class=\"icon-bar\"></span>\n\t            <span class=\"icon-bar\"></span>\n\t          </button>\n\t            </div>\n\t            <div class=\"navbar-collapse collapse\" id=\"navbar-main\">\n\t                <ul class=\"nav navbar-nav\">\n\t                    <li class=\"dropdown\">\n\t                        <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\" id=\"scheduling\">Scheduling <span class=\"caret\" /></a>\n\t                        <ul class=\"dropdown-menu\" aria-labelledby=\"scheduling\">\n\t                            <li><a th:href=\"@{/schedules}\"><span class=\"glyphicon glyphicon-time\"></span>&nbsp;&nbsp; Schedules</a></li>\n\t                            <li><a th:href=\"@{/hosts}\"><span class=\"glyphicon glyphicon-cloud\"></span>&nbsp;&nbsp; Hosts</a></li>\n\t                        </ul>\n\t                    </li>\n\t                    <li>\n\t                        <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\" id=\"settings\">Settings <span class=\"caret\" /></a>\n\t                        <ul class=\"dropdown-menu\" aria-labelledby=\"settings\">\n\t                            <li><a th:href=\"@{/schedules/new}\"><span class=\"glyphicon glyphicon-plus\"></span>&nbsp;&nbsp; New Schedule</a></li>\n\t                            <li><a th:href=\"@{/hosts/new}\"><span class=\"glyphicon glyphicon-plus\"></span>&nbsp;&nbsp; New Host</a></li>\n\t                            <li class=\"divider\"></li>\n\t                            <li><a th:href=\"@{/settings}\"><span class=\"glyphicon glyphicon-cog\"></span>&nbsp;&nbsp; App Settings</a></li>\n\t                        </ul>\n\t                    </li>\n\t\n\t                </ul>\n\t\n\t            </div>\n\t        </div>\n\t    </div>\n\t    \n\t    <div class=\"container\" id=\"announcement\" th:fragment=\"announcement\" th:if=\"${versionChecker.newVersionAvailable}\">\n\t\t\t<div class=\"row\">\n                <div class=\"col-md-12\">   \n\t\t\t\t    <div class=\"alert alert-dismissible alert-info\">\n\t\t\t\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n\t\t\t\t\t\t <span class=\"glyphicon glyphicon-gift\"></span>&nbsp;\n\t\t\t\t\t\t<strong>New version available!</strong> Version <span th:text=\"${versionChecker.newVersion}\"></span> of davos has been released. You can get it in the usual way. \n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>"
  },
  {
    "path": "src/main/resources/templates/fragments/pushbullet.html",
    "content": "<div class=\"well notification pushbullet\">\n\n\t<h4>Pushbullet</h4>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Access Token</label>\n        <div class=\"col-lg-10\">\n            <input type=\"text\" class=\"form-control apiKey\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <div class=\"col-lg-12 align-right\">\n            <button type=\"button\" class=\"btn btn-danger btn-sm remove-notification\">Remove</button>\n        </div>\n    </div>\n\n</div>\n"
  },
  {
    "path": "src/main/resources/templates/fragments/sns.html",
    "content": "<div class=\"well notification sns\">\n\n\t<h4>Amazon SNS</h4>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Topic Arn</label>\n        <div class=\"col-lg-10\">\n            <input type=\"text\" class=\"form-control topicArn\" placeholder=\"e.g. arn:aws:sns:*:123456789012:my_notification_topic\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Region</label>\n        <div class=\"col-lg-10\">\n\t\t\t<input type=\"text\" class=\"form-control region\" placeholder=\"e.g. eu-west-1\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Access Key <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Your specified IAM User's access key\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n        <div class=\"col-lg-10\">\n            <input type=\"text\" class=\"form-control accessKey\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <label class=\"col-lg-2 control-label\">Secret Access Key <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Your specified IAM User's secret access key\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n        <div class=\"col-lg-10\">\n            <input type=\"text\" class=\"form-control secretAccessKey\" autocomplete=\"off\" />\n        </div>\n    </div>\n\n    <div class=\"form-group\">\n        <div class=\"col-lg-12 align-right\">\n            <button type=\"button\" class=\"btn btn-danger btn-sm remove-notification\">Remove</button>\n        </div>\n    </div>\n\n</div>\n"
  },
  {
    "path": "src/main/resources/templates/fragments/transfers.html",
    "content": "<table class=\"table\" th:if=\"${not #lists.isEmpty(schedule.transfers)}\">\n    <thead>\n        <tr>\n            <th>File</th>\n            <th>Size</th>\n            <th>Status</th>\n            <th>Progress</th>\n            <th>Speed</th>\n        </tr>\n    </thead>\n    <tbody>\n        <tr th:each=\"transfer : ${schedule.transfers}\">\n            <td th:text=\"${transfer.fileName}\"></td>\n            <td th:unless=\"${transfer.directory}\" th:text=\"${(transfer.fileSize / 1000000) + 'MB'}\"></td>\n            <td th:if=\"${transfer.directory}\"></td>\n            <td>\n                <span th:text=\"${transfer.status}\"></span>\n            </td>\n            <td th:if=\"${transfer.progress}\" class=\"transfer-progress\">\n                <div class=\"progress\">\n                    <div class=\"progress-bar\" th:style=\"'width:' + ${transfer.progress.percentageComplete} + '%'\"></div>\n                </div>\n            </td>\n            <td th:unless=\"${transfer.progress}\" class=\"transfer-progress\">\n                <div class=\"progress\" th:if=\"${transfer.status == 'PENDING'}\">\n                    <div class=\"progress-bar\" style=\"width: 0%;\"></div>\n                </div>\n                <div class=\"progress\" th:if=\"${transfer.status == 'SKIPPED'}\">\n                    <div class=\"progress-bar progress-bar-warning\" style=\"width: 100%;\"></div>\n                </div>\n            </td>\n            <td th:if=\"${transfer.progress}\" class=\"transfer-speed\">\n                <span th:if=\"${transfer.progress.percentageComplete &lt; 100}\" th:text=\"${#numbers.formatDecimal(transfer.progress.transferSpeed, 0, 2, 'POINT') + 'MB/s'}\"></span>\n            </td>\n            <td th:unless=\"${transfer.progress}\"></td>\n        </tr>\n    </tbody>\n</table>"
  },
  {
    "path": "src/main/resources/templates/v2/edit-host.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"description\" content=\"An automated FTP client for your home server\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <title>Edit Host</title>\n\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n\t<link rel=\"manifest\" href=\"/manifest.json\" />\n\t<link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#5bbad5\" />\n\t<meta name=\"theme-color\" content=\"#ffffff\" />\n\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/bootstrap.min.css}\" media=\"screen,projection\" />\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/davos.css}\" media=\"screen\" />\n\n</head>\n\n<body>\n\n    <div class=\"navbar navbar-default navbar-fixed-top\" th:replace=\"fragments/header :: header\">\n    </div>\n\n    <div class=\"container\">\n\n        <div class=\"schedules-section\">\n\n            <div class=\"row\">\n                <div class=\"col-md-12\">\n                    <div class=\"page-header\">\n                        <h2>Edit Host</h2>\n                    </div>\n                </div>\n            </div>\n\n\t\t\t<div class=\"well\">\n\t            <form class=\"form-horizontal\" onsubmit=\"return false;\" action=\"#\" th:object=\"${host}\">\n\t                <fieldset>\n\n\t                    <div class=\"row\">\n\n\t                        <div class=\"col-md-12\">\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"host-name\" class=\"col-lg-2 control-label\">Name <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"What you want to refer to this host as\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"hidden\" th:if=\"*{id}\" th:field=\"*{id}\" />\n\t                                    <input type=\"text\" class=\"form-control validate\" th:field=\"*{name}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Protocol <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"What kind of server is this?\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"radio\" th:each=\"protocol : ${allProtocols}\">\n\t                                        <label th:inline=\"text\">\n\t                                            <input type=\"radio\" th:field=\"*{protocol}\" th:value=\"${protocol}\" /> [[${protocol}]]\n\t                                        </label>\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Host Address <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Name or IP address of the host\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control validate\" th:field=\"*{address}\" autocomplete=\"off\" />\n\t                                </div>\n                                </div>\n\n                                <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Port <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"The port used by the host for this protocol\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-4\">\n\t                                    <input type=\"number\" class=\"form-control validate\" th:field=\"*{port}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Username</label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control validate\" th:field=\"*{username}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\t                            \n\t\t\t\t\t\t\t\t <div th:unless=\"${host.protocol.toString().equals('SFTP')}\" class=\"form-group\" id=\"toggleIdentity-group\" style=\"display: none\">\n\t                                <label class=\"col-lg-2 control-label\">Use Identity File <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"Use a local private identity file instead of a password\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"checkbox\">\n\t                                      <label></label>\n\t                                      <input type=\"checkbox\" th:field=\"*{identityFileEnabled}\" />\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\n\t                            <div th:if=\"${host.protocol.toString().equals('SFTP')}\" class=\"form-group\" id=\"toggleIdentity-group\">\n\t                                <label class=\"col-lg-2 control-label\">Use Identity File <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"Use a local private identity file instead of a password\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"checkbox\">\n\t                                      <label></label>\n\t                                      <input type=\"checkbox\" th:field=\"*{identityFileEnabled}\" />\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\t                            \n\t                            <div th:unless=\"${host.identityFileEnabled}\" class=\"form-group\" id=\"password-group\">\n\t                                <label class=\"col-lg-2 control-label\">Password</label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"password\" class=\"form-control\" th:value=\"*{password}\" name=\"password\" id=\"password\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\t                            \n\t                            <div th:if=\"${host.identityFileEnabled}\" class=\"form-group\" id=\"password-group\" style=\"display: none\">\n\t                                <label class=\"col-lg-2 control-label\">Password</label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"password\" class=\"form-control\" th:value=\"*{password}\" name=\"password\" id=\"password\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\t                            \n\t                            <div th:if=\"${host.identityFileEnabled}\" class=\"form-group\" id=\"identityFile-group\">\n\t                                <label class=\"col-lg-2 control-label\">Identity File</label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control\" th:value=\"*{identityFile}\" name=\"identityFile\" id=\"identityFile\" autocomplete=\"off\" placeholder=\"e.g. /config/.ssh/id_rsa\" />\n\t                                </div>\n\t                            </div>\n\t                            \n\t                            <div th:unless=\"${host.identityFileEnabled}\" class=\"form-group\" id=\"identityFile-group\" style=\"display: none\">\n\t                                <label class=\"col-lg-2 control-label\">Identity File</label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control\" th:value=\"*{identityFile}\" name=\"identityFile\" id=\"identityFile\" autocomplete=\"off\" placeholder=\"e.g. /config/.ssh/id_rsa\" />\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\"></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <button type=\"button\" class=\"btn btn-sm btn-primary\" id=\"testConnection\">Test Connection</button>\n\t                                </div>\n\t                            </div>\n\n\t                        </div>\n\t                    </div>\n\n\n\t                    <div class=\"row\">\n\n\t                        <div class=\"col-md-12\">\n\n\t                            <div class=\"form-group last\">\n\t                                <div class=\"col-lg-6\">\n\t                                    <div th:if=\"${host.id}\">\n\t                                        <div th:unless=\"${#lists.isEmpty(usedBy)}\">\n\t                                            <button type=\"button\" class=\"btn btn-danger\" title=\"You can't delete a host that is in use by a schedule\" disabled=\"disabled\">Delete</button>\n\t                                        </div>\n\t                                        <button type=\"button\" th:if=\"${#lists.isEmpty(usedBy)}\" class=\"btn btn-danger\" data-toggle=\"modal\" data-target=\"#deleteHostModal\">Delete</button>\n\t                                    </div>\n\t                                </div>\n\t                                <div class=\"col-lg-6 align-right\">\n\t                                    <button type=\"button\" class=\"btn btn-primary\" id=\"saveHost\">Save</button>\n\t                                </div>\n\t                            </div>\n\n\t                        </div>\n\n\t                    </div>\n\n\t                </fieldset>\n\t            </form>\n\t\t\t</div>\n        </div>\n\n    </div>\n\n    <div id=\"deleteHostModal\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"deleteHostModal\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <h4 class=\"modal-title\">Delete this host</h4>\n                </div>\n                <div class=\"modal-body\" th:if=\"${#lists.isEmpty(usedBy)}\">\n                    <p>Are you sure?</p>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Cancel</button>\n                    <button type=\"button\" class=\"btn btn-danger\" data-dismiss=\"modal\" id=\"deleteHost\">Yes, delete this host</button>\n                </div>\n                <div class=\"modal-body\" th:unless=\"${#lists.isEmpty(usedBy)}\">\n                    <p>This host is being used by active schedules. Please check them before attempting to delete this host.</p>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <script type=\"text/javascript\" th:src=\"@{/js/jquery-2.1.4.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap-notify.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/davos.js}\"></script>\n\n    <script>\n        $('body').popover({ container: 'body', selector: '[data-toggle=\"popover\"]', trigger: 'hover' });\n    </script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "src/main/resources/templates/v2/edit-schedule.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"description\" content=\"An automated FTP client for your home server\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <title>Edit Schedule</title>\n\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n\t<link rel=\"manifest\" href=\"/manifest.json\" />\n\t<link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#5bbad5\" />\n\t<meta name=\"theme-color\" content=\"#ffffff\" />\n\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/bootstrap.min.css}\" media=\"screen,projection\" />\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/davos.css}\" media=\"screen\" />\n\n</head>\n\n<body>\n\n    <div class=\"navbar navbar-default navbar-fixed-top\" th:replace=\"fragments/header :: header\">\n    </div>\n\n    <div class=\"container\">\n\n        <div class=\"schedules-section\">\n\n            <div class=\"row\">\n                <div class=\"col-md-12\">\n                    <div class=\"page-header\">\n                        <h2>Edit Schedule</h2>\n                    </div>\n                </div>\n            </div>\n\n\n            <form class=\"form-horizontal\" id=\"schedule-form\" action=\"#\" th:object=\"${schedule}\">\n                <fieldset>\n\n                    <div class=\"row\">\n                        <div class=\"col-md-12\">\n                            <div class=\"page-header\">\n                                <h3>General</h3>\n                            </div>\n                        </div>\n                    </div>\n\n\t\t\t\t\t<div class=\"well\">\n\t                    <div class=\"row\">\n\n\t                        <div class=\"col-md-12\">\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"sch-name\" class=\"col-lg-2 control-label\">Name <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"What you want to refer to this schedule as\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"hidden\" class=\"form-control\" th:if=\"*{id}\" th:field=\"*{id}\" />\n\t                                    <input type=\"text\" class=\"form-control validate\" th:field=\"*{name}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"select\" class=\"col-lg-2 control-label\">Interval <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"How often this schedule should run\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <select class=\"form-control\" th:field=\"*{interval}\">\n\t                                        <option th:each=\"interval : ${allIntervals}\" th:value=\"${interval.minutes}\" th:text=\"${interval.text}\"></option>\n\t                                    </select>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"select\" class=\"col-lg-2 control-label\">Host <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Which host to connect to\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <select class=\"form-control\" th:field=\"*{host}\">\n\t                                        <option th:each=\"host: ${allHosts}\" th:value=\"${host.id}\" th:text=\"${host.name}\"></option>\n\t                                    </select>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"sch-name\" class=\"col-lg-2 control-label\">Host Directory <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Remote directory containing files to download\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control validate\" th:field=\"*{hostDirectory}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"sch-name\" class=\"col-lg-2 control-label\">Local Directory <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Where files should be downloaded\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control validate\" th:field=\"*{localDirectory}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Transfer Type <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"Should it download only files, or get nested folders too\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"radio\" th:each=\"type : ${allTransferTypes}\">\n\t                                        <label th:inline=\"text\">\n\t                                            <input type=\"radio\" th:field=\"*{transferType}\" th:value=\"${type}\" /> [[${type}]]\n\t                                        </label>\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Start Automatically <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"Should this schedule run automatically when davos starts\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"checkbox\">\n\t                                      <label></label>\n\t                                      <input type=\"checkbox\" th:field=\"*{automatic}\" />\n\t                                    </div>\n\t                                </div>\n\n\t                            </div>\n\n\t                        </div>\n\t                    </div>\n                    </div>\n\n                    <div class=\"row\">\n                        <div class=\"col-md-12\">\n                            <div class=\"page-header\">\n                                <h3>Filtering</h3>\n                            </div>\n                        </div>\n                    </div>\n\n\t\t\t\t\t<div class=\"well\">\n\t                    <div class=\"row\">\n\n\t                        <div class=\"col-md-12\">\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Mandatory <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"Only download new files when there are filters defined. If no filters are defined, nothing will be downloaded.\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"checkbox\">\n\t                                      <label></label>\n\t                                      <input type=\"checkbox\" th:field=\"*{filtersMandatory}\" />\n\t                                    </div>\n\t                                </div>\n\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Invert <span data-toggle=\"popover\" data-placement=\"top\" data-trigger=\"hover\" data-content=\"Only download new files that DO NOT match the filters defined for this schedule. This is the opposite of its default behaviour\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"checkbox\">\n\t                                      <label></label>\n\t                                      <input type=\"checkbox\" th:field=\"*{invertFilters}\" />\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label class=\"col-lg-2 control-label\">Filters <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Define which files to download based on file name\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"input-group\">\n\t                                        <input type=\"text\" id=\"newFilter\" class=\"form-control\" placeholder=\"Add a filter (? = single char wildcard. * = multi char wildcard)\" autocomplete=\"off\" />\n\t                                        <span class=\"input-group-btn\">\n\t                                            <button type=\"button\" class=\"btn btn-primary\" id=\"addFilter\">Add</button>\n\t                                          </span>\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <div class=\"col-lg-2\"></div>\n\t                                <div class=\"col-lg-10\" id=\"filters\">\n\t                                    <span th:each=\"filter : *{filters}\" th:inline=\"text\" class=\"label label-default filter-label\" th:attr=\"data-filter-value=${filter.value},data-filter-id=${filter.id}\">[[${filter.value}]] <span class=\"filter-close\">&times;</span></span>\n\t                                </div>\n\t                            </div>\n\n\t                        </div>\n\t                    </div>\n                    </div>\n\n                    <div class=\"row\">\n                        <div class=\"col-md-12\">\n                            <div class=\"page-header\">\n                                <h3>File Management</h3>\n                            </div>\n                        </div>\n                    </div>\n\n\t\t\t\t\t<div class=\"well\">\n\t                    <div class=\"row\">\n\t                        <div class=\"col-md-12\">\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"sch-move-file\" class=\"col-lg-2 control-label\">Delete from Host <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Delete the originating file/directory on the host FTP server once the download has completed\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <div class=\"checkbox\">\n\t                                      <label></label>\n\t                                      <input type=\"checkbox\" th:field=\"*{deleteHostFile}\" />\n\t                                    </div>\n\t                                </div>\n\t                            </div>\n\n\t                            <div class=\"form-group\">\n\t                                <label for=\"sch-move-file\" class=\"col-lg-2 control-label\">Move Downloaded File <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Move the file to the given directory once the download has completed\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t                                <div class=\"col-lg-10\">\n\t                                    <input type=\"text\" class=\"form-control\" th:field=\"*{moveFileTo}\" autocomplete=\"off\" />\n\t                                </div>\n\t                            </div>\n\n\t                        </div>\n                        </div>\n                    </div>\n\n                    <div class=\"row\">\n                        <div class=\"col-md-12\">\n                            <div class=\"page-header\">\n                                <h3>Downstream Actions</h3>\n                            </div>\n                        </div>\n                        <div class=\"col-md-12\">\n                            These provide a way for <em>davos</em> to inform other applications, such as file managers or 3rd party notifiers that the schedule has completed downloading a file. This is useful when part of a wider workflow. Each downstream\n                            action will be triggered after <em>each file has been downloaded</em>, in order for a more granular workflow.\n                        </div>\n                    </div>\n\n                    <div class=\"row\">\n\n                        <div class=\"col-md-12\">\n                            <div class=\"page-header\">\n                                <h4>Notifications</h4>\n                            </div>\n                        </div>\n\n                        <div class=\"col-md-12\" id=\"notifications\">\n\n                            <div class=\"well notification pushbullet\" th:each=\"notification : *{notifications.pushbullet}\" th:attr=\"data-notification-id=${notification.id}\">\n\n\t\t\t\t\t\t\t\t<h4>Pushbullet</h4>\n\n                                <div class=\"form-group\">\n                                    <label for=\"sch-pushbullet\" class=\"col-lg-2 control-label\">Access Token</label>\n                                    <div class=\"col-lg-10\">\n                                        <input type=\"text\" class=\"form-control apiKey\" th:value=\"${notification.apiKey}\" placeholder=\"API Key\" autocomplete=\"off\" />\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <div class=\"col-lg-12 align-right\">\n                                        <button type=\"button\" class=\"btn btn-danger btn-sm remove-notification\">Remove</button>\n                                    </div>\n                                </div>\n\n                            </div>\n\n\t\t\t\t\t\t\t<div class=\"well notification sns\" th:each=\"sns : *{notifications.sns}\" th:attr=\"data-notification-id=${sns.id}\">\n\n\t\t\t\t\t\t\t\t<h4>Amazon SNS</h4>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t    <div class=\"form-group\">\n\t\t\t\t\t\t\t        <label class=\"col-lg-2 control-label\">Topic Arn</label>\n\t\t\t\t\t\t\t        <div class=\"col-lg-10\">\n\t\t\t\t\t\t\t            <input type=\"text\" class=\"form-control topicArn\" th:value=\"${sns.topicArn}\" placeholder=\"e.g. arn:aws:sns:*:123456789012:my_notification_topic\" autocomplete=\"off\" />\n\t\t\t\t\t\t\t        </div>\n\t\t\t\t\t\t\t    </div>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t    <div class=\"form-group\">\n\t\t\t\t\t\t\t        <label class=\"col-lg-2 control-label\">Region</label>\n\t\t\t\t\t\t\t        <div class=\"col-lg-10\">\n\t\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control region\" th:value=\"${sns.region}\" placeholder=\"e.g. eu-west-1\" autocomplete=\"off\" />\n\t\t\t\t\t\t\t        </div>\n\t\t\t\t\t\t\t    </div>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t    <div class=\"form-group\">\n\t\t\t\t\t\t\t        <label class=\"col-lg-2 control-label\">Access Key <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Your specified IAM User's access key\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t\t\t\t\t\t\t        <div class=\"col-lg-10\">\n\t\t\t\t\t\t\t            <input type=\"text\" class=\"form-control accessKey\" th:value=\"${sns.accessKey}\" autocomplete=\"off\" />\n\t\t\t\t\t\t\t        </div>\n\t\t\t\t\t\t\t    </div>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t    <div class=\"form-group\">\n\t\t\t\t\t\t\t        <label class=\"col-lg-2 control-label\">Secret Access Key <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Your specified IAM User's secret access key\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n\t\t\t\t\t\t\t        <div class=\"col-lg-10\">\n\t\t\t\t\t\t\t            <input type=\"text\" class=\"form-control secretAccessKey\" th:value=\"${sns.secretAccessKey}\" autocomplete=\"off\" />\n\t\t\t\t\t\t\t        </div>\n\t\t\t\t\t\t\t    </div>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t    <div class=\"form-group\">\n\t\t\t\t\t\t\t        <div class=\"col-lg-12 align-right\">\n\t\t\t\t\t\t\t            <button type=\"button\" class=\"btn btn-danger btn-sm remove-notification\">Remove</button>\n\t\t\t\t\t\t\t        </div>\n\t\t\t\t\t\t\t    </div>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</div>\n\n                        </div>\n\n                        <div class=\"col-md-12 align-right\">\n\n                            <div class=\"btn-group\" role=\"group\">\n                                <button type=\"button\" class=\"btn btn-default btn-sm dropdown-toggle\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                                    Add\n                                    <span class=\"caret\"></span>\n                                </button>\n                                <ul class=\"dropdown-menu\">\n                                    <li><a id=\"newPushbullet\">Pushbullet</a></li>\n                                    <li><a id=\"newSns\">Amazon SNS</a></li>\n                                </ul>\n                            </div>\n                        </div>\n\n                    </div>\n                    <div class=\"row\">\n\n                        <div class=\"col-md-12\">\n                            <div class=\"page-header\">\n                                <h4>API Calls</h4>\n                            </div>\n                        </div>\n\n                        <div class=\"col-md-12\" id=\"apis\">\n\n                            <div class=\"well api\" th:each=\"api : *{apis}\" th:attr=\"data-api-id=${api.id}\">\n\n\t\t\t\t\t\t\t\t<h4>API Call</h4>\n\t\n                                <div class=\"form-group\">\n                                    <label for=\"sch-api-url\" class=\"col-lg-2 control-label\">URL</label>\n                                    <div class=\"col-lg-10\">\n                                        <input type=\"text\" class=\"form-control url\" th:value=\"${api.url}\" autocomplete=\"off\" />\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"sch-api-method\" class=\"col-lg-2 control-label\">Method</label>\n                                    <div class=\"col-lg-10\">\n                                        <select class=\"form-control method\">\n                                            <option th:each=\"method : ${allMethods}\" th:value=\"${method}\" th:text=\"${method}\" th:selected=\"${api.method == method}\"></option>\n                                        </select>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"sch-api-content-type\" class=\"col-lg-2 control-label\">Content-Type</label>\n                                    <div class=\"col-lg-10\">\n                                        <input type=\"text\" class=\"form-control contentType\" th:value=\"${api.contentType}\" placeholder=\"e.g. application/json\" autocomplete=\"off\" />\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <label for=\"sch-api-body\" class=\"col-lg-2 control-label\">Message Body <span data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Use $filename to reference the downloaded file\" class=\"help glyphicon glyphicon-question-sign\"></span></label>\n                                    <div class=\"col-lg-10\">\n                                        <textarea class=\"form-control body\" rows=\"3\" th:text=\"${api.body}\"></textarea>\n                                        <span class=\"help-block\"></span>\n                                    </div>\n                                </div>\n\n                                <div class=\"form-group\">\n                                    <div class=\"col-lg-12 align-right\">\n                                        <button type=\"button\" class=\"btn btn-danger btn-sm remove-api\">Remove</button>\n                                    </div>\n                                </div>\n\n                            </div>\n\n                        </div>\n\n                        <div class=\"col-md-12 align-right\">\n\n                            <div class=\"btn-group\" role=\"group\">\n                                <button type=\"button\" class=\"btn btn-default btn-sm dropdown-toggle\" data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                                    Add\n                                    <span class=\"caret\"></span>\n                                </button>\n                                <ul class=\"dropdown-menu\">\n                                    <li><a id=\"newAPI\">API Call</a></li>\n                                </ul>\n                            </div>\n                        </div>\n\n                    </div>\n\n                    <div class=\"row\">\n\n                        <div class=\"col-md-12\">\n\n                            <div class=\"form-group last\">\n                                <div class=\"col-lg-6\">\n                                    <button th:if=\"${schedule.id}\" type=\"button\" class=\"btn btn-danger\" data-toggle=\"modal\" data-target=\"#deleteScheduleModal\">Delete</button>\n                                </div>\n                                <div class=\"col-lg-6 align-right\">\n                                    <button type=\"button\" class=\"btn btn-primary\" id=\"saveSchedule\">Save</button>\n                                </div>\n                            </div>\n\n                        </div>\n\n                    </div>\n\n                </fieldset>\n            </form>\n\n        </div>\n\n    </div>\n\n    <div th:if=\"${#lists.isEmpty(allHosts)}\" id=\"noHostsModal\" class=\"modal fade\" data-keyboard=\"false\" data-backdrop=\"static\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"noHostsModal\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <h4 class=\"modal-title\">No hosts found</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <p>You can't create a Schedule without first creating a Host for it to use. Please create a host.</p>\n                    <a th:href=\"@{/hosts/new}\" class=\"btn btn-primary\" id=\"createHost\">Create Host</a>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div id=\"deleteScheduleModal\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"deleteScheduleModal\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content\">\n                <div class=\"modal-header\">\n                    <h4 class=\"modal-title\">Delete this schedule</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <p>Are you sure?</p>\n                    <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Cancel</button>\n                    <button type=\"button\" class=\"btn btn-danger\" data-dismiss=\"modal\" id=\"deleteSchedule\">Yes, delete this schedule</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    <script type=\"text/javascript\" th:src=\"@{/js/jquery-2.1.4.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap-notify.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/davos.js}\"></script>\n\n    <script th:if=\"${#lists.isEmpty(allHosts)}\">\n        $(window).load(function(){\n            $('#noHostsModal').modal('show');\n        });\n    </script>\n\n    <script>\n        $('body').popover({ container: 'body', selector: '[data-toggle=\"popover\"]', trigger: 'hover' });\n    </script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "src/main/resources/templates/v2/hosts.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"description\" content=\"An automated FTP client for your home server\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <title>Hosts</title>\n\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n\t<link rel=\"manifest\" href=\"/manifest.json\" />\n\t<link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#5bbad5\" />\n\t<meta name=\"theme-color\" content=\"#ffffff\" />\n\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/bootstrap.min.css}\" media=\"screen,projection\" />\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/davos.css}\" media=\"screen\" />\n\n</head>\n\n<body>\n\n    <div class=\"navbar navbar-default navbar-fixed-top\" th:replace=\"fragments/header :: header\">\n    </div>\n    \n    <div class=\"container\">\n\n        <div class=\"schedules-section\">\n\n            <div class=\"row\">\n                <div class=\"col-md-12\">\n                    <div class=\"page-header\">\n                        <h2>Hosts</h2>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"row\">\n\n                <div class=\"col-md-12\">\n\n                    <table class=\"table table-striped\">\n                        <thead>\n                            <tr>\n                                <th>Name</th>\n                                <th>Protocol</th>\n                                <th>Address</th>\n                                <th>Username</th>\n                                <th></th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <tr th:each=\"host : ${allHosts}\">\n                                <td th:text=\"${host.name}\"></td>\n                                <td th:text=\"${host.protocol}\"></td>\n                                <td th:text=\"${host.address}\"></td>\n                                <td th:text=\"${host.username}\"></td>\n                                <td><a class=\"link\" th:href=\"@{/hosts/} + ${host.id}\"><span class=\"glyphicon glyphicon-wrench edit-host\" data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Edit\"></span></a></td>\n                            </tr>\n                        </tbody>\n                    </table>\n\n                </div>\n\n            </div>\n\n        </div>\n\n    </div>\n\n    <script src=\"js/jquery-2.1.4.min.js\"></script>\n    <script src=\"js/bootstrap.min.js\"></script>\n\n    <script>\n        $(document).ready(function(){\n            $('[data-toggle=\"popover\"]').popover();\n        });\n    </script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "src/main/resources/templates/v2/schedules.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"description\" content=\"An automated FTP client for your home server\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <title>Schedules</title>\n\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n\t<link rel=\"manifest\" href=\"/manifest.json\" />\n\t<link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#5bbad5\" />\n\t<meta name=\"theme-color\" content=\"#ffffff\" />\n\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/bootstrap.min.css}\" media=\"screen,projection\" />\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/davos.css}\" media=\"screen\" />\n\n</head>\n\n<body>\n\n    <div class=\"navbar navbar-default navbar-fixed-top\" th:replace=\"fragments/header :: header\"></div>\n    <div class=\"container\" th:replace=\"fragments/header :: announcement\"></div>\n\n    <div class=\"container\">\n\n        <div class=\"schedules-section\">\n\n            <div class=\"row\">\n                <div class=\"col-md-12\">\n                    <div class=\"page-header\">\n                        <h2>Schedules</h2>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"row\" th:if=\"${#lists.isEmpty(schedules)}\">\n                <div class=\"col-md-12\">\n                    <div class=\"well\" id=\"noSchedules\">\n                        No schedules!\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"row\" th:each=\"schedule : ${schedules}\">\n\n                <div class=\"col-md-12\">\n\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-heading\" th:inline=\"text\">[[${schedule.name}]] &nbsp;&nbsp;\n                            <span th:classappend=\"${not schedule.running} ? 'hide'\">\n                                <span class=\"label label-default label-title\">Running</span>&nbsp;&nbsp;\n                                <span class=\"glyphicon glyphicon-stop stop-schedule\" data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Stop\" th:attr=\"data-schedule-id=${schedule.id},data-schedule-name=${schedule.name}\"></span>\n                            </span>\n                            <span th:classappend=\"${schedule.running} ? 'hide'\" class=\"glyphicon glyphicon-play start-schedule\" data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Start\" th:attr=\"data-schedule-id=${schedule.id},data-schedule-name=${schedule.name}\"></span>\n                            &nbsp;&nbsp;<a class=\"link\" th:href=\"@{/schedules/} + ${schedule.id}\"><span class=\"glyphicon glyphicon-wrench\" data-toggle=\"popover\" data-trigger=\"hover\" data-placement=\"top\" data-content=\"Edit\"></span></a>\n                            &nbsp;&nbsp;<span class=\"glyphicon glyphicon-time\" data-toggle=\"modal\" th:attr=\"data-target='#lastScanned' + ${schedule.id}\"></span>\n                        </div>\n                        <div class=\"panel-body\">\n                            <div class=\"downloads\" th:attr=\"data-schedule-id=${schedule.id}\">\n                            \n                            \t<table class=\"table\" th:if=\"${not #lists.isEmpty(schedule.transfers)}\">\n\t\t\t\t\t\t\t\t    <thead>\n\t\t\t\t\t\t\t\t        <tr>\n\t\t\t\t\t\t\t\t            <th>File</th>\n\t\t\t\t\t\t\t\t            <th>Size</th>\n\t\t\t\t\t\t\t\t            <th>Status</th>\n\t\t\t\t\t\t\t\t            <th>Progress</th>\n\t\t\t\t\t\t\t\t            <th>Speed</th>\n\t\t\t\t\t\t\t\t        </tr>\n\t\t\t\t\t\t\t\t    </thead>\n\t\t\t\t\t\t\t\t    <tbody>\n\t\t\t\t\t\t\t\t        <tr th:each=\"transfer : ${schedule.transfers}\">\n\t\t\t\t\t\t\t\t            <td th:text=\"${transfer.fileName}\"></td>\n\t\t\t\t\t\t\t\t            <td th:unless=\"${transfer.directory}\" th:text=\"${(transfer.fileSize / 1000000) + 'MB'}\"></td>\n\t\t\t\t\t\t\t\t            <td th:if=\"${transfer.directory}\"></td>\n\t\t\t\t\t\t\t\t            <td>\n\t\t\t\t\t\t\t\t                <span th:text=\"${transfer.status}\"></span>\n\t\t\t\t\t\t\t\t            </td>\n\t\t\t\t\t\t\t\t            <td th:if=\"${transfer.progress}\" class=\"transfer-progress\">\n\t\t\t\t\t\t\t\t                <div class=\"progress\">\n\t\t\t\t\t\t\t\t                    <div class=\"progress-bar\" th:style=\"'width:' + ${transfer.progress.percentageComplete} + '%'\"></div>\n\t\t\t\t\t\t\t\t                </div>\n\t\t\t\t\t\t\t\t            </td>\n\t\t\t\t\t\t\t\t            <td th:unless=\"${transfer.progress}\" class=\"transfer-progress\">\n\t\t\t\t\t\t\t\t                <div class=\"progress\" th:if=\"${transfer.status == 'PENDING'}\">\n\t\t\t\t\t\t\t\t                    <div class=\"progress-bar\" style=\"width: 0%;\"></div>\n\t\t\t\t\t\t\t\t                </div>\n\t\t\t\t\t\t\t\t                <div class=\"progress\" th:if=\"${transfer.status == 'SKIPPED'}\">\n\t\t\t\t\t\t\t\t                    <div class=\"progress-bar progress-bar-warning\" style=\"width: 100%;\"></div>\n\t\t\t\t\t\t\t\t                </div>\n\t\t\t\t\t\t\t\t            </td>\n\t\t\t\t\t\t\t\t            <td th:if=\"${transfer.progress}\" class=\"transfer-speed\">\n\t\t\t\t\t\t\t\t                <span th:if=\"${transfer.progress.percentageComplete &lt; 100}\" th:text=\"${#numbers.formatDecimal(transfer.progress.transferSpeed, 0, 2, 'POINT') + 'MB/s'}\"></span>\n\t\t\t\t\t\t\t\t            </td>\n\t\t\t\t\t\t\t\t            <td th:unless=\"${transfer.progress}\"></td>\n\t\t\t\t\t\t\t\t        </tr>\n\t\t\t\t\t\t\t\t    </tbody>\n\t\t\t\t\t\t\t\t</table>\n                            \n                            </div>\n                        </div>\n                    </div>\n\n                </div>\n\n                <div th:attr=\"id='lastScanned' + ${schedule.id},aria-labelledby='lastScanned' + ${schedule.id}\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n                    <div class=\"modal-dialog modal-lg\" role=\"document\">\n                        <div class=\"modal-content\">\n                            <div class=\"modal-header\">\n                            \t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n                                <h4 class=\"modal-title\">Files on the host during the last scan</h4>\n                            </div>\n                            <div class=\"modal-body\">\n                            \t<span th:if=\"${schedule.lastRunTime}\" th:inline=\"text\">Last Run: <strong>[[${schedule.lastRunTime}]]</strong></span>\n                            \t<span th:unless=\"${schedule.lastRunTime}\">Last Run: <strong>Never</strong></span>\n                            \t\n                            \t<button type=\"button\" style=\"float:right\" th:attr=\"data-schedule-id=${schedule.id}\" class=\"clearLastScanned btn btn-sm\" aria-hidden=\"true\">Clear</button>\n                                <table class=\"table table-striped\">\n                                    <thead>\n                                        <tr>\n                                            <th>Name</th>\n                                        </tr>\n                                    </thead>\n                                    <tbody>\n                                        <tr th:each=\"file : ${schedule.lastScannedFiles}\">\n                                            <td th:text=\"${file}\"></td>\n                                        </tr>\n                                    </tbody>\n                                </table>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\n            </div>\n\n        </div>\n\n    </div>\n\n    <script type=\"text/javascript\" th:src=\"@{/js/jquery-2.1.4.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap-notify.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/davos.js}\"></script>\n\n    <script>\n        $('body').popover({ container: 'body', selector: '[data-toggle=\"popover\"]', trigger: 'hover' });\n    </script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "src/main/resources/templates/v2/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"description\" content=\"An automated FTP client for your home server\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <title>App Settings</title>\n\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n\t<link rel=\"manifest\" href=\"/manifest.json\" />\n\t<link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#5bbad5\" />\n\t<meta name=\"theme-color\" content=\"#ffffff\" />\n\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/bootstrap.min.css}\" media=\"screen,projection\" />\n    <link type=\"text/css\" rel=\"stylesheet\" th:href=\"@{/css/davos.css}\" media=\"screen\" />\n\n\n</head>\n\n<body>\n\n    <div class=\"navbar navbar-default navbar-fixed-top\" th:replace=\"fragments/header :: header\"></div>\n\n    <div class=\"container\">\n\n        <div class=\"row\">\n            <div class=\"col-md-12\">\n                <div class=\"page-header\">\n                    <h2>App Settings</h2>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"row\">\n            <form class=\"form-horizontal\" onsubmit=\"return false;\" th:object=\"${settings}\">\n                <fieldset>\n\n\t\t\t\t\t<div class=\"col-md-12\">\n\n\t                    <div class=\"form-group\">\n\t                        <label for=\"logging-level\" class=\"col-md-2 control-label\">Logging Level</label>\n\t                        <div class=\"col-md-2\">\n\t                            <select class=\"form-control\" th:field=\"*{logLevel}\">\n\t                                <option th:each=\"level : ${allLogLevels}\" th:value=\"${level}\" th:text=\"${level}\">DEBUG</option>\n\t                            </select>\n\t                        </div>\n\t                    </div>\n\n                    </div>\n\n                </fieldset>\n            </form>\n        </div>\n\n\n        <div class=\"row\">\n            <div class=\"col-md-12\">\n                <div class=\"page-header\">\n                    <h2>About</h2>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"row\">\n            <form class=\"form-horizontal\" onsubmit=\"return false;\">\n                <fieldset>\n\n\t\t\t\t\t<div class=\"col-md-12\">\n\n\t                    <div class=\"form-group\">\n\t                        <label class=\"col-md-2 control-label\" style=\"padding-top: 0px\">Version</label>\n\t                        <div class=\"col-md-10\">\n\t                            <span th:text=\"${currentVersion}\"></span>\n\t                        </div>\n\t                    </div>\n\n                    </div>\n\n                </fieldset>\n            </form>\n        </div>\n\n    </div>\n\n    <script type=\"text/javascript\" th:src=\"@{/js/jquery-2.1.4.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/bootstrap-notify.min.js}\"></script>\n    <script type=\"text/javascript\" th:src=\"@{/js/davos.js}\"></script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/VersionTest.java",
    "content": "package io.linuxserver.davos;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.Version;\n\npublic class VersionTest {\n\n    @Test\n    public void shouldSetVersionBitsFromString() {\n        \n        Version version = new Version(\"2.1.3\");\n        \n        assertThat(version.getMajor()).isEqualTo(2);\n        assertThat(version.getMinor()).isEqualTo(1);\n        assertThat(version.getPatch()).isEqualTo(3);\n    }\n    \n    @Test\n    public void shouldSetVersionBits() {\n        \n        Version version = new Version(2, 1, 3);\n        \n        assertThat(version.getMajor()).isEqualTo(2);\n        assertThat(version.getMinor()).isEqualTo(1);\n        assertThat(version.getPatch()).isEqualTo(3);\n    }\n    \n    @Test\n    public void shouldCompareToOthers() {\n        \n        assertThat(new Version(\"0.0.2\").isNewerThan(new Version(\"0.0.1\"))).isTrue();\n        assertThat(new Version(\"0.1.0\").isNewerThan(new Version(\"0.0.2\"))).isTrue();\n        assertThat(new Version(\"1.0.0\").isNewerThan(new Version(\"0.2.0\"))).isTrue();\n        assertThat(new Version(\"1.1.0\").isNewerThan(new Version(\"1.0.0\"))).isTrue();\n        assertThat(new Version(\"1.1.1\").isNewerThan(new Version(\"1.0.1\"))).isTrue();\n        \n        assertThat(new Version(\"1.1.1\").isNewerThan(new Version(\"1.2.1\"))).isFalse();\n        assertThat(new Version(\"0.1.1\").isNewerThan(new Version(\"0.2.1\"))).isFalse();\n        assertThat(new Version(\"0.0.0\").isNewerThan(new Version(\"0.0.1\"))).isFalse();\n        \n        assertThat(new Version(\"2.1.2\").isNewerThan(new Version(\"2.2.0\"))).isFalse();\n        assertThat(new Version(\"2.2.0\").isNewerThan(new Version(\"2.2.1\"))).isFalse();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/delegation/services/ScheduleServiceImplTest.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\n\nimport io.linuxserver.davos.converters.ScheduleConverter;\nimport io.linuxserver.davos.persistence.dao.HostDAO;\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.persistence.model.ScannedFileModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.schedule.ScheduleExecutor;\nimport io.linuxserver.davos.web.Schedule;\n\npublic class ScheduleServiceImplTest {\n\n    @InjectMocks\n    private ScheduleService scheduleService = new ScheduleServiceImpl();\n        \n    @Mock\n    private ScheduleDAO mockScheduleDAO;\n    \n    @Spy\n    private ScheduleConverter scheduleConverter;\n    \n    @Mock\n    private ScheduleExecutor mockExecutor;\n    \n    @Mock\n    private HostDAO mockHostDAO;\n    \n    @Captor\n    public ArgumentCaptor<ScheduleModel> scheduleCaptor;\n    \n    @Before\n    public void before() {\n        initMocks(this);\n    }\n    \n    @Test\n    public void shouldStartScheduleFromExecutor() {\n        \n        scheduleService.startSchedule(1L);\n        verify(mockExecutor).startSchedule(1L);\n    }\n    \n    @Test\n    public void shouldStopScheduleFromExecutor() {\n        \n        scheduleService.stopSchedule(1L);\n        verify(mockExecutor).stopSchedule(1L);\n    }\n    \n    @Test\n    public void shouldDeleteScheduleWhenNotRunning() {\n        \n        scheduleService.deleteSchedule(1L);\n        \n        verify(mockExecutor, never()).stopSchedule(1L);\n        verify(mockScheduleDAO).deleteSchedule(1L);\n    }\n    \n    @Test\n    public void shouldCheckIfScheduleIsRunningAndStopIfSoBeforeDeleting() {\n        \n        when(mockExecutor.isScheduleRunning(1L)).thenReturn(true);\n        \n        scheduleService.deleteSchedule(1L);\n        \n        verify(mockExecutor).stopSchedule(1L);\n        verify(mockScheduleDAO).deleteSchedule(1L);\n    }\n    \n    @Test\n    public void shouldGetAllSchedulesAndConvert() {\n        \n        List<ScheduleModel> models = new ArrayList<ScheduleModel>();\n        \n        ScheduleModel model1 = new ScheduleModel();\n        model1.id = 1L;\n        model1.name = \"Test 1\";\n        model1.host = new HostModel();\n        \n        ScheduleModel model2 = new ScheduleModel();\n        model2.id = 2L;\n        model2.name = \"Test 2\";\n        model2.host = new HostModel();\n        \n        models.add(model1);\n        models.add(model2);\n        \n        when(mockScheduleDAO.getAll()).thenReturn(models);\n        \n        List<Schedule> schedules = scheduleService.fetchAllSchedules();\n        \n        assertThat(schedules).hasSize(2);\n        \n        assertThat(schedules.get(0).getId()).isEqualTo(1L);\n        assertThat(schedules.get(0).getName()).isEqualTo(\"Test 1\");\n          \n        assertThat(schedules.get(1).getId()).isEqualTo(2L);\n        assertThat(schedules.get(1).getName()).isEqualTo(\"Test 2\");\n    }\n    \n    @Test\n    public void shouldReturnOneSchedule() {\n        \n        ScheduleModel model1 = new ScheduleModel();\n        model1.id = 1L;\n        model1.name = \"Test 1\";\n        model1.host = new HostModel();\n        \n        when(mockScheduleDAO.fetchSchedule(1L)).thenReturn(model1);        \n        \n        Schedule schedule = scheduleService.fetchSchedule(1L);\n        \n        assertThat(schedule.getId()).isEqualTo(1L);\n        assertThat(schedule.getName()).isEqualTo(\"Test 1\");\n    }\n    \n    @Test(expected = IllegalArgumentException.class)\n    public void shouldGetHostFromDatabaseToCheckItExistsWhenCreating() {\n        \n        Schedule schedule = new Schedule();\n        schedule.setHost(null);\n        \n        scheduleService.createSchedule(schedule);\n    }\n    \n    @Test\n    public void shouldOverlayHostFromDatabaseInScheduleWhenCreating() {\n        \n        ScheduleModel model1 = new ScheduleModel();\n        model1.id = 1L;\n        model1.name = \"Test 1\";\n        model1.host = new HostModel();\n        \n        when(mockScheduleDAO.updateConfig(any(ScheduleModel.class))).thenReturn(model1);\n        \n        HostModel hostModell = new HostModel();\n        when(mockHostDAO.fetchHost(2L)).thenReturn(hostModell);\n        \n        Schedule schedule = new Schedule();\n        schedule.setHost(2L);\n        scheduleService.createSchedule(schedule);\n        \n        verify(mockScheduleDAO).updateConfig(scheduleCaptor.capture());\n        \n        assertThat(scheduleCaptor.getValue().host).isEqualTo(hostModell);\n    }\n    \n    @Test\n    public void shouldReturnConvertedScheduleOnceCreated() {\n        \n        ScheduleModel model1 = new ScheduleModel();\n        model1.id = 1L;\n        model1.name = \"Test 1\";\n        model1.host = new HostModel();\n        \n        when(mockScheduleDAO.updateConfig(any(ScheduleModel.class))).thenReturn(model1);\n        \n        HostModel hostModell = new HostModel();\n        when(mockHostDAO.fetchHost(2L)).thenReturn(hostModell);\n        \n        Schedule schedule = new Schedule();\n        schedule.setHost(2L);\n        Schedule createdSchedule = scheduleService.createSchedule(schedule);\n                \n        assertThat(createdSchedule.getId()).isEqualTo(1L);\n        assertThat(createdSchedule.getName()).isEqualTo(\"Test 1\");\n    }\n    \n    @Test\n    public void shouldOverlayHostFromDatabaseInScheduleWhenUpdating() {\n        \n        setUpScheduleMocks();\n        \n        HostModel hostModel1 = new HostModel();\n        when(mockHostDAO.fetchHost(2L)).thenReturn(hostModel1);\n        \n        Schedule schedule = new Schedule();\n        schedule.setId(1L);\n        schedule.setHost(2L);\n        scheduleService.updateSchedule(schedule);\n        \n        verify(mockScheduleDAO).updateConfig(scheduleCaptor.capture());\n        \n        assertThat(scheduleCaptor.getValue().host).isEqualTo(hostModel1);\n    }\n    \n    @Test\n    public void shouldReturnConvertedScheduleOnceUpdated() {\n        \n        setUpScheduleMocks();\n        \n        Schedule schedule = new Schedule();\n        schedule.setId(1L);\n        schedule.setHost(2L);\n        Schedule createdSchedule = scheduleService.updateSchedule(schedule);\n                \n        assertThat(createdSchedule.getId()).isEqualTo(1L);\n        assertThat(createdSchedule.getName()).isEqualTo(\"Test 1\");\n    }\n    \n    @Test\n    public void shouldOverlayLastRunTimeOfExistingScheduleToNewOne() {\n        \n        setUpScheduleMocks();\n        \n        Schedule schedule = new Schedule();\n        schedule.setId(1L);\n        schedule.setHost(2L);\n        scheduleService.updateSchedule(schedule);\n        \n        verify(mockScheduleDAO).updateConfig(scheduleCaptor.capture());\n        \n        assertThat(scheduleCaptor.getValue().getLastRunTime()).isEqualTo(12345L);\n    }\n    \n    @Test(expected = IllegalArgumentException.class)\n    public void shouldThrowExceptionIfScheduleHasNoIdWhenUpdating() {\n        \n        Schedule schedule = new Schedule();\n        schedule.setHost(2L);\n        \n        HostModel hostModell = new HostModel();\n        when(mockHostDAO.fetchHost(2L)).thenReturn(hostModell);\n        \n        scheduleService.updateSchedule(schedule);\n    }\n    \n    @Test\n    public void shouldClearScannedFiles() {\n        \n        ScheduleModel model = new ScheduleModel();\n        model.scannedFiles = new ArrayList<ScannedFileModel>();\n        model.scannedFiles.add(new ScannedFileModel());\n        \n        when(mockScheduleDAO.fetchSchedule(1L)).thenReturn(model);\n        \n        assertThat(model.scannedFiles).hasSize(1);\n        \n        scheduleService.clearScannedFilesFromSchedule(1L);\n        \n        assertThat(model.scannedFiles).isEmpty();\n        verify(mockScheduleDAO).updateConfig(model);\n    }\n    \n    private void setUpScheduleMocks() {\n        \n        ScheduleModel model1 = new ScheduleModel();\n        model1.id = 1L;\n        model1.name = \"Test 1\";\n        model1.setLastRunTime(12345L);\n        model1.host = new HostModel();\n        \n        when(mockScheduleDAO.updateConfig(any(ScheduleModel.class))).thenReturn(model1);\n        when(mockScheduleDAO.fetchSchedule(1L)).thenReturn(model1);\n        \n        when(mockHostDAO.fetchHost(2L)).thenReturn(new HostModel());\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/delegation/services/SettingsServiceImplTest.java",
    "content": "package io.linuxserver.davos.delegation.services;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.client.RestTemplate;\n\nimport io.linuxserver.davos.Version;\n\npublic class SettingsServiceImplTest {\n\n    @Mock\n    private RestTemplate mockRestTemplate;\n\n    @Captor\n    private ArgumentCaptor<HttpEntity<String>> entityCaptor;\n\n    @InjectMocks\n    private SettingsService settingsService = new SettingsServiceImpl();\n\n    @Before\n    public void before() {\n        initMocks(this);\n\n        when(mockRestTemplate.exchange(eq(\"https://raw.githubusercontent.com/linuxserver/davos/LatestRelease/version.txt\"),\n                eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class)))\n                        .thenReturn(new ResponseEntity<String>(\"2.2.2\", HttpStatus.OK));\n    }\n\n    @Test\n    public void checkVersionShouldCallGitHub() {\n\n        settingsService.retrieveRemoteVersion();\n\n        verify(mockRestTemplate).exchange(eq(\"https://raw.githubusercontent.com/linuxserver/davos/LatestRelease/version.txt\"),\n                eq(HttpMethod.GET), entityCaptor.capture(), eq(String.class));\n    }\n\n    @Test\n    public void checkVersionShouldReturnVersionFromGithub() {\n\n        Version version = settingsService.retrieveRemoteVersion();\n\n        assertThat(version.toString()).isEqualTo(\"2.2.2\");\n    }\n\n    @Test\n    public void ifRestTemplateFailsThenReturnEmptyVersion() {\n\n        when(mockRestTemplate.exchange(eq(\"https://raw.githubusercontent.com/linuxserver/davos/LatestRelease/version.txt\"),\n                eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))).thenThrow(new RestClientException(\"\"));\n\n        Version version = settingsService.retrieveRemoteVersion();\n\n        assertThat(version.toString()).isEqualTo(\"0.0.0\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/persistence/dao/DefaultScheduleDAOTest.java",
    "content": "package io.linuxserver.davos.persistence.dao;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\n\nimport io.linuxserver.davos.persistence.model.ScannedFileModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.persistence.repository.ScheduleRepository;\n\npublic class DefaultScheduleDAOTest {\n\n    @Mock\n    private ScheduleRepository mockRepository;\n\n    @InjectMocks\n    private DefaultScheduleDAO configDAO = new DefaultScheduleDAO();\n\n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n\n    @Test\n    public void updatingScannedFilesShouldWork() {\n        \n        ScheduleModel model = new ScheduleModel();\n        model.id = 1L;\n        model.scannedFiles.add(toScannedFileModel(\"oldFile\", model));\n        model.scannedFiles.add(toScannedFileModel(\"another\", model));\n        model.scannedFiles.add(toScannedFileModel(\"blah\", model));\n        \n        List<String> files = Arrays.asList(\"file1\", \"file2\");\n        \n        when(mockRepository.findOne(1L)).thenReturn(model);\n        \n        configDAO.updateScannedFilesOnSchedule(1L, files);\n        \n        assertThat(model.scannedFiles).hasSize(2);\n        assertThat(model.scannedFiles.get(0).file).isEqualTo(\"file1\");\n        assertThat(model.scannedFiles.get(0).schedule).isEqualTo(model);\n        \n        assertThat(model.scannedFiles.get(1).file).isEqualTo(\"file2\");\n        assertThat(model.scannedFiles.get(1).schedule).isEqualTo(model);\n    }\n\n    private ScannedFileModel toScannedFileModel(String fileName, ScheduleModel model) {\n\n        ScannedFileModel scannedFileModel = new ScannedFileModel();\n        scannedFileModel.file = fileName;\n        scannedFileModel.schedule = model;\n\n        return scannedFileModel;\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/ScheduleConfigurationFactoryTest.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.ArrayList;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.persistence.model.ActionModel;\nimport io.linuxserver.davos.persistence.model.FilterModel;\nimport io.linuxserver.davos.persistence.model.HostModel;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\nimport io.linuxserver.davos.schedule.workflow.actions.HttpAPICallAction;\nimport io.linuxserver.davos.schedule.workflow.actions.MoveFileAction;\nimport io.linuxserver.davos.schedule.workflow.actions.PushbulletNotifyAction;\nimport io.linuxserver.davos.schedule.workflow.actions.SNSNotifyAction;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ScheduleConfigurationFactoryTest {\n\n    @Test\n    public void shouldConvertAllMainFields() {\n\n        ScheduleModel model = new ScheduleModel();\n\n        model.host = new HostModel();\n        model.host.protocol = TransferProtocol.FTP;\n        model.host.address = \"hostname\";\n        model.host.password = \"password\";\n        model.host.port = 8;\n        model.host.username = \"username\";\n        model.setFiltersMandatory(true);\n        model.localFilePath = \"local/\";\n        model.name = \"schedulename\";\n        model.remoteFilePath = \"thing/\";\n        model.setStartAutomatically(true);\n        model.transferType = FileTransferType.FILE;\n\n        ScheduleConfiguration config = ScheduleConfigurationFactory.createConfig(model);\n\n        assertThat(config.getConnectionType()).isEqualTo(model.host.protocol);\n        assertThat(config.getHostName()).isEqualTo(model.host.address);\n        assertThat(config.getLocalFilePath()).isEqualTo(model.localFilePath);\n        assertThat(config.getScheduleName()).isEqualTo(model.name);\n        assertThat(config.getCredentials().getPassword()).isEqualTo(model.host.password);\n        assertThat(config.getCredentials().getIdentity()).isNull();\n        assertThat(config.getPort()).isEqualTo(model.host.port);\n        assertThat(config.getRemoteFilePath()).isEqualTo(model.remoteFilePath);\n        assertThat(config.getTransferType()).isEqualTo(model.transferType);\n        assertThat(config.getCredentials().getUsername()).isEqualTo(model.host.username);\n        assertThat(config.isFiltersMandatory()).isTrue();\n    }\n    \n    @Test\n    public void shouldUseCorrectCredentialsIfIdentityPresent() {\n\n        ScheduleModel model = new ScheduleModel();\n\n        model.host = new HostModel();\n        model.host.protocol = TransferProtocol.FTP;\n        model.host.address = \"hostname\";\n        model.host.password = \"password\";\n        model.host.port = 8;\n        model.host.username = \"username\";\n        model.host.setIdentityFileEnabled(true);\n        model.host.identityFile = \"blah\";\n        model.setFiltersMandatory(true);\n        model.localFilePath = \"local/\";\n        model.name = \"schedulename\";\n        model.remoteFilePath = \"thing/\";\n        model.setStartAutomatically(true);\n        model.transferType = FileTransferType.FILE;\n\n        ScheduleConfiguration config = ScheduleConfigurationFactory.createConfig(model);\n\n        assertThat(config.getConnectionType()).isEqualTo(model.host.protocol);\n        assertThat(config.getHostName()).isEqualTo(model.host.address);\n        assertThat(config.getLocalFilePath()).isEqualTo(model.localFilePath);\n        assertThat(config.getScheduleName()).isEqualTo(model.name);\n        assertThat(config.getCredentials().getPassword()).isNull();\n        assertThat(config.getCredentials().getIdentity().getIdentityFile()).isEqualTo(\"blah\");\n        assertThat(config.getPort()).isEqualTo(model.host.port);\n        assertThat(config.getRemoteFilePath()).isEqualTo(model.remoteFilePath);\n        assertThat(config.getTransferType()).isEqualTo(model.transferType);\n        assertThat(config.getCredentials().getUsername()).isEqualTo(model.host.username);\n        assertThat(config.isFiltersMandatory()).isTrue();\n    }\n\n    @Test\n    public void shouldAddAllFiltersIfAny() {\n\n        ScheduleModel model = new ScheduleModel();\n\n        model.host = new HostModel();\n        model.host.protocol = TransferProtocol.FTP;\n        model.host.address = \"hostname\";\n        model.host.password = \"password\";\n        model.host.port = 8;\n        model.host.username = \"username\";\n\n        model.filters = new ArrayList<FilterModel>();\n\n        FilterModel filterModel = new FilterModel();\n        filterModel.value = \"filter1\";\n\n        FilterModel filterModel2 = new FilterModel();\n        filterModel2.value = \"filter2\";\n\n        model.filters.add(filterModel);\n        model.filters.add(filterModel2);\n\n        ScheduleConfiguration config = ScheduleConfigurationFactory.createConfig(model);\n\n        assertThat(config.getFilters()).contains(\"filter1\", \"filter2\");\n        assertThat(config.getFilters()).hasSize(2);\n    }\n\n    @Test\n    public void shouldAddAllActionsIfAny() {\n\n        ScheduleModel model = new ScheduleModel();\n\n        model.host = new HostModel();\n        model.host.protocol = TransferProtocol.FTP;\n        model.host.address = \"hostname\";\n        model.host.password = \"password\";\n        model.host.port = 8;\n        model.host.username = \"username\";\n        \n        model.localFilePath = \"a/local/path/\";\n        model.moveFileTo = \"/local/path\";\n        model.actions = new ArrayList<ActionModel>();\n\n        ActionModel action2 = new ActionModel();\n        action2.actionType = \"pushbullet\";\n        action2.f1 = \"apiKey\";\n\n        ActionModel action3 = new ActionModel();\n        action3.actionType = \"api\";\n        action3.f1 = \"url\";\n        action3.f2 = \"POST\";\n        action3.f3 = \"application/json\";\n        action3.f4 = \"some body\";\n        \n        ActionModel action4 = new ActionModel();\n        action4.actionType = \"sns\";\n        action4.f1 = \"topic\";\n        action4.f2 = \"region\";\n        action4.f3 = \"Access\";\n        action4.f4 = \"secret\";\n\n        model.actions.add(action2);\n        model.actions.add(action3);\n        model.actions.add(action4);\n\n        ScheduleConfiguration config = ScheduleConfigurationFactory.createConfig(model);\n\n        assertThat(config.getActions().get(0)).isInstanceOf(MoveFileAction.class);\n        assertThat(config.getActions().get(1)).isInstanceOf(PushbulletNotifyAction.class);\n        assertThat(config.getActions().get(2)).isInstanceOf(HttpAPICallAction.class);\n        assertThat(config.getActions().get(3)).isInstanceOf(SNSNotifyAction.class);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/ScheduleExecutorTest.java",
    "content": "package io.linuxserver.davos.schedule;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.TimeUnit;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\n\nimport io.linuxserver.davos.exception.ScheduleAlreadyRunningException;\nimport io.linuxserver.davos.exception.ScheduleNotRunningException;\nimport io.linuxserver.davos.persistence.dao.ScheduleDAO;\nimport io.linuxserver.davos.persistence.model.ScheduleModel;\n\npublic class ScheduleExecutorTest {\n\n    @InjectMocks\n    private ScheduleExecutor scheduleExecutor = new ScheduleExecutor();\n\n    @Mock\n    private ScheduleDAO mockConfigurationDAO;\n\n    @Mock\n    private ScheduledExecutorService mockExecutorService;\n\n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n\n    @Test\n    public void shouldScheduleBasedOnIntervalAndAutoStartup() {\n\n        List<ScheduleModel> models = new ArrayList<ScheduleModel>();\n\n        ScheduleModel nonAutoModel = new ScheduleModel();\n        nonAutoModel.setStartAutomatically(false);\n\n        ScheduleModel autoModel = new ScheduleModel();\n        autoModel.setStartAutomatically(true);\n        autoModel.interval = 50;\n\n        models.add(nonAutoModel);\n        models.add(autoModel);\n\n        when(mockConfigurationDAO.getAll()).thenReturn(models);\n\n        scheduleExecutor.runAutomaticStartupSchedules();\n\n        verify(mockExecutorService).scheduleAtFixedRate(any(RunnableSchedule.class), eq(0l), eq(50l), eq(TimeUnit.MINUTES));\n    }\n\n    @Test\n    public void startScheduleShouldRunThatSchedule() {\n\n        ScheduleModel config = new ScheduleModel();\n        config.interval = 86;\n\n        when(mockConfigurationDAO.fetchSchedule(1337L)).thenReturn(config);\n\n        scheduleExecutor.startSchedule(1337L);\n\n        verify(mockExecutorService).scheduleAtFixedRate(any(RunnableSchedule.class), eq(0l), eq(86l), eq(TimeUnit.MINUTES));\n    }\n\n    @Test(expected = ScheduleAlreadyRunningException.class)\n    public void startScheduleShouldNotRunScheduleIfAlreadyRunning() {\n\n        ScheduleModel config = new ScheduleModel();\n        config.interval = 86;\n        config.id = 1337L;\n\n        when(mockConfigurationDAO.fetchSchedule(1337L)).thenReturn(config);\n\n        scheduleExecutor.startSchedule(1337L);\n        scheduleExecutor.startSchedule(1337L);\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void stopScheduleShouldStopRunningSchedule() {\n\n        ScheduleModel config = new ScheduleModel();\n        config.interval = 86;\n        config.id = 1337L;\n\n        @SuppressWarnings(\"rawtypes\")\n        ScheduledFuture mockFuture = mock(ScheduledFuture.class);\n\n        when(mockConfigurationDAO.fetchSchedule(1337L)).thenReturn(config);\n        when(mockExecutorService.scheduleAtFixedRate(any(Runnable.class), eq(0l), eq(86l), eq(TimeUnit.MINUTES))).thenReturn(mockFuture);\n\n        scheduleExecutor.startSchedule(1337L);\n        scheduleExecutor.stopSchedule(1337L);\n        \n        verify(mockFuture).cancel(true);\n    }\n    \n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void shouldBeAbleToInformWhetherScheduleIsRunningOrNot() {\n\n        ScheduleModel config = new ScheduleModel();\n        config.interval = 86;\n        config.id = 1337L;\n\n        @SuppressWarnings(\"rawtypes\")\n        ScheduledFuture mockFuture = mock(ScheduledFuture.class);\n\n        when(mockConfigurationDAO.fetchSchedule(1337L)).thenReturn(config);\n        when(mockExecutorService.scheduleAtFixedRate(any(Runnable.class), eq(0l), eq(86l), eq(TimeUnit.MINUTES))).thenReturn(mockFuture);\n\n        scheduleExecutor.startSchedule(1337L);\n        assertThat(scheduleExecutor.isScheduleRunning(1337L)).isTrue();\n        \n        scheduleExecutor.stopSchedule(1337L);\n        assertThat(scheduleExecutor.isScheduleRunning(1337L)).isFalse();\n        \n        verify(mockFuture).cancel(true);\n    }\n    \n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void stopScheduleShouldNotStopRunningScheduleIfItHasAlreadyBeenCancelled() {\n\n        ScheduleModel config = new ScheduleModel();\n        config.interval = 86;\n        config.id = 1337L;\n\n        @SuppressWarnings(\"rawtypes\")\n        ScheduledFuture mockFuture = mock(ScheduledFuture.class);\n        when(mockFuture.isCancelled()).thenReturn(true);\n        \n        when(mockConfigurationDAO.fetchSchedule(1337L)).thenReturn(config);\n        when(mockExecutorService.scheduleAtFixedRate(any(Runnable.class), eq(0l), eq(86l), eq(TimeUnit.MINUTES))).thenReturn(mockFuture);\n\n        scheduleExecutor.startSchedule(1337L);\n        scheduleExecutor.stopSchedule(1337L);\n        \n        verify(mockFuture, never()).cancel(true);\n    }\n    \n    @Test(expected = ScheduleNotRunningException.class)\n    public void stopScheduleShouldNotAttemptToStopNonRunningSchedule() {\n\n        ScheduleModel config = new ScheduleModel();\n        config.interval = 86;\n        config.id = 1337L;\n\n        when(mockConfigurationDAO.fetchSchedule(1337L)).thenReturn(config);\n\n        scheduleExecutor.stopSchedule(1337L);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/ConnectWorkflowStepTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport io.linuxserver.davos.schedule.ScheduleConfiguration;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\nimport io.linuxserver.davos.transfer.ftp.client.Client;\nimport io.linuxserver.davos.transfer.ftp.client.ClientFactory;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientConnectionException;\n\npublic class ConnectWorkflowStepTest {\n\n    @InjectMocks\n    private ConnectWorkflowStep workflowStep = new ConnectWorkflowStep();\n\n    @Mock\n    private ClientFactory mockClientFactory;\n\n    @Mock\n    private Client mockClient;\n\n    @Mock(name = \"nextStep\")\n    private WorkflowStep mockNextStep;\n    \n    @Mock(name = \"backoutStep\")\n    private WorkflowStep mockBackoutStep;\n    \n    @Before\n    public void setUp() {\n\n        initMocks(this);\n\n        when(mockClientFactory.getClient(TransferProtocol.SFTP)).thenReturn(mockClient);\n    }\n\n    @Test\n    public void runStepShouldCreateNewClient() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        workflowStep.runStep(new ScheduleWorkflow(config));\n\n        verify(mockClientFactory).getClient(TransferProtocol.SFTP);\n    }\n\n    @Test\n    public void runStepShouldSetClientIntoWorkflow() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getClient()).isEqualTo(mockClient);\n    }\n\n    @Test\n    public void runStepShouldConnectToNewlyCreatedClient() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        workflowStep.runStep(new ScheduleWorkflow(config));\n\n        verify(mockClient).connect();\n    }\n\n    @Test\n    public void runStepShouldConnectToTheClientUsingTheConfigsHostAndCredentialInformation() {\n\n        String hostIP = \"123.456.789.0\";\n        int port = 1337;\n        UserCredentials credentials = new UserCredentials(hostIP, hostIP);\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"scheduleName\", TransferProtocol.SFTP, hostIP, port, credentials,\n                \"remotePath\", \"localPath\", FileTransferType.FILE, false, false, false);\n\n        workflowStep.runStep(new ScheduleWorkflow(config));\n\n        InOrder inOrder = Mockito.inOrder(mockClient);\n\n        inOrder.verify(mockClient).setCredentials(credentials);\n        inOrder.verify(mockClient).setHost(hostIP);\n        inOrder.verify(mockClient).setPort(port);\n        inOrder.verify(mockClient).connect();\n    }\n\n    @Test\n    public void runStepShouldPlaceConnectedClientConnectionIntoSchedule() {\n\n        Connection mockConnection = mock(Connection.class);\n        when(mockClient.connect()).thenReturn(mockConnection);\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getConnection()).isEqualTo(mockConnection);\n    }\n\n    @Test\n    public void runStepShouldCallOnNextStepWhenComplete() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        workflowStep.runStep(schedule);\n\n        InOrder inOrder = Mockito.inOrder(mockClient, mockNextStep);\n\n        inOrder.verify(mockClient).connect();\n        inOrder.verify(mockNextStep).runStep(schedule);\n    }\n\n    @Test\n    public void ifClientCannotConnectThenDoNotCallNextStep() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        when(mockClient.connect()).thenThrow(new ClientConnectionException());\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        workflowStep.runStep(schedule);\n\n        InOrder inOrder = Mockito.inOrder(mockClient, mockNextStep);\n\n        inOrder.verify(mockClient).connect();\n        inOrder.verify(mockNextStep, never()).runStep(schedule);\n\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/DisconnectWorkflowStepTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\n\nimport io.linuxserver.davos.schedule.ScheduleConfiguration;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\nimport io.linuxserver.davos.transfer.ftp.client.Client;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientDisconnectException;\n\npublic class DisconnectWorkflowStepTest {\n\n    private DisconnectWorkflowStep workflowStep = new DisconnectWorkflowStep();\n\n    @Mock\n    private Client mockClient;\n    \n    @Mock\n    private Connection mockConnection;\n\n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n    \n    @Test\n    public void runStepShouldCloseTheConnection() {\n        \n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n        schedule.setClient(mockClient);\n        \n        workflowStep.runStep(schedule);\n        \n        verify(mockClient).disconnect();\n    }\n    \n    @Test\n    public void ifDisconnectingFailsThenDoNothing() {\n        \n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n        schedule.setClient(mockClient);\n        \n        doThrow(new ClientDisconnectException()).when(mockClient).disconnect();\n        \n        workflowStep.runStep(schedule);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/DownloadFilesWorkflowStepTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport static java.util.stream.Collectors.toList;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.ArrayList;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport io.linuxserver.davos.schedule.ScheduleConfiguration;\nimport io.linuxserver.davos.schedule.workflow.actions.MoveFileAction;\nimport io.linuxserver.davos.schedule.workflow.actions.PostDownloadAction;\nimport io.linuxserver.davos.schedule.workflow.transfer.FTPTransfer;\nimport io.linuxserver.davos.schedule.workflow.transfer.TransferStrategy;\nimport io.linuxserver.davos.schedule.workflow.transfer.TransferStrategyFactory;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class DownloadFilesWorkflowStepTest {\n\n    @InjectMocks\n    private DownloadFilesWorkflowStep workflowStep = new DownloadFilesWorkflowStep();\n\n    @Mock(name = \"nextStep\")\n    private WorkflowStep mockNextStep;\n\n    @Mock(name = \"backoutStep\")\n    private WorkflowStep mockBackoutStep;\n    \n    @Mock\n    private Connection mockConnection;\n\n    @Mock\n    private TransferStrategyFactory mockTransferStrategyFactory;\n\n    @Mock\n    private FileUtils mockFileUtils;\n    \n    @Captor\n    private ArgumentCaptor<FTPTransfer> transferCaptor;\n    \n    private TransferStrategy mockTransferStrategy;\n\n    @Before\n    public void setUp() {\n\n        initMocks(this);\n\n        mockTransferStrategy = mock(TransferStrategy.class);\n        \n        when(mockTransferStrategyFactory.getStrategy(any(FileTransferType.class), eq(mockConnection)))\n                .thenReturn(mockTransferStrategy);\n    }\n\n    @Test\n    public void shouldCallStrategyFactoryToGetCorrectStrategyAndPassFileThrough() {\n\n        ArrayList<FTPFile> filesToDownload = new ArrayList<FTPFile>();\n\n        FTPFile file = new FTPFile(\"\", 0, \"\", 0, false);\n        FTPFile file2 = new FTPFile(\"\", 0, \"\", 0, false);\n\n        filesToDownload.add(file);\n        filesToDownload.add(file2);\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"local/\", FileTransferType.FILE, false, false, false);\n        \n        ArrayList<PostDownloadAction> actions = new ArrayList<PostDownloadAction>();\n        actions.add(new MoveFileAction(\"\", \"\"));\n        \n        config.setActions(actions);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n        schedule.getFilesToDownload().addAll(filesToDownload.stream().map(f -> new FTPTransfer(f)).collect(toList()));\n        \n        workflowStep.runStep(schedule);\n\n        verify(mockTransferStrategyFactory).getStrategy(FileTransferType.FILE, mockConnection);\n        \n        verify(mockTransferStrategy).setPostDownloadActions(actions);\n        verify(mockTransferStrategy, times(2)).transferFile(transferCaptor.capture(), eq(\"local/\"));\n        \n        assertThat(transferCaptor.getAllValues().get(0).getFile()).isEqualTo(file);\n        assertThat(transferCaptor.getAllValues().get(1).getFile()).isEqualTo(file2);\n    }\n    \n    @Test\n    public void shouldCallOnNextStepWhenFinished() {\n        \n        ArrayList<FTPFile> filesToDownload = new ArrayList<FTPFile>();\n        FTPFile file = new FTPFile(\"\", 0, \"\", 0, false);\n        filesToDownload.add(file);\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"local/\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.getFilesToDownload().addAll(filesToDownload.stream().map(f -> new FTPTransfer(f)).collect(toList()));\n        schedule.setConnection(mockConnection);\n        workflowStep.runStep(schedule);\n        \n        InOrder inOrder = Mockito.inOrder(mockTransferStrategy, mockNextStep);\n        \n        inOrder.verify(mockTransferStrategy).transferFile(any(FTPTransfer.class), eq(\"local/\"));\n        inOrder.verify(mockNextStep).runStep(schedule);\n    }\n    \n    @Test\n    public void ifStrategyTranferFailsThenShouldStillCallNextStep() {\n        \n        ArrayList<FTPFile> filesToDownload = new ArrayList<FTPFile>();\n        FTPFile file = new FTPFile(\"\", 0, \"\", 0, false);\n        filesToDownload.add(file);\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"local/\", FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.getFilesToDownload().addAll(filesToDownload.stream().map(f -> new FTPTransfer(f)).collect(toList()));\n        schedule.setConnection(mockConnection);\n\n        doThrow(new DownloadFailedException()).when(mockTransferStrategy).transferFile(any(FTPTransfer.class), eq(\"local/\"));\n        \n        workflowStep.runStep(schedule);\n        \n        verify(mockNextStep).runStep(schedule);\n    }\n    \n    @Test\n    public void shouldDeleteHostFileIfOptionSet() {\n        \n        ArrayList<FTPFile> filesToDownload = new ArrayList<FTPFile>();\n        FTPFile file = new FTPFile(\"\", 0, \"\", 0, false);\n        filesToDownload.add(file);\n\n        ScheduleConfiguration config = new ScheduleConfiguration(\"\", TransferProtocol.SFTP, \"\", 0, new UserCredentials(\"\", \"\"),\n                \"\", \"local/\", FileTransferType.FILE, false, false, true);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.getFilesToDownload().addAll(filesToDownload.stream().map(f -> new FTPTransfer(f)).collect(toList()));\n        schedule.setConnection(mockConnection);\n        workflowStep.runStep(schedule);\n        \n        verify(mockConnection).deleteRemoteFile(file);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/FilterFilesWorkflowStepTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow;\n\nimport static java.util.stream.Collectors.toList;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\nimport org.joda.time.DateTime;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport io.linuxserver.davos.schedule.ScheduleConfiguration;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.exception.FileListingException;\n\npublic class FilterFilesWorkflowStepTest {\n\n    @InjectMocks\n    private FilterFilesWorkflowStep workflowStep = new FilterFilesWorkflowStep();\n\n    @Mock(name = \"nextStep\")\n    private DownloadFilesWorkflowStep mockNextStep;\n\n    @Mock(name = \"backoutStep\")\n    private DisconnectWorkflowStep mockBackupStep;\n\n    @Mock\n    private Connection mockConnection;\n\n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n\n    @Test\n    public void workflowStepShouldListFilesInTheRemoteDirectory() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        verify(mockConnection).listFiles(\"remote/\");\n    }\n\n    @Test\n    public void workflowStepShouldSetTheFilesFromLastScanToThisScanAtEnd() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n        config.setFilters(Arrays.asList(\"file1\", \"file2\", \"file4\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n\n        schedule.getFilesFromLastScan().addAll(Arrays.asList(\"some\", \"old\", \"files\"));\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesFromLastScan()).isEqualTo(Arrays.asList(\"file1\", \"file2\", \"file3\", \"file4\", \"file5\"));\n    }\n\n    @Test\n    public void workflowStepShouldFilterOutAnyFilesThatAreNotInTheGivenConfigList() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n        config.setFilters(Arrays.asList(\"file1\", \"file2\", \"file4\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file1, file2, file4));\n    }\n\n    @Test\n    public void workflowStepShouldFilterOutAnyFilesThatAreNotInTheGivenConfigListAndWereNotScannedInLastRun() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n        config.setFilters(Arrays.asList(\"file1\", \"file2\", \"file4\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n\n        schedule.getFilesFromLastScan().addAll(Arrays.asList(\"file1\", \"file3\", \"file4\", \"file5\"));\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file2));\n    }\n\n    @Test\n    public void shouldOnlyAddOneInstanceOfAFileEvenIfTwoFiltersMatch() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n        config.setFilters(Arrays.asList(\"file1\", \"file2\", \"file2\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file1, file2));\n    }\n\n    @Test\n    public void workflowStepShouldFilterOutAnyFilesThatDoNotMatchTheWildcards() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n        config.setFilters(Arrays.asList(\"file1?and?Stuff\", \"file2*something\", \"file4*\", \"file5\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1.and-stuff\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2.andMoreTextsomething\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4.txt\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5.txt\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file1, file2, file4));\n    }\n\n    @Test\n    public void workflowStepShouldCallNextStepRunMethodOnceSettingFilters() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n        config.setFilters(Arrays.asList(\"file1\", \"file2\", \"file4\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        InOrder inOrder = Mockito.inOrder(mockNextStep);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file1));\n        inOrder.verify(mockNextStep).runStep(schedule);\n    }\n\n    @Test\n    public void ifFilterListIsInitiallyEmptyThenAssumeThatAllFilesShouldBeDownloaded() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file1, file2, file3, file4, file5));\n    }\n\n    @Test\n    public void ifFilterListIsInitiallyEmptyButFiltersAreMandatoryThenNoFilesShouldBeDownloaded() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, true, false, false);\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList())).isEmpty();\n    }\n    \n    @Test\n    public void shouldFilterFilesThatDoNotMatchSetFiltersIfInvertingSet() {\n        \n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, true, false);\n        config.setFilters(Arrays.asList(\"file1?and?Stuff\", \"file2*something\", \"file4*\", \"file5\"));\n\n        ArrayList<FTPFile> files = new ArrayList<FTPFile>();\n\n        FTPFile file1 = new FTPFile(\"file1.and-stuff\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file2 = new FTPFile(\"file2.andMoreTextsomething\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file3 = new FTPFile(\"file3\", 0, \"remote/\", DateTime.now().minusDays(2).getMillis(), false);\n        FTPFile file4 = new FTPFile(\"file4.txt\", 0, \"remote/\", DateTime.now().getMillis(), false);\n        FTPFile file5 = new FTPFile(\"file5.txt\", 0, \"remote/\", DateTime.now().getMillis(), false);\n\n        files.add(file1);\n        files.add(file2);\n        files.add(file3);\n        files.add(file4);\n        files.add(file5);\n\n        when(mockConnection.listFiles(\"remote/\")).thenReturn(files);\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        assertThat(schedule.getFilesToDownload().stream().map(t -> t.getFile()).collect(toList()))\n                .isEqualTo(Arrays.asList(file3, file5));\n    }\n\n    @Test\n    public void ifListingFilesIsUnsuccessfulThenDoNotCallNextStepAndCallBackupStepInstead() {\n\n        ScheduleConfiguration config = new ScheduleConfiguration(null, null, null, 0, null, \"remote/\", \"local/\",\n                FileTransferType.FILE, false, false, false);\n\n        when(mockConnection.listFiles(\"remote/\")).thenThrow(new FileListingException());\n\n        ScheduleWorkflow schedule = new ScheduleWorkflow(config);\n        schedule.setConnection(mockConnection);\n\n        workflowStep.runStep(schedule);\n\n        verify(mockNextStep, never()).runStep(schedule);\n        verify(mockBackupStep).runStep(schedule);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/actions/HttpAPICallActionTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.converter.HttpMessageConversionException;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.client.RestTemplate;\n\npublic class HttpAPICallActionTest {\n\n    @InjectMocks\n    private HttpAPICallAction httpAPICallAction;\n\n    @Mock\n    private RestTemplate mockRestTemplate;\n\n    @Captor\n    private ArgumentCaptor<HttpEntity<String>> entityCaptor;\n\n    @Before\n    public void setUp() {\n\n        httpAPICallAction = new HttpAPICallAction(\"http://url\", \"POST\", \"application/json\", \"{\\\"hello\\\":\\\"$filename\\\"}\");\n\n        initMocks(this);\n    }\n\n    @Test\n    public void shouldCallRestTemplateWithCorrectParams() {\n\n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"file.txt\";\n\n        httpAPICallAction.execute(execution);\n\n        verify(mockRestTemplate).exchange(eq(\"http://url\"), eq(HttpMethod.POST), entityCaptor.capture(), eq(Object.class));\n\n        String body = entityCaptor.getValue().getBody();\n\n        assertThat(body).isEqualTo(\"{\\\"hello\\\":\\\"file.txt\\\"}\");\n    }\n    \n    @Test\n    public void shouldResolveFilenameInUrlAsWell() {\n\n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"file.txt\";\n\n        httpAPICallAction = new HttpAPICallAction(\"http://url?file=$filename\", \"POST\", \"application/json\", \"{\\\"hello\\\":\\\"$filename\\\"}\");\n        initMocks(this);\n        \n        httpAPICallAction.execute(execution);\n\n        verify(mockRestTemplate).exchange(eq(\"http://url?file=file.txt\"), eq(HttpMethod.POST), entityCaptor.capture(), eq(Object.class));\n\n        String body = entityCaptor.getValue().getBody();\n\n        assertThat(body).isEqualTo(\"{\\\"hello\\\":\\\"file.txt\\\"}\");\n    }\n\n    @Test\n    public void postDataShouldHaveCorrectHeaderValue() {\n\n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n\n        httpAPICallAction.execute(execution);\n\n        verify(mockRestTemplate).exchange(eq(\"http://url\"), eq(HttpMethod.POST), entityCaptor.capture(), eq(Object.class));\n\n        HttpHeaders headers = entityCaptor.getValue().getHeaders();\n\n        assertThat(headers.getContentType()).isEqualTo(MediaType.APPLICATION_JSON);\n    }\n\n    @Test\n    public void ifRestTemplateFailsThenDoNothing() {\n\n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n        \n        when(mockRestTemplate.exchange(eq(\"http://url\"), eq(HttpMethod.POST), any(HttpEntity.class), eq(Object.class)))\n                .thenThrow(new RestClientException(\"\"));\n\n        httpAPICallAction.execute(execution);\n    }\n\n    @Test\n    public void ifRestTemplateFailsBecauseMessageIsUnreadbleThenDoNothing() {\n\n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n        \n        when(mockRestTemplate.exchange(eq(\"http://url\"), eq(HttpMethod.POST), any(HttpEntity.class), eq(Object.class)))\n                .thenThrow(new HttpMessageConversionException(\"\"));\n\n        httpAPICallAction.execute(execution);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/actions/MoveFileActionTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.io.IOException;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\n\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class MoveFileActionTest {\n\n    @InjectMocks\n    private MoveFileAction moveFileAction = new MoveFileAction(\"oldPath\", \"newPath\");\n    \n    @Mock\n    private FileUtils mockFileUtils;\n    \n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n    \n    @Test\n    public void executeShouldMoveTheFile() throws IOException {\n        \n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n        \n        moveFileAction.execute(execution);\n        \n        verify(mockFileUtils).moveFileToDirectory(\"oldPath/filename\", \"newPath/\");\n    }\n    \n    @Test\n    public void ifMovingOfFileFailsThenDoNotPerpetuateError() throws IOException {\n        \n        doThrow(new IOException()).when(mockFileUtils).moveFileToDirectory(anyString(), anyString());\n        \n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n        \n        moveFileAction.execute(execution);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/actions/PushbulletNotifyActionTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.actions;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.converter.HttpMessageConversionException;\nimport org.springframework.web.client.RestClientException;\nimport org.springframework.web.client.RestTemplate;\n\nimport io.linuxserver.davos.schedule.workflow.actions.PushbulletNotifyAction.PushbulletRequest;\n\npublic class PushbulletNotifyActionTest {\n\n    @InjectMocks\n    private PushbulletNotifyAction pushbulletNotifyAction;\n\n    @Mock\n    private RestTemplate mockRestTemplate;\n\n    @Captor\n    private ArgumentCaptor<HttpEntity<PushbulletRequest>> entityCaptor;\n\n    @Before\n    public void setUp() {\n\n        pushbulletNotifyAction = new PushbulletNotifyAction(\"apiKey\");\n\n        initMocks(this);\n    }\n    \n    @Test\n    public void executeShouldSendCorrectData() {\n\n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n\n        pushbulletNotifyAction.execute(execution);\n\n        verify(mockRestTemplate).exchange(eq(\"https://api.pushbullet.com/v2/pushes\"), eq(HttpMethod.POST), entityCaptor.capture(),\n                eq(Object.class));\n\n        PushbulletRequest request = entityCaptor.getValue().getBody();\n\n        assertThat(request.type).isEqualTo(\"note\");\n        assertThat(request.title).isEqualTo(\"A new file has been downloaded\");\n        assertThat(request.body).isEqualTo(\"filename\");\n    }\n\n    @Test\n    public void postDataShouldHaveCorrectHeaderValue() {\n        \n        PostDownloadExecution execution = new PostDownloadExecution();\n        execution.fileName = \"filename\";\n\n        pushbulletNotifyAction.execute(execution);\n\n        verify(mockRestTemplate).exchange(eq(\"https://api.pushbullet.com/v2/pushes\"), eq(HttpMethod.POST), entityCaptor.capture(),\n                eq(Object.class));\n\n        HttpHeaders headers = entityCaptor.getValue().getHeaders();\n        \n        assertThat(headers.getContentType()).isEqualTo(MediaType.APPLICATION_JSON);\n        assertThat(headers.get(\"Authorization\").get(0)).isEqualTo(\"Bearer apiKey\");\n    }\n    \n    @Test\n    public void ifRestTemplateFailsThenDoNothing() {\n        \n        when(mockRestTemplate.exchange(eq(\"https://api.pushbullet.com/v2/pushes\"), eq(HttpMethod.POST), any(HttpEntity.class),\n                eq(Object.class))).thenThrow(new RestClientException(\"\"));\n        \n        pushbulletNotifyAction.execute(new PostDownloadExecution());\n    }\n    \n    @Test\n    public void ifRestTemplateFailsBecauseMessageIsUnreadbleThenDoNothing() {\n        \n        when(mockRestTemplate.exchange(eq(\"https://api.pushbullet.com/v2/pushes\"), eq(HttpMethod.POST), any(HttpEntity.class),\n                eq(Object.class))).thenThrow(new HttpMessageConversionException(\"\"));\n        \n        pushbulletNotifyAction.execute(new PostDownloadExecution());\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/filter/ReferentialFileFilterTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.filter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\n\npublic class ReferentialFileFilterTest {\n\n    @Test\n    public void shouldReturnAllFTPFilesIfLastScanIsEmpty() {\n        \n        List<FTPFile> newFiles = new ArrayList<>();\n        newFiles.add(new FTPFile(\"file1\", 0, \"\", 0, false));\n        newFiles.add(new FTPFile(\"file2\", 0, \"\", 0, false));\n        newFiles.add(new FTPFile(\"file3\", 0, \"\", 0, false));\n        \n        List<String> oldFiles = new ArrayList<>();\n        \n        List<FTPFile> filteredFiles = new ReferentialFileFilter(oldFiles).filter(newFiles);\n        \n        assertThat(filteredFiles).hasSize(3);\n        assertThat(filteredFiles.get(0).getName()).isEqualTo(\"file1\");\n        assertThat(filteredFiles.get(1).getName()).isEqualTo(\"file2\");\n        assertThat(filteredFiles.get(2).getName()).isEqualTo(\"file3\");\n    }\n    \n    @Test\n    public void shouldReturnFilteredFTPFilesIfLastScanIsMissingFiles() {\n        \n        List<FTPFile> newFiles = new ArrayList<>();\n        newFiles.add(new FTPFile(\"file1\", 0, \"\", 0, false));\n        newFiles.add(new FTPFile(\"file2\", 0, \"\", 0, false));\n        newFiles.add(new FTPFile(\"file3\", 0, \"\", 0, false));\n        \n        List<String> oldFiles = Arrays.asList(\"file1\", \"file3\");\n        \n        List<FTPFile> filteredFiles = new ReferentialFileFilter(oldFiles).filter(newFiles);\n        \n        assertThat(filteredFiles).hasSize(1);\n        assertThat(filteredFiles.get(0).getName()).isEqualTo(\"file2\");\n    }\n    \n    @Test\n    public void shouldReturnEmptyListIfNewFilesAreEmpty() {\n\n        List<FTPFile> newFiles = new ArrayList<>();\n        List<String> oldFiles = Arrays.asList(\"file1\", \"file3\");\n        \n        List<FTPFile> filteredFiles = new ReferentialFileFilter(oldFiles).filter(newFiles);\n        \n        assertThat(filteredFiles).isEmpty();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/transfer/FilesAndFoldersTranferStrategyTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\n\npublic class FilesAndFoldersTranferStrategyTest {\n\n    private FilesAndFoldersTranferStrategy strategy;\n    \n    @Mock\n    private Connection mockConnection;\n    \n    @Before\n    public void setUp() {\n        \n        initMocks(this);\n        \n        strategy = new FilesAndFoldersTranferStrategy(mockConnection);\n    }\n    \n    @Test\n    public void strategyShouldCallDownloadMethodForFiles() {\n        \n        FTPFile file = new FTPFile(\"file1\", 0, \"remotePath/\", 0, false);\n        \n        strategy.transferFile(new FTPTransfer(file), \"destination\");\n        \n        verify(mockConnection).download(file, \"destination/\");\n    }\n    \n    @Test\n    public void strategyShouldCallDownloadMethodForDirectories() {\n        \n        FTPFile file = new FTPFile(\"file1\", 0, \"remotePath/\", 0, true);\n        \n        strategy.transferFile(new FTPTransfer(file), \"destination\");\n        \n        verify(mockConnection).download(file, \"destination/\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/transfer/FilesOnlyTransferStrategyTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\n\npublic class FilesOnlyTransferStrategyTest {\n\n    private FilesOnlyTransferStrategy strategy;\n\n    @Mock\n    private Connection mockConnection;\n\n    @Before\n    public void setUp() {\n\n        initMocks(this);\n\n        strategy = new FilesOnlyTransferStrategy(mockConnection);\n    }\n\n    @Test\n    public void strategyShouldCallDownloadMethodForFiles() {\n\n        FTPFile file = new FTPFile(\"file1\", 0, \"remotePath/\", 0, false);\n\n        strategy.transferFile(new FTPTransfer(file), \"destination\");\n\n        verify(mockConnection).download(file, \"destination/\");\n    }\n\n    @Test\n    public void strategyShouldNotCallDownloadMethodForDirectories() {\n\n        FTPFile file = new FTPFile(\"file1\", 0, \"remotePath/\", 0, true);\n\n        strategy.transferFile(new FTPTransfer(file), \"destination\");\n\n        verify(mockConnection, never()).download(any(FTPFile.class), anyString());\n    }\n\n    @Test\n    public void shouldNullifyListenerIfTransferIsForAFolder() {\n\n        FTPFile file = new FTPFile(\"file1\", 0, \"remotePath/\", 0, true);\n        FTPTransfer transfer = new FTPTransfer(file);\n        transfer.setListener(new ProgressListener());\n        \n        \n        assertThat(transfer.getListener()).isNotNull();\n        strategy.transferFile(transfer, \"destination\");\n        assertThat(transfer.getListener()).isNull();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/transfer/TransferStrategyFactoryTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.transfer.ftp.FileTransferType;\n\npublic class TransferStrategyFactoryTest {\n\n    @Test\n    public void shouldReturnCorrectStrategies() {\n\n        assertThat(new TransferStrategyFactory().getStrategy(FileTransferType.FILE, null))\n                .isInstanceOf(FilesOnlyTransferStrategy.class);\n\n        assertThat(new TransferStrategyFactory().getStrategy(FileTransferType.RECURSIVE, null))\n                .isInstanceOf(FilesAndFoldersTranferStrategy.class);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/schedule/workflow/transfer/TransferStrategyTest.java",
    "content": "package io.linuxserver.davos.schedule.workflow.transfer;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\n\nimport java.util.Arrays;\n\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\n\nimport io.linuxserver.davos.schedule.workflow.actions.PostDownloadAction;\nimport io.linuxserver.davos.schedule.workflow.actions.PostDownloadExecution;\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\n\npublic class TransferStrategyTest {\n\n    @Test\n    public void toStringShouldPrintClassName() {\n        assertThat(new TestTransferStrategy(null).toString()).isEqualTo(\"TestTransferStrategy\");\n        assertThat(new AnotherTestTransferStrategy(null).toString()).isEqualTo(\"AnotherTestTransferStrategy\");\n    }\n\n    @Test\n    public void runPostDownloadActionShouldCallAllGivenActionsWithTheFile() {\n\n        ArgumentCaptor<PostDownloadExecution> captor = ArgumentCaptor.forClass(PostDownloadExecution.class);\n\n        DownloadActionImplTestTransferStrategy strategy = new DownloadActionImplTestTransferStrategy(null);\n\n        PostDownloadAction mockAction1 = mock(PostDownloadAction.class);\n        PostDownloadAction mockAction2 = mock(PostDownloadAction.class);\n\n        strategy.setPostDownloadActions(Arrays.asList(mockAction1, mockAction2));\n\n        strategy.transferFile(new FTPTransfer(new FTPFile(\"file1\", 0, null, 0, false)), \"destination/\");\n\n        verify(mockAction1).execute(captor.capture());\n        verify(mockAction2).execute(captor.capture());\n\n        assertThat(captor.getAllValues().get(0).fileName).isEqualTo(\"file1\");\n        assertThat(captor.getAllValues().get(1).fileName).isEqualTo(\"file1\");\n    }\n\n    @Test\n    public void ensureNulLActionsAreCheckedBeforeAttemptingToRun() {\n        \n        DownloadActionImplTestTransferStrategy strategy = new DownloadActionImplTestTransferStrategy(null);\n        strategy.setPostDownloadActions(null);\n        strategy.transferFile(new FTPTransfer(new FTPFile(\"file1\", 0, null, 0, false)), \"destination/\");\n    }\n\n    class TestTransferStrategy extends TransferStrategy {\n\n        public TestTransferStrategy(Connection connection) {\n            super(connection);\n        }\n\n        @Override\n        public void transferFile(FTPTransfer fileToTransfer, String destination) {\n        }\n    }\n\n    class AnotherTestTransferStrategy extends TransferStrategy {\n\n        public AnotherTestTransferStrategy(Connection connection) {\n            super(connection);\n        }\n\n        @Override\n        public void transferFile(FTPTransfer fileToTransfer, String destination) {\n        }\n    }\n\n    class DownloadActionImplTestTransferStrategy extends TransferStrategy {\n\n        public DownloadActionImplTestTransferStrategy(Connection connection) {\n            super(connection);\n        }\n\n        @Override\n        public void transferFile(FTPTransfer fileToTransfer, String destination) {\n            runPostDownloadAction(fileToTransfer.getFile());\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/client/ClientFactoryTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ClientFactoryTest {\n\n    @Test\n    public void shouldReturnSFTPClientWhenProtocolIsSFTP() {\n        assertThat(new ClientFactory().getClient(TransferProtocol.SFTP)).isInstanceOf(SFTPClient.class);\n    }\n    \n    @Test\n    public void shouldReturnFTPClientForAnythingElse() {\n        \n        assertThat(new ClientFactory().getClient(TransferProtocol.FTP)).isInstanceOf(FTPClient.class);\n        assertThat(new ClientFactory().getClient(TransferProtocol.FTPS)).isInstanceOf(FTPSClient.class);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/client/FTPClientTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.io.IOException;\nimport java.net.SocketException;\nimport java.net.UnknownHostException;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.connection.ConnectionFactory;\nimport io.linuxserver.davos.transfer.ftp.connection.FTPConnection;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientConnectionException;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientDisconnectException;\n\npublic class FTPClientTest {\n\n    @InjectMocks\n    public FTPClient ftpClient = new FTPClient();\n\n    @Mock\n    private org.apache.commons.net.ftp.FTPClient mockFtpClient;\n\n    @Mock\n    private ConnectionFactory mockConnectionFactory;\n\n    @Rule\n    public ExpectedException expectedException = ExpectedException.none();\n\n    private String hostname;\n    private int port;\n    private UserCredentials userCredentials;\n\n    @Before\n    public void setUp() throws IOException {\n        \n        initMocks(this);\n\n        hostname = \"this is a hostname\";\n        port = 80;\n\n        userCredentials = new UserCredentials(\"thisisausername\", \"thisisapassword\");\n\n        ftpClient.setHost(hostname);\n        ftpClient.setPort(port);\n        ftpClient.setCredentials(userCredentials);\n\n        when(mockFtpClient.getReplyCode()).thenReturn(200);\n        when(mockFtpClient.login(userCredentials.getUsername(), userCredentials.getPassword())).thenReturn(true);\n        when(mockFtpClient.isConnected()).thenReturn(true);\n\n        when(mockConnectionFactory.createFTPConnection(mockFtpClient)).thenReturn(new FTPConnection(mockFtpClient));\n    }\n\n    @Test\n    public void newFtpClientShouldCreateFTPClientInstance() {\n        assertThat(ftpClient.ftpClient).isInstanceOf(org.apache.commons.net.ftp.FTPClient.class);\n    }\n\n    @Test\n    public void connectMethodShouldCallonUnderlyingFtpClientConnectMethodWithHostname() throws SocketException, IOException {\n\n        ftpClient.connect();\n\n        verify(mockFtpClient).connect(hostname, port);\n    }\n\n    @Test\n    public void connectMethodShouldEnterPassiveModeLoginToUnderlyingFtpClient() throws IOException {\n\n        ftpClient.connect();\n\n        InOrder inOrder = Mockito.inOrder(mockFtpClient);\n\n        inOrder.verify(mockFtpClient).enterLocalPassiveMode();\n        inOrder.verify(mockFtpClient).login(userCredentials.getUsername(), userCredentials.getPassword());\n    }\n\n    @Test\n    public void connectMethodShouldSetKeepAliveCommandToEveryFiveMinutes() {\n\n        ftpClient.connect();\n\n        verify(mockFtpClient).setControlKeepAliveTimeout(300);\n    }\n\n    @Test\n    public void onceLoggedInTheClientShouldHaveFileTypeSetToBinary() throws IOException {\n        \n        ftpClient.connect();\n\n        InOrder inOrder = Mockito.inOrder(mockFtpClient);\n        \n        inOrder.verify(mockFtpClient).login(userCredentials.getUsername(), userCredentials.getPassword());\n        inOrder.verify(mockFtpClient).setFileType(org.apache.commons.net.ftp.FTPClient.BINARY_FILE_TYPE);\n    }\n\n    @Test\n    public void connectMethodShouldReturnNewFtpConnectionTakingInUnderlyingFtpClient() {\n\n        Connection connection = ftpClient.connect();\n\n        verify(mockConnectionFactory).createFTPConnection(mockFtpClient);\n        assertThat(connection).isInstanceOf(FTPConnection.class);\n    }\n\n    @Test\n    public void disconnectMethodShouldCallOnUnderlyingFtpClientDisconnectMethod() throws IOException {\n\n        ftpClient.disconnect();\n\n        verify(mockFtpClient).disconnect();\n    }\n\n    @Test\n    public void ifConnectionFailsThenCatchThrownExceptionAndThrowFtpException() throws SocketException, IOException {\n\n        expectedException.expect(ClientConnectionException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to connect to host \" + hostname + \" on port \" + port)));\n\n        doThrow(new IOException()).when(mockFtpClient).connect(hostname, port);\n\n        ftpClient.connect();\n    }\n\n    @Test\n    public void ifConnectionFailsDueToUnknownHostThenCatchThrownExceptionAndThrowFtpException() throws SocketException,\n            IOException {\n\n        expectedException.expect(ClientConnectionException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to connect to host \" + hostname + \" on port \" + port)));\n\n        doThrow(new UnknownHostException()).when(mockFtpClient).connect(hostname, port);\n\n        ftpClient.connect();\n    }\n\n    @Test\n    public void ifUnderlyingClientReturnsBadConnectionCodeThenThrowConnectionException() {\n\n        expectedException.expect(ClientConnectionException.class);\n        expectedException\n                .expectMessage(is(equalTo(\"The host \" + hostname + \" on port \" + port + \" returned a bad status code.\")));\n\n        when(mockFtpClient.getReplyCode()).thenReturn(500);\n\n        ftpClient.connect();\n    }\n\n    @Test\n    public void ifUnableToLoginToFtpClientThenThrowFtpException() throws IOException {\n\n        expectedException.expect(ClientConnectionException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to login for user \" + userCredentials.getUsername())));\n\n        when(mockFtpClient.login(userCredentials.getUsername(), userCredentials.getPassword())).thenReturn(false);\n\n        ftpClient.connect();\n    }\n\n    @Test\n    public void whenDisconnectingThenClientShouldCheckToSeeIfAlreadyDisconnected() {\n\n        ftpClient.disconnect();\n\n        verify(mockFtpClient).isConnected();\n    }\n\n    @Test\n    public void whenAlreadyDisconnectedThenClientShoudlNotCallOnUnderlyingClientDisconnectMethod() throws IOException {\n\n        when(mockFtpClient.isConnected()).thenReturn(false);\n\n        ftpClient.disconnect();\n\n        verify(mockFtpClient, times(0)).disconnect();\n    }\n\n    @Test\n    public void whenClientIsStillConnectedThenShouldCallOnUnderlyingClientDisconnectMethod() throws IOException {\n\n        ftpClient.disconnect();\n\n        verify(mockFtpClient).disconnect();\n    }\n\n    @Test\n    public void ifUnderlyingClientThrowsExceptionWhenDisconnectingThenClientShouldCatchAndRethrow() throws IOException {\n\n        expectedException.expect(ClientDisconnectException.class);\n        expectedException.expectMessage(is(equalTo(\"There was an unexpected error while trying to disconnect.\")));\n\n        doThrow(new IOException()).when(mockFtpClient).disconnect();\n\n        ftpClient.disconnect();\n    }\n    \n    @Test\n    public void ifUnderlyingClientIsNullifiedBeforeDisconnectionThenDisconnectShouldThrow() {\n        \n        expectedException.expect(ClientDisconnectException.class);\n        expectedException.expectMessage(is(equalTo(\"The underlying client was null.\")));\n        \n        ftpClient.ftpClient = null;\n        ftpClient.disconnect();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/client/FTPSClientTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\npublic class FTPSClientTest {\n\n    private FTPClient client = new FTPSClient();\n\n    @Test\n    public void newFtpsClientShouldCreateFTPSClientInstance() {\n        assertThat(client.ftpClient).isInstanceOf(org.apache.commons.net.ftp.FTPSClient.class);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/client/SFTPClientTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.client;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.Answers;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport com.jcraft.jsch.Channel;\nimport com.jcraft.jsch.ChannelSftp;\nimport com.jcraft.jsch.JSch;\nimport com.jcraft.jsch.JSchException;\nimport com.jcraft.jsch.Session;\n\nimport io.linuxserver.davos.transfer.ftp.client.UserCredentials.Identity;\nimport io.linuxserver.davos.transfer.ftp.connection.Connection;\nimport io.linuxserver.davos.transfer.ftp.connection.ConnectionFactory;\nimport io.linuxserver.davos.transfer.ftp.connection.SFTPConnection;\nimport io.linuxserver.davos.transfer.ftp.exception.ClientDisconnectException;\n\npublic class SFTPClientTest {\n\n    private static final String SFTP = \"sftp\";\n    private static final String CONNECTION_ERROR_MESSAGE = \"Unable to connect to host %s on port %d\";\n\n    @InjectMocks\n    private SFTPClient SFTPClient = new SFTPClient();\n\n    @Mock(answer = Answers.RETURNS_DEEP_STUBS)\n    private JSch mockJsch;\n\n    @Mock\n    private ConnectionFactory mockConnectionFactory;\n\n    @Rule\n    public ExpectedException expectedException = ExpectedException.none();\n\n    private UserCredentials userCredentials;\n\n    @Before\n    public void setUp() throws JSchException {\n\n        initMocks(this);\n\n        userCredentials = new UserCredentials(\"user\", \"password\");\n\n        SFTPClient.setHost(\"host\");\n        SFTPClient.setPort(999);\n        SFTPClient.setCredentials(userCredentials);\n\n        when(mockConnectionFactory.createSFTPConnection(any(Channel.class))).thenReturn(new SFTPConnection(new ChannelSftp()));\n    }\n\n    @Test\n    public void connectMethodShouldCreateSessionUsingHostPortAndUsername() throws JSchException {\n\n        SFTPClient.connect();\n\n        verify(mockJsch).getSession(\"user\", \"host\", 999);\n    }\n\n    @Test\n    public void sessionFromInitialConnectionNeedsConfigAndIdentitySettingBeforeConnecting() throws JSchException {\n\n        Session mockSession = mockJsch.getSession(\"user\", \"host\", 999);\n\n        InOrder inOrder = Mockito.inOrder(mockJsch, mockSession);\n\n        userCredentials = new UserCredentials(\"user\", new Identity(\".ssh/id_rsa\"));\n        \n        SFTPClient.setCredentials(userCredentials);\n        SFTPClient.connect();\n\n        inOrder.verify(mockJsch).addIdentity(\".ssh/id_rsa\");\n        inOrder.verify(mockSession).setConfig(\"StrictHostKeyChecking\", \"no\");\n        inOrder.verify(mockSession, never()).setPassword(\"password\");\n    \n        inOrder.verify(mockSession).connect();\n    }\n\n    @Test\n    public void sessionFromInitialConnectionNeedsConfigAndPasswordSettingBeforeConnecting() throws JSchException {\n\n        Session mockSession = mockJsch.getSession(\"user\", \"host\", 999);\n\n        InOrder inOrder = Mockito.inOrder(mockSession);\n\n        SFTPClient.connect();\n\n        inOrder.verify(mockSession).setConfig(\"StrictHostKeyChecking\", \"no\");\n        inOrder.verify(mockSession).setPassword(\"password\");\n        inOrder.verify(mockSession).connect();\n    }\n    \n    @Test\n    public void returnedSessionObjectShouldSetChannelToSftpAndOpen() throws JSchException {\n\n        Session mockSession = mockJsch.getSession(\"user\", \"host\", 999);\n\n        SFTPClient.connect();\n\n        verify(mockSession).openChannel(SFTP);\n    }\n\n    @Test\n    public void ifForAnyReasonTheUnderlyingSessionCantConnectThenCatchTheExceptionAndRethrow() throws JSchException {\n\n        expectedException.expect(RuntimeException.class);\n        expectedException.expectMessage(is(equalTo(String.format(CONNECTION_ERROR_MESSAGE, \"host\", 999))));\n\n        Session mockSession = mockJsch.getSession(\"user\", \"host\", 999);\n        doThrow(new JSchException()).when(mockSession).connect();\n\n        SFTPClient.connect();\n    }\n\n    @Test\n    public void sessionChannelShouldBeConnectedTo() throws JSchException {\n\n        Session mockSession = mockJsch.getSession(\"user\", \"host\", 999);\n        Channel mockChannel = mockSession.openChannel(SFTP);\n\n        SFTPClient.connect();\n\n        verify(mockChannel).connect();\n    }\n\n    @Test\n    public void connectMethodShouldReturnLiveInstanceOfSftpChannelWrappedInStfpConnection() {\n\n        Connection connection = SFTPClient.connect();\n\n        assertThat(connection).isInstanceOf(SFTPConnection.class);\n    }\n\n    @Test\n    public void disconnectMethodShouldDisconnectUnderlyingChannelAndSession() throws JSchException {\n\n        Session mockSession = mockJsch.getSession(\"user\", \"host\", 999);\n        Channel mockChannel = mockSession.openChannel(SFTP);\n\n        SFTPClient.connect();\n        SFTPClient.disconnect();\n\n        verify(mockSession).disconnect();\n        verify(mockChannel).disconnect();\n    }\n\n    @Test\n    public void disconnectMethodShouldThrowExceptionWhenNotInitiallyConnected() {\n\n        expectedException.expect(ClientDisconnectException.class);\n        expectedException.expectMessage(is(equalTo(\"The underlying connection was never initially made.\")));\n\n        SFTPClient.disconnect();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/connection/FTPConnectionTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.List;\n\nimport org.apache.commons.io.output.CountingOutputStream;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;\nimport io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\nimport io.linuxserver.davos.transfer.ftp.exception.FileListingException;\nimport io.linuxserver.davos.util.FileStreamFactory;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class FTPConnectionTest {\n\n    private static final String LOCAL_DIRECTORY = \".\";\n    private static final String DIRECTORY_PATH = \"this/is/a/directory\";\n\n    @InjectMocks\n    private FTPConnection ftpConnection;\n\n    @Mock\n    private FileStreamFactory mockFileStreamFactory;\n\n    @Mock\n    private FileUtils mockFileUtils;\n\n    @Mock\n    private FileOutputStream mockFileOutputStream;\n\n    private org.apache.commons.net.ftp.FTPClient mockFtpClient;\n\n    @Rule\n    public ExpectedException expectedException = ExpectedException.none();\n\n    @Before\n    public void setUp() throws IOException {\n\n        mockFtpClient = mock(org.apache.commons.net.ftp.FTPClient.class);\n\n        when(mockFtpClient.changeWorkingDirectory(anyString())).thenReturn(true);\n        when(mockFtpClient.printWorkingDirectory()).thenReturn(DIRECTORY_PATH);\n        when(mockFtpClient.retrieveFile(anyString(), any(OutputStream.class))).thenReturn(true);\n        when(mockFtpClient.deleteFile(any(String.class))).thenReturn(true);\n\n        org.apache.commons.net.ftp.FTPFile[] files = createRemoteFTPFiles();\n\n        ftpConnection = new FTPConnection(mockFtpClient);\n\n        initMocks(this);\n\n        when(mockFtpClient.listFiles(anyString())).thenReturn(files);\n        when(mockFileStreamFactory.createOutputStream(\"./remote.file\")).thenReturn(mockFileOutputStream);\n    }\n\n    @Test\n    public void whenListingFilesThenFtpClientListFilesMethodShouldBeCalledForCurrentWorkingDirectory() throws IOException {\n\n        ftpConnection.listFiles();\n\n        verify(mockFtpClient).listFiles(\"this/is/a/directory/\");\n    }\n\n    @Test\n    public void ifWhenListingFilesFtpClientThrowsExceptionThenCatchAndRethrowFileListingExcepton() throws IOException {\n\n        expectedException.expect(FileListingException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to list files in directory \" + DIRECTORY_PATH)));\n\n        when(mockFtpClient.listFiles(\"this/is/a/directory/\")).thenThrow(new IOException());\n\n        ftpConnection.listFiles();\n    }\n\n    @Test\n    public void whenListingFilesThenFileArrayThatListFilesReturnsShouldBeConvertedToListOfFtpFilesAndReturned()\n            throws IOException {\n\n        List<FTPFile> returnedFiles = ftpConnection.listFiles();\n\n        assertThat(returnedFiles.get(0).getName()).isEqualTo(\"File 1\");\n        assertThat(returnedFiles.get(0).getSize()).isEqualTo(1000l);\n        assertThat(returnedFiles.get(0).getPath()).isEqualTo(\"this/is/a/directory/\");\n        assertThat(returnedFiles.get(0).isDirectory()).isFalse();\n\n        assertThat(returnedFiles.get(1).getName()).isEqualTo(\"File 2\");\n        assertThat(returnedFiles.get(1).getSize()).isEqualTo(2000l);\n        assertThat(returnedFiles.get(1).getPath()).isEqualTo(\"this/is/a/directory/\");\n        assertThat(returnedFiles.get(1).isDirectory()).isTrue();\n\n        assertThat(returnedFiles.get(2).getName()).isEqualTo(\"File 3\");\n        assertThat(returnedFiles.get(2).getSize()).isEqualTo(3000l);\n        assertThat(returnedFiles.get(2).getPath()).isEqualTo(\"this/is/a/directory/\");\n        assertThat(returnedFiles.get(2).isDirectory()).isFalse();\n    }\n\n    @Test\n    public void returnedFtpFilesShouldHaveCorrectModifiedDateTimesAgainstThem() {\n\n        List<FTPFile> files = ftpConnection.listFiles();\n\n        assertThat(files.get(0).getLastModified().toString(\"dd/MM/yyyy HH:mm:ss\")).isEqualTo(\"19/03/2014 21:40:00\");\n        assertThat(files.get(1).getLastModified().toString(\"dd/MM/yyyy HH:mm:ss\")).isEqualTo(\"19/03/2014 21:40:00\");\n        assertThat(files.get(2).getLastModified().toString(\"dd/MM/yyyy HH:mm:ss\")).isEqualTo(\"19/03/2014 21:40:00\");\n    }\n\n    @Test\n    public void whenListingFilesAndGivingRelativePathThenThatPathShouldBeUsedAlongsideCurrentWorkingDir() throws IOException {\n\n        ftpConnection.listFiles(\"relativePath\");\n\n        verify(mockFtpClient).listFiles(\"relativePath/\");\n    }\n\n    @Test\n    public void downloadMethodShouldCreateLocalFileStreamFromCorrectPathBasedOnRemoteFileName() throws FileNotFoundException {\n\n        FTPFile file = new FTPFile(\"remote.file\", 0l, \"path/to\", 0, false);\n        ftpConnection.download(file, LOCAL_DIRECTORY);\n\n        verify(mockFileStreamFactory).createOutputStream(LOCAL_DIRECTORY + \"/remote.file\");\n    }\n\n    @Test\n    public void downloadMethodShouldCreateLocalFileStreamContainingProgressListener() throws IOException {\n\n        FTPFile file = new FTPFile(\"remote.file\", 0l, \"path/to\", 0, false);\n\n        ftpConnection.setProgressListener(new ProgressListener());\n        ftpConnection.download(file, LOCAL_DIRECTORY);\n\n        verify(mockFileStreamFactory).createOutputStream(LOCAL_DIRECTORY + \"/remote.file\");\n        verify(mockFtpClient).retrieveFile(eq(\"path/to/remote.file\"), any(CountingOutputStream.class));\n    }\n\n    @Test\n    public void downloadMethodShouldCallOnFtpClientRetrieveFilesMethodWithRemoteFilename() throws IOException {\n\n        FTPFile file = new FTPFile(\"remote.file\", 0l, \"path/to\", 0, false);\n        ftpConnection.download(file, LOCAL_DIRECTORY);\n\n        verify(mockFtpClient).retrieveFile(\"path/to/remote.file\", mockFileOutputStream);\n    }\n\n    @Test\n    public void downloadMethodShouldThrowExceptionIfUnableToOpenStreamToLocalFile() throws IOException {\n\n        expectedException.expect(DownloadFailedException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to write to local directory \" + LOCAL_DIRECTORY + \"/remote.file\")));\n\n        when(mockFtpClient.retrieveFile(\"path/to/remote.file\", mockFileOutputStream)).thenThrow(new FileNotFoundException());\n\n        FTPFile file = new FTPFile(\"remote.file\", 0l, \"path/to\", 0, false);\n        ftpConnection.download(file, LOCAL_DIRECTORY);\n    }\n\n    @Test\n    public void shouldDownloadFailForAnyReasonWhileInProgressThenCatchIOExceptionAndThrowNewDownloadFailedException()\n            throws IOException {\n\n        expectedException.expect(DownloadFailedException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to download file path/to/remote.file\")));\n\n        when(mockFtpClient.retrieveFile(\"path/to/remote.file\", mockFileOutputStream)).thenThrow(new IOException());\n\n        FTPFile file = new FTPFile(\"remote.file\", 0l, \"path/to\", 0, false);\n        ftpConnection.download(file, LOCAL_DIRECTORY);\n    }\n\n    @Test\n    public void ifRetrieveFileMethodInClientReturnsFalseThenThrowDownloadFailedException() throws IOException {\n\n        expectedException.expect(DownloadFailedException.class);\n        expectedException.expectMessage(is(equalTo(\"Server returned failure while downloading.\")));\n\n        when(mockFtpClient.retrieveFile(\"path/to/remote.file\", mockFileOutputStream)).thenReturn(false);\n\n        FTPFile file = new FTPFile(\"remote.file\", 0l, \"path/to\", 0, false);\n        ftpConnection.download(file, LOCAL_DIRECTORY);\n    }\n\n    @Test\n    public void printingWorkingDirectoryShouldCallOnUnderlyingClientMethodToGetCurrentDirectory() throws IOException {\n\n        ftpConnection.currentDirectory();\n\n        verify(mockFtpClient).printWorkingDirectory();\n    }\n\n    @Test\n    public void printingWorkingDirectoryShouldReturnExactlyWhatTheUnderlyingClientReturns() {\n        assertThat(ftpConnection.currentDirectory()).isEqualTo(DIRECTORY_PATH);\n    }\n\n    @Test\n    public void ifClientThrowsExceptionWhenTryingToGetWorkingDirectoryThenCatchExceptionAndRethrow() throws IOException {\n\n        expectedException.expect(FileListingException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to print the working directory\")));\n\n        when(mockFtpClient.printWorkingDirectory()).thenThrow(new IOException());\n\n        ftpConnection.currentDirectory();\n    }\n\n    @Test\n    public void shouldDeleteRemoteFile() throws IOException {\n\n        FTPFile file = new FTPFile(\"file.name\", 0, \"/some/directory\", 0, false);\n\n        ftpConnection.deleteRemoteFile(file);\n\n        verify(mockFtpClient).deleteFile(\"/some/directory/file.name\");\n    }\n\n    @Test\n    public void ifDeleteFailsThenExceptionShouldBeThrown() throws IOException {\n\n        expectedException.expect(FTPException.class);\n        expectedException.expectMessage(equalTo(\"Unable to delete file on remote server. Unknown reason\"));\n        \n        FTPFile file = new FTPFile(\"file.name\", 0, \"/some/directory\", 0, false);\n\n        when(mockFtpClient.deleteFile(anyString())).thenReturn(false);\n        ftpConnection.deleteRemoteFile(file);\n    }\n    \n    @Test\n    public void ifDeleteThrowsExceptionItShouldBeCaughtAndRethrown() throws IOException {\n        \n        expectedException.expect(FTPException.class);\n        expectedException.expectMessage(equalTo(\"Unable to delete file on remote server\"));\n        \n        FTPFile file = new FTPFile(\"file.name\", 0, \"/some/directory\", 0, false);\n\n        when(mockFtpClient.deleteFile(anyString())).thenThrow(new IOException());\n        ftpConnection.deleteRemoteFile(file);\n    }\n    \n    @Test\n    public void shouldRecursivelyDeleteRemoteFileIfItIsADirectoryWithContents() throws IOException {\n        \n        initRecursiveListings();\n        \n        ftpConnection.deleteRemoteFile(new FTPFile(\"folder\", 0, \"path/to\", 0, true));\n        \n        InOrder inOrder = Mockito.inOrder(mockFtpClient);\n        \n        inOrder.verify(mockFtpClient).listFiles(\"path/to/folder/\");\n        inOrder.verify(mockFtpClient).deleteFile(\"path/to/folder/file1.txt\");\n        inOrder.verify(mockFtpClient).deleteFile(\"path/to/folder/file2.txt\");\n        inOrder.verify(mockFtpClient).listFiles(\"path/to/folder/directory1/\");\n        inOrder.verify(mockFtpClient).deleteFile(\"path/to/folder/directory1/file3.txt\");\n        inOrder.verify(mockFtpClient).listFiles(\"path/to/folder/directory1/directory2/\");\n        inOrder.verify(mockFtpClient).deleteFile(\"path/to/folder/directory1/directory2/file5.txt\");\n        inOrder.verify(mockFtpClient).deleteFile(\"path/to/folder/directory1/directory2/file6.txt\");\n        inOrder.verify(mockFtpClient).removeDirectory(\"path/to/folder/directory1/directory2\");\n        inOrder.verify(mockFtpClient).deleteFile(\"path/to/folder/directory1/file4.txt\");\n        inOrder.verify(mockFtpClient).removeDirectory(\"path/to/folder/directory1\");\n        inOrder.verify(mockFtpClient).removeDirectory(\"path/to/folder\");\n    }\n\n    @Test\n    public void downloadShouldRecursivelyCheckFileIfFolderThenLsThatAndGetOnlyFiles() throws IOException {\n\n        initRecursiveListings();\n\n        FileOutputStream stream1 = mock(FileOutputStream.class);\n        FileOutputStream stream2 = mock(FileOutputStream.class);\n        FileOutputStream stream3 = mock(FileOutputStream.class);\n        FileOutputStream stream4 = mock(FileOutputStream.class);\n        FileOutputStream stream5 = mock(FileOutputStream.class);\n        FileOutputStream stream6 = mock(FileOutputStream.class);\n\n        when(mockFileStreamFactory.createOutputStream(\"some/directory/folder/file1.txt\")).thenReturn(stream1);\n        when(mockFileStreamFactory.createOutputStream(\"some/directory/folder/file2.txt\")).thenReturn(stream2);\n        when(mockFileStreamFactory.createOutputStream(\"some/directory/folder/directory1/file3.txt\")).thenReturn(stream3);\n        when(mockFileStreamFactory.createOutputStream(\"some/directory/folder/directory1/directory2/file5.txt\"))\n                .thenReturn(stream4);\n        when(mockFileStreamFactory.createOutputStream(\"some/directory/folder/directory1/directory2/file6.txt\"))\n                .thenReturn(stream5);\n        when(mockFileStreamFactory.createOutputStream(\"some/directory/folder/directory1/file4.txt\")).thenReturn(stream6);\n\n        FTPFile directory = new FTPFile(\"folder\", 0, \"path/to\", 0, true);\n        ftpConnection.download(directory, \"some/directory\");\n\n        verify(mockFileUtils).createLocalDirectory(\"some/directory/folder/\");\n        verify(mockFtpClient).listFiles(\"path/to/folder/\");\n\n        verify(mockFileUtils).createLocalDirectory(\"some/directory/folder/directory1/\");\n        verify(mockFtpClient).listFiles(\"path/to/folder/directory1/\");\n\n        verify(mockFileUtils).createLocalDirectory(\"some/directory/folder/directory1/directory2/\");\n        verify(mockFtpClient).listFiles(\"path/to/folder/directory1/directory2/\");\n\n        InOrder inOrder = Mockito.inOrder(mockFtpClient, stream1, stream2, stream3, stream4, stream5, stream6);\n\n        inOrder.verify(mockFtpClient).retrieveFile(\"path/to/folder/file1.txt\", stream1);\n        inOrder.verify(stream1).close();\n        inOrder.verify(mockFtpClient).retrieveFile(\"path/to/folder/file2.txt\", stream2);\n        inOrder.verify(stream2).close();\n        inOrder.verify(mockFtpClient).retrieveFile(\"path/to/folder/directory1/file3.txt\", stream3);\n        inOrder.verify(stream3).close();\n        inOrder.verify(mockFtpClient).retrieveFile(\"path/to/folder/directory1/directory2/file5.txt\", stream4);\n        inOrder.verify(stream4).close();\n        inOrder.verify(mockFtpClient).retrieveFile(\"path/to/folder/directory1/directory2/file6.txt\", stream5);\n        inOrder.verify(stream5).close();\n        inOrder.verify(mockFtpClient).retrieveFile(\"path/to/folder/directory1/file4.txt\", stream6);\n        inOrder.verify(stream6).close();\n    }\n\n    private void initRecursiveListings() throws IOException {\n\n        org.apache.commons.net.ftp.FTPFile[] entries = new org.apache.commons.net.ftp.FTPFile[5];\n\n        entries[0] = (createSingleEntry(\".\", 123l, 1394525265, true));\n        entries[1] = (createSingleEntry(\"..\", 123l, 1394525265, true));\n        entries[2] = (createSingleEntry(\"file1.txt\", 123l, 1394525265, false));\n        entries[3] = (createSingleEntry(\"file2.txt\", 456l, 1394652161, false));\n        entries[4] = (createSingleEntry(\"directory1\", 789l, 1391879364, true));\n\n        when(mockFtpClient.listFiles(\"path/to/folder/\")).thenReturn(entries);\n\n        org.apache.commons.net.ftp.FTPFile[] subEntries = new org.apache.commons.net.ftp.FTPFile[5];\n\n        subEntries[0] = (createSingleEntry(\".\", 123l, 1394525265, true));\n        subEntries[1] = (createSingleEntry(\"..\", 123l, 1394525265, true));\n        subEntries[2] = (createSingleEntry(\"file3.txt\", 789l, 1394525265, false));\n        subEntries[3] = (createSingleEntry(\"directory2\", 789l, 1394525265, true));\n        subEntries[4] = (createSingleEntry(\"file4.txt\", 789l, 1394525265, false));\n\n        when(mockFtpClient.listFiles(\"path/to/folder/directory1/\")).thenReturn(subEntries);\n\n        org.apache.commons.net.ftp.FTPFile[] subSubEntries = new org.apache.commons.net.ftp.FTPFile[4];\n\n        subSubEntries[0] = (createSingleEntry(\".\", 123l, 1394525265, true));\n        subSubEntries[1] = (createSingleEntry(\"..\", 123l, 1394525265, true));\n        subSubEntries[2] = (createSingleEntry(\"file5.txt\", 789l, 1394525265, false));\n        subSubEntries[3] = (createSingleEntry(\"file6.txt\", 789l, 1394525265, false));\n\n        when(mockFtpClient.listFiles(\"path/to/folder/directory1/directory2/\")).thenReturn(subSubEntries);\n    }\n\n    private org.apache.commons.net.ftp.FTPFile createSingleEntry(String fileName, long size, int mTime, boolean directory) {\n\n        org.apache.commons.net.ftp.FTPFile file = mock(org.apache.commons.net.ftp.FTPFile.class);\n\n        Calendar calendar = Calendar.getInstance();\n        calendar.setTime(new Date(mTime));\n\n        when(file.getName()).thenReturn(fileName);\n        when(file.getTimestamp()).thenReturn(calendar);\n        when(file.getSize()).thenReturn(size);\n        when(file.isDirectory()).thenReturn(directory);\n\n        return file;\n    }\n\n    private org.apache.commons.net.ftp.FTPFile[] createRemoteFTPFiles() {\n\n        Calendar calendar = Calendar.getInstance();\n        calendar.set(2014, 2, 19, 21, 40, 00);\n\n        org.apache.commons.net.ftp.FTPFile[] files = new org.apache.commons.net.ftp.FTPFile[5];\n\n        org.apache.commons.net.ftp.FTPFile currentDir = mock(org.apache.commons.net.ftp.FTPFile.class);\n        when(currentDir.getName()).thenReturn(\".\");\n        when(currentDir.getTimestamp()).thenReturn(calendar);\n\n        org.apache.commons.net.ftp.FTPFile parentDir = mock(org.apache.commons.net.ftp.FTPFile.class);\n        when(parentDir.getName()).thenReturn(\"..\");\n        when(parentDir.getTimestamp()).thenReturn(calendar);\n\n        files[0] = currentDir;\n        files[1] = parentDir;\n\n        for (int i = 2; i < 5; i++) {\n\n            org.apache.commons.net.ftp.FTPFile file = mock(org.apache.commons.net.ftp.FTPFile.class);\n\n            when(file.getName()).thenReturn(\"File \" + (i - 1));\n            when(file.getSize()).thenReturn((long) (i - 1) * 1000);\n            when(file.getTimestamp()).thenReturn(calendar);\n            when(file.isDirectory()).thenReturn(setTrueIfNumberIsEven(i));\n\n            files[i] = file;\n        }\n\n        return files;\n    }\n\n    private boolean setTrueIfNumberIsEven(int i) {\n        return (i + 1) % 2 == 0 ? true : false;\n    }\n}"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/connection/SFTPConnectionTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.List;\nimport java.util.Vector;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.InOrder;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\n\nimport com.jcraft.jsch.ChannelSftp;\nimport com.jcraft.jsch.ChannelSftp.LsEntry;\nimport com.jcraft.jsch.SftpATTRS;\nimport com.jcraft.jsch.SftpException;\n\nimport io.linuxserver.davos.transfer.ftp.FTPFile;\nimport io.linuxserver.davos.transfer.ftp.connection.progress.SFTPProgressListener;\nimport io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;\nimport io.linuxserver.davos.transfer.ftp.exception.FTPException;\nimport io.linuxserver.davos.transfer.ftp.exception.FileListingException;\nimport io.linuxserver.davos.util.FileUtils;\n\npublic class SFTPConnectionTest {\n\n    @InjectMocks\n    private SFTPConnection sftpConnection;\n\n    @Mock\n    private FileUtils mockFileUtils;\n\n    private ChannelSftp mockChannel;\n\n    @Rule\n    public ExpectedException expectedException = ExpectedException.none();\n\n    @Before\n    public void setUp() throws SftpException {\n\n        mockChannel = mock(ChannelSftp.class);\n\n        Vector<LsEntry> lsEntries = createEntries();\n\n        when(mockChannel.ls(anyString())).thenReturn(lsEntries);\n        when(mockChannel.pwd()).thenReturn(\"a/directory\");\n\n        sftpConnection = new SFTPConnection(mockChannel);\n\n        initMocks(this);\n    }\n\n    @Test\n    public void listFilesMethodShouldCallOnChannelLsMethodForPresentDirectory() throws SftpException {\n\n        sftpConnection.listFiles();\n\n        verify(mockChannel).ls(\"a/directory/\");\n    }\n\n    @Test\n    public void whenListingFilesGivingRelativePathThenChannelLsMethodShouldUseGivenPath() throws SftpException {\n\n        sftpConnection.listFiles(\"some/other/path\");\n\n        verify(mockChannel).ls(\"some/other/path/\");\n    }\n\n    @Test\n    public void ifUnderlyingChannelIsUnableToListFilesInPWDThenExceptionShouldBeCaughtAndRethrown() throws SftpException {\n\n        expectedException.expect(FileListingException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to list files in directory a/directory\")));\n\n        when(mockChannel.ls(\"a/directory/\")).thenThrow(new SftpException(0, \"\"));\n\n        sftpConnection.listFiles();\n    }\n\n    @Test\n    public void lsEntriesReturnedFromChannelShouldBeParsedIntoFtpFileAndReturnedInList() {\n\n        List<FTPFile> files = sftpConnection.listFiles();\n\n        assertThat(files.get(0).getName()).isEqualTo(\"File 1\");\n        assertThat(files.get(0).getSize()).isEqualTo(123l);\n        assertThat(files.get(0).getPath()).isEqualTo(\"a/directory/\");\n        assertThat(files.get(0).isDirectory()).isTrue();\n\n        assertThat(files.get(1).getName()).isEqualTo(\"File 2\");\n        assertThat(files.get(1).getSize()).isEqualTo(456l);\n        assertThat(files.get(1).getPath()).isEqualTo(\"a/directory/\");\n        assertThat(files.get(1).isDirectory()).isFalse();\n\n        assertThat(files.get(2).getName()).isEqualTo(\"File 3\");\n        assertThat(files.get(2).getSize()).isEqualTo(789l);\n        assertThat(files.get(2).getPath()).isEqualTo(\"a/directory/\");\n        assertThat(files.get(2).isDirectory()).isTrue();\n    }\n\n    @Test\n    public void returnedFtpFilesShouldHaveCorrectModifiedDateTimesAgainstThem() {\n\n        List<FTPFile> files = sftpConnection.listFiles();\n\n        assertThat(files.get(0).getLastModified().toString(\"dd/MM/yyyy HH:mm:ss\")).isEqualTo(\"11/03/2014 08:07:45\");\n        assertThat(files.get(1).getLastModified().toString(\"dd/MM/yyyy HH:mm:ss\")).isEqualTo(\"12/03/2014 19:22:41\");\n        assertThat(files.get(2).getLastModified().toString(\"dd/MM/yyyy HH:mm:ss\")).isEqualTo(\"08/02/2014 17:09:24\");\n    }\n\n    @Test\n    public void printingWorkingDirectoryShouldCallOnUnderlyingClientMethodToGetCurrentDirectory() throws SftpException {\n\n        sftpConnection.currentDirectory();\n\n        verify(mockChannel).pwd();\n    }\n\n    @Test\n    public void printingWorkingDirectoryShouldReturnExactlyWhatTheUnderlyingClientReturns() {\n\n        assertThat(sftpConnection.currentDirectory()).isEqualTo(\"a/directory\");\n    }\n\n    @Test\n    public void ifClientThrowsExceptionWhenTryingToGetWorkingDirectoryThenCatchExceptionAndRethrow() throws SftpException {\n\n        expectedException.expect(FileListingException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to print the working directory\")));\n\n        when(mockChannel.pwd()).thenThrow(new SftpException(0, \"\"));\n\n        sftpConnection.currentDirectory();\n    }\n\n    @Test\n    public void downloadMethodShouldCallChannelGetMethodWithFtpFileNameAndDirectory() throws SftpException {\n\n        FTPFile file = new FTPFile(\"name\", 0, \"path\", 0, false);\n        sftpConnection.download(file, \"some/directory\");\n\n        verify(mockChannel).get(\"path/name\", \"some/directory/\");\n    }\n\n    @Test\n    public void downloadMethodShouldCallChannelGetMethodWithListenerIfSet() throws SftpException {\n\n        FTPFile file = new FTPFile(\"name\", 0, \"path\", 0, false);\n        SFTPProgressListener progressListener = new SFTPProgressListener();\n\n        sftpConnection.setProgressListener(progressListener);\n        sftpConnection.download(file, \"some/directory\");\n\n        verify(mockChannel).get(\"path/name\", \"some/directory/\", progressListener);\n        verify(mockChannel, never()).get(\"path/name\", \"some/directory/\");\n    }\n\n    @Test\n    public void downloadMethodShouldThrowDownloadFailedExceptionWhenChannelThrowsSftpConnection() throws SftpException {\n\n        expectedException.expect(DownloadFailedException.class);\n        expectedException.expectMessage(is(equalTo(\"Unable to download file path/to/file.txt\")));\n\n        doThrow(new SftpException(999, \"\")).when(mockChannel).get(\"path/to/file.txt\", \"some/directory/\");\n\n        sftpConnection.download(new FTPFile(\"file.txt\", 0, \"path/to\", 0, false), \"some/directory\");\n    }\n\n    @Test\n    public void downloadShouldRecursivelyCheckFileIfFolderThenLsThatAndGetOnlyFiles() throws SftpException {\n\n        initRecursiveListings();\n\n        FTPFile directory = new FTPFile(\"folder\", 0, \"path/to\", 0, true);\n\n        sftpConnection.download(directory, \"some/directory\");\n\n        verify(mockFileUtils).createLocalDirectory(\"some/directory/folder/\");\n        verify(mockChannel).ls(\"path/to/folder/\");\n\n        verify(mockFileUtils).createLocalDirectory(\"some/directory/folder/directory1/\");\n        verify(mockChannel).ls(\"path/to/folder/directory1/\");\n\n        verify(mockFileUtils).createLocalDirectory(\"some/directory/folder/directory1/directory2/\");\n        verify(mockChannel).ls(\"path/to/folder/directory1/directory2/\");\n\n        InOrder inOrder = Mockito.inOrder(mockChannel);\n\n        inOrder.verify(mockChannel).get(\"path/to/folder/file1.txt\", \"some/directory/folder/\");\n        inOrder.verify(mockChannel).get(\"path/to/folder/file2.txt\", \"some/directory/folder/\");\n        inOrder.verify(mockChannel).get(\"path/to/folder/directory1/file3.txt\", \"some/directory/folder/directory1/\");\n        inOrder.verify(mockChannel).get(\"path/to/folder/directory1/directory2/file5.txt\",\n                \"some/directory/folder/directory1/directory2/\");\n        inOrder.verify(mockChannel).get(\"path/to/folder/directory1/directory2/file6.txt\",\n                \"some/directory/folder/directory1/directory2/\");\n        inOrder.verify(mockChannel).get(\"path/to/folder/directory1/file4.txt\", \"some/directory/folder/directory1/\");\n    }\n\n    @Test\n    public void shouldRecursivelyDeleteRemoteFileIfItIsADirectoryAndHasContents() throws SftpException {\n\n        initRecursiveListings();\n\n        sftpConnection.deleteRemoteFile(new FTPFile(\"folder\", 0, \"path/to\", 0, true));\n\n        InOrder inOrder = Mockito.inOrder(mockChannel);\n\n        inOrder.verify(mockChannel).ls(\"path/to/folder/\");\n        inOrder.verify(mockChannel).rm(\"path/to/folder/file1.txt\");\n        inOrder.verify(mockChannel).rm(\"path/to/folder/file2.txt\");\n        inOrder.verify(mockChannel).ls(\"path/to/folder/directory1/\");\n        inOrder.verify(mockChannel).rm(\"path/to/folder/directory1/file3.txt\");\n        inOrder.verify(mockChannel).ls(\"path/to/folder/directory1/directory2/\");\n        inOrder.verify(mockChannel).rm(\"path/to/folder/directory1/directory2/file5.txt\");\n        inOrder.verify(mockChannel).rm(\"path/to/folder/directory1/directory2/file6.txt\");\n        inOrder.verify(mockChannel).rmdir(\"path/to/folder/directory1/directory2\");\n        inOrder.verify(mockChannel).rm(\"path/to/folder/directory1/file4.txt\");\n        inOrder.verify(mockChannel).rmdir(\"path/to/folder/directory1\");\n        inOrder.verify(mockChannel).rmdir(\"path/to/folder\");\n    }\n\n    @Test\n    public void shouldDeleteRemoteFile() throws SftpException {\n\n        FTPFile file = new FTPFile(\"file.name\", 0, \"/some/directory\", 0, false);\n\n        sftpConnection.deleteRemoteFile(file);\n\n        verify(mockChannel).rm(\"/some/directory/file.name\");\n    }\n\n    @Test\n    public void shouldDeleteRemoteDirectory() throws SftpException {\n\n        when(mockChannel.ls(\"/some/directory/file.name/\")).thenReturn(new Vector<LsEntry>());\n        \n        FTPFile file = new FTPFile(\"file.name\", 0, \"/some/directory\", 0, true);\n\n        sftpConnection.deleteRemoteFile(file);\n\n        verify(mockChannel).rmdir(\"/some/directory/file.name\");\n    }\n\n    @Test\n    public void shouldCatchAndRethrowExceptionIfCaught() throws SftpException {\n\n        expectedException.expect(FTPException.class);\n        expectedException.expectMessage(equalTo(\"Unable to delete file on remote server\"));\n\n        when(mockChannel.ls(\"/some/directory/file.name/\")).thenReturn(new Vector<LsEntry>());\n        \n        FTPFile file = new FTPFile(\"file.name\", 0, \"/some/directory\", 0, true);\n\n        doThrow(new SftpException(0, \"\")).when(mockChannel).rmdir(\"/some/directory/file.name\");\n        sftpConnection.deleteRemoteFile(file);\n    }\n\n    private void initRecursiveListings() throws SftpException {\n\n        Vector<LsEntry> entries = new Vector<LsEntry>();\n\n        entries.add(createSingleEntry(\".\", 123l, 1394525265, true));\n        entries.add(createSingleEntry(\"..\", 123l, 1394525265, true));\n        entries.add(createSingleEntry(\"file1.txt\", 123l, 1394525265, false));\n        entries.add(createSingleEntry(\"file2.txt\", 456l, 1394652161, false));\n        entries.add(createSingleEntry(\"directory1\", 789l, 1391879364, true));\n\n        when(mockChannel.ls(\"path/to/folder/\")).thenReturn(entries);\n\n        Vector<LsEntry> subEntries = new Vector<LsEntry>();\n\n        subEntries.add(createSingleEntry(\".\", 123l, 1394525265, true));\n        subEntries.add(createSingleEntry(\"..\", 123l, 1394525265, true));\n        subEntries.add(createSingleEntry(\"file3.txt\", 789l, 1394525265, false));\n        subEntries.add(createSingleEntry(\"directory2\", 789l, 1394525265, true));\n        subEntries.add(createSingleEntry(\"file4.txt\", 789l, 1394525265, false));\n\n        when(mockChannel.ls(\"path/to/folder/directory1/\")).thenReturn(subEntries);\n\n        Vector<LsEntry> subSubEntries = new Vector<LsEntry>();\n\n        subSubEntries.add(createSingleEntry(\".\", 123l, 1394525265, true));\n        subSubEntries.add(createSingleEntry(\"..\", 123l, 1394525265, true));\n        subSubEntries.add(createSingleEntry(\"file5.txt\", 789l, 1394525265, false));\n        subSubEntries.add(createSingleEntry(\"file6.txt\", 789l, 1394525265, false));\n\n        when(mockChannel.ls(\"path/to/folder/directory1/directory2/\")).thenReturn(subSubEntries);\n    }\n\n    private Vector<LsEntry> createEntries() {\n\n        Vector<LsEntry> vector = new Vector<LsEntry>();\n\n        vector.add(createSingleEntry(\"File 1\", 123l, 1394525265, true));\n        vector.add(createSingleEntry(\"File 2\", 456l, 1394652161, false));\n        vector.add(createSingleEntry(\"File 3\", 789l, 1391879364, true));\n\n        return vector;\n    }\n\n    private LsEntry createSingleEntry(String fileName, long size, int mTime, boolean directory) {\n\n        SftpATTRS attributes = mock(SftpATTRS.class);\n        when(attributes.getSize()).thenReturn(size);\n        when(attributes.getMTime()).thenReturn(mTime);\n\n        LsEntry entry = mock(LsEntry.class);\n        when(entry.getAttrs()).thenReturn(attributes);\n        when(entry.getFilename()).thenReturn(fileName);\n        when(entry.getAttrs().isDir()).thenReturn(directory);\n\n        return entry;\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/connection/progress/ListenerFactoryTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection.progress;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.transfer.ftp.TransferProtocol;\n\npublic class ListenerFactoryTest {\n\n    @Test\n    public void shouldReturnCorrectListener() {\n        \n        ListenerFactory listenerFactory = new ListenerFactory();\n        \n        assertThat(listenerFactory.createListener(TransferProtocol.FTP)).isInstanceOf(ProgressListener.class);\n        assertThat(listenerFactory.createListener(TransferProtocol.FTPS)).isInstanceOf(ProgressListener.class);\n        assertThat(listenerFactory.createListener(TransferProtocol.SFTP)).isInstanceOf(SFTPProgressListener.class);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/connection/progress/ProgressListenerTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection.progress;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\npublic class ProgressListenerTest {\n\n    @Test\n    public void shouldGiveCorrectSpeed() throws InterruptedException {\n\n        ProgressListener listener = new ProgressListener();\n\n        listener.start();\n        Thread.sleep(1000);\n        listener.setBytesWritten(1000000);\n\n        assertThat(listener.getTransferSpeed()).isBetween(0.9, 1.1);\n    }\n\n    @Test\n    public void shouldGiveCorrectSpeedWhenAlternating() throws InterruptedException {\n\n        ProgressListener listener = new ProgressListener();\n\n        listener.start();\n        Thread.sleep(1000);\n        listener.setBytesWritten(1000000);\n        assertThat(listener.getTransferSpeed()).isBetween(0.9, 1.1);\n\n        Thread.sleep(1000);\n        listener.setBytesWritten(1500000);\n        assertThat(listener.getTransferSpeed()).isBetween(0.45, 0.51);\n    }\n\n    @Test\n    public void shouldReturn100IfTotalSizeIsZero() {\n\n        ProgressListener listener = new ProgressListener();\n\n        listener.start();\n        listener.setTotalBytes(0);\n        listener.setBytesWritten(0);\n        \n        assertThat(listener.getProgress()).isEqualTo(100);\n    }\n    \n    @Test\n    public void shouldReturn0IfTotalBytesWrittenIsZero() {\n\n        ProgressListener listener = new ProgressListener();\n\n        listener.start();\n        listener.setTotalBytes(110);\n        listener.setBytesWritten(0);\n        \n        assertThat(listener.getProgress()).isEqualTo(0);\n    }\n\n    @Test\n    public void shouldShowProgress() {\n\n        ProgressListener listener = new ProgressListener();\n\n        listener.setTotalBytes(2000);\n\n        listener.setBytesWritten(500);\n        assertThat(listener.getProgress()).isEqualTo(25);\n\n        listener.setBytesWritten(1000);\n        assertThat(listener.getProgress()).isEqualTo(50);\n\n        listener.setBytesWritten(2000);\n        assertThat(listener.getProgress()).isEqualTo(100);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/transfer/ftp/connection/progress/SFTPProgressListenerTest.java",
    "content": "package io.linuxserver.davos.transfer.ftp.connection.progress;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\nimport io.linuxserver.davos.transfer.ftp.connection.progress.SFTPProgressListener;\n\npublic class SFTPProgressListenerTest {\n\n    @Test\n    public void shouldReturnCorrectProgress() {\n        \n        SFTPProgressListener listener = new SFTPProgressListener();\n        \n        listener.init(0, \"\", \"\", 500);\n\n        listener.count(100);\n        assertThat(listener.getProgress()).isEqualTo(20);\n        \n        listener.count(250);\n        assertThat(listener.getProgress()).isEqualTo(70);\n        \n        listener.count(150);\n        assertThat(listener.getProgress()).isEqualTo(100);\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/util/PatternBuilderTest.java",
    "content": "package io.linuxserver.davos.util;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\npublic class PatternBuilderTest {\n\n    @Test\n    public void builderShouldTurnQuestionMarksIntoSingleCharacterRegexMatcher() {\n\n        String filter = \"This?is?a filter\";\n        String expected = \"This.{1}is.{1}a filter\";\n\n        assertThat(PatternBuilder.buildFromFilterString(filter)).isEqualTo(expected);\n    }\n\n    @Test\n    public void builderShouldTurnAsterixesIntoManyCharacterRegexMatcher() {\n\n        String filter = \"This*is*a filter\";\n        String expected = \"This.*is.*a filter\";\n\n        assertThat(PatternBuilder.buildFromFilterString(filter)).isEqualTo(expected);\n    }\n\n    @Test\n    public void regexStringReturnedShouldBeAbleToActuallyMatchUsingRegexOperation() {\n\n        String normalValue = \"Clean Code.pdf\";\n        String filteredValue = \"Clean?Code*\";\n\n        assertThat(normalValue.matches(PatternBuilder.buildFromFilterString(filteredValue))).isTrue();\n    }\n\n    @Test\n    public void stringWithBothAsterixAndQuestionMarkShouldMatchProperly() {\n\n        String anotherValue = \"File Name with a Prefix12Then some text\";\n        String slightlyMoreComplicated = \"File?Name*Prefix??Then some text\";\n\n        assertThat(anotherValue.matches(PatternBuilder.buildFromFilterString(slightlyMoreComplicated))).isTrue();\n    }\n    \n    @Test\n    public void dotsShouldBeTreatedVerbatim() {\n        \n        String normalValue = \"Clean Code.pdf\";\n        String filteredValue = \"Clean?Code.pdf\";\n        \n        assertThat(normalValue.matches(PatternBuilder.buildFromFilterString(filteredValue))).isTrue();\n        assertThat(\"Clean Code_pdf\".matches(PatternBuilder.buildFromFilterString(filteredValue))).isFalse();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/web/controller/APIControllerTest.java",
    "content": "package io.linuxserver.davos.web.controller;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\n\nimport io.linuxserver.davos.delegation.services.HostService;\nimport io.linuxserver.davos.delegation.services.ScheduleService;\nimport io.linuxserver.davos.web.Host;\nimport io.linuxserver.davos.web.Schedule;\nimport io.linuxserver.davos.web.controller.response.APIResponse;\n\npublic class APIControllerTest {\n\n    @InjectMocks\n    private APIController controller = new APIController();\n\n    @Mock\n    private ScheduleService mockScheduleFacade;\n\n    @Mock\n    private HostService mockHostFacade;\n\n    @Before\n    public void before() {\n        initMocks(this);\n    }\n\n    @Test\n    public void createScheduleShouldCallFacadeMethod() {\n\n        Schedule schedule = new Schedule();\n\n        controller.createSchedule(schedule);\n\n        verify(mockScheduleFacade).createSchedule(schedule);\n    }\n\n    @Test\n    public void onSuccessNewScheduleShouldBeReturnedWhenCreated() {\n\n        Schedule newSchedule = new Schedule();\n        Schedule schedule = new Schedule();\n\n        when(mockScheduleFacade.createSchedule(schedule)).thenReturn(newSchedule);\n\n        ResponseEntity<APIResponse> response = controller.createSchedule(schedule);\n\n        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);\n        assertThat(response.getBody().body).isEqualTo(newSchedule);\n    }\n\n    @Test\n    public void updateScheduleShouldCallFacadeWithIdInMethod() {\n\n        Schedule schedule = new Schedule();\n\n        controller.updateSchedule(1L, schedule);\n\n        verify(mockScheduleFacade).updateSchedule(schedule);\n\n        assertThat(schedule.getId()).isEqualTo(1L);\n    }\n\n    @Test\n    public void onSuccessUpdatedScheduleShouldBeReturnedWhenSaved() {\n\n        Schedule schedule = new Schedule();\n\n        when(mockScheduleFacade.updateSchedule(schedule)).thenReturn(schedule);\n\n        ResponseEntity<APIResponse> response = controller.updateSchedule(1L, schedule);\n\n        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);\n        assertThat(response.getBody().body).isEqualTo(schedule);\n    }\n\n    @Test\n    public void deleteScheduleShouldCallFacade() {\n\n        ResponseEntity<APIResponse> response = controller.deleteSchedule(1L);\n\n        verify(mockScheduleFacade).deleteSchedule(1L);\n\n        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);\n        assertThat(response.getBody().body).isNull();\n    }\n\n    @Test\n    public void createHostShouldCallFacade() {\n\n        Host host = new Host();\n\n        controller.createHost(host);\n\n        verify(mockHostFacade).saveHost(host);\n    }\n\n    @Test\n    public void saveHostShouldReturnResponse() {\n\n        Host host = new Host();\n        Host createdHost = new Host();\n        \n        when(mockHostFacade.saveHost(host)).thenReturn(createdHost);\n        \n        ResponseEntity<APIResponse> response = controller.createHost(host);\n        \n        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);\n        assertThat(response.getBody().body).isEqualTo(createdHost);\n    }\n    \n    @Test\n    public void updateHostShouldCallFacadeWithIdInMethod() {\n\n        Host host = new Host();\n\n        controller.updateHost(1L, host);\n\n        verify(mockHostFacade).saveHost(host);\n\n        assertThat(host.getId()).isEqualTo(1L);\n    }\n\n    @Test\n    public void onSuccessUpdatedHostShouldBeReturnedWhenSaved() {\n\n        Host host = new Host();\n\n        when(mockHostFacade.saveHost(host)).thenReturn(host);\n\n        ResponseEntity<APIResponse> response = controller.updateHost(1L, host);\n\n        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);\n        assertThat(response.getBody().body).isEqualTo(host);\n    }\n    \n    @Test\n    public void deleteHostShouldCallFacade() {\n\n        ResponseEntity<APIResponse> response = controller.deleteHost(1L);\n\n        verify(mockHostFacade).deleteHost(1L);\n\n        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);\n        assertThat(response.getBody().body).isNull();\n    }\n}\n"
  },
  {
    "path": "src/test/java/io/linuxserver/davos/web/controller/ViewControllerTest.java",
    "content": "package io.linuxserver.davos.web.controller;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport java.util.ArrayList;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.springframework.ui.Model;\n\nimport io.linuxserver.davos.delegation.services.HostService;\nimport io.linuxserver.davos.delegation.services.ScheduleService;\nimport io.linuxserver.davos.delegation.services.SettingsService;\nimport io.linuxserver.davos.web.Host;\nimport io.linuxserver.davos.web.Schedule;\nimport io.linuxserver.davos.web.selectors.LogLevelSelector;\n\npublic class ViewControllerTest {\n\n    @InjectMocks\n    private ViewController controller = new ViewController();\n    \n    @Mock\n    private HostService mockHostFacade;\n    \n    @Mock\n    private ScheduleService mockScheduleFacade;\n    \n    @Mock\n    private SettingsService mockSettingsService;\n    \n    @Mock\n    private Model mockModel;\n    \n    @Before\n    public void before() {\n        initMocks(this);\n        \n        when(mockSettingsService.getCurrentLoggingLevel()).thenReturn(LogLevelSelector.DEBUG);\n    }\n    \n    @Test\n    public void viewsShouldResolveCorrectly() {\n        \n        assertThat(controller.index()).isEqualTo(\"redirect:/schedules\");\n        assertThat(controller.settings(mockModel)).isEqualTo(\"v2/settings\");\n        assertThat(controller.schedules(mockModel)).isEqualTo(\"v2/schedules\");\n        assertThat(controller.schedules(1L, mockModel)).isEqualTo(\"v2/edit-schedule\");\n        assertThat(controller.newSchedule(mockModel)).isEqualTo(\"v2/edit-schedule\");\n        assertThat(controller.hosts()).isEqualTo(\"v2/hosts\");\n        assertThat(controller.newHost(mockModel)).isEqualTo(\"v2/edit-host\");\n        assertThat(controller.hosts(1L, mockModel)).isEqualTo(\"v2/edit-host\");\n    }\n    \n    @Test \n    public void schedulesShouldAddAllSchedulesToModel() {\n        \n        ArrayList<Schedule> schedules = new ArrayList<Schedule>();\n        schedules.add(new Schedule());\n        \n        when(mockScheduleFacade.fetchAllSchedules()).thenReturn(schedules);\n        \n        controller.schedules(mockModel);\n        \n        verify(mockModel).addAttribute(\"schedules\", schedules);\n    }\n    \n    @Test \n    public void schedulesWithIdShouldAddSpecificScheduleToModel() {\n        \n        Schedule schedule = new Schedule();\n        \n        when(mockScheduleFacade.fetchSchedule(1L)).thenReturn(schedule);\n        \n        controller.schedules(1L, mockModel);\n        \n        verify(mockModel).addAttribute(\"schedule\", schedule);\n    }\n    \n    @Test \n    public void newScheduleShouldAddScheduleToModel() {\n        \n        controller.newSchedule(mockModel);\n        \n        verify(mockModel).addAttribute(eq(\"schedule\"), any(Schedule.class));\n    }\n    \n    @Test\n    public void allHostsShouldAddHostsToModel() {\n        \n        controller.allHosts();\n        \n        verify(mockHostFacade).fetchAllHosts();\n    }\n    \n    @Test \n    public void newHostShouldAddNewHostToModel() {\n        \n        controller.newHost(mockModel);\n        \n        verify(mockModel).addAttribute(eq(\"host\"), any(Host.class));\n    }\n    \n    @Test\n    public void hostsWithIdShouldAddSpecificHostToModel() {\n        \n        Host host = new Host();\n        when(mockHostFacade.fetchHost(1L)).thenReturn(host);\n        \n        controller.hosts(1L, mockModel);\n        \n        verify(mockModel).addAttribute(\"host\", host);\n    }\n}\n"
  },
  {
    "path": "version.txt",
    "content": "2.2.2"
  }
]