diff --git a/filemanager.pro b/filemanager.pro new file mode 100644 index 0000000..65d9262 --- /dev/null +++ b/filemanager.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS = src diff --git a/rpm/nemo-qml-plugin-filemanager.spec b/rpm/nemo-qml-plugin-filemanager.spec new file mode 100644 index 0000000..897f738 --- /dev/null +++ b/rpm/nemo-qml-plugin-filemanager.spec @@ -0,0 +1,34 @@ +Name: nemo-qml-plugin-filemanager +Summary: File manager plugin for Nemo Mobile +Version: 0.0.0 +Release: 1 +Group: System/Libraries +License: BSD +URL: https://git.merproject.org/mer-core/nemo-qml-plugin-filemanager +Source0: %{name}-%{version}.tar.bz2 +BuildRequires: pkgconfig(Qt5Core) +BuildRequires: pkgconfig(Qt5Gui) +BuildRequires: pkgconfig(Qt5Qml) +BuildRequires: pkgconfig(Qt5DBus) +BuildRequires: pkgconfig(Qt5Test) + +%description +%{summary}. + +%prep +%setup -q -n %{name}-%{version} + +%build + +%qmake5 + +make %{?_smp_mflags} + +%install +rm -rf %{buildroot} +%qmake5_install + +%files +%defattr(-,root,root,-) +%{_libdir}/qt5/qml/Nemo/FileManager/libnemofilemanager.so +%{_libdir}/qt5/qml/Nemo/FileManager/qmldir diff --git a/src/consolemodel.cpp b/src/consolemodel.cpp new file mode 100644 index 0000000..b26e885 --- /dev/null +++ b/src/consolemodel.cpp @@ -0,0 +1,114 @@ +#include "consolemodel.h" +#include "globals.h" + +enum { + ModelDataRole = Qt::UserRole + 1 +}; + +ConsoleModel::ConsoleModel(QObject *parent) : + QAbstractListModel(parent), m_process(0) +{ +} + +ConsoleModel::~ConsoleModel() +{ +} + +int ConsoleModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_lines.count(); +} + +QVariant ConsoleModel::data(const QModelIndex &index, int role) const +{ + Q_UNUSED(role); + if (!index.isValid() || index.row() > m_lines.count()-1) + return QVariant(); + + QString line = m_lines.at(index.row()); + return line; +} + +QHash ConsoleModel::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(ModelDataRole, QByteArray("modelData")); + return roles; +} + +void ConsoleModel::setLines(QStringList lines) +{ + if (m_lines == lines) + return; + + beginResetModel(); + m_lines = lines; + endResetModel(); + + emit linesChanged(); +} + +void ConsoleModel::setLines(QString lines) +{ + beginResetModel(); + m_lines = lines.split(QRegExp("[\n\r]")); + endResetModel(); + emit linesChanged(); +} + +void ConsoleModel::appendLine(QString line) +{ + beginInsertRows(QModelIndex(), m_lines.count(), m_lines.count()); + m_lines.append(line); + endInsertRows(); +} + +bool ConsoleModel::executeCommand(QString command, QStringList arguments) +{ + // don't execute the command if an old command is still running + if (m_process && m_process->state() != QProcess::NotRunning) { + // if the old process doesn't stop in 1/2 secs, then don't run the new command + if (!m_process->waitForFinished(500)) + return false; + } + setLines(QStringList()); + m_process = new QProcess(this); + m_process->setReadChannel(QProcess::StandardOutput); + m_process->setProcessChannelMode(QProcess::MergedChannels); // merged stderr channel with stdout channel + connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcessChannels())); + connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleProcessFinish(int, QProcess::ExitStatus))); + connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(handleProcessError(QProcess::ProcessError))); + m_process->start(command, arguments); + // the process is killed when ConsoleModel is destroyed (usually when Page is closed) + // should we run the process in bg thread to allow the command to finish(?) + + return true; +} + +void ConsoleModel::readProcessChannels() +{ + while (m_process->canReadLine()) { + QString line = m_process->readLine(); + appendLine(line); + } +} + +void ConsoleModel::handleProcessFinish(int exitCode, QProcess::ExitStatus status) +{ + if (status == QProcess::CrashExit) { // if it crashed, then use some error exit code + exitCode = -99999; + appendLine(tr("** crashed")); + + } else if (exitCode != 0) { + appendLine(tr("** error: %1").arg(exitCode)); + } + emit processExited(exitCode); +} + +void ConsoleModel::handleProcessError(QProcess::ProcessError error) +{ + Q_UNUSED(error); + emit processExited(-88888); // if error, then use some error exit code + appendLine(tr("** error")); +} diff --git a/src/consolemodel.h b/src/consolemodel.h new file mode 100644 index 0000000..44074d9 --- /dev/null +++ b/src/consolemodel.h @@ -0,0 +1,48 @@ +#ifndef CONSOLEMODEL_H +#define CONSOLEMODEL_H + +#include +#include +#include + +/** + * @brief The ConsoleModel class holds a list of strings for a QML list model. + */ +class ConsoleModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QStringList lines READ lines() WRITE setLines(QString) NOTIFY linesChanged()) + +public: + explicit ConsoleModel(QObject *parent = 0); + ~ConsoleModel(); + + // methods needed by ListView + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + QHash roleNames() const; + + // property accessors + QStringList lines() const { return m_lines; } + void setLines(QStringList lines); + void setLines(QString lines); + + void appendLine(QString line); + + Q_INVOKABLE bool executeCommand(QString command, QStringList arguments); + +signals: + void linesChanged(); + void processExited(int exitCode); + +private slots: + void readProcessChannels(); + void handleProcessFinish(int exitCode, QProcess::ExitStatus status); + void handleProcessError(QProcess::ProcessError error); + +private: + QProcess *m_process; + QStringList m_lines; +}; + +#endif // CONSOLEMODEL_H diff --git a/src/engine.cpp b/src/engine.cpp new file mode 100644 index 0000000..e17f0df --- /dev/null +++ b/src/engine.cpp @@ -0,0 +1,440 @@ +#include "engine.h" +#include +#include +#include +#include +#include +#include "globals.h" +#include "fileworker.h" +#include "statfileinfo.h" + +Q_GLOBAL_STATIC(Engine, engine); + +Engine::Engine(QObject *parent) : + QObject(parent), + m_clipboardContainsCopy(false), + m_progress(0) +{ + m_fileWorker = new FileWorker; + + // update progress property when worker progresses + connect(m_fileWorker, SIGNAL(progressChanged(int, QString)), + this, SLOT(setProgress(int, QString))); + + // pass worker end signals to QML + connect(m_fileWorker, SIGNAL(done()), this, SIGNAL(workerDone())); + connect(m_fileWorker, SIGNAL(errorOccurred(QString, QString)), + this, SIGNAL(workerErrorOccurred(QString, QString))); + connect(m_fileWorker, SIGNAL(fileDeleted(QString)), this, SIGNAL(fileDeleted(QString))); +} + +Engine::~Engine() +{ + // is this the way to force stop the worker thread? + m_fileWorker->cancel(); // stop possibly running background thread + m_fileWorker->wait(); // wait until thread stops + delete m_fileWorker; // delete it +} + +Engine *Engine::instance() +{ + return engine(); +} + +void Engine::deleteFiles(QStringList filenames) +{ + setProgress(0, ""); + m_fileWorker->startDeleteFiles(filenames); +} + +void Engine::cutFiles(QStringList filenames) +{ + m_clipboardFiles = filenames; + m_clipboardContainsCopy = false; + emit clipboardCountChanged(); + emit clipboardContainsCopyChanged(); +} + +void Engine::copyFiles(QStringList filenames) +{ + // don't copy special files (chr/blk/fifo/sock) + QMutableStringListIterator i(filenames); + while (i.hasNext()) { + QString filename = i.next(); + StatFileInfo info(filename); + if (info.isSystem()) + i.remove(); + } + + m_clipboardFiles = filenames; + m_clipboardContainsCopy = true; + emit clipboardCountChanged(); + emit clipboardContainsCopyChanged(); +} + +void Engine::pasteFiles(QString destDirectory) +{ + if (m_clipboardFiles.isEmpty()) { + emit workerErrorOccurred(tr("No files to paste"), ""); + return; + } + + QStringList files = m_clipboardFiles; + setProgress(0, ""); + + QDir dest(destDirectory); + if (!dest.exists()) { + emit workerErrorOccurred(tr("Destination does not exist"), destDirectory); + return; + } + + foreach (QString filename, files) { + QFileInfo fileInfo(filename); + QString newname = dest.absoluteFilePath(fileInfo.fileName()); + + // source and dest filenames are the same? + if (filename == newname) { + emit workerErrorOccurred(tr("Can't overwrite itself"), newname); + return; + } + + // dest is under source? (directory) + if (newname.startsWith(filename)) { + emit workerErrorOccurred(tr("Can't move/copy to itself"), filename); + return; + } + } + + m_clipboardFiles.clear(); + emit clipboardCountChanged(); + + if (m_clipboardContainsCopy) { + m_fileWorker->startCopyFiles(files, destDirectory); + return; + } + + m_fileWorker->startMoveFiles(files, destDirectory); +} + +void Engine::cancel() +{ + m_fileWorker->cancel(); +} + +QString Engine::homeFolder() const +{ + return QStandardPaths::writableLocation(QStandardPaths::HomeLocation); +} + +QString Engine::sdcardPath() const +{ + // get sdcard dir candidates + QDir dir("/media/sdcard"); + if (!dir.exists()) + return QString(); + dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); + QStringList sdcards = dir.entryList(); + if (sdcards.isEmpty()) + return QString(); + + // remove all directories which are not mount points + QStringList mps = mountPoints(); + QMutableStringListIterator i(sdcards); + while (i.hasNext()) { + QString dirname = i.next(); + QString abspath = dir.absoluteFilePath(dirname); + if (!mps.contains(abspath)) + i.remove(); + } + + // none found, return empty string + if (sdcards.isEmpty()) + return QString(); + + // if only one directory, then return it + if (sdcards.count() == 1) + return dir.absoluteFilePath(sdcards.first()); + + // if multiple directories, then return "/media/sdcard", which is the parent for them + return "/media/sdcard"; +} + +QString Engine::androidSdcardPath() const +{ + return QStandardPaths::writableLocation(QStandardPaths::HomeLocation)+"/android_storage"; +} + +bool Engine::exists(QString filename) +{ + if (filename.isEmpty()) + return false; + + return QFile::exists(filename); +} + +QStringList Engine::diskSpace(QString path) +{ + if (path.isEmpty()) + return QStringList(); + + // return no disk space for sdcard parent directory + if (path == "/media/sdcard") + return QStringList(); + + // run df for the given path to get disk space + QString blockSize = "--block-size=1024"; + QString result = execute("/bin/df", QStringList() << blockSize << path, false); + if (result.isEmpty()) + return QStringList(); + + // split result to lines + QStringList lines = result.split(QRegExp("[\n\r]")); + if (lines.count() < 2) + return QStringList(); + + // get first line and its columns + QString line = lines.at(1); + QStringList columns = line.split(QRegExp("\\s+"), QString::SkipEmptyParts); + if (columns.count() < 5) + return QStringList(); + + QString totalString = columns.at(1); + QString usedString = columns.at(2); + QString percentageString = columns.at(4); + qint64 total = totalString.toLongLong() * 1024LL; + qint64 used = usedString.toLongLong() * 1024LL; + + return QStringList() << percentageString << filesizeToString(used)+"/"+filesizeToString(total); +} + +QStringList Engine::readFile(QString filename) +{ + int maxLines = 1000; + int maxSize = 10240; + int maxBinSize = 2048; + + // check existence + StatFileInfo fileInfo(filename); + if (!fileInfo.exists()) { + if (!fileInfo.isSymLink()) + return makeStringList(tr("File does not exist") + "\n" + filename); + else + return makeStringList(tr("Broken symbolic link") + "\n" + filename); + } + + // don't read unsafe system files + if (!fileInfo.isSafeToRead()) { + return makeStringList(tr("Can't read this type of file") + "\n" + filename); + } + + // check permissions + if (access(filename, R_OK) == -1) + return makeStringList(tr("No permission to read the file") + "\n" + filename); + + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) + return makeStringList(tr("Error reading file") + "\n" + filename); + + // read start of file + char buffer[maxSize+1]; + qint64 readSize = file.read(buffer, maxSize); + if (readSize < 0) + return makeStringList(tr("Error reading file") + "\n" + filename); + + if (readSize == 0) + return makeStringList(tr("Empty file")); + + bool atEnd = file.atEnd(); + file.close(); + + // detect binary or text file, it is binary if it contains zeros + bool isText = true; + for (int i = 0; i < readSize; ++i) { + if (buffer[i] == 0) { + isText = false; + break; + } + } + + // binary output + if (!isText) { + // two different line widths + if (readSize > maxBinSize) { + readSize = maxBinSize; + atEnd = false; + } + QString out8 = createHexDump(buffer, readSize, 8); + QString out16 = createHexDump(buffer, readSize, 16); + QString msg = ""; + + if (!atEnd) { + msg = tr("--- Binary file preview clipped at %1 kB ---").arg(maxBinSize/1024); + msg = tr("--- Binary file preview clipped at %1 kB ---").arg(maxBinSize/1024); + } + + return QStringList() << msg << out8 << out16; + } + + // read lines to a string list and join + QByteArray ba(buffer, readSize); + QTextStream in(&ba); + QStringList lines; + int lineCount = 0; + while (!in.atEnd() && lineCount < maxLines) { + QString line = in.readLine(); + lines.append(line); + lineCount++; + } + + QString msg = ""; + if (lineCount == maxLines) + msg = tr("--- Text file preview clipped at %1 lines ---").arg(maxLines); + else if (!atEnd) + msg = tr("--- Text file preview clipped at %1 kB ---").arg(maxSize/1024); + + return makeStringList(msg, lines.join("\n")); +} + +QString Engine::mkdir(QString path, QString name) +{ + QDir dir(path); + + if (!dir.mkdir(name)) { + if (access(dir.absolutePath(), W_OK) == -1) + return tr("No permissions to create %1").arg(name); + + return tr("Cannot create folder %1").arg(name); + } + + return QString(); +} + +QStringList Engine::rename(QString fullOldFilename, QString newName) +{ + QFile file(fullOldFilename); + QFileInfo fileInfo(fullOldFilename); + QDir dir = fileInfo.absoluteDir(); + QString fullNewFilename = dir.absoluteFilePath(newName); + + QString errorMessage; + if (!file.rename(fullNewFilename)) { + QString oldName = fileInfo.fileName(); + errorMessage = tr("Cannot rename %1").arg(oldName) + "\n" + file.errorString(); + } + + return QStringList() << fullNewFilename << errorMessage; +} + +QString Engine::chmod(QString path, + bool ownerRead, bool ownerWrite, bool ownerExecute, + bool groupRead, bool groupWrite, bool groupExecute, + bool othersRead, bool othersWrite, bool othersExecute) +{ + QFile file(path); + QFileDevice::Permissions p; + if (ownerRead) p |= QFileDevice::ReadOwner; + if (ownerWrite) p |= QFileDevice::WriteOwner; + if (ownerExecute) p |= QFileDevice::ExeOwner; + if (groupRead) p |= QFileDevice::ReadGroup; + if (groupWrite) p |= QFileDevice::WriteGroup; + if (groupExecute) p |= QFileDevice::ExeGroup; + if (othersRead) p |= QFileDevice::ReadOther; + if (othersWrite) p |= QFileDevice::WriteOther; + if (othersExecute) p |= QFileDevice::ExeOther; + if (!file.setPermissions(p)) + return tr("Cannot change permissions") + "\n" + file.errorString(); + + return QString(); +} + +QString Engine::readSetting(QString key, QString defaultValue) +{ + QSettings settings; + return settings.value(key, defaultValue).toString(); +} + +void Engine::writeSetting(QString key, QString value) +{ + QSettings settings; + + // do nothing if value didn't change + if (settings.value(key) == value) + return; + + settings.setValue(key, value); + + emit settingsChanged(); +} + +void Engine::setProgress(int progress, QString filename) +{ + m_progress = progress; + m_progressFilename = filename; + emit progressChanged(); + emit progressFilenameChanged(); +} + +QStringList Engine::mountPoints() const +{ + // read /proc/mounts and return all mount points for the filesystem + QFile file("/proc/mounts"); + if (!file.open(QFile::ReadOnly | QFile::Text)) + return QStringList(); + + QTextStream in(&file); + QString result = in.readAll(); + + // split result to lines + QStringList lines = result.split(QRegExp("[\n\r]")); + + // get columns + QStringList dirs; + foreach (QString line, lines) { + QStringList columns = line.split(QRegExp("\\s+"), QString::SkipEmptyParts); + if (columns.count() < 6) // sanity check + continue; + + QString dir = columns.at(1); + dirs.append(dir); + } + + return dirs; +} + +QString Engine::createHexDump(char *buffer, int size, int bytesPerLine) +{ + QString out; + QString ascDump; + int i; + for (i = 0; i < size; ++i) { + if ((i % bytesPerLine) == 0) { // line change + out += " "+ascDump+"\n"+ + QString("%1").arg(QString::number(i, 16), 4, QLatin1Char('0'))+": "; + ascDump.clear(); + } + + out += QString("%1").arg(QString::number((unsigned char)buffer[i], 16), + 2, QLatin1Char('0'))+" "; + if (buffer[i] >= 32 && buffer[i] <= 126) + ascDump += buffer[i]; + else + ascDump += "."; + } + // write out remaining asc dump + if ((i % bytesPerLine) > 0) { + int emptyBytes = bytesPerLine - (i % bytesPerLine); + for (int j = 0; j < emptyBytes; ++j) { + out += " "; + } + } + out += " "+ascDump; + + return out; +} + +QStringList Engine::makeStringList(QString msg, QString str) +{ + QStringList list; + list << msg << str << str; + return list; +} diff --git a/src/engine.h b/src/engine.h new file mode 100644 index 0000000..b8e4558 --- /dev/null +++ b/src/engine.h @@ -0,0 +1,94 @@ +#ifndef ENGINE_H +#define ENGINE_H + +#include +#include + +class FileWorker; + +/** + * @brief Engine to handle file operations, settings and other generic functionality. + */ +class Engine : public QObject +{ + Q_OBJECT + Q_PROPERTY(int clipboardCount READ clipboardCount() NOTIFY clipboardCountChanged()) + Q_PROPERTY(int clipboardContainsCopy READ clipboardContainsCopy() NOTIFY clipboardContainsCopyChanged()) + Q_PROPERTY(int progress READ progress() NOTIFY progressChanged()) + Q_PROPERTY(QString progressFilename READ progressFilename() NOTIFY progressFilenameChanged()) + +public: + explicit Engine(QObject *parent = 0); + ~Engine(); + + // properties + int clipboardCount() const { return m_clipboardFiles.count(); } + bool clipboardContainsCopy() const { return m_clipboardContainsCopy; } + int progress() const { return m_progress; } + QString progressFilename() const { return m_progressFilename; } + + // For C++ + static Engine *instance(); + + // methods accessible from QML + + // asynch methods send signals when done or error occurs + Q_INVOKABLE void deleteFiles(QStringList filenames); + Q_INVOKABLE void cutFiles(QStringList filenames); + Q_INVOKABLE void copyFiles(QStringList filenames); + Q_INVOKABLE void pasteFiles(QString destDirectory); + + // cancel asynch methods + Q_INVOKABLE void cancel(); + + // returns error msg + Q_INVOKABLE QString errorMessage() const { return m_errorMessage; } + + // file paths + Q_INVOKABLE QString homeFolder() const; + Q_INVOKABLE QString sdcardPath() const; + Q_INVOKABLE QString androidSdcardPath() const; + + // synchronous methods + Q_INVOKABLE bool exists(QString filename); + Q_INVOKABLE QStringList diskSpace(QString path); + Q_INVOKABLE QStringList readFile(QString filename); + Q_INVOKABLE QString mkdir(QString path, QString name); + Q_INVOKABLE QStringList rename(QString fullOldFilename, QString newName); + Q_INVOKABLE QString chmod(QString path, + bool ownerRead, bool ownerWrite, bool ownerExecute, + bool groupRead, bool groupWrite, bool groupExecute, + bool othersRead, bool othersWrite, bool othersExecute); + + // access settings + Q_INVOKABLE QString readSetting(QString key, QString defaultValue = QString()); + Q_INVOKABLE void writeSetting(QString key, QString value); + +signals: + void clipboardCountChanged(); + void clipboardContainsCopyChanged(); + void progressChanged(); + void progressFilenameChanged(); + void workerDone(); + void workerErrorOccurred(QString message, QString filename); + void fileDeleted(QString fullname); + + void settingsChanged(); + +private slots: + void setProgress(int progress, QString filename); + +private: + QStringList mountPoints() const; + QString createHexDump(char *buffer, int size, int bytesPerLine); + QStringList makeStringList(QString msg, QString str = QString()); + + QStringList m_clipboardFiles; + bool m_clipboardContainsCopy; + int m_progress; + QString m_progressFilename; + QString m_errorMessage; + FileWorker *m_fileWorker; +}; + +#endif // ENGINE_H diff --git a/src/filedata.cpp b/src/filedata.cpp new file mode 100644 index 0000000..01d02b3 --- /dev/null +++ b/src/filedata.cpp @@ -0,0 +1,212 @@ +#include "filedata.h" +#include +#include +#include +#include +#include "globals.h" + +FileData::FileData(QObject *parent) : + QObject(parent) +{ + m_file = ""; +} + +FileData::~FileData() +{ +} + +void FileData::setFile(QString file) +{ + if (m_file == file) + return; + + m_file = file; + readInfo(); +} + +QString FileData::icon() const +{ + return infoToIconName(m_fileInfo); +} + +QString FileData::permissions() const +{ + return permissionsToString(m_fileInfo.permissions()); +} + +QString FileData::owner() const +{ + QString owner = m_fileInfo.owner(); + if (owner.isEmpty()) { + uint id = m_fileInfo.ownerId(); + if (id != (uint)-2) + owner = QString::number(id); + } + return owner; +} + +QString FileData::group() const +{ + QString group = m_fileInfo.group(); + if (group.isEmpty()) { + uint id = m_fileInfo.groupId(); + if (id != (uint)-2) + group = QString::number(id); + } + return group; +} + +QString FileData::size() const +{ + if (m_fileInfo.isDirAtEnd()) return "-"; + return m_fileInfo.size(); +} + +QString FileData::modified() const +{ + return datetimeToString(m_fileInfo.lastModified()); +} + +QString FileData::created() const +{ + return datetimeToString(m_fileInfo.created()); +} + +QString FileData::absolutePath() const +{ + if (m_file.isEmpty()) + return QString(); + return m_fileInfo.absolutePath(); +} + +void FileData::refresh() +{ + readInfo(); +} + +bool FileData::mimeTypeInherits(QString parentMimeType) +{ + return m_mimeType.inherits(parentMimeType); +} + +void FileData::readInfo() +{ + m_errorMessage = ""; + m_metaData.clear(); + + m_fileInfo.setFile(m_file); + + // exists() checks for target existence in symlinks, so ignore it for symlinks + if (!m_fileInfo.exists() && !m_fileInfo.isSymLink()) + m_errorMessage = tr("File does not exist"); + + readMetaData(); + + emit fileChanged(); + emit isDirChanged(); + emit isSymLinkChanged(); + emit kindChanged(); + emit iconChanged(); + emit permissionsChanged(); + emit ownerChanged(); + emit groupChanged(); + emit sizeChanged(); + emit modifiedChanged(); + emit createdChanged(); + emit absolutePathChanged(); + emit nameChanged(); + emit suffixChanged(); + emit symLinkTargetChanged(); + emit isSymLinkBrokenChanged(); + emit metaDataChanged(); + emit mimeTypeChanged(); + emit mimeTypeCommentChanged(); + emit errorMessageChanged(); +} + +void FileData::readMetaData() +{ + // special file types + // do not sniff mimetype or metadata for these, because these can't really be read + + m_mimeType = QMimeType(); + if (m_fileInfo.isBlkAtEnd()) { + m_mimeTypeName = "inode/blockdevice"; + m_mimeTypeComment = tr("block device"); + return; + } else if (m_fileInfo.isChrAtEnd()) { + m_mimeTypeName = "inode/chardevice"; + m_mimeTypeComment = tr("character device"); + return; + } else if (m_fileInfo.isFifoAtEnd()) { + m_mimeTypeName = "inode/fifo"; + m_mimeTypeComment = tr("pipe"); + return; + } else if (m_fileInfo.isSocketAtEnd()) { + m_mimeTypeName = "inode/socket"; + m_mimeTypeComment = tr("socket"); + return; + } else if (m_fileInfo.isDirAtEnd()) { + m_mimeTypeName = "inode/directory"; + m_mimeTypeComment = tr("folder"); + return; + } + if (!m_fileInfo.isFileAtEnd()) { // something strange + m_mimeTypeName = "application/octet-stream"; + m_mimeTypeComment = tr("unknown"); + return; + } + + // normal files - match content to find mimetype, which means that the file is read + + QMimeDatabase db; + QString filename = m_fileInfo.isSymLink() ? m_fileInfo.symLinkTarget() : + m_fileInfo.absoluteFilePath(); + m_mimeType = db.mimeTypeForFile(filename); + m_mimeTypeName = m_mimeType.name(); + m_mimeTypeComment = m_mimeType.comment(); + + // read metadata for images + // store in m_metaData, first char is priority, then label:value + if (m_mimeType.name() == "image/jpeg" || m_mimeType.name() == "image/png" || + m_mimeType.name() == "image/gif") { + + // read size + QImageReader reader(m_file); + QSize s = reader.size(); + if (s.width() >= 0 && s.height() >= 0) { + QString ar = calculateAspectRatio(s.width(), s.height()); + m_metaData.append("0" + tr("Image Size") + + QString(":%1 x %2 %3").arg(s.width()).arg(s.height()).arg(ar)); + } + + // read comments + QStringList textKeys = reader.textKeys(); + foreach (QString key, textKeys) { + QString value = reader.text(key); + m_metaData.append("9"+key+":"+value); + } + } +} + +const int aspectWidths[] = { 16, 4, 3, 5, 5, -1 }; +const int aspectHeights[] = { 9, 3, 2, 3, 4, -1 }; + +QString FileData::calculateAspectRatio(int width, int height) const +{ + // Jolla Camera almost 16:9 aspect ratio + if ((width == 3264 && height == 1840) || (height == 1840 && width == 3264)) { + return QString("(16:9)"); + } + + int i = 0; + while (aspectWidths[i] != -1) { + if (width * aspectWidths[i] == height * aspectHeights[i] || + height * aspectWidths[i] == width * aspectHeights[i]) { + return QString("(%1:%2)").arg(aspectWidths[i]).arg(aspectHeights[i]); + } + ++i; + } + return QString(); +} + diff --git a/src/filedata.h b/src/filedata.h new file mode 100644 index 0000000..778e1f2 --- /dev/null +++ b/src/filedata.h @@ -0,0 +1,108 @@ +#ifndef FILEDATA_H +#define FILEDATA_H + +#include +#include +#include +#include +#include +#include "statfileinfo.h" + +/** + * @brief The FileData class provides info about one file. + */ +class FileData : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString file READ file() WRITE setFile(QString) NOTIFY fileChanged()) + Q_PROPERTY(bool isDir READ isDir() NOTIFY isDirChanged()) + Q_PROPERTY(bool isSymLink READ isSymLink() NOTIFY isSymLinkChanged()) + Q_PROPERTY(QString kind READ kind() NOTIFY kindChanged()) + Q_PROPERTY(QString icon READ icon() NOTIFY iconChanged()) + Q_PROPERTY(QString permissions READ permissions() NOTIFY permissionsChanged()) + Q_PROPERTY(QString owner READ owner() NOTIFY ownerChanged()) + Q_PROPERTY(QString group READ group() NOTIFY groupChanged()) + Q_PROPERTY(QString size READ size() NOTIFY sizeChanged()) + Q_PROPERTY(QString modified READ modified() NOTIFY modifiedChanged()) + Q_PROPERTY(QString created READ created() NOTIFY createdChanged()) + Q_PROPERTY(QString absolutePath READ absolutePath() NOTIFY absolutePathChanged()) + Q_PROPERTY(QString name READ name() NOTIFY nameChanged()) + Q_PROPERTY(QString suffix READ suffix() NOTIFY suffixChanged()) + Q_PROPERTY(QString symLinkTarget READ symLinkTarget() NOTIFY symLinkTargetChanged()) + Q_PROPERTY(bool isSymLinkBroken READ isSymLinkBroken() NOTIFY isSymLinkBrokenChanged()) + Q_PROPERTY(QString mimeType READ mimeType() NOTIFY mimeTypeChanged()) + Q_PROPERTY(QString mimeTypeComment READ mimeTypeComment() NOTIFY mimeTypeCommentChanged()) + Q_PROPERTY(QStringList metaData READ metaData() NOTIFY metaDataChanged()) + Q_PROPERTY(QString errorMessage READ errorMessage() NOTIFY errorMessageChanged()) + +public: + explicit FileData(QObject *parent = 0); + ~FileData(); + + // property accessors + QString file() const { return m_file; } + void setFile(QString file); + + bool isDir() const { return m_fileInfo.isDirAtEnd(); } + bool isSymLink() const { return m_fileInfo.isSymLink(); } + QString kind() const { return m_fileInfo.kind(); } + QString icon() const; + QString permissions() const; + QString owner() const; + QString group() const; + QString size() const; + QString modified() const; + QString created() const; + QString absolutePath() const; + QString name() const { return m_fileInfo.fileName(); } + QString suffix() const { return m_fileInfo.suffix().toLower(); } + QString symLinkTarget() const { return m_fileInfo.symLinkTarget(); } + bool isSymLinkBroken() const { return m_fileInfo.isSymLinkBroken(); } + QString mimeType() const { return m_mimeTypeName; } + QString mimeTypeComment() const { return m_mimeTypeComment; } + QStringList metaData() const { return m_metaData; } + QString errorMessage() const { return m_errorMessage; } + + // methods accessible from QML + Q_INVOKABLE void refresh(); + Q_INVOKABLE bool mimeTypeInherits(QString parentMimeType); + Q_INVOKABLE bool isSafeToOpen() const { return m_fileInfo.isSafeToRead(); } + +signals: + void fileChanged(); + void isDirChanged(); + void isSymLinkChanged(); + void kindChanged(); + void iconChanged(); + void permissionsChanged(); + void ownerChanged(); + void groupChanged(); + void sizeChanged(); + void modifiedChanged(); + void createdChanged(); + void nameChanged(); + void suffixChanged(); + void absolutePathChanged(); + void symLinkTargetChanged(); + void isSymLinkBrokenChanged(); + void metaDataChanged(); + void mimeTypeChanged(); + void mimeTypeCommentChanged(); + void errorMessageChanged(); + +private: + void readInfo(); + void readMetaData(); + QString calculateAspectRatio(int width, int height) const; + QStringList readExifData(QString filename); + + QString m_file; + StatFileInfo m_fileInfo; + QMimeType m_mimeType; + QString m_mimeTypeName; + QString m_mimeTypeComment; + QStringList m_metaData; + QString m_errorMessage; +}; + +#endif // FILEDATA_H diff --git a/src/filemodel.cpp b/src/filemodel.cpp new file mode 100644 index 0000000..e0953c8 --- /dev/null +++ b/src/filemodel.cpp @@ -0,0 +1,424 @@ +#include "filemodel.h" +#include +#include +#include +#include +#include "engine.h" +#include "globals.h" +#include + +enum { + FilenameRole = Qt::UserRole + 1, + FileKindRole = Qt::UserRole + 2, + FileIconRole = Qt::UserRole + 3, + PermissionsRole = Qt::UserRole + 4, + SizeRole = Qt::UserRole + 5, + LastModifiedRole = Qt::UserRole + 6, + CreatedRole = Qt::UserRole + 7, + IsDirRole = Qt::UserRole + 8, + IsLinkRole = Qt::UserRole + 9, + SymLinkTargetRole = Qt::UserRole + 10, + IsSelectedRole = Qt::UserRole + 11 +}; + +FileModel::FileModel(QObject *parent) : + QAbstractListModel(parent), + m_selectedFileCount(0), + m_active(false), + m_dirty(false) +{ + m_dir = ""; + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(refresh())); + connect(m_watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(refresh())); + + // refresh model every time settings are changed + Engine *engine = Engine::instance(); + connect(engine, SIGNAL(settingsChanged()), this, SLOT(refreshFull())); +} + +FileModel::~FileModel() +{ +} + +int FileModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_files.count(); +} + +QVariant FileModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() > m_files.size()-1) + return QVariant(); + + StatFileInfo info = m_files.at(index.row()); + switch (role) { + + case Qt::DisplayRole: + case FilenameRole: + return info.fileName(); + + case FileKindRole: + return info.kind(); + + case FileIconRole: + return infoToIconName(info); + + case PermissionsRole: + return permissionsToString(info.permissions()); + + case SizeRole: + if (info.isSymLink() && info.isDirAtEnd()) return tr("dir-link"); + if (info.isDir()) return tr("dir"); + return filesizeToString(info.size()); + + case LastModifiedRole: + return info.lastModified(); + + case CreatedRole: + return info.created(); + + case IsDirRole: + return info.isDirAtEnd(); + + case IsLinkRole: + return info.isSymLink(); + + case SymLinkTargetRole: + return info.symLinkTarget(); + + case IsSelectedRole: + return info.isSelected(); + + default: + return QVariant(); + } +} + +QHash FileModel::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(FilenameRole, QByteArray("filename")); + roles.insert(FileKindRole, QByteArray("filekind")); + roles.insert(FileIconRole, QByteArray("fileIcon")); + roles.insert(PermissionsRole, QByteArray("permissions")); + roles.insert(SizeRole, QByteArray("size")); + roles.insert(LastModifiedRole, QByteArray("modified")); + roles.insert(CreatedRole, QByteArray("created")); + roles.insert(IsDirRole, QByteArray("isDir")); + roles.insert(IsLinkRole, QByteArray("isLink")); + roles.insert(SymLinkTargetRole, QByteArray("symLinkTarget")); + roles.insert(IsSelectedRole, QByteArray("isSelected")); + return roles; +} + +int FileModel::fileCount() const +{ + return m_files.count(); +} + +QString FileModel::errorMessage() const +{ + return m_errorMessage; +} + +void FileModel::setDir(QString dir) +{ + if (m_dir == dir) + return; + + // update watcher to watch the new directory + if (!m_dir.isEmpty()) + m_watcher->removePath(m_dir); + + if (!dir.isEmpty()) + m_watcher->addPath(dir); + + m_dir = dir; + + readDirectory(); + + m_dirty = false; + + emit dirChanged(); +} + +QString FileModel::appendPath(QString dirName) +{ + return QDir::cleanPath(QDir(m_dir).absoluteFilePath(dirName)); +} + +void FileModel::setActive(bool active) +{ + if (m_active == active) + return; + + m_active = active; + emit activeChanged(); + + if (m_dirty) + readDirectory(); + + m_dirty = false; +} + +QString FileModel::parentPath() +{ + return QDir::cleanPath(QDir(m_dir).absoluteFilePath("..")); +} + +QString FileModel::fileNameAt(int fileIndex) +{ + if (fileIndex < 0 || fileIndex >= m_files.count()) + return QString(); + + return m_files.at(fileIndex).absoluteFilePath(); +} + +void FileModel::toggleSelectedFile(int fileIndex) +{ + if (!m_files.at(fileIndex).isSelected()) { + StatFileInfo info = m_files.at(fileIndex); + info.setSelected(true); + m_files[fileIndex] = info; + m_selectedFileCount++; + } else { + StatFileInfo info = m_files.at(fileIndex); + info.setSelected(false); + m_files[fileIndex] = info; + m_selectedFileCount--; + } + // emit signal for views + QModelIndex topLeft = index(fileIndex, 0); + QModelIndex bottomRight = index(fileIndex, 0); + emit dataChanged(topLeft, bottomRight); + + emit selectedFileCountChanged(); +} + +void FileModel::clearSelectedFiles() +{ + QMutableListIterator iter(m_files); + int row = 0; + while (iter.hasNext()) { + StatFileInfo &info = iter.next(); + info.setSelected(false); + // emit signal for views + QModelIndex topLeft = index(row, 0); + QModelIndex bottomRight = index(row, 0); + emit dataChanged(topLeft, bottomRight); + row++; + } + m_selectedFileCount = 0; + emit selectedFileCountChanged(); +} + +void FileModel::selectAllFiles() +{ + QMutableListIterator iter(m_files); + int row = 0; + while (iter.hasNext()) { + StatFileInfo &info = iter.next(); + info.setSelected(true); + // emit signal for views + QModelIndex topLeft = index(row, 0); + QModelIndex bottomRight = index(row, 0); + emit dataChanged(topLeft, bottomRight); + row++; + } + m_selectedFileCount = m_files.count(); + emit selectedFileCountChanged(); +} + +QStringList FileModel::selectedFiles() const +{ + if (m_selectedFileCount == 0) + return QStringList(); + + QStringList filenames; + foreach (const StatFileInfo &info, m_files) { + if (info.isSelected()) + filenames.append(info.absoluteFilePath()); + } + return filenames; +} + +void FileModel::refresh() +{ + if (!m_active) { + m_dirty = true; + return; + } + + refreshEntries(); + m_dirty = false; +} + +void FileModel::refreshFull() +{ + if (!m_active) { + m_dirty = true; + return; + } + + readDirectory(); + m_dirty = false; +} + +void FileModel::readDirectory() +{ + // wrapped in reset model methods to get views notified + beginResetModel(); + + m_files.clear(); + m_errorMessage = ""; + + if (!m_dir.isEmpty()) + readAllEntries(); + + endResetModel(); + emit fileCountChanged(); + emit errorMessageChanged(); + recountSelectedFiles(); +} + +void FileModel::recountSelectedFiles() +{ + int count = 0; + foreach (const StatFileInfo &info, m_files) { + if (info.isSelected()) + count++; + } + if (m_selectedFileCount != count) { + m_selectedFileCount = count; + emit selectedFileCountChanged(); + } +} + +void FileModel::readAllEntries() +{ + QDir dir(m_dir); + if (!dir.exists()) { + m_errorMessage = tr("Folder does not exist"); + return; + } + + if (access(m_dir, R_OK) == -1) { + m_errorMessage = tr("No permission to read the folder"); + return; + } + + QSettings settings; + bool hiddenSetting = settings.value("show-hidden-files", false).toBool(); + QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0; + dir.setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | QDir::System | hidden); + + if (settings.value("show-dirs-first", false).toBool()) + dir.setSorting(QDir::Name | QDir::DirsFirst); + + QStringList fileList = dir.entryList(); + foreach (QString filename, fileList) { + QString fullpath = dir.absoluteFilePath(filename); + StatFileInfo info(fullpath); + m_files.append(info); + } +} + +void FileModel::refreshEntries() +{ + m_errorMessage = ""; + + // empty dir name + if (m_dir.isEmpty()) { + clearModel(); + emit errorMessageChanged(); + return; + } + + QDir dir(m_dir); + if (!dir.exists()) { + clearModel(); + m_errorMessage = tr("Folder does not exist"); + emit errorMessageChanged(); + return; + } + + if (access(m_dir, R_OK) == -1) { + clearModel(); + m_errorMessage = tr("No permission to read the folder"); + emit errorMessageChanged(); + return; + } + + QSettings settings; + bool hiddenSetting = settings.value("show-hidden-files", false).toBool(); + QDir::Filter hidden = hiddenSetting ? QDir::Hidden : (QDir::Filter)0; + dir.setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot | QDir::System | hidden); + + if (settings.value("show-dirs-first", false).toBool()) + dir.setSorting(QDir::Name | QDir::DirsFirst); + + // read all files + QList newFiles; + + QStringList fileList = dir.entryList(); + foreach (QString filename, fileList) { + QString fullpath = dir.absoluteFilePath(filename); + StatFileInfo info(fullpath); + newFiles.append(info); + } + + int oldFileCount = m_files.count(); + + // compare old and new files and do removes if needed + for (int i = m_files.count()-1; i >= 0; --i) { + StatFileInfo data = m_files.at(i); + if (!filesContains(newFiles, data)) { + beginRemoveRows(QModelIndex(), i, i); + m_files.removeAt(i); + endRemoveRows(); + } + } + + // compare old and new files and do inserts if needed + for (int i = 0; i < newFiles.count(); ++i) { + StatFileInfo data = newFiles.at(i); + if (!filesContains(m_files, data)) { + beginInsertRows(QModelIndex(), i, i); + m_files.insert(i, data); + endInsertRows(); + } + } + + if (m_files.count() != oldFileCount) + emit fileCountChanged(); + + emit errorMessageChanged(); + + recountSelectedFiles(); +} + +void FileModel::clearModel() +{ + beginResetModel(); + m_files.clear(); + endResetModel(); + emit fileCountChanged(); +} + +bool FileModel::filesContains(const QList &files, const StatFileInfo &fileData) const +{ + // check if list contains fileData with relevant info + foreach (const StatFileInfo &f, files) { + if (f.fileName() == fileData.fileName() && + f.size() == fileData.size() && + f.permissions() == fileData.permissions() && + f.lastModified() == fileData.lastModified() && + f.isSymLink() == fileData.isSymLink() && + f.isDirAtEnd() == fileData.isDirAtEnd()) { + return true; + } + } + return false; +} diff --git a/src/filemodel.h b/src/filemodel.h new file mode 100644 index 0000000..98f0718 --- /dev/null +++ b/src/filemodel.h @@ -0,0 +1,89 @@ +#ifndef FILEMODEL_H +#define FILEMODEL_H + +#include +#include +#include +#include "statfileinfo.h" + +/** + * @brief The FileModel class can be used as a model in a ListView to display a list of files + * in the current directory. It has methods to change the current directory and to access + * file info. + * It also actively monitors the directory. If the directory changes, then the model is + * updated automatically if active is true. If active is false, then the directory is + * updated when active becomes true. + */ +class FileModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString dir READ dir() WRITE setDir(QString) NOTIFY dirChanged()) + Q_PROPERTY(int fileCount READ fileCount() NOTIFY fileCountChanged()) + Q_PROPERTY(QString errorMessage READ errorMessage() NOTIFY errorMessageChanged()) + Q_PROPERTY(bool active READ active() WRITE setActive(bool) NOTIFY activeChanged()) + Q_PROPERTY(int selectedFileCount READ selectedFileCount() NOTIFY selectedFileCountChanged()) + +public: + explicit FileModel(QObject *parent = 0); + ~FileModel(); + + // methods needed by ListView + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + QHash roleNames() const; + + // property accessors + QString dir() const { return m_dir; } + void setDir(QString dir); + int fileCount() const; + QString errorMessage() const; + bool active() const { return m_active; } + void setActive(bool active); + int selectedFileCount() const { return m_selectedFileCount; } + + // methods accessible from QML + Q_INVOKABLE QString appendPath(QString dirName); + Q_INVOKABLE QString parentPath(); + Q_INVOKABLE QString fileNameAt(int fileIndex); + + // file selection + Q_INVOKABLE void toggleSelectedFile(int fileIndex); + Q_INVOKABLE void clearSelectedFiles(); + Q_INVOKABLE void selectAllFiles(); + Q_INVOKABLE QStringList selectedFiles() const; + +public slots: + // reads the directory and inserts/removes model items as needed + Q_INVOKABLE void refresh(); + // reads the directory and sets all model items + Q_INVOKABLE void refreshFull(); + +signals: + void dirChanged(); + void fileCountChanged(); + void errorMessageChanged(); + void activeChanged(); + void selectedFileCountChanged(); + +private slots: + void readDirectory(); + +private: + void recountSelectedFiles(); + void readAllEntries(); + void refreshEntries(); + void clearModel(); + bool filesContains(const QList &files, const StatFileInfo &fileData) const; + + QString m_dir; + QList m_files; + int m_selectedFileCount; + QString m_errorMessage; + bool m_active; + bool m_dirty; + QFileSystemWatcher *m_watcher; +}; + + + +#endif // FILEMODEL_H diff --git a/src/fileworker.cpp b/src/fileworker.cpp new file mode 100644 index 0000000..643fcee --- /dev/null +++ b/src/fileworker.cpp @@ -0,0 +1,304 @@ +#include "fileworker.h" +#include +#include "globals.h" + +FileWorker::FileWorker(QObject *parent) : + QThread(parent), + m_mode(DeleteMode), + m_cancelled(KeepRunning), + m_progress(0) +{ +} + +FileWorker::~FileWorker() +{ +} + +void FileWorker::startDeleteFiles(QStringList filenames) +{ + if (isRunning()) { + emit errorOccurred(tr("File operation already in progress"), ""); + return; + } + + if (!validateFilenames(filenames)) + return; + + m_mode = DeleteMode; + m_filenames = filenames; + m_cancelled.storeRelease(KeepRunning); + start(); +} + +void FileWorker::startCopyFiles(QStringList filenames, QString destDirectory) +{ + if (isRunning()) { + emit errorOccurred(tr("File operation already in progress"), ""); + return; + } + + if (!validateFilenames(filenames)) + return; + + m_mode = CopyMode; + m_filenames = filenames; + m_destDirectory = destDirectory; + m_cancelled.storeRelease(KeepRunning); + start(); +} + +void FileWorker::startMoveFiles(QStringList filenames, QString destDirectory) +{ + if (isRunning()) { + emit errorOccurred(tr("File operation already in progress"), ""); + return; + } + + if (!validateFilenames(filenames)) + return; + + m_mode = MoveMode; + m_filenames = filenames; + m_destDirectory = destDirectory; + m_cancelled.storeRelease(KeepRunning); + start(); +} + +void FileWorker::cancel() +{ + m_cancelled.storeRelease(Cancelled); +} + +void FileWorker::run() +{ + switch (m_mode) { + case DeleteMode: + deleteFiles(); + break; + + case MoveMode: + case CopyMode: + copyOrMoveFiles(); + break; + } +} + +bool FileWorker::validateFilenames(const QStringList &filenames) +{ + // basic validity check + foreach (QString filename, filenames) { + if (filename.isEmpty()) { + emit errorOccurred(tr("Empty filename"), ""); + return false; + } + } + return true; +} + +QString FileWorker::deleteFile(QString filename) +{ + QFileInfo info(filename); + if (!info.exists() && !info.isSymLink()) + return tr("File not found"); + + if (info.isDir() && info.isSymLink()) { + // only delete the link and do not remove recursively subfolders + QFile file(info.absoluteFilePath()); + bool ok = file.remove(); + if (!ok) + return file.errorString(); + + } else if (info.isDir()) { + // this should be custom function to get better error reporting + bool ok = QDir(info.absoluteFilePath()).removeRecursively(); + if (!ok) + return tr("Folder delete failed"); + + } else { + QFile file(info.absoluteFilePath()); + bool ok = file.remove(); + if (!ok) + return file.errorString(); + } + return QString(); +} + +void FileWorker::deleteFiles() +{ + int fileIndex = 0; + int fileCount = m_filenames.count(); + + foreach (QString filename, m_filenames) { + m_progress = 100 * fileIndex / fileCount; + emit progressChanged(m_progress, filename); + + // stop if cancelled + if (m_cancelled.loadAcquire() == Cancelled) { + emit errorOccurred(tr("Cancelled"), filename); + return; + } + + // delete file and stop if errors + QString errMsg = deleteFile(filename); + if (!errMsg.isEmpty()) { + emit errorOccurred(errMsg, filename); + return; + } + emit fileDeleted(filename); + + fileIndex++; + } + + m_progress = 100; + emit progressChanged(m_progress, ""); + emit done(); +} + +void FileWorker::copyOrMoveFiles() +{ + int fileIndex = 0; + int fileCount = m_filenames.count(); + + QDir dest(m_destDirectory); + foreach (QString filename, m_filenames) { + m_progress = 100 * fileIndex / fileCount; + emit progressChanged(m_progress, filename); + + // stop if cancelled + if (m_cancelled.loadAcquire() == Cancelled) { + emit errorOccurred(tr("Cancelled"), filename); + return; + } + + QFileInfo fileInfo(filename); + QString newname = dest.absoluteFilePath(fileInfo.fileName()); + + // move or copy and stop if errors + QFile file(filename); + if (m_mode == MoveMode) { + if (fileInfo.isSymLink()) { + // move symlink by creating a new link and deleting the old one + QFile targetFile(fileInfo.symLinkTarget()); + if (!targetFile.link(newname)) { + emit errorOccurred(targetFile.errorString(), filename); + return; + } + if (!file.remove()) { + emit errorOccurred(targetFile.errorString(), filename); + return; + } + + } else if (!file.rename(newname)) { + emit errorOccurred(file.errorString(), filename); + return; + } + + } else { // CopyMode + if (fileInfo.isDir()) { + QString errmsg = copyDirRecursively(filename, newname); + if (!errmsg.isEmpty()) { + emit errorOccurred(errmsg, filename); + return; + } + } else { + QString errmsg = copyOverwrite(filename, newname); + if (!errmsg.isEmpty()) { + emit errorOccurred(errmsg, filename); + return; + } + } + } + + fileIndex++; + } + + m_progress = 100; + emit progressChanged(m_progress, ""); + emit done(); +} + +QString FileWorker::copyDirRecursively(QString srcDirectory, QString destDirectory) +{ + QFileInfo srcInfo(srcDirectory); + if (srcInfo.isSymLink()) { + // copy dir symlink by creating a new link + QFile targetFile(srcInfo.symLinkTarget()); + if (!targetFile.link(destDirectory)) + return targetFile.errorString(); + + return QString(); + } + + QDir srcDir(srcDirectory); + if (!srcDir.exists()) + return tr("Source folder doesn't exist"); + + QDir destDir(destDirectory); + if (!destDir.exists()) { + QDir d(destDir); + d.cdUp(); + if (!d.mkdir(destDir.dirName())) + return tr("Can't create target folder %1").arg(destDirectory); + } + + // copy files + QStringList names = srcDir.entryList(QDir::Files); + for (int i = 0 ; i < names.count() ; ++i) { + // stop if cancelled + if (m_cancelled.loadAcquire() == Cancelled) + return tr("Cancelled"); + + QString filename = names.at(i); + emit progressChanged(m_progress, filename); + QString spath = srcDir.absoluteFilePath(filename); + QString dpath = destDir.absoluteFilePath(filename); + QString errmsg = copyOverwrite(spath, dpath); + if (!errmsg.isEmpty()) + return errmsg; + } + + // copy dirs + names = srcDir.entryList(QDir::NoDotAndDotDot | QDir::AllDirs); + for (int i = 0 ; i < names.count() ; ++i) { + // stop if cancelled + if (m_cancelled.loadAcquire() == Cancelled) + return tr("Cancelled"); + + QString filename = names.at(i); + emit progressChanged(m_progress, filename); + QString spath = srcDir.absoluteFilePath(filename); + QString dpath = destDir.absoluteFilePath(filename); + QString errmsg = copyDirRecursively(spath, dpath); + if (!errmsg.isEmpty()) + return errmsg; + } + + return QString(); +} + +QString FileWorker::copyOverwrite(QString src, QString dest) +{ + // delete destination if it exists + QFile dfile(dest); + if (dfile.exists()) { + if (!dfile.remove()) + return dfile.errorString(); + } + + QFileInfo fileInfo(src); + if (fileInfo.isSymLink()) { + // copy symlink by creating a new link + QFile targetFile(fileInfo.symLinkTarget()); + if (!targetFile.link(dest)) + return targetFile.errorString(); + + return QString(); + } + + // normal file copy + QFile sfile(src); + if (!sfile.copy(dest)) + return sfile.errorString(); + + return QString(); +} diff --git a/src/fileworker.h b/src/fileworker.h new file mode 100644 index 0000000..f88f5ce --- /dev/null +++ b/src/fileworker.h @@ -0,0 +1,60 @@ +#ifndef FILEWORKER_H +#define FILEWORKER_H + +#include +#include + +/** + * @brief FileWorker does delete, copy and move files in the background. + */ +class FileWorker : public QThread +{ + Q_OBJECT + +public: + explicit FileWorker(QObject *parent = 0); + ~FileWorker(); + + // call these to start the thread, returns false if start failed + void startDeleteFiles(QStringList filenames); + void startCopyFiles(QStringList filenames, QString destDirectory); + void startMoveFiles(QStringList filenames, QString destDirectory); + + void cancel(); + +signals: // signals, can be connected from a thread to another + void progressChanged(int progress, QString filename); + + // one of these is emitted when thread ends + void done(); + void errorOccurred(QString message, QString filename); + + void fileDeleted(QString fullname); + +protected: + void run(); + +private: + enum Mode { + DeleteMode, CopyMode, MoveMode + }; + enum CancelStatus { + Cancelled = 0, KeepRunning = 1 + }; + + bool validateFilenames(const QStringList &filenames); + + QString deleteFile(QString filenames); + void deleteFiles(); + void copyOrMoveFiles(); + QString copyDirRecursively(QString srcDirectory, QString destDirectory); + QString copyOverwrite(QString src, QString dest); + + FileWorker::Mode m_mode; + QStringList m_filenames; + QString m_destDirectory; + QAtomicInt m_cancelled; // atomic so no locks needed + int m_progress; +}; + +#endif // FILEWORKER_H diff --git a/src/globals.cpp b/src/globals.cpp new file mode 100644 index 0000000..7c7c67f --- /dev/null +++ b/src/globals.cpp @@ -0,0 +1,104 @@ +#include "globals.h" +#include +#include +#include +#include + +QString suffixToIconName(QString suffix) +{ + // only formats that are understood by File Browser or Sailfish get a special icon + if (suffix == "txt") + return "file-txt"; + if (suffix == "rpm") + return "file-rpm"; + if (suffix == "apk") + return "file-apk"; + if (suffix == "png" || suffix == "jpeg" || suffix == "jpg" || + suffix == "gif") + return "file-image"; + if (suffix == "wav" || suffix == "mp3" || suffix == "flac" || + suffix == "aac" || suffix == "ogg" || suffix == "m4a") + return "file-audio"; + if (suffix == "mp4" || suffix == "m4v") + return "file-video"; + + return "file"; +} + +QString permissionsToString(QFile::Permissions permissions) +{ + char str[] = "---------"; + if (permissions & 0x4000) str[0] = 'r'; + if (permissions & 0x2000) str[1] = 'w'; + if (permissions & 0x1000) str[2] = 'x'; + if (permissions & 0x0040) str[3] = 'r'; + if (permissions & 0x0020) str[4] = 'w'; + if (permissions & 0x0010) str[5] = 'x'; + if (permissions & 0x0004) str[6] = 'r'; + if (permissions & 0x0002) str[7] = 'w'; + if (permissions & 0x0001) str[8] = 'x'; + return QString::fromLatin1(str); +} + +QString filesizeToString(qint64 filesize) +{ + // convert to kB, MB, GB: use 1000 instead of 1024 as divisor because it seems to be + // the usual way to display file size (like on Ubuntu) + QLocale locale; + if (filesize < 1000LL) + return QObject::tr("%1 bytes").arg(locale.toString(filesize)); + + if (filesize < 1000000LL) + return QObject::tr("%1 kB").arg(locale.toString((double)filesize/1000.0, 'f', 2)); + + if (filesize < 1000000000LL) + return QObject::tr("%1 MB").arg(locale.toString((double)filesize/1000000.0, 'f', 2)); + + return QObject::tr("%1 GB").arg(locale.toString((double)filesize/1000000000.0, 'f', 2)); +} + +QString datetimeToString(QDateTime datetime) +{ + QLocale locale; + + // return time for today or date for older + if (datetime.date() == QDate::currentDate()) + return locale.toString(datetime.time(), QLocale::NarrowFormat); + + return locale.toString(datetime.date(), QLocale::NarrowFormat); +} + +QString infoToIconName(const StatFileInfo &info) +{ + if (info.isSymLink() && info.isDirAtEnd()) return "folder-link"; + if (info.isDir()) return "folder"; + if (info.isSymLink()) return "link"; + if (info.isFileAtEnd()) { + QString suffix = info.suffix().toLower(); + return suffixToIconName(suffix); + } + return "file"; +} + +int access(QString filename, int how) +{ + QByteArray fab = filename.toUtf8(); + char *fn = fab.data(); + return access(fn, how); +} + +QString execute(QString command, QStringList arguments, bool mergeErrorStream) +{ + QProcess process; + process.setReadChannel(QProcess::StandardOutput); + if (mergeErrorStream) + process.setProcessChannelMode(QProcess::MergedChannels); + process.start(command, arguments); + if (!process.waitForStarted()) + return QString(); + if (!process.waitForFinished()) + return QString(); + + QByteArray result = process.readAll(); + return QString::fromUtf8(result); +} diff --git a/src/globals.h b/src/globals.h new file mode 100644 index 0000000..17d25f5 --- /dev/null +++ b/src/globals.h @@ -0,0 +1,22 @@ +#ifndef GLOBALS_H +#define GLOBALS_H + +#include +#include +#include +#include "statfileinfo.h" + +// Global functions + +QString suffixToIconName(QString suffix); +QString permissionsToString(QFile::Permissions permissions); +QString filesizeToString(qint64 filesize); +QString datetimeToString(QDateTime datetime); + +QString infoToIconName(const StatFileInfo &info); + +int access(QString filename, int how); + +QString execute(QString command, QStringList arguments, bool mergeErrorStream); + +#endif // GLOBALS_H diff --git a/src/plugin.cpp b/src/plugin.cpp new file mode 100644 index 0000000..c05fb53 --- /dev/null +++ b/src/plugin.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 Jolla Ltd. + * Contact: Joona Petrell + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Jolla Ltd. nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#include + +#include +#include +#include + +#include "filemodel.h" +#include "filedata.h" +#include "engine.h" +#include "consolemodel.h" + +static QObject *engine_api_factory(QQmlEngine *, QJSEngine *) +{ + return Engine::instance(); +} + +class Q_DECL_EXPORT NemoFileManagerPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.nemomobile.FileManager") + +public: + virtual ~NemoFileManagerPlugin() { } + + void initializeEngine(QQmlEngine *engine, const char *uri) + { + Q_ASSERT(uri == QLatin1String("Nemo.FileManager")); + } + + void registerTypes(const char *uri) + { + Q_ASSERT(uri == QLatin1String("Nemo.FileManager")); + qmlRegisterType(uri, 1, 0, "FileModel"); + qmlRegisterType(uri, 1, 0, "FileData"); + qmlRegisterType(uri, 1, 0, "ConsoleModel"); + qmlRegisterSingletonType(uri, 1, 0, "FileEngine", engine_api_factory); + } +}; + +#include "plugin.moc" diff --git a/src/qmldir b/src/qmldir new file mode 100644 index 0000000..1a1656c --- /dev/null +++ b/src/qmldir @@ -0,0 +1,2 @@ +module Nemo.FileManager +plugin nemofilemanager diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..c17ab7a --- /dev/null +++ b/src/src.pro @@ -0,0 +1,21 @@ +TARGET = nemofilemanager +PLUGIN_IMPORT_PATH = Nemo/FileManager + +TEMPLATE = lib +CONFIG += qt plugin hide_symbols c++11 +QT += qml + +target.path = $$[QT_INSTALL_QML]/$$PLUGIN_IMPORT_PATH +INSTALLS += target + +qmldir.files += $$_PRO_FILE_PWD_/qmldir +qmldir.path += $$target.path +INSTALLS += qmldir + +SOURCES += plugin.cpp filemodel.cpp filedata.cpp engine.cpp fileworker.cpp \ + consolemodel.cpp statfileinfo.cpp globals.cpp + +HEADERS += filemodel.h filedata.h engine.h fileworker.h \ + consolemodel.h statfileinfo.cpp globals.h + +INCLUDEPATH += $$PWD diff --git a/src/statfileinfo.cpp b/src/statfileinfo.cpp new file mode 100644 index 0000000..927a489 --- /dev/null +++ b/src/statfileinfo.cpp @@ -0,0 +1,95 @@ +#include "statfileinfo.h" + +StatFileInfo::StatFileInfo() : + m_filename(""), m_selected(false) +{ + refresh(); +} + +StatFileInfo::StatFileInfo(QString filename) : + m_filename(filename), m_selected(false) +{ + refresh(); +} + +StatFileInfo::~StatFileInfo() +{ +} + +void StatFileInfo::setFile(QString filename) +{ + m_filename = filename; + refresh(); +} + +QString StatFileInfo::kind() const +{ + if (isSymLink()) return "l"; + if (isDir()) return "d"; + if (isBlk()) return "b"; + if (isChr()) return "c"; + if (isFifo()) return "p"; + if (isSocket()) return "s"; + if (isFile()) return "-"; + return "?"; +} + +bool StatFileInfo::exists() const +{ + return m_fileInfo.exists(); +} + +bool StatFileInfo::isSafeToRead() const +{ + // it is safe to read non-existing files + if (!exists()) + return true; + + // check the file is a regular file and not a special file + return isFileAtEnd(); +} + +bool StatFileInfo::isSymLinkBroken() const +{ + // if it is a symlink but it doesn't exist, then it is broken + if (m_fileInfo.isSymLink() && !m_fileInfo.exists()) + return true; + return false; +} + +void StatFileInfo::setSelected(bool selected) +{ + m_selected = selected; +} + +void StatFileInfo::refresh() +{ + memset(&m_stat, 0, sizeof(m_stat)); + memset(&m_lstat, 0, sizeof(m_lstat)); + + m_fileInfo = QFileInfo(m_filename); + if (m_filename.isEmpty()) + return; + + QByteArray ba = m_filename.toUtf8(); + char *fn = ba.data(); + + // check the file without following symlinks + int res = lstat(fn, &m_lstat); + if (res != 0) { // if error, then set to undefined + m_lstat.st_mode = 0; + } + // if not symlink, then just copy lstat data to stat + if (!S_ISLNK(m_lstat.st_mode)) { + memcpy(&m_stat, &m_lstat, sizeof(m_stat)); + return; + } + + // check the file after following possible symlinks + res = stat(fn, &m_stat); + if (res != 0) { // if error, then set to undefined + m_stat.st_mode = 0; + } + +} + diff --git a/src/statfileinfo.h b/src/statfileinfo.h new file mode 100644 index 0000000..e22b6cf --- /dev/null +++ b/src/statfileinfo.h @@ -0,0 +1,96 @@ +#ifndef STATFILEINFO_H +#define STATFILEINFO_H + +#include +#include +#include +#include + +/** + * @brief The StatFileInfo class is like QFileInfo, but has more detailed information about file types. + */ +class StatFileInfo +{ +public: + explicit StatFileInfo(); + explicit StatFileInfo(QString filename); + ~StatFileInfo(); + + void setFile(QString filename); + QString fileName() const { return m_fileInfo.fileName(); } + + // these inspect the file itself without following symlinks + + // directory + bool isDir() const { return S_ISDIR(m_lstat.st_mode); } + // symbolic link + bool isSymLink() const { return S_ISLNK(m_lstat.st_mode); } + // block special file + bool isBlk() const { return S_ISBLK(m_lstat.st_mode); } + // character special file + bool isChr() const { return S_ISCHR(m_lstat.st_mode); } + // pipe of FIFO special file + bool isFifo() const { return S_ISFIFO(m_lstat.st_mode); } + // socket + bool isSocket() const { return S_ISSOCK(m_lstat.st_mode); } + // regular file + bool isFile() const { return S_ISREG(m_lstat.st_mode); } + // system file (not a dir, regular file or symlink) + bool isSystem() const { return !S_ISDIR(m_lstat.st_mode) && !S_ISREG(m_lstat.st_mode) && + !S_ISLNK(m_lstat.st_mode); } + + // these inspect the file or if it is a symlink, then its target end point + + // directory + bool isDirAtEnd() const { return S_ISDIR(m_stat.st_mode); } + // block special file + bool isBlkAtEnd() const { return S_ISBLK(m_stat.st_mode); } + // character special file + bool isChrAtEnd() const { return S_ISCHR(m_stat.st_mode); } + // pipe of FIFO special file + bool isFifoAtEnd() const { return S_ISFIFO(m_stat.st_mode); } + // socket + bool isSocketAtEnd() const { return S_ISSOCK(m_stat.st_mode); } + // regular file + bool isFileAtEnd() const { return S_ISREG(m_stat.st_mode); } + // system file (not a dir or regular file) + bool isSystemAtEnd() const { return !S_ISDIR(m_stat.st_mode) && !S_ISREG(m_stat.st_mode); } + + // these inspect the file or if it is a symlink, then its target end point + + QString kind() const; + QFile::Permissions permissions() const { return m_fileInfo.permissions(); } + QString group() const { return m_fileInfo.group(); } + uint groupId() const { return m_fileInfo.groupId(); } + QString owner() const { return m_fileInfo.owner(); } + uint ownerId() const { return m_fileInfo.ownerId(); } + qint64 size() const { return m_fileInfo.size(); } + QDateTime lastModified() const { return m_fileInfo.lastModified(); } + QDateTime created() const { return m_fileInfo.created(); } + bool exists() const; + bool isSafeToRead() const; + + // path accessors + + QDir absoluteDir() const { return m_fileInfo.absoluteDir(); } + QString absolutePath() const { return m_fileInfo.absolutePath(); } + QString absoluteFilePath() const { return m_fileInfo.absoluteFilePath(); } + QString suffix() const { return m_fileInfo.suffix(); } + QString symLinkTarget() const { return m_fileInfo.symLinkTarget(); } + bool isSymLinkBroken() const; + + // selection + void setSelected(bool selected); + bool isSelected() const { return m_selected; } + + void refresh(); + +private: + QString m_filename; + QFileInfo m_fileInfo; + struct stat m_stat; // after following possible symlinks + struct stat m_lstat; // file itself without following symlinks + bool m_selected; +}; + +#endif // STATFILEINFO_H