#!/bin/bash # Simple Note System # Copyright (C) 2016, Jon Lewis # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Prevent freak accidents involving the root directory if [ -z "$HOME" ]; then HOME=/home/"$(whoami)"; fi # Store files and locations readonly PROD_STR="Simple Note System" readonly VER_STR="v2.0a12" readonly ROOT_DIR="$HOME"/.local/sns readonly NOTES_DIR="$ROOT_DIR"/notes readonly TMP_DIR="$ROOT_DIR"/tmp readonly CONFIG_FILE="$ROOT_DIR/sns.conf" #Color codes for messages readonly RED_COLOR='\033[1;31m' readonly YELLOW_COLOR='\033[1;33m' readonly RESET_COLOR='\033[0m' #Print the program header to stdout printf "%s\n" "$PROD_STR" printf "%s\n" "------------------" # Section: Functions function encrypt(){ # This function, given a recipient, $PUBKEY; a file to encrypt, $TMP_NOTE; and an # output file, $NOTE, will encrypt $TMP_NOTE to $NOTE against $PUBKEY's private # GPG key. gpg -r "$PUBKEY" -o "$FILE" -e "$TMP_NOTE" } function decrypt(){ # This function, given a file to decrypt, will attempt to decrypt the file # against the specified recipient's private key, and print the result to # stdout. gpg -d "$NOTES_DIR/$NOTE" } function init_store { if [ ! -d "$ROOT_DIR" ]; then mkdir -p "$ROOT_DIR" ; WILL_INIT="TRUE"; fi if [ ! -d "$TMP_DIR" ]; then mkdir -p "$TMP_DIR" ; WILL_INIT="TRUE"; fi if [ ! -d "$NOTES_DIR" ]; then mkdir -p "$NOTES_DIR"; fi if [ ! -r "$CONFIG_FILE" ]; then cat > "$CONFIG_FILE" << EOF # This file contains directives for the Simple Note System. EXT=note # File extension to use (for listing notes) #EDITOR= # Preferred Editor: # If you would like to specify a different editor for # sns to use, you may do so here, otherwise, sns will # use the editor specified in the environment. DATE_FMT="+%D %T" # Date Format: # If you would like to modify the date format, you may # specify one appropriate to the \`date\` command. ENCRYPTION="FALSE" # Main Encryption Toggle: # WARNING: ANY PREVIOUSLY UNENCRYPTED NOTES WILL BE LOST # Change this to TRUE to enable encryption. PUBKEY="" # Public Key # Encryption is done using GPG. You must enter your # public key's identifier here. #VCTL="" # Version Control Program # Set this to the name of your preferred version control # program to use it in SNS. Examples: git, hg, svn EOF chmod 600 "$CONFIG_FILE" printf "%s\n" "- Rewrote Default Configuration" else printf "$RED_COLOR!$RESET_COLOR - %s" "Refusing to overwrite existing config" fi if [ "$WILL_INIT" == "TRUE" ]; then printf "%s\n" "- Store initialized in $ROOT_DIR" else printf "%s\n" "- Store already initialized." fi } function verify_store { ETC_DIR=$(dirname "$CONFIG_FILE") STORE_DIRS=("$ROOT_DIR" "$NOTES_DIR" "$TMP_DIR" "$ETC_DIR") for DIR in "${STORE_DIRS[@]}"; do if [ ! -d "$DIR" ]; then mkdir -p "$DIR" fi done } function verctl { "$@" if [ $2 == "init" ]; then "$VCTL" add . "$VCTL" commit -m "Initial Commit" fi } function create(){ # Depends : p_header # Requires: $NOTE, $NOTE_DIR, # Optional: $ENCRYPTION, $SESSION_ID, $TMP_DIR encrypt # Given a valid setup, create writes the standard note header as specified # by p_header, to $NOTE. declare -r FILE="$NOTES_DIR/$NOTE.$EXT" echo "$FILE" # Refuse to overwrite a note if [ -e "$FILE" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n\t%s\n"\ "Note already exists"\ "Hint: use -e to edit the note." exit 200 fi # If the note's notebook/section does not exist, # create the appropriate folders. mkdir -p "$NOTES_DIR"/"$(dirname "$FILE")" # Write the standard note header if [ "$ENCRYPTION" == "TRUE" ]; then TMP_NOTE="$TMP_DIR"/"$SESSION_ID" p_header > "$TMP_NOTE" encrypt else p_header > "$FILE" fi # Make sure the note exists, and inform the user of the result. if [ -e "$FILE" ]; then printf "%s\n" "- Created note: ${NOTE%.*}" else >&2 printf "$RED_COLOR!$RESET_COLOR %s\n"\ "Something went wrong, and the note was not created." fi } function delete(){ # Requires: $NOTE, $NOTE_DIR # Given a valid $NOTE, delete removes $NOTE from sns. declare -r FILE="$NOTES_DIR/$NOTE.$EXT" if [ -e "$FILE" ]; then printf "$RED_COLOR!!$RESET_COLOR %s%s" "Delete " "${NOTE%.*}" read -p " (y/N) " YN case "$YN" in Y|y) rm "$FILE" printf "%s\n" "- Deleted note: ${NOTE%.*}." ;; *) printf "%s\n" "Aborted." ;; esac #Cleanup empty notebooks/sections] #find "$NOTES_DIR" -mindepth 1 -type d | tac |\ # while read -r DIR ; do # if [ ! "$(ls -A "$DIR")" ]; then # rmdir "$DIR" # fi #done else >&2 printf "$RED_COLOR!$RESET_COLOR %s\n" "Note ${NOTE%.*} does not exist." fi } function edit(){ # Requires: $EDITOR, $NOTE # Optional: $ENCRYPTION, $TMP_DIR, $SESSION_ID, decrypt, encrypt # Set filename declare -r FILE="$NOTES_DIR/$NOTE.$EXT" # Verify an editor was specified if [ -z "$EDITOR" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n"\ "No editor specified in environment." exit # Verify the note exists elif [ ! -r "$FILE" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n"\ "Note cannot be opened for editing." exit 40; fi # If encryption is enabled, decrypt $NOTE to a temp file, otherwise # operate on the note directly. if [ "$ENCRYPTION" == "TRUE" ]; then cp "$FILE" "$FILE.bk" #Insurance TMP_NOTE="$TMP_DIR/$SESSION_ID" echo "$TMP_NOTE" decrypt > "$TMP_NOTE" else TMP_NOTE="$FILE"; echo "$TMP_NOTE" fi # Write an ammendment header if [ -z "$CREATE" ]; then printf "\n%s\n" "edit - $(date "$DATE_FMT")" >> "$TMP_NOTE" printf "%s\n" "===================================" >> "$TMP_NOTE" fi # Call the editor printf "%s\n" "- editing ${NOTE%.*}" "$EDITOR" "$TMP_NOTE" # If the file was previously decrypted, encrypt it back if [ "$ENCRYPTION" == "TRUE" ]; then printf "%s\n" "- encrypting ${NOTE%.*}" rm "$FILE" encrypt; rm "$TMP_NOTE" if [ ! -r "$FILE" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n" "error: note was not saved." cp "$FILE.bk" "$FILE" else rm "$FILE.bk"; fi fi # If $VCTL is defined, add the edited note to the repo. if [ "$VCTL" ]; then >&2 printf "%s %s %s\n" "- adding to" "$VCTL" "repo". verctl "$VCTL" add "$FILE" verctl "$VCTL" commit -m "Added/Changed $NOTE" > /dev/null fi } function help { printf "\n%s" "usage: sns [-cedlp] " if [ -z "$VCTL" ]; then printf "\n%s%s%s" "usage: sns " "$VCTL" " ..." fi printf "\n%s" " sns [-hi]" printf "\n%s" " -c | --create : Create note" printf "\n%s" " -C | --config : Edit Config" printf "\n%s" " -d | --delete : Delete note" printf "\n%s" " -e | --edit : Open note for editing" printf "\n%s" " -h | --help : Display this message" printf "\n%s" " -i | --init : Write default config and initalize SNS store" printf "\n%s" " -l | --list : List all notes in NOTEBOOK" printf "\n%s" " -p | --print : Print note to console" printf "\n\n" } function list(){ # This function, given a folder, $NOTE, will list the contents of $NOTE. # If not given a folder, it will list all notes in the store. if [ ! "$NOTEBOOK" ]; then NOTEBOOK="."; fi cd "$NOTES_DIR" 2>/dev/null || exit 0 find "$NOTEBOOK" -type f -name "*$EXT" 2>/dev/null || exit 0 | while read file; do printf "%s\n" "${file%.*}" done } function p_header(){ printf "# %s\n## %s\n" "$(basename "${NOTE%.*}")" "$(date "$DATE_FMT")" } function print(){ # Given an existing file, $NOTE, print prints the contents of $NOTE to stdout. if [ -r "$NOTE" ]; then if [ "$ENCRYPTION" == "TRUE" ]; then decrypt #to stdout else cat "$NOTE"; fi else >&2 printf "$RED_COLOR!$RESET_COLOR %s\n\t%s\n"\ "Note cannot be found." exit 205 #ERR_NOTE_NO_READ fi } # End Section: Functions #============================================================================== # Stage 1: Read Configuration / Verify Integrity #============================================================================== if [ -r "$CONFIG_FILE" ]; then source "$CONFIG_FILE" verify_store elif [ "$1" != "-i" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n\t%s\n"\ "Configuration not found. Please run sns -i." exit 5 #ERR_NO_STORE fi if [ -d "$NOTES_DIR" ]; then cd "$NOTES_DIR"; fi if [ "$ENCRYPTION" == "TRUE" ]; then # If the user chose not to decrypt notes before, clear that preference. if [ -r "$NOTES_DIR"/.do_not_decrypt ]; then rm "$NOTES_DIR"/.do_not_decrypt; fi # Check if GPG is installed. if [ ! -r "$(which gpg)" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n\t%s\n"\ "Encryption was specified, but GPG is not installed." exit 100 # Check if we have a GPG recipient elif [ -z "$PUBKEY" ]; then >&2 printf "$RED_COLOR!$RESET_COLOR %s\n\t%s\n"\ "No GPG recipient was provided in $CONFIG_FILE. " exit 110 # All is good. If any previously unencrypted notes exist, encrypt them. # No harm in extra security. else find . -type f -name "*.$EXT" | grep -v "gpg" | while read TMP_NOTE; do NOTE="${TMP_NOTE%.$EXT}.gpg.$EXT" encrypt if [ -r "$NOTE" ]; then printf "$YELLOW_COLOR!$RESET_COLOR %s\n" "Encrypted ${NOTE%.$EXT}" rm "$TMP_NOTE" fi done fi # Set the extension to denote encryption. declare -r EXT=gpg.note SESSION_ID="$RANDOM" #SESSION_ID later becomes the temporary filename # If encryption isn't enabled, make sure either all notes are decrypted or the user # does not wish to decrypt all notes. else if [ ! -r "$NOTES_DIR"/.do_not_decrypt ]; then if [ -n "$(find "$NOTES_DIR" -type f -name "*.gpg.$EXT" > /dev/null)" ]; then while true; do read -p "Would you like to de-encrypt previously encrypted notes? " YN case $YN in [Yy]* ) read -s -p "Please enter your passphrase: " PASSPHRASE cd "$NOTES_DIR" find . -type f -name "*.gpg.$EXT" | while read -r NOTE; do gpg\ --passphrase "$PASSPHRASE"\ -o "${NOTE%.gpg.note}.note"\ --decrypt "$NOTE" if [ -r "${NOTE%.gpg.note}.note" ]; then printf "$YELLOW_COLOR!$RESET_COLOR %s\n"\ "De-encrypted ${NOTE%.gpg.$EXT}" rm "$NOTE"; fi done break;; [Nn]* ) # Remember the user's preference. touch "$NOTES_DIR/.do_not_decrypt" break;; *) printf " $RED_COLOR!$RESET_COLOR %s\n" "Please enter Y or N" ;; esac done fi fi # Set the extension declare -r EXT=note fi #============================================================================== # Stage 2: Argument Parsing #============================================================================== if [ -z "$1" ]; then help; exit 20 else ARGS=("${@}") declare -i INDEX INDEX=0 while getopts ":cCd:e:lp:hi" ARG $VCTL; do let INDEX++ case "$ARG" in c|--create) NOTE="$ARGOPT" create CREATE="TRUE" OP="TRUE" ;; C|--config) if [ -z "$EDITOR" ]; then printf "$YELLOW_COLOR!$RESET_COLOR - %s\n"\ "No editor defined. Defaulting to vi." EDITOR=vi fi "$EDITOR" "$CONFIG_FILE" exit 0; ;; d|--delete) NOTE="$ARGOPT" delete DELETE="TRUE" OP="TRUE" ;; e|--edit) NOTE="${ARGS[$INDEX]}" edit EDIT="TRUE" OP="TRUE" ;; l|--list) NOTEBOOK="${ARGS[$INDEX]}" list exit 0 LIST="TRUE" OP="TRUE" ;; p|--print) NOTE="${ARGS[$INDEX]}" print PRINT="TRUE" OP="TRUE" ;; h|--help) help exit 0 ;; i|--init-store) init_store exit 0 ;; "$VCTL") cd "$ROOT_DIR" verctl "${@:$INDEX}" exit 0 ;; *) NOTE="$ARG" break; ;; esac done fi #============================================================================== # Section: Actions / Stage 3 #============================================================================== # Default behavior # If no operation was specified, print help and exit on ERR_NO_OP if [ "$OP" != "TRUE" ]; then help; exit 20 fi # All options not requiring a note to be specified have been dealt # with; if one isn't specified, exit on ERR_NO_NOTE. #if [ -z "$NOTE" ]; then # printf "$RED_COLOR!$RESET_COLOR %s\n" "No note specified." # exit 30 #fi if [ "$LIST" == "TRUE" ]; then list ; exit 0; fi if [ "$PRINT" == "TRUE" ]; then print ; exit 0; fi if [ "$DELETE" == "TRUE" ]; then delete; exit 0; fi if [ "$CREATE" == "TRUE" ]; then create; fi if [ "$EDIT" == "TRUE" ]; then edit ; fi #============================================================================== # End Section: Actions / Stage 3 #==============================================================================