SimpleDatabasePlugin.cxx 10.5 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
	light_song.Construct(song->Export());
241

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

246
	return &light_song.Get();
247 248
}

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

255 256 257 258
	if (prefixed_light_song != nullptr) {
		delete prefixed_light_song;
		prefixed_light_song = nullptr;
	} else {
259
#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
260 261
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
262
#endif
263 264 265

		light_song.Destruct();
	}
266 267
}

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

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

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

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

292 293
		if (selection.recursive && visit_directory)
			visit_directory(r.directory->Export());
294

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

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

				return;
310
			}
311
		}
312 313
	}

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

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

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

332 333
void
SimpleDatabase::Save()
334
{
335 336
	{
		const ScopeDatabaseLock protect;
337

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

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

345
	LogDebug(simple_db_domain, "writing DB");
346

347
	FileOutputStream fos(path);
348

349 350
	OutputStream *os = &fos;

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

	BufferedOutputStream bos(*os);
360

361
	db_save_internal(bos, *root);
362

363
	bos.Flush();
364

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

372
	fos.Commit();
373

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

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

	ScopeDatabaseLock protect;

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

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

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

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

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

426
	const auto name_fs = AllocatedPath::FromUTF8Throw(name.c_str());
427

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

	// TODO: update the new database instance?

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

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

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