Recently Written · git

sbm

dmenu bookmarks. Instantly fuzzy search thousands of bookmarks and plumb them. LOOKING FOR SUCKLESS SOFTWARE EDITION? GO TO sbm-suckless INSTEAD

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

Log | Files | Refs


bm-sync (8271 bytes)

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