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

Rosen Penev's avatar
Rosen Penev committed
52
#include <cerrno>
53 54
#include <memory>

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

57
inline SimpleDatabase::SimpleDatabase(const ConfigBlock &block)
58
	:Database(simple_db_plugin),
59
	 path(block.GetPath("path")),
60
#ifdef ENABLE_ZLIB
61
	 compress(block.GetBlockValue("compress", true)),
62
#endif
63
	 cache_path(block.GetPath("cache_directory"))
64 65 66 67 68 69
{
	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
Rosen Penev's avatar
Rosen Penev committed
73
				      [[maybe_unused]]
74
#endif
75
				      bool _compress) noexcept
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 83
	 cache_path(nullptr)
{
Max Kellermann's avatar
Max Kellermann committed
84
}
85

86
DatabasePtr
87
SimpleDatabase::Create(EventLoop &, EventLoop &,
Rosen Penev's avatar
Rosen Penev committed
88
		       [[maybe_unused]] DatabaseListener &listener,
89
		       const ConfigBlock &block)
90
{
91
	return std::make_unique<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 (...) {
		LogError(std::current_exception());
180

181
		delete root;
182

183
		Check();
184

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

189
void
190
SimpleDatabase::Close() noexcept
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(std::string_view 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.rest);
Max Kellermann's avatar
Max Kellermann committed
216 217 218 219
		if (song == nullptr)
			return nullptr;

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

225
	if (r.rest.empty())
226
		/* it's a directory */
227 228
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
229

230
	if (r.rest.find('/') != std::string_view::npos)
231
		/* refers to a URI "below" the actual song */
232 233
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
234

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

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

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

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

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

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

		light_song.Destruct();
	}
267 268
}

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

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

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

	if (r.directory->IsMount()) {
		/* pass the request and the remaining uri to the mounted database */
		protect.unlock();

292
		WalkMount(r.uri, *(r.directory->mounted_database),
293 294
			  r.rest,
			  selection,
295
			  visit_directory, visit_song, visit_playlist);
296 297 298 299

		return;
	}

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

302
	if (r.rest.data() == nullptr) {
303 304
		/* 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 (r.rest.find('/') == std::string_view::npos) {
316
		if (visit_song) {
317
			Song *song = r.directory->FindSong(r.rest);
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
	if (r.rest.data() == nullptr)
407 408
		throw DatabaseError(DatabaseErrorCode::CONFLICT,
				    "Already exists");
Max Kellermann's avatar
Max Kellermann committed
409

410
	if (r.rest.find('/') != std::string_view::npos)
411 412
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "Parent not found");
Max Kellermann's avatar
Max Kellermann committed
413

414
	Directory *mnt = r.directory->CreateChild(r.rest);
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
bool
431
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);
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
	bool exists = db->FileExists();
Max Kellermann's avatar
Max Kellermann committed
450

451
	Mount(local_uri, std::move(db));
452 453

	return exists;
Max Kellermann's avatar
Max Kellermann committed
454 455
}

456
inline DatabasePtr
457
SimpleDatabase::LockUmountSteal(const char *uri) noexcept
Max Kellermann's avatar
Max Kellermann committed
458 459 460 461
{
	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
462
	if (r.rest.data() != nullptr || !r.directory->IsMount())
Max Kellermann's avatar
Max Kellermann committed
463 464
		return nullptr;

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

	return db;
}

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

	db->Close();
	return true;
}

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