diff --git a/NEWS b/NEWS
index afc5e7c63..1f0a6caae 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,6 @@
ver 0.19 (not yet released)
* protocol
- - new commands "addtagid", "cleartagid"
+ - new commands "addtagid", "cleartagid", "listfiles"
- "lsinfo" and "readcomments" allowed for remote files
- "listneighbors" lists file servers on the local network
- "playlistadd" supports file:///
diff --git a/doc/protocol.xml b/doc/protocol.xml
index 625fef874..1c7e928b1 100644
--- a/doc/protocol.xml
+++ b/doc/protocol.xml
@@ -1600,6 +1600,31 @@ OK
+
+
+
+ listfiles
+ URI
+
+
+
+
+ Lists the contents of the directory
+ URI, including files are not
+ recognized by MPD. URI can be a path
+ relative to the music directory or an URI understood by
+ one of the storage plugins. The response contains at
+ least one line for each directory entry with the prefix
+ "file: " or "directory: ", and may be followed by file
+ attributes such as "Last-Modified" and "size".
+
+
+ For example, "smb://SERVER" returns a list of all shares
+ on the given SMB/CIFS server; "nfs://servername/path"
+ obtains a directory listing from the NFS server.
+
+
+
diff --git a/src/SongPrint.cxx b/src/SongPrint.cxx
index 30c248d1e..f607fc151 100644
--- a/src/SongPrint.cxx
+++ b/src/SongPrint.cxx
@@ -30,44 +30,50 @@
#define SONG_FILE "file: "
static void
-song_print_uri(Client &client, const char *uri)
+song_print_uri(Client &client, const char *uri, bool base)
{
+ std::string allocated;
+
+ if (base) {
+ uri = PathTraitsUTF8::GetBase(uri);
+ } else {
#ifdef ENABLE_DATABASE
- const Storage *storage = client.GetStorage();
- if (storage != nullptr) {
- const char *suffix = storage->MapToRelativeUTF8(uri);
- if (suffix != nullptr)
- uri = suffix;
- }
+ const Storage *storage = client.GetStorage();
+ if (storage != nullptr) {
+ const char *suffix = storage->MapToRelativeUTF8(uri);
+ if (suffix != nullptr)
+ uri = suffix;
+ }
#endif
- const std::string allocated = uri_remove_auth(uri);
- if (!allocated.empty())
- uri = allocated.c_str();
+ allocated = uri_remove_auth(uri);
+ if (!allocated.empty())
+ uri = allocated.c_str();
+ }
client_printf(client, "%s%s\n", SONG_FILE, uri);
}
void
-song_print_uri(Client &client, const LightSong &song)
+song_print_uri(Client &client, const LightSong &song, bool base)
{
- if (song.directory != nullptr) {
+ if (!base && song.directory != nullptr) {
client_printf(client, "%s%s/%s\n", SONG_FILE,
song.directory, song.uri);
} else
- song_print_uri(client, song.uri);
+ song_print_uri(client, song.uri, base);
}
void
-song_print_uri(Client &client, const DetachedSong &song)
+song_print_uri(Client &client, const DetachedSong &song, bool base)
{
- song_print_uri(client, song.GetURI());
+ song_print_uri(client, song.GetURI(), base);
}
void
-song_print_info(Client &client, const LightSong &song)
+song_print_info(Client &client, const LightSong &song, bool base)
{
- song_print_uri(client, song);
+ song_print_uri(client, song, base);
if (song.end_ms > 0)
client_printf(client, "Range: %u.%03u-%u.%03u\n",
@@ -87,9 +93,9 @@ song_print_info(Client &client, const LightSong &song)
}
void
-song_print_info(Client &client, const DetachedSong &song)
+song_print_info(Client &client, const DetachedSong &song, bool base)
{
- song_print_uri(client, song);
+ song_print_uri(client, song, base);
const unsigned start_ms = song.GetStartMS();
const unsigned end_ms = song.GetEndMS();
diff --git a/src/SongPrint.hxx b/src/SongPrint.hxx
index 16a9ee6ff..5e4c93a74 100644
--- a/src/SongPrint.hxx
+++ b/src/SongPrint.hxx
@@ -25,15 +25,15 @@ class DetachedSong;
class Client;
void
-song_print_info(Client &client, const DetachedSong &song);
+song_print_info(Client &client, const DetachedSong &song, bool base=false);
void
-song_print_info(Client &client, const LightSong &song);
+song_print_info(Client &client, const LightSong &song, bool base=false);
void
-song_print_uri(Client &client, const LightSong &song);
+song_print_uri(Client &client, const LightSong &song, bool base=false);
void
-song_print_uri(Client &client, const DetachedSong &song);
+song_print_uri(Client &client, const DetachedSong &song, bool base=false);
#endif
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index 488c27daa..ab4de823b 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -107,6 +107,9 @@ static const struct command commands[] = {
{ "list", PERMISSION_READ, 1, -1, handle_list },
{ "listall", PERMISSION_READ, 0, 1, handle_listall },
{ "listallinfo", PERMISSION_READ, 0, 1, handle_listallinfo },
+#endif
+ { "listfiles", PERMISSION_READ, 0, 1, handle_listfiles },
+#ifdef ENABLE_DATABASE
{ "listmounts", PERMISSION_READ, 0, 0, handle_listmounts },
#endif
#ifdef ENABLE_NEIGHBOR_PLUGINS
diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx
index 9d00698c6..96ea357bc 100644
--- a/src/command/DatabaseCommands.cxx
+++ b/src/command/DatabaseCommands.cxx
@@ -31,6 +31,18 @@
#include "SongFilter.hxx"
#include "protocol/Result.hxx"
+CommandResult
+handle_listfiles_db(Client &client, const char *uri)
+{
+ const DatabaseSelection selection(uri, false);
+
+ Error error;
+ if (!db_selection_print(client, selection, false, true, error))
+ return print_error(client, error);
+
+ return CommandResult::OK;
+}
+
CommandResult
handle_lsinfo2(Client &client, int argc, char *argv[])
{
@@ -42,7 +54,7 @@ handle_lsinfo2(Client &client, int argc, char *argv[])
const DatabaseSelection selection(uri, false);
Error error;
- if (!db_selection_print(client, selection, true, error))
+ if (!db_selection_print(client, selection, true, false, error))
return print_error(client, error);
return CommandResult::OK;
@@ -60,7 +72,7 @@ handle_match(Client &client, int argc, char *argv[], bool fold_case)
const DatabaseSelection selection("", true, &filter);
Error error;
- return db_selection_print(client, selection, true, error)
+ return db_selection_print(client, selection, true, false, error)
? CommandResult::OK
: print_error(client, error);
}
diff --git a/src/command/DatabaseCommands.hxx b/src/command/DatabaseCommands.hxx
index 8678f19c8..7a9c68ffe 100644
--- a/src/command/DatabaseCommands.hxx
+++ b/src/command/DatabaseCommands.hxx
@@ -24,6 +24,9 @@
class Client;
+CommandResult
+handle_listfiles_db(Client &client, const char *uri);
+
CommandResult
handle_lsinfo2(Client &client, int argc, char *argv[]);
diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx
index e5cc9690f..f7ca28b50 100644
--- a/src/command/FileCommands.cxx
+++ b/src/command/FileCommands.cxx
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
+#define __STDC_FORMAT_MACROS /* for PRIu64 */
+
#include "config.h"
#include "FileCommands.hxx"
#include "CommandError.hxx"
@@ -33,9 +35,79 @@
#include "TagFile.hxx"
#include "storage/StorageInterface.hxx"
#include "fs/AllocatedPath.hxx"
+#include "fs/FileSystem.hxx"
+#include "TimePrint.hxx"
#include "ls.hxx"
#include
+#include
+#include /* for PRIu64 */
+
+gcc_pure
+static bool
+SkipNameFS(const char *name_fs)
+{
+ return name_fs[0] == '.' &&
+ (name_fs[1] == 0 ||
+ (name_fs[1] == '.' && name_fs[2] == 0));
+}
+
+gcc_pure
+static bool
+skip_path(const char *name_fs)
+{
+ return strchr(name_fs, '\n') != nullptr;
+}
+
+CommandResult
+handle_listfiles_local(Client &client, const char *path_utf8)
+{
+ const auto path_fs = AllocatedPath::FromUTF8(path_utf8);
+ if (path_fs.IsNull()) {
+ command_error(client, ACK_ERROR_NO_EXIST,
+ "unsupported file name");
+ return CommandResult::ERROR;
+ }
+
+ Error error;
+ if (!client.AllowFile(path_fs, error))
+ return print_error(client, error);
+
+ DirectoryReader reader(path_fs);
+ if (reader.HasFailed()) {
+ error.FormatErrno("Failed to open '%s'", path_utf8);
+ return print_error(client, error);
+ }
+
+ while (reader.ReadEntry()) {
+ const Path name_fs = reader.GetEntry();
+ if (SkipNameFS(name_fs.c_str()) || skip_path(name_fs.c_str()))
+ continue;
+
+ std::string name_utf8 = name_fs.ToUTF8();
+ if (name_utf8.empty())
+ continue;
+
+ const AllocatedPath full_fs =
+ AllocatedPath::Build(path_fs, name_fs);
+ struct stat st;
+ if (!StatFile(full_fs, st, false))
+ continue;
+
+ if (S_ISREG(st.st_mode)) {
+ client_printf(client, "file: %s\n"
+ "size: %" PRIu64 "\n",
+ name_utf8.c_str(),
+ uint64_t(st.st_size));
+ } else if (S_ISDIR(st.st_mode))
+ client_printf(client, "directory: %s\n",
+ name_utf8.c_str());
+
+ time_print(client, "Last-Modified", st.st_mtime);
+ }
+
+ return CommandResult::OK;
+}
gcc_pure
static bool
diff --git a/src/command/FileCommands.hxx b/src/command/FileCommands.hxx
index 8858b62c9..51467a009 100644
--- a/src/command/FileCommands.hxx
+++ b/src/command/FileCommands.hxx
@@ -24,6 +24,9 @@
class Client;
+CommandResult
+handle_listfiles_local(Client &client, const char *path_utf8);
+
CommandResult
handle_read_comments(Client &client, int argc, char *argv[]);
diff --git a/src/command/OtherCommands.cxx b/src/command/OtherCommands.cxx
index 6415e84df..eac26735b 100644
--- a/src/command/OtherCommands.cxx
+++ b/src/command/OtherCommands.cxx
@@ -19,6 +19,8 @@
#include "config.h"
#include "OtherCommands.hxx"
+#include "FileCommands.hxx"
+#include "StorageCommands.hxx"
#include "CommandError.hxx"
#include "db/Uri.hxx"
#include "storage/StorageInterface.hxx"
@@ -112,6 +114,41 @@ print_tag(TagType type, const char *value, void *ctx)
tag_print(client, type, value);
}
+CommandResult
+handle_listfiles(Client &client, int argc, char *argv[])
+{
+ const char *const uri = argc == 2
+ ? argv[1]
+ /* default is root directory */
+ : "";
+
+ if (memcmp(uri, "file:///", 8) == 0)
+ /* list local directory */
+ return handle_listfiles_local(client, uri + 7);
+
+#ifdef ENABLE_DATABASE
+ if (uri_has_scheme(uri))
+ /* use storage plugin to list remote directory */
+ return handle_listfiles_storage(client, uri);
+
+ /* must be a path relative to the configured
+ music_directory */
+
+ if (client.partition.instance.storage != nullptr)
+ /* if we have a storage instance, obtain a list of
+ files from it */
+ return handle_listfiles_storage(client,
+ *client.partition.instance.storage,
+ uri);
+
+ /* fall back to entries from database if we have no storage */
+ return handle_listfiles_db(client, uri);
+#else
+ command_error(client, ACK_ERROR_NO_EXIST, "No database");
+ return CommandResult::ERROR;
+#endif
+}
+
static constexpr tag_handler print_tag_handler = {
nullptr,
print_tag,
diff --git a/src/command/OtherCommands.hxx b/src/command/OtherCommands.hxx
index 4f54303bd..f487e9605 100644
--- a/src/command/OtherCommands.hxx
+++ b/src/command/OtherCommands.hxx
@@ -39,6 +39,9 @@ handle_kill(Client &client, int argc, char *argv[]);
CommandResult
handle_close(Client &client, int argc, char *argv[]);
+CommandResult
+handle_listfiles(Client &client, int argc, char *argv[]);
+
CommandResult
handle_lsinfo(Client &client, int argc, char *argv[]);
diff --git a/src/command/StorageCommands.cxx b/src/command/StorageCommands.cxx
index f0698f04b..ffee8cb74 100644
--- a/src/command/StorageCommands.cxx
+++ b/src/command/StorageCommands.cxx
@@ -17,6 +17,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
+#define __STDC_FORMAT_MACROS /* for PRIu64 */
+
#include "config.h"
#include "StorageCommands.hxx"
#include "CommandError.hxx"
@@ -29,10 +31,103 @@
#include "Instance.hxx"
#include "storage/Registry.hxx"
#include "storage/CompositeStorage.hxx"
+#include "storage/FileInfo.hxx"
#include "db/plugins/simple/SimpleDatabasePlugin.hxx"
#include "db/update/Service.hxx"
+#include "TimePrint.hxx"
#include "Idle.hxx"
+#include /* for PRIu64 */
+
+gcc_pure
+static bool
+skip_path(const char *name_utf8)
+{
+ return strchr(name_utf8, '\n') != nullptr;
+}
+
+static bool
+handle_listfiles_storage(Client &client, StorageDirectoryReader &reader,
+ Error &error)
+{
+ const char *name_utf8;
+ while ((name_utf8 = reader.Read()) != nullptr) {
+ if (skip_path(name_utf8))
+ continue;
+
+ FileInfo info;
+ if (!reader.GetInfo(false, info, error))
+ continue;
+
+ switch (info.type) {
+ case FileInfo::Type::OTHER:
+ /* ignore */
+ continue;
+
+ case FileInfo::Type::REGULAR:
+ client_printf(client, "file: %s\n"
+ "size: %" PRIu64 "\n",
+ name_utf8,
+ info.size);
+ break;
+
+ case FileInfo::Type::DIRECTORY:
+ client_printf(client, "directory: %s\n", name_utf8);
+ break;
+ }
+
+ if (info.mtime != 0)
+ time_print(client, "Last-Modified", info.mtime);
+ }
+
+ return true;
+}
+
+static bool
+handle_listfiles_storage(Client &client, Storage &storage, const char *uri,
+ Error &error)
+{
+ auto reader = storage.OpenDirectory(uri, error);
+ if (reader == nullptr)
+ return false;
+
+ bool success = handle_listfiles_storage(client, *reader, error);
+ delete reader;
+ return success;
+}
+
+CommandResult
+handle_listfiles_storage(Client &client, Storage &storage, const char *uri)
+{
+ Error error;
+ if (!handle_listfiles_storage(client, storage, uri, error))
+ return print_error(client, error);
+
+ return CommandResult::OK;
+}
+
+CommandResult
+handle_listfiles_storage(Client &client, const char *uri)
+{
+ Error error;
+ Storage *storage = CreateStorageURI(uri, error);
+ if (storage == nullptr) {
+ if (error.IsDefined())
+ return print_error(client, error);
+
+ command_error(client, ACK_ERROR_ARG,
+ "Unrecognized storage URI");
+ return CommandResult::ERROR;
+ }
+
+ bool success = handle_listfiles_storage(client, *storage, "", error);
+ delete storage;
+ if (!success)
+ return print_error(client, error);
+
+ return CommandResult::OK;
+}
+
static void
print_storage_uri(Client &client, const Storage &storage)
{
diff --git a/src/command/StorageCommands.hxx b/src/command/StorageCommands.hxx
index 82470a0e2..905cd636e 100644
--- a/src/command/StorageCommands.hxx
+++ b/src/command/StorageCommands.hxx
@@ -23,6 +23,13 @@
#include "CommandResult.hxx"
class Client;
+class Storage;
+
+CommandResult
+handle_listfiles_storage(Client &client, Storage &storage, const char *uri);
+
+CommandResult
+handle_listfiles_storage(Client &client, const char *uri);
CommandResult
handle_listmounts(Client &client, int argc, char *argv[]);
diff --git a/src/db/DatabasePrint.cxx b/src/db/DatabasePrint.cxx
index 0ffcad35e..c8ffa102f 100644
--- a/src/db/DatabasePrint.cxx
+++ b/src/db/DatabasePrint.cxx
@@ -29,29 +29,39 @@
#include "LightDirectory.hxx"
#include "PlaylistInfo.hxx"
#include "Interface.hxx"
+#include "fs/Traits.hxx"
#include
-static void
-PrintDirectoryURI(Client &client, const LightDirectory &directory)
+static const char *
+ApplyBaseFlag(const char *uri, bool base)
{
- client_printf(client, "directory: %s\n", directory.GetPath());
+ if (base)
+ uri = PathTraitsUTF8::GetBase(uri);
+ return uri;
+}
+
+static void
+PrintDirectoryURI(Client &client, bool base, const LightDirectory &directory)
+{
+ client_printf(client, "directory: %s\n",
+ ApplyBaseFlag(directory.GetPath(), base));
}
static bool
-PrintDirectoryBrief(Client &client, const LightDirectory &directory)
+PrintDirectoryBrief(Client &client, bool base, const LightDirectory &directory)
{
if (!directory.IsRoot())
- PrintDirectoryURI(client, directory);
+ PrintDirectoryURI(client, base, directory);
return true;
}
static bool
-PrintDirectoryFull(Client &client, const LightDirectory &directory)
+PrintDirectoryFull(Client &client, bool base, const LightDirectory &directory)
{
if (!directory.IsRoot()) {
- PrintDirectoryURI(client, directory);
+ PrintDirectoryURI(client, base, directory);
if (directory.mtime > 0)
time_print(client, "Last-Modified", directory.mtime);
@@ -61,23 +71,24 @@ PrintDirectoryFull(Client &client, const LightDirectory &directory)
}
static void
-print_playlist_in_directory(Client &client,
+print_playlist_in_directory(Client &client, bool base,
const char *directory,
const char *name_utf8)
{
- if (directory == nullptr)
- client_printf(client, "playlist: %s\n", name_utf8);
+ if (base || directory == nullptr)
+ client_printf(client, "playlist: %s\n",
+ ApplyBaseFlag(name_utf8, base));
else
client_printf(client, "playlist: %s/%s\n",
directory, name_utf8);
}
static void
-print_playlist_in_directory(Client &client,
+print_playlist_in_directory(Client &client, bool base,
const LightDirectory *directory,
const char *name_utf8)
{
- if (directory == nullptr || directory->IsRoot())
+ if (base || directory == nullptr || directory->IsRoot())
client_printf(client, "playlist: %s\n", name_utf8);
else
client_printf(client, "playlist: %s/%s\n",
@@ -85,44 +96,48 @@ print_playlist_in_directory(Client &client,
}
static bool
-PrintSongBrief(Client &client, const LightSong &song)
+PrintSongBrief(Client &client, bool base, const LightSong &song)
{
- song_print_uri(client, song);
+ song_print_uri(client, song, base);
if (song.tag->has_playlist)
/* this song file has an embedded CUE sheet */
- print_playlist_in_directory(client, song.directory, song.uri);
+ print_playlist_in_directory(client, base,
+ song.directory, song.uri);
return true;
}
static bool
-PrintSongFull(Client &client, const LightSong &song)
+PrintSongFull(Client &client, bool base, const LightSong &song)
{
- song_print_info(client, song);
+ song_print_info(client, song, base);
if (song.tag->has_playlist)
/* this song file has an embedded CUE sheet */
- print_playlist_in_directory(client, song.directory, song.uri);
+ print_playlist_in_directory(client, base,
+ song.directory, song.uri);
return true;
}
static bool
-PrintPlaylistBrief(Client &client,
+PrintPlaylistBrief(Client &client, bool base,
const PlaylistInfo &playlist,
const LightDirectory &directory)
{
- print_playlist_in_directory(client, &directory, playlist.name.c_str());
+ print_playlist_in_directory(client, base,
+ &directory, playlist.name.c_str());
return true;
}
static bool
-PrintPlaylistFull(Client &client,
+PrintPlaylistFull(Client &client, bool base,
const PlaylistInfo &playlist,
const LightDirectory &directory)
{
- print_playlist_in_directory(client, &directory, playlist.name.c_str());
+ print_playlist_in_directory(client, base,
+ &directory, playlist.name.c_str());
if (playlist.mtime > 0)
time_print(client, "Last-Modified", playlist.mtime);
@@ -132,7 +147,7 @@ PrintPlaylistFull(Client &client,
bool
db_selection_print(Client &client, const DatabaseSelection &selection,
- bool full, Error &error)
+ bool full, bool base, Error &error)
{
const Database *db = client.GetDatabase(error);
if (db == nullptr)
@@ -141,13 +156,13 @@ db_selection_print(Client &client, const DatabaseSelection &selection,
using namespace std::placeholders;
const auto d = selection.filter == nullptr
? std::bind(full ? PrintDirectoryFull : PrintDirectoryBrief,
- std::ref(client), _1)
+ std::ref(client), base, _1)
: VisitDirectory();
const auto s = std::bind(full ? PrintSongFull : PrintSongBrief,
- std::ref(client), _1);
+ std::ref(client), base, _1);
const auto p = selection.filter == nullptr
? std::bind(full ? PrintPlaylistFull : PrintPlaylistBrief,
- std::ref(client), _1, _2)
+ std::ref(client), base, _1, _2)
: VisitPlaylist();
return db->Visit(selection, d, s, p, error);
@@ -202,7 +217,7 @@ bool
printAllIn(Client &client, const char *uri_utf8, Error &error)
{
const DatabaseSelection selection(uri_utf8, true);
- return db_selection_print(client, selection, false, error);
+ return db_selection_print(client, selection, false, false, error);
}
bool
@@ -210,7 +225,7 @@ printInfoForAllIn(Client &client, const char *uri_utf8,
Error &error)
{
const DatabaseSelection selection(uri_utf8, true);
- return db_selection_print(client, selection, true, error);
+ return db_selection_print(client, selection, true, false, error);
}
static bool
diff --git a/src/db/DatabasePrint.hxx b/src/db/DatabasePrint.hxx
index f336d9ff5..ef75dae36 100644
--- a/src/db/DatabasePrint.hxx
+++ b/src/db/DatabasePrint.hxx
@@ -29,10 +29,11 @@ class Error;
/**
* @param full print attributes/tags
+ * @param base print only base name of songs/directories?
*/
bool
db_selection_print(Client &client, const DatabaseSelection &selection,
- bool full, Error &error);
+ bool full, bool base, Error &error);
gcc_nonnull(2)
bool