7 file.inc file_save_upload($form_field_name, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME)
4.6 file.inc file_save_upload($source, $dest = 0, $replace = FILE_EXISTS_RENAME)
4.7 file.inc file_save_upload($source, $dest = false, $replace = FILE_EXISTS_RENAME)
5 file.inc file_save_upload($source, $dest = FALSE, $replace = FILE_EXISTS_RENAME)
6 file.inc file_save_upload($source, $validators = array(), $dest = FALSE, $replace = FILE_EXISTS_RENAME)
8 file.module file_save_upload($form_field_name, $validators = array(), $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME)

Saves a file upload to a new location.

The source file is validated as a proper upload and handled as such. The file will be added to the files table as a temporary file. Temporary files are periodically cleaned. To make the file permanent file call file_set_status() to change its status.


$source: A string specifying the name of the upload field to save.

$validators: (optional) An associative array of callback functions used to validate the file. The keys are function names and the values arrays of callback parameters which will be passed in after the file object. The functions should return an array of error messages; an empty array indicates that the file passed validation. The functions will be called in the order specified.

$dest: A string containing the directory $source should be copied to. If this is not provided or is not writable, the temporary directory will be used.

$replace: Replace behavior when the destination file already exists:

Return value

An object containing the file information, or 0 in the event of an error.

Related topics

5 calls to file_save_upload()
hook_prepare in developer/hooks/node.php
This is a hook used by node modules. It is called after load but before the node is shown on the add/edit form.
locale_translate_import_form_submit in includes/locale.inc
Process the locale import form submission.
system_theme_settings in modules/system/system.admin.inc
Form builder; display theme configuration for entire site and individual themes.
upload_node_form_submit in modules/upload/upload.module
Save new uploads and store them in the session to be associated to the node on upload_save.
user_validate_picture in modules/user/user.module


includes/file.inc, line 594
API for handling file uploads and server file management.


function file_save_upload($source, $validators = array(), $dest = FALSE, $replace = FILE_EXISTS_RENAME) {
  global $user;
  static $upload_cache;

  // Add in our check of the the file name length.
  $validators['file_validate_name_length'] = array();

  // Return cached objects without processing since the file will have
  // already been processed and the paths in _FILES will be invalid.
  if (isset($upload_cache[$source])) {
    return $upload_cache[$source];

  // If a file was uploaded, process it.
  if (isset($_FILES['files']) && $_FILES['files']['name'][$source] && is_uploaded_file($_FILES['files']['tmp_name'][$source])) {
    // Check for file upload errors and return FALSE if a
    // lower level system error occurred.
    switch ($_FILES['files']['error'][$source]) {
      // @see http://php.net/manual/en/features.file-upload.errors.php
      case UPLOAD_ERR_OK:

        drupal_set_message(t('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $source, '%maxsize' => format_size(file_upload_max_size()))), 'error');
        return 0;

      case UPLOAD_ERR_NO_FILE:
        drupal_set_message(t('The file %file could not be saved, because the upload did not complete.', array('%file' => $source)), 'error');
        return 0;

        // Unknown error
        drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $source)), 'error');
        return 0;

    // Build the list of non-munged extensions.
    // @todo: this should not be here. we need to figure out the right place.
    $extensions = '';
    foreach ($user->roles as $rid => $name) {
      $extensions .= ' ' . variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));

    // Begin building file object.
    $file = new stdClass();
    $file->filename = file_munge_filename(trim(basename($_FILES['files']['name'][$source]), '.'), $extensions);
    $file->filepath = $_FILES['files']['tmp_name'][$source];
    $file->filemime = file_get_mimetype($file->filename);

    // If the destination is not provided, or is not writable, then use the
    // temporary directory.
    if (empty($dest) || file_check_path($dest) === FALSE) {
      $dest = file_directory_temp();

    $file->source = $source;
    $file->destination = file_destination(file_create_path($dest . '/' . $file->filename), $replace);
    $file->filesize = $_FILES['files']['size'][$source];

    // Call the validation functions.
    $errors = array();
    foreach ($validators as $function => $args) {
      array_unshift($args, $file);
      // Make sure $file is passed around by reference.
      $args[0] = &$file;
      $errors = array_merge($errors, call_user_func_array($function, $args));

    // Rename potentially executable files, to help prevent exploits.
    if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
      $file->filemime = 'text/plain';
      $file->filepath .= '.txt';
      $file->filename .= '.txt';
      // As the file may be named example.php.txt, we need to munge again to
      // convert to example.php_.txt, then create the correct destination.
      $file->filename = file_munge_filename($file->filename, $extensions);
      $file->destination = file_destination(file_create_path($dest . '/' . $file->filename), $replace);

    // Check for validation errors.
    if (!empty($errors)) {
      $message = t('The selected file %name could not be uploaded.', array('%name' => $file->filename));
      if (count($errors) > 1) {
        $message .= '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>';
      else {
        $message .= ' ' . array_pop($errors);
      form_set_error($source, $message);
      return 0;

    // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary directory.
    // This overcomes open_basedir restrictions for future file operations.
    $file->filepath = $file->destination;
    if (!move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->filepath)) {
      form_set_error($source, t('File upload error. Could not move uploaded file.'));
      watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->filepath));
      return 0;

    // If we made it this far it's safe to record this file in the database.
    $file->uid = $user->uid;
    $file->status = FILE_STATUS_TEMPORARY;
    $file->timestamp = time();
    drupal_write_record('files', $file);

    // Add file to the cache.
    $upload_cache[$source] = $file;
    return $file;
  return 0;


$replace is not a boolean. Use one of these constants from the API docs for file_destination():

$replace Replace behavior when the destination file already exists.

  • FILE_EXISTS_REPLACE - Replace the existing file
  • FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique
  • FILE_EXISTS_ERROR - Do nothing and return FALSE.


Note that the checks against $_FILES['files']['error'][$source] only happen when $_FILES['files']['tmp_name'][$source] is set, and depending on the PHP environment, this might not be set in certain error cases. e.g., when the upload size exceeds the amount allowed in php.ini and the directives are set not to allow a file write in that case (I forget where those settings are - somewhere in php.ini, if memory serves), then even though ['error'] is set but ['tmp_name'] is not, so the check against the error code never occurs in the current version of the logic above.

For myself, I've added an addtional check before calling file_save_upload(), copying the error checking logic, but only running it when ['tmp_name'] is empty and ['error'] is populated and not 0.

edit - here is a copy of that code:

// this check is essentially a copy of the check made in file_save_upload(),
  // but with some modifications to capture unique error conditions. See the
  // note at http://api.drupal.org/api/function/file_save_upload/6#comment-7154
$source = 'your_field_name';
  if (
array_key_exists('files', $_FILES)
array_key_exists($source, $_FILES['files']['name'])
  ) {
    switch (
$_FILES['files']['error'][$source]) {

'The file %file could not be saved, because it exceeds %maxsize, '.
'the maximum allowed size for uploads.',
'%file' => $source,
'%maxsize' => format_size(file_upload_max_size()),

'The file %file could not be saved, because the upload did not complete.',
'%file' => $source,

// Unknown error
'The file %file could not be saved. An unknown error has occurred '.
'(ref error with value %error at %url).',
'%file' => $source,
'%error' => $_FILES['files']['error'][$source],
'%$url' => l(
  } else {
form_set_error($source, t('Video file field is required.'));

Happy Coding!

Sean P. O. MacCath-Moran

Do not treat the $replace parameter like a boolean. This function calls file_copy and passes the $replace parameter. There are three predefined constants that the file_copy function uses when a file exists:


Use a constant, not a TRUE or FALSE, nor 1 nor 0.

Note that you have to add the correct enctype to your form to be able to upload files:

$form['#attributes'] = array('enctype' => "multipart/form-data");

Whithout this file_save_upload will always return 0.