From 9ae7f186bc43749383594807b1d751b5389161e7 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Wed, 29 Jan 2014 18:14:57 +0100
Subject: [PATCH] LocalStorage: new API abstracting filesystem walk

Prepare to make this a new plugin API, for example to use a SMB share
for the music_directory.
---
 Makefile.am                  |  15 +++
 src/Mapper.cxx               |  13 ++-
 src/Mapper.hxx               |  22 -----
 src/db/update/Archive.cxx    |  18 ++--
 src/db/update/Container.cxx  |  27 +++---
 src/db/update/UpdateIO.cxx   |  97 +++++++++----------
 src/db/update/UpdateIO.hxx   |  28 ++++--
 src/db/update/UpdateSong.cxx |  15 +--
 src/db/update/Walk.cxx       | 181 +++++++++++++++++++----------------
 src/db/update/Walk.hxx       |  27 +++---
 src/storage/FileInfo.hxx     |  62 ++++++++++++
 src/storage/LocalStorage.cxx | 154 +++++++++++++++++++++++++++++
 src/storage/LocalStorage.hxx |  89 +++++++++++++++++
 13 files changed, 546 insertions(+), 202 deletions(-)
 create mode 100644 src/storage/FileInfo.hxx
 create mode 100644 src/storage/LocalStorage.cxx
 create mode 100644 src/storage/LocalStorage.hxx

diff --git a/Makefile.am b/Makefile.am
index 5e3f3b260..fe3932975 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -31,6 +31,7 @@ src_mpd_CPPFLAGS = $(AM_CPPFLAGS) \
 src_mpd_LDADD = \
 	$(NEIGHBOR_LIBS) \
 	$(DB_LIBS) \
+	$(STORAGE_LIBS) \
 	$(PLAYLIST_LIBS) \
 	$(AVAHI_LIBS) \
 	$(LIBWRAP_LDFLAGS) \
@@ -410,6 +411,20 @@ libfs_a_SOURCES = \
 	src/fs/StandardDirectory.cxx src/fs/StandardDirectory.hxx \
 	src/fs/DirectoryReader.hxx
 
+# Storage library
+
+if ENABLE_DATABASE
+
+noinst_LIBRARIES += libstorage.a
+
+libstorage_a_SOURCES = \
+	src/storage/LocalStorage.cxx src/storage/LocalStorage.hxx \
+	src/storage/FileInfo.hxx
+
+STORAGE_LIBS = libstorage.a
+
+endif
+
 # neighbor plugins
 
 if ENABLE_NEIGHBOR_PLUGINS
diff --git a/src/Mapper.cxx b/src/Mapper.cxx
index 60928dbb9..786634ebb 100644
--- a/src/Mapper.cxx
+++ b/src/Mapper.cxx
@@ -193,7 +193,8 @@ map_uri_fs(const char *uri)
 	return AllocatedPath::Build(music_dir_fs, uri_fs);
 }
 
-AllocatedPath
+gcc_pure
+static AllocatedPath
 map_directory_fs(const Directory &directory)
 {
 	assert(!music_dir_fs.IsNull());
@@ -204,7 +205,15 @@ map_directory_fs(const Directory &directory)
 	return map_uri_fs(directory.GetPath());
 }
 
-AllocatedPath
+/**
+ * Determines the file system path of a directory's child (may be a
+ * sub directory or a song).
+ *
+ * @param directory the parent directory object
+ * @param name the child's name in UTF-8
+ * @return the path in file system encoding, or nullptr if mapping failed
+ */
+static AllocatedPath
 map_directory_child_fs(const Directory &directory, const char *name)
 {
 	assert(!music_dir_fs.IsNull());
diff --git a/src/Mapper.hxx b/src/Mapper.hxx
index 33d3b94f5..a94a4fbb5 100644
--- a/src/Mapper.hxx
+++ b/src/Mapper.hxx
@@ -91,28 +91,6 @@ gcc_pure
 AllocatedPath
 map_uri_fs(const char *uri);
 
-/**
- * Determines the file system path of a directory object.
- *
- * @param directory the directory object
- * @return the path in file system encoding, or nullptr if mapping failed
- */
-gcc_pure
-AllocatedPath
-map_directory_fs(const Directory &directory);
-
-/**
- * Determines the file system path of a directory's child (may be a
- * sub directory or a song).
- *
- * @param directory the parent directory object
- * @param name the child's name in UTF-8
- * @return the path in file system encoding, or nullptr if mapping failed
- */
-gcc_pure
-AllocatedPath
-map_directory_child_fs(const Directory &directory, const char *name);
-
 /**
  * "Detach" the #Song object, i.e. convert it to a #DetachedSong
  * instance.
diff --git a/src/db/update/Archive.cxx b/src/db/update/Archive.cxx
index 0d7127685..9874eb1f2 100644
--- a/src/db/update/Archive.cxx
+++ b/src/db/update/Archive.cxx
@@ -23,8 +23,8 @@
 #include "db/DatabaseLock.hxx"
 #include "db/Directory.hxx"
 #include "db/Song.hxx"
-#include "Mapper.hxx"
 #include "fs/AllocatedPath.hxx"
+#include "storage/FileInfo.hxx"
 #include "archive/ArchiveList.hxx"
 #include "archive/ArchivePlugin.hxx"
 #include "archive/ArchiveFile.hxx"
@@ -103,20 +103,24 @@ class UpdateArchiveVisitor final : public ArchiveVisitor {
  */
 void
 UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
-			      const struct stat *st,
+			      const FileInfo &info,
 			      const archive_plugin &plugin)
 {
 	db_lock();
 	Directory *directory = parent.FindChild(name);
 	db_unlock();
 
-	if (directory != nullptr && directory->mtime == st->st_mtime &&
+	if (directory != nullptr && directory->mtime == info.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);
+	const auto path_fs = storage.MapChildFS(parent.GetPath(), name);
+	if (path_fs.IsNull())
+		/* not a local file: skip, because the archive API
+		   supports only local files */
+		return;
 
 	/* open archive */
 	Error error;
@@ -141,7 +145,7 @@ UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
 		db_unlock();
 	}
 
-	directory->mtime = st->st_mtime;
+	directory->mtime = info.mtime;
 
 	UpdateArchiveVisitor visitor(*this, directory);
 	file->Visit(visitor);
@@ -151,13 +155,13 @@ UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
 bool
 UpdateWalk::UpdateArchiveFile(Directory &directory,
 			      const char *name, const char *suffix,
-			      const struct stat *st)
+			      const FileInfo &info)
 {
 	const struct archive_plugin *plugin =
 		archive_plugin_from_suffix(suffix);
 	if (plugin == nullptr)
 		return false;
 
-	UpdateArchiveFile(directory, name, st, *plugin);
+	UpdateArchiveFile(directory, name, info, *plugin);
 	return true;
 }
diff --git a/src/db/update/Container.cxx b/src/db/update/Container.cxx
index 33e29953d..956db7209 100644
--- a/src/db/update/Container.cxx
+++ b/src/db/update/Container.cxx
@@ -25,8 +25,8 @@
 #include "db/Song.hxx"
 #include "decoder/DecoderPlugin.hxx"
 #include "decoder/DecoderList.hxx"
-#include "Mapper.hxx"
 #include "fs/AllocatedPath.hxx"
+#include "storage/FileInfo.hxx"
 #include "tag/TagHandler.hxx"
 #include "tag/TagBuilder.hxx"
 #include "Log.hxx"
@@ -37,13 +37,13 @@
 
 Directory *
 UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name,
-				    const struct stat *st)
+				    const FileInfo &info)
 {
 	Directory *directory = parent.FindChild(name);
 
 	// directory exists already
 	if (directory != nullptr) {
-		if (directory->mtime == st->st_mtime && !walk_discard) {
+		if (directory->mtime == info.mtime && !walk_discard) {
 			/* not modified */
 			return nullptr;
 		}
@@ -53,7 +53,7 @@ UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name,
 	}
 
 	directory = parent.MakeChild(name);
-	directory->mtime = st->st_mtime;
+	directory->mtime = info.mtime;
 	return directory;
 }
 
@@ -67,7 +67,7 @@ SupportsContainerSuffix(const DecoderPlugin &plugin, const char *suffix)
 bool
 UpdateWalk::UpdateContainerFile(Directory &directory,
 				const char *name, const char *suffix,
-				const struct stat *st)
+				const FileInfo &info)
 {
 	const DecoderPlugin *_plugin = decoder_plugins_find([suffix](const DecoderPlugin &plugin){
 			return SupportsContainerSuffix(plugin, suffix);
@@ -77,7 +77,7 @@ UpdateWalk::UpdateContainerFile(Directory &directory,
 	const DecoderPlugin &plugin = *_plugin;
 
 	db_lock();
-	Directory *contdir = MakeDirectoryIfModified(directory, name, st);
+	Directory *contdir = MakeDirectoryIfModified(directory, name, info);
 	if (contdir == nullptr) {
 		/* not modified */
 		db_unlock();
@@ -87,7 +87,13 @@ UpdateWalk::UpdateContainerFile(Directory &directory,
 	contdir->device = DEVICE_CONTAINER;
 	db_unlock();
 
-	const auto pathname = map_directory_child_fs(directory, name);
+	const auto pathname = storage.MapFS(contdir->GetPath());
+	if (pathname.IsNull()) {
+		/* not a local file: skip, because the container API
+		   supports only local files */
+		editor.LockDeleteDirectory(contdir);
+		return false;
+	}
 
 	char *vtrack;
 	unsigned int tnum = 0;
@@ -96,11 +102,10 @@ UpdateWalk::UpdateContainerFile(Directory &directory,
 		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);
+		song->mtime = info.mtime;
 
+		const auto child_path_fs = AllocatedPath::Build(pathname,
+								vtrack);
 		plugin.ScanFile(child_path_fs.c_str(),
 				add_tag_handler, &tag_builder);
 
diff --git a/src/db/update/UpdateIO.cxx b/src/db/update/UpdateIO.cxx
index f91caf359..58b1fe296 100644
--- a/src/db/update/UpdateIO.cxx
+++ b/src/db/update/UpdateIO.cxx
@@ -21,91 +21,84 @@
 #include "UpdateIO.hxx"
 #include "UpdateDomain.hxx"
 #include "db/Directory.hxx"
-#include "Mapper.hxx"
-#include "fs/AllocatedPath.hxx"
+#include "storage/FileInfo.hxx"
+#include "storage/LocalStorage.hxx"
+#include "fs/Traits.hxx"
 #include "fs/FileSystem.hxx"
+#include "util/Error.hxx"
 #include "Log.hxx"
 
 #include <errno.h>
 #include <unistd.h>
 
-int
-stat_directory(const Directory &directory, struct stat *st)
+bool
+GetInfo(LocalStorage &storage, const char *uri_utf8, FileInfo &info)
 {
-	const auto path_fs = map_directory_fs(directory);
-	if (path_fs.IsNull())
-		return -1;
-
-	if (!StatFile(path_fs, *st)) {
-		int error = errno;
-		const std::string path_utf8 = path_fs.ToUTF8();
-		FormatErrno(update_domain, error,
-			    "Failed to stat %s", path_utf8.c_str());
-		return -1;
-	}
-
-	return 0;
-}
-
-int
-stat_directory_child(const Directory &parent, const char *name,
-		     struct stat *st)
-{
-	const auto path_fs = map_directory_child_fs(parent, name);
-	if (path_fs.IsNull())
-		return -1;
-
-	if (!StatFile(path_fs, *st)) {
-		int error = errno;
-		const std::string path_utf8 = path_fs.ToUTF8();
-		FormatErrno(update_domain, error,
-			    "Failed to stat %s", path_utf8.c_str());
-		return -1;
-	}
-
-	return 0;
+	Error error;
+	bool success = storage.GetInfo(uri_utf8, true, info, error);
+	if (!success)
+		LogError(error);
+	return success;
 }
 
 bool
-directory_exists(const Directory &directory)
+GetInfo(LocalDirectoryReader &reader, FileInfo &info)
 {
-	const auto path_fs = map_directory_fs(directory);
-	if (path_fs.IsNull())
-		/* invalid path: cannot exist */
+	Error error;
+	bool success = reader.GetInfo(true, info, error);
+	if (!success)
+		LogError(error);
+	return success;
+}
+
+bool
+DirectoryExists(LocalStorage &storage, const Directory &directory)
+{
+	FileInfo info;
+	if (!storage.GetInfo(directory.GetPath(), true, info, IgnoreError()))
 		return false;
 
 	return directory.device == DEVICE_INARCHIVE ||
 		directory.device == DEVICE_CONTAINER
-		? FileExists(path_fs)
-		: DirectoryExists(path_fs);
+		? info.IsRegular()
+		: info.IsDirectory();
+}
+
+static bool
+GetDirectoryChildInfo(LocalStorage &storage, const Directory &directory,
+		      const char *name_utf8, FileInfo &info, Error &error)
+{
+	const auto uri_utf8 = PathTraitsUTF8::Build(directory.GetPath(),
+						    name_utf8);
+	return storage.GetInfo(uri_utf8.c_str(), true, info, error);
 }
 
 bool
-directory_child_is_regular(const Directory &directory,
+directory_child_is_regular(LocalStorage &storage, const Directory &directory,
 			   const char *name_utf8)
 {
-	const auto path_fs = map_directory_child_fs(directory, name_utf8);
-	if (path_fs.IsNull())
-		return false;
-
-	return FileExists(path_fs);
+	FileInfo info;
+	return GetDirectoryChildInfo(storage, directory, name_utf8, info,
+				     IgnoreError()) &&
+		info.IsRegular();
 }
 
 bool
-directory_child_access(const Directory &directory,
+directory_child_access(LocalStorage &storage, const Directory &directory,
 		       const char *name, int mode)
 {
 #ifdef WIN32
 	/* CheckAccess() is useless on WIN32 */
+	(void)storage;
 	(void)directory;
 	(void)name;
 	(void)mode;
 	return true;
 #else
-	const auto path = map_directory_child_fs(directory, name);
+	const auto path = storage.MapChildFS(directory.GetPath(), name);
 	if (path.IsNull())
-		/* something went wrong, but that isn't a permission
-		   problem */
+		/* does not point to local file: silently ignore the
+		   check */
 		return true;
 
 	return CheckAccess(path, mode) || errno != EACCES;
diff --git a/src/db/update/UpdateIO.hxx b/src/db/update/UpdateIO.hxx
index c33b79dc0..d5cbb2a5b 100644
--- a/src/db/update/UpdateIO.hxx
+++ b/src/db/update/UpdateIO.hxx
@@ -23,24 +23,32 @@
 #include "check.h"
 #include "Compiler.h"
 
-#include <sys/stat.h>
-
 struct Directory;
+struct FileInfo;
+class LocalStorage;
+class LocalDirectoryReader;
 
-int
-stat_directory(const Directory &directory, struct stat *st);
+/**
+ * Wrapper for LocalStorage::GetInfo() that logs errors instead of
+ * returning them.
+ */
+bool
+GetInfo(LocalStorage &storage, const char *uri_utf8, FileInfo &info);
 
-int
-stat_directory_child(const Directory &parent, const char *name,
-		     struct stat *st);
+/**
+ * Wrapper for LocalDirectoryReader::GetInfo() that logs errors
+ * instead of returning them.
+ */
+bool
+GetInfo(LocalDirectoryReader &reader, FileInfo &info);
 
 gcc_pure
 bool
-directory_exists(const Directory &directory);
+DirectoryExists(LocalStorage &storage, const Directory &directory);
 
 gcc_pure
 bool
-directory_child_is_regular(const Directory &directory,
+directory_child_is_regular(LocalStorage &storage, const Directory &directory,
 			   const char *name_utf8);
 
 /**
@@ -48,7 +56,7 @@ directory_child_is_regular(const Directory &directory,
  */
 gcc_pure
 bool
-directory_child_access(const Directory &directory,
+directory_child_access(LocalStorage &storage, const Directory &directory,
 		       const char *name, int mode);
 
 #endif
diff --git a/src/db/update/UpdateSong.cxx b/src/db/update/UpdateSong.cxx
index 751d8bfe9..2868249e7 100644
--- a/src/db/update/UpdateSong.cxx
+++ b/src/db/update/UpdateSong.cxx
@@ -25,6 +25,7 @@
 #include "db/Directory.hxx"
 #include "db/Song.hxx"
 #include "decoder/DecoderList.hxx"
+#include "storage/FileInfo.hxx"
 #include "Log.hxx"
 
 #include <unistd.h>
@@ -32,13 +33,13 @@
 inline void
 UpdateWalk::UpdateSongFile2(Directory &directory,
 			    const char *name, const char *suffix,
-			    const struct stat *st)
+			    const FileInfo &info)
 {
 	db_lock();
 	Song *song = directory.FindSong(name);
 	db_unlock();
 
-	if (!directory_child_access(directory, name, R_OK)) {
+	if (!directory_child_access(storage, directory, name, R_OK)) {
 		FormatError(update_domain,
 			    "no read permissions on %s/%s",
 			    directory.GetPath(), name);
@@ -48,9 +49,9 @@ UpdateWalk::UpdateSongFile2(Directory &directory,
 		return;
 	}
 
-	if (!(song != nullptr && st->st_mtime == song->mtime &&
+	if (!(song != nullptr && info.mtime == song->mtime &&
 	      !walk_discard) &&
-	    UpdateContainerFile(directory, name, suffix, st)) {
+	    UpdateContainerFile(directory, name, suffix, info)) {
 		if (song != nullptr)
 			editor.LockDeleteSong(directory, song);
 
@@ -75,7 +76,7 @@ UpdateWalk::UpdateSongFile2(Directory &directory,
 		modified = true;
 		FormatDefault(update_domain, "added %s/%s",
 			      directory.GetPath(), name);
-	} else if (st->st_mtime != song->mtime || walk_discard) {
+	} else if (info.mtime != song->mtime || walk_discard) {
 		FormatDefault(update_domain, "updating %s/%s",
 			      directory.GetPath(), name);
 		if (!song->UpdateFile()) {
@@ -92,11 +93,11 @@ UpdateWalk::UpdateSongFile2(Directory &directory,
 bool
 UpdateWalk::UpdateSongFile(Directory &directory,
 			   const char *name, const char *suffix,
-			   const struct stat *st)
+			   const FileInfo &info)
 {
 	if (!decoder_plugins_supports_suffix(suffix))
 		return false;
 
-	UpdateSongFile2(directory, name, suffix, st);
+	UpdateSongFile2(directory, name, suffix, info);
 	return true;
 }
diff --git a/src/db/update/Walk.cxx b/src/db/update/Walk.cxx
index c54d5a8d7..e6bfdcfd9 100644
--- a/src/db/update/Walk.cxx
+++ b/src/db/update/Walk.cxx
@@ -35,9 +35,10 @@
 #include "fs/AllocatedPath.hxx"
 #include "fs/Traits.hxx"
 #include "fs/FileSystem.hxx"
-#include "fs/DirectoryReader.hxx"
+#include "storage/FileInfo.hxx"
 #include "util/Alloc.hxx"
 #include "util/UriUtil.hxx"
+#include "util/Error.hxx"
 #include "Log.hxx"
 
 #include <assert.h>
@@ -47,7 +48,9 @@
 #include <errno.h>
 
 UpdateWalk::UpdateWalk(EventLoop &_loop, DatabaseListener &_listener)
-	:editor(_loop, _listener)
+	:storage(mapper_get_music_directory_utf8(),
+		 mapper_get_music_directory_fs()),
+	 editor(_loop, _listener)
 {
 #ifndef WIN32
 	follow_inside_symlinks =
@@ -61,10 +64,10 @@ UpdateWalk::UpdateWalk(EventLoop &_loop, DatabaseListener &_listener)
 }
 
 static void
-directory_set_stat(Directory &dir, const struct stat *st)
+directory_set_stat(Directory &dir, const FileInfo &info)
 {
-	dir.inode = st->st_ino;
-	dir.device = st->st_dev;
+	dir.inode = info.inode;
+	dir.device = info.device;
 	dir.have_stat = true;
 }
 
@@ -103,7 +106,7 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
 {
 	Directory *child, *n;
 	directory_for_each_child_safe(child, n, directory) {
-		if (directory_exists(*child))
+		if (DirectoryExists(storage, *child))
 			continue;
 
 		editor.LockDeleteDirectory(child);
@@ -113,8 +116,8 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
 
 	Song *song, *ns;
 	directory_for_each_song_safe(song, ns, directory) {
-		const auto path = map_song_fs(*song);
-		if (path.IsNull() || !FileExists(path)) {
+		if (!directory_child_is_regular(storage, directory,
+						song->uri)) {
 			editor.LockDeleteSong(directory, song);
 
 			modified = true;
@@ -124,7 +127,8 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
 	for (auto i = directory.playlists.begin(),
 		     end = directory.playlists.end();
 	     i != end;) {
-		if (!directory_child_is_regular(directory, i->name.c_str())) {
+		if (!directory_child_is_regular(storage, directory,
+						i->name.c_str())) {
 			db_lock();
 			i = directory.playlists.erase(i);
 			db_unlock();
@@ -134,24 +138,26 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
 }
 
 #ifndef WIN32
-static int
-update_directory_stat(Directory &directory)
+static bool
+update_directory_stat(LocalStorage &storage, Directory &directory)
 {
-	struct stat st;
-	if (stat_directory(directory, &st) < 0)
-		return -1;
+	FileInfo info;
+	if (!GetInfo(storage, directory.GetPath(), info))
+		return false;
 
-	directory_set_stat(directory, &st);
-	return 0;
+	directory_set_stat(directory, info);
+	return true;
 }
 #endif
 
 static int
-find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
+find_inode_ancestor(LocalStorage &storage, Directory *parent,
+		    unsigned inode, unsigned device)
 {
 #ifndef WIN32
 	while (parent) {
-		if (!parent->have_stat && update_directory_stat(*parent) < 0)
+		if (!parent->have_stat &&
+		    !update_directory_stat(storage, *parent))
 			return -1;
 
 		if (parent->inode == inode && parent->device == device) {
@@ -162,6 +168,7 @@ find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
 		parent = parent->parent;
 	}
 #else
+	(void)storage;
 	(void)parent;
 	(void)inode;
 	(void)device;
@@ -173,12 +180,12 @@ find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
 inline bool
 UpdateWalk::UpdatePlaylistFile(Directory &directory,
 			       const char *name, const char *suffix,
-			       const struct stat *st)
+			       const FileInfo &info)
 {
 	if (!playlist_suffix_supported(suffix))
 		return false;
 
-	PlaylistInfo pi(name, st->st_mtime);
+	PlaylistInfo pi(name, info.mtime);
 
 	db_lock();
 	if (directory.playlists.UpdateOrInsert(std::move(pi)))
@@ -189,27 +196,28 @@ UpdateWalk::UpdatePlaylistFile(Directory &directory,
 
 inline bool
 UpdateWalk::UpdateRegularFile(Directory &directory,
-			      const char *name, const struct stat *st)
+			      const char *name, const FileInfo &info)
 {
 	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);
+	return UpdateSongFile(directory, name, suffix, info) ||
+		UpdateArchiveFile(directory, name, suffix, info) ||
+		UpdatePlaylistFile(directory, name, suffix, info);
 }
 
 void
 UpdateWalk::UpdateDirectoryChild(Directory &directory,
-				 const char *name, const struct stat *st)
+				 const char *name, const FileInfo &info)
 {
 	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))
+	if (info.IsRegular()) {
+		UpdateRegularFile(directory, name, info);
+	} else if (info.IsDirectory()) {
+		if (find_inode_ancestor(storage, &directory,
+					info.inode, info.device))
 			return;
 
 		db_lock();
@@ -218,7 +226,7 @@ UpdateWalk::UpdateDirectoryChild(Directory &directory,
 
 		assert(&directory == subdir->parent);
 
-		if (!UpdateDirectory(*subdir, st))
+		if (!UpdateDirectory(*subdir, info))
 			editor.LockDeleteDirectory(subdir);
 	} else {
 		FormatDebug(update_domain,
@@ -228,12 +236,10 @@ UpdateWalk::UpdateDirectoryChild(Directory &directory,
 
 /* we don't look at "." / ".." nor files with newlines in their name */
 gcc_pure
-static bool skip_path(Path path_fs)
+static bool
+skip_path(const char *name_utf8)
 {
-	const char *path = path_fs.c_str();
-	return (path[0] == '.' && path[1] == 0) ||
-		(path[0] == '.' && path[1] == '.' && path[2] == 0) ||
-		strchr(path, '\n') != nullptr;
+	return strchr(name_utf8, '\n') != nullptr;
 }
 
 gcc_pure
@@ -242,9 +248,11 @@ UpdateWalk::SkipSymlink(const Directory *directory,
 			const char *utf8_name) const
 {
 #ifndef WIN32
-	const auto path_fs = map_directory_child_fs(*directory, utf8_name);
+	const auto path_fs = storage.MapChildFS(directory->GetPath(),
+						utf8_name);
 	if (path_fs.IsNull())
-		return true;
+		/* not a local file: don't skip */
+		return false;
 
 	const auto target = ReadLink(path_fs);
 	if (target.IsNull())
@@ -304,63 +312,68 @@ UpdateWalk::SkipSymlink(const Directory *directory,
 }
 
 bool
-UpdateWalk::UpdateDirectory(Directory &directory, const struct stat *st)
+UpdateWalk::UpdateDirectory(Directory &directory, const FileInfo &info)
 {
-	assert(S_ISDIR(st->st_mode));
+	assert(info.IsDirectory());
 
-	directory_set_stat(directory, st);
+	directory_set_stat(directory, info);
 
-	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());
+	Error error;
+	LocalDirectoryReader *const reader =
+		storage.OpenDirectory(directory.GetPath(), error);
+	if (reader == nullptr) {
+		LogError(error);
 		return false;
 	}
 
 	ExcludeList exclude_list;
-	exclude_list.LoadFile(AllocatedPath::Build(path_fs, ".mpdignore"));
+
+	{
+		const auto exclude_path_fs =
+			storage.MapChildFS(directory.GetPath(), ".mpdignore");
+		if (!exclude_path_fs.IsNull())
+			exclude_list.LoadFile(exclude_path_fs);
+	}
 
 	if (!exclude_list.IsEmpty())
 		RemoveExcludedFromDirectory(directory, exclude_list);
 
 	PurgeDeletedFromDirectory(directory);
 
-	while (reader.ReadEntry()) {
-		const auto entry = reader.GetEntry();
-
-		if (skip_path(entry) || exclude_list.Check(entry))
+	const char *name_utf8;
+	while ((name_utf8 = reader->Read()) != nullptr) {
+		if (skip_path(name_utf8))
 			continue;
 
-		const std::string utf8 = entry.ToUTF8();
-		if (utf8.empty())
-			continue;
+		{
+			const auto name_fs = AllocatedPath::FromUTF8(name_utf8);
+			if (name_fs.IsNull() || exclude_list.Check(name_fs))
+				continue;
+		}
 
-		if (SkipSymlink(&directory, utf8.c_str())) {
-			modified |= editor.DeleteNameIn(directory, utf8.c_str());
+		if (SkipSymlink(&directory, name_utf8)) {
+			modified |= editor.DeleteNameIn(directory, name_utf8);
 			continue;
 		}
 
-		struct stat st2;
-		if (stat_directory_child(directory, utf8.c_str(), &st2) == 0)
-			UpdateDirectoryChild(directory, utf8.c_str(), &st2);
-		else
-			modified |= editor.DeleteNameIn(directory, utf8.c_str());
+		FileInfo info2;
+		if (!GetInfo(*reader, info2)) {
+			modified |= editor.DeleteNameIn(directory, name_utf8);
+			continue;
+		}
+
+		UpdateDirectoryChild(directory, name_utf8, info2);
 	}
 
-	directory.mtime = st->st_mtime;
+	directory.mtime = info.mtime;
 
 	return true;
 }
 
 inline Directory *
-UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8)
+UpdateWalk::DirectoryMakeChildChecked(Directory &parent,
+				      const char *uri_utf8,
+				      const char *name_utf8)
 {
 	db_lock();
 	Directory *directory = parent.FindChild(name_utf8);
@@ -369,9 +382,9 @@ UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8)
 	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))
+	FileInfo info;
+	if (!GetInfo(storage, uri_utf8, info) ||
+	    find_inode_ancestor(storage, &parent, info.inode, info.device))
 		return nullptr;
 
 	if (SkipSymlink(&parent, name_utf8))
@@ -387,7 +400,7 @@ UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8)
 	directory = parent.CreateChild(name_utf8);
 	db_unlock();
 
-	directory_set_stat(*directory, &st);
+	directory_set_stat(*directory, info);
 	return directory;
 }
 
@@ -405,6 +418,7 @@ UpdateWalk::DirectoryMakeUriParentChecked(Directory &root, const char *uri)
 			continue;
 
 		directory = DirectoryMakeChildChecked(*directory,
+						      duplicated,
 						      name_utf8);
 		if (directory == nullptr)
 			break;
@@ -425,12 +439,18 @@ UpdateWalk::UpdateUri(Directory &root, const char *uri)
 
 	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
+	if (SkipSymlink(parent, name)) {
 		modified |= editor.DeleteNameIn(*parent, name);
+		return;
+	}
+
+	FileInfo info;
+	if (!GetInfo(storage, uri, info)) {
+		modified |= editor.DeleteNameIn(*parent, name);
+		return;
+	}
+
+	UpdateDirectoryChild(*parent, name, info);
 }
 
 bool
@@ -442,10 +462,11 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard)
 	if (path != nullptr && !isRootDirectory(path)) {
 		UpdateUri(root, path);
 	} else {
-		struct stat st;
+		FileInfo info;
+		if (!GetInfo(storage, "", info))
+			return false;
 
-		if (stat_directory(root, &st) == 0)
-			UpdateDirectory(root, &st);
+		UpdateDirectory(root, info);
 	}
 
 	return modified;
diff --git a/src/db/update/Walk.hxx b/src/db/update/Walk.hxx
index aa4516917..c465ea7e1 100644
--- a/src/db/update/Walk.hxx
+++ b/src/db/update/Walk.hxx
@@ -22,10 +22,12 @@
 
 #include "check.h"
 #include "Editor.hxx"
+#include "storage/LocalStorage.hxx"
 
 #include <sys/stat.h>
 
 struct stat;
+struct FileInfo;
 struct Directory;
 struct archive_plugin;
 class ExcludeList;
@@ -46,6 +48,8 @@ class UpdateWalk final {
 	bool walk_discard;
 	bool modified;
 
+	LocalStorage storage;
+
 	DatabaseEditor editor;
 
 public:
@@ -68,15 +72,15 @@ private:
 
 	void UpdateSongFile2(Directory &directory,
 			     const char *name, const char *suffix,
-			     const struct stat *st);
+			     const FileInfo &info);
 
 	bool UpdateSongFile(Directory &directory,
 			    const char *name, const char *suffix,
-			    const struct stat *st);
+			    const FileInfo &info);
 
 	bool UpdateContainerFile(Directory &directory,
 				 const char *name, const char *suffix,
-				 const struct stat *st);
+				 const FileInfo &info);
 
 
 #ifdef ENABLE_ARCHIVE
@@ -84,10 +88,10 @@ private:
 
 	bool UpdateArchiveFile(Directory &directory,
 			       const char *name, const char *suffix,
-			       const struct stat *st);
+			       const FileInfo &info);
 
 	void UpdateArchiveFile(Directory &directory, const char *name,
-			       const struct stat *st,
+			       const FileInfo &info,
 			       const archive_plugin &plugin);
 
 
@@ -95,22 +99,22 @@ private:
 	bool UpdateArchiveFile(gcc_unused Directory &directory,
 			       gcc_unused const char *name,
 			       gcc_unused const char *suffix,
-			       gcc_unused const struct stat *st) {
+			       gcc_unused const FileInfo &info) {
 		return false;
 	}
 #endif
 
 	bool UpdatePlaylistFile(Directory &directory,
 				const char *name, const char *suffix,
-				const struct stat *st);
+				const FileInfo &info);
 
 	bool UpdateRegularFile(Directory &directory,
-			       const char *name, const struct stat *st);
+			       const char *name, const FileInfo &info);
 
 	void UpdateDirectoryChild(Directory &directory,
-				  const char *name, const struct stat *st);
+				  const char *name, const FileInfo &info);
 
-	bool UpdateDirectory(Directory &directory, const struct stat *st);
+	bool UpdateDirectory(Directory &directory, const FileInfo &info);
 
 	/**
 	 * Create the specified directory object if it does not exist
@@ -121,9 +125,10 @@ private:
 	 * The caller must lock the database.
 	 */
 	Directory *MakeDirectoryIfModified(Directory &parent, const char *name,
-					   const struct stat *st);
+					   const FileInfo &info);
 
 	Directory *DirectoryMakeChildChecked(Directory &parent,
+					     const char *uri_utf8,
 					     const char *name_utf8);
 
 	Directory *DirectoryMakeUriParentChecked(Directory &root,
diff --git a/src/storage/FileInfo.hxx b/src/storage/FileInfo.hxx
new file mode 100644
index 000000000..8dd152c0a
--- /dev/null
+++ b/src/storage/FileInfo.hxx
@@ -0,0 +1,62 @@
+/*
+ * 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_STORAGE_FILE_INFO_HXX
+#define MPD_STORAGE_FILE_INFO_HXX
+
+#include "check.h"
+
+#include <time.h>
+#include <stdint.h>
+
+struct FileInfo {
+	enum class Type : uint8_t {
+		OTHER,
+		REGULAR,
+		DIRECTORY,
+	};
+
+	Type type;
+
+	/**
+	 * The file size in bytes.  Only valid for #Type::REGULAR.
+	 */
+	uint64_t size;
+
+	/**
+	 * The modification time.  0 means unknown / not applicable.
+	 */
+	time_t mtime;
+
+	/**
+	 * Device id and inode number.  0 means unknown / not
+	 * applicable.
+	 */
+	unsigned device, inode;
+
+	bool IsRegular() const {
+		return type == Type::REGULAR;
+	}
+
+	bool IsDirectory() const {
+		return type == Type::DIRECTORY;
+	}
+};
+
+#endif
diff --git a/src/storage/LocalStorage.cxx b/src/storage/LocalStorage.cxx
new file mode 100644
index 000000000..a229b3fe7
--- /dev/null
+++ b/src/storage/LocalStorage.cxx
@@ -0,0 +1,154 @@
+/*
+ * 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 "LocalStorage.hxx"
+#include "FileInfo.hxx"
+#include "util/Error.hxx"
+#include "fs/FileSystem.hxx"
+
+static bool
+Stat(Path path, bool follow, FileInfo &info, Error &error)
+{
+	struct stat st;
+	if (!StatFile(path, st, follow)) {
+		error.SetErrno();
+
+		const auto path_utf8 = path.ToUTF8();
+		error.FormatPrefix("Failed to stat %s: ", path_utf8.c_str());
+		return false;
+	}
+
+	if (S_ISREG(st.st_mode))
+		info.type = FileInfo::Type::REGULAR;
+	else if (S_ISDIR(st.st_mode))
+		info.type = FileInfo::Type::DIRECTORY;
+	else
+		info.type = FileInfo::Type::OTHER;
+
+	info.size = st.st_size;
+	info.mtime = st.st_mtime;
+	info.device = st.st_dev;
+	info.inode = st.st_ino;
+	return true;
+}
+
+std::string
+LocalStorage::MapUTF8(const char *uri_utf8) const
+{
+	assert(uri_utf8 != nullptr);
+
+	if (*uri_utf8 == 0)
+		return base_utf8;
+
+	return PathTraitsUTF8::Build(base_utf8.c_str(), uri_utf8);
+}
+
+AllocatedPath
+LocalStorage::MapFS(const char *uri_utf8, Error &error) const
+{
+	assert(uri_utf8 != nullptr);
+
+	if (*uri_utf8 == 0)
+		return base_fs;
+
+	AllocatedPath path_fs = AllocatedPath::FromUTF8(uri_utf8, error);
+	if (!path_fs.IsNull())
+		path_fs = AllocatedPath::Build(base_fs, path_fs);
+
+	return path_fs;
+}
+
+AllocatedPath
+LocalStorage::MapFS(const char *uri_utf8) const
+{
+	return MapFS(uri_utf8, IgnoreError());
+}
+
+AllocatedPath
+LocalStorage::MapChildFS(const char *uri_utf8,
+			 const char *child_utf8) const
+{
+	const auto uri2 = PathTraitsUTF8::Build(uri_utf8, child_utf8);
+	return MapFS(uri2.c_str());
+}
+
+bool
+LocalStorage::GetInfo(const char *uri_utf8, bool follow, FileInfo &info,
+		      Error &error)
+{
+	AllocatedPath path_fs = MapFS(uri_utf8, error);
+	if (path_fs.IsNull())
+		return false;
+
+	return Stat(path_fs, follow, info, error);
+}
+
+LocalDirectoryReader *
+LocalStorage::OpenDirectory(const char *uri_utf8, Error &error)
+{
+	AllocatedPath path_fs = MapFS(uri_utf8, error);
+	if (path_fs.IsNull())
+		return nullptr;
+
+	LocalDirectoryReader *reader =
+		new LocalDirectoryReader(std::move(path_fs));
+	if (reader->HasFailed()) {
+		error.FormatErrno("Failed to open '%s'", uri_utf8);
+		delete reader;
+		return nullptr;
+	}
+
+	return reader;
+}
+
+gcc_pure
+static bool
+SkipNameFS(const char *name_fs)
+{
+	return name_fs[0] == '.' &&
+		(name_fs[1] == 0 ||
+		 (name_fs[1] == '.' && name_fs[2] == 0));
+}
+
+const char *
+LocalDirectoryReader::Read()
+{
+	while (reader.ReadEntry()) {
+		const Path name_fs = reader.GetEntry();
+		if (SkipNameFS(name_fs.c_str()))
+			continue;
+
+		name_utf8 = name_fs.ToUTF8();
+		if (name_utf8.empty())
+			continue;
+
+		return name_utf8.c_str();
+	}
+
+	return nullptr;
+}
+
+bool
+LocalDirectoryReader::GetInfo(bool follow, FileInfo &info, Error &error)
+{
+	const AllocatedPath path_fs =
+		AllocatedPath::Build(base_fs, reader.GetEntry());
+	return Stat(path_fs, follow, info, error);
+}
diff --git a/src/storage/LocalStorage.hxx b/src/storage/LocalStorage.hxx
new file mode 100644
index 000000000..73c11dd80
--- /dev/null
+++ b/src/storage/LocalStorage.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_STORAGE_LOCAL_HXX
+#define MPD_STORAGE_LOCAL_HXX
+
+#include "check.h"
+#include "fs/AllocatedPath.hxx"
+#include "fs/DirectoryReader.hxx"
+
+#include <string>
+
+struct FileInfo;
+
+class LocalDirectoryReader {
+	AllocatedPath base_fs;
+
+	DirectoryReader reader;
+
+	std::string name_utf8;
+
+public:
+	LocalDirectoryReader(AllocatedPath &&_base_fs)
+		:base_fs(std::move(_base_fs)), reader(base_fs) {}
+
+	bool HasFailed() {
+		return reader.HasFailed();
+	}
+
+	const char *Read();
+
+	bool GetInfo(bool follow, FileInfo &info, Error &error);
+};
+
+class LocalStorage {
+	const std::string base_utf8;
+	const AllocatedPath base_fs;
+
+public:
+	LocalStorage(const char *_base_utf8, Path _base_fs)
+		:base_utf8(_base_utf8), base_fs(_base_fs) {}
+
+	LocalStorage(const LocalStorage &) = delete;
+
+	bool GetInfo(const char *uri_utf8, bool follow, FileInfo &info,
+		     Error &error);
+
+	LocalDirectoryReader *OpenDirectory(const char *uri_utf8,
+					    Error &error);
+
+	/**
+	 * Map the given relative URI to an absolute URI.
+	 */
+	gcc_pure
+	std::string MapUTF8(const char *uri_utf8) const;
+
+	/**
+	 * Map the given relative URI to a local file path.  Returns
+	 * AllocatedPath::Null() on error or if this storage does not
+	 * support local files.
+	 */
+	gcc_pure
+	AllocatedPath MapFS(const char *uri_utf8) const;
+
+	gcc_pure
+	AllocatedPath MapChildFS(const char *uri_utf8,
+				 const char *child_utf8) const;
+
+private:
+	AllocatedPath MapFS(const char *uri_utf8, Error &error) const;
+};
+
+#endif