/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.service.simple; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import divconq.bus.IService; import divconq.bus.Message; import divconq.bus.MessageUtil; import divconq.filestore.CommonPath; import divconq.filestore.IFileStoreFile; import divconq.filestore.local.FileSystemDriver; import divconq.hub.DomainInfo; import divconq.hub.Hub; import divconq.hub.HubEvents; import divconq.hub.IEventSubscriber; import divconq.lang.op.FuncCallback; import divconq.lang.op.FuncResult; import divconq.lang.op.OperationCallback; import divconq.lang.op.OperationContext; import divconq.log.Logger; import divconq.mod.ExtensionBase; import divconq.session.Session; import divconq.session.DataStreamChannel; import divconq.struct.CompositeParser; import divconq.struct.CompositeStruct; import divconq.struct.FieldStruct; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.util.MimeUtil; import divconq.util.StringUtil; import divconq.work.ScriptWork; import divconq.work.Task; import divconq.work.TaskRun; import divconq.xml.XElement; public class FileServerService extends ExtensionBase implements IService { protected FileSystemDriver fsd = new FileSystemDriver(); protected Map<String, FileSystemDriver> domainFsd = new HashMap<>(); protected Session channels = null; protected String bestEvidence = null; protected String minEvidence = null; @Override public void init(XElement config) { super.init(config); this.fsd.setRootFolder(".\temp"); this.bestEvidence = "SHA256"; this.minEvidence = "Size"; if (config != null) { if (config.hasAttribute("FileStorePath")) { this.fsd.setRootFolder(config.getAttribute("FileStorePath")); // don't wait on this, it'll log correctly this.fsd.connect(null, new OperationCallback() { @Override public void callback() { // NA } }); } if (config.hasAttribute("BestEvidence")) this.bestEvidence = config.getAttribute("BestEvidence"); if (config.hasAttribute("MinimumEvidence")) this.minEvidence = config.getAttribute("MinimumEvidence"); } // load domain related details only after we are noted to be running Hub.instance.subscribeToEvent(HubEvents.Running, new IEventSubscriber() { @Override public void eventFired(Object e) { for (DomainInfo domain : Hub.instance.getDomains().getDomains()) { XElement dset = domain.getSettings(); if (dset != null) { XElement fsset = dset.find("FileServer"); if ((fsset != null) && fsset.hasAttribute("FileStorePath")) { FileSystemDriver dfsd = new FileSystemDriver(); dfsd.setRootFolder(fsset.getAttribute("FileStorePath")); // don't wait on this, it'll log correctly dfsd.connect(null, new OperationCallback() { @Override public void callback() { // NA } }); FileServerService.this.domainFsd.put(domain.getId(), dfsd); // TODO support Best and Min Evidence at domain level } } } } }); this.channels = Hub.instance.getSessions().createForService(); } @Override public void start() { super.start(); /* * Select: { * Path: "/User/karabiner", * Recursion: 999, * Content: false * } * */ /* EventLoopGroup el = Hub.instance.getEventLoopGroup(); try { ServerBootstrap sb = new ServerBootstrap() .group(el) .channel(NioServerSocketChannel.class) .handler(new ChannelInitializer<ServerSocketChannel>() { @Override public void initChannel(ServerSocketChannel ch) throws Exception { //ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO)); } }) //.childOption(ChannelOption.AUTO_READ, false) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { CtpAdapter adapter = FileServerService.this.channels.allocateCtpAdapter(); adapter.setMapper(CtpfCommandMapper.instance); adapter.setHandler(new ICommandHandler() { protected IFileSelector lastSelector = null; @Override public void handle(CtpCommand cmd, CtpAdapter adapter) { //System.out.println("Server got command: " + cmd.getCmdCode()); if ((cmd instanceof RequestCommand) && ((RequestCommand)cmd).isOp(CtpConstants.CTP_F_OP_SELECT)) { FileSelection sel = new FileSelection() .withInstructions(((RequestCommand)cmd).getBody().getFieldAsRecord("Select")); this.lastSelector = FileServerService.this.fsd.select(sel); try { RecordStruct res = new RecordStruct( new FieldStruct("Messages", new ListStruct( new RecordStruct( new FieldStruct("Code", 1), new FieldStruct("Message", "failed to list!!"), new FieldStruct("Level", "Info"), // Error to test error new FieldStruct("Occurred", "20141118T063704Z") ) )) ); ResponseCommand resp = new ResponseCommand(); resp.setBody(res); adapter.sendCommand(resp); } catch (Exception x) { System.out.println("Ctp-F Server error: " + x); } } if (cmd instanceof ResponseCommand) { //System.out.println("Ctp-F Server: Client ACK the final block!"); } if (cmd instanceof EngageCommand) { System.out.println("Ctp-F Server: Client sent engage"); try { adapter.sendCommand(new ResponseCommand()); } catch (Exception x) { System.out.println("Ctp-F Server error: " + x); } } /* if ((cmd instanceof RequestCommand) && ((RequestCommand)cmd).isOp(CtpConstants.CTP_F_OP_INIT)) { System.out.println("Ctp-F Server: Client sent init"); try { adapter.sendCommand(new ResponseCommand()); } catch (Exception x) { System.out.println("Ctp-F Server error: " + x); } } * / if ((cmd instanceof SimpleCommand) && (cmd.getCmdCode() == CtpConstants.CTP_F_CMD_STREAM_READ)) { if (this.lastSelector != null) { this.lastSelector.read(adapter); // TODO we should probably handle final back here?? } else { System.out.println("Error - no select"); // TODO send error response... } } if (cmd instanceof BlockCommand) { BlockCommand file = (BlockCommand) cmd; System.out.println("% " + file.getPath() + " " + file.getSize() + " " + (file.isFolder() ? "FOLDER" : "FILE")); adapter.read(); } if ((cmd instanceof SimpleCommand) && (cmd.getCmdCode() == CtpConstants.CTP_F_CMD_STREAM_WRITE)) { System.out.println("Ctp-F Server: Client sent WRITE"); try { adapter.sendCommand(new ResponseCommand()); } catch (Exception x) { System.out.println("Ctp-F Server error: " + x); } } // get more, unless a stream command - then let it handle automatically if (!(cmd instanceof IStreamCommand)) { //System.out.println("Server issues read!!"); adapter.read(); } cmd.release(); } @Override public void close() { System.out.println("Server Connection closed"); } }); //tunnel.read(); ch.pipeline().addLast( //new SslHandler(SslContextFactory.getServerEngine()), // TODO put Zlib encoding directly into CtpHandler so "read" works better //new JdkZlibEncoder(ZlibWrapper.ZLIB), //new JdkZlibDecoder(ZlibWrapper.ZLIB), //new LoggingHandler(LogLevel.INFO), //new LoggingHandler(LogLevel.INFO), new CtpHandler(adapter, true) ); /* server not responsible for keep alive Task alive = new Task().withWork(new IWork() { @Override public void run(TaskRun trun) { //System.out.println("Ctp Client - Active: " + chx.isActive()); try { adapter.sendCommand(CtpCommand.ALIVE); } catch (Exception x) { System.out.println("Ctp-F Client error: " + x); } trun.complete(); } }); Hub.instance.getScheduler().runEvery(alive, 30); * / } }); // Start the server. sb.bind(8181).sync(); } catch (Exception x) { System.out.println("FS Start Error: " + x); } */ /* System.out.println("Folder Listing: "); IFileSelector selector = this.fsd.select( new FileSelection() .withRelativeTo("/User") .withFileSet("/karabiner", 2) .withForListing(true) ); selector.forEach(new FuncCallback<IFileStoreFile>() { @Override public void callback() { System.out.println("Path: " + this.getResult().getPath()); System.out.println("RelativePath: " + this.getResult().path().subpath(selector.selection().relativeTo())); } }); System.out.println(); System.out.println("Folder Detail: "); IFileSelector selector2 = this.fsd.select( new FileSelection() .withFileSet("/User/karabiner", 0) ); selector2.forEach(new FuncCallback<IFileStoreFile>() { @Override public void callback() { System.out.println("FileName: " + this.getResult().getName()); System.out.println("Path: " + this.getResult().getPath()); System.out.println("LastModified: " + this.getResult().getModification()); } }); */ //CommonPath path = new CommonPath("/User/karabiner"); /* CommonPath path = new CommonPath("/User/Salt"); this.fsd.getFolderListing(path, new FuncCallback<List<IFileStoreFile>>() { @Override public void callback() { if (this.hasErrors()) { System.out.println("Folder Listing Failed: " + this.getMessage()); return; } for (IFileStoreFile file : this.getResult()) { System.out.println(); System.out.println("FileName: " + file.getName()); System.out.println("IsFolder: " + file.isFolder()); System.out.println("LastModified: " + file.getModification()); System.out.println("Size: " + file.getSize()); } System.out.println(); } }); */ } @Override public void handle(TaskRun request) { Message msg = (Message) request.getTask().getParams(); String feature = msg.getFieldAsString("Feature"); String op = msg.getFieldAsString("Op"); // find the correct file store for this domain FileSystemDriver fs = this.domainFsd.get(request.getContext().getUserContext().getDomainId()); if (fs == null) fs = this.fsd; if ("FileStore".equals(feature)) { if ("FileDetail".equals(op)) { this.handleFileDetail(request, fs); return; } if ("DeleteFile".equals(op)) { this.handleDeleteFile(request, fs); return; } if ("DeleteFolder".equals(op)) { this.handleDeleteFolder(request, fs); return; } if ("AddFolder".equals(op)) { this.handleAddFolder(request, fs); return; } if ("ListFiles".equals(op)) { this.handleListFiles(request, fs); return; } if ("StartUpload".equals(op)) { this.handleStartUpload(request, fs); return; } if ("FinishUpload".equals(op)) { this.handleFinishUpload(request, fs); return; } if ("StartDownload".equals(op)) { this.handleStartDownload(request, fs); return; } if ("FinishDownload".equals(op)) { this.handleFinishDownload(request, fs); return; } } request.errorTr(441, this.serviceName(), feature, op); request.complete(); } public void handleFileDetail(final TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FilePath"); CommonPath path = new CommonPath(fpath); fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } IFileStoreFile fi = this.getResult(); if (!fi.exists()) { request.error("File does not exist"); request.complete(); return; } final RecordStruct fdata = new RecordStruct(); fdata.setField("FileName", fi.getName()); fdata.setField("IsFolder", fi.isFolder()); fdata.setField("LastModified", fi.getModificationTime()); fdata.setField("Size", fi.getSize()); String meth = rec.getFieldAsString("Method"); if (StringUtil.isEmpty(meth) || fi.isFolder()) { request.setResult(fdata); request.complete(); return; } fi.hash(meth, new FuncCallback<String>() { @Override public void callback() { if (!request.hasErrors()) { fdata.setField("Hash", this.getResult()); request.setResult(fdata); } request.complete(); } }); } }); } public void handleDeleteFile(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FilePath"); CommonPath path = new CommonPath(fpath); fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } IFileStoreFile fi = this.getResult(); if (!fi.exists()) { request.complete(); return; } if (fi.isFolder()) { request.error("Path is folder, use DeleteFolder to remove a folder"); request.complete(); return; } fi.remove(new OperationCallback() { @Override public void callback() { request.complete(); } }); } }); } public void handleDeleteFolder(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FolderPath"); CommonPath path = new CommonPath(fpath); fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } IFileStoreFile fi = this.getResult(); if (!fi.exists()) { request.complete(); return; } if (!fi.isFolder()) { request.error("Path is not folder, use DeleteFile to remove a file"); request.complete(); return; } fi.remove(new OperationCallback() { @Override public void callback() { request.complete(); } }); } }); } public void handleAddFolder(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FolderPath"); CommonPath path = new CommonPath(fpath); fs.addFolder(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { request.complete(); } }); } public void handleListFiles(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FolderPath"); CommonPath path = new CommonPath(fpath); fs.getFolderListing(path, new FuncCallback<List<IFileStoreFile>>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } ListStruct files = new ListStruct(); for (IFileStoreFile file : this.getResult()) { RecordStruct fdata = new RecordStruct(); fdata.setField("FileName", file.getName()); fdata.setField("IsFolder", file.isFolder()); fdata.setField("LastModified", file.getModificationTime()); fdata.setField("Size", file.getSize()); files.addItem(fdata); } request.returnValue(files); } }); } public void handleStartUpload(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FilePath"); CommonPath path = new CommonPath(fpath); fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } boolean forceover = rec.getFieldAsBooleanOrFalse("ForceOverwrite"); boolean resume = !forceover && this.getResult().exists(); // define channel binding RecordStruct binding = new RecordStruct( new FieldStruct("FilePath", path), new FieldStruct("FileSize", rec.getFieldAsInteger("FileSize")), new FieldStruct("Hub", OperationContext.get().getSessionId().substring(0, 5)), new FieldStruct("Session", OperationContext.get().getSessionId()), new FieldStruct("Channel", rec.getFieldAsString("Channel")), new FieldStruct("Append", resume) ); final DataStreamChannel chan = new DataStreamChannel(FileServerService.this.channels.getId(), "Uploading " + fpath, binding); if (rec.hasField("Params")) chan.setParams(rec.getField("Params")); // apply the channel to a write stream from selected file this.getResult().openWrite(chan, new FuncCallback<RecordStruct>() { @Override public void callback() { if (!request.hasErrors()) { // add the channel only after we know it is open FileServerService.this.channels.addChannel(chan); RecordStruct res = this.getResult(); res.setField("BestEvidence", FileServerService.this.bestEvidence); res.setField("MinimumEvidence", FileServerService.this.minEvidence); // get the binding info to return request.setResult(res); } request.complete(); } }); } }); } public void handleFinishUpload(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FilePath"); CommonPath path = new CommonPath(fpath); if ("Faliure".equals(rec.getFieldAsString("Status"))) { request.warn("File upload incomplete or corrupt: " + path); if (!rec.isFieldEmpty("Note")) request.warn("File upload note: " + rec.getFieldAsString("Note")); request.complete(); return; } fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } RecordStruct evidinfo = rec.getFieldAsRecord("Evidence"); String evidenceType = null; // pick best evidence if available, we really don't care if higher is available if (!evidinfo.isFieldEmpty(FileServerService.this.bestEvidence)) { evidenceType = FileServerService.this.bestEvidence; } // else pick the highest available evidence given else { for (FieldStruct fld : evidinfo.getFields()) evidenceType = FileServerService.this.maxEvidence(fld.getName(), evidenceType); } final String selEvidenceType = evidenceType; IFileStoreFile fresult = this.getResult(); final Consumer<Boolean> afterVerify = (pass) -> { if (pass) { if (FileServerService.this.isSufficentEvidence(FileServerService.this.bestEvidence, selEvidenceType)) request.info("Verified best evidence for upload: " + path); else if (FileServerService.this.isSufficentEvidence(FileServerService.this.minEvidence, selEvidenceType)) request.info("Verified minimum evidence for upload: " + path); else request.error("Verified evidence for upload, however evidence is insuffcient: " + path); } else { request.error("File upload incomplete or corrupt: " + path); } if (!request.hasErrors()) FileServerService.this.watch("Upload", fresult); if (!rec.isFieldEmpty("Note")) request.info("File upload note: " + rec.getFieldAsString("Note")); request.complete(); }; // TODO wait for file to flush out? if ("Size".equals(selEvidenceType)) { Long src = evidinfo.getFieldAsInteger("Size"); long dest = fresult.getSize(); boolean match = (src == dest); if (match) request.info("File sizes match"); else request.error("File sizes do not match"); afterVerify.accept(match); } else if (StringUtil.isNotEmpty(selEvidenceType)) { fresult.hash(selEvidenceType, new FuncCallback<String>() { @Override public void callback() { if (request.hasErrors()) { afterVerify.accept(false); } else { String src = evidinfo.getFieldAsString(selEvidenceType); String dest = this.getResult(); boolean match = (src.equals(dest)); if (match) request.info("File hashes match (" + selEvidenceType + ")"); else request.error("File hashes do not match (" + selEvidenceType + ")"); afterVerify.accept(match); } } }); } else { request.error("Missing any form of evidence, supply at least size"); afterVerify.accept(false); } } }); } public Message handleStartDownload(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FilePath"); CommonPath path = new CommonPath(fpath); fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } // define channel binding RecordStruct binding = new RecordStruct( new FieldStruct("FilePath", path), new FieldStruct("Offset", rec.getFieldAsInteger("Offset")), new FieldStruct("Hub", OperationContext.get().getSessionId().substring(0, 5)), new FieldStruct("Session", OperationContext.get().getSessionId()), new FieldStruct("Channel", rec.getFieldAsString("Channel")) ); final DataStreamChannel chan = new DataStreamChannel(FileServerService.this.channels.getId(), "Downloading " + fpath, binding); if (rec.hasField("Params")) chan.setParams(rec.getField("Params")); this.getResult().openRead(chan, new FuncCallback<RecordStruct>() { @Override public void callback() { if (!request.hasErrors()) { // add the channel only after we know it is open FileServerService.this.channels.addChannel(chan); RecordStruct res = this.getResult(); // always return path - if token was used to get file then here is the first chance to know the path/file we are collecting res.setField("FilePath", path); res.setField("Mime", MimeUtil.getMimeType(path)); res.setField("BestEvidence", FileServerService.this.bestEvidence); res.setField("MinimumEvidence", FileServerService.this.minEvidence); // get the binding info to return request.setResult(res); } request.complete(); } }); } }); return null; } public void handleFinishDownload(TaskRun request, FileSystemDriver fs) { RecordStruct rec = MessageUtil.bodyAsRecord(request); String fpath = rec.getFieldAsString("FilePath"); CommonPath path = new CommonPath(fpath); if ("Faliure".equals(rec.getFieldAsString("Status"))) { request.warn("File download incomplete or corrupt: " + path); if (!rec.isFieldEmpty("Note")) request.warn("File download note: " + rec.getFieldAsString("Note")); request.complete(); return; } fs.getFileDetail(path, new FuncCallback<IFileStoreFile>() { @Override public void callback() { if (request.hasErrors()) { request.complete(); return; } RecordStruct evidinfo = rec.getFieldAsRecord("Evidence"); String evidenceType = null; // pick best evidence if available, we really don't care if higher is available if (!evidinfo.isFieldEmpty(FileServerService.this.bestEvidence)) { evidenceType = FileServerService.this.bestEvidence; } // else pick the highest available evidence given else { for (FieldStruct fld : evidinfo.getFields()) evidenceType = FileServerService.this.maxEvidence(fld.getName(), evidenceType); } final String selEvidenceType = evidenceType; final Consumer<Boolean> afterVerify = (pass) -> { if (pass) { if (FileServerService.this.isSufficentEvidence(FileServerService.this.bestEvidence, selEvidenceType)) request.info("Verified best evidence for download: " + path); else if (FileServerService.this.isSufficentEvidence(FileServerService.this.minEvidence, selEvidenceType)) request.info("Verified minimum evidence for download: " + path); else request.error("Verified evidence for download, however evidence is insuffcient: " + path); } else { request.error("File download incomplete or corrupt: " + path); } if (!rec.isFieldEmpty("Note")) request.info("File download note: " + rec.getFieldAsString("Note")); request.complete(); }; if ("Size".equals(selEvidenceType)) { Long src = evidinfo.getFieldAsInteger("Size"); long dest = this.getResult().getSize(); boolean match = (src == dest); if (match) request.info("File sizes match"); else request.error("File sizes do not match"); afterVerify.accept(match); } else if (StringUtil.isNotEmpty(selEvidenceType)) { this.getResult().hash(selEvidenceType, new FuncCallback<String>() { @Override public void callback() { if (request.hasErrors()) { afterVerify.accept(false); } else { String src = evidinfo.getFieldAsString(selEvidenceType); String dest = this.getResult(); boolean match = (src.equals(dest)); if (match) request.info("File hashes match (" + selEvidenceType + ")"); else request.error("File hashes do not match (" + selEvidenceType + ")"); afterVerify.accept(match); } } }); } else { request.error("Missing any form of evidence, supply at least size"); afterVerify.accept(false); } } }); } public boolean isSufficentEvidence(String lookingfor, String got) { if ("Size".equals(lookingfor)) return ("Size".equals(got) || "MD5".equals(got) || "SHA128".equals(got) || "SHA256".equals(got) || "SHA512".equals(got)); if ("MD5".equals(lookingfor)) return ("MD5".equals(got) || "SHA128".equals(got) || "SHA256".equals(got) || "SHA512".equals(got)); if ("SHA128".equals(lookingfor)) return ("SHA128".equals(got) || "SHA256".equals(got) || "SHA512".equals(got)); if ("SHA256".equals(lookingfor)) return ("SHA256".equals(got) || "SHA512".equals(got)); if ("SHA512".equals(lookingfor)) return ("SHA512".equals(got)); return false; } public String maxEvidence(String lhs, String rhs) { if ("Size".equals(lhs) && ("MD5".equals(rhs) || "SHA128".equals(rhs) || "SHA256".equals(rhs) || "SHA512".equals(rhs))) return rhs; if ("MD5".equals(lhs) && ("SHA128".equals(rhs) || "SHA256".equals(rhs) || "SHA512".equals(rhs))) return rhs; if ("SHA128".equals(lhs) && ("SHA256".equals(rhs) || "SHA512".equals(rhs))) return rhs; if ("SHA256".equals(lhs) && "SHA512".equals(rhs)) return rhs; return lhs; } public void watch(String op, IFileStoreFile file) { XElement settings = this.getLoader().getSettings(); if (settings != null) { for (XElement watch : settings.selectAll("Watch")) { String wpath = watch.getAttribute("FilePath"); // if we are filtering on path make sure the path is a parent of the triggered path if (StringUtil.isNotEmpty(wpath)) { CommonPath wp = new CommonPath(wpath); if (!wp.isParent(file.path())) continue; } String tasktag = op + "Task"; for (XElement task : watch.selectAll(tasktag)) { String id = task.getAttribute("Id"); if (StringUtil.isEmpty(id)) id = Task.nextTaskId(); String title = task.getAttribute("Title"); String script = task.getAttribute("Script"); String params = task.selectFirstText("Params"); RecordStruct prec = null; if (StringUtil.isNotEmpty(params)) { FuncResult<CompositeStruct> pres = CompositeParser.parseJson(params); if (pres.isNotEmptyResult()) prec = (RecordStruct) pres.getResult(); } if (prec == null) prec = new RecordStruct(); prec.setField("File", file); if (script.startsWith("$")) script = script.substring(1); Task t = new Task() .withId(id) .withTitle(title) .withParams(prec) .withRootContext(); if (!ScriptWork.addScript(t, Paths.get(script))) { Logger.error("Unable to run script for file watcher: " + watch.getAttribute("FilePath")); continue; } Hub.instance.getWorkPool().submit(t); } } } } }