$file_row['file_id'], 'size' => $file_row['file_size'], 'createdAt' => Database::to_js_time($file_row['file_created_at']), 'deletedAt' => Database::to_js_time($file_row['file_deleted_at']), 'mime' => $file_row['file_mime'], 'filename' => $file_row['file_filename'], 'extension' => $file_row['file_extension'], 'author' => $file_row['file_author'], 'sha256' => $file_row['file_sha256']); } function uploaded_file_path_for_row($file_row) { return config_path('uploadDirectory', $file_row['file_id'] . $file_row['file_extension']); } function validate_uploaded_file($field_name) { if (array_get($_SERVER, 'CONTENT_LENGTH') && empty($_POST) && empty($_FILES)) exit_with_error('FileSizeLimitExceeded'); if (!is_dir(config_path('uploadDirectory', ''))) exit_with_error('NotSupported'); $input_file = array_get($_FILES, $field_name); if (!$input_file) exit_with_error('NoFileSpecified'); if ($input_file['error'] == UPLOAD_ERR_INI_SIZE || $input_file['error'] == UPLOAD_ERR_FORM_SIZE) exit_with_error('FileSizeLimitExceeded'); if ($input_file['error'] != UPLOAD_ERR_OK) exit_with_error('FailedToUploadFile', array('name' => $input_file['name'], 'error' => $input_file['error'])); if (config('uploadFileLimitInMB') * MEGABYTES < $input_file['size']) exit_with_error('FileSizeLimitExceeded'); return $input_file; } function query_file_usage_for_user($db, $user) { if ($user) $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL AND file_author = $1', array($user)); else $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL AND file_author IS NULL'); if (!$count_result) exit_with_error('FailedToQueryDiskUsagePerUser'); return intval($count_result[0]["sum"]); } function query_total_file_usage($db) { $count_result = $db->query_and_fetch_all('SELECT sum(file_size) as "sum" FROM uploaded_files WHERE file_deleted_at IS NULL'); if (!$count_result) exit_with_error('FailedToQueryTotalDiskUsage'); return intval($count_result[0]["sum"]); } function create_uploaded_file_from_form_data($input_file, $remote_user) { $file_sha256 = hash_file('sha256', $input_file['tmp_name']); if (!$file_sha256) exit_with_error('FailedToComputeSHA256'); $matches = array(); $file_extension = null; if (preg_match('/(\.[a-zA-Z0-9]{1,5}){1,2}$/', $input_file['name'], $matches)) { $file_extension = $matches[0]; assert(strlen($file_extension) <= 16); } return array( 'author' => $remote_user, 'filename' => $input_file['name'], 'extension' => $file_extension, 'mime' => $input_file['type'], // Sanitize MIME types. 'size' => $input_file['size'], 'sha256' => $file_sha256 ); } function upload_file_in_transaction($db, $input_file, $remote_user, $additional_work = NULL) { $new_file_size = $input_file['size']; if (config('uploadUserQuotaInMB') * MEGABYTES - query_file_usage_for_user($db, $remote_user) < $new_file_size || config('uploadTotalQuotaInMB') * MEGABYTES - query_total_file_usage($db) < $new_file_size) { // Instead of - - , just ask for // since finding files to delete is an expensive operation. if (!prune_old_files($db, $new_file_size, $remote_user)) exit_with_error('FileSizeQuotaExceeded'); } $uploaded_file = create_uploaded_file_from_form_data($input_file, $remote_user); $db->begin_transaction(); $file_row = $db->select_or_insert_row('uploaded_files', 'file', array('sha256' => $uploaded_file['sha256'], 'deleted_at' => null), $uploaded_file, '*'); if (!$file_row) exit_with_error('FailedToInsertFileData'); // A concurrent session may have inserted another file. if (config('uploadUserQuotaInMB') * MEGABYTES < query_file_usage_for_user($db, $remote_user) || config('uploadTotalQuotaInMB') * MEGABYTES < query_total_file_usage($db)) { $db->rollback_transaction(); exit_with_error('FileSizeQuotaExceeded'); } if ($additional_work) { $error = $additional_work($db, $file_row); if ($error) { $db->rollback_transaction(); exit_with_error($error['status'], $error); } } $new_path = uploaded_file_path_for_row($file_row); if (!move_uploaded_file($input_file['tmp_name'], $new_path)) { $db->rollback_transaction(); exit_with_error('FailedToMoveUploadedFile'); } $db->commit_transaction(); return format_uploaded_file($file_row); } function delete_file($db, $file_row) { $db->begin_transaction(); if (!$db->query_and_get_affected_rows("UPDATE uploaded_files SET file_deleted_at = CURRENT_TIMESTAMP AT TIME ZONE 'UTC' WHERE file_id = $1", array($file_row['file_id']))) { $db->rollback_transaction(); return FALSE; } $file_path = uploaded_file_path_for_row($file_row); // The file may have been deleted by a concurrent session by the time we get here. if (file_exists($file_path) && !unlink($file_path)) { $db->rollback_transaction(); return FALSE; } $db->commit_transaction(); return TRUE; } function prune_old_files($db, $size_needed, $remote_user) { $user_filter = $remote_user ? 'AND file_author = $1' : 'AND file_author IS NULL'; $params = $remote_user ? array($remote_user) : array(); // 1. Delete old build products created for a patch not associated with any pending or in-progress builds. $build_product_query = $db->query("SELECT file_id, file_extension, file_size FROM uploaded_files, commit_set_items WHERE file_id = commitset_root_file AND commitset_patch_file IS NOT NULL AND file_deleted_at IS NULL AND NOT EXISTS (SELECT request_id FROM build_requests WHERE request_commit_set = commitset_set AND request_status <= 'running') $user_filter ORDER BY file_created_at", $params); if (!$build_product_query) return FALSE; while ($row = $db->fetch_next_row($build_product_query)) { if (!$row || !delete_file($db, $row)) return FALSE; $size_needed -= $row['file_size']; if ($size_needed <= 0) return TRUE; } // 2. Delete any uploaded file not associated with any pending or in-progress builds. $unused_file_query = $db->query("SELECT file_id, file_extension, file_size FROM uploaded_files WHERE NOT EXISTS (SELECT request_id FROM build_requests, commit_set_items WHERE (commitset_root_file = file_id OR commitset_patch_file = file_id) AND request_commit_set = commitset_set AND request_status <= 'running') AND file_deleted_at IS NULL $user_filter ORDER BY file_created_at", $params); if (!$unused_file_query) return FALSE; while ($row = $db->fetch_next_row($unused_file_query)) { if (!$row || !delete_file($db, $row)) return FALSE; $size_needed -= $row['file_size']; if ($size_needed <= 0) return TRUE; } return FALSE; } ?>