8.3.x file.module file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME)
8.0.x file.module file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME)
8.1.x file.module file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME)
8.2.x file.module file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME)
8.4.x file.module file_move(FileInterface $source, $destination = NULL, $replace = FILE_EXISTS_RENAME)
4.6.x file.inc file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME)
4.7.x file.inc file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME)
5.x file.inc file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME)
6.x file.inc file_move(&$source, $dest = 0, $replace = FILE_EXISTS_RENAME)
7.x file.inc file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME)

Moves a file to a new location and update the file's database entry.

Moving a file is performed by copying the file to the new location and then deleting the original.

  • Checks if $source and $destination are valid and readable/writable.
  • Performs a file move if $source is not equal to $destination.
  • If file already exists in $destination either the call will error out, replace the file or rename the file based on the $replace parameter.
  • Adds the new file to the files database.

Parameters

$source: A file object.

$destination: A string containing the destination that $source should be moved to. This must be a stream wrapper URI.

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

  • FILE_EXISTS_REPLACE - Replace the existing file. If a managed file with the destination name exists then its database entry will be updated and file_delete() called on the source file after hook_file_move is called. If no database entry is found then the source files record will be updated.
  • FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique.
  • FILE_EXISTS_ERROR - Do nothing and return FALSE.

Return value

Resulting file object for success, or FALSE in the event of an error.

See also

file_unmanaged_move()

hook_file_move()

Related topics

10 calls to file_move()
FileMoveTest::testExistingError in modules/simpletest/tests/file.test
Test that moving onto an existing file fails when FILE_EXISTS_ERROR is specified.
FileMoveTest::testExistingRename in modules/simpletest/tests/file.test
Test renaming when moving onto a file that already exists.
FileMoveTest::testExistingReplace in modules/simpletest/tests/file.test
Test replacement when moving onto a file that already exists.
FileMoveTest::testExistingReplaceSelf in modules/simpletest/tests/file.test
Test replacement when moving onto itself.
FileMoveTest::testNormal in modules/simpletest/tests/file.test
Move a normal file.

... See full list

File

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

Code

function file_move(stdClass $source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
  if (!file_valid_uri($destination)) {
    if (($realpath = drupal_realpath($source->uri)) !== FALSE) {
      watchdog('file', 'File %file (%realpath) could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%realpath' => $realpath, '%destination' => $destination));
    }
    else {
      watchdog('file', 'File %file could not be moved, because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', array('%file' => $source->uri, '%destination' => $destination));
    }
    drupal_set_message(t('The specified file %file could not be moved, because the destination is invalid. More information is available in the system log.', array('%file' => $source->uri)), 'error');
    return FALSE;
  }

  if ($uri = file_unmanaged_move($source->uri, $destination, $replace)) {
    $delete_source = FALSE;

    $file = clone $source;
    $file->uri = $uri;
    // If we are replacing an existing file re-use its database record.
    if ($replace == FILE_EXISTS_REPLACE) {
      $existing_files = file_load_multiple(array(), array('uri' => $uri));
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $delete_source = TRUE;
        $file->fid = $existing->fid;
      }
    }
    // If we are renaming around an existing file (rather than a directory),
    // use its basename for the filename.
    elseif ($replace == FILE_EXISTS_RENAME && is_file($destination)) {
      $file->filename = drupal_basename($destination);
    }

    $file = file_save($file);

    // Inform modules that the file has been moved.
    module_invoke_all('file_move', $file, $source);

    if ($delete_source) {
      // Try a soft delete to remove original if it's not in use elsewhere.
      file_delete($source);
    }

    return $file;
  }
  return FALSE;
}

Comments

patacra’s picture

I was wondering why the "filename" column in the database table was not updated. Only the "uri" is changed to the destination. Is this a bug?

On a website with Transliteration, Pathauto and File Field Sources installed, I noticed that a bunch of files where renamed following the Pathauto rules that were set. Unfortunately, by default, Pathauto removes underscores instead of replacing them with the defined separator ("-" by default). So I ended up with a whole lot of stupid file names (ex: thisismyfile.pdf instead of this-is-my-file.pdf). I decided to bulk rename them with a simple database query and then call transliteration_clean_filename() and file_move(). The files where properly renamed on the file system. But in the database, only the "uri" field was updated.

As I wasn't sure this was correct or not, I ended up with a ugly fix: just modify the source filename before calling the function:

<?php
$query = <<<END_OF_QUERY
SELECT
  fm.*
FROM
  `file_managed` fm
INNER JOIN
  ...
  ...
END_OF_QUERY;

$files = db_query($query);

foreach ($files as $file) {
  $source = file_load($file->fid);
  $new_filename = transliteration_clean_filename($source->origname);
  $new_destination = str_replace($source->filename, $new_filename, $source->uri);
  // Ugly fix to make the filename column update correctly in the db table.
  $source->filename = $new_filename;
  $result = file_move($source, $new_destination, FILE_EXIST_ERROR);
  // Check $result ...
}
?>
simon.westyn’s picture

I fixed it with HOOK_file_move

<?php
function HOOK_file_move($file, $source) {
    if ($file->filename != drupal_basename($file->uri)) {
        $file->filename = drupal_basename($file->uri);
        file_save($file);
    }
}
?>
ginosuave’s picture

I've got a module that moves uploaded files into folders based on their filenames and timestamps. Everything works fine with public files: the file is moved and the database entry is updated.

When I try uploading a file to the private directory, the file gets moved but the URI doesn't get updated in the databsae.

Here's the code:

  $old_uri = $new_file->uri;
  // Clean up filename for directory name.
  $dir_name = preg_replace('/\./', '_', $new_file->filename);

  $user_folder = variable_get('upload_archives_foldername');
  // If the variable has been set, add a slash at the end.
  $user_folder?$user_folder.='/':null;
  
  $file_streamwrapper = substr($old_uri, 0, strpos($old_uri,':'));

  $file_location = $file_streamwrapper.'://'.$user_folder.$dir_name.'/'.date('Y\-m\-d\_H\h\-i\m\-s\s',$new_file->timestamp);

  // file_move will trigger another call to hook_file_update, so we check to see if the file has already been moved before trying to move it.
  if ($old_uri != $new_uri) {
    file_prepare_directory($file_location, FILE_CREATE_DIRECTORY || FILE_MODIFY_PERMISSIONS);
    $moved_file = file_move($new_file, $new_uri, FILE_EXISTS_RENAME);
    if ($moved_file) {
      drupal_set_message(t('Uploaded file moved from @old to @new.', array('@old' => $old_uri, '@new' => $moved_file->uri)));
    }
  }

Why aren't the private files' URIs being updated?

spiderman’s picture

I have managed to get this to work, but I wonder if the difference is that you seem to be moving the uploaded file into a folder which may be outside of the system-wide configured private:// folder. It's also not entirely clear to me where you put the code you've copied here, so I had to futz a bit to get this logic in the correct place.

In my case, I have a content type with a single-value filefield that is configured for private storage. I have another option list field with 2 possible values, "bpso" and "paid", which are the levels of access I am providing to these files. I want to move the uploaded file into a sub-folder of the main private dirs folder configured on the filefield.

Anyway, here's what I did:

1. form_alter the node edit form for the content type which has my private filefield, adding an extra submit handler function (nos_files.module is a custom module, and the content type is called "nursing_order_set":

function nos_files_form_nursing_order_set_node_form_alter(&$form, &$form_state, $form_id) {
  $form['actions']['submit']['#submit'][] = 'nos_files_node_form_submit';
}

2. When the node form is submitted, look up where the file is, and move it if necessary into a folder based on the value of another field on the node ("availability"):

function nos_files_node_form_submit($form, &$form_state) {
  // Grab key values from the form, and load the file entity in question
  $values = $form_state['values'];
  $avail = $values['field_nursing_order_set_avail']['und'][0]['value'];
  $fid = $values['field_nursing_order_set_pdf']['und'][0]['fid'];
  $file = file_load($fid);

  // Extract directory information
  $target = file_uri_target($file->uri);
  $dirs = preg_split('!/!', $target);
  $size = count($dirs)-1; // Last index
  $filename = $dirs[$size]; // Save filename
  $lastdir = $dirs[$size-1];

  // If the file is not currently in the correct directory
  if ($lastdir != $avail) {
    drupal_set_message("File not currently in subdir $avail target $target");

    if (in_array($lastdir, array('paid', 'bpso'))) {
      // This indicates an update to an existing file
      $file_status = "existing";
      // It's already in a subdir, but needs to go in the other
      unset($dirs[$size]);
      $dirs[$size-1] = $avail;
    } else {
      // This is a new file just uploaded
      $file_status = "uploaded";
      // It's not in a paid/bpso dir, so move it there
      $dirs[$size] = $avail;
    }

    // Prepare new destination directory, and move the file there
    $destination = "private://" . implode('/', $dirs); // Recreate uri
    file_prepare_directory($destination, FILE_CREATE_DIRECTORY|FILE_MODIFY_PERMISSIONS);
    $moved_file = file_move($file, $destination . '/' . $filename, FILE_EXISTS_REPLACE);
    drupal_set_message(t('Moving @status file to new location based on availability: @new', array('@new' => $moved_file->uri, @status => $file_status )));

    // Update the form_state to reflect (possible) new fid
    $form_state['values']['field_nursing_order_set_pdf']['und'][0]['fid'] = $moved_file->fid;
  }
}

This is all somewhat rough, at the moment, but I've convinced myself it works: newly uploaded files get moved into the correct folder based on the "availability" field, and existing files get moved if I change the field and resave the node. When I peek at the file_managed table in the db, the uri field consistently gets updated with the new directory.

The last little section is the real work of the submit handler- the earlier 2/3rds just do the work of checking and preparing a new $destination directory for the file, and then the 3 key lines are basically:

  // $destination is the *directory* where we will move the file to
  file_prepare_directory($destination, FILE_CREATE_DIRECTORY|FILE_MODIFY_PERMISSIONS);

  // $file is the file entity, and $filename is a string. 
  // I've used FILE_EXISTS_REPLACE here, but you may want the default RENAME
  $moved_file = file_move($file, $destination . '/' . $filename, FILE_EXISTS_REPLACE);

  // Update the form_state to reflect (possible) new fid
  $form_state['values']['field_nursing_order_set_pdf']['und'][0]['fid'] = $moved_file->fid;
littletiger’s picture

Hi Ginosuave, i've been looking for exactly what you're describing (functionality, not problem ;).
This code of yours, how can I implement it? Could you provide a link to the module files you use?

BarisW’s picture

<?php
$result = db_query('SELECT field_slide_image_fid fid FROM {field_data_field_slide_image} GROUP BY field_slide_image_fid ORDER BY field_slide_image_fid');
while ($row = $result->fetchAssoc()) {
  $file = file_load($row['fid']);
  if (strstr($file->uri, 'private://') !== FALSE) {
    file_move($file, str_replace('private://', 'public://', $file->uri));
  }
}
?>