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 <memory>

51 52
#include <errno.h>

53
static constexpr Domain simple_db_domain("simple_db");
54

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

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

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

90
	return db;
91 92
}

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

104
	path_utf8 = path.ToUTF8();
105

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

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

114
	return true;
115 116
}

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

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

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

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

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

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

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

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

	return true;
}

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

187
	TextFile file(path);
188

189
	if (!db_load_internal(file, *root, 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
try {
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
	}

	return true;
224 225 226
} catch (const std::exception &e) {
	error.Set(e);
	return false;
227 228
}

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

236
	delete root;
237 238
}

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

246
	ScopeDatabaseLock protect;
247 248

	auto r = root->LookupDirectory(uri);
Max Kellermann's avatar
Max Kellermann committed
249 250 251

	if (r.directory->IsMount()) {
		/* pass the request to the mounted database */
252
		protect.unlock();
Max Kellermann's avatar
Max Kellermann committed
253 254 255 256 257 258 259 260 261 262 263

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

264 265
	if (r.uri == nullptr) {
		/* it's a directory */
266
		error.Format(db_domain, (int)DatabaseErrorCode::NOT_FOUND,
267 268 269 270 271 272
			     "No such song: %s", uri);
		return nullptr;
	}

	if (strchr(r.uri, '/') != nullptr) {
		/* refers to a URI "below" the actual song */
273
		error.Format(db_domain, (int)DatabaseErrorCode::NOT_FOUND,
274 275 276 277 278
			     "No such song: %s", uri);
		return nullptr;
	}

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

	light_song = song->Export();

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

292
	return &light_song;
293 294
}

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

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
303 304

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

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

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

346 347
	error.Set(db_domain, (int)DatabaseErrorCode::NOT_FOUND,
		  "No such directory");
348
	return false;
349 350
}

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

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

369 370
void
SimpleDatabase::Save()
371
{
372 373
	{
		const ScopeDatabaseLock protect;
374

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

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

382
	LogDebug(simple_db_domain, "writing DB");
383

384
	FileOutputStream fos(path);
385

386 387
	OutputStream *os = &fos;

388
#ifdef ENABLE_ZLIB
389
	std::unique_ptr<GzipOutputStream> gzip;
390
	if (compress) {
391
		gzip.reset(new GzipOutputStream(*os));
392
		os = gzip.get();
393 394 395 396
	}
#endif

	BufferedOutputStream bos(*os);
397

398
	db_save_internal(bos, *root);
399

400
	bos.Flush();
401

402
#ifdef ENABLE_ZLIB
403
	if (gzip != nullptr) {
404
		gzip->Flush();
405
		gzip.reset();
406 407 408
	}
#endif

409
	fos.Commit();
410

411 412 413
	FileInfo fi;
	if (GetFileInfo(path, fi))
		mtime = fi.GetModificationTime();
414 415
}

Max Kellermann's avatar
Max Kellermann committed
416 417 418
bool
SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
{
419 420
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
421 422
	assert(uri != nullptr);
	assert(db != nullptr);
423 424
#endif
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
425 426 427 428 429

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
	if (r.uri == nullptr) {
430
		error.Format(db_domain, (int)DatabaseErrorCode::CONFLICT,
Max Kellermann's avatar
Max Kellermann committed
431
			     "Already exists: %s", uri);
432
		return false;
Max Kellermann's avatar
Max Kellermann committed
433 434 435
	}

	if (strchr(r.uri, '/') != nullptr) {
436
		error.Format(db_domain, (int)DatabaseErrorCode::NOT_FOUND,
Max Kellermann's avatar
Max Kellermann committed
437
			     "Parent not found: %s", uri);
438
		return false;
Max Kellermann's avatar
Max Kellermann committed
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
	}

	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()) {
463
		error.Format(db_domain, (int)DatabaseErrorCode::NOT_FOUND,
Max Kellermann's avatar
Max Kellermann committed
464
			     "No 'cache_directory' configured");
465
		return false;
Max Kellermann's avatar
Max Kellermann committed
466 467 468 469 470
	}

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

471 472 473 474
	const auto name_fs = AllocatedPath::FromUTF8(name.c_str(), error);
	if (name_fs.IsNull())
		return false;

475
#ifndef ENABLE_ZLIB
476 477
	constexpr bool compress = false;
#endif
Max Kellermann's avatar
Max Kellermann committed
478
	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
479
							  name_fs.c_str()),
480
				     compress);
Max Kellermann's avatar
Max Kellermann committed
481 482 483 484 485 486 487 488 489 490 491 492 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
	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;
}

525 526
const DatabasePlugin simple_db_plugin = {
	"simple",
527
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
528 529
	SimpleDatabase::Create,
};