Pārlūkot izejas kodu

Merge pull request #9224 from AttilaVamos/HPCC-16376-impr-6.2.0

HPCC-16376 Extend DFUplus to list or erase history

Reviewed-By: Jake Smith <jake.smith@lexisnexis.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 8 gadi atpakaļ
vecāks
revīzija
8c81f7e672

+ 6 - 8
dali/base/dadfs.cpp

@@ -2746,6 +2746,12 @@ public:
         return nullptr;
     }
 
+    void resetHistory()
+    {
+        DistributedFilePropertyLock lock(this);
+        queryAttributes().removeTree(queryHistory());
+    }
+
 protected:
     class CFileChangeWriteLock
     {
@@ -2790,14 +2796,6 @@ protected:
         root->removeProp("Attr");
         return NULL;
     }
-    IPropertyTree * resetHistory(IPropertyTree *history=NULL)
-    {
-        if (!history)
-            return queryAttributes().setPropTree("History",createPTree("History"));
-        else
-            queryAttributes().removeTree("History");
-        return nullptr;
-    }
     void updateFS(const CDfsLogicalFileName &lfn, unsigned timeoutMs)
     {
         // Update the file system

+ 1 - 0
dali/base/dadfs.hpp

@@ -400,6 +400,7 @@ interface IDistributedFile: extends IInterface
     virtual void validate() = 0;
 
     virtual IPropertyTree *queryHistory() const = 0;                         // DFile History records
+    virtual void resetHistory() = 0;
 };
 
 

+ 256 - 47
dali/dfuplus/dfuplus.cpp

@@ -54,7 +54,7 @@ class CDafsThread: public Thread
     Owned<IException> exc;
 
 public:
-    CDafsThread(SocketEndpoint &_listenep,bool requireauthenticate) 
+    CDafsThread(SocketEndpoint &_listenep,bool requireauthenticate)
         : listenep(_listenep)
     {
         if (listenep.port==0)
@@ -88,7 +88,7 @@ public:
     void stop()
     {
         try {
-            if (server) 
+            if (server)
                 server->stop();
         }
         catch (IException *e) {
@@ -107,7 +107,7 @@ public:
 
 bool CDfuPlusHelper::runLocalDaFileSvr(SocketEndpoint &listenep,bool requireauthenticate, unsigned timeout)
 {
-    Owned<CDafsThread> thr = new CDafsThread(listenep,requireauthenticate); 
+    Owned<CDafsThread> thr = new CDafsThread(listenep,requireauthenticate);
     if (!thr->ok())
         return false;
     thr->start();
@@ -123,7 +123,7 @@ bool CDfuPlusHelper::runLocalDaFileSvr(SocketEndpoint &listenep,bool requireauth
     else {
         loop {
             Sleep(500);
-            if (thr->idleTime()>timeout) { 
+            if (thr->idleTime()>timeout) {
                 thr->stop();
                 break;
             }
@@ -172,7 +172,7 @@ CDfuPlusHelper::CDfuPlusHelper(IProperties* _globals,   CDfuPlusMessagerIntercep
     const char* server = globals->queryProp("server");
     if(server == NULL)
         throw MakeStringException(-1, "Server url not specified");
-    
+
     StringBuffer url;
     if(Utils::strncasecmp(server, "http://", 7) != 0 && Utils::strncasecmp(server, "https://", 8) != 0)
         url.append("http://");
@@ -187,7 +187,7 @@ CDfuPlusHelper::CDfuPlusHelper(IProperties* _globals,   CDfuPlusMessagerIntercep
     StringBuffer fsurl(url.str());
     fsurl.append("FileSpray");
     sprayclient->addServiceUrl(fsurl.str());
-    
+
     dfuclient.setown(createWsDfuClient());
     StringBuffer dfuurl(url.str());
     dfuurl.append("WsDfu");
@@ -264,6 +264,10 @@ int CDfuPlusHelper::doit()
         return resubmit();
     else if(stricmp(action, "monitor") == 0)
         return monitor();
+    else if(stricmp(action, "listhistory") == 0)
+        return listhistory();
+    else if(stricmp(action, "erasehistory") == 0)
+        return erasehistory();
 #ifdef DAFILESRV_LOCAL
     else if(stricmp(action, "dafilesrv") == 0)
         return rundafs();
@@ -277,13 +281,13 @@ bool CDfuPlusHelper::fixedSpray(const char* srcxml,const char* srcip,const char*
 {
     int recordsize;
     if(stricmp(format, "recfmvb") == 0) {
-        recordsize = RECFMVB_RECSIZE_ESCAPE;  // special value for recfmvb 
+        recordsize = RECFMVB_RECSIZE_ESCAPE;  // special value for recfmvb
     }
     else if(stricmp(format, "recfmv") == 0) {
         recordsize = RECFMV_RECSIZE_ESCAPE;  // special value for recfmv
     }
     else if(stricmp(format, "variable") == 0) {
-        recordsize = PREFIX_VARIABLE_RECSIZE_ESCAPE;  // special value for variable 
+        recordsize = PREFIX_VARIABLE_RECSIZE_ESCAPE;  // special value for variable
     }
     else if(stricmp(format, "variablebigendian") == 0) {
         recordsize = PREFIX_VARIABLE_BIGENDIAN_RECSIZE_ESCAPE;  // special value for variable bigendian
@@ -328,14 +332,14 @@ bool CDfuPlusHelper::fixedSpray(const char* srcxml,const char* srcip,const char*
     if(globals->hasProp("push")) {
         if (globals->getPropBool("push"))
             req->setPush(true);
-        else 
+        else
             req->setPull(true);
     }
     if(globals->hasProp("norecover"))
         req->setNorecover(globals->getPropBool("norecover", false));
     else if(globals->hasProp("noRecover"))
         req->setNorecover(globals->getPropBool("noRecover", false));
-    
+
     if(globals->hasProp("connect"))
         req->setMaxConnections(globals->getPropInt("connect"));
     if(globals->hasProp("throttle"))
@@ -439,7 +443,7 @@ bool CDfuPlusHelper::variableSpray(const char* srcxml,const char* srcip,const ch
         if(escape && *escape)
             req->setSourceCsvEscape(escape);
     }
-    else 
+    else
         encoding = format; // may need extra later
 
     req->setSourceFormat(CDFUfileformat::decode(encoding));
@@ -459,7 +463,7 @@ bool CDfuPlusHelper::variableSpray(const char* srcxml,const char* srcip,const ch
     if(globals->hasProp("push")) {
         if (globals->getPropBool("push"))
             req->setPush(true);
-        else 
+        else
             req->setPull(true);
     }
     if(globals->hasProp("compress"))
@@ -509,7 +513,7 @@ int CDfuPlusHelper::spray()
     const char* srcxml = globals->queryProp("srcxml");
     const char* srcip = globals->queryProp("srcip");
     const char* srcfile = globals->queryProp("srcfile");
-    
+
     bool nowait = globals->getPropBool("nowait", false);
 
     MemoryBuffer xmlbuf;
@@ -596,15 +600,15 @@ int CDfuPlusHelper::replicate()
     bool onlyrepeated = repeatlast&&globals->getPropBool("onlyrepeated");
     StringBuffer cluster;
     globals->getProp("cluster",cluster);
-    if (cluster.length()) 
+    if (cluster.length())
         req->setCluster(cluster.str());
     else if (repeatlast) {
         error("replicate repeatlast specified with no cluster\n");
         return 0;
     }
-    if (repeatlast) 
+    if (repeatlast)
         req->setRepeatLast(true);
-    if (onlyrepeated) 
+    if (onlyrepeated)
         req->setOnlyRepeated(true);
 
     Owned<IClientReplicateResponse> result = sprayclient->Replicate(req);
@@ -630,7 +634,7 @@ int CDfuPlusHelper::despray()
     const char* srcname = globals->queryProp("srcname");
     if(srcname == NULL)
         throw MakeStringException(-1, "srcname not specified");
-    
+
     const char* dstxml = globals->queryProp("dstxml");
     const char* dstip = globals->queryProp("dstip");
     const char* dstfile = globals->queryProp("dstfile");
@@ -748,7 +752,7 @@ int CDfuPlusHelper::copy()
     const char* diffkeysrc = globals->queryProp("diffkeydst");
 
     bool nowait = globals->getPropBool("nowait", false);
-    
+
     info("\nCopying from %s to %s\n", srcname, dstname);
 
     Owned<IClientCopy> req = sprayclient->createCopyRequest();
@@ -776,7 +780,7 @@ int CDfuPlusHelper::copy()
     if(globals->hasProp("push")) {
         if (globals->getPropBool("push"))
             req->setPush(true);
-        else 
+        else
             req->setPull(true);
     }
     if(globals->hasProp("compress"))
@@ -842,7 +846,7 @@ int CDfuPlusHelper::copysuper()
     const char* srcpassword = globals->queryProp("srcpassword");
 
     bool nowait = globals->getPropBool("nowait", false);
-    
+
     info("\nCopying superfile from %s to %s\n", srcname, dstname);
 
     Owned<IClientCopy> req = sprayclient->createCopyRequest();
@@ -866,7 +870,7 @@ int CDfuPlusHelper::copysuper()
     if(globals->hasProp("push")) {
         if (globals->getPropBool("push"))
             req->setPush(true);
-        else 
+        else
             req->setPull(true);
     }
     if(globals->hasProp("compress"))
@@ -889,7 +893,7 @@ int CDfuPlusHelper::copysuper()
         req->setNorecover(globals->getPropBool("noRecover", false));
 
     Owned<IClientCopyResponse> result = sprayclient->Copy(req);
-    const char* ret = result->getResult();  
+    const char* ret = result->getResult();
     if(ret == NULL || *ret == '\0')
         exc(result->getExceptions(),"copying");
     else if (stricmp(ret,"OK")==0)
@@ -911,7 +915,7 @@ int CDfuPlusHelper::monitor()
         throw MakeStringException(-1, "neither lfn nor filename specified");
     int shotlimit = globals->getPropInt("shotlimit");
     if (shotlimit == 0)
-        shotlimit = 1;  
+        shotlimit = 1;
     if (lfn)
         info("\nEvent %s: Monitoring logical file name %s\n", eventname?eventname:"", lfn);
     else if (eps)
@@ -925,7 +929,7 @@ int CDfuPlusHelper::monitor()
         req->setEventName(eventname);
     if (lfn)
         req->setLogicalName(lfn);
-    if (eps) 
+    if (eps)
         req->setIp(eps);
     if (filename)
         req->setFilename(filename);
@@ -947,7 +951,7 @@ int CDfuPlusHelper::rundafs()
 {
     unsigned idletimeoutsecs = (unsigned)globals->getPropInt("idletimeout",INFINITE);
     SocketEndpoint listenep;
-    if (runLocalDaFileSvr(listenep,false,(idletimeoutsecs!=INFINITE)?idletimeoutsecs*1000:INFINITE)) 
+    if (runLocalDaFileSvr(listenep,false,(idletimeoutsecs!=INFINITE)?idletimeoutsecs*1000:INFINITE))
         return 0;
     return 1;
 }
@@ -1018,7 +1022,7 @@ int CDfuPlusHelper::remove()
 
     if(files.length() < 1)
         throw MakeStringException(-1, "file name not specified");
-    
+
     Owned<IClientDFUArrayActionRequest> req = dfuclient->createDFUArrayActionRequest();
     req->setType("Delete");
     req->setLogicalFiles(files);
@@ -1085,7 +1089,7 @@ int CDfuPlusHelper::rename()
         throw MakeStringException(-1, "dstname not specified");
 
     bool nowait = globals->getPropBool("nowait", false);
-    
+
     info("\nRenaming from %s to %s\n", srcname, dstname);
 
     Owned<IClientRename> req = sprayclient->createRenameRequest();
@@ -1123,7 +1127,7 @@ int CDfuPlusHelper::list()
     Owned<IClientDFUQueryRequest> req = dfuclient->createDFUQueryRequest();
     if(name != NULL)
         req->setLogicalName(name);
-    
+
     req->setPageSize(-1);
 
     Owned<IClientDFUQueryResponse> resp = dfuclient->DFUQuery(req);
@@ -1180,7 +1184,7 @@ int CDfuPlusHelper::superfile(const char* action)
     const char* superfile = globals->queryProp("superfile");
     if(superfile == NULL || *superfile == '\0')
         throw MakeStringException(-1, "superfile name is not specified");
-    
+
     if(stricmp(action, "add") == 0 || stricmp(action, "remove") == 0)
     {
         const char* before = globals->queryProp("before");
@@ -1236,7 +1240,7 @@ int CDfuPlusHelper::superfile(const char* action)
         Owned<IClientSuperfileListRequest> req = dfuclient->createSuperfileListRequest();
         req->setSuperfile(superfile);
         Owned<IClientSuperfileListResponse> resp = dfuclient->SuperfileList(req);
-        
+
         const IMultiException* excep = &resp->getExceptions();
         if(excep != NULL && excep->ordinality() > 0)
         {
@@ -1261,12 +1265,12 @@ int CDfuPlusHelper::savexml()
     const char* lfn = globals->queryProp("srcname");
     if(lfn == NULL || *lfn == '\0')
         throw MakeStringException(-1, "srcname not specified");
-    
+
     Owned<IClientSavexmlRequest> req = dfuclient->createSavexmlRequest();
     req->setName(lfn);
-    
+
     Owned<IClientSavexmlResponse> resp = dfuclient->Savexml(req);
-    
+
     const IMultiException* excep = &resp->getExceptions();
     if(excep != NULL && excep->ordinality() > 0)
     {
@@ -1284,7 +1288,7 @@ int CDfuPlusHelper::savexml()
         if(ofile == -1)
             throw MakeStringException(-1, "can't open file %s\n", dstxml);
     }
-    
+
     const MemoryBuffer& xmlmap = resp->getXmlmap();
     ssize_t written = write(ofile, xmlmap.toByteArray(), xmlmap.length());
     if (written < 0)
@@ -1300,7 +1304,7 @@ int CDfuPlusHelper::add()
     const char* lfn = globals->queryProp("dstname");
     if(lfn == NULL || *lfn == '\0')
         throw MakeStringException(-1, "dstname not specified");
-    
+
     bool isRemote = false;
     const char* xmlfname = globals->queryProp("srcxml");
     const char* srcname = globals->queryProp("srcname");
@@ -1330,9 +1334,9 @@ int CDfuPlusHelper::add()
         Owned<IClientAddRequest> req = dfuclient->createAddRequest();
         req->setDstname(lfn);
         req->setXmlmap(xmlbuf);
-        
+
         Owned<IClientAddResponse> resp = dfuclient->Add(req);
-        
+
         const IMultiException* excep = &resp->getExceptions();
         if(excep != NULL && excep->ordinality() > 0)
         {
@@ -1354,7 +1358,7 @@ int CDfuPlusHelper::add()
             req->setSrcpassword(srcpassword);
 
         Owned<IClientAddRemoteResponse> resp = dfuclient->AddRemote(req);
-        
+
         const IMultiException* excep = &resp->getExceptions();
         if(excep != NULL && excep->ordinality() > 0)
         {
@@ -1390,7 +1394,7 @@ int CDfuPlusHelper::status()
     }
 
     IConstDFUWorkunit & dfuwu = resp->getResult();
-    
+
     switch(dfuwu.getState())
     {
         case DFUstate_unknown:
@@ -1436,7 +1440,7 @@ int CDfuPlusHelper::abort()
     req->setWuid(wuid);
 
     Owned<IClientAbortDFUWorkunitResponse> resp = sprayclient->AbortDFUWorkunit(req);
-    
+
     const IMultiException* excep = &resp->getExceptions();
     if(excep != NULL && excep->ordinality() > 0)
     {
@@ -1448,7 +1452,212 @@ int CDfuPlusHelper::abort()
     {
         error("Aborted %s\n", wuid);
     }
-    
+
+    return 0;
+}
+
+int CDfuPlusHelper::listhistory()
+{
+    const char* lfn = globals->queryProp("lfn");
+    if (isEmptyString(lfn))
+        throw MakeStringException(-1, "srcname not specified");
+
+    Owned<IClientListHistoryRequest> req = dfuclient->createListHistoryRequest();
+    req->setName(lfn);
+
+    Owned<IClientListHistoryResponse> resp = dfuclient->ListHistory(req);
+
+    const IMultiException* excep = &resp->getExceptions();
+    if (excep != nullptr && excep->ordinality() > 0)
+    {
+        StringBuffer errmsg;
+        excep->errorMessage(errmsg);
+        error("%s\n", errmsg.str());
+        return -1;
+    }
+
+    typedef enum
+    {
+        xml = 0,
+        ascii = 1,
+        csv = 2,
+        json = 3,
+    } out_t;
+
+    out_t format;
+
+    const char *formatPar = globals->queryProp("outformat");
+    if (formatPar == nullptr)
+        format = xml;
+    else if (stricmp(formatPar, "csv") == 0)
+        format=csv;
+    else if (stricmp(formatPar, "xml") == 0)
+        format=xml;
+    else if (stricmp(formatPar, "json") == 0)
+        format=json;
+    else if (stricmp(formatPar, "ascii") == 0)
+        format=ascii;
+    else
+    {
+        error("Unsupported output format: '%s'\n",formatPar);
+        return -1;
+    }
+
+    // The generated getXmlmap() returns with const Memory buffer reference
+    const MemoryBuffer &respMsg = resp->getXmlmap();
+    if (0 == respMsg.length())
+    {
+        error("%s doesn't have stored history!\n", lfn);
+        return -2;
+    }
+
+    Owned<IPropertyTree> history;
+    // Based on the missing createPTree( const MemoryBuffer &)
+    // need to cast from const MemeoryBuffer & to MemoryBuffer & to create PTree
+    history.setown(createPTree((MemoryBuffer &)respMsg));
+    bool sorted = true;
+
+    switch (format)
+    {
+    case xml:
+        {
+            StringBuffer out;
+            toXML(history, out, true);
+            info("\n%s\n",out.trim().str());
+        }
+        break;
+
+    case ascii:
+        {
+            progress("\nHistory of %s:\n", lfn);
+            unsigned index = 1;
+            StringBuffer asciiDump;
+            Owned<IPropertyTreeIterator> historyIter = history->getElements("*");
+            ForEach(*historyIter)
+            {
+                asciiDump.append(index++);
+                Owned<IAttributeIterator> attribIter = historyIter->query().getAttributes(sorted);
+                ForEach(*attribIter)
+                {
+                    asciiDump.append(", ");
+                    const char *propName = attribIter->queryName();
+                    propName++;
+                    const char *propVal =  attribIter->queryValue();
+                    asciiDump.append(propName).append("=").append(propVal);
+                }
+                asciiDump.append("\n");
+            }
+            info("%s", asciiDump.str());
+        }
+        break;
+
+    case csv:
+        {
+            // TODO We need more sophisticated method if the record structure change in the future
+            bool showCsvHeader = globals->getPropBool("csvheader", true);
+            StringBuffer csvDump("\n");
+            unsigned index = 1;
+            Owned<IPropertyTreeIterator> historyIter = history->getElements("*");
+            bool first = true;
+            if (showCsvHeader)
+            {
+                // Get header from the first record
+                if (historyIter->first())
+                {
+                    Owned<IAttributeIterator> attribIter = historyIter->query().getAttributes(sorted);
+                    ForEach(*attribIter)
+                    {
+                        if (!first)
+                            csvDump.append(",");
+                        else
+                            first = false;
+
+                        const char *propName = attribIter->queryName();
+                        propName++;
+                        csvDump.append(propName);
+                    }
+                    csvDump.append("\n");
+                }
+            }
+
+            ForEach(*historyIter)
+            {
+                first = true;
+                Owned<IAttributeIterator> attribIter = historyIter->query().getAttributes(sorted);
+                ForEach(*attribIter)
+                {
+                    if (!first)
+                        csvDump.append(",");
+                    else
+                        first = false;
+
+                    const char *propVal =  attribIter->queryValue();
+                    csvDump.append(propVal);
+                }
+                csvDump.append("\n");
+            }
+            info("%s", csvDump.str());
+        }
+        break;
+
+    case json:
+        {
+            StringBuffer out;
+            toJSON(history, out, 3);
+            info("\n%s\n",out.trim().str());
+        }
+        break;
+    }
+
+    return 0;
+}
+
+int CDfuPlusHelper::erasehistory()
+{
+    const char* lfn = globals->queryProp("lfn");
+    if (isEmptyString(lfn))
+        throw MakeStringException(-1, "srcname not specified");
+
+    bool backup = globals->getPropBool("backup", true);
+    const char* dstxml = globals->queryProp("dstxml");
+    if (backup && isEmptyString(dstxml))
+        throw MakeStringException(-1, "dstxml not specified");
+
+    progress("\nErase history of '%s' with%s backup.\n", lfn, (backup ? "" : "out"));
+
+    Owned<IClientEraseHistoryRequest> req = dfuclient->createEraseHistoryRequest();
+    req->setName(lfn);
+
+    Owned<IClientEraseHistoryResponse> resp = dfuclient->EraseHistory(req);
+
+    const IMultiException* excep = &resp->getExceptions();
+    if (excep != NULL && excep->ordinality() > 0)
+    {
+        StringBuffer errmsg;
+        excep->errorMessage(errmsg);
+        error("%s\n", errmsg.str());
+        return -1;
+    }
+
+    if (backup)
+    {
+        // The generated getXmlmap() return with a const Memory buffer reference
+        const MemoryBuffer &respMsg = resp->getXmlmap();
+        if (0 == respMsg.length())
+        {
+            error("%s doesn't have stored history!\n", lfn);
+            return -2;
+        }
+
+        Owned<IPropertyTree> history;
+        // Based on the missing createPTree( const MemoryBuffer &)
+        // need to cast from const MemeoryBuffer & to MemoryBuffer & to create PTree
+        history.setown(createPTree((MemoryBuffer &)respMsg));
+
+        saveXML(dstxml, history, 3);
+        info("History written into %s.\n",dstxml);
+    }
+
     return 0;
 }
 
@@ -1462,7 +1671,7 @@ int CDfuPlusHelper::resubmit()
     req->setWuid(wuid);
 
     Owned<IClientSubmitDFUWorkunitResponse> resp = sprayclient->SubmitDFUWorkunit(req);
-    
+
     const IMultiException* excep = &resp->getExceptions();
     if(excep != NULL &&  excep->ordinality() > 0)
     {
@@ -1474,7 +1683,7 @@ int CDfuPlusHelper::resubmit()
     {
         info("Resubmitted %s\n", wuid);
     }
-    
+
     return 0;
 }
 
@@ -1501,7 +1710,7 @@ int CDfuPlusHelper::waitToFinish(const char* wuid)
 
         IConstDFUWorkunit & dfuwu = resp->getResult();
 
-        
+
         switch(dfuwu.getState())
         {
             case DFUstate_unknown:
@@ -1588,7 +1797,7 @@ void CDfuPlusHelper::exc(const IMultiException& excep,const char *title)
     error("%s\n",errmsg.str());
 }
 
-void CDfuPlusHelper::info(const char *fmt, ...) 
+void CDfuPlusHelper::info(const char *fmt, ...)
 {
     va_list args;
     va_start(args, fmt);
@@ -1608,7 +1817,7 @@ void CDfuPlusHelper::info(const char *fmt, ...)
         printf("%s",buf.str());
 }
 
-void CDfuPlusHelper::progress(const char *fmt, ...) 
+void CDfuPlusHelper::progress(const char *fmt, ...)
 {
     va_list args;
     va_start(args, fmt);
@@ -1628,7 +1837,7 @@ void CDfuPlusHelper::progress(const char *fmt, ...)
         printf("%s",buf.str());
 }
 
-void CDfuPlusHelper::error(const char *fmt, ...) 
+void CDfuPlusHelper::error(const char *fmt, ...)
 {
     va_list args;
     va_start(args, fmt);

+ 2 - 0
dali/dfuplus/dfuplus.hpp

@@ -63,6 +63,8 @@ private:
     int rundafs();
     int copysuper();
     int updatejobname(const char* wuid, const char* jobname);
+    int listhistory();
+    int erasehistory();
 
     bool fixedSpray(const char* srcxml,const char* srcip,const char* srcfile,const MemoryBuffer &xmlbuf,const char* dstcluster,const char* dstname,const char *format,StringBuffer &retwuid,StringBuffer &except );
     bool variableSpray(const char* srcxml,const char* srcip,const char* srcfile,const MemoryBuffer &xmlbuf,const char* dstcluster,const char* dstname,const char *format,StringBuffer &retwuid,StringBuffer &except );

+ 78 - 53
dali/dfuplus/main.cpp

@@ -31,31 +31,36 @@ void handleSyntax()
     StringBuffer out;
 
     out.append("Usage:\n");
-    out.append("    dfuplus [-v|--version] | action=[spray|replicate|despray|copy|remove|rename|list|\n");
-    out.append("                    addsuper|removesuper|listsuper|copysuper|dafilesrv|\n");
-    out.append("                    savexml|add|status|abort|resubmit|monitor] {<options>}\n\n");
-    out.append("        -v | --version -- display version info\n\n");
+    out.append("    dfuplus [-v|--version] | action=[spray|replicate|despray|copy|remove|rename|\n");
+    out.append("                                     list|addsuper|removesuper|listsuper|\n");
+    out.append("                                     copysuper|dafilesrv|savexml|add|status|\n");
+    out.append("                                     abort|resubmit|monitor] {<options>}\n\n");
+    out.append("        -v | --version  -- display version info\n\n");
     out.append("    general options:\n");
     out.append("        server=<esp-server-url> \n");
     out.append("        username=<user-name>\n");
     out.append("        password=<password>\n");
     out.append("        overwrite=0|1\n");
     out.append("        replicate=1|0\n");
-    out.append("        replicateoffset=N -- node relative offset to find replicate (default 1)\n");
-    out.append("        partlist=<part-list> -- for one or more parts   \n");
-    out.append("        @filename -- read options from filename \n");
-    out.append("        nowait=0|1 -- return immediately without waiting for completion.\n");
-    out.append("        connect=<nn> -- restrict to nn connections at a time.\n");
-    out.append("        transferbuffersize=<n> -- use buffer of size n bytes when transferring data.\n");
-    out.append("        throttle=<nnn> -- restrict the entire transfer speed to nnn Mbits/second\n");
-    out.append("        norecover=0|1 -- don't create or restore from recovery information\n");
-    out.append("        nosplit=0|1 -- optional, don't split a file part to multiple target parts\n");
-    out.append("        compress=0|1 -- optional, compress target\n");
-    out.append("        encrypt=<password> -- optional, encrypt target\n");
-    out.append("        decrypt=<password> -- optional, decrypt source\n");
-    out.append("        push=0|1 -- optional override pull/push default\n");
-    out.append("        jobname=<jobname> -- specify the jobname for spray, despray, copy, rename and replicate.\n");
-    out.append("        failifnosourcefile=0|1 -- optional, don't generate exception when source file set is empty.\n");
+    out.append("        replicateoffset=N  -- node relative offset to find replicate (default 1)\n");
+    out.append("        partlist=<part-list>  -- for one or more parts   \n");
+    out.append("        @filename  -- read options from filename \n");
+    out.append("        nowait=0|1  -- return immediately without waiting for completion.\n");
+    out.append("        connect=<nn>  -- restrict to nn connections at a time.\n");
+    out.append("        transferbuffersize=<n>  -- use buffer of size n bytes when transferring\n");
+    out.append("                                   data.\n");
+    out.append("        throttle=<nnn>  -- restrict the entire transfer speed to nnn Mbits/sec\n");
+    out.append("        norecover=0|1  -- don't create or restore from recovery information\n");
+    out.append("        nosplit=0|1  -- optional, don't split a file part to multiple target\n");
+    out.append("                        parts\n");
+    out.append("        compress=0|1  -- optional, compress target\n");
+    out.append("        encrypt=<password>  -- optional, encrypt target\n");
+    out.append("        decrypt=<password>  -- optional, decrypt source\n");
+    out.append("        push=0|1  -- optional override pull/push default\n");
+    out.append("        jobname=<jobname>  -- specify the jobname for spray, despray, copy,\n");
+    out.append("                              rename and replicate.\n");
+    out.append("        failifnosourcefile=0|1  -- optional, don't generate exception when\n");
+    out.append("                                   source file set is empty.\n");
     out.append("    spray options:\n");
     out.append("        srcip=<source-machine-ip>\n");
     out.append("        srcfile=<source-file-path>\n");
@@ -67,47 +72,54 @@ void handleSyntax()
     out.append("        options for fixed:\n");
     out.append("            recordsize=<record-size>\n");
     out.append("        options for csv/delimited:\n");
-    out.append("            encoding=ascii|utf8|utf8n|utf16|utf16le|utf16be|utf32|utf32le|utf32be -- optional, default is ascii\n");
-    out.append("            maxrecordsize=<max-record-size> -- optional, default is 8192\n");
-    out.append("            separator=<separator> -- optional, default is \\,\n");
-    out.append("            terminator=<terminator> -- optional, default is \\r,\\r\\n\n");
-    out.append("            quote=<quote> -- optional, default is \"\n");
-    out.append("            escape=<escape> -- optional, no default value \n");
-    out.append("            recordstructurepresent=0|1 -- optional, default is 0 (no field names in first row) \n");
-    out.append("            quotedTerminator=1|0 -- optional, default is 1 (quoted terminators in rows) \n");
+    out.append("            encoding=ascii|utf8|utf8n|utf16|utf16le|utf16be|utf32|\n");
+    out.append("                     utf32le|utf32be\n");
+    out.append("                -- optional, default is ascii\n");
+    out.append("            maxrecordsize=<max-record-size>  -- optional, default is 8192\n");
+    out.append("            separator=<separator>  -- optional, default is \\,\n");
+    out.append("            terminator=<terminator>  -- optional, default is \\r,\\r\\n\n");
+    out.append("            quote=<quote>  -- optional, default is \"\n");
+    out.append("            escape=<escape>  -- optional, no default value \n");
+    out.append("            recordstructurepresent=0|1  -- optional, default is 0 (no field \n");
+    out.append("                                           names in first row) \n");
+    out.append("            quotedTerminator=1|0  -- optional, default is 1 (quoted terminators\n");
+    out.append("                                     in rows) \n");
     out.append("        options for xml:\n");
-    out.append("            rowtag=rowTag -- required\n");
-    out.append("            encoding=utf8|utf8n|utf16|utf16le|utf16be|utf32|utf32le|utf32be -- optional, default is utf8\n");
-    out.append("            maxrecordsize=<max-record-size> -- optional, default is 8192\n");
+    out.append("            rowtag=rowTag  -- required\n");
+    out.append("            encoding=utf8|utf8n|utf16|utf16le|utf16be|utf32|utf32le|utf32be \n");
+    out.append("                -- optional, default is utf8\n");
+    out.append("            maxrecordsize=<max-record-size>  -- optional, default is 8192\n");
     out.append("        options for json:\n");
-    out.append("            rowpath=rowPath - optional, default is \"/\"\n");
-    out.append("            maxrecordsize=<max-record-size> -- optional, default is 8192\n");
+    out.append("            rowpath=rowPath  -- optional, default is \"/\"\n");
+    out.append("            maxrecordsize=<max-record-size>  -- optional, default is 8192\n");
     out.append("    replicate options:\n");
     out.append("        srcname=<source-logical-name>\n");
-    out.append("        cluster=<cluster-name>     -- replicates to (additional) cluster\n");
-    out.append("        repeatlast=0|1             -- repeats last part on every node (requires cluster)\n");
-    out.append("        onlyrepeated=0|1           -- ignores parts not repeated (e.g. by repeatlast)\n");
+    out.append("        cluster=<cluster-name>  -- replicates to (additional) cluster\n");
+    out.append("        repeatlast=0|1  -- repeats last part on every node (requires cluster)\n");
+    out.append("        onlyrepeated=0|1  -- ignores parts not repeated (e.g. by repeatlast)\n");
     out.append("    despray options:\n");
     out.append("        srcname=<source-logical-name>\n");
     out.append("        dstip=<destination-machine-ip>\n");
     out.append("        dstfile=<destination-file-path>\n");
     out.append("        dstxml=<xml-file> -- replaces dstip and dstfile\n");
     out.append("        splitprefix=... use prefix (same format as /prefix) to split file up\n");
-    out.append("        wrap=0|1 -- desprays as multiple files\n");
-    out.append("        multicopy=0|1   -- each destination part gets whole file\n");
+    out.append("        wrap=0|1  -- desprays as multiple files\n");
+    out.append("        multicopy=0|1  -- each destination part gets whole file\n");
     out.append("    copy options:\n");
     out.append("        srcname=<source-logical-name>\n");
     out.append("        dstname=<destination-logical-name>\n");
     out.append("        dstcluster=<cluster-name>\n");
-    out.append("        dstclusterroxie=No|Yes <destination cluster is a roxie cluster> -- optional\n");
-    out.append("        srcdali=<foreign-dali-ip> -- optional\n");
-    out.append("        srcusername=<username-for-accessing-srcdali> -- optional\n");
-    out.append("        srcpassword=<password-for-accessing-srcdali> -- optional\n");
-    out.append("        wrap=0|1 -- copies from a larger to smaller cluster without spliting parts\n");
-    out.append("        diffkeysrc=<old-key-name>   -- use keydiff/keypatch (src old name)\n");
-    out.append("        diffkeydst=<old-key-name>   -- use keydiff/keypatch (dst old name)\n");
-    out.append("        multicopy=0|1   -- each destination part gets whole file\n");
-    out.append("        preservecompression=1|0 -- optional, default is 1 (preserve compression)\n");
+    out.append("        dstclusterroxie=No|Yes <destination cluster is a roxie cluster>\n");
+    out.append("            -- optional\n");
+    out.append("        srcdali=<foreign-dali-ip>  -- optional\n");
+    out.append("        srcusername=<username-for-accessing-srcdali>  -- optional\n");
+    out.append("        srcpassword=<password-for-accessing-srcdali>  -- optional\n");
+    out.append("        wrap=0|1  -- copies from a larger to smaller cluster without spliting\n");
+    out.append("                     parts\n");
+    out.append("        diffkeysrc=<old-key-name>  -- use keydiff/keypatch (src old name)\n");
+    out.append("        diffkeydst=<old-key-name>  -- use keydiff/keypatch (dst old name)\n");
+    out.append("        multicopy=0|1  -- each destination part gets whole file\n");
+    out.append("        preservecompression=1|0  -- optional, default is 1 (preserve)\n");
     out.append("    remove options:\n");
     out.append("        name=<logical-name>\n");
     out.append("        names=<multiple-logical-names-separated-by-comma>\n");
@@ -121,11 +133,13 @@ void handleSyntax()
     out.append("            (more to be defined)\n");
     out.append("    addsuper options:\n");
     out.append("        superfile=<logical-name>\n");
-    out.append("        subfiles=<logical-name>{,<logical-name>}  -- no spaces between logical-names.\n");
+    out.append("        subfiles=<logical-name>{,<logical-name>}  -- no spaces between \n");
+    out.append("             		                                 logical-names.\n");
     out.append("        before=<logical-name> -- optional\n");
     out.append("    removesuper options:\n");
     out.append("        superfile=<logical-name>\n");
-    out.append("        subfiles=<logical-name>{,<logical-name> -- optional. Can be *, which means all subfiles\n");
+    out.append("        subfiles=<logical-name>{,<logical-name>  -- optional. Can be *, which\n");
+    out.append("                                                    means all subfiles\n");
     out.append("        delete=1|0 -- optional. Whether or not actually remove the files.\n");
     out.append("    copysuper options:\n");
     out.append("        srcname=<source-super-name>\n");
@@ -142,7 +156,8 @@ void handleSyntax()
     out.append("    add options:\n");
     out.append("        srcxml=<xml-file>\n");
     out.append("        dstname=<destination-logical-name>\n");
-    out.append("        -- To add remote files from another dali directly, use these options instead of srcxml:\n");
+    out.append("            -- To add remote files from another dali directly, use these\n");
+    out.append("               options instead of srcxml:\n");
     out.append("        srcname=<source-logical-name>\n");
     out.append("        srcdali=<source-dali-ip>\n");
     out.append("        srcusername=<user-name-for-source-dali>\n");
@@ -162,6 +177,16 @@ void handleSyntax()
     out.append("        shotlimit=<number>\n");
     out.append("    dafilesrv options:  \n");
     out.append("        idletimeout=<idle-timeout-secs> -- how long idle before stops \n");
+    out.append("    listhistory options:  \n");
+    out.append("        lfn=<logical-name>\n");
+    out.append("        outformat=csv|xml|json|ascii  -- optional, default is xml.\n");
+    out.append("        csvheader=1|0   -- optional, enable/disable to put header at the first\n");
+    out.append("                           CSV row, default is enable.\n");
+    out.append("    erasehistory options:  \n");
+    out.append("        lfn=<logical-name>\n");
+    out.append("        backup=1|0  -- optional, enable/disable to write file history into xml\n");
+    out.append("                        file before erase it, default is enable. \n");
+    out.append("        dstxml=<xml-file>  -- effective only if backup enabled.\n");
 
     printf("%s",out.str());
 }
@@ -217,7 +242,7 @@ int main(int argc, const char* argv[])
         return 0;
     }
 
-    if ((argc >= 2) && ((argv[1][0]=='/' || argv[1][0]=='-') && (argv[1][1]=='?' || argv[1][1]=='h'))) 
+    if ((argc >= 2) && ((argv[1][0]=='/' || argv[1][0]=='-') && (argv[1][1]=='?' || argv[1][1]=='h')))
     {
         handleSyntax();
         return 0;
@@ -236,7 +261,7 @@ int main(int argc, const char* argv[])
 
 
 
-    
+
     const char* action = globals->queryProp("action");
     if(!action || !*action)
     {
@@ -250,7 +275,7 @@ int main(int argc, const char* argv[])
     if (!server || !*server) {
         if (stricmp(action,"dafilesrv")==0)
             globals->setProp("server","127.0.0.1"); // dummy
-        else { 
+        else {
             fprintf(stderr, "ERROR: Esp server url not specified.\n");
             releaseAtoms();
             return DFUERR_TooFewArguments;
@@ -268,7 +293,7 @@ int main(int argc, const char* argv[])
         e->errorMessage(errmsg);
         fprintf(stderr, "%s\n", errmsg.str());
     }
-    
+
     releaseAtoms();
     return 0;
 }

+ 26 - 0
esp/scm/ws_dfu.ecm

@@ -707,6 +707,28 @@ ESPresponse [exceptions_inline, nil_remove, http_encode(0)] DFUGetFileMetaDataRe
 };
 
 
+ESPrequest ListHistoryRequest
+{
+    string name;
+};
+
+ESPresponse [exceptions_inline, nil_remove] ListHistoryResponse
+{
+    binary xmlmap;
+};
+
+ESPrequest EraseHistoryRequest
+{
+    string name;
+};
+
+ESPresponse [exceptions_inline, nil_remove] EraseHistoryResponse
+{
+    binary xmlmap;
+};
+
+
+
 //  ===========================================================================
 ESPservice [
     auth_feature("DEFERRED"),
@@ -735,9 +757,13 @@ ESPservice [
     ESPmethod [resp_xsl_default("/esp/xslt/addto_superfile.xslt")] AddtoSuperfile(AddtoSuperfileRequest, AddtoSuperfileResponse);
     ESPmethod [auth_feature("DfuAccess:READ"), resp_xsl_default("/esp/xslt/dfu_superedit.xslt")] SuperfileList(SuperfileListRequest, SuperfileListResponse);
     ESPmethod [resp_xsl_default("/esp/xslt/dfu_superresult.xslt")] SuperfileAction(SuperfileActionRequest, SuperfileActionResponse);
+
     ESPmethod [auth_feature("DfuAccess:READ")] Savexml(SavexmlRequest, SavexmlResponse);
     ESPmethod [auth_feature("DfuAccess:WRITE")] Add(AddRequest, AddResponse);
     ESPmethod [auth_feature("DfuAccess:WRITE")] AddRemote(AddRemoteRequest, AddRemoteResponse);
+
+    ESPmethod ListHistory(ListHistoryRequest, ListHistoryResponse);
+    ESPmethod EraseHistory(EraseHistoryRequest, EraseHistoryResponse);
 };
 
 SCMexportdef(WSDFU);

+ 165 - 85
esp/services/ws_dfu/ws_dfuService.cpp

@@ -117,9 +117,9 @@ CThorNodeGroup* CThorNodeGroupCache::lookup(const char* groupName, unsigned time
 void CWsDfuEx::init(IPropertyTree *cfg, const char *process, const char *service)
 {
     StringBuffer xpath;
-    
+
     DBGLOG("Initializing %s service [process = %s]", service, process);
-    
+
     xpath.appendf("Software/EspProcess[@name=\"%s\"]/EspService[@name=\"%s\"]/DefaultScope", process, service);
     cfg->getProp(xpath.str(), defaultScope_);
 
@@ -213,7 +213,7 @@ bool CWsDfuEx::onDFUSearch(IEspContext &context, IEspDFUSearchRequest & req, IEs
         resp.setFileTypes(ftarray);
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -258,7 +258,7 @@ void parseTwoStringArrays(const char *input, StringArray& strarray1, StringArray
     if (name_len > 0 && value_len > 0)
     {
         char * inputText = (char *) input;
-        inputText += 2; //skip 2 chars 
+        inputText += 2; //skip 2 chars
 
         loop
         {
@@ -282,18 +282,18 @@ void parseTwoStringArrays(const char *input, StringArray& strarray1, StringArray
 
             if (!inputText || strlen(inputText) < columnNameLen + columnValueLen)
                 break;
-            
+
             char * colon = inputText + columnNameLen;
             if (!colon)
                 break;
 
-            StringAttr tmp;         
+            StringAttr tmp;
             tmp.set(inputText, columnNameLen);
             strarray1.append(tmp.get());
             tmp.set(colon, columnValueLen);
-            //tmp.toUpperCase(); 
+            //tmp.toUpperCase();
             strarray2.append(tmp.get());
-                
+
             inputText = colon + columnValueLen;
         }
     }
@@ -322,7 +322,7 @@ bool CWsDfuEx::onDFUQuery(IEspContext &context, IEspDFUQueryRequest & req, IEspD
         doLogicalFileSearch(context, userdesc.get(), req, resp);
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
 
@@ -357,7 +357,7 @@ bool CWsDfuEx::onDFUInfo(IEspContext &context, IEspDFUInfoRequest &req, IEspDFUI
         }
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
 
@@ -435,7 +435,7 @@ bool CWsDfuEx::onDFUSpace(IEspContext &context, IEspDFUSpaceRequest & req, IEspD
             wuTime.getDate(yearTo, monthTo, dayTo, true);
             wuTime.getTime(hour, minute, second, nano, true);
             wuTo.appendf("%4d-%02d-%02d %02d:%02d:%02d",yearTo,monthTo,dayTo,hour,minute,second);
-        
+
             StringBuffer endDate;
             endDate.appendf("%02d/%02d/%04d", monthTo, dayTo, yearTo);
             resp.setEndDate(endDate.str());
@@ -449,7 +449,7 @@ bool CWsDfuEx::onDFUSpace(IEspContext &context, IEspDFUSpaceRequest & req, IEspD
             {
                 StringBuffer wuFrom, wuTo;
                 bool bFirst = true;
-                ForEach(*fi) 
+                ForEach(*fi)
                 {
                     IPropertyTree &attr=fi->query();
                     StringBuffer modf(attr.queryProp("@modified"));
@@ -514,7 +514,7 @@ bool CWsDfuEx::onDFUSpace(IEspContext &context, IEspDFUSpaceRequest & req, IEspD
             SpaceItems64.append(*item64.getClear());
         }
 
-        ForEach(*fi) 
+        ForEach(*fi)
         {
             IPropertyTree &attr=fi->query();
 
@@ -546,7 +546,7 @@ bool CWsDfuEx::onDFUSpace(IEspContext &context, IEspDFUSpaceRequest & req, IEspD
             {
                 setSpaceItemByOwner(SpaceItems64, attr.queryProp("@owner"), attr.queryProp("@name"), attr.getPropInt64("@size",-1));
             }
-            else 
+            else
             {
                 setSpaceItemByScope(SpaceItems64, scopeName, attr.queryProp("@name"), attr.getPropInt64("@size",-1));
             }
@@ -592,7 +592,7 @@ bool CWsDfuEx::onDFUSpace(IEspContext &context, IEspDFUSpaceRequest & req, IEspD
         resp.setCountBy(req.getCountBy());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -626,7 +626,7 @@ bool CWsDfuEx::setSpaceItemByScope(IArrayOf<IEspSpaceItem>& SpaceItems64, const
             scope[pName - logicalName] = 0;
         }
     }
-            
+
     if (strlen(scope) > 0)
     {
         IEspSpaceItem *item0 = NULL;
@@ -807,7 +807,7 @@ bool CWsDfuEx::setSpaceItemByOwner(IArrayOf<IEspSpaceItem>& SpaceItems64, const
     return true;
 }
 
-bool CWsDfuEx::createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, StringBuffer interval, unsigned& yearFrom, 
+bool CWsDfuEx::createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, StringBuffer interval, unsigned& yearFrom,
     unsigned& monthFrom, unsigned& dayFrom, unsigned& yearTo, unsigned& monthTo, unsigned& dayTo)
 {
     if (!stricmp(interval, COUNTBY_YEAR))
@@ -831,7 +831,7 @@ bool CWsDfuEx::createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, Strin
     else if (!stricmp(interval, COUNTBY_QUARTER))
     {
         for (unsigned i = yearFrom; i <= yearTo; i++)
-        {   
+        {
             int quartStart = 1;
             int quartEnd = 4;
             if (i == yearFrom)
@@ -885,7 +885,7 @@ bool CWsDfuEx::createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, Strin
     else if (!stricmp(interval, COUNTBY_MONTH))
     {
         for (unsigned i = yearFrom; i <= yearTo; i++)
-        {   
+        {
             int jFrom = (i != yearFrom) ? 1 : monthFrom;
             int jTo =  (i != yearTo) ? 12 : monthTo;
             for (int j = jFrom; j <= jTo; j++)
@@ -908,7 +908,7 @@ bool CWsDfuEx::createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, Strin
     else
     {
         for (unsigned i = yearFrom; i <= yearTo; i++)
-        {   
+        {
             int jFrom = (i != yearFrom) ? 1 : monthFrom;
             int jTo =  (i != yearTo) ? 12 : monthTo;
             for (int j = jFrom; j <= jTo; j++)
@@ -1436,7 +1436,7 @@ bool CWsDfuEx::onDFUArrayAction(IEspContext &context, IEspDFUArrayActionRequest
 
             strncpy(curfile, file, len);
             curfile[len] = 0;
-            
+
             try
             {
                 Owned<IDistributedFile> df = queryDistributedFileDirectory().lookup(curfile, userdesc.get(), true);
@@ -1481,7 +1481,7 @@ bool CWsDfuEx::onDFUArrayAction(IEspContext &context, IEspDFUArrayActionRequest
             resp.setRedirectTo(subfiles.str());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -1532,7 +1532,7 @@ bool CWsDfuEx::onDFUDefFile(IEspContext &context,IEspDFUDefFileRequest &req, IEs
         resp.setDefFile_mimetype(type.str());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -1562,7 +1562,7 @@ void CWsDfuEx::getDefFile(IUserDescriptor* udesc, const char* FileName,StringBuf
 
     StringBuffer text;
     text.append(df->queryAttributes().queryProp("ECL"));
-    
+
     MultiErrorReceiver errs;
     OwnedHqlExpr record = parseQuery(text.str(), &errs);
     if (errs.errCount())
@@ -1571,7 +1571,7 @@ void CWsDfuEx::getDefFile(IUserDescriptor* udesc, const char* FileName,StringBuf
         IError *first = errs.firstError();
         first->toString(errtext);
         throw MakeStringException(ECLWATCH_CANNOT_PARSE_ECL_QUERY, "Failed in parsing ECL query: %s @ %d:%d.", errtext.str(), first->getColumn(), first->getLine());
-    } 
+    }
 
     if(!record)
         throw MakeStringException(ECLWATCH_CANNOT_PARSE_ECL_QUERY, "Failed in parsing ECL query.");
@@ -1583,7 +1583,7 @@ void CWsDfuEx::getDefFile(IUserDescriptor* udesc, const char* FileName,StringBuf
 
     data->setProp("filename",fname ? fname+1 : FileName);
 
-    toXML(data, returnStr, 0, 0); 
+    toXML(data, returnStr, 0, 0);
 }
 
 bool CWsDfuEx::checkFileContent(IEspContext &context, IUserDescriptor* udesc, const char * logicalName, const char * cluster)
@@ -1654,7 +1654,7 @@ bool FindInStringArray(StringArray& clusters, const char *cluster)
     }
     else
     {
-#if 0 //Comment out since clusters are not set for some old files  
+#if 0 //Comment out since clusters are not set for some old files
         if (!clusters.ordinality())
             return true;
 
@@ -1768,7 +1768,7 @@ void CWsDfuEx::getFilePartsOnClusters(IEspContext &context, const char* clusterR
         {
             IPartDescriptor& part = pi->query();
             unsigned partIndex = part.queryPartIndex();
-                        
+
             StringBuffer partSizeStr;
             IPropertyTree* partPropertyTree = &part.queryProperties();
             if (!partPropertyTree)
@@ -1971,7 +1971,7 @@ void CWsDfuEx::doGetFileDetails(IEspContext &context, IUserDescriptor* udesc, co
         Owned<IDistributedSuperFileIterator> iter = df->getOwningSuperFiles();
         if(iter.get() != NULL)
         {
-            ForEach(*iter) 
+            ForEach(*iter)
             {
                 //printf("%s,%s\n",iter->query().queryLogicalName(),lname);
                 Owned<IEspDFULogicalFile> File = createDFULogicalFile("","");
@@ -1979,7 +1979,7 @@ void CWsDfuEx::doGetFileDetails(IEspContext &context, IUserDescriptor* udesc, co
                 LogicalFiles.append(*File.getClear());
             }
         }
-        
+
         if(LogicalFiles.length() > 0)
         {
             FileDetails.setSuperfiles(LogicalFiles);
@@ -2014,7 +2014,7 @@ void CWsDfuEx::doGetFileDetails(IEspContext &context, IUserDescriptor* udesc, co
     if (version >= 1.20)
         FileDetails.setCsvEscape(df->queryAttributes().queryProp("@csvEscape"));
 
-  
+
     //Time and date of the file
     tmpstr.clear();
     dt.getDateString(tmpstr);
@@ -2220,12 +2220,12 @@ void CWsDfuEx::getLogicalFileAndDirectory(IEspContext &context, IUserDescriptor*
         StringBuffer filter;
         filter.append(dirname);
         filter.append("::*");
-        
+
         Owned<IDFAttributesIterator> fi = queryDistributedFileDirectory().getDFAttributesIterator(filter.toLowerCase().str(), udesc, false,true, NULL);
         if(fi)
         {
             StringBuffer size;
-            ForEach(*fi) 
+            ForEach(*fi)
             {
                 IPropertyTree &attr=fi->query();
 
@@ -2245,7 +2245,7 @@ void CWsDfuEx::getLogicalFileAndDirectory(IEspContext &context, IUserDescriptor*
     Owned<IDFScopeIterator> iter = queryDistributedFileDirectory().getScopeIterator(udesc,dirname,false);
     if(iter)
     {
-        ForEach(*iter) 
+        ForEach(*iter)
         {
             const char *scope = iter->query();
             if (scope && *scope)
@@ -2293,7 +2293,7 @@ bool CWsDfuEx::onDFUFileView(IEspContext &context, IEspDFUFileViewRequest &req,
         resp.setDFULogicalFiles(logicalFiles);
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -3593,11 +3593,11 @@ bool CWsDfuEx::onSuperfileList(IEspContext &context, IEspSuperfileListRequest &r
         }
         if(farray.length() > 0)
             resp.setSubfiles(farray);
-        
+
         resp.setSuperfile(req.getSuperfile());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -3633,7 +3633,7 @@ bool CWsDfuEx::onSuperfileAction(IEspContext &context, IEspSuperfileActionReques
         resp.setSuperfile(req.getSuperfile());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -3666,7 +3666,7 @@ bool CWsDfuEx::onSavexml(IEspContext &context, IEspSavexmlRequest &req, IEspSave
         resp.setXmlmap(xmlmap);
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -3691,11 +3691,11 @@ bool CWsDfuEx::onAdd(IEspContext &context, IEspAddRequest &req, IEspAddResponse
         PROGLOG("addFileXML: %s", req.getDstname());
 
         Owned<IDFUhelper> dfuhelper = createIDFUhelper();
-        StringBuffer xmlstr(req.getXmlmap().length(),(const char*)req.getXmlmap().bufferBase()); 
+        StringBuffer xmlstr(req.getXmlmap().length(),(const char*)req.getXmlmap().bufferBase());
         dfuhelper->addFileXML(req.getDstname(), xmlstr, userdesc.get());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -3741,7 +3741,7 @@ bool CWsDfuEx::onAddRemote(IEspContext &context, IEspAddRemoteRequest &req, IEsp
         dfuhelper->addFileRemote(dstname, ep, srcname, srcuserdesc.get(), userdesc.get());
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -3851,7 +3851,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                         bNaturalColumn = false;
                     }
 
-                    item->setColumnLabel(columnLabel.str()); 
+                    item->setColumnLabel(columnLabel.str());
 
                     if (version > 1.04 && filterByNames.length() > 0)
                     {
@@ -3863,7 +3863,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                                 const char* value = filterByValues.item(ii);
                                 if (value && *value)
                                 {
-                                    item->setColumnValue(value); 
+                                    item->setColumnValue(value);
                                     break;
                                 }
                             }
@@ -3886,7 +3886,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                         item->setColumnSize(strlen(columnLabel.str()));
                         columnSize = 2;
                     }
-                    else 
+                    else
                     {
                         if (columnType == TypeInteger || columnType == TypeUnsignedInteger)
                         {
@@ -3930,7 +3930,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                             item->setColumnSize(columnSize);
                             item->setMaxSize(columnSize);
                         }
-                        else 
+                        else
                         {
                             item->setColumnType("Others");
                             columnSize = STRINGSIZE;
@@ -3948,7 +3948,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                         dataKeyedColumns[lineCount].append(*item.getLink());
                         lineCount++;
                     }
-                    else 
+                    else
                     {
                         if (lineSizeCount + columnSize < STRINGSIZE)
                         {
@@ -3987,7 +3987,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                         bNaturalColumn = false;
                     }
 
-                    item->setColumnLabel(columnLabel.str()); 
+                    item->setColumnLabel(columnLabel.str());
 
                     if (version > 1.04 && filterByNames.length() > 0)
                     {
@@ -3999,7 +3999,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                                 const char* value = filterByValues.item(ii);
                                 if (value && *value)
                                 {
-                                    item->setColumnValue(value); 
+                                    item->setColumnValue(value);
                                     break;
                                 }
                             }
@@ -4020,7 +4020,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                         item->setColumnSize(strlen(columnLabel.str()));
                         columnSize = 2;
                     }
-                    else 
+                    else
                     {
                         if (columnType == TypeInteger || columnType == TypeUnsignedInteger)
                         {
@@ -4064,7 +4064,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                             item->setColumnSize(columnSize);
                             item->setMaxSize(columnSize);
                         }
-                        else 
+                        else
                         {
                             item->setColumnType("Others");
                             columnSize = STRINGSIZE;
@@ -4082,7 +4082,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                         dataNonKeyedColumns[lineCount].append(*item.getLink());
                         lineCount++;
                     }
-                    else 
+                    else
                     {
                         if (lineSizeCount + columnSize < STRINGSIZE)
                         {
@@ -4190,7 +4190,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
                 //resp.setColumnCount(columnCount);
                 resp.setRowCount(total);
             }
-            
+
             resp.setLogicalName(logicalNameStr.str());
             resp.setStartIndex(startIndex);
             resp.setEndIndex(endIndex);
@@ -4214,7 +4214,7 @@ bool CWsDfuEx::onDFUGetDataColumns(IEspContext &context, IEspDFUGetDataColumnsRe
             resp.setChooseFile(0);
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -4236,7 +4236,7 @@ bool CWsDfuEx::onDFUSearchData(IEspContext &context, IEspDFUSearchDataRequest &r
 
         const char* selectedKey = req.getSelectedKey();
 
-        if (strlen(selectedKey) > 0)  
+        if (strlen(selectedKey) > 0)
         {
             resp.setSelectedKey(req.getSelectedKey());
         }
@@ -4351,7 +4351,7 @@ bool CWsDfuEx::onDFUSearchData(IEspContext &context, IEspDFUSearchDataRequest &r
             const char* parentName = req.getParentName();
             if (parentName && *parentName)
                 browseDataRequest->setParentName(parentName);
-            
+
             browseDataRequest->setFilterBy(req.getFilterBy());
             browseDataRequest->setShowColumns(req.getShowColumns());
             browseDataRequest->setStartForGoback(req.getStartForGoback());
@@ -4388,7 +4388,7 @@ bool CWsDfuEx::onDFUSearchData(IEspContext &context, IEspDFUSearchDataRequest &r
         }
     }
     catch(IException* e)
-    {   
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
@@ -4594,7 +4594,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
             throw MakeStringException(ECLWATCH_TOO_MANY_DATA_ROWS,"Browser Cannot display more than %d data rows.", MAX_VIEWKEYFILE_ROWS);
 
         bool bSchemaOnly=req.getSchemaOnly() ? req.getSchemaOnly() : false;
-        bool bDisableUppercaseTranslation = req.getDisableUppercaseTranslation() ? req.getDisableUppercaseTranslation() : false; 
+        bool bDisableUppercaseTranslation = req.getDisableUppercaseTranslation() ? req.getDisableUppercaseTranslation() : false;
 
 #define HPCCBROWSER 1
 #ifdef HPCCBROWSER
@@ -4602,7 +4602,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
         const char* showColumns = req.getShowColumns();
 
         __int64 read=0;
-        __int64 total = 0;  
+        __int64 total = 0;
         StringBuffer msg;
         StringArray columnLabels, columnLabelsType;
         IArrayOf<IEspDFUData> DataList;
@@ -4616,7 +4616,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
         unsigned int max_name_length = 3; //max length for name length
         unsigned int max_value_length = 4; //max length for value length:
         StringBuffer filterByStr, filterByStr0;
-        filterByStr0.appendf("%d%d", max_name_length, max_value_length);  
+        filterByStr0.appendf("%d%d", max_name_length, max_value_length);
 
         unsigned columnCount = columnLabels.length();
         IArrayOf<IEspDFUDataColumn> dataColumns;
@@ -4656,7 +4656,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
 
                 Owned<IEspDFUDataColumn> item = createDFUDataColumn("","");
 
-                item->setColumnLabel(label); 
+                item->setColumnLabel(label);
                 item->setColumnType(type);
                 item->setColumnSize(0); //not show this column
 
@@ -4723,7 +4723,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
                 {
                     CWUWrapper wu(wuid, context);
                     if (wu)
-                    {   
+                    {
                         SCMStringBuffer eclqueue0, cluster0;
                         eclqueue.append(wu->getQueue(eclqueue0).str());
                         cluster.append(wu->getClusterName(cluster0).str());
@@ -4785,7 +4785,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
 
                 SCMStringBuffer scmbuf;
                 meta.getColumnLabel(scmbuf, col);
-                item->setColumnLabel(scmbuf.str()); 
+                item->setColumnLabel(scmbuf.str());
                 if (!showColumns || !*showColumns)
                 {
                     item->setColumnSize(1); //Show this column
@@ -4823,11 +4823,11 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
         StringBuffer filterByStr, filterByStr0;
         unsigned int max_name_length = 3; //max length for name length
         unsigned int max_value_length = 4; //max length for value length:
-        filterByStr0.appendf("%d%d", max_name_length, max_value_length);  
+        filterByStr0.appendf("%d%d", max_name_length, max_value_length);
         if (columnCount > 0 && filterByNames.length() > 0)
         {
             Owned<IFilteredResultSet> filter = result->createFiltered();
-            
+
             for (int ii = 0; ii < filterByNames.length(); ii++)
             {
                 const char* columnName = filterByNames.item(ii);
@@ -4844,7 +4844,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
                             filter->addFilter(col, columnValue);
                             filterByStr.appendf("%s[%s]", columnName, columnValue);
                             filterByStr0.appendf("%03d%04d%s%s", strlen(columnName), strlen(columnValue), columnName, columnValue);
-                    
+
                             break;
                         }
                     }
@@ -4894,7 +4894,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
                 if(read>=count)
                     break;
             }
-        }   
+        }
         catch(IException* e)
         {
             if ((version < 1.08) || (e->errorCode() != FVERR_FilterTooRestrictive))
@@ -4929,13 +4929,13 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
         resp.setResult(buf.toByteArray());
 #endif
 
-        //resp.setFilterBy(filterByStr.str()); 
+        //resp.setFilterBy(filterByStr.str());
         if (filterByStr.length() > 0)
         {
             const char* oldStr = "&";
             const char* newStr = "&amp;";
             filterByStr.replaceString(oldStr, newStr);
-            resp.setFilterBy(filterByStr.str()); 
+            resp.setFilterBy(filterByStr.str());
         }
         if (version > 1.04)
         {
@@ -4945,7 +4945,7 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
                 const char* oldStr = "&";
                 const char* newStr = "&amp;";
                 filterByStr0.replaceString(oldStr, newStr);
-                resp.setFilterForGoBack(filterByStr0.str()); 
+                resp.setFilterForGoBack(filterByStr0.str());
             }
             resp.setColumnCount(columnCount);
             if (dataColumns.length() > 0)
@@ -4993,12 +4993,92 @@ bool CWsDfuEx::onDFUBrowseData(IEspContext &context, IEspDFUBrowseDataRequest &r
         }
     }
     catch(IException* e)
-    {   
+    {
+        FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}
+
+
+bool CWsDfuEx::onListHistory(IEspContext &context, IEspListHistoryRequest &req, IEspListHistoryResponse &resp)
+{
+    try
+    {
+        StringBuffer username;
+        context.getUserID(username);
+        Owned<IUserDescriptor> userdesc;
+        if (username.length() > 0)
+        {
+            const char* passwd = context.queryPassword();
+            userdesc.setown(createUserDescriptor());
+            userdesc->set(username.str(), passwd);
+        }
+
+        if (!req.getName() || !*req.getName())
+            throw MakeStringException(ECLWATCH_MISSING_PARAMS, "Name required");
+        PROGLOG("onListHistory: %s", req.getName());
+
+        MemoryBuffer xmlmap;
+        Owned<IDistributedFile> file = queryDistributedFileDirectory().lookup(req.getName(),userdesc.get());
+        if (file)
+        {
+            IPropertyTree *history = file->queryHistory();
+            if (history)
+                history->serialize(xmlmap);
+        }
+        else
+            throw MakeStringException(ECLWATCH_FILE_NOT_EXIST,"CWsDfuEx::onListHistory: Could not find file '%s'.", req.getName());
+
+        resp.setXmlmap(xmlmap);
+    }
+    catch(IException* e)
+    {
         FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
     }
     return true;
 }
 
+bool CWsDfuEx::onEraseHistory(IEspContext &context, IEspEraseHistoryRequest &req, IEspEraseHistoryResponse &resp)
+{
+    try
+    {
+        StringBuffer username;
+        context.getUserID(username);
+        Owned<IUserDescriptor> userdesc;
+        if (username.length() > 0)
+        {
+            const char* passwd = context.queryPassword();
+            userdesc.setown(createUserDescriptor());
+            userdesc->set(username.str(), passwd);
+        }
+
+        if (!req.getName() || !*req.getName())
+            throw MakeStringException(ECLWATCH_MISSING_PARAMS, "Name required");
+        PROGLOG("onEraseHistory: %s", req.getName());
+
+        MemoryBuffer xmlmap;
+        Owned<IDistributedFile> file = queryDistributedFileDirectory().lookup(req.getName(),userdesc.get());
+        if (file)
+        {
+            IPropertyTree *history = file->queryHistory();
+            if (history)
+            {
+                history->serialize(xmlmap);
+                file->resetHistory();
+            }
+        }
+        else
+            throw MakeStringException(ECLWATCH_FILE_NOT_EXIST,"CWsDfuEx::onEraseHistory: Could not find file '%s'.", req.getName());
+
+
+        resp.setXmlmap(xmlmap);
+    }
+    catch(IException* e)
+    {
+        FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}
 
 void CWsDfuEx::getRoxieClusterConfig(char const * clusterType, char const * clusterName, char const * processName, StringBuffer& netAddress, int& port)
 {
@@ -5053,7 +5133,7 @@ void CWsDfuEx::getRoxieClusterConfig(char const * clusterType, char const * clus
             netAddress.append(addr);
     }
 #endif
-    
+
     return;
 }
 
@@ -5139,7 +5219,7 @@ void CWsDfuEx::getMappingColumns(IRelatedBrowseFile * file, bool isPrimary, Unsi
     IViewRelation* parentRelation = file->queryParentRelation();
     for (unsigned i=0; i < parentRelation->numMappingFields(); i++)
     {
-        //find out the column numbers to remove 
+        //find out the column numbers to remove
         unsigned col = parentRelation->queryMappingField(i, isPrimary);
         cols.append(col);
     }
@@ -5179,13 +5259,13 @@ void CWsDfuEx::readColumnsForDisplay(StringBuffer& schemaText, StringArray& colu
         else if (hasChildren)
             columnsDisplayType.append("Object");
         else
-            columnsDisplayType.append("Unknown");   
+            columnsDisplayType.append("Unknown");
     }
 
     return;
 }
 
-void CWsDfuEx::mergeSchema(IRelatedBrowseFile * file, StringBuffer& schemaText, StringBuffer schemaText2, 
+void CWsDfuEx::mergeSchema(IRelatedBrowseFile * file, StringBuffer& schemaText, StringBuffer schemaText2,
                                     StringArray& columnsDisplay, StringArray& columnsDisplayType, StringArray& columnsHide)
 {
     if (schemaText.length() < 1)
@@ -5274,7 +5354,7 @@ void CWsDfuEx::mergeSchema(IRelatedBrowseFile * file, StringBuffer& schemaText,
         bool bRename = false;
         if (cols.ordinality() != 0)
         {
-            ForEachItemIn(i1,cols) 
+            ForEachItemIn(i1,cols)
             {
                 unsigned col = cols.item(i1);
                 if (col == col0)
@@ -5368,7 +5448,7 @@ void CWsDfuEx::mergeDataRow(StringBuffer& newRow, int depth, IPropertyTreeIterat
                         const char* key = columnsHide.item(i);
                         if (!key || strcmp(key, label))
                             continue;
-                        
+
                         bHide = true;
                         break;
                     }
@@ -5382,7 +5462,7 @@ void CWsDfuEx::mergeDataRow(StringBuffer& newRow, int depth, IPropertyTreeIterat
                         const char* key = columnsUsed.item(i);
                         if (!key || strcmp(key, label))
                             continue;
-                        
+
                         StringBuffer newName(label);
                         newName.append("-2");
                         e->renameProp("/", newName.str());
@@ -5424,12 +5504,12 @@ void CWsDfuEx::mergeDataRow(StringBuffer& newRow, StringBuffer dataRow1, StringB
     if (it)
     {
         StringArray columnLabels0;
-        mergeDataRow(newRow, 0, it, columnLabels0, columnLabels);   
+        mergeDataRow(newRow, 0, it, columnLabels0, columnLabels);
     }
 
     Owned<IPropertyTreeIterator> it2 = data2->getElements("*");
     if (it2)
-    {   
+    {
         mergeDataRow(newRow, 1, it2, columnsHide, columnLabels);
     }
 
@@ -5438,7 +5518,7 @@ void CWsDfuEx::mergeDataRow(StringBuffer& newRow, StringBuffer dataRow1, StringB
     return;
 }
 
-void CWsDfuEx::browseRelatedFileSchema(IRelatedBrowseFile * file, const char* parentName, unsigned depth, StringBuffer& schemaText, 
+void CWsDfuEx::browseRelatedFileSchema(IRelatedBrowseFile * file, const char* parentName, unsigned depth, StringBuffer& schemaText,
                                                     StringArray& columnsDisplay, StringArray& columnsDisplayType, StringArray& columnsHide)
 {
     //if (file in set of files to display or iterate)
@@ -5528,7 +5608,7 @@ schemaText0.append("</xs:schema>");
     return;
 }
 
-int CWsDfuEx::browseRelatedFileDataSet(double version, IRelatedBrowseFile * file, const char* parentName, unsigned depth, __int64 start, __int64& count, __int64& read, 
+int CWsDfuEx::browseRelatedFileDataSet(double version, IRelatedBrowseFile * file, const char* parentName, unsigned depth, __int64 start, __int64& count, __int64& read,
                                         StringArray& columnsHide, StringArray& dataSetOutput)
 {
     int iRet = 0;
@@ -5667,8 +5747,8 @@ rows++;
 
 //sample filterBy: 340020001id1
 //sample data: <XmlSchema name="myschema">...</XmlSchema><Dataset xmlSchema="myschema">...</Dataset>
-int CWsDfuEx::GetIndexData(IEspContext &context, bool bSchemaOnly, const char* indexName, const char* parentName, const char* filterBy, __int64 start, 
-                                        __int64& count, __int64& read, __int64& total, StringBuffer& message, StringArray& columnLabels, 
+int CWsDfuEx::GetIndexData(IEspContext &context, bool bSchemaOnly, const char* indexName, const char* parentName, const char* filterBy, __int64 start,
+                                        __int64& count, __int64& read, __int64& total, StringBuffer& message, StringArray& columnLabels,
                                         StringArray& columnLabelsType, IArrayOf<IEspDFUData>& DataList, bool webDisableUppercaseTranslation)
 {
     if (!indexName || !*indexName)
@@ -5755,7 +5835,7 @@ int CWsDfuEx::GetIndexData(IEspContext &context, bool bSchemaOnly, const char* i
     {
         web->gatherWeb(indexName0, df, options);
         browser.setown(web->createBrowseTree(indexName0));
-    }   
+    }
     catch(IException* e)
     {
         if ((e->errorCode() != FVERR_CouldNotResolveX) || (indexName[0] != '~'))

+ 8 - 6
esp/services/ws_dfu/ws_dfuService.hpp

@@ -143,6 +143,8 @@ public:
     virtual bool onAddRemote(IEspContext &context, IEspAddRemoteRequest &req, IEspAddRemoteResponse &resp);
     virtual bool onSuperfileList(IEspContext &context, IEspSuperfileListRequest &req, IEspSuperfileListResponse &resp);
     virtual bool onSuperfileAction(IEspContext &context, IEspSuperfileActionRequest &req, IEspSuperfileActionResponse &resp);
+    virtual bool onListHistory(IEspContext &context, IEspListHistoryRequest &req, IEspListHistoryResponse &resp);
+    virtual bool onEraseHistory(IEspContext &context, IEspEraseHistoryRequest &req, IEspEraseHistoryResponse &resp);
 
 private:
     const char* getPrefixFromLogicalName(const char* logicalName, StringBuffer& prefix);
@@ -161,7 +163,7 @@ private:
     bool doLogicalFileSearch(IEspContext &context, IUserDescriptor* udesc, IEspDFUQueryRequest & req, IEspDFUQueryResponse & resp);
     void doGetFileDetails(IEspContext &context, IUserDescriptor* udesc, const char *name,const char *cluster,
         const char *description,IEspDFUFileDetail& FileDetails);
-    bool createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, StringBuffer interval, unsigned& yearFrom, 
+    bool createSpaceItemsByDate(IArrayOf<IEspSpaceItem>& SpaceItems, StringBuffer interval, unsigned& yearFrom,
         unsigned& monthFrom, unsigned& dayFrom, unsigned& yearTo, unsigned& monthTo, unsigned& dayTo);
     bool setSpaceItemByScope(IArrayOf<IEspSpaceItem>& SpaceItems64, const char*scopeName, const char*logicalName, __int64 size);
     bool setSpaceItemByOwner(IArrayOf<IEspSpaceItem>& SpaceItems64, const char *owner, const char *logicalName, __int64 size);
@@ -188,16 +190,16 @@ private:
     void setRootFilter(INewResultSet* result, const char* filterBy, IResultSetFilter* filter, bool disableUppercaseTranslation = true);
     void getMappingColumns(IRelatedBrowseFile * file, bool isPrimary, UnsignedArray& cols);
     void readColumnsForDisplay(StringBuffer& schemaText, StringArray& columnsDisplay, StringArray& columnsDisplayType);
-    void mergeSchema(IRelatedBrowseFile * file, StringBuffer& schemaText, StringBuffer schemaText2, 
+    void mergeSchema(IRelatedBrowseFile * file, StringBuffer& schemaText, StringBuffer schemaText2,
         StringArray& columnsDisplay, StringArray& columnsDisplayType, StringArray& columnsHide);
     void mergeDataRow(StringBuffer& newRow, int depth, IPropertyTreeIterator* it, StringArray& columnsHide, StringArray& columnsUsed);
     void mergeDataRow(StringBuffer& newRow, StringBuffer dataRow1, StringBuffer dataRow2, StringArray& columnsHide);
-    void browseRelatedFileSchema(IRelatedBrowseFile * file, const char* parentName, unsigned depth, StringBuffer& schemaText, 
+    void browseRelatedFileSchema(IRelatedBrowseFile * file, const char* parentName, unsigned depth, StringBuffer& schemaText,
         StringArray& columnsDisplay, StringArray& columnsDisplayType, StringArray& columnsHide);
-    int browseRelatedFileDataSet(double version, IRelatedBrowseFile * file, const char* parentName, unsigned depth, __int64 start, __int64& count, __int64& read, 
+    int browseRelatedFileDataSet(double version, IRelatedBrowseFile * file, const char* parentName, unsigned depth, __int64 start, __int64& count, __int64& read,
                                         StringArray& columnsHide, StringArray& dataSetOutput);
-    int GetIndexData(IEspContext &context, bool bSchemaOnly, const char* indexName, const char* parentName, const char* filterBy, __int64 start, 
-                                        __int64& count, __int64& read, __int64& total, StringBuffer& message, StringArray& columnLabels, 
+    int GetIndexData(IEspContext &context, bool bSchemaOnly, const char* indexName, const char* parentName, const char* filterBy, __int64 start,
+                                        __int64& count, __int64& read, __int64& total, StringBuffer& message, StringArray& columnLabels,
                                         StringArray& columnLabelsType, IArrayOf<IEspDFUData>& DataList, bool webDisableUppercaseTranslation);
     bool getUserFilePermission(IEspContext &context, IUserDescriptor* udesc, const char* logicalName, SecAccessFlags& permission);
     void parseStringArray(const char *input, StringArray& strarray);