db/simple: mount points

A SimpleDatabase instance can now "mount" other Database instances at
certain locations.  This is used to use a new SimpleDatabase instance
for each storage mount (issued with the "mount" protocol command).
Each such instance has its own database file, stored in the directory
that is specified with the "cache_directory" option.
This commit is contained in:
Max Kellermann
2014-02-26 08:39:44 +01:00
parent 2a16fc74fd
commit e9a85aa4e4
19 changed files with 603 additions and 24 deletions

View File

@@ -21,10 +21,12 @@
#include "Directory.hxx"
#include "SongSort.hxx"
#include "Song.hxx"
#include "Mount.hxx"
#include "db/LightDirectory.hxx"
#include "db/LightSong.hxx"
#include "db/Uri.hxx"
#include "db/DatabaseLock.hxx"
#include "db/Interface.hxx"
#include "SongFilter.hxx"
#include "lib/icu/Collate.hxx"
#include "fs/Traits.hxx"
@@ -42,7 +44,8 @@ extern "C" {
Directory::Directory(std::string &&_path_utf8, Directory *_parent)
:parent(_parent),
mtime(0), have_stat(false),
path(std::move(_path_utf8))
path(std::move(_path_utf8)),
mounted_database(nullptr)
{
INIT_LIST_HEAD(&children);
INIT_LIST_HEAD(&songs);
@@ -50,6 +53,8 @@ Directory::Directory(std::string &&_path_utf8, Directory *_parent)
Directory::~Directory()
{
delete mounted_database;
Song *song, *ns;
directory_for_each_song_safe(song, ns, *this)
song->Free();
@@ -113,6 +118,11 @@ Directory::PruneEmpty()
Directory *child, *n;
directory_for_each_child_safe(child, n, *this) {
if (child->IsMount())
/* never prune mount points; they're always
empty by definition, but that's ok */
continue;
child->PruneEmpty();
if (child->IsEmpty())
@@ -233,6 +243,22 @@ Directory::Walk(bool recursive, const SongFilter *filter,
{
assert(!error.IsDefined());
if (IsMount()) {
assert(IsEmpty());
/* TODO: eliminate this unlock/lock; it is necessary
because the child's SimpleDatabasePlugin::Visit()
call will lock it again */
db_unlock();
bool result = WalkMount(GetPath(), *mounted_database,
recursive, filter,
visit_directory, visit_song,
visit_playlist,
error);
db_lock();
return result;
}
if (visit_song) {
Song *song;
directory_for_each_song(song, *this) {

View File

@@ -57,6 +57,7 @@ struct Song;
struct db_visitor;
class SongFilter;
class Error;
class Database;
struct Directory {
/**
@@ -94,6 +95,12 @@ struct Directory {
std::string path;
/**
* If this is not nullptr, then this directory does not really
* exist, but is a mount point for another #Database.
*/
Database *mounted_database;
public:
Directory(std::string &&_path_utf8, Directory *_parent);
~Directory();
@@ -106,6 +113,10 @@ public:
return new Directory(std::string(), nullptr);
}
bool IsMount() const {
return mounted_database != nullptr;
}
/**
* Remove this #Directory object from its parent and free it. This
* must not be called with the root Directory.

View File

@@ -88,7 +88,8 @@ directory_save(FILE *fp, const Directory &directory)
directory_for_each_child(cur, directory) {
fprintf(fp, DIRECTORY_DIR "%s\n", cur->GetName());
directory_save(fp, *cur);
if (!cur->IsMount())
directory_save(fp, *cur);
if (ferror(fp))
return;

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2003-2014 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 "Mount.hxx"
#include "PrefixedLightSong.hxx"
#include "db/Selection.hxx"
#include "db/LightDirectory.hxx"
#include "db/LightSong.hxx"
#include "db/Interface.hxx"
#include "fs/Traits.hxx"
#include "util/Error.hxx"
#include <string>
struct PrefixedLightDirectory : LightDirectory {
std::string buffer;
PrefixedLightDirectory(const LightDirectory &directory,
const char *base)
:LightDirectory(directory),
buffer(IsRoot()
? std::string(base)
: PathTraitsUTF8::Build(base, uri)) {
uri = buffer.c_str();
}
};
static bool
PrefixVisitDirectory(const char *base, const VisitDirectory &visit_directory,
const LightDirectory &directory, Error &error)
{
return visit_directory(PrefixedLightDirectory(directory, base), error);
}
static bool
PrefixVisitSong(const char *base, const VisitSong &visit_song,
const LightSong &song, Error &error)
{
return visit_song(PrefixedLightSong(song, base), error);
}
static bool
PrefixVisitPlaylist(const char *base, const VisitPlaylist &visit_playlist,
const PlaylistInfo &playlist,
const LightDirectory &directory,
Error &error)
{
return visit_playlist(playlist,
PrefixedLightDirectory(directory, base),
error);
}
bool
WalkMount(const char *base, const Database &db,
bool recursive, const SongFilter *filter,
const VisitDirectory &visit_directory, const VisitSong &visit_song,
const VisitPlaylist &visit_playlist,
Error &error)
{
using namespace std::placeholders;
VisitDirectory vd;
if (visit_directory)
vd = std::bind(PrefixVisitDirectory,
base, std::ref(visit_directory), _1, _2);
VisitSong vs;
if (visit_song)
vs = std::bind(PrefixVisitSong,
base, std::ref(visit_song), _1, _2);
VisitPlaylist vp;
if (visit_playlist)
vp = std::bind(PrefixVisitPlaylist,
base, std::ref(visit_playlist), _1, _2, _3);
return db.Visit(DatabaseSelection("", recursive, filter),
vd, vs, vp, error);
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2003-2014 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.
*/
#ifndef MPD_DB_SIMPLE_MOUNT_HXX
#define MPD_DB_SIMPLE_MOUNT_HXX
#include "db/Visitor.hxx"
class Database;
class SongFilter;
class Error;
bool
WalkMount(const char *base, const Database &db,
bool recursive, const SongFilter *filter,
const VisitDirectory &visit_directory, const VisitSong &visit_song,
const VisitPlaylist &visit_playlist,
Error &error);
#endif

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2003-2014 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.
*/
#ifndef MPD_DB_SIMPLE_PREFIXED_LIGHT_SONG_HXX
#define MPD_DB_SIMPLE_PREFIXED_LIGHT_SONG_HXX
#include "check.h"
#include "db/LightSong.hxx"
#include "fs/Traits.hxx"
#include <string>
class PrefixedLightSong : public LightSong {
std::string buffer;
public:
PrefixedLightSong(const LightSong &song, const char *base)
:LightSong(song),
buffer(PathTraitsUTF8::Build(base, GetURI().c_str())) {
uri = buffer.c_str();
directory = nullptr;
}
};
#endif

View File

@@ -19,6 +19,7 @@
#include "config.h"
#include "SimpleDatabasePlugin.hxx"
#include "PrefixedLightSong.hxx"
#include "db/DatabasePlugin.hxx"
#include "db/Selection.hxx"
#include "db/Helpers.hxx"
@@ -32,6 +33,7 @@
#include "fs/TextFile.hxx"
#include "config/ConfigData.hxx"
#include "fs/FileSystem.hxx"
#include "util/CharUtil.hxx"
#include "util/Error.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
@@ -42,7 +44,17 @@ static constexpr Domain simple_db_domain("simple_db");
inline SimpleDatabase::SimpleDatabase()
:Database(simple_db_plugin),
path(AllocatedPath::Null()) {}
path(AllocatedPath::Null()),
cache_path(AllocatedPath::Null()),
prefixed_light_song(nullptr) {}
inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path)
:Database(simple_db_plugin),
path(std::move(_path)),
path_utf8(path.ToUTF8()),
cache_path(AllocatedPath::Null()),
prefixed_light_song(nullptr) {
}
Database *
SimpleDatabase::Create(gcc_unused EventLoop &loop,
@@ -71,6 +83,10 @@ SimpleDatabase::Configure(const config_param &param, Error &error)
path_utf8 = path.ToUTF8();
cache_path = param.GetBlockPath("cache_directory", error);
if (path.IsNull() && error.IsDefined())
return false;
return true;
}
@@ -169,6 +185,8 @@ SimpleDatabase::Load(Error &error)
bool
SimpleDatabase::Open(Error &error)
{
assert(prefixed_light_song == nullptr);
root = Directory::NewRoot();
mtime = 0;
@@ -195,6 +213,7 @@ void
SimpleDatabase::Close()
{
assert(root != nullptr);
assert(prefixed_light_song == nullptr);
assert(borrowed_song_count == 0);
delete root;
@@ -204,11 +223,27 @@ const LightSong *
SimpleDatabase::GetSong(const char *uri, Error &error) const
{
assert(root != nullptr);
assert(prefixed_light_song == nullptr);
assert(borrowed_song_count == 0);
db_lock();
auto r = root->LookupDirectory(uri);
if (r.directory->IsMount()) {
/* pass the request to the mounted database */
db_unlock();
const LightSong *song =
r.directory->mounted_database->GetSong(r.uri, error);
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 */
db_unlock();
@@ -245,11 +280,17 @@ SimpleDatabase::GetSong(const char *uri, Error &error) const
void
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
{
assert(song == &light_song);
assert(song != nullptr);
assert(song == &light_song || song == prefixed_light_song);
delete prefixed_light_song;
prefixed_light_song = nullptr;
#ifndef NDEBUG
assert(borrowed_song_count > 0);
--borrowed_song_count;
if (song == &light_song) {
assert(borrowed_song_count > 0);
--borrowed_song_count;
}
#endif
}
@@ -347,6 +388,104 @@ SimpleDatabase::Save(Error &error)
return true;
}
bool
SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
{
assert(uri != nullptr);
assert(*uri != 0);
assert(db != nullptr);
ScopeDatabaseLock protect;
auto r = root->LookupDirectory(uri);
if (r.uri == nullptr) {
error.Format(db_domain, DB_CONFLICT,
"Already exists: %s", uri);
return nullptr;
}
if (strchr(r.uri, '/') != nullptr) {
error.Format(db_domain, DB_NOT_FOUND,
"Parent not found: %s", uri);
return nullptr;
}
Directory *mnt = r.directory->CreateChild(r.uri);
mnt->mounted_database = db;
return true;
}
static constexpr bool
IsSafeChar(char ch)
{
return IsAlphaNumericASCII(ch) || ch == '-' || ch == '_' || ch == '%';
}
static constexpr bool
IsUnsafeChar(char ch)
{
return !IsSafeChar(ch);
}
bool
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri,
Error &error)
{
if (cache_path.IsNull()) {
error.Format(db_domain, DB_NOT_FOUND,
"No 'cache_directory' configured");
return nullptr;
}
std::string name(storage_uri);
std::replace_if(name.begin(), name.end(), IsUnsafeChar, '_');
auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
name.c_str()));
if (!db->Open(error)) {
delete db;
return false;
}
// TODO: update the new database instance?
if (!Mount(local_uri, db, error)) {
db->Close();
delete db;
return false;
}
return true;
}
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,

View File

@@ -32,15 +32,27 @@ struct Directory;
struct DatabasePlugin;
class EventLoop;
class DatabaseListener;
class PrefixedLightSong;
class SimpleDatabase : public Database {
AllocatedPath path;
std::string path_utf8;
/**
* The path where cache files for Mount() are located.
*/
AllocatedPath cache_path;
Directory *root;
time_t mtime;
/**
* A buffer for GetSong() when prefixing the #LightSong
* instance from a mounted #Database.
*/
mutable PrefixedLightSong *prefixed_light_song;
/**
* A buffer for GetSong().
*/
@@ -52,6 +64,8 @@ class SimpleDatabase : public Database {
SimpleDatabase();
SimpleDatabase(AllocatedPath &&_path);
public:
static Database *Create(EventLoop &loop, DatabaseListener &listener,
const config_param &param,
@@ -73,6 +87,20 @@ public:
return mtime > 0;
}
/**
* @param db the #Database to be mounted; must be "open"; on
* success, this object gains ownership of the given #Database
*/
gcc_nonnull_all
bool Mount(const char *uri, Database *db, Error &error);
gcc_nonnull_all
bool Mount(const char *local_uri, const char *storage_uri,
Error &error);
gcc_nonnull_all
bool Unmount(const char *uri);
/* virtual methods from class Database */
virtual bool Open(Error &error) override;
virtual void Close() override;
@@ -107,6 +135,8 @@ private:
bool Check(Error &error) const;
bool Load(Error &error);
Database *LockUmountSteal(const char *uri);
};
extern const DatabasePlugin simple_db_plugin;