Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DirectoryDAO: Fix file path matching when relocating directories #4146

Merged
merged 6 commits into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ endif()


if(WIN32)
target_compile_definitions(mixxx-lib PRIVATE __WINDOWS__)
target_compile_definitions(mixxx-lib PUBLIC __WINDOWS__)

# Helps prevent duplicate symbols
target_compile_definitions(mixxx-lib PUBLIC _ATL_MIN_CRT)
Expand Down
57 changes: 36 additions & 21 deletions src/library/dao/directorydao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,15 @@ int DirectoryDAO::removeDirectory(const QString& dir) const {
QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
const QString& oldFolder,
const QString& newFolder) const {
DEBUG_ASSERT(oldFolder == QDir(oldFolder).absolutePath());
DEBUG_ASSERT(newFolder == QDir(newFolder).absolutePath());
// TODO(rryan): This method could use error reporting. It can fail in
// mysterious ways for example if a track in the oldFolder also has a zombie
// track location in newFolder then the replace query will fail because the
// location column becomes non-unique.
QSqlQuery query(m_database);
query.prepare("UPDATE " % DIRECTORYDAO_TABLE % " SET " % DIRECTORYDAO_DIR %
"=:newFolder WHERE " % DIRECTORYDAO_DIR % " = :oldFolder");
"=:newFolder WHERE " % DIRECTORYDAO_DIR % "=:oldFolder");
query.bindValue(":newFolder", newFolder);
query.bindValue(":oldFolder", oldFolder);
if (!query.exec()) {
Expand All @@ -95,19 +97,22 @@ QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
return {};
}

// on Windows the absolute path starts with the drive name
// we also need to check for that
QString startsWithOldFolder = SqlLikeWildcardEscaper::apply(
QDir(oldFolder).absolutePath() + "/", kSqlLikeMatchAll) + kSqlLikeMatchAll;

// Also update information in the track_locations table. This is where mixxx
// gets the location information for a track. Put marks around %1 so that
// this also works on windows
query.prepare(QString("SELECT library.id, track_locations.id, track_locations.location "
"FROM library INNER JOIN track_locations ON "
"track_locations.id = library.location WHERE "
"track_locations.location LIKE '%1' ESCAPE '%2'")
.arg(startsWithOldFolder, kSqlLikeMatchAll));
// Appending '/' is required to disambiguate files from parent
// directories, e.g. "a/b.mp3" and "a/b/c.mp3" where "a/b" would
// match both instead of only files in the parent directory "a/b/".
DEBUG_ASSERT(!oldFolder.endsWith('/'));
const QString oldFolderPrefix = oldFolder + '/';
const QString startsWithOldFolder = SqlLikeWildcardEscaper::apply(
uklotzde marked this conversation as resolved.
Show resolved Hide resolved
oldFolderPrefix, kSqlLikeMatchAll) +
kSqlLikeMatchAll;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is duplicated in TrackDAO. Should this be moved into a utility function?

Also, please use the term "directory" instead of "folder". "Folder" is used on windows because it displays stuff as "Folders" in the WIndows Explorer. And the Control Panel or the Network Neighborhood are folders, but not directories. We are referring to the file system here, so directory is the correct term. /smartass-mode off

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have decided to keep the existing names to minimize the changes. But we could rename the variables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that important, but it would be good to keep it in mind for new code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escape character must match the following SQL query. This gets lost when moving the code into a function. Not really helpful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, I am not going to write any new database code in C++ ;) Only inevitable bug fixes.


query.prepare(QStringLiteral(
"SELECT library.id,track_locations.id,track_locations.location "
"FROM library INNER JOIN track_locations ON "
"track_locations.id=library.location WHERE "
"track_locations.location LIKE :startsWithOldFolder ESCAPE :escape"));
query.bindValue(":startsWithOldFolder", startsWithOldFolder);
query.bindValue(":escape", kSqlLikeMatchAll);
if (!query.exec()) {
LOG_FAILED_QUERY(query) << "could not relocate path of tracks";
return {};
Expand All @@ -116,23 +121,33 @@ QList<RelocatedTrack> DirectoryDAO::relocateDirectory(
QList<DbId> loc_ids;
QList<RelocatedTrack> relocatedTracks;
while (query.next()) {
const auto oldLocation = query.value(2).toString();
const int oldSuffixLen = oldLocation.size() - oldFolder.size();
QString newLocation = newFolder + oldLocation.right(oldSuffixLen);
// LIKE is case-insensitive! We cannot decide if the file system
// at the old location was case-sensitive or case-insensitive
// and must assume that the stored path is at least case-correct.
DEBUG_ASSERT(oldLocation.startsWith(oldFolderPrefix, Qt::CaseInsensitive));
if (!oldLocation.startsWith(oldFolderPrefix, Qt::CaseSensitive)) {
qDebug() << "Skipping relocation of" << oldLocation
<< "to" << newLocation;
continue;
}
loc_ids.append(DbId(query.value(1).toInt()));
auto trackId = TrackId(query.value(0));
auto oldLocation = query.value(2).toString();
const auto trackId = TrackId(query.value(0));
auto missingTrackRef = TrackRef::fromFileInfo(
TrackFile(oldLocation),
std::move(trackId));
const int oldSuffixLen = oldLocation.size() - oldFolder.size();
QString newLocation = newFolder + oldLocation.right(oldSuffixLen);
auto addedTrackRef = TrackRef::fromFileInfo(
TrackFile(newLocation) /*without TrackId*/);
TrackFile(newLocation)); // without TrackId, because no new track will be added!
relocatedTracks.append(RelocatedTrack(
std::move(missingTrackRef),
std::move(addedTrackRef)));
}

QString replacement = "UPDATE track_locations SET location = :newloc "
"WHERE id = :id";
// Also update information in the track_locations table. This is where mixxx
// gets the location information for a track.
const QString replacement = "UPDATE track_locations SET location=:newloc WHERE id=:id";
query.prepare(replacement);
for (int i = 0; i < loc_ids.size(); ++i) {
query.bindValue("newloc", relocatedTracks.at(i).updatedTrackRef().getLocation());
Expand Down
17 changes: 11 additions & 6 deletions src/library/dao/trackdao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1770,14 +1770,19 @@ void TrackDAO::hideAllTracks(const QDir& rootDir) const {
// Capture entries that start with the directory prefix dir.
// dir needs to end in a slash otherwise we might match other
// directories.
QString likeClause = SqlLikeWildcardEscaper::apply(rootDir.absolutePath() + "/", kSqlLikeMatchAll) + kSqlLikeMatchAll;
const QString rootDirPath = rootDir.absolutePath();
DEBUG_ASSERT(!rootDirPath.endsWith('/'));
const QString likeClause =
SqlLikeWildcardEscaper::apply(rootDirPath + '/', kSqlLikeMatchAll) +
kSqlLikeMatchAll;

QSqlQuery query(m_database);
query.prepare(QString("SELECT library.id FROM library INNER JOIN track_locations "
"ON library.location = track_locations.id "
"WHERE track_locations.location LIKE %1 ESCAPE '%2'")
.arg(SqlStringFormatter::format(m_database, likeClause), kSqlLikeMatchAll));

query.prepare(QStringLiteral(
"SELECT library.id FROM library INNER JOIN track_locations "
"ON library.location = track_locations.id "
"WHERE track_locations.location LIKE :likeClause ESCAPE :escape"));
query.bindValue(":likeClause", likeClause);
query.bindValue(":escape", kSqlLikeMatchAll);
VERIFY_OR_DEBUG_ASSERT(query.exec()) {
LOG_FAILED_QUERY(query) << "could not get tracks within directory:" << rootDir;
}
Expand Down
93 changes: 75 additions & 18 deletions src/test/directorydaotest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,50 +137,107 @@ TEST_F(DirectoryDAOTest, relocateDirectory) {
const QTemporaryDir tempDir2;
ASSERT_TRUE(tempDir2.isValid());

//create temp dirs
QString testdir(tempDir1.filePath("TestDir"));
ASSERT_TRUE(QDir(tempDir1.path()).mkpath(testdir));
QString test2(tempDir2.filePath("TestDir2"));
ASSERT_TRUE(QDir(tempDir2.path()).mkpath(test2));
QString testnew(tempDir2.filePath("TestDirNew"));
ASSERT_TRUE(QDir(tempDir2.path()).mkpath(testnew));
// Create temp dirs (with LIKE/GLOB wildcards and quotes in name)
#if defined(__WINDOWS__)
// Exclude reserved characters from path names: ", *, ?
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
const QString oldDir = tempDir1.filePath("Test'_%Dir");
const QString newDir = tempDir2.filePath("TestDir'_%New");
const QString otherDir = tempDir2.filePath("TestDir'_%Other");
#else
const QString oldDir = tempDir1.filePath("Test'\"_*%?Dir");
const QString newDir = tempDir2.filePath("TestDir'\"_*%?New");
const QString otherDir = tempDir2.filePath("TestDir'\"_*%?Other");
#endif
ASSERT_TRUE(QDir(tempDir1.path()).mkpath(oldDir));
ASSERT_TRUE(QDir(tempDir2.path()).mkpath(newDir));
ASSERT_TRUE(QDir(tempDir2.path()).mkpath(otherDir));

const DirectoryDAO& dao = internalCollection()->getDirectoryDAO();

ASSERT_EQ(ALL_FINE, dao.addDirectory(testdir));
ASSERT_EQ(ALL_FINE, dao.addDirectory(test2));
ASSERT_EQ(ALL_FINE, dao.addDirectory(oldDir));
ASSERT_EQ(ALL_FINE, dao.addDirectory(otherDir));

const QStringList oldDirs = dao.getDirs();
ASSERT_EQ(2, oldDirs.size());
ASSERT_THAT(oldDirs, UnorderedElementsAre(oldDir, otherDir));

// ok now lets create some tracks here
// Add tracks that should be relocated
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(oldDir, "a." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(oldDir, "b." + getSupportedFileExt())),
false)
.isValid());

// Add tracks that should be unaffected by the relocation
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(newDir, "c." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(otherDir, "d." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(oldDir + "." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(newDir + "." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(testdir, "a." + getSupportedFileExt())),
TrackFile(otherDir + "." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(testdir, "b." + getSupportedFileExt())),
TrackFile(oldDir.toLower(), "a." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(test2, "c." + getSupportedFileExt())),
TrackFile(oldDir.toUpper(), "b." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(test2, "d." + getSupportedFileExt())),
TrackFile(newDir.toLower(), "c." + getSupportedFileExt())),
false)
.isValid());
ASSERT_TRUE(
internalCollection()
->addTrack(
Track::newTemporary(
TrackFile(otherDir.toUpper(), "d." + getSupportedFileExt())),
false)
.isValid());

QList<RelocatedTrack> relocatedTracks =
dao.relocateDirectory(testdir, testnew);
dao.relocateDirectory(oldDir, newDir);
EXPECT_EQ(2, relocatedTracks.size());

const QStringList allDirs = dao.getDirs();
EXPECT_EQ(2, allDirs.size());
EXPECT_THAT(allDirs, UnorderedElementsAre(test2, testnew));
const QStringList newDirs = dao.getDirs();
EXPECT_EQ(2, newDirs.size());
EXPECT_THAT(newDirs, UnorderedElementsAre(newDir, otherDir));
}