#!/usr/bin/perl

=head1 NAME

nextcloud - LibreNMS JSON SNMP extend for gathering backups for Nextcloud

=head1 VERSION

0.0.1

=head1 DESCRIPTION

For more information, see L<https://docs.librenms.org/Extensions/Applications/#nextcloud>.

=head1 SWITCHES

=head2 -i <dir>

Dir location for the Nextcloud install.

The defaults are as below.

FreeBSD: /usr/local/www/nextcloud
Linux: /var/www/nextcloud

=head2 -m

If set, does consider the user directories to not all be under the same mountpoint.

=head2 -o <output dir>

Where to write the output to.

Default: /var/cache/nextcloud_extend

=head2 -q

Don't print the JSON results when done.

=head1 SETUP

Create the required directory to write to.

    mkdir /var/cache/nextcloud_extend
    chown -R $nextcloud_user /var/cache/nextcloud_extend

snmpd.conf

    extend nextcloud /bin/cat /var/cache/nextcloud_extend/snmp

cron, specify -o or -i if needed/desired

      */5 * * * * /usr/lib64/librenms/snmpd/nextcloud -q 2> /dev/null

=head1 REQUIREMENTS

Debian...

    apt-get install libjson-perl libfile-slurp-perl libmime-base64-perl cpanminus
    cpanm Time::Piece

FreeBSD...

    pkg install p5-JSON p5-File-Slurp p5-MIME-Base64 p5-Time-Piece

Generic cpanm...

    cpanm JSON File::Slurp Mime::Base64

=cut

#Copyright (c) 2024, Zane C. Bowers-Hadley
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without modification,
#are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
#THE POSSIBILITY OF SUCH DAMAGE.

# Many thanks to Ben Rockwood, Jason J. Hellenthal, and Martin Matuska
# for zfs-stats and figuring out the math for all the stats
#
# Thanks to dlangille for pointing out the issues on 14 and Bobzikwick figuring out the fix in issues/501

use strict;
use warnings;
use JSON;
use Getopt::Long;
use File::Slurp;
use MIME::Base64;
use IO::Compress::Gzip qw(gzip $GzipError);
use Pod::Usage;
use String::ShellQuote;
use Time::Piece;

sub main::VERSION_MESSAGE {
	pod2usage( -exitval => 255, -verbose => 99, -sections => qw(VERSION), -output => \*STDOUT, );
}

sub main::HELP_MESSAGE {
	pod2usage( -exitval => 255, -verbose => 2, -output => \*STDOUT, );
}

#this will be dumped to json at the end
my %tojson;
$tojson{total}              = 0;
$tojson{user_count}         = 0;
$tojson{free}               = 0;
$tojson{used}               = 0;
$tojson{enabled_apps}       = 0;
$tojson{disabled_apps}      = 0;
$tojson{encryption_enabled} = 0;
$tojson{calendars}          = 0;
$tojson{multimount}         = 0;
$tojson{users}              = {};
$tojson{quota}              = 0;

# current user
my $current_user = $ENV{LOGNAME} || $ENV{USER} || getpwuid($<);

#gets the options
my %opts;
my $be_quiet;
my $output_dir = '/var/cache/nextcloud_extend';
my $install_dir;
my $version;
my $help;
my $multimount;
GetOptions(
	q       => \$be_quiet,
	'o=s'   => \$output_dir,
	'i=s'   => \$install_dir,
	v       => \$version,
	version => \$version,
	h       => \$help,
	help    => \$help,
	m       => \$multimount,
);

if ($version) {
	pod2usage( -exitval => 255, -verbose => 99, -sections => qw(VERSION), -output => \*STDOUT, );
}

if ($help) {
	pod2usage( -exitval => 255, -verbose => 2, -output => \*STDOUT, );
}

if ($multimount) {
	$tojson{multimount} = 1;
}

# get what to use for the install dir if not specified
if ( !defined($install_dir) ) {
	if ( $^O eq 'freebsd' ) {
		$install_dir = '/usr/local/www/nextcloud';
	} elsif ( $^O eq 'linux' ) {
		$install_dir = '/var/www/nextcloud';
	} else {
		die('-i <dir> not specified for the install dir for Nextcloud');
	}
}

# ensure the install dir exists
if ( !-d $install_dir ) {
	die( 'the Nextcloud install directory, "' . $install_dir . '", is not a directory or does not exist' );
}

# change to the install dir
chdir($install_dir) || die( 'failed to chdir to the Nextcloud install dir, "' . $install_dir . '",' );

# ensure the config exists
if ( !-f './config/config.php' ) {
	die( '"./config/config.php" does not exist under the Nextcloud install dir ,"' . $install_dir . '",' );
}

# ensure ./occ happens
if ( !-f './occ' ) {
	die( '"./occ" does not exist under the Nextcloud install dir ,"' . $install_dir . '",' );
}

# ensure the install dir exists and try to create it if it does not
if ( !-d $output_dir ) {
	mkdir($output_dir) || die( '"' . $output_dir . '" does not exist and could not be created' );
}

###
###
### get user info
###
###
my $user_list_raw = `php occ user:list --output=json`;
if ( $? != 0 ) {
	die( '"php occ user:list" existed non-zero with.... ' . "\n" . $user_list_raw . "\n..." );
}
my @users;
eval {
	my $decodes_users = decode_json($user_list_raw);
	@users = keys( %{$decodes_users} );
};
$tojson{user_count} = $#users;
$tojson{user_count}++;

foreach my $user (@users) {
	my $quoted_user   = shell_quote($user);
	my $user_info_raw = `php occ user:info --output=json $quoted_user 2> /dev/null`;
	eval {
		my $user_info = decode_json($user_info_raw);
		if (   defined( $user_info->{user_id} )
			&& defined( $user_info->{storage} )
			&& ref( $user_info->{storage} ) eq 'HASH'
			&& defined( $user_info->{last_seen} ) )
		{
			my $last_seen = $user_info->{last_seen};
			if ( $last_seen eq '1970-01-01T00:00:00+00:00' ) {
				$last_seen = -1;
			} else {
				eval {
					$last_seen =~ s/(\d+)\:(\d+)$/$1$2/;
					my $t1 = gmtime;
					my $t2 = Time::Piece->strptime( $last_seen, "%Y-%m-%dT%H:%M:%S%z" );
					$last_seen = $t1->epoch - $t2->epoch;
				};
				if ($@) {
					$last_seen = undef;
				}
			} ## end else [ if ( $last_seen eq '1970-01-01T00:00:00+00:00')]
			$tojson{users}{$user} = {
				'free'             => $user_info->{storage}{free},
				'quota'            => $user_info->{storage}{quota},
				'relative'         => $user_info->{storage}{relative},
				'total'            => $user_info->{storage}{total},
				'used'             => $user_info->{storage}{used},
				'last_seen'        => $last_seen,
				'last_seen_string' => $user_info->{last_seen},
				'calendars'        => 0,
			};
			$tojson{free}  = $user_info->{storage}{free};
			$tojson{total} = $user_info->{storage}{total};
			$tojson{used}  = $tojson{used} + $user_info->{storage}{used};
			if ( $user_info->{storage}{quota} > 0 ) {
				$tojson{quota} = $tojson{quota} + $user_info->{storage}{quota};
			}
			# does not currently support output options
			my $calendar_info_raw = `php occ dav:list-calendars $quoted_user 2> /dev/null`;
			if ( $? == 0 ) {
				# if the table has more than 4 lines the other lines contain calender info
				# so given it is zero index the number of calendars can be fournd via subtracting 3
				my @calendar_info_split = split( /\n/, $calendar_info_raw );
				if ( $#calendar_info_split > 3 ) {
					$tojson{users}{$user}{'calendars'} = $#calendar_info_split - 3;
					$tojson{calendars} = $tojson{'calendars'} + $tojson{users}{$user}{'calendars'};
				}
			}
		} ## end if ( defined( $user_info->{user_id} ) && defined...)
	};
} ## end foreach my $user (@users)

###
###
### get app info
###
###
my $app_info_raw = `php occ app:list --output=json  2> /dev/null`;
if ( $? == 0 ) {
	eval {
		my $app_info = decode_json($app_info_raw);
		if ( defined( $app_info->{disabled} )
			&& ref( $app_info->{disabled} ) eq 'HASH' )
		{
			my @disabled_apps = keys( %{ $app_info->{disabled} } );
			$tojson{disabled_apps} = $#disabled_apps + 1;
		}
		if ( defined( $app_info->{enabled} )
			&& ref( $app_info->{enabled} ) eq 'HASH' )
		{
			my @disabled_apps = keys( %{ $app_info->{enabled} } );
			$tojson{enabled_apps} = $#disabled_apps + 1;
		}
	};
} ## end if ( $? == 0 )

###
###
### get encryption status
###
###
my $encrption_info_raw = `php occ encryption:status --output=json  2> /dev/null`;
if ( $? == 0 ) {
	eval {
		my $encrption_info = decode_json($encrption_info_raw);
		if (   defined($encrption_info)
			&& ref( $encrption_info->{enabled} ) eq ''
			&& $encrption_info->{enabled} =~ /^(1|[Tt][Rr][Uu][Ee])$/ )
		{
			$tojson{encryption_enabled} = 1;
		}
	};
} ## end if ( $? == 0 )

my %head_hash;
$head_hash{data}        = \%tojson;
$head_hash{version}     = 1;
$head_hash{error}       = 0;
$head_hash{errorString} = '';

my $json_output = encode_json( \%head_hash );

if ( !$be_quiet ) {
	print $json_output. "\n";
}

eval { write_file( $output_dir . '/json', { atomic => 1 }, $json_output ); };
if ($@) {
	warn( 'failed to write out "' . $output_dir . '/json" ... ' . $@ );
}

my $toReturnCompressed;
gzip \$json_output => \$toReturnCompressed;
my $compressed = encode_base64($toReturnCompressed);
$compressed =~ s/\n//g;
$compressed = $compressed . "\n";

eval { write_file( $output_dir . '/snmp', { atomic => 1 }, $compressed ); };
if ($@) {
	warn( 'failed to write out "' . $output_dir . '/snmp" ... ' . $@ );
}
