1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, either version 3 of the
7 // License, or (at your option) any later version.
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU Affero General Public License for more details.
14 // You should have received a copy of the GNU Affero General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
17 //-----------------------------------------------------------------------------
19 //-----------------------------------------------------------------------------
22 #include "nel/misc/variable.h"
23 #include "nel/misc/file.h"
24 #include "nel/misc/common.h"
27 #include "game_share/utils.h"
30 #include "file_manager.h"
33 //-------------------------------------------------------------------------------------------------
35 //-------------------------------------------------------------------------------------------------
38 using namespace NLMISC
;
39 //using namespace NLNET;
40 using namespace PATCHMAN
;
43 //-----------------------------------------------------------------------------
44 // some NLMISC Variable
45 //-----------------------------------------------------------------------------
47 NLMISC::CVariable
<uint32
> FileCacheSize("patchman","FileCacheSize","Minimum size for re file cache",100*1024*1024,0,true);
52 //-----------------------------------------------------------------------------
54 //-----------------------------------------------------------------------------
56 static NLMISC::CSString
getTempFileName(const NLMISC::CSString
& fileName
)
58 return fileName
+".download.tmp";
61 static NLMISC::CSString
getIndexFileName(const NLMISC::CSString
& rootDirectory
)
63 return rootDirectory
+".patchman.file_index";
67 //-----------------------------------------------------------------------------
69 //-----------------------------------------------------------------------------
71 bool SFileInfo::updateFileInfo(const NLMISC::CSString
& fileName
,const NLMISC::CSString
& fullFileName
, SFileInfo::TUpdateMethod updateMethod
, IFileInfoUpdateListener
* updateListener
)
73 // if file doesn't exist then just drop out
74 if (!NLMISC::CFile::fileExists(fullFileName
))
80 // give ourselves 5 attempts in case the file is being accessed...
81 for(uint32 i
=0;i
<5;++i
)
85 uint32 newFileSize
= NLMISC::CFile::getFileSize(fullFileName
);
86 uint32 newFileTime
= NLMISC::CFile::getFileModificationDate(fullFileName
);
88 // work out whether the record has changed (based on size and time stamp)
89 bool changed
= ( (FileTime
!=newFileTime
) || (FileSize
!=newFileSize
) );
91 // note: it is possible to hit an exception in the checksum calculation if the file is being accessed
93 if (updateMethod
==FORCE_RECALCULATE
|| (updateMethod
==RECALCULATE_IF_CHANGED
&& (newFileSize
!=FileSize
|| newFileTime
!=FileTime
)))
95 // call the updateListener object's callback so that they can write a log, etc
96 if (updateListener
!=NULL
)
98 updateListener
->cbFileInfoRescanning(fileName
,newFileSize
);
101 // workout the new checksum and update the 'changed' flag accordingly
102 CHashKeyMD5 newChecksum
= NLMISC::getMD5(fullFileName
);
103 changed
|= (Checksum
!=newChecksum
);
104 Checksum
= newChecksum
;
107 // update the fields in our new record
109 FileSize
= newFileSize
;
110 FileTime
= newFileTime
;
112 // if there's an update listener object, then let them know about the file update
113 if (changed
&& updateListener
!=NULL
)
115 updateListener
->cbFileInfoUpdate(*this);
118 // return true if we are unchanged, otherwise false
123 nlwarning("Exception thrown in getMD5(\"%s\") ... will try again in a few seconds",fullFileName
.c_str());
128 nlwarning("Failed to get info on file: %s",fullFileName
.c_str());
134 //-----------------------------------------------------------------------------
136 //-----------------------------------------------------------------------------
138 CFileSpec::CFileSpec()
142 CFileSpec::CFileSpec(const NLMISC::CSString
& fileSpec
)
144 _NameSpec
= NLMISC::CFile::getFilename(fileSpec
);
145 _PathSpec
= NLMISC::CFile::getPath(fileSpec
);
147 _NameIsWild
= _NameSpec
.contains('*') || _NameSpec
.contains('?');
148 _PathIsWild
= _PathSpec
.contains('*') || _PathSpec
.contains('?');
150 _AcceptAllNames
= (_NameSpec
=="*");
151 _AcceptAllPaths
= (_PathSpec
=="*/");
154 bool CFileSpec::matches(const NLMISC::CSString
& fullFileName
) const
156 CSString name
= NLMISC::CFile::getFilename(fullFileName
);
157 CSString path
= NLMISC::CFile::getPath(fullFileName
);
159 return (_AcceptAllPaths
|| pathMatches(path
)) && (_AcceptAllNames
|| nameMatches(name
));
162 bool CFileSpec::nameMatches(const NLMISC::CSString
& fileName
) const
164 return _AcceptAllNames
|| ( _NameIsWild
? testWildCard(fileName
,_NameSpec
) : (fileName
==_NameSpec
) );
167 bool CFileSpec::pathMatches(const NLMISC::CSString
& path
) const
169 // treat the special case where the file that we're set to accept all paths
173 // treat the special case where the file that we're testing has no path and we
174 // are looking for files in the root directory only
175 if (path
.empty() && (_PathSpec
=="./") )
178 return _PathIsWild
? testWildCard(path
,_PathSpec
) : (path
==_PathSpec
);
181 NLMISC::CSString
CFileSpec::toString() const
183 return _PathSpec
+_NameSpec
;
186 const CSString
& CFileSpec::nameSpec() const
191 const CSString
& CFileSpec::pathSpec() const
196 bool CFileSpec::isWild() const
198 return _NameIsWild
|| _PathIsWild
;
201 bool CFileSpec::nameIsWild() const
206 bool CFileSpec::pathIsWild() const
213 //-----------------------------------------------------------------------------
214 // methods CRepositoryDirectory
215 //-----------------------------------------------------------------------------
217 CRepositoryDirectory::CRepositoryDirectory(const NLMISC::CSString
& path
)
219 _Root
= NLMISC::CPath::getFullPath(path
);
220 _IndexFileIsUpToDate
= false;
223 void CRepositoryDirectory::clear()
225 // clear out our directories map
226 _DirectoryTree
.clear();
229 void CRepositoryDirectory::rescanFull(IFileInfoUpdateListener
* updateListener
)
231 // rescan our directories recursively, starting at the root
232 _rescanDirectory("",true,updateListener
);
234 // update the index file as required
235 if (!_IndexFileIsUpToDate
)
239 void CRepositoryDirectory::rescanPartial(IFileInfoUpdateListener
* updateListener
)
241 // if the directory tree is empty then start with the rooot directory...
242 if (_DirectoryTree
.empty())
244 _rescanDirectory("",false,updateListener
);
248 // try to get hold of a ref to the last directory that we rescanned
249 TDirectoryTree::iterator it
= _DirectoryTree
.find(_LastRescan
);
250 if (it
==_DirectoryTree
.end())
252 // we failed to get a ref to the last directory so wrap back round to the start
253 it
= _DirectoryTree
.begin();
257 // increement our iterator to get hold of a ref to the next directory in the map
259 // if we reach end of map then wrap back to the start
260 if (it
==_DirectoryTree
.end())
262 it
= _DirectoryTree
.begin();
266 // scan the next directory in our map (this is not recursive
267 _rescanDirectory(it
->first
,false,updateListener
);
268 _LastRescan
= it
->first
;
270 // update the index file as required
271 if (!_IndexFileIsUpToDate
)
275 void CRepositoryDirectory::updateFile(const NLMISC::CSString
& fileName
,SFileInfo::TUpdateMethod updateMethod
, IFileInfoUpdateListener
* updateListener
)
277 // if the file doesn't exist then give up
278 if (!NLMISC::CFile::fileExists(_Root
+fileName
))
281 // split the file name into path and fileName
282 const NLMISC::CSString path
= NLMISC::CFile::getPath(fileName
);
284 // get hold of the directory object for this file (or create a new one if need be)
285 TFileInfoMap
& directory
= _DirectoryTree
[path
];
287 // get hold of the file info object for this (or create a new one if need be)
288 SFileInfo
& fileInfo
= directory
[fileName
];
290 // update the file info for the given file
291 _IndexFileIsUpToDate
&= fileInfo
.updateFileInfo(fileName
,_Root
+fileName
,updateMethod
,updateListener
);
295 void CRepositoryDirectory::getFileInfo(const NLMISC::CSString
& fileSpec
,TFileInfoVector
& result
,IFileRequestValidator
* validator
,const NLNET::IModuleProxy
*sender
) const
297 // split into directory and file name
298 const CFileSpec
spec(fileSpec
);
300 // setup a set of paths to scan
301 std::vector
<TDirectoryTree::const_iterator
> paths
;
303 // if the path has wirlcards in it then do a search for wildcard matches otherwise just do a lookup
304 if (spec
.pathIsWild())
306 for (TDirectoryTree::const_iterator it
=_DirectoryTree
.begin(); it
!=_DirectoryTree
.end(); ++it
)
308 if (spec
.pathMatches(it
->first
))
314 TDirectoryTree::const_iterator it
= _DirectoryTree
.find(spec
.pathSpec());
315 if (it
!= _DirectoryTree
.end())
319 // run through the selected paths looking for files
320 for (uint32 i
=0;i
<paths
.size();++i
)
322 // compose the file name that we're supposed to match
323 NLMISC::CSString fullFilePattern
= paths
[i
]->first
+spec
.nameSpec();
325 // if the filename doesn't have wildcards then just do a straight lookup...
326 if (!spec
.nameIsWild())
328 // see whether we have a match...
329 TFileInfoMap::const_iterator fit
= paths
[i
]->second
.find(fullFilePattern
);
330 if (fit
!=paths
[i
]->second
.end())
332 // call the overloadable validation callback before adding the file info record to the result container
333 if (validator
==NULL
|| validator
->cbValidateFileInfoRequest(sender
,fit
->second
.FileName
))
335 result
.push_back(fit
->second
);
341 // run through the files in the given path
342 for (TFileInfoMap::const_iterator fit
= paths
[i
]->second
.begin(); fit
!= paths
[i
]->second
.end(); ++fit
)
344 // get hold of the file name for this map entry
345 const NLMISC::CSString
& name
=fit
->second
.FileName
;
347 // do either a wildcard compare or a quick and dirty string compare
348 if (testWildCard(name
,fullFilePattern
))
350 // call the overloadable validation callback before adding the file info record to the result container
351 if (validator
==NULL
|| validator
->cbValidateFileInfoRequest(sender
,name
))
353 result
.push_back(fit
->second
);
360 void CRepositoryDirectory::getFile(const NLMISC::CSString
& fileName
,NLMISC::CSString
& resultData
,IFileRequestValidator
* validator
,const NLNET::IModuleProxy
*sender
) const
362 // start by clearing out the result container...
365 // allow the overloadable validation callback a chance to prohibit read
366 if (validator
!=NULL
&& !validator
->cbValidateDownloadRequest(sender
,fileName
))
369 // if the file exists then go ahead and read it
370 NLMISC::CSString fullFileName
= _Root
+fileName
;
371 if (NLMISC::CFile::fileExists(fullFileName
))
373 resultData
.readFromFile(fullFileName
);
377 const NLMISC::CSString
&CRepositoryDirectory::getRootDirectory() const
382 bool CRepositoryDirectory::readIndex()
384 // start by clearing out our containers
387 // make sure the file exists (return false if not found)
388 NLMISC::CSString indexFileName
= getIndexFileName(_Root
);
389 if (!NLMISC::CFile::fileExists(indexFileName
))
392 // read the file contents
393 NLMISC::CSString index
;
394 index
.readFromFile(indexFileName
);
395 DROP_IF(index
.empty(),"Failed to read data from index file: "+indexFileName
,return false);
397 nlinfo("CRepositoryDirectory_Reading index file: %s",indexFileName
.c_str());
398 NLMISC::CVectorSString lines
;
399 index
.splitLines(lines
);
400 for (uint32 i
=0;i
<lines
.size();++i
)
402 // get hold of the line and strip off comments and spurious blanks
403 NLMISC::CSString line
= lines
[i
].splitTo("//").strip();
404 if (line
.empty()) continue;
406 // break the line down into constituent parts
408 fileInfo
.FileSize
= line
.strtok(",").strip().atoi();
409 fileInfo
.FileTime
= line
.strtok(",").strip().atoi();
410 fileInfo
.Checksum
.fromString(line
.strtok(",").strip());
411 fileInfo
.FileName
= line
.strip();
413 // make sure that the text in the line was valid and that the file on disk looks like the one in our record
414 DROP_IF(fileInfo
.FileName
.empty(),"Skipping line due to parse error: "+lines
[i
],continue);
416 // add the result to one of our maps of files
417 NLMISC::CSString path
= NLMISC::CFile::getPath(fileInfo
.FileName
);
418 _DirectoryTree
[path
][fileInfo
.FileName
]=fileInfo
;
420 nlinfo("%d entries read from index file",lines
.size());
421 _IndexFileIsUpToDate
= true;
425 bool CRepositoryDirectory::writeIndex() const
427 // get hold of the file name for the index file
428 NLMISC::CSString indexFileName
= getIndexFileName(_Root
);
429 // nldebug("Flushing changes to index file: %s",indexFileName.c_str());
431 // setup a text buffer to build our output file in
432 NLMISC::CSString outputText
= "// Index file for patchman service\n// *FORMAT*: size, time, checksum, filename\n\n";
434 // iterate over our directories and their files, building entries
435 for (TDirectoryTree::const_iterator dit
= _DirectoryTree
.begin(); dit
!= _DirectoryTree
.end(); ++dit
)
437 const TFileInfoMap
& theDirectory
= dit
->second
;
438 for (TFileInfoMap::const_iterator fit
= theDirectory
.begin(); fit
!=theDirectory
.end(); ++fit
)
440 const SFileInfo
& theInfo
= fit
->second
;
441 outputText
+= NLMISC::toString("%10u,%12u, %s, %s\n",theInfo
.FileSize
,theInfo
.FileTime
,theInfo
.Checksum
.toString().c_str(),theInfo
.FileName
.c_str());
446 // write the resulting buffer to disk
447 _IndexFileIsUpToDate
= outputText
.writeToFile(indexFileName
);
448 return _IndexFileIsUpToDate
;
451 void CRepositoryDirectory::_rescanDirectory(const NLMISC::CSString
& directoryName
, bool recurse
, IFileInfoUpdateListener
* updateListener
)
453 // nldebug("VERBOSE_Scanning directory: root=%s directory=%s",_Root.c_str(),directoryName.c_str());
455 // make sure we exist in the '_DirectoryTree' map (and get a handle to it)
456 TFileInfoMap
& theDirectory
= _DirectoryTree
[directoryName
];
458 // first scan for directories
459 std::vector
<std::string
> pathContents
;
460 NLMISC::CPath::getPathContent(_Root
+directoryName
,false,true,false,pathContents
);
462 // run through the directories we found...
463 for (uint32 i
=(uint32
)pathContents
.size();i
--;)
465 NLMISC::CSString childDirectoryName
= NLMISC::CSString(pathContents
[i
]).leftCrop((uint32
)_Root
.size());
467 // make sure they exist in the '_DirectoryTree' map
468 _DirectoryTree
[childDirectoryName
];
470 // if we're recursing then go for it
473 _rescanDirectory(childDirectoryName
,recurse
,updateListener
);
477 // run through all of the files in our map flagging them as 'not updated'
478 for (TFileInfoMap::iterator fit
= theDirectory
.begin(); fit
!= theDirectory
.end(); ++fit
)
480 fit
->second
.FileName
.clear();
483 // now scan for files
484 pathContents
.clear();
485 NLMISC::CPath::getPathContent(_Root
+directoryName
,false,false,true,pathContents
);
487 // run through the files adding them to ourself
488 for (uint32 i
=(uint32
)pathContents
.size();i
--;)
490 // if the file is system file then skip it
491 if (pathContents
[i
].find("/.")!=std::string::npos
)
494 // construct the file name
495 NLMISC::CSString fileName
= NLMISC::CSString(pathContents
[i
]).leftCrop((uint32
)_Root
.size());
496 // get hold of the directory entry for this file (or create a new one if not exist) and update it
497 _IndexFileIsUpToDate
&= _DirectoryTree
[directoryName
][fileName
].updateFileInfo(fileName
,pathContents
[i
],SFileInfo::RECALCULATE_IF_CHANGED
,updateListener
);
500 // run through all of the files in our map looking for files that are not updated and that need erasing
501 TFileInfoMap::iterator fit
= theDirectory
.begin();
502 while (fit
!=theDirectory
.end())
504 TFileInfoMap::iterator thisIt
= fit
;
506 if (thisIt
->second
.FileName
.empty())
508 // if there's an update listener object, then let them know about the file update
509 if (updateListener
!=NULL
)
511 updateListener
->cbFileInfoErased(thisIt
->first
);
514 // erase the entry in our files map and flag the index file as out of date
515 theDirectory
.erase(thisIt
);
516 _IndexFileIsUpToDate
= false;
522 //-----------------------------------------------------------------------------
523 // methods CFileManager
524 //-----------------------------------------------------------------------------
526 bool CFileManager::load(const NLMISC::CSString
& fileName
, uint32 startOffset
, uint32 numBytes
, NLMISC::CSString
& result
)
528 // clear out the return value before we begin
531 // make sure the file exists
532 if (!NLMISC::CFile::fileExists(fileName
))
535 // nldebug("Loading file data for: %s",fileName.c_str());
537 // get the file's vital statistics from disk
538 uint32 fileSize
= NLMISC::CFile::getFileSize(fileName
);
539 uint32 fileTime
= NLMISC::CFile::getFileModificationDate(fileName
);
541 // run through the files to see if the one we're after is here
542 TCacheFiles::iterator it
= _CacheFiles
.begin();
543 for (; it
!=_CacheFiles
.end();++it
)
545 // if we've found the file we're after then break out here
546 if (it
->FileName
==fileName
&& fileSize
==it
->FileSize
&& fileTime
==it
->FileTime
)
548 // nldebug("- Found data in Ram @ offset: %d",it->StartOffset);
553 // if we didn't find the file then we have to load it
554 if (it
==_CacheFiles
.end())
556 // nldebug("- Found data NOT already in Ram");
558 // setup a data block for this file
559 SCacheFileEntry newFileEntry
;
560 newFileEntry
.FileName
= fileName
;
561 newFileEntry
.FileSize
= fileSize
;
562 newFileEntry
.FileTime
= fileTime
;
563 newFileEntry
.StartOffset
= ~0u;
565 // if the buffer is too small to load this file then reallocate it and clear out the _CacheFiles vector
566 if (_CacheBuffer
.size()<fileSize
|| _CacheFiles
.empty())
569 _CacheBuffer
.clear();
570 _CacheBuffer
.resize(max(uint32(fileSize
*2),uint32(FileCacheSize
)));
571 newFileEntry
.StartOffset
= 0;
572 // nldebug("- Grew buffer to %d bytes",_CacheBuffer.size());
576 newFileEntry
.StartOffset
= _CacheFiles
.back().StartOffset
+_CacheFiles
.back().FileSize
;
577 uint32 requiredEndOffset
= newFileEntry
.StartOffset
+ fileSize
;
578 // see whether we'll fit in between the 'back file and end of buffer
579 if (requiredEndOffset
>_CacheBuffer
.size())
581 // clear out all remaining files between us and the end of buffer
582 while (!_CacheFiles
.empty() && _CacheFiles
.front().StartOffset
>=newFileEntry
.StartOffset
)
584 // nldebug("- Ditching cache entry: %s",_CacheFiles.front().FileName.c_str());
585 _CacheFiles
.pop_front();
586 nlassert(!_CacheFiles
.empty());
588 // not enough space at end of file so we'll need to spin round to the start
589 newFileEntry
.StartOffset
= 0;
590 requiredEndOffset
= fileSize
;
592 // our start offset is now OK, so make a bit of room for our data as required
593 while (!_CacheFiles
.empty() && _CacheFiles
.front().StartOffset
>=newFileEntry
.StartOffset
&& _CacheFiles
.front().StartOffset
<requiredEndOffset
)
595 // nldebug("- Ditching cache entry: %s",_CacheFiles.front().FileName.c_str());
596 _CacheFiles
.pop_front();
600 // nldebug("- Reading file data @offset: %d (%d bytes)",newFileEntry.StartOffset,fileSize);
603 FILE* inf
= nlfopen(fileName
, "rb");
604 BOMB_IF(inf
==NULL
,"Failed to open input file for reading: "+fileName
,return false);
605 uint32 bytesRead
=(uint32
)fread(&_CacheBuffer
[newFileEntry
.StartOffset
],1,fileSize
,inf
);
607 BOMB_IF(bytesRead
!=fileSize
,"Failed to read data from input file: "+fileName
,return false);
609 // add our new file descriptioon block to the _CacheFiles container
610 _CacheFiles
.push_back(newFileEntry
);
611 it
=_CacheFiles
.end();
615 // make sure the requested file segment is valid
616 uint32 endOffset
= startOffset
+numBytes
;
617 DROP_IF(endOffset
>fileSize
,"Ignoring request for data where end offset > file size: "+fileName
,return false);
619 // nldebug("- Retrieving data from buffer @offset: %d (%d bytes)",it->StartOffset+startOffset,numBytes);
621 // copy out the data chunk that we're after
622 result
.resize(numBytes
);
623 memcpy(&result
[0],&_CacheBuffer
[it
->StartOffset
+startOffset
],numBytes
);
625 // we succeeded so return true
629 TRepositoryDirectoryPtr
CFileManager::getRepositoryDirectory(const NLMISC::CSString
& path
)
631 // get hold of a ref to the map entry pointing at the directory we want (create a new map entry if need be)
632 TRepositoryDirectoryRefPtr
& thePtr
=_RepositoryDirectories
[path
];
634 // if the map entry is null then create a new one
637 thePtr
= new CRepositoryDirectory(path
);
644 bool CFileManager::save(const NLMISC::CSString
& fileName
, const NLMISC::CMemStream
& data
)
646 NLMISC::CSString tmpFileName
= fileName
+"__patchman__.sav";
648 // make sure the destination file is deleted before we begin
649 if (NLMISC::CFile::fileExists(fileName
))
651 NLMISC::CFile::deleteFile(fileName
);
654 // try to write the tmp file
657 // make sure that the directory structure exists foe the file
658 NLMISC::CSString path
= NLMISC::CFile::getPath(fileName
);
661 NLMISC::CFile::createDirectoryTree(path
);
664 // go ahead and write the data to disk...
665 COFile
outputFile(tmpFileName
);
666 outputFile
.serialBuffer(const_cast<uint8
*>(data
.buffer()),data
.size());
672 // make sure that the file write succeeded
673 if (NLMISC::CFile::getFileSize(tmpFileName
)!=data
.size())
675 nlwarning("Failed to save file '%s' because failed to save tmp file: %s",fileName
.c_str(),tmpFileName
.c_str());
676 NLMISC::CFile::deleteFile(tmpFileName
);
680 // write succeeded so rename the tmp file to the correct file name
681 bool ok
= NLMISC::CFile::moveFile(fileName
, tmpFileName
);
682 DROP_IF(!ok
,"Failed to save file '"+fileName
+"' because failed to rename tmp file: '"+tmpFileName
+"'",return false);
687 uint32
CFileManager::getFileSize(const NLMISC::CSString
& fileName
)
691 return NLMISC::CFile::getFileSize(fileName
);
699 } // end of namespace
702 //-----------------------------------------------------------------------------
703 // NLMISC_COMMANDS for testing the singleton interface
704 //-----------------------------------------------------------------------------
706 NLMISC_CATEGORISED_COMMAND(patchman
,fileManagerLoad
,"Load a file segment via the file manager","<file name> <start offset> <num bytes>")
711 CSString fileName
= args
[0];
712 CSString startOffset
=args
[1];
713 CSString numBytes
=args
[2];
716 bool ok
= CFileManager::getInstance().load(fileName
,startOffset
.atoui(),numBytes
.atoui(),data
);
719 log
.displayNL("Loaded %d bytes from file: %s[%d] (requested %d) starting: %s",data
.size(),fileName
.c_str(),startOffset
.atoui(),numBytes
.atoui(),data
.left(20).quote().c_str());
723 log
.displayNL("Load failed for file: %s (from offset %d to %d)",fileName
.c_str(),startOffset
.atoui(),startOffset
.atoui()+numBytes
.atoui()-1);
729 NLMISC_CATEGORISED_COMMAND(patchman
,fileManagerSave
,"Save a file via the file manager","<file name> <text to save>")
734 CSString fileName
= args
[0];
736 data
.serialBuffer((uint8
*)(&args
[1][0]),(uint32
)args
[1].size());
737 CFileManager::getInstance().save(fileName
,data
);
742 NLMISC_CATEGORISED_COMMAND(patchman
,testCFileSpec
,"test the CFileSpec class","")
749 CFileSpec
fsa("foo/bar");
750 nlassert(fsa
.matches("foo/bar"));
751 nlassert(!fsa
.matches("foo/bard"));
752 nlassert(!fsa
.matches("food/bar"));
753 nlassert(!fsa
.matches("foo/d/bar"));
754 nlassert(!fsa
.nameIsWild());
755 nlassert(!fsa
.pathIsWild());
756 nlassert(fsa
.nameMatches("bar"));
757 nlassert(fsa
.pathMatches("foo/"));
758 nlassert(!fsa
.nameMatches("bard"));
759 nlassert(!fsa
.pathMatches("food/"));
760 nlassert(!fsa
.pathMatches("foo/d/"));
761 nlassert(fsa
.nameSpec()=="bar");
762 nlassert(fsa
.pathSpec()=="foo/");
763 nlassert(fsa
.toString()=="foo/bar");
768 CFileSpec
fsb("foo/bar*");
769 nlassert(fsb
.matches("foo/bar"));
770 nlassert(fsb
.matches("foo/bard"));
771 nlassert(!fsb
.matches("food/bar"));
772 nlassert(!fsb
.matches("foo/d/bar"));
773 nlassert(fsb
.nameIsWild());
774 nlassert(!fsb
.pathIsWild());
775 nlassert(fsb
.nameMatches("bard"));
776 nlassert(fsb
.pathMatches("foo/"));
777 nlassert(fsb
.nameMatches("bard"));
778 nlassert(!fsb
.pathMatches("food/"));
779 nlassert(!fsb
.pathMatches("foo/d/"));
780 nlassert(fsb
.nameSpec()=="bar*");
781 nlassert(fsb
.pathSpec()=="foo/");
782 nlassert(fsb
.toString()=="foo/bar*");
787 CFileSpec
fsc("foo*/bar");
788 nlassert(fsc
.matches("foo/bar"));
789 nlassert(!fsc
.matches("foo/bard"));
790 nlassert(fsc
.matches("food/bar"));
791 nlassert(fsc
.matches("foo/d/bar"));
792 nlassert(!fsc
.nameIsWild());
793 nlassert(fsc
.pathIsWild());
794 nlassert(fsc
.nameMatches("bar"));
795 nlassert(fsc
.pathMatches("foo/"));
796 nlassert(!fsc
.nameMatches("bard"));
797 nlassert(fsc
.pathMatches("food/"));
798 nlassert(fsc
.pathMatches("foo/d/"));
799 nlassert(fsc
.nameSpec()=="bar");
800 nlassert(fsc
.pathSpec()=="foo*/");
801 nlassert(fsc
.toString()=="foo*/bar");
806 CFileSpec
fsd("foo*/bar*");
807 nlassert(fsd
.matches("foo/bar"));
808 nlassert(fsd
.matches("foo/bard"));
809 nlassert(fsd
.matches("food/bar"));
810 nlassert(fsd
.matches("foo/d/bar"));
811 nlassert(fsd
.nameIsWild());
812 nlassert(fsd
.pathIsWild());
813 nlassert(fsd
.nameMatches("bar"));
814 nlassert(fsd
.pathMatches("foo/"));
815 nlassert(fsd
.nameMatches("bard"));
816 nlassert(fsd
.pathMatches("food/"));
817 nlassert(fsd
.pathMatches("foo/d/"));
818 nlassert(fsd
.nameSpec()=="bar*");
819 nlassert(fsd
.pathSpec()=="foo*/");
820 nlassert(fsd
.toString()=="foo*/bar*");
831 CFileSpec(const NLMISC::CSString
& fileSpec
);
833 // test a complete match (filename and path)
834 bool matches(const NLMISC::CSString
& fullFileName
) const;
836 // test whether the given file name matches the filename part of the filespec
837 // note - the supplied filename should already have been stripped of its path
838 bool nameMatches(const NLMISC::CSString
& fileName
) const;
840 // test whether the given path matches the path part of the filespec
841 // note - the supplied path should not have an attached file name
842 bool pathMatches(const NLMISC::CSString
& path
) const;
844 // retrieve the filespec as a single string (for serialising, etc)
845 NLMISC::CSString
toString() const;
848 const NLMISC::CSString
& nameSpec() const;
849 const NLMISC::CSString
& pathSpec() const;
850 bool nameIsWild() const;
851 bool pathIsWild() const;
854 NLMISC::CSString _NameSpec
;
855 NLMISC::CSString _PathSpec
;
856 bool _NameIsWild
; // true if _NameSpec contains wildcards ('*' or '?')
857 bool _PathIsWild
; // true if _PathSpec contains wildcards ('*' or '?')