/** * fileinfo.cpp * This file is part of the YATE Project http://YATE.null.ro * * File info holder * * Yet Another Telephony Engine - a fully featured software PBX and IVR * Copyright (C) 2013-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 FIItem; class FIDirectory; class FIFileData; class FIFile; class FileInfoMsgHandler; class FIAccount; // Holds account data and share items class FileInfo; class ResultSetMngt { public: inline ResultSetMngt() { reset(); } inline ResultSetMngt(const NamedList& list) { reset(&list); } void reset(const NamedList* list = 0); bool m_addRsm; int m_max; int m_index; }; class FIFileData : public RefObject { YNOCOPY(FIFileData); public: inline FIFileData(const char* file, u_int64_t size, unsigned int time, const char* desc = 0) : m_file(file), m_size(size), m_time(time), m_description(desc) {} inline FIFileData(const char* file, const char* desc = 0) : m_file(file), m_size(0), m_time(0), m_description(desc) {} inline const String& file() const { return m_file; } inline u_int64_t size() const { return m_size; } inline unsigned int time() const { return m_time; } inline const String& description() const { return m_description; } void addToList(NamedList& list, const String& prefix) const; bool operator==(const FIFileData& other); inline bool operator!=(const FIFileData& other) { return !operator==(other); } virtual const String& toString() const { return m_file; } static FIFileData* build(const char* file, const char* desc = 0, int* error = 0); protected: String m_file; // File path and name u_int64_t m_size; // File size unsigned int m_time; // File time String m_description; // File description private: FIFileData() {} // No default contructor }; class FIItem : public RefObject { public: inline FIItem(const char* name) : m_name(name) {} inline const String& name() const { return m_name; } virtual FIDirectory* directory() { return 0; } virtual FIFile* file() { return 0; } virtual const String& toString() const { return name(); } private: FIItem() {} String m_name; }; class FIDirectory : public FIItem { friend class FileInfo; public: FIDirectory(const char* name, const char* path, bool setMutex = false, bool updated = false); ~FIDirectory(); inline const String& path() const { return m_path; } inline Mutex* mutex() { return m_mutex; } inline bool lock(long maxwait = -1) { return m_mutex && m_mutex->lock(maxwait); } inline bool unlock() { return m_mutex && m_mutex->unlock(); } inline bool updated() const { return m_updated; } // Update content from file system void update(); // Add/replace an item. This method is not thread safe bool setItemUnsafe(FIItem* item, const String& oldName); // Remove an item. This method is not thread safe bool removeUnsafe(const String& itemName); // Find a directory by path. This method is not thread safe FIDirectory* findDirPath(const String& path); // Find a file by path. This method is not thread safe FIFile* findFilePath(const String& path); // Clear children inline void clear() { m_children.clear(); } virtual FIDirectory* directory() { return this; } void addDirInfoRsp(NamedList& list, const ResultSetMngt& rsm); // Append FIItem data to a list of parameters static void addFIItem(NamedList& list, FIItem* fi, bool full = true, const String& prefix = String::empty()); protected: inline FIItem* findChild(const String& name) { ObjList* o = m_children.find(name); return o ? static_cast(o->get()) : 0; } inline FIDirectory* findDir(const String& name) { FIItem* ch = findChild(name); return ch ? ch->directory() : 0; } inline FIFile* findFile(const String& name) { FIItem* ch = findChild(name); return ch ? ch->file() : 0; } // Add a file // Replace file data if already in the list and changed FIFile* internalAddFile(FIFileData* fd, const String& name); String m_path; Mutex* m_mutex; bool m_updated; ObjList m_children; }; class FIFile : public FIItem { friend class FileInfo; public: inline FIFile(const char* name, FIFileData* data) : FIItem(name), m_data(0) { if (data && data->ref()) m_data = data; } virtual ~FIFile() { TelEngine::destruct(m_data); } inline const FIFileData* data() const { return m_data; } virtual FIFile* file() { return this; } protected: FIFileData* m_data; }; class FileInfoMsgHandler : public MessageHandler { public: // Message handlers // Non-negative enum values will be used as handler priority enum { EngineStart = -1, FileInfo = -2, CallRoute = 90, }; FileInfoMsgHandler(int handler, int prio); protected: virtual bool received(Message& msg); private: int m_handler; }; // Holds account data and share items class FIAccount : public RefObject, public Mutex { public: FIAccount(const char* name); inline const String& name() const { return m_name; } inline bool canRoute() const { return m_canRoute; } inline void canRoute(bool val) { m_canRoute = val; } // Handle file info set bool handleFileInfoSet(const NamedList& list); // Handle file info remove bool handleFileInfoRemove(const NamedList& list, const String& contact); // Handle file info query bool handleFileInfoQuery(const NamedList& list); // Handle call.route bool route(Message& msg, const String& contact); // Remove share for a given contact bool removeShare(const String& contact); virtual const String& toString() const { return name(); } protected: // Find shared for a given contact bool findShare(const String& contact, RefPointer& dir, bool add = false); virtual void destroyed(); ObjList m_share; // List of share dirs. Dir name is the contact name bool m_canRoute; // File transfers can be routed for this account private: FIAccount() {} // No default contructor String m_name; }; class FileInfo : public Module { public: FileInfo(); ~FileInfo(); inline Message* message(const char* msg) { Message* m = new Message(msg); m->addParam("module",name()); return m; } inline void getSendTarget(String& buf) { Lock lck(this); buf = m_sendTarget; } inline void copyRouteParams(NamedList& dest) { Lock lck(this); dest.copyParams(m_routeParams); } virtual void initialize(); bool findAccount(const String& name, RefPointer& acc, bool add = false); bool removeAccount(FIAccount* acc, bool delObj = true); bool handleFileInfo(Message& msg); bool handleCallRoute(Message& msg); protected: virtual bool received(Message& msg, int id); ObjList m_accounts; String m_sendTarget; // File send target to set when routing NamedList m_routeParams; // Parameters to be set when routing }; INIT_PLUGIN(FileInfo); static const char* cfgFile = "fileinfo"; static bool s_engineStarted = false; // Message handlers installed by the module static const TokenDict s_msgHandler[] = { {"engine.start", FileInfoMsgHandler::EngineStart}, {"file.info", FileInfoMsgHandler::FileInfo}, {"call.route", FileInfoMsgHandler::CallRoute}, {0,0} }; // // ResultSetMngt // void ResultSetMngt::reset(const NamedList* list) { m_max = -1; m_index = -1; m_addRsm = false; if (!list) return; m_max = list->getIntValue(YSTRING("rsm_max"),-1); m_index = list->getIntValue(YSTRING("rsm_index"),-1); m_addRsm = (m_max >= 0) || (m_index >= 0); } // // FIFileData // void FIFileData::addToList(NamedList& list, const String& prefix) const { if (time()) list.addParam(prefix + "time",String(time())); list.addParam(prefix + "size",String((unsigned int)size())); if (description()) list.addParam(prefix + "description",description()); } bool FIFileData::operator==(const FIFileData& other) { if (m_file != other.file()) return false; if (m_size != other.size() || m_time != other.time()) return false; return m_description == other.description(); } FIFileData* FIFileData::build(const char* file, const char* desc, int* error) { XDebug(&__plugin,DebugAll,"FIFileData::build(%s,%s)",file,desc); if (TelEngine::null(file)) return 0; File f; bool ok = false; u_int64_t size = 0; unsigned int time = 0; while (f.openPath(file)) { int64_t len = f.length(); if (len < 0 || f.error()) break; size = len; if (!f.getFileTime(time)) break; ok = true; break; } if (ok) return new FIFileData(file,size,time,desc); String tmp; Thread::errorString(tmp,f.error()); Debug(&__plugin,DebugNote,"FileData failed to build file '%s': %d '%s'", file,f.error(),tmp.c_str()); if (error) *error = f.error(); return 0; } // // FIDirectory // FIDirectory::FIDirectory(const char* name, const char* path, bool setMutex, bool updated) : FIItem(name), m_path(path), m_mutex(0), m_updated(updated) { if (setMutex) m_mutex = new Mutex(false,"FIDirectory"); } FIDirectory::~FIDirectory() { if (m_mutex) delete m_mutex; } // Update content from file system void FIDirectory::update() { if (m_updated || !m_path) return; int error = 0; ObjList dirs; ObjList files; bool ok = File::listDirectory(m_path,&dirs,&files,&error); if (Thread::check(false)) return; if (!ok) { String s; Thread::errorString(s,error); Debug(&__plugin,DebugNote,"Failed to list directory '%s': %d %s", m_path.c_str(),error,s.c_str()); return; } if (m_updated) return; m_children.clear(); String p; p << m_path << Engine::pathSeparator(); ObjList* o = dirs.skipNull(); ObjList* last = &m_children; for (; o; o = o->skipNext()) { String* n = static_cast(o->get()); if (!*n) continue; if (Thread::check(false)) break; last = last->append(new FIDirectory(*n,p + *n)); } o = !Thread::check(false) ? files.skipNull() : 0; for (; o; o = o->skipNext()) { String* n = static_cast(o->get()); if (!*n) continue; String tmp = p + *n; FIFileData* fd = FIFileData::build(tmp); if (fd) { last = last->append(new FIFile(*n,fd)); TelEngine::destruct(fd); } if (Thread::check(false)) break; } m_updated = !Thread::check(false); if (!m_updated) m_children.clear(); } // Add/replace an item. This method is not thread safe bool FIDirectory::setItemUnsafe(FIItem* item, const String& oldName) { if (!item) return false; ObjList* last = 0; if (oldName && oldName != item->name()) { ObjList* o = m_children.find(oldName); if (o) { if (!m_children.find(item->name())) { o->set(item); return true; } o->remove(); } } for (ObjList* o = m_children.skipNull(); o;) { FIItem* it = static_cast(o->get()); if (it == item) return true; if (it->name() == item->name()) { o->remove(); o->append(item); return true; } ObjList* tmp = o->skipNext(); if (!tmp) { last = o; break; } o = tmp; } if (last) last->append(item); else m_children.append(item); return true; } // Remove an item. This method is not thread safe bool FIDirectory::removeUnsafe(const String& itemName) { if (!itemName) return false; GenObject* gen = m_children.remove(itemName,false); XDebug(&__plugin,DebugAll,"FIDirectory::removeUnsafe(%s) found=%p [%p]", itemName.c_str(),gen,this); if (!gen) return false; TelEngine::destruct(gen); return true; } // Find a directory by path. This method is not thread safe FIDirectory* FIDirectory::findDirPath(const String& path) { ObjList* list = path.split('/',false); FIDirectory* dir = this; for (ObjList* o = list->skipNull(); dir && o; o = o->skipNext()) dir = dir->findDir(o->get()->toString()); TelEngine::destruct(list); return dir; } // Find a file by path. This method is not thread safe FIFile* FIDirectory::findFilePath(const String& path) { int pos = path.rfind('/'); if (pos < 0) return findFile(path); FIDirectory* d = findDirPath(path.substr(0,pos)); if (d) return d->findFile(path.substr(pos + 1)); return 0; } void FIDirectory::addDirInfoRsp(NamedList& list, const ResultSetMngt& rsm) { // Item count request if (rsm.m_max == 0) { if (!rsm.m_addRsm) return; list.addParam("rsm_count",String(m_children.count())); return; } ObjList* o = m_children.skipNull(); int index = 0; if (rsm.m_index >= 0) { while (index < rsm.m_index && o) { index++; o = o->skipNext(); } } FIItem* first = 0; FIItem* last = 0; int maxItems = rsm.m_max > 0 ? rsm.m_max : 0; unsigned int n = 1; for (; o; o = o->skipNext()) { FIItem* item = static_cast(o->get()); if (!(item->file() || item->directory())) continue; String prefix = "item."; prefix << n++; prefix << "."; addFIItem(list,item,true,prefix); if (!first) first = item; last = item; if (maxItems) { maxItems--; if (!maxItems) break; } } if (!rsm.m_addRsm) return; if (first) { list.addParam("rsm_first",first->name()); list.addParam("rsm_first.index",String(index)); } if (last) list.addParam("rsm_last",last->name()); list.addParam("rsm_count",String(m_children.count())); } // Append FIItem data to a list of parameters void FIDirectory::addFIItem(NamedList& list, FIItem* fi, bool full, const String& prefix) { if (!fi) return; if (prefix) { if (prefix.endsWith(".")) list.addParam(prefix.substr(0,prefix.length() - 1),fi->name()); else list.addParam(prefix,fi->name()); if (fi->file()) list.addParam(prefix + "isfile",String::boolText(true)); } else list.addParam("name",fi->name()); FIFile* f = fi->file(); if (!f) return; const FIFileData* d = f->data(); if (d) d->addToList(list,prefix); } // Add a file FIFile* FIDirectory::internalAddFile(FIFileData* fd, const String& fn) { if (!(fd && fn)) return 0; ObjList* o = m_children.find(fn); FIFile* f = 0; if (!o) { if (fd->ref()) { f = new FIFile(fn,fd); m_children.append(f); DDebug(&__plugin,DebugAll,"Dir(%s) added file '%s' (%s) [%p]", name().c_str(),fn.c_str(),fd->file().c_str(),this); } } else { FIItem* ch = static_cast(o->get()); f = ch->file(); if (f) { const FIFileData* e = f->data(); if (e != fd || *fd != *e) { if (fd->ref()) { DDebug(&__plugin,DebugAll,"Dir(%s) replacing file '%s' %s -> %s [%p]", name().c_str(),fn.c_str(),e ? e->file().c_str() : "", fd->file().c_str(),this); f = new FIFile(fn,fd); o->set(f); } else f = 0; } } else DDebug(&__plugin,DebugInfo, "Dir(%s) can't add file '%s': a non-file item already in the list [%p]", name().c_str(),fn.c_str(),this); } return f; } // // FileInfoMsgHandler // FileInfoMsgHandler::FileInfoMsgHandler(int handler, int prio) : MessageHandler(lookup(handler,s_msgHandler),prio,__plugin.name()), m_handler(handler) { } bool FileInfoMsgHandler::received(Message& msg) { String* module = msg.getParam(YSTRING("module")); if (module && *module == __plugin.name()) return false; switch (m_handler) { case FileInfo: return __plugin.handleFileInfo(msg); case CallRoute: return __plugin.handleCallRoute(msg); case EngineStart: s_engineStarted = true; return false; } return false; } // // FIAccount // FIAccount::FIAccount(const char* name) : Mutex(false,"FIAccount"), m_canRoute(true), m_name(name) { } // Handle file info set for a given account bool FIAccount::handleFileInfoSet(const NamedList& list) { const String& contact = list[YSTRING("contact")]; XDebug(&__plugin,DebugAll,"Account(%s) handleFileInfoSet(%s) [%p]", name().c_str(),contact.c_str(),this); RefPointer dir; String prefix = "item"; for (unsigned int i = 0; true; i++) { String pref = prefix; if (i) pref << "." << i; String* shareName = list.getParam(pref); if (!shareName) { if (i) break; continue; } if (!*shareName) continue; // Share name can't contain '/' if (shareName->find('/') >= 0) { Debug(&__plugin,DebugNote,"Share name '%s' contains '/' (not accepted)", shareName->c_str()); continue; } String path = list[pref + ".path"]; if (path.endsWith("/") || path.endsWith("\\")) path = path.substr(0,path.length() - 1); if (!path) continue; FIItem* item = 0; if (list.getBoolValue(pref + ".isfile")) { Debug(&__plugin,DebugNote,"Can't set share file: not implemented"); continue; } else item = new FIDirectory(*shareName,path); if (!dir) { if (!findShare(contact,dir,true)) { TelEngine::destruct(item); break; } dir->lock(); } bool ok = dir->setItemUnsafe(item,list[pref + ".oldname"]); Debug(&__plugin,ok ? DebugAll : DebugNote, "Account(%s) contact=%s %s item name=%s path=%s [%p]", name().c_str(),contact.c_str(),ok ? "set" : "failed to set", shareName->c_str(),path.c_str(),this); } if (dir) dir->unlock(); dir = 0; return true; } // Handle file info remove bool FIAccount::handleFileInfoRemove(const NamedList& list, const String& contact) { XDebug(&__plugin,DebugAll,"Account(%s) handleFileInfoRemove(%s) [%p]", name().c_str(),contact.c_str(),this); RefPointer dir; String prefix = "item"; bool something = false; for (unsigned int i = 0; true; i++) { String pref = prefix; if (i) pref << "." << i; String* shareName = list.getParam(pref); if (!shareName) { if (i) break; continue; } something = true; if (!*shareName) continue; if (!dir) { if (!findShare(contact,dir)) break; dir->lock(); } if (dir->removeUnsafe(*shareName)) Debug(&__plugin,DebugAll,"Account(%s) contact=%s removed item %s [%p]", name().c_str(),contact.c_str(),shareName->c_str(),this); } if (dir) dir->unlock(); dir = 0; if (!something) return removeShare(contact); return true; } // Handle file info query for a given account bool FIAccount::handleFileInfoQuery(const NamedList& list) { const String& contact = list[YSTRING("from")]; XDebug(&__plugin,DebugAll,"Account(%s) handleFileInfoQuery(%s) [%p]", name().c_str(),contact.c_str(),this); if (!contact) return false; String* dir = list.getParam(YSTRING("dir")); String* file = dir ? 0 : list.getParam(YSTRING("file")); if (!dir && TelEngine::null(file)) return false; RefPointer cdir; bool ok = findShare(contact,cdir); if (!ok) { // Don't respond if there is no subscription const String& sub = list[YSTRING("subscription")]; ok = sub && (sub == YSTRING("both") || sub == YSTRING("from")); } XDebug(&__plugin,ok ? DebugAll : DebugNote, "Account(%s) query from '%s' dir=%s file=%s [%p]",name().c_str(), contact.c_str(),TelEngine::c_safe(dir),TelEngine::c_safe(file),this); Message* m = __plugin.message("file.info"); m->copyParams(list,"account,id"); m->addParam("to",contact); m->addParam("to_instance",list.getValue(YSTRING("from_instance")),false); m->addParam("operation","result"); if (cdir) { cdir->lock(); if (dir) { FIDirectory* directory = cdir; if (*dir) directory = directory->findDirPath(*dir); if (directory) { // TODO: Use a separate thread for update in server mode ? directory->update(); ResultSetMngt rsm(list); directory->addDirInfoRsp(*m,rsm); } } else // TODO: update path ? FIDirectory::addFIItem(*m,cdir->findFilePath(*file)); cdir->unlock(); cdir = 0; } Engine::enqueue(m); return true; } // Handle call.route bool FIAccount::route(Message& msg, const String& contact) { if (!contact) return false; const String& file = msg[YSTRING("file_name")]; if (!file) return false; RefPointer cdir; if (!findShare(contact,cdir)) { Debug(&__plugin,DebugAll,"Account(%s) routing: contact '%s' not found [%p]", name().c_str(),contact.c_str(),this); return false; } Lock lck(cdir->mutex()); FIFile* f = cdir->findFilePath(file); String s; if (f && f->data()) s = f->data()->file(); lck.drop(); Debug(&__plugin,DebugAll,"Account(%s) routing contact='%s' file='%s' found='%s' [%p]", name().c_str(),contact.c_str(),file.c_str(),s.c_str(),this); if (!s) return false; __plugin.copyRouteParams(msg); __plugin.getSendTarget(msg.retValue()); msg.retValue() << s; return true; } // Find shared for a given contact bool FIAccount::findShare(const String& contact, RefPointer& dir, bool add) { if (!contact) return false; Lock lck(this); ObjList* o = m_share.find(contact); XDebug(&__plugin,DebugInfo,"Account(%s) findShare('%s',%u) found=%p [%p]", name().c_str(),contact.c_str(),add,o,this); if (!o) { if (!add) return false; FIDirectory* d = new FIDirectory(contact,0,true,true); o = m_share.append(d); Debug(&__plugin,DebugInfo,"Account(%s) added contact '%s' [%p]", name().c_str(),contact.c_str(),this); } dir = static_cast(o->get()); return dir != 0; } // Remove share for a given contact bool FIAccount::removeShare(const String& contact) { if (!contact) return false; Lock lck(this); GenObject* gen = m_share.remove(contact,false); if (!gen) return false; lck.drop(); Debug(&__plugin,DebugInfo,"Account(%s) removed contact '%s' [%p]", name().c_str(),contact.c_str(),this); TelEngine::destruct(gen); return true; } void FIAccount::destroyed() { m_share.clear(); __plugin.removeAccount(this,false); RefObject::destroyed(); } // // FileInfo // FileInfo::FileInfo() : Module("fileinfo","misc"), m_routeParams("") { Output("Loaded module FileInfo"); } FileInfo::~FileInfo() { Output("Unloading module FileInfo"); } // Utility used in FileInfo::initialize() static inline const NamedList* getSafeSect(Configuration& cfg, const String& name) { NamedList* tmp = cfg.getSection(name); if (tmp) return tmp; return &NamedList::empty(); } void FileInfo::initialize() { static bool first = true; Output("Initializing module FileInfo"); Configuration cfg(Engine::configFile(cfgFile)); const NamedList* callRoute = getSafeSect(cfg,YSTRING("call.route")); if (first) { first = false; setup(); for (const TokenDict* d = s_msgHandler; d->token; d++) { int prio = d->value; if (d->value == FileInfoMsgHandler::CallRoute) prio = callRoute->getIntValue("priority",prio); if (prio < 0) prio = 100; FileInfoMsgHandler* h = new FileInfoMsgHandler(d->value,prio); Engine::install(h); } } lock(); m_sendTarget = callRoute->getValue(YSTRING("file_send_target"),"filetransfer/send/"); m_routeParams.clearParams(); if (callRoute->getBoolValue(YSTRING("set_default_params"),true)) { m_routeParams.addParam("autoclose",String::boolText(true)); m_routeParams.addParam("wait_on_drop","10000"); } m_routeParams.copySubParams(*callRoute,YSTRING("param_"),true); unlock(); } bool FileInfo::findAccount(const String& name, RefPointer& acc, bool add) { if (!name) return false; Lock lck(this); ObjList* o = m_accounts.find(name); if (!o) { if (!add) return false; FIAccount* a = new FIAccount(name); o = m_accounts.append(a); Debug(this,DebugInfo,"Added account '%s' (%p)",a->name().c_str(),a); } acc = static_cast(o->get()); return acc != 0; } bool FileInfo::removeAccount(FIAccount* acc, bool delObj) { if (!acc) return false; Lock lck(this); GenObject* gen = m_accounts.remove(acc,false); if (!gen) return false; lck.drop(); Debug(this,DebugInfo,"Removed account '%s' (%p) delObj=%u",acc->name().c_str(),acc,delObj); if (delObj) TelEngine::destruct(gen); return true; } bool FileInfo::handleFileInfo(Message& msg) { const String& account = msg[YSTRING("account")]; if (!account) return false; bool set = false; bool create = false; bool remove = false; bool query = false; const String& oper = msg[YSTRING("operation")]; if (oper == YSTRING("set")) create = set = true; else if (oper == YSTRING("remove")) remove = true; else if (oper == YSTRING("query")) query = true; else return false; RefPointer acc; if (!findAccount(account,acc,create)) return false; if (create) { const String* canRoute = msg.getParam(YSTRING("canroute")); if (canRoute) acc->canRoute(canRoute->toBoolean()); } bool ok = false; while (true) { if (set) { ok = acc->handleFileInfoSet(msg); break; } if (remove) { const String& contact = msg[YSTRING("contact")]; if (contact) ok = acc->handleFileInfoRemove(msg,contact); else removeAccount(acc); break; } if (query) { ok = acc->handleFileInfoQuery(msg); break; } break; } acc = 0; return ok; } bool FileInfo::handleCallRoute(Message& msg) { if (msg[YSTRING("format")] != YSTRING("data")) return false; const String& oper = msg[YSTRING("operation")]; bool send = (oper == YSTRING("send")); if (!send) return false; const String& account = msg[YSTRING("in_line")]; if (!account) return false; // Jingle puts caller party as 'callername' const String* contact = 0; if (msg[YSTRING("module")] == YSTRING("jingle")) contact = msg.getParam(YSTRING("callername")); else contact = msg.getParam(YSTRING("caller")); if (TelEngine::null(contact)) return false; lock(); ObjList* o = m_accounts.find(account); FIAccount* a = o ? static_cast(o->get()) : 0; if (a && !(a->canRoute() && a->ref())) a = 0; unlock(); bool ok = a && a->route(msg,*contact); TelEngine::destruct(a); return ok; } bool FileInfo::received(Message& msg, int id) { return Module::received(msg,id); } }; // anonymous namespace /* vi: set ts=8 sw=4 sts=4 noet: */