fix: avoid duplicate capacity reservation for hardlinks
This commit is contained in:
@@ -5,11 +5,12 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Callable
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from archive_clients.protocol import decode_message, encode_message
|
||||
from archive_clients.eviction import (
|
||||
@@ -541,15 +542,29 @@ class ClientJobExecutor:
|
||||
sha256_hex=hashlib.sha256(resource.metainfo_bytes).hexdigest(),
|
||||
)
|
||||
del artifact
|
||||
route_root = self.route_path(definition.transfer.route_id)
|
||||
# Staging is normally zero-copy when qB's data root and the paired
|
||||
# Syncthing route share a filesystem. Do not reserve the complete
|
||||
# logical payload in that case: FileMaterializer will use link(2),
|
||||
# which consumes only directory/inode metadata. Retain the metainfo
|
||||
# allowance and reserve, and account for any source files that really
|
||||
# must fall back to a data-copy path.
|
||||
self._require_space(
|
||||
self.route_path(definition.transfer.route_id),
|
||||
definition.transfer.transfer_delta_logical_bytes
|
||||
route_root,
|
||||
self._copy_required_bytes(
|
||||
self.qb_root,
|
||||
route_root,
|
||||
(
|
||||
(entry.target_canonical_path, entry.logical_bytes)
|
||||
for entry in manifest.files
|
||||
),
|
||||
)
|
||||
+ len(resource.metainfo_bytes),
|
||||
)
|
||||
stage_transfer(
|
||||
manifest,
|
||||
source_root=self.qb_root,
|
||||
sync_root=self.route_path(definition.transfer.route_id),
|
||||
sync_root=route_root,
|
||||
store=self.store,
|
||||
artifact_sources={"metainfo/source.torrent": metainfo_path},
|
||||
sparse_supported=self.sparse_supported,
|
||||
@@ -607,9 +622,18 @@ class ClientJobExecutor:
|
||||
"target materialization was sent to the wrong client"
|
||||
)
|
||||
published = load_published_transfer(self._job_directory(definition))
|
||||
# The target can likewise hardlink an arrived Syncthing payload into
|
||||
# qB's content root when those directories share a filesystem.
|
||||
self._require_space(
|
||||
self.qb_root,
|
||||
definition.transfer.transfer_delta_logical_bytes,
|
||||
self._copy_required_bytes(
|
||||
published.job_directory,
|
||||
self.qb_root,
|
||||
(
|
||||
(entry.payload_relative_path, entry.logical_bytes)
|
||||
for entry in published.manifest.files
|
||||
),
|
||||
),
|
||||
)
|
||||
info_hash = _info_hash(definition)
|
||||
resource = self.qbittorrent.get_resource(info_hash)
|
||||
@@ -843,6 +867,36 @@ class ClientJobExecutor:
|
||||
f"{required} bytes required including reserve"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _copy_required_bytes(
|
||||
source_root: Path,
|
||||
destination_root: Path,
|
||||
files: Iterable[tuple[str, int]],
|
||||
) -> int:
|
||||
"""Return logical bytes that cannot be materialized by hardlink.
|
||||
|
||||
A hardlink is possible only for regular files on the destination
|
||||
filesystem. Conservatively charge a file when it cannot be inspected;
|
||||
the normal materializer will then provide the precise integrity error.
|
||||
"""
|
||||
|
||||
destination_device = destination_root.stat().st_dev
|
||||
required = 0
|
||||
for relative_path, logical_bytes in files:
|
||||
relative = PurePosixPath(relative_path)
|
||||
source = source_root.joinpath(*relative.parts)
|
||||
try:
|
||||
metadata = source.stat(follow_symlinks=False)
|
||||
except OSError:
|
||||
required += logical_bytes
|
||||
continue
|
||||
if (
|
||||
not stat.S_ISREG(metadata.st_mode)
|
||||
or metadata.st_dev != destination_device
|
||||
):
|
||||
required += logical_bytes
|
||||
return required
|
||||
|
||||
def _observer(
|
||||
self, definition: job_pb2.JobDefinition
|
||||
) -> SyncthingTransferObserver:
|
||||
|
||||
Reference in New Issue
Block a user