/* * Hub File Manager Sychronization * * Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Change History: * * Date Who What * ---- --- ---- * 18Oct2022 thebearmay New Code */ static String version() { return '1.0.0' } import groovy.json.JsonOutput import java.text.SimpleDateFormat #include thebearmay.localFileMethods /* ********************************************************************************************* * https://raw.githubusercontent.com/thebearmay/hubitat/main/libraries/localFileMethods.groovy * ***********************************************************************************************/ import groovy.transform.Field definition ( name: "Hub File Sync", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Keep a current copy of File Manager Files on a second hub", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/heHa/hubFileSync.groovy", installOnOpen: true, oauth: false, iconUrl: "", iconX2Url: "" ) preferences { page name: "mainPage" } mappings { path("/remLogin") { action: [POST: "remLogin"] } path("/getList") { action: [POST: "getList"] } } void installed() { if(debugEnabled) log.trace "installed()" state?.isInstalled = true initialize() } void initialize(){ } void logsOff(){ app.updateSetting("debugEnabled",[value:"false",type:"bool"]) } def mainPage(){ dynamicPage (name: "mainPage", title: "", install: true, uninstall: true) { if (app.getInstallationState() == 'COMPLETE') { section("${app.getLabel()} v${version()}") { input "debugEnabled", "bool", title:"Enable Debug Logging:", submitOnChange:true, required:false, defaultValue:false, width:4 if(debugEnabled) { unschedule() runIn(1800,logsOff) } } section("Synchronization Settings", hideable: true, hidden: false){ input "syncMaster", "string", title:"IP Address of Source Hub", width:5, submitOnChange:true input "syncDestination", "string", title: "IP Address of Destination Hub", width:5, submitOnChange:true roleList = ["Unknown", "Source", "Destination"] roleIndex = ["","$syncMaster", "$syncDestination"].indexOf("${location.hub.localIP}") if(roleIndex < 0) roleIndex = 0 paragraph "This hub's role is ${roleList[roleIndex]}" state.hubRole = "${roleList[roleIndex]}" if(state.hubRole == "Destination"){ input "syncIntervalUnit", "enum", title: "Sync Interval Units", options: ["Minutes", "Hours", "Days", "Weeks"], width:4, submitOnChange:true input "syncInterval", "number", title: "Sync Interval", width:4, constraints:["NUMBER"], submitOnChange:true input "firstSync", "time", title: "Time for First Sync", width:4, submitOnChange:true input "checkRead", "button", title:"Test 1 File", width:4 if(state.checkRead) { fileSync() paragraph "${state.lastSyncResult}" state.checkRead = false } input "syncEnabled", "bool", title: "Enable File Synchronization", width: 4, submitOnChange:true, defaultValue: false if(state.currentSe != syncEnabled){ state.currentSe = settings["syncEnabled"] if(syncEnabled){ log.debug firstSync SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") fSync = sdf.parse(firstSync) if(fSync.getTime() < new Date().getTime()) { log.debug "Time in the Past adding 24 hours" newTime = new Date(fSync.getTime() + 86400000) log.debug "New time: $newTime" runOnce(newTime, 'fileSync') } else runOnce(fSync, 'fileSync') } else unschedule('fileSync') } } } section("Source Hub Security", hideable: true, hidden: true){ if(state.accessToken == null) createAccessToken() if(state.hubRole == "Source"){ tDefault = state.accessToken aDefault = getFullLocalApiServerUrl() } input "remoteAPI", "text", title:"Source Server API:",submitOnChange:true, defaultValue: aDefault input "token","text", title:"Source Access Token:",submitOnChange:true, defaultValue: tDefault input "sourceSecurity", "bool", title: "Source Hub Security Enabled", defaultValue: false, submitOnChange: true, width:4 if (sourceSecurity) { input("sUsername", "string", title: "Source Hub Security Username", required: false) input("sPassword", "password", title: "Source Hub Security Password", required: false) } } section("Local Hub Information", hideable: true, hidden: true){ paragraph "Local Server API: ${getFullLocalApiServerUrl()}" paragraph "Cloud Server API: ${getFullApiServerUrl()}" if(state.accessToken == null) createAccessToken() paragraph "Access Token: ${state.accessToken}" input "resetToken", "button", title:"Reset Token" } section("Reset Application Name", hideable: true, hidden: true){ input "nameOverride", "text", title: "New Name for Application", multiple: false, required: false, submitOnChange: true, defaultValue: app.getLabel() if(nameOverride != app.getLabel()) app.updateLabel(nameOverride) } } else { section("") { paragraph title: "Click Done", "Please click Done to install app before continuing" } } } } def fileSync(){ fileList = listRemoteFiles() i=0 fileList.each{ if((state.checkRead && i < 1) || !state.checkRead) copyFile("http://$syncMaster/local/$it") i++ } if(state.checkRead){ log.info "1 file copied from $syncMaster, $i files in list" state.lastSyncResult = "1 file copied from $syncMaster, $i files in list" } else { log.info "$i files copied from $syncMaster" state.lastSyncResult = "$i files copied from $syncMaster" if(syncEnabled) { // "Minutes", "Hours", "Days", "Weeks" switch (syncIntervalType) { case "Minutes": multiplier = 60 break case "Hours": multiplier = 3600 break case "Days": multiplier = 86400 break case "Weeks": multiplier = 604800 break default: log.error "Invalid Interval - $syncIntervalType, scheduling daily" multiplier = 86400 break } Long compSeconds = syncInterval.toLong() * mulitplier runIn(compSeconds, 'fileSync') } } } void copyFile(fPath){ fExist = extFileExists("$fPath") if(fExist.exist == "false") return oName = fPath.substring(fPath.lastIndexOf("/")+1) if(!fExist.fType.contains("text") && !fExist.fType.contains("json")) { imageData = readImage(fPath) writeImageFile(oName, imageData.iContent, imageData.iType) } else { fData = readExtFile(fPath) writeFile("$oName", "$fData") } } List listRemoteFiles(){ if(sourceSecurity){ sendRemote("/remLogin",[username:"$sUsername", password:"$sPassword"]) } sendRemote("/getList", [:]) if(debugEnabled) log.debug state.fList return state.fList.files } HashMap remoteLogin(rName, rPwd){ if(debugEnabled) log.debug "Remote login requested: $rName:$rPwd" def result = false try{ httpPost( [ uri: "http://127.0.0.1:8080", path: "/login", query: [ loginRedirect: "/" ], body: [ username: rName, password: rPwd, submit: "Login" ], textParser: true, ignoreSSLIssues: true ] ) { resp -> if (resp.data?.text?.contains("The login information you supplied was incorrect.")) result = false else { cookie = resp?.headers?.'Set-Cookie'?.split(';')?.getAt(0) result = true } } }catch (e){ log.error "Error logging in: ${e}" result = false cookie = null } if(result){ app.updateSetting("security",[value:"true",type:"bool"]) app.updateSetting("username",[value:"$rName",type:"string"]) app.updateSetting("password",[value:"$rPwd",type:"password"]) } return [result: result, cookie: cookie] } def remLogin() { //request from Destination Server jsonData = (HashMap) request.JSON if(debugEnabled) log.debug "${jsonData.username}, ${jsonData.password}" jsonText = JsonOutput.toJson(remoteLogin("${jsonData.username}", "${jsonData.password}")) if(debugEnabled) log.debug "rendering $jsonText" render contentType:'application/json', data: "$jsonText", status:200 } def getList() {//request from Destination Server jsonText = JsonOutput.toJson(files:listFiles()) if(debugEnabled) log.debug "rendering $jsonText" render contentType:'application/json', data: "$jsonText", status:200 } def sendRemote(command, bodyMap){ def bodyText = JsonOutput.toJson(bodyMap) Map requestParams = [ uri: "$remoteAPI$command?access_token=$token", requestContentType: 'application/json', contentType: 'application/json', body: "$bodyText" ] if(debugEnabled) log.debug "$requestParams" httpPost(requestParams) { resp -> try { if(debugEnabled) log.debug "$resp.properties ${resp.getStatus()}" if(resp.getStatus() == 200 || resp.getStatus() == 207){ if(resp.data) if(debugEnabled) log.debug "$resp.data" if(command == '/remLogin') state.cookie = (HashMap) resp.data else if(command == '/getList') state.fList = (HashMap) resp.data } } catch (Exception ex) { log.error ex } } } @SuppressWarnings('unused') List listFiles(){ if(security) cookie = securityLogin().cookie if(debugEnabled) log.debug "Getting list of files" uri = "http://${location.hub.localIP}:8080/hub/fileManager/json"; def params = [ uri: uri, headers: [ "Cookie": cookie ] ] try { fileList = [] httpGet(params) { resp -> if (resp != null){ if(logEnable) log.debug "Found the files" def json = resp.data for (rec in json.files) { fileList << rec.name } } else { // } } if(debugEnabled) log.debug fileList.sort() return fileList.sort() } catch (e) { log.error e } } void appButtonHandler(btn) { switch(btn) { case "checkRead": state.checkRead = true break case "resetToken": createAccessToken() break default: if(debugEnabled) log.error "Undefined button $btn pushed" break } } void intialize() { } void uninstalled(){ }