/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.io.dropbox.internal; import static org.apache.commons.lang.StringUtils.*; import static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Dictionary; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.quartz.CronScheduleBuilder; import org.quartz.CronTrigger; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.dropbox.core.DbxAppInfo; import com.dropbox.core.DbxClient; import com.dropbox.core.DbxDelta; import com.dropbox.core.DbxDelta.Entry; import com.dropbox.core.DbxEntry; import com.dropbox.core.DbxEntry.WithChildren; import com.dropbox.core.DbxException; import com.dropbox.core.DbxRequestConfig; import com.dropbox.core.DbxWebAuthNoRedirect; import com.dropbox.core.DbxWriteMode; /** * The {@link DropboxSynchronizer} is able to synchronize contents of your Dropbox * to the local file system and vice versa. There three synchronization modes * available: local to dropbox (which is the default mode), dropbox to local and * bidirectional. * * Note: The {@link DropboxSynchronizer} must be authorized against Dropbox one * time. Watch the logfile for the URL to open in your Browser and allow openHAB * to connect to a predefined App-Folder (see <a * href="https://www.dropbox.com/developers/apps">Dropbox Documentation</a> for more information). * * @author Thomas.Eichstaedt-Engelen - Initial Contribution * @author Theo Weiss - add smarthome.userdata support * @since 1.0.0 */ public class DropboxSynchronizer implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(DropboxSynchronizer.class); private static final String DROPBOX_SCHEDULER_GROUP = "Dropbox"; private static final String FIELD_DELIMITER = "@@"; private static final String LINE_DELIMITER = System.getProperty("line.separator"); private static final String DELTA_CURSOR_FILE_NAME = File.separator + "deltacursor.dbx"; private static final String DROPBOX_ENTRIES_FILE_NAME = File.separator + "dropbox-entries.dbx"; private static final String AUTH_FILE_NAME = File.separator + "authfile.dbx"; /** holds the id of the last synchronisation cursor. This is needed to define the delta to download from Dropbox. */ private static String lastCursor = null; private static String lastHash = null; //// Authentication: user must configure either the personalAccessToken or //// BOTH the AppKey AND the AppSecret; if all 3 are defined, then the //// personalAccessToken will be used and the others ignored. /** a user's personal access token retrieved from configuration only; null by default */ private static String personalAccessToken; /// AppKey and AppSecret: /// These are legacy attributes from when there was an official openHAB Dropbox app. /// These should not generally be used, as they are more difficult to set up than /// the newer method via personalAccessToken. /** the configured AppKey (optional; see notes above) */ private static String appKey; /** the configured AppSecret (optional; see notes above) */ private static String appSecret; /** The default directory to download files from Dropbox to (currently '.') */ private static final String DEFAULT_CONTENT_DIR = getConfigDirFolder(); /** * the base directory to synchronize with openHAB (defaults to * DEFAULT_CONTENT_DIR) */ private static String contentDir = DEFAULT_CONTENT_DIR; /** the base directory for the .dbx files */ public final static String DBX_FOLDER = getUserDbxDataFolder(); /** the configured synchronization mode (defaults to LOCAL_TO_DROPBOX) */ private static DropboxSyncMode syncMode = DropboxSyncMode.LOCAL_TO_DROPBOX; /** the upload interval as a Cron Expression (optional; defaults to '0 0 2 * * ?', which means once a day at 2am) */ private static String uploadInterval = "0 0 2 * * ?"; /** * the download interval as a Cron Expression (optional; defaults to '0 0/5 * * * ?' which means every 5 minutes) */ private static String downloadInterval = "0 0/5 * * * ?"; private static final List<String> DEFAULT_UPLOAD_FILE_FILTER = Arrays.asList("^([^/]*/){1}[^/]*$", "/configurations.*", "/logs/.*", "/etc/.*"); private static final List<String> DEFAULT_DOWNLOAD_FILE_FILTER = Arrays.asList("^([^/]*/){1}[^/]*$", "/configurations.*"); /** * defines a comma separated list of regular expressions which matches the filenames to upload to Dropbox (optional; * defaults to '/configurations.*, /logs/.*, /etc/.*') */ private static List<String> uploadFilterElements = DEFAULT_UPLOAD_FILE_FILTER; /** * defines a comma separated list of regular expressions which matches the filenames to download from Dropbox * (optional; defaults to '/configurations.*') */ private static List<String> downloadFilterElements = DEFAULT_DOWNLOAD_FILE_FILTER; /** * operates the Synchronizer in fake mode which avoids sending files to or from Dropbox. This is meant * as a test mode for the filter settings. (optional; defaults to false) */ private static boolean fakeMode = false; private static boolean isProperlyConfigured = false; private static DropboxSynchronizer instance = null; private static final DbxAppInfo appInfo = new DbxAppInfo(DropboxSynchronizer.appKey, DropboxSynchronizer.appSecret); private final static DbxRequestConfig requestConfig = new DbxRequestConfig("openHAB/1.0", Locale.getDefault().toString()); public void activate() { DropboxSynchronizer.instance = this; } public void deactivate() { logger.debug("About to shut down Dropbox Synchronizer ..."); cancelAllJobs(); isProperlyConfigured = false; lastCursor = null; uploadFilterElements = DEFAULT_UPLOAD_FILE_FILTER; downloadFilterElements = DEFAULT_DOWNLOAD_FILE_FILTER; DropboxSynchronizer.instance = null; logger.debug("Shutdown completed."); } private void activateSynchronizer() { if (isAuthenticated()) { startSynchronizationJobs(); } else { try { startAuthentication(); } catch (DbxException e) { logger.warn("Couldn't start authentication process: {}", e.getMessage()); } } } /** * Starts the OAuth authorization process with Dropbox. This is a * multi-step process which is described in the Wiki. * * @throws DbxException if there are technical or application level errors * in the Dropbox communication * * @see <a href="https://github.com/openhab/openhab/wiki/Dropbox-IO">openHAB Dropbox IO Wiki</a> */ public void startAuthentication() throws DbxException { if (personalAccessToken == null) { DbxWebAuthNoRedirect webAuth = new DbxWebAuthNoRedirect(requestConfig, appInfo); String authUrl = webAuth.start(); logger.info("#########################################################################################"); logger.info("# Dropbox Integration: U S E R I N T E R A C T I O N R E Q U I R E D !!"); logger.info("# 1. Open URL '{}'", authUrl); logger.info("# 2. Allow openHAB to access Dropbox"); logger.info("# 3. Paste the authorisation code here using the command 'finishAuthentication \"<token>\"'"); logger.info("#########################################################################################"); } else { logger.info("#########################################################################################"); logger.info("# Starting auth using personal access token"); logger.info("#########################################################################################"); writeAccessToken(personalAccessToken); startSynchronizationJobs(); } } /** * Finishes the OAuth authorization process by taking the given {@code token} and creating * an accessToken out of it. The authorization process is a multi step process which is * described in the Wiki in detail. * * @throws DbxException if there are technical or application level errors * in the Dropbox communication * * @see <a href="https://github.com/openhab/openhab/wiki/Dropbox-IO">openHAB Dropbox IO Wiki</a> */ public void finishAuthentication(String code) throws DbxException { DbxWebAuthNoRedirect webAuth = new DbxWebAuthNoRedirect(requestConfig, appInfo); String accessToken = webAuth.finish(code).accessToken; writeAccessToken(accessToken); logger.info("#########################################################################################"); logger.info("# OAuth2 authentication flow has been finished successfully "); logger.info("#########################################################################################"); startSynchronizationJobs(); } /** * Synchronizes all changes from Dropbox to the local file system. Changes are * identified by the Dropbox delta mechanism which takes the <code>lastCursor</code> * field into account. If <code>lastCursor</code> is <code>null</code> it * tries to recreate it from the file <code>deltacursor.dbx</code>. If * it is still <code>null</code> all files are downloaded from the specified * location. * * Note: Since we define Dropbox as data master we do not care about local * changes while downloading files! * * @throws DbxException if there are technical or application level * errors in the Dropbox communication * @throws IOException */ public void syncDropboxToLocal(DbxClient client) throws DbxException, IOException { logger.debug("Started synchronization from Dropbox to local ..."); lastCursor = readDeltaCursor(); if (StringUtils.isBlank(lastCursor)) { logger.trace("Last cursor was NULL and has now been recreated from the filesystem '{}'", lastCursor); } DbxDelta<DbxEntry> deltaPage = client.getDelta(lastCursor); if (deltaPage.entries != null && deltaPage.entries.size() == 0) { logger.debug("There are no deltas to download from Dropbox ..."); } else { do { logger.debug("There are '{}' deltas to process ...", deltaPage.entries.size()); int processedDelta = 0; for (Entry<DbxEntry> entry : deltaPage.entries) { boolean matches = false; for (String filter : downloadFilterElements) { matches |= entry.lcPath.matches(filter); } if (matches) { if (entry.metadata != null) { downloadFile(client, entry); } else { String fqPath = contentDir + entry.lcPath; deleteLocalFile(fqPath); } processedDelta++; } else { logger.trace("skipped file '{}' since it doesn't match the given filter arguments.", entry.lcPath); } } logger.debug("'{}' deltas met the given downloadFilter {}", processedDelta, downloadFilterElements); // query again to check if there more entries to process! deltaPage = client.getDelta(lastCursor); } while (deltaPage.hasMore); } writeDeltaCursor(deltaPage.cursor); } /** * Synchronizes all changes from the local filesystem into Dropbox. Changes * are identified by the files' <code>lastModified</code> attribute. If there * are less files locally the additional files will be deleted from the * Dropbox. New files will be uploaded or overwritten if they exist already. * * @throws DbxException if there are technical or application level * errors in the Dropbox communication * @throws IOException */ public void syncLocalToDropbox(DbxClient client) throws DbxException, IOException { logger.debug("Started synchronization from local to Dropbox ..."); Map<String, Long> dropboxEntries = new HashMap<String, Long>(); WithChildren metadata = client.getMetadataWithChildren("/"); File dropboxEntryFile = new File(DBX_FOLDER + DROPBOX_ENTRIES_FILE_NAME); if (!dropboxEntryFile.exists() || !metadata.hash.equals(lastHash)) { collectDropboxEntries(client, dropboxEntries, "/"); serializeDropboxEntries(dropboxEntryFile, dropboxEntries); lastHash = metadata.hash; // TODO: TEE: we could think about writing the 'lastHash' to a file? // let's see what daily use brings whether this a necessary feature! } else { logger.trace("Dropbox entry file '{}' exists -> extract content", dropboxEntryFile.getPath()); dropboxEntries = extractDropboxEntries(dropboxEntryFile); } Map<String, Long> localEntries = new HashMap<String, Long>(); collectLocalEntries(localEntries, contentDir); logger.debug("There are '{}' local entries that met the upload filters ...", localEntries.size()); boolean isChanged = false; for (java.util.Map.Entry<String, Long> entry : localEntries.entrySet()) { if (dropboxEntries.containsKey(entry.getKey())) { if (entry.getValue().compareTo(dropboxEntries.get(entry.getKey())) > 0) { logger.trace("Local file '{}' is newer - upload to Dropbox!", entry.getKey()); if (!fakeMode) { uploadFile(client, entry.getKey(), true); } isChanged = true; } } else { logger.trace("Local file '{}' doesn't exist in Dropbox - upload to Dropbox!", entry.getKey()); if (!fakeMode) { uploadFile(client, entry.getKey(), false); } isChanged = true; } dropboxEntries.remove(entry.getKey()); } // all left dropboxEntries are only present in Dropbox and not locally (anymore) // so delete them from Dropbox! for (String path : dropboxEntries.keySet()) { for (String filter : uploadFilterElements) { if (path.matches(filter)) { if (!fakeMode) { client.delete(path); } isChanged = true; logger.debug("Successfully deleted file '{}' from Dropbox", path); } else { logger.trace("Skipped file '{}' since it doesn't match the given filter arguments.", path); } } } // when something changed we will remove the entry file // which causes a new generation during the next sync if (isChanged) { boolean success = FileUtils.deleteQuietly(dropboxEntryFile); if (!success) { logger.warn("Couldn't delete file '{}'", dropboxEntryFile.getPath()); } else { logger.debug( "Deleted cache file '{}' since there are changes. It will be recreated on the next synchronization loop.", dropboxEntryFile.getPath()); } // since there are changes we have to update the lastCursor (and // the corresponding file) to have the right starting point for the // next synchronization loop DbxDelta<DbxEntry> delta = client.getDelta(lastCursor); writeDeltaCursor(delta.cursor); } else { logger.debug("No files changed locally. No deltas to upload to Dropbox ..."); } } private void downloadFile(DbxClient client, Entry<DbxEntry> entry) throws DbxException, IOException { String fqPath = contentDir + entry.metadata.path; File newLocalFile = new File(fqPath); if (entry.metadata.isFolder()) { // create intermediary directories boolean success = newLocalFile.mkdirs(); if (!success) { logger.debug("Didn't create any intermediary directories for '{}'", fqPath); } } else { // if the parent directory doesn't exist create all intermediary // directorys ... if (!newLocalFile.getParentFile().exists()) { newLocalFile.getParentFile().mkdirs(); } try { FileOutputStream os = new FileOutputStream(newLocalFile); if (!fakeMode) { client.getFile(entry.metadata.path, null, os); } logger.debug("Successfully downloaded file '{}'", fqPath); } catch (FileNotFoundException fnfe) { throw new DbxException("Couldn't write file '" + fqPath + "'", fnfe); } long lastModified = entry.metadata.asFile().lastModified.getTime(); boolean success = newLocalFile.setLastModified(lastModified); if (!success) { logger.debug("Couldn't change attribute 'lastModified' of file '{}'", fqPath); } } } private Map<String, Long> extractDropboxEntries(File dropboxEntryFile) { Map<String, Long> dropboxEntries = new HashMap<String, Long>(); try { List<String> lines = FileUtils.readLines(dropboxEntryFile); for (String line : lines) { String[] lineComponents = line.split(FIELD_DELIMITER); if (lineComponents.length == 2) { dropboxEntries.put(lineComponents[0], Long.valueOf(lineComponents[1])); } else { logger.trace("Couldn't parse line '{}' - it does not contain two elements delimited by '{}'", line, FIELD_DELIMITER); } } } catch (IOException ioe) { logger.warn("Couldn't read lines from file '{}'", dropboxEntryFile.getPath()); } return dropboxEntries; } private void serializeDropboxEntries(File file, Map<String, Long> dropboxEntries) { try { StringBuffer sb = new StringBuffer(); for (java.util.Map.Entry<String, Long> line : dropboxEntries.entrySet()) { sb.append(line.getKey()).append(FIELD_DELIMITER).append(line.getValue()).append(LINE_DELIMITER); } FileUtils.writeStringToFile(file, sb.toString()); } catch (IOException e) { logger.warn("Couldn't write file '{}'", file.getPath()); } } private void collectLocalEntries(Map<String, Long> localEntries, String path) { File[] files = new File(path).listFiles(new FileFilter() { @Override public boolean accept(File file) { String normalizedPath = StringUtils.substringAfter(file.getPath(), contentDir); for (String filter : uploadFilterElements) { if (FilenameUtils.getName(normalizedPath).startsWith(".")) { return false; } else if (FilenameUtils.getName(normalizedPath).endsWith(".dbx")) { return false; } else if (normalizedPath.matches(filter)) { return true; } } logger.trace("Skipped file '{}' since it doesn't match the given filter arguments.", file.getAbsolutePath()); return false; } }); for (File file : files) { String normalizedPath = StringUtils.substringAfter(file.getPath(), contentDir); if (file.isDirectory()) { collectLocalEntries(localEntries, file.getPath()); } else { // if we are on a Windows filesystem we need to change the separator for dropbox if (isWindows()) { normalizedPath = normalizedPath.replace('\\', '/'); } localEntries.put(normalizedPath, file.lastModified()); } } } private void collectDropboxEntries(DbxClient client, Map<String, Long> dropboxEntries, String path) throws DbxException { WithChildren entries = client.getMetadataWithChildren(path); for (DbxEntry entry : entries.children) { if (entry.isFolder()) { collectDropboxEntries(client, dropboxEntries, entry.path); } else { dropboxEntries.put(entry.path, entry.asFile().lastModified.getTime()); } } } /* * TODO: TEE: Currently there is no way to change the attribute * 'lastModified' of the files to upload via Dropbox API. See the * discussion below for more details. * * Since this is a missing feature (from my point of view) we should * check the improvements of the API development on a regular basis. * * @see http://forums.dropbox.com/topic.php?id=22347 */ private void uploadFile(DbxClient client, String dropboxPath, boolean overwrite) throws DbxException, IOException { File file = new File(contentDir + File.separator + dropboxPath); FileInputStream inputStream = new FileInputStream(file); try { DbxWriteMode mode = overwrite ? DbxWriteMode.force() : DbxWriteMode.add(); DbxEntry.File uploadedFile = client.uploadFile(dropboxPath, mode, file.length(), inputStream); logger.debug("Successfully uploaded file '{}'. New revision is '{}'", uploadedFile, uploadedFile.rev); } finally { inputStream.close(); } } private void writeAccessToken(String content) { // create folder for .dbx files if it does not exist File folder = new File(DBX_FOLDER); if (!folder.exists()) { folder.mkdirs(); } File tokenFile = new File(DBX_FOLDER + AUTH_FILE_NAME); writeLocalFile(tokenFile, content); } private String readAccessToken() { File tokenFile = new File(DBX_FOLDER + AUTH_FILE_NAME); return readFile(tokenFile); } private boolean isAuthenticated() { return StringUtils.isNotBlank(readAccessToken()); } private void writeDeltaCursor(String deltaCursor) { if (!deltaCursor.equals(lastCursor)) { logger.trace("Delta-Cursor changed (lastCursor '{}', newCursor '{}')", lastCursor, deltaCursor); File cursorFile = new File(DBX_FOLDER + DELTA_CURSOR_FILE_NAME); writeLocalFile(cursorFile, deltaCursor); lastCursor = deltaCursor; } } private String readDeltaCursor() { File cursorFile = new File(DBX_FOLDER + DELTA_CURSOR_FILE_NAME); return readFile(cursorFile); } private String readFile(File file) { String content = null; if (file.exists()) { try { List<String> lines = FileUtils.readLines(file); if (lines.size() > 0) { content = lines.get(0); } } catch (IOException ioe) { logger.debug("Handling of cursor file threw an Exception", ioe); } } return content; } private static void writeLocalFile(File file, String content) { try { FileUtils.writeStringToFile(file, content); logger.debug("Created file '{}' with content '{}'", file.getAbsolutePath(), content); } catch (IOException e) { logger.error("Couldn't write to file '{}'.", file.getPath(), e); } } private static void deleteLocalFile(String fqPath) { File fileToDelete = new File(fqPath); if (!fileToDelete.isDirectory()) { boolean success = true; if (!fakeMode) { FileUtils.deleteQuietly(fileToDelete); } if (success) { logger.debug("Successfully deleted local file '{}'", fqPath); } else { logger.debug("Local file '{}' couldn't be deleted", fqPath); } } else { logger.trace("Local item '{}' wasn't deleted because it is a directory."); } } @SuppressWarnings("rawtypes") @Override public void updated(Dictionary config) throws ConfigurationException { if (config == null) { logger.debug("Updated() was called with a null config!"); return; } isProperlyConfigured = false; String appKeyString = Objects.toString(config.get("appkey"), null); if (isNotBlank(appKeyString)) { DropboxSynchronizer.appKey = appKeyString; } String appSecretString = Objects.toString(config.get("appsecret"), null); if (isNotBlank(appSecretString)) { DropboxSynchronizer.appSecret = appSecretString; } String pat = Objects.toString(config.get("personalAccessToken"), null); if (isNotBlank(pat)) { DropboxSynchronizer.personalAccessToken = pat; } if (logger.isDebugEnabled()) { StringBuffer message = new StringBuffer(); message.append("Authentication parameters to be used:\r\n"); if (isNotBlank(pat)) { message.append(" Personal access token = " + pat + "\r\n"); } else { message.append(" appkey = " + appKeyString + "\r\n"); message.append(" appsecret = " + appSecretString + "\r\n"); } logger.debug(message.toString()); } if (isBlank(pat) && (isBlank(appKeyString) || isBlank(appSecretString))) { throw new ConfigurationException("dropbox:authentication", "The Dropbox authentication parameters are incorrect! The parameter 'personalAccesstoken' must be set, or both of the parameters 'appkey' and 'appsecret' must be set. Please check your configuration."); } String fakeModeString = Objects.toString(config.get("fakemode"), null); if (isNotBlank(fakeModeString)) { DropboxSynchronizer.fakeMode = BooleanUtils.toBoolean(fakeModeString); } String contentDirString = Objects.toString(config.get("contentdir"), null); if (isNotBlank(contentDirString)) { DropboxSynchronizer.contentDir = contentDirString; } logger.debug("contentdir: {}", contentDir); String uploadIntervalString = Objects.toString(config.get("uploadInterval"), null); if (isNotBlank(uploadIntervalString)) { DropboxSynchronizer.uploadInterval = uploadIntervalString; } String downloadIntervalString = Objects.toString(config.get("downloadInterval"), null); if (isNotBlank(downloadIntervalString)) { DropboxSynchronizer.downloadInterval = downloadIntervalString; } String syncModeString = Objects.toString(config.get("syncmode"), null); if (isNotBlank(syncModeString)) { try { DropboxSynchronizer.syncMode = DropboxSyncMode.valueOf(syncModeString.toUpperCase()); } catch (IllegalArgumentException iae) { throw new ConfigurationException("dropbox:syncmode", "Unknown SyncMode '" + syncModeString + "'. Valid SyncModes are 'DROPBOX_TO_LOCAL', 'LOCAL_TO_DROPBOX' and 'BIDIRECTIONAL'."); } } String uploadFilterString = Objects.toString(config.get("uploadfilter"), null); if (isNotBlank(uploadFilterString)) { String[] newFilterElements = uploadFilterString.split(","); uploadFilterElements = Arrays.asList(newFilterElements); } String downloadFilterString = Objects.toString(config.get("downloadfilter"), null); if (isNotBlank(downloadFilterString)) { String[] newFilterElements = downloadFilterString.split(","); downloadFilterElements = Arrays.asList(newFilterElements); } // we got this far, so we define this synchronizer as properly configured ... isProperlyConfigured = true; logger.debug("Bundle is properly configured. Activating synchronizer."); activateSynchronizer(); } // **************************************************************************** // Synchronisation Jobs // **************************************************************************** private void startSynchronizationJobs() { if (isProperlyConfigured) { cancelAllJobs(); if (isAuthenticated()) { logger.debug("Authenticated. Scheduling jobs."); scheduleJobs(); } else { logger.debug("Dropbox bundle isn't authorized properly, so the synchronization jobs " + "won't be started! Please re-initiate the authorization process by restarting the " + "Dropbox bundle through the OSGi console."); } } } /** * Schedules the quartz synchronization according to the synchronization mode */ private void scheduleJobs() { switch (syncMode) { case DROPBOX_TO_LOCAL: logger.debug("Scheduling DROPBOX_TO_LOCAL download interval: {}", DropboxSynchronizer.downloadInterval); schedule(DropboxSynchronizer.downloadInterval, false); break; case LOCAL_TO_DROPBOX: logger.debug("Scheduling LOCAL_TO_DROPBOX upload interval: {}", DropboxSynchronizer.uploadInterval); schedule(DropboxSynchronizer.uploadInterval, true); break; case BIDIRECTIONAL: logger.debug("Scheduling BIDIRECTIONAL download interval: {}, upload interval: {}", DropboxSynchronizer.downloadInterval, DropboxSynchronizer.uploadInterval); schedule(DropboxSynchronizer.downloadInterval, false); schedule(DropboxSynchronizer.uploadInterval, true); break; default: throw new IllegalArgumentException("Unknown SyncMode '" + syncMode + "'"); } } /** * Schedules either a job handling the Upload (<code>LOCAL_TO_DROPBOX</code>) * or Download (<code>DROPBOX_TO_LOCAL</code>) direction depending on * <code>isUpload</code>. * * @param interval the Trigger interval as cron expression * @param isUpload */ private void schedule(String interval, boolean isUpload) { String direction = isUpload ? "Upload" : "Download"; try { Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); JobDetail job = newJob(SynchronizationJob.class).withIdentity(direction, DROPBOX_SCHEDULER_GROUP).build(); CronTrigger trigger = newTrigger().withIdentity(direction, DROPBOX_SCHEDULER_GROUP) .withSchedule(CronScheduleBuilder.cronSchedule(interval)).build(); logger.debug("Scheduled synchronization job (direction={}) with cron expression '{}'", direction, interval); sched.scheduleJob(job, trigger); } catch (SchedulerException e) { logger.warn("Could not create synchronization job: {}", e.getMessage()); } } /** * Delete all quartz scheduler jobs of the group <code>Dropbox</code>. */ private void cancelAllJobs() { try { Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); Set<JobKey> jobKeys = sched.getJobKeys(jobGroupEquals(DROPBOX_SCHEDULER_GROUP)); if (jobKeys.size() > 0) { sched.deleteJobs(new ArrayList<JobKey>(jobKeys)); logger.debug("Found {} synchronization jobs to delete from DefaultScheduler (keys={})", jobKeys.size(), jobKeys); } } catch (SchedulerException e) { logger.warn("Couldn't remove synchronization job: {}", e.getMessage()); } } /** * A quartz scheduler job to execute the synchronization. There can be only * one instance of a specific job type running at the same time. */ @DisallowConcurrentExecution public static class SynchronizationJob implements Job { private final static JobKey UPLOAD_JOB_KEY = new JobKey("Upload", DROPBOX_SCHEDULER_GROUP); @Override public void execute(JobExecutionContext context) throws JobExecutionException { boolean isUpload = UPLOAD_JOB_KEY.compareTo(context.getJobDetail().getKey()) == 0; DropboxSynchronizer synchronizer = DropboxSynchronizer.instance; if (synchronizer != null) { try { DbxClient client = getClient(synchronizer); if (client != null) { if (isUpload) { synchronizer.syncLocalToDropbox(client); } else { synchronizer.syncDropboxToLocal(client); } } else { logger.info("Couldn't create Dropbox client. Most likely there has been no " + "access token found. Please restart the authentication process by" + " typing 'startAuthentication' on the OSGi console"); } } catch (Exception e) { logger.warn("Synchronizing data with Dropbox threw an exception", e); } } else { logger.debug("DropboxSynchronizer instance hasn't been initialized properly!"); } } /** * Creates and returns a new {@link DbxClient} initialized with the store access token. * Returns {@code null} if no access token has been found. * * @return a new {@link DbxClient} or <code>null</code> if no access token has been found. */ private DbxClient getClient(DropboxSynchronizer synchronizer) { String accessToken = synchronizer.readAccessToken(); if (StringUtils.isNotBlank(accessToken)) { logger.debug("Creating new DbxClient"); return new DbxClient(requestConfig, accessToken); } return null; } } static private String getUserDbxDataFolder() { String progArg = System.getProperty("smarthome.userdata"); if (progArg != null) { return progArg + File.separator + "dropbox"; } else { return "."; } } static private String getConfigDirFolder() { String smartHomeProgArg = System.getProperty("smarthome.configdir"); String openHABProgArg = System.getProperty("openhab.configdir"); if (smartHomeProgArg != null) { return smartHomeProgArg; } else if (openHABProgArg != null) { return openHABProgArg; } else { return "."; } } private boolean isWindows() { return (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0); } }