/* memofile-conduit.cc KPilot ** ** Copyright (C) 2004-2007 by Jason 'vanRijn' Kasper ** */ /* ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU Lesser General Public License as published by ** the Free Software Foundation; either version 2.1 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU Lesser General Public License for more details. ** ** You should have received a copy of the GNU Lesser General Public License ** along with this program in a file called COPYING; if not, write to ** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, ** MA 02110-1301, USA. */ /* ** Bug reports and questions can be sent to kde-pim@kde.org */ #include "options.h" #include "memofiles.h" #include "memofile.h" TQString Memofiles::FIELD_SEP = CSL1("\t"); Memofiles::Memofiles (MemoCategoryMap & categories, PilotMemoInfo &appInfo, TQString & baseDirectory, CUDCounter &fCtrPC) : _categories(categories), _memoAppInfo(appInfo), _baseDirectory(baseDirectory), _cudCounter(fCtrPC) { FUNCTIONSETUP; _memofiles.clear(); _memoMetadataFile = _baseDirectory + TQDir::separator() + CSL1(".ids"); _categoryMetadataFile = _baseDirectory + TQDir::separator() + CSL1(".categories"); _memofiles.setAutoDelete(true); _ready = ensureDirectoryReady(); _metadataLoaded = loadFromMetadata(); } Memofiles::~Memofiles() { FUNCTIONSETUP; } void Memofiles::load (bool loadAll) { FUNCTIONSETUP; DEBUGKPILOT << fname << ": now looking at all memofiles in your directory." << endl; // now go through each of our known categories and look in each directory // for that category for memo files MemoCategoryMap::ConstIterator it; int counter = -1; for ( it = _categories.begin(); it != _categories.end(); ++it ) { int category = it.key(); TQString categoryName = it.data(); TQString categoryDirname = _baseDirectory + TQDir::separator() + categoryName; TQDir dir = TQDir(categoryDirname); if (! dir.exists() ) { DEBUGKPILOT << fname << ": category directory: [" << categoryDirname << "] doesn't exist. skipping." << endl; continue; } TQStringList entries = dir.entryList(TQDir::Files); TQString file; for(TQStringList::Iterator it = entries.begin(); it != entries.end(); ++it) { file = *it; TQFileInfo info(dir, file); if(info.isFile() && info.isReadable()) { // DEBUGKPILOT << fname // << ": checking category: [" << categoryName // << "], file: [" << file << "]." << endl; Memofile * memofile = find(categoryName, file); if (NULL == memofile) { memofile = new Memofile(category, categoryName, file, _baseDirectory); memofile->setModified(true); _memofiles.append(memofile); DEBUGKPILOT << fname << ": looks like we didn't know about this one until now. " << "created new memofile for category: [" << categoryName << "], file: [" << file << "]." << endl; } counter++; // okay, we should have a memofile for this file now. see if we need // to load its text... if (memofile->isModified() || loadAll) { DEBUGKPILOT << fname << ": now loading text for: [" << info.filePath() << "]." << endl; memofile->load(); } } else { DEBUGKPILOT << fname << ": couldn't read file: [" << info.filePath() << "]. skipping it." << endl; } } // end of iterating through files in this directory } // end of iterating through our categories/directories DEBUGKPILOT << fname << ": looked at: [" << counter << "] files from your directories." << endl; // okay, now we've loaded everything from our directories. make one last // pass through our loaded memofiles and see if we need to mark any of them // as deleted (i.e. we created a memofile object from our metadata, but // the file is now gone, so it's deleted. Memofile * memofile; for ( memofile = _memofiles.first(); memofile; memofile = _memofiles.next() ) { if (! memofile->fileExists()) { memofile->setDeleted( true ); } } } /** * Make sure that our directory is ready to synchronize with our * Palm's database. This means we need to make sure that the directory * that our user has specified for storing his/her memos exists, as well * as a directory inside that directory for each of his/her memo categories. */ bool Memofiles::ensureDirectoryReady() { FUNCTIONSETUP; if (!checkDirectory(_baseDirectory)) return false; int failures = 0; // now make sure that a directory for each category exists. TQString _category_name; TQString dir; MemoCategoryMap::Iterator it; for ( it = _categories.begin(); it != _categories.end(); ++it ) { _category_name = it.data(); dir = _baseDirectory + TQDir::separator() + _category_name; DEBUGKPILOT << fname << ": checking directory: [" << dir << "]" << endl; if (!checkDirectory(dir)) failures++; } return failures == 0; } bool Memofiles::checkDirectory(TQString & dir) { FUNCTIONSETUP; // make sure that the directory we're asked to write to exists TQDir d(dir); TQFileInfo fid( dir ); if ( ! fid.isDir() ) { DEBUGKPILOT << fname << ": directory: [" << dir << "] doesn't exist. creating...." << endl; if (!d.mkdir(dir)) { DEBUGKPILOT << fname << ": could not create directory: [" << dir << "]. this won't end well." << endl; return false; } else { DEBUGKPILOT << fname << ": directory created: [" << dir << "]." << endl; } } else { DEBUGKPILOT << fname << ": directory already existed: [" << dir << "]." << endl; } return true; } void Memofiles::eraseLocalMemos () { FUNCTIONSETUP; MemoCategoryMap::Iterator it; for ( it = _categories.begin(); it != _categories.end(); ++it ) { TQString dir = _baseDirectory + TQDir::separator() + it.data(); if (!folderRemove(TQDir(dir))) { DEBUGKPILOT << fname << ": couldn't erase all local memos from: [" << dir << "]." << endl; } } TQDir d(_baseDirectory); d.remove(_memoMetadataFile); ensureDirectoryReady(); _memofiles.clear(); } void Memofiles::setPilotMemos (TQPtrList & memos) { FUNCTIONSETUP; PilotMemo * memo; _memofiles.clear(); for ( memo = memos.first(); memo; memo = memos.next() ) { addModifiedMemo(memo); } DEBUGKPILOT << fname << ": set: [" << _memofiles.count() << "] from Palm to local." << endl; } bool Memofiles::loadFromMetadata () { FUNCTIONSETUP; _memofiles.clear(); TQFile f( _memoMetadataFile ); if ( !f.open( IO_ReadOnly ) ) { DEBUGKPILOT << fname << ": ooh, bad. couldn't open your memo-id file for reading." << endl; return false; } TQTextStream t( &f ); Memofile * memofile; while ( !t.atEnd() ) { TQString data = t.readLine(); int errors = 0; bool ok; TQStringList fields = TQStringList::split( FIELD_SEP, data ); if ( fields.count() >= 4 ) { int id = fields[0].toInt( &ok ); if ( !ok ) errors++; int category = fields[1].toInt( &ok ); if ( !ok ) errors++; uint lastModified = fields[2].toInt( &ok ); if ( !ok ) errors++; uint size = fields[3].toInt( &ok ); if ( !ok ) errors++; TQString filename = fields[4]; if ( filename.isEmpty() ) errors++; if (errors <= 0) { memofile = new Memofile(id, category, lastModified, size, _categories[category], filename, _baseDirectory); _memofiles.append(memofile); // DEBUGKPILOT << fname // << ": created memofile from metadata. id: [" << id // << "], category: [" // << _categories[category] << "], filename: [" << filename << "]." // << endl; } } else { errors++; } if (errors > 0) { DEBUGKPILOT << fname << ": error: couldn't understand this line: [" << data << "]." << endl; } } DEBUGKPILOT << fname << ": loaded: [" << _memofiles.count() << "] memofiles." << endl; f.close(); return true; } Memofile * Memofiles::find (recordid_t id) { Memofile * memofile; for ( memofile = _memofiles.first(); memofile; memofile = _memofiles.next() ) { if ( memofile->id() == id) { return memofile; } } return NULL; } Memofile * Memofiles::find (const TQString & category, const TQString & filename) { Memofile * memofile; for ( memofile = _memofiles.first(); memofile; memofile = _memofiles.next() ) { if ( memofile->getCategoryName() == category && memofile->getFilename() == filename ) { return memofile; } } return NULL; } void Memofiles::deleteMemo(PilotMemo * memo) { FUNCTIONSETUP; if (! memo->isDeleted()) return; Memofile * memofile = find(memo->id()); if (memofile) { memofile->deleteFile(); _memofiles.remove(memofile); _cudCounter.deleted(); } } void Memofiles::addModifiedMemo (PilotMemo * memo) { FUNCTIONSETUP; if (memo->isDeleted()) { deleteMemo(memo); return; } TQString debug = CSL1(": adding a PilotMemo. id: [") + TQString::number(memo->id()) + CSL1("], title: [") + memo->getTitle() + CSL1("]. "); Memofile * memofile = find(memo->id()); if (NULL == memofile) { _cudCounter.created(); debug += CSL1(" new from pilot."); } else { // we have found a local memofile that was modified on the palm. for the time // being (until someone complains, etc.), we will always overwrite changes to // the local filesystem with changes to the palm (palm overrides local). at // some point in the future, we should probably honor a user preference for // this... _cudCounter.updated(); _memofiles.remove(memofile); debug += CSL1(" modified from pilot."); } DEBUGKPILOT << fname << debug << endl; memofile = new Memofile(memo, _categories[memo->category()], filename(memo), _baseDirectory); memofile->setModifiedByPalm(true); _memofiles.append(memofile); } TQPtrList Memofiles::getModified () { FUNCTIONSETUP; TQPtrList modList; modList.clear(); Memofile * memofile; for ( memofile = _memofiles.first(); memofile; memofile = _memofiles.next() ) { if ( memofile->isModified() && ! memofile->isModifiedByPalm() ) { modList.append(memofile); } } DEBUGKPILOT << fname << ": found: [" << modList.count() << "] memofiles modified on filesystem." << endl; return modList; } void Memofiles::save() { FUNCTIONSETUP; saveCategoryMetadata(); saveMemos(); // this needs to be done last, because saveMemos() might change // attributes of the Memofiles saveMemoMetadata(); } bool Memofiles::saveMemoMetadata() { FUNCTIONSETUP; DEBUGKPILOT << fname << ": saving memo metadata to file: [" << _memoMetadataFile << "]" << endl; TQFile f( _memoMetadataFile ); TQTextStream stream(&f); if( !f.open(IO_WriteOnly) ) { DEBUGKPILOT << fname << ": ooh, bad. couldn't open your memo-id file for writing." << endl; return false; } Memofile * memofile; // each line looks like this, but FIELD_SEP is the separator instead of "," // id,category,lastModifiedTime,filesize,filename for ( memofile = _memofiles.first(); memofile; memofile = _memofiles.next() ) { // don't save deleted memos to our id file if (! memofile->isDeleted()) { stream << memofile->id() << FIELD_SEP << memofile->category() << FIELD_SEP << memofile->lastModified() << FIELD_SEP << memofile->size() << FIELD_SEP << memofile->filename() << endl; } } f.close(); return true; } MemoCategoryMap Memofiles::readCategoryMetadata() { FUNCTIONSETUP; DEBUGKPILOT << fname << ": reading categories from file: [" << _categoryMetadataFile << "]" << endl; MemoCategoryMap map; map.clear(); TQFile f( _categoryMetadataFile ); TQTextStream stream(&f); if( !f.open(IO_ReadOnly) ) { DEBUGKPILOT << fname << ": ooh, bad. couldn't open your categories file for reading." << endl; return map; } while ( !stream.atEnd() ) { TQString data = stream.readLine(); int errors = 0; bool ok; TQStringList fields = TQStringList::split( FIELD_SEP, data ); if ( fields.count() >= 2 ) { int id = fields[0].toInt( &ok ); if ( !ok ) errors++; TQString categoryName = fields[1]; if ( categoryName.isEmpty() ) errors++; if (errors <= 0) { map[id] = categoryName; } } else { errors++; } if (errors > 0) { DEBUGKPILOT << fname << ": error: couldn't understand this line: [" << data << "]." << endl; } } DEBUGKPILOT << fname << ": loaded: [" << map.count() << "] categories." << endl; f.close(); return map; } bool Memofiles::saveCategoryMetadata() { FUNCTIONSETUP; DEBUGKPILOT << fname << ": saving categories to file: [" << _categoryMetadataFile << "]" << endl; TQFile f( _categoryMetadataFile ); TQTextStream stream(&f); if( !f.open(IO_WriteOnly) ) { DEBUGKPILOT << fname << ": ooh, bad. couldn't open your categories file for writing." << endl; return false; } MemoCategoryMap::Iterator it; for ( it = _categories.begin(); it != _categories.end(); ++it ) { stream << it.key() << FIELD_SEP << it.data() << endl; } f.close(); return true; } bool Memofiles::saveMemos() { FUNCTIONSETUP; Memofile * memofile; bool result = true; for ( memofile = _memofiles.first(); memofile; memofile = _memofiles.next() ) { if (memofile->isDeleted()) { _memofiles.remove(memofile); } else { result = memofile->save(); // Fix prompted by Bug #103922 // if we weren't able to save the file, then remove it from the list. // if we don't do this, the next sync will think that the user deliberately // deleted the memofile and will then delete it from the Pilot. // TODO -- at some point, we should probably tell the user that this // did not work, but that will require a String change. // Also, this is a partial fix since at this point // this memo will never make its way onto the PC, but at least // we won't delete it from the Pilot erroneously either. *sigh* if (!result) { DEBUGKPILOT << fname << ": unable to save memofile: [" << memofile->filename() << "], now removing it from the metadata list." << endl; _memofiles.remove(memofile); } } } return true; } bool Memofiles::isFirstSync() { FUNCTIONSETUP; bool metadataExists = TQFile::exists(_memoMetadataFile) && TQFile::exists(_categoryMetadataFile); bool valid = metadataExists && _metadataLoaded; DEBUGKPILOT << fname << ": local metadata exists: [" << metadataExists << "], metadata loaded: [" << _metadataLoaded << "], returning: [" << ! valid << "]" << endl; return ! valid; } bool Memofiles::folderRemove(const TQDir &_d) { FUNCTIONSETUP; TQDir d = _d; TQStringList entries = d.entryList(); for(TQStringList::Iterator it = entries.begin(); it != entries.end(); ++it) { if(*it == CSL1(".") || *it == CSL1("..")) continue; TQFileInfo info(d, *it); if(info.isDir()) { if(!folderRemove(TQDir(info.filePath()))) return FALSE; } else { DEBUGKPILOT << fname << ": deleting file: [" << info.filePath() << "]" << endl; d.remove(info.filePath()); } } TQString name = d.dirName(); if(!d.cdUp()) return FALSE; DEBUGKPILOT << fname << ": removing folder: [" << name << "]" << endl; d.rmdir(name); return TRUE; } TQString Memofiles::filename(PilotMemo * memo) { FUNCTIONSETUP; TQString filename = memo->getTitle(); if (filename.isEmpty()) { TQString text = memo->text(); int i = text.find(CSL1("\n")); if (i > 1) { filename = text.left(i); } if (filename.isEmpty()) { filename = CSL1("empty"); } } filename = sanitizeName(filename); TQString category = _categories[memo->category()]; Memofile * memofile = find(category, filename); // if we couldn't find a memofile with this filename, or if the // memofile that is found is the same as the memo that we're looking // at, then use the filename if (NULL == memofile || memofile == memo) { return filename; } int uniq = 2; TQString newfilename; // try to find a good filename, but only do this 20 times at the most. // if our user has 20 memos with the same filename, he/she is asking // for trouble. while (NULL != memofile && uniq <=20) { newfilename = TQString(filename + CSL1(".") + TQString::number(uniq++) ); memofile = find(category, newfilename); } return newfilename; } TQString Memofiles::sanitizeName(TQString name) { TQString clean = name; // safety net. we can't save a // filesystem separator as part of a filename, now can we? clean.replace('/', CSL1("-")); return clean; }