4cf5d04ca1
This patch massively reduces the amount of heap allocations at the interface/command layer. Most commands with minimal output should not allocate memory from the heap at all. Things like repeatedly polling status, currentsong, and volume changes should be faster as a result, and more importantly, not a source of memory fragmentation. These changes should be safe in that there's no way for a remote-client to corrupt memory or otherwise do bad stuff to MPD, but an extra set of eyes to review would be good. Of course there's never any warranty :) No longer do we use FILE * structures in the interface, which means we don't have to allocate any new memory for most connections. Now, before you go on about losing the buffering that FILE * +implies+, remember that myfprintf() never took advantage of any of the stdio buffering features. To reduce the diff and make bugs easier to spot in the diff, I've kept myfprintf in places where we write to files (and not network interfaces). Expect myfprintf to go away entirely soon (we'll use fprintf for writing regular files). git-svn-id: https://svn.musicpd.org/mpd/trunk@4483 09075e82-0dd4-0310-85a5-a0d7c8717e4f
1377 lines
31 KiB
C
1377 lines
31 KiB
C
/* the Music Player Daemon (MPD)
|
|
* (c)2003-2006 by Warren Dukes (warren.dukes@gmail.com)
|
|
* This project's homepage is: 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "directory.h"
|
|
|
|
#include "ls.h"
|
|
#include "command.h"
|
|
#include "utils.h"
|
|
#include "path.h"
|
|
#include "log.h"
|
|
#include "conf.h"
|
|
#include "stats.h"
|
|
#include "playlist.h"
|
|
#include "listen.h"
|
|
#include "interface.h"
|
|
#include "volume.h"
|
|
#include "mpd_types.h"
|
|
#include "sig_handlers.h"
|
|
#include "list.h"
|
|
#include "dbUtils.h"
|
|
#include "tagTracker.h"
|
|
|
|
#include <sys/wait.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <assert.h>
|
|
#include <libgen.h>
|
|
|
|
#define DIRECTORY_DIR "directory: "
|
|
#define DIRECTORY_MTIME "mtime: "
|
|
#define DIRECTORY_BEGIN "begin: "
|
|
#define DIRECTORY_END "end: "
|
|
#define DIRECTORY_INFO_BEGIN "info_begin"
|
|
#define DIRECTORY_INFO_END "info_end"
|
|
#define DIRECTORY_MPD_VERSION "mpd_version: "
|
|
#define DIRECTORY_FS_CHARSET "fs_charset: "
|
|
|
|
#define DIRECTORY_UPDATE_EXIT_NOUPDATE 0
|
|
#define DIRECTORY_UPDATE_EXIT_UPDATE 1
|
|
#define DIRECTORY_UPDATE_EXIT_ERROR 2
|
|
|
|
#define DIRECTORY_RETURN_NOUPDATE 0
|
|
#define DIRECTORY_RETURN_UPDATE 1
|
|
#define DIRECTORY_RETURN_ERROR -1
|
|
|
|
Directory *mp3rootDirectory = NULL;
|
|
|
|
time_t directory_dbModTime = 0;
|
|
|
|
volatile int directory_updatePid = 0;
|
|
|
|
volatile int directory_reReadDB = 0;
|
|
|
|
volatile mpd_uint16 directory_updateJobId = 0;
|
|
|
|
static DirectoryList *newDirectoryList();
|
|
|
|
static int addToDirectory(Directory * directory, char *shortname, char *name);
|
|
|
|
static void freeDirectoryList(DirectoryList * list);
|
|
|
|
static void freeDirectory(Directory * directory);
|
|
|
|
static int exploreDirectory(Directory * directory);
|
|
|
|
static int updateDirectory(Directory * directory);
|
|
|
|
static void deleteEmptyDirectoriesInDirectory(Directory * directory);
|
|
|
|
static void removeSongFromDirectory(Directory * directory, char *shortname);
|
|
|
|
static int addSubDirectoryToDirectory(Directory * directory, char *shortname,
|
|
char *name, struct stat *st);
|
|
|
|
static Directory *getDirectoryDetails(char *name, char **shortname);
|
|
|
|
static Directory *getDirectory(char *name);
|
|
|
|
static Song *getSongDetails(char *file, char **shortnameRet,
|
|
Directory ** directoryRet);
|
|
|
|
static int updatePath(char *utf8path);
|
|
|
|
static void sortDirectory(Directory * directory);
|
|
|
|
static int inodeFoundInParent(Directory * parent, ino_t inode, dev_t device);
|
|
|
|
static int statDirectory(Directory * dir);
|
|
|
|
static char *getDbFile()
|
|
{
|
|
ConfigParam *param = parseConfigFilePath(CONF_DB_FILE, 1);
|
|
|
|
assert(param);
|
|
assert(param->value);
|
|
|
|
return param->value;
|
|
}
|
|
|
|
static void clearUpdatePid()
|
|
{
|
|
directory_updatePid = 0;
|
|
}
|
|
|
|
int isUpdatingDB()
|
|
{
|
|
if (directory_updatePid > 0 || directory_reReadDB) {
|
|
return directory_updateJobId;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void directory_sigChldHandler(int pid, int status)
|
|
{
|
|
if (directory_updatePid == pid) {
|
|
if (WIFSIGNALED(status) && WTERMSIG(status) != SIGTERM) {
|
|
ERROR("update process died from a "
|
|
"non-TERM signal: %i\n", WTERMSIG(status));
|
|
} else if (!WIFSIGNALED(status)) {
|
|
switch (WEXITSTATUS(status)) {
|
|
case DIRECTORY_UPDATE_EXIT_UPDATE:
|
|
directory_reReadDB = 1;
|
|
DEBUG("directory_sigChldHandler: "
|
|
"updated db\n");
|
|
case DIRECTORY_UPDATE_EXIT_NOUPDATE:
|
|
DEBUG("directory_sigChldHandler: "
|
|
"update exited succesffully\n");
|
|
break;
|
|
default:
|
|
ERROR("error updating db\n");
|
|
}
|
|
}
|
|
clearUpdatePid();
|
|
}
|
|
}
|
|
|
|
void readDirectoryDBIfUpdateIsFinished()
|
|
{
|
|
if (directory_reReadDB && 0 == directory_updatePid) {
|
|
DEBUG("readDirectoryDB since update finished successfully\n");
|
|
readDirectoryDB();
|
|
playlistVersionChange();
|
|
directory_reReadDB = 0;
|
|
}
|
|
}
|
|
|
|
int updateInit(int fd, List * pathList)
|
|
{
|
|
if (directory_updatePid > 0) {
|
|
commandError(fd, ACK_ERROR_UPDATE_ALREADY, "already updating",
|
|
NULL);
|
|
return -1;
|
|
}
|
|
|
|
/* need to block CHLD signal, cause it can exit before we
|
|
even get a chance to assign directory_updatePID */
|
|
blockSignals();
|
|
directory_updatePid = fork();
|
|
if (directory_updatePid == 0) {
|
|
/* child */
|
|
int dbUpdated = 0;
|
|
|
|
unblockSignals();
|
|
|
|
finishSigHandlers();
|
|
closeAllListenSockets();
|
|
freeAllInterfaces();
|
|
finishPlaylist();
|
|
finishVolume();
|
|
|
|
if (pathList) {
|
|
ListNode *node = pathList->firstNode;
|
|
|
|
while (node) {
|
|
switch (updatePath(node->key)) {
|
|
case 1:
|
|
dbUpdated = 1;
|
|
break;
|
|
case 0:
|
|
break;
|
|
default:
|
|
exit(DIRECTORY_UPDATE_EXIT_ERROR);
|
|
}
|
|
node = node->nextNode;
|
|
}
|
|
} else {
|
|
if ((dbUpdated = updateDirectory(mp3rootDirectory)) < 0) {
|
|
exit(DIRECTORY_UPDATE_EXIT_ERROR);
|
|
}
|
|
}
|
|
|
|
if (!dbUpdated)
|
|
exit(DIRECTORY_UPDATE_EXIT_NOUPDATE);
|
|
|
|
/* ignore signals since we don't want them to corrupt the db */
|
|
ignoreSignals();
|
|
if (writeDirectoryDB() < 0) {
|
|
exit(DIRECTORY_UPDATE_EXIT_ERROR);
|
|
}
|
|
exit(DIRECTORY_UPDATE_EXIT_UPDATE);
|
|
} else if (directory_updatePid < 0) {
|
|
unblockSignals();
|
|
ERROR("updateInit: Problems forking()'ing\n");
|
|
commandError(fd, ACK_ERROR_SYSTEM,
|
|
"problems trying to update", NULL);
|
|
directory_updatePid = 0;
|
|
return -1;
|
|
}
|
|
unblockSignals();
|
|
|
|
directory_updateJobId++;
|
|
if (directory_updateJobId > 1 << 15)
|
|
directory_updateJobId = 1;
|
|
DEBUG("updateInit: fork()'d update child for update job id %i\n",
|
|
(int)directory_updateJobId);
|
|
fdprintf(fd, "updating_db: %i\n", (int)directory_updateJobId);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static DirectoryStat *newDirectoryStat(struct stat *st)
|
|
{
|
|
DirectoryStat *ret = malloc(sizeof(DirectoryStat));
|
|
ret->inode = st->st_ino;
|
|
ret->device = st->st_dev;
|
|
return ret;
|
|
}
|
|
|
|
static void freeDirectoryStatFromDirectory(Directory * dir)
|
|
{
|
|
if (dir->stat)
|
|
free(dir->stat);
|
|
dir->stat = NULL;
|
|
}
|
|
|
|
static Directory *newDirectory(char *dirname, Directory * parent)
|
|
{
|
|
Directory *directory;
|
|
|
|
directory = malloc(sizeof(Directory));
|
|
|
|
if (dirname && strlen(dirname))
|
|
directory->path = strdup(dirname);
|
|
else
|
|
directory->path = NULL;
|
|
directory->subDirectories = newDirectoryList();
|
|
directory->songs = newSongList();
|
|
directory->stat = NULL;
|
|
directory->parent = parent;
|
|
|
|
return directory;
|
|
}
|
|
|
|
static void freeDirectory(Directory * directory)
|
|
{
|
|
freeDirectoryList(directory->subDirectories);
|
|
freeSongList(directory->songs);
|
|
if (directory->path)
|
|
free(directory->path);
|
|
freeDirectoryStatFromDirectory(directory);
|
|
free(directory);
|
|
/* this resets last dir returned */
|
|
/*getDirectoryPath(NULL); */
|
|
}
|
|
|
|
static DirectoryList *newDirectoryList()
|
|
{
|
|
return makeList((ListFreeDataFunc *) freeDirectory, 1);
|
|
}
|
|
|
|
static void freeDirectoryList(DirectoryList * directoryList)
|
|
{
|
|
freeList(directoryList);
|
|
}
|
|
|
|
static void removeSongFromDirectory(Directory * directory, char *shortname)
|
|
{
|
|
void *song;
|
|
|
|
if (findInList(directory->songs, shortname, &song)) {
|
|
LOG("removing: %s\n", getSongUrl((Song *) song));
|
|
deleteFromList(directory->songs, shortname);
|
|
}
|
|
}
|
|
|
|
static void deleteEmptyDirectoriesInDirectory(Directory * directory)
|
|
{
|
|
ListNode *node = directory->subDirectories->firstNode;
|
|
ListNode *nextNode;
|
|
Directory *subDir;
|
|
|
|
while (node) {
|
|
subDir = (Directory *) node->data;
|
|
deleteEmptyDirectoriesInDirectory(subDir);
|
|
nextNode = node->nextNode;
|
|
if (subDir->subDirectories->numberOfNodes == 0 &&
|
|
subDir->songs->numberOfNodes == 0) {
|
|
deleteNodeFromList(directory->subDirectories, node);
|
|
}
|
|
node = nextNode;
|
|
}
|
|
}
|
|
|
|
/* return values:
|
|
-1 -> error
|
|
0 -> no error, but nothing updated
|
|
1 -> no error, and stuff updated
|
|
*/
|
|
static int updateInDirectory(Directory * directory, char *shortname, char *name)
|
|
{
|
|
void *song;
|
|
void *subDir;
|
|
struct stat st;
|
|
|
|
if (myStat(name, &st))
|
|
return -1;
|
|
|
|
if (S_ISREG(st.st_mode) && hasMusicSuffix(name, 0)) {
|
|
if (0 == findInList(directory->songs, shortname, &song)) {
|
|
addToDirectory(directory, shortname, name);
|
|
return DIRECTORY_RETURN_UPDATE;
|
|
} else if (st.st_mtime != ((Song *) song)->mtime) {
|
|
LOG("updating %s\n", name);
|
|
if (updateSongInfo((Song *) song) < 0) {
|
|
removeSongFromDirectory(directory, shortname);
|
|
}
|
|
return 1;
|
|
}
|
|
} else if (S_ISDIR(st.st_mode)) {
|
|
if (findInList
|
|
(directory->subDirectories, shortname, (void **)&subDir)) {
|
|
freeDirectoryStatFromDirectory(subDir);
|
|
((Directory *) subDir)->stat = newDirectoryStat(&st);
|
|
return updateDirectory((Directory *) subDir);
|
|
} else {
|
|
return addSubDirectoryToDirectory(directory, shortname,
|
|
name, &st);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* return values:
|
|
-1 -> error
|
|
0 -> no error, but nothing removed
|
|
1 -> no error, and stuff removed
|
|
*/
|
|
static int removeDeletedFromDirectory(Directory * directory, DIR * dir)
|
|
{
|
|
char cwd[2];
|
|
struct dirent *ent;
|
|
char *dirname = getDirectoryPath(directory);
|
|
List *entList = makeList(free, 1);
|
|
void *name;
|
|
char *s;
|
|
char *utf8;
|
|
ListNode *node;
|
|
ListNode *tmpNode;
|
|
int ret = 0;
|
|
|
|
cwd[0] = '.';
|
|
cwd[1] = '\0';
|
|
if (dirname == NULL)
|
|
dirname = cwd;
|
|
|
|
while ((ent = readdir(dir))) {
|
|
if (ent->d_name[0] == '.')
|
|
continue; /* hide hidden stuff */
|
|
if (strchr(ent->d_name, '\n'))
|
|
continue;
|
|
|
|
utf8 = fsCharsetToUtf8(ent->d_name);
|
|
|
|
if (!utf8)
|
|
continue;
|
|
|
|
if (directory->path) {
|
|
s = malloc(strlen(getDirectoryPath(directory))
|
|
+ strlen(utf8) + 2);
|
|
sprintf(s, "%s/%s", getDirectoryPath(directory), utf8);
|
|
} else
|
|
s = strdup(utf8);
|
|
insertInList(entList, utf8, s);
|
|
}
|
|
|
|
node = directory->subDirectories->firstNode;
|
|
while (node) {
|
|
tmpNode = node->nextNode;
|
|
if (findInList(entList, node->key, &name)) {
|
|
if (!isDir((char *)name)) {
|
|
LOG("removing directory: %s\n", (char *)name);
|
|
deleteFromList(directory->subDirectories,
|
|
node->key);
|
|
ret = 1;
|
|
}
|
|
} else {
|
|
LOG("removing directory: %s/%s\n",
|
|
getDirectoryPath(directory), node->key);
|
|
deleteFromList(directory->subDirectories, node->key);
|
|
ret = 1;
|
|
}
|
|
node = tmpNode;
|
|
}
|
|
|
|
node = directory->songs->firstNode;
|
|
while (node) {
|
|
tmpNode = node->nextNode;
|
|
if (findInList(entList, node->key, (void **)&name)) {
|
|
if (!isMusic(name, NULL, 0)) {
|
|
removeSongFromDirectory(directory, node->key);
|
|
ret = 1;
|
|
}
|
|
} else {
|
|
removeSongFromDirectory(directory, node->key);
|
|
ret = 1;
|
|
}
|
|
node = tmpNode;
|
|
}
|
|
|
|
freeList(entList);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static Directory *addDirectoryPathToDB(char *utf8path, char **shortname)
|
|
{
|
|
char *parent;
|
|
Directory *parentDirectory;
|
|
void *directory;
|
|
|
|
parent = strdup(parentPath(utf8path));
|
|
|
|
if (strlen(parent) == 0)
|
|
parentDirectory = (void *)mp3rootDirectory;
|
|
else
|
|
parentDirectory = addDirectoryPathToDB(parent, shortname);
|
|
|
|
if (!parentDirectory) {
|
|
free(parent);
|
|
return NULL;
|
|
}
|
|
|
|
*shortname = utf8path + strlen(parent);
|
|
while (*(*shortname) && *(*shortname) == '/')
|
|
(*shortname)++;
|
|
|
|
if (!findInList
|
|
(parentDirectory->subDirectories, *shortname, &directory)) {
|
|
struct stat st;
|
|
if (myStat(utf8path, &st) < 0 ||
|
|
inodeFoundInParent(parentDirectory, st.st_ino, st.st_dev)) {
|
|
free(parent);
|
|
return NULL;
|
|
} else {
|
|
directory = newDirectory(utf8path, parentDirectory);
|
|
insertInList(parentDirectory->subDirectories,
|
|
*shortname, directory);
|
|
}
|
|
}
|
|
|
|
/* if we're adding directory paths, make sure to delete filenames
|
|
with potentially the same name */
|
|
removeSongFromDirectory(parentDirectory, *shortname);
|
|
|
|
free(parent);
|
|
|
|
return (Directory *) directory;
|
|
}
|
|
|
|
static Directory *addParentPathToDB(char *utf8path, char **shortname)
|
|
{
|
|
char *parent;
|
|
Directory *parentDirectory;
|
|
|
|
parent = strdup(parentPath(utf8path));
|
|
|
|
if (strlen(parent) == 0)
|
|
parentDirectory = (void *)mp3rootDirectory;
|
|
else
|
|
parentDirectory = addDirectoryPathToDB(parent, shortname);
|
|
|
|
if (!parentDirectory) {
|
|
free(parent);
|
|
return NULL;
|
|
}
|
|
|
|
*shortname = utf8path + strlen(parent);
|
|
while (*(*shortname) && *(*shortname) == '/')
|
|
(*shortname)++;
|
|
|
|
free(parent);
|
|
|
|
return (Directory *) parentDirectory;
|
|
}
|
|
|
|
/* return values:
|
|
-1 -> error
|
|
0 -> no error, but nothing updated
|
|
1 -> no error, and stuff updated
|
|
*/
|
|
static int updatePath(char *utf8path)
|
|
{
|
|
Directory *directory;
|
|
Directory *parentDirectory;
|
|
Song *song;
|
|
char *shortname;
|
|
char *path = sanitizePathDup(utf8path);
|
|
time_t mtime;
|
|
int ret = 0;
|
|
|
|
if (NULL == path)
|
|
return -1;
|
|
|
|
/* if path is in the DB try to update it, or else delete it */
|
|
if ((directory = getDirectoryDetails(path, &shortname))) {
|
|
parentDirectory = directory->parent;
|
|
|
|
/* if this update directory is successfull, we are done */
|
|
if ((ret = updateDirectory(directory)) >= 0) {
|
|
free(path);
|
|
sortDirectory(directory);
|
|
return ret;
|
|
}
|
|
/* we don't want to delete the root directory */
|
|
else if (directory == mp3rootDirectory) {
|
|
free(path);
|
|
return 0;
|
|
}
|
|
/* if updateDirectory fials, means we should delete it */
|
|
else {
|
|
LOG("removing directory: %s\n", path);
|
|
deleteFromList(parentDirectory->subDirectories,
|
|
shortname);
|
|
ret = 1;
|
|
/* don't return, path maybe a song now */
|
|
}
|
|
} else if ((song = getSongDetails(path, &shortname, &parentDirectory))) {
|
|
if (!parentDirectory->stat
|
|
&& statDirectory(parentDirectory) < 0) {
|
|
free(path);
|
|
return 0;
|
|
}
|
|
/* if this song update is successfull, we are done */
|
|
else if (0 == inodeFoundInParent(parentDirectory->parent,
|
|
parentDirectory->stat->inode,
|
|
parentDirectory->stat->device)
|
|
&& song && isMusic(getSongUrl(song), &mtime, 0)) {
|
|
free(path);
|
|
if (song->mtime == mtime)
|
|
return 0;
|
|
else if (updateSongInfo(song) == 0)
|
|
return 1;
|
|
else {
|
|
removeSongFromDirectory(parentDirectory,
|
|
shortname);
|
|
return 1;
|
|
}
|
|
}
|
|
/* if updateDirectory fials, means we should delete it */
|
|
else {
|
|
removeSongFromDirectory(parentDirectory, shortname);
|
|
ret = 1;
|
|
/* don't return, path maybe a directory now */
|
|
}
|
|
}
|
|
|
|
/* path not found in the db, see if it actually exists on the fs.
|
|
* Also, if by chance a directory was replaced by a file of the same
|
|
* name or vice versa, we need to add it to the db
|
|
*/
|
|
if (isDir(path) || isMusic(path, NULL, 0)) {
|
|
parentDirectory = addParentPathToDB(path, &shortname);
|
|
if (!parentDirectory || (!parentDirectory->stat &&
|
|
statDirectory(parentDirectory) < 0)) {
|
|
} else if (0 == inodeFoundInParent(parentDirectory->parent,
|
|
parentDirectory->stat->inode,
|
|
parentDirectory->stat->
|
|
device)
|
|
&& addToDirectory(parentDirectory, shortname, path)
|
|
> 0) {
|
|
ret = 1;
|
|
}
|
|
}
|
|
|
|
free(path);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* return values:
|
|
-1 -> error
|
|
0 -> no error, but nothing updated
|
|
1 -> no error, and stuff updated
|
|
*/
|
|
static int updateDirectory(Directory * directory)
|
|
{
|
|
DIR *dir;
|
|
char cwd[2];
|
|
struct dirent *ent;
|
|
char *s;
|
|
char *utf8;
|
|
char *dirname = getDirectoryPath(directory);
|
|
int ret = 0;
|
|
|
|
{
|
|
if (!directory->stat && statDirectory(directory) < 0) {
|
|
return -1;
|
|
} else if (inodeFoundInParent(directory->parent,
|
|
directory->stat->inode,
|
|
directory->stat->device)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
cwd[0] = '.';
|
|
cwd[1] = '\0';
|
|
if (dirname == NULL)
|
|
dirname = cwd;
|
|
|
|
if ((dir = opendir(rmp2amp(utf8ToFsCharset(dirname)))) == NULL)
|
|
return -1;
|
|
|
|
if (removeDeletedFromDirectory(directory, dir) > 0)
|
|
ret = 1;
|
|
|
|
rewinddir(dir);
|
|
|
|
while ((ent = readdir(dir))) {
|
|
if (ent->d_name[0] == '.')
|
|
continue; /* hide hidden stuff */
|
|
if (strchr(ent->d_name, '\n'))
|
|
continue;
|
|
|
|
utf8 = fsCharsetToUtf8(ent->d_name);
|
|
|
|
if (!utf8)
|
|
continue;
|
|
|
|
utf8 = strdup(utf8);
|
|
|
|
if (directory->path) {
|
|
s = malloc(strlen(getDirectoryPath(directory)) +
|
|
strlen(utf8) + 2);
|
|
sprintf(s, "%s/%s", getDirectoryPath(directory), utf8);
|
|
} else
|
|
s = strdup(utf8);
|
|
if (updateInDirectory(directory, utf8, s) > 0)
|
|
ret = 1;
|
|
free(utf8);
|
|
free(s);
|
|
}
|
|
|
|
closedir(dir);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* return values:
|
|
-1 -> error
|
|
0 -> no error, but nothing found
|
|
1 -> no error, and stuff found
|
|
*/
|
|
static int exploreDirectory(Directory * directory)
|
|
{
|
|
DIR *dir;
|
|
char cwd[2];
|
|
struct dirent *ent;
|
|
char *s;
|
|
char *utf8;
|
|
char *dirname = getDirectoryPath(directory);
|
|
int ret = 0;
|
|
|
|
cwd[0] = '.';
|
|
cwd[1] = '\0';
|
|
if (dirname == NULL)
|
|
dirname = cwd;
|
|
|
|
DEBUG("explore: attempting to opendir: %s\n", dirname);
|
|
if ((dir = opendir(rmp2amp(utf8ToFsCharset(dirname)))) == NULL)
|
|
return -1;
|
|
|
|
DEBUG("explore: %s\n", dirname);
|
|
while ((ent = readdir(dir))) {
|
|
if (ent->d_name[0] == '.')
|
|
continue; /* hide hidden stuff */
|
|
if (strchr(ent->d_name, '\n'))
|
|
continue;
|
|
|
|
utf8 = fsCharsetToUtf8(ent->d_name);
|
|
|
|
if (!utf8)
|
|
continue;
|
|
|
|
utf8 = strdup(utf8);
|
|
|
|
DEBUG("explore: found: %s (%s)\n", ent->d_name, utf8);
|
|
|
|
if (directory->path) {
|
|
s = malloc(strlen(getDirectoryPath(directory)) +
|
|
strlen(utf8) + 2);
|
|
sprintf(s, "%s/%s", getDirectoryPath(directory), utf8);
|
|
} else
|
|
s = strdup(utf8);
|
|
if (addToDirectory(directory, utf8, s) > 0)
|
|
ret = 1;
|
|
free(utf8);
|
|
free(s);
|
|
}
|
|
|
|
closedir(dir);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int statDirectory(Directory * dir)
|
|
{
|
|
struct stat st;
|
|
|
|
if (myStat(getDirectoryPath(dir) ? getDirectoryPath(dir) : "", &st) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
dir->stat = newDirectoryStat(&st);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int inodeFoundInParent(Directory * parent, ino_t inode, dev_t device)
|
|
{
|
|
while (parent) {
|
|
if (!parent->stat) {
|
|
if (statDirectory(parent) < 0)
|
|
return -1;
|
|
}
|
|
if (parent->stat->inode == inode &&
|
|
parent->stat->device == device) {
|
|
DEBUG("recursive directory found\n");
|
|
return 1;
|
|
}
|
|
parent = parent->parent;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int addSubDirectoryToDirectory(Directory * directory, char *shortname,
|
|
char *name, struct stat *st)
|
|
{
|
|
Directory *subDirectory;
|
|
|
|
if (inodeFoundInParent(directory, st->st_ino, st->st_dev))
|
|
return 0;
|
|
|
|
subDirectory = newDirectory(name, directory);
|
|
subDirectory->stat = newDirectoryStat(st);
|
|
|
|
if (exploreDirectory(subDirectory) < 1) {
|
|
freeDirectory(subDirectory);
|
|
return 0;
|
|
}
|
|
|
|
insertInList(directory->subDirectories, shortname, subDirectory);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int addToDirectory(Directory * directory, char *shortname, char *name)
|
|
{
|
|
struct stat st;
|
|
|
|
if (myStat(name, &st)) {
|
|
DEBUG("failed to stat %s: %s\n", name, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
if (S_ISREG(st.st_mode) && hasMusicSuffix(name, 0)) {
|
|
Song *song;
|
|
song = addSongToList(directory->songs, shortname, name,
|
|
SONG_TYPE_FILE, directory);
|
|
if (!song)
|
|
return -1;
|
|
LOG("added %s\n", name);
|
|
return 1;
|
|
} else if (S_ISDIR(st.st_mode)) {
|
|
return addSubDirectoryToDirectory(directory, shortname, name,
|
|
&st);
|
|
}
|
|
|
|
DEBUG("addToDirectory: %s is not a directory or music\n", name);
|
|
|
|
return -1;
|
|
}
|
|
|
|
void closeMp3Directory()
|
|
{
|
|
freeDirectory(mp3rootDirectory);
|
|
}
|
|
|
|
static Directory *findSubDirectory(Directory * directory, char *name)
|
|
{
|
|
void *subDirectory;
|
|
char *dup = strdup(name);
|
|
char *key;
|
|
|
|
key = strtok(dup, "/");
|
|
if (!key) {
|
|
free(dup);
|
|
return NULL;
|
|
}
|
|
|
|
if (findInList(directory->subDirectories, key, &subDirectory)) {
|
|
free(dup);
|
|
return (Directory *) subDirectory;
|
|
}
|
|
|
|
free(dup);
|
|
return NULL;
|
|
}
|
|
|
|
static Directory *getSubDirectory(Directory * directory, char *name,
|
|
char **shortname)
|
|
{
|
|
Directory *subDirectory;
|
|
int len;
|
|
|
|
if (name == NULL || name[0] == '\0' || strcmp(name, "/") == 0) {
|
|
return directory;
|
|
}
|
|
|
|
if ((subDirectory = findSubDirectory(directory, name)) == NULL)
|
|
return NULL;
|
|
|
|
*shortname = name;
|
|
|
|
len = 0;
|
|
while (name[len] != '/' && name[len] != '\0')
|
|
len++;
|
|
while (name[len] == '/')
|
|
len++;
|
|
|
|
return getSubDirectory(subDirectory, &(name[len]), shortname);
|
|
}
|
|
|
|
static Directory *getDirectoryDetails(char *name, char **shortname)
|
|
{
|
|
*shortname = NULL;
|
|
|
|
return getSubDirectory(mp3rootDirectory, name, shortname);
|
|
}
|
|
|
|
static Directory *getDirectory(char *name)
|
|
{
|
|
char *shortname;
|
|
|
|
return getSubDirectory(mp3rootDirectory, name, &shortname);
|
|
}
|
|
|
|
static int printDirectoryList(int fd, DirectoryList * directoryList)
|
|
{
|
|
ListNode *node = directoryList->firstNode;
|
|
Directory *directory;
|
|
|
|
while (node != NULL) {
|
|
directory = (Directory *) node->data;
|
|
fdprintf(fd, "%s%s\n", DIRECTORY_DIR,
|
|
getDirectoryPath(directory));
|
|
node = node->nextNode;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int printDirectoryInfo(int fd, char *name)
|
|
{
|
|
Directory *directory;
|
|
|
|
if ((directory = getDirectory(name)) == NULL) {
|
|
commandError(fd, ACK_ERROR_NO_EXIST, "directory not found",
|
|
NULL);
|
|
return -1;
|
|
}
|
|
|
|
printDirectoryList(fd, directory->subDirectories);
|
|
printSongInfoFromList(fd, directory->songs);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void writeDirectoryInfo(FILE * fp, Directory * directory)
|
|
{
|
|
ListNode *node = (directory->subDirectories)->firstNode;
|
|
Directory *subDirectory;
|
|
|
|
if (directory->path) {
|
|
myfprintf(fp, "%s%s\n", DIRECTORY_BEGIN,
|
|
getDirectoryPath(directory));
|
|
}
|
|
|
|
while (node != NULL) {
|
|
subDirectory = (Directory *) node->data;
|
|
myfprintf(fp, "%s%s\n", DIRECTORY_DIR, node->key);
|
|
writeDirectoryInfo(fp, subDirectory);
|
|
node = node->nextNode;
|
|
}
|
|
|
|
writeSongInfoFromList(fp, directory->songs);
|
|
|
|
if (directory->path) {
|
|
myfprintf(fp, "%s%s\n", DIRECTORY_END,
|
|
getDirectoryPath(directory));
|
|
}
|
|
}
|
|
|
|
static void readDirectoryInfo(FILE * fp, Directory * directory)
|
|
{
|
|
char buffer[MAXPATHLEN * 2];
|
|
int bufferSize = MAXPATHLEN * 2;
|
|
char *key;
|
|
Directory *subDirectory;
|
|
int strcmpRet;
|
|
char *name;
|
|
ListNode *nextDirNode = directory->subDirectories->firstNode;
|
|
ListNode *nodeTemp;
|
|
|
|
while (myFgets(buffer, bufferSize, fp)
|
|
&& 0 != strncmp(DIRECTORY_END, buffer, strlen(DIRECTORY_END))) {
|
|
if (0 == strncmp(DIRECTORY_DIR, buffer, strlen(DIRECTORY_DIR))) {
|
|
key = strdup(&(buffer[strlen(DIRECTORY_DIR)]));
|
|
if (myFgets(buffer, bufferSize, fp) < 0) {
|
|
ERROR("Error reading db, fgets\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
/* for compatibility with db's prior to 0.11 */
|
|
if (0 == strncmp(DIRECTORY_MTIME, buffer,
|
|
strlen(DIRECTORY_MTIME))) {
|
|
if (myFgets(buffer, bufferSize, fp) < 0) {
|
|
ERROR("Error reading db, fgets\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
if (strncmp
|
|
(DIRECTORY_BEGIN, buffer,
|
|
strlen(DIRECTORY_BEGIN))) {
|
|
ERROR("Error reading db at line: %s\n", buffer);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
name = strdup(&(buffer[strlen(DIRECTORY_BEGIN)]));
|
|
|
|
while (nextDirNode && (strcmpRet =
|
|
strcmp(key,
|
|
nextDirNode->key)) > 0) {
|
|
nodeTemp = nextDirNode->nextNode;
|
|
deleteNodeFromList(directory->subDirectories,
|
|
nextDirNode);
|
|
nextDirNode = nodeTemp;
|
|
}
|
|
|
|
if (NULL == nextDirNode) {
|
|
subDirectory = newDirectory(name, directory);
|
|
insertInList(directory->subDirectories,
|
|
key, (void *)subDirectory);
|
|
} else if (strcmpRet == 0) {
|
|
subDirectory = (Directory *) nextDirNode->data;
|
|
nextDirNode = nextDirNode->nextNode;
|
|
} else {
|
|
subDirectory = newDirectory(name, directory);
|
|
insertInListBeforeNode(directory->
|
|
subDirectories,
|
|
nextDirNode, -1, key,
|
|
(void *)subDirectory);
|
|
}
|
|
|
|
free(name);
|
|
free(key);
|
|
readDirectoryInfo(fp, subDirectory);
|
|
} else if (0 == strncmp(SONG_BEGIN, buffer, strlen(SONG_BEGIN))) {
|
|
readSongInfoIntoList(fp, directory->songs, directory);
|
|
} else {
|
|
ERROR("Unknown line in db: %s\n", buffer);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
while (nextDirNode) {
|
|
nodeTemp = nextDirNode->nextNode;
|
|
deleteNodeFromList(directory->subDirectories, nextDirNode);
|
|
nextDirNode = nodeTemp;
|
|
}
|
|
}
|
|
|
|
static void sortDirectory(Directory * directory)
|
|
{
|
|
ListNode *node = directory->subDirectories->firstNode;
|
|
Directory *subDir;
|
|
|
|
sortList(directory->subDirectories);
|
|
sortList(directory->songs);
|
|
|
|
while (node != NULL) {
|
|
subDir = (Directory *) node->data;
|
|
sortDirectory(subDir);
|
|
node = node->nextNode;
|
|
}
|
|
}
|
|
|
|
int checkDirectoryDB()
|
|
{
|
|
struct stat st;
|
|
char *dbFile;
|
|
char *dirPath;
|
|
char *dbPath;
|
|
|
|
dbFile = getDbFile();
|
|
|
|
/* Check if the file exists */
|
|
if (access(dbFile, F_OK)) {
|
|
/* 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 */
|
|
dbPath = strdup(dbFile);
|
|
dirPath = dirname(dbPath);
|
|
|
|
/* Check that the parent part of the path is a directory */
|
|
if (stat(dirPath, &st) < 0) {
|
|
ERROR("Couldn't stat parent directory of db file "
|
|
"\"%s\": %s\n", dbFile, strerror(errno));
|
|
free(dbPath);
|
|
return -1;
|
|
}
|
|
|
|
if (!S_ISDIR(st.st_mode)) {
|
|
ERROR("Couldn't create db file \"%s\" because the "
|
|
"parent path is not a directory\n", dbFile);
|
|
free(dbPath);
|
|
return -1;
|
|
}
|
|
|
|
/* Check if we can write to the directory */
|
|
if (access(dirPath, R_OK | W_OK)) {
|
|
ERROR("Can't create db file in \"%s\": %s\n", dirPath,
|
|
strerror(errno));
|
|
free(dbPath);
|
|
return -1;
|
|
|
|
}
|
|
|
|
free(dbPath);
|
|
return 0;
|
|
}
|
|
|
|
/* Path exists, now check if it's a regular file */
|
|
if (stat(dbFile, &st) < 0) {
|
|
ERROR("Couldn't stat db file \"%s\": %s\n", dbFile,
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
if (!S_ISREG(st.st_mode)) {
|
|
ERROR("db file \"%s\" is not a regular file\n", dbFile);
|
|
return -1;
|
|
}
|
|
|
|
/* And check that we can write to it */
|
|
if (access(dbFile, R_OK | W_OK)) {
|
|
ERROR("Can't open db file \"%s\" for reading/writing: %s\n",
|
|
dbFile, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int writeDirectoryDB()
|
|
{
|
|
FILE *fp;
|
|
char *dbFile = getDbFile();
|
|
|
|
DEBUG("removing empty directories from DB\n");
|
|
deleteEmptyDirectoriesInDirectory(mp3rootDirectory);
|
|
|
|
DEBUG("sorting DB\n");
|
|
|
|
sortDirectory(mp3rootDirectory);
|
|
|
|
DEBUG("writing DB\n");
|
|
|
|
while (!(fp = fopen(dbFile, "w")) && errno == EINTR) ;
|
|
if (!fp) {
|
|
ERROR("unable to write to db file \"%s\": %s\n",
|
|
dbFile, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* block signals when writing the db so we don't get a corrupted db */
|
|
myfprintf(fp, "%s\n", DIRECTORY_INFO_BEGIN);
|
|
myfprintf(fp, "%s%s\n", DIRECTORY_MPD_VERSION, VERSION);
|
|
myfprintf(fp, "%s%s\n", DIRECTORY_FS_CHARSET, getFsCharset());
|
|
myfprintf(fp, "%s\n", DIRECTORY_INFO_END);
|
|
|
|
writeDirectoryInfo(fp, mp3rootDirectory);
|
|
|
|
while (fclose(fp) && errno == EINTR) ;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int readDirectoryDB()
|
|
{
|
|
FILE *fp = NULL;
|
|
char *dbFile = getDbFile();
|
|
struct stat st;
|
|
|
|
if (!mp3rootDirectory)
|
|
mp3rootDirectory = newDirectory(NULL, NULL);
|
|
while (!(fp = fopen(dbFile, "r")) && errno == EINTR) ;
|
|
if (fp == NULL) {
|
|
ERROR("unable open db file \"%s\": %s\n",
|
|
dbFile, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* get initial info */
|
|
{
|
|
char buffer[100];
|
|
int bufferSize = 100;
|
|
int foundFsCharset = 0;
|
|
int foundVersion = 0;
|
|
|
|
if (myFgets(buffer, bufferSize, fp) < 0) {
|
|
ERROR("Error reading db, fgets\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
if (0 == strcmp(DIRECTORY_INFO_BEGIN, buffer)) {
|
|
while (myFgets(buffer, bufferSize, fp) &&
|
|
0 != strcmp(DIRECTORY_INFO_END, buffer)) {
|
|
if (0 == strncmp(DIRECTORY_MPD_VERSION, buffer,
|
|
strlen(DIRECTORY_MPD_VERSION)))
|
|
{
|
|
if (foundVersion) {
|
|
ERROR("already found "
|
|
"version in db\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
foundVersion = 1;
|
|
} else if (0 ==
|
|
strncmp(DIRECTORY_FS_CHARSET, buffer,
|
|
strlen
|
|
(DIRECTORY_FS_CHARSET))) {
|
|
char *fsCharset;
|
|
char *tempCharset;
|
|
|
|
if (foundFsCharset) {
|
|
WARNING("already found "
|
|
"fs charset in db\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
foundFsCharset = 1;
|
|
|
|
fsCharset =
|
|
&(buffer
|
|
[strlen(DIRECTORY_FS_CHARSET)]);
|
|
if ((tempCharset =
|
|
getConfigParamValue
|
|
(CONF_FS_CHARSET))
|
|
&& strcmp(fsCharset, tempCharset)) {
|
|
WARNING("Using \"%s\" for the "
|
|
"filesystem charset "
|
|
"instead of \"%s\"\n",
|
|
fsCharset, tempCharset);
|
|
WARNING("maybe you need to "
|
|
"recreate the db?\n");
|
|
setFsCharset(fsCharset);
|
|
}
|
|
} else {
|
|
ERROR
|
|
("directory: unknown line in db info: %s\n",
|
|
buffer);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
} else {
|
|
ERROR("db info not found in db file\n");
|
|
ERROR("you should recreate the db using --create-db\n");
|
|
fseek(fp, 0, SEEK_SET);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
DEBUG("reading DB\n");
|
|
|
|
readDirectoryInfo(fp, mp3rootDirectory);
|
|
while (fclose(fp) && errno == EINTR) ;
|
|
|
|
stats.numberOfSongs = countSongsIn(STDERR_FILENO, NULL);
|
|
stats.dbPlayTime = sumSongTimesIn(STDERR_FILENO, NULL);
|
|
|
|
if (stat(dbFile, &st) == 0)
|
|
directory_dbModTime = st.st_mtime;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void updateMp3Directory()
|
|
{
|
|
switch (updateDirectory(mp3rootDirectory)) {
|
|
case 0:
|
|
/* nothing updated */
|
|
return;
|
|
case 1:
|
|
if (writeDirectoryDB() < 0) {
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
break;
|
|
default:
|
|
/* something was updated and db should be written */
|
|
ERROR("problems updating music db\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
static int traverseAllInSubDirectory(int fd, Directory * directory,
|
|
int (*forEachSong) (int, Song *,
|
|
void *),
|
|
int (*forEachDir) (int, Directory *,
|
|
void *), void *data)
|
|
{
|
|
ListNode *node = directory->songs->firstNode;
|
|
Song *song;
|
|
Directory *dir;
|
|
int errFlag = 0;
|
|
|
|
if (forEachDir) {
|
|
errFlag = forEachDir(fd, directory, data);
|
|
if (errFlag)
|
|
return errFlag;
|
|
}
|
|
|
|
if (forEachSong) {
|
|
while (node != NULL && !errFlag) {
|
|
song = (Song *) node->data;
|
|
errFlag = forEachSong(fd, song, data);
|
|
node = node->nextNode;
|
|
}
|
|
if (errFlag)
|
|
return errFlag;
|
|
}
|
|
|
|
node = directory->subDirectories->firstNode;
|
|
|
|
while (node != NULL && !errFlag) {
|
|
dir = (Directory *) node->data;
|
|
errFlag = traverseAllInSubDirectory(fd, dir, forEachSong,
|
|
forEachDir, data);
|
|
node = node->nextNode;
|
|
}
|
|
|
|
return errFlag;
|
|
}
|
|
|
|
int traverseAllIn(int fd, char *name,
|
|
int (*forEachSong) (int, Song *, void *),
|
|
int (*forEachDir) (int, Directory *, void *), void *data)
|
|
{
|
|
Directory *directory;
|
|
|
|
if ((directory = getDirectory(name)) == NULL) {
|
|
Song *song;
|
|
if ((song = getSongFromDB(name)) && forEachSong) {
|
|
return forEachSong(fd, song, data);
|
|
}
|
|
commandError(fd, ACK_ERROR_NO_EXIST,
|
|
"directory or file not found", NULL);
|
|
return -1;
|
|
}
|
|
|
|
return traverseAllInSubDirectory(fd, directory, forEachSong, forEachDir,
|
|
data);
|
|
}
|
|
|
|
static void freeAllDirectoryStats(Directory * directory)
|
|
{
|
|
ListNode *node = directory->subDirectories->firstNode;
|
|
|
|
while (node != NULL) {
|
|
freeAllDirectoryStats((Directory *) node->data);
|
|
node = node->nextNode;
|
|
}
|
|
|
|
freeDirectoryStatFromDirectory(directory);
|
|
}
|
|
|
|
void initMp3Directory()
|
|
{
|
|
struct stat st;
|
|
|
|
mp3rootDirectory = newDirectory(NULL, NULL);
|
|
exploreDirectory(mp3rootDirectory);
|
|
freeAllDirectoryStats(mp3rootDirectory);
|
|
stats.numberOfSongs = countSongsIn(STDERR_FILENO, NULL);
|
|
stats.dbPlayTime = sumSongTimesIn(STDERR_FILENO, NULL);
|
|
|
|
if (stat(getDbFile(), &st) == 0)
|
|
directory_dbModTime = st.st_mtime;
|
|
}
|
|
|
|
static Song *getSongDetails(char *file, char **shortnameRet,
|
|
Directory ** directoryRet)
|
|
{
|
|
void *song = NULL;
|
|
Directory *directory;
|
|
char *dir = NULL;
|
|
char *dup = strdup(file);
|
|
char *shortname = dup;
|
|
char *c = strtok(dup, "/");
|
|
|
|
DEBUG("get song: %s\n", file);
|
|
|
|
while (c) {
|
|
shortname = c;
|
|
c = strtok(NULL, "/");
|
|
}
|
|
|
|
if (shortname != dup) {
|
|
for (c = dup; c < shortname - 1; c++) {
|
|
if (*c == '\0')
|
|
*c = '/';
|
|
}
|
|
dir = dup;
|
|
}
|
|
|
|
if (!(directory = getDirectory(dir))) {
|
|
free(dup);
|
|
return NULL;
|
|
}
|
|
|
|
if (!findInList(directory->songs, shortname, &song)) {
|
|
free(dup);
|
|
return NULL;
|
|
}
|
|
|
|
free(dup);
|
|
if (shortnameRet)
|
|
*shortnameRet = shortname;
|
|
if (directoryRet)
|
|
*directoryRet = directory;
|
|
return (Song *) song;
|
|
}
|
|
|
|
Song *getSongFromDB(char *file)
|
|
{
|
|
return getSongDetails(file, NULL, NULL);
|
|
}
|
|
|
|
time_t getDbModTime()
|
|
{
|
|
return directory_dbModTime;
|
|
}
|