Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] [WARNING] [paperless.handlers] Exception during file handling: [Errno 18] Invalid cross-device link: 
### Description

Hi,

i created 3 cifs MountPoints in folder: /usr/src/paperless/media/documents/archive/
till the latest updates this was no Problem ... moving files from one to another subfolder  was no problem ...

now i get this messages:
[2023-04-30 19:50:04,148] [DEBUG] [paperless.filehandling] Document has storage_path 1 (/schatzmeister/Rechnungen/PDF/{correspondent}/{created_year}/{created_month}/{created_year}_{created_month}_{created_day} {correspondent}-{title}) set

[2023-04-30 19:50:04,236] [DEBUG] [paperless.filehandling] Document has storage_path 3 (/wohlfahrt/SEPA-Mandate/PDF/{created_year}/{created_month}/{created_year}_{created_month}_{created_day} {correspondent}-{title}) set

[2023-04-30 19:50:04,332] [WARNING] [paperless.handlers] Exception during file handling: [Errno 18] Invalid cross-device link: '/usr/src/paperless/media/documents/archive/schatzmeister/Rechnungen/PDF/UNIELEKTRO/2023/04/2023_04_21 UNIELEKTRO-Zugangsdaten Unielektro.pdf' -> '/usr/src/paperless/media/documents/archive/wohlfahrt/SEPA-Mandate/PDF/2023/04/2023_04_21 UNIELEKTRO-Zugangsdaten Unielektro.pdf'

[2023-04-30 19:50:04,332] [INFO] [paperless.handlers] Restoring previous original path

here parts from my dockercompose.yaml:

    volumes:
      - ./export:/usr/src/paperless/export
      - ./consume:/usr/src/paperless/consume
      - data:/usr/src/paperless/data
      - media:/usr/src/paperless/media
      # Mountpoint for SCHATZMEISTER
      - schatzmeister_mount:/usr/src/paperless/media/documents/archive/schatzmeister
      # Mountpoint for VORSTAND
      - vorstand_mount:/usr/src/paperless/media/documents/archive/vorstand
      # Mountpoint for WuS
      - wohlfahrt_mount:/usr/src/paperless/media/documents/archive/wohlfahrt
    
…

volumes:
  data:
  media:
  pgdata:
  redisdata:  
  # Mountpoint for SCHATZMEISTER
  schatzmeister_mount:
    driver: local
    driver_opts:
      type: cifs
      device: //192.168.100.250/Schatzmeister
      o: #####
  # Mountpoint for VORSTAND
  vorstand_mount:
    driver: local
    driver_opts:
      type: cifs
      device: //192.168.100.250/Vorstand
      o: #####
  # Mountpoint for WOHLFAHRT
  wohlfahrt_mount:
    driver: local
    driver_opts:
      type: cifs
      device: //192.168.100.250/WuS-Leitung
      o: #####

### Steps to reproduce

send file per mail or upload ...
then move it to other "storagepath"


### Webserver logs

```bash
none
```


### Browser logs

_No response_

### Paperless-ngx version

1.14.2

### Host OS

MacOSx

### Installation method

Docker - official image

### Browser

_No response_

### Configuration changes

_No response_

### Other

_No response_


Discussion:
This is more likely to be caused by an update to Docker.  It manages the volumes in this case, including how they look internally to the system.  It should have _always_ been erroring out with this, as each volume appears as a different file system.

The good news, it should be easy enough to fix for us.

Resolving patch:
diff --git a/src/documents/management/commands/document_thumbnails.py b/src/documents/management/commands/document_thumbnails.py
--- a/src/documents/management/commands/document_thumbnails.py
+++ b/src/documents/management/commands/document_thumbnails.py
@@ -10,8 +10,8 @@
 from documents.parsers import get_parser_class_for_mime_type
 
 
-def _process_document(doc_in):
-    document: Document = Document.objects.get(id=doc_in)
+def _process_document(doc_id):
+    document: Document = Document.objects.get(id=doc_id)
     parser_class = get_parser_class_for_mime_type(document.mime_type)
 
     if parser_class:
diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -429,12 +429,12 @@ def update_filename_and_move_files(sender, instance: Document, **kwargs):
             if move_original:
                 validate_move(instance, old_source_path, instance.source_path)
                 create_source_path_directory(instance.source_path)
-                os.rename(old_source_path, instance.source_path)
+                shutil.move(old_source_path, instance.source_path)
 
             if move_archive:
                 validate_move(instance, old_archive_path, instance.archive_path)
                 create_source_path_directory(instance.archive_path)
-                os.rename(old_archive_path, instance.archive_path)
+                shutil.move(old_archive_path, instance.archive_path)
 
             # Don't save() here to prevent infinite recursion.
             Document.objects.filter(pk=instance.pk).update(
@@ -453,11 +453,11 @@ def update_filename_and_move_files(sender, instance: Document, **kwargs):
             try:
                 if move_original and os.path.isfile(instance.source_path):
                     logger.info("Restoring previous original path")
-                    os.rename(instance.source_path, old_source_path)
+                    shutil.move(instance.source_path, old_source_path)
 
                 if move_archive and os.path.isfile(instance.archive_path):
                     logger.info("Restoring previous archive path")
-                    os.rename(instance.archive_path, old_archive_path)
+                    shutil.move(instance.archive_path, old_archive_path)
 
             except Exception:
                 # This is fine, since:


Test patch:
diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py
--- a/src/documents/tests/test_file_handling.py
+++ b/src/documents/tests/test_file_handling.py
@@ -697,7 +697,7 @@ def test_move_archive_only(self):
         self.assertIsFile(doc.archive_path)
 
     @override_settings(FILENAME_FORMAT="{correspondent}/{title}")
-    @mock.patch("documents.signals.handlers.os.rename")
+    @mock.patch("documents.signals.handlers.shutil.move")
     def test_move_archive_error(self, m):
         def fake_rename(src, dst):
             if "archive" in str(src):
@@ -748,7 +748,7 @@ def test_move_file_gone(self):
         self.assertIsFile(doc.archive_path)
 
     @override_settings(FILENAME_FORMAT="{correspondent}/{title}")
-    @mock.patch("documents.signals.handlers.os.rename")
+    @mock.patch("documents.signals.handlers.shutil.move")
     def test_move_file_error(self, m):
         def fake_rename(src, dst):
             if "original" in str(src):
diff --git a/src/documents/tests/test_management_thumbnails.py b/src/documents/tests/test_management_thumbnails.py
--- a/src/documents/tests/test_management_thumbnails.py
+++ b/src/documents/tests/test_management_thumbnails.py
@@ -47,12 +47,16 @@ def test_process_document(self):
         self.assertIsFile(self.d1.thumbnail_path)
 
     @mock.patch("documents.management.commands.document_thumbnails.shutil.move")
-    def test_process_document_invalid_mime_type(self, m):
+    def test_process_document_invalid_mime_type(self, m: mock.Mock):
         self.d1.mime_type = "asdasdasd"
         self.d1.save()
 
+        # .save() triggers filename handling
+        m.reset_mock()
+
         _process_document(self.d1.id)
 
+        # Not called during processing of document
         m.assert_not_called()
 
     def test_command(self):