If `SimpleDatabase::Visit` is called on a database that contains a mounted directry the URIs of the elements passed to the callbacks are not prefixed by the mountpoint path. This leads to lsinfo and add not working because they use the wrong URI. This pull request is using the `WalkMount` helper function to create prefixed versions of `VisitDirectory`, `VisitSong` and `VisitPlaylist` to add the correct prefix to the parameters of the callback functions.
482 lines
10 KiB
C++
482 lines
10 KiB
C++
/*
|
|
* Copyright 2003-2017 The Music Player Daemon Project
|
|
* http://www.musicpd.org
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "SimpleDatabasePlugin.hxx"
|
|
#include "PrefixedLightSong.hxx"
|
|
#include "Mount.hxx"
|
|
#include "db/DatabasePlugin.hxx"
|
|
#include "db/Selection.hxx"
|
|
#include "db/Helpers.hxx"
|
|
#include "db/Stats.hxx"
|
|
#include "db/UniqueTags.hxx"
|
|
#include "db/LightDirectory.hxx"
|
|
#include "Directory.hxx"
|
|
#include "Song.hxx"
|
|
#include "DatabaseSave.hxx"
|
|
#include "db/DatabaseLock.hxx"
|
|
#include "db/DatabaseError.hxx"
|
|
#include "fs/io/TextFile.hxx"
|
|
#include "fs/io/BufferedOutputStream.hxx"
|
|
#include "fs/io/FileOutputStream.hxx"
|
|
#include "fs/FileInfo.hxx"
|
|
#include "config/Block.hxx"
|
|
#include "fs/FileSystem.hxx"
|
|
#include "util/CharUtil.hxx"
|
|
#include "util/Domain.hxx"
|
|
#include "Log.hxx"
|
|
|
|
#ifdef ENABLE_ZLIB
|
|
#include "fs/io/GzipOutputStream.hxx"
|
|
#endif
|
|
|
|
#include <memory>
|
|
|
|
#include <errno.h>
|
|
|
|
static constexpr Domain simple_db_domain("simple_db");
|
|
|
|
inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
|
|
:Database(simple_db_plugin),
|
|
path(block.GetPath("path")),
|
|
#ifdef ENABLE_ZLIB
|
|
compress(block.GetBlockValue("compress", true)),
|
|
#endif
|
|
cache_path(block.GetPath("cache_directory")),
|
|
prefixed_light_song(nullptr)
|
|
{
|
|
if (path.IsNull())
|
|
throw std::runtime_error("No \"path\" parameter specified");
|
|
|
|
path_utf8 = path.ToUTF8();
|
|
}
|
|
|
|
inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path,
|
|
#ifndef ENABLE_ZLIB
|
|
gcc_unused
|
|
#endif
|
|
bool _compress)
|
|
:Database(simple_db_plugin),
|
|
path(std::move(_path)),
|
|
path_utf8(path.ToUTF8()),
|
|
#ifdef ENABLE_ZLIB
|
|
compress(_compress),
|
|
#endif
|
|
cache_path(AllocatedPath::Null()),
|
|
prefixed_light_song(nullptr) {
|
|
}
|
|
|
|
Database *
|
|
SimpleDatabase::Create(gcc_unused EventLoop &loop,
|
|
gcc_unused DatabaseListener &listener,
|
|
const ConfigBlock &block)
|
|
{
|
|
return new SimpleDatabase(block);
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Check() const
|
|
{
|
|
assert(!path.IsNull());
|
|
|
|
/* Check if the file exists */
|
|
if (!PathExists(path)) {
|
|
/* If the file doesn't exist, we can't check if we can write
|
|
* it, so we are going to try to get the directory path, and
|
|
* see if we can write a file in that */
|
|
const auto dirPath = path.GetDirectoryName();
|
|
|
|
/* Check that the parent part of the path is a directory */
|
|
FileInfo fi;
|
|
|
|
try {
|
|
fi = FileInfo(dirPath);
|
|
} catch (...) {
|
|
std::throw_with_nested(std::runtime_error("On parent directory of db file"));
|
|
}
|
|
|
|
if (!fi.IsDirectory())
|
|
throw std::runtime_error("Couldn't create db file \"" +
|
|
path_utf8 + "\" because the "
|
|
"parent path is not a directory");
|
|
|
|
#ifndef _WIN32
|
|
/* Check if we can write to the directory */
|
|
if (!CheckAccess(dirPath, X_OK | W_OK)) {
|
|
const int e = errno;
|
|
const std::string dirPath_utf8 = dirPath.ToUTF8();
|
|
throw FormatErrno(e, "Can't create db file in \"%s\"",
|
|
dirPath_utf8.c_str());
|
|
}
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
|
|
/* Path exists, now check if it's a regular file */
|
|
const FileInfo fi(path);
|
|
|
|
if (!fi.IsRegular())
|
|
throw std::runtime_error("db file \"" + path_utf8 + "\" is not a regular file");
|
|
|
|
#ifndef _WIN32
|
|
/* And check that we can write to it */
|
|
if (!CheckAccess(path, R_OK | W_OK))
|
|
throw FormatErrno("Can't open db file \"%s\" for reading/writing",
|
|
path_utf8.c_str());
|
|
#endif
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Load()
|
|
{
|
|
assert(!path.IsNull());
|
|
assert(root != nullptr);
|
|
|
|
TextFile file(path);
|
|
|
|
LogDebug(simple_db_domain, "reading DB");
|
|
|
|
db_load_internal(file, *root);
|
|
|
|
FileInfo fi;
|
|
if (GetFileInfo(path, fi))
|
|
mtime = fi.GetModificationTime();
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Open()
|
|
{
|
|
assert(prefixed_light_song == nullptr);
|
|
|
|
root = Directory::NewRoot();
|
|
mtime = 0;
|
|
|
|
#ifndef NDEBUG
|
|
borrowed_song_count = 0;
|
|
#endif
|
|
|
|
try {
|
|
Load();
|
|
} catch (const std::exception &e) {
|
|
LogError(e);
|
|
|
|
delete root;
|
|
|
|
Check();
|
|
|
|
root = Directory::NewRoot();
|
|
}
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Close()
|
|
{
|
|
assert(root != nullptr);
|
|
assert(prefixed_light_song == nullptr);
|
|
assert(borrowed_song_count == 0);
|
|
|
|
delete root;
|
|
}
|
|
|
|
const LightSong *
|
|
SimpleDatabase::GetSong(const char *uri) const
|
|
{
|
|
assert(root != nullptr);
|
|
assert(prefixed_light_song == nullptr);
|
|
assert(borrowed_song_count == 0);
|
|
|
|
ScopeDatabaseLock protect;
|
|
|
|
auto r = root->LookupDirectory(uri);
|
|
|
|
if (r.directory->IsMount()) {
|
|
/* pass the request to the mounted database */
|
|
protect.unlock();
|
|
|
|
const LightSong *song =
|
|
r.directory->mounted_database->GetSong(r.uri);
|
|
if (song == nullptr)
|
|
return nullptr;
|
|
|
|
prefixed_light_song =
|
|
new PrefixedLightSong(*song, r.directory->GetPath());
|
|
return prefixed_light_song;
|
|
}
|
|
|
|
if (r.uri == nullptr)
|
|
/* it's a directory */
|
|
throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
|
|
"No such song");
|
|
|
|
if (strchr(r.uri, '/') != nullptr)
|
|
/* refers to a URI "below" the actual song */
|
|
throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
|
|
"No such song");
|
|
|
|
const Song *song = r.directory->FindSong(r.uri);
|
|
protect.unlock();
|
|
if (song == nullptr)
|
|
throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
|
|
"No such song");
|
|
|
|
light_song = song->Export();
|
|
|
|
#ifndef NDEBUG
|
|
++borrowed_song_count;
|
|
#endif
|
|
|
|
return &light_song;
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
|
|
{
|
|
assert(song != nullptr);
|
|
assert(song == &light_song || song == prefixed_light_song);
|
|
|
|
delete prefixed_light_song;
|
|
prefixed_light_song = nullptr;
|
|
|
|
#ifndef NDEBUG
|
|
if (song == &light_song) {
|
|
assert(borrowed_song_count > 0);
|
|
--borrowed_song_count;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Visit(const DatabaseSelection &selection,
|
|
VisitDirectory visit_directory,
|
|
VisitSong visit_song,
|
|
VisitPlaylist visit_playlist) const
|
|
{
|
|
ScopeDatabaseLock protect;
|
|
|
|
auto r = root->LookupDirectory(selection.uri.c_str());
|
|
|
|
if (r.directory->IsMount()) {
|
|
/* pass the request and the remaining uri to the mounted database */
|
|
protect.unlock();
|
|
|
|
WalkMount(r.directory->GetPath(), *(r.directory->mounted_database),
|
|
(r.uri == nullptr)?"":r.uri, selection.recursive, selection.filter,
|
|
visit_directory, visit_song, visit_playlist);
|
|
|
|
return;
|
|
}
|
|
|
|
if (r.uri == nullptr) {
|
|
/* it's a directory */
|
|
|
|
if (selection.recursive && visit_directory)
|
|
visit_directory(r.directory->Export());
|
|
|
|
r.directory->Walk(selection.recursive, selection.filter,
|
|
visit_directory, visit_song,
|
|
visit_playlist);
|
|
return;
|
|
}
|
|
|
|
if (strchr(r.uri, '/') == nullptr) {
|
|
if (visit_song) {
|
|
Song *song = r.directory->FindSong(r.uri);
|
|
if (song != nullptr) {
|
|
const LightSong song2 = song->Export();
|
|
if (selection.Match(song2))
|
|
visit_song(song2);
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
|
|
"No such directory");
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
|
|
TagType tag_type, tag_mask_t group_mask,
|
|
VisitTag visit_tag) const
|
|
{
|
|
::VisitUniqueTags(*this, selection, tag_type, group_mask, visit_tag);
|
|
}
|
|
|
|
DatabaseStats
|
|
SimpleDatabase::GetStats(const DatabaseSelection &selection) const
|
|
{
|
|
return ::GetStats(*this, selection);
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Save()
|
|
{
|
|
{
|
|
const ScopeDatabaseLock protect;
|
|
|
|
LogDebug(simple_db_domain, "removing empty directories from DB");
|
|
root->PruneEmpty();
|
|
|
|
LogDebug(simple_db_domain, "sorting DB");
|
|
root->Sort();
|
|
}
|
|
|
|
LogDebug(simple_db_domain, "writing DB");
|
|
|
|
FileOutputStream fos(path);
|
|
|
|
OutputStream *os = &fos;
|
|
|
|
#ifdef ENABLE_ZLIB
|
|
std::unique_ptr<GzipOutputStream> gzip;
|
|
if (compress) {
|
|
gzip.reset(new GzipOutputStream(*os));
|
|
os = gzip.get();
|
|
}
|
|
#endif
|
|
|
|
BufferedOutputStream bos(*os);
|
|
|
|
db_save_internal(bos, *root);
|
|
|
|
bos.Flush();
|
|
|
|
#ifdef ENABLE_ZLIB
|
|
if (gzip != nullptr) {
|
|
gzip->Flush();
|
|
gzip.reset();
|
|
}
|
|
#endif
|
|
|
|
fos.Commit();
|
|
|
|
FileInfo fi;
|
|
if (GetFileInfo(path, fi))
|
|
mtime = fi.GetModificationTime();
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Mount(const char *uri, Database *db)
|
|
{
|
|
#if !CLANG_CHECK_VERSION(3,6)
|
|
/* disabled on clang due to -Wtautological-pointer-compare */
|
|
assert(uri != nullptr);
|
|
assert(db != nullptr);
|
|
#endif
|
|
assert(*uri != 0);
|
|
|
|
ScopeDatabaseLock protect;
|
|
|
|
auto r = root->LookupDirectory(uri);
|
|
if (r.uri == nullptr)
|
|
throw DatabaseError(DatabaseErrorCode::CONFLICT,
|
|
"Already exists");
|
|
|
|
if (strchr(r.uri, '/') != nullptr)
|
|
throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
|
|
"Parent not found");
|
|
|
|
Directory *mnt = r.directory->CreateChild(r.uri);
|
|
mnt->mounted_database = db;
|
|
}
|
|
|
|
static constexpr bool
|
|
IsSafeChar(char ch)
|
|
{
|
|
return IsAlphaNumericASCII(ch) || ch == '-' || ch == '_' || ch == '%';
|
|
}
|
|
|
|
static constexpr bool
|
|
IsUnsafeChar(char ch)
|
|
{
|
|
return !IsSafeChar(ch);
|
|
}
|
|
|
|
void
|
|
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri)
|
|
{
|
|
if (cache_path.IsNull())
|
|
throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
|
|
"No 'cache_directory' configured");
|
|
|
|
std::string name(storage_uri);
|
|
std::replace_if(name.begin(), name.end(), IsUnsafeChar, '_');
|
|
|
|
const auto name_fs = AllocatedPath::FromUTF8Throw(name.c_str());
|
|
|
|
#ifndef ENABLE_ZLIB
|
|
constexpr bool compress = false;
|
|
#endif
|
|
auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
|
|
name_fs.c_str()),
|
|
compress);
|
|
try {
|
|
db->Open();
|
|
} catch (...) {
|
|
delete db;
|
|
throw;
|
|
}
|
|
|
|
// TODO: update the new database instance?
|
|
|
|
try {
|
|
Mount(local_uri, db);
|
|
} catch (...) {
|
|
db->Close();
|
|
delete db;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
Database *
|
|
SimpleDatabase::LockUmountSteal(const char *uri)
|
|
{
|
|
ScopeDatabaseLock protect;
|
|
|
|
auto r = root->LookupDirectory(uri);
|
|
if (r.uri != nullptr || !r.directory->IsMount())
|
|
return nullptr;
|
|
|
|
Database *db = r.directory->mounted_database;
|
|
r.directory->mounted_database = nullptr;
|
|
r.directory->Delete();
|
|
|
|
return db;
|
|
}
|
|
|
|
bool
|
|
SimpleDatabase::Unmount(const char *uri)
|
|
{
|
|
Database *db = LockUmountSteal(uri);
|
|
if (db == nullptr)
|
|
return false;
|
|
|
|
db->Close();
|
|
delete db;
|
|
return true;
|
|
}
|
|
|
|
const DatabasePlugin simple_db_plugin = {
|
|
"simple",
|
|
DatabasePlugin::FLAG_REQUIRE_STORAGE,
|
|
SimpleDatabase::Create,
|
|
};
|