Skip to content

Instantly share code, notes, and snippets.

@andyg2
Created September 25, 2026 16:20
Show Gist options
  • Select an option

  • Save andyg2/e431afad45cc57c1d10505da7ecdc10e to your computer and use it in GitHub Desktop.

Select an option

Save andyg2/e431afad45cc57c1d10505da7ecdc10e to your computer and use it in GitHub Desktop.
ulz is a Bash command that zips files, folders or wildcards and uploads them with basic auth to a single-file PHP receiver you can drop into any Apache directory, asking you to confirm if the zip is over a size limit.
<?php
/**
* ulz.php - receiver for the `ulz` upload script.
*
* Drop this file into any directory on an Apache/PHP server. Uploaded zips
* are stored in ./uploads next to this file (created automatically, with an
* .htaccess that blocks web access to it).
*
* If PHP runs as CGI/FPM and auth always fails, Apache is stripping the
* Authorization header. Add this to an .htaccess next to this file:
*
* CGIPassAuth On
* # or, on older Apache:
* # RewriteEngine On
* # RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
*
* Big uploads also need PHP's limits raised (php.ini, or .htaccess with mod_php):
*
* php_value upload_max_filesize 2G
* php_value post_max_size 2G
* php_value max_execution_time 600
* php_value max_input_time 600
*/
// ------------------------------------------------------------- config ----
const AUTH_USER = 'uploader';
const AUTH_PASS = 'change-me-please';
const UPLOAD_DIR = __DIR__ . '/uploads';
const MAX_BYTES = 2 * 1024 * 1024 * 1024; // hard server-side cap: 2 GB
// -------------------------------------------------------------------------
header('Content-Type: text/plain; charset=utf-8');
header('X-Robots-Tag: noindex');
function out(int $code, string $msg): void {
http_response_code($code);
echo $msg, "\n";
exit;
}
// --- Basic auth (works under mod_php and CGI/FPM) ---
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$pass = $_SERVER['PHP_AUTH_PW'] ?? null;
if ($user === null) {
$hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
if (stripos($hdr, 'basic ') === 0) {
$decoded = base64_decode(substr($hdr, 6), true);
if ($decoded !== false && strpos($decoded, ':') !== false) {
[$user, $pass] = explode(':', $decoded, 2);
}
}
}
if ($user === null || !hash_equals(AUTH_USER, (string)$user) || !hash_equals(AUTH_PASS, (string)$pass)) {
header('WWW-Authenticate: Basic realm="ulz"');
out(401, 'ERROR: authentication required');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
out(405, 'ERROR: POST a file as multipart field "file"');
}
// POST bigger than post_max_size arrives with empty $_FILES/$_POST
if (empty($_FILES) && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
out(413, 'ERROR: upload exceeds post_max_size (' . ini_get('post_max_size') . ')');
}
$f = $_FILES['file'] ?? null;
if (!$f || is_array($f['error'])) {
out(400, 'ERROR: no file received (expected multipart field "file")');
}
$errors = [
UPLOAD_ERR_INI_SIZE => 'exceeds upload_max_filesize (' . ini_get('upload_max_filesize') . ')',
UPLOAD_ERR_FORM_SIZE => 'exceeds form MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'only partially uploaded',
UPLOAD_ERR_NO_FILE => 'no file uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'server has no temp directory',
UPLOAD_ERR_CANT_WRITE => 'server failed to write to disk',
UPLOAD_ERR_EXTENSION => 'blocked by a PHP extension',
];
if ($f['error'] !== UPLOAD_ERR_OK) {
$code = in_array($f['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) ? 413 : 500;
out($code, 'ERROR: ' . ($errors[$f['error']] ?? 'upload error ' . $f['error']));
}
if (!is_uploaded_file($f['tmp_name'])) {
out(400, 'ERROR: invalid upload');
}
if ($f['size'] > MAX_BYTES) {
out(413, 'ERROR: file larger than server limit');
}
// --- Only accept real zip files ---
$fh = fopen($f['tmp_name'], 'rb');
$magic = $fh ? fread($fh, 4) : '';
if ($fh) fclose($fh);
if (!in_array($magic, ["PK\x03\x04", "PK\x05\x06"], true)) {
out(415, 'ERROR: not a zip file');
}
// --- Safe, non-clobbering filename ---
$name = basename(str_replace('\\', '/', (string)$f['name']));
$name = preg_replace('/[^A-Za-z0-9._-]+/', '_', $name);
$name = ltrim($name, '.');
$stem = preg_replace('/\.zip$/i', '', $name);
if ($stem === '' || $stem === null) $stem = 'upload-' . date('Ymd-His');
// --- Prepare storage dir (locked down against web access / execution) ---
if (!is_dir(UPLOAD_DIR) && !mkdir(UPLOAD_DIR, 0750, true)) {
out(500, 'ERROR: cannot create upload directory');
}
$ht = UPLOAD_DIR . '/.htaccess';
if (!file_exists($ht)) {
@file_put_contents($ht,
"<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n" .
"<IfModule !mod_authz_core.c>\n Order allow,deny\n Deny from all\n</IfModule>\n" .
"Options -Indexes -ExecCGI\n<IfModule mod_php.c>\n php_flag engine off\n</IfModule>\n");
}
$dest = UPLOAD_DIR . "/$stem.zip";
for ($i = 1; file_exists($dest); $i++) {
$dest = UPLOAD_DIR . "/$stem-$i.zip";
}
if (!move_uploaded_file($f['tmp_name'], $dest)) {
out(500, 'ERROR: could not save file (check directory permissions)');
}
@chmod($dest, 0640);
out(200, sprintf('OK: saved %s (%s bytes, sha256 %s)',
basename($dest), number_format(filesize($dest)), hash_file('sha256', $dest)));
#!/usr/bin/env bash
#
# ulz - zip files/folders and upload them to a receiver endpoind
#
# Usage:
# ulz *.txt
# ulz ./files
# ulz me.txt you.txt ./somedir
#
# Options:
# -y Don't ask for confirmation when over the size limit
# -n NAME Name of the zip to upload (default: derived from input + timestamp)
# -k Keep the local zip after upload (saved in current directory)
# -h Help
#
# Environment overrides:
# ULZ_MAX_MB size (MB) above which you're asked to confirm (default below)
# ULZ_URL / ULZ_USER / ULZ_PASS override the hard-coded values
#
# ---------------------------------------------------------------- config ----
URL="${ULZ_URL:-https://example.com/probably-some-directory/}"
AUTH_USER="${ULZ_USER:-uploader}"
AUTH_PASS="${ULZ_PASS:-change-me-please}"
MAX_MB="${ULZ_MAX_MB:-100}"
# ----------------------------------------------------------------------------
set -uo pipefail
die() { echo "ulz: $*" >&2; exit 1; }
usage() { sed -n '3,21p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
ASSUME_YES=0
KEEP=0
NAME=""
while getopts ":yn:kh" opt; do
case "$opt" in
y) ASSUME_YES=1 ;;
n) NAME="$OPTARG" ;;
k) KEEP=1 ;;
h) usage 0 ;;
:) die "option -$OPTARG needs an argument" ;;
*) die "unknown option -$OPTARG (try -h)" ;;
esac
done
shift $((OPTIND - 1))
[ $# -gt 0 ] || usage 1
command -v zip >/dev/null 2>&1 || die "'zip' is not installed"
command -v curl >/dev/null 2>&1 || die "'curl' is not installed"
[[ "$MAX_MB" =~ ^[0-9]+$ ]] || die "ULZ_MAX_MB must be a whole number"
# Validate inputs (an unmatched wildcard arrives literally, e.g. '*.txt')
inputs=()
for p in "$@"; do
[ -e "$p" ] || die "no such file or directory: $p"
# stop zip treating names beginning with '-' as options
[[ "$p" == -* ]] && p="./$p"
inputs+=("$p")
done
# Work out zip name
stamp="$(date +%Y%m%d-%H%M%S)"
if [ -z "$NAME" ]; then
if [ ${#inputs[@]} -eq 1 ]; then
base="$(basename -- "${inputs[0]%/}")"
[ "$base" = "." ] && base="$(basename -- "$PWD")"
base="${base%.*}"; [ -n "$base" ] || base="ulz"
NAME="${base}-${stamp}.zip"
else
NAME="ulz-${stamp}.zip"
fi
fi
[[ "$NAME" == *.zip ]] || NAME="${NAME}.zip"
NAME="$(basename -- "$NAME")"
tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t ulz)" || die "can't create temp dir"
trap 'rm -rf "$tmpdir"' EXIT INT TERM
zipfile="$tmpdir/$NAME"
echo "Zipping ${#inputs[@]} item(s) -> $NAME"
zip -r -q -y "$zipfile" "${inputs[@]}" || die "zip failed"
bytes=$(wc -c < "$zipfile" | tr -d ' ')
human=$(awk -v b="$bytes" 'BEGIN{
split("B KB MB GB TB",u," "); i=1
while (b>=1024 && i<5) { b/=1024; i++ }
printf (i==1 ? "%d %s" : "%.1f %s"), b, u[i] }')
echo "Zip size: $human"
limit=$((MAX_MB * 1024 * 1024))
if [ "$bytes" -gt "$limit" ] && [ "$ASSUME_YES" -ne 1 ]; then
echo "WARNING: $human is over the ${MAX_MB} MB limit." >&2
if [ -t 0 ]; then
read -r -p "Upload anyway? [y/N] " ans
[[ "$ans" =~ ^[Yy]([Ee][Ss])?$ ]] || { echo "Aborted."; exit 2; }
else
die "not a terminal; re-run with -y to upload anyway"
fi
fi
echo "Uploading to $URL"
resp="$tmpdir/response"
code=$(curl --progress-bar --fail-with-body -u "$AUTH_USER:$AUTH_PASS" \
-F "file=@${zipfile};type=application/zip" \
-o "$resp" -w '%{http_code}' "$URL")
rc=$?
if [ "$KEEP" -eq 1 ]; then
cp -- "$zipfile" "./$NAME" && echo "Kept local copy: ./$NAME"
fi
if [ $rc -ne 0 ]; then
echo "Upload FAILED (curl exit $rc, HTTP $code)" >&2
[ -s "$resp" ] && cat "$resp" >&2 && echo >&2
exit 1
fi
cat "$resp"; echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment