From b04195aee86ea25c9b8cf6189c559bba12b91069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hermann?= Date: Sun, 15 Mar 2026 22:59:20 +0100 Subject: [PATCH] feat: add curl installer with checksum verification --- README.md | 3 + docs/user-guide.md | 15 ++++ scripts/install.sh | 193 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100755 scripts/install.sh diff --git a/README.md b/README.md index 56c0a18..5cf339c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Google Drive and OneDrive work out of the box. On first run, Cloudstic opens you brew install cloudstic/tap/cloudstic # macOS / Linux winget install Cloudstic.CLI # Windows go install github.com/cloudstic/cli/cmd/cloudstic@latest # with Go + +# Curl installer (macOS / Linux) +curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh ``` Or download a binary from [Releases](https://github.com/cloudstic/cli/releases). See the [User Guide](docs/user-guide.md#installation) for all options. diff --git a/docs/user-guide.md b/docs/user-guide.md index de4cd10..48086dd 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -99,6 +99,21 @@ winget upgrade Cloudstic.CLI winget uninstall Cloudstic.CLI ``` +### Curl installer (macOS / Linux) + +```bash +# Install latest +curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh + +# Install a specific version +curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh -s -- --version v1.2.3 + +# Install to a user-writable directory +curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh -s -- --install-dir "$HOME/.local/bin" +``` + +The installer verifies release checksums by default. + ### Pre-built binaries Download the latest release for your platform from the [GitHub Releases](https://github.com/cloudstic/cli/releases) page. Binaries are available for macOS (Intel & Apple Silicon), Linux (amd64 & arm64), and Windows. diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..48abb17 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env sh + +set -eu + +REPO="Cloudstic/cli" +BIN_NAME="cloudstic" +VERSION="latest" +INSTALL_DIR="/usr/local/bin" +VERIFY_CHECKSUMS=1 + +usage() { + cat < Install a specific version (e.g. v1.2.3). + Defaults to latest. + -d, --install-dir Destination directory for binary. + Defaults to /usr/local/bin. + --no-verify Skip SHA256 checksum verification. + -h, --help Show this help. + +Examples: + curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh + curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh -s -- --version v1.2.3 + curl -fsSL https://raw.githubusercontent.com/Cloudstic/cli/main/scripts/install.sh | sh -s -- --install-dir "$HOME/.local/bin" +EOF +} + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Error: required command not found: $1" >&2 + exit 1 + fi +} + +detect_os() { + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$os" in + darwin) echo "darwin" ;; + linux) echo "linux" ;; + *) + echo "Error: unsupported OS: $os (supported: darwin, linux)" >&2 + exit 1 + ;; + esac +} + +detect_arch() { + arch="$(uname -m)" + case "$arch" in + x86_64|amd64) echo "amd64" ;; + arm64|aarch64) echo "arm64" ;; + *) + echo "Error: unsupported architecture: $arch (supported: amd64, arm64)" >&2 + exit 1 + ;; + esac +} + +sha256_file() { + file="$1" + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + return + fi + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + return + fi + if command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 "$file" | awk '{print $NF}' + return + fi + echo "Error: no checksum tool found (shasum/sha256sum/openssl)." >&2 + exit 1 +} + +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + -v|--version) + [ "$#" -ge 2 ] || { echo "Error: missing value for $1" >&2; exit 1; } + VERSION="$2" + shift 2 + ;; + -d|--install-dir) + [ "$#" -ge 2 ] || { echo "Error: missing value for $1" >&2; exit 1; } + INSTALL_DIR="$2" + shift 2 + ;; + --no-verify) + VERIFY_CHECKSUMS=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Error: unknown option: $1" >&2 + usage + exit 1 + ;; + esac + done +} + +install_binary() { + os="$1" + arch="$2" + + if [ "$VERSION" = "latest" ]; then + tag="latest" + else + tag="$VERSION" + fi + + if [ "$tag" = "latest" ]; then + base_url="https://github.com/$REPO/releases/latest/download" + version_for_name="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | awk -F '"' '/tag_name/{gsub(/^v/,"",$4); print $4; exit}')" + if [ -z "$version_for_name" ]; then + echo "Error: failed to resolve latest release version." >&2 + exit 1 + fi + else + base_url="https://github.com/$REPO/releases/download/$tag" + version_for_name="${tag#v}" + fi + + archive_name="${BIN_NAME}_${version_for_name}_${os}_${arch}.tar.gz" + archive_url="$base_url/$archive_name" + checksums_url="$base_url/checksums.txt" + + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' EXIT INT TERM + + echo "Downloading $archive_name..." + curl -fsSL "$archive_url" -o "$tmpdir/$archive_name" + + if [ "$VERIFY_CHECKSUMS" -eq 1 ]; then + echo "Downloading checksums.txt..." + curl -fsSL "$checksums_url" -o "$tmpdir/checksums.txt" + + expected="$(awk -v f="$archive_name" '$2 == f {print $1}' "$tmpdir/checksums.txt")" + if [ -z "$expected" ]; then + echo "Error: checksum entry not found for $archive_name" >&2 + exit 1 + fi + actual="$(sha256_file "$tmpdir/$archive_name")" + if [ "$actual" != "$expected" ]; then + echo "Error: checksum mismatch for $archive_name" >&2 + echo "Expected: $expected" >&2 + echo "Actual: $actual" >&2 + exit 1 + fi + echo "Checksum verified." + fi + + tar -xzf "$tmpdir/$archive_name" -C "$tmpdir" + if [ ! -f "$tmpdir/$BIN_NAME" ]; then + echo "Error: extracted archive does not contain $BIN_NAME" >&2 + exit 1 + fi + + mkdir -p "$INSTALL_DIR" + target="$INSTALL_DIR/$BIN_NAME" + if cp "$tmpdir/$BIN_NAME" "$target" 2>/dev/null; then + chmod +x "$target" + else + echo "Permission denied writing to $INSTALL_DIR." >&2 + echo "Try running with sudo or choose a user-writable directory:" >&2 + echo " sh -s -- --install-dir \"$HOME/.local/bin\"" >&2 + exit 1 + fi + + echo "Installed $BIN_NAME to $target" + echo "Run: $BIN_NAME version" +} + +main() { + need_cmd curl + need_cmd tar + parse_args "$@" + os="$(detect_os)" + arch="$(detect_arch)" + install_binary "$os" "$arch" +} + +main "$@"