From 723cd7dcea992cc86a50d6d73cb9682d4f7e6415 Mon Sep 17 00:00:00 2001 From: kua-agent Date: Sat, 5 Sep 2026 09:54:58 +0400 Subject: [PATCH] =?UTF-8?q?fix(core):=20File=20Provider=20write=20probe=20?= =?UTF-8?q?=E2=80=94=20OneDriveLocator.probeWritable(at:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isWritableDirectory (permissions bits) is not enough: a OneDrive Files-On-Demand directory whose provider domain is signed out can report as existing and POSIX-writable while an actual write fails. probeWritable writes a small ".redline-probe-" file into the folder via AtomicFile.write (open+write+fsync+rename+directory-fsync), then removes it; any failure at write, fsync, or removal means false. Three unit tests: an ordinary writable directory (true, and no probe file left behind), a chmod 500 directory (false; permissions restored in teardown), and a plain file path (false). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp --- .../Support/TransportSettings.swift | 25 ++++++++++++ .../TransportSettingsTests.swift | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/Sources/ShotdeckCore/Support/TransportSettings.swift b/Sources/ShotdeckCore/Support/TransportSettings.swift index 9ced9dd..cf907f1 100644 --- a/Sources/ShotdeckCore/Support/TransportSettings.swift +++ b/Sources/ShotdeckCore/Support/TransportSettings.swift @@ -176,4 +176,29 @@ public enum OneDriveLocator { guard exists, isDirectory.boolValue else { return false } return fileManager.isWritableFile(atPath: url.path) } + + /// Writes a tiny probe file into `folder`, fsyncs it, then removes it — the only + /// reliable way to catch a OneDrive Files-On-Demand directory whose provider domain + /// is signed out: such a directory can report as existing and POSIX-writable + /// (`isWritableDirectory` returns true) while an actual write fails. True only when + /// the write, fsync, AND removal of the probe file all succeed; any failure at any + /// of those steps means false, so the caller treats the folder as unavailable + /// rather than proceeding to compose a real PDF into it. + public static func probeWritable( + at folder: URL, + fileManager: FileManager = .default + ) -> Bool { + let probeURL = folder.appendingPathComponent(".redline-probe-\(UUID().uuidString)") + do { + try AtomicFile.write(Data(), to: probeURL) + } catch { + return false + } + do { + try fileManager.removeItem(at: probeURL) + } catch { + return false + } + return true + } } diff --git a/Tests/ShotdeckCoreTests/TransportSettingsTests.swift b/Tests/ShotdeckCoreTests/TransportSettingsTests.swift index 8b8991f..835cfef 100644 --- a/Tests/ShotdeckCoreTests/TransportSettingsTests.swift +++ b/Tests/ShotdeckCoreTests/TransportSettingsTests.swift @@ -122,6 +122,44 @@ func isWritableDirectoryFalseForAPlainFileAndForANonexistentPath() throws { #expect(!OneDriveLocator.isWritableDirectory(at: dir.appendingPathComponent("does-not-exist"))) } +@Test +func probeWritableTrueForAnOrdinaryWritableDirectoryAndLeavesNoProbeFileBehind() throws { + let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-writable") + defer { try? FileManager.default.removeItem(at: dir) } + + #expect(OneDriveLocator.probeWritable(at: dir)) + + let leftovers = try FileManager.default.contentsOfDirectory(atPath: dir.path) + #expect(leftovers.isEmpty) +} + +@Test +func probeWritableFalseForAChmod500Directory() throws { + // The File Provider edge case this probe exists for: isWritableDirectory can be + // true (as verified by the isWritableDirectory tests above) while an actual write + // still fails. A chmod 500 directory reproduces that "looks writable, isn't" + // shape closely enough to prove the probe itself does a real write, not just + // another permissions-bit check. + let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-unwritable") + defer { + try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: dir.path) + try? FileManager.default.removeItem(at: dir) + } + try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: dir.path) + + #expect(!OneDriveLocator.probeWritable(at: dir)) +} + +@Test +func probeWritableFalseForAPlainFilePath() throws { + let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-file-parent") + defer { try? FileManager.default.removeItem(at: dir) } + let filePath = dir.appendingPathComponent("plain-file.txt") + FileManager.default.createFile(atPath: filePath.path, contents: Data("x".utf8)) + + #expect(!OneDriveLocator.probeWritable(at: filePath)) +} + struct TransportDefaultsSuite { let name: String let defaults: UserDefaults