/*
 *
 *	Set disk quota from command line 
 *
 *	Jan Kara <jack@suse.cz> - sponsored by SuSE CR
 */

#include "config.h"

#if defined(RPC)
#include <rpc/rpc.h>
#endif
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <getopt.h>
#include <time.h>
#include <ctype.h>
#include <stdlib.h>
#include <libgen.h>

#if defined(RPC)
#include "rquota.h"
#include "rquota_client.h"
#endif
#include "pot.h"
#include "quotaops.h"
#include "common.h"
#include "quotasys.h"

#define FL_USER 1
#define FL_GROUP 2
#define FL_RPC 4
#define FL_ALL 8
#define FL_PROTO 16
#define FL_GRACE 32
#define FL_INDIVIDUAL_GRACE 64
#define FL_BATCH 128
#define FL_NUMNAMES 256
#define FL_NO_MIXED_PATHS 512
#define FL_CONTINUE_BATCH 1024
#define FL_PROJECT 2048

static int flags, fmt = -1;
static char **mnt;
char *progname;
static int mntcnt;
static qid_t protoid, id;
static struct util_dqblk toset;

/* Print usage information */
static void usage(void)
{
#if defined(RPC_SETQUOTA)
	char *ropt = "[-rm] ";
#else
	char *ropt = "";
#endif
	errstr(_("Usage:\n\
  setquota [-u|-g|-P] %1$s[-F quotaformat] <user|group|project>\n\
\t<block-softlimit> <block-hardlimit> <inode-softlimit> <inode-hardlimit> -a|<filesystem>...\n\
  setquota [-u|-g|-P] %1$s[-F quotaformat] <-p protouser|protogroup|protoproject> <user|group|project> -a|<filesystem>...\n\
  setquota [-u|-g|-P] %1$s[-F quotaformat] -b [-c] -a|<filesystem>...\n\
  setquota [-u|-g|-P] [-F quotaformat] -t <blockgrace> <inodegrace> -a|<filesystem>...\n\
  setquota [-u|-g|-P] [-F quotaformat] <user|group|project> -T <blockgrace> <inodegrace> -a|<filesystem>...\n\n\
-u, --user                 set limits for user\n\
-g, --group                set limits for group\n\
-P, --project              set limits for project\n\
-a, --all                  set limits for all filesystems\n\
    --always-resolve       always try to resolve name, even if is\n\
                           composed only of digits\n\
-F, --format=formatname    operate on specific quota format\n\
-p, --prototype=protoname  copy limits from user/group/project\n\
-b, --batch                read limits from standard input\n\
-c, --continue-batch       continue in input processing in case of an error\n"), ropt);
#if defined(RPC_SETQUOTA)
	fputs(_("-r, --remote               set remote quota (via RPC)\n\
-m, --no-mixed-pathnames      trim leading slashes from NFSv4 mountpoints\n"), stderr);
#endif
	fputs(_("-t, --edit-period          edit grace period\n\
-T, --edit-times           edit grace times for user/group/project\n\
-h, --help                 display this help text and exit\n\
-V, --version              display version information and exit\n\n"), stderr);
	fprintf(stderr, _("Bugs to: %s\n"), PACKAGE_BUGREPORT);
	exit(1);
}

/* Convert string to number - print errstr message in case of failure */
static qsize_t parse_unum(char *str, char *msg)
{
	char *errch;
	qsize_t ret = strtoull(str, &errch, 0);

	if (*errch) {
		errstr(_("%s: %s\n"), msg, str);
		usage();
	}
	return ret;
}

/* Convert block size to number - print errstr message in case of failure */
static qsize_t parse_blocksize(const char *str, const char *msg)
{
	qsize_t ret;
	const char *error = str2space(str, &ret);

	if (error) {
		errstr(_("%s: %s: %s\n"), msg, str, error);
		usage();
	}
	return ret;
}

/* Convert inode count to number - print errstr message in case of failure */
static qsize_t parse_inodecount(const char *str, const char *msg)
{
	qsize_t ret;
	const char *error = str2number(str, &ret);

	if (error) {
		errstr(_("%s: %s: %s\n"), msg, str, error);
		usage();
	}
	return ret;
}

/* Convert our flags to quota type */
static inline int flag2type(int flags)
{
	if (flags & FL_USER)
		return USRQUOTA;
	if (flags & FL_GROUP)
		return GRPQUOTA;
	if (flags & FL_PROJECT)
		return PRJQUOTA;
	return -1;
}

/* Parse options of setquota */
static void parse_options(int argcnt, char **argstr)
{
	int ret, otherargs;
	char *protoname = NULL;
	int type = 0;
#ifdef RPC_SETQUOTA
	char *opts = "ghp:uPrmVF:taTbc";
#else
	char *opts = "ghp:uPVF:taTbc";
#endif
	struct option long_opts[] = {
		{ "user", 0, NULL, 'u' },
		{ "group", 0, NULL, 'g' },
		{ "project", 0, NULL, 'P' },
		{ "prototype", 1, NULL, 'p' },
#ifdef RPC_SETQUOTA
		{ "remote", 0, NULL, 'r' },
		{ "no-mixed-pathnames", 0, NULL, 'm' },
#endif
		{ "all", 0, NULL, 'a' },
		{ "always-resolve", 0, NULL, 256},
		{ "edit-period", 0, NULL, 't' },
		{ "edit-times", 0, NULL, 'T' },
		{ "batch", 0, NULL, 'b' },
		{ "continue", 0, NULL, 'c' },
		{ "format", 1, NULL, 'F' },
		{ "version", 0, NULL, 'V' },
		{ "help", 0, NULL, 'h' },
		{ NULL, 0, NULL, 0 }
	};

	while ((ret = getopt_long(argcnt, argstr, opts, long_opts, NULL)) != -1) {
		switch (ret) {
		  case '?':
		  case 'h':
			  usage();
		  case 'g':
			  flags |= FL_GROUP;
			  break;
		  case 'u':
			  flags |= FL_USER;
			  break;
		  case 'P':
			  flags |= FL_PROJECT;
			  break;
		  case 'p':
			  flags |= FL_PROTO;
			  protoname = optarg;
			  break;
		  case 'r':
			  flags |= FL_RPC;
			  break;
		  case 'm':
			  flags |= FL_NO_MIXED_PATHS;
			  break;
		  case 'a':
			  flags |= FL_ALL;
			  break;
		  case 256:
			  flags |= FL_NUMNAMES;
			  break;
		  case 't':
			  flags |= FL_GRACE;
			  break;
		  case 'b':
			  flags |= FL_BATCH;
			  break;
		  case 'c':
			  flags |= FL_CONTINUE_BATCH;
			  break;
		  case 'T':
			  flags |= FL_INDIVIDUAL_GRACE;
			  break;
		  case 'F':
			  if ((fmt = name2fmt(optarg)) == QF_ERROR)
				  exit(1);
			  break;
		  case 'V':
			  version();
			  exit(0);
		}
	}
	if (flags & FL_USER)
		type++;
	if (flags & FL_GROUP)
		type++;
	if (flags & FL_PROJECT)
		type++;
	if (type > 1) {
		errstr(_("Group/user/project quotas cannot be used together.\n"));
		usage();
	}
	if (flags & FL_PROTO && flags & FL_GRACE) {
		errstr(_("Prototype user has no sense when editing grace times.\n"));
		usage();
	}
	if (flags & FL_INDIVIDUAL_GRACE && flags & FL_GRACE) {
		errstr(_("Cannot set both individual and global grace time.\n"));
		usage();
	}
	if (flags & FL_BATCH && flags & (FL_GRACE | FL_INDIVIDUAL_GRACE)) {
		errstr(_("Batch mode cannot be used for setting grace times.\n"));
		usage();
	}
	if (flags & FL_BATCH && flags & FL_PROTO) {
		errstr(_("Batch mode and prototype user cannot be used together.\n"));
		usage();
	}
	if (flags & FL_RPC && (flags & (FL_GRACE | FL_INDIVIDUAL_GRACE))) {
		errstr(_("Cannot set grace times over RPC protocol.\n"));
		usage();
	}
	if (flags & FL_GRACE)
		otherargs = 2;
	else if (flags & FL_INDIVIDUAL_GRACE)
		otherargs = 3;
	else if (flags & FL_BATCH)
		otherargs = 0;
	else {
		otherargs = 1;
		if (!(flags & FL_PROTO))
			otherargs += 4;
	}
	if (optind + otherargs > argcnt) {
		errstr(_("Bad number of arguments.\n"));
		usage();
	}
	if (!(flags & (FL_USER | FL_GROUP | FL_PROJECT)))
		flags |= FL_USER;
	if (!(flags & (FL_GRACE | FL_BATCH))) {
		id = name2id(argstr[optind++], flag2type(flags), !!(flags & FL_NUMNAMES), NULL);
		if (!(flags & (FL_GRACE | FL_INDIVIDUAL_GRACE | FL_PROTO))) {
			toset.dqb_bsoftlimit = parse_blocksize(argstr[optind++], _("Bad block softlimit"));
			toset.dqb_bhardlimit = parse_blocksize(argstr[optind++], _("Bad block hardlimit"));
			toset.dqb_isoftlimit = parse_inodecount(argstr[optind++], _("Bad inode softlimit"));
			toset.dqb_ihardlimit = parse_inodecount(argstr[optind++], _("Bad inode hardlimit"));
		}
		else if (flags & FL_PROTO)
			protoid = name2id(protoname, flag2type(flags), !!(flags & FL_NUMNAMES), NULL);
	}
	if (flags & FL_GRACE) {
		toset.dqb_btime = parse_unum(argstr[optind++], _("Bad block grace time"));
		toset.dqb_itime = parse_unum(argstr[optind++], _("Bad inode grace time"));
	}
	else if (flags & FL_INDIVIDUAL_GRACE) {
		time_t now;

		time(&now);
		if (!strcmp(argstr[optind], _("unset"))) {
			toset.dqb_btime = 0;
			optind++;
		}
		else
			toset.dqb_btime = now + parse_unum(argstr[optind++], _("Bad block grace time"));
		if (!strcmp(argstr[optind], _("unset"))) {
			toset.dqb_itime = 0;
			optind++;
		}
		else
			toset.dqb_itime = now + parse_unum(argstr[optind++], _("Bad inode grace time"));
	}
	if (!(flags & FL_ALL)) {
		mntcnt = argcnt - optind;
		mnt = argstr + optind;
		if (!mntcnt) {
			errstr(_("Mountpoint not specified.\n"));
			usage();
		}
	}
}

/* Set user limits */
static int setlimits(struct quota_handle **handles)
{
	struct dquot *q, *protoq, *protoprivs = NULL, *curprivs;
	int ret = 0;

	curprivs = getprivs(id, handles, !!(flags & FL_ALL));
	if (!curprivs) {
		errstr(_("Error getting quota information to update.\n"));
		return -1;
	}
	if (flags & FL_PROTO) {
		protoprivs = getprivs(protoid, handles, 0);
		if (!protoprivs) {
			errstr(_("Error getting prototype quota information.\n"));
			ret = -1;
			goto out;
		}
		for (q = curprivs, protoq = protoprivs; q && protoq; q = q->dq_next, protoq = protoq->dq_next) {
			q->dq_dqb.dqb_bsoftlimit = protoq->dq_dqb.dqb_bsoftlimit;
			q->dq_dqb.dqb_bhardlimit = protoq->dq_dqb.dqb_bhardlimit;
			q->dq_dqb.dqb_isoftlimit = protoq->dq_dqb.dqb_isoftlimit;
			q->dq_dqb.dqb_ihardlimit = protoq->dq_dqb.dqb_ihardlimit;
			update_grace_times(q);
		}
		freeprivs(protoprivs);
	}
	else {
		for (q = curprivs; q; q = q->dq_next) {
			q->dq_dqb.dqb_bsoftlimit = toset.dqb_bsoftlimit;
			q->dq_dqb.dqb_bhardlimit = toset.dqb_bhardlimit;
			q->dq_dqb.dqb_isoftlimit = toset.dqb_isoftlimit;
			q->dq_dqb.dqb_ihardlimit = toset.dqb_ihardlimit;
			update_grace_times(q);
		}
	}
	if (putprivs(curprivs, COMMIT_LIMITS) == -1)
		ret = -1;
out:
	freeprivs(curprivs);
	return ret;
}

#define MAXLINELEN 65536

/* Read & parse one batch entry */
static int read_entry(qid_t *id, qsize_t *isoftlimit, qsize_t *ihardlimit, qsize_t *bsoftlimit, qsize_t *bhardlimit)
{
	static int line = 0;
	char name[MAXNAMELEN+1];
	char linebuf[MAXLINELEN], *chptr;
	char is[MAXNAMELEN+1], ih[MAXNAMELEN+1];
	char bs[MAXNAMELEN+1], bh[MAXNAMELEN+1];
	const char *error;
	int ret;

	while (1) {
		line++;
		if (!fgets(linebuf, sizeof(linebuf), stdin))
			return -1;
		if (linebuf[strlen(linebuf)-1] != '\n')
			die(1, _("Line %d too long.\n"), line);
		/* Comment? */
		if (linebuf[0] == '#')
			continue;
		/* Blank line? */
		chptr = linebuf;
		while (isblank(*chptr))
			chptr++;
		if (*chptr == '\n')
			continue;
		ret = sscanf(chptr, "%s %s %s %s %s", name, bs, bh, is, ih);
		if (ret != 5) {
			errstr(_("Cannot parse input line %d.\n"), line);
			if (!(flags & FL_CONTINUE_BATCH))
				die(1, _("Exitting.\n"));
			errstr(_("Skipping line.\n"));
			continue;
		}
		*id = name2id(name, flag2type(flags), !!(flags & FL_NUMNAMES), &ret);
		if (ret) {
			errstr(_("Unable to resolve name '%s' on line %d.\n"), name, line);
			if (!(flags & FL_CONTINUE_BATCH))
				die(1, _("Exitting.\n"));
			errstr(_("Skipping line.\n"));
			continue;
		}
		error = str2space(bs, bsoftlimit);
		if (error) {
			errstr(_("Unable to parse block soft limit '%s' "
				    "on line %d: %s\n"), bs, line, error);
			if (!(flags & FL_CONTINUE_BATCH))
				die(1, _("Exitting.\n"));
			errstr(_("Skipping line.\n"));
			continue;
		}
		error = str2space(bh, bhardlimit);
		if (error) {
			errstr(_("Unable to parse block hard limit '%s' "
				    "on line %d: %s\n"), bh, line, error);
			if (!(flags & FL_CONTINUE_BATCH))
				die(1, _("Exitting.\n"));
			errstr(_("Skipping line.\n"));
			continue;
		}
		error = str2number(is, isoftlimit);
		if (error) {
			errstr(_("Unable to parse inode soft limit '%s' "
				    "on line %d: %s\n"), is, line, error);
			if (!(flags & FL_CONTINUE_BATCH))
				die(1, _("Exitting.\n"));
			errstr(_("Skipping line.\n"));
			continue;
		}
		error = str2number(ih, ihardlimit);
		if (error) {
			errstr(_("Unable to parse inode hard limit '%s' "
				    "on line %d: %s\n"), ih, line, error);
			if (!(flags & FL_CONTINUE_BATCH))
				die(1, _("Exitting.\n"));
			errstr(_("Skipping line.\n"));
			continue;
		}
		break;
	}
	return 0;
}

/* Set user limits in batch mode */
static int batch_setlimits(struct quota_handle **handles)
{
	struct dquot *curprivs, *q;
	qsize_t bhardlimit, bsoftlimit, ihardlimit, isoftlimit;
	qid_t id;
	int ret = 0;

	while (!read_entry(&id, &isoftlimit, &ihardlimit, &bsoftlimit, &bhardlimit)) {
		curprivs = getprivs(id, handles, !!(flags & FL_ALL));
		if (!curprivs) {
			errstr(_("Error getting quota information to update.\n"));
			return -1;
		}
		for (q = curprivs; q; q = q->dq_next) {
			q->dq_dqb.dqb_bsoftlimit = bsoftlimit;
			q->dq_dqb.dqb_bhardlimit = bhardlimit;
			q->dq_dqb.dqb_isoftlimit = isoftlimit;
			q->dq_dqb.dqb_ihardlimit = ihardlimit;
			update_grace_times(q);
		}
		if (putprivs(curprivs, COMMIT_LIMITS) == -1)
			ret = -1;
		freeprivs(curprivs);
	}
	return ret;
}

/* Set grace times */
static int setgraces(struct quota_handle **handles)
{
	int i, ret = 0;

	for (i = 0; handles[i]; i++) {
		if (!handles[i]->qh_ops->write_info) {
			errstr(_("Setting grace period on %s is not supported.\n"), handles[i]->qh_quotadev);
			ret = -1;
			continue;
		}
		handles[i]->qh_info.dqi_bgrace = toset.dqb_btime;
		handles[i]->qh_info.dqi_igrace = toset.dqb_itime;
		mark_quotafile_info_dirty(handles[i]);
	}
	return ret;
}

/* Set grace times for individual user */
static int setindivgraces(struct quota_handle **handles)
{
	int ret = 0;
	struct dquot *q, *curprivs;

	curprivs = getprivs(id, handles, !!(flags & FL_ALL));
	if (!curprivs) {
		errstr(_("Error getting quota information to update.\n"));
		return -1;
	}
	for (q = curprivs; q; q = q->dq_next) {
		if (!toset.dqb_btime ||
		    (q->dq_dqb.dqb_bsoftlimit && toqb(q->dq_dqb.dqb_curspace) > q->dq_dqb.dqb_bsoftlimit))
			q->dq_dqb.dqb_btime = toset.dqb_btime;
		else
			errstr(_("Not setting block grace time on %s because softlimit is not exceeded.\n"), q->dq_h->qh_quotadev);
		if (!toset.dqb_itime ||
		    (q->dq_dqb.dqb_isoftlimit && q->dq_dqb.dqb_curinodes > q->dq_dqb.dqb_isoftlimit))
			q->dq_dqb.dqb_itime = toset.dqb_itime;
		else
			errstr(_("Not setting inode grace time on %s because softlimit is not exceeded.\n"), q->dq_h->qh_quotadev);
	}
	if (putprivs(curprivs, COMMIT_TIMES) == -1) {
		int type;

		if (flags & FL_USER)
			type = USRQUOTA;
		else if (flags & FL_GROUP)
			type = GRPQUOTA;
		else
			type = PRJQUOTA;
		errstr(_("cannot write times for %s. Maybe kernel does not support such operation?\n"), _(type2name(type)));
		ret = -1;
	}
	freeprivs(curprivs);
	return ret;
}

int main(int argc, char **argv)
{
	struct quota_handle **handles;
	int ret;

	gettexton();
	progname = basename(argv[0]);

	parse_options(argc, argv);
	init_kernel_interface();

	if (flags & FL_ALL)
		handles = create_handle_list(0, NULL, flag2type(flags), fmt,
			(flags & FL_NO_MIXED_PATHS) ? 0 : IOI_NFS_MIXED_PATHS,
			(flags & FL_RPC) ? 0 : MS_LOCALONLY);
	else
		handles = create_handle_list(mntcnt, mnt, flag2type(flags), fmt,
			(flags & FL_NO_MIXED_PATHS) ? 0 : IOI_NFS_MIXED_PATHS,
			(flags & FL_RPC) ? 0 : MS_LOCALONLY);

	if (flags & FL_GRACE)
		ret = setgraces(handles);
	else if (flags & FL_INDIVIDUAL_GRACE)
		ret = setindivgraces(handles);
	else if (flags & FL_BATCH)
		ret = batch_setlimits(handles);
	else
		ret = setlimits(handles);

	if (dispose_handle_list(handles) == -1)
		ret = -1;

	return ret ? 1 : 0;
}
