def __init__(self, dataRemote: bool = False, allowedDirs: List[str] = None, allowedFileTypes: List[str] = None): """ Args: dataRemote (bool): whether data will be served from the local instance or requests forwarded to a remote instance for handling. allowedDirs (list): list of directories from which files are allowed to be read/writting. File operations will not be permitted unless the file path is a child of an allowed directory. allowedFileTypes (list): list of file extensions, such as '.dcm', '.txt', for which file operations are permitted. No file operations will be done unless the file extension matches one on the list. """ super().__init__(isRemote=dataRemote) if dataRemote is True: return self.initWatchSet = False self.watchDir = None self.currentStreamId = 0 self.streamInfo = None self.allowedDirs = allowedDirs # Remove trailing slash from dir names if allowedDirs is not None: self.allowedDirs = [dir.rstrip('/') for dir in allowedDirs] self.allowedFileTypes = allowedFileTypes # make sure allowed file extensions start with '.' if allowedFileTypes is not None: if allowedFileTypes[0] != '*': for i in range(len(allowedFileTypes)): if not allowedFileTypes[i].startswith('.'): allowedFileTypes[i] = '.' + allowedFileTypes[i] self.fileWatchLock = threading.Lock() # instantiate local FileWatcher self.fileWatcher = FileWatcher()
def __init__(self, filesremote=False, commPipes=None): self.local = not filesremote self.commPipes = commPipes self.fileWatcher = None self.initWatchSet = False if self.local: self.fileWatcher = FileWatcher()
class WsFileWatcher: ''' A server that watches for files on the scanner computer and replies to cloud service requests with the file data. The communication connection is made with webSockets (ws) ''' fileWatcher = FileWatcher() allowedDirs = None allowedTypes = None serverAddr = None sessionCookie = None needLogin = True shouldExit = False validationError = None # Synchronizing across threads clientLock = threading.Lock() fileWatchLock = threading.Lock() @staticmethod def runFileWatcher(serverAddr, retryInterval=10, allowedDirs=defaultAllowedDirs, allowedTypes=defaultAllowedTypes, username=None, password=None, testMode=False): WsFileWatcher.serverAddr = serverAddr WsFileWatcher.allowedDirs = allowedDirs for i in range(len(allowedTypes)): if not allowedTypes[i].startswith('.'): allowedTypes[i] = '.' + allowedTypes[i] WsFileWatcher.allowedTypes = allowedTypes # go into loop trying to do webSocket connection periodically WsFileWatcher.shouldExit = False while not WsFileWatcher.shouldExit: try: if WsFileWatcher.needLogin or WsFileWatcher.sessionCookie is None: WsFileWatcher.sessionCookie = login(serverAddr, username, password, testMode=testMode) wsAddr = os.path.join('wss://', serverAddr, 'wsData') if testMode: print( "Warning: using non-encrypted connection for test mode" ) wsAddr = os.path.join('ws://', serverAddr, 'wsData') logging.log(DebugLevels.L6, "Trying connection: %s", wsAddr) ws = websocket.WebSocketApp( wsAddr, on_message=WsFileWatcher.on_message, on_close=WsFileWatcher.on_close, on_error=WsFileWatcher.on_error, cookie="login="******"Connected to: %s", wsAddr) print("Connected to: {}".format(wsAddr)) ws.run_forever(sslopt={"ca_certs": certFile}) except Exception as err: logging.log( logging.INFO, "WSFileWatcher Exception {}: {}".format( type(err).__name__, str(err))) print('sleep {}'.format(retryInterval)) time.sleep(retryInterval) @staticmethod def stop(): WsFileWatcher.shouldExit = True @staticmethod def on_message(client, message): fileWatcher = WsFileWatcher.fileWatcher response = {'status': 400, 'error': 'unhandled request'} try: request = json.loads(message) response = request.copy() if 'data' in response: del response['data'] cmd = request.get('cmd') dir = request.get('dir') filename = request.get('filename') timeout = request.get('timeout', 0) compress = request.get('compress', False) logging.log(logging.INFO, "{}: {} {}".format(cmd, dir, filename)) # Do Validation Checks if cmd not in ['getAllowedFileTypes', 'ping', 'error']: # All other commands must have a filename or directory parameter if dir is None and filename is not None: dir, filename = os.path.split(filename) if filename is None: errStr = "{}: Missing filename param".format(cmd) return send_error_response(client, response, errStr) if dir is None: errStr = "{}: Missing dir param".format(cmd) return send_error_response(client, response, errStr) if cmd in ('watchFile', 'getFile', 'getNewestFile'): if not os.path.isabs(dir): # make path relative to the watch dir dir = os.path.join(fileWatcher.watchDir, dir) if WsFileWatcher.validateRequestedFile(dir, filename, cmd) is False: errStr = '{}: {}'.format(cmd, WsFileWatcher.validationError) return send_error_response(client, response, errStr) if cmd in ('putTextFile', 'putBinaryFile', 'dataLog'): if not os.path.exists(dir): os.makedirs(dir) if not os.path.exists(dir): errStr = '{}: No such directory: {}'.format(cmd, dir) return send_error_response(client, response, errStr) # Now handle requests if cmd == 'initWatch': minFileSize = request.get('minFileSize') demoStep = request.get('demoStep') if minFileSize is None: errStr = "InitWatch: Missing minFileSize param" return send_error_response(client, response, errStr) WsFileWatcher.fileWatchLock.acquire() try: fileWatcher.initFileNotifier(dir, filename, minFileSize, demoStep) finally: WsFileWatcher.fileWatchLock.release() response.update({'status': 200}) return send_response(client, response) elif cmd == 'watchFile': WsFileWatcher.fileWatchLock.acquire() filename = os.path.join(dir, filename) try: retVal = fileWatcher.waitForFile(filename, timeout=timeout) finally: WsFileWatcher.fileWatchLock.release() if retVal is None: errStr = "WatchFile: 408 Timeout {}s: {}".format( timeout, filename) response.update({'status': 408, 'error': errStr}) logging.log(logging.WARNING, errStr) return send_response(client, response) else: response.update({'status': 200, 'filename': filename}) return send_data_response(client, response, compress) elif cmd == 'getFile': filename = os.path.join(dir, filename) if not os.path.exists(filename): errStr = "GetFile: File not found {}".format(filename) return send_error_response(client, response, errStr) response.update({'status': 200, 'filename': filename}) return send_data_response(client, response, compress) elif cmd == 'getNewestFile': resultFilename = findNewestFile(dir, filename) if resultFilename is None or not os.path.exists( resultFilename): errStr = 'GetNewestFile: file not found: {}'.format( os.path.join(dir, filename)) return send_error_response(client, response, errStr) response.update({'status': 200, 'filename': resultFilename}) return send_data_response(client, response, compress) elif cmd == 'listFiles': if not os.path.isabs(dir): errStr = "listFiles must have an absolute path: {}".format( dir) return send_error_response(client, response, errStr) filePattern = os.path.join(dir, filename) fileList = [x for x in glob.iglob(filePattern, recursive=True)] fileList = WsFileWatcher.filterFileList(fileList) response.update({ 'status': 200, 'filePattern': filePattern, 'fileList': fileList }) return send_response(client, response) elif cmd == 'getAllowedFileTypes': response.update({ 'status': 200, 'fileTypes': WsFileWatcher.allowedTypes }) return send_response(client, response) elif cmd == 'putTextFile': text = request.get('text') if text is None: errStr = 'PutTextFile: Missing text field' return send_error_response(client, response, errStr) elif type(text) is not str: errStr = "PutTextFile: Only text data allowed" return send_error_response(client, response, errStr) fullPath = os.path.join(dir, filename) with open(fullPath, 'w') as volFile: volFile.write(text) response.update({'status': 200}) return send_response(client, response) elif cmd == 'putBinaryFile': try: data = unpackDataMessage(request) except Exception as err: errStr = 'putBinaryFile: {}'.format(err) return send_error_response(client, response, errStr) # If data is None - Incomplete multipart data, more will follow if data is not None: fullPath = os.path.join(dir, filename) with open(fullPath, 'wb') as binFile: binFile.write(data) response.update({'status': 200}) return send_response(client, response) elif cmd == 'dataLog': logLine = request.get('logLine') if logLine is None: errStr = 'DataLog: Missing logLine field' return send_error_response(client, response, errStr) fullPath = os.path.join(dir, filename) with open(fullPath, 'a') as logFile: logFile.write(logLine + '\n') response.update({'status': 200}) return send_response(client, response) elif cmd == 'ping': response.update({'status': 200}) return send_response(client, response) elif cmd == 'error': errorCode = request.get('status', 400) errorMsg = request.get('error', 'missing error msg') if errorCode == 401: WsFileWatcher.needLogin = True WsFileWatcher.sessionCookie = None errStr = 'Error {}: {}'.format(errorCode, errorMsg) logging.log(logging.ERROR, errStr) return else: errStr = 'OnMessage: Unrecognized command {}'.format(cmd) return send_error_response(client, response, errStr) except Exception as err: errStr = "OnMessage Exception: {}: {}".format(cmd, err) send_error_response(client, response, errStr) if cmd == 'error': sys.exit() return errStr = 'unhandled request' send_error_response(client, response, errStr) return @staticmethod def on_close(client): logging.info('connection closed') @staticmethod def on_error(client, error): if type(error) is KeyboardInterrupt: WsFileWatcher.shouldExit = True else: logging.log( logging.WARNING, "on_error: WSFileWatcher: {} {}".format( type(error), str(error))) @staticmethod def validateRequestedFile(dir, file, cmd): textFileTypeOnly = False wildcardAllowed = False if cmd in ('putTextFile', 'dataLog'): textFileTypeOnly = True if cmd in ('listFiles'): wildcardAllowed = True # Restrict requests to certain directories and file types WsFileWatcher.validationError = None if WsFileWatcher.allowedDirs is None or WsFileWatcher.allowedTypes is None: raise StateError( 'FileServer: Allowed Directories or File Types is not set') if file is not None and file != '': fileDir, filename = os.path.split(file) fileExtension = Path(filename).suffix if textFileTypeOnly: if fileExtension != '.txt': WsFileWatcher.validationError = \ 'Only .txt files allowed with command putTextFile() or dataLog()' return False if wildcardAllowed: pass # wildcard searches will be filtered for filetype later elif fileExtension not in WsFileWatcher.allowedTypes: WsFileWatcher.validationError = \ "File type {} not in list of allowed file types {}. " \ "Specify allowed filetypes with FileServer -f parameter.". \ format(fileExtension, WsFileWatcher.allowedTypes) return False if fileDir is not None and fileDir != '': # and os.path.isabs(fileDir): dirMatch = False for allowedDir in WsFileWatcher.allowedDirs: if fileDir.startswith(allowedDir): dirMatch = True break if dirMatch is False: WsFileWatcher.validationError = \ "Path {} not within list of allowed directories {}. " \ "Make sure you specified a full (absolute) path. " \ "Specify allowed directories with FileServer -d parameter.". \ format(fileDir, WsFileWatcher.allowedDirs) return False if dir is not None and dir != '': for allowedDir in WsFileWatcher.allowedDirs: if dir.startswith(allowedDir): return True WsFileWatcher.validationError = \ "Path {} not within list of allowed directories {}. " \ "Make sure you specified a full (absolute) path. " \ "Specify allowed directories with FileServer -d parameter.". \ format(dir, WsFileWatcher.allowedDirs) return False # default case return True @staticmethod def filterFileList(fileList): filteredList = [] for filename in fileList: if os.path.isdir(filename): continue fileExtension = Path(filename).suffix if fileExtension in WsFileWatcher.allowedTypes: filteredList.append(filename) return filteredList