Repository: pulp/pulpcore

Issue:
FS Exporter should copy if it can't hard link
**Version**
Pulpcore 3.16 and above
 
**Describe the bug**
The FS Exporter works well when exporting with hardlinks, but does not handle the case where the linking might fail because pulp and exports directory might be on different mounted volumes.
**To Reproduce**
Steps to reproduce the behavior:

- Setup an nfs or sshfs mount
- add the mount directory to `ALLOWED_EXPORT_PATHS` in `/etc/pulp/settings.py`
- Export a repo to this mount point via fs exporter

**Actual Behavior**
```
Error: [Errno 18] Invalid cross-device link: '/var/lib/pulp/media/artifact/7a/831f9f90bf4d21027572cb503d20b702de8e8785b02c0397445c2e481d81b3' -> '/exports/repo/Packages/b/bear-4.1-1.noarch.rpm'
```
**Expected behavior**

If it can't hard link, it should instead try to make a right along the lines of -> https://github.com/pulp/pulp-2to3-migration/blob/main/pulp_2to3_migration/app/plugin/content.py#L163-L170


**Additional context**
It was discussed during the https://github.com/pulp/pulpcore/pull/2951 PR but got missed in that commit.

BZ: https://bugzilla.redhat.com/show_bug.cgi?id=2125444
FS Exporter should copy if it can't hard link
**Version**
Pulpcore 3.16 and above
 
**Describe the bug**
The FS Exporter works well when exporting with hardlinks, but does not handle the case where the linking might fail because pulp and exports directory might be on different mounted volumes.
**To Reproduce**
Steps to reproduce the behavior:

- Setup an nfs or sshfs mount
- add the mount directory to `ALLOWED_EXPORT_PATHS` in `/etc/pulp/settings.py`
- Export a repo to this mount point via fs exporter

**Actual Behavior**
```
Error: [Errno 18] Invalid cross-device link: '/var/lib/pulp/media/artifact/7a/831f9f90bf4d21027572cb503d20b702de8e8785b02c0397445c2e481d81b3' -> '/exports/repo/Packages/b/bear-4.1-1.noarch.rpm'
```
**Expected behavior**

If it can't hard link, it should instead try to make a right along the lines of -> https://github.com/pulp/pulp-2to3-migration/blob/main/pulp_2to3_migration/app/plugin/content.py#L163-L170


**Additional context**
It was discussed during the https://github.com/pulp/pulpcore/pull/2951 PR but got missed in that commit.

BZ: https://bugzilla.redhat.com/show_bug.cgi?id=2125444


Discussion:

Resolving patch:
diff --git a/pulpcore/app/tasks/export.py b/pulpcore/app/tasks/export.py
--- a/pulpcore/app/tasks/export.py
+++ b/pulpcore/app/tasks/export.py
@@ -2,6 +2,7 @@
 import json
 import logging
 import os
+import os.path
 import subprocess
 import tarfile
 
@@ -75,6 +76,15 @@ def _export_to_file_system(path, relative_paths_to_artifacts, method=FS_EXPORT_M
         and method != FS_EXPORT_METHODS.WRITE
     ):
         raise RuntimeError(_("Only write is supported for non-filesystem storage."))
+    os.makedirs(path)
+    export_not_on_same_filesystem = (
+        settings.DEFAULT_FILE_STORAGE == "pulpcore.app.models.storage.FileSystem"
+        and os.stat(settings.MEDIA_ROOT).st_dev != os.stat(path).st_dev
+    )
+
+    if method == FS_EXPORT_METHODS.HARDLINK and export_not_on_same_filesystem:
+        log.info(_("Hard link cannot be created, file will be copied."))
+        method = FS_EXPORT_METHODS.WRITE
 
     for relative_path, artifact in relative_paths_to_artifacts.items():
         dest = os.path.join(path, relative_path)
@@ -82,9 +92,11 @@ def _export_to_file_system(path, relative_paths_to_artifacts, method=FS_EXPORT_M
 
         if method == FS_EXPORT_METHODS.SYMLINK:
             src = os.path.join(settings.MEDIA_ROOT, artifact.file.name)
+            os.path.lexists(dest) and os.unlink(dest)
             os.symlink(src, dest)
         elif method == FS_EXPORT_METHODS.HARDLINK:
             src = os.path.join(settings.MEDIA_ROOT, artifact.file.name)
+            os.path.lexists(dest) and os.unlink(dest)
             os.link(src, dest)
         elif method == FS_EXPORT_METHODS.WRITE:
             with open(dest, "wb") as f, artifact.file as af:


Test patch: