blob: 3832c44b715961fa3e6ee70577b6428fcbcad171 [file] [log] [blame]
<?php
define('MEGABYTES', 1024 * 1024);
function format_uploaded_file($file_row)
{
return array(
'id' => $file_row['file_id'],
'size' => $file_row['file_size'],
'createdAt' => Database::to_js_time($file_row['file_created_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 <quota> - <used> - <new file size>, just ask for <new file size>
// 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;
}
?>