@@ -3,6 +3,12 @@ set -euo pipefail
33
44bin=" $( dirname " $0 " ) "
55
6+ # Temp artifacts, cleaned up however the script exits. Globals so the EXIT trap
7+ # sees them regardless of where in main() we leave (return, `exit`, or set -e).
8+ tmp=" "
9+ workdir=" "
10+ trap ' [[ -n "$workdir" ]] && rm -rf "$workdir"; [[ -n "$tmp" ]] && rm -f "$tmp"' EXIT
11+
612main () {
713 local quiet=0
814
@@ -20,28 +26,62 @@ main() {
2026 local dst=" ${2:? A destination s3:// URL is required as the second argument.} "
2127 local cloudfront_domain=" ${3:- } "
2228
29+ # Fail early if $src is unreadable. Otherwise `tee < "$src"` below fails, but
30+ # the background hasher/counter -- already blocked opening their fifos -- get
31+ # no writer and hang instead of being cleaned up.
32+ [[ -r " $src " ]] || { echo " upload-to-s3: cannot read source file: $src " >&2 ; exit 1; }
33+
2334 local s3path=" ${dst# s3:// } "
2435 local bucket=" ${s3path%%/* } "
2536 local key=" ${s3path#*/ } "
2637
27- local src_hash dst_hash no_hash=0000000000000000000000000000000000000000000000000000000000000000
28- src_hash=" $( " $bin /sha256sum" < " $src " ) "
29- dst_hash=" $( aws s3api head-object --bucket " $bucket " --key " $key " --query Metadata.sha256sum --output text 2> /dev/null || echo " $no_hash " ) "
38+ # Read $src exactly once: while compressing it to a temp file, tee the same
39+ # bytes to the hasher and the line counter. The old code read $src three
40+ # separate times (sha256sum, wc -l, zstd); for the large aligned/sequences
41+ # files those reads -- not the S3 transfer -- dominated the upload rules.
42+ #
43+ # Uploading a real file (rather than piping to `aws s3 cp -`) also lets the
44+ # CLI parallelize the multipart transfer; a stdin stream can't seek and
45+ # degrades to one slow stream. The temp lives next to $src (same big volume).
46+ local upload_file=" $src "
47+ workdir=" $( mktemp -d) "
3048
31- if [[ $src_hash != " $dst_hash " ]]; then
32- # The record count may have changed
49+ local -a compressor=()
50+ case " $dst " in
51+ * .gz) compressor=(gzip -c) ;;
52+ * .xz) compressor=(xz -2 -T0 -c) ;;
53+ * .zst) compressor=(zstd -T0 -c) ;;
54+ esac
55+
56+ local src_hash src_record_count
57+ if [[ ${# compressor[@]} -gt 0 ]]; then
58+ tmp=" $( mktemp " $src .upload.XXXXXX" ) "
59+ upload_file=" $tmp "
60+ mkfifo " $workdir /hash.fifo" " $workdir /count.fifo"
61+ " $bin /sha256sum" < " $workdir /hash.fifo" > " $workdir /hash" &
62+ local hash_pid=$!
63+ wc -l < " $workdir /count.fifo" > " $workdir /count" &
64+ local count_pid=$!
65+ tee " $workdir /hash.fifo" " $workdir /count.fifo" < " $src " | " ${compressor[@]} " > " $tmp "
66+ # Wait one at a time: `wait a b` returns only b's status, so a failing
67+ # hasher would be masked from set -e (uploading with a bad sha256sum).
68+ wait " $hash_pid "
69+ wait " $count_pid "
70+ src_hash=" $( cat " $workdir /hash" ) "
71+ src_record_count=" $( cat " $workdir /count" ) "
72+ else
73+ # No compression (e.g. the small *.json files) -- hash and count directly.
74+ src_hash=" $( " $bin /sha256sum" < " $src " ) "
3375 src_record_count=" $( wc -l < " $src " ) "
76+ fi
77+
78+ local dst_hash no_hash=0000000000000000000000000000000000000000000000000000000000000000
79+ dst_hash=" $( aws s3api head-object --bucket " $bucket " --key " $key " --query Metadata.sha256sum --output text 2> /dev/null || echo " $no_hash " ) "
3480
81+ if [[ $src_hash != " $dst_hash " ]]; then
3582 echo " Uploading $src → $dst "
36- if [[ " $dst " == * .gz ]]; then
37- gzip -c " $src "
38- elif [[ " $dst " == * .xz ]]; then
39- xz -2 -T0 -c " $src "
40- elif [[ " $dst " == * .zst ]]; then
41- zstd -T0 -c " $src "
42- else
43- cat " $src "
44- fi | aws s3 cp --no-progress - " $dst " --metadata sha256sum=" $src_hash " ,recordcount=" $src_record_count " " $( content-type " $dst " ) "
83+
84+ aws s3 cp --no-progress " $upload_file " " $dst " --metadata sha256sum=" $src_hash " ,recordcount=" $src_record_count " " $( content-type " $dst " ) "
4585
4686 if [[ -n $cloudfront_domain ]]; then
4787 echo " Creating CloudFront invalidation for $cloudfront_domain /$key "
0 commit comments