Added the first version of Multicities
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user