FileCommands.cxx 7.85 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 * 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.
 */

20 21
#define __STDC_FORMAT_MACROS /* for PRIu64 */

22 23
#include "config.h"
#include "FileCommands.hxx"
24
#include "Request.hxx"
25 26
#include "CommandError.hxx"
#include "protocol/Ack.hxx"
27
#include "client/Client.hxx"
28
#include "client/Response.hxx"
29
#include "util/CharUtil.hxx"
30
#include "util/UriUtil.hxx"
31
#include "tag/Handler.hxx"
32
#include "tag/Generic.hxx"
33
#include "TagStream.hxx"
34
#include "TagFile.hxx"
35
#include "storage/StorageInterface.hxx"
36
#include "fs/AllocatedPath.hxx"
37
#include "fs/FileInfo.hxx"
38
#include "fs/DirectoryReader.hxx"
Ryan Walklin's avatar
Ryan Walklin committed
39
#include "input/InputStream.hxx"
40
#include "LocateUri.hxx"
41
#include "TimePrint.hxx"
Ryan Walklin's avatar
Ryan Walklin committed
42
#include "thread/Mutex.hxx"
43 44

#include <assert.h>
45 46 47 48
#include <inttypes.h> /* for PRIu64 */

gcc_pure
static bool
49
SkipNameFS(PathTraitsFS::const_pointer_type name_fs) noexcept
50 51 52 53 54 55 56 57
{
	return name_fs[0] == '.' &&
		(name_fs[1] == 0 ||
		 (name_fs[1] == '.' && name_fs[2] == 0));
}

gcc_pure
static bool
58
skip_path(Path name_fs) noexcept
59
{
60
	return name_fs.HasNewline();
61 62
}

63
#if defined(_WIN32) && GCC_CHECK_VERSION(4,6)
64 65 66 67 68 69
/* PRIu64 causes bogus compiler warning */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat"
#pragma GCC diagnostic ignored "-Wformat-extra-args"
#endif

70
CommandResult
71
handle_listfiles_local(Response &r, Path path_fs)
72 73 74 75 76
{
	DirectoryReader reader(path_fs);

	while (reader.ReadEntry()) {
		const Path name_fs = reader.GetEntry();
77
		if (SkipNameFS(name_fs.c_str()) || skip_path(name_fs))
78 79 80 81 82 83
			continue;

		std::string name_utf8 = name_fs.ToUTF8();
		if (name_utf8.empty())
			continue;

84
		const auto full_fs = path_fs / name_fs;
85 86
		FileInfo fi;
		if (!GetFileInfo(full_fs, fi, false))
87 88
			continue;

89
		if (fi.IsRegular())
90 91 92 93
			r.Format("file: %s\n"
				 "size: %" PRIu64 "\n",
				 name_utf8.c_str(),
				 fi.GetSize());
94
		else if (fi.IsDirectory())
95
			r.Format("directory: %s\n", name_utf8.c_str());
96 97
		else
			continue;
98

99
		time_print(r, "Last-Modified", fi.GetModificationTime());
100 101 102 103
	}

	return CommandResult::OK;
}
104

105
#if defined(_WIN32) && GCC_CHECK_VERSION(4,6)
106 107 108
#pragma GCC diagnostic pop
#endif

109 110
gcc_pure
static bool
111
IsValidName(const char *p) noexcept
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
{
	if (!IsAlphaASCII(*p))
		return false;

	while (*++p) {
		const char ch = *p;
		if (!IsAlphaASCII(ch) && ch != '_' && ch != '-')
			return false;
	}

	return true;
}

gcc_pure
static bool
127
IsValidValue(const char *p) noexcept
128 129 130 131
{
	while (*p) {
		const char ch = *p++;

132
		if ((unsigned char)ch < 0x20)
133 134 135 136 137 138
			return false;
	}

	return true;
}

139 140
class PrintCommentHandler final : public NullTagHandler {
	Response &response;
141

142 143 144
public:
	explicit PrintCommentHandler(Response &_response) noexcept
		:NullTagHandler(WANT_PAIR), response(_response) {}
145

146 147 148 149
	void OnPair(const char *key, const char *value) noexcept override {
		if (IsValidName(key) && IsValidValue(value))
			response.Format("%s: %s\n", key, value);
	}
150 151
};

152
static CommandResult
153
read_stream_comments(Response &r, const char *uri)
154
{
155 156
	PrintCommentHandler h(r);
	if (!tag_stream_scan(uri, h)) {
157
		r.Error(ACK_ERROR_NO_EXIST, "Failed to load file");
158 159 160 161 162 163 164
		return CommandResult::ERROR;
	}

	return CommandResult::OK;

}

165
static CommandResult
166
read_file_comments(Response &r, const Path path_fs)
167
{
168
	PrintCommentHandler h(r);
169
	if (!ScanFileTagsNoGeneric(path_fs, h)) {
170
		r.Error(ACK_ERROR_NO_EXIST, "Failed to load file");
171 172 173
		return CommandResult::ERROR;
	}

174
	ScanGenericTags(path_fs, h);
175 176 177 178 179

	return CommandResult::OK;

}

180 181
static CommandResult
read_db_comments(Client &client, Response &r, const char *uri)
182
{
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
#ifdef ENABLE_DATABASE
	const Storage *storage = client.GetStorage();
	if (storage == nullptr) {
#else
		(void)client;
		(void)uri;
#endif
		r.Error(ACK_ERROR_NO_EXIST, "No database");
		return CommandResult::ERROR;
#ifdef ENABLE_DATABASE
	}

	{
		AllocatedPath path_fs = storage->MapFS(uri);
		if (!path_fs.IsNull())
			return read_file_comments(r, path_fs);
	}
200

201 202 203 204 205 206 207 208 209
	{
		const std::string uri2 = storage->MapUTF8(uri);
		if (uri_has_scheme(uri2.c_str()))
			return read_stream_comments(r, uri2.c_str());
	}

	r.Error(ACK_ERROR_NO_EXIST, "No such file");
	return CommandResult::ERROR;
#endif
210 211
}

212
CommandResult
213
handle_read_comments(Client &client, Request args, Response &r)
214
{
215
	assert(args.size == 1);
216

217
	const char *const uri = args.front();
218

219
	const auto located_uri = LocateUri(uri, &client
220
#ifdef ENABLE_DATABASE
221
					   , nullptr
222
#endif
223
					   );
224 225 226
	switch (located_uri.type) {
	case LocatedUri::Type::ABSOLUTE:
		return read_stream_comments(r, located_uri.canonical_uri);
227

228 229
	case LocatedUri::Type::RELATIVE:
		return read_db_comments(client, r, located_uri.canonical_uri);
230

231 232
	case LocatedUri::Type::PATH:
		return read_file_comments(r, located_uri.path);
233
	}
234 235

	gcc_unreachable();
236
}
Ryan Walklin's avatar
Ryan Walklin committed
237 238 239 240 241 242 243 244

/**
 * Searches for the files listed in #artnames in the UTF8 folder
 * URI #directory. This can be a local path or protocol-based
 * URI that #InputStream supports. Returns the first successfully
 * opened file or #nullptr on failure.
 */
static InputStreamPtr
245
find_stream_art(const char *directory, Mutex &mutex)
Ryan Walklin's avatar
Ryan Walklin committed
246 247 248 249 250 251 252 253 254 255 256 257
{
	static constexpr char const * art_names[] = {
		"cover.png",
		"cover.jpg",
		"cover.tiff",
		"cover.bmp"
	};

	for(const auto name: art_names) {
		std::string art_file = PathTraitsUTF8::Build(directory, name);

		try {
258
			return InputStream::OpenReady(art_file.c_str(), mutex);
Ryan Walklin's avatar
Ryan Walklin committed
259 260 261 262 263 264 265 266
		} catch (const std::exception &e) {}
	}
	return nullptr;
}

static CommandResult
read_stream_art(Response &r, const char *uri, size_t offset)
{
267
	std::string art_directory = PathTraitsUTF8::GetParent(uri);
Ryan Walklin's avatar
Ryan Walklin committed
268 269 270

	Mutex mutex;

271
	InputStreamPtr is = find_stream_art(art_directory.c_str(), mutex);
Ryan Walklin's avatar
Ryan Walklin committed
272 273 274 275 276 277 278 279 280 281

	if (is == nullptr) {
		r.Error(ACK_ERROR_NO_EXIST, "No file exists");
		return CommandResult::ERROR;
	}
	if (!is->KnownSize()) {
		r.Error(ACK_ERROR_NO_EXIST, "Cannot get size for stream");
		return CommandResult::ERROR;
	}

282
	const offset_type art_file_size = is->GetSize();
Ryan Walklin's avatar
Ryan Walklin committed
283 284 285 286 287 288 289 290

	constexpr size_t CHUNK_SIZE = 8192;
	uint8_t buffer[CHUNK_SIZE];
	size_t read_size;

	is->Seek(offset);
	read_size = is->Read(&buffer, CHUNK_SIZE);

291
	r.Format("size: %" PRIoffset "\n"
Ryan Walklin's avatar
Ryan Walklin committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
			 "binary: %u\n",
			 art_file_size,
			 read_size
			 );

	r.Write(buffer, read_size);
	r.Write("\n");

	return CommandResult::OK;
}

#ifdef ENABLE_DATABASE
static CommandResult
read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset)
{
	const Storage *storage = client.GetStorage();
	if (storage == nullptr) {
		r.Error(ACK_ERROR_NO_EXIST, "No database");
		return CommandResult::ERROR;
	}
	std::string uri2 = storage->MapUTF8(uri);
	return read_stream_art(r, uri2.c_str(), offset);
}
#endif

CommandResult
handle_album_art(Client &client, Request args, Response &r)
{
	assert(args.size == 2);

	const char *uri = args.front();
	size_t offset = args.ParseUnsigned(1);

	const auto located_uri = LocateUri(uri, &client
#ifdef ENABLE_DATABASE
					   , nullptr
#endif
					   );

	switch (located_uri.type) {
	case LocatedUri::Type::ABSOLUTE:
	case LocatedUri::Type::PATH:
		return read_stream_art(r, located_uri.canonical_uri, offset);
	case LocatedUri::Type::RELATIVE:
#ifdef ENABLE_DATABASE
		return read_db_art(client, r, located_uri.canonical_uri, offset);
#else
		r.Error(ACK_ERROR_NO_EXIST, "Database disabled");
		return CommandResult::ERROR;
#endif
	}
	r.Error(ACK_ERROR_NO_EXIST, "No art file exists");
	return CommandResult::ERROR;
}