SimpleDatabasePlugin.cxx 10.7 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2020 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"
29
#include "db/VHelper.hxx"
Max Kellermann's avatar
Max Kellermann committed
30
#include "db/LightDirectory.hxx"
31 32 33
#include "Directory.hxx"
#include "Song.hxx"
#include "DatabaseSave.hxx"
Max Kellermann's avatar
Max Kellermann committed
34 35
#include "db/DatabaseLock.hxx"
#include "db/DatabaseError.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 45
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
46
#include "Log.hxx"
47

48
#ifdef ENABLE_ZLIB
49 50 51
#include "fs/io/GzipOutputStream.hxx"
#endif

52 53
#include <memory>

54 55
#include <errno.h>

56
static constexpr Domain simple_db_domain("simple_db");
57

58
inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
59
	:Database(simple_db_plugin),
60
	 path(block.GetPath("path")),
61
#ifdef ENABLE_ZLIB
62
	 compress(block.GetBlockValue("compress", true)),
63
#endif
64
	 cache_path(block.GetPath("cache_directory"))
65 66 67 68 69 70
{
	if (path.IsNull())
		throw std::runtime_error("No \"path\" parameter specified");

	path_utf8 = path.ToUTF8();
}
Max Kellermann's avatar
Max Kellermann committed
71

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

87
DatabasePtr
88
SimpleDatabase::Create(EventLoop &, EventLoop &,
89
		       gcc_unused DatabaseListener &listener,
90
		       const ConfigBlock &block)
91
{
92
	return std::make_unique<SimpleDatabase>(block);
93 94
}

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

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

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

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

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

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

		return;
132 133 134
	}

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

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

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

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

154
	TextFile file(path);
155

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

158
	db_load_internal(file, *root);
159

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

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

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

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

177
	try {
178
		Load();
179 180
	} catch (...) {
		LogError(std::current_exception());
181

182
		delete root;
183

184
		Check();
185

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

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

197
	delete root;
198 199
}

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

207
	ScopeDatabaseLock protect;
208 209

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

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

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

		prefixed_light_song =
			new PrefixedLightSong(*song, r.directory->GetPath());
222
		r.directory->mounted_database->ReturnSong(song);
Max Kellermann's avatar
Max Kellermann committed
223 224 225
		return prefixed_light_song;
	}

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

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

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

242
	light_song.Construct(song->Export());
243

244
#ifndef NDEBUG
245
	++borrowed_song_count;
246
#endif
247

248
	return &light_song.Get();
249 250
}

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

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

		light_song.Destruct();
	}
268 269
}

270 271 272 273 274 275 276 277 278
gcc_const
static DatabaseSelection
CheckSelection(DatabaseSelection selection) noexcept
{
	selection.uri.clear();
	selection.filter = nullptr;
	return selection;
}

279
void
280
SimpleDatabase::Visit(const DatabaseSelection &selection,
281 282
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
283
		      VisitPlaylist visit_playlist) const
284
{
285 286
	ScopeDatabaseLock protect;

287
	auto r = root->LookupDirectory(selection.uri.c_str());
288 289 290 291 292 293

	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),
294 295
			  (r.uri == nullptr)?"":r.uri, selection,
			  visit_directory, visit_song, visit_playlist);
296 297 298 299

		return;
	}

300 301
	DatabaseVisitorHelper helper(CheckSelection(selection), visit_song);

302 303 304
	if (r.uri == nullptr) {
		/* it's a directory */

305 306
		if (selection.recursive && visit_directory)
			visit_directory(r.directory->Export());
307

308 309 310
		r.directory->Walk(selection.recursive, selection.filter,
				  visit_directory, visit_song,
				  visit_playlist);
311
		helper.Commit();
312
		return;
313 314 315
	}

	if (strchr(r.uri, '/') == nullptr) {
316
		if (visit_song) {
317
			Song *song = r.directory->FindSong(r.uri);
318 319
			if (song != nullptr) {
				const LightSong song2 = song->Export();
320 321
				if (selection.Match(song2))
					visit_song(song2);
322

323
				helper.Commit();
324
				return;
325
			}
326
		}
327 328
	}

329 330
	throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
			    "No such directory");
331 332
}

333
RecursiveMap<std::string>
334
SimpleDatabase::CollectUniqueTags(const DatabaseSelection &selection,
335
				  ConstBuffer<TagType> tag_types) const
336
{
337
	return ::CollectUniqueTags(*this, selection, tag_types);
338 339
}

340 341
DatabaseStats
SimpleDatabase::GetStats(const DatabaseSelection &selection) const
342
{
343
	return ::GetStats(*this, selection);
344 345
}

346 347
void
SimpleDatabase::Save()
348
{
349 350
	{
		const ScopeDatabaseLock protect;
351

352 353
		LogDebug(simple_db_domain, "removing empty directories from DB");
		root->PruneEmpty();
354

355 356 357
		LogDebug(simple_db_domain, "sorting DB");
		root->Sort();
	}
358

359
	LogDebug(simple_db_domain, "writing DB");
360

361
	FileOutputStream fos(path);
362

363 364
	OutputStream *os = &fos;

365
#ifdef ENABLE_ZLIB
366
	std::unique_ptr<GzipOutputStream> gzip;
367
	if (compress) {
368
		gzip = std::make_unique<GzipOutputStream>(*os);
369
		os = gzip.get();
370 371 372 373
	}
#endif

	BufferedOutputStream bos(*os);
374

375
	db_save_internal(bos, *root);
376

377
	bos.Flush();
378

379
#ifdef ENABLE_ZLIB
380
	if (gzip != nullptr) {
381
		gzip->Flush();
382
		gzip.reset();
383 384 385
	}
#endif

386
	fos.Commit();
387

388 389
	FileInfo fi;
	if (GetFileInfo(path, fi))
390
		mtime = fi.GetModificationTime();
391 392
}

393
void
394
SimpleDatabase::Mount(const char *uri, DatabasePtr db)
Max Kellermann's avatar
Max Kellermann committed
395
{
396 397
#if !CLANG_CHECK_VERSION(3,6)
	/* disabled on clang due to -Wtautological-pointer-compare */
Max Kellermann's avatar
Max Kellermann committed
398
	assert(uri != nullptr);
399
#endif
400
	assert(db != nullptr);
401
	assert(*uri != 0);
Max Kellermann's avatar
Max Kellermann committed
402 403 404 405

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
406 407 408
	if (r.uri == nullptr)
		throw DatabaseError(DatabaseErrorCode::CONFLICT,
				    "Already exists");
Max Kellermann's avatar
Max Kellermann committed
409

410 411 412
	if (strchr(r.uri, '/') != nullptr)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "Parent not found");
Max Kellermann's avatar
Max Kellermann committed
413 414

	Directory *mnt = r.directory->CreateChild(r.uri);
415
	mnt->mounted_database = std::move(db);
Max Kellermann's avatar
Max Kellermann committed
416 417 418 419 420 421 422 423 424 425 426 427 428 429
}

static constexpr bool
IsSafeChar(char ch)
{
	return IsAlphaNumericASCII(ch) || ch == '-' || ch == '_' || ch == '%';
}

static constexpr bool
IsUnsafeChar(char ch)
{
	return !IsSafeChar(ch);
}

430 431
void
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri)
Max Kellermann's avatar
Max Kellermann committed
432
{
433 434 435
	if (cache_path.IsNull())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No 'cache_directory' configured");
Max Kellermann's avatar
Max Kellermann committed
436 437 438 439

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

440
	const auto name_fs = AllocatedPath::FromUTF8Throw(name.c_str());
441

442
#ifndef ENABLE_ZLIB
443 444
	constexpr bool compress = false;
#endif
445 446 447
	auto db = std::make_unique<SimpleDatabase>(cache_path / name_fs,
						   compress);
	db->Open();
Max Kellermann's avatar
Max Kellermann committed
448 449 450

	// TODO: update the new database instance?

451
	try {
452
		Mount(local_uri, std::move(db));
453
	} catch (...) {
Max Kellermann's avatar
Max Kellermann committed
454
		db->Close();
455
		throw;
Max Kellermann's avatar
Max Kellermann committed
456 457 458
	}
}

459
inline DatabasePtr
460
SimpleDatabase::LockUmountSteal(const char *uri) noexcept
Max Kellermann's avatar
Max Kellermann committed
461 462 463 464 465 466 467
{
	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
	if (r.uri != nullptr || !r.directory->IsMount())
		return nullptr;

468
	auto db = std::move(r.directory->mounted_database);
Max Kellermann's avatar
Max Kellermann committed
469 470 471 472 473 474
	r.directory->Delete();

	return db;
}

bool
475
SimpleDatabase::Unmount(const char *uri) noexcept
Max Kellermann's avatar
Max Kellermann committed
476
{
477
	auto db = LockUmountSteal(uri);
Max Kellermann's avatar
Max Kellermann committed
478 479 480 481 482 483 484
	if (db == nullptr)
		return false;

	db->Close();
	return true;
}

485 486
const DatabasePlugin simple_db_plugin = {
	"simple",
487
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
488 489
	SimpleDatabase::Create,
};