SimpleDatabasePlugin.cxx 10.7 KB
Newer Older
1
/*
2
 * Copyright 2003-2019 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
#include "tag/Mask.hxx"
37 38 39
#include "fs/io/TextFile.hxx"
#include "fs/io/BufferedOutputStream.hxx"
#include "fs/io/FileOutputStream.hxx"
40
#include "fs/FileInfo.hxx"
41
#include "config/Block.hxx"
42
#include "fs/FileSystem.hxx"
Max Kellermann's avatar
Max Kellermann committed
43
#include "util/CharUtil.hxx"
44
#include "util/Domain.hxx"
45 46
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
47
#include "Log.hxx"
48

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

53 54
#include <memory>

55 56
#include <errno.h>

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

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

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

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

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

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

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

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

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

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

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

		return;
133 134 135
	}

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

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

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

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

155
	TextFile file(path);
156

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

159
	db_load_internal(file, *root);
160

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

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

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

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

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

183
		delete root;
184

185
		Check();
186

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

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

198
	delete root;
199 200
}

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

208
	ScopeDatabaseLock protect;
209 210

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

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

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

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

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

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

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

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

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

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

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

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

		light_song.Destruct();
	}
269 270
}

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

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

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

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

		return;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

362
	FileOutputStream fos(path);
363

364 365
	OutputStream *os = &fos;

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

	BufferedOutputStream bos(*os);
375

376
	db_save_internal(bos, *root);
377

378
	bos.Flush();
379

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

387
	fos.Commit();
388

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

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

	ScopeDatabaseLock protect;

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

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

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

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

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

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

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

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

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

450
	bool exists = db->FileExists();
Max Kellermann's avatar
Max Kellermann committed
451

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

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

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

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

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

	return db;
}

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

	db->Close();
	return true;
}

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