
// Project: webCDwriter - Network CD Writing
// File: Store.cpp
// Author: Jörg Haeger

#include <dirent.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#ifdef FreeBSD
#include <sys/param.h>
#include <sys/mount.h>
#else
#include <sys/vfs.h>
#endif
#include "File.h"
#include "Config.h"
#include "Server.h"
#include "Session.h"
#include "Store.h"
#include "Queue.h"

Store::Store()
{
	pthread_mutex_init(&mutex, NULL);

	spoolDir = new File(config.getSpoolDir());

	maxOpenSessions = config.getMaxOpenSessions();
	sessionsNum = 0;
	sessions = new (Session *)[maxOpenSessions];
	int i;
	for (i = 0; i < maxOpenSessions; i++)
		sessions[i] = NULL;

	queue = new Queue(config.getMaxOpenConnections());

	// get blocksize of filesystem
	struct statfs statfsBuf;
	statfs(config.getSpoolDir(), &statfsBuf);
	fsBlockSize = statfsBuf.f_bsize;

	long long n = 1024 * 1024 * config.getReservedMBytes();
	n /= fsBlockSize;
	reservedBlocks = n;

	maxSizeInKBytes = 1024 * config.getMaxMBytesInSpoolDir();
	sizeInKBytes = 0;

	if (!config.getImageOnTheFly())
	{
		FILE *imageFile = fopen(config.getImageFile(), "w");
		if (imageFile == NULL)
			throw "can't create imageFile";
		fclose(imageFile);

		struct stat statBuf;
		if (stat(config.getImageFile(), &statBuf) != 0)
			throw "can't stat imageFile";
		imageFileDevice = statBuf.st_dev;
		if (stat(config.getSpoolDir(), &statBuf) != 0)
			throw "can't stat spoolDir";
		spoolDirDevice = statBuf.st_dev;
		log.put("imageFileDevice = %d, spoolDirDevice = %d",
			(int)imageFileDevice, (int)spoolDirDevice);

		if (imageFileDevice == spoolDirDevice)
			maxSizeInKBytes -=
				1024 * config.getMaxMBytesPerSession();
	}

	DIR *dir = opendir(config.getSpoolDir());
	if (dir == NULL)
		throw "spoolDir does not exist";

	sessionsTotal = 0;
	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL)
	{
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		File dir(*spoolDir, dirEntry->d_name);
		if (!Session::isSessionDir(dir))
			continue;

		sessionsTotal++;
		sizeInKBytes += Session::getSessionSize(&dir);
	}

	closedir(dir);

	log.put("sessionsTotal=%d, sizeInKBytes = %d, reservedBlocks = %d",
		sessionsTotal, sizeInKBytes, reservedBlocks);
}

Store::~Store()
{
}

void Store::deleteOldestSession()
{
	DIR *dir = opendir(spoolDir->getName());
	if (dir == NULL)
		throw "can't open spoolDir";

	File *oldestFile = NULL;
	int t;
	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL)
	{
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		if (isSessionIDInUse(dirEntry->d_name))
			continue;

		File sessionDir(spoolDir, dirEntry->d_name);
		if (!Session::isSessionDir(sessionDir))
			continue;

		struct stat statBuf;
		if (stat(sessionDir.getName(), &statBuf) != 0)
			continue;

		if (oldestFile == NULL || statBuf.st_mtime < t)
		{
			if (oldestFile != NULL)
				delete oldestFile;
			oldestFile = new File(spoolDir, dirEntry->d_name);
			t = statBuf.st_mtime;
		}
	}

	closedir(dir);

	if (oldestFile != NULL)
	{
		removeDir2(oldestFile->getName());
		delete oldestFile;
		sessionsTotal--;
	}
	else
		throw "out of disk space";
}

void Store::freeSpace()
{
	while (1)
	{
		struct statfs statfsBuf;
		statfs(config.getSpoolDir(), &statfsBuf);

		long free = statfsBuf.f_bavail;
		long reserved = reservedBlocks
			+ getReservedBlocksForImageFile();

		if (free > reserved
			&& sizeInKBytes <= maxSizeInKBytes)
			break;

		log.putv("freeSpace: free = %d, reserved = %d",
			free, reserved);
		if (sizeInKBytes > maxSizeInKBytes)
			log.putv("sizeInKBytes = %d, maxSizeInKBytes = %d",
				sizeInKBytes, maxSizeInKBytes);

		deleteOldestSession();
	}
}

int Store::getReservedBlocksForImageFile()
{
	if (config.getImageOnTheFly()
		|| imageFileDevice != spoolDirDevice)
		return 0;

	long n = 1024 * 1024 * config.getMaxMBytesPerSession();
	n /= fsBlockSize;

	struct stat statBuf;
	if (stat(config.getImageFile(), &statBuf) == 0)
		n -= sizeToBlocks(statBuf.st_size);

	return n;
}

int Store::isSessionIDInUse(const char *sessionID)
{
	int i;
	for (i = 0; i < maxOpenSessions; i++)
		if (sessions[i] != NULL)
			if (strcmp(sessionID,
				sessions[i]->getID()) == 0)
				break;

	return i < maxOpenSessions;
}

File *Store::registerSession(Session *session)
{
	const char *sessionID = session->getID();
	if (isSessionIDInUse(sessionID))
		throw "sessionID already in use";

	File *sessionDir = new File(config.getSpoolDir(), sessionID);

	if (sessionDir->exists()
		&& !Session::isSessionDir(*sessionDir))
	{
		delete sessionDir;
		throw "cannot get sessionDir";
	}

	queue->add();
	try {
		register2(session);
		if (!sessionDir->exists())
			sessionsTotal++;
		return sessionDir;
	}
	catch (const char *msg) {
		queue->remove();
		delete sessionDir;
		throw msg;
	}
}

void Store::register2(Session *session)
{
	int cancel = session->getServer()->waitForService(
		queue, maxOpenSessions - 1);
	if (cancel)
		throw "wait for session cancelled";

	pthread_mutex_lock(&mutex);
	try {
		register3(session);
		pthread_mutex_unlock(&mutex);
	}
	catch (const char *msg) {
		pthread_mutex_unlock(&mutex);
		throw msg;
	}
}

void Store::register3(Session *session)
{
	if (isSessionIDInUse(session->getID()))
		throw "sessionID already in use";

	int i;
	for (i = 0; i < maxOpenSessions && sessions[i] != NULL; i++);
	if (i < maxOpenSessions)
	{
		sessions[i] = session;
		sessionsNum++;
	}
	else
		throw "internal error in Store::register3";
}

void Store::removeDir(const char *dirName)
{
	try {
		pthread_mutex_lock(&mutex);
		removeDir2(dirName);
		pthread_mutex_unlock(&mutex);
	}
	catch (const char *msg)
	{
		pthread_mutex_unlock(&mutex);
		throw msg;
	}
}

void Store::removeDir2(const char *dirName)
{
	log.put("remove(%s)", dirName);
	File dir(dirName);
	sizeInKBytes -= Session::getSessionSize(&dir);
	removeFile(dirName);
	log.putv("remove(%s) done", dirName);
}

void Store::removeFile(const char *pathName)
{
	while (Server::getWriterStatus() == writing
		&& Server::getFifoPercent() < 100)
		sleep(1);

	struct stat statBuf;
	if (lstat(pathName, &statBuf) != 0)
		return;

	if (!S_ISDIR(statBuf.st_mode))
	{
		unlink(pathName);
		return;
	}

	DIR *dir = opendir(pathName);
	if (dir == NULL)
		return;

	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL)
	{
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		char tmpPathName[strlen(pathName) + 1
			+ strlen(dirEntry->d_name) + 1];
		sprintf(tmpPathName, "%s/%s",
			pathName, dirEntry->d_name);
		removeFile(tmpPathName);
	}

	closedir(dir);
	rmdir(pathName);
}

int Store::sizeToBlocks(long size)
{
	long blocks = size / fsBlockSize;
	if (blocks * fsBlockSize < size)
		blocks++;
	return blocks;
}

void Store::unregister(Session *session)
{
	int i;
	for (i = 0; i < maxOpenSessions && sessions[i] != session; i++);
	if (i < maxOpenSessions)
	{
		sessions[i] = NULL;
		sessionsNum--;
	}
	else
		log.put("internal error in Store::unregister");

	queue->remove();
}

void Store::write(const char *buf, int size, int fd)
{
	pthread_mutex_lock(&mutex);
	try {
		while (Server::getWriterStatus() == writing
			&& Server::getFifoPercent() < 100)
			sleep(1);

		if (size > 0)
			sizeInKBytes += (size - 1) / 1024 + 1;
		freeSpace();
		while (size > 0)
		{
			int n = ::write(fd, buf, size);
			if (n < 0)
			if (errno == EBADF)
				throw "bad file descriptor";
			else
			if (errno == EFBIG)
				throw "file to big";
			else
			if (errno == EIO)
				throw "hardware error";
			else
			if (errno == ENOSPC)
			{
				freeSpace();
				continue;
			}
			else
				throw "unknown error";
			buf += n;
			size -= n;
		}
		pthread_mutex_unlock(&mutex);
	}
	catch (const char *msg)
	{
		pthread_mutex_unlock(&mutex);
		throw msg;
	}
}

Store *store;
