Dateien mit LuaSocket in Lua nach Amazon S3 exportieren
Um eine Datei aus Lua nach Amazon S3 zu exportieren, lassen Sie libcurl den Upload signieren und HTTPS verifizieren. Laden Sie anschließend das Objekt herunter, um seine Bytes zu prüfen. Dieses Beispiel ruft libcurl über die FFI von LuaJIT auf und nutzt LuaSocket für die Wartezeiten zwischen Retries. Es verarbeitet binäre und leere Dateien, temporäre AWS-Zugangsdaten sowie Schlüssel mit Leerzeichen, Unicode und wörtlichen Prozentzeichen.
Installation und Einrichtung
Die getestete Umgebung ist Linux mit LuaJIT 2.1.1788460057, LuaRocks 3.13.0, LuaSocket 3.1.0-1,
libcurl 8.22.0 und AWS CLI v2 2.37.10.
Dies sind getestete Versionen, keine grundsätzlich erforderlichen Mindestversionen.
Verwenden Sie einen aktuellen Build von LuaJIT 2.1 und eine
libcurl-Version mit aktuellen Patches mit aktiviertem HTTPS und
AWS SigV4. Ältere libcurl-Optionen allein belegen keine Kompatibilität mit dessen S3-Signierer.
Installieren Sie LuaJIT und die zugehörigen Entwicklungsheader, LuaRocks, einen C-Compiler, Make,
pkg-config, libcurl, ein systemweites CA-Zertifikatspaket,
AWS CLI v2, cmp und
mktemp über Ihren vertrauenswürdigen Systempaketmanager.
Das LuaJIT-Entwicklungspaket muss den pkg-config-Eintrag luajit
bereitstellen. Arbeiten Sie in einem leeren Verzeichnis außerhalb eines bestehenden Lua-Projekts
und installieren Sie das LuaSocket-Rock in einem eigenen
Verzeichnisbaum:
(
set -eu
for tool in luajit luarocks cc make pkg-config aws cmp mktemp; do
command -v "$tool" >/dev/null || { printf 'Missing prerequisite: %s\n' "$tool" >&2; exit 1; }
done
case "$(aws --version)" in
aws-cli/2.*) ;;
*) printf 'AWS CLI v2 is required\n' >&2; exit 1 ;;
esac
lua_headers=$(pkg-config --variable=includedir luajit)
test -f "$lua_headers/lua.h"
luarocks --lua-version=5.1 --tree="$PWD/rocks" install luasocket 3.1.0-1 \
LUA_INCDIR="$lua_headers" LUA="$(command -v luajit)"
)
Falls die Installation fehlschlägt, beheben Sie die gemeldete fehlende Voraussetzung und führen
Sie diesen Block im selben Verzeichnis erneut aus. Er überschreibt Ihre Lua-Quelldateien nicht.
Das folgende Startskript lädt LuaSocket aus ./rocks, statt sich auf die
Lua-Suchpfade eines übergeordneten Projekts zu verlassen. Setzen Sie
LIBCURL_PATH nur dann auf einen vertrauenswürdigen absoluten Pfad zu einer
gemeinsam genutzten Bibliothek, wenn libcurl außerhalb des normalen Suchpfads des Loaders liegt.
OpenResty und LuaSec sind nicht erforderlich.
Das Modul stellt eine direkte HTTPS-Verbindung her und deaktiviert Proxys und Weiterleitungen.
Ihr Netzwerk muss daher direkten Zugriff auf S3 erlauben.
AWS-Zugangsdaten einrichten
Verwenden Sie einen vorhandenen Bucket für allgemeine Zwecke und Zugangsdaten mit Berechtigungen
für s3:PutObject und s3:GetObject für Ihr gewähltes Präfix.
Das Startskript nutzt die
Ermittlung der Zugangsdaten durch die AWS CLI,
um Zugangsdaten im Prozessspeicher an Lua zu übergeben. Melden Sie sich zuerst über den bestehenden
AWS-CLI-Workflow Ihrer Organisation an oder stellen Sie Zugangsdaten über die Verwaltung sensibler
Zugangsdaten Ihrer Deployment-Umgebung bereit. Temporäre Zugangsdaten benötigen
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY und AWS_SESSION_TOKEN.
Bei langlebigen Zugangsdaten entfällt das Token. Fügen Sie keine Zugangsdaten in das Programm ein
und aktivieren Sie keine Shell-Ablaufverfolgung.
Setzen Sie AWS_REGION und S3_BUCKET auf die tatsächliche
Region und den Namen des Buckets. Der folgende Standardendpunkt unterstützt herkömmliche Buckets
in kommerziellen AWS-Regionen, deren Namen Kleinbuchstaben, Ziffern und Bindestriche enthalten.
Andere Partitionen, Namen mit Punkten, Access Points und Verzeichnis-Buckets benötigen eine eigene
verifizierte Endpunktkonfiguration. Dieses Beispiel konfiguriert weder IAM noch erstellt es einen
Bucket.
Verwendung eines verifizierten Transports zur Signierung
Mit CURLOPT_AWS_SIGV4 von libcurl wird die Anfrage signiert.
Das Sitzungstoken wird als x-amz-security-token gesendet und in die signierten Header
aufgenommen. Schlüssel werden als UTF-8-Bytes codiert, wobei Schrägstriche als Trennzeichen erhalten
bleiben. Das Modul weist eigenständige Pfadkomponenten . und
.. zurück, damit die URL-Normalisierung den Objektschlüssel nicht
verändert. Der Signierer berechnet den Hash der exakten Bytes von POSTFIELDS,
obwohl CUSTOMREQUEST die HTTP-Methode PUT auswählt.
So muss das Tutorial keine separate kryptografische Implementierung pflegen.
Hochladen aus LuaJIT
Speichern Sie dieses Modul als s3_upload.lua. Es begrenzt jedes Objekt bewusst auf
16 MiB im Speicher. Größere Uploads benötigen eine separat verifizierte
Streaming- oder Multipart-Implementierung. Jeder Versuch hat ein Verbindungslimit von 10 Sekunden
und ein Limit von 60 Sekunden für die gesamte Übertragung. Daher kann selbst eine kleinere Datei
bei einer langsamen Verbindung das Zeitlimit überschreiten. endpoint und
cafile sind optionale, vertrauenswürdige Deployment-Einstellungen, die für
Tests mit einer HTTPS-Fixture nützlich sind. Übernehmen Sie diese niemals aus einer nicht
authentifizierten Anfrage.
local ffi = require("ffi")
local socket = require("socket")
ffi.cdef[[
typedef void CURL;
struct curl_slist { char *data; struct curl_slist *next; };
int curl_global_init(long flags);
CURL *curl_easy_init(void);
void curl_easy_cleanup(CURL *handle);
int curl_easy_setopt(CURL *handle, int option, ...);
int curl_easy_perform(CURL *handle);
int curl_easy_getinfo(CURL *handle, int info, ...);
struct curl_slist *curl_slist_append(struct curl_slist *list, const char *value);
void curl_slist_free_all(struct curl_slist *list);
]]
local curl = ffi.load(os.getenv("LIBCURL_PATH") or "curl")
assert(curl.curl_global_init(3) == 0, "Cannot initialize libcurl")
-- Stable option identifiers from libcurl's public curl.h ABI.
local O = { URL=10002, PROXY=10004, WRITEFUNCTION=20011, POSTFIELDS=10015,
HTTPHEADER=10023, CUSTOMREQUEST=10036, SSL_VERIFYPEER=64, CAINFO=10065,
POSTFIELDSIZE=60, SSL_VERIFYHOST=81, CONNECTTIMEOUT=78, TIMEOUT=13,
USERNAME=10173, PASSWORD=10174, AWS_SIGV4=10305,
PROTOCOLS_STR=10318, FOLLOWLOCATION=52 }
local M = {}
function M.encode_key(key)
assert(type(key) == "string" and #key > 0 and #key <= 1024, "Invalid object key")
for part in key:gmatch("[^/]+") do
assert(part ~= "." and part ~= "..", "Dot path components are unsupported")
end
return (key:gsub("([^A-Za-z0-9/_.~-])", function(byte)
return string.format("%%%02X", byte:byte())
end))
end
local function header(value)
assert(type(value) == "string" and #value > 0 and not value:find("[%c]"), "Invalid header value")
return value
end
local function request(config, key, body, content_type)
local handle = curl.curl_easy_init()
assert(handle ~= nil, "Cannot allocate curl handle")
local headers = nil
local function append_header(value)
local updated = curl.curl_slist_append(headers, value)
assert(updated ~= nil, "Cannot allocate curl header")
headers = updated
end
local received = 0
local writer = ffi.cast("size_t (*)(char *, size_t, size_t, void *)", function(_, size, count)
local length = tonumber(size * count)
received = received + length
if received > 1024 * 1024 then return 0 end
return length
end)
local function option(name, value)
if type(value) == "number" then value = ffi.new("long", value) end
assert(curl.curl_easy_setopt(handle, O[name], value) == 0, "Unsupported curl option: " .. name)
end
local ok, result = pcall(function()
local endpoint = config.endpoint or ("https://" .. config.bucket .. ".s3." .. config.region .. ".amazonaws.com")
assert(endpoint:match("^https://[A-Za-z0-9.-]+:?%d*$"), "Expected a trusted HTTPS origin")
option("URL", endpoint .. "/" .. M.encode_key(key))
option("PROTOCOLS_STR", "https")
option("PROXY", "")
option("FOLLOWLOCATION", 0)
option("SSL_VERIFYPEER", 1)
option("SSL_VERIFYHOST", 2)
if config.cafile then option("CAINFO", config.cafile) end
option("CONNECTTIMEOUT", 10)
option("TIMEOUT", 60)
option("USERNAME", header(config.access_key))
option("PASSWORD", header(config.secret_key))
option("AWS_SIGV4", "aws:amz:" .. config.region .. ":s3")
option("CUSTOMREQUEST", "PUT")
option("POSTFIELDSIZE", #body)
option("POSTFIELDS", body)
option("WRITEFUNCTION", writer)
for _, value in ipairs({"Content-Type: " .. header(content_type), "Expect:"}) do
append_header(value)
end
if config.session_token then
append_header("x-amz-security-token: " .. header(config.session_token))
end
option("HTTPHEADER", headers)
local code = curl.curl_easy_perform(handle)
local status = ffi.new("long[1]")
assert(curl.curl_easy_getinfo(handle, 0x200002, status) == 0, "Cannot read HTTP status")
return { transport=tonumber(code), status=tonumber(status[0]) }
end)
curl.curl_easy_cleanup(handle)
curl.curl_slist_free_all(headers)
writer:free()
if not ok then error(result) end
return result
end
-- libcurl calls a Lua callback; this call path must stay outside compiled FFI traces.
jit.off(request, true)
function M.retryable(result)
if result.transport ~= 0 then
return ({[6]=true, [7]=true, [18]=true, [28]=true, [55]=true, [56]=true})[result.transport] == true
end
return ({[408]=true, [429]=true, [500]=true, [502]=true, [503]=true, [504]=true})[result.status] == true
end
function M.upload(config, file_path, key, content_type)
assert(config.region and config.region:match("^[a-z0-9-]+$"), "Invalid region")
assert(config.bucket and #config.bucket >= 3 and #config.bucket <= 63
and config.bucket:match("^[a-z0-9][a-z0-9-]*[a-z0-9]$"), "Invalid bucket name")
local file = assert(io.open(file_path, "rb"))
local body, read_error = file:read(16 * 1024 * 1024 + 1)
file:close()
if read_error then error(read_error) end
body = body or ""
assert(#body <= 16 * 1024 * 1024, "Use multipart upload for larger files")
for attempt = 1, 4 do
local result = request(config, key, body, content_type)
if result.transport == 0 and result.status == 200 then return true end
if attempt == 4 or not M.retryable(result) then
return nil, "Upload failed (transport " .. result.transport .. ", HTTP " .. result.status .. ")"
end
socket.sleep(math.min(8, 0.25 * 2^(attempt - 1)) * (0.5 + math.random()))
end
end
return M
Speichern Sie dieses aufrufende Programm als upload.lua:
local s3 = require("s3_upload")
local ok, err = s3.upload({
access_key = os.getenv("AWS_ACCESS_KEY_ID"),
secret_key = os.getenv("AWS_SECRET_ACCESS_KEY"),
session_token = os.getenv("AWS_SESSION_TOKEN"),
region = os.getenv("AWS_REGION"),
bucket = os.getenv("S3_BUCKET")
}, assert(arg[1], "local file required"), assert(arg[2], "object key required"), "application/octet-stream")
assert(ok, err)
print("Uploaded")
Speichern Sie Folgendes als upload.sh. Führen Sie es aus dem Verzeichnis aus,
das beide Lua-Dateien enthält. Die Zugangsdaten werden von der vertrauenswürdigen ausführbaren
AWS-CLI-Datei exportiert. Das Skript entfernt ein veraltetes Sitzungstoken, bevor es die ermittelten
Zugangsdaten lädt. Seine Einstellungen gelten nur für den Kindprozess.
#!/usr/bin/env bash
set +x
set -eu
for tool in aws luajit; do
command -v "$tool" >/dev/null || { printf 'Missing prerequisite: %s\n' "$tool" >&2; exit 1; }
done
: "${AWS_REGION:?Set AWS_REGION to the bucket region}"
: "${S3_BUCKET:?Set S3_BUCKET to the bucket name}"
credentials=$(aws configure export-credentials --format env)
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
eval "$credentials"
export LUA_PATH='./?.lua;./rocks/share/lua/5.1/?.lua;./rocks/share/lua/5.1/?/init.lua'
export LUA_CPATH='./rocks/lib/lua/5.1/?.so'
exec luajit upload.lua "$@"
Verwenden Sie eine vorhandene reguläre Datei mit bis zu 16 MiB oder erstellen Sie ein winziges
binäres Beispiel. Dieser Block ersetzt keine vorhandene Datei report.bin:
(
set -eu
test ! -e report.bin
printf 'September report\000\377\n' > report.bin
)
Ersetzen Sie your-bucket-name durch Ihren Bucket. Setzen Sie
AWS_PROFILE in der Umgebung, falls Ihre AWS-CLI-Anmeldung ein benanntes Profil
verwendet. Lassen Sie den Objektschlüssel in Anführungszeichen und übergeben Sie seine ursprüngliche
Schreibweise, statt ihn vorab zu codieren:
AWS_REGION=us-east-1 S3_BUCKET=your-bucket-name \
bash upload.sh report.bin 'reports/September café %.bin'
Das aufrufende Programm gibt Uploaded erst aus,
nachdem S3 HTTP 200 zurückgegeben hat.
Ein Upload unter einem vorhandenen Schlüssel ersetzt dessen aktuelles Objekt. Ein Bucket mit
Versionierung behält eine weitere Version. Verwenden Sie für Ihren ersten Test einen neuen Schlüssel.
Die entfernten Bytes prüfen
Speichern Sie diese unabhängige Downloadprüfung als verify.sh.
Sie verwendet dieselbe AWS-CLI-Quelle für Zugangsdaten, lädt genau den angegebenen Schlüssel
herunter, vergleicht jedes Byte mit der lokalen Datei und entfernt ihre temporäre Kopie sowohl
bei Erfolg als auch bei einem Fehler:
#!/usr/bin/env bash
set -eu
: "${AWS_REGION:?Set AWS_REGION to the bucket region}"
: "${S3_BUCKET:?Set S3_BUCKET to the bucket name}"
test "$#" -eq 2
test -f "$1"
download=$(mktemp)
trap 'rm -f "$download"' EXIT
aws s3api get-object --bucket "$S3_BUCKET" --key "$2" --region "$AWS_REGION" \
"$download" >/dev/null
cmp -- "$1" "$download"
printf 'Verified identical bytes\n'
AWS_REGION=us-east-1 S3_BUCKET=your-bucket-name \
bash verify.sh report.bin 'reports/September café %.bin'
Erwarten Sie Verified identical bytes. Ein fehlendes Objekt, ein verweigerter Download oder abweichende Bytes führen stattdessen zu einem Exit-Code ungleich null. Lassen Sie die Quelldatei zwischen Upload und Verifizierung unverändert. Ein erfolgreiches PutObject speichert den vollständigen Anfragekörper. Dieser Vergleich prüft aber auch, ob Sie die beabsichtigte Datei und den beabsichtigten Schlüssel übergeben haben.
Fehlerbehandlung und Retries
Nur HTTP 200 bestätigt diesen
Vorgang PutObject.
Weiterleitungen, Authentifizierungsfehler und andere dauerhafte HTTP-Fehler führen zum sofortigen
Abbruch. Bei ausgewählten vorübergehenden Transportfehlern, Drosselung und Dienstfehlern erfolgen
höchstens vier Versuche, jeweils mit den oben genannten Zeitlimits. Fehler bei der
Zertifikatsvalidierung werden nicht erneut versucht. Korrigieren Sie das CA-Zertifikatspaket oder
den Endpunkt, statt die Validierung zu deaktivieren. Eine fehlende Eingabe oder eine zu große Datei
führt bereits vor jedem Upload zu einem Fehler. Die Wartezeit nutzt
socket.sleep() mit Sekundenbruchteilen. Dabei wird niemals ein Shell-Befehl erstellt
oder ein Bruchteil mit einem Ganzzahlplatzhalter formatiert.
Jeder erneute Versuch signiert eine neue Anfrage mit denselben Bytes und demselben Schlüssel.
Eine verlorene Antwort kann dennoch mehrere Versionen in einem Bucket mit Versionierung erzeugen
oder Benachrichtigungen wiederholen. Klären Sie uneindeutige Ergebnisse, wenn diese Auswirkungen
relevant sind. Dieses begrenzte Beispiel implementiert weder adaptive Drosselung noch die
Behandlung von Retry-After, automatische Erneuerung der Zugangsdaten oder eine
garantiert genau einmalige Auslieferung.
Behandlung des Inhaltstyps
Das aufrufende Programm verwendet application/octet-stream. Um den Inhaltstyp einer PDF-Datei
zu speichern, ersetzen Sie diese Zeichenfolge in upload.lua durch
application/pdf. Die Argumente für Datei und Schlüssel bleiben gleich. Dies setzt
Objektmetadaten und validiert nicht den Dateiinhalt. Das Modul weist Steuerzeichen in Headerwerten
zurück und gibt den Antwortkörper des Dienstes niemals als Fehlermeldung zurück.
Fazit
Führen Sie den Upload und den Bytevergleich mit einem neuen Schlüssel in Ihrem autorisierten Test-Bucket aus, bevor Sie das Modul integrieren. Wählen Sie für Dateien oberhalb der Speichergrenze dieses Beispiels einen Streaming- oder Multipart-Client und verifizieren Sie dessen Verhalten beim Abschluss separat. Auch eine höhere Grenze würde weiterhin die gesamte Datei in Lua puffern.
