SimpleDatabasePlugin.cxx 11.3 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2015 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 "db/DatabasePlugin.hxx"
Max Kellermann's avatar
Max Kellermann committed
24 25
#include "db/Selection.hxx"
#include "db/Helpers.hxx"
26
#include "db/UniqueTags.hxx"
Max Kellermann's avatar
Max Kellermann committed
27
#include "db/LightDirectory.hxx"
28 29
#include "Directory.hxx"
#include "Song.hxx"
30
#include "SongFilter.hxx"
31
#include "DatabaseSave.hxx"
Max Kellermann's avatar
Max Kellermann committed
32 33
#include "db/DatabaseLock.hxx"
#include "db/DatabaseError.hxx"
34 35 36
#include "fs/io/TextFile.hxx"
#include "fs/io/BufferedOutputStream.hxx"
#include "fs/io/FileOutputStream.hxx"
37
#include "fs/FileInfo.hxx"
38
#include "config/Block.hxx"
39
#include "fs/FileSystem.hxx"
Max Kellermann's avatar
Max Kellermann committed
40
#include "util/CharUtil.hxx"
41 42
#include "util/Error.hxx"
#include "util/Domain.hxx"
43
#include "Log.hxx"
44

45
#ifdef ENABLE_ZLIB
46 47 48
#include "fs/io/GzipOutputStream.hxx"
#endif

49 50
#include <errno.h>

51
static constexpr Domain simple_db_domain("simple_db");
52

53 54
inline SimpleDatabase::SimpleDatabase()
	:Database(simple_db_plugin),
Max Kellermann's avatar
Max Kellermann committed
55
	 path(AllocatedPath::Null()),
56
#ifdef ENABLE_ZLIB
57 58
	 compress(true),
#endif
Max Kellermann's avatar
Max Kellermann committed
59 60 61
	 cache_path(AllocatedPath::Null()),
	 prefixed_light_song(nullptr) {}

62
inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path,
63
#ifndef ENABLE_ZLIB
64 65 66
				      gcc_unused
#endif
				      bool _compress)
Max Kellermann's avatar
Max Kellermann committed
67 68 69
	:Database(simple_db_plugin),
	 path(std::move(_path)),
	 path_utf8(path.ToUTF8()),
70
#ifdef ENABLE_ZLIB
71 72
	 compress(_compress),
#endif
Max Kellermann's avatar
Max Kellermann committed
73 74 75
	 cache_path(AllocatedPath::Null()),
	 prefixed_light_song(nullptr) {
}
76

77
Database *
78 79
SimpleDatabase::Create(gcc_unused EventLoop &loop,
		       gcc_unused DatabaseListener &listener,
80
		       const ConfigBlock &block, Error &error)
81
{
82
	SimpleDatabase *db = new SimpleDatabase();
83
	if (!db->Configure(block, error)) {
84
		delete db;
85
		db = nullptr;
86
	}
87

88
	return db;
89 90
}

91
bool
92
SimpleDatabase::Configure(const ConfigBlock &block, Error &error)
93
{
94
	path = block.GetBlockPath("path", error);
95
	if (path.IsNull()) {
96 97 98
		if (!error.IsDefined())
			error.Set(simple_db_domain,
				  "No \"path\" parameter specified");
99
		return false;
100 101
	}

102
	path_utf8 = path.ToUTF8();
103

104
	cache_path = block.GetBlockPath("cache_directory", error);
Max Kellermann's avatar
Max Kellermann committed
105 106 107
	if (path.IsNull() && error.IsDefined())
		return false;

108
#ifdef ENABLE_ZLIB
109
	compress = block.GetBlockValue("compress", compress);
110 111
#endif

112
	return true;
113 114
}

115
bool
116
SimpleDatabase::Check(Error &error) const
117
{
118
	assert(!path.IsNull());
119 120

	/* Check if the file exists */
121
	if (!PathExists(path)) {
122 123 124
		/* 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 */
125
		const auto dirPath = path.GetDirectoryName();
126 127

		/* Check that the parent part of the path is a directory */
128 129 130
		FileInfo fi;
		if (!GetFileInfo(dirPath, fi, error)) {
			error.AddPrefix("On parent directory of db file: ");
131 132 133
			return false;
		}

134
		if (!fi.IsDirectory()) {
135 136 137 138
			error.Format(simple_db_domain,
				     "Couldn't create db file \"%s\" because the "
				     "parent path is not a directory",
				     path_utf8.c_str());
139 140 141
			return false;
		}

142
#ifndef WIN32
143
		/* Check if we can write to the directory */
144
		if (!CheckAccess(dirPath, X_OK | W_OK)) {
145
			const int e = errno;
146
			const std::string dirPath_utf8 = dirPath.ToUTF8();
147 148
			error.FormatErrno(e, "Can't create db file in \"%s\"",
					  dirPath_utf8.c_str());
149 150
			return false;
		}
151
#endif
152 153 154 155
		return true;
	}

	/* Path exists, now check if it's a regular file */
156 157
	FileInfo fi;
	if (!GetFileInfo(path, fi, error))
158 159
		return false;

160
	if (!fi.IsRegular()) {
161 162 163
		error.Format(simple_db_domain,
			     "db file \"%s\" is not a regular file",
			     path_utf8.c_str());
164 165 166
		return false;
	}

167
#ifndef WIN32
168
	/* And check that we can write to it */
169
	if (!CheckAccess(path, R_OK | W_OK)) {
170 171
		error.FormatErrno("Can't open db file \"%s\" for reading/writing",
				  path_utf8.c_str());
172 173
		return false;
	}
174
#endif
175 176 177 178

	return true;
}

179
bool
180
SimpleDatabase::Load(Error &error)
181
{
182
	assert(!path.IsNull());
183
	assert(root != nullptr);
184

185 186
	TextFile file(path, error);
	if (file.HasFailed())
187 188
		return false;

189
	if (!db_load_internal(file, *root, error) || !file.Check(error))
190 191
		return false;

192 193 194
	FileInfo fi;
	if (GetFileInfo(path, fi))
		mtime = fi.GetModificationTime();
195 196 197 198

	return true;
}

199
bool
200
SimpleDatabase::Open(Error &error)
201
{
Max Kellermann's avatar
Max Kellermann committed
202 203
	assert(prefixed_light_song == nullptr);

204
	root = Directory::NewRoot();
205
	mtime = 0;
206

207 208 209 210
#ifndef NDEBUG
	borrowed_song_count = 0;
#endif

211
	if (!Load(error)) {
212
		delete root;
213

214
		LogError(error);
215
		error.Clear();
216

217
		if (!Check(error))
218 219
			return false;

220
		root = Directory::NewRoot();
221 222 223 224 225
	}

	return true;
}

226 227
void
SimpleDatabase::Close()
228
{
229
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
230
	assert(prefixed_light_song == nullptr);
231
	assert(borrowed_song_count == 0);
232

233
	delete root;
234 235
}

236
const LightSong *
237
SimpleDatabase::GetSong(const char *uri, Error &error) const
238
{
239
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
240
	assert(prefixed_light_song == nullptr);
241
	assert(borrowed_song_count == 0);
242

243
	db_lock();
244 245

	auto r = root->LookupDirectory(uri);
Max Kellermann's avatar
Max Kellermann committed
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260

	if (r.directory->IsMount()) {
		/* pass the request to the mounted database */
		db_unlock();

		const LightSong *song =
			r.directory->mounted_database->GetSong(r.uri, error);
		if (song == nullptr)
			return nullptr;

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

261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
	if (r.uri == nullptr) {
		/* it's a directory */
		db_unlock();
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
		return nullptr;
	}

	if (strchr(r.uri, '/') != nullptr) {
		/* refers to a URI "below" the actual song */
		db_unlock();
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
		return nullptr;
	}

	const Song *song = r.directory->FindSong(r.uri);
278
	db_unlock();
279
	if (song == nullptr) {
280 281
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
282 283 284 285 286
		return nullptr;
	}

	light_song = song->Export();

287
#ifndef NDEBUG
288
	++borrowed_song_count;
289
#endif
290

291
	return &light_song;
292 293
}

294
void
295
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
296
{
Max Kellermann's avatar
Max Kellermann committed
297 298 299 300 301
	assert(song != nullptr);
	assert(song == &light_song || song == prefixed_light_song);

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
302 303

#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
304 305 306 307
	if (song == &light_song) {
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
	}
308 309 310
#endif
}

311
bool
312
SimpleDatabase::Visit(const DatabaseSelection &selection,
313 314 315
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
		      VisitPlaylist visit_playlist,
316
		      Error &error) const
317
{
318 319
	ScopeDatabaseLock protect;

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
	auto r = root->LookupDirectory(selection.uri.c_str());
	if (r.uri == nullptr) {
		/* it's a directory */

		if (selection.recursive && visit_directory &&
		    !visit_directory(r.directory->Export(), error))
			return false;

		return r.directory->Walk(selection.recursive, selection.filter,
					 visit_directory, visit_song,
					 visit_playlist,
					 error);
	}

	if (strchr(r.uri, '/') == nullptr) {
335
		if (visit_song) {
336
			Song *song = r.directory->FindSong(r.uri);
337 338 339 340 341
			if (song != nullptr) {
				const LightSong song2 = song->Export();
				return !selection.Match(song2) ||
					visit_song(song2, error);
			}
342
		}
343 344
	}

345 346
	error.Set(db_domain, DB_NOT_FOUND, "No such directory");
	return false;
347 348
}

349 350
bool
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
351 352
				TagType tag_type, uint32_t group_mask,
				VisitTag visit_tag,
353
				Error &error) const
354
{
355 356
	return ::VisitUniqueTags(*this, selection, tag_type, group_mask,
				 visit_tag,
357
				 error);
358 359
}

360 361
bool
SimpleDatabase::GetStats(const DatabaseSelection &selection,
362
			 DatabaseStats &stats, Error &error) const
363
{
364
	return ::GetStats(*this, selection, stats, error);
365 366
}

367
bool
368
SimpleDatabase::Save(Error &error)
369
{
370 371
	db_lock();

372
	LogDebug(simple_db_domain, "removing empty directories from DB");
373
	root->PruneEmpty();
374

375
	LogDebug(simple_db_domain, "sorting DB");
376
	root->Sort();
377

378 379
	db_unlock();

380
	LogDebug(simple_db_domain, "writing DB");
381

382 383
	FileOutputStream fos(path, error);
	if (!fos.IsDefined())
384 385
		return false;

386 387
	OutputStream *os = &fos;

388
#ifdef ENABLE_ZLIB
389 390 391 392 393 394 395 396 397 398 399 400 401
	GzipOutputStream *gzip = nullptr;
	if (compress) {
		gzip = new GzipOutputStream(*os, error);
		if (!gzip->IsDefined()) {
			delete gzip;
			return false;
		}

		os = gzip;
	}
#endif

	BufferedOutputStream bos(*os);
402

403
	db_save_internal(bos, *root);
404

405
	if (!bos.Flush(error)) {
406
#ifdef ENABLE_ZLIB
407 408 409 410 411
		delete gzip;
#endif
		return false;
	}

412
#ifdef ENABLE_ZLIB
413 414 415 416 417 418 419 420 421
	if (gzip != nullptr) {
		bool success = gzip->Flush(error);
		delete gzip;
		if (!success)
			return false;
	}
#endif

	if (!fos.Commit(error))
422
		return false;
423

424 425 426
	FileInfo fi;
	if (GetFileInfo(path, fi))
		mtime = fi.GetModificationTime();
427 428 429 430

	return true;
}

Max Kellermann's avatar
Max Kellermann committed
431 432 433
bool
SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
{
434 435
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
436 437
	assert(uri != nullptr);
	assert(db != nullptr);
438 439
#endif
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
440 441 442 443 444 445 446

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
	if (r.uri == nullptr) {
		error.Format(db_domain, DB_CONFLICT,
			     "Already exists: %s", uri);
447
		return false;
Max Kellermann's avatar
Max Kellermann committed
448 449 450 451 452
	}

	if (strchr(r.uri, '/') != nullptr) {
		error.Format(db_domain, DB_NOT_FOUND,
			     "Parent not found: %s", uri);
453
		return false;
Max Kellermann's avatar
Max Kellermann committed
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
	}

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

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

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

bool
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri,
		      Error &error)
{
	if (cache_path.IsNull()) {
		error.Format(db_domain, DB_NOT_FOUND,
			     "No 'cache_directory' configured");
480
		return false;
Max Kellermann's avatar
Max Kellermann committed
481 482 483 484 485
	}

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

486 487 488 489
	const auto name_fs = AllocatedPath::FromUTF8(name.c_str(), error);
	if (name_fs.IsNull())
		return false;

490
#ifndef ENABLE_ZLIB
491 492
	constexpr bool compress = false;
#endif
Max Kellermann's avatar
Max Kellermann committed
493
	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
494
							  name_fs.c_str()),
495
				     compress);
Max Kellermann's avatar
Max Kellermann committed
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
	if (!db->Open(error)) {
		delete db;
		return false;
	}

	// TODO: update the new database instance?

	if (!Mount(local_uri, db, error)) {
		db->Close();
		delete db;
		return false;
	}

	return true;
}

Database *
SimpleDatabase::LockUmountSteal(const char *uri)
{
	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
SimpleDatabase::Unmount(const char *uri)
{
	Database *db = LockUmountSteal(uri);
	if (db == nullptr)
		return false;

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

540 541
const DatabasePlugin simple_db_plugin = {
	"simple",
542
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
543 544
	SimpleDatabase::Create,
};