/**
  * Handle S3 request
  *
  * Permission-checking provided by items()
  */
 private function _handleFileRequest($item)
 {
     if (!$this->permissions->canAccess($this->objectLibraryID, 'files')) {
         $this->e403();
     }
     $this->allowMethods(array('HEAD', 'GET', 'POST', 'PATCH'));
     if (!$item->isAttachment()) {
         $this->e400("Item is not an attachment");
     }
     // File info for client sync
     //
     // Use of HEAD method is deprecated after 2.0.8/2.1b1 due to
     // compatibility problems with proxies and security software
     if ($this->method == 'HEAD' || $this->method == 'GET' && $this->fileMode == 'info') {
         $info = Zotero_S3::getLocalFileItemInfo($item);
         if (!$info) {
             $this->e404();
         }
         /*
         header("Last-Modified: " . gmdate('r', $info['uploaded']));
         header("Content-Type: " . $info['type']);
         */
         header("Content-Length: " . $info['size']);
         header("ETag: " . $info['hash']);
         header("X-Zotero-Filename: " . $info['filename']);
         header("X-Zotero-Modification-Time: " . $info['mtime']);
         header("X-Zotero-Compressed: " . ($info['zip'] ? 'Yes' : 'No'));
         header_remove("X-Powered-By");
     } else {
         if ($this->method == 'GET' || $this->method == 'POST' && $this->fileView) {
             if ($this->fileView) {
                 $info = Zotero_S3::getLocalFileItemInfo($item);
                 if (!$info) {
                     $this->e404();
                 }
                 // For zip files, redirect to files domain
                 if ($info['zip']) {
                     $url = Zotero_Attachments::getTemporaryURL($item, !empty($_GET['int']));
                     if (!$url) {
                         $this->e500();
                     }
                     header("Location: {$url}");
                     exit;
                 }
             }
             // For single files, redirect to S3
             $url = Zotero_S3::getDownloadURL($item, 60);
             if (!$url) {
                 $this->e404();
             }
             Zotero_S3::logDownload($item, $this->userID, IPAddress::getIP());
             header("Location: {$url}");
             exit;
         } else {
             if ($this->method == 'POST' || $this->method == 'PATCH') {
                 if (!$item->isImportedAttachment()) {
                     $this->e400("Cannot upload file for linked file/URL attachment item");
                 }
                 $libraryID = $item->libraryID;
                 $type = Zotero_Libraries::getType($libraryID);
                 if ($type == 'group') {
                     $groupID = Zotero_Groups::getGroupIDFromLibraryID($libraryID);
                     $group = Zotero_Groups::get($groupID);
                     if (!$group->userCanEditFiles($this->userID)) {
                         $this->e403("You do not have file editing access");
                     }
                 } else {
                     $group = null;
                 }
                 // If not the client, require If-Match or If-None-Match
                 if (!$this->httpAuth) {
                     if (empty($_SERVER['HTTP_IF_MATCH']) && empty($_SERVER['HTTP_IF_NONE_MATCH'])) {
                         $this->e428("If-Match/If-None-Match header not provided");
                     }
                     if (!empty($_SERVER['HTTP_IF_MATCH'])) {
                         if (!preg_match('/^"?([a-f0-9]{32})"?$/', $_SERVER['HTTP_IF_MATCH'], $matches)) {
                             $this->e400("Invalid ETag in If-Match header");
                         }
                         if (!$item->attachmentStorageHash) {
                             $info = Zotero_S3::getLocalFileItemInfo($item);
                             $this->e412("ETag set but file does not exist");
                         }
                         if ($item->attachmentStorageHash != $matches[1]) {
                             $this->e412("ETag does not match current version of file");
                         }
                     } else {
                         if ($_SERVER['HTTP_IF_NONE_MATCH'] != "*") {
                             $this->e400("Invalid value for If-None-Match header");
                         }
                         if ($this->attachmentStorageHash) {
                             $this->e412("If-None-Match: * set but file exists");
                         }
                     }
                 }
                 //
                 // Upload authorization
                 //
                 if (!isset($_POST['update']) && !isset($_REQUEST['upload'])) {
                     $info = new Zotero_StorageFileInfo();
                     // Validate upload metadata
                     if (empty($_REQUEST['md5'])) {
                         $this->e400('MD5 hash not provided');
                     }
                     $info->hash = $_REQUEST['md5'];
                     if (!preg_match('/[abcdefg0-9]{32}/', $info->hash)) {
                         $this->e400('Invalid MD5 hash');
                     }
                     if (empty($_REQUEST['mtime'])) {
                         $this->e400('File modification time not provided');
                     }
                     $info->mtime = $_REQUEST['mtime'];
                     if (!isset($_REQUEST['filename']) || $_REQUEST['filename'] === "") {
                         $this->e400('File name not provided');
                     }
                     $info->filename = $_REQUEST['filename'];
                     if (!isset($_REQUEST['filesize'])) {
                         $this->e400('File size not provided');
                     }
                     $info->size = $_REQUEST['filesize'];
                     if (!is_numeric($info->size)) {
                         $this->e400("Invalid file size");
                     }
                     $info->contentType = isset($_REQUEST['contentType']) ? $_REQUEST['contentType'] : "";
                     if (!preg_match("/^[a-zA-Z0-9\\-\\/]+\$/", $info->contentType)) {
                         $info->contentType = "";
                     }
                     $info->charset = isset($_REQUEST['charset']) ? $_REQUEST['charset'] : "";
                     if (!preg_match("/^[a-zA-Z0-9\\-]+\$/", $info->charset)) {
                         $info->charset = "";
                     }
                     $contentTypeHeader = $info->contentType . ($info->contentType && $info->charset ? "; charset=" . $info->charset : "");
                     $info->zip = !empty($_REQUEST['zip']);
                     // Reject file if it would put account over quota
                     if ($group) {
                         $quota = Zotero_S3::getEffectiveUserQuota($group->ownerUserID);
                         $usage = Zotero_S3::getUserUsage($group->ownerUserID);
                     } else {
                         $quota = Zotero_S3::getEffectiveUserQuota($this->objectUserID);
                         $usage = Zotero_S3::getUserUsage($this->objectUserID);
                     }
                     $total = $usage['total'];
                     $fileSizeMB = round($info->size / 1024 / 1024, 1);
                     if ($total + $fileSizeMB > $quota) {
                         $this->e413("File would exceed quota ({$total} + {$fileSizeMB} > {$quota})");
                     }
                     Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
                     Zotero_DB::beginTransaction();
                     // See if file exists with this filename
                     $localInfo = Zotero_S3::getLocalFileInfo($info);
                     if ($localInfo) {
                         $storageFileID = $localInfo['storageFileID'];
                         // Verify file size
                         if ($localInfo['size'] != $info->size) {
                             throw new Exception("Specified file size incorrect for existing file " . $info->hash . "/" . $info->filename . " ({$localInfo['size']} != {$info->size})");
                         }
                     } else {
                         $oldStorageFileID = Zotero_S3::getFileByHash($info->hash, $info->zip);
                         if ($oldStorageFileID) {
                             // Verify file size
                             $localInfo = Zotero_S3::getFileInfoByID($oldStorageFileID);
                             if ($localInfo['size'] != $info->size) {
                                 throw new Exception("Specified file size incorrect for duplicated file " . $info->hash . "/" . $info->filename . " ({$localInfo['size']} != {$info->size})");
                             }
                             // Create new file on S3 with new name
                             $storageFileID = Zotero_S3::duplicateFile($oldStorageFileID, $info->filename, $info->zip, $contentTypeHeader);
                             if (!$storageFileID) {
                                 $this->e500("File duplication failed");
                             }
                         }
                     }
                     // If we already have a file, add/update storageFileItems row and stop
                     if (!empty($storageFileID)) {
                         Zotero_S3::updateFileItemInfo($item, $storageFileID, $info);
                         Zotero_DB::commit();
                         if ($this->httpAuth) {
                             header('Content-Type: application/xml');
                             echo "<exists/>";
                         } else {
                             header('Content-Type: application/json');
                             echo json_encode(array('exists' => 1));
                         }
                         exit;
                     }
                     Zotero_DB::commit();
                     // Add request to upload queue
                     $uploadKey = Zotero_S3::queueUpload($this->userID, $info);
                     // User over queue limit
                     if (!$uploadKey) {
                         header('Retry-After: ' . Zotero_S3::$uploadQueueTimeout);
                         if ($this->httpAuth) {
                             $this->e413("Too many queued uploads");
                         } else {
                             $this->e429("Too many queued uploads");
                         }
                     }
                     // Output XML for client requests (which use HTTP Auth)
                     if ($this->httpAuth) {
                         $params = Zotero_S3::generateUploadPOSTParams($item, $info, true);
                         header('Content-Type: application/xml');
                         $xml = new SimpleXMLElement('<upload/>');
                         $xml->url = Zotero_S3::getUploadBaseURL();
                         $xml->key = $uploadKey;
                         foreach ($params as $key => $val) {
                             $xml->params->{$key} = $val;
                         }
                         echo $xml->asXML();
                     } else {
                         if (!empty($_REQUEST['params']) && $_REQUEST['params'] == "1") {
                             $params = array("url" => Zotero_S3::getUploadBaseURL(), "params" => array());
                             foreach (Zotero_S3::generateUploadPOSTParams($item, $info) as $key => $val) {
                                 $params['params'][$key] = $val;
                             }
                         } else {
                             $params = Zotero_S3::getUploadPOSTData($item, $info);
                         }
                         $params['uploadKey'] = $uploadKey;
                         header('Content-Type: application/json');
                         echo json_encode($params);
                     }
                     exit;
                 }
                 //
                 // API partial upload and post-upload file registration
                 //
                 if (isset($_REQUEST['upload'])) {
                     $uploadKey = $_REQUEST['upload'];
                     if (!$uploadKey) {
                         $this->e400("Upload key not provided");
                     }
                     $info = Zotero_S3::getUploadInfo($uploadKey);
                     if (!$info) {
                         $this->e400("Upload key not found");
                     }
                     // Partial upload
                     if ($this->method == 'PATCH') {
                         if (empty($_REQUEST['algorithm'])) {
                             throw new Exception("Algorithm not specified", Z_ERROR_INVALID_INPUT);
                         }
                         $storageFileID = Zotero_S3::patchFile($item, $info, $_REQUEST['algorithm'], $this->body);
                     } else {
                         $remoteInfo = Zotero_S3::getRemoteFileInfo($info);
                         if (!$remoteInfo) {
                             error_log("Remote file {$info->hash}/{$info->filename} not found");
                             $this->e400("Remote file not found");
                         }
                         if ($remoteInfo['size'] != $info->size) {
                             error_log("Uploaded file size does not match ({$remoteInfo['size']} != {$info->size}) for file {$info->hash}/{$info->filename}");
                         }
                     }
                     // Set an automatic shared lock in getLocalFileInfo() to prevent
                     // two simultaneous transactions from adding a file
                     Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
                     Zotero_DB::beginTransaction();
                     if (!isset($storageFileID)) {
                         // Check if file already exists, which can happen if two identical
                         // files are uploaded simultaneously
                         $fileInfo = Zotero_S3::getLocalFileInfo($info);
                         if ($fileInfo) {
                             $storageFileID = $fileInfo['storageFileID'];
                         } else {
                             $storageFileID = Zotero_S3::addFile($info);
                         }
                     }
                     Zotero_S3::updateFileItemInfo($item, $storageFileID, $info);
                     Zotero_S3::logUpload($this->userID, $item, $uploadKey, IPAddress::getIP());
                     Zotero_DB::commit();
                     header("HTTP/1.1 204 No Content");
                     exit;
                 }
                 //
                 // Client post-upload file registration
                 //
                 if (isset($_POST['update'])) {
                     $this->allowMethods(array('POST'));
                     if (empty($_POST['mtime'])) {
                         throw new Exception('File modification time not provided');
                     }
                     $uploadKey = $_POST['update'];
                     $info = Zotero_S3::getUploadInfo($uploadKey);
                     if (!$info) {
                         $this->e400("Upload key not found");
                     }
                     $remoteInfo = Zotero_S3::getRemoteFileInfo($info);
                     if (!$remoteInfo) {
                         $this->e400("Remote file not found");
                     }
                     if (!isset($info->size)) {
                         throw new Exception("Size information not available");
                     }
                     $info->mtime = $_POST['mtime'];
                     // Set an automatic shared lock in getLocalFileInfo() to prevent
                     // two simultaneous transactions from adding a file
                     Zotero_DB::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
                     Zotero_DB::beginTransaction();
                     // Check if file already exists, which can happen if two identical
                     // files are uploaded simultaneously
                     $fileInfo = Zotero_S3::getLocalFileInfo($info);
                     if ($fileInfo) {
                         $storageFileID = $fileInfo['storageFileID'];
                     } else {
                         $storageFileID = Zotero_S3::addFile($info);
                     }
                     Zotero_S3::updateFileItemInfo($item, $storageFileID, $info);
                     Zotero_S3::logUpload($this->userID, $item, $uploadKey, IPAddress::getIP());
                     Zotero_DB::commit();
                     header("HTTP/1.1 204 No Content");
                     exit;
                 }
             }
         }
     }
     exit;
 }