diff --git a/src/main/java/io/linuxserver/davos/dto/ScheduleProcessResponse.java b/src/main/java/io/linuxserver/davos/dto/ScheduleProcessResponse.java new file mode 100644 index 0000000..01b4dde --- /dev/null +++ b/src/main/java/io/linuxserver/davos/dto/ScheduleProcessResponse.java @@ -0,0 +1,6 @@ +package io.linuxserver.davos.dto; + +public class ScheduleProcessResponse { + + public String message = "OK"; +} diff --git a/src/main/java/io/linuxserver/davos/exception/ScheduleAlreadyRunningException.java b/src/main/java/io/linuxserver/davos/exception/ScheduleAlreadyRunningException.java new file mode 100644 index 0000000..985838d --- /dev/null +++ b/src/main/java/io/linuxserver/davos/exception/ScheduleAlreadyRunningException.java @@ -0,0 +1,10 @@ +package io.linuxserver.davos.exception; + +public class ScheduleAlreadyRunningException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ScheduleAlreadyRunningException() { + super(); + } +} diff --git a/src/main/java/io/linuxserver/davos/exception/ScheduleNotRunningException.java b/src/main/java/io/linuxserver/davos/exception/ScheduleNotRunningException.java new file mode 100644 index 0000000..2ead6e8 --- /dev/null +++ b/src/main/java/io/linuxserver/davos/exception/ScheduleNotRunningException.java @@ -0,0 +1,10 @@ +package io.linuxserver.davos.exception; + +public class ScheduleNotRunningException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ScheduleNotRunningException() { + super(); + } +} diff --git a/src/main/java/io/linuxserver/davos/schedule/ScheduleExecutor.java b/src/main/java/io/linuxserver/davos/schedule/ScheduleExecutor.java index 13f6eeb..ed76584 100644 --- a/src/main/java/io/linuxserver/davos/schedule/ScheduleExecutor.java +++ b/src/main/java/io/linuxserver/davos/schedule/ScheduleExecutor.java @@ -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> 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(); + } } } diff --git a/src/main/java/io/linuxserver/davos/web/MainController.java b/src/main/java/io/linuxserver/davos/web/MainController.java index 6a5ac7a..9ec3e73 100644 --- a/src/main/java/io/linuxserver/davos/web/MainController.java +++ b/src/main/java/io/linuxserver/davos/web/MainController.java @@ -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"; } } diff --git a/src/main/java/io/linuxserver/davos/web/RestAPIController.java b/src/main/java/io/linuxserver/davos/web/RestAPIController.java index 93e9025..e967e97 100644 --- a/src/main/java/io/linuxserver/davos/web/RestAPIController.java +++ b/src/main/java/io/linuxserver/davos/web/RestAPIController.java @@ -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) { diff --git a/src/main/resources/static/css/davos.css b/src/main/resources/static/css/davos.css index f0463fc..e8b3939 100644 --- a/src/main/resources/static/css/davos.css +++ b/src/main/resources/static/css/davos.css @@ -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; +} \ No newline at end of file diff --git a/src/main/resources/static/js/davos.js b/src/main/resources/static/js/davos.js new file mode 100644 index 0000000..4cb0411 --- /dev/null +++ b/src/main/resources/static/js/davos.js @@ -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('
' + + filter + 'close
 '); + + $('#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); diff --git a/src/main/resources/templates/scheduling.html b/src/main/resources/templates/scheduling.html index 5f92b63..f942d89 100644 --- a/src/main/resources/templates/scheduling.html +++ b/src/main/resources/templates/scheduling.html @@ -55,12 +55,12 @@

Current Schedules

- +
- + add
diff --git a/src/main/resources/templates/schedulingedit.html b/src/main/resources/templates/schedulingedit.html index 6716864..96685d6 100644 --- a/src/main/resources/templates/schedulingedit.html +++ b/src/main/resources/templates/schedulingedit.html @@ -7,6 +7,8 @@ + + Edit [[${schedule.name}]] @@ -44,223 +46,250 @@
-
- -
-
-

-

Click on the list items below to edit the schedule

-
+
+
+

+ +

Click on the list items below to edit the schedule

+
-
+
-
    -
  • -
    settings_applicationsGeneral
    -
    +
      +
    • +
      settings_applicationsGeneral
      +
      -
      +
      -
      - - -
      -
      -

      - Give this schedule a meaningful name so you know what its purpose is. -

      -
      +
      + +
      - -
      -
      - - Start Automatically -
      -
      -

      - 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. -

      -
      -
      - -
      - -
      - -
      -
      -

      - The interval that defines how often this schedule will poll the server to check for new files. -

      -
      +
      +

      + Give this schedule a meaningful name so you know what its purpose is. +

      -
    • -
    • -
      businessServer
      -
      -
      -
      - - -
      -
      - - -
      -
      - - -
      -
      -
      -
      - -
      - business - - -
      -
      - settings_input_svideo - - -
      +
      +
      + + Start Automatically
      -
      -
      - perm_identity - - -
      -
      - vpn_key - - -
      -
      -
      -
      - track_changes - - -
      -
      - play_for_work - - -
      -
      - -
      -
    • -
    • -
      swap_vertFile Transfers
      -
      -
      -
      - - -
      -
      -

      - Only download files in the remote folder. Ignore sub-folders. -

      -
      -
      -
      -
      - - -
      -
      -

      - Look inside any sub-folders in the remote folder. If any files are found, download them. -

      -
      -
      -
      -
    • -
    • -
      sort_by_alphaFilters
      -
      -
      - -
      -

      - 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 .doc files, you should enter a filter of *.doc. You may use a question mark (?) to denote a single wildcard character, or an asterisk (*) to denote any number of wildcard characters. -

      - -
      -
      -
      - - -
      -
      - +
      +

      + 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. +

      -
    • -
    • -
      codePost-download Actions
      -
      -
      + +
      + +
      + +
      +
      +

      + The interval that defines how often this schedule will poll the server to check for new files. +

      +
      +
      +
      +
    • +
    • +
      businessServer
      +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + +
      + +
      + business + + +
      +
      + settings_input_svideo + + +
      +
      +
      +
      + perm_identity + + +
      +
      + vpn_key + + +
      +
      +
      +
      + track_changes + + +
      +
      + play_for_work + + +
      +
      + +
      +
    • +
    • +
      swap_vertFile Transfers
      +
      +
      +
      + + +
      +
      +

      + Only download files in the remote folder. Ignore sub-folders. +

      +
      +
      +
      +
      + + +
      +
      +

      + Look inside any sub-folders in the remote folder. If any files are found, download them. +

      +
      +
      +
      +
    • +
    • +
      sort_by_alphaFilters
      +
      +
      +

      - 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 .doc files, you should enter a filter of *.doc. You may use a question mark (?) to denote a single wildcard character, or an asterisk (*) to denote any number of wildcard characters.

      + +
      +
      + + +
      +
      + +
      +
      + +
      +
      + +
      + + + close +
      + +
      +
      +
      +
      +
      +
    • +
    • +
      codePost-download Actions
      +
      +
      +
      +

      + Add actions for davos to perform after each file has been successfully downloaded. +

      -
    • -
    +
    + +
    + + +
    +
    + +
    + +
    + +
    + +
    +
    +
  • +
+ +
+ +
+ +
+ +
+
+
Schedule Options
+ +
- -
- -
- -
-
-
Schedule Options
- - - -
-
- - +
-
- -
- - - + + + diff --git a/src/test/java/io/linuxserver/davos/schedule/ScheduleExecutorTest.java b/src/test/java/io/linuxserver/davos/schedule/ScheduleExecutorTest.java index 74cd556..3c96a6f 100644 --- a/src/test/java/io/linuxserver/davos/schedule/ScheduleExecutorTest.java +++ b/src/test/java/io/linuxserver/davos/schedule/ScheduleExecutorTest.java @@ -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); + } }