I have requirement in which user need to edit/update the s3 file metadata that are uploaded in the previous sessions. I have implemented Initial File List, but I need to make file metadata (filename, caption - new field in my case) editable in the display list. Can it be accomplished?
I see edit files feature, but that is limited to before file gets uploaded. Looks like my requirement not easily supported out of the box FU. I have followed below approach.
- In template I have a button with text 'Update Caption', which has onclick="captionUpdate()",that will set JS variable(isCaptionUpdate) to true.
- Caption update will trigger DeleteFile endpoint except that it will set param data for caption value from text field that is defines in template
- In server side code, the process checks for Caption param, and then call function updateObjectWithCaption()
All of the above works seamlessly with following challenges.Please see the screenshot.
- When user click on 'Update Caption', it follows DELETE steps and since I am passing Caption param, it updates S3 file. But problem is in the file list, I will see a status text called 'Deleting.....' appears for brief time. How can I change status to 'Updating Caption....' or something similar
- Another issue with #1 is that as soon as S3 update, the File in file list gets removed. UI part still thinks that it is DELETE step for some reason, how can I say to UI that it not really delete?
- As you can see in the deleteFile section of JS, caption is taken from document.getElementById('caption').value; that means, even if I click 'Update Caption' of 2nd or 3rd or 4th files, it is taking first occurrence of Caption element. How can I get the caption of the specific file ?
- Last but not least, how can I show 'Update Caption' button only for previously uploaded file. I do not want show this button on fresh upload.
Sorry for too many question. I could not separate these question as they are all related to S3 file metadata update topic.
Template:
<div class="qq-uploader-selector qq-uploader" qq-drop-area-text="Drop files here">
<div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div>
</div>
<div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
<span class="qq-upload-drop-area-text-selector"></span>
</div>
<div class="buttons">
<div class="qq-upload-button-selector qq-upload-button">
<div>Select files</div>
</div>
<button type="button" id="trigger-upload-section1" class="btn btn-primary">
<i class="icon-upload icon-white"></i> Upload
</button>
</div>
<span class="qq-drop-processing-selector qq-drop-processing">
<span>Processing dropped files...</span>
<span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
</span>
<ul class="qq-upload-list-selector qq-upload-list" aria-live="polite" aria-relevant="additions removals">
<li>
<div class="qq-progress-bar-container-selector">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
</div>
<span class="qq-upload-spinner-selector qq-upload-spinner"></span>
<img class="qq-thumbnail-selector" qq-max-size="100" qq-server-scale>
<span class="qq-upload-file-selector qq-upload-file"></span>
<span class="qq-edit-filename-icon-selector qq-edit-filename-icon qq-editable" aria-label="Edit filename"></span>
<input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
<span class="qq-upload-caption-selector qq-upload-caption"></span>
<span class="qq-edit-caption-icon-selector qq-edit-caption-icon qq-editable" aria-label="Edit caption"></span>
<input class="qq-edit-caption-selector qq-edit-caption qq-editing" placeholder="Caption here ..." tabindex="0" type="text" id="caption">
<span class="qq-upload-size-selector qq-upload-size"></span>
<button type="button" class="qq-btn qq-upload-cancel-selector qq-upload-cancel">Cancel</button>
<button type="button" class="qq-btn qq-upload-retry-selector qq-upload-retry">Retry</button>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">Delete</button>
<button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete" onclick="captionUpdate();">Update Caption</button>
<span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
</li>
</ul>
<dialog class="qq-alert-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Close</button>
</div>
</dialog>
<dialog class="qq-confirm-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">No</button>
<button type="button" class="qq-ok-button-selector">Yes</button>
</div>
</dialog>
<dialog class="qq-prompt-dialog-selector">
<div class="qq-dialog-message-selector"></div>
<input type="text">
<div class="qq-dialog-buttons">
<button type="button" class="qq-cancel-button-selector">Cancel</button>
<button type="button" class="qq-ok-button-selector">Ok</button>
</div>
</dialog>
</div>
JS
var isCaptionUpdate = false;
function captionUpdate(){
isCaptionUpdate = true;
};
var manualUploaderSection1 = new qq.s3.FineUploader({
element: document.getElementById('fine-uploader-manual-trigger-section1'),
template: 'qq-template-manual-trigger-section1',
autoUpload: false,
debug: true,
request: {
endpoint: "http://xx_my_bucket_xx.s3.amazonaws.com",
accessKey: "AKIAIAABIA",
},
signature: {
endpoint: "http://localhost/app/ci/php-s3-server/endpoint-cors.php"
},
uploadSuccess: {
endpoint: "http://localhost/app/ci/php-s3-server/endpoint-cors.php?success",
params: {
isBrowserPreviewCapable: qq.supportedFeatures.imagePreviews
}
},
session: {
endpoint: "http://localhost/app/ci/php-s3-server/endpoint-cors.php?filelist"
},
iframeSupport: {
localBlankPagePath: "success.html"
},
cors: {
expected: true
},
chunking: {
enabled: true
},
resume: {
enabled: true
},
deleteFile: {
enabled: true,
method: "POST",
endpoint: "http://localhost/app/ci/php-s3-server/endpoint-cors.php",
params: {
caption: function() {
if (isCaptionUpdate === true) {
isCaptionUpdate = false;
return document.getElementById('caption').value;
}
}
}
},
validation: {
itemLimit: 5,
sizeLimit: 15000000
},
thumbnails: {
placeholders: {
notAvailablePath: "http://localhost/app/ci/s3.fine-uploader/placeholders/not_available-generic.png",
waitingPath: "http://localhost/app/ci/s3.fine-uploader/placeholders/waiting-generic.png"
}
},
callbacks: {
onComplete: function(id, name, response) {
var previewLink = qq(this.getItemByFileId(id)).getByClass('preview-link')[0];
if (response.success) {
previewLink.setAttribute("href", response.tempLink)
}
},
onUpload: function(id, fileName) {
var caption = document.getElementById('caption').value;
this.setParams({'caption':caption});
}
}
});
qq(document.getElementById("trigger-upload-section1")).attach("click", function() {
manualUploaderSection1.uploadStoredFiles();
});
Server side code:
require '/vendor/autoload.php';
use Aws\S3\S3Client;
$clientPrivateKey = 'LB7r54Rgh9sCuTAC8V5F';
$serverPublicKey = 'AKIAU2ZEQ';
$serverPrivateKey = '8Xu6lxcDfKifHfn4pdELnM1E';
$expectedBucketName = 'xx_my_bucket_xx';
$expectedHostName = 'http://s3.amazonaws.com'; // v4-only
$expectedMaxSize = 15000000;
$method = getRequestMethod();
// This first conditional will only ever evaluate to true in a
// CORS environment
if ($method == 'OPTIONS') {
handlePreflight();
}
// This second conditional will only ever evaluate to true if
// the delete file feature is enabled
else if ($method == "DELETE") {
handleCorsRequest();
if (isset($_REQUEST['caption'])) {
updateObjectWithCaption();
} else {
deleteObject();
}
}
// This is all you really need if not using the delete file feature
// and not working in a CORS environment
else if ($method == 'POST') {
handleCorsRequest();
// Assumes the successEndpoint has a parameter of "success" associated with it,
// to allow the server to differentiate between a successEndpoint request
// and other POST requests (all requests are sent to the same endpoint in this example).
// This condition is not needed if you don't require a callback on upload success.
if (isset($_REQUEST["success"])) {
verifyFileInS3(shouldIncludeThumbnail());
}
else {
signRequest();
}
}
//filelist - this is to list already uploaded files
else if ($method == 'GET') {
if (isset($_REQUEST["filelist"])) {
getFileList('test/');
}
}
function getFileList($filePrefix) {
global $expectedBucketName;
$objects = getS3Client()->getIterator('ListObjects', array(
//$objects = getS3Client()->ListObjects(array(
'Bucket' => $expectedBucketName,
'Prefix' => $filePrefix //must have the trailing forward slash "/"
));
$object_list = array();
foreach ($objects as $object) {
//echo $object['Key'] . "<br>";
$object_metadata = getHeadObject($expectedBucketName, $object['Key']);
if (isset($object_metadata['Metadata']['qqfilename'])) {
$keyArr = explode("/", $object['Key']);
$posOfLastString = sizeof($keyArr) - 1;
$uuidArry = explode(".", $keyArr[$posOfLastString]);
$link = getTempLink($expectedBucketName, $object['Key']);
$object_new = array();
$object_new['name'] = $object_metadata['Metadata']['qqfilename'];
$object_new['uuid'] = $uuidArry[0];
$object_new['s3Key'] = $object['Key'];
$object_new['size'] = $object['Size'];
$object_new['s3Bucket'] = $expectedBucketName;
$object_new['thumbnailUrl'] = $link;
array_push($object_list, (object)$object_new);
}
}
echo json_encode($object_list);
}
// This will retrieve the "intended" request method. Normally, this is the
// actual method of the request. Sometimes, though, the intended request method
// must be hidden in the parameters of the request. For example, when attempting to
// send a DELETE request in a cross-origin environment in IE9 or older, it is not
// possible to send a DELETE request. So, we send a POST with the intended method,
// DELETE, in a "_method" parameter.
function getRequestMethod() {
global $HTTP_RAW_POST_DATA;
// This should only evaluate to true if the Content-Type is undefined
// or unrecognized, such as when XDomainRequest has been used to
// send the request.
if(isset($HTTP_RAW_POST_DATA)) {
parse_str($HTTP_RAW_POST_DATA, $_POST);
}
if (isset($_REQUEST['_method'])) {
return $_REQUEST['_method'];
}
return $_SERVER['REQUEST_METHOD'];
}
// Only needed in cross-origin setups
function handleCorsRequest()
// If you are relying on CORS, you will need to adjust the allowed domain here.
header('Access-Control-Allow-Origin: http://localhost');
}
// Only needed in cross-origin setups
function handlePreflight() {
handleCorsRequest();
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
}
function getS3Client() {
global $serverPublicKey, $serverPrivateKey;
return S3Client::factory(array(
'key' => $serverPublicKey,
'secret' => $serverPrivateKey
));
}
// Only needed if the delete file feature is enabled
function deleteObject() {
getS3Client()->deleteObject(array(
'Bucket' => $_REQUEST['bucket'],
'Key' => $_REQUEST['key']
));
}
function getHeadObject($bucket, $key) {
$object_metadata = getS3Client()->headObject(array('Bucket' => $bucket,'Key' => $key));
$object_metadata = $object_metadata->toArray();
return $object_metadata;
}
function updateObjectWithCaption() {
$bucket = $_REQUEST['bucket'];
$key = $_REQUEST['key'];
$caption = $_REQUEST['caption'];
$object_metadata = getHeadObject($bucket, $key);
$filename = $object_metadata['Metadata']['qqfilename'];
$fileType = getFileType($key);
getS3Client()->copyObject(array(
'Bucket' => $bucket,
'Key' => $key,
'CopySource' => urlencode($_REQUEST['bucket'] . '/' . $key),
'MetadataDirective' => 'REPLACE',
//'CacheControl' => 'max-age=31536000',
//'Expires' => gmdate('D, d M Y H:i:s T', strtotime('+1 years')), // Set EXPIRES and CACHE-CONTROL headers to +1 year (RFC guidelines max.)
'ContentType' => $fileType,
'Metadata'=>array(
'qqcaption' => $caption,
'qqfilename' => $filename,
),
));
}
function getFileType($key) {
$file_parts = pathinfo($key);
$filetype = "";
switch($file_parts['extension'])
{
case "jpg":
$filetype = "image/jpeg";
break;
case "jpeg":
$filetype = "image/jpeg";
break;
case "png":
$filetype = "image/png";
break;
case "gif":
$filetype = "image/gif";
break;
case "tif":
$filetype = "image/tiff";
break;
case "tiff":
$filetype = "image/tiff";
break;
case "bmp":
$filetype = "image/bmp";
break;
}
return $filetype;
}
function signRequest() {
header('Content-Type: application/json');
$responseBody = file_get_contents('php://input');
$contentAsObject = json_decode($responseBody, true);
$jsonContent = json_encode($contentAsObject);
if (!empty($contentAsObject["headers"])) {
signRestRequest($contentAsObject["headers"]);
}
else {
signPolicy($jsonContent);
}
}
function signRestRequest($headersStr) {
$version = isset($_REQUEST["v4"]) ? 4 : 2;
if (isValidRestRequest($headersStr, $version)) {
if ($version == 4) {
$response = array('signature' => signV4RestRequest($headersStr));
}
else {
$response = array('signature' => sign($headersStr));
}
echo json_encode($response);
}
else {
echo json_encode(array("invalid" => true));
}
}
function isValidRestRequest($headersStr, $version) {
if ($version == 2) {
global $expectedBucketName;
$pattern = "/\/$expectedBucketName\/.+$/";
}
else {
global $expectedHostName;
$pattern = "/host:$expectedHostName/";
}
preg_match($pattern, $headersStr, $matches);
return count($matches) > 0;
}
function signPolicy($policyStr) {
$policyObj = json_decode($policyStr, true);
if (isPolicyValid($policyObj)) {
$encodedPolicy = base64_encode($policyStr);
if (isset($_REQUEST["v4"])) {
$response = array('policy' => $encodedPolicy, 'signature' => signV4Policy($encodedPolicy, $policyObj));
}
else {
$response = array('policy' => $encodedPolicy, 'signature' => sign($encodedPolicy));
}
echo json_encode($response);
}
else {
echo json_encode(array("invalid" => true));
}
}
function isPolicyValid($policy) {
global $expectedMaxSize, $expectedBucketName;
$conditions = $policy["conditions"];
$bucket = null;
$parsedMaxSize = null;
for ($i = 0; $i < count($conditions); ++$i) {
$condition = $conditions[$i];
if (isset($condition["bucket"])) {
$bucket = $condition["bucket"];
}
else if (isset($condition[0]) && $condition[0] == "content-length-range") {
$parsedMaxSize = $condition[2];
}
}
return $bucket == $expectedBucketName && $parsedMaxSize == (string)$expectedMaxSize;
}
function sign($stringToSign) {
global $clientPrivateKey;
return base64_encode(hash_hmac(
'sha1',
$stringToSign,
$clientPrivateKey,
true
));
}
function signV4Policy($stringToSign, $policyObj) {
global $clientPrivateKey;
foreach ($policyObj["conditions"] as $condition) {
if (isset($condition["x-amz-credential"])) {
$credentialCondition = $condition["x-amz-credential"];
}
}
$pattern = "/.+\/(.+)\\/(.+)\/s3\/aws4_request/";
preg_match($pattern, $credentialCondition, $matches);
$dateKey = hash_hmac('sha256', $matches[1], 'AWS4' . $clientPrivateKey, true);
$dateRegionKey = hash_hmac('sha256', $matches[2], $dateKey, true);
$dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true);
$signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true);
return hash_hmac('sha256', $stringToSign, $signingKey);
}
function signV4RestRequest($rawStringToSign) {
global $clientPrivateKey;
$pattern = "/.+\\n.+\\n(\\d+)\/(.+)\/s3\/aws4_request\\n(.+)/s";
preg_match($pattern, $rawStringToSign, $matches);
$hashedCanonicalRequest = hash('sha256', $matches[3]);
$stringToSign = preg_replace("/^(.+)\/s3\/aws4_request\\n.+$/s", '$1/s3/aws4_request'."\n".$hashedCanonicalRequest, $rawStringToSign);
$dateKey = hash_hmac('sha256', $matches[1], 'AWS4' . $clientPrivateKey, true);
$dateRegionKey = hash_hmac('sha256', $matches[2], $dateKey, true);
$dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true);
$signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true);
return hash_hmac('sha256', $stringToSign, $signingKey);
}
// This is not needed if you don't require a callback on upload success.
function verifyFileInS3($includeThumbnail) {
global $expectedMaxSize;
$bucket = $_REQUEST["bucket"];
$key = $_REQUEST["key"];
// If utilizing CORS, we return a 200 response with the error message in the body
// to ensure Fine Uploader can parse the error message in IE9 and IE8,
// since XDomainRequest is used on those browsers for CORS requests. XDomainRequest
// does not allow access to the response body for non-success responses.
if (isset($expectedMaxSize) && getObjectSize($bucket, $key) > $expectedMaxSize) {
// You can safely uncomment this next line if you are not depending on CORS
header("HTTP/1.0 500 Internal Server Error");
deleteObject();
echo json_encode(array("error" => "File is too big!", "preventRetry" => true));
}
else {
$link = getTempLink($bucket, $key);
$response = array("tempLink" => $link);
if ($includeThumbnail) {
$response["thumbnailUrl"] = $link;
}
echo json_encode($response);
}
}
// Provide a time-bombed public link to the file.
function getTempLink($bucket, $key) {
$client = getS3Client();
$url = "{$bucket}/{$key}";
$request = $client->get($url);
return $client->createPresignedUrl($request, '+15 minutes');
}
function getObjectSize($bucket, $key) {
$objInfo = getS3Client()->headObject(array(
'Bucket' => $bucket,
'Key' => $key
));
return $objInfo['ContentLength'];
}
// Return true if it's likely that the associate file is natively
// viewable in a browser. For simplicity, just uses the file extension
// to make this determination, along with an array of extensions that one
// would expect all supported browsers are able to render natively.
function isFileViewableImage($filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$viewableExtensions = array("jpeg", "jpg", "gif", "png", "tif", "tiff");
return in_array($ext, $viewableExtensions);
}
// Returns true if we should attempt to include a link
// to a thumbnail in the uploadSuccess response. In it's simplest form
// (which is our goal here - keep it simple) we only include a link to
// a viewable image and only if the browser is not capable of generating a client-side preview.
function shouldIncludeThumbnail() {
$filename = $_REQUEST["name"];
$isPreviewCapable = $_REQUEST["isBrowserPreviewCapable"] == "true";
$isFileViewableImage = isFileViewableImage($filename);
return !$isPreviewCapable && $isFileViewableImage;
}