Repository: fsspec/filesystem_spec (python)

Issue:
Invalid cross-device link
Over at https://github.com/Lightning-AI/pytorch-lightning/issues/20270 (full traceback: https://github.com/Lightning-AI/pytorch-lightning/issues/20270#issuecomment-2829581394) the following error is raised

```python
OSError: [Errno 18] Invalid cross-device link: '/tmp/tmp657zwu1c' -> '/path/to/checkpoint.ckpt'
```
and during handling of the above exception
```python
PermissionError: [Errno 1] Operation not permitted
```
if trying to save checkpoints via lightning's `_atomic_save` function that now uses a transaction whereas there were no issues before the transaction was used.

An example of a source and target device interaction is saving training checkpoints from a compute node to a slow storage device.

Pull request description:
Bugfix/1827 invalid cross device link
Fixes #1827

Patch:
diff --git a/fsspec/implementations/local.py b/fsspec/implementations/local.py
index 70dfd90..0152816 100644
--- a/fsspec/implementations/local.py
+++ b/fsspec/implementations/local.py
@@ -430,12 +430,22 @@ class LocalFileOpener(io.IOBase):
     def commit(self):
         if self.autocommit:
             raise RuntimeError("Can only commit if not already set to autocommit")
-        shutil.move(self.temp, self.path)
         try:
-            mask = 0o666
-            os.chmod(self.path, mask & ~get_umask(mask))
-        except RuntimeError:
-            pass
+            shutil.move(self.temp, self.path)
+        except PermissionError as e:
+            # shutil.move raises PermissionError if os.rename
+            # and the default copy2 fallback with shutil.copystats fail.
+            # The file should be there nonetheless, but without copied permissions.
+            # If it doesn't exist, there was no permission to create the file.
+            if not os.path.exists(self.path):
+                raise e
+        else:
+            # If PermissionError is not raised, permissions can be set.
+            try:
+                mask = 0o666
+                os.chmod(self.path, mask & ~get_umask(mask))
+            except RuntimeError:
+                pass
 
     def discard(self):
         if self.autocommit:


Test patch:
diff --git a/fsspec/implementations/tests/test_local.py b/fsspec/implementations/tests/test_local.py
index 2e4c07c..fd6b656 100644
--- a/fsspec/implementations/tests/test_local.py
+++ b/fsspec/implementations/tests/test_local.py
@@ -1,4 +1,5 @@
 import bz2
+import errno
 import gzip
 import os
 import os.path
@@ -562,6 +563,25 @@ def test_multiple_filesystems_use_umask_cache(tmpdir):
     assert get_umask.cache_info().hits == 1
 
 
+def test_transaction_cross_device_but_mock_temp_dir_on_wrong_device(tmpdir):
+    # If the temporary file for a transaction is not on the correct device,
+    # os.rename in shutil.move will raise EXDEV and lookup('chmod') will raise
+    # a PermissionError.
+    fs = LocalFileSystem()
+    with (
+        patch(
+            "os.rename",
+            side_effect=OSError(errno.EXDEV, "Invalid cross-device link"),
+        ),
+        patch(
+            "os.chmod",
+            side_effect=PermissionError("Operation not permitted"),
+        ),
+    ):
+        with fs.transaction, fs.open(tmpdir + "/afile", "wb") as f:
+            f.write(b"data")
+
+
 def test_make_path_posix():
     cwd = os.getcwd()
     if WIN: