SimpleDatabasePlugin.cxx 10.8 KB
Newer Older
1
/*
2
 * Copyright 2003-2018 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 * 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"
21
#include "SimpleDatabasePlugin.hxx"
Max Kellermann's avatar
Max Kellermann committed
22
#include "PrefixedLightSong.hxx"
23
#include "Mount.hxx"
24
#include "db/DatabasePlugin.hxx"
Max Kellermann's avatar
Max Kellermann committed
25 26
#include "db/Selection.hxx"
#include "db/Helpers.hxx"
27
#include "db/Stats.hxx"
28
#include "db/UniqueTags.hxx"
29
#include "db/VHelper.hxx"
Max Kellermann's avatar
Max Kellermann committed
30
#include "db/LightDirectory.hxx"
31 32 33
#include "Directory.hxx"
#include "Song.hxx"
#include "DatabaseSave.hxx"
Max Kellermann's avatar
Max Kellermann committed
34 35
#include "db/DatabaseLock.hxx"
#include "db/DatabaseError.hxx"
36
#include "tag/Mask.hxx"
37 38 39
#include "fs/io/TextFile.hxx"
#include "fs/io/BufferedOutputStream.hxx"
#include "fs/io/FileOutputStream.hxx"
40
#include "fs/FileInfo.hxx"
41
#include "config/Block.hxx"
42
#include "fs/FileSystem.hxx"
Max Kellermann's avatar
Max Kellermann committed
43
#include "util/CharUtil.hxx"
44
#include "util/Domain.hxx"
45
#include "Log.hxx"
46

47
#ifdef ENABLE_ZLIB
48 49 50
#include "fs/io/GzipOutputStream.hxx"
#endif

51 52
#include <memory>

53 54
#include <errno.h>

55
static constexpr Domain simple_db_domain("simple_db");
56

57
inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
58
	:Database(simple_db_plugin),
59
	 path(block.GetPath("path")),
60
#ifdef ENABLE_ZLIB
61
	 compress(block.GetBlockValue("compress", true)),
62
#endif
63 64 65 66 67 68 69 70
	 cache_path(block.GetPath("cache_directory")),
	 prefixed_light_song(nullptr)
{
	if (path.IsNull())
		throw std::runtime_error("No \"path\" parameter specified");

	path_utf8 = path.ToUTF8();
}
Max Kellermann's avatar
Max Kellermann committed
71

72
inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path,
73
#ifndef ENABLE_ZLIB
74 75
				      gcc_unused
#endif
76
				      bool _compress) noexcept
Max Kellermann's avatar
Max Kellermann committed
77 78 79
	:Database(simple_db_plugin),
	 path(std::move(_path)),
	 path_utf8(path.ToUTF8()),
80
#ifdef ENABLE_ZLIB
81 82
	 compress(_compress),
#endif
83
	 cache_path(nullptr),
Max Kellermann's avatar
Max Kellermann committed
84 85
	 prefixed_light_song(nullptr) {
}
86

87
Database *
88
SimpleDatabase::Create(EventLoop &, EventLoop &,
89
		       gcc_unused DatabaseListener &listener,
90
		       const ConfigBlock &block)
91
{
92
	return new SimpleDatabase(block);
93 94
}

95 96
void
SimpleDatabase::Check() const
97
{
98
	assert(!path.IsNull());
99 100

	/* Check if the file exists */
101
	if (!PathExists(path)) {
102 103 104
		/* If the file doesn't exist, we can't check if we can write
		 * it, so we are going to try to get the directory path, and
		 * see if we can write a file in that */
105
		const auto dirPath = path.GetDirectoryName();
106 107

		/* Check that the parent part of the path is a directory */
108
		FileInfo fi;
109

110 111 112 113
		try {
			fi = FileInfo(dirPath);
		} catch (...) {
			std::throw_with_nested(std::runtime_error("On parent directory of db file"));
114 115
		}

116 117 118 119 120
		if (!fi.IsDirectory())
			throw std::runtime_error("Couldn't create db file \"" +
						 path_utf8 + "\" because the "
						 "parent path is not a directory");

121
#ifndef _WIN32
122
		/* Check if we can write to the directory */
123
		if (!CheckAccess(dirPath, X_OK | W_OK)) {
124
			const int e = errno;
125
			const std::string dirPath_utf8 = dirPath.ToUTF8();
126
			throw FormatErrno(e, "Can't create db file in \"%s\"",
127
					  dirPath_utf8.c_str());
128
		}
129
#endif
130 131

		return;
132 133 134
	}

	/* Path exists, now check if it's a regular file */
135
	const FileInfo fi(path);
136

137 138
	if (!fi.IsRegular())
		throw std::runtime_error("db file \"" + path_utf8 + "\" is not a regular file");
139

140
#ifndef _WIN32
141
	/* And check that we can write to it */
142 143
	if (!CheckAccess(path, R_OK | W_OK))
		throw FormatErrno("Can't open db file \"%s\" for reading/writing",
144
				  path_utf8.c_str());
145
#endif
146 147
}

148 149
void
SimpleDatabase::Load()
150
{
151
	assert(!path.IsNull());
152
	assert(root != nullptr);
153

154
	TextFile file(path);
155

156 157
	LogDebug(simple_db_domain, "reading DB");

158
	db_load_internal(file, *root);
159

160 161
	FileInfo fi;
	if (GetFileInfo(path, fi))
162
		mtime = fi.GetModificationTime();
163 164
}

165 166
void
SimpleDatabase::Open()
167
{
Max Kellermann's avatar
Max Kellermann committed
168 169
	assert(prefixed_light_song == nullptr);

170
	root = Directory::NewRoot();
171
	mtime = std::chrono::system_clock::time_point::min();
172

173 174 175 176
#ifndef NDEBUG
	borrowed_song_count = 0;
#endif

177
	try {
178
		Load();
179 180
	} catch (...) {
		LogError(std::current_exception());
181

182
		delete root;
183

184
		Check();
185

186
		root = Directory::NewRoot();
187 188 189
	}
}

190
void
191
SimpleDatabase::Close() noexcept
192
{
193
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
194
	assert(prefixed_light_song == nullptr);
195
	assert(borrowed_song_count == 0);
196

197
	delete root;
198 199
}

200
const LightSong *
201
SimpleDatabase::GetSong(const char *uri) const
202
{
203
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
204
	assert(prefixed_light_song == nullptr);
205
	assert(borrowed_song_count == 0);
206

207
	ScopeDatabaseLock protect;
208 209

	auto r = root->LookupDirectory(uri);
Max Kellermann's avatar
Max Kellermann committed
210 211 212

	if (r.directory->IsMount()) {
		/* pass the request to the mounted database */
213
		protect.unlock();
Max Kellermann's avatar
Max Kellermann committed
214 215

		const LightSong *song =
216
			r.directory->mounted_database->GetSong(r.uri);
Max Kellermann's avatar
Max Kellermann committed
217 218 219 220 221 222 223 224
		if (song == nullptr)
			return nullptr;

		prefixed_light_song =
			new PrefixedLightSong(*song, r.directory->GetPath());
		return prefixed_light_song;
	}

225
	if (r.uri == nullptr)
226
		/* it's a directory */
227 228
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
229

230
	if (strchr(r.uri, '/') != nullptr)
231
		/* refers to a URI "below" the actual song */
232 233
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
234 235

	const Song *song = r.directory->FindSong(r.uri);
236
	protect.unlock();
237 238 239
	if (song == nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
240

241
	light_song.Construct(song->Export());
242

243
#ifndef NDEBUG
244
	++borrowed_song_count;
245
#endif
246

247
	return &light_song.Get();
248 249
}

250
void
251
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const noexcept
252
{
Max Kellermann's avatar
Max Kellermann committed
253
	assert(song != nullptr);
254
	assert(song == &light_song.Get() || song == prefixed_light_song);
255

256 257 258 259
	if (prefixed_light_song != nullptr) {
		delete prefixed_light_song;
		prefixed_light_song = nullptr;
	} else {
260
#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
261 262
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
263
#endif
264 265 266

		light_song.Destruct();
	}
267 268
}

269 270 271 272 273 274 275 276 277
gcc_const
static DatabaseSelection
CheckSelection(DatabaseSelection selection) noexcept
{
	selection.uri.clear();
	selection.filter = nullptr;
	return selection;
}

278
void
279
SimpleDatabase::Visit(const DatabaseSelection &selection,
280 281
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
282
		      VisitPlaylist visit_playlist) const
283
{
284 285
	ScopeDatabaseLock protect;

286
	auto r = root->LookupDirectory(selection.uri.c_str());
287 288 289 290 291 292

	if (r.directory->IsMount()) {
		/* pass the request and the remaining uri to the mounted database */
		protect.unlock();

		WalkMount(r.directory->GetPath(), *(r.directory->mounted_database),
293 294
			  (r.uri == nullptr)?"":r.uri, selection,
			  visit_directory, visit_song, visit_playlist);
295 296 297 298

		return;
	}

299 300
	DatabaseVisitorHelper helper(CheckSelection(selection), visit_song);

301 302 303
	if (r.uri == nullptr) {
		/* it's a directory */

304 305
		if (selection.recursive && visit_directory)
			visit_directory(r.directory->Export());
306

307 308 309
		r.directory->Walk(selection.recursive, selection.filter,
				  visit_directory, visit_song,
				  visit_playlist);
310
		helper.Commit();
311
		return;
312 313 314
	}

	if (strchr(r.uri, '/') == nullptr) {
315
		if (visit_song) {
316
			Song *song = r.directory->FindSong(r.uri);
317 318
			if (song != nullptr) {
				const LightSong song2 = song->Export();
319 320
				if (selection.Match(song2))
					visit_song(song2);
321

322
				helper.Commit();
323
				return;
324
			}
325
		}
326 327
	}

328 329
	throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
			    "No such directory");
330 331
}

332 333 334
std::map<std::string, std::set<std::string>>
SimpleDatabase::CollectUniqueTags(const DatabaseSelection &selection,
				  TagType tag_type, TagType group) const
335
{
336
	return ::CollectUniqueTags(*this, selection, tag_type, group);
337 338
}

339 340
DatabaseStats
SimpleDatabase::GetStats(const DatabaseSelection &selection) const
341
{
342
	return ::GetStats(*this, selection);
343 344
}

345 346
void
SimpleDatabase::Save()
347
{
348 349
	{
		const ScopeDatabaseLock protect;
350

351 352
		LogDebug(simple_db_domain, "removing empty directories from DB");
		root->PruneEmpty();
353

354 355 356
		LogDebug(simple_db_domain, "sorting DB");
		root->Sort();
	}
357

358
	LogDebug(simple_db_domain, "writing DB");
359

360
	FileOutputStream fos(path);
361

362 363
	OutputStream *os = &fos;

364
#ifdef ENABLE_ZLIB
365
	std::unique_ptr<GzipOutputStream> gzip;
366
	if (compress) {
367
		gzip.reset(new GzipOutputStream(*os));
368
		os = gzip.get();
369 370 371 372
	}
#endif

	BufferedOutputStream bos(*os);
373

374
	db_save_internal(bos, *root);
375

376
	bos.Flush();
377

378
#ifdef ENABLE_ZLIB
379
	if (gzip != nullptr) {
380
		gzip->Flush();
381
		gzip.reset();
382 383 384
	}
#endif

385
	fos.Commit();
386

387 388
	FileInfo fi;
	if (GetFileInfo(path, fi))
389
		mtime = fi.GetModificationTime();
390 391
}

392 393
void
SimpleDatabase::Mount(const char *uri, Database *db)
Max Kellermann's avatar
Max Kellermann committed
394
{
395 396
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
397 398
	assert(uri != nullptr);
	assert(db != nullptr);
399 400
#endif
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
401 402 403 404

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
405 406 407
	if (r.uri == nullptr)
		throw DatabaseError(DatabaseErrorCode::CONFLICT,
				    "Already exists");
Max Kellermann's avatar
Max Kellermann committed
408

409 410 411
	if (strchr(r.uri, '/') != nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "Parent not found");
Max Kellermann's avatar
Max Kellermann committed
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428

	Directory *mnt = r.directory->CreateChild(r.uri);
	mnt->mounted_database = db;
}

static constexpr bool
IsSafeChar(char ch)
{
	return IsAlphaNumericASCII(ch) || ch == '-' || ch == '_' || ch == '%';
}

static constexpr bool
IsUnsafeChar(char ch)
{
	return !IsSafeChar(ch);
}

429 430
void
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri)
Max Kellermann's avatar
Max Kellermann committed
431
{
432 433 434
	if (cache_path.IsNull())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No 'cache_directory' configured");
Max Kellermann's avatar
Max Kellermann committed
435 436 437 438

	std::string name(storage_uri);
	std::replace_if(name.begin(), name.end(), IsUnsafeChar, '_');

439
	const auto name_fs = AllocatedPath::FromUTF8Throw(name.c_str());
440

441
#ifndef ENABLE_ZLIB
442 443
	constexpr bool compress = false;
#endif
444
	auto db = new SimpleDatabase(cache_path / name_fs,
445
				     compress);
446
	try {
447
		db->Open();
448
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
449
		delete db;
450
		throw;
Max Kellermann's avatar
Max Kellermann committed
451 452 453 454
	}

	// TODO: update the new database instance?

455 456 457
	try {
		Mount(local_uri, db);
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
458 459
		db->Close();
		delete db;
460
		throw;
Max Kellermann's avatar
Max Kellermann committed
461 462 463
	}
}

464 465
inline Database *
SimpleDatabase::LockUmountSteal(const char *uri) noexcept
Max Kellermann's avatar
Max Kellermann committed
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
{
	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
	if (r.uri != nullptr || !r.directory->IsMount())
		return nullptr;

	Database *db = r.directory->mounted_database;
	r.directory->mounted_database = nullptr;
	r.directory->Delete();

	return db;
}

bool
481
SimpleDatabase::Unmount(const char *uri) noexcept
Max Kellermann's avatar
Max Kellermann committed
482 483 484 485 486 487 488 489 490 491
{
	Database *db = LockUmountSteal(uri);
	if (db == nullptr)
		return false;

	db->Close();
	delete db;
	return true;
}

492 493
const DatabasePlugin simple_db_plugin = {
	"simple",
494
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
495 496
	SimpleDatabase::Create,
};