bm-sync (7107 bytes)
1 #!/bin/sh 2 3 # bm-sync: keep $BOOKMARKS the same on all your devices through an sbm-sync 4 # server (https://github.com/equwal/sbm-sync). bm runs it after each add. 5 # 6 # bm-sync sends the file and the version of the last sync. The server merges 7 # the changes into its copy. Then it sends back the result and its version. 8 9 CONFIG=${SBM_SYNC_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/sbm/sync} 10 SERVER=https://sbm.subread.space 11 STATE=$BOOKMARKS.sync # the version of the last sync 12 LOCK=$BOOKMARKS.sync.lock 13 WLOCK=$BOOKMARKS.lock # the write lock of bm 14 quiet= 15 16 usage () { 17 cat <<'EOF' 18 usage: bm-sync [-q] [login [server] | logout | status] 19 Without a command, bm-sync syncs $BOOKMARKS. 20 login sign in and sync (default server: https://sbm.subread.space) 21 logout sign out on this device 22 status show the server and the time of the last sync 23 -q print only errors 24 EOF 25 } 26 27 die () { 28 printf 'bm-sync: %s\n' "$*" >&2 29 exit 1 30 } 31 32 say () { 33 [ -n "$quiet" ] || printf 'bm-sync: %s\n' "$*" 34 } 35 36 # setting <name>: the value of <name> in the config file. 37 setting () { 38 sed -n "s/^$1=//p" "$CONFIG" 2>/dev/null | head -n 1 39 } 40 41 cleanup () { 42 rm -rf "$work" 43 [ -z "$locked" ] || rm -rf "$LOCK" 44 wunlock 45 } 46 47 # wlock: take the write lock of bm, as bm does, so that neither writes over 48 # a change of the other. After $SBM_LOCK_WAIT seconds (default 10) bm-sync 49 # stops, and the next add in bm syncs again. 50 wlock () { 51 waited=0 52 until mkdir "$WLOCK" 2>/dev/null; do 53 pid=$(cat "$WLOCK/pid" 2>/dev/null) 54 if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then 55 [ "$(cat "$WLOCK/pid" 2>/dev/null)" != "$pid" ] || rm -rf "$WLOCK" 56 continue 57 fi 58 [ "$waited" -lt "${SBM_LOCK_WAIT:-10}" ] \ 59 || die "$BOOKMARKS is in use: sync again later" 60 sleep 1 61 waited=$((waited + 1)) 62 done 63 echo $$ > "$WLOCK/pid" 64 wlocked=1 65 } 66 67 wunlock () { 68 [ -z "$wlocked" ] || rm -rf "$WLOCK" 69 wlocked= 70 } 71 72 login () { 73 server=${1:-$SERVER} 74 server=${server%/} 75 printf 'email: ' >&2 76 read -r email || exit 1 77 printf 'password: ' >&2 78 stty -echo 2>/dev/null 79 IFS= read -r password 80 rc=$? 81 stty echo 2>/dev/null 82 printf '\n' >&2 83 [ $rc -eq 0 ] || exit 1 84 85 # curl gets the password through stdin and the token through a file. 86 # Other users can see command lines with ps. 87 code=$(printf '%s' "$password" | curl -sS -o "$work/out" -w '%{http_code}' \ 88 --data-urlencode "email=$email" --data-urlencode 'password@-' \ 89 -- "$server/api/login") || die "cannot reach $server" 90 [ "$code" = 200 ] || die "$(cat "$work/out")" 91 92 # The config file holds the token, so only you can read it. 93 mkdir -p "$(dirname "$CONFIG")" && : > "$CONFIG" && chmod 600 "$CONFIG" || exit 1 94 printf 'server=%s\ntoken=%s\n' "$server" "$(tr -d '\r\n' < "$work/out")" > "$CONFIG" 95 rm -f "$STATE" 96 say "signed in to $server" 97 sync 98 } 99 100 logout () { 101 if [ -e "$CONFIG" ]; then 102 printf 'Authorization: Bearer %s\n' "$(setting token)" > "$work/auth" 103 curl -sS -o /dev/null -X POST -H "@$work/auth" -- "$(setting server)/api/logout" \ 104 2>/dev/null 105 rm -f "$CONFIG" "$STATE" 106 fi 107 say 'signed out' 108 } 109 110 status () { 111 if [ ! -e "$CONFIG" ]; then 112 echo 'not signed in: run bm-sync login' 113 return 114 fi 115 echo "server: $(setting server)" 116 if [ -e "$STATE" ]; then 117 echo "last sync: $(ls -l "$STATE" | awk '{ print $6, $7, $8 }')" 118 else 119 echo 'last sync: never' 120 fi 121 } 122 123 sync () { 124 [ -e "$CONFIG" ] || die 'not signed in: run bm-sync login' 125 server=$(setting server) 126 token=$(setting token) 127 [ -n "$server" ] && [ -n "$token" ] || die "$CONFIG is damaged: run bm-sync login" 128 129 # One sync at a time. A second bm-sync leaves a note and stops. The 130 # first bm-sync then syncs again. 131 if ! mkdir "$LOCK" 2>/dev/null; then 132 pid=$(cat "$LOCK/pid" 2>/dev/null) 133 if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then 134 : > "$LOCK/again" 135 say "bm-sync $pid syncs now, and then syncs again" 136 exit 0 137 fi 138 rm -rf "$LOCK" # a killed bm-sync left the lock 139 mkdir "$LOCK" 2>/dev/null || die "cannot lock $LOCK" 140 fi 141 locked=1 142 echo $$ > "$LOCK/pid" 143 144 printf 'Authorization: Bearer %s\n' "$token" > "$work/auth" 145 : >> "$BOOKMARKS" || exit 1 # a new device has no file yet 146 tries=0 147 while :; do 148 rm -f "$LOCK/again" 149 wlock 150 cat "$BOOKMARKS" > "$work/sent" || die "cannot read $BOOKMARKS" 151 wunlock 152 # An empty file with a version tells the server to delete all 153 # bookmarks. Without a version, the server deletes nothing. 154 [ -s "$work/sent" ] || rm -f "$STATE" 155 base=$(cat "$STATE" 2>/dev/null) 156 code=$(curl -sS --max-time 60 -o "$work/out" -D "$work/head" -w '%{http_code}' \ 157 -H "@$work/auth" -H 'Content-Type: text/plain; charset=utf-8' \ 158 --data-binary "@$work/sent" -- "$server/api/sync?base=$base") \ 159 || die "cannot reach $server" 160 case $code in 161 200) ;; 162 401) die 'not signed in: run bm-sync login' ;; 163 *) die "$(cat "$work/out")" ;; 164 esac 165 version=$(tr -d '\r' < "$work/head" | sed -n 's/^[Ss][Bb][Mm]-[Vv][Ee][Rr][Ss][Ii][Oo][Nn]: *//p') 166 [ -n "$version" ] || die 'the server gave no version' 167 168 # Hold the lock from the check to the write, so that no add of bm 169 # comes between them. 170 wlock 171 # bm changed the file during the request: send the new file. 172 if ! cmp -s "$work/sent" "$BOOKMARKS"; then 173 wunlock 174 tries=$((tries + 1)) 175 [ $tries -lt 5 ] || die 'the file changes all the time: try again later' 176 continue 177 fi 178 if ! cmp -s "$work/out" "$BOOKMARKS"; then 179 # Forget the version first. If the write fails, the next sync 180 # then deletes nothing on the server. 181 rm -f "$STATE" 182 # Write through cat, not mv: a symlink stays a symlink. 183 cat "$work/out" > "$BOOKMARKS" || die "cannot write $BOOKMARKS" 184 fi 185 printf '%s\n' "$version" > "$STATE" 186 wunlock 187 [ -e "$LOCK/again" ] || break 188 done 189 say "in sync: $(grep -c . "$BOOKMARKS") bookmarks" 190 } 191 192 while [ $# -gt 0 ]; do 193 case $1 in 194 -q) quiet=1 ;; 195 -h|--help) usage; exit 0 ;; 196 -*) usage >&2; exit 2 ;; 197 *) break ;; 198 esac 199 shift 200 done 201 202 [ -n "$BOOKMARKS" ] || die 'BOOKMARKS is not set: set it to your bookmark file, as the README says' 203 command -v curl >/dev/null 2>&1 || die 'curl is not installed' 204 # mkdir fails when the directory exists. Thus bm-sync does not use a 205 # directory that another user made. 206 work=${TMPDIR:-/tmp}/bm-sync.$$ 207 mkdir -m 700 "$work" || die "cannot create $work" 208 locked= 209 wlocked= 210 trap cleanup EXIT 211 trap 'exit 1' HUP INT TERM 212 213 case ${1:-sync} in 214 login) shift; login "$@" ;; 215 logout) logout ;; 216 status) status ;; 217 sync) sync ;; 218 *) usage >&2; exit 2 ;; 219 esac