aboutsummaryrefslogtreecommitdiffstats
path: root/src/db/plugins/simple
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/db/plugins/simple/DatabaseSave.cxx160
-rw-r--r--src/db/plugins/simple/DatabaseSave.hxx34
-rw-r--r--src/db/plugins/simple/Directory.cxx277
-rw-r--r--src/db/plugins/simple/Directory.hxx285
-rw-r--r--src/db/plugins/simple/DirectorySave.cxx207
-rw-r--r--src/db/plugins/simple/DirectorySave.hxx34
-rw-r--r--src/db/plugins/simple/Mount.cxx96
-rw-r--r--src/db/plugins/simple/Mount.hxx36
-rw-r--r--src/db/plugins/simple/PrefixedLightSong.hxx41
-rw-r--r--src/db/plugins/simple/SimpleDatabasePlugin.cxx544
-rw-r--r--src/db/plugins/simple/SimpleDatabasePlugin.hxx149
-rw-r--r--src/db/plugins/simple/Song.cxx112
-rw-r--r--src/db/plugins/simple/Song.hxx130
-rw-r--r--src/db/plugins/simple/SongSort.cxx108
-rw-r--r--src/db/plugins/simple/SongSort.hxx30
15 files changed, 2243 insertions, 0 deletions
diff --git a/src/db/plugins/simple/DatabaseSave.cxx b/src/db/plugins/simple/DatabaseSave.cxx
new file mode 100644
index 000000000..c766843b6
--- /dev/null
+++ b/src/db/plugins/simple/DatabaseSave.cxx
@@ -0,0 +1,160 @@
+/*
+ * 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 "DatabaseSave.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/DatabaseError.hxx"
+#include "Directory.hxx"
+#include "DirectorySave.hxx"
+#include "fs/io/BufferedOutputStream.hxx"
+#include "fs/io/TextFile.hxx"
+#include "tag/Tag.hxx"
+#include "tag/TagSettings.h"
+#include "fs/Charset.hxx"
+#include "util/StringUtil.hxx"
+#include "util/Error.hxx"
+#include "Log.hxx"
+
+#include <string.h>
+#include <stdlib.h>
+
+#define DIRECTORY_INFO_BEGIN "info_begin"
+#define DIRECTORY_INFO_END "info_end"
+#define DB_FORMAT_PREFIX "format: "
+#define DIRECTORY_MPD_VERSION "mpd_version: "
+#define DIRECTORY_FS_CHARSET "fs_charset: "
+#define DB_TAG_PREFIX "tag: "
+
+static constexpr unsigned DB_FORMAT = 2;
+
+/**
+ * The oldest database format understood by this MPD version.
+ */
+static constexpr unsigned OLDEST_DB_FORMAT = 1;
+
+void
+db_save_internal(BufferedOutputStream &os, const Directory &music_root)
+{
+ os.Format("%s\n", DIRECTORY_INFO_BEGIN);
+ os.Format(DB_FORMAT_PREFIX "%u\n", DB_FORMAT);
+ os.Format("%s%s\n", DIRECTORY_MPD_VERSION, VERSION);
+ os.Format("%s%s\n", DIRECTORY_FS_CHARSET, GetFSCharset());
+
+ for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i)
+ if (!ignore_tag_items[i])
+ os.Format(DB_TAG_PREFIX "%s\n", tag_item_names[i]);
+
+ os.Format("%s\n", DIRECTORY_INFO_END);
+
+ directory_save(os, music_root);
+}
+
+bool
+db_load_internal(TextFile &file, Directory &music_root, Error &error)
+{
+ char *line;
+ unsigned format = 0;
+ bool found_charset = false, found_version = false;
+ bool success;
+ bool tags[TAG_NUM_OF_ITEM_TYPES];
+
+ /* get initial info */
+ line = file.ReadLine();
+ if (line == nullptr || strcmp(DIRECTORY_INFO_BEGIN, line) != 0) {
+ error.Set(db_domain, "Database corrupted");
+ return false;
+ }
+
+ memset(tags, false, sizeof(tags));
+
+ while ((line = file.ReadLine()) != nullptr &&
+ strcmp(line, DIRECTORY_INFO_END) != 0) {
+ if (StringStartsWith(line, DB_FORMAT_PREFIX)) {
+ format = atoi(line + sizeof(DB_FORMAT_PREFIX) - 1);
+ } else if (StringStartsWith(line, DIRECTORY_MPD_VERSION)) {
+ if (found_version) {
+ error.Set(db_domain, "Duplicate version line");
+ return false;
+ }
+
+ found_version = true;
+ } else if (StringStartsWith(line, DIRECTORY_FS_CHARSET)) {
+ const char *new_charset;
+
+ if (found_charset) {
+ error.Set(db_domain, "Duplicate charset line");
+ return false;
+ }
+
+ found_charset = true;
+
+ new_charset = line + sizeof(DIRECTORY_FS_CHARSET) - 1;
+ const char *const old_charset = GetFSCharset();
+ if (*old_charset != 0
+ && strcmp(new_charset, old_charset) != 0) {
+ error.Format(db_domain,
+ "Existing database has charset "
+ "\"%s\" instead of \"%s\"; "
+ "discarding database file",
+ new_charset, old_charset);
+ return false;
+ }
+ } else if (StringStartsWith(line, DB_TAG_PREFIX)) {
+ const char *name = line + sizeof(DB_TAG_PREFIX) - 1;
+ TagType tag = tag_name_parse(name);
+ if (tag == TAG_NUM_OF_ITEM_TYPES) {
+ error.Format(db_domain,
+ "Unrecognized tag '%s', "
+ "discarding database file",
+ name);
+ return false;
+ }
+
+ tags[tag] = true;
+ } else {
+ error.Format(db_domain, "Malformed line: %s", line);
+ return false;
+ }
+ }
+
+ if (format < OLDEST_DB_FORMAT || format > DB_FORMAT) {
+ error.Set(db_domain,
+ "Database format mismatch, "
+ "discarding database file");
+ return false;
+ }
+
+ for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) {
+ if (!ignore_tag_items[i] && !tags[i]) {
+ error.Set(db_domain,
+ "Tag list mismatch, "
+ "discarding database file");
+ return false;
+ }
+ }
+
+ LogDebug(db_domain, "reading DB");
+
+ db_lock();
+ success = directory_load(file, music_root, error);
+ db_unlock();
+
+ return success;
+}
diff --git a/src/db/plugins/simple/DatabaseSave.hxx b/src/db/plugins/simple/DatabaseSave.hxx
new file mode 100644
index 000000000..bb7f57115
--- /dev/null
+++ b/src/db/plugins/simple/DatabaseSave.hxx
@@ -0,0 +1,34 @@
+/*
+ * 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_DATABASE_SAVE_HXX
+#define MPD_DATABASE_SAVE_HXX
+
+struct Directory;
+class BufferedOutputStream;
+class TextFile;
+class Error;
+
+void
+db_save_internal(BufferedOutputStream &os, const Directory &root);
+
+bool
+db_load_internal(TextFile &file, Directory &root, Error &error);
+
+#endif
diff --git a/src/db/plugins/simple/Directory.cxx b/src/db/plugins/simple/Directory.cxx
new file mode 100644
index 000000000..218652b03
--- /dev/null
+++ b/src/db/plugins/simple/Directory.cxx
@@ -0,0 +1,277 @@
+/*
+ * 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 "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"
+#include "util/Alloc.hxx"
+#include "util/Error.hxx"
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+Directory::Directory(std::string &&_path_utf8, Directory *_parent)
+ :parent(_parent),
+ mtime(0),
+ inode(0), device(0),
+ path(std::move(_path_utf8)),
+ mounted_database(nullptr)
+{
+}
+
+Directory::~Directory()
+{
+ delete mounted_database;
+
+ songs.clear_and_dispose(Song::Disposer());
+ children.clear_and_dispose(Disposer());
+}
+
+void
+Directory::Delete()
+{
+ assert(holding_db_lock());
+ assert(parent != nullptr);
+
+ parent->children.erase_and_dispose(parent->children.iterator_to(*this),
+ Disposer());
+}
+
+const char *
+Directory::GetName() const
+{
+ assert(!IsRoot());
+
+ return PathTraitsUTF8::GetBase(path.c_str());
+}
+
+Directory *
+Directory::CreateChild(const char *name_utf8)
+{
+ assert(holding_db_lock());
+ assert(name_utf8 != nullptr);
+ assert(*name_utf8 != 0);
+
+ std::string path_utf8 = IsRoot()
+ ? std::string(name_utf8)
+ : PathTraitsUTF8::Build(GetPath(), name_utf8);
+
+ Directory *child = new Directory(std::move(path_utf8), this);
+ children.push_back(*child);
+ return child;
+}
+
+const Directory *
+Directory::FindChild(const char *name) const
+{
+ assert(holding_db_lock());
+
+ for (const auto &child : children)
+ if (strcmp(child.GetName(), name) == 0)
+ return &child;
+
+ return nullptr;
+}
+
+void
+Directory::PruneEmpty()
+{
+ assert(holding_db_lock());
+
+ for (auto child = children.begin(), end = children.end();
+ child != end;) {
+ child->PruneEmpty();
+
+ if (child->IsEmpty())
+ child = children.erase_and_dispose(child, Disposer());
+ else
+ ++child;
+ }
+}
+
+Directory::LookupResult
+Directory::LookupDirectory(const char *uri)
+{
+ assert(holding_db_lock());
+ assert(uri != nullptr);
+
+ if (isRootDirectory(uri))
+ return { this, nullptr };
+
+ char *duplicated = xstrdup(uri), *name = duplicated;
+
+ Directory *d = this;
+ while (true) {
+ char *slash = strchr(name, '/');
+ if (slash == name)
+ break;
+
+ if (slash != nullptr)
+ *slash = '\0';
+
+ Directory *tmp = d->FindChild(name);
+ if (tmp == nullptr)
+ /* not found */
+ break;
+
+ d = tmp;
+
+ if (slash == nullptr) {
+ /* found everything */
+ name = nullptr;
+ break;
+ }
+
+ name = slash + 1;
+ }
+
+ free(duplicated);
+
+ const char *rest = name == nullptr
+ ? nullptr
+ : uri + (name - duplicated);
+
+ return { d, rest };
+}
+
+void
+Directory::AddSong(Song *song)
+{
+ assert(holding_db_lock());
+ assert(song != nullptr);
+ assert(song->parent == this);
+
+ songs.push_back(*song);
+}
+
+void
+Directory::RemoveSong(Song *song)
+{
+ assert(holding_db_lock());
+ assert(song != nullptr);
+ assert(song->parent == this);
+
+ songs.erase(songs.iterator_to(*song));
+}
+
+const Song *
+Directory::FindSong(const char *name_utf8) const
+{
+ assert(holding_db_lock());
+ assert(name_utf8 != nullptr);
+
+ for (auto &song : songs) {
+ assert(song.parent == this);
+
+ if (strcmp(song.uri, name_utf8) == 0)
+ return &song;
+ }
+
+ return nullptr;
+}
+
+gcc_pure
+static bool
+directory_cmp(const Directory &a, const Directory &b)
+{
+ return IcuCollate(a.path.c_str(), b.path.c_str()) < 0;
+}
+
+void
+Directory::Sort()
+{
+ assert(holding_db_lock());
+
+ children.sort(directory_cmp);
+ song_list_sort(songs);
+
+ for (auto &child : children)
+ child.Sort();
+}
+
+bool
+Directory::Walk(bool recursive, const SongFilter *filter,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ 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) {
+ for (auto &song : songs){
+ const LightSong song2 = song.Export();
+ if ((filter == nullptr || filter->Match(song2)) &&
+ !visit_song(song2, error))
+ return false;
+ }
+ }
+
+ if (visit_playlist) {
+ for (const PlaylistInfo &p : playlists)
+ if (!visit_playlist(p, Export(), error))
+ return false;
+ }
+
+ for (auto &child : children) {
+ if (visit_directory &&
+ !visit_directory(child.Export(), error))
+ return false;
+
+ if (recursive &&
+ !child.Walk(recursive, filter,
+ visit_directory, visit_song, visit_playlist,
+ error))
+ return false;
+ }
+
+ return true;
+}
+
+LightDirectory
+Directory::Export() const
+{
+ return LightDirectory(GetPath(), mtime);
+}
diff --git a/src/db/plugins/simple/Directory.hxx b/src/db/plugins/simple/Directory.hxx
new file mode 100644
index 000000000..acef62143
--- /dev/null
+++ b/src/db/plugins/simple/Directory.hxx
@@ -0,0 +1,285 @@
+/*
+ * 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_DIRECTORY_HXX
+#define MPD_DIRECTORY_HXX
+
+#include "check.h"
+#include "Compiler.h"
+#include "db/Visitor.hxx"
+#include "db/PlaylistVector.hxx"
+#include "Song.hxx"
+
+#include <boost/intrusive/list.hpp>
+
+#include <string>
+
+/**
+ * Virtual directory that is really an archive file or a folder inside
+ * the archive (special value for Directory::device).
+ */
+static constexpr unsigned DEVICE_INARCHIVE = -1;
+
+/**
+ * Virtual directory that is really a song file with one or more "sub"
+ * songs as specified by DecoderPlugin::container_scan() (special
+ * value for Directory::device).
+ */
+static constexpr unsigned DEVICE_CONTAINER = -2;
+
+struct db_visitor;
+class SongFilter;
+class Error;
+class Database;
+
+struct Directory {
+ static constexpr auto link_mode = boost::intrusive::normal_link;
+ typedef boost::intrusive::link_mode<link_mode> LinkMode;
+ typedef boost::intrusive::list_member_hook<LinkMode> Hook;
+
+ struct Disposer {
+ void operator()(Directory *directory) const {
+ delete directory;
+ }
+ };
+
+ /**
+ * Pointers to the siblings of this directory within the
+ * parent directory. It is unused (undefined) in the root
+ * directory.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ Hook siblings;
+
+ typedef boost::intrusive::member_hook<Directory, Hook,
+ &Directory::siblings> SiblingsHook;
+ typedef boost::intrusive::list<Directory, SiblingsHook,
+ boost::intrusive::constant_time_size<false>> List;
+
+ /**
+ * A doubly linked list of child directories.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ List children;
+
+ /**
+ * A doubly linked list of songs within this directory.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ SongList songs;
+
+ PlaylistVector playlists;
+
+ Directory *parent;
+ time_t mtime;
+ unsigned inode, device;
+
+ 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();
+
+ /**
+ * Create a new root #Directory object.
+ */
+ gcc_malloc
+ static Directory *NewRoot() {
+ 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.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ void Delete();
+
+ /**
+ * Create a new #Directory object as a child of the given one.
+ *
+ * Caller must lock the #db_mutex.
+ *
+ * @param name_utf8 the UTF-8 encoded name of the new sub directory
+ */
+ gcc_malloc
+ Directory *CreateChild(const char *name_utf8);
+
+ /**
+ * Caller must lock the #db_mutex.
+ */
+ gcc_pure
+ const Directory *FindChild(const char *name) const;
+
+ gcc_pure
+ Directory *FindChild(const char *name) {
+ const Directory *cthis = this;
+ return const_cast<Directory *>(cthis->FindChild(name));
+ }
+
+ /**
+ * Look up a sub directory, and create the object if it does not
+ * exist.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ Directory *MakeChild(const char *name_utf8) {
+ Directory *child = FindChild(name_utf8);
+ if (child == nullptr)
+ child = CreateChild(name_utf8);
+ return child;
+ }
+
+ struct LookupResult {
+ /**
+ * The last directory that was found. If the given
+ * URI could not be resolved at all, then this is the
+ * root directory.
+ */
+ Directory *directory;
+
+ /**
+ * The remaining URI part (without leading slash) or
+ * nullptr if the given URI was consumed completely.
+ */
+ const char *uri;
+ };
+
+ /**
+ * Looks up a directory by its relative URI.
+ *
+ * @param uri the relative URI
+ * @return the Directory, or nullptr if none was found
+ */
+ gcc_pure
+ LookupResult LookupDirectory(const char *uri);
+
+ gcc_pure
+ bool IsEmpty() const {
+ return children.empty() &&
+ songs.empty() &&
+ playlists.empty();
+ }
+
+ gcc_pure
+ const char *GetPath() const {
+ return path.c_str();
+ }
+
+ /**
+ * Returns the base name of the directory.
+ */
+ gcc_pure
+ const char *GetName() const;
+
+ /**
+ * Is this the root directory of the music database?
+ */
+ gcc_pure
+ bool IsRoot() const {
+ return parent == nullptr;
+ }
+
+ template<typename T>
+ void ForEachChildSafe(T &&t) {
+ const auto end = children.end();
+ for (auto i = children.begin(), next = i; i != end; i = next) {
+ next = std::next(i);
+ t(*i);
+ }
+ }
+
+ template<typename T>
+ void ForEachSongSafe(T &&t) {
+ const auto end = songs.end();
+ for (auto i = songs.begin(), next = i; i != end; i = next) {
+ next = std::next(i);
+ t(*i);
+ }
+ }
+
+ /**
+ * Look up a song in this directory by its name.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ gcc_pure
+ const Song *FindSong(const char *name_utf8) const;
+
+ gcc_pure
+ Song *FindSong(const char *name_utf8) {
+ const Directory *cthis = this;
+ return const_cast<Song *>(cthis->FindSong(name_utf8));
+ }
+
+ /**
+ * Add a song object to this directory. Its "parent" attribute must
+ * be set already.
+ */
+ void AddSong(Song *song);
+
+ /**
+ * Remove a song object from this directory (which effectively
+ * invalidates the song object, because the "parent" attribute becomes
+ * stale), but does not free it.
+ */
+ void RemoveSong(Song *song);
+
+ /**
+ * Caller must lock the #db_mutex.
+ */
+ void PruneEmpty();
+
+ /**
+ * Sort all directory entries recursively.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ void Sort();
+
+ /**
+ * Caller must lock #db_mutex.
+ */
+ bool Walk(bool recursive, const SongFilter *match,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const;
+
+ gcc_pure
+ LightDirectory Export() const;
+};
+
+#endif
diff --git a/src/db/plugins/simple/DirectorySave.cxx b/src/db/plugins/simple/DirectorySave.cxx
new file mode 100644
index 000000000..e1650cbe8
--- /dev/null
+++ b/src/db/plugins/simple/DirectorySave.cxx
@@ -0,0 +1,207 @@
+/*
+ * 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 "DirectorySave.hxx"
+#include "Directory.hxx"
+#include "Song.hxx"
+#include "SongSave.hxx"
+#include "DetachedSong.hxx"
+#include "PlaylistDatabase.hxx"
+#include "fs/io/TextFile.hxx"
+#include "fs/io/BufferedOutputStream.hxx"
+#include "util/StringUtil.hxx"
+#include "util/NumberParser.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+
+#include <stddef.h>
+#include <string.h>
+
+#define DIRECTORY_DIR "directory: "
+#define DIRECTORY_TYPE "type: "
+#define DIRECTORY_MTIME "mtime: "
+#define DIRECTORY_BEGIN "begin: "
+#define DIRECTORY_END "end: "
+
+static constexpr Domain directory_domain("directory");
+
+gcc_const
+static const char *
+DeviceToTypeString(unsigned device)
+{
+ switch (device) {
+ case DEVICE_INARCHIVE:
+ return "archive";
+
+ case DEVICE_CONTAINER:
+ return "container";
+
+ default:
+ return nullptr;
+ }
+}
+
+gcc_pure
+static unsigned
+ParseTypeString(const char *type)
+{
+ if (strcmp(type, "archive") == 0)
+ return DEVICE_INARCHIVE;
+ else if (strcmp(type, "container") == 0)
+ return DEVICE_CONTAINER;
+ else
+ return 0;
+}
+
+void
+directory_save(BufferedOutputStream &os, const Directory &directory)
+{
+ if (!directory.IsRoot()) {
+ const char *type = DeviceToTypeString(directory.device);
+ if (type != nullptr)
+ os.Format(DIRECTORY_TYPE "%s\n", type);
+
+ if (directory.mtime != 0)
+ os.Format(DIRECTORY_MTIME "%lu\n",
+ (unsigned long)directory.mtime);
+
+ os.Format("%s%s\n", DIRECTORY_BEGIN, directory.GetPath());
+ }
+
+ for (const auto &child : directory.children) {
+ os.Format(DIRECTORY_DIR "%s\n", child.GetName());
+
+ if (!child.IsMount())
+ directory_save(os, child);
+
+ if (!os.Check())
+ return;
+ }
+
+ for (const auto &song : directory.songs)
+ song_save(os, song);
+
+ playlist_vector_save(os, directory.playlists);
+
+ if (!directory.IsRoot())
+ os.Format(DIRECTORY_END "%s\n", directory.GetPath());
+}
+
+static bool
+ParseLine(Directory &directory, const char *line)
+{
+ if (StringStartsWith(line, DIRECTORY_MTIME)) {
+ directory.mtime =
+ ParseUint64(line + sizeof(DIRECTORY_MTIME) - 1);
+ } else if (StringStartsWith(line, DIRECTORY_TYPE)) {
+ directory.device =
+ ParseTypeString(line + sizeof(DIRECTORY_TYPE) - 1);
+ } else
+ return false;
+
+ return true;
+}
+
+static Directory *
+directory_load_subdir(TextFile &file, Directory &parent, const char *name,
+ Error &error)
+{
+ bool success;
+
+ if (parent.FindChild(name) != nullptr) {
+ error.Format(directory_domain,
+ "Duplicate subdirectory '%s'", name);
+ return nullptr;
+ }
+
+ Directory *directory = parent.CreateChild(name);
+
+ while (true) {
+ const char *line = file.ReadLine();
+ if (line == nullptr) {
+ error.Set(directory_domain, "Unexpected end of file");
+ directory->Delete();
+ return nullptr;
+ }
+
+ if (StringStartsWith(line, DIRECTORY_BEGIN))
+ break;
+
+ if (!ParseLine(*directory, line)) {
+ error.Format(directory_domain,
+ "Malformed line: %s", line);
+ directory->Delete();
+ return nullptr;
+ }
+ }
+
+ success = directory_load(file, *directory, error);
+ if (!success) {
+ directory->Delete();
+ return nullptr;
+ }
+
+ return directory;
+}
+
+bool
+directory_load(TextFile &file, Directory &directory, Error &error)
+{
+ const char *line;
+
+ while ((line = file.ReadLine()) != nullptr &&
+ !StringStartsWith(line, DIRECTORY_END)) {
+ if (StringStartsWith(line, DIRECTORY_DIR)) {
+ Directory *subdir =
+ directory_load_subdir(file, directory,
+ line + sizeof(DIRECTORY_DIR) - 1,
+ error);
+ if (subdir == nullptr)
+ return false;
+ } else if (StringStartsWith(line, SONG_BEGIN)) {
+ const char *name = line + sizeof(SONG_BEGIN) - 1;
+
+ if (directory.FindSong(name) != nullptr) {
+ error.Format(directory_domain,
+ "Duplicate song '%s'", name);
+ return false;
+ }
+
+ DetachedSong *song = song_load(file, name, error);
+ if (song == nullptr)
+ return false;
+
+ directory.AddSong(Song::NewFrom(std::move(*song),
+ directory));
+ delete song;
+ } else if (StringStartsWith(line, PLAYLIST_META_BEGIN)) {
+ const char *name = line + sizeof(PLAYLIST_META_BEGIN) - 1;
+ if (!playlist_metadata_load(file, directory.playlists,
+ name, error))
+ return false;
+ } else {
+ error.Format(directory_domain,
+ "Malformed line: %s", line);
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/db/plugins/simple/DirectorySave.hxx b/src/db/plugins/simple/DirectorySave.hxx
new file mode 100644
index 000000000..f464f9946
--- /dev/null
+++ b/src/db/plugins/simple/DirectorySave.hxx
@@ -0,0 +1,34 @@
+/*
+ * 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_DIRECTORY_SAVE_HXX
+#define MPD_DIRECTORY_SAVE_HXX
+
+struct Directory;
+class TextFile;
+class BufferedOutputStream;
+class Error;
+
+void
+directory_save(BufferedOutputStream &os, const Directory &directory);
+
+bool
+directory_load(TextFile &file, Directory &directory, Error &error);
+
+#endif
diff --git a/src/db/plugins/simple/Mount.cxx b/src/db/plugins/simple/Mount.cxx
new file mode 100644
index 000000000..96c7bbb5c
--- /dev/null
+++ b/src/db/plugins/simple/Mount.cxx
@@ -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);
+}
diff --git a/src/db/plugins/simple/Mount.hxx b/src/db/plugins/simple/Mount.hxx
new file mode 100644
index 000000000..a4690114c
--- /dev/null
+++ b/src/db/plugins/simple/Mount.hxx
@@ -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
diff --git a/src/db/plugins/simple/PrefixedLightSong.hxx b/src/db/plugins/simple/PrefixedLightSong.hxx
new file mode 100644
index 000000000..3664de001
--- /dev/null
+++ b/src/db/plugins/simple/PrefixedLightSong.hxx
@@ -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
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
new file mode 100644
index 000000000..d6ad5e91f
--- /dev/null
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -0,0 +1,544 @@
+/*
+ * 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 "SimpleDatabasePlugin.hxx"
+#include "PrefixedLightSong.hxx"
+#include "db/DatabasePlugin.hxx"
+#include "db/Selection.hxx"
+#include "db/Helpers.hxx"
+#include "db/UniqueTags.hxx"
+#include "db/LightDirectory.hxx"
+#include "Directory.hxx"
+#include "Song.hxx"
+#include "SongFilter.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 "config/ConfigData.hxx"
+#include "fs/FileSystem.hxx"
+#include "util/CharUtil.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#ifdef HAVE_ZLIB
+#include "fs/io/GzipOutputStream.hxx"
+#endif
+
+#include <errno.h>
+
+static constexpr Domain simple_db_domain("simple_db");
+
+inline SimpleDatabase::SimpleDatabase()
+ :Database(simple_db_plugin),
+ path(AllocatedPath::Null()),
+#ifdef HAVE_ZLIB
+ compress(true),
+#endif
+ cache_path(AllocatedPath::Null()),
+ prefixed_light_song(nullptr) {}
+
+inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path,
+#ifndef HAVE_ZLIB
+ gcc_unused
+#endif
+ bool _compress)
+ :Database(simple_db_plugin),
+ path(std::move(_path)),
+ path_utf8(path.ToUTF8()),
+#ifdef HAVE_ZLIB
+ compress(_compress),
+#endif
+ cache_path(AllocatedPath::Null()),
+ prefixed_light_song(nullptr) {
+}
+
+Database *
+SimpleDatabase::Create(gcc_unused EventLoop &loop,
+ gcc_unused DatabaseListener &listener,
+ const config_param &param, Error &error)
+{
+ SimpleDatabase *db = new SimpleDatabase();
+ if (!db->Configure(param, error)) {
+ delete db;
+ db = nullptr;
+ }
+
+ return db;
+}
+
+bool
+SimpleDatabase::Configure(const config_param &param, Error &error)
+{
+ path = param.GetBlockPath("path", error);
+ if (path.IsNull()) {
+ if (!error.IsDefined())
+ error.Set(simple_db_domain,
+ "No \"path\" parameter specified");
+ return false;
+ }
+
+ path_utf8 = path.ToUTF8();
+
+ cache_path = param.GetBlockPath("cache_directory", error);
+ if (path.IsNull() && error.IsDefined())
+ return false;
+
+#ifdef HAVE_ZLIB
+ compress = param.GetBlockValue("compress", compress);
+#endif
+
+ return true;
+}
+
+bool
+SimpleDatabase::Check(Error &error) const
+{
+ assert(!path.IsNull());
+
+ /* Check if the file exists */
+ if (!CheckAccess(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 */
+ struct stat st;
+ if (!StatFile(dirPath, st)) {
+ error.FormatErrno("Couldn't stat parent directory of db file "
+ "\"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ error.Format(simple_db_domain,
+ "Couldn't create db file \"%s\" because the "
+ "parent path is not a directory",
+ path_utf8.c_str());
+ return false;
+ }
+
+#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();
+ error.FormatErrno(e, "Can't create db file in \"%s\"",
+ dirPath_utf8.c_str());
+ return false;
+ }
+#endif
+ return true;
+ }
+
+ /* Path exists, now check if it's a regular file */
+ struct stat st;
+ if (!StatFile(path, st)) {
+ error.FormatErrno("Couldn't stat db file \"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ if (!S_ISREG(st.st_mode)) {
+ error.Format(simple_db_domain,
+ "db file \"%s\" is not a regular file",
+ path_utf8.c_str());
+ return false;
+ }
+
+#ifndef WIN32
+ /* And check that we can write to it */
+ if (!CheckAccess(path, R_OK | W_OK)) {
+ error.FormatErrno("Can't open db file \"%s\" for reading/writing",
+ path_utf8.c_str());
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+bool
+SimpleDatabase::Load(Error &error)
+{
+ assert(!path.IsNull());
+ assert(root != nullptr);
+
+ TextFile file(path, error);
+ if (file.HasFailed())
+ return false;
+
+ if (!db_load_internal(file, *root, error) || !file.Check(error))
+ return false;
+
+ struct stat st;
+ if (StatFile(path, st))
+ mtime = st.st_mtime;
+
+ return true;
+}
+
+bool
+SimpleDatabase::Open(Error &error)
+{
+ assert(prefixed_light_song == nullptr);
+
+ root = Directory::NewRoot();
+ mtime = 0;
+
+#ifndef NDEBUG
+ borrowed_song_count = 0;
+#endif
+
+ if (!Load(error)) {
+ delete root;
+
+ LogError(error);
+ error.Clear();
+
+ if (!Check(error))
+ return false;
+
+ root = Directory::NewRoot();
+ }
+
+ return true;
+}
+
+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, 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();
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such song: %s", uri);
+ return nullptr;
+ }
+
+ if (strchr(r.uri, '/') != nullptr) {
+ /* refers to a URI "below" the actual song */
+ db_unlock();
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such song: %s", uri);
+ return nullptr;
+ }
+
+ const Song *song = r.directory->FindSong(r.uri);
+ db_unlock();
+ if (song == nullptr) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such song: %s", uri);
+ return nullptr;
+ }
+
+ 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
+}
+
+bool
+SimpleDatabase::Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ ScopeDatabaseLock protect;
+
+ auto r = root->LookupDirectory(selection.uri.c_str());
+ if (r.uri == nullptr) {
+ /* it's a directory */
+
+ if (selection.recursive && visit_directory &&
+ !visit_directory(r.directory->Export(), error))
+ return false;
+
+ return r.directory->Walk(selection.recursive, selection.filter,
+ visit_directory, visit_song,
+ visit_playlist,
+ error);
+ }
+
+ if (strchr(r.uri, '/') == nullptr) {
+ if (visit_song) {
+ Song *song = r.directory->FindSong(r.uri);
+ if (song != nullptr) {
+ const LightSong song2 = song->Export();
+ return !selection.Match(song2) ||
+ visit_song(song2, error);
+ }
+ }
+ }
+
+ error.Set(db_domain, DB_NOT_FOUND, "No such directory");
+ return false;
+}
+
+bool
+SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const
+{
+ return ::VisitUniqueTags(*this, selection, tag_type, group_mask,
+ visit_tag,
+ error);
+}
+
+bool
+SimpleDatabase::GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats, Error &error) const
+{
+ return ::GetStats(*this, selection, stats, error);
+}
+
+bool
+SimpleDatabase::Save(Error &error)
+{
+ db_lock();
+
+ LogDebug(simple_db_domain, "removing empty directories from DB");
+ root->PruneEmpty();
+
+ LogDebug(simple_db_domain, "sorting DB");
+ root->Sort();
+
+ db_unlock();
+
+ LogDebug(simple_db_domain, "writing DB");
+
+ FileOutputStream fos(path, error);
+ if (!fos.IsDefined())
+ return false;
+
+ OutputStream *os = &fos;
+
+#ifdef HAVE_ZLIB
+ GzipOutputStream *gzip = nullptr;
+ if (compress) {
+ gzip = new GzipOutputStream(*os, error);
+ if (!gzip->IsDefined()) {
+ delete gzip;
+ return false;
+ }
+
+ os = gzip;
+ }
+#endif
+
+ BufferedOutputStream bos(*os);
+
+ db_save_internal(bos, *root);
+
+ if (!bos.Flush(error)) {
+#ifdef HAVE_ZLIB
+ delete gzip;
+#endif
+ return false;
+ }
+
+#ifdef HAVE_ZLIB
+ if (gzip != nullptr) {
+ bool success = gzip->Flush(error);
+ delete gzip;
+ if (!success)
+ return false;
+ }
+#endif
+
+ if (!fos.Commit(error))
+ return false;
+
+ struct stat st;
+ if (StatFile(path, st))
+ mtime = st.st_mtime;
+
+ return true;
+}
+
+bool
+SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
+{
+#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) {
+ error.Format(db_domain, DB_CONFLICT,
+ "Already exists: %s", uri);
+ return false;
+ }
+
+ if (strchr(r.uri, '/') != nullptr) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "Parent not found: %s", uri);
+ return false;
+ }
+
+ 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 false;
+ }
+
+ std::string name(storage_uri);
+ std::replace_if(name.begin(), name.end(), IsUnsafeChar, '_');
+
+#ifndef HAVE_ZLIB
+ constexpr bool compress = false;
+#endif
+ auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
+ name.c_str()),
+ compress);
+ 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,
+ SimpleDatabase::Create,
+};
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.hxx b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
new file mode 100644
index 000000000..d82225f8c
--- /dev/null
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
@@ -0,0 +1,149 @@
+/*
+ * 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_SIMPLE_DATABASE_PLUGIN_HXX
+#define MPD_SIMPLE_DATABASE_PLUGIN_HXX
+
+#include "check.h"
+#include "db/Interface.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "db/LightSong.hxx"
+#include "Compiler.h"
+
+#include <cassert>
+
+struct config_param;
+struct Directory;
+struct DatabasePlugin;
+class EventLoop;
+class DatabaseListener;
+class PrefixedLightSong;
+
+class SimpleDatabase : public Database {
+ AllocatedPath path;
+ std::string path_utf8;
+
+#ifdef HAVE_ZLIB
+ bool compress;
+#endif
+
+ /**
+ * 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().
+ */
+ mutable LightSong light_song;
+
+#ifndef NDEBUG
+ mutable unsigned borrowed_song_count;
+#endif
+
+ SimpleDatabase();
+
+ SimpleDatabase(AllocatedPath &&_path, bool _compress);
+
+public:
+ static Database *Create(EventLoop &loop, DatabaseListener &listener,
+ const config_param &param,
+ Error &error);
+
+ gcc_pure
+ Directory &GetRoot() {
+ assert(root != NULL);
+
+ return *root;
+ }
+
+ bool Save(Error &error);
+
+ /**
+ * Returns true if there is a valid database file on the disk.
+ */
+ bool FileExists() const {
+ 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;
+
+ const LightSong *GetSong(const char *uri_utf8,
+ Error &error) const override;
+ void ReturnSong(const LightSong *song) const override;
+
+ virtual bool Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const override;
+
+ virtual bool VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const override;
+
+ virtual bool GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats,
+ Error &error) const override;
+
+ virtual time_t GetUpdateStamp() const override {
+ return mtime;
+ }
+
+private:
+ bool Configure(const config_param &param, Error &error);
+
+ gcc_pure
+ bool Check(Error &error) const;
+
+ bool Load(Error &error);
+
+ Database *LockUmountSteal(const char *uri);
+};
+
+extern const DatabasePlugin simple_db_plugin;
+
+#endif
diff --git a/src/db/plugins/simple/Song.cxx b/src/db/plugins/simple/Song.cxx
new file mode 100644
index 000000000..fbfc2ec19
--- /dev/null
+++ b/src/db/plugins/simple/Song.cxx
@@ -0,0 +1,112 @@
+/*
+ * 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 "Song.hxx"
+#include "Directory.hxx"
+#include "tag/Tag.hxx"
+#include "util/VarSize.hxx"
+#include "DetachedSong.hxx"
+#include "db/LightSong.hxx"
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+inline Song::Song(const char *_uri, size_t uri_length, Directory &_parent)
+ :parent(&_parent), mtime(0),
+ start_time(SongTime::zero()), end_time(SongTime::zero())
+{
+ memcpy(uri, _uri, uri_length + 1);
+}
+
+inline Song::~Song()
+{
+}
+
+static Song *
+song_alloc(const char *uri, Directory &parent)
+{
+ size_t uri_length;
+
+ assert(uri);
+ uri_length = strlen(uri);
+ assert(uri_length);
+
+ return NewVarSize<Song>(sizeof(Song::uri),
+ uri_length + 1,
+ uri, uri_length, parent);
+}
+
+Song *
+Song::NewFrom(DetachedSong &&other, Directory &parent)
+{
+ Song *song = song_alloc(other.GetURI(), parent);
+ song->tag = std::move(other.WritableTag());
+ song->mtime = other.GetLastModified();
+ song->start_time = other.GetStartTime();
+ song->end_time = other.GetEndTime();
+ return song;
+}
+
+Song *
+Song::NewFile(const char *path, Directory &parent)
+{
+ return song_alloc(path, parent);
+}
+
+void
+Song::Free()
+{
+ DeleteVarSize(this);
+}
+
+std::string
+Song::GetURI() const
+{
+ assert(*uri);
+
+ if (parent->IsRoot())
+ return std::string(uri);
+ else {
+ const char *path = parent->GetPath();
+
+ std::string result;
+ result.reserve(strlen(path) + 1 + strlen(uri));
+ result.assign(path);
+ result.push_back('/');
+ result.append(uri);
+ return result;
+ }
+}
+
+LightSong
+Song::Export() const
+{
+ LightSong dest;
+ dest.directory = parent->IsRoot()
+ ? nullptr : parent->GetPath();
+ dest.uri = uri;
+ dest.real_uri = nullptr;
+ dest.tag = &tag;
+ dest.mtime = mtime;
+ dest.start_time = start_time;
+ dest.end_time = end_time;
+ return dest;
+}
diff --git a/src/db/plugins/simple/Song.hxx b/src/db/plugins/simple/Song.hxx
new file mode 100644
index 000000000..9f3a4a3ef
--- /dev/null
+++ b/src/db/plugins/simple/Song.hxx
@@ -0,0 +1,130 @@
+/*
+ * 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_SONG_HXX
+#define MPD_SONG_HXX
+
+#include "Chrono.hxx"
+#include "tag/Tag.hxx"
+#include "Compiler.h"
+
+#include <boost/intrusive/list.hpp>
+
+#include <string>
+
+#include <assert.h>
+#include <time.h>
+
+struct LightSong;
+struct Directory;
+class DetachedSong;
+class Storage;
+
+/**
+ * A song file inside the configured music directory. Internal
+ * #SimpleDatabase class.
+ */
+struct Song {
+ static constexpr auto link_mode = boost::intrusive::normal_link;
+ typedef boost::intrusive::link_mode<link_mode> LinkMode;
+ typedef boost::intrusive::list_member_hook<LinkMode> Hook;
+
+ struct Disposer {
+ void operator()(Song *song) const {
+ song->Free();
+ }
+ };
+
+ /**
+ * Pointers to the siblings of this directory within the
+ * parent directory. It is unused (undefined) if this song is
+ * not in the database.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ Hook siblings;
+
+ Tag tag;
+
+ /**
+ * The #Directory that contains this song. Must be
+ * non-nullptr. directory this way.
+ */
+ Directory *const parent;
+
+ time_t mtime;
+
+ /**
+ * Start of this sub-song within the file.
+ */
+ SongTime start_time;
+
+ /**
+ * End of this sub-song within the file.
+ * Unused if zero.
+ */
+ SongTime end_time;
+
+ /**
+ * The file name.
+ */
+ char uri[sizeof(int)];
+
+ Song(const char *_uri, size_t uri_length, Directory &parent);
+ ~Song();
+
+ gcc_malloc
+ static Song *NewFrom(DetachedSong &&other, Directory &parent);
+
+ /** allocate a new song with a local file name */
+ gcc_malloc
+ static Song *NewFile(const char *path_utf8, Directory &parent);
+
+ /**
+ * allocate a new song structure with a local file name and attempt to
+ * load its metadata. If all decoder plugin fail to read its meta
+ * data, nullptr is returned.
+ */
+ gcc_malloc
+ static Song *LoadFile(Storage &storage, const char *name_utf8,
+ Directory &parent);
+
+ void Free();
+
+ bool UpdateFile(Storage &storage);
+ bool UpdateFileInArchive(const Storage &storage);
+
+ /**
+ * Returns the URI of the song in UTF-8 encoding, including its
+ * location within the music directory.
+ */
+ gcc_pure
+ std::string GetURI() const;
+
+ gcc_pure
+ LightSong Export() const;
+};
+
+typedef boost::intrusive::list<Song,
+ boost::intrusive::member_hook<Song, Song::Hook,
+ &Song::siblings>,
+ boost::intrusive::constant_time_size<false>> SongList;
+
+#endif
diff --git a/src/db/plugins/simple/SongSort.cxx b/src/db/plugins/simple/SongSort.cxx
new file mode 100644
index 000000000..4b7144937
--- /dev/null
+++ b/src/db/plugins/simple/SongSort.cxx
@@ -0,0 +1,108 @@
+/*
+ * 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 "SongSort.hxx"
+#include "Song.hxx"
+#include "tag/Tag.hxx"
+#include "lib/icu/Collate.hxx"
+
+#include <stdlib.h>
+
+static int
+compare_utf8_string(const char *a, const char *b)
+{
+ if (a == nullptr)
+ return b == nullptr ? 0 : -1;
+
+ if (b == nullptr)
+ return 1;
+
+ return IcuCollate(a, b);
+}
+
+/**
+ * Compare two string tag values, ignoring case. Either one may be
+ * nullptr.
+ */
+static int
+compare_string_tag_item(const Tag &a, const Tag &b,
+ TagType type)
+{
+ return compare_utf8_string(a.GetValue(type),
+ b.GetValue(type));
+}
+
+/**
+ * Compare two tag values which should contain an integer value
+ * (e.g. disc or track number). Either one may be nullptr.
+ */
+static int
+compare_number_string(const char *a, const char *b)
+{
+ long ai = a == nullptr ? 0 : strtol(a, nullptr, 10);
+ long bi = b == nullptr ? 0 : strtol(b, nullptr, 10);
+
+ if (ai <= 0)
+ return bi <= 0 ? 0 : -1;
+
+ if (bi <= 0)
+ return 1;
+
+ return ai - bi;
+}
+
+static int
+compare_tag_item(const Tag &a, const Tag &b, TagType type)
+{
+ return compare_number_string(a.GetValue(type),
+ b.GetValue(type));
+}
+
+/* Only used for sorting/searchin a songvec, not general purpose compares */
+gcc_pure
+static bool
+song_cmp(const Song &a, const Song &b)
+{
+ int ret;
+
+ /* first sort by album */
+ ret = compare_string_tag_item(a.tag, b.tag, TAG_ALBUM);
+ if (ret != 0)
+ return ret < 0;
+
+ /* then sort by disc */
+ ret = compare_tag_item(a.tag, b.tag, TAG_DISC);
+ if (ret != 0)
+ return ret < 0;
+
+ /* then by track number */
+ ret = compare_tag_item(a.tag, b.tag, TAG_TRACK);
+ if (ret != 0)
+ return ret < 0;
+
+ /* still no difference? compare file name */
+ return IcuCollate(a.uri, b.uri) < 0;
+}
+
+void
+song_list_sort(SongList &songs)
+{
+ songs.sort(song_cmp);
+}
diff --git a/src/db/plugins/simple/SongSort.hxx b/src/db/plugins/simple/SongSort.hxx
new file mode 100644
index 000000000..2a0c4383b
--- /dev/null
+++ b/src/db/plugins/simple/SongSort.hxx
@@ -0,0 +1,30 @@
+/*
+ * 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_SONG_SORT_HXX
+#define MPD_SONG_SORT_HXX
+
+#include "Song.hxx"
+
+struct list_head;
+
+void
+song_list_sort(SongList &songs);
+
+#endif