В тонкой загрузке, как редактировать / обновлять метаданные файла S3 для файлов, загруженных в предыдущих сеансах?

У меня есть требование, в котором пользователь должен редактировать / обновлять метаданные файла s3, которые были загружены в предыдущих сеансах. Я реализовал Initial File List, но мне нужно сделать метаданные файла (имя файла, заголовок - новое поле в моем случае) редактируемыми в списке отображения. Можно ли это сделать?

я вижуфункция редактирования файлов, но это ограничено до загрузки файла. Похоже, мое требование не легко поддерживается из коробки FU. Я следовал нижеприведенному подходу.

В шаблоне у меня есть кнопка с текстом «Обновить заголовок», которая имеет onclick = "captionUpdate ()", которая установит для переменной JS (isCaptionUpdate) значение true.Обновление заголовка вызовет конечную точку DeleteFile, за исключением того, что оно установит данные параметров для значения заголовка из текстового поля, определенного в шаблоне.В коде на стороне сервера процесс проверяет параметр Caption, а затем вызывает функцию updateObjectWithCaption ()

Все вышеперечисленное работает без проблем со следующимипроблемы. Пожалуйста, посмотритеСкриншот.

Когда пользователь нажимает «Обновить подпись», он выполняет шаги УДАЛИТЬ, и, поскольку я передаю параметр «Заголовок», он обновляет файл S3. Но проблема в списке файлов, на короткое время появится текст статуса «Удаление .....». Как я могу изменить статус на «Обновление заголовка ....» или что-то подобноеЕще одна проблема, связанная с # 1, заключается в том, что как только S3 обновляется, список Файл в файле удаляется. Пользовательский интерфейс по-прежнему считает, что это шаг DELETE по какой-то причине, как я могу сказать пользовательскому интерфейсу, что он на самом деле не удаляется?Как вы можете видеть в разделе deleteFile в JS, заголовок взят из document.getElementById ('caption'). Value; это означает, что даже если я нажму «Обновить подпись» 2-го, 3-го или 4-го файла, это будет первое появление элемента «Заголовок». Как я могу получить заголовок конкретного файла?И последнее, но не менее важное: как показать кнопку «Обновить подпись» только для ранее загруженного файла. Я не хочу показывать эту кнопку при новой загрузке.

Извините за слишком много вопросов. Я не мог отделить эти вопросы, так как они все связаны с темой обновления метаданных файла S3.

Шаблон:

<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 exa,mple).
    // 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)

Ваш ответ на вопрос