问题
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;
}
回答1:
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
There are various text options you can set for the delete file feature, such as deleteFile.deletingStatusText
.
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 ?
Your markup/template is flawed in that you will end up with multiple elements with an ID of "caption". This is not allowed in HTML. You'll need to restructure your markup accordingly. An ID is not appropriate here. Instead, use a class. You can always get the container element for a file using Fine Uploader's getItemByFileId API method. With this, you can query the descendant elements to look for one with a specific attribute.
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.
Files submitted by the user (non-canned/initial files) will result in a call to your onSubmitted
callback handler after they are represented in the DOM. At this point, you can use the previously mentioned getItemByFileId
to retrieve the container element and hide the button.
来源:https://stackoverflow.com/questions/36365148/in-fine-uploader-how-to-edit-update-s3-file-metadata-for-files-that-are-uploade