#!/usr/bin/env bash # bashcached - memcached built on bash + socat # (C) TSUYUSATO "MakeNowJust" Kitsune 2017 # # USAGE: bashcached [--help] [--version] [--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 setglobal IFS = '$' \t\r',' VERSION = '5.1.2-bashcached'; export LANG=C trap 'exit 0' INT TERM if [[ "$SOCAT_VERSION" ]] { setglobal send = $[mktemp -u]; mkfifo -m 600 $send setglobal recv = $[mktemp -u]; mkfifo -m 600 $recv { cat $recv !2 >/dev/null; } & trap 'rm -f "$recv" "$send"' EXIT; while read -ra cmd { match $(cmd[0]) { with set|add|replace|append|prepend|cas while true { printf 'recv=%q send=%q cmd=%q\n' $recv $send $(cmd[*]) >$BASHCACHED_PIPE !2 >/dev/null && break }; head -c $(cmd[4]-0) >$send with quit exit with '' with * while true { printf 'recv=%q send=%q cmd=%q\n' $recv $send $(cmd[*]) >$BASHCACHED_PIPE !2 >/dev/null && break; } }; } } else { proc help { tail -n+2 <$0 | head -n11 | cut -c3-; exit; }; proc version { echo $VERSION; exit; } proc license { curl 'https://raw.githubusercontent.com/MakeNowJust/bashcached/master/LICENSE.MIT.md'; echo; curl 'https://raw.githubusercontent.com/MakeNowJust/bashcached/master/LICENSE.%F0%9F%8D%A3.md'; exit; } for v in [@Argv] { [[ $v == --* ]] && eval $(v:2); } # global variables setglobal unique = '1', before = $[printf '%(%s)T' -1] declare -A flags=() exptime=() casUnique=() data=() # cache operator proc cache_has { setglobal t = $(exptime[$1]) && [[ $t && ( $t -eq 0 || $t -gt $time ) ]]; } proc cache_update { compat array-assign data '$1' $2; [[ $3 ]] && compat array-assign casUnique '$1' $3 || compat array-assign casUnique '$1' $shExpr('unique++'); } proc cache_set { compat array-assign flags '$1' $2,array-assign exptime '$1' $shExpr('0 < $3 && $3 <= 2592000 ? $3 + time : $3'); cache_update $1 $4 $5; } proc cache_get { cache_has $1 && setglobal 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'; } proc cache_delete { unset "flags[$1]" "exptime[$1]" "casUnique[$1]" "data[$1]"; } # utils proc read_data { setglobal d = $[head -c $1 $send | base64]; } proc base64_cat { cat $[echo -n $1 | base64 -d] $[echo -n $2 | base64 -d] | base64; } setglobal BASHCACHED_PIPE = $[mktemp -u]; export BASHCACHED_PIPE; mkfifo -m 600 $BASHCACHED_PIPE trap 'rm -f "$BASHCACHED_PIPE"' EXIT sh-expr ' ${check-60} != 0 ' && while echo { sleep $(check-60); } >"$BASHCACHED_PIPE" & while true { cat $BASHCACHED_PIPE; } | while read -r line { setglobal cmd = '','' recv = '','' send = ''; eval $line; setglobal cmd = '',($cmd) time = $[printf '%(%s)T' -1] sh-expr ' time - before >= ${check-60} ' && for k in [$(!exptime[@])] { ! cache_has $k && cache_delete $k; } && setglobal before = $time [[ ! -p $recv ]] && continue match $(cmd[0]) { with set read_data $(cmd[4]) || return 1; cache_set $(cmd[1]) $(cmd[2]) $(cmd[3]) $d [[ ${cmd[5]} != noreply ]] && echo -e "STORED\r">$recv with add read_data $(cmd[4]) || return 1; ! cache_has $(cmd[1]) && cache_set $(cmd[1]) $(cmd[2]) $(cmd[3]) $d && setglobal result = 'STORED' || setglobal result = 'NOT_STORED' [[ ${cmd[5]} != noreply ]] && echo -e "$result\r">$recv with replace read_data $(cmd[4]) || return 1; cache_has $(cmd[1]) && cache_set $(cmd[1]) $(cmd[2]) $(cmd[3]) $d && setglobal result = 'STORED' || setglobal result = 'NOT_STORED' [[ ${cmd[5]} != noreply ]] && echo -e "$result\r">$recv with append read_data $(cmd[4]) || return 1; cache_has $(cmd[1]) && cache_update $(cmd[1]) \ $[base64_cat $(data[${cmd[1]}]) $d] && setglobal result = 'STORED' || setglobal result = 'NOT_STORED' [[ ${cmd[5]} != noreply ]] && echo -e "$result\r">$recv with prepend read_data $(cmd[4]) || return 1; cache_has $(cmd[1]) && cache_update $(cmd[1]) \ $[base64_cat $d $(data[${cmd[1]}])] && setglobal result = 'STORED' || setglobal result = 'NOT_STORED' [[ ${cmd[5]} != noreply ]] && echo -e "$result\r">$recv with cas read_data $(cmd[4]) || return 1; if ! cache_has $(cmd[1]) { setglobal result = 'NOT_FOUND' } else { [[ ${casUnique[${cmd[1]}]} -eq ${cmd[5]} ]] && cache_set $(cmd[1]) $(cmd[2]) $(cmd[3]) $d && setglobal result = 'STORED' || setglobal result = 'EXISTS'; } [[ ${cmd[6]} != noreply ]] && echo -e "$result\r">$recv with get shell {for ((i=1; i < ${#cmd[@]}; i++)); do cache_get ${cmd[$i]}; done echo -e 'END\r'}>"$recv" with gets shell {for ((i=1; i < ${#cmd[@]}; i++)); do cache_get ${cmd[$i]} 1; done echo -e 'END\r'}>"$recv" with delete cache_has $(cmd[1]) && cache_delete $(cmd[1]) && setglobal result = 'DELETED' || setglobal result = 'NOT_FOUND' [[ ${cmd[2]} != noreply ]] && echo -e "$result\r">$recv with incr cache_has $(cmd[1]) && setglobal result = $shExpr('$(echo -n "${data[${cmd[1]}]}" | base64 -d) + ${cmd[2]-0}') && cache_update $(cmd[1]) $[echo -n $result | base64] || setglobal result = 'NOT_FOUND' [[ ${cmd[3]} != noreply ]] && echo -e "$result\r">$recv with decr cache_has $(cmd[1]) && setglobal result = $shExpr('$(echo -n "${data[${cmd[1]}]}" | base64 -d) - ${cmd[2]-0}') && cache_update $(cmd[1]) $[echo -n $result | base64] || setglobal result = 'NOT_FOUND' [[ ${cmd[3]} != noreply ]] && echo -e "$result\r">$recv with touch cache_has $(cmd[1]) && cache_set $(cmd[1]) $(flags[${cmd[1]}]) $(cmd[2]) $(data[${cmd[1]}]) $(casUnique[${cmd[1]}]) && setglobal result = 'TOUCHED' || setglobal result = 'NOT_FOUND' [[ ${cmd[3]} != noreply ]] && echo -e "$result\r">$recv with flush_all for k in [$(!exptime[@])] { compat array-assign exptime '$k' $shExpr('time + ${cmd[1]-0}'); } [[ ${cmd[-1]} != noreply ]] && echo -e "OK\r">$recv with version echo -e "VERSION $VERSION\r">$recv & }; } & socat "$(protocol-tcp)-listen:$(port-25252),reuseaddr,fork" system:"$0" }