Implemented UI into API. Can now create/update schedules

This commit is contained in:
Josh Stark 2015-10-23 21:04:12 +01:00
parent c15d8fca38
commit e7eba69e96
11 changed files with 542 additions and 214 deletions

View File

@ -0,0 +1,6 @@
package io.linuxserver.davos.dto;
public class ScheduleProcessResponse {
public String message = "OK";
}

View File

@ -0,0 +1,10 @@
package io.linuxserver.davos.exception;
public class ScheduleAlreadyRunningException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ScheduleAlreadyRunningException() {
super();
}
}

View File

@ -0,0 +1,10 @@
package io.linuxserver.davos.exception;
public class ScheduleNotRunningException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ScheduleNotRunningException() {
super();
}
}

View File

@ -1,7 +1,10 @@
package io.linuxserver.davos.schedule;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
@ -11,6 +14,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import io.linuxserver.davos.exception.ScheduleAlreadyRunningException;
import io.linuxserver.davos.exception.ScheduleNotRunningException;
import io.linuxserver.davos.persistence.dao.ScheduleConfigurationDAO;
import io.linuxserver.davos.persistence.model.ScheduleConfigurationModel;
@ -18,7 +23,9 @@ import io.linuxserver.davos.persistence.model.ScheduleConfigurationModel;
public class ScheduleExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleExecutor.class);
private Map<Long, ScheduledFuture<?>> runningSchedules = new HashMap<>();
@Resource
private ScheduleConfigurationDAO scheduleConfigurationDAO;
@ -32,23 +39,55 @@ public class ScheduleExecutor {
public void runAutomaticStartupSchedules() {
LOGGER.info("Initialising automatic startup schedules");
for (ScheduleConfigurationModel model : scheduleConfigurationDAO.getAll()) {
if (model.startAutomatically) {
RunnableSchedule runnable = new RunnableSchedule(model.id, scheduleConfigurationDAO);
scheduledExecutorService.scheduleAtFixedRate(runnable, 0, model.interval, TimeUnit.MINUTES);
ScheduledFuture<?> runningSchedule = scheduledExecutorService.scheduleAtFixedRate(runnable, 0, model.interval,
TimeUnit.MINUTES);
runningSchedules.put(model.id, runningSchedule);
}
}
LOGGER.info("Automatic startup schedules should now be running");
}
public void startSchedule(Long id) {
ScheduleConfigurationModel model = scheduleConfigurationDAO.getConfig(id);
RunnableSchedule runnable = new RunnableSchedule(model.id, scheduleConfigurationDAO);
scheduledExecutorService.scheduleAtFixedRate(runnable, 0, model.interval, TimeUnit.MINUTES);
public void startSchedule(Long id) throws ScheduleAlreadyRunningException {
if (!runningSchedules.containsKey(id)) {
ScheduleConfigurationModel model = scheduleConfigurationDAO.getConfig(id);
RunnableSchedule runnable = new RunnableSchedule(model.id, scheduleConfigurationDAO);
LOGGER.info("Starting schedule {}", id);
ScheduledFuture<?> runningSchedule = scheduledExecutorService.scheduleAtFixedRate(runnable, 0, model.interval,
TimeUnit.MINUTES);
runningSchedules.put(model.id, runningSchedule);
} else {
throw new ScheduleAlreadyRunningException();
}
}
public void stopSchedule(Long id) throws ScheduleNotRunningException {
if (runningSchedules.containsKey(id)) {
LOGGER.info("Stopping schedule {}", id);
if (!runningSchedules.get(id).isCancelled()) {
runningSchedules.get(id).cancel(true);
runningSchedules.remove(id);
LOGGER.info("Schedule should now be stopped");
}
} else {
throw new ScheduleNotRunningException();
}
}
}

View File

@ -7,36 +7,52 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import io.linuxserver.davos.dto.ScheduleConfigurationDTO;
import io.linuxserver.davos.dto.converters.ScheduleConfigurationDTOConverter;
import io.linuxserver.davos.persistence.dao.ScheduleConfigurationDAO;
import io.linuxserver.davos.persistence.model.ScheduleConfigurationModel;
import io.linuxserver.davos.persistence.repository.ScheduleConfigurationRepository;
import io.linuxserver.davos.transfer.ftp.FileTransferType;
import io.linuxserver.davos.transfer.ftp.TransferProtocol;
@Controller
@RequestMapping("/")
public class MainController {
@Resource
private ScheduleConfigurationDAO scheduleConfigurationDAO;
@RequestMapping("/")
@RequestMapping
public String index() {
return "index";
}
@RequestMapping("/scheduling")
public String scheduling(Model model) {
model.addAttribute("schedules", scheduleConfigurationDAO.getAll());
return "scheduling";
}
@RequestMapping("/scheduling/{id}")
public String schedulingEdit(@PathVariable Long id, Model model) {
ScheduleConfigurationModel config = scheduleConfigurationDAO.getConfig(id);
model.addAttribute("schedule", new ScheduleConfigurationDTOConverter().convert(config));
return "schedulingedit";
}
@RequestMapping("/scheduling/new")
public String schedulingNew(Model model) {
ScheduleConfigurationDTO schedule = new ScheduleConfigurationDTO();
schedule.interval = 60;
schedule.transferType = FileTransferType.FILE;
schedule.connectionType = TransferProtocol.FTP;
model.addAttribute("schedule", schedule);
return "schedulingedit";
}
}

View File

@ -5,6 +5,8 @@ import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -12,23 +14,64 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import io.linuxserver.davos.dto.ScheduleConfigurationDTO;
import io.linuxserver.davos.dto.ScheduleProcessResponse;
import io.linuxserver.davos.dto.converters.ScheduleConfigurationDTOConverter;
import io.linuxserver.davos.dto.converters.ScheduleConfigurationModelConverter;
import io.linuxserver.davos.exception.ScheduleAlreadyRunningException;
import io.linuxserver.davos.exception.ScheduleNotRunningException;
import io.linuxserver.davos.persistence.dao.ScheduleConfigurationDAO;
import io.linuxserver.davos.persistence.model.ScheduleConfigurationModel;
import io.linuxserver.davos.schedule.ScheduleExecutor;
@RestController
@RequestMapping("/api/v1")
public class RestAPIController {
private static final Logger LOGGER = LoggerFactory.getLogger(RestAPIController.class);
@Resource
private ScheduleConfigurationDAO scheduleConfigurationDAO;
@Resource
private ScheduleExecutor scheduleExecutor;
@RequestMapping(value = "/schedule/{id}")
public ScheduleConfigurationDTO getScheduleConfig(@PathVariable("id") Long id) {
return toDTO(scheduleConfigurationDAO.getConfig(id));
}
@RequestMapping(value = "/schedule/{id}/stop")
public ScheduleProcessResponse stopScheduleConfig(@PathVariable("id") Long id) {
ScheduleProcessResponse scheduleProcessResponse = new ScheduleProcessResponse();
try {
scheduleExecutor.stopSchedule(id);
scheduleProcessResponse.message = "Schedule stopped";
} catch (ScheduleNotRunningException e) {
LOGGER.info("Unable to stop schedule {} as it was not initially running", id);
scheduleProcessResponse.message = "Unable to stop schedule. Wasn't running.";
}
return scheduleProcessResponse;
}
@RequestMapping(value = "/schedule/{id}/start")
public ScheduleProcessResponse startScheduleConfig(@PathVariable("id") Long id) {
ScheduleProcessResponse scheduleProcessResponse = new ScheduleProcessResponse();
try {
scheduleExecutor.startSchedule(id);
scheduleProcessResponse.message = "Schedule started";
} catch (ScheduleAlreadyRunningException e) {
LOGGER.info("Not starting schedule {} as it is already running", id);
scheduleProcessResponse.message = "Unable to start schedule. May already be running";
}
return scheduleProcessResponse;
}
@RequestMapping(value = "/schedule", method = RequestMethod.POST)
public ScheduleConfigurationDTO createScheduleConfig(@RequestBody ScheduleConfigurationDTO dto) {

View File

@ -1,7 +1,7 @@
#main-nav {
position: fixed;
height: 100%;
width: 200px;
width: 250px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
#logo {
@ -58,7 +58,7 @@ html {
bottom: 0;
}
main {
padding-left: 200px;
padding-left: 250px;
}
h1.header {
color: #fff;
@ -108,3 +108,7 @@ a.collection-item.schedule {
font-size: 12px;
padding: 10px !important;
}
.chip {
margin-bottom: 10px;
}

View File

@ -0,0 +1,157 @@
/*global $, jQuery, base, Materialize */
var filter = (function ($) {
'use strict';
var initialise;
initialise = function () {
$('#add_new_filter').on('click', function () {
var filter = $('#new_filter').val();
if ($.trim(filter.length) > 0) {
$('#filter_chips').append('<div class="chip"><input type="hidden" value="" class="filter_id" /><span class="filter_value">' +
filter + '</span><i class="material-icons">close</i></div>&nbsp;');
$('#new_filter').val('');
}
});
};
return {
init: initialise
};
}(jQuery));
var action = (function ($) {
'use strict';
var initialise;
initialise = function () {
};
return {
init: initialise
};
}(jQuery));
var schedule = (function ($) {
'use strict';
var initialise, save, onSuccess, onError, cleanId, startSchedule, stopSchedule, startResponse, stopResponse;
initialise = function () {
$('#start_schedule').on('click', startSchedule);
$('#stop_schedule').on('click', stopSchedule);
$('#save_schedule').on('click', function () {
var postData = {
id: cleanId($('#schedule_id').val()),
name: $('#schedule_name').val(),
startAutomatically: $('#start_automatically').prop('checked'),
interval: parseInt($('#interval option:checked').attr('value'), 10),
connectionType: $('input[name="protocol_type"]:checked').val(),
hostName: $('#server_name').val(),
port: parseInt($('#server_port').val(), 10),
username: $('#username').val(),
password: $('#password').val(),
remoteFilePath: $('#remote_path').val(),
localFilePath: $('#local_path').val(),
transferType: $('input[name="scan_type"]:checked').val(),
filters: [],
actions: []
};
$('#filter_chips .chip').each(function () {
var filter = {
id: cleanId($(this).find('.filter_id').val()),
value: $(this).find('.filter_value').text()
};
postData.filters.push(filter);
});
$.ajax({
method: "POST",
url: "/api/v1/schedule",
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(postData)
}).done(onSuccess);
});
};
cleanId = function (id) {
if ($.trim(id).length === 0) {
return null;
}
return parseInt(id, 10);
};
startSchedule = function () {
$.ajax({
method: "GET",
url: "/api/v1/schedule/" + cleanId($('#schedule_id').val()) + "/start"
}).done(startResponse).fail(startResponse);
};
stopSchedule = function () {
$.ajax({
method: "GET",
url: "/api/v1/schedule/" + cleanId($('#schedule_id').val()) + "/stop"
}).done(stopResponse).fail(stopResponse);
};
startResponse = function (msg) {
Materialize.toast(msg.message, 3000, 'rounded');
};
stopResponse = function (msg) {
Materialize.toast(msg.message, 3000, 'rounded');
};
onSuccess = function (msg) {
if (msg.id === cleanId($('#schedule_id').val())) {
Materialize.toast('Schedule Saved', 3000, 'rounded');
} else {
window.location.replace('/scheduling/' + msg.id);
}
};
onError = function (msg) {
Materialize.toast('Unable to save schedule config', 3000, 'rounded');
};
return {
init: initialise
};
}(jQuery));
jQuery(document).ready(filter.init);
jQuery(document).ready(action.init);
jQuery(document).ready(schedule.init);

View File

@ -55,12 +55,12 @@
<h4 class="light">Current Schedules</h4>
<div class="collection">
<a th:each="schedule : ${schedules}" th:href="@{/scheduling/edit/} + ${schedule.id}" class="collection-item schedule" th:text="${schedule.name}"></a>
<a th:each="schedule : ${schedules}" th:href="@{/scheduling/} + ${schedule.id}" class="collection-item schedule" th:text="${schedule.name}"></a>
</div>
</div>
<div class="fixed-action-btn" style="bottom: 45px; right: 24px;">
<a class="btn-floating btn-large teal waves-effect waves-teal">
<a th:href="@{/scheduling/new}" class="btn-floating btn-large teal waves-effect waves-teal">
<i class="large material-icons">add</i>
</a>
</div>

View File

@ -7,6 +7,8 @@
<link type="text/css" rel="stylesheet" th:href="@{/css/davos.css}" media="screen" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title th:inline="text">Edit [[${schedule.name}]]</title>
</head>
<body>
@ -44,223 +46,250 @@
</div>
<div class="container">
<form class="col s12">
<div class="row">
<div class="section">
<h4 class="light" th:text="${schedule.name}"></h4>
<p class="caption light">Click on the list items below to edit the schedule</p>
</div>
<div class="row">
<div class="section">
<h4 class="light" th:text="${schedule.name}"></h4>
<input type="hidden" id="schedule_id" th:attr="value=${schedule.id}" />
<p class="caption light">Click on the list items below to edit the schedule</p>
</div>
</div>
<div class="row">
<div class="row">
<ul class="collapsible" data-collapsible="expandable">
<li>
<div class="collapsible-header active"><i class="material-icons">settings_applications</i>General</div>
<div class="collapsible-body">
<ul class="collapsible" data-collapsible="expandable">
<li>
<div class="collapsible-header active"><i class="material-icons">settings_applications</i>General</div>
<div class="collapsible-body">
<div class="row valign-wrapper">
<div class="row valign-wrapper">
<div class="input-field col s4">
<input id="schedule_name" placeholder="Please choose a name for this schedule" th:attr="value=${schedule.name}" type="text" class="validate" />
<label for="schedule_name">Schedule name</label>
</div>
<div class="col s8">
<p class="subtext">
Give this schedule a meaningful name so you know what its purpose is.
</p>
</div>
<div class="input-field col s4">
<input id="schedule_name" placeholder="Please choose a name for this schedule" th:attr="value=${schedule.name}" type="text" class="validate" />
<label for="schedule_name">Schedule name</label>
</div>
<div class="row valign-wrapper">
<div class="switch col s4 ">
<label>
<input id="start-automatically" type="checkbox" th:checked="${schedule.startAutomatically}" />
<span class="lever"></span>
</label>
Start Automatically
</div>
<div class="col s8">
<p class="subtext">
Select this if you want this schedule to automatically start when davos starts. This is a useful option if davos needs to restart and you don't want to have to manually kick off this process.
</p>
</div>
</div>
<div class="row">
<div class="input-field col s4">
<select class="browser-default">
<option value="" disabled="disabled" selected="selected">Please select...</option>
<option th:selected="${schedule.interval == 15}" value="15">Every 15 minutes</option>
<option th:selected="${schedule.interval == 30}" value="30">Every 30 minutes</option>
<option th:selected="${schedule.interval == 60}" value="60">Every hour</option>
<option th:selected="${schedule.interval == 120}" value="120">Every 2 hours</option>
<option th:selected="${schedule.interval == 720}" value="720">Twice a day</option>
<option th:selected="${schedule.interval == 1440}" value="1440">Once a day</option>
</select>
</div>
<div class="col s8">
<p class="subtext">
The interval that defines how often this schedule will poll the server to check for new files.
</p>
</div>
<div class="col s8">
<p class="subtext">
Give this schedule a meaningful name so you know what its purpose is.
</p>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">business</i>Server</div>
<div class="collapsible-body">
<div class="row valign-wrapper">
<div class="switch col s2">
<input name="protocol_type" type="radio" id="protocol_ftp" th:checked="${schedule.connectionType == 'FTP'}" />
<label for="protocol_ftp">FTP</label>
</div>
<div class="switch col s2">
<input name="protocol_type" type="radio" id="protocol_ftps" th:checked="${schedule.connectionType == 'FTPS'}" />
<label for="protocol_ftps">FTPS</label>
</div>
<div class="switch col s2">
<input name="protocol_type" type="radio" id="protocol_sftp" th:checked="${schedule.connectionType == 'SFTP'}" />
<label for="protocol_sftp">SFTP</label>
</div>
<div class="switch col s6"></div>
</div>
<div class="row">
<div class="input-field col s6">
<i class="material-icons prefix">business</i>
<input id="server_name" type="text" th:attr="value=${schedule.hostName}" />
<label for="server_name">Server Name (or IP address)</label>
</div>
<div class="input-field col s2">
<i class="material-icons prefix">settings_input_svideo</i>
<input id="server_port" type="number" th:attr="value=${schedule.port}" />
<label for="server_port">Port</label>
</div>
<div class="row valign-wrapper">
<div class="switch col s4 ">
<label>
<input id="start_automatically" type="checkbox" th:checked="${schedule.startAutomatically}" />
<span class="lever"></span>
</label>
Start Automatically
</div>
<div class="row">
<div class="input-field col s6">
<i class="material-icons prefix">perm_identity</i>
<input id="username" type="text" th:attr="value=${schedule.username}" />
<label for="username">Username</label>
</div>
<div class="input-field col s6">
<i class="material-icons prefix">vpn_key</i>
<input id="password" type="password" th:attr="value=${schedule.password}" />
<label for="password">Password</label>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<i class="material-icons prefix">track_changes</i>
<input id="remote-path" type="text" th:attr="value=${schedule.remoteFilePath}" />
<label for="remote-path">Remote folder to scan</label>
</div>
<div class="input-field col s6">
<i class="material-icons prefix">play_for_work</i>
<input id="local-path" type="text" th:attr="value=${schedule.localFilePath}" />
<label for="local-path">Local destination folder</label>
</div>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">swap_vert</i>File Transfers</div>
<div class="collapsible-body">
<div class="row valign-wrapper">
<div class="switch col s2">
<input name="scan_type" type="radio" id="scan_files_only" th:checked="${schedule.transferType == 'FILE'}" />
<label for="scan_files_only">Files only</label>
</div>
<div class="col s10">
<p class="subtext">
Only download files in the remote folder. Ignore sub-folders.
</p>
</div>
</div>
<div class="row valign-wrapper">
<div class="switch col s2">
<input name="scan_type" type="radio" id="scan_recursive" th:checked="${schedule.transferType == 'RECURSIVE'}" />
<label for="scan_recursive">Recursive</label>
</div>
<div class="col s10">
<p class="subtext">
Look inside any sub-folders in the remote folder. If any files are found, download them.
</p>
</div>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">sort_by_alpha</i>Filters</div>
<div class="collapsible-body">
<div class="row">
<form class="col s12">
<div class="col s12">
<p class="subtext">
If you do not want davos to pull down all recent files from the server during the next scheduled run, please define a filter (or more). A filter works by telling davos that a file must adhere to a particular naming structure (file name or extension) before it decides to download it. For example, if you only want the schedule to download <i>.doc</i> files, you should enter a filter of <strong>*.doc</strong>. You may use a question mark (<strong>?</strong>) to denote a single wildcard character, or an asterisk (<strong>*</strong>) to denote any number of wildcard characters.
</p>
</div>
<div class="row">
<div class="input-field col s12">
<textarea id="filter-keywords" class="materialize-textarea"></textarea>
<label for="filter-keywords">Filter keywords (one per line)</label>
</div>
</div>
</form>
<div class="col s8">
<p class="subtext">
Select this if you want this schedule to automatically start when davos starts. This is a useful option if davos needs to restart and you don't want to have to manually kick off this process.
</p>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">code</i>Post-download Actions</div>
<div class="collapsible-body">
<div class="row">
<div class="row">
<div class="input-field col s4">
<select class="browser-default" id="interval">
<option th:selected="${schedule.interval == 15}" th:value="15">Every 15 minutes</option>
<option th:selected="${schedule.interval == 30}" th:value="30">Every 30 minutes</option>
<option th:selected="${schedule.interval == 60}" selected="selected" th:value="60">Every hour</option>
<option th:selected="${schedule.interval == 120}" th:value="120">Every 2 hours</option>
<option th:selected="${schedule.interval == 720}" th:value="720">Twice a day</option>
<option th:selected="${schedule.interval == 1440}" th:value="1440">Once a day</option>
</select>
</div>
<div class="col s8">
<p class="subtext">
The interval that defines how often this schedule will poll the server to check for new files.
</p>
</div>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">business</i>Server</div>
<div class="collapsible-body">
<div class="row valign-wrapper">
<div class="switch col s2">
<input name="protocol_type" value="FTP" type="radio" id="protocol_ftp" th:checked="${schedule.connectionType != null and schedule.connectionType.name() == 'FTP'}" />
<label for="protocol_ftp">FTP</label>
</div>
<div class="switch col s2">
<input name="protocol_type" value="FTPS" type="radio" id="protocol_ftps" th:checked="${schedule.connectionType != null and schedule.connectionType.name() == 'FTPS'}" />
<label for="protocol_ftps">FTPS</label>
</div>
<div class="switch col s2">
<input name="protocol_type" value="SFTP" type="radio" id="protocol_sftp" th:checked="${schedule.connectionType != null and schedule.connectionType.name() == 'SFTP'}" />
<label for="protocol_sftp">SFTP</label>
</div>
<div class="switch col s6"></div>
</div>
<div class="row">
<div class="input-field col s6">
<i class="material-icons prefix">business</i>
<input id="server_name" type="text" th:attr="value=${schedule.hostName}" />
<label for="server_name">Server Name (or IP address)</label>
</div>
<div class="input-field col s2">
<i class="material-icons prefix">settings_input_svideo</i>
<input id="server_port" type="number" th:attr="value=${schedule.port}" />
<label for="server_port">Port</label>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<i class="material-icons prefix">perm_identity</i>
<input id="username" type="text" th:attr="value=${schedule.username}" />
<label for="username">Username</label>
</div>
<div class="input-field col s6">
<i class="material-icons prefix">vpn_key</i>
<input id="password" type="password" th:attr="value=${schedule.password}" />
<label for="password">Password</label>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<i class="material-icons prefix">track_changes</i>
<input id="remote_path" type="text" th:attr="value=${schedule.remoteFilePath}" />
<label for="remote-path">Remote folder to scan</label>
</div>
<div class="input-field col s6">
<i class="material-icons prefix">play_for_work</i>
<input id="local_path" type="text" th:attr="value=${schedule.localFilePath}" />
<label for="local-path">Local destination folder</label>
</div>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">swap_vert</i>File Transfers</div>
<div class="collapsible-body">
<div class="row valign-wrapper">
<div class="switch col s2">
<input name="scan_type" value="FILE" type="radio" id="scan_files_only" th:checked="${schedule.transferType != null and schedule.transferType.name() == 'FILE'}" />
<label for="scan_files_only">Files only</label>
</div>
<div class="col s10">
<p class="subtext">
Only download files in the remote folder. Ignore sub-folders.
</p>
</div>
</div>
<div class="row valign-wrapper">
<div class="switch col s2">
<input name="scan_type" value="RECURSIVE" type="radio" id="scan_recursive" th:checked="${schedule.transferType != null and schedule.transferType.name() == 'RECURSIVE'}" />
<label for="scan_recursive">Recursive</label>
</div>
<div class="col s10">
<p class="subtext">
Look inside any sub-folders in the remote folder. If any files are found, download them.
</p>
</div>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">sort_by_alpha</i>Filters</div>
<div class="collapsible-body">
<div class="row">
<form class="col s12">
<div class="col s12">
<p class="subtext">
Add actions for davos to perform after each file has been successfully downloaded.
If you do not want davos to pull down all recent files from the server during the next scheduled run, please define a filter (or more). A filter works by telling davos that a file must adhere to a particular naming structure (file name or extension) before it decides to download it. For example, if you only want the schedule to download <i>.doc</i> files, you should enter a filter of <strong>*.doc</strong>. You may use a question mark (<strong>?</strong>) to denote a single wildcard character, or an asterisk (<strong>*</strong>) to denote any number of wildcard characters.
</p>
</div>
<div class="row">
<div class="input-field col s6">
<input id="new_filter" type="text" />
<label for="new_filter">Add filter</label>
</div>
<div class="input-field col s6">
<button class="btn waves-effect waves-light" type="button" id="add_new_filter">Add</button>
</div>
</div>
<div class="row">
<div class="input-field col s12" id="filter_chips">
<div class="chip" th:each="filter : ${schedule.filters}">
<input type="hidden" class="filter_id" th:attr="value=${filter.id}" />
<span class="filter_value" th:text="${filter.value}"></span>
<i class="material-icons">close</i>
</div>
</div>
</div>
</form>
</div>
</div>
</li>
<li>
<div class="collapsible-header active"><i class="material-icons">code</i>Post-download Actions</div>
<div class="collapsible-body">
<div class="row">
<div class="col s12">
<p class="subtext">
Add actions for davos to perform after each file has been successfully downloaded.
</p>
</div>
</div>
</li>
</ul>
<div class="row">
<div class="input-field col s4">
<select class="browser-default">
<option value="" disabled="disabled" selected="selected">Add action...</option>
<option value="pushbullet">Pushbullet Notification</option>
<option value="move">Move downloaded file</option>
</select>
</div>
</div>
<div id="download_actions">
<div class="row download_action">
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="row">
<button class="btn waves-effect waves-light" type="button" id="save_schedule">Save changes</button>
</div>
<div class="row">
<div class="section">
<h5 class="light">Schedule Options</h5>
<button class="btn waves-effect waves-light green lighten-2" id="start_schedule" type="button">Start</button>
<button class="btn waves-effect waves-light red lighten-2" id="stop_schedule" type="button">Stop</button>
</div>
<div class="row">
<button class="btn waves-effect waves-light" type="submit" name="action">Save changes</button>
</div>
<div class="row">
<div class="section">
<h5 class="light">Schedule Options</h5>
<button class="btn waves-effect waves-light green lighten-2" type="submit" name="action">Start</button>
<button class="btn waves-effect waves-light orange lighten-2" type="submit" name="action">Restart</button>
<button class="btn waves-effect waves-light red lighten-2" type="submit" name="action">Stop</button>
</div>
</div>
</form>
</div>
</div>
</main>
<footer>
</footer>
<script type="text/javascript" th:src="@{/js/jquery-2.1.4.min.js}"></script>
<script type="text/javascript" th:src="@{/js/materialize.min.js}"></script>
<script type="text/javascript">
<script type="text/javascript" th:src="@{/js/davos.js}"></script>
<script type="text/javascript" th:inline="text">
var base = "[[@{/}]]"
</script>
</body>
</html>

View File

@ -16,6 +16,7 @@ import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import io.linuxserver.davos.exception.ScheduleAlreadyRunningException;
import io.linuxserver.davos.persistence.dao.ScheduleConfigurationDAO;
import io.linuxserver.davos.persistence.model.ScheduleConfigurationModel;
@ -62,11 +63,24 @@ public class ScheduleExecutorTest {
ScheduleConfigurationModel config = new ScheduleConfigurationModel();
config.interval = 86;
when(mockConfigurationDAO.getConfig(1337L)).thenReturn(config);
scheduleExecutor.startSchedule(1337L);
verify(mockExecutorService).scheduleAtFixedRate(any(RunnableSchedule.class), eq(0l), eq(86l), eq(TimeUnit.MINUTES));
}
@Test(expected = ScheduleAlreadyRunningException.class)
public void startScheduleShouldNotRunScheduleIfAlreadyRunning() {
ScheduleConfigurationModel config = new ScheduleConfigurationModel();
config.interval = 86;
config.id = 1337L;
when(mockConfigurationDAO.getConfig(1337L)).thenReturn(config);
scheduleExecutor.startSchedule(1337L);
scheduleExecutor.startSchedule(1337L);
}
}