#include "vnotefile.h" #include #include #include #include #include #include #include #include "vdirectory.h" VNoteFile::VNoteFile(VDirectory *p_directory, const QString &p_name, FileType p_type, bool p_modifiable, QDateTime p_createdTimeUtc, QDateTime p_modifiedTimeUtc) : VFile(p_directory, p_name, p_type, p_modifiable, p_createdTimeUtc, p_modifiedTimeUtc) { } QString VNoteFile::fetchPath() const { return QDir(getDirectory()->fetchPath()).filePath(m_name); } QString VNoteFile::fetchBasePath() const { return getDirectory()->fetchPath(); } QString VNoteFile::fetchImageFolderPath() const { return QDir(fetchBasePath()).filePath(getNotebook()->getImageFolder()); } bool VNoteFile::useRelativeImageFolder() const { // Always use relative image folder. return true; } QString VNoteFile::getImageFolderInLink() const { return getNotebook()->getImageFolder(); } void VNoteFile::setName(const QString &p_name) { m_name = p_name; } bool VNoteFile::rename(const QString &p_name) { if (m_name == p_name) { return true; } QString oldName = m_name; VDirectory *dir = getDirectory(); Q_ASSERT(dir); // Rename it in disk. QDir diskDir(dir->fetchPath()); if (!diskDir.rename(m_name, p_name)) { qWarning() << "fail to rename file" << m_name << "to" << p_name << "in disk"; return false; } m_name = p_name; // Update parent directory's config file. if (!dir->updateFileConfig(this)) { m_name = oldName; diskDir.rename(p_name, m_name); return false; } // Can't not change doc type. Q_ASSERT(m_docType == DocType::Unknown || m_docType == VUtils::docTypeFromName(m_name)); m_docType = VUtils::docTypeFromName(m_name); qDebug() << "file renamed from" << oldName << "to" << m_name; return true; } VDirectory *VNoteFile::getDirectory() { Q_ASSERT(parent()); return (VDirectory *)parent(); } const VDirectory *VNoteFile::getDirectory() const { Q_ASSERT(parent()); return (const VDirectory *)parent(); } VNotebook *VNoteFile::getNotebook() { return getDirectory()->getNotebook(); } const VNotebook *VNoteFile::getNotebook() const { return getDirectory()->getNotebook(); } QString VNoteFile::getNotebookName() const { return getDirectory()->getNotebookName(); } QString VNoteFile::fetchRelativePath() const { return QDir(getDirectory()->fetchRelativePath()).filePath(m_name); } VNoteFile *VNoteFile::fromJson(VDirectory *p_directory, const QJsonObject &p_json, FileType p_type, bool p_modifiable) { VNoteFile *file = new VNoteFile(p_directory, p_json[DirConfig::c_name].toString(), p_type, p_modifiable, QDateTime::fromString(p_json[DirConfig::c_createdTime].toString(), Qt::ISODate), QDateTime::fromString(p_json[DirConfig::c_modifiedTime].toString(), Qt::ISODate)); // Attachment Folder. file->m_attachmentFolder = p_json[DirConfig::c_attachmentFolder].toString(); // Attachments. QJsonArray attachmentJson = p_json[DirConfig::c_attachments].toArray(); for (int i = 0; i < attachmentJson.size(); ++i) { QJsonObject attachmentItem = attachmentJson[i].toObject(); file->m_attachments.push_back(VAttachment(attachmentItem[DirConfig::c_name].toString())); } // Tags. QJsonArray tagsJson = p_json[DirConfig::c_tags].toArray(); for (int i = 0; i < tagsJson.size(); ++i) { file->m_tags.append(tagsJson[i].toString()); } return file; } QJsonObject VNoteFile::toConfigJson() const { QJsonObject item; item[DirConfig::c_name] = m_name; item[DirConfig::c_createdTime] = m_createdTimeUtc.toString(Qt::ISODate); item[DirConfig::c_modifiedTime] = m_modifiedTimeUtc.toString(Qt::ISODate); item[DirConfig::c_attachmentFolder] = m_attachmentFolder; // Attachments. QJsonArray attachmentJson; for (int i = 0; i < m_attachments.size(); ++i) { const VAttachment &att = m_attachments[i]; QJsonObject attachmentItem; attachmentItem[DirConfig::c_name] = att.m_name; attachmentJson.append(attachmentItem); } item[DirConfig::c_attachments] = attachmentJson; // Tags. QJsonArray tags; for (auto const & tag : m_tags) { tags.append(tag); } item[DirConfig::c_tags] = tags; return item; } bool VNoteFile::deleteFile(QString *p_errMsg) { Q_ASSERT(!m_opened); Q_ASSERT(parent()); bool ret = true; // Delete local images if it is Markdown. if (m_docType == DocType::Markdown) { if (!deleteInternalImages()) { ret = false; VUtils::addErrMsg(p_errMsg, tr("Fail to delete images of this note.")); } } // Delete attachments. if (!deleteAttachments()) { ret = false; VUtils::addErrMsg(p_errMsg, tr("Fail to delete attachments of this note.")); } // Delete the file. QString filePath = fetchPath(); if (VUtils::deleteFile(getNotebook(), filePath, false)) { qDebug() << "deleted" << m_name << filePath; } else { ret = false; VUtils::addErrMsg(p_errMsg, tr("Fail to delete the note file.")); qWarning() << "fail to delete" << m_name << filePath; } return ret; } bool VNoteFile::deleteInternalImages() { Q_ASSERT(parent() && m_docType == DocType::Markdown); QVector images = VUtils::fetchImagesFromMarkdownFile(this, ImageLink::LocalRelativeInternal); int deleted = 0; for (int i = 0; i < images.size(); ++i) { if (VUtils::deleteFile(getNotebook(), images[i].m_path, false)) { ++deleted; } } qDebug() << "delete" << deleted << "images for" << m_name << fetchPath(); return deleted == images.size(); } bool VNoteFile::addAttachment(const QString &p_file, QString *p_destFile) { if (p_file.isEmpty() || !QFileInfo::exists(p_file)) { return false; } QString folderPath = fetchAttachmentFolderPath(); QString name = VUtils::fileNameFromPath(p_file); Q_ASSERT(!name.isEmpty()); // For attachments, we do not use complete base name. // abc.tar.gz should be abc_001.tar.gz instead of abc.tar_001.gz. name = VUtils::getFileNameWithSequence(folderPath, name, false); QString destPath = QDir(folderPath).filePath(name); if (!VUtils::copyFile(p_file, destPath, false)) { return false; } m_attachments.push_back(VAttachment(name)); if (!getDirectory()->updateFileConfig(this)) { qWarning() << "fail to update config of file" << m_name << "in directory" << fetchBasePath(); return false; } if (p_destFile) { *p_destFile = destPath; } return true; } QString VNoteFile::fetchAttachmentFolderPath() { QString folderPath = QDir(fetchBasePath()).filePath(getNotebook()->getAttachmentFolder()); if (m_attachmentFolder.isEmpty()) { m_attachmentFolder = VUtils::getRandomFileName(folderPath); } folderPath = QDir(folderPath).filePath(m_attachmentFolder); if (!QFileInfo::exists(folderPath)) { QDir dir; if (!dir.mkpath(folderPath)) { qWarning() << "fail to create attachment folder of notebook" << m_name << folderPath; } } return folderPath; } bool VNoteFile::deleteAttachments(bool p_omitMissing) { if (m_attachments.isEmpty()) { return true; } QVector attas; for (int i = 0; i < m_attachments.size(); ++i) { attas.push_back(m_attachments[i].m_name); } return deleteAttachments(attas, p_omitMissing); } bool VNoteFile::deleteAttachments(const QVector &p_names, bool p_omitMissing) { if (p_names.isEmpty()) { return true; } QDir dir(fetchAttachmentFolderPath()); bool ret = true; for (int i = 0; i < p_names.size(); ++i) { int idx = findAttachment(p_names[i]); if (idx == -1) { ret = false; continue; } m_attachments.remove(idx); QString filePath = dir.filePath(p_names[i]); if (p_omitMissing && !QFileInfo::exists(filePath)) { // The attachment file does not exist. We skip it to avoid error. continue; } if (!VUtils::deleteFile(getNotebook(), filePath, false)) { ret = false; qWarning() << "fail to delete attachment" << p_names[i] << "for note" << m_name; } } // Delete the attachment folder if m_attachments is empty now. if (m_attachments.isEmpty()) { dir.cdUp(); if (!dir.rmdir(m_attachmentFolder)) { ret = false; qWarning() << "fail to delete attachment folder" << m_attachmentFolder << "for note" << m_name; } } if (!getDirectory()->updateFileConfig(this)) { ret = false; qWarning() << "fail to update config of file" << m_name << "in directory" << fetchBasePath(); } return ret; } int VNoteFile::findAttachment(const QString &p_name, bool p_caseSensitive) { const QString name = p_caseSensitive ? p_name : p_name.toLower(); for (int i = 0; i < m_attachments.size(); ++i) { QString attaName = p_caseSensitive ? m_attachments[i].m_name : m_attachments[i].m_name.toLower(); if (name == attaName) { return i; } } return -1; } int VNoteFile::findAttachmentByPath(const QString &p_file, bool p_caseSensitive) { QFileInfo fi(p_file); int idx = findAttachment(fi.fileName(), p_caseSensitive); if (idx == -1) { return -1; } // Check path. QString attPath = QDir(fetchAttachmentFolderPath()).filePath(m_attachments[idx].m_name); bool equal = false; if (p_caseSensitive) { equal = VUtils::equalPath(attPath, fi.absoluteFilePath()); } else { equal = VUtils::equalPath(attPath.toLower(), fi.absoluteFilePath().toLower()); } return equal ? idx : -1; } bool VNoteFile::sortAttachments(const QVector &p_sortedIdx) { V_ASSERT(m_opened); V_ASSERT(p_sortedIdx.size() == m_attachments.size()); auto ori = m_attachments; for (int i = 0; i < p_sortedIdx.size(); ++i) { m_attachments[i] = ori[p_sortedIdx[i]]; } bool ret = true; if (!getDirectory()->updateFileConfig(this)) { qWarning() << "fail to reorder attachments in config" << p_sortedIdx; m_attachments = ori; ret = false; } return ret; } bool VNoteFile::renameAttachment(const QString &p_oldName, const QString &p_newName) { int idx = findAttachment(p_oldName); if (idx == -1) { return false; } QDir dir(fetchAttachmentFolderPath()); if (!dir.rename(p_oldName, p_newName)) { qWarning() << "fail to rename attachment file" << p_oldName << p_newName; return false; } m_attachments[idx].m_name = p_newName; if (!getDirectory()->updateFileConfig(this)) { qWarning() << "fail to rename attachment in config" << p_oldName << p_newName; m_attachments[idx].m_name = p_oldName; dir.rename(p_newName, p_oldName); return false; } return true; } QVector VNoteFile::checkAttachments() { QVector missing; QDir dir(fetchAttachmentFolderPath()); for (auto const & atta : m_attachments) { QString file = dir.filePath(atta.m_name); if (!QFileInfo::exists(file)) { missing.push_back(atta.m_name); } } return missing; } bool VNoteFile::deleteFile(VNoteFile *p_file, QString *p_errMsg) { Q_ASSERT(!p_file->isOpened()); bool ret = true; QString name = p_file->getName(); QString path = p_file->fetchPath(); if (!p_file->deleteFile(p_errMsg)) { qWarning() << "fail to delete file" << name << path; ret = false; } VDirectory *dir = p_file->getDirectory(); Q_ASSERT(dir); if (!dir->removeFile(p_file)) { qWarning() << "fail to remove file from directory" << name << path; VUtils::addErrMsg(p_errMsg, tr("Fail to remove the note from the folder configuration.")); ret = false; } delete p_file; return ret; } bool VNoteFile::copyFile(VDirectory *p_destDir, const QString &p_destName, VNoteFile *p_file, bool p_isCut, int p_idx, VNoteFile **p_targetFile, QString *p_errMsg) { bool ret = true; *p_targetFile = NULL; int nrImageCopied = 0; bool attachmentFolderCopied = false; QString srcPath = QDir::cleanPath(p_file->fetchPath()); QString destPath = QDir::cleanPath(QDir(p_destDir->fetchPath()).filePath(p_destName)); if (VUtils::equalPath(srcPath, destPath)) { *p_targetFile = p_file; return false; } if (!p_destDir->isOpened()) { VUtils::addErrMsg(p_errMsg, tr("Fail to open target folder.")); return false; } QString opStr = p_isCut ? tr("cut") : tr("copy"); VDirectory *srcDir = p_file->getDirectory(); DocType docType = p_file->getDocType(); Q_ASSERT(srcDir->isOpened()); Q_ASSERT(docType == VUtils::docTypeFromName(p_destName)); // Images to be copied. QVector images; if (docType == DocType::Markdown) { images = VUtils::fetchImagesFromMarkdownFile(p_file, ImageLink::LocalRelativeInternal); } // Attachments to be copied. QString attaFolder = p_file->getAttachmentFolder(); QString attaFolderPath; if (!attaFolder.isEmpty()) { attaFolderPath = p_file->fetchAttachmentFolderPath(); } // Copy the note file. if (!VUtils::copyFile(srcPath, destPath, p_isCut)) { VUtils::addErrMsg(p_errMsg, tr("Fail to %1 the note file.").arg(opStr)); qWarning() << "fail to" << opStr << "the note file" << srcPath << "to" << destPath; return false; } // Add file to VDirectory. VNoteFile *destFile = NULL; if (p_isCut) { srcDir->removeFile(p_file); p_file->setName(p_destName); if (p_destDir->addFile(p_file, p_idx)) { destFile = p_file; } else { destFile = NULL; } } else { destFile = p_destDir->addFile(p_destName, p_idx); // Copy tags to this file. if (destFile) { const QStringList &tags = p_file->getTags(); for (auto const & tag : tags) { destFile->addTag(tag); destFile->getNotebook()->addTag(tag); } } } if (!destFile) { VUtils::addErrMsg(p_errMsg, tr("Fail to add the note to target folder's configuration.")); return false; } // Copy images. if (!copyInternalImages(images, destFile->fetchBasePath(), p_isCut, &nrImageCopied, p_errMsg)) { ret = false; } // Copy attachment folder. if (!attaFolderPath.isEmpty()) { QDir dir(destFile->fetchBasePath()); QString folderPath = dir.filePath(destFile->getNotebook()->getAttachmentFolder()); attaFolder = VUtils::getDirNameWithSequence(folderPath, attaFolder); folderPath = QDir(folderPath).filePath(attaFolder); // Copy attaFolderPath to folderPath. if (!VUtils::copyDirectory(attaFolderPath, folderPath, p_isCut)) { VUtils::addErrMsg(p_errMsg, tr("Fail to %1 attachments folder %2 to %3. " "Please manually maintain it.") .arg(opStr).arg(attaFolderPath).arg(folderPath)); QVector emptyAttas; destFile->setAttachments(emptyAttas); ret = false; } else { attachmentFolderCopied = true; destFile->setAttachmentFolder(attaFolder); if (!p_isCut) { destFile->setAttachments(p_file->getAttachments()); } } if (!p_destDir->updateFileConfig(destFile)) { VUtils::addErrMsg(p_errMsg, tr("Fail to update configuration of note %1.") .arg(destFile->fetchPath())); ret = false; } } qDebug() << "copyFile:" << p_file << "to" << destFile << "copied_images:" << nrImageCopied << "copied_attachments:" << attachmentFolderCopied; *p_targetFile = destFile; return ret; } bool VNoteFile::copyInternalImages(const QVector &p_images, const QString &p_destDirPath, bool p_isCut, int *p_nrImageCopied, QString *p_errMsg) { bool ret = true; QDir parentDir(p_destDirPath); QSet processedImages; QString opStr = p_isCut ? tr("cut") : tr("copy"); int nrImageCopied = 0; for (int i = 0; i < p_images.size(); ++i) { const ImageLink &link = p_images[i]; if (processedImages.contains(link.m_path)) { continue; } processedImages.insert(link.m_path); if (!QFileInfo::exists(link.m_path)) { VUtils::addErrMsg(p_errMsg, tr("Source image %1 does not exist.") .arg(link.m_path)); ret = false; continue; } QString imageFolder = VUtils::directoryNameFromPath(VUtils::basePathFromPath(link.m_path)); QString destImagePath = QDir(parentDir.filePath(imageFolder)).filePath(VUtils::fileNameFromPath(link.m_path)); if (VUtils::equalPath(link.m_path, destImagePath)) { VUtils::addErrMsg(p_errMsg, tr("Skip image with the same source and target path %1.") .arg(link.m_path)); ret = false; continue; } if (!VUtils::copyFile(link.m_path, destImagePath, p_isCut)) { VUtils::addErrMsg(p_errMsg, tr("Fail to %1 image %2 to %3. " "Please manually %1 it and modify the note.") .arg(opStr).arg(link.m_path).arg(destImagePath)); ret = false; } else { ++nrImageCopied; qDebug() << opStr << "image" << link.m_path << "to" << destImagePath; } } *p_nrImageCopied = nrImageCopied; return ret; } void VNoteFile::removeTag(const QString &p_tag) { if (p_tag.isEmpty() || m_tags.isEmpty()) { return; } int nr = m_tags.removeAll(p_tag); if (nr > 0) { if (!getDirectory()->updateFileConfig(this)) { qWarning() << "fail to update config of file" << m_name << "in directory" << fetchBasePath(); } } } bool VNoteFile::addTag(const QString &p_tag) { if (p_tag.isEmpty() || hasTag(p_tag)) { return false; } m_tags.append(p_tag); if (!getDirectory()->updateFileConfig(this)) { qWarning() << "fail to update config of file" << m_name << "in directory" << fetchBasePath(); m_tags.removeAll(p_tag); return false; } return true; } bool VNoteFile::save() { bool ret = VFile::save(); if (ret) { if (!getDirectory()->updateFileConfig(this)) { qWarning() << "fail to update config of file" << m_name << "in directory" << fetchBasePath(); ret = false; } } return ret; }