{"id":796,"date":"2020-12-01T22:56:20","date_gmt":"2020-12-01T22:56:20","guid":{"rendered":"https:\/\/www.antpace.com\/blog\/?p=796"},"modified":"2025-08-25T17:46:08","modified_gmt":"2025-08-25T17:46:08","slug":"image-upload-gallery","status":"publish","type":"post","link":"https:\/\/www.antpace.com\/blog\/image-upload-gallery\/","title":{"rendered":"Build an image upload gallery"},"content":{"rendered":"<p>Allowing users to upload images to your app can be a pivotal feature. Many digital products rely on it. This post will show you how to do it using PHP and AWS S3.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-1043\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2020\/11\/image-upload.png\" alt=\"image upload gallery\" width=\"1145\" height=\"568\" \/><\/p>\n<p>After launching version 1.0 of SplitWit, I decided to enhance the platform by adding features. <a href=\"https:\/\/www.splitwit.com\/blog\/image-a-b-test\/\" target=\"_blank\" rel=\"noopener\">An important A\/B experiment involves swapping images<\/a>. This is particularly useful on ecommerce stores that sell physical products.<\/p>\n<p>Originally, users could only swap images by entering a URL. To the average website owner, this would seem lame. For SplitWit to be legit, adding images on the fly had to be a feature.<\/p>\n<p>I wrote three scripts &#8211; one to upload files, one to fetch them, and one to delete them. Each leverages a <a href=\"https:\/\/github.com\/tpyo\/amazon-s3-php-class\" target=\"_blank\" rel=\"noopener\">standalone PHP class<\/a>\u00a0written by\u00a0<a href=\"https:\/\/github.com\/tpyo\" target=\"_blank\" rel=\"noopener\">Donovan Sch\u00f6nknecht<\/a>, making it easy to interact with AWS S3. All you&#8217;ll need is your S3 bucket name and IAM user credentials. The library provides methods to do everything you need.<\/p>\n<h2>AWS S3<\/h2>\n<p><a href=\"https:\/\/aws.amazon.com\/s3\/\">Amazon S3 stands for &#8220;simple storage service&#8221;. <\/a>It provides data storage that is scalable, secure, highly available, and performant.<\/p>\n<p><a href=\"https:\/\/docs.aws.amazon.com\/AmazonS3\/latest\/user-guide\/create-bucket.html\" target=\"_blank\" rel=\"noopener\">A new bucket can be created directly from the management console.<\/a><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-1051 size-full\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2020\/12\/create-new-s3-bucket.png\" alt=\"create new s3 bucket\" width=\"959\" height=\"763\" \/><\/p>\n<p>You&#8217;ll want to create a new IAM user to programmatically interact with this bucket. Make sure that new user is added to a group that includes the permission policy \u201cAmazonS3FullAccess\u201d. You can find the\u00a0access key ID and secret in the &#8220;Security credentials&#8221; tab.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-1053\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2020\/12\/iam-user-creds.png\" alt=\"IAM user in AWS with permissions for S3\" width=\"814\" height=\"319\" \/><\/p>\n<h2>Uploading image files<\/h2>\n<p>When users select an image in the visual editor, they are shown a button to upload a new file. Clicking on it opens the gallery modal.<\/p>\n<pre>&lt;div id=\"image-gallery-modal\" class=\"modal image-gallery-modal\" style=\"display: none;\"&gt;\n  &lt;div class=\"modal-content\"&gt;\n    &lt;h3&gt;Your image gallery&lt;\/h3&gt;\n    &lt;p&gt;&lt;strong&gt;Upload a new file:&lt;\/strong&gt;&lt;\/p&gt;\n    &lt;input class=\"uploadimage\" id=\"uploadimage\" type=\"file\" name=\"uploadimage\" \/&gt;\n    &lt;p class=\"display-none file-error\"&gt;&lt;\/p&gt;\n    &lt;div&gt;&lt;hr \/&gt;&lt;\/div&gt;\n    &lt;div class=\"image-gallery-content\"&gt;&lt;\/div&gt;\n    &lt;p class=\"loading-images\"&gt;&lt;i class=\"fas fa-spinner fa-spin\"&gt;&lt;\/i&gt; Loading images...&lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<p>The HTML file-type input element presents a browser dialog to select a file. Once selected, the image data is posted to the S3 upload script.\u00a0<span style=\"font-size: 1rem;\">The newly uploaded image then replaces the existing image in the visual editor.\u00a0<\/span><\/p>\n<pre>$(\".uploadimage\").change(function(){\n\n    var file = $(this)[0].files[0];\n    var formData = new FormData();\n    formData.append(\"file\", file, file.name);\n    formData.append(\"upload_file\", true);\n\n    $.ajax({\n      type: \"POST\",\n      url: \"\/s3-upload.php\",\n      xhr: function () {\n        var myXhr = $.ajaxSettings.xhr();\n        if (myXhr.upload) {\n            \/\/ myXhr.upload.addEventListener('progress', that.progressHandling, false);\n        }\n        return myXhr;\n      },\n      success: function (response) {\n        console.log(response);\n\n        document.getElementById(\"uploadimage\").value = \"\";\n\n        if(response !== \"success\"){\n          $(\".file-error\").text(response).show();\n          setTimeout(function(){ $(\".file-error\").fadeOut();}, 3000)\n          return;\n        }\n\n        $(\"#image-gallery-modal\").hide();\n        loadS3images();\n        var newImageUrl = \"https:\/\/splitwit-image-upload.s3.amazonaws.com\/&lt;?php echo $_SESSION['userid'];?&gt;\/\" + file.name;\n        $(\"input.img-url\").val(newImageUrl);\n        $(\".image-preview\").attr(\"src\", newImageUrl).show();\n        $(\".image-label .change-indicator\").show();\n\n        \/\/update editor (right side)\n        var selector = $(\".selector-input\").val();\n        var iFrameDOM = $(\"iframe#page-iframe\").contents()\n        if($(\".element-change-wrap\").is(\":visible\")){\n          iFrameDOM.find(selector).attr(\"src\", newImageUrl).attr(\"srcset\", \"\");\n          $(\".element-change-save-btn\").removeAttr(\"disabled\");\n        }\n        if($(\".insert-html-wrap\").is(\":visible\")){\n          var position = $(\".position-select\").val();\n          var htmlInsertText = \"&lt;img style='display: block; margin: 10px auto;' class='htmlInsertText' src='\"+newImageUrl+\"'&gt;\";\n          iFrameDOM.find(\".htmlInsertText\").remove();\n          if(position == \"before\"){\n            iFrameDOM.find(selector).before(htmlInsertText);\n          }\n          if(position == \"after\"){\n            iFrameDOM.find(selector).after(htmlInsertText);\n          }\n        }\n      },\n      error: function (error) {\n        console.log(\"error: \");\n        console.log(error);\n      },\n      async: true,\n      data: formData,\n      cache: false,\n      contentType: false,\n      processData: false,\n      timeout: 60000\n  });\n\n});\n<\/pre>\n<p>The upload script puts files in the same S3 bucket, under a separate sub-directory for each user account ID. It checks the MIME type on the file to make sure an image is being uploaded.<\/p>\n<pre>&lt;?php\nrequire 's3.php';\n\n$s3 = new S3(\"XXXX\", \"XXXX\"); \/\/access key ID and secret\n\n\/\/ echo \"S3::listBuckets(): \".print_r($s3-&gt;listBuckets(), 1).\"\\n\";\n\n$bucketName = 'image-upload';\n\nif(isset($_FILES['file'])){\n\t$file_name = $_FILES['file']['name'];\n\t$uploadFile = $_FILES['file']['tmp_name'];\n\n\tif ($_FILES['file']['size'] &gt; 5000000) { \/\/5 megabyte\n     \t   echo 'Exceeded filesize limit.';\n     \t   die();\n    \t}\n    \t$finfo = new finfo(FILEINFO_MIME_TYPE);\n\tif (false === $ext = array_search(\n\t        $finfo-&gt;file($uploadFile),\n\t        array(\n\t            'jpg' =&gt; 'image\/jpeg',\n\t            'png' =&gt; 'image\/png',\n\t            'gif' =&gt; 'image\/gif',\n\t        ),\n\t        true\n\t    )) {\n\t    \tif($_FILES['file']['type'] == \"\"){\n\t    \t\techo 'File format not found. Please re-save the file.';\n\t    \t}else{\n\t\t    \techo 'Invalid file format.';\n\t\t    }\n     \t    die();\n\t }\n\n\t\/\/create new directory with account ID, if it doesn't already exist\n\tsession_start();\n\t$account_id = $_SESSION['userid'];\n\n\tif ($s3-&gt;putObjectFile($uploadFile, $bucketName, $account_id.\"\/\".$file_name, S3::ACL_PUBLIC_READ)) {\n\t\techo \"success\";\n\t}\n\n}\n?&gt;\n<\/pre>\n<p><span style=\"font-size: 1rem;\">After upload, the gallery list is reloaded by the loadS3images() function.<\/span><\/p>\n<h2>Fetching image files from S3<\/h2>\n<p>When the image gallery modal first shows, that same\u00a0<span style=\"font-size: 1rem;\">loadS3images()<\/span> runs to populate any images that have been previously uploaded.<\/p>\n<pre>function loadS3images(){\n\n  $.ajax({\n      url:\"\/s3-get-objects.php\",\n      complete: function(response){\n        gotImages = true;\n        $(\".loading-images\").hide();\n        var data = JSON.parse(response.responseText);\n        var x;\n        var html = \"&lt;p&gt;&lt;strong&gt;Select existing file:&lt;\/strong&gt;&lt;\/p&gt;\";\n        var l = 0;\n        for (x in data) {\n          l++;\n          var name = data[x][\"name\"];\n          nameArr = name.split(\"\/\");\n          name = nameArr[1];\n          var imgUrl = \"https:\/\/splitwit-image-upload.s3.amazonaws.com\/&lt;?php echo $_SESSION['userid'];?&gt;\/\" + name;\n          html += \"&lt;div class='image-data-wrap'&gt;&lt;p class='filename'&gt;\"+name+\"&lt;\/p&gt;&lt;img style='width:50px;display:block;margin:10px;' src='' class='display-none'&gt;&lt;button type='button' class='btn select-image'&gt;Select&lt;\/button&gt; &lt;button type='button' class='btn preview-image'&gt;Preview&lt;\/button&gt; &lt;button type='button' class='btn delete-image'&gt;Delete&lt;\/button&gt;&lt;hr \/&gt;&lt;\/div&gt;\"\n        }\n        if(l){\n          $(\".image-gallery-content\").html(html);\n        }\n\n      }\n    });\n}\n<\/pre>\n<p>It hits the &#8220;get objects&#8221; PHP script to pull the files in the account&#8217;s directory.<\/p>\n<pre>&lt;?php\nrequire 's3.php';\n\n$s3 = new S3(\"XXX\", \"XXX\"); \/\/access key ID and secret\n\n$bucketName = 'image-upload';\nsession_start();\n$account_id = $_SESSION['userid'];\n$info = $s3-&gt;getBucket($bucketName, $account_id);\necho json_encode($info);\n\n?&gt;\n<\/pre>\n<p>Existing images can be chosen to replace the one currently selected in the editor. There are also options to preview and delete.<\/p>\n<h2>Delete an S3 object<\/h2>\n<p>When the delete button is pressed for a file in the image gallery, all we need to do is pass the filename along. If the image is currently being used, we also remove it from the editor.<\/p>\n<pre>$(\".image-gallery-content\").on(\"click\", \".delete-image\", function() {\n    var parent = $(this).parent();\n    var filename = parent.find(\".filename\").text();\n    var currentImageUrl = $(\".img-url\").val();\n    if(currentImageUrl ==\"https:\/\/splitwit-image-upload.s3.amazonaws.com\/&lt;?php echo $_SESSION['userid'];?&gt;\/\" + filename){\n      $(\".img-url\").val(testSelectorElImage);\n      $(\".image-preview\").attr(\"src\", testSelectorElImage);\n      var selector = $(\".selector-input\").val();\n      var iFrameDOM = $(\"iframe#page-iframe\").contents()\n      iFrameDOM.find(selector).attr(\"src\", testSelectorElImage);\n    }\n    $.ajax({\n      method:\"POST\",\n      data: {\n        'filename': filename,\n      },\n      url: \"\/s3-delete.php?filename=\"+filename,\n      complete: function(response){\n        parent.remove();\n        if(!$(\".image-data-wrap\").length){\n          $(\".image-gallery-content\").html(\"\");\n        }\n      }\n    })\n\n});\n<\/pre>\n<p>&nbsp;<\/p>\n<pre>&lt;?php\nrequire 's3.php';\n\n$s3 = new S3(\"XXX\", \"XXX\"); \/\/access key ID and secret\n\n$bucketName = 'image-upload';\nsession_start();\n$account_id = $_SESSION['userid'];\n$filename = $_POST['filename'];\nif ($s3-&gt;deleteObject($bucketName, $account_id.\"\/\".$filename) ){\n\techo \"S3::deleteObject(): Deleted file\\n\";\n}\n\n?&gt;\n<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Allowing users to upload images to your app can be a pivotal feature. Many digital products rely on it. This post will show you how to do it using PHP and AWS S3. After launching version 1.0 of SplitWit, I decided to enhance the platform by adding features. An important A\/B experiment involves swapping images. &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.antpace.com\/blog\/image-upload-gallery\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Build an image upload gallery&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":3211,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[12,92,111,112],"class_list":["post-796","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development","tag-aws","tag-php","tag-s3","tag-saas"],"_links":{"self":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts\/796","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/comments?post=796"}],"version-history":[{"count":1,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts\/796\/revisions"}],"predecessor-version":[{"id":3212,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts\/796\/revisions\/3212"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/media\/3211"}],"wp:attachment":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/media?parent=796"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/categories?post=796"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/tags?post=796"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}