在上载器中,如何编辑/更新在先前会话中上载的文件的S3文件元数据? [英] In fine uploader, how to edit/update S3 file metadata for files that are uploaded in previous sessions?

查看:79
本文介绍了在上载器中,如何编辑/更新在先前会话中上载的文件的S3文件元数据?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个要求用户需要编辑/更新在先前会话中上载的s3文件元数据.我已经实现了初始文件列表",但是我需要在显示列表中使文件元数据(文件名,标题-我的情况下为新字段)可编辑.能做到吗?

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?

我看到了编辑文件功能,但这是有限的到文件上传之前.看起来我的要求不容易获得即用型FU.我遵循以下方法.

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.

  1. 在模板中,我有一个带有文本"Update Caption"的按钮,该按钮具有onclick ="captionUpdate()",它将JS变量(isCaptionUpdate)设置为true.
  2. 字幕更新将触发DeleteFile端点,除了它将为模板中定义的文本字段中的字幕值设置参数数据
  3. 在服务器端代码中,该过程检查Caption参数,然后调用函数updateObjectWithCaption()

以上所有功能均可与以下挑战无缝配合.请参见屏幕截图.

All of the above works seamlessly with following challenges.Please see the screenshot.

  1. 当用户单击更新标题"时,它遵循DELETE步骤,并且由于我传递了标题参数,因此它会更新S3文件.但是问题出在文件列表中,我将看到一小段状态文字正在删除....."出现.如何将状态更改为正在更新字幕..."或类似的内容
  2. #1的另一个问题是,一旦S3更新,文件中的文件列表就会被删除. UI部分出于某种原因仍然认为这是DELETE步骤,如何对UI说它并没有真正删除?
  3. 正如您在JS的deleteFile部分所看到的那样,标题取自document.getElementById('caption').value;.这意味着,即使我单击第二个,第三个或第四个文件的更新标题",它也将首次出现Caption元素.如何获得特定文件的标题?
  4. 最后但并非最不重要的一点,我如何仅对以前上传的文件显示更新标题"按钮.我不希望在新上传时显示此按钮.

很抱歉有太多问题.我无法分开这些问题,因为它们都与S3文件元数据更新主题有关.

Sorry for too many question. I could not separate these question as they are all related to S3 file metadata update topic.

模板:

<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();
});

服务器端代码:

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;
}

推荐答案

当用户单击更新标题"时,它将遵循DELETE步骤,并且由于我正在传递标题参数,因此它将更新S3文件.但是问题出在文件列表中,我将看到一小段状态文字正在删除....."出现.如何将状态更改为正在更新字幕..."或类似的内容

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

您可以为删除文件设置各种文本选项功能,例如deleteFile.deletingStatusText.

There are various text options you can set for the delete file feature, such as deleteFile.deletingStatusText.

正如您在JS的deleteFile部分所看到的那样,标题取自document.getElementById('caption').value;.这意味着,即使我单击第二个,第三个或第四个文件的更新标题",它也将首次出现Caption元素.如何获取特定文件的标题?

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 ?

您的标记/模板存在缺陷,因为您最终将获得ID为标题"的多个元素. HTML不允许这样做.您需要相应地重组标记.在此不适合使用ID.而是使用类.您始终可以使用精细上传器的getItemByFileId API获取文件的容器元素方法.这样,您可以查询后代元素以查找具有特定属性的元素.

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.

用户提交的

文件(非固定/初始文件)在DOM中表示后,将导致对onSubmitted回调处理程序的调用.此时,您可以使用前面提到的getItemByFileId来检索容器元素并隐藏按钮.

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.

这篇关于在上载器中,如何编辑/更新在先前会话中上载的文件的S3文件元数据?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆