ProcessWireUpgrade icon indicating copy to clipboard operation
ProcessWireUpgrade copied to clipboard

Invalid cross-device link when using e.g. Docker volumes

Open moqmar opened this issue 4 years ago • 5 comments

When running an upgrade in a Docker container with the site directory being a volume (I'm currently working on this image: https://codeberg.org/container-library/processwire), I'm getting the following message:

Warning: rename(): The first argument to copy() function cannot be a directory in /var/www/html/site/modules/ProcessWireUpgrade/ProcessWireUpgrade.module on line 854
Warning: rename(/var/www/html/site/assets/cache/ProcessWireUpgrade/processwire-dev/wire/,/var/www/html/wire-3.0.175/): Invalid cross-device link in /var/www/html/site/modules/ProcessWireUpgrade/ProcessWireUpgrade.module on line 854
ProcessWireUpgrade: Unable to rename /site/assets/cache/ProcessWireUpgrade/processwire-dev/wire/ => /wire-3.0.175/

This is obviously due to the fact that the site directory is on a different filesystem, as it's a Docker volume - this could also happen with classical filesystems though. Would it be possible to recursively copy the directory in that case instead of using "rename"?

moqmar avatar Apr 17 '21 13:04 moqmar

@moqmar Thanks, I've not observed this before, but also not using the environment you've mentioned, so interested in learning more. Is it just a rename of directories that would fail here, or any rename() call on any file? A recursive copy() on its own wouldn't be a suitable replacement because a rename also ensures the source is also removed, so I'd probably want to detect this situation in PW's $files->rename() method and have it take care of the situation by doing a copy and then an unlink. Though I'm uncertain why/if an unlink() would be allowed if a rename() isn't, but that is apparently the case.

ryancramerdesign avatar May 03 '21 10:05 ryancramerdesign

I'm on windows and I'm using recursive copy in my own version of this module. There are 2 reasons, at least in my case:

  1. Sometimes rename fails because the folder is locked in windows explorer (windows file manager) even there is no apparent reason why the folder is locked. When I try to delete or rename a folder, I get an "access denied" error. Sometimes even the parent folder is locked. After I manually unlock the parent folder, then rename works.
  2. On windows, when you rename a folder that has a different parent (eg. from /site/assets/cache/foo to /site/modules/foo), permissions are applied differently when you rename or when you copy a folder. In a production, /site/assets/cache can have more lax folder permissions then on the /site/modules.

I know that recursive copy is not a good solution, since deleting/unlinking a folder after recursive copy might also fail, but at least I have a folder where I want it (at the destination) with the correct permissions. I'm using this:

		$result = wireCopy($old, $new);
		$this->log("wireCopy $old -> $new | result=$result");

		if($result) {
			$result1 = wireRmdir($old, true);
			$this->log("wireRmdir $old | result=$result1");
			$this->message("Renamed $_old => $_new");
		} else {
			$this->error("Unable to rename $_old => $_new");
			if(basename(rtrim($new, '/')) == 'wire') {
				throw new WireException("Upgrade aborted. Unable to rename $_old => $_new");
			}
		}

I like your idea to tackle this in $files->rename() method (I wanted to add a feature request, but decided it's just too specific to bother you with this). Maybe some config option that would trigger copy/unlink instead of rename and/or an option to the rename() method?

matjazpotocnik avatar May 03 '21 11:05 matjazpotocnik

On linux filesystems, the rename() method doesn't delete anything, it just changes the path on the filesystem. That means that it won't work between different filesystems at all, which is the error I'm getting here. You can "rename" a file from /hello/world to /somewhere/else on a Linux filesystem, but not to /media/usb-stick/blubb, as it's on its own filesystem, and it would have to be moved over byte by byte.

Calling copy() creates a new file on the target filesystem (and copies its content), and calling unlink() removes the file on the old filesystem - both possible by its own, so it would make sense as a fallback in this case.

moqmar avatar May 03 '21 11:05 moqmar

Thanks for the info @matjazpotocnik @moqmar this is helpful. The rename() behavior on linux file systems is actually exactly what we want for best case (safest and fastest), but I just wasn't aware that one might have the PW installation split across file systems, where /wire/ would be a different file system than /site/, or that the condition would cause PHP's rename function to fail. It actually makes me wonder if this module should be used for that type of upgrade at all since presumably /wire/ might be on a different file system because it is managed elsewhere or shared among other installs (making this sort of upgrade potentially more consequential). But in any case, I'll update PW's $files->rename() to support this as an option and a default fallback if PHP rename() fails, as well as add a dedicated $files->renameCopy() method. Since ProcessWireUpgrade can't count on PW versions having that method I'll probably just duplicate the code for that in the module, for the short term at least.

ryancramerdesign avatar May 03 '21 13:05 ryancramerdesign

What do you think about having a (config) option so that rename() method would always act as a copy/unlink? In my case, I would want that behavior, not just in instances where rename fails.

matjazpotocnik avatar May 03 '21 14:05 matjazpotocnik