archive_fileJuly 31, 2016

Save Copies of Files in an Archive

Introduction

This is my oldest script that I still use almost every day. It came into being in the late '80s. It was first written in the Bourne shell, then the Korn shell, and finally with Bash. It is a short simple script and it is usually the first script I upload to a Linux/Unix server after I setup my account. The script simply copies a file to an archive directory and notes the file's complete path, the date, the user, and a comment read from the user as to why the file was archived. Usually my comments are like:

Before changing something to correct a bug or add a new feature.

This script has saved my butt several times throughout the years. It is easy to save a version of a file before changing it without cluttering up the file's current directory with all sorts of backup copies. It is great for saving configuration files before changing them.

The convenience, and therefore the usefulness, of archive_file is greatly increased by:

  1. enabling the invocation of archive_file with a single letter (a), and
  2. setting a one-character environment variable ($a) to the archive directory path.

Shared Archives

The archive_file script was originally written as a personal tool saving backup files in a personal archive directory. The script has been enhanced so that multiple users in the same security group can share the same archive. Simply create a directory in /var/archive with the same name as the user's group name and archive_file will automatically start using that archive directory to save the backup files.

Tricks

I am a long time vi user and usually find myself archiving my files just before I change something. Within vi, the characters :! execute an external command (like archive_file) and vi substitutes the current file name for the % character. Together with the above recommendations of invoking archive_file with just the a command and assigning the archive directory path to the $a environment variable, the following tricks are available.

To archive the file currently being edited:

:!a %

To view the previous version of the current file:

:n $a/%

To view the fourth version of the current file archived:

:n $a/%.004

To find the differences between the current file and the previously archived version:

:!diff % $a/%

To view the log of archive files:

:!a -l | less

Script

#!/bin/bash

# Archive/Backup file(s)
#
# This program copies the named file(s) to an archive directory. Version numbers
# are appended to archived files, beginning with version 000. The lastest
# version of a file is linked to a file with its same name -- the name without
# the version number.
#
# A log file named LOG is kept of all the files archived. It has the file's original path,
# the date it was archived, and it has a comment that is asked for from the user if it
# is not specified on the command line.
#
# This version tries to save files in a common archive shared by a login group
# located in /var/archive. If the directory /var/archive/GROUP_NAME does not exist,
# the archive is located in the user's home directory.
#
# Copyright 1989-2019, Mack Pexton (mack@mackpexton.com)
# License: https://opensource.org/licenses/mit-license.php

USAGE="Usage: ${0##*/} [ -a ] [ -l ] [ -ccomment ] [ file_name ... ]"

GROUP_ARCHIVE=/var/archive/$(id -gn)	# group archive directory path
USER_ARCHIVE=~/archive			# personal archive directory path

if [ -d "$GROUP_ARCHIVE" ]
then
	archive_dir="$GROUP_ARCHIVE"
else
	archive_dir="$USER_ARCHIVE"
fi

log="$archive_dir/LOG"
user="${REMOTE_USER:-$(id -un)}"	# used to brand comments in log file

if [ ! -d "$archive_dir" ]
then
	# Initialize archive.
	mkdir -m771 "$archive_dir" || exit 1
	> "$log"
	chmod 666 "$log"
fi

for arg
do
	case "$arg" in
	-a)	echo "$archive_dir" ;;
	-c*)	comment=${arg#-c} ;;		# like: -c"This is a Comment"
	-l)	cat "$log" ;;
	-*)	echo "$USAGE" 1>&2; exit 1 ;;
	*)
		# Argument is a file name.
		if [ -d "$arg" ]
		then
			# Do nothing for directories
			echo "$arg is a directory. Only regular files can be archived."
		elif [ -e "$arg" ]
		then
			# Get user comment to record in log file.
			if [ -z "$comment" ]
			then
				echo -n "Comment: "
				read comment
			fi

			file_name="${arg##*/}"

			# Determine source file's absolute path.
			src="$arg";
			if [ ${arg:0:1} != '/' ]
			then
				src="$(pwd)/$src"
			fi

			(
				cd "${archive_dir}"

				# Find next available version number for archive file.
				# 0 fill the number to 3 places.
				typeset v=0
				while vv=$(echo $v | sed -e 's/^/000/' -e 's/.*\(...\)$/\1/')
				      test -a "${file_name}.${vv}"
				do	
					v=$[ v+1 ]
				done
				
				archive_file="${file_name}.${vv}"

				# Copy file to archive file.
				cp --preserve -- "$src" "${archive_file}"

				# Link last archive version to archive file name (w/o version).
				if [ -a "${file_name}" ]
				then
					rm -f -- "${file_name}"
				fi
				ln -s -- "${archive_file}" "${file_name}"

				# Make archive file readonly.
				chmod a-w -- "${archive_file}"

				# Write archive details to log file.
				echo >> $log \
"$archive_file	$(date +"%m/%d/%y %H:%M")	$user	$src
     $comment"
			)
		else
			echo "File $arg not found." 1>&2
		fi
		;;
	esac
done

Installation

Copy the script to a directory in your $PATH. You can copy it to your personal bin directory ~/bin but I usually copy it to /usr/local/bin so that it is available to all my login accounts.

As user root:

# cd /usr/local/bin
# cp /PATH/TO/DOWNLOADED_SCRIPT_FILE archive_file
# chmod a+x archive_file

Create a one-character synonym for archive_file.

# ln -s archive_file a

Automatically set environment variable $a when logging in.

echo 'export a=$(a -a)' >> ~/.bash_profile