#!/usr/bin/perl use strict; use warnings; # This script is released to the public domain. ############################################################################## # VERSION HISTORY # # 2012-06-24 - 0.1 - Initial Release # 2012-06-30 - 0.5 - Added support for linking Dropbox only, upgrading # versions, and automatically searching for installers # 2012-07-04 - 0.6 - Look for YNAB installer in . too # 2014-02-09 - 0.7 - Automatically download the YNAB4 installer # ############################################################################## $| =1 ; sub mydie { warn @_; unless ($ENV{_}) { print "Press enter to quit"; my $ans = ; } exit 1; } # Ensure that all dependencies are met (Base64 & WINE) eval "use MIME::Base64;"; mydie "This script requires the Perl MIME::Base64 module to work, which you seem to be missing: $@\n" if $@; my $WINE = '/usr/bin/wine'; mydie "\nYNAB 4 requires WINE to work, please install WINE and try again\n" unless -x $WINE; # Take an (optional) argument to be a YNAB windows installer my $YNAB_WINDOWS = $ARGV[0]; my $INSTALL_MODE = 'YNAB'; # If an installer wasn't specified, ask the user what they want to do unless ($YNAB_WINDOWS && -s $YNAB_WINDOWS) { print <<"END_MESSAGE"; Would you like to: 1. Install YNAB4 and link Dropbox 2. Link Dropbox ONLY 3. Download YNAB4, install it, and link Dropbox END_MESSAGE ; print "Select an option: [1] "; # Take the user's response from STDIN, and we'll go from there my $ans = ; my ($num) = ($ans =~ /(\d+)/); $num = 1 if $ans =~ /^\s*$/; if ($num == 1) { $INSTALL_MODE = 'YNAB'; } elsif ($num == 2) { $INSTALL_MODE = 'DROPBOX'; } else { $INSTALL_MODE = 'DOWNLOAD'; } } # If we're trying to install YNAB, but no installer has been specified # or the installer specified is just an empty file if ($INSTALL_MODE eq 'YNAB' && (!$YNAB_WINDOWS || !-s $YNAB_WINDOWS)) { print "\nSearching for YNAB4 Installer...\n"; # empty array my @installers; # places to search for an installer my @search_paths = ('.', $ENV{HOME} . "/Downloads", '/tmp', $ENV{HOME} . "/Dropbox", $ENV{HOME}, ); # Search through each of the paths for an installer foreach my $search_path (@search_paths) { print "Searching in $search_path\n"; &find_installers($search_path, \@installers); last if @installers; } if (!@installers) { # If no installers are found, quit mydie("Unable to find YNAB4 installer\n"); } if (@installers == 1) { # If one (1) installer is found, use that $YNAB_WINDOWS = $installers[0]; print "\nFound Installer: '$installers[0]'\n\n"; } else { $YNAB_WINDOWS = ''; while (!$YNAB_WINDOWS) { # If multiple installers are found, list them print "\nAvailable Installers:\n"; @installers = reverse(@installers); for (my $i = 0; $i < @installers; $i++) { print " " . $i+1 . ". " . $installers[$i] . "\n"; } print "Select an installer: [1] "; # Ask the user to select which installer to use my $ans = ; # check if the response is a digit my ($num) = ($ans =~ /(\d+)/); # if the response is just whitespace, assume the default [1] (newest version found) $num = 1 if $ans =~ /^\s*$/; if ($num > 0 && $num <= @installers) { # select the installer, provided it's a valid selection $YNAB_WINDOWS = $installers[$num-1]; } } print "\n"; } } # The user is trying to install YNAB, but something has gone wrong if ($INSTALL_MODE eq 'YNAB' && (!$YNAB_WINDOWS || !-s $YNAB_WINDOWS)) { mydie "\nNo YNAB4 Installer found!\n"; } if ($INSTALL_MODE eq 'DOWNLOAD') { print "\nDownloading the most current version of YNAB4...\n"; # Setting some variables to use through the various download options my $DOWNLOAD_LOCATION = "/tmp/ynab4_installer.exe"; my $UPDATE_PAGE = "http://www.youneedabudget.com/dev/ynab4/liveCaptive/Win/update.xml"; my $UPDATE_LOCATION = "/tmp/ynab4_update.xml"; # Check to see if the LWP::Simple perl module is installed eval("use LWP::Simple;"); if ($@) { # If LWP::Simple is not installed, let's try wget my $WGET = '/usr/bin/wget'; if (-x $WGET) { # If wget is installed, let's download the update page, system($WGET, '-O', $UPDATE_LOCATION, $UPDATE_PAGE); my $UPDATE_DATA = &save_file_data($UPDATE_LOCATION); # look through the xml to find the download url and md5sum, my ($CURRENT_VERSION, $INSTALLER_URL, $GIVEN_MD5) = &find_version_url_and_md5($UPDATE_DATA, $DOWNLOAD_LOCATION); # quit if the current version is the same as the installed version mydie "It looks like you already have the latest version of YNAB4 installed: $CURRENT_VERSION" unless &compare_versions($CURRENT_VERSION); # download the installer, system($WGET, '-O', $DOWNLOAD_LOCATION, $INSTALLER_URL); # and check to make sure that the file we downloaded matches the md5 that YNAB gave us &validate_download($GIVEN_MD5, $DOWNLOAD_LOCATION); } else { # If wget is not installed, let's try curl my $CURL = '/usr/bin/curl'; if (-x $CURL) { # If curl is installed, let's download the update page, system($CURL, '-o', $UPDATE_LOCATION, $UPDATE_PAGE); my $UPDATE_DATA = &save_file_data($UPDATE_LOCATION); # look through the xml to find the download url and md5sum, my ($CURRENT_VERSION, $INSTALLER_URL, $GIVEN_MD5) = &find_version_url_and_md5($UPDATE_DATA, $DOWNLOAD_LOCATION); # quit if the current version is the same as the installed version mydie "It looks like you already have the latest version of YNAB4 installed: $CURRENT_VERSION" unless &compare_versions($CURRENT_VERSION); # download the installer, system($CURL, '-o', $DOWNLOAD_LOCATION, $INSTALLER_URL); # and check to make sure that the file we downloaded matches the md5 that YNAB gave us &validate_download($GIVEN_MD5, $DOWNLOAD_LOCATION); } else { # If LWP::Simple, wget, and curl are all NOT installed, I don't know how # else we could try to download the file, ask the user to download it # on their own and come back to us. mydie "It looks like you don't have anything installed that we can use to download the latest version of YNAB4. Please download the Windows installer from here:\n\n https://www.youneedabudget.com/download\n\n and then try running this script with Option 1.\n"; } } } else { # LWP::Simple is installed, let's download the update page, my $UPDATE_DATA = get($UPDATE_PAGE); # look through the xml to find the download url and md5sum, my ($CURRENT_VERSION, $INSTALLER_URL, $GIVEN_MD5) = &find_version_url_and_md5($UPDATE_DATA, $DOWNLOAD_LOCATION); # quit if the current version is the same as the installed version mydie "It looks like you already have the latest version of YNAB4 installed: $CURRENT_VERSION" unless &compare_versions($CURRENT_VERSION); # download the installer, getstore($INSTALLER_URL, $DOWNLOAD_LOCATION); # and check to make sure that the file we downloaded matches the md5 that YNAB gave us &validate_download($GIVEN_MD5, $DOWNLOAD_LOCATION); } } # Get started by opening the dropbox configuration my $DROPBOX_HOSTDB = $ENV{HOME} . "/.dropbox/host.db"; my $DROPBOX_INSTALLDIR = ""; if (-s $DROPBOX_HOSTDB) { # Find and return the location of the Dropbox installation open(HOSTDB, $DROPBOX_HOSTDB) or mydie "Unable to read Dropbox configuration file"; my $line1 = ; my $b64_location = ; chomp $b64_location; #print "'$b64_location'\n"; close HOSTDB; $DROPBOX_INSTALLDIR = decode_base64($b64_location); } # For debugging: #$DROPBOX_INSTALLDIR = undef; if ($DROPBOX_INSTALLDIR) { if (! -d $DROPBOX_INSTALLDIR) { # Dropbox setup hasn't been completed yet print "\nDropbox detected but not found in '$DROPBOX_INSTALLDIR'\n"; $DROPBOX_INSTALLDIR = ''; } else { # Dropbox was successfully found print "\nFound Dropbox Installation: '$DROPBOX_INSTALLDIR'\n"; } } else { if ($INSTALL_MODE eq 'DROPBOX') { print <<"END_MESSAGE"; No Dropbox installation found. To complete the Dropbox installation, start Dropbox, register, select a plan, and optionally view the tutorial. When you have a "Dropbox" folder in your home directory, setup is complete. END_MESSAGE ; mydie "Please start the script again after setup is complete\n"; } print <<"END_MESSAGE"; No Dropbox installation found. Cloud Sync will still work, but you will have to navigate to the Z: drive and save your budget file in the correct location. If you want this script to create the Dropbox link for YNAB4, you will need to complete the Dropbox installation and restart the script. To complete the Dropbox installation, start Dropbox, register, select a plan, and optionally view the tutorial. When you have a "Dropbox" folder in your home directory, setup is complete. NOTE: You can install YNAB4 now and re-run the script later to link Dropbox if you wish. END_MESSAGE ; print "Continue Installation? [yN] "; my $ans = ; if ($ans !~ /^y/i) { exit; } } # Suggest a winedir for YNAB, but ask for input from the user my $WINEDIR = $ENV{HOME} . "/.wine_YNAB4"; print "\nSpecify WINE directory to use: [$WINEDIR] "; my $input = ; chomp $input; $WINEDIR = $input if $input !~ /^\s*$/; my $WINE_DRIVEC_DIR = "$WINEDIR/drive_c"; my $WINE_APPDATA_DIR = "$WINE_DRIVEC_DIR/users/$ENV{USER}/Application\ Data"; if ($INSTALL_MODE eq 'YNAB' || $INSTALL_MODE eq 'DOWNLOAD') { # Create the winedir, unless it already exists # Might need to use $ENV{LOGNAME} here? system('mkdir', '-p', "$WINEDIR"); mydie "Unable to create $WINEDIR\n" unless -d $WINEDIR; } else { # Check to see if YNAB is installed already.. my $YNAB_APPDATA_DIR = "$WINE_APPDATA_DIR/com.ynab.YNAB4.LiveCaptive"; if (! -d $YNAB_APPDATA_DIR) { # Something is here, but it doesn't look like YNAB. Better check with the user print "\nWARNING: YNAB4 does not appear to be installed in $WINEDIR\n"; print "Continue Linking Dropbox? [yN] "; my $ans = ; if ($ans !~ /^y/i) { exit; } } } # Twist WINE's arm to play nice with Dropbox if ($DROPBOX_INSTALLDIR) { print "\nConfiguring $WINEDIR for Dropbox\n"; my $DROPBOX_WINE_CONFIG_DIR = "$WINE_APPDATA_DIR/Dropbox"; my $DROPBOX_WINE_HOSTDB = "$DROPBOX_WINE_CONFIG_DIR/host.db"; system('mkdir', '-p', "$DROPBOX_WINE_CONFIG_DIR"); mydie "Unable to create $DROPBOX_WINE_CONFIG_DIR\n" unless -d "$DROPBOX_WINE_CONFIG_DIR"; open(WINEHOSTDB, '>', "$DROPBOX_WINE_HOSTDB") or mydie "Unable to create host.db file for Dropbox in WINE"; print WINEHOSTDB "0000000000000000000000000000000000000000\n"; print WINEHOSTDB "QzpcRHJvcGJveA==\n"; close WINEHOSTDB; my $DROPBOX_SYMLINK = "Dropbox"; symlink($DROPBOX_INSTALLDIR, "$WINE_DRIVEC_DIR/$DROPBOX_SYMLINK"); if ($INSTALL_MODE eq 'DROPBOX') { print "\n\nDone!\n"; unless ($ENV{_}) { print "Press enter to quit"; my $ans = ; } exit; } } # Actually get down to installing YNAB, and keep track of everything in our log print "\nInstalling YNAB4 in $WINEDIR\n"; my $INSTALL_LOG = '/tmp/ynab4_install.log'; print "Installer output will be in $INSTALL_LOG\n"; $ENV{WINEPREFIX} = $WINEDIR; open(my $oldout, ">&STDOUT") or mydie "Can't dup STDOUT: $!"; no warnings; open(OLDERR, ">&", \*STDERR) or mydie "Can't dup STDERR: $!"; use warnings; open(STDOUT, '>>', $INSTALL_LOG) or mydie "Can't redirect STDOUT: $!"; open(STDERR, ">&STDOUT") or mydie "Can't dup STDOUT: $!"; select STDERR; $| = 1; # make unbuffered select STDOUT; $| = 1; # make unbuffered print scalar localtime, ": BEGIN INSTALLATION OF '$YNAB_WINDOWS'\n"; system($WINE, $YNAB_WINDOWS); print scalar localtime, ": END INSTALLATION OF '$YNAB_WINDOWS'\n\n\n"; open(STDOUT, ">&", $oldout) or mydie "Can't dup \$oldout: $!"; open(STDERR, ">&OLDERR") or mydie "Can't dup OLDERR: $!"; print "\n\nDone!\n"; unless ($ENV{_}) { print "Press enter to quit"; my $ans = ; } sub find_installers ($\@) { # Take our two arguments and recursively search for a YNAB installer my ($dir, $found) = @_; &recursive_find_installers($dir, $found); } sub recursive_find_installers ($\@) { # Snatch up our two arguments again my ($dir, $found) = @_; # Open the directory we're currently looking in and create an array of filenames opendir(DIR, $dir) or return; my @files = readdir DIR; closedir DIR; foreach my $file (sort @files) { # Don't even think about those pesky hidden files next if $file =~ /^\./; my $path = "$dir/$file"; # Don't bother with symbolic links, either next if -l $path; # If we've stumbled upon a directory, search through that, too if (-d $path) { &recursive_find_installers($path, $found); } # If an installer exists, add it to the end of our @installers array if ($file =~ /^YNAB.*4.*setup.*\.exe$/i) { push @$found, $path; } } } sub save_file_data ($) { local $/ = undef; # Get the location of the update file that was provided, and store it as DATA my $UPDATE_LOCATION = $_[0]; open DATA, $UPDATE_LOCATION or mydie "Couldn't open file: $!"; binmode DATA; # Read from and store it as a string: $UPDATE_DATA and return my $UPDATE_DATA = ; close DATA; return $UPDATE_DATA; } sub find_version_url_and_md5 ($\@) { # Get the update information and name of the download file my ($DATA, $FILE_LOCATION) = @_; $DATA =~ /(.*)<\/version>/g; # Find the current version number my $VERSION = $1; $DATA =~ /(.*)<\/url>/g; # Find the installer URL my $URL = $1; $DATA =~ /(.*)<\/md5>/g; # Find the MD5 and store it my $MD5SUM = $1; # Return both return ($VERSION, $URL, $MD5SUM); } sub validate_download ($\@) { eval("use Digest::MD5 qw( md5_hex )"); mydie "Validating the downloaded installer requires the Perl Digest::MD5 module to work, which you seem to be missing: $@\n" if $@; # Grab the MD5 we got from upstream, and the location of the downloaded file my ($GOOD_MD5, $FILE_DOWNLOAD) = @_; print "\nValidating installer...\n"; # Generate the MD5 hash of the file that was downloaded my $CALC_MD5 = md5_hex(&save_file_data($FILE_DOWNLOAD)); if (uc($CALC_MD5) eq $GOOD_MD5) { # If the MD5 is good, save the location as our windows installer $YNAB_WINDOWS = $FILE_DOWNLOAD; } else { # Otherwise, something went wrong. # Quit the script and instruct the user to try again. mydie "Could not validate downloaded file. Please try again."; } } sub compare_versions ($) { # Pass in the current version of YNAB4 from the ynab4_update.xml file my $CURRENT_VERSION = $_[0]; my $APPLICATION_XML; my $WINEDIR = $ENV{HOME} . "/.wine_YNAB4"; if (-d $WINEDIR) { # If the suggested wine directory exists, check what the installed version is if (!qx(find $WINEDIR -name application.xml)) { mydie "Wine directory exists. Could not confirm installed version of YNAB4.\n Please ensure that YNAB4 is actually installed. If a previous version of\n YNAB4 failed to install, please move or delete the $WINEDIR directory.\n"; } else { $APPLICATION_XML = &save_file_data(qx(find $WINEDIR -name application.xml)); } $APPLICATION_XML =~ /(.*)<\/versionNumber>/g; my $INSTALLED_VERSION = $1; # If the installed version is the same as the current version, return false if ($INSTALLED_VERSION eq $CURRENT_VERSION) { return 0; } # If the installed and current versions are different, return true else { return 1; } } # If the suggested wine directory does not exist, return true else { return 1; } }