#!/usr/bin/env bash
# bashcached - memcached built on bash + socat
# (C) TSUYUSATO "MakeNowJust" Kitsune 2016-2024 <make.just.on@gmail.com>
#
# USAGE: bashcached [--help] [--version] [--license] [--protocol=tcp|unix] [--port=PORT] [--check=CHECK]
#
# OPTIONS:
#   --protocol=tcp|unix      protocol name to bind and listen (default: tcp)
#   --port=PORT              port (or filename) to bind and listen (default: 25252)
#   --check=CHECK            interval to check each cache's expire (default: 60)
#   --help                   show this help
#   --version                show bashcached's version
#   --license                show bashcached's license
IFS=$' \t\r' VERSION=5.2.0-bashcached; export LANG=C
trap 'exit 0' INT TERM

if [[ "$SOCAT_VERSION" ]]; then
  send="$(mktemp -u)"; mkfifo -m 600 "$send"
  recv="$(mktemp -u)"; mkfifo -m 600 "$recv"; while [[ -p "$recv" ]]; do cat "$recv" 2>/dev/null; done &
  trap 'rm -f "$recv" "$send"' EXIT; while read -ra cmd; do case ${cmd[0]} in
    set|add|replace|append|prepend|cas)
      while true; do printf 'recv=%q send=%q cmd=%q\n' "$recv" "$send" "${cmd[*]}" >"$BASHCACHED_PIPE" 2>/dev/null && break
      done; head -c "${cmd[4]-0}" >"$send";;
    quit) exit;; '') ;; *) while true; do
      printf 'recv=%q send=%q cmd=%q\n' "$recv" "$send" "${cmd[*]}" >"$BASHCACHED_PIPE" 2>/dev/null && break; done;;
  esac; done
else
  help() { tail -n+2 <"$0" | head -n12 | cut -c3-; exit; }; version() { echo $VERSION; exit; }
  license() { curl -s 'https://raw.githubusercontent.com/MakeNowJust/bashcached/master/LICENSE.md'; echo;
    curl -s 'https://raw.githubusercontent.com/MakeNowJust/bashcached/master/LICENSE.%F0%9F%8D%A3.md'; exit; }
  for v in "$@"; do [[ $v == --* ]] && eval "${v:2}"; done

  # global variables
  unique=1 before=$(printf '%(%s)T' -1)
  declare -A flags=() exptime=() casUnique=() data=()

  # cache operator
  cache_has() { t=${exptime[$1]} && [[ $t && ( $t -eq 0 || $t -gt $time ) ]]; }
  cache_update() { data[$1]=$2; [[ $3 ]] && casUnique[$1]="$3" || casUnique[$1]=$((unique++)); }
  cache_set() { flags[$1]=$2 exptime[$1]=$((0 < $3 && $3 <= 2592000 ? $3 + time : $3)); cache_update "$1" "$4" "$5"; }
  cache_get() { cache_has "$1" && d="${data[$1]}" && printf $'VALUE %s %s %s%s\r\n' \
    "$1" "${flags[$1]}" "$(echo -n "$d" | base64 -d | wc -c)" "$([[ $2 ]] && echo " ${casUnique[$1]}")" &&
    echo -n "$d" | base64 -d && echo -e '\r'; }
  cache_delete() { unset "flags[$1]" "exptime[$1]" "casUnique[$1]" "data[$1]"; }

  # utils
  read_data() { d="$(head -c "$1" "$send" | base64)"; }
  base64_cat() { cat <(echo -n "$1" | base64 -d) <(echo -n "$2" | base64 -d) | base64; }

  BASHCACHED_PIPE="$(mktemp -u)"; export BASHCACHED_PIPE; mkfifo -m 600 "$BASHCACHED_PIPE"
  trap 'rm -f "$BASHCACHED_PIPE"' EXIT
  (( ${check-60} != 0 )) && while [[ -p "$BASHCACHED_PIPE" ]]; do echo; sleep "${check-60}"; done >"$BASHCACHED_PIPE" &

  while [[ -p "$BASHCACHED_PIPE" ]]; do cat "$BASHCACHED_PIPE" 2>/dev/null; done | while read -r line; do
    cmd=() recv='' send=; eval "$line"; read -ra cmd<<<"${cmd[*]}"; time=$(printf '%(%s)T' -1)
    (( time - before >= ${check-60} )) && for k in "${!exptime[@]}"; do
      ! cache_has $k && cache_delete $k; done && before=$time
    [[ ! -p $recv ]] && continue
    case ${cmd[0]} in
    set) read_data "${cmd[4]}" || return 1; cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d"
      [[ "${cmd[5]}" != noreply ]] && echo -e 'STORED\r'>"$recv";;
    add) read_data "${cmd[4]}" || return 1; ! cache_has "${cmd[1]}" && cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" &&
        result=STORED || result=NOT_STORED
      [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    replace) read_data "${cmd[4]}" || return 1; cache_has "${cmd[1]}" && cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" &&
        result=STORED || result=NOT_STORED
      [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    append) read_data "${cmd[4]}" || return 1; cache_has "${cmd[1]}" && cache_update "${cmd[1]}" \
        "$(base64_cat "${data[${cmd[1]}]}" "$d")" && result=STORED || result=NOT_STORED
      [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    prepend) read_data "${cmd[4]}" || return 1; cache_has "${cmd[1]}" && cache_update "${cmd[1]}" \
        "$(base64_cat "$d" "${data[${cmd[1]}]}")" && result=STORED || result=NOT_STORED
      [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    cas) read_data "${cmd[4]}" || return 1; if ! cache_has "${cmd[1]}"; then result=NOT_FOUND
      else [[ "${casUnique[${cmd[1]}]}" -eq "${cmd[5]}" ]] && cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" &&
        result=STORED || result=EXISTS; fi
      [[ "${cmd[6]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    get) (for ((i=1; i < ${#cmd[@]}; i++)); do cache_get "${cmd[$i]}"; done
      echo -e 'END\r')>"$recv";;
    gets) (for ((i=1; i < ${#cmd[@]}; i++)); do cache_get "${cmd[$i]}" 1; done
      echo -e 'END\r')>"$recv";;
    delete) cache_has "${cmd[1]}" && cache_delete "${cmd[1]}" &&
        result=DELETED || result=NOT_FOUND
      [[ "${cmd[2]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    incr) cache_has "${cmd[1]}" && result=$(($(echo -n "${data[${cmd[1]}]}" | base64 -d) + ${cmd[2]-0})) &&
        cache_update "${cmd[1]}" "$(echo -n "$result" | base64)" || result=NOT_FOUND
      [[ "${cmd[3]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    decr) cache_has "${cmd[1]}" && result=$(($(echo -n "${data[${cmd[1]}]}" | base64 -d) - ${cmd[2]-0})) &&
        cache_update "${cmd[1]}" "$(echo -n $result | base64)" || result=NOT_FOUND
      [[ "${cmd[3]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    touch) cache_has "${cmd[1]}" &&
        cache_set "${cmd[1]}" "${flags[${cmd[1]}]}" "${cmd[2]}" "${data[${cmd[1]}]}" "${casUnique[${cmd[1]}]}" &&
        result=TOUCHED || result=NOT_FOUND
      [[ "${cmd[3]}" != noreply ]] && echo -e "$result\\r">"$recv";;
    flush_all) for k in "${!exptime[@]}"; do exptime[$k]=$((time + ${cmd[1]-0})); done
      [[ "${cmd[-1]}" != noreply ]] && echo -e 'OK\r'>"$recv";;
    version) echo -e "VERSION $VERSION\\r">"$recv" &;;
    esac; done &
  socat "${protocol-tcp}-listen:${port-25252},reuseaddr,fork" system:"$0"; fi