Recently Written · git

sbm-suckless

sbm before the rewrite: bookmarks in a plain file, picked with dmenu. Suckless style.

git clone https://github.com/equwal/sbm-suckless

Log | Files | Refs


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