/** * filetransfer.cpp * This file is part of the YATE Project http://YATE.null.ro * * File transfer Driver * * Yet Another Telephony Engine - a fully featured software PBX and IVR * Copyright (C) 2004-2014 Null Team * * This software is distributed under multiple licenses; * see the COPYING file in the main directory for licensing * information for this specific distribution. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ #include using namespace TelEngine; namespace { // anonymous class FileHolder; // A file info holder class FileSource; // File data source class FileConsumer; // File data consumer class FileSourceWorker; // Worker for data source class FileChan; // A file transfer channel class FileDriver; // The driver // NOTE: This module's semantic of data/channel direction is from the point // of view of the local machine: // outgoing/send means from storage to engine // Minimum value allowed for send chunk buffer #define SEND_CHUNK_MIN 4096 // Minimum/Default value for send interval #define SEND_SLEEP_MIN 10 #define SEND_SLEEP_DEF 50 // A file info holder class FileHolder { public: inline FileHolder(const String& name, const String& dropChan) : m_fileName(name), m_fileTime(0), m_fileSize(-1), m_transferred(0), m_params(""), m_dropChan(dropChan), m_waitOnDropMs(0) {} // Get file name inline const String& fileName() const { return m_fileName; } // Retrieve MD5 digest inline const String& md5() const { return m_md5HexDigest; } // Retrieve file info inline int64_t fileSize(bool update = false) { if (update || m_fileSize < 0) m_fileSize = m_file.length(); return m_fileSize; } inline unsigned int fileTime(bool update = false) { if (update || !m_fileTime) { m_fileTime = 0; m_file.getFileTime(m_fileTime); } return m_fileTime; } // Set drop chan id inline void setDropChan(const String& id) { m_dropChan = id; } // Build drop message. Reset drop chan inline Message* dropMessage() { if (!m_dropChan) return 0; Message* m = new Message("call.drop"); m->addParam("id",m_dropChan); m_dropChan = ""; return m; } // Add MD5 and/or file info parameters void addFileInfo(NamedList& params, bool md5, bool extra); // Add saved params to another list void addParams(NamedList& params); protected: File m_file; // Source file String m_fileName; // File name and location unsigned int m_fileTime; // File time int64_t m_fileSize; // File size int64_t m_transferred; // Transferred bytes String m_md5HexDigest; // MD5 digest of the file NamedList m_params; // Parameters to copy in notifications String m_dropChan; // Channel to drop on termination unsigned int m_waitOnDropMs; // Time to wait to drop channel }; // A file data source class FileSource : public DataSource, public FileHolder { friend class FileDriver; friend class FileSourceWorker; public: // Create the data source, and init it FileSource(const String& file, NamedList* params = 0, const char* chan = 0, const char* format = 0); // Check if this data source is connected inline bool connected() { Lock mylock(this); return 0 != m_consumers.skipNull(); } // Initialize and start worker // Return true on success bool init(bool buildMd5, String& error); // Wait for a consumer to be attached. Send the file void run(); private: // Release memory virtual void destroyed(); String m_notify; // Target id to notify bool m_notifyProgress; // Notify file transfer progress bool m_notifyPercent; // Notify percent changes only int m_percent; // Notify current percent unsigned int m_buflen; // Transfer buffer length unsigned int m_sleepMs; // Sleep between data transfer unsigned int m_retryableReadErrors; // How many retryable read erros occured DataBlock m_buffer; // Read buffer FileSourceWorker* m_worker; // The worker thread }; // A file data consumer class FileConsumer : public DataConsumer, public FileHolder { friend class FileDriver; public: FileConsumer(const String& file, NamedList* params = 0, const char* chan = 0, const char* format = "data"); // Check if file should be overwritten inline bool overWrite() const { return m_overWrite; } // Check if this data consumer is connected inline bool connected() const { return 0 != getConnSource(); } // Check file(s) existence inline bool fileExists(bool tmp = true, bool file = true) { return (tmp && File::exists(m_tmpFileName)) || (file && File::exists(m_fileName)); } virtual unsigned long Consume(const DataBlock& data, unsigned long tStamp, unsigned long flags); protected: // Release memory virtual void destroyed(); // Terminate: close file, notify, check MD5 (if used) void terminate(const char* error = 0); // Make sure a file path exists bool createPath(String* error); private: String m_notify; // Target id to notify String m_tmpFileName; bool m_notifyProgress; // Notify file transfer progress bool m_notifyPercent; // Notify percent changes only int m_percent; // Notify current percent MD5 m_md5; // Calculate the MD5 if used u_int64_t m_startTime; bool m_terminated; bool m_delTemp; // Delete temporary file bool m_createPath; // Create file path bool m_overWrite; // Overwright existing file }; // File source worker class FileSourceWorker : public Thread { public: inline FileSourceWorker(FileSource* src, Thread::Priority prio = Thread::Normal) : Thread("FileSource Worker",prio), m_source(src) {} virtual void cleanup(); virtual void run(); private: FileSource* m_source; }; class FileChan : public Channel { public: // Build a file transfer channel FileChan(FileSource* src, FileConsumer* cons, bool autoclose); ~FileChan(); }; // The plugin class FileDriver : public Driver { public: enum { ChanAttach = Private, }; FileDriver(); virtual ~FileDriver(); virtual bool msgExecute(Message& msg, String& dest); virtual void initialize(); virtual bool received(Message& msg, int id); // Process chan.attach messages bool chanAttach(Message& msg); // Uninstall the relays bool unload(); // Notify file transfer status static bool notifyStatus(bool send, const String& id, const char* status, const char* filename, int64_t transferred, int64_t total, const char* error = 0, const NamedList* params = 0, const char* chan = 0); // Copy params inline void copyParams(NamedList& dest, const NamedList& src, bool exec = false) { Lock lock(this); const String& list = !exec ? m_copyParams : m_copyExecParams; if (list) dest.copyParams(src,list); } // Attach default path to a file if file path is missing void getPath(String& file); // Add/remove sources and consumers from list // The driver doesn't own the objects: the lists are used only // to show them in status output inline void addSource(FileSource* src) { if (!src) return; Lock lock(this); m_sources.append(src)->setDelete(false); } inline void removeSource(FileSource* src, bool delObj = false) { if (!src) return; Lock lock(this); m_sources.remove(src,delObj); } inline void addConsumer(FileConsumer* cons) { if (!cons) return; Lock lock(this); m_consumers.append(cons)->setDelete(false); } inline void removeConsumer(FileConsumer* cons, bool delObj = false) { if (!cons) return; Lock lock(this); m_consumers.remove(cons,delObj); } protected: // Execute commands virtual bool commandExecute(String& retVal, const String& line); // Handle command complete requests virtual bool commandComplete(Message& msg, const String& partLine, const String& partWord); private: bool m_init; String m_copyParams; // Parameters to send in notifications String m_copyExecParams; // Parameters to copy when a call.execute without // call endpoint is handled ObjList m_sources; ObjList m_consumers; }; /* * Local data and functions */ INIT_PLUGIN(FileDriver); static unsigned int s_sendChunk = 4096; // Buffer size used when sending data static unsigned int s_sendIntervalMs = SEND_SLEEP_DEF; // Interval to send packets static unsigned int s_srcLingerIntervals = 10; // How many intervals to wait before terminating // an autoclose source static int s_retryableReadErrors = 1000; // How many retryable read errors are // allowed when sending a file (-1 to retry forever) static bool s_notifyProgress = true; // Notify file transfer progress static bool s_notifyPercent = true; // Notify transfer percent static bool s_srcFileInfo = true; // Set file info params in call.execute static bool s_srcFileMd5 = true; // Set file MD5 call.execute static String s_path; // Default path to save files static String s_dirSend = "send"; // Methods allowed in chan.attach and call.execute static String s_dirRecv = "receive"; static String s_statusCmd = "status filetransfer"; // Status commands handled by this module static String s_statusCmds[] = { "send", // Show data sources "receive", // Show data consumers "all", // Show all "" }; // Commands handled by this module static const char* s_cmds[] = { "send", // Send a file "receive", // Receive a file 0 }; // Command line static const char* s_cmdsLine = " filetransfer {{send|receive} filename [callto:]target [[paramname=value]...]}"; UNLOAD_PLUGIN(unloadNow) { if (unloadNow && !__plugin.unload()) return false; return true; } // Get an integer value from a parameter // Check it to be in requested interval static unsigned int getIntValue(const NamedList& params, const char* param, unsigned int defVal, unsigned int minVal, bool allowZero) { int tmp = params.getIntValue(param,defVal); if (!tmp && allowZero) return 0; return (tmp >= (int)minVal) ? tmp : minVal; } // Get the string indicating data direction inline String& dirStr(bool outgoing) { return outgoing ? s_dirSend : s_dirRecv; } // Make sure a path contains only current system path separators static void toNativeSeparators(String& path) { char repl = (*Engine::pathSeparator() == '/') ? '\\' : '/'; char* s = (char*)path.c_str(); for (unsigned int i = 0; i < path.length(); i++, s++) if (*s == repl) *s = *Engine::pathSeparator(); } /* * FileHolder */ void FileHolder::addFileInfo(NamedList& params, bool md5, bool extra) { params.setParam("file_name",m_fileName); if (md5) params.setParam("file_md5",m_md5HexDigest); if (!extra) return; if (m_fileSize >= 0) params.setParam("file_size",String((unsigned int)m_fileSize)); else params.clearParam("file_size"); fileTime(); params.setParam("file_time",String(m_fileTime)); } /* * FileSource */ FileSource::FileSource(const String& file, NamedList* params, const char* chan, const char* format) : DataSource(!null(format) ? format : "data"), FileHolder(file,chan), m_notifyProgress(s_notifyProgress), m_notifyPercent(s_notifyPercent), m_percent(0), m_buflen(s_sendChunk), m_sleepMs(s_sendIntervalMs), m_retryableReadErrors(0), m_worker(0) { if (params) { m_notify = params->getValue("notify"); m_notifyProgress = params->getBoolValue("notify_progress",m_notifyProgress); m_buflen = getIntValue(*params,"send_chunk_size",s_sendChunk,SEND_CHUNK_MIN,true); m_sleepMs = getIntValue(*params,"send_interval",s_sendIntervalMs,SEND_SLEEP_MIN,false); m_waitOnDropMs = params->getIntValue("wait_on_drop",0,0); __plugin.copyParams(m_params,*params); } if (!m_sleepMs) m_sleepMs = SEND_SLEEP_DEF; Debug(&__plugin,DebugAll,"FileSource('%s') [%p]",file.c_str(),this); } // Initialize and start worker bool FileSource::init(bool buildMd5, String& error) { XDebug(&__plugin,DebugAll,"FileSource('%s') init [%p]",m_fileName.c_str(),this); if (!m_file.openPath(m_fileName,false,true,false,false,true)) { Thread::errorString(error,m_file.error()); return false; } if (fileSize() < 0) { Thread::errorString(error,m_file.error()); m_fileSize = 0; return false; } if (!m_buflen) m_buflen = (unsigned int)m_fileSize; m_buffer.assign(0,m_buflen); if (buildMd5 && !m_file.md5(m_md5HexDigest)) { Thread::errorString(error,m_file.error()); return false; } m_worker = new FileSourceWorker(this); if (m_worker->startup()) return true; error = "Failed to start thread"; m_worker = 0; return false; } // Wait for a consumer to be attached. Send the file void FileSource::run() { DDebug(&__plugin,DebugAll,"FileSource(%s) start running [%p]", m_fileName.c_str(),this); m_transferred = 0; FileDriver::notifyStatus(true,m_notify,"pending",m_fileName,0,m_fileSize,0,&m_params); String error; u_int64_t start = 0; // Use a while() to break to the end to cleanup properly while (true) { // Wait until at least one consumer is attached while (true) { if (Thread::check(false)) { error = "cancelled"; break; } if (!lock(100000)) continue; bool cons = (0 != m_consumers.skipNull()); unlock(); Thread::idle(); if (cons) break; } if (error) break; DDebug(&__plugin,DebugAll, "FileSource(%s) starting size=" FMT64 " buflen=%u interval=%u [%p]", m_fileName.c_str(),m_fileSize,m_buflen,m_sleepMs,this); FileDriver::notifyStatus(true,m_notify,"start",m_fileName,0,m_fileSize,0, &m_params,m_dropChan); unsigned long tStamp = 0; start = Time::msecNow(); if (!m_fileSize) break; // Set file pos at start if (-1 == m_file.Stream::seek(0)) { Thread::errorString(error,m_file.error()); break; } unsigned char* buf = 0; unsigned int len = 0; while (true) { if (Thread::check(false)) { error = "cancelled"; break; } if (!buf) { int rd = m_file.readData(m_buffer.data(),m_buffer.length()); if (rd <= 0) { if (m_file.canRetry()) { m_retryableReadErrors++; if (m_retryableReadErrors != (unsigned int)s_retryableReadErrors) continue; } Thread::errorString(error,m_file.error()); break; } buf = (unsigned char*)m_buffer.data(); len = rd; } DataBlock tmp(buf,len,false); XDebug(&__plugin,DebugAll,"FileSource(%s) forwarding %u bytes [%p]", m_fileName.c_str(),len,this); unsigned int sent = Forward(tmp,tStamp); tmp.clear(false); if (sent && sent != invalidStamp()) { m_transferred += sent; if (m_notifyProgress) { bool notif = true; if (m_notifyPercent) { int tmp = (int)((int64_t)m_transferred * 100 / m_fileSize); notif = (m_percent != tmp); if (notif) m_percent = tmp; } if (notif) FileDriver::notifyStatus(true,m_notify,"progressing",m_fileName, m_transferred,m_fileSize); } if (sent == len) { buf = 0; len = 0; } else { buf += sent; len -= sent; } if (m_transferred >= m_fileSize) break; } tStamp += m_sleepMs; Thread::msleep(m_sleepMs,false); } break; } YIGNORE(start); if (error.null()) DDebug(&__plugin,DebugAll, "FileSource(%s) terminated. Transferred " FMT64 " bytes in " FMT64 "ms [%p]", m_fileName.c_str(),m_fileSize,Time::msecNow() - start,this); else { int dbg = DebugMild; if (error == "cancelled") dbg = DebugInfo; Debug(&__plugin,dbg,"FileSource(%s) terminated error='%s' [%p]", m_fileName.c_str(),error.c_str(),this); } m_file.terminate(); FileDriver::notifyStatus(true,m_notify,"terminated",m_fileName, m_transferred,m_fileSize,error,&m_params); Message* m = dropMessage(); if (m) { // Wait for a while to give some time to the remote party to receive the data unsigned int n = 0; if (!error) { if (m_waitOnDropMs) { n = m_waitOnDropMs / m_sleepMs; if (!n) n = 1; } else n = s_srcLingerIntervals; } XDebug(&__plugin,DebugAll, "FileSource(%s) dropping chan '%s' waiting %u intervals of %ums [%p]", m_fileName.c_str(),m->getValue("id"),n,m_sleepMs,this); for (; n && !Thread::check(false); n--) Thread::msleep(m_sleepMs,false); // Drop channel if (error) { if (error == "cancelled") m->addParam("reason","cancelled"); else { m->addParam("reason","failure"); m->addParam("error",error); } } Engine::enqueue(m); } } // Release memory void FileSource::destroyed() { lock(); Thread* th = m_worker; if (m_worker) { Debug(&__plugin,DebugInfo,"FileSource terminating worker [%p]",this); m_worker->cancel(false); } unlock(); while (m_worker) Thread::yield(false); if (th) Debug(&__plugin,DebugInfo,"FileSource worker terminated [%p]",this); FileDriver::notifyStatus(true,m_notify,"destroyed",m_fileName, m_transferred,m_fileSize,0,&m_params); __plugin.removeSource(this); Debug(&__plugin,DebugAll, "FileSource('%s') destroyed transferred " FMT64 "/" FMT64 " [%p]", m_fileName.c_str(),m_transferred,m_fileSize,this); DataSource::destroyed(); } /* * FileConsumer */ FileConsumer::FileConsumer(const String& file, NamedList* params, const char* chan, const char* format) : DataConsumer(!null(format) ? format : "data"), FileHolder(file,chan), m_notifyProgress(s_notifyProgress), m_notifyPercent(s_notifyPercent), m_percent(0), m_startTime(0), m_terminated(false), m_delTemp(true), m_createPath(false), m_overWrite(false) { toNativeSeparators(m_fileName); __plugin.getPath(m_fileName); if (params) { m_notify = params->getValue("notify"); m_notifyProgress = params->getBoolValue("notify_progress",m_notifyProgress); m_fileSize = params->getIntValue("file_size",0); m_md5HexDigest = params->getValue("file_md5"); m_fileTime = params->getIntValue("file_time"); m_createPath = params->getBoolValue(YSTRING("create_path")); m_overWrite = params->getBoolValue(YSTRING("overwrite")); __plugin.copyParams(m_params,*params); } Debug(&__plugin,DebugAll,"FileConsumer('%s') [%p]",m_fileName.c_str(),this); if (m_fileName && m_fileName[m_fileName.length() - 1] != *Engine::pathSeparator()) { m_tmpFileName << m_fileName << ".tmp"; m_delTemp = !File::exists(m_tmpFileName); } else m_delTemp = false; } unsigned long FileConsumer::Consume(const DataBlock& data, unsigned long tStamp, unsigned long flags) { if (m_terminated) return 0; if (!m_startTime) { m_startTime = Time::now(); FileDriver::notifyStatus(false,m_notify,"start",m_fileName,0,m_fileSize,0, &m_params,m_dropChan); // Check file existence if (fileExists(true,false)) { if (!m_overWrite) { terminate("File exists"); Debug(&__plugin,DebugNote, "FileConsumer(%s) failed to start: temporary file already exists! [%p]", m_fileName.c_str(),this); return 0; } int code = 0; if (!File::remove(m_tmpFileName,&code)) { String error; Thread::errorString(error,code); terminate(error); Debug(&__plugin,DebugNote, "FileConsumer(%s) failed to delete temporary file. %d: '%s' [%p]", m_fileName.c_str(),code,error.c_str(),this); return 0; } } else if (m_createPath) { String error; if (!createPath(&error)) { terminate(error); return 0; } } m_delTemp = true; if (!m_file.openPath(m_tmpFileName,true,false,true,true,true)) { String error; Thread::errorString(error,m_file.error()); terminate(error); Debug(&__plugin,DebugNote, "FileConsumer(%s) failed to create temporary file. %d: '%s' [%p]", m_fileName.c_str(),m_file.error(),error.c_str(),this); return 0; } } XDebug(&__plugin,DebugAll,"FileConsumer(%s) consuming %u bytes [%p]", m_fileName.c_str(),data.length(),this); if (data.length() && m_file.valid()) { if (m_file.writeData(data.data(),data.length())) { if (m_md5HexDigest) m_md5 << data; if (m_notifyProgress) { bool notif = true; if (m_notifyPercent) { int tmp = (int)((int64_t)m_transferred * 100 / m_fileSize); notif = (m_percent != tmp); if (notif) m_percent = tmp; } if (notif) FileDriver::notifyStatus(false,m_notify,"progressing",m_fileName, m_transferred,m_fileSize); } } else { String error; Thread::errorString(error,m_file.error()); terminate(error); } } m_transferred += data.length(); if (m_transferred >= m_fileSize) terminate(); return data.length(); } // Release memory void FileConsumer::destroyed() { terminate("cancelled"); FileDriver::notifyStatus(false,m_notify,"destroyed",m_fileName, m_transferred,m_fileSize,0,&m_params); __plugin.removeConsumer(this); Debug(&__plugin,DebugAll, "FileConsumer('%s') destroyed transferred " FMT64 "/" FMT64 " [%p]", m_fileName.c_str(),m_transferred,m_fileSize,this); DataConsumer::destroyed(); } // Terminate: close file, notify, check MD5 (if used) void FileConsumer::terminate(const char* error) { m_file.terminate(); if (m_terminated) return; m_terminated = true; String err = error; while (!err) { // Check MD5 if (m_md5HexDigest && m_md5HexDigest != m_md5.hexDigest()) { err = "Invalid checksum"; break; } // Check file existence if (!m_overWrite && fileExists(false,true)) { err = "File exists"; break; } // Rename file and set its modification time // Don't set error if failed to set file time int code = 0; if (File::rename(m_tmpFileName,m_fileName,&code)) { if (m_fileTime) File::setFileTime(m_fileName,m_fileTime); } else { File::remove(m_fileName); // Avoid error=No error if (code) Thread::errorString(err,code); else err = "Unknown error"; } break; } if (m_delTemp) File::remove(m_tmpFileName); // Notify and terminate drop the channel FileDriver::notifyStatus(false,m_notify,"terminated",m_fileName, m_transferred,m_fileSize,err,&m_params); Message* m = dropMessage(); if (m) { if (err) { m->addParam("reason","failure"); m->addParam("error",err); } Engine::enqueue(m); } } // Make sure a file path exists bool FileConsumer::createPath(String* error) { const String& orig = m_tmpFileName; if (!orig) return true; char sep = *Engine::pathSeparator(); int pos = orig.rfind(sep); if (pos <= 0) return true; String path = orig.substr(0,pos); ObjList list; bool exists = false; while (path) { exists = File::exists(path); if (exists) break; int pos = path.rfind(sep); if (pos < 0) break; String* s = new String(path.substr(pos + 1)); if (!TelEngine::null(s)) list.insert(s); else TelEngine::destruct(s); path = path.substr(0,pos); } int code = 0; bool ok = true; if (path && !exists) ok = File::mkDir(path,&code); while (ok) { ObjList* o = list.skipNull(); if (!o) break; path.append(*static_cast(o->get()),Engine::pathSeparator()); o->remove(); ok = File::mkDir(path,&code); } if (ok) return true; String tmp; if (!error) error = &tmp; Thread::errorString(*error,code); Debug(&__plugin,DebugNote, "FileConsumer(%s) failed to create path for '%s'. %d: '%s' [%p]", m_fileName.c_str(),orig.c_str(),code,error->c_str(),this); return false; } /* * FileSourceWorker */ void FileSourceWorker::cleanup() { if (!m_source) return; Debug(&__plugin,DebugWarn,"FileSource worker destroyed while holding source (%p)",m_source); m_source->m_worker = 0; m_source = 0; } void FileSourceWorker::run() { if (!m_source) return; m_source->run(); m_source->m_worker = 0; m_source = 0; } /* * FileChan */ FileChan::FileChan(FileSource* src, FileConsumer* cons, bool autoclose) : Channel(__plugin,0,src != 0) { if (src) m_address = src->fileName(); else if (cons) m_address = cons->fileName(); Debug(this,DebugAll,"FileChan(%s,%s) [%p]", dirStr(isOutgoing()).c_str(),m_address.c_str(),this); if (src) setSource(src,src->getFormat()); else setConsumer(cons,cons->getFormat()); if (autoclose) { if (src) src->setDropChan(id()); else if (cons) cons->setDropChan(id()); } TelEngine::destruct(src); TelEngine::destruct(cons); } FileChan::~FileChan() { Debug(this,DebugAll,"FileChan(%s,%s) destroyed [%p]", dirStr(isOutgoing()).c_str(),m_address.c_str(),this); } /* * FileDriver */ FileDriver::FileDriver() : Driver("filetransfer","misc"), m_init(false) { Output("Loaded module File Transfer"); Engine::pluginMode(Engine::LoadEarly); } FileDriver::~FileDriver() { Output("Unloading module File Transfer"); } // Execute/accept file transfer requests bool FileDriver::msgExecute(Message& msg, String& dest) { static const Regexp r("^\\([^/]*\\)/\\(.*\\)$"); if (!dest.matches(r)) return false; bool outgoing = (dest.matchString(1) == s_dirSend); if (!outgoing && dest.matchString(1) != s_dirRecv) { Debug(this,DebugWarn,"Invalid file transfer method '%s', use '%s' or '%s'", dest.matchString(1).c_str(),s_dirSend.c_str(),s_dirRecv.c_str()); return false; } const char* format = msg.getValue("format","data"); // Call execute request from a call endpoint CallEndpoint* ch = YOBJECT(CallEndpoint,msg.userData()); if (ch) { Debug(this,DebugInfo,"%s file '%s'",(outgoing ? "Sending" : "Receiving"), dest.matchString(2).c_str()); // Build source/consumer FileSource* src = 0; FileConsumer* cons = 0; String error; bool ok = true; if (outgoing) { src = new FileSource(dest.matchString(2),&msg,0,format); bool md5 = msg.getBoolValue("getfilemd5"); ok = src->init(md5,error); if (ok) { addSource(src); src->addFileInfo(msg,md5,msg.getBoolValue("getfileinfo")); } } else { cons = new FileConsumer(dest.matchString(2),&msg,0,format); ok = cons->overWrite() || !cons->fileExists(); if (ok) addConsumer(cons); else error = "File exists"; } if (!ok) { Debug(this,DebugWarn,"File %s ('%s') failed error='%s'!",dirStr(outgoing).c_str(), src ? src->fileName().c_str() : cons->fileName().c_str(),error.c_str()); TelEngine::destruct(src); TelEngine::destruct(cons); msg.setParam("error",error); return false; } // Build channel FileChan* c = new FileChan(src,cons,msg.getBoolValue("autoclose")); c->setChanParams(msg); c->initChan(); ok = ch->connect(c,msg.getValue("reason")); if (ok) { c->callConnect(msg); msg.setParam("peerid",c->id()); } TelEngine::destruct(c); return ok; } // Init call from here Message m("call.route"); m.addParam("module",name()); copyParams(m,msg,true); const String& cp = msg[YSTRING("copyparams")]; if (cp) m.copyParams(msg,cp); String callto(msg.getValue("direct")); if (callto.null()) { const char* targ = msg.getValue("target"); if (!targ) { Debug(this,DebugWarn,"No target to %s file!",dirStr(outgoing).c_str()); return false; } callto = msg.getValue("caller"); if (callto.null()) callto << prefix() << dest; m.addParam("called",targ); m.addParam("caller",callto); if (!Engine::dispatch(m)) { Debug(this,DebugWarn,"No route to %s file!",dirStr(outgoing).c_str()); return false; } callto = m.retValue(); m.retValue().clear(); } m = "call.execute"; m.addParam("callto",callto); // Build source/consumer FileSource* src = 0; FileConsumer* cons = 0; FileHolder* fileHolder = 0; bool copyMD5 = msg.getBoolValue("getfilemd5",s_srcFileMd5); String error; if (outgoing) { src = new FileSource(dest.matchString(2),&msg,0,format); if (src->init(copyMD5,error)) { addSource(src); fileHolder = static_cast(src); } } else { cons = new FileConsumer(dest.matchString(2),&msg,0,format); if (cons->overWrite() || !cons->fileExists()) { addConsumer(cons); fileHolder = static_cast(cons); } else error = "File exists"; } if (!fileHolder) { Debug(this,DebugWarn,"File %s ('%s') failed error='%s'!",dirStr(outgoing).c_str(), src ? src->fileName().c_str() : cons->fileName().c_str(),error.c_str()); TelEngine::destruct(src); TelEngine::destruct(cons); msg.setParam("error",error); return false; } // Build message and dispatch it FileChan* c = new FileChan(src,cons,msg.getBoolValue("autoclose")); c->initChan(); m.setParam("id",c->id()); m.userData(c); m.addParam("format",format); m.addParam("operation",dirStr(outgoing)); fileHolder->addFileInfo(m,copyMD5,msg.getBoolValue("getfileinfo",s_srcFileInfo)); const String& remoteFile = msg[YSTRING("remote_file")]; if (remoteFile) m.setParam(YSTRING("file_name"),remoteFile); m.addParam("cdrtrack","false"); bool ok = Engine::dispatch(m); if (ok) msg.setParam("id",c->id()); else { msg.copyParams(m,"error"); Debug(this,DebugWarn,"File %s not accepted!",dirStr(outgoing).c_str()); } TelEngine::destruct(c); return ok; } // Process chan.attach messages bool FileDriver::chanAttach(Message& msg) { // Expect file/[send|receive]/filename static const Regexp r("^filetransfer/\\([^/]*\\)/\\(.*\\)$"); String file(msg.getValue("source")); // Direction bool src = !file.null(); if (!file) file = msg.getValue("consumer"); if (!file) return false; if (file.matches(r)) { if (file.matchString(1) == dirStr(src)) file = file.matchString(2); else { Debug(this,DebugWarn,"Could not attach %s with method '%s', use '%s'", src ? "source" : "consumer",file.matchString(1).c_str(), dirStr(src).c_str()); return false; } } else return false; if (!file) { DDebug(this,DebugNote,"File %s attach request with no file!", src ? "source" : "consumer"); return false; } CallEndpoint* ch = static_cast(msg.userData()); if (!ch) { Debug(this,DebugWarn,"File %s '%s' attach request with no data channel!", src ? "source" : "consumer",file.c_str()); return false; } const char* dropChan = 0; if (msg.getBoolValue("autoclose",true)) dropChan = ch->id(); bool ok = false; const char* format = msg.getValue("format"); if (src) { FileSource* s = new FileSource(file,&msg,dropChan,format); String error; bool md5 = msg.getBoolValue("getfilemd5"); ok = s->init(md5 != 0,error); if (ok) { addSource(s); s->addFileInfo(msg,md5,msg.getBoolValue("getfileinfo")); ch->setSource(s,s->getFormat()); } else msg.setParam("error",error); TelEngine::destruct(s); msg.clearParam("source"); } else { FileConsumer* c = new FileConsumer(file,&msg,dropChan,format); ch->setConsumer(c,c->getFormat()); addConsumer(c); TelEngine::destruct(c); msg.clearParam("consumer"); } return ok; } void FileDriver::initialize() { Output("Initializing module File Transfer"); Configuration cfg(Engine::configFile("filetransfer")); if (!m_init) { setup(); installRelay(Halt); installRelay(Execute); installRelay(Help); installRelay(ChanAttach,"chan.attach",100); } NamedList dummy(""); NamedList* general = cfg.getSection("general"); if (!general) general = &dummy; lock(); m_copyExecParams = "line,account,caller,username,password,subject"; m_copyParams = general->getValue("parameters"); s_sendChunk = getIntValue(*general,"send_chunk_size",4096,SEND_CHUNK_MIN,true); s_sendIntervalMs = getIntValue(*general,"send_interval", SEND_SLEEP_DEF,SEND_SLEEP_MIN,false); s_srcLingerIntervals = getIntValue(*general,"send_linger_intervals",20,1,false); s_notifyProgress = general->getBoolValue("notify_progress",Engine::clientMode()); s_srcFileInfo = general->getBoolValue("source_file_info",true); s_srcFileMd5 = general->getBoolValue("source_file_md5",true); s_path = general->getValue("path","."); if (s_path && !s_path.endsWith(Engine::pathSeparator())) s_path << Engine::pathSeparator(); unlock(); if (debugAt(DebugInfo)) { String s; s << "send_chunk_size=" << s_sendChunk; s << " send_interval=" << s_sendIntervalMs << "ms"; s << " send_linger_intervals=" << s_srcLingerIntervals; s << " notify_progress=" << String::boolText(s_notifyProgress); Debug(this,DebugInfo,"Initialized %s",s.c_str()); } m_init = true; } // Common message relay handler bool FileDriver::received(Message& msg, int id) { if (id == ChanAttach) return chanAttach(msg); if (id == Help) { String line = msg.getValue("line"); if (line.null()) { msg.retValue() << s_cmdsLine << "\r\n"; return false; } if (line != name()) return false; msg.retValue() << s_cmdsLine << "\r\n"; msg.retValue() << "Commands used to control the File Transfer module\r\n"; return true; } if (id == Status) { String target = msg.getValue("module"); // Target is the driver or channel if (!target || target == name() || target.startsWith(prefix())) return Driver::received(msg,id); // Check additional commands if (!target.startSkip(name(),false)) return false; target.trimBlanks(); bool all = (target == "all"); bool src = all || (target == "send"); bool cons = all || (target == "receive"); if (!(src || cons)) return false; Lock lock(this); msg.retValue() << "name=" << name() << ",type=" << type(); unsigned int count = 0; if (src) count += m_sources.count(); if (cons) count += m_consumers.count(); msg.retValue() << ";count=" << count; msg.retValue() << ";format=Direction|Total|Transferred|Connected"; if (src) for (ObjList* os = m_sources.skipNull(); os; os = os->skipNext()) { FileSource* s = static_cast(os->get()); msg.retValue() << ";" << s->m_fileName << "=" << dirStr(true) << "|" << (unsigned int)s->m_fileSize << "|" << (unsigned int)s->m_transferred << "|" << String::boolText(s->connected()); } if (cons) for (ObjList* oc = m_consumers.skipNull(); oc; oc = oc->skipNext()) { FileConsumer* c = static_cast(oc->get()); msg.retValue() << ";" << c->m_fileName << "=" << dirStr(false) << "|" << (unsigned int)c->m_fileSize << "|" << (unsigned int)c->m_transferred << "|" << String::boolText(c->connected()); } msg.retValue() << "\r\n"; return true; } if (id == Halt) unload(); return Driver::received(msg,id); } // Unload the Driver: uninstall the relays bool FileDriver::unload() { DDebug(this,DebugAll,"Unloading..."); if (!lock(500000)) return false; uninstallRelays(); unlock(); return true; } // Notify file transfer status bool FileDriver::notifyStatus(bool send, const String& id, const char* status, const char* filename, int64_t transferred, int64_t total, const char* error, const NamedList* params, const char* chan) { Message* m = new Message("transfer.notify"); m->addParam("targetid",id); m->addParam("send",String::boolText(send)); m->addParam("status",status); if (!null(filename)) m->addParam("file",filename); if (transferred >= 0) m->addParam("transferred",String((unsigned int)transferred)); if (total >= 0) m->addParam("total",String((unsigned int)total)); if (error) m->addParam("error",error); if (chan) m->addParam("channelid",chan); // Add params if (params) { unsigned int n = params->length(); for (unsigned int i = 0; i < n; i++) { NamedString* ns = params->getParam(i); if (ns) m->addParam(ns->name(),*ns); } } return Engine::enqueue(m); } // Attach default path to a file if file path is missing void FileDriver::getPath(String& file) { // Check if the file already have a path separator if (-1 != file.find(*Engine::pathSeparator())) return; Lock lock(this); if (s_path) file = s_path + file; } // Execute commands bool FileDriver::commandExecute(String& retVal, const String& line) { String l = line; l.startSkip(name()); l.trimSpaces(); bool outgoing = l.startSkip("send"); if (outgoing || l.startSkip("receive")) { l.trimSpaces(); String filename, target; int posFile = l.find(' '); int posTarget = -1; bool direct = false; if (posFile > 0) { filename = l.substr(0,posFile); posTarget = l.find(' ',posFile + 1); target = l.substr(posFile + 1,posTarget - posFile - 1); direct = target.startSkip("callto:",false); } if (!(filename && target)) { retVal << "Invalid parameters\r\n"; return true; } Message m("call.execute"); m.addParam(direct ? "direct" : "target",target); // Set parameters if (posTarget > 0) { l.trimSpaces(); ObjList* list = l.substr(posTarget + 1).split(' ',false); for (ObjList* o = list->skipNull(); o; o = o->skipNext()) { int pos = o->get()->toString().find('='); if (pos > 0) { String pname = o->get()->toString().substr(0,pos); String pval = o->get()->toString().substr(pos + 1); Debug(this,DebugAll,"commandExecute() adding param %s=%s", pname.c_str(),pval.c_str()); m.addParam(pname,pval); } } TelEngine::destruct(list); } String dest; dest << dirStr(outgoing) << "/" << filename; if (msgExecute(m,dest)) retVal << (outgoing ? "Sending" : "Receiving"); else retVal << "Failed to " << dirStr(outgoing); retVal << " '" << filename << "' " << (outgoing ? "to " : "from ") << target; retVal << "\r\n"; } else return false; return true; } // Handle command complete requests bool FileDriver::commandComplete(Message& msg, const String& partLine, const String& partWord) { if (partLine.null() && partWord.null()) return false; if (partLine.null() || (partLine == "help")) Module::itemComplete(msg.retValue(),name(),partWord); else if (partLine == name()) { for (const char** list = s_cmds; *list; list++) Module::itemComplete(msg.retValue(),*list,partWord); return true; } bool status = partLine.startsWith("status"); bool drop = !status && partLine.startsWith("drop"); if (!(status || drop)) return Driver::commandComplete(msg,partLine,partWord); // 'status' command Lock lock(this); // line='status filetransfer': add additional commands if (partLine == s_statusCmd) { for (String* list = s_statusCmds; !null(list); list++) if (!partWord || list->startsWith(partWord)) Module::itemComplete(msg.retValue(),*list,partWord); return true; } lock.drop(); return Driver::commandComplete(msg,partLine,partWord); } }; // anonymous namespace /* vi: set ts=8 sw=4 sts=4 noet: */