/*************************************************************************** batchrenamer.cpp - description ------------------- begin : Sat Aug 18 2001 copyright : (C) 2001 by Dominik Seichter email : domseichter@web.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifdef HAVE_CONFIG_H #include #endif #ifndef VERSION #define VERSION "unknown" #endif // OS includes #include #include #include #include // chmod: #include #include // QT includes #include #include // KDE includes #include #include #include #include // Own includes #include "ProgressDialog.h" #include "batchrenamer.h" #include "fileoperation.h" #include "pluginloader.h" #include "kmylistview.h" using namespace KIO; BatchRenamer::BatchRenamer() : m_index( 0 ) { plug = PluginLoader::instance(); m_counter_index = 0; } BatchRenamer::~BatchRenamer() { } void BatchRenamer::processFiles( ProgressDialog* p, TQObject* object ) { delete object; t.start(); m_counters.clear(); for( unsigned int i = 0; i < m_files.count(); i++) { m_counter_index = 0; if( m_mode == RENAME ) {// final Path = source Path m_files[i].dst.directory = m_files[i].src.directory; m_files[i].dst.url = m_files[i].src.url; m_files[i].dst.url.setFileName( TQString() ); } else { m_files[i].dst.directory = dirname.path(); m_files[i].dst.url = dirname; } if( i == 0 ) p->setDestination( m_files[i].dst.url ); else { if( m_reset ) findCounterReset( i ); } m_files[i].dst.name = processString( text, m_files[i].src.name, i ); if( !extext.isEmpty() ) m_files[i].dst.extension = processString( extext, m_files[i].src.extension, i ); (void)applyManualChanges( i ); // Assemble filenames parseSubdirs( &m_files[i] ); // TODO: DOM // ESCAPE HERE m_files[i].src.name = BatchRenamer::buildFilename( &m_files[i].src, true ); // Let's run the plugins that change the final filename, // i.e the encodingsplugin m_files[i].dst.name = parsePlugins( i, m_files[i].dst.name, TYPE_FINAL_FILENAME ); m_files[i].dst.name = BatchRenamer::buildFilename( &m_files[i].dst, true ); /* * take care of renamed directories and * correct the paths of their contents */ if( m_files[i].dir && (m_mode == RENAME || m_mode == MOVE) ) { for( unsigned int c = i; c < m_files.count(); c++ ) { if( m_files[c].src.directory.left( m_files[i].src.name.length() + 1 ) == ( m_files[i].src.name + "/" ) ) { m_files[c].src.directory.replace( 0, m_files[i].src.name.length(), m_files[i].dst.name ); m_files[c].src.url.setPath( BatchRenamer::buildFilename( &m_files[c].src, true ) ); } } } } p->print( TQString( i18n("Filenames Processed after %1 seconds.")).tqarg(t.elapsed()/1000) ); work( p ); } TQString BatchRenamer::processString( TQString text, TQString oldname, int i ) { /* * Come on! Grep for this text and help me! * * note about krename escape sequences * for certain characters: * * Krename will have problems with files * which contain one of the following * tqunicode characters: 60000, 60001, 60002 * 60003, 60004, 60005, 60006. * * This is not a good solution, if you have a * better one please tell me about it! */ doEscape( oldname ); /* * Call here all functions that handle * arguments that are single tokens (&,%,...). * or in [brackets] */ text = findBrackets( oldname, text, i ); text = findAndProcess( "$", text, oldname ); text = findAndProcess( "%", text, oldname.lower() ); text = findAndProcess( "&", text, oldname.upper() ); text = findAndProcess( "\\", text, oldname.stripWhiteSpace() ); text = findStar( oldname, text ); text = findNumbers( text, m_files.count(), i ); /* * text is used as argument token for plugins! */ text = parsePlugins( i, text, TYPE_TOKEN ); /* * Replace after Plugins ! * Replace shoud be the last the * before re-escaping tokens ! */ text = findReplace( text ); // convert special chars back (e.g. &,$) // TODO: this is to early, because // parseSubdirs creates subdirectories // for "/" returned by plugins!!!! // text = unEscape( text ); return text; } TQString BatchRenamer::parsePlugins( int i, const TQString& text, int type ) { TQPtrListIterator it( plug->libs ); TQString ret = text; if( type == TYPE_FINAL_FILE ) ret = ""; for( ; it.current(); ++it ) if( (*it)->usePlugin && (*it)->plugin->type() == type ) { ret = (*it)->plugin->processFile( this, i, text, type ); doEscape( ret ); } return ret; } void BatchRenamer::createPreview( TQListView* list ) { KMyListViewItem* item1 = NULL; TQString tmp; m_counters.clear(); for( unsigned int i = 0; i < m_files.count(); i++) { m_counter_index = 0; if( i && m_reset ) findCounterReset( i ); m_files[i].dst.name = processString( text, m_files[i].src.name, i ); if( !extext.isEmpty() ) m_files[i].dst.extension = processString( extext, m_files[i].src.extension, i ); bool modified = applyManualChanges( i ); TQString sname = BatchRenamer::buildFilename( &m_files[i].src, false ); // Let's run the plugins that change the final filename, // i.e the encodingsplugin m_files[i].dst.name = parsePlugins( i, m_files[i].dst.name, TYPE_FINAL_FILENAME ); TQString dname = BatchRenamer::buildFilename( &m_files[i].dst, false ); item1 = new KMyListViewItem( modified, list, item1, sname, dname ); } } void BatchRenamer::work( ProgressDialog* p ) { // TODO: use CopyJob here FileOperation fop; TQFile* fundo ( NULL ); TQTextStream* tundo ( NULL ); if( undo ) { // Create header for undo script fundo = new TQFile( m_undoScript ); if( fundo->open( IO_WriteOnly ) ) { tundo = new TQTextStream( fundo ); writeUndoScript( tundo ); } else { undo = false; p->error( i18n("Can't create undo script :") + fundo->name() ); delete fundo; } } int error = 0; RenamedList* renamedFiles = new RenamedList[m_files.count()]; p->setProgressTotalSteps( m_files.count() + 1 ); /* * Give the user some information... */ if( m_mode == COPY) p->print( TQString( i18n("Files will be copied to: %1") ).tqarg(m_files[0].dst.directory) ); else if( m_mode == MOVE ) p->print( TQString( i18n("Files will be moved to: %1") ).tqarg(m_files[0].dst.directory) ); else if( m_mode == LINK ) p->print( TQString( i18n("Symbolic links will be created in: %1") ).tqarg(m_files[0].dst.directory) ); else if( m_mode == RENAME ) p->print( i18n("Input files will be renamed.") ); unsigned int i; for( i = 0; i < m_files.count(); i++) { p->setProgress( i+1 ); if( p->wasCancelled() ) break; KURL src = m_files[i].src.url; KURL dst = m_files[i].dst.url; dst.setPath( m_files[i].dst.name ); renamedFiles[i].src = src; renamedFiles[i].dst = dst; renamedFiles[i].dir = m_files[i].dir; FileOperation fop; if( !fop.start( src, dst, m_mode, overwrite ) ) { p->error( fop.error() ); renamedFiles[i].error = true; error++; continue; } else { renamedFiles[i].error = false; } // TODO: overwriting of files! /* * The renamed file should be on its correct location now, * so that we can call the last plugins (e.g. for changing permissions) * * Remember, the token argument is the filename for this type of plugins! * * If the return value is not empty an error has occured! * The plugin should return an error message in this case! */ TQString eplug = parsePlugins( i, TQString(), TYPE_FINAL_FILE ); if( !eplug.isEmpty() ) { p->error( eplug ); error++; } /* Create the undo script now */ if( undo ) if( dst.isLocalFile() && src.isLocalFile() ) { // Plugins ??? (*tundo) << "echo \"" << src.fileName() << " -> " << dst.fileName() << "\"" << endl; (*tundo) << "mv --force -b --suffix=.krename_ \"" << m_files[i].dst.name << "\" \"" << m_files[i].src.name << "\"" << endl; } else p->warning( TQString( i18n("Undo is not possible for remote file: %1") ).tqarg( dst.prettyURL() ) ); } if( !p->wasCancelled() ) { TQPtrListIterator it( plug->libs ); for( ; it.current(); ++it ) { if( (*it)->usePlugin ) (*it)->plugin->finished(); } } const TQString m = TQString( i18n("Renamed %1 files successfully.") ).tqarg(i-error); ( i - error ) ? p->print( m ) : p->warning( m ); if( error > 0 ) p->warning( TQString( i18n("%2 errors occurred!") ).tqarg(error)); p->print( TQString( i18n("Elapsed time: %1 seconds") ).tqarg( t.elapsed()/1000 ), "kalarm" ); p->print( i18n("KRename finished the renaming process."), "krename" ); p->print( i18n("Press close to quit!") ); p->setRenamedFiles( renamedFiles, m_files.count() ); if( undo ) { (*tundo) << endl << "echo \"Finished undoing " << m_files.count() << " actions.\"" << endl; delete tundo; fundo->close(); // Make fundo exuteable if( chmod( (const char*)m_undoScript, (unsigned int) S_IRUSR | S_IWUSR | S_IXUSR ) ) p->error( i18n("Can't set executable bit on undo script.") ); delete fundo; } p->done( error, i-error, m_mode == MOVE || m_mode == RENAME ); m_files.clear(); delete this; } void BatchRenamer::escape( TQString & text, const TQString & token, const TQString & sequence ) { text.replace( token, sequence ); } TQString & BatchRenamer::doEscape( TQString text, bool filename ) { if( filename ) { BatchRenamer::escape( text, "&", TQChar( 60000 ) ); BatchRenamer::escape( text, "$", TQChar( 60001 ) ); BatchRenamer::escape( text, "%", TQChar( 60002 ) ); BatchRenamer::escape( text, "#", TQChar( 60004 ) ); BatchRenamer::escape( text, "[", TQChar( 60005 ) ); BatchRenamer::escape( text, "]", TQChar( 60006 ) ); BatchRenamer::escape( text, "\\", TQChar( 60007 ) ); BatchRenamer::escape( text, "/", TQChar( 60008 ) ); BatchRenamer::escape( text, "{", TQChar( 60009 ) ); BatchRenamer::escape( text, "}", TQChar( 60010 ) ); BatchRenamer::escape( text, "*", TQChar( 60011 ) ); } else { BatchRenamer::escape( text, "\\&", TQChar( 60000 ) ); BatchRenamer::escape( text, "\\$", TQChar( 60001 ) ); BatchRenamer::escape( text, "\\%", TQChar( 60002 ) ); BatchRenamer::escape( text, "\\#", TQChar( 60004 ) ); BatchRenamer::escape( text, "\\[", TQChar( 60005 ) ); BatchRenamer::escape( text, "\\]", TQChar( 60006 ) ); BatchRenamer::escape( text, "\\\\", TQChar( 60007 ) ); BatchRenamer::escape( text, "\\/", TQChar( 60008 ) ); BatchRenamer::escape( text, "\\{", TQChar( 60009 ) ); BatchRenamer::escape( text, "\\}", TQChar( 60010 ) ); BatchRenamer::escape( text, "\\*", TQChar( 60011 ) ); } return text; } TQString & BatchRenamer::unEscape( TQString & text ) { BatchRenamer::escape( text, TQChar( 60000 ), "&" ); BatchRenamer::escape( text, TQChar( 60001 ), "$" ); BatchRenamer::escape( text, TQChar( 60002 ), "%" ); BatchRenamer::escape( text, TQChar( 60004 ), "#" ); BatchRenamer::escape( text, TQChar( 60005 ), "[" ); BatchRenamer::escape( text, TQChar( 60006 ), "]" ); BatchRenamer::escape( text, TQChar( 60007 ), "\\" ); // %252f == /, it seems that filenames on unix cannot contain // a /. So I use %252f, at least konqui displays it correctly // this was needed, so that plugins that return a slash do not cause errors BatchRenamer::escape( text, TQChar( 60008 ), "%2f" ); BatchRenamer::escape( text, TQChar( 60009 ), "{" ); BatchRenamer::escape( text, TQChar( 60010 ), "}" ); BatchRenamer::escape( text, TQChar( 60011 ), "*" ); return text; } int BatchRenamer::getCharacters( int n ) { TQString s; s.sprintf( "%i", n ); return s.length(); } TQString BatchRenamer::findAndProcess( const TQString & token, TQString text, const TQString & replace ) { /* * pos can here be -1 because * findRev is called with it as a * value ! */ text.replace( token, replace ); return text; } TQString BatchRenamer::findNumbers( TQString text, int count, int i ) { // Rewritten in Version 0.8 // Added numbers skipping in 1.3 // Changed again in Version 1.8 to optimize it and fix a bug with skipping numbers int pos = 0, counter = 1; tCounterValues countervalues; countervalues.start = m_index; countervalues.step = m_step; if( text.contains( "#", FALSE ) <= 0 ) return text; pos = text.find("#", pos); pos++; while( text[pos] == '#' ) { text.remove(pos, 1); counter++; } findNumberAppendix( text, pos, &countervalues.start, &countervalues.step ); pos = text.find("#", 0); if( (signed int)m_counters.count() <= m_counter_index ) { countervalues.value = countervalues.start - countervalues.step; // other wise the counter would start at: // start + step instead of start m_counters.append( countervalues ); } do { m_counters[m_counter_index].value += m_counters[m_counter_index].step; } while( m_skip.contains( m_counters[m_counter_index].value ) ); /* int v = start + (i*step) + m_skip_add[m_counter_index]; for( unsigned int z = 0; z < m_skip.count(); z++ ) { if( m_skip[z] == v ) { m_skip_add[m_counter_index] += step; v += step; } } */ TQString temp; temp.sprintf("%0*i", counter, m_counters[m_counter_index].value ); text.replace( pos, 1, temp); ++m_counter_index; return findNumbers( text, count, i ); } void BatchRenamer::findNumberAppendix( TQString & text, int pos, int* start, int* step ) { TQString appendix = TQString(); int tmp = 0; int end = 0; bool ok = false; if( text[pos] == '{' && (end = text.find( "}", pos )) > -1) { //qDebug("Found an appendix:" + appendix ); appendix = text.mid( pos + 1, end - pos - 1); text.remove( pos, end - pos + 1 ); tmp = appendix.section( ';', 0, 0 ).toInt( &ok ); // first section = start index if( ok ) *start = tmp; tmp = appendix.section( ';', 1, 1 ).toInt( &ok ); // second section = stepping if( ok ) *step = tmp; } } TQString BatchRenamer::findStar( const TQString & oldname, TQString text ) { int pos = -1; do { pos = text.findRev("*", pos); if( pos >= 0 ) { TQString tmp = oldname.lower(); if( tmp[0].isLetter() ) tmp[0] = tmp[0].upper(); for( unsigned int i = 0; i < tmp.length(); i++ ) if( tmp[i+1].isLetter() && !tmp[i].isLetter() && tmp[i] != '\'' && tmp[i] != '?' && tmp[i] != '`' ) tmp[i+1] = tmp[i+1].upper(); text.replace( pos, 1, tmp); } } while( pos >= 0 ); return text; } TQString BatchRenamer::findBrackets( TQString oldname, TQString text, int i ) { /* * looks for a statement in brackets [ ] * and calls findToken() with this statement. */ int num, pos = -1, a; TQString token; if( text.contains("]", FALSE) <= 0 || text.isEmpty() ) return text; num = text.contains("[", FALSE); if(num <= 0 ) return text; pos = text.findRev("[", pos); a = text.find("]", pos ); if( a < 0 && pos >= 0 ) return text; if( pos < 0 && a >= 0 ) return text; if( pos >= 0 && a >= 0 ) { token = text.mid( pos+1, (a-pos)-1 ); // support [4-[length]] token = findBrackets( oldname, token, i ); text.remove( pos, (a-pos)+1 ); text.insert( pos, findToken( oldname, token, i )); } return findBrackets( oldname, text, i ); } TQString BatchRenamer::processToken( TQString token, TQString oldname, int i ) { TQString tmp; /* * Call here all functions that handle * arguments in brackets. */ tmp = findPartStrings( oldname, token ); if( !tmp.isEmpty() ) return tmp; tmp = findDirName( token, m_files[i].src.directory ); if( !tmp.isEmpty() ) return tmp; tmp = findLength( token, m_files[i].src.name ); if( !tmp.isEmpty() ) return tmp; Plugin* p = plug->findPlugin( token ); if( p ) { tmp = p->processFile( this, i, token, TYPE_BRACKET ); if( !tmp.isNull() ) { doEscape( tmp ); return tmp; } } /* * Maybe I should remove this! * Krename simply ignores unknown tokens! * Usefull for the MP3 Plugin! */ return TQString(); } TQString BatchRenamer::findToken( TQString oldname, TQString token, int i ) { enum conversion { LOWER, UPPER, MIXED, STAR, STRIP, NONE, EMPTY, NUMBER }; unsigned int numwidth = 0; conversion c = EMPTY; if( !token.left(1).compare("$") ) c = NONE; else if( !token.left(1).compare("%") ) c = LOWER; else if( !token.left(1).compare("&") ) c = UPPER; else if( !token.left(1).compare("") ) c = MIXED; else if( !token.left(1).compare("*") ) c = STAR; else if( !token.left(1).compare("\\") ) c = STRIP; else if( !token.left(1).compare("#") ) { while( !token.left(1).compare("#") ) { token.remove( 0, 1 ); ++numwidth; } c = NUMBER; } if( c != EMPTY && c != NUMBER ) token.remove( 0, 1 ); TQString save = token; token = processToken( token, oldname, i ); switch( c ) { case LOWER: token = token.lower(); break; case UPPER: token = token.upper(); break; case MIXED: token = token.lower(); token.replace( 0, 1, token[0].upper()); break; case STAR: token = findStar( token, "*" ); break; case STRIP: token = token.stripWhiteSpace(); break; case NUMBER: { bool b = false; int n = token.toInt( &b ); if( b ) token = token.sprintf("%0*i", numwidth, n ); } break; default: break; } doEscape( token ); return token; } TQString BatchRenamer::findPartStrings( TQString oldname, TQString token ) { TQString first, second; int pos = -1; // parse things like [2;4{[dirname]}] if( token.contains( "{" ) >= 1 && token.contains( "}" ) >= 1 ) { int pos = token.find( "{" ); oldname = token.mid( pos + 1, token.findRev( "}" ) - pos - 1 ); token = token.left( pos ); } if( token.contains("-") ) { pos = token.find( "-", 0 ); first = token.left( pos ); // ------- Code OK ^ ! second = token.mid( pos+1, token.length() ); // version < 1.7 // return oldname.mid( first.toInt()-1, second.toInt()-first.toInt() +1 ); // version > 1.7 //return oldname.mid( first.toInt()-1, second.toInt()-first.toInt() ); // version > 1.8 bool ok; int sec = second.toInt( &ok ); if( !ok || sec == 0 ) sec = oldname.length(); /* * x should not be larger than the old name * and not smaller than zero. */ int x = sec-first.toInt(); if( x > (signed int)oldname.length() || x < 0 ) x = oldname.length()-first.toInt(); /* * if I would comment my code I would understand this line :) * without this line, there is sometimes the last letter * of a filename missing. */ if( x != -1 ) x++; return oldname.mid( first.toInt()-1, x ); } else if( token.contains(";") ) { pos = token.find( ";", 0 ); first = token.left( pos ); second = token.mid( pos+1, token.length() ); return oldname.mid( first.toInt()-1, second.toInt() ); } else { bool ok = false; int number = token.toInt( &ok ); if( ok && (number <= (signed int)oldname.length() && number > 0 ) ) return TQString(oldname[ number -1 ]); else return TQString(); } } TQString BatchRenamer::findDirName( TQString token, TQString path ) { if( token.left( 7 ).lower() == "dirname" ) { if( path.right( 1 ) == "/" ) path = path.left( path.length() - 1); int recursion = 1; if( token.length() > 7 ) { token = token.right( token.length() - 7 ); recursion = token.contains( "." ); if( recursion != (signed int)token.length() ) return TQString(); recursion++; } return path.section( "/", recursion * -1, recursion * -1); } return TQString(); } TQString BatchRenamer::findLength( const TQString & token, const TQString & name ) { if( token.lower().startsWith( "length" ) ) { int minus = 0; if( token[6] == '-' ) { bool n = false; minus = token.mid( 7, token.length() - 7 ).toInt( &n ); if( !n ) minus = 0; } return TQString::number( name.length() - minus ); } return TQString(); } TQString BatchRenamer::findReplace( TQString text ) { // Call for each element in replace strings doReplace with correct values for( unsigned int i = 0; i < m_replace.count(); i++ ) { replacestrings s = m_replace[i]; text = doReplace( text, unEscape( s.find ), s.replace, s.reg ); } return text; } TQString BatchRenamer::doReplace( TQString text, TQString find, TQString replace, bool reg ) { if( !reg ) { // we use the escaped text here because the user might want // to find a "&" and replace it text.replace( doEscape( find ), replace ); } else { // no doEscape() here for the regexp, because it would destroy our regular expression // other wise we will not find stuff like $, [ in the text text = doEscape( unEscape( text ).replace( TQRegExp( find ), replace ) ); } return text; } void BatchRenamer::writeUndoScript( TQTextStream* t ) { // write header comments (*t) << "#!/bin/bash" << endl << "# KRename Undo Script" << endl << "#" << endl << "# KRename was written by:" << endl << "# Dominik Seichter " << endl << "# http://krename.sourceforge.net" << endl << "#" << endl << "# Script generated by KRename Version: " << VERSION << endl << endl << "# This script must be started with the option --krename to work!" << endl; // write functions: (*t) << "echo \"KRename Undo Script\"" << endl << "echo \"http://krename.sourceforge.net\"" << endl << "echo \"\"" << endl; (*t) << "if test --krename = $1 ; then" << endl << " echo \"\"" << endl << "else" << endl << " echo \"You have to start this script\"" << endl << " echo \"with the command line option\"" << endl << " echo \"--krename\"" << endl << " echo \"to undo a rename operation.\"" << endl << " exit" << endl << "fi" << endl; } void BatchRenamer::parseSubdirs( data* f ) { int pos = 0; if( (pos = f->dst.name.findRev( "/", -1 ) ) > 0 ) { TQString dirs = f->dst.name.left( pos ); f->dst.name = f->dst.name.right( f->dst.name.length() - pos - 1 ); f->dst.directory += ( f->dst.directory.right( 1 ) == "/" ) ? "" : "/"; // create the missing subdir now int i = 0; TQString d = ""; while( (d = dirs.section( "/", i, i, TQString::SectionSkipEmpty )) && ! d.isEmpty() ) { // asignment here! KURL url = f->dst.url; // it is important to unescape here // to support dirnames containing "&" or // similar tokens url.addPath( unEscape( d ) ); if( !NetAccess::exists( url ) && !NetAccess::mkdir( url ) ) // TODO: GUI bug report qDebug("Can't create %s", url.prettyURL().latin1() ); f->dst.url.addPath( d ); f->dst.directory.append( d + "/" ); i++; } } } TQString BatchRenamer::buildFilename( fileentry* entry, bool dir ) { TQString filename = ( dir ? entry->directory : TQString() ) + entry->name + ( entry->extension.isEmpty() ? TQString() : TQString(".") ) + entry->extension; // unescape here as filename is still escaped unEscape( filename ); return filename; } bool BatchRenamer::applyManualChanges( int i ) { /* * The last step: make changes of * the user visible */ if( !m_changes.isEmpty() ) for( unsigned int z = 0; z < m_changes.count(); z++ ) { KURL file = m_changes[z].url; if( file == m_files[i].src.url ) { m_files[i].dst.name = m_changes[z].user; // the file extension is already included // in the users name m_files[i].dst.extension = TQString(); return true; } } return false; } void BatchRenamer::findCounterReset( int i ) { int z; if( m_files[i-1].src.directory != m_files[i].src.directory ) for( z=0;z<(int)m_counters.count();z++ ) { m_counters[z].value = m_counters[z].start - m_counters[z].step; } }