SimpleDatabasePlugin.cxx 11.2 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2014 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 "config/ConfigData.hxx"
38
#include "fs/FileSystem.hxx"
Max Kellermann's avatar
Max Kellermann committed
39
#include "util/CharUtil.hxx"
40 41
#include "util/Error.hxx"
#include "util/Domain.hxx"
42
#include "Log.hxx"
43

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

48 49
#include <errno.h>

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

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

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

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

87
	return db;
88 89
}

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

101
	path_utf8 = path.ToUTF8();
102

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

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

111
	return true;
112 113
}

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

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

		/* Check that the parent part of the path is a directory */
		struct stat st;
128
		if (!StatFile(dirPath, st)) {
129 130 131
			error.FormatErrno("Couldn't stat parent directory of db file "
					  "\"%s\"",
					  path_utf8.c_str());
132 133 134 135
			return false;
		}

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

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

	/* Path exists, now check if it's a regular file */
	struct stat st;
158
	if (!StatFile(path, st)) {
159 160
		error.FormatErrno("Couldn't stat db file \"%s\"",
				  path_utf8.c_str());
161 162 163 164
		return false;
	}

	if (!S_ISREG(st.st_mode)) {
165 166 167
		error.Format(simple_db_domain,
			     "db file \"%s\" is not a regular file",
			     path_utf8.c_str());
168 169 170
		return false;
	}

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

	return true;
}

183
bool
184
SimpleDatabase::Load(Error &error)
185
{
186
	assert(!path.IsNull());
187
	assert(root != nullptr);
188

189 190
	TextFile file(path, error);
	if (file.HasFailed())
191 192
		return false;

193
	if (!db_load_internal(file, *root, error) || !file.Check(error))
194 195 196
		return false;

	struct stat st;
197
	if (StatFile(path, st))
198
		mtime = st.st_mtime;
199 200 201 202

	return true;
}

203
bool
204
SimpleDatabase::Open(Error &error)
205
{
Max Kellermann's avatar
Max Kellermann committed
206 207
	assert(prefixed_light_song == nullptr);

208
	root = Directory::NewRoot();
209
	mtime = 0;
210

211 212 213 214
#ifndef NDEBUG
	borrowed_song_count = 0;
#endif

215
	if (!Load(error)) {
216
		delete root;
217

218
		LogError(error);
219
		error.Clear();
220

221
		if (!Check(error))
222 223
			return false;

224
		root = Directory::NewRoot();
225 226 227 228 229
	}

	return true;
}

230 231
void
SimpleDatabase::Close()
232
{
233
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
234
	assert(prefixed_light_song == nullptr);
235
	assert(borrowed_song_count == 0);
236

237
	delete root;
238 239
}

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

247
	db_lock();
248 249

	auto r = root->LookupDirectory(uri);
Max Kellermann's avatar
Max Kellermann committed
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264

	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;
	}

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
	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);
282
	db_unlock();
283
	if (song == nullptr) {
284 285
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
286 287 288 289 290
		return nullptr;
	}

	light_song = song->Export();

291
#ifndef NDEBUG
292
	++borrowed_song_count;
293
#endif
294

295
	return &light_song;
296 297
}

298
void
299
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
300
{
Max Kellermann's avatar
Max Kellermann committed
301 302 303 304 305
	assert(song != nullptr);
	assert(song == &light_song || song == prefixed_light_song);

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
306 307

#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
308 309 310 311
	if (song == &light_song) {
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
	}
312 313 314
#endif
}

315
bool
316
SimpleDatabase::Visit(const DatabaseSelection &selection,
317 318 319
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
		      VisitPlaylist visit_playlist,
320
		      Error &error) const
321
{
322 323
	ScopeDatabaseLock protect;

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
	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) {
339
		if (visit_song) {
340
			Song *song = r.directory->FindSong(r.uri);
341 342 343 344 345
			if (song != nullptr) {
				const LightSong song2 = song->Export();
				return !selection.Match(song2) ||
					visit_song(song2, error);
			}
346
		}
347 348
	}

349 350
	error.Set(db_domain, DB_NOT_FOUND, "No such directory");
	return false;
351 352
}

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

364 365
bool
SimpleDatabase::GetStats(const DatabaseSelection &selection,
366
			 DatabaseStats &stats, Error &error) const
367
{
368
	return ::GetStats(*this, selection, stats, error);
369 370
}

371
bool
372
SimpleDatabase::Save(Error &error)
373
{
374 375
	db_lock();

376
	LogDebug(simple_db_domain, "removing empty directories from DB");
377
	root->PruneEmpty();
378

379
	LogDebug(simple_db_domain, "sorting DB");
380
	root->Sort();
381

382 383
	db_unlock();

384
	LogDebug(simple_db_domain, "writing DB");
385

386 387
	FileOutputStream fos(path, error);
	if (!fos.IsDefined())
388 389
		return false;

390 391
	OutputStream *os = &fos;

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

		os = gzip;
	}
#endif

	BufferedOutputStream bos(*os);
406

407
	db_save_internal(bos, *root);
408

409
	if (!bos.Flush(error)) {
410
#ifdef ENABLE_ZLIB
411 412 413 414 415
		delete gzip;
#endif
		return false;
	}

416
#ifdef ENABLE_ZLIB
417 418 419 420 421 422 423 424 425
	if (gzip != nullptr) {
		bool success = gzip->Flush(error);
		delete gzip;
		if (!success)
			return false;
	}
#endif

	if (!fos.Commit(error))
426
		return false;
427 428

	struct stat st;
429
	if (StatFile(path, st))
430
		mtime = st.st_mtime;
431 432 433 434

	return true;
}

Max Kellermann's avatar
Max Kellermann committed
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 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 480 481 482 483 484 485 486
bool
SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
{
	assert(uri != nullptr);
	assert(*uri != 0);
	assert(db != nullptr);

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
	if (r.uri == nullptr) {
		error.Format(db_domain, DB_CONFLICT,
			     "Already exists: %s", uri);
		return nullptr;
	}

	if (strchr(r.uri, '/') != nullptr) {
		error.Format(db_domain, DB_NOT_FOUND,
			     "Parent not found: %s", uri);
		return nullptr;
	}

	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");
		return nullptr;
	}

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

487
#ifndef ENABLE_ZLIB
488 489
	constexpr bool compress = false;
#endif
Max Kellermann's avatar
Max Kellermann committed
490
	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
491 492
							  name.c_str()),
				     compress);
Max Kellermann's avatar
Max Kellermann committed
493 494 495 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
	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;
}

537 538
const DatabasePlugin simple_db_plugin = {
	"simple",
539
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
540 541
	SimpleDatabase::Create,
};