SimpleDatabasePlugin.cxx 10.4 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 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"
Max Kellermann's avatar
Max Kellermann committed
29
#include "db/LightDirectory.hxx"
30 31 32
#include "Directory.hxx"
#include "Song.hxx"
#include "DatabaseSave.hxx"
Max Kellermann's avatar
Max Kellermann committed
33 34
#include "db/DatabaseLock.hxx"
#include "db/DatabaseError.hxx"
35
#include "tag/Mask.hxx"
36 37 38
#include "fs/io/TextFile.hxx"
#include "fs/io/BufferedOutputStream.hxx"
#include "fs/io/FileOutputStream.hxx"
39
#include "fs/FileInfo.hxx"
40
#include "config/Block.hxx"
41
#include "fs/FileSystem.hxx"
Max Kellermann's avatar
Max Kellermann committed
42
#include "util/CharUtil.hxx"
43
#include "util/Domain.hxx"
44
#include "Log.hxx"
45

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

50 51
#include <memory>

52 53
#include <errno.h>

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

56
inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
57
	:Database(simple_db_plugin),
58
	 path(block.GetPath("path")),
59
#ifdef ENABLE_ZLIB
60
	 compress(block.GetBlockValue("compress", true)),
61
#endif
62 63 64 65 66 67 68 69
	 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
70

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

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

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

	/* Check if the file exists */
100
	if (!PathExists(path)) {
101 102 103
		/* 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 */
104
		const auto dirPath = path.GetDirectoryName();
105 106

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

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

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

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

		return;
131 132 133
	}

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

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

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

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

153
	TextFile file(path);
154

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

157
	db_load_internal(file, *root);
158

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

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

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

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

176
	try {
177
		Load();
178 179
	} catch (const std::exception &e) {
		LogError(e);
180

181
		delete root;
182

183
		Check();
184

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

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

196
	delete root;
197 198
}

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

206
	ScopeDatabaseLock protect;
207 208

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

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

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

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

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

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

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

	light_song = song->Export();

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

246
	return &light_song;
247 248
}

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

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
257 258

#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
259 260 261 262
	if (song == &light_song) {
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
	}
263 264 265
#endif
}

266
void
267
SimpleDatabase::Visit(const DatabaseSelection &selection,
268 269
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
270
		      VisitPlaylist visit_playlist) const
271
{
272 273
	ScopeDatabaseLock protect;

274
	auto r = root->LookupDirectory(selection.uri.c_str());
275 276 277 278 279 280 281 282 283 284 285 286

	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),
			(r.uri == nullptr)?"":r.uri, selection.recursive, selection.filter,
			visit_directory, visit_song, visit_playlist);

		return;
	}

287 288 289
	if (r.uri == nullptr) {
		/* it's a directory */

290 291
		if (selection.recursive && visit_directory)
			visit_directory(r.directory->Export());
292

293 294 295 296
		r.directory->Walk(selection.recursive, selection.filter,
				  visit_directory, visit_song,
				  visit_playlist);
		return;
297 298 299
	}

	if (strchr(r.uri, '/') == nullptr) {
300
		if (visit_song) {
301
			Song *song = r.directory->FindSong(r.uri);
302 303
			if (song != nullptr) {
				const LightSong song2 = song->Export();
304 305
				if (selection.Match(song2))
					visit_song(song2);
306 307

				return;
308
			}
309
		}
310 311
	}

312 313
	throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
			    "No such directory");
314 315
}

316
void
317
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
318
				TagType tag_type, TagMask group_mask,
319
				VisitTag visit_tag) const
320
{
321
	::VisitUniqueTags(*this, selection, tag_type, group_mask, visit_tag);
322 323
}

324 325
DatabaseStats
SimpleDatabase::GetStats(const DatabaseSelection &selection) const
326
{
327
	return ::GetStats(*this, selection);
328 329
}

330 331
void
SimpleDatabase::Save()
332
{
333 334
	{
		const ScopeDatabaseLock protect;
335

336 337
		LogDebug(simple_db_domain, "removing empty directories from DB");
		root->PruneEmpty();
338

339 340 341
		LogDebug(simple_db_domain, "sorting DB");
		root->Sort();
	}
342

343
	LogDebug(simple_db_domain, "writing DB");
344

345
	FileOutputStream fos(path);
346

347 348
	OutputStream *os = &fos;

349
#ifdef ENABLE_ZLIB
350
	std::unique_ptr<GzipOutputStream> gzip;
351
	if (compress) {
352
		gzip.reset(new GzipOutputStream(*os));
353
		os = gzip.get();
354 355 356 357
	}
#endif

	BufferedOutputStream bos(*os);
358

359
	db_save_internal(bos, *root);
360

361
	bos.Flush();
362

363
#ifdef ENABLE_ZLIB
364
	if (gzip != nullptr) {
365
		gzip->Flush();
366
		gzip.reset();
367 368 369
	}
#endif

370
	fos.Commit();
371

372 373
	FileInfo fi;
	if (GetFileInfo(path, fi))
374
		mtime = fi.GetModificationTime();
375 376
}

377 378
void
SimpleDatabase::Mount(const char *uri, Database *db)
Max Kellermann's avatar
Max Kellermann committed
379
{
380 381
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
382 383
	assert(uri != nullptr);
	assert(db != nullptr);
384 385
#endif
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
386 387 388 389

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
390 391 392
	if (r.uri == nullptr)
		throw DatabaseError(DatabaseErrorCode::CONFLICT,
				    "Already exists");
Max Kellermann's avatar
Max Kellermann committed
393

394 395 396
	if (strchr(r.uri, '/') != nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "Parent not found");
Max Kellermann's avatar
Max Kellermann committed
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413

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

414 415
void
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri)
Max Kellermann's avatar
Max Kellermann committed
416
{
417 418 419
	if (cache_path.IsNull())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No 'cache_directory' configured");
Max Kellermann's avatar
Max Kellermann committed
420 421 422 423

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

424
	const auto name_fs = AllocatedPath::FromUTF8Throw(name.c_str());
425

426
#ifndef ENABLE_ZLIB
427 428
	constexpr bool compress = false;
#endif
Max Kellermann's avatar
Max Kellermann committed
429
	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
430
							  name_fs.c_str()),
431
				     compress);
432
	try {
433
		db->Open();
434
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
435
		delete db;
436
		throw;
Max Kellermann's avatar
Max Kellermann committed
437 438 439 440
	}

	// TODO: update the new database instance?

441 442 443
	try {
		Mount(local_uri, db);
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
444 445
		db->Close();
		delete db;
446
		throw;
Max Kellermann's avatar
Max Kellermann committed
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
	}
}

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

478 479
const DatabasePlugin simple_db_plugin = {
	"simple",
480
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
481 482
	SimpleDatabase::Create,
};