|
|
@@ -314,3 +314,90 @@ function s3_delete_gallery_objects(array $gallery): void
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/** Human-readable reason for a PHP upload error code. */
|
|
|
+function upload_error_message(int $code): string
|
|
|
+{
|
|
|
+ return match ($code) {
|
|
|
+ UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'file exceeds the server upload size limit',
|
|
|
+ UPLOAD_ERR_PARTIAL => 'upload was interrupted',
|
|
|
+ UPLOAD_ERR_NO_FILE => 'no file received',
|
|
|
+ UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE => 'server cannot store the upload',
|
|
|
+ default => 'upload error ' . $code,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Ingest one uploaded image into a gallery: stream the original (and optional
|
|
|
+ * browser-generated thumbnail) to S3, then append it to the gallery's JSON file.
|
|
|
+ *
|
|
|
+ * Shared by admin/api.php (trusted admin) and upload-api.php (public guest link).
|
|
|
+ * The gallery is re-loaded under a fresh read before appending to reduce lost
|
|
|
+ * updates between concurrent uploads. Object keys are generated server-side
|
|
|
+ * under the gallery's own prefix — never taken from the client.
|
|
|
+ *
|
|
|
+ * $original / $thumb are $_FILES entries (or null). When $imagesOnly is true the
|
|
|
+ * original must have a recognised image extension and decode via getimagesize(),
|
|
|
+ * so a public link cannot be used to store arbitrary file types.
|
|
|
+ *
|
|
|
+ * Returns [int $httpStatus, array $payload] for the caller to hand to
|
|
|
+ * json_response(); a thumbnail failure is non-fatal (the grid falls back to the
|
|
|
+ * original key).
|
|
|
+ */
|
|
|
+function gallery_store_s3_upload(array $gallery, ?array $original, ?array $thumb, bool $imagesOnly = false): array
|
|
|
+{
|
|
|
+ if (!is_array($original) || ($original['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
|
|
+ return [400, ['error' => upload_error_message((int)($original['error'] ?? UPLOAD_ERR_NO_FILE))]];
|
|
|
+ }
|
|
|
+ if (!is_uploaded_file((string)$original['tmp_name'])) {
|
|
|
+ return [400, ['error' => 'Invalid upload']];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($imagesOnly) {
|
|
|
+ $ext = strtolower(pathinfo((string)($original['name'] ?? ''), PATHINFO_EXTENSION));
|
|
|
+ if (!in_array($ext, MEDIA_EXTENSIONS, true) || getimagesize((string)$original['tmp_name']) === false) {
|
|
|
+ return [400, ['error' => 'Only image files are allowed']];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $slug = $gallery['slug'];
|
|
|
+ $name = substr(safe_filename((string)($original['name'] ?? '')), 0, 120);
|
|
|
+ $token = random_token(6);
|
|
|
+ $base = s3_gallery_prefix($slug);
|
|
|
+ $key = "$base/originals/$token-$name";
|
|
|
+
|
|
|
+ // Stream the original to S3 byte-for-byte from the PHP upload temp file.
|
|
|
+ $type = (string)($original['type'] ?? '') ?: 'application/octet-stream';
|
|
|
+ [$status] = s3_put_file($key, (string)$original['tmp_name'], $type);
|
|
|
+ if ($status < 200 || $status >= 300) {
|
|
|
+ return [502, ['error' => "S3 rejected the original (HTTP $status)"]];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional browser-generated thumbnail. A thumb failure is non-fatal: the
|
|
|
+ // original stays, and the grid falls back to the original key.
|
|
|
+ $thumbKey = null;
|
|
|
+ if (is_array($thumb)
|
|
|
+ && ($thumb['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK
|
|
|
+ && is_uploaded_file((string)$thumb['tmp_name'])
|
|
|
+ ) {
|
|
|
+ $candidate = "$base/thumbs/$token-$name.jpg";
|
|
|
+ [$tstatus] = s3_put_file($candidate, (string)$thumb['tmp_name'], 'image/jpeg');
|
|
|
+ if ($tstatus >= 200 && $tstatus < 300) {
|
|
|
+ $thumbKey = $candidate;
|
|
|
+ } else {
|
|
|
+ s3_delete($candidate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Append under a fresh load to reduce lost updates between concurrent uploads.
|
|
|
+ $gallery = gallery_load($slug);
|
|
|
+ $gallery['images'][] = [
|
|
|
+ 'key' => $key,
|
|
|
+ 'thumb' => $thumbKey,
|
|
|
+ 'name' => substr((string)($original['name'] ?? basename($key)), 0, 200),
|
|
|
+ 'size' => (int)($original['size'] ?? 0),
|
|
|
+ ];
|
|
|
+ gallery_save($gallery);
|
|
|
+
|
|
|
+ return [200, ['ok' => true, 'key' => $key, 'thumb' => $thumbKey, 'count' => count($gallery['images'])]];
|
|
|
+}
|