SimpleDatabasePlugin.cxx 10 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/Stats.hxx"
27
#include "db/UniqueTags.hxx"
Max Kellermann's avatar
Max Kellermann committed
28
#include "db/LightDirectory.hxx"
29 30 31
#include "Directory.hxx"
#include "Song.hxx"
#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
#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
inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
55
	:Database(simple_db_plugin),
56
	 path(block.GetPath("path")),
57
#ifdef ENABLE_ZLIB
58
	 compress(block.GetBlockValue("compress", true)),
59
#endif
60 61 62 63 64 65 66 67
	 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
68

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

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

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

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

		/* Check that the parent part of the path is a directory */
105
		FileInfo fi;
106

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

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

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

		return;
129 130 131
	}

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

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

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

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

151
	TextFile file(path);
152

153 154
	LogDebug(simple_db_domain, "reading DB");

155
	db_load_internal(file, *root);
156

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

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

167
	root = Directory::NewRoot();
168
	mtime = 0;
169

170 171 172 173
#ifndef NDEBUG
	borrowed_song_count = 0;
#endif

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

179
		delete root;
180

181
		Check();
182

183
		root = Directory::NewRoot();
184 185 186
	}
}

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

194
	delete root;
195 196
}

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

204
	ScopeDatabaseLock protect;
205 206

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

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

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

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

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

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

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

	light_song = song->Export();

240
#ifndef NDEBUG
241
	++borrowed_song_count;
242
#endif
243

244
	return &light_song;
245 246
}

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

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
255 256

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

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

272 273 274 275
	auto r = root->LookupDirectory(selection.uri.c_str());
	if (r.uri == nullptr) {
		/* it's a directory */

276 277
		if (selection.recursive && visit_directory)
			visit_directory(r.directory->Export());
278

279 280 281 282
		r.directory->Walk(selection.recursive, selection.filter,
				  visit_directory, visit_song,
				  visit_playlist);
		return;
283 284 285
	}

	if (strchr(r.uri, '/') == nullptr) {
286
		if (visit_song) {
287
			Song *song = r.directory->FindSong(r.uri);
288 289
			if (song != nullptr) {
				const LightSong song2 = song->Export();
290 291
				if (selection.Match(song2))
					visit_song(song2);
292 293

				return;
294
			}
295
		}
296 297
	}

298 299
	throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
			    "No such directory");
300 301
}

302
void
303
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
304
				TagType tag_type, tag_mask_t group_mask,
305
				VisitTag visit_tag) const
306
{
307
	::VisitUniqueTags(*this, selection, tag_type, group_mask, visit_tag);
308 309
}

310 311
DatabaseStats
SimpleDatabase::GetStats(const DatabaseSelection &selection) const
312
{
313
	return ::GetStats(*this, selection);
314 315
}

316 317
void
SimpleDatabase::Save()
318
{
319 320
	{
		const ScopeDatabaseLock protect;
321

322 323
		LogDebug(simple_db_domain, "removing empty directories from DB");
		root->PruneEmpty();
324

325 326 327
		LogDebug(simple_db_domain, "sorting DB");
		root->Sort();
	}
328

329
	LogDebug(simple_db_domain, "writing DB");
330

331
	FileOutputStream fos(path);
332

333 334
	OutputStream *os = &fos;

335
#ifdef ENABLE_ZLIB
336
	std::unique_ptr<GzipOutputStream> gzip;
337
	if (compress) {
338
		gzip.reset(new GzipOutputStream(*os));
339
		os = gzip.get();
340 341 342 343
	}
#endif

	BufferedOutputStream bos(*os);
344

345
	db_save_internal(bos, *root);
346

347
	bos.Flush();
348

349
#ifdef ENABLE_ZLIB
350
	if (gzip != nullptr) {
351
		gzip->Flush();
352
		gzip.reset();
353 354 355
	}
#endif

356
	fos.Commit();
357

358 359 360
	FileInfo fi;
	if (GetFileInfo(path, fi))
		mtime = fi.GetModificationTime();
361 362
}

363 364
void
SimpleDatabase::Mount(const char *uri, Database *db)
Max Kellermann's avatar
Max Kellermann committed
365
{
366 367
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
368 369
	assert(uri != nullptr);
	assert(db != nullptr);
370 371
#endif
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
372 373 374 375

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
376 377 378
	if (r.uri == nullptr)
		throw DatabaseError(DatabaseErrorCode::CONFLICT,
				    "Already exists");
Max Kellermann's avatar
Max Kellermann committed
379

380 381 382
	if (strchr(r.uri, '/') != nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "Parent not found");
Max Kellermann's avatar
Max Kellermann committed
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399

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

400 401
void
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri)
Max Kellermann's avatar
Max Kellermann committed
402
{
403 404 405
	if (cache_path.IsNull())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No 'cache_directory' configured");
Max Kellermann's avatar
Max Kellermann committed
406 407 408 409

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

410
	const auto name_fs = AllocatedPath::FromUTF8Throw(name.c_str());
411

412
#ifndef ENABLE_ZLIB
413 414
	constexpr bool compress = false;
#endif
Max Kellermann's avatar
Max Kellermann committed
415
	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
416
							  name_fs.c_str()),
417
				     compress);
418
	try {
419
		db->Open();
420
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
421
		delete db;
422
		throw;
Max Kellermann's avatar
Max Kellermann committed
423 424 425 426
	}

	// TODO: update the new database instance?

427 428 429
	try {
		Mount(local_uri, db);
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
430 431
		db->Close();
		delete db;
432
		throw;
Max Kellermann's avatar
Max Kellermann committed
433 434 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
	}
}

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

464 465
const DatabasePlugin simple_db_plugin = {
	"simple",
466
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
467 468
	SimpleDatabase::Create,
};