From c16125b7bb155291ea10ba778a6727e3c737a0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20B=C3=B6decs?= Date: Tue, 6 Oct 2026 12:35:11 +0200 Subject: [PATCH 1/2] prep for rc1 --- .github/workflows/deployment.yml | 3 +- .../{testing.yml => pull-request.yml} | 16 ++++----- Package.resolved | 18 +++++----- Package.swift | 35 ++++++++----------- README.md | 10 +++--- .../FeatherStorage/ByteBufferSequence.swift | 2 +- Sources/FeatherStorage/StorageClient.swift | 19 +++++++++- .../FeatherStorage/StorageClientError.swift | 2 +- Sources/FeatherStorage/StorageSequence.swift | 2 +- .../FeatherStorageTestSuite.swift | 17 +++++++++ .../Mock/MockStorageClient.swift | 28 ++++++++++++--- .../Mock/MockStorageState.swift | 18 +++++++++- .../StorageSequenceTestSuite.swift | 2 +- docker/tests/Dockerfile | 2 +- 14 files changed, 119 insertions(+), 55 deletions(-) rename .github/workflows/{testing.yml => pull-request.yml} (66%) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 680623b..ca036e3 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -5,6 +5,7 @@ on: tags: - 'v*' - '[0-9]*' + workflow_dispatch: jobs: @@ -13,4 +14,4 @@ jobs: permissions: contents: read pages: write - id-token: write \ No newline at end of file + id-token: write diff --git a/.github/workflows/testing.yml b/.github/workflows/pull-request.yml similarity index 66% rename from .github/workflows/testing.yml rename to .github/workflows/pull-request.yml index 4d89771..e1b5d96 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/pull-request.yml @@ -1,4 +1,4 @@ -name: Testing +name: Pull request on: pull_request: @@ -7,9 +7,9 @@ on: jobs: - swiftlang_checks: - name: Swiftlang Checks - uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.7 + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.15 with: license_header_check_project_name: "project" format_check_enabled : true @@ -30,10 +30,10 @@ jobs: headers_check_enabled : true docc_warnings_check_enabled : true - swiftlang_tests: - name: Swiftlang Tests - uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + tests: + name: Tests + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.15 with: enable_windows_checks : false linux_build_command: "swift test --parallel --enable-code-coverage" - linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"nightly-6.0\"}, {\"swift_version\": \"nightly-6.1\"}, {\"swift_version\": \"nightly-6.3\"}]" + linux_swift_versions: '["6.3", "6.4"]' diff --git a/Package.resolved b/Package.resolved index 320cd7c..3b80b13 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "3a96a169432fa3e3a6825ae591e8dd28bcb50872f5f911feb31cfe61a8b0c9ba", + "originHash" : "3244b00acf96e248edae83203554eebee546b5ca8df006a6cc59b5120b89ad5e", "pins" : [ { "identity" : "swift-atomics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", - "version" : "1.3.0" + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" + "revision" : "98ef3c98609a1e31b7e157b5b619579001a789d6", + "version" : "1.7.1" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio", "state" : { - "revision" : "e932d3c4d8f77433c8f7093b5ebcbf91463948a0", - "version" : "2.95.0" + "revision" : "6db9d9abd92ad7399b8a564cafbb87d3507245fa", + "version" : "2.104.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", - "version" : "1.6.4" + "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", + "version" : "1.8.1" } } ], diff --git a/Package.swift b/Package.swift index 35bed44..8c23ad2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,26 +1,22 @@ -// swift-tools-version:6.1 +// swift-tools-version:6.3 import PackageDescription -// NOTE: https://github.com/swift-server/swift-http-server/blob/main/Package.swift -var defaultSwiftSettings: [SwiftSetting] = -[ - // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0441-formalize-language-mode-terminology.md +let swiftSettings: [SwiftSetting] = [ .swiftLanguageMode(.v6), - // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + .strictMemorySafety(), + .treatAllWarnings(as: .error), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("MemberImportVisibility"), - // https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("ImmutableWeakCaptures"), + .enableExperimentalFeature("SuppressedAssociatedTypes"), + .enableExperimentalFeature("LifetimeDependence"), .enableExperimentalFeature("Lifetimes"), - // https://github.com/swiftlang/swift/pull/65218 - .enableExperimentalFeature("AvailabilityMacro=featherStorage 1.0:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), + .enableUpcomingFeature("StrictConcurrency"), ] -#if compiler(>=6.2) -defaultSwiftSettings.append( - // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md - .enableUpcomingFeature("NonisolatedNonsendingByDefault") -) -#endif - let package = Package( name: "feather-storage", platforms: [ @@ -34,8 +30,7 @@ let package = Package( .library(name: "FeatherStorage", targets: ["FeatherStorage"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio", from: "2.9.0"), - // [docc-plugin-placeholder] + .package(url: "https://github.com/apple/swift-nio", from: "2.100.0"), ], targets: [ .target( @@ -43,7 +38,7 @@ let package = Package( dependencies: [ .product(name: "NIOCore", package: "swift-nio"), ], - swiftSettings: defaultSwiftSettings + swiftSettings: swiftSettings ), .testTarget( name: "FeatherStorageTests", @@ -51,7 +46,7 @@ let package = Package( .target(name: "FeatherStorage"), .product(name: "NIOFoundationCompat", package: "swift-nio"), ], - swiftSettings: defaultSwiftSettings + swiftSettings: swiftSettings ), ] ) diff --git a/README.md b/README.md index 1c846b2..e791232 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Abstract storage component, providing a shared API surface for file storage drivers written in Swift. [ - ![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138) + ![Release: 1.0.0-rc.1](https://img.shields.io/badge/Release-1%2E0%2E0--rc%2E1-F05138) ]( - https://github.com/feather-framework/feather-storage/releases/tag/1.0.0-beta.2 + https://github.com/feather-framework/feather-storage/releases/tag/1.0.0-rc.1 ) ## Features @@ -17,10 +17,10 @@ Abstract storage component, providing a shared API surface for file storage driv ## Requirements -![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138) +![Swift 6.3+](https://img.shields.io/badge/Swift-6%2E3%2B-F05138) ![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138) -- Swift 6.1+ +- Swift 6.3+ - Platforms: - Linux @@ -35,7 +35,7 @@ Abstract storage component, providing a shared API surface for file storage driv Use Swift Package Manager; add the dependency to your `Package.swift` file: ```swift -.package(url: "https://github.com/feather-framework/feather-storage", exact: "1.0.0-beta.2"), +.package(url: "https://github.com/feather-framework/feather-storage", exact: "1.0.0-rc.1"), ``` Then add `FeatherStorage` to your target dependencies: diff --git a/Sources/FeatherStorage/ByteBufferSequence.swift b/Sources/FeatherStorage/ByteBufferSequence.swift index f11cdf0..79377f2 100644 --- a/Sources/FeatherStorage/ByteBufferSequence.swift +++ b/Sources/FeatherStorage/ByteBufferSequence.swift @@ -4,7 +4,7 @@ // // Created by Tibor Bödecs on 2023. 01. 16. -import NIOCore +public import NIOCore /// An async sequence that streams a `ByteBuffer` in fixed-size chunks. public struct ByteBufferSequence: AsyncSequence, Sendable { diff --git a/Sources/FeatherStorage/StorageClient.swift b/Sources/FeatherStorage/StorageClient.swift index 49e8179..66862d8 100644 --- a/Sources/FeatherStorage/StorageClient.swift +++ b/Sources/FeatherStorage/StorageClient.swift @@ -12,10 +12,12 @@ public protocol StorageClient: Sendable { /// - Parameters: /// - key: The destination object key. /// - sequence: The source byte stream to upload. + /// - contentType: Optional MIME type stored with the object metadata. /// - Throws: `StorageClientError` if the upload fails. func upload( key: String, - sequence: StorageSequence + sequence: StorageSequence, + contentType: String? ) async throws(StorageClientError) /// Downloads an object as an asynchronous byte stream. @@ -136,6 +138,21 @@ public protocol StorageClient: Sendable { extension StorageClient { + /// Uploads an object without additional metadata. + /// + /// This convenience preserves the original storage API for callers that + /// do not need to provide object metadata. + public func upload( + key: String, + sequence: StorageSequence + ) async throws(StorageClientError) { + try await upload( + key: key, + sequence: sequence, + contentType: nil + ) + } + /// Moves an object from one key to another by copying then deleting the source. /// /// - Parameters: diff --git a/Sources/FeatherStorage/StorageClientError.swift b/Sources/FeatherStorage/StorageClientError.swift index 82acc84..3d14299 100644 --- a/Sources/FeatherStorage/StorageClientError.swift +++ b/Sources/FeatherStorage/StorageClientError.swift @@ -16,5 +16,5 @@ public enum StorageClientError: Error { /// A multipart chunk descriptor is invalid. case invalidMultipartChunk /// Driver-specific underlying failure. - case unknown(Error) + case unknown(any Error) } diff --git a/Sources/FeatherStorage/StorageSequence.swift b/Sources/FeatherStorage/StorageSequence.swift index e97041f..e348d04 100644 --- a/Sources/FeatherStorage/StorageSequence.swift +++ b/Sources/FeatherStorage/StorageSequence.swift @@ -4,7 +4,7 @@ // // Created by Tibor Bodecs on 2023. 01. 16. -import NIOCore +public import NIOCore /// A type-erased async sequence of storage byte buffers. public struct StorageSequence: Sendable, AsyncSequence { diff --git a/Tests/FeatherStorageTests/FeatherStorageTestSuite.swift b/Tests/FeatherStorageTests/FeatherStorageTestSuite.swift index e45b9b1..e521abb 100644 --- a/Tests/FeatherStorageTests/FeatherStorageTestSuite.swift +++ b/Tests/FeatherStorageTests/FeatherStorageTestSuite.swift @@ -12,6 +12,23 @@ import Testing @Suite struct FeatherStorageTestSuite { + @Test + func storageClientUploadPreservesContentType() async throws { + let client = MockStorageClient() + let sequence = StorageSequence( + asyncSequence: [1, 2, 3].byteBufferAsync, + length: 3 + ) + + try await client.upload( + key: "image.webp", + sequence: sequence, + contentType: "image/webp" + ) + + #expect(await client.contentType(for: "image.webp") == "image/webp") + } + @Test func storageClientMoveDeletesSourceOnSuccess() async throws { let client = MockStorageClient() diff --git a/Tests/FeatherStorageTests/Mock/MockStorageClient.swift b/Tests/FeatherStorageTests/Mock/MockStorageClient.swift index e0167d4..7e1b437 100644 --- a/Tests/FeatherStorageTests/Mock/MockStorageClient.swift +++ b/Tests/FeatherStorageTests/Mock/MockStorageClient.swift @@ -40,9 +40,18 @@ struct MockStorageClient: StorageClient { func setObject( key: String, - values: [Int] + values: [Int], + contentType: String? = nil ) async { - await state.setObject(key, values: values) + await state.setObject( + key, + values: values, + contentType: contentType + ) + } + + func contentType(for key: String) async -> String? { + await state.objectContentType(for: key) } func multipartExists( @@ -53,7 +62,8 @@ struct MockStorageClient: StorageClient { func upload( key: String, - sequence: StorageSequence + sequence: StorageSequence, + contentType: String? ) async throws(StorageClientError) { let values: [Int] do { @@ -62,7 +72,11 @@ struct MockStorageClient: StorageClient { catch { throw .unknown(error) } - await state.setObject(key, values: values) + await state.setObject( + key, + values: values, + contentType: contentType + ) } func download( @@ -104,7 +118,11 @@ struct MockStorageClient: StorageClient { guard let values = await state.object(for: source) else { throw .invalidKey } - await state.setObject(destination, values: values) + await state.setObject( + destination, + values: values, + contentType: await state.objectContentType(for: source) + ) } func list( diff --git a/Tests/FeatherStorageTests/Mock/MockStorageState.swift b/Tests/FeatherStorageTests/Mock/MockStorageState.swift index cec932c..03887ba 100644 --- a/Tests/FeatherStorageTests/Mock/MockStorageState.swift +++ b/Tests/FeatherStorageTests/Mock/MockStorageState.swift @@ -7,10 +7,21 @@ actor MockStorageState { var objects: [String: [Int]] = [:] + var objectContentTypes: [String: String] = [:] var multiparts: [String: [Int: [Int]]] = [:] - func setObject(_ key: String, values: [Int]) { + func setObject( + _ key: String, + values: [Int], + contentType: String? = nil + ) { objects[key] = values + if let contentType { + objectContentTypes[key] = contentType + } + else { + objectContentTypes.removeValue(forKey: key) + } } func object(for key: String) -> [Int]? { @@ -25,8 +36,13 @@ actor MockStorageState { objects[key]?.count ?? 0 } + func objectContentType(for key: String) -> String? { + objectContentTypes[key] + } + func deleteObject(_ key: String) { objects.removeValue(forKey: key) + objectContentTypes.removeValue(forKey: key) } func listKeys(prefix: String) -> [String] { diff --git a/Tests/FeatherStorageTests/StorageSequenceTestSuite.swift b/Tests/FeatherStorageTests/StorageSequenceTestSuite.swift index 7a05d97..6f95090 100644 --- a/Tests/FeatherStorageTests/StorageSequenceTestSuite.swift +++ b/Tests/FeatherStorageTests/StorageSequenceTestSuite.swift @@ -107,7 +107,7 @@ struct StorageSequenceTestSuite { func initFromThrowingSequencePropagatesErrors() async { let allocator = ByteBufferAllocator() let sequence = StorageSequence( - asyncSequence: AsyncThrowingStream { + asyncSequence: AsyncThrowingStream { continuation in continuation.yield(Self.makeBuffer([1], allocator: allocator)) continuation.finish(throwing: TestError.failed) diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile index 73be175..80800a9 100644 --- a/docker/tests/Dockerfile +++ b/docker/tests/Dockerfile @@ -1,4 +1,4 @@ -FROM swift:6.1 +FROM swift:6.3 WORKDIR /app From 02f0c394be8c7a98d1996b092bdaa10afbbaef98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20B=C3=B6decs?= Date: Tue, 6 Oct 2026 12:38:57 +0200 Subject: [PATCH 2/2] remove check --- .github/workflows/pull-request.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e1b5d96..ae6110e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -22,14 +22,6 @@ jobs: yamllint_check_enabled : false python_lint_check_enabled : false - bb_checks: - name: BB Checks - uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main - with: - local_swift_dependencies_check_enabled : true - headers_check_enabled : true - docc_warnings_check_enabled : true - tests: name: Tests uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.15