From 58adddd9eeea74110cfcf8b4abadd44fbcdde0a9 Mon Sep 17 00:00:00 2001 From: Kevin Heneveld <1192102+kevinheneveld@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:44:47 -0800 Subject: [PATCH] fix(files): file registration records the proc-fd link size (64 bytes) instead of the real file length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #717, file registration reads sizes from the lease's metadata path, which on Linux is a /proc/self/fd magic link. FileSystemInfo.Length reads the link inode itself (lstat), and proc magic symlinks report a constant st_size of 64 — so every file registered through a pinned lease persists Size = 64 regardless of the actual file. The UI then shows 0 KB wherever sizes are rendered (duplicate comparison, library size totals, quality scoring inputs). Open the metadata path instead and take the stream length: opening follows the magic link to the exact pinned object the lease holds, so the length is read from the real file without reintroducing the rename race the metadata path exists to avoid. Applies to both initial registration and physical-generation replacement snapshots (the latter falls back to the prior recorded size when the open fails). Co-Authored-By: Claude Fable 5 --- ...AudiobookFileService.PhysicalGeneration.cs | 4 +-- .../Audiobooks/Files/AudiobookFileService.cs | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/listenarr.application/Audiobooks/Files/AudiobookFileService.PhysicalGeneration.cs b/listenarr.application/Audiobooks/Files/AudiobookFileService.PhysicalGeneration.cs index 3dad98b99..964c765b3 100644 --- a/listenarr.application/Audiobooks/Files/AudiobookFileService.PhysicalGeneration.cs +++ b/listenarr.application/Audiobooks/Files/AudiobookFileService.PhysicalGeneration.cs @@ -274,10 +274,10 @@ private static AudiobookFile CreatePhysicalGenerationSnapshot( string? source, bool replaceMetadata) { - var fileInfo = new FileInfo(registrationLease.MetadataPath); var replacement = AudiobookFile.CreateUnresolved(currentFile.Path); replacement.AudiobookId = currentFile.AudiobookId; - replacement.Size = fileInfo.Exists ? fileInfo.Length : currentFile.Size; + replacement.Size = TryGetRegisteredFileLength(registrationLease.MetadataPath) + ?? currentFile.Size; replacement.DurationSeconds = replaceMetadata ? metadata?.Duration.TotalSeconds : Math.Abs(metadata?.Duration.TotalSeconds ?? 0) > double.Epsilon diff --git a/listenarr.application/Audiobooks/Files/AudiobookFileService.cs b/listenarr.application/Audiobooks/Files/AudiobookFileService.cs index 823fe2f2c..0bdcae55d 100644 --- a/listenarr.application/Audiobooks/Files/AudiobookFileService.cs +++ b/listenarr.application/Audiobooks/Files/AudiobookFileService.cs @@ -327,10 +327,9 @@ private async Task EnsureAudiobookFileCoreAsync( cacheIdentity, filePath); - var fi = new FileInfo(metadataPath); var fileRecord = AudiobookFile.CreateUnresolved(filePath); fileRecord.AudiobookId = audiobook.Id; - fileRecord.Size = fi.Exists ? fi.Length : null; + fileRecord.Size = TryGetRegisteredFileLength(metadataPath); fileRecord.Source = source; fileRecord.CreatedAt = DateTime.UtcNow; fileRecord.DurationSeconds = meta?.Duration.TotalSeconds; @@ -442,6 +441,32 @@ private static string ResolveAbsolutePath(string? path) => ? string.Empty : FileSystemPathIdentity.ResolveNativeAbsolutePath(path); + /// + /// Read the byte length of the file behind a registration metadata path. + /// The pinned lease's metadata path is a /proc/self/fd magic link on + /// Linux, and FileSystemInfo reads the link inode itself, whose reported + /// size is a constant 64 — not the file's length. Opening the path + /// follows the link to the pinned file, so the stream length is the real + /// size of the exact object the lease holds open. + /// + private static long? TryGetRegisteredFileLength(string metadataPath) + { + try + { + using var stream = new FileStream( + metadataPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete); + return stream.Length; + } + catch (Exception exception) when (exception is IOException + or UnauthorizedAccessException or NotSupportedException) + { + return null; + } + } + private void LogClaimRejection( int audiobookId, string path,