App-MHFS

 view release on metacpan or  search on metacpan

lib/MHFS/Settings.pm  view on Meta::CPAN

package MHFS::Settings v0.7.0;
use 5.014;
use strict; use warnings;
use feature 'say';
use Scalar::Util qw(reftype);
use MIME::Base64 qw(encode_base64url);
use File::Basename;
use Digest::MD5 qw(md5);
use Storable qw(freeze);
use Cwd qw(abs_path);
use File::ShareDir qw(dist_dir);
use File::Path qw(make_path);
use File::Spec::Functions qw(rel2abs);
use MHFS::Util qw(write_text_file parse_ipv4);

sub write_settings_file {
    my ($SETTINGS, $filepath) = @_;
    my $indentcnst = 4;
    my $indentspace = '';
    my $settingscontents = "#!/usr/bin/perl\nuse strict; use warnings;\n\nmy \$SETTINGS = ";

    # we only encode SCALARS. Loop through expanding HASH and ARRAY refs into SCALARS
    my @values = ($SETTINGS);
    while(@values) {
        my $value = shift @values;
        my $type = reftype($value);
        say "value: $value type: " . ($type // 'undef');
        my $raw;
        my $noindent;
        if(! defined $type) {
            if(defined $value) {
                # process lead control code if provided
                $raw = ($value eq '__raw');
                $noindent = ($value eq '__noindent');
                if($raw || $noindent) {
                    $value = shift @values;
                }
            }

            if(! defined $value) {
                $raw = 1;
                $value = 'undef';
                $type = 'SCALAR';
            }
            elsif($value eq '__indent-') {
                substr($indentspace, -4, 4, '');
                # don't actually encode anything
                $value = '';
                $type = 'NOP';
            }
            else {
                $type = reftype($value) // 'SCALAR';
            }
        }

        say "v2: $value type $type";
        if($type eq 'NOP') {
            next;
        }

        $settingscontents .= $indentspace if(! $noindent);
        if($type eq 'SCALAR') {
            # encode the value
            if(! $raw) {
                $value =~ s/'/\\'/g;
                $value = "'".$value."'";
            }

            # add the value to the buffer
            $settingscontents .= $value;
            $settingscontents .= ",\n" if(! $raw);
        }
        elsif($type eq 'HASH') {
            $settingscontents .= "{\n";
            $indentspace .= (' ' x $indentcnst);
            my @toprepend;
            foreach my $key (keys %{$value}) {
                push @toprepend, '__raw', "'$key' => ", '__noindent', $value->{$key};
            }
            push @toprepend, '__indent-', '__raw', "},\n";
            unshift(@values, @toprepend);
        }
        elsif($type eq 'ARRAY') {
            $settingscontents .= "[\n";
            $indentspace .= (' ' x $indentcnst);
            my @toprepend = @{$value};
            push @toprepend, '__indent-', '__raw', "],\n";
            unshift(@values, @toprepend);
        }
        else {
            die("Unknown type: $type");
        }
    }
    chop $settingscontents;
    chop $settingscontents;
    $settingscontents .= ";\n\n\$SETTINGS;\n";
    say "making settings folder $filepath";
    make_path(dirname($filepath));
    write_text_file($filepath,  $settingscontents);
}

sub calc_source_id {
    my ($source) = @_;
    if($source->{'type'} ne 'local') {
        say "only local sources supported right now";
        return undef;
    }
    return encode_base64url(md5('local:'.$source->{folder}));
}

sub add_source {
    my ($sources, $source) = @_;
    my $id = calc_source_id($source);
    my $len = 6;
    my $shortid = substr($id, 0, $len);
    if (exists $sources->{$shortid}) {
        my $oldid = calc_source_id($sources->{$shortid});
        while(1) {
            $len++;
            substr($oldid, 0, $len) eq substr($id, 0, $len) or last;
            length($id) > $len or die "matching hash";
        }
        $sources->{substr($oldid, 0, $len)} = $sources->{$shortid};
        delete $sources->{$shortid};
        $shortid = substr($id, 0, $len);
    }
    $sources->{$shortid} = $source;
    return $shortid;
}

sub load {
    my ($launchsettings) = @_;
    my $scriptpath = abs_path(__FILE__);

    # settings are loaded with the following precedence
    # $launchsettings (@ARGV) > settings.pl > General environment vars
    # Directory preference goes from declared to defaults and specific to general:
    # For example $CFGDIR > $XDG_CONFIG_HOME > $XDG_CONFIG_DIRS > $FALLBACK_DATA_ROOT

    # load in the launchsettings
    my ($CFGDIR, $APPDIR, $FALLBACK_DATA_ROOT);
    if(exists $launchsettings->{CFGDIR}) {
        make_path($launchsettings->{CFGDIR});
        $CFGDIR = $launchsettings->{CFGDIR};
    }
    if(exists $launchsettings->{APPDIR}) {
        -d $launchsettings->{APPDIR} or die("Bad APPDIR provided");
        $APPDIR = $launchsettings->{APPDIR};
    }
    if(exists $launchsettings->{FALLBACK_DATA_ROOT}) {
        make_path($launchsettings->{FALLBACK_DATA_ROOT});
        $FALLBACK_DATA_ROOT = $launchsettings->{FALLBACK_DATA_ROOT};
    }

    # determine the settings dir
    if(! $CFGDIR){
        my $cfg_fallback = $FALLBACK_DATA_ROOT // $ENV{'HOME'};
        $cfg_fallback //= ($ENV{APPDATA}.'/mhfs') if($ENV{APPDATA}); # Windows
        # set the settings dir to the first that exists of $XDG_CONFIG_HOME and $XDG_CONFIG_DIRS
        # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
        my $XDG_CONFIG_HOME = $ENV{'XDG_CONFIG_HOME'};
        $XDG_CONFIG_HOME //= ($cfg_fallback . '/.config') if($cfg_fallback);
        my @configdirs;
        push @configdirs, $XDG_CONFIG_HOME if($XDG_CONFIG_HOME);
        my $XDG_CONFIG_DIRS = $ENV{'XDG_CONFIG_DIRS'} || '/etc/xdg';
        push @configdirs, split(':', $XDG_CONFIG_DIRS);
        foreach my $cfgdir (@configdirs) {
            if(-d "$cfgdir/mhfs") {
                $CFGDIR = "$cfgdir/mhfs";
                last;
            }
        }
        $CFGDIR //= ($XDG_CONFIG_HOME.'/mhfs') if($XDG_CONFIG_HOME);
        defined($CFGDIR) or die("Failed to find valid candidate for \$CFGDIR");
    }
    $CFGDIR = rel2abs($CFGDIR);

    # load from the settings file
    my $SETTINGS_FILE = rel2abs($CFGDIR . '/settings.pl');
    my $SETTINGS = do ($SETTINGS_FILE);
    if(! $SETTINGS) {
        die "Error parsing settingsfile: $@" if($@);
        die "Cannot read settingsfile: $!" if(-e $SETTINGS_FILE);
        warn("No settings file found, using default settings");
        $SETTINGS = {};
    }

    # load defaults for unset values
    $SETTINGS->{'HOST'} ||= "127.0.0.1";
    $SETTINGS->{'PORT'} ||= 8000;

    $SETTINGS->{'ALLOWED_REMOTEIP_HOSTS'} ||= [
        ['127.0.0.1'],
    ];

    # write the default settings
    if(! -f $SETTINGS_FILE) {
        write_settings_file($SETTINGS, $SETTINGS_FILE);
    }
    $SETTINGS->{'CFGDIR'} = $CFGDIR;
    $SETTINGS->{flush} = $launchsettings->{flush} if(exists $launchsettings->{flush});

    # locate files based on appdir
    $APPDIR ||= $SETTINGS->{'APPDIR'} || dist_dir('App-MHFS');
    $APPDIR = abs_path($APPDIR);
    say __PACKAGE__.": using APPDIR " . $APPDIR;
    $SETTINGS->{'APPDIR'} = $APPDIR;

    # determine the fallback data root
    $FALLBACK_DATA_ROOT ||= $SETTINGS->{'FALLBACK_DATA_ROOT'} || $ENV{'HOME'};
    $FALLBACK_DATA_ROOT ||= ($ENV{APPDATA}.'/mhfs') if($ENV{APPDATA}); # Windows
    if($FALLBACK_DATA_ROOT) {
        $FALLBACK_DATA_ROOT = abs_path($FALLBACK_DATA_ROOT);
    }
    # determine the allowed remoteip host combos. only ipv4 now sorry
    $SETTINGS->{'ARIPHOSTS_PARSED'} = [];
    foreach my $rule (@{$SETTINGS->{'ALLOWED_REMOTEIP_HOSTS'}}) {
        # parse IPv4 with optional CIDR
        $rule->[0] =~ /^([^\/]+)(?:\/(\d{1,2}))?$/ or die("Invalid rule: " . $rule->[0]);
        my $ipstr = $1; my $cidr = $2 // 32;
        my $ip = parse_ipv4($ipstr);
        $cidr >= 0 && $cidr <= 32  or die("Invalid rule: " . $rule->[0]);
        my $mask = (0xFFFFFFFF << (32-$cidr)) & 0xFFFFFFFF;
        my %ariphost = (
            'ip' => $ip,
            'subnetmask' => $mask
        );
        # store the server hostname if verification is required for this rule
        $ariphost{'hostname'} = $rule->[1] if($rule->[1]);
        # store overriding absurl from this host if provided
        if($rule->[2]) {
            my $absurl = $rule->[2];
            chop $absurl if(index($absurl, '/', length($absurl)-1) != -1);
            $ariphost{'absurl'} = $absurl;
        }
        # store whether to trust connections with this host
        if($rule->[3]) {
            $ariphost{'X-MHFS-PROXY-KEY'} = $rule->[3];
        }
        push @{ $SETTINGS->{'ARIPHOSTS_PARSED'}}, \%ariphost;
    }

    if( ! $SETTINGS->{'DOCUMENTROOT'}) {
        $SETTINGS->{'DOCUMENTROOT'} = "$APPDIR/public_html";
    }
    $SETTINGS->{'XSEND'} //= 0;
    my $tmpdir = $SETTINGS->{'TMPDIR'};
    $tmpdir ||= ($ENV{'XDG_CACHE_HOME'}.'/mhfs') if($ENV{'XDG_CACHE_HOME'});
    $tmpdir ||= "$FALLBACK_DATA_ROOT/.cache/mhfs" if($FALLBACK_DATA_ROOT);
    defined($tmpdir) or die("Failed to find valid candidate for \$tmpdir");
    delete $SETTINGS->{'TMPDIR'}; # Use specific temp dir instead
    if(!$SETTINGS->{'RUNTIME_DIR'} ) {
        my $RUNTIMEDIR = $ENV{'XDG_RUNTIME_DIR'};
        if(! $RUNTIMEDIR ) {
            $RUNTIMEDIR = $tmpdir;
            warn("XDG_RUNTIME_DIR not defined!, using $RUNTIMEDIR instead");
        }
        $SETTINGS->{'RUNTIME_DIR'} = $RUNTIMEDIR.'/mhfs';
    }
    my $datadir = $SETTINGS->{'DATADIR'};
    $datadir ||= ($ENV{'XDG_DATA_HOME'}.'/mhfs') if($ENV{'XDG_DATA_HOME'});
    $datadir ||= "$FALLBACK_DATA_ROOT/.local/share/mhfs" if($FALLBACK_DATA_ROOT);
    defined($datadir) or die("Failed to find valid candidate for \$datadir");
    $SETTINGS->{'DATADIR'} = $datadir;
    $SETTINGS->{'MHFS_TRACKER_TORRENT_DIR'} ||= $SETTINGS->{'DATADIR'}.'/torrent';
    $SETTINGS->{'VIDEO_TMPDIR'} ||= $tmpdir.'/video';
    $SETTINGS->{'MUSIC_TMPDIR'} ||= $tmpdir.'/music';
    $SETTINGS->{'GENERIC_TMPDIR'} ||= $tmpdir.'/tmp';
    $SETTINGS->{'SECRET_TMPDIR'} ||= $tmpdir.'/secret';
    $SETTINGS->{'MEDIALIBRARIES'}{'movies'} ||= $SETTINGS->{'DOCUMENTROOT'} . "/media/movies",
    $SETTINGS->{'MEDIALIBRARIES'}{'tv'} ||= $SETTINGS->{'DOCUMENTROOT'} . "/media/tv",
    $SETTINGS->{'MEDIALIBRARIES'}{'music'} ||= $SETTINGS->{'DOCUMENTROOT'} . "/media/music",
    my %sources;
    my %mediasources;
    foreach my $lib ('movies', 'tv', 'music') {
        my $srcs = $SETTINGS->{'MEDIALIBRARIES'}{$lib};
        if(ref($srcs) ne 'ARRAY') {
            $srcs = [$srcs];
        }
        my @subsrcs;
        foreach my $source (@$srcs) {
            my $stype = ref($source);
            my $tohash = $source;
            if($stype ne 'HASH') {
                if($stype ne '') {
                    say __PACKAGE__.": skipping source";
                    next;
                }
                $tohash = {type => 'local',  folder => $source};
            }
            if ($tohash->{type} eq 'local') {
                my $absfolder = abs_path($tohash->{folder});
                $absfolder // do {
                    say __PACKAGE__.": skipping source $tohash->{folder} - abs_path failed";
                    next;
                };
                $tohash->{folder} = $absfolder;
            }
            my $sid = add_source(\%sources, $tohash);
            push @subsrcs, $sid;
        }
        $mediasources{$lib} = \@subsrcs;
    }
    $SETTINGS->{'MEDIASOURCES'} = \%mediasources;

    my $videotmpdirsrc = {type => 'local',  folder => $SETTINGS->{'VIDEO_TMPDIR'}};
    my $vtempsrcid = add_source(\%sources, $videotmpdirsrc);
    $SETTINGS->{'VIDEO_TMPDIR_QS'} = 'sid='.$vtempsrcid;
    $SETTINGS->{'SOURCES'} = \%sources;

    $SETTINGS->{'BINDIR'} ||= $APPDIR . '/bin';
    $SETTINGS->{'DOCDIR'} ||= $APPDIR . '/doc';

    # specify timeouts in seconds
    $SETTINGS->{'TIMEOUT'} ||= 75;
    # time to recieve the requestline and headers before closing the conn
    $SETTINGS->{'recvrequestimeout'} ||= 10;
    # maximum time allowed between sends
    $SETTINGS->{'sendresponsetimeout'} ||= $SETTINGS->{'TIMEOUT'};

    $SETTINGS->{'Torrent'}{'pyroscope'} ||= $FALLBACK_DATA_ROOT .'/.local/pyroscope' if($FALLBACK_DATA_ROOT);

    return $SETTINGS;
}

1;



( run in 0.672 second using v1.01-cache-2.11-cpan-d7a12ab2c7f )