From c6725884bc155da431889468c86c546f0f64f9a1 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Wed, 29 Jan 2014 20:16:43 +0100
Subject: db/update: convert to OO API

Move global variables into the new classes.  That may allow multiple
update threads for multiple databases one day.
---
 src/db/update/Archive.cxx         | 161 +++++++++++++
 src/db/update/Container.cxx       | 127 ++++++++++
 src/db/update/Editor.cxx          | 104 ++++++++
 src/db/update/Editor.hxx          |  60 +++++
 src/db/update/InotifyQueue.cxx    |   4 +-
 src/db/update/InotifyQueue.hxx    |   7 +-
 src/db/update/InotifyUpdate.cxx   |   4 +-
 src/db/update/InotifyUpdate.hxx   |   7 +-
 src/db/update/Queue.cxx           |  42 ++++
 src/db/update/Queue.hxx           |  55 +++++
 src/db/update/Remove.cxx          |  86 +++++++
 src/db/update/Remove.hxx          |  57 +++++
 src/db/update/Service.cxx         |  21 ++
 src/db/update/Service.hxx         |  89 +++++++
 src/db/update/UpdateArchive.cxx   | 169 -------------
 src/db/update/UpdateArchive.hxx   |  50 ----
 src/db/update/UpdateContainer.cxx | 136 -----------
 src/db/update/UpdateContainer.hxx |  36 ---
 src/db/update/UpdateDatabase.cxx  | 104 --------
 src/db/update/UpdateDatabase.hxx  |  50 ----
 src/db/update/UpdateGlue.cxx      |  82 +++----
 src/db/update/UpdateGlue.hxx      |  43 ----
 src/db/update/UpdateInternal.hxx  |  28 ---
 src/db/update/UpdateQueue.cxx     |  49 ----
 src/db/update/UpdateQueue.hxx     |  48 ----
 src/db/update/UpdateRemove.cxx    |  97 --------
 src/db/update/UpdateRemove.hxx    |  38 ---
 src/db/update/UpdateSong.cxx      |  29 +--
 src/db/update/UpdateSong.hxx      |  34 ---
 src/db/update/UpdateWalk.cxx      | 485 --------------------------------------
 src/db/update/UpdateWalk.hxx      |  37 ---
 src/db/update/Walk.cxx            | 463 ++++++++++++++++++++++++++++++++++++
 src/db/update/Walk.hxx            | 134 +++++++++++
 33 files changed, 1455 insertions(+), 1481 deletions(-)
 create mode 100644 src/db/update/Archive.cxx
 create mode 100644 src/db/update/Container.cxx
 create mode 100644 src/db/update/Editor.cxx
 create mode 100644 src/db/update/Editor.hxx
 create mode 100644 src/db/update/Queue.cxx
 create mode 100644 src/db/update/Queue.hxx
 create mode 100644 src/db/update/Remove.cxx
 create mode 100644 src/db/update/Remove.hxx
 create mode 100644 src/db/update/Service.cxx
 create mode 100644 src/db/update/Service.hxx
 delete mode 100644 src/db/update/UpdateArchive.cxx
 delete mode 100644 src/db/update/UpdateArchive.hxx
 delete mode 100644 src/db/update/UpdateContainer.cxx
 delete mode 100644 src/db/update/UpdateContainer.hxx
 delete mode 100644 src/db/update/UpdateDatabase.cxx
 delete mode 100644 src/db/update/UpdateDatabase.hxx
 delete mode 100644 src/db/update/UpdateGlue.hxx
 delete mode 100644 src/db/update/UpdateInternal.hxx
 delete mode 100644 src/db/update/UpdateQueue.cxx
 delete mode 100644 src/db/update/UpdateQueue.hxx
 delete mode 100644 src/db/update/UpdateRemove.cxx
 delete mode 100644 src/db/update/UpdateRemove.hxx
 delete mode 100644 src/db/update/UpdateSong.hxx
 delete mode 100644 src/db/update/UpdateWalk.cxx
 delete mode 100644 src/db/update/UpdateWalk.hxx
 create mode 100644 src/db/update/Walk.cxx
 create mode 100644 src/db/update/Walk.hxx

(limited to 'src/db/update')

diff --git a/src/db/update/Archive.cxx b/src/db/update/Archive.cxx
new file mode 100644
index 000000000..6d47ae0d1
--- /dev/null
+++ b/src/db/update/Archive.cxx
@@ -0,0 +1,161 @@
+/*
+ * 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" /* must be first for large file support */
+#include "Walk.hxx"
+#include "UpdateDomain.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/Directory.hxx"
+#include "db/Song.hxx"
+#include "Mapper.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "archive/ArchiveList.hxx"
+#include "archive/ArchivePlugin.hxx"
+#include "archive/ArchiveFile.hxx"
+#include "archive/ArchiveVisitor.hxx"
+#include "util/Error.hxx"
+#include "Log.hxx"
+
+#include <string>
+
+#include <sys/stat.h>
+#include <string.h>
+
+void
+UpdateWalk::UpdateArchiveTree(Directory &directory, const char *name)
+{
+	const char *tmp = strchr(name, '/');
+	if (tmp) {
+		const std::string child_name(name, tmp);
+		//add dir is not there already
+		db_lock();
+		Directory *subdir =
+			directory.MakeChild(child_name.c_str());
+		subdir->device = DEVICE_INARCHIVE;
+		db_unlock();
+
+		//create directories first
+		UpdateArchiveTree(*subdir, tmp + 1);
+	} else {
+		if (strlen(name) == 0) {
+			LogWarning(update_domain,
+				   "archive returned directory only");
+			return;
+		}
+
+		//add file
+		db_lock();
+		Song *song = directory.FindSong(name);
+		db_unlock();
+		if (song == nullptr) {
+			song = Song::LoadFile(name, directory);
+			if (song != nullptr) {
+				db_lock();
+				directory.AddSong(song);
+				db_unlock();
+
+				modified = true;
+				FormatDefault(update_domain, "added %s/%s",
+					      directory.GetPath(), name);
+			}
+		}
+	}
+}
+
+class UpdateArchiveVisitor final : public ArchiveVisitor {
+	UpdateWalk &walk;
+	Directory *directory;
+
+ public:
+	UpdateArchiveVisitor(UpdateWalk &_walk, Directory *_directory)
+		:walk(_walk), directory(_directory) {}
+
+	virtual void VisitArchiveEntry(const char *path_utf8) override {
+		FormatDebug(update_domain,
+			    "adding archive file: %s", path_utf8);
+		walk.UpdateArchiveTree(*directory, path_utf8);
+	}
+};
+
+/**
+ * Updates the file listing from an archive file.
+ *
+ * @param parent the parent directory the archive file resides in
+ * @param name the UTF-8 encoded base name of the archive file
+ * @param st stat() information on the archive file
+ * @param plugin the archive plugin which fits this archive type
+ */
+void
+UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
+			      const struct stat *st,
+			      const archive_plugin &plugin)
+{
+	db_lock();
+	Directory *directory = parent.FindChild(name);
+	db_unlock();
+
+	if (directory != nullptr && directory->mtime == st->st_mtime &&
+	    !walk_discard)
+		/* MPD has already scanned the archive, and it hasn't
+		   changed since - don't consider updating it */
+		return;
+
+	const auto path_fs = map_directory_child_fs(parent, name);
+
+	/* open archive */
+	Error error;
+	ArchiveFile *file = archive_file_open(&plugin, path_fs.c_str(), error);
+	if (file == nullptr) {
+		LogError(error);
+		return;
+	}
+
+	FormatDebug(update_domain, "archive %s opened", path_fs.c_str());
+
+	if (directory == nullptr) {
+		FormatDebug(update_domain,
+			    "creating archive directory: %s", name);
+		db_lock();
+		directory = parent.CreateChild(name);
+		/* mark this directory as archive (we use device for
+		   this) */
+		directory->device = DEVICE_INARCHIVE;
+		db_unlock();
+	}
+
+	directory->mtime = st->st_mtime;
+
+	UpdateArchiveVisitor visitor(*this, directory);
+	file->Visit(visitor);
+	file->Close();
+}
+
+bool
+UpdateWalk::UpdateArchiveFile(Directory &directory,
+			      const char *name, const char *suffix,
+			      const struct stat *st)
+{
+	const struct archive_plugin *plugin =
+		archive_plugin_from_suffix(suffix);
+	if (plugin == nullptr)
+		return false;
+
+	UpdateArchiveFile(directory, name, st, *plugin);
+	return true;
+}
diff --git a/src/db/update/Container.cxx b/src/db/update/Container.cxx
new file mode 100644
index 000000000..38aacea0a
--- /dev/null
+++ b/src/db/update/Container.cxx
@@ -0,0 +1,127 @@
+/*
+ * 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" /* must be first for large file support */
+#include "Walk.hxx"
+#include "UpdateDomain.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/Directory.hxx"
+#include "db/Song.hxx"
+#include "decoder/DecoderPlugin.hxx"
+#include "decoder/DecoderList.hxx"
+#include "Mapper.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "tag/TagHandler.hxx"
+#include "tag/TagBuilder.hxx"
+#include "Log.hxx"
+
+#include <sys/stat.h>
+
+#include <glib.h>
+
+Directory *
+UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name,
+				    const struct stat *st)
+{
+	Directory *directory = parent.FindChild(name);
+
+	// directory exists already
+	if (directory != nullptr) {
+		if (directory->mtime == st->st_mtime && !walk_discard) {
+			/* not modified */
+			return nullptr;
+		}
+
+		editor.DeleteDirectory(directory);
+		modified = true;
+	}
+
+	directory = parent.MakeChild(name);
+	directory->mtime = st->st_mtime;
+	return directory;
+}
+
+static bool
+SupportsContainerSuffix(const DecoderPlugin &plugin, const char *suffix)
+{
+	return plugin.container_scan != nullptr &&
+		plugin.SupportsSuffix(suffix);
+}
+
+bool
+UpdateWalk::UpdateContainerFile(Directory &directory,
+				const char *name, const char *suffix,
+				const struct stat *st)
+{
+	const DecoderPlugin *_plugin = decoder_plugins_find([suffix](const DecoderPlugin &plugin){
+			return SupportsContainerSuffix(plugin, suffix);
+		});
+	if (_plugin == nullptr)
+		return false;
+	const DecoderPlugin &plugin = *_plugin;
+
+	db_lock();
+	Directory *contdir = MakeDirectoryIfModified(directory, name, st);
+	if (contdir == nullptr) {
+		/* not modified */
+		db_unlock();
+		return true;
+	}
+
+	contdir->device = DEVICE_CONTAINER;
+	db_unlock();
+
+	const auto pathname = map_directory_child_fs(directory, name);
+
+	char *vtrack;
+	unsigned int tnum = 0;
+	TagBuilder tag_builder;
+	while ((vtrack = plugin.container_scan(pathname.c_str(), ++tnum)) != nullptr) {
+		Song *song = Song::NewFile(vtrack, *contdir);
+
+		// shouldn't be necessary but it's there..
+		song->mtime = st->st_mtime;
+
+		const auto child_path_fs =
+			map_directory_child_fs(*contdir, vtrack);
+
+		plugin.ScanFile(child_path_fs.c_str(),
+				add_tag_handler, &tag_builder);
+
+		tag_builder.Commit(song->tag);
+
+		db_lock();
+		contdir->AddSong(song);
+		db_unlock();
+
+		modified = true;
+
+		FormatDefault(update_domain, "added %s/%s",
+			      directory.GetPath(), vtrack);
+		g_free(vtrack);
+	}
+
+	if (tnum == 1) {
+		db_lock();
+		editor.DeleteDirectory(contdir);
+		db_unlock();
+		return false;
+	} else
+		return true;
+}
diff --git a/src/db/update/Editor.cxx b/src/db/update/Editor.cxx
new file mode 100644
index 000000000..369bad24b
--- /dev/null
+++ b/src/db/update/Editor.cxx
@@ -0,0 +1,104 @@
+/*
+ * 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" /* must be first for large file support */
+#include "Editor.hxx"
+#include "Remove.hxx"
+#include "db/PlaylistVector.hxx"
+#include "db/Directory.hxx"
+#include "db/Song.hxx"
+#include "db/DatabaseLock.hxx"
+
+#include <assert.h>
+#include <stddef.h>
+
+void
+DatabaseEditor::DeleteSong(Directory &dir, Song *del)
+{
+	assert(del->parent == &dir);
+
+	/* first, prevent traversers in main task from getting this */
+	dir.RemoveSong(del);
+
+	db_unlock(); /* temporary unlock, because update_remove_song() blocks */
+
+	/* now take it out of the playlist (in the main_task) */
+	remove.Remove(del);
+
+	/* finally, all possible references gone, free it */
+	del->Free();
+
+	db_lock();
+}
+
+/**
+ * Recursively remove all sub directories and songs from a directory,
+ * leaving an empty directory.
+ *
+ * Caller must lock the #db_mutex.
+ */
+inline void
+DatabaseEditor::ClearDirectory(Directory &directory)
+{
+	Directory *child, *n;
+	directory_for_each_child_safe(child, n, directory)
+		DeleteDirectory(child);
+
+	Song *song, *ns;
+	directory_for_each_song_safe(song, ns, directory) {
+		assert(song->parent == &directory);
+		DeleteSong(directory, song);
+	}
+}
+
+void
+DatabaseEditor::DeleteDirectory(Directory *directory)
+{
+	assert(directory->parent != nullptr);
+
+	ClearDirectory(*directory);
+
+	directory->Delete();
+}
+
+bool
+DatabaseEditor::DeleteNameIn(Directory &parent, const char *name)
+{
+	bool modified = false;
+
+	db_lock();
+	Directory *directory = parent.FindChild(name);
+
+	if (directory != nullptr) {
+		DeleteDirectory(directory);
+		modified = true;
+	}
+
+	Song *song = parent.FindSong(name);
+	if (song != nullptr) {
+		DeleteSong(parent, song);
+		modified = true;
+	}
+
+	parent.playlists.erase(name);
+
+	db_unlock();
+
+	return modified;
+}
diff --git a/src/db/update/Editor.hxx b/src/db/update/Editor.hxx
new file mode 100644
index 000000000..a9093d662
--- /dev/null
+++ b/src/db/update/Editor.hxx
@@ -0,0 +1,60 @@
+/*
+ * 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_UPDATE_DATABASE_HXX
+#define MPD_UPDATE_DATABASE_HXX
+
+#include "check.h"
+#include "Remove.hxx"
+
+struct Directory;
+struct Song;
+class UpdateRemoveService;
+
+class DatabaseEditor final {
+	UpdateRemoveService remove;
+
+public:
+	DatabaseEditor(EventLoop &_loop)
+		:remove(_loop) {}
+
+	/**
+	 * Caller must lock the #db_mutex.
+	 */
+	void DeleteSong(Directory &parent, Song *song);
+
+	/**
+	 * Recursively free a directory and all its contents.
+	 *
+	 * Caller must lock the #db_mutex.
+	 */
+	void DeleteDirectory(Directory *directory);
+
+	/**
+	 * Caller must NOT lock the #db_mutex.
+	 *
+	 * @return true if the database was modified
+	 */
+	bool DeleteNameIn(Directory &parent, const char *name);
+
+private:
+	void ClearDirectory(Directory &directory);
+};
+
+#endif
diff --git a/src/db/update/InotifyQueue.cxx b/src/db/update/InotifyQueue.cxx
index f4bccf7ae..013200f98 100644
--- a/src/db/update/InotifyQueue.cxx
+++ b/src/db/update/InotifyQueue.cxx
@@ -20,7 +20,7 @@
 #include "config.h"
 #include "InotifyQueue.hxx"
 #include "InotifyDomain.hxx"
-#include "UpdateGlue.hxx"
+#include "Service.hxx"
 #include "Log.hxx"
 
 #include <string.h>
@@ -40,7 +40,7 @@ InotifyQueue::OnTimeout()
 	while (!queue.empty()) {
 		const char *uri_utf8 = queue.front().c_str();
 
-		id = update_enqueue(uri_utf8, false);
+		id = update.Enqueue(uri_utf8, false);
 		if (id == 0) {
 			/* retry later */
 			ScheduleSeconds(INOTIFY_UPDATE_DELAY_S);
diff --git a/src/db/update/InotifyQueue.hxx b/src/db/update/InotifyQueue.hxx
index 99e2635b1..a9abc2969 100644
--- a/src/db/update/InotifyQueue.hxx
+++ b/src/db/update/InotifyQueue.hxx
@@ -26,11 +26,16 @@
 #include <list>
 #include <string>
 
+class UpdateService;
+
 class InotifyQueue final : private TimeoutMonitor {
+	UpdateService &update;
+
 	std::list<std::string> queue;
 
 public:
-	InotifyQueue(EventLoop &_loop):TimeoutMonitor(_loop) {}
+	InotifyQueue(EventLoop &_loop, UpdateService &_update)
+		:TimeoutMonitor(_loop), update(_update) {}
 
 	void Enqueue(const char *uri_utf8);
 
diff --git a/src/db/update/InotifyUpdate.cxx b/src/db/update/InotifyUpdate.cxx
index 72f22ceb1..3ba92ebf0 100644
--- a/src/db/update/InotifyUpdate.cxx
+++ b/src/db/update/InotifyUpdate.cxx
@@ -285,7 +285,7 @@ mpd_inotify_callback(int wd, unsigned mask,
 }
 
 void
-mpd_inotify_init(EventLoop &loop, unsigned max_depth)
+mpd_inotify_init(EventLoop &loop, UpdateService &update, unsigned max_depth)
 {
 	LogDebug(inotify_domain, "initializing inotify");
 
@@ -320,7 +320,7 @@ mpd_inotify_init(EventLoop &loop, unsigned max_depth)
 
 	recursive_watch_subdirectories(inotify_root, path, 0);
 
-	inotify_queue = new InotifyQueue(loop);
+	inotify_queue = new InotifyQueue(loop, update);
 
 	LogDebug(inotify_domain, "watching music directory");
 }
diff --git a/src/db/update/InotifyUpdate.hxx b/src/db/update/InotifyUpdate.hxx
index 7cf486e5a..0260a4ff5 100644
--- a/src/db/update/InotifyUpdate.hxx
+++ b/src/db/update/InotifyUpdate.hxx
@@ -24,11 +24,12 @@
 #include "Compiler.h"
 
 class EventLoop;
+class UpdateService;
 
 #ifdef HAVE_INOTIFY_INIT
 
 void
-mpd_inotify_init(EventLoop &loop, unsigned max_depth);
+mpd_inotify_init(EventLoop &loop, UpdateService &update, unsigned max_depth);
 
 void
 mpd_inotify_finish(void);
@@ -36,7 +37,9 @@ mpd_inotify_finish(void);
 #else /* !HAVE_INOTIFY_INIT */
 
 static inline void
-mpd_inotify_init(gcc_unused EventLoop &loop, gcc_unused unsigned max_depth)
+mpd_inotify_init(gcc_unused EventLoop &loop,
+		 gcc_unused UpdateService &update,
+		 gcc_unused unsigned max_depth)
 {
 }
 
diff --git a/src/db/update/Queue.cxx b/src/db/update/Queue.cxx
new file mode 100644
index 000000000..4bb0ae725
--- /dev/null
+++ b/src/db/update/Queue.cxx
@@ -0,0 +1,42 @@
+/*
+ * 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 "Queue.hxx"
+
+bool
+UpdateQueue::Push(const char *path, bool discard, unsigned id)
+{
+	if (update_queue.size() >= MAX_UPDATE_QUEUE_SIZE)
+		return false;
+
+	update_queue.emplace(path, discard, id);
+	return true;
+}
+
+UpdateQueueItem
+UpdateQueue::Pop()
+{
+	if (update_queue.empty())
+		return UpdateQueueItem();
+
+	auto i = std::move(update_queue.front());
+	update_queue.pop();
+	return i;
+}
diff --git a/src/db/update/Queue.hxx b/src/db/update/Queue.hxx
new file mode 100644
index 000000000..a5b2aa9ac
--- /dev/null
+++ b/src/db/update/Queue.hxx
@@ -0,0 +1,55 @@
+/*
+ * 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_UPDATE_QUEUE_HXX
+#define MPD_UPDATE_QUEUE_HXX
+
+#include "check.h"
+
+#include <string>
+#include <queue>
+#include <list>
+
+struct UpdateQueueItem {
+	std::string path_utf8;
+	unsigned id;
+	bool discard;
+
+	UpdateQueueItem():id(0) {}
+	UpdateQueueItem(const char *_path, bool _discard,
+			unsigned _id)
+		:path_utf8(_path), id(_id), discard(_discard) {}
+
+	bool IsDefined() const {
+		return id != 0;
+	}
+};
+
+class UpdateQueue {
+	static constexpr unsigned MAX_UPDATE_QUEUE_SIZE = 32;
+
+	std::queue<UpdateQueueItem, std::list<UpdateQueueItem>> update_queue;
+
+public:
+	bool Push(const char *path, bool discard, unsigned id);
+
+	UpdateQueueItem Pop();
+};
+
+#endif
diff --git a/src/db/update/Remove.cxx b/src/db/update/Remove.cxx
new file mode 100644
index 000000000..e2c172a4b
--- /dev/null
+++ b/src/db/update/Remove.cxx
@@ -0,0 +1,86 @@
+/*
+ * 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" /* must be first for large file support */
+#include "Remove.hxx"
+#include "UpdateDomain.hxx"
+#include "GlobalEvents.hxx"
+#include "thread/Mutex.hxx"
+#include "thread/Cond.hxx"
+#include "db/Song.hxx"
+#include "db/LightSong.hxx"
+#include "Main.hxx"
+#include "Instance.hxx"
+#include "Log.hxx"
+
+#ifdef ENABLE_SQLITE
+#include "sticker/StickerDatabase.hxx"
+#include "sticker/SongSticker.hxx"
+#endif
+
+#include <assert.h>
+
+/**
+ * Safely remove a song from the database.  This must be done in the
+ * main task, to be sure that there is no pointer left to it.
+ */
+void
+UpdateRemoveService::RunDeferred()
+{
+	assert(removed_song != nullptr);
+
+	{
+		const auto uri = removed_song->GetURI();
+		FormatDefault(update_domain, "removing %s", uri.c_str());
+	}
+
+#ifdef ENABLE_SQLITE
+	/* if the song has a sticker, remove it */
+	if (sticker_enabled())
+		sticker_song_delete(removed_song->Export());
+#endif
+
+	{
+		const auto uri = removed_song->GetURI();
+		instance->DeleteSong(uri.c_str());
+	}
+
+	/* clear "removed_song" and send signal to update thread */
+	remove_mutex.lock();
+	removed_song = nullptr;
+	remove_cond.signal();
+	remove_mutex.unlock();
+}
+
+void
+UpdateRemoveService::Remove(const Song *song)
+{
+	assert(removed_song == nullptr);
+
+	removed_song = song;
+
+	DeferredMonitor::Schedule();
+
+	remove_mutex.lock();
+
+	while (removed_song != nullptr)
+		remove_cond.wait(remove_mutex);
+
+	remove_mutex.unlock();
+}
diff --git a/src/db/update/Remove.hxx b/src/db/update/Remove.hxx
new file mode 100644
index 000000000..1970c0a07
--- /dev/null
+++ b/src/db/update/Remove.hxx
@@ -0,0 +1,57 @@
+/*
+ * 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_UPDATE_REMOVE_HXX
+#define MPD_UPDATE_REMOVE_HXX
+
+#include "check.h"
+#include "event/DeferredMonitor.hxx"
+#include "thread/Mutex.hxx"
+#include "thread/Cond.hxx"
+
+struct Song;
+
+/**
+ * This class handles #Song removal.  It defers the action to the main
+ * thread to ensure that all references to the #Song are gone.
+ */
+class UpdateRemoveService final : DeferredMonitor {
+	Mutex remove_mutex;
+	Cond remove_cond;
+
+	const Song *removed_song;
+
+public:
+	UpdateRemoveService(EventLoop &_loop)
+		:DeferredMonitor(_loop) {}
+
+	/**
+	 * Sends a signal to the main thread which will in turn remove
+	 * the song: from the sticker database and from the playlist.
+	 * This serialized access is implemented to avoid excessive
+	 * locking.
+	 */
+	void Remove(const Song *song);
+
+private:
+	/* virtual methods from class DeferredMonitor */
+	virtual void RunDeferred() override;
+};
+
+#endif
diff --git a/src/db/update/Service.cxx b/src/db/update/Service.cxx
new file mode 100644
index 000000000..cc2a86968
--- /dev/null
+++ b/src/db/update/Service.cxx
@@ -0,0 +1,21 @@
+/*
+ * 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 "Service.hxx"
diff --git a/src/db/update/Service.hxx b/src/db/update/Service.hxx
new file mode 100644
index 000000000..3ea4baea1
--- /dev/null
+++ b/src/db/update/Service.hxx
@@ -0,0 +1,89 @@
+/*
+ * 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_UPDATE_SERVICE_HXX
+#define MPD_UPDATE_SERVICE_HXX
+
+#include "check.h"
+#include "Queue.hxx"
+#include "Walk.hxx"
+#include "event/DeferredMonitor.hxx"
+#include "thread/Thread.hxx"
+
+/**
+ * This class manages the update queue and runs the update thread.
+ */
+class UpdateService final : DeferredMonitor {
+	enum Progress {
+		UPDATE_PROGRESS_IDLE = 0,
+		UPDATE_PROGRESS_RUNNING = 1,
+		UPDATE_PROGRESS_DONE = 2
+	};
+
+	Progress progress;
+
+	bool modified;
+
+	Thread update_thread;
+
+	static const unsigned update_task_id_max = 1 << 15;
+
+	unsigned update_task_id;
+
+	UpdateQueue queue;
+
+	UpdateQueueItem next;
+
+	UpdateWalk walk;
+
+public:
+	UpdateService(EventLoop &_loop);
+
+	/**
+	 * Returns a non-zero job id when we are currently updating
+	 * the database.
+	 */
+	unsigned GetId() const {
+		return next.id;
+	}
+
+	/**
+	 * Add this path to the database update queue.
+	 *
+	 * @param path a path to update; if an empty string,
+	 * the whole music directory is updated
+	 * @return the job id, or 0 on error
+	 */
+	gcc_nonnull_all
+	unsigned Enqueue(const char *path, bool discard);
+
+private:
+	/* virtual methods from class DeferredMonitor */
+	virtual void RunDeferred() override;
+
+	/* the update thread */
+	void Task();
+	static void Task(void *ctx);
+
+	void StartThread(UpdateQueueItem &&i);
+
+	unsigned GenerateId();
+};
+
+#endif
diff --git a/src/db/update/UpdateArchive.cxx b/src/db/update/UpdateArchive.cxx
deleted file mode 100644
index 5e733202d..000000000
--- a/src/db/update/UpdateArchive.cxx
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * 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" /* must be first for large file support */
-#include "UpdateArchive.hxx"
-#include "UpdateInternal.hxx"
-#include "UpdateDomain.hxx"
-#include "db/DatabaseLock.hxx"
-#include "db/Directory.hxx"
-#include "db/Song.hxx"
-#include "Mapper.hxx"
-#include "fs/AllocatedPath.hxx"
-#include "archive/ArchiveList.hxx"
-#include "archive/ArchivePlugin.hxx"
-#include "archive/ArchiveFile.hxx"
-#include "archive/ArchiveVisitor.hxx"
-#include "util/Error.hxx"
-#include "Log.hxx"
-
-#include <string>
-
-#include <string.h>
-
-static void
-update_archive_tree(Directory &directory, const char *name)
-{
-	const char *tmp = strchr(name, '/');
-	if (tmp) {
-		const std::string child_name(name, tmp);
-		//add dir is not there already
-		db_lock();
-		Directory *subdir =
-			directory.MakeChild(child_name.c_str());
-		subdir->device = DEVICE_INARCHIVE;
-		db_unlock();
-
-		//create directories first
-		update_archive_tree(*subdir, tmp+1);
-	} else {
-		if (strlen(name) == 0) {
-			LogWarning(update_domain,
-				   "archive returned directory only");
-			return;
-		}
-
-		//add file
-		db_lock();
-		Song *song = directory.FindSong(name);
-		db_unlock();
-		if (song == nullptr) {
-			song = Song::LoadFile(name, directory);
-			if (song != nullptr) {
-				db_lock();
-				directory.AddSong(song);
-				db_unlock();
-
-				modified = true;
-				FormatDefault(update_domain, "added %s/%s",
-					      directory.GetPath(), name);
-			}
-		}
-	}
-}
-
-/**
- * Updates the file listing from an archive file.
- *
- * @param parent the parent directory the archive file resides in
- * @param name the UTF-8 encoded base name of the archive file
- * @param st stat() information on the archive file
- * @param plugin the archive plugin which fits this archive type
- */
-static void
-update_archive_file2(Directory &parent, const char *name,
-		     const struct stat *st,
-		     const struct archive_plugin *plugin)
-{
-	db_lock();
-	Directory *directory = parent.FindChild(name);
-	db_unlock();
-
-	if (directory != nullptr && directory->mtime == st->st_mtime &&
-	    !walk_discard)
-		/* MPD has already scanned the archive, and it hasn't
-		   changed since - don't consider updating it */
-		return;
-
-	const auto path_fs = map_directory_child_fs(parent, name);
-
-	/* open archive */
-	Error error;
-	ArchiveFile *file = archive_file_open(plugin, path_fs.c_str(), error);
-	if (file == nullptr) {
-		LogError(error);
-		return;
-	}
-
-	FormatDebug(update_domain, "archive %s opened", path_fs.c_str());
-
-	if (directory == nullptr) {
-		FormatDebug(update_domain,
-			    "creating archive directory: %s", name);
-		db_lock();
-		directory = parent.CreateChild(name);
-		/* mark this directory as archive (we use device for
-		   this) */
-		directory->device = DEVICE_INARCHIVE;
-		db_unlock();
-	}
-
-	directory->mtime = st->st_mtime;
-
-	class UpdateArchiveVisitor final : public ArchiveVisitor {
-		Directory *directory;
-
-	public:
-		UpdateArchiveVisitor(Directory *_directory)
-			:directory(_directory) {}
-
-		virtual void VisitArchiveEntry(const char *path_utf8) override {
-			FormatDebug(update_domain,
-				    "adding archive file: %s", path_utf8);
-			update_archive_tree(*directory, path_utf8);
-		}
-	};
-
-	UpdateArchiveVisitor visitor(directory);
-	file->Visit(visitor);
-	file->Close();
-}
-
-bool
-update_archive_file(Directory &directory,
-		    const char *name, const char *suffix,
-		    const struct stat *st)
-{
-#ifdef ENABLE_ARCHIVE
-	const struct archive_plugin *plugin =
-		archive_plugin_from_suffix(suffix);
-	if (plugin == nullptr)
-		return false;
-
-	update_archive_file2(directory, name, st, plugin);
-	return true;
-#else
-	(void)directory;
-	(void)name;
-	(void)suffix;
-	(void)st;
-
-	return false;
-#endif
-}
diff --git a/src/db/update/UpdateArchive.hxx b/src/db/update/UpdateArchive.hxx
deleted file mode 100644
index 1fc9af349..000000000
--- a/src/db/update/UpdateArchive.hxx
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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_UPDATE_ARCHIVE_HXX
-#define MPD_UPDATE_ARCHIVE_HXX
-
-#include "check.h"
-#include "Compiler.h"
-
-#include <sys/stat.h>
-
-struct Directory;
-
-#ifdef ENABLE_ARCHIVE
-
-bool
-update_archive_file(Directory &directory,
-		    const char *name, const char *suffix,
-		    const struct stat *st);
-
-#else
-
-static inline bool
-update_archive_file(gcc_unused Directory &directory,
-		    gcc_unused const char *name,
-		    gcc_unused const char *suffix,
-		    gcc_unused const struct stat *st)
-{
-	return false;
-}
-
-#endif
-
-#endif
diff --git a/src/db/update/UpdateContainer.cxx b/src/db/update/UpdateContainer.cxx
deleted file mode 100644
index c03d88748..000000000
--- a/src/db/update/UpdateContainer.cxx
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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" /* must be first for large file support */
-#include "UpdateContainer.hxx"
-#include "UpdateInternal.hxx"
-#include "UpdateDatabase.hxx"
-#include "UpdateDomain.hxx"
-#include "db/DatabaseLock.hxx"
-#include "db/Directory.hxx"
-#include "db/Song.hxx"
-#include "decoder/DecoderPlugin.hxx"
-#include "decoder/DecoderList.hxx"
-#include "Mapper.hxx"
-#include "fs/AllocatedPath.hxx"
-#include "tag/TagHandler.hxx"
-#include "tag/TagBuilder.hxx"
-#include "Log.hxx"
-
-#include <glib.h>
-
-/**
- * Create the specified directory object if it does not exist already
- * or if the #stat object indicates that it has been modified since
- * the last update.  Returns nullptr when it exists already and is
- * unmodified.
- *
- * The caller must lock the database.
- */
-static Directory *
-make_directory_if_modified(Directory &parent, const char *name,
-			   const struct stat *st)
-{
-	Directory *directory = parent.FindChild(name);
-
-	// directory exists already
-	if (directory != nullptr) {
-		if (directory->mtime == st->st_mtime && !walk_discard) {
-			/* not modified */
-			return nullptr;
-		}
-
-		delete_directory(directory);
-		modified = true;
-	}
-
-	directory = parent.MakeChild(name);
-	directory->mtime = st->st_mtime;
-	return directory;
-}
-
-static bool
-SupportsContainerSuffix(const DecoderPlugin &plugin, const char *suffix)
-{
-	return plugin.container_scan != nullptr &&
-		plugin.SupportsSuffix(suffix);
-}
-
-bool
-update_container_file(Directory &directory,
-		      const char *name,
-		      const struct stat *st,
-		      const char *suffix)
-{
-	const DecoderPlugin *_plugin = decoder_plugins_find([suffix](const DecoderPlugin &plugin){
-			return SupportsContainerSuffix(plugin, suffix);
-		});
-	if (_plugin == nullptr)
-		return false;
-	const DecoderPlugin &plugin = *_plugin;
-
-	db_lock();
-	Directory *contdir = make_directory_if_modified(directory, name, st);
-	if (contdir == nullptr) {
-		/* not modified */
-		db_unlock();
-		return true;
-	}
-
-	contdir->device = DEVICE_CONTAINER;
-	db_unlock();
-
-	const auto pathname = map_directory_child_fs(directory, name);
-
-	char *vtrack;
-	unsigned int tnum = 0;
-	TagBuilder tag_builder;
-	while ((vtrack = plugin.container_scan(pathname.c_str(), ++tnum)) != nullptr) {
-		Song *song = Song::NewFile(vtrack, *contdir);
-
-		// shouldn't be necessary but it's there..
-		song->mtime = st->st_mtime;
-
-		const auto child_path_fs =
-			map_directory_child_fs(*contdir, vtrack);
-
-		plugin.ScanFile(child_path_fs.c_str(),
-				add_tag_handler, &tag_builder);
-
-		tag_builder.Commit(song->tag);
-
-		db_lock();
-		contdir->AddSong(song);
-		db_unlock();
-
-		modified = true;
-
-		FormatDefault(update_domain, "added %s/%s",
-			      directory.GetPath(), vtrack);
-		g_free(vtrack);
-	}
-
-	if (tnum == 1) {
-		db_lock();
-		delete_directory(contdir);
-		db_unlock();
-		return false;
-	} else
-		return true;
-}
diff --git a/src/db/update/UpdateContainer.hxx b/src/db/update/UpdateContainer.hxx
deleted file mode 100644
index 8125f71ee..000000000
--- a/src/db/update/UpdateContainer.hxx
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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_UPDATE_CONTAINER_HXX
-#define MPD_UPDATE_CONTAINER_HXX
-
-#include "check.h"
-
-#include <sys/stat.h>
-
-struct Directory;
-struct DecoderPlugin;
-
-bool
-update_container_file(Directory &directory,
-		      const char *name,
-		      const struct stat *st,
-		      const char *suffix);
-
-#endif
diff --git a/src/db/update/UpdateDatabase.cxx b/src/db/update/UpdateDatabase.cxx
deleted file mode 100644
index 72a68de14..000000000
--- a/src/db/update/UpdateDatabase.cxx
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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" /* must be first for large file support */
-#include "UpdateDatabase.hxx"
-#include "UpdateRemove.hxx"
-#include "db/PlaylistVector.hxx"
-#include "db/Directory.hxx"
-#include "db/Song.hxx"
-#include "db/DatabaseLock.hxx"
-
-#include <assert.h>
-#include <stddef.h>
-
-void
-delete_song(Directory &dir, Song *del)
-{
-	assert(del->parent == &dir);
-
-	/* first, prevent traversers in main task from getting this */
-	dir.RemoveSong(del);
-
-	db_unlock(); /* temporary unlock, because update_remove_song() blocks */
-
-	/* now take it out of the playlist (in the main_task) */
-	update_remove_song(del);
-
-	/* finally, all possible references gone, free it */
-	del->Free();
-
-	db_lock();
-}
-
-/**
- * Recursively remove all sub directories and songs from a directory,
- * leaving an empty directory.
- *
- * Caller must lock the #db_mutex.
- */
-static void
-clear_directory(Directory &directory)
-{
-	Directory *child, *n;
-	directory_for_each_child_safe(child, n, directory)
-		delete_directory(child);
-
-	Song *song, *ns;
-	directory_for_each_song_safe(song, ns, directory) {
-		assert(song->parent == &directory);
-		delete_song(directory, song);
-	}
-}
-
-void
-delete_directory(Directory *directory)
-{
-	assert(directory->parent != nullptr);
-
-	clear_directory(*directory);
-
-	directory->Delete();
-}
-
-bool
-delete_name_in(Directory &parent, const char *name)
-{
-	bool modified = false;
-
-	db_lock();
-	Directory *directory = parent.FindChild(name);
-
-	if (directory != nullptr) {
-		delete_directory(directory);
-		modified = true;
-	}
-
-	Song *song = parent.FindSong(name);
-	if (song != nullptr) {
-		delete_song(parent, song);
-		modified = true;
-	}
-
-	parent.playlists.erase(name);
-
-	db_unlock();
-
-	return modified;
-}
diff --git a/src/db/update/UpdateDatabase.hxx b/src/db/update/UpdateDatabase.hxx
deleted file mode 100644
index bd7c395f2..000000000
--- a/src/db/update/UpdateDatabase.hxx
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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_UPDATE_DATABASE_HXX
-#define MPD_UPDATE_DATABASE_HXX
-
-#include "check.h"
-
-struct Directory;
-struct Song;
-
-/**
- * Caller must lock the #db_mutex.
- */
-void
-delete_song(Directory &parent, Song *song);
-
-/**
- * Recursively free a directory and all its contents.
- *
- * Caller must lock the #db_mutex.
- */
-void
-delete_directory(Directory *directory);
-
-/**
- * Caller must NOT lock the #db_mutex.
- *
- * @return true if the database was modified
- */
-bool
-delete_name_in(Directory &parent, const char *name);
-
-#endif
diff --git a/src/db/update/UpdateGlue.cxx b/src/db/update/UpdateGlue.cxx
index d18747ba1..2214bb2f7 100644
--- a/src/db/update/UpdateGlue.cxx
+++ b/src/db/update/UpdateGlue.cxx
@@ -18,15 +18,11 @@
  */
 
 #include "config.h"
-#include "UpdateGlue.hxx"
-#include "UpdateQueue.hxx"
-#include "UpdateWalk.hxx"
-#include "UpdateRemove.hxx"
+#include "Service.hxx"
 #include "UpdateDomain.hxx"
 #include "Mapper.hxx"
 #include "db/DatabaseSimple.hxx"
 #include "Idle.hxx"
-#include "GlobalEvents.hxx"
 #include "util/Error.hxx"
 #include "Log.hxx"
 #include "Main.hxx"
@@ -38,30 +34,8 @@
 
 #include <assert.h>
 
-static enum update_progress {
-	UPDATE_PROGRESS_IDLE = 0,
-	UPDATE_PROGRESS_RUNNING = 1,
-	UPDATE_PROGRESS_DONE = 2
-} progress;
-
-static bool modified;
-
-static Thread update_thread;
-
-static const unsigned update_task_id_max = 1 << 15;
-
-static unsigned update_task_id;
-
-static UpdateQueueItem next;
-
-unsigned
-isUpdatingDB(void)
-{
-	return next.id;
-}
-
-static void
-update_task(gcc_unused void *ctx)
+inline void
+UpdateService::Task()
 {
 	if (!next.path_utf8.empty())
 		FormatDebug(update_domain, "starting: %s",
@@ -71,7 +45,7 @@ update_task(gcc_unused void *ctx)
 
 	SetThreadIdlePriority();
 
-	modified = update_walk(next.path_utf8.c_str(), next.discard);
+	modified = walk.Walk(next.path_utf8.c_str(), next.discard);
 
 	if (modified || !db_exists()) {
 		Error error;
@@ -86,11 +60,18 @@ update_task(gcc_unused void *ctx)
 		LogDebug(update_domain, "finished");
 
 	progress = UPDATE_PROGRESS_DONE;
-	GlobalEvents::Emit(GlobalEvents::UPDATE);
+	DeferredMonitor::Schedule();
 }
 
-static void
-spawn_update_task(UpdateQueueItem &&i)
+void
+UpdateService::Task(void *ctx)
+{
+	UpdateService &service = *(UpdateService *)ctx;
+	return service.Task();
+}
+
+void
+UpdateService::StartThread(UpdateQueueItem &&i)
 {
 	assert(main_thread.IsInside());
 
@@ -100,15 +81,15 @@ spawn_update_task(UpdateQueueItem &&i)
 	next = std::move(i);
 
 	Error error;
-	if (!update_thread.Start(update_task, nullptr, error))
+	if (!update_thread.Start(Task, this, error))
 		FatalError(error);
 
 	FormatDebug(update_domain,
 		    "spawned thread for update job id %i", next.id);
 }
 
-static unsigned
-generate_update_id()
+unsigned
+UpdateService::GenerateId()
 {
 	unsigned id = update_task_id + 1;
 	if (id > update_task_id_max)
@@ -117,7 +98,7 @@ generate_update_id()
 }
 
 unsigned
-update_enqueue(const char *path, bool discard)
+UpdateService::Enqueue(const char *path, bool discard)
 {
 	assert(main_thread.IsInside());
 
@@ -125,16 +106,16 @@ update_enqueue(const char *path, bool discard)
 		return 0;
 
 	if (progress != UPDATE_PROGRESS_IDLE) {
-		const unsigned id = generate_update_id();
-		if (!update_queue_push(path, discard, id))
+		const unsigned id = GenerateId();
+		if (!queue.Push(path, discard, id))
 			return 0;
 
 		update_task_id = id;
 		return id;
 	}
 
-	const unsigned id = update_task_id = generate_update_id();
-	spawn_update_task(UpdateQueueItem(path, discard, id));
+	const unsigned id = update_task_id = GenerateId();
+	StartThread(UpdateQueueItem(path, discard, id));
 
 	idle_add(IDLE_UPDATE);
 
@@ -144,7 +125,8 @@ update_enqueue(const char *path, bool discard)
 /**
  * Called in the main thread after the database update is finished.
  */
-static void update_finished_event(void)
+void
+UpdateService::RunDeferred()
 {
 	assert(progress == UPDATE_PROGRESS_DONE);
 	assert(next.IsDefined());
@@ -158,24 +140,16 @@ static void update_finished_event(void)
 		/* send "idle" events */
 		instance->DatabaseModified();
 
-	auto i = update_queue_shift();
+	auto i = queue.Pop();
 	if (i.IsDefined()) {
 		/* schedule the next path */
-		spawn_update_task(std::move(i));
+		StartThread(std::move(i));
 	} else {
 		progress = UPDATE_PROGRESS_IDLE;
 	}
 }
 
-void update_global_init(void)
-{
-	GlobalEvents::Register(GlobalEvents::UPDATE, update_finished_event);
-
-	update_remove_global_init();
-	update_walk_global_init();
-}
-
-void update_global_finish(void)
+UpdateService::UpdateService(EventLoop &_loop)
+	:DeferredMonitor(_loop), walk(_loop)
 {
-	update_walk_global_finish();
 }
diff --git a/src/db/update/UpdateGlue.hxx b/src/db/update/UpdateGlue.hxx
deleted file mode 100644
index 6e247414e..000000000
--- a/src/db/update/UpdateGlue.hxx
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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_UPDATE_GLUE_HXX
-#define MPD_UPDATE_GLUE_HXX
-
-#include "Compiler.h"
-
-void update_global_init(void);
-
-void update_global_finish(void);
-
-unsigned
-isUpdatingDB(void);
-
-/**
- * Add this path to the database update queue.
- *
- * @param path a path to update; if an empty string,
- * the whole music directory is updated
- * @return the job id, or 0 on error
- */
-gcc_nonnull_all
-unsigned
-update_enqueue(const char *path, bool discard);
-
-#endif
diff --git a/src/db/update/UpdateInternal.hxx b/src/db/update/UpdateInternal.hxx
deleted file mode 100644
index 2e373bd06..000000000
--- a/src/db/update/UpdateInternal.hxx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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_UPDATE_INTERNAL_H
-#define MPD_UPDATE_INTERNAL_H
-
-#include "check.h"
-
-extern bool walk_discard;
-extern bool modified;
-
-#endif
diff --git a/src/db/update/UpdateQueue.cxx b/src/db/update/UpdateQueue.cxx
deleted file mode 100644
index a6002f854..000000000
--- a/src/db/update/UpdateQueue.cxx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 "UpdateQueue.hxx"
-
-#include <queue>
-#include <list>
-
-static constexpr unsigned MAX_UPDATE_QUEUE_SIZE = 32;
-
-static std::queue<UpdateQueueItem, std::list<UpdateQueueItem>> update_queue;
-
-bool
-update_queue_push(const char *path, bool discard, unsigned id)
-{
-	if (update_queue.size() >= MAX_UPDATE_QUEUE_SIZE)
-		return false;
-
-	update_queue.emplace(path, discard, id);
-	return true;
-}
-
-UpdateQueueItem
-update_queue_shift()
-{
-	if (update_queue.empty())
-		return UpdateQueueItem();
-
-	auto i = std::move(update_queue.front());
-	update_queue.pop();
-	return i;
-}
diff --git a/src/db/update/UpdateQueue.hxx b/src/db/update/UpdateQueue.hxx
deleted file mode 100644
index e4228f5ed..000000000
--- a/src/db/update/UpdateQueue.hxx
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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_UPDATE_QUEUE_HXX
-#define MPD_UPDATE_QUEUE_HXX
-
-#include "check.h"
-
-#include <string>
-
-struct UpdateQueueItem {
-	std::string path_utf8;
-	unsigned id;
-	bool discard;
-
-	UpdateQueueItem():id(0) {}
-	UpdateQueueItem(const char *_path, bool _discard,
-			unsigned _id)
-		:path_utf8(_path), id(_id), discard(_discard) {}
-
-	bool IsDefined() const {
-		return id != 0;
-	}
-};
-
-bool
-update_queue_push(const char *path, bool discard, unsigned id);
-
-UpdateQueueItem
-update_queue_shift();
-
-#endif
diff --git a/src/db/update/UpdateRemove.cxx b/src/db/update/UpdateRemove.cxx
deleted file mode 100644
index c57758aef..000000000
--- a/src/db/update/UpdateRemove.cxx
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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" /* must be first for large file support */
-#include "UpdateRemove.hxx"
-#include "UpdateDomain.hxx"
-#include "GlobalEvents.hxx"
-#include "thread/Mutex.hxx"
-#include "thread/Cond.hxx"
-#include "db/Song.hxx"
-#include "db/LightSong.hxx"
-#include "Main.hxx"
-#include "Instance.hxx"
-#include "Log.hxx"
-
-#ifdef ENABLE_SQLITE
-#include "sticker/StickerDatabase.hxx"
-#include "sticker/SongSticker.hxx"
-#endif
-
-#include <assert.h>
-
-static const Song *removed_song;
-
-static Mutex remove_mutex;
-static Cond remove_cond;
-
-/**
- * Safely remove a song from the database.  This must be done in the
- * main task, to be sure that there is no pointer left to it.
- */
-static void
-song_remove_event(void)
-{
-	assert(removed_song != nullptr);
-
-	{
-		const auto uri = removed_song->GetURI();
-		FormatDefault(update_domain, "removing %s", uri.c_str());
-	}
-
-#ifdef ENABLE_SQLITE
-	/* if the song has a sticker, remove it */
-	if (sticker_enabled())
-		sticker_song_delete(removed_song->Export());
-#endif
-
-	{
-		const auto uri = removed_song->GetURI();
-		instance->DeleteSong(uri.c_str());
-	}
-
-	/* clear "removed_song" and send signal to update thread */
-	remove_mutex.lock();
-	removed_song = nullptr;
-	remove_cond.signal();
-	remove_mutex.unlock();
-}
-
-void
-update_remove_global_init(void)
-{
-	GlobalEvents::Register(GlobalEvents::DELETE, song_remove_event);
-}
-
-void
-update_remove_song(const Song *song)
-{
-	assert(removed_song == nullptr);
-
-	removed_song = song;
-
-	GlobalEvents::Emit(GlobalEvents::DELETE);
-
-	remove_mutex.lock();
-
-	while (removed_song != nullptr)
-		remove_cond.wait(remove_mutex);
-
-	remove_mutex.unlock();
-}
diff --git a/src/db/update/UpdateRemove.hxx b/src/db/update/UpdateRemove.hxx
deleted file mode 100644
index d54e3aa80..000000000
--- a/src/db/update/UpdateRemove.hxx
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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_UPDATE_REMOVE_HXX
-#define MPD_UPDATE_REMOVE_HXX
-
-#include "check.h"
-
-struct Song;
-
-void
-update_remove_global_init(void);
-
-/**
- * Sends a signal to the main thread which will in turn remove the
- * song: from the sticker database and from the playlist.  This
- * serialized access is implemented to avoid excessive locking.
- */
-void
-update_remove_song(const Song *song);
-
-#endif
diff --git a/src/db/update/UpdateSong.cxx b/src/db/update/UpdateSong.cxx
index ac2d01cd2..afd609f9a 100644
--- a/src/db/update/UpdateSong.cxx
+++ b/src/db/update/UpdateSong.cxx
@@ -18,11 +18,8 @@
  */
 
 #include "config.h" /* must be first for large file support */
-#include "UpdateSong.hxx"
-#include "UpdateInternal.hxx"
+#include "Service.hxx"
 #include "UpdateIO.hxx"
-#include "UpdateDatabase.hxx"
-#include "UpdateContainer.hxx"
 #include "UpdateDomain.hxx"
 #include "db/DatabaseLock.hxx"
 #include "db/Directory.hxx"
@@ -32,10 +29,10 @@
 
 #include <unistd.h>
 
-static void
-update_song_file2(Directory &directory,
-		  const char *name, const struct stat *st,
-		  const char *suffix)
+inline void
+UpdateWalk::UpdateSongFile2(Directory &directory,
+			    const char *name, const char *suffix,
+			    const struct stat *st)
 {
 	db_lock();
 	Song *song = directory.FindSong(name);
@@ -47,7 +44,7 @@ update_song_file2(Directory &directory,
 			    directory.GetPath(), name);
 		if (song != nullptr) {
 			db_lock();
-			delete_song(directory, song);
+			editor.DeleteSong(directory, song);
 			db_unlock();
 		}
 
@@ -56,10 +53,10 @@ update_song_file2(Directory &directory,
 
 	if (!(song != nullptr && st->st_mtime == song->mtime &&
 	      !walk_discard) &&
-	    update_container_file(directory, name, st, suffix)) {
+	    UpdateContainerFile(directory, name, suffix, st)) {
 		if (song != nullptr) {
 			db_lock();
-			delete_song(directory, song);
+			editor.DeleteSong(directory, song);
 			db_unlock();
 		}
 
@@ -92,7 +89,7 @@ update_song_file2(Directory &directory,
 				    "deleting unrecognized file %s/%s",
 				    directory.GetPath(), name);
 			db_lock();
-			delete_song(directory, song);
+			editor.DeleteSong(directory, song);
 			db_unlock();
 		}
 
@@ -101,13 +98,13 @@ update_song_file2(Directory &directory,
 }
 
 bool
-update_song_file(Directory &directory,
-		 const char *name, const char *suffix,
-		 const struct stat *st)
+UpdateWalk::UpdateSongFile(Directory &directory,
+			   const char *name, const char *suffix,
+			   const struct stat *st)
 {
 	if (!decoder_plugins_supports_suffix(suffix))
 		return false;
 
-	update_song_file2(directory, name, st, suffix);
+	UpdateSongFile2(directory, name, suffix, st);
 	return true;
 }
diff --git a/src/db/update/UpdateSong.hxx b/src/db/update/UpdateSong.hxx
deleted file mode 100644
index 5feb01928..000000000
--- a/src/db/update/UpdateSong.hxx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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_UPDATE_SONG_HXX
-#define MPD_UPDATE_SONG_HXX
-
-#include "check.h"
-
-#include <sys/stat.h>
-
-struct Directory;
-
-bool
-update_song_file(Directory &directory,
-		 const char *name, const char *suffix,
-		 const struct stat *st);
-
-#endif
diff --git a/src/db/update/UpdateWalk.cxx b/src/db/update/UpdateWalk.cxx
deleted file mode 100644
index 95c7e6a81..000000000
--- a/src/db/update/UpdateWalk.cxx
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * 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" /* must be first for large file support */
-#include "UpdateWalk.hxx"
-#include "UpdateIO.hxx"
-#include "UpdateDatabase.hxx"
-#include "UpdateSong.hxx"
-#include "UpdateArchive.hxx"
-#include "UpdateDomain.hxx"
-#include "db/DatabaseLock.hxx"
-#include "db/DatabaseSimple.hxx"
-#include "db/Directory.hxx"
-#include "db/Song.hxx"
-#include "db/PlaylistVector.hxx"
-#include "db/Uri.hxx"
-#include "playlist/PlaylistRegistry.hxx"
-#include "Mapper.hxx"
-#include "ExcludeList.hxx"
-#include "config/ConfigGlobal.hxx"
-#include "config/ConfigOption.hxx"
-#include "fs/AllocatedPath.hxx"
-#include "fs/Traits.hxx"
-#include "fs/FileSystem.hxx"
-#include "fs/DirectoryReader.hxx"
-#include "util/Alloc.hxx"
-#include "util/UriUtil.hxx"
-#include "Log.hxx"
-
-#include <assert.h>
-#include <sys/stat.h>
-#include <string.h>
-#include <stdlib.h>
-#include <errno.h>
-
-bool walk_discard;
-bool modified;
-
-#ifndef WIN32
-
-static constexpr bool DEFAULT_FOLLOW_INSIDE_SYMLINKS = true;
-static constexpr bool DEFAULT_FOLLOW_OUTSIDE_SYMLINKS = true;
-
-static bool follow_inside_symlinks;
-static bool follow_outside_symlinks;
-
-#endif
-
-void
-update_walk_global_init(void)
-{
-#ifndef WIN32
-	follow_inside_symlinks =
-		config_get_bool(CONF_FOLLOW_INSIDE_SYMLINKS,
-				DEFAULT_FOLLOW_INSIDE_SYMLINKS);
-
-	follow_outside_symlinks =
-		config_get_bool(CONF_FOLLOW_OUTSIDE_SYMLINKS,
-				DEFAULT_FOLLOW_OUTSIDE_SYMLINKS);
-#endif
-}
-
-void
-update_walk_global_finish(void)
-{
-}
-
-static void
-directory_set_stat(Directory &dir, const struct stat *st)
-{
-	dir.inode = st->st_ino;
-	dir.device = st->st_dev;
-	dir.have_stat = true;
-}
-
-static void
-remove_excluded_from_directory(Directory &directory,
-			       const ExcludeList &exclude_list)
-{
-	db_lock();
-
-	Directory *child, *n;
-	directory_for_each_child_safe(child, n, directory) {
-		const auto name_fs = AllocatedPath::FromUTF8(child->GetName());
-
-		if (name_fs.IsNull() || exclude_list.Check(name_fs)) {
-			delete_directory(child);
-			modified = true;
-		}
-	}
-
-	Song *song, *ns;
-	directory_for_each_song_safe(song, ns, directory) {
-		assert(song->parent == &directory);
-
-		const auto name_fs = AllocatedPath::FromUTF8(song->uri);
-		if (name_fs.IsNull() || exclude_list.Check(name_fs)) {
-			delete_song(directory, song);
-			modified = true;
-		}
-	}
-
-	db_unlock();
-}
-
-static void
-purge_deleted_from_directory(Directory &directory)
-{
-	Directory *child, *n;
-	directory_for_each_child_safe(child, n, directory) {
-		if (directory_exists(*child))
-			continue;
-
-		db_lock();
-		delete_directory(child);
-		db_unlock();
-
-		modified = true;
-	}
-
-	Song *song, *ns;
-	directory_for_each_song_safe(song, ns, directory) {
-		const auto path = map_song_fs(*song);
-		if (path.IsNull() || !FileExists(path)) {
-			db_lock();
-			delete_song(directory, song);
-			db_unlock();
-
-			modified = true;
-		}
-	}
-
-	for (auto i = directory.playlists.begin(),
-		     end = directory.playlists.end();
-	     i != end;) {
-		if (!directory_child_is_regular(directory, i->name.c_str())) {
-			db_lock();
-			i = directory.playlists.erase(i);
-			db_unlock();
-		} else
-			++i;
-	}
-}
-
-#ifndef WIN32
-static int
-update_directory_stat(Directory &directory)
-{
-	struct stat st;
-	if (stat_directory(directory, &st) < 0)
-		return -1;
-
-	directory_set_stat(directory, &st);
-	return 0;
-}
-#endif
-
-static int
-find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
-{
-#ifndef WIN32
-	while (parent) {
-		if (!parent->have_stat && update_directory_stat(*parent) < 0)
-			return -1;
-
-		if (parent->inode == inode && parent->device == device) {
-			LogDebug(update_domain, "recursive directory found");
-			return 1;
-		}
-
-		parent = parent->parent;
-	}
-#else
-	(void)parent;
-	(void)inode;
-	(void)device;
-#endif
-
-	return 0;
-}
-
-static bool
-update_playlist_file2(Directory &directory,
-		      const char *name, const char *suffix,
-		      const struct stat *st)
-{
-	if (!playlist_suffix_supported(suffix))
-		return false;
-
-	PlaylistInfo pi(name, st->st_mtime);
-
-	db_lock();
-	if (directory.playlists.UpdateOrInsert(std::move(pi)))
-		modified = true;
-	db_unlock();
-	return true;
-}
-
-static bool
-update_regular_file(Directory &directory,
-		    const char *name, const struct stat *st)
-{
-	const char *suffix = uri_get_suffix(name);
-	if (suffix == nullptr)
-		return false;
-
-	return update_song_file(directory, name, suffix, st) ||
-		update_archive_file(directory, name, suffix, st) ||
-		update_playlist_file2(directory, name, suffix, st);
-}
-
-static bool
-update_directory(Directory &directory, const struct stat *st);
-
-static void
-update_directory_child(Directory &directory,
-		       const char *name, const struct stat *st)
-{
-	assert(strchr(name, '/') == nullptr);
-
-	if (S_ISREG(st->st_mode)) {
-		update_regular_file(directory, name, st);
-	} else if (S_ISDIR(st->st_mode)) {
-		if (find_inode_ancestor(&directory, st->st_ino, st->st_dev))
-			return;
-
-		db_lock();
-		Directory *subdir = directory.MakeChild(name);
-		db_unlock();
-
-		assert(&directory == subdir->parent);
-
-		if (!update_directory(*subdir, st)) {
-			db_lock();
-			delete_directory(subdir);
-			db_unlock();
-		}
-	} else {
-		FormatDebug(update_domain,
-			    "%s is not a directory, archive or music", name);
-	}
-}
-
-/* we don't look at "." / ".." nor files with newlines in their name */
-gcc_pure
-static bool skip_path(Path path_fs)
-{
-	const char *path = path_fs.c_str();
-	return (path[0] == '.' && path[1] == 0) ||
-		(path[0] == '.' && path[1] == '.' && path[2] == 0) ||
-		strchr(path, '\n') != nullptr;
-}
-
-gcc_pure
-static bool
-skip_symlink(const Directory *directory, const char *utf8_name)
-{
-#ifndef WIN32
-	const auto path_fs = map_directory_child_fs(*directory, utf8_name);
-	if (path_fs.IsNull())
-		return true;
-
-	const auto target = ReadLink(path_fs);
-	if (target.IsNull())
-		/* don't skip if this is not a symlink */
-		return errno != EINVAL;
-
-	if (!follow_inside_symlinks && !follow_outside_symlinks) {
-		/* ignore all symlinks */
-		return true;
-	} else if (follow_inside_symlinks && follow_outside_symlinks) {
-		/* consider all symlinks */
-		return false;
-	}
-
-	const char *target_str = target.c_str();
-
-	if (PathTraitsFS::IsAbsolute(target_str)) {
-		/* if the symlink points to an absolute path, see if
-		   that path is inside the music directory */
-		const char *relative = map_to_relative_path(target_str);
-		return relative > target_str
-			? !follow_inside_symlinks
-			: !follow_outside_symlinks;
-	}
-
-	const char *p = target_str;
-	while (*p == '.') {
-		if (p[1] == '.' && PathTraitsFS::IsSeparator(p[2])) {
-			/* "../" moves to parent directory */
-			directory = directory->parent;
-			if (directory == nullptr) {
-				/* we have moved outside the music
-				   directory - skip this symlink
-				   if such symlinks are not allowed */
-				return !follow_outside_symlinks;
-			}
-			p += 3;
-		} else if (PathTraitsFS::IsSeparator(p[1]))
-			/* eliminate "./" */
-			p += 2;
-		else
-			break;
-	}
-
-	/* we are still in the music directory, so this symlink points
-	   to a song which is already in the database - skip according
-	   to the follow_inside_symlinks param*/
-	return !follow_inside_symlinks;
-#else
-	/* no symlink checking on WIN32 */
-
-	(void)directory;
-	(void)utf8_name;
-
-	return false;
-#endif
-}
-
-static bool
-update_directory(Directory &directory, const struct stat *st)
-{
-	assert(S_ISDIR(st->st_mode));
-
-	directory_set_stat(directory, st);
-
-	const auto path_fs = map_directory_fs(directory);
-	if (path_fs.IsNull())
-		return false;
-
-	DirectoryReader reader(path_fs);
-	if (reader.HasFailed()) {
-		int error = errno;
-		const auto path_utf8 = path_fs.ToUTF8();
-		FormatErrno(update_domain, error,
-			    "Failed to open directory %s",
-			    path_utf8.c_str());
-		return false;
-	}
-
-	ExcludeList exclude_list;
-	exclude_list.LoadFile(AllocatedPath::Build(path_fs, ".mpdignore"));
-
-	if (!exclude_list.IsEmpty())
-		remove_excluded_from_directory(directory, exclude_list);
-
-	purge_deleted_from_directory(directory);
-
-	while (reader.ReadEntry()) {
-		std::string utf8;
-		struct stat st2;
-
-		const auto entry = reader.GetEntry();
-
-		if (skip_path(entry) || exclude_list.Check(entry))
-			continue;
-
-		utf8 = entry.ToUTF8();
-		if (utf8.empty())
-			continue;
-
-		if (skip_symlink(&directory, utf8.c_str())) {
-			modified |= delete_name_in(directory, utf8.c_str());
-			continue;
-		}
-
-		if (stat_directory_child(directory, utf8.c_str(), &st2) == 0)
-			update_directory_child(directory, utf8.c_str(), &st2);
-		else
-			modified |= delete_name_in(directory, utf8.c_str());
-	}
-
-	directory.mtime = st->st_mtime;
-
-	return true;
-}
-
-static Directory *
-directory_make_child_checked(Directory &parent, const char *name_utf8)
-{
-	db_lock();
-	Directory *directory = parent.FindChild(name_utf8);
-	db_unlock();
-
-	if (directory != nullptr)
-		return directory;
-
-	struct stat st;
-	if (stat_directory_child(parent, name_utf8, &st) < 0 ||
-	    find_inode_ancestor(&parent, st.st_ino, st.st_dev))
-		return nullptr;
-
-	if (skip_symlink(&parent, name_utf8))
-		return nullptr;
-
-	/* if we're adding directory paths, make sure to delete filenames
-	   with potentially the same name */
-	db_lock();
-	Song *conflicting = parent.FindSong(name_utf8);
-	if (conflicting)
-		delete_song(parent, conflicting);
-
-	directory = parent.CreateChild(name_utf8);
-	db_unlock();
-
-	directory_set_stat(*directory, &st);
-	return directory;
-}
-
-static Directory *
-directory_make_uri_parent_checked(const char *uri)
-{
-	Directory *directory = db_get_root();
-	char *duplicated = xstrdup(uri);
-	char *name_utf8 = duplicated, *slash;
-
-	while ((slash = strchr(name_utf8, '/')) != nullptr) {
-		*slash = 0;
-
-		if (*name_utf8 == 0)
-			continue;
-
-		directory = directory_make_child_checked(*directory,
-							 name_utf8);
-		if (directory == nullptr)
-			break;
-
-		name_utf8 = slash + 1;
-	}
-
-	free(duplicated);
-	return directory;
-}
-
-static void
-update_uri(const char *uri)
-{
-	Directory *parent = directory_make_uri_parent_checked(uri);
-	if (parent == nullptr)
-		return;
-
-	const char *name = PathTraitsUTF8::GetBase(uri);
-
-	struct stat st;
-	if (!skip_symlink(parent, name) &&
-	    stat_directory_child(*parent, name, &st) == 0)
-		update_directory_child(*parent, name, &st);
-	else
-		modified |= delete_name_in(*parent, name);
-}
-
-bool
-update_walk(const char *path, bool discard)
-{
-	walk_discard = discard;
-	modified = false;
-
-	if (path != nullptr && !isRootDirectory(path)) {
-		update_uri(path);
-	} else {
-		Directory *directory = db_get_root();
-		struct stat st;
-
-		if (stat_directory(*directory, &st) == 0)
-			update_directory(*directory, &st);
-	}
-
-	return modified;
-}
diff --git a/src/db/update/UpdateWalk.hxx b/src/db/update/UpdateWalk.hxx
deleted file mode 100644
index e908829e3..000000000
--- a/src/db/update/UpdateWalk.hxx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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_UPDATE_WALK_HXX
-#define MPD_UPDATE_WALK_HXX
-
-#include "check.h"
-
-void
-update_walk_global_init(void);
-
-void
-update_walk_global_finish(void);
-
-/**
- * Returns true if the database was modified.
- */
-bool
-update_walk(const char *path, bool discard);
-
-#endif
diff --git a/src/db/update/Walk.cxx b/src/db/update/Walk.cxx
new file mode 100644
index 000000000..5a799d8f7
--- /dev/null
+++ b/src/db/update/Walk.cxx
@@ -0,0 +1,463 @@
+/*
+ * 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" /* must be first for large file support */
+#include "Walk.hxx"
+#include "UpdateIO.hxx"
+#include "Editor.hxx"
+#include "UpdateDomain.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/DatabaseSimple.hxx"
+#include "db/Directory.hxx"
+#include "db/Song.hxx"
+#include "db/PlaylistVector.hxx"
+#include "db/Uri.hxx"
+#include "playlist/PlaylistRegistry.hxx"
+#include "Mapper.hxx"
+#include "ExcludeList.hxx"
+#include "config/ConfigGlobal.hxx"
+#include "config/ConfigOption.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "fs/Traits.hxx"
+#include "fs/FileSystem.hxx"
+#include "fs/DirectoryReader.hxx"
+#include "util/Alloc.hxx"
+#include "util/UriUtil.hxx"
+#include "Log.hxx"
+
+#include <assert.h>
+#include <sys/stat.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+
+UpdateWalk::UpdateWalk(EventLoop &_loop)
+	:editor(_loop)
+{
+#ifndef WIN32
+	follow_inside_symlinks =
+		config_get_bool(CONF_FOLLOW_INSIDE_SYMLINKS,
+				DEFAULT_FOLLOW_INSIDE_SYMLINKS);
+
+	follow_outside_symlinks =
+		config_get_bool(CONF_FOLLOW_OUTSIDE_SYMLINKS,
+				DEFAULT_FOLLOW_OUTSIDE_SYMLINKS);
+#endif
+}
+
+static void
+directory_set_stat(Directory &dir, const struct stat *st)
+{
+	dir.inode = st->st_ino;
+	dir.device = st->st_dev;
+	dir.have_stat = true;
+}
+
+inline void
+UpdateWalk::RemoveExcludedFromDirectory(Directory &directory,
+					const ExcludeList &exclude_list)
+{
+	db_lock();
+
+	Directory *child, *n;
+	directory_for_each_child_safe(child, n, directory) {
+		const auto name_fs = AllocatedPath::FromUTF8(child->GetName());
+
+		if (name_fs.IsNull() || exclude_list.Check(name_fs)) {
+			editor.DeleteDirectory(child);
+			modified = true;
+		}
+	}
+
+	Song *song, *ns;
+	directory_for_each_song_safe(song, ns, directory) {
+		assert(song->parent == &directory);
+
+		const auto name_fs = AllocatedPath::FromUTF8(song->uri);
+		if (name_fs.IsNull() || exclude_list.Check(name_fs)) {
+			editor.DeleteSong(directory, song);
+			modified = true;
+		}
+	}
+
+	db_unlock();
+}
+
+inline void
+UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
+{
+	Directory *child, *n;
+	directory_for_each_child_safe(child, n, directory) {
+		if (directory_exists(*child))
+			continue;
+
+		db_lock();
+		editor.DeleteDirectory(child);
+		db_unlock();
+
+		modified = true;
+	}
+
+	Song *song, *ns;
+	directory_for_each_song_safe(song, ns, directory) {
+		const auto path = map_song_fs(*song);
+		if (path.IsNull() || !FileExists(path)) {
+			db_lock();
+			editor.DeleteSong(directory, song);
+			db_unlock();
+
+			modified = true;
+		}
+	}
+
+	for (auto i = directory.playlists.begin(),
+		     end = directory.playlists.end();
+	     i != end;) {
+		if (!directory_child_is_regular(directory, i->name.c_str())) {
+			db_lock();
+			i = directory.playlists.erase(i);
+			db_unlock();
+		} else
+			++i;
+	}
+}
+
+#ifndef WIN32
+static int
+update_directory_stat(Directory &directory)
+{
+	struct stat st;
+	if (stat_directory(directory, &st) < 0)
+		return -1;
+
+	directory_set_stat(directory, &st);
+	return 0;
+}
+#endif
+
+static int
+find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
+{
+#ifndef WIN32
+	while (parent) {
+		if (!parent->have_stat && update_directory_stat(*parent) < 0)
+			return -1;
+
+		if (parent->inode == inode && parent->device == device) {
+			LogDebug(update_domain, "recursive directory found");
+			return 1;
+		}
+
+		parent = parent->parent;
+	}
+#else
+	(void)parent;
+	(void)inode;
+	(void)device;
+#endif
+
+	return 0;
+}
+
+inline bool
+UpdateWalk::UpdatePlaylistFile(Directory &directory,
+			       const char *name, const char *suffix,
+			       const struct stat *st)
+{
+	if (!playlist_suffix_supported(suffix))
+		return false;
+
+	PlaylistInfo pi(name, st->st_mtime);
+
+	db_lock();
+	if (directory.playlists.UpdateOrInsert(std::move(pi)))
+		modified = true;
+	db_unlock();
+	return true;
+}
+
+inline bool
+UpdateWalk::UpdateRegularFile(Directory &directory,
+			      const char *name, const struct stat *st)
+{
+	const char *suffix = uri_get_suffix(name);
+	if (suffix == nullptr)
+		return false;
+
+	return UpdateSongFile(directory, name, suffix, st) ||
+		UpdateArchiveFile(directory, name, suffix, st) ||
+		UpdatePlaylistFile(directory, name, suffix, st);
+}
+
+void
+UpdateWalk::UpdateDirectoryChild(Directory &directory,
+				 const char *name, const struct stat *st)
+{
+	assert(strchr(name, '/') == nullptr);
+
+	if (S_ISREG(st->st_mode)) {
+		UpdateRegularFile(directory, name, st);
+	} else if (S_ISDIR(st->st_mode)) {
+		if (find_inode_ancestor(&directory, st->st_ino, st->st_dev))
+			return;
+
+		db_lock();
+		Directory *subdir = directory.MakeChild(name);
+		db_unlock();
+
+		assert(&directory == subdir->parent);
+
+		if (!UpdateDirectory(*subdir, st)) {
+			db_lock();
+			editor.DeleteDirectory(subdir);
+			db_unlock();
+		}
+	} else {
+		FormatDebug(update_domain,
+			    "%s is not a directory, archive or music", name);
+	}
+}
+
+/* we don't look at "." / ".." nor files with newlines in their name */
+gcc_pure
+static bool skip_path(Path path_fs)
+{
+	const char *path = path_fs.c_str();
+	return (path[0] == '.' && path[1] == 0) ||
+		(path[0] == '.' && path[1] == '.' && path[2] == 0) ||
+		strchr(path, '\n') != nullptr;
+}
+
+gcc_pure
+bool
+UpdateWalk::SkipSymlink(const Directory *directory,
+			const char *utf8_name) const
+{
+#ifndef WIN32
+	const auto path_fs = map_directory_child_fs(*directory, utf8_name);
+	if (path_fs.IsNull())
+		return true;
+
+	const auto target = ReadLink(path_fs);
+	if (target.IsNull())
+		/* don't skip if this is not a symlink */
+		return errno != EINVAL;
+
+	if (!follow_inside_symlinks && !follow_outside_symlinks) {
+		/* ignore all symlinks */
+		return true;
+	} else if (follow_inside_symlinks && follow_outside_symlinks) {
+		/* consider all symlinks */
+		return false;
+	}
+
+	const char *target_str = target.c_str();
+
+	if (PathTraitsFS::IsAbsolute(target_str)) {
+		/* if the symlink points to an absolute path, see if
+		   that path is inside the music directory */
+		const char *relative = map_to_relative_path(target_str);
+		return relative > target_str
+			? !follow_inside_symlinks
+			: !follow_outside_symlinks;
+	}
+
+	const char *p = target_str;
+	while (*p == '.') {
+		if (p[1] == '.' && PathTraitsFS::IsSeparator(p[2])) {
+			/* "../" moves to parent directory */
+			directory = directory->parent;
+			if (directory == nullptr) {
+				/* we have moved outside the music
+				   directory - skip this symlink
+				   if such symlinks are not allowed */
+				return !follow_outside_symlinks;
+			}
+			p += 3;
+		} else if (PathTraitsFS::IsSeparator(p[1]))
+			/* eliminate "./" */
+			p += 2;
+		else
+			break;
+	}
+
+	/* we are still in the music directory, so this symlink points
+	   to a song which is already in the database - skip according
+	   to the follow_inside_symlinks param*/
+	return !follow_inside_symlinks;
+#else
+	/* no symlink checking on WIN32 */
+
+	(void)directory;
+	(void)utf8_name;
+
+	return false;
+#endif
+}
+
+bool
+UpdateWalk::UpdateDirectory(Directory &directory, const struct stat *st)
+{
+	assert(S_ISDIR(st->st_mode));
+
+	directory_set_stat(directory, st);
+
+	const auto path_fs = map_directory_fs(directory);
+	if (path_fs.IsNull())
+		return false;
+
+	DirectoryReader reader(path_fs);
+	if (reader.HasFailed()) {
+		int error = errno;
+		const auto path_utf8 = path_fs.ToUTF8();
+		FormatErrno(update_domain, error,
+			    "Failed to open directory %s",
+			    path_utf8.c_str());
+		return false;
+	}
+
+	ExcludeList exclude_list;
+	exclude_list.LoadFile(AllocatedPath::Build(path_fs, ".mpdignore"));
+
+	if (!exclude_list.IsEmpty())
+		RemoveExcludedFromDirectory(directory, exclude_list);
+
+	PurgeDeletedFromDirectory(directory);
+
+	while (reader.ReadEntry()) {
+		std::string utf8;
+		struct stat st2;
+
+		const auto entry = reader.GetEntry();
+
+		if (skip_path(entry) || exclude_list.Check(entry))
+			continue;
+
+		utf8 = entry.ToUTF8();
+		if (utf8.empty())
+			continue;
+
+		if (SkipSymlink(&directory, utf8.c_str())) {
+			modified |= editor.DeleteNameIn(directory, utf8.c_str());
+			continue;
+		}
+
+		if (stat_directory_child(directory, utf8.c_str(), &st2) == 0)
+			UpdateDirectoryChild(directory, utf8.c_str(), &st2);
+		else
+			modified |= editor.DeleteNameIn(directory, utf8.c_str());
+	}
+
+	directory.mtime = st->st_mtime;
+
+	return true;
+}
+
+inline Directory *
+UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8)
+{
+	db_lock();
+	Directory *directory = parent.FindChild(name_utf8);
+	db_unlock();
+
+	if (directory != nullptr)
+		return directory;
+
+	struct stat st;
+	if (stat_directory_child(parent, name_utf8, &st) < 0 ||
+	    find_inode_ancestor(&parent, st.st_ino, st.st_dev))
+		return nullptr;
+
+	if (SkipSymlink(&parent, name_utf8))
+		return nullptr;
+
+	/* if we're adding directory paths, make sure to delete filenames
+	   with potentially the same name */
+	db_lock();
+	Song *conflicting = parent.FindSong(name_utf8);
+	if (conflicting)
+		editor.DeleteSong(parent, conflicting);
+
+	directory = parent.CreateChild(name_utf8);
+	db_unlock();
+
+	directory_set_stat(*directory, &st);
+	return directory;
+}
+
+inline Directory *
+UpdateWalk::DirectoryMakeUriParentChecked(const char *uri)
+{
+	Directory *directory = db_get_root();
+	char *duplicated = xstrdup(uri);
+	char *name_utf8 = duplicated, *slash;
+
+	while ((slash = strchr(name_utf8, '/')) != nullptr) {
+		*slash = 0;
+
+		if (*name_utf8 == 0)
+			continue;
+
+		directory = DirectoryMakeChildChecked(*directory,
+						      name_utf8);
+		if (directory == nullptr)
+			break;
+
+		name_utf8 = slash + 1;
+	}
+
+	free(duplicated);
+	return directory;
+}
+
+inline void
+UpdateWalk::UpdateUri(const char *uri)
+{
+	Directory *parent = DirectoryMakeUriParentChecked(uri);
+	if (parent == nullptr)
+		return;
+
+	const char *name = PathTraitsUTF8::GetBase(uri);
+
+	struct stat st;
+	if (!SkipSymlink(parent, name) &&
+	    stat_directory_child(*parent, name, &st) == 0)
+		UpdateDirectoryChild(*parent, name, &st);
+	else
+		modified |= editor.DeleteNameIn(*parent, name);
+}
+
+bool
+UpdateWalk::Walk(const char *path, bool discard)
+{
+	walk_discard = discard;
+	modified = false;
+
+	if (path != nullptr && !isRootDirectory(path)) {
+		UpdateUri(path);
+	} else {
+		Directory *directory = db_get_root();
+		struct stat st;
+
+		if (stat_directory(*directory, &st) == 0)
+			UpdateDirectory(*directory, &st);
+	}
+
+	return modified;
+}
diff --git a/src/db/update/Walk.hxx b/src/db/update/Walk.hxx
new file mode 100644
index 000000000..12274ccdf
--- /dev/null
+++ b/src/db/update/Walk.hxx
@@ -0,0 +1,134 @@
+/*
+ * 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_UPDATE_WALK_HXX
+#define MPD_UPDATE_WALK_HXX
+
+#include "check.h"
+#include "Editor.hxx"
+
+#include <sys/stat.h>
+
+struct stat;
+struct Directory;
+struct archive_plugin;
+class ExcludeList;
+
+class UpdateWalk final {
+#ifdef ENABLE_ARCHIVE
+	friend class UpdateArchiveVisitor;
+#endif
+
+#ifndef WIN32
+	static constexpr bool DEFAULT_FOLLOW_INSIDE_SYMLINKS = true;
+	static constexpr bool DEFAULT_FOLLOW_OUTSIDE_SYMLINKS = true;
+
+	bool follow_inside_symlinks;
+	bool follow_outside_symlinks;
+#endif
+
+	bool walk_discard;
+	bool modified;
+
+	DatabaseEditor editor;
+
+public:
+	UpdateWalk(EventLoop &_loop);
+
+	/**
+	 * Returns true if the database was modified.
+	 */
+	bool Walk(const char *path, bool discard);
+
+private:
+	gcc_pure
+	bool SkipSymlink(const Directory *directory,
+			 const char *utf8_name) const;
+
+	void RemoveExcludedFromDirectory(Directory &directory,
+					 const ExcludeList &exclude_list);
+
+	void PurgeDeletedFromDirectory(Directory &directory);
+
+	void UpdateSongFile2(Directory &directory,
+			     const char *name, const char *suffix,
+			     const struct stat *st);
+
+	bool UpdateSongFile(Directory &directory,
+			    const char *name, const char *suffix,
+			    const struct stat *st);
+
+	bool UpdateContainerFile(Directory &directory,
+				 const char *name, const char *suffix,
+				 const struct stat *st);
+
+
+#ifdef ENABLE_ARCHIVE
+	void UpdateArchiveTree(Directory &parent, const char *name);
+
+	bool UpdateArchiveFile(Directory &directory,
+			       const char *name, const char *suffix,
+			       const struct stat *st);
+
+	void UpdateArchiveFile(Directory &directory, const char *name,
+			       const struct stat *st,
+			       const archive_plugin &plugin);
+
+
+#else
+	bool UpdateArchiveFile(gcc_unused Directory &directory,
+			       gcc_unused const char *name,
+			       gcc_unused const char *suffix,
+			       gcc_unused const struct stat *st) {
+		return false;
+	}
+#endif
+
+	bool UpdatePlaylistFile(Directory &directory,
+				const char *name, const char *suffix,
+				const struct stat *st);
+
+	bool UpdateRegularFile(Directory &directory,
+			       const char *name, const struct stat *st);
+
+	void UpdateDirectoryChild(Directory &directory,
+				  const char *name, const struct stat *st);
+
+	bool UpdateDirectory(Directory &directory, const struct stat *st);
+
+	/**
+	 * Create the specified directory object if it does not exist
+	 * already or if the #stat object indicates that it has been
+	 * modified since the last update.  Returns nullptr when it
+	 * exists already and is unmodified.
+	 *
+	 * The caller must lock the database.
+	 */
+	Directory *MakeDirectoryIfModified(Directory &parent, const char *name,
+					   const struct stat *st);
+
+	Directory *DirectoryMakeChildChecked(Directory &parent,
+					     const char *name_utf8);
+
+	Directory *DirectoryMakeUriParentChecked(const char *uri);
+
+	void UpdateUri(const char *uri);
+};
+
+#endif
-- 
cgit v1.2.3