SimpleDatabasePlugin.cxx 10.8 KB
Newer Older
1
/*
2
 * Copyright 2003-2016 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 30
#include "Directory.hxx"
#include "Song.hxx"
#include "DatabaseSave.hxx"
Max Kellermann's avatar
Max Kellermann committed
31 32
#include "db/DatabaseLock.hxx"
#include "db/DatabaseError.hxx"
33 34 35
#include "fs/io/TextFile.hxx"
#include "fs/io/BufferedOutputStream.hxx"
#include "fs/io/FileOutputStream.hxx"
36
#include "fs/FileInfo.hxx"
37
#include "config/Block.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 <memory>

50 51
#include <errno.h>

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

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

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

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

89
	return db;
90 91
}

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

103
	path_utf8 = path.ToUTF8();
104

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

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

113
	return true;
114 115
}

116 117
void
SimpleDatabase::Check() const
118
{
119
	assert(!path.IsNull());
120 121

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

		/* Check that the parent part of the path is a directory */
129
		FileInfo fi;
130

131 132 133 134
		try {
			fi = FileInfo(dirPath);
		} catch (...) {
			std::throw_with_nested(std::runtime_error("On parent directory of db file"));
135 136
		}

137 138 139 140 141
		if (!fi.IsDirectory())
			throw std::runtime_error("Couldn't create db file \"" +
						 path_utf8 + "\" because the "
						 "parent path is not a directory");

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
			throw FormatErrno(e, "Can't create db file in \"%s\"",
148
					  dirPath_utf8.c_str());
149
		}
150
#endif
151 152

		return;
153 154 155
	}

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

158 159
	if (!fi.IsRegular())
		throw std::runtime_error("db file \"" + path_utf8 + "\" is not a regular file");
160

161
#ifndef WIN32
162
	/* And check that we can write to it */
163 164
	if (!CheckAccess(path, R_OK | W_OK))
		throw FormatErrno("Can't open db file \"%s\" for reading/writing",
165
				  path_utf8.c_str());
166
#endif
167 168
}

169
bool
170
SimpleDatabase::Load(Error &error)
171
{
172
	assert(!path.IsNull());
173
	assert(root != nullptr);
174

175
	TextFile file(path);
176

177
	if (!db_load_internal(file, *root, error))
178 179
		return false;

180 181 182
	FileInfo fi;
	if (GetFileInfo(path, fi))
		mtime = fi.GetModificationTime();
183 184 185 186

	return true;
}

187 188
void
SimpleDatabase::Open()
189
{
Max Kellermann's avatar
Max Kellermann committed
190 191
	assert(prefixed_light_song == nullptr);

192
	root = Directory::NewRoot();
193
	mtime = 0;
194

195 196 197 198
#ifndef NDEBUG
	borrowed_song_count = 0;
#endif

199 200 201 202 203 204 205
	try {
		Error error2;
		if (!Load(error2)) {
			LogError(error2);

			delete root;

206
			Check();
207 208 209 210 211

			root = Directory::NewRoot();
		}
	} catch (const std::exception &e) {
		LogError(e);
212

213
		delete root;
214

215
		Check();
216

217
		root = Directory::NewRoot();
218 219 220
	}
}

221 222
void
SimpleDatabase::Close()
223
{
224
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
225
	assert(prefixed_light_song == nullptr);
226
	assert(borrowed_song_count == 0);
227

228
	delete root;
229 230
}

231
const LightSong *
232
SimpleDatabase::GetSong(const char *uri) const
233
{
234
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
235
	assert(prefixed_light_song == nullptr);
236
	assert(borrowed_song_count == 0);
237

238
	ScopeDatabaseLock protect;
239 240

	auto r = root->LookupDirectory(uri);
Max Kellermann's avatar
Max Kellermann committed
241 242 243

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

		const LightSong *song =
247
			r.directory->mounted_database->GetSong(r.uri);
Max Kellermann's avatar
Max Kellermann committed
248 249 250 251 252 253 254 255
		if (song == nullptr)
			return nullptr;

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

256
	if (r.uri == nullptr)
257
		/* it's a directory */
258 259
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
260

261
	if (strchr(r.uri, '/') != nullptr)
262
		/* refers to a URI "below" the actual song */
263 264
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
265 266

	const Song *song = r.directory->FindSong(r.uri);
267
	protect.unlock();
268 269 270
	if (song == nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
271 272 273

	light_song = song->Export();

274
#ifndef NDEBUG
275
	++borrowed_song_count;
276
#endif
277

278
	return &light_song;
279 280
}

281
void
282
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
283
{
Max Kellermann's avatar
Max Kellermann committed
284 285 286 287 288
	assert(song != nullptr);
	assert(song == &light_song || song == prefixed_light_song);

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
289 290

#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
291 292 293 294
	if (song == &light_song) {
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
	}
295 296 297
#endif
}

298
bool
299
SimpleDatabase::Visit(const DatabaseSelection &selection,
300 301 302
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
		      VisitPlaylist visit_playlist,
303
		      Error &error) const
304
{
305 306
	ScopeDatabaseLock protect;

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
	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) {
322
		if (visit_song) {
323
			Song *song = r.directory->FindSong(r.uri);
324 325 326 327 328
			if (song != nullptr) {
				const LightSong song2 = song->Export();
				return !selection.Match(song2) ||
					visit_song(song2, error);
			}
329
		}
330 331
	}

332 333
	throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
			    "No such directory");
334 335
}

336 337
bool
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
338
				TagType tag_type, tag_mask_t group_mask,
339
				VisitTag visit_tag,
340
				Error &error) const
341
{
342 343
	return ::VisitUniqueTags(*this, selection, tag_type, group_mask,
				 visit_tag,
344
				 error);
345 346
}

347 348
bool
SimpleDatabase::GetStats(const DatabaseSelection &selection,
349
			 DatabaseStats &stats, Error &error) const
350
{
351
	return ::GetStats(*this, selection, stats, error);
352 353
}

354 355
void
SimpleDatabase::Save()
356
{
357 358
	{
		const ScopeDatabaseLock protect;
359

360 361
		LogDebug(simple_db_domain, "removing empty directories from DB");
		root->PruneEmpty();
362

363 364 365
		LogDebug(simple_db_domain, "sorting DB");
		root->Sort();
	}
366

367
	LogDebug(simple_db_domain, "writing DB");
368

369
	FileOutputStream fos(path);
370

371 372
	OutputStream *os = &fos;

373
#ifdef ENABLE_ZLIB
374
	std::unique_ptr<GzipOutputStream> gzip;
375
	if (compress) {
376
		gzip.reset(new GzipOutputStream(*os));
377
		os = gzip.get();
378 379 380 381
	}
#endif

	BufferedOutputStream bos(*os);
382

383
	db_save_internal(bos, *root);
384

385
	bos.Flush();
386

387
#ifdef ENABLE_ZLIB
388
	if (gzip != nullptr) {
389
		gzip->Flush();
390
		gzip.reset();
391 392 393
	}
#endif

394
	fos.Commit();
395

396 397 398
	FileInfo fi;
	if (GetFileInfo(path, fi))
		mtime = fi.GetModificationTime();
399 400
}

401 402
void
SimpleDatabase::Mount(const char *uri, Database *db)
Max Kellermann's avatar
Max Kellermann committed
403
{
404 405
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
406 407
	assert(uri != nullptr);
	assert(db != nullptr);
408 409
#endif
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
410 411 412 413

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
414 415 416
	if (r.uri == nullptr)
		throw DatabaseError(DatabaseErrorCode::CONFLICT,
				    "Already exists");
Max Kellermann's avatar
Max Kellermann committed
417

418 419 420
	if (strchr(r.uri, '/') != nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "Parent not found");
Max Kellermann's avatar
Max Kellermann committed
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441

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

bool
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri,
		      Error &error)
{
442 443 444
	if (cache_path.IsNull())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No 'cache_directory' configured");
Max Kellermann's avatar
Max Kellermann committed
445 446 447 448

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

449 450 451 452
	const auto name_fs = AllocatedPath::FromUTF8(name.c_str(), error);
	if (name_fs.IsNull())
		return false;

453
#ifndef ENABLE_ZLIB
454 455
	constexpr bool compress = false;
#endif
Max Kellermann's avatar
Max Kellermann committed
456
	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
457
							  name_fs.c_str()),
458
				     compress);
459
	try {
460
		db->Open();
461
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
462
		delete db;
463
		throw;
Max Kellermann's avatar
Max Kellermann committed
464 465 466 467
	}

	// TODO: update the new database instance?

468 469 470
	try {
		Mount(local_uri, db);
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
471 472
		db->Close();
		delete db;
473
		throw;
Max Kellermann's avatar
Max Kellermann committed
474 475 476 477 478 479 480 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
	}

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

507 508
const DatabasePlugin simple_db_plugin = {
	"simple",
509
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
510 511
	SimpleDatabase::Create,
};