Commit 9ae7f186 authored by Max Kellermann's avatar Max Kellermann

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.
parent f8d114be
...@@ -31,6 +31,7 @@ src_mpd_CPPFLAGS = $(AM_CPPFLAGS) \ ...@@ -31,6 +31,7 @@ src_mpd_CPPFLAGS = $(AM_CPPFLAGS) \
src_mpd_LDADD = \ src_mpd_LDADD = \
$(NEIGHBOR_LIBS) \ $(NEIGHBOR_LIBS) \
$(DB_LIBS) \ $(DB_LIBS) \
$(STORAGE_LIBS) \
$(PLAYLIST_LIBS) \ $(PLAYLIST_LIBS) \
$(AVAHI_LIBS) \ $(AVAHI_LIBS) \
$(LIBWRAP_LDFLAGS) \ $(LIBWRAP_LDFLAGS) \
...@@ -410,6 +411,20 @@ libfs_a_SOURCES = \ ...@@ -410,6 +411,20 @@ libfs_a_SOURCES = \
src/fs/StandardDirectory.cxx src/fs/StandardDirectory.hxx \ src/fs/StandardDirectory.cxx src/fs/StandardDirectory.hxx \
src/fs/DirectoryReader.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 # neighbor plugins
if ENABLE_NEIGHBOR_PLUGINS if ENABLE_NEIGHBOR_PLUGINS
......
...@@ -193,7 +193,8 @@ map_uri_fs(const char *uri) ...@@ -193,7 +193,8 @@ map_uri_fs(const char *uri)
return AllocatedPath::Build(music_dir_fs, uri_fs); return AllocatedPath::Build(music_dir_fs, uri_fs);
} }
AllocatedPath gcc_pure
static AllocatedPath
map_directory_fs(const Directory &directory) map_directory_fs(const Directory &directory)
{ {
assert(!music_dir_fs.IsNull()); assert(!music_dir_fs.IsNull());
...@@ -204,7 +205,15 @@ map_directory_fs(const Directory &directory) ...@@ -204,7 +205,15 @@ map_directory_fs(const Directory &directory)
return map_uri_fs(directory.GetPath()); 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) map_directory_child_fs(const Directory &directory, const char *name)
{ {
assert(!music_dir_fs.IsNull()); assert(!music_dir_fs.IsNull());
......
...@@ -92,28 +92,6 @@ AllocatedPath ...@@ -92,28 +92,6 @@ AllocatedPath
map_uri_fs(const char *uri); 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 * "Detach" the #Song object, i.e. convert it to a #DetachedSong
* instance. * instance.
*/ */
......
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
#include "db/DatabaseLock.hxx" #include "db/DatabaseLock.hxx"
#include "db/Directory.hxx" #include "db/Directory.hxx"
#include "db/Song.hxx" #include "db/Song.hxx"
#include "Mapper.hxx"
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "storage/FileInfo.hxx"
#include "archive/ArchiveList.hxx" #include "archive/ArchiveList.hxx"
#include "archive/ArchivePlugin.hxx" #include "archive/ArchivePlugin.hxx"
#include "archive/ArchiveFile.hxx" #include "archive/ArchiveFile.hxx"
...@@ -103,20 +103,24 @@ class UpdateArchiveVisitor final : public ArchiveVisitor { ...@@ -103,20 +103,24 @@ class UpdateArchiveVisitor final : public ArchiveVisitor {
*/ */
void void
UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name, UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
const struct stat *st, const FileInfo &info,
const archive_plugin &plugin) const archive_plugin &plugin)
{ {
db_lock(); db_lock();
Directory *directory = parent.FindChild(name); Directory *directory = parent.FindChild(name);
db_unlock(); db_unlock();
if (directory != nullptr && directory->mtime == st->st_mtime && if (directory != nullptr && directory->mtime == info.mtime &&
!walk_discard) !walk_discard)
/* MPD has already scanned the archive, and it hasn't /* MPD has already scanned the archive, and it hasn't
changed since - don't consider updating it */ changed since - don't consider updating it */
return; 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 */ /* open archive */
Error error; Error error;
...@@ -141,7 +145,7 @@ UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name, ...@@ -141,7 +145,7 @@ UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
db_unlock(); db_unlock();
} }
directory->mtime = st->st_mtime; directory->mtime = info.mtime;
UpdateArchiveVisitor visitor(*this, directory); UpdateArchiveVisitor visitor(*this, directory);
file->Visit(visitor); file->Visit(visitor);
...@@ -151,13 +155,13 @@ UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name, ...@@ -151,13 +155,13 @@ UpdateWalk::UpdateArchiveFile(Directory &parent, const char *name,
bool bool
UpdateWalk::UpdateArchiveFile(Directory &directory, UpdateWalk::UpdateArchiveFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st) const FileInfo &info)
{ {
const struct archive_plugin *plugin = const struct archive_plugin *plugin =
archive_plugin_from_suffix(suffix); archive_plugin_from_suffix(suffix);
if (plugin == nullptr) if (plugin == nullptr)
return false; return false;
UpdateArchiveFile(directory, name, st, *plugin); UpdateArchiveFile(directory, name, info, *plugin);
return true; return true;
} }
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
#include "db/Song.hxx" #include "db/Song.hxx"
#include "decoder/DecoderPlugin.hxx" #include "decoder/DecoderPlugin.hxx"
#include "decoder/DecoderList.hxx" #include "decoder/DecoderList.hxx"
#include "Mapper.hxx"
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "storage/FileInfo.hxx"
#include "tag/TagHandler.hxx" #include "tag/TagHandler.hxx"
#include "tag/TagBuilder.hxx" #include "tag/TagBuilder.hxx"
#include "Log.hxx" #include "Log.hxx"
...@@ -37,13 +37,13 @@ ...@@ -37,13 +37,13 @@
Directory * Directory *
UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name, UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name,
const struct stat *st) const FileInfo &info)
{ {
Directory *directory = parent.FindChild(name); Directory *directory = parent.FindChild(name);
// directory exists already // directory exists already
if (directory != nullptr) { if (directory != nullptr) {
if (directory->mtime == st->st_mtime && !walk_discard) { if (directory->mtime == info.mtime && !walk_discard) {
/* not modified */ /* not modified */
return nullptr; return nullptr;
} }
...@@ -53,7 +53,7 @@ UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name, ...@@ -53,7 +53,7 @@ UpdateWalk::MakeDirectoryIfModified(Directory &parent, const char *name,
} }
directory = parent.MakeChild(name); directory = parent.MakeChild(name);
directory->mtime = st->st_mtime; directory->mtime = info.mtime;
return directory; return directory;
} }
...@@ -67,7 +67,7 @@ SupportsContainerSuffix(const DecoderPlugin &plugin, const char *suffix) ...@@ -67,7 +67,7 @@ SupportsContainerSuffix(const DecoderPlugin &plugin, const char *suffix)
bool bool
UpdateWalk::UpdateContainerFile(Directory &directory, UpdateWalk::UpdateContainerFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st) const FileInfo &info)
{ {
const DecoderPlugin *_plugin = decoder_plugins_find([suffix](const DecoderPlugin &plugin){ const DecoderPlugin *_plugin = decoder_plugins_find([suffix](const DecoderPlugin &plugin){
return SupportsContainerSuffix(plugin, suffix); return SupportsContainerSuffix(plugin, suffix);
...@@ -77,7 +77,7 @@ UpdateWalk::UpdateContainerFile(Directory &directory, ...@@ -77,7 +77,7 @@ UpdateWalk::UpdateContainerFile(Directory &directory,
const DecoderPlugin &plugin = *_plugin; const DecoderPlugin &plugin = *_plugin;
db_lock(); db_lock();
Directory *contdir = MakeDirectoryIfModified(directory, name, st); Directory *contdir = MakeDirectoryIfModified(directory, name, info);
if (contdir == nullptr) { if (contdir == nullptr) {
/* not modified */ /* not modified */
db_unlock(); db_unlock();
...@@ -87,7 +87,13 @@ UpdateWalk::UpdateContainerFile(Directory &directory, ...@@ -87,7 +87,13 @@ UpdateWalk::UpdateContainerFile(Directory &directory,
contdir->device = DEVICE_CONTAINER; contdir->device = DEVICE_CONTAINER;
db_unlock(); 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; char *vtrack;
unsigned int tnum = 0; unsigned int tnum = 0;
...@@ -96,11 +102,10 @@ UpdateWalk::UpdateContainerFile(Directory &directory, ...@@ -96,11 +102,10 @@ UpdateWalk::UpdateContainerFile(Directory &directory,
Song *song = Song::NewFile(vtrack, *contdir); Song *song = Song::NewFile(vtrack, *contdir);
// shouldn't be necessary but it's there.. // shouldn't be necessary but it's there..
song->mtime = st->st_mtime; song->mtime = info.mtime;
const auto child_path_fs =
map_directory_child_fs(*contdir, vtrack);
const auto child_path_fs = AllocatedPath::Build(pathname,
vtrack);
plugin.ScanFile(child_path_fs.c_str(), plugin.ScanFile(child_path_fs.c_str(),
add_tag_handler, &tag_builder); add_tag_handler, &tag_builder);
......
...@@ -21,91 +21,84 @@ ...@@ -21,91 +21,84 @@
#include "UpdateIO.hxx" #include "UpdateIO.hxx"
#include "UpdateDomain.hxx" #include "UpdateDomain.hxx"
#include "db/Directory.hxx" #include "db/Directory.hxx"
#include "Mapper.hxx" #include "storage/FileInfo.hxx"
#include "fs/AllocatedPath.hxx" #include "storage/LocalStorage.hxx"
#include "fs/Traits.hxx"
#include "fs/FileSystem.hxx" #include "fs/FileSystem.hxx"
#include "util/Error.hxx"
#include "Log.hxx" #include "Log.hxx"
#include <errno.h> #include <errno.h>
#include <unistd.h> #include <unistd.h>
int bool
stat_directory(const Directory &directory, struct stat *st) GetInfo(LocalStorage &storage, const char *uri_utf8, FileInfo &info)
{ {
const auto path_fs = map_directory_fs(directory); Error error;
if (path_fs.IsNull()) bool success = storage.GetInfo(uri_utf8, true, info, error);
return -1; if (!success)
LogError(error);
if (!StatFile(path_fs, *st)) { return success;
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 bool
stat_directory_child(const Directory &parent, const char *name, GetInfo(LocalDirectoryReader &reader, FileInfo &info)
struct stat *st)
{ {
const auto path_fs = map_directory_child_fs(parent, name); Error error;
if (path_fs.IsNull()) bool success = reader.GetInfo(true, info, error);
return -1; if (!success)
LogError(error);
if (!StatFile(path_fs, *st)) { return success;
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;
} }
bool bool
directory_exists(const Directory &directory) DirectoryExists(LocalStorage &storage, const Directory &directory)
{ {
const auto path_fs = map_directory_fs(directory); FileInfo info;
if (path_fs.IsNull()) if (!storage.GetInfo(directory.GetPath(), true, info, IgnoreError()))
/* invalid path: cannot exist */
return false; return false;
return directory.device == DEVICE_INARCHIVE || return directory.device == DEVICE_INARCHIVE ||
directory.device == DEVICE_CONTAINER directory.device == DEVICE_CONTAINER
? FileExists(path_fs) ? info.IsRegular()
: DirectoryExists(path_fs); : 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 bool
directory_child_is_regular(const Directory &directory, directory_child_is_regular(LocalStorage &storage, const Directory &directory,
const char *name_utf8) const char *name_utf8)
{ {
const auto path_fs = map_directory_child_fs(directory, name_utf8); FileInfo info;
if (path_fs.IsNull()) return GetDirectoryChildInfo(storage, directory, name_utf8, info,
return false; IgnoreError()) &&
info.IsRegular();
return FileExists(path_fs);
} }
bool bool
directory_child_access(const Directory &directory, directory_child_access(LocalStorage &storage, const Directory &directory,
const char *name, int mode) const char *name, int mode)
{ {
#ifdef WIN32 #ifdef WIN32
/* CheckAccess() is useless on WIN32 */ /* CheckAccess() is useless on WIN32 */
(void)storage;
(void)directory; (void)directory;
(void)name; (void)name;
(void)mode; (void)mode;
return true; return true;
#else #else
const auto path = map_directory_child_fs(directory, name); const auto path = storage.MapChildFS(directory.GetPath(), name);
if (path.IsNull()) if (path.IsNull())
/* something went wrong, but that isn't a permission /* does not point to local file: silently ignore the
problem */ check */
return true; return true;
return CheckAccess(path, mode) || errno != EACCES; return CheckAccess(path, mode) || errno != EACCES;
......
...@@ -23,24 +23,32 @@ ...@@ -23,24 +23,32 @@
#include "check.h" #include "check.h"
#include "Compiler.h" #include "Compiler.h"
#include <sys/stat.h>
struct Directory; 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, * Wrapper for LocalDirectoryReader::GetInfo() that logs errors
struct stat *st); * instead of returning them.
*/
bool
GetInfo(LocalDirectoryReader &reader, FileInfo &info);
gcc_pure gcc_pure
bool bool
directory_exists(const Directory &directory); DirectoryExists(LocalStorage &storage, const Directory &directory);
gcc_pure gcc_pure
bool bool
directory_child_is_regular(const Directory &directory, directory_child_is_regular(LocalStorage &storage, const Directory &directory,
const char *name_utf8); const char *name_utf8);
/** /**
...@@ -48,7 +56,7 @@ directory_child_is_regular(const Directory &directory, ...@@ -48,7 +56,7 @@ directory_child_is_regular(const Directory &directory,
*/ */
gcc_pure gcc_pure
bool bool
directory_child_access(const Directory &directory, directory_child_access(LocalStorage &storage, const Directory &directory,
const char *name, int mode); const char *name, int mode);
#endif #endif
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
#include "db/Directory.hxx" #include "db/Directory.hxx"
#include "db/Song.hxx" #include "db/Song.hxx"
#include "decoder/DecoderList.hxx" #include "decoder/DecoderList.hxx"
#include "storage/FileInfo.hxx"
#include "Log.hxx" #include "Log.hxx"
#include <unistd.h> #include <unistd.h>
...@@ -32,13 +33,13 @@ ...@@ -32,13 +33,13 @@
inline void inline void
UpdateWalk::UpdateSongFile2(Directory &directory, UpdateWalk::UpdateSongFile2(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st) const FileInfo &info)
{ {
db_lock(); db_lock();
Song *song = directory.FindSong(name); Song *song = directory.FindSong(name);
db_unlock(); db_unlock();
if (!directory_child_access(directory, name, R_OK)) { if (!directory_child_access(storage, directory, name, R_OK)) {
FormatError(update_domain, FormatError(update_domain,
"no read permissions on %s/%s", "no read permissions on %s/%s",
directory.GetPath(), name); directory.GetPath(), name);
...@@ -48,9 +49,9 @@ UpdateWalk::UpdateSongFile2(Directory &directory, ...@@ -48,9 +49,9 @@ UpdateWalk::UpdateSongFile2(Directory &directory,
return; return;
} }
if (!(song != nullptr && st->st_mtime == song->mtime && if (!(song != nullptr && info.mtime == song->mtime &&
!walk_discard) && !walk_discard) &&
UpdateContainerFile(directory, name, suffix, st)) { UpdateContainerFile(directory, name, suffix, info)) {
if (song != nullptr) if (song != nullptr)
editor.LockDeleteSong(directory, song); editor.LockDeleteSong(directory, song);
...@@ -75,7 +76,7 @@ UpdateWalk::UpdateSongFile2(Directory &directory, ...@@ -75,7 +76,7 @@ UpdateWalk::UpdateSongFile2(Directory &directory,
modified = true; modified = true;
FormatDefault(update_domain, "added %s/%s", FormatDefault(update_domain, "added %s/%s",
directory.GetPath(), name); 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", FormatDefault(update_domain, "updating %s/%s",
directory.GetPath(), name); directory.GetPath(), name);
if (!song->UpdateFile()) { if (!song->UpdateFile()) {
...@@ -92,11 +93,11 @@ UpdateWalk::UpdateSongFile2(Directory &directory, ...@@ -92,11 +93,11 @@ UpdateWalk::UpdateSongFile2(Directory &directory,
bool bool
UpdateWalk::UpdateSongFile(Directory &directory, UpdateWalk::UpdateSongFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st) const FileInfo &info)
{ {
if (!decoder_plugins_supports_suffix(suffix)) if (!decoder_plugins_supports_suffix(suffix))
return false; return false;
UpdateSongFile2(directory, name, suffix, st); UpdateSongFile2(directory, name, suffix, info);
return true; return true;
} }
...@@ -35,9 +35,10 @@ ...@@ -35,9 +35,10 @@
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "fs/Traits.hxx" #include "fs/Traits.hxx"
#include "fs/FileSystem.hxx" #include "fs/FileSystem.hxx"
#include "fs/DirectoryReader.hxx" #include "storage/FileInfo.hxx"
#include "util/Alloc.hxx" #include "util/Alloc.hxx"
#include "util/UriUtil.hxx" #include "util/UriUtil.hxx"
#include "util/Error.hxx"
#include "Log.hxx" #include "Log.hxx"
#include <assert.h> #include <assert.h>
...@@ -47,7 +48,9 @@ ...@@ -47,7 +48,9 @@
#include <errno.h> #include <errno.h>
UpdateWalk::UpdateWalk(EventLoop &_loop, DatabaseListener &_listener) 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 #ifndef WIN32
follow_inside_symlinks = follow_inside_symlinks =
...@@ -61,10 +64,10 @@ UpdateWalk::UpdateWalk(EventLoop &_loop, DatabaseListener &_listener) ...@@ -61,10 +64,10 @@ UpdateWalk::UpdateWalk(EventLoop &_loop, DatabaseListener &_listener)
} }
static void 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.inode = info.inode;
dir.device = st->st_dev; dir.device = info.device;
dir.have_stat = true; dir.have_stat = true;
} }
...@@ -103,7 +106,7 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) ...@@ -103,7 +106,7 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
{ {
Directory *child, *n; Directory *child, *n;
directory_for_each_child_safe(child, n, directory) { directory_for_each_child_safe(child, n, directory) {
if (directory_exists(*child)) if (DirectoryExists(storage, *child))
continue; continue;
editor.LockDeleteDirectory(child); editor.LockDeleteDirectory(child);
...@@ -113,8 +116,8 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) ...@@ -113,8 +116,8 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
Song *song, *ns; Song *song, *ns;
directory_for_each_song_safe(song, ns, directory) { directory_for_each_song_safe(song, ns, directory) {
const auto path = map_song_fs(*song); if (!directory_child_is_regular(storage, directory,
if (path.IsNull() || !FileExists(path)) { song->uri)) {
editor.LockDeleteSong(directory, song); editor.LockDeleteSong(directory, song);
modified = true; modified = true;
...@@ -124,7 +127,8 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) ...@@ -124,7 +127,8 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
for (auto i = directory.playlists.begin(), for (auto i = directory.playlists.begin(),
end = directory.playlists.end(); end = directory.playlists.end();
i != 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(); db_lock();
i = directory.playlists.erase(i); i = directory.playlists.erase(i);
db_unlock(); db_unlock();
...@@ -134,24 +138,26 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) ...@@ -134,24 +138,26 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory)
} }
#ifndef WIN32 #ifndef WIN32
static int static bool
update_directory_stat(Directory &directory) update_directory_stat(LocalStorage &storage, Directory &directory)
{ {
struct stat st; FileInfo info;
if (stat_directory(directory, &st) < 0) if (!GetInfo(storage, directory.GetPath(), info))
return -1; return false;
directory_set_stat(directory, &st); directory_set_stat(directory, info);
return 0; return true;
} }
#endif #endif
static int 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 #ifndef WIN32
while (parent) { while (parent) {
if (!parent->have_stat && update_directory_stat(*parent) < 0) if (!parent->have_stat &&
!update_directory_stat(storage, *parent))
return -1; return -1;
if (parent->inode == inode && parent->device == device) { if (parent->inode == inode && parent->device == device) {
...@@ -162,6 +168,7 @@ find_inode_ancestor(Directory *parent, ino_t inode, dev_t device) ...@@ -162,6 +168,7 @@ find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
parent = parent->parent; parent = parent->parent;
} }
#else #else
(void)storage;
(void)parent; (void)parent;
(void)inode; (void)inode;
(void)device; (void)device;
...@@ -173,12 +180,12 @@ find_inode_ancestor(Directory *parent, ino_t inode, dev_t device) ...@@ -173,12 +180,12 @@ find_inode_ancestor(Directory *parent, ino_t inode, dev_t device)
inline bool inline bool
UpdateWalk::UpdatePlaylistFile(Directory &directory, UpdateWalk::UpdatePlaylistFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st) const FileInfo &info)
{ {
if (!playlist_suffix_supported(suffix)) if (!playlist_suffix_supported(suffix))
return false; return false;
PlaylistInfo pi(name, st->st_mtime); PlaylistInfo pi(name, info.mtime);
db_lock(); db_lock();
if (directory.playlists.UpdateOrInsert(std::move(pi))) if (directory.playlists.UpdateOrInsert(std::move(pi)))
...@@ -189,27 +196,28 @@ UpdateWalk::UpdatePlaylistFile(Directory &directory, ...@@ -189,27 +196,28 @@ UpdateWalk::UpdatePlaylistFile(Directory &directory,
inline bool inline bool
UpdateWalk::UpdateRegularFile(Directory &directory, UpdateWalk::UpdateRegularFile(Directory &directory,
const char *name, const struct stat *st) const char *name, const FileInfo &info)
{ {
const char *suffix = uri_get_suffix(name); const char *suffix = uri_get_suffix(name);
if (suffix == nullptr) if (suffix == nullptr)
return false; return false;
return UpdateSongFile(directory, name, suffix, st) || return UpdateSongFile(directory, name, suffix, info) ||
UpdateArchiveFile(directory, name, suffix, st) || UpdateArchiveFile(directory, name, suffix, info) ||
UpdatePlaylistFile(directory, name, suffix, st); UpdatePlaylistFile(directory, name, suffix, info);
} }
void void
UpdateWalk::UpdateDirectoryChild(Directory &directory, UpdateWalk::UpdateDirectoryChild(Directory &directory,
const char *name, const struct stat *st) const char *name, const FileInfo &info)
{ {
assert(strchr(name, '/') == nullptr); assert(strchr(name, '/') == nullptr);
if (S_ISREG(st->st_mode)) { if (info.IsRegular()) {
UpdateRegularFile(directory, name, st); UpdateRegularFile(directory, name, info);
} else if (S_ISDIR(st->st_mode)) { } else if (info.IsDirectory()) {
if (find_inode_ancestor(&directory, st->st_ino, st->st_dev)) if (find_inode_ancestor(storage, &directory,
info.inode, info.device))
return; return;
db_lock(); db_lock();
...@@ -218,7 +226,7 @@ UpdateWalk::UpdateDirectoryChild(Directory &directory, ...@@ -218,7 +226,7 @@ UpdateWalk::UpdateDirectoryChild(Directory &directory,
assert(&directory == subdir->parent); assert(&directory == subdir->parent);
if (!UpdateDirectory(*subdir, st)) if (!UpdateDirectory(*subdir, info))
editor.LockDeleteDirectory(subdir); editor.LockDeleteDirectory(subdir);
} else { } else {
FormatDebug(update_domain, FormatDebug(update_domain,
...@@ -228,12 +236,10 @@ UpdateWalk::UpdateDirectoryChild(Directory &directory, ...@@ -228,12 +236,10 @@ UpdateWalk::UpdateDirectoryChild(Directory &directory,
/* we don't look at "." / ".." nor files with newlines in their name */ /* we don't look at "." / ".." nor files with newlines in their name */
gcc_pure 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 strchr(name_utf8, '\n') != nullptr;
return (path[0] == '.' && path[1] == 0) ||
(path[0] == '.' && path[1] == '.' && path[2] == 0) ||
strchr(path, '\n') != nullptr;
} }
gcc_pure gcc_pure
...@@ -242,9 +248,11 @@ UpdateWalk::SkipSymlink(const Directory *directory, ...@@ -242,9 +248,11 @@ UpdateWalk::SkipSymlink(const Directory *directory,
const char *utf8_name) const const char *utf8_name) const
{ {
#ifndef WIN32 #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()) if (path_fs.IsNull())
return true; /* not a local file: don't skip */
return false;
const auto target = ReadLink(path_fs); const auto target = ReadLink(path_fs);
if (target.IsNull()) if (target.IsNull())
...@@ -304,63 +312,68 @@ UpdateWalk::SkipSymlink(const Directory *directory, ...@@ -304,63 +312,68 @@ UpdateWalk::SkipSymlink(const Directory *directory,
} }
bool 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); Error error;
if (path_fs.IsNull()) LocalDirectoryReader *const reader =
return false; storage.OpenDirectory(directory.GetPath(), error);
if (reader == nullptr) {
DirectoryReader reader(path_fs); LogError(error);
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; return false;
} }
ExcludeList exclude_list; 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()) if (!exclude_list.IsEmpty())
RemoveExcludedFromDirectory(directory, exclude_list); RemoveExcludedFromDirectory(directory, exclude_list);
PurgeDeletedFromDirectory(directory); PurgeDeletedFromDirectory(directory);
while (reader.ReadEntry()) { const char *name_utf8;
const auto entry = reader.GetEntry(); while ((name_utf8 = reader->Read()) != nullptr) {
if (skip_path(name_utf8))
if (skip_path(entry) || exclude_list.Check(entry))
continue; continue;
const std::string utf8 = entry.ToUTF8(); {
if (utf8.empty()) const auto name_fs = AllocatedPath::FromUTF8(name_utf8);
if (name_fs.IsNull() || exclude_list.Check(name_fs))
continue;
}
if (SkipSymlink(&directory, name_utf8)) {
modified |= editor.DeleteNameIn(directory, name_utf8);
continue; continue;
}
if (SkipSymlink(&directory, utf8.c_str())) { FileInfo info2;
modified |= editor.DeleteNameIn(directory, utf8.c_str()); if (!GetInfo(*reader, info2)) {
modified |= editor.DeleteNameIn(directory, name_utf8);
continue; continue;
} }
struct stat st2; UpdateDirectoryChild(directory, name_utf8, info2);
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; directory.mtime = info.mtime;
return true; return true;
} }
inline Directory * inline Directory *
UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8) UpdateWalk::DirectoryMakeChildChecked(Directory &parent,
const char *uri_utf8,
const char *name_utf8)
{ {
db_lock(); db_lock();
Directory *directory = parent.FindChild(name_utf8); Directory *directory = parent.FindChild(name_utf8);
...@@ -369,9 +382,9 @@ UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8) ...@@ -369,9 +382,9 @@ UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8)
if (directory != nullptr) if (directory != nullptr)
return directory; return directory;
struct stat st; FileInfo info;
if (stat_directory_child(parent, name_utf8, &st) < 0 || if (!GetInfo(storage, uri_utf8, info) ||
find_inode_ancestor(&parent, st.st_ino, st.st_dev)) find_inode_ancestor(storage, &parent, info.inode, info.device))
return nullptr; return nullptr;
if (SkipSymlink(&parent, name_utf8)) if (SkipSymlink(&parent, name_utf8))
...@@ -387,7 +400,7 @@ UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8) ...@@ -387,7 +400,7 @@ UpdateWalk::DirectoryMakeChildChecked(Directory &parent, const char *name_utf8)
directory = parent.CreateChild(name_utf8); directory = parent.CreateChild(name_utf8);
db_unlock(); db_unlock();
directory_set_stat(*directory, &st); directory_set_stat(*directory, info);
return directory; return directory;
} }
...@@ -405,6 +418,7 @@ UpdateWalk::DirectoryMakeUriParentChecked(Directory &root, const char *uri) ...@@ -405,6 +418,7 @@ UpdateWalk::DirectoryMakeUriParentChecked(Directory &root, const char *uri)
continue; continue;
directory = DirectoryMakeChildChecked(*directory, directory = DirectoryMakeChildChecked(*directory,
duplicated,
name_utf8); name_utf8);
if (directory == nullptr) if (directory == nullptr)
break; break;
...@@ -425,12 +439,18 @@ UpdateWalk::UpdateUri(Directory &root, const char *uri) ...@@ -425,12 +439,18 @@ UpdateWalk::UpdateUri(Directory &root, const char *uri)
const char *name = PathTraitsUTF8::GetBase(uri); const char *name = PathTraitsUTF8::GetBase(uri);
struct stat st; if (SkipSymlink(parent, name)) {
if (!SkipSymlink(parent, name) &&
stat_directory_child(*parent, name, &st) == 0)
UpdateDirectoryChild(*parent, name, &st);
else
modified |= editor.DeleteNameIn(*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 bool
...@@ -442,10 +462,11 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) ...@@ -442,10 +462,11 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard)
if (path != nullptr && !isRootDirectory(path)) { if (path != nullptr && !isRootDirectory(path)) {
UpdateUri(root, path); UpdateUri(root, path);
} else { } else {
struct stat st; FileInfo info;
if (!GetInfo(storage, "", info))
return false;
if (stat_directory(root, &st) == 0) UpdateDirectory(root, info);
UpdateDirectory(root, &st);
} }
return modified; return modified;
......
...@@ -22,10 +22,12 @@ ...@@ -22,10 +22,12 @@
#include "check.h" #include "check.h"
#include "Editor.hxx" #include "Editor.hxx"
#include "storage/LocalStorage.hxx"
#include <sys/stat.h> #include <sys/stat.h>
struct stat; struct stat;
struct FileInfo;
struct Directory; struct Directory;
struct archive_plugin; struct archive_plugin;
class ExcludeList; class ExcludeList;
...@@ -46,6 +48,8 @@ class UpdateWalk final { ...@@ -46,6 +48,8 @@ class UpdateWalk final {
bool walk_discard; bool walk_discard;
bool modified; bool modified;
LocalStorage storage;
DatabaseEditor editor; DatabaseEditor editor;
public: public:
...@@ -68,15 +72,15 @@ private: ...@@ -68,15 +72,15 @@ private:
void UpdateSongFile2(Directory &directory, void UpdateSongFile2(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st); const FileInfo &info);
bool UpdateSongFile(Directory &directory, bool UpdateSongFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st); const FileInfo &info);
bool UpdateContainerFile(Directory &directory, bool UpdateContainerFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st); const FileInfo &info);
#ifdef ENABLE_ARCHIVE #ifdef ENABLE_ARCHIVE
...@@ -84,10 +88,10 @@ private: ...@@ -84,10 +88,10 @@ private:
bool UpdateArchiveFile(Directory &directory, bool UpdateArchiveFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st); const FileInfo &info);
void UpdateArchiveFile(Directory &directory, const char *name, void UpdateArchiveFile(Directory &directory, const char *name,
const struct stat *st, const FileInfo &info,
const archive_plugin &plugin); const archive_plugin &plugin);
...@@ -95,22 +99,22 @@ private: ...@@ -95,22 +99,22 @@ private:
bool UpdateArchiveFile(gcc_unused Directory &directory, bool UpdateArchiveFile(gcc_unused Directory &directory,
gcc_unused const char *name, gcc_unused const char *name,
gcc_unused const char *suffix, gcc_unused const char *suffix,
gcc_unused const struct stat *st) { gcc_unused const FileInfo &info) {
return false; return false;
} }
#endif #endif
bool UpdatePlaylistFile(Directory &directory, bool UpdatePlaylistFile(Directory &directory,
const char *name, const char *suffix, const char *name, const char *suffix,
const struct stat *st); const FileInfo &info);
bool UpdateRegularFile(Directory &directory, bool UpdateRegularFile(Directory &directory,
const char *name, const struct stat *st); const char *name, const FileInfo &info);
void UpdateDirectoryChild(Directory &directory, 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 * Create the specified directory object if it does not exist
...@@ -121,9 +125,10 @@ private: ...@@ -121,9 +125,10 @@ private:
* The caller must lock the database. * The caller must lock the database.
*/ */
Directory *MakeDirectoryIfModified(Directory &parent, const char *name, Directory *MakeDirectoryIfModified(Directory &parent, const char *name,
const struct stat *st); const FileInfo &info);
Directory *DirectoryMakeChildChecked(Directory &parent, Directory *DirectoryMakeChildChecked(Directory &parent,
const char *uri_utf8,
const char *name_utf8); const char *name_utf8);
Directory *DirectoryMakeUriParentChecked(Directory &root, Directory *DirectoryMakeUriParentChecked(Directory &root,
......
/*
* 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
/*
* 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);
}
/*
* 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment