From caffe8b6f3f9d2177e88606026fe5b09a56d6434 Mon Sep 17 00:00:00 2001 From: Jean-Francois Dockes Date: Sun, 28 Apr 2013 09:48:40 +0200 Subject: [PATCH] added function to save multiple result selection in restable into files --- src/qtgui/multisave.cpp | 140 ++++++++++++++++++++++++++++++++++++++++ src/qtgui/multisave.h | 24 +++++++ src/qtgui/recoll.pro.in | 1 - src/qtgui/restable.cpp | 24 +++++++ src/qtgui/restable.h | 1 + src/utils/pathut.cpp | 58 +++++++++++++++-- src/utils/pathut.h | 86 ++++++++++++------------ 7 files changed, 288 insertions(+), 46 deletions(-) create mode 100644 src/qtgui/multisave.cpp create mode 100644 src/qtgui/multisave.h diff --git a/src/qtgui/multisave.cpp b/src/qtgui/multisave.cpp new file mode 100644 index 00000000..26af1ea6 --- /dev/null +++ b/src/qtgui/multisave.cpp @@ -0,0 +1,140 @@ +/* Copyright (C) 2005 J.F.Dockes + * 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. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include "autoconfig.h" + +#include + +#include +#include +#include +using namespace std; + +#include +#include +#include + +#include "recoll.h" +#include "multisave.h" +#include "smallut.h" +#include "debuglog.h" +#include "pathut.h" +#include "internfile.h" + +const unsigned int maxlen = 200; + +void multiSave(QWidget *p, vector& docs) +{ + QFileDialog fdialog(p, p->tr("Create or choose save directory")); + fdialog.setAcceptMode(QFileDialog::AcceptSave); + fdialog.setFileMode(QFileDialog::Directory); + fdialog.setOption(QFileDialog::ShowDirsOnly); + if (fdialog.exec() == 0) + return; + QStringList dirl = fdialog.selectedFiles(); + if (dirl.size() != 1) { + // Can't happen ? + QMessageBox::warning(0, "Recoll", + p->tr("Choose exactly one directory")); + return; + } + string dir((const char *)dirl[0].toLocal8Bit()); + LOGDEB2(("multiSave: got dir %s\n", dir.c_str())); + + /* Save doc to files in target directory. Issues: + - It is quite common to have docs in the array with the save + file names, e.g. all messages in a folder have the save file + name (the folder's). + - There is no warranty that the ipath is going to be acceptable + as a file name or interesting at all. We don't use it. + - We have to make sure the names don't end up too long. + + If collisions occur, we add a numeric infix (e.g. somefile.23.pdf). + + We never overwrite existing files and don't give the user an + option to do it (they can just as well save to an empty + directory and use the file manager to accomplish whatever they + want). + + We don't try hard to protect against race-conditions + though. The existing file names are read before beginning the + save sequence, and collisions appearing after this are handled + by aborting. There is a window between existence check and creation + because idoctofile does not use O_EXCL + */ + set existingNames; + string reason; + if (!readdir(dir, reason, existingNames)) { + QMessageBox::warning(0, "Recoll", + p->tr("Could not read directory: ") + + QString::fromLocal8Bit(reason.c_str())); + return; + } + + set toBeCreated; + vector filenames; + for (vector::iterator it = docs.begin(); it != docs.end(); it++) { + string utf8fn; + it->getmeta(Rcl::Doc::keyfn, &utf8fn); + string suffix = path_suffix(utf8fn); + if (suffix.empty() || suffix.size() > 10) { + suffix = theconfig->getSuffixFromMimeType(it->mimetype); + } + string simple = path_basename(utf8fn, suffix); + if (simple.empty()) + simple = "rclsave"; + if (simple.size() > maxlen) { + simple = simple.substr(0, maxlen); + } + for (int vers = 0; ; vers++) { + ostringstream ss; + ss << simple; + if (vers) + ss << "." << vers; + if (!suffix.empty()) + ss << suffix; + + string fn = + (const char *)QString::fromUtf8(ss.str().c_str()).toLocal8Bit(); + if (existingNames.find(fn) == existingNames.end() && + toBeCreated.find(fn) == toBeCreated.end()) { + toBeCreated.insert(fn); + filenames.push_back(fn); + break; + } + } + } + + for (unsigned int i = 0; i != docs.size(); i++) { + string fn = path_cat(dir, filenames[i]); + if (access(fn.c_str(), 0) == 0) { + QMessageBox::warning(0, "Recoll", + p->tr("Unexpected file name collision, " + "cancelling.")); + return; + } + // There is still a race condition here, should we care ? + TempFile temp;// not used + if (!FileInterner::idocToFile(temp, fn, theconfig, docs[i])) { + QMessageBox::warning(0, "Recoll", + p->tr("Cannot extract document: ") + + QString::fromLocal8Bit(docs[i].url.c_str()) + + " | " + + QString::fromLocal8Bit(docs[i].ipath.c_str()) + ); + } + } +} diff --git a/src/qtgui/multisave.h b/src/qtgui/multisave.h new file mode 100644 index 00000000..e8bb595e --- /dev/null +++ b/src/qtgui/multisave.h @@ -0,0 +1,24 @@ +/* + * 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. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#ifndef _MULTISAVE_W_H_INCLUDED_ +#define _MULTISAVE_W_H_INCLUDED_ + +#include + +extern void multiSave(QWidget *parent, vector& docs); + +#endif /* _MULTISAVE_W_H_INCLUDED_ */ diff --git a/src/qtgui/recoll.pro.in b/src/qtgui/recoll.pro.in index f12edb88..cfe73b7c 100644 --- a/src/qtgui/recoll.pro.in +++ b/src/qtgui/recoll.pro.in @@ -68,7 +68,6 @@ FORMS = \ firstidx.ui \ idxsched.ui \ listdialog.ui \ - multisave.ui \ ptrans.ui \ rclmain.ui \ restable.ui \ diff --git a/src/qtgui/restable.cpp b/src/qtgui/restable.cpp index 73fbefe0..d949f313 100644 --- a/src/qtgui/restable.cpp +++ b/src/qtgui/restable.cpp @@ -47,6 +47,7 @@ #include "indexer.h" #include "respopup.h" #include "rclmain_w.h" +#include "multisave.h" static const QKeySequence quitKeySeq("Ctrl+q"); static const QKeySequence closeKeySeq("Ctrl+w"); @@ -759,6 +760,8 @@ void ResTable::createPopupMenu(const QPoint& pos) QMenu *popup = ResultPopup::create(this, m_ismainres? ResultPopup::isMain : 0, m_model->getDocSource(), m_detaildoc); + popup->addAction(this->tr("Save selection to files"), + this, SLOT(menuSaveSelection())); popup->popup(mapToGlobal(pos)); } } @@ -780,6 +783,27 @@ void ResTable::menuSaveToFile() emit docSaveToFileClicked(m_detaildoc); } +void ResTable::menuSaveSelection() +{ + if (m_model == 0 || m_model->getDocSource().isNull()) + return; + + QModelIndexList indexl = tableView->selectionModel()->selectedRows(); + vector v; + for (int i = 0; i < indexl.size(); i++) { + Rcl::Doc doc; + if (m_model->getDocSource()->getDoc(indexl[i].row(), doc)) + v.push_back(doc); + } + if (v.size() == 0) { + return; + } else if (v.size() == 1) { + emit docSaveToFileClicked(v[0]); + } else { + multiSave(this, v); + } +} + void ResTable::menuPreviewParent() { if (m_detaildocnum >= 0 && m_model && diff --git a/src/qtgui/restable.h b/src/qtgui/restable.h index 7aa11b0f..e6032de6 100644 --- a/src/qtgui/restable.h +++ b/src/qtgui/restable.h @@ -134,6 +134,7 @@ public slots: virtual void createPopupMenu(const QPoint& pos); virtual void menuPreview(); virtual void menuSaveToFile(); + virtual void menuSaveSelection(); virtual void menuEdit(); virtual void menuCopyFN(); virtual void menuCopyURL(); diff --git a/src/utils/pathut.cpp b/src/utils/pathut.cpp index 8fb63d50..73345b02 100644 --- a/src/utils/pathut.cpp +++ b/src/utils/pathut.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -48,11 +49,10 @@ #include #include #include +#include #include -#ifndef NO_NAMESPACES -using std::string; -using std::stack; -#endif /* NO_NAMESPACES */ +#include +using namespace std; #include "pathut.h" #include "transcode.h" @@ -274,6 +274,14 @@ string path_basename(const string &s, const string &suff) return simple; } +string path_suffix(const string& s) +{ + string::size_type dotp = s.rfind('.'); + if (dotp == string::npos) + return string(); + return s.substr(dotp); +} + string path_home() { uid_t uid = getuid(); @@ -543,6 +551,48 @@ bool printableUrl(const string &fcharset, const string &in, string &out) return true; } +bool readdir(const string& dir, string& reason, set& entries) +{ + struct stat st; + int statret; + ostringstream msg; + DIR *d = 0; + statret = lstat(dir.c_str(), &st); + if (statret == -1) { + msg << "readdir: cant stat " << dir << " errno " << errno; + goto out; + } + if (!S_ISDIR(st.st_mode)) { + msg << "readdir: " << dir << " not a directory"; + goto out; + } + if (access(dir.c_str(), R_OK) < 0) { + msg << "readdir: no read access to " << dir; + goto out; + } + + d = opendir(dir.c_str()); + if (d == 0) { + msg << "readdir: cant opendir " << dir << ", errno " << errno; + goto out; + } + + struct dirent *ent; + while ((ent = readdir(d)) != 0) { + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) + continue; + entries.insert(ent->d_name); + } + +out: + if (d) + closedir(d); + reason = msg.str(); + if (reason.empty()) + return true; + return false; +} + // We do not want to mess with the pidfile content in the destructor: // the lock might still be in use in a child process. In fact as much // as we'd like to reset the pid inside the file when we're done, it diff --git a/src/utils/pathut.h b/src/utils/pathut.h index 2ef8ff3a..82f0d45f 100644 --- a/src/utils/pathut.h +++ b/src/utils/pathut.h @@ -20,79 +20,83 @@ #include #include +#include + #include "refcntr.h" -#ifndef NO_NAMESPACES -using std::string; -using std::vector; -#endif - /// Add a / at the end if none there yet. -extern void path_catslash(string &s); +extern void path_catslash(std::string &s); /// Concatenate 2 paths -extern string path_cat(const string &s1, const string &s2); +extern std::string path_cat(const std::string &s1, const std::string &s2); /// Get the simple file name (get rid of any directory path prefix -extern string path_getsimple(const string &s); +extern std::string path_getsimple(const std::string &s); /// Simple file name + optional suffix stripping -extern string path_basename(const string &s, const string &suff=string()); +extern std::string path_basename(const std::string &s, + const std::string &suff = std::string()); +/// Component after last '.' +extern std::string path_suffix(const std::string &s); /// Get the father directory -extern string path_getfather(const string &s); +extern std::string path_getfather(const std::string &s); /// Get the current user's home directory -extern string path_home(); -/// Expand ~ at the beginning of string -extern string path_tildexpand(const string &s); +extern std::string path_home(); +/// Expand ~ at the beginning of std::string +extern std::string path_tildexpand(const std::string &s); /// Use getcwd() to make absolute path if needed. Beware: ***this can fail*** /// we return an empty path in this case. -extern string path_absolute(const string &s); +extern std::string path_absolute(const std::string &s); /// Clean up path by removing duplicated / and resolving ../ + make it absolute -extern string path_canon(const string &s); +extern std::string path_canon(const std::string &s); /// Use glob(3) to return the file names matching pattern inside dir -extern vector path_dirglob(const string &dir, - const string pattern); +extern std::vector path_dirglob(const std::string &dir, + const std::string pattern); /// Encode according to rfc 1738 -extern string url_encode(const string& url, - string::size_type offs = 0); +extern std::string url_encode(const std::string& url, + std::string::size_type offs = 0); /// Transcode to utf-8 if possible or url encoding, for display. -extern bool printableUrl(const string &fcharset, - const string &in, string &out); +extern bool printableUrl(const std::string &fcharset, + const std::string &in, std::string &out); //// Convert to file path if url is like file://. This modifies the //// input (and returns a copy for convenience) -extern string fileurltolocalpath(string url); +extern std::string fileurltolocalpath(std::string url); /// Test for file:/// url -extern bool urlisfileurl(const string& url); +extern bool urlisfileurl(const std::string& url); /// Return the host+path part of an url. This is not a general /// routine, it does the right thing only in the recoll context -extern string url_gpath(const string& url); +extern std::string url_gpath(const std::string& url); /// Stat parameter and check if it's a directory -extern bool path_isdir(const string& path); +extern bool path_isdir(const std::string& path); + +/// Dump directory +extern bool readdir(const std::string& dir, std::string& reason, + std::set& entries); /** A small wrapper around statfs et al, to return percentage of disk occupation */ -bool fsocc(const string &path, int *pc, // Percent occupied +bool fsocc(const std::string &path, int *pc, // Percent occupied long *avmbs = 0 // Mbs available to non-superuser ); /// Retrieve the temp dir location: $RECOLL_TMPDIR else $TMPDIR else /tmp -extern const string& tmplocation(); +extern const std::string& tmplocation(); /// Create temporary directory (inside the temp location) -extern bool maketmpdir(string& tdir, string& reason); +extern bool maketmpdir(std::string& tdir, std::string& reason); /// mkdir -p -extern bool makepath(const string& path); +extern bool makepath(const std::string& path); /// Temporary file class class TempFileInternal { public: - TempFileInternal(const string& suffix); + TempFileInternal(const std::string& suffix); ~TempFileInternal(); const char *filename() { return m_filename.c_str(); } - const string &getreason() + const std::string &getreason() { return m_reason; } @@ -105,8 +109,8 @@ public: return !m_filename.empty(); } private: - string m_filename; - string m_reason; + std::string m_filename; + std::string m_reason; bool m_noremove; }; @@ -118,13 +122,13 @@ public: TempDir(); ~TempDir(); const char *dirname() {return m_dirname.c_str();} - const string &getreason() {return m_reason;} + const std::string &getreason() {return m_reason;} bool ok() {return !m_dirname.empty();} /// Recursively delete contents but not self. bool wipe(); private: - string m_dirname; - string m_reason; + std::string m_dirname; + std::string m_reason; TempDir(const TempDir &) {} TempDir& operator=(const TempDir &) {return *this;}; }; @@ -134,7 +138,7 @@ private: /// the freebsd code if it was available elsewhere class Pidfile { public: - Pidfile(const string& path) : m_path(path), m_fd(-1) {} + Pidfile(const std::string& path) : m_path(path), m_fd(-1) {} ~Pidfile(); /// Open/create the pid file. /// @return 0 if ok, > 0 for pid of existing process, -1 for other error. @@ -146,11 +150,11 @@ public: int close(); /// Delete the pid file int remove(); - const string& getreason() {return m_reason;} + const std::string& getreason() {return m_reason;} private: - string m_path; + std::string m_path; int m_fd; - string m_reason; + std::string m_reason; pid_t read_pid(); int flopen(); }; @@ -160,7 +164,7 @@ private: // Freedesktop thumbnail standard path routine // On return, path will have the appropriate value in all cases, // returns true if the file already exists -extern bool thumbPathForUrl(const string& url, int size, string& path); +extern bool thumbPathForUrl(const std::string& url, int size, std::string& path); // Must be called in main thread before starting other threads extern void pathut_init_mt();