Added the first version of Multicities

This commit is contained in:
Luxferre
2025-12-14 12:46:20 +02:00
parent 7954949269
commit 397241cb44
2 changed files with 141 additions and 0 deletions
+1
View File
@@ -9,3 +9,4 @@
* [LShar](./lshar.sh): create self-extracting archives in the form of shell scripts
* [ShellBeat](./shellbeat.sh): a [bytebeat](https://stellartux.github.io/websynth/guide.html) audio formula player in pure shell (also depends on xxd)
* [GHMD](./ghmd.sh): a command suite to set up your own self-hosted Git repositories
* [Multicities](./cities.sh): Neocities static site manager with multiple account support
Executable
+140
View File
@@ -0,0 +1,140 @@
#!/bin/sh
# Multicities: Neocities static site manager with multiple account support
# Written in POSIX shell
# Depends on mktemp, basename, realpath, sha1sum, curl and jq
# Run without parameters to get help
# Created by Luxferre in 2025, released into public domain
REQCMD='curl -sSL'
APIROOT='https://neocities.org/api'
APICONF="${HOME}/.multicities.json"
[ ! -f "$APICONF" ] && echo '{}' > $APICONF
# Read secret string
read_secret() {
trap 'stty echo' EXIT
stty -echo
read -r "$@"
stty echo
trap - EXIT
printf '\n'
}
# login [username]: convert password to token
login() {
user="$1"
printf 'Password: '
read_secret npass
token=$($REQCMD -u "$user:$npass" ${APIROOT}/key | jq -rj '.api_key')
if [ "$token" != 'null' ]; then
tmpfile=$(mktemp -t 'cities.XXXXXX')
cat $APICONF | jq --arg u "$user" --arg t $token '. += {$u:$t}' > $tmpfile && mv $tmpfile $APICONF
echo 'Login successful!'
else
echo 'Cannot login, please check the username and password!'
exit 1
fi
}
# auth [username]: get auth header (empty if no username present)
auth() {
token="$(cat $APICONF | jq -rj --arg u "$1" '.[$u]')"
if [ "$token" != 'null' ]; then
printf 'Authorization: Bearer %s' "$token"
else
printf ''
fi
}
# api call helper
apiget() {
$REQCMD -H "$AUTH_HDR" ${APIROOT}/$1
}
# single file upload
upload() {
fpath="$(realpath "$1")"
if [ -z "$2" ]; then
localname="$(basename "$fpath")"
else
localname="$2"
fi
$REQCMD -H "$AUTH_HDR" -F "${localname}=@${fpath}" ${APIROOT}/upload
}
# site info
cmd_info() {
info_obj="$(apiget info | jq -r '.info | @text')"
printf 'Sitename: %s\n' "$(echo $info_obj | jq -r '.sitename')"
printf 'Views: %s\n' "$(echo $info_obj | jq -r '.views')"
printf 'Hits: %s\n' "$(echo $info_obj | jq -r '.hits')"
printf 'Created at: %s\n' "$(echo $info_obj | jq -r '.created_at')"
printf 'Last updated at: %s\n' "$(echo $info_obj | jq -r '.last_updated')"
printf 'Custom domain: %s\n' "$(echo $info_obj | jq -r '.domain // "None"')"
printf 'Tags: %s\n' "$(echo $info_obj | jq -r '.tags | @csv')"
}
# site file list
cmd_list() {
apiget list | jq -r '.files[] | [if .is_directory then "dir" else "file" end, .size // 0, .path] | @tsv'
}
# upload individual files
cmd_upload() {
upload "$1" "$2"
}
# delete individual remote files
cmd_delete() {
echo -n 'Confirm file deletion (y to confirm)? ';
read -r resp
if [ "$resp" == "y" ]; then
$REQCMD -H "$AUTH_HDR" -d "filenames[]=$1" ${APIROOT}/delete
else
echo 'Operation cancelled'
fi
}
# upload the entire site directory
cmd_sync() {
echo "Getting file list..."
# shape the path list with path as key and hash as value
flist="$(apiget list | jq -r '[.files[] | [.path, .sha1_hash] | { (.[0]) : .[1] }] | add | @json')"
projdir="$1"
[ -z "$1" ] && projdir="$(pwd)"
# iterate over the project directory
find "$projdir" \( ! -path '*/.*' \) -type f -printf '%P\n' | while read -r f; do
fullpath=$(realpath "${projdir}/$f")
cursha="$(sha1sum -b "$fullpath" | cut -f 1 -d ' ')"
prevsha="$(printf '%s' "$flist" | jq -r --arg p "$f" '.[$p]')"
if [ "$prevsha" != "$cursha" ]; then # do the upload
echo "Updating $f (prev: $prevsha, cur: $cursha)"
upload "$fullpath" "$f"
fi
done
echo "All files synced!"
}
# entry point
if [ -z "$1" ]; then
echo "Usage: cities.sh sitename command"
echo "where command is one of:"
echo "* login"
echo "* info"
echo "* list"
echo "* upload file_path [remote_path]"
echo "* delete remote_path (will ask for confirmation)"
echo "* sync [local_dir]"
else
username="$1"; shift # fetch and shift out username
cmdname="$1"; shift # fetch and shift out command name
if [ "$cmdname" != "login" ]; then
AUTH_HDR="$(auth $username)"
if [ -z "$AUTH_HDR" ]; then
login "$username" && AUTH_HDR="$(auth $username)"
fi
cmd_${cmdname} $@ || echo "Invalid command!"
else
login "$username"
fi
fi