/* JFileSystem centralizes all access to resources in JGE. It allows to have files for the game split in two subfolders, a "system" subfolder (read only) and a "user" subfolder (read/write) Additionally, these two subfolders can have some of their resources in zip file (see zfsystem.h). Zip files can contain duplicates of the same file, the one that will eventually be used is the one is the latest zip file (by alphabetical order) Read access priority: User folder, real file User folder, zip file System folder, real file System folder, zip file Write access: User folder, real file User folder is the only one that is really needed to guarantee both read and write access, the system folder is not necessary but provides a nice way to distinguish The content that users should not be touching. */ #if defined (ANDROID) #include "PrecompiledHeader.h" #endif //ANDROID #if defined (LINUX) #include "../../projects/mtg/include/PrecompiledHeader.h" #endif //LINUX #ifdef WIN32 #pragma warning(disable : 4786) #include #define MAKEDIR(name) _mkdir(name) #else #include #define MAKEDIR(name) mkdir(name, 0777) #endif #include "../include/JGE.h" #include "../include/JFileSystem.h" #include "../include/JLogger.h" #include #ifdef QT_CONFIG #include #endif /* #ifdef IOS #include #endif */ JFileSystem* JFileSystem::mInstance = NULL; JZipCache::JZipCache() {} JZipCache::~JZipCache() { dir.clear(); } void JFileSystem::Pause() { filesystem::closeTempFiles(); } void JFileSystem::preloadZip(const string& filename) { map::iterator it = mZipCache.find(filename); if (it != mZipCache.end()) return; //random number of files stored in the cache. // This is based on the idea that an average filepath (in Wagic) for image is 37 characters (=bytes) + 8 bytes to store the fileinfo = 45 bytes, //so 4500 files represent roughly 200kB, an "ok" size for the PSP if (mZipCachedElementsCount > 4500) { clearZipCache(); } JZipCache * cache = new JZipCache(); mZipCache[filename] = cache; if (!mZipAvailable || !mZipFile) { AttachZipFile(filename); if (!mZipAvailable || !mZipFile) return; } if ((mUserFS->PreloadZip(filename.c_str(), cache->dir) || (mSystemFS && mSystemFS->PreloadZip(filename.c_str(), cache->dir)))) { mZipCachedElementsCount+= cache->dir.size(); } else { DetachZipFile(); } } void JFileSystem::init(const string & userPath, const string & systemPath) { Destroy(); mInstance = new JFileSystem(userPath, systemPath); } JFileSystem* JFileSystem::GetInstance() { if (!mInstance) { #ifdef RESPATH init( RESPATH"/"); #else init("Res/"); #endif } return mInstance; } // Tries to set the system and user paths. // On some OSes, the parameters get overriden by hardcoded values JFileSystem::JFileSystem(const string & _userPath, const string & _systemPath) { string systemPath = _systemPath; string userPath = _userPath; #ifdef IOS NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; userPath = [[documentsDirectory stringByAppendingString: @"/User/"] cStringUsingEncoding:1]; systemPath = [[documentsDirectory stringByAppendingString: @"/Res/"] cStringUsingEncoding:1]; #elif defined (ANDROID) userPath = JGE::GetInstance()->getFileSystemLocation(); systemPath = ""; DebugTrace("User path " << userPath); #elif defined (QT_CONFIG) QDir sysDir(RESDIR); QDir dir(QDir::homePath()); dir.mkdir(USERDIR); dir.cd(USERDIR); userPath = QDir::toNativeSeparators(dir.absolutePath()).toStdString(); systemPath = QDir::toNativeSeparators(sysDir.absolutePath()).toStdString(); DebugTrace("User path " << userPath); DebugTrace("System path " << systemPath); DebugTrace("Current path " << QDir::currentPath().toStdString()); #else //Find the Res.txt file and matching Res folders for backwards compatibility ifstream mfile("Res.txt"); string resPath; if (mfile) { bool found = false; while (!found && std::getline(mfile, resPath)) { if (!resPath.size()) continue; if (resPath[resPath.size() - 1] == '\r') resPath.erase(resPath.size() - 1); //Handle DOS files string testfile = resPath; testfile.append("graphics/simon.dat"); ifstream file(testfile.c_str()); if (file) { userPath = resPath; systemPath = ""; found = true; file.close(); } } mfile.close(); } #endif // Make sure the base paths finish with a '/' or a '\' if (! userPath.empty()) { string::iterator c = userPath.end();//userPath.at(userPath.size()-1); c--; if ((*c != '/') && (*c != '\\')) userPath += '/'; } if (! systemPath.empty()) { string::iterator c = systemPath.end();//systemPath.at(systemPath.size()-1); c--; if ((*c != '/') && (*c != '\\')) systemPath += '/'; } mUserFSPath = userPath; MAKEDIR(userPath.c_str()); mSystemFSPath = systemPath; mUserFS = new filesystem(userPath.c_str()); mSystemFS = (mSystemFSPath.size() && (mSystemFSPath.compare(mUserFSPath) != 0)) ? new filesystem(systemPath.c_str()) : NULL; mZipAvailable = false; mZipCachedElementsCount = 0; mPassword = NULL; mFileSize = 0; mCurrentFileInZip = NULL; }; void JFileSystem::Destroy() { if (mInstance) { delete mInstance; mInstance = NULL; } } bool JFileSystem::DirExists(const string& strDirname) { return (mSystemFS && mSystemFS->DirExists(strDirname)) || mUserFS->DirExists(strDirname); } bool JFileSystem::FileExists(const string& strFilename) { if (strFilename.length() < 1 ) return false; return (mSystemFS && mSystemFS->FileExists(strFilename)) || mUserFS->FileExists(strFilename); } bool JFileSystem::MakeDir(const string & dir) { string fullDir = mUserFSPath + dir; MAKEDIR(fullDir.c_str()); return true; } JFileSystem::~JFileSystem() { clearZipCache(); filesystem::closeTempFiles(); SAFE_DELETE(mUserFS); SAFE_DELETE(mSystemFS); } void JFileSystem::clearZipCache() { DetachZipFile(); map::iterator it; for (it = mZipCache.begin(); it != mZipCache.end(); ++it){ delete(it->second); } mZipCache.clear(); mZipCachedElementsCount = 0; } bool JFileSystem::AttachZipFile(const string &zipfile, char *password /* = NULL */) { if (mZipAvailable && mZipFile.is_open()) { if (mZipFileName != zipfile) DetachZipFile(); // close the previous zip file else return true; } mZipFileName = zipfile; mPassword = password; openForRead(mZipFile, mZipFileName); if (!mZipFile) return false; //A hack for a zip inside a zip: instead we open the zip containing it if (mZipFile.Zipped()) { mZipFile.close(); assert(filesystem::getCurrentFS()); mZipFile.open(filesystem::getCurrentZipName().c_str(), filesystem::getCurrentFS()); assert(mZipFile); } mZipAvailable = true; return true; } void JFileSystem::DetachZipFile() { if (mZipFile) { mZipFile.close(); } mCurrentFileInZip = NULL; mZipAvailable = false; } bool JFileSystem::openForRead(izfstream & File, const string & FilePath) { File.open(FilePath.c_str(), mUserFS); if (File) return true; if(!mSystemFS) return false; File.open(FilePath.c_str(), mSystemFS); if (File) return true; return false; } bool JFileSystem::readIntoString(const string & FilePath, string & target) { izfstream file; if (!openForRead(file, FilePath)) return false; int fileSize = GetFileSize(file); #ifndef __MINGW32__ try { #endif target.resize((std::string::size_type) fileSize); #ifndef __MINGW32__ } catch (bad_alloc&) { return false; } #endif if (fileSize) file.read(&target[0], fileSize); file.close(); return true; } bool JFileSystem::openForWrite(ofstream & File, const string & FilePath, ios_base::openmode mode) { string filename = mUserFSPath; filename.append(FilePath); #if defined(ANDROID) DebugTrace("ANDROID"); std::vector dirs; string path = filename.substr( 0, filename.find_last_of( '/' ) + 1 ); // put it into list dirs.push_back(path); //make list of directories that need to be created do { path = path.substr( 0, path.find_last_of( '/', path.size() - 2 ) + 1 ); dirs.push_back(path); } while (path.compare(mUserFSPath) != 0); // remove mUserFSPath from list dirs.pop_back(); // create missing directories for (std::vector::reverse_iterator it = dirs.rbegin(); it != dirs.rend(); ++it) { if(!DirExists(*it)) { MAKEDIR((*it).c_str()); } } #endif File.open(filename.c_str(), mode); if (File) { return true; } return false; } bool JFileSystem::OpenFile(const string &filename) { mCurrentFileInZip = NULL; if (!mZipAvailable || !mZipFile) return openForRead(mFile, filename); preloadZip(mZipFileName); map::iterator it = mZipCache.find(mZipFileName); if (it == mZipCache.end()) { //DetachZipFile(); //return OpenFile(filename); return openForRead(mFile, filename); } JZipCache * zc = it->second; map::iterator it2 = zc->dir.find(filename); if (it2 == zc->dir.end()) { /*DetachZipFile(); return OpenFile(filename); */ return openForRead(mFile, filename); } mCurrentFileInZip = &(it2->second); mFileSize = it2->second.m_Size; return true; } void JFileSystem::CloseFile() { if (mZipAvailable && mZipFile) { mCurrentFileInZip = NULL; } if (mFile) mFile.close(); } //returns 0 if less than "size" bits were read int JFileSystem::ReadFile(void *buffer, int size) { if (mCurrentFileInZip) { assert(mZipFile); if((size_t)size > mCurrentFileInZip->m_Size) //only support "store" method for zip inside zips return 0; std::streamoff offset = filesystem::SkipLFHdr(mZipFile, mCurrentFileInZip->m_Offset); if (!mZipFile.seekg(offset)) return 0; mZipFile.read((char *) buffer, size); //TODO what if can't read return size; } if (!mFile) return 0; assert(!mFile.Zipped() || (size_t)size <= mFile.getUncompSize()); mFile.read((char *)buffer, size); if (mFile.eof()) return 0; return size; } std::vector& JFileSystem::scanRealFolder(const std::string& folderName, std::vector& results) { DIR *dip = opendir(folderName.c_str()); if (!dip) return results; while (struct dirent * dit = readdir(dip)) { results.push_back(dit->d_name); } closedir(dip); return results; } std::vector& JFileSystem::scanfolder(const std::string& _folderName, std::vector& results) { if (!_folderName.size()) return results; map seen; string folderName = _folderName; if (folderName[folderName.length() - 1] != '/') folderName.append("/"); //we scan zips first, then normal folders. // This is to avoid duplicate folders coming from the real folder // (a folder "foo" in the zip comes out as "foo/", while on the real FS it comes out as "foo") //user zips { //Scan the zip filesystem std::vector userZips; mUserFS->scanfolder(folderName, userZips); for (size_t i = 0; i < userZips.size(); ++i) seen[userZips[i]] = true; } //system zips if (mSystemFS) { //Scan the zip filesystem std::vector systemZips; mSystemFS->scanfolder(folderName, systemZips); for (size_t i = 0; i < systemZips.size(); ++i) seen[systemZips[i]] = true; } //user real files { //scan the real files std::vector userReal; string realFolderName = mUserFSPath; realFolderName.append(folderName); scanRealFolder(realFolderName, userReal); for (size_t i = 0; i < userReal.size(); ++i) { string asFolder = userReal[i] + "/"; if (seen.find(asFolder) == seen.end()) seen[userReal[i]] = true; } } //system real files if (mSystemFS) { //scan the real files std::vector systemReal; string realFolderName = mSystemFSPath; realFolderName.append(folderName); scanRealFolder(realFolderName, systemReal); for (size_t i = 0; i < systemReal.size(); ++i) { string asFolder = systemReal[i] + "/"; if (seen.find(asFolder) == seen.end()) seen[systemReal[i]] = true; } } for(map::iterator it = seen.begin(); it != seen.end(); ++it) { results.push_back(it->first); } return results; } std::vector JFileSystem::scanfolder(const std::string& folderName) { std::vector result; return scanfolder(folderName, result); } int JFileSystem::GetFileSize() { if (mCurrentFileInZip) return mFileSize; return GetFileSize(mFile); } bool JFileSystem::Rename(string _from, string _to) { string from = mUserFSPath + _from; string to = mUserFSPath + _to; std::remove(to.c_str()); return (rename(from.c_str(), to.c_str()) == 0); } bool JFileSystem::Remove(string aFile) { string toRemove = mUserFSPath + aFile; return (std::remove(toRemove.c_str()) == 0); } int JFileSystem::GetFileSize(izfstream & file) { if (!file) return 0; if (file.Zipped()) { //There's a bug in zipped version that prevents from sending a correct filesize with the "standard" seek method //The hack below only works for the "stored" version I think... return file.getUncompSize(); } file.seekg (0, ios::end); int length = (int) file.tellg(); file.seekg (0, ios::beg); return length; } // AVOID Using This function!!! /* This function is deprecated, but some code is still using it It used to give a pathname to a file in the file system. Now with the support of zip resources, a pathname does not make sense anymore However some of our code still relies on "physical" files not being in zip. So this call is now super heavy: it checks where the file is, and if it's in a zip, it extracts it to the user Filesystem, assuming that whoever called this needs to access the file through its pathname later on. As a result, this function isvery inefficient and shouldn't be used in the general case. */ string JFileSystem::GetResourceFile(string filename) { izfstream temp; bool result = openForRead(temp, filename); if (!temp || !result) return ""; if (!temp.Zipped()) { string result = temp.FullFilePath(); temp.close(); return result; } // File is inside a zip archive, //we copy it to the user FS string destFile = mUserFSPath + filename; ofstream dest; if (openForWrite(dest, filename, ios_base::binary)) { // allocate memory: size_t length = temp.getUncompSize(); char * buffer = new char [length]; // read data as a block: temp.read(buffer, length); temp.close(); dest.write (buffer,length); delete[] buffer; dest.close(); return destFile; } return ""; }