SimpleDatabasePlugin.cxx 10.4 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2014 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 26
#include "db/Selection.hxx"
#include "db/Helpers.hxx"
#include "db/LightDirectory.hxx"
27 28
#include "Directory.hxx"
#include "Song.hxx"
29
#include "SongFilter.hxx"
30
#include "DatabaseSave.hxx"
Max Kellermann's avatar
Max Kellermann committed
31 32
#include "db/DatabaseLock.hxx"
#include "db/DatabaseError.hxx"
33
#include "fs/TextFile.hxx"
34
#include "config/ConfigData.hxx"
35
#include "fs/FileSystem.hxx"
Max Kellermann's avatar
Max Kellermann committed
36
#include "util/CharUtil.hxx"
37 38
#include "util/Error.hxx"
#include "util/Domain.hxx"
39
#include "Log.hxx"
40

41 42
#include <errno.h>

43
static constexpr Domain simple_db_domain("simple_db");
44

45 46
inline SimpleDatabase::SimpleDatabase()
	:Database(simple_db_plugin),
Max Kellermann's avatar
Max Kellermann committed
47 48 49 50 51 52 53 54 55 56 57
	 path(AllocatedPath::Null()),
	 cache_path(AllocatedPath::Null()),
	 prefixed_light_song(nullptr) {}

inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path)
	:Database(simple_db_plugin),
	 path(std::move(_path)),
	 path_utf8(path.ToUTF8()),
	 cache_path(AllocatedPath::Null()),
	 prefixed_light_song(nullptr) {
}
58

59
Database *
60 61 62
SimpleDatabase::Create(gcc_unused EventLoop &loop,
		       gcc_unused DatabaseListener &listener,
		       const config_param &param, Error &error)
63
{
64
	SimpleDatabase *db = new SimpleDatabase();
65
	if (!db->Configure(param, error)) {
66
		delete db;
67
		db = nullptr;
68
	}
69

70
	return db;
71 72
}

73
bool
74
SimpleDatabase::Configure(const config_param &param, Error &error)
75
{
76
	path = param.GetBlockPath("path", error);
77
	if (path.IsNull()) {
78 79 80
		if (!error.IsDefined())
			error.Set(simple_db_domain,
				  "No \"path\" parameter specified");
81
		return false;
82 83
	}

84
	path_utf8 = path.ToUTF8();
85

Max Kellermann's avatar
Max Kellermann committed
86 87 88 89
	cache_path = param.GetBlockPath("cache_directory", error);
	if (path.IsNull() && error.IsDefined())
		return false;

90
	return true;
91 92
}

93
bool
94
SimpleDatabase::Check(Error &error) const
95
{
96
	assert(!path.IsNull());
97 98

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

		/* Check that the parent part of the path is a directory */
		struct stat st;
107
		if (!StatFile(dirPath, st)) {
108 109 110
			error.FormatErrno("Couldn't stat parent directory of db file "
					  "\"%s\"",
					  path_utf8.c_str());
111 112 113 114
			return false;
		}

		if (!S_ISDIR(st.st_mode)) {
115 116 117 118
			error.Format(simple_db_domain,
				     "Couldn't create db file \"%s\" because the "
				     "parent path is not a directory",
				     path_utf8.c_str());
119 120 121
			return false;
		}

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 128
			error.FormatErrno(e, "Can't create db file in \"%s\"",
					  dirPath_utf8.c_str());
129 130
			return false;
		}
131
#endif
132 133 134 135 136
		return true;
	}

	/* Path exists, now check if it's a regular file */
	struct stat st;
137
	if (!StatFile(path, st)) {
138 139
		error.FormatErrno("Couldn't stat db file \"%s\"",
				  path_utf8.c_str());
140 141 142 143
		return false;
	}

	if (!S_ISREG(st.st_mode)) {
144 145 146
		error.Format(simple_db_domain,
			     "db file \"%s\" is not a regular file",
			     path_utf8.c_str());
147 148 149
		return false;
	}

150
#ifndef WIN32
151
	/* And check that we can write to it */
152
	if (!CheckAccess(path, R_OK | W_OK)) {
153 154
		error.FormatErrno("Can't open db file \"%s\" for reading/writing",
				  path_utf8.c_str());
155 156
		return false;
	}
157
#endif
158 159 160 161

	return true;
}

162
bool
163
SimpleDatabase::Load(Error &error)
164
{
165
	assert(!path.IsNull());
166
	assert(root != nullptr);
167

168
	TextFile file(path);
169
	if (file.HasFailed()) {
170 171
		error.FormatErrno("Failed to open database file \"%s\"",
				  path_utf8.c_str());
172 173 174
		return false;
	}

175
	if (!db_load_internal(file, *root, error))
176 177 178
		return false;

	struct stat st;
179
	if (StatFile(path, st))
180
		mtime = st.st_mtime;
181 182 183 184

	return true;
}

185
bool
186
SimpleDatabase::Open(Error &error)
187
{
Max Kellermann's avatar
Max Kellermann committed
188 189
	assert(prefixed_light_song == nullptr);

190
	root = Directory::NewRoot();
191
	mtime = 0;
192

193 194 195 196
#ifndef NDEBUG
	borrowed_song_count = 0;
#endif

197
	if (!Load(error)) {
198
		delete root;
199

200
		LogError(error);
201
		error.Clear();
202

203
		if (!Check(error))
204 205
			return false;

206
		root = Directory::NewRoot();
207 208 209 210 211
	}

	return true;
}

212 213
void
SimpleDatabase::Close()
214
{
215
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
216
	assert(prefixed_light_song == nullptr);
217
	assert(borrowed_song_count == 0);
218

219
	delete root;
220 221
}

222
const LightSong *
223
SimpleDatabase::GetSong(const char *uri, Error &error) const
224
{
225
	assert(root != nullptr);
Max Kellermann's avatar
Max Kellermann committed
226
	assert(prefixed_light_song == nullptr);
227
	assert(borrowed_song_count == 0);
228

229
	db_lock();
230 231

	auto r = root->LookupDirectory(uri);
Max Kellermann's avatar
Max Kellermann committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246

	if (r.directory->IsMount()) {
		/* pass the request to the mounted database */
		db_unlock();

		const LightSong *song =
			r.directory->mounted_database->GetSong(r.uri, error);
		if (song == nullptr)
			return nullptr;

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

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
	if (r.uri == nullptr) {
		/* it's a directory */
		db_unlock();
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
		return nullptr;
	}

	if (strchr(r.uri, '/') != nullptr) {
		/* refers to a URI "below" the actual song */
		db_unlock();
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
		return nullptr;
	}

	const Song *song = r.directory->FindSong(r.uri);
264
	db_unlock();
265
	if (song == nullptr) {
266 267
		error.Format(db_domain, DB_NOT_FOUND,
			     "No such song: %s", uri);
268 269 270 271 272
		return nullptr;
	}

	light_song = song->Export();

273
#ifndef NDEBUG
274
	++borrowed_song_count;
275
#endif
276

277
	return &light_song;
278 279
}

280
void
281
SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
282
{
Max Kellermann's avatar
Max Kellermann committed
283 284 285 286 287
	assert(song != nullptr);
	assert(song == &light_song || song == prefixed_light_song);

	delete prefixed_light_song;
	prefixed_light_song = nullptr;
288 289

#ifndef NDEBUG
Max Kellermann's avatar
Max Kellermann committed
290 291 292 293
	if (song == &light_song) {
		assert(borrowed_song_count > 0);
		--borrowed_song_count;
	}
294 295 296
#endif
}

297
bool
298
SimpleDatabase::Visit(const DatabaseSelection &selection,
299 300 301
		      VisitDirectory visit_directory,
		      VisitSong visit_song,
		      VisitPlaylist visit_playlist,
302
		      Error &error) const
303
{
304 305
	ScopeDatabaseLock protect;

306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
	auto r = root->LookupDirectory(selection.uri.c_str());
	if (r.uri == nullptr) {
		/* it's a directory */

		if (selection.recursive && visit_directory &&
		    !visit_directory(r.directory->Export(), error))
			return false;

		return r.directory->Walk(selection.recursive, selection.filter,
					 visit_directory, visit_song,
					 visit_playlist,
					 error);
	}

	if (strchr(r.uri, '/') == nullptr) {
321
		if (visit_song) {
322
			Song *song = r.directory->FindSong(r.uri);
323 324 325 326 327
			if (song != nullptr) {
				const LightSong song2 = song->Export();
				return !selection.Match(song2) ||
					visit_song(song2, error);
			}
328
		}
329 330
	}

331 332
	error.Set(db_domain, DB_NOT_FOUND, "No such directory");
	return false;
333 334
}

335 336
bool
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
337
				TagType tag_type,
338
				VisitString visit_string,
339
				Error &error) const
340 341
{
	return ::VisitUniqueTags(*this, selection, tag_type, visit_string,
342
				 error);
343 344
}

345 346
bool
SimpleDatabase::GetStats(const DatabaseSelection &selection,
347
			 DatabaseStats &stats, Error &error) const
348
{
349
	return ::GetStats(*this, selection, stats, error);
350 351
}

352
bool
353
SimpleDatabase::Save(Error &error)
354
{
355 356
	db_lock();

357
	LogDebug(simple_db_domain, "removing empty directories from DB");
358
	root->PruneEmpty();
359

360
	LogDebug(simple_db_domain, "sorting DB");
361
	root->Sort();
362

363 364
	db_unlock();

365
	LogDebug(simple_db_domain, "writing DB");
366

367
	FILE *fp = FOpen(path, FOpenMode::WriteText);
368
	if (!fp) {
369 370
		error.FormatErrno("unable to write to db file \"%s\"",
				  path_utf8.c_str());
371 372 373
		return false;
	}

374
	db_save_internal(fp, *root);
375 376

	if (ferror(fp)) {
377
		error.SetErrno("Failed to write to database file");
378 379 380 381 382 383 384
		fclose(fp);
		return false;
	}

	fclose(fp);

	struct stat st;
385
	if (StatFile(path, st))
386
		mtime = st.st_mtime;
387 388 389 390

	return true;
}

Max Kellermann's avatar
Max Kellermann committed
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 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 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
bool
SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
{
	assert(uri != nullptr);
	assert(*uri != 0);
	assert(db != nullptr);

	ScopeDatabaseLock protect;

	auto r = root->LookupDirectory(uri);
	if (r.uri == nullptr) {
		error.Format(db_domain, DB_CONFLICT,
			     "Already exists: %s", uri);
		return nullptr;
	}

	if (strchr(r.uri, '/') != nullptr) {
		error.Format(db_domain, DB_NOT_FOUND,
			     "Parent not found: %s", uri);
		return nullptr;
	}

	Directory *mnt = r.directory->CreateChild(r.uri);
	mnt->mounted_database = db;
	return true;
}

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

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

bool
SimpleDatabase::Mount(const char *local_uri, const char *storage_uri,
		      Error &error)
{
	if (cache_path.IsNull()) {
		error.Format(db_domain, DB_NOT_FOUND,
			     "No 'cache_directory' configured");
		return nullptr;
	}

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

	auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
							  name.c_str()));
	if (!db->Open(error)) {
		delete db;
		return false;
	}

	// TODO: update the new database instance?

	if (!Mount(local_uri, db, error)) {
		db->Close();
		delete db;
		return false;
	}

	return true;
}

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

489 490
const DatabasePlugin simple_db_plugin = {
	"simple",
491
	DatabasePlugin::FLAG_REQUIRE_STORAGE,
492 493
	SimpleDatabase::Create,
};