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