From 169ffa2d9cfdceb8f8ff70b45a8205967b291300 Mon Sep 17 00:00:00 2001 From: Iceman Date: Mon, 15 Dec 2025 10:39:35 +0900 Subject: [PATCH 1/3] Add move operation --- Sources/GoogleCloud/Storage/Bucket.swift | 21 ++++++++++++++++--- .../Storage/Entity/StorageFile.swift | 2 +- Tests/SharedTests/Storage/BucketTest.swift | 20 ++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Sources/GoogleCloud/Storage/Bucket.swift b/Sources/GoogleCloud/Storage/Bucket.swift index aa68ee2..19c2172 100644 --- a/Sources/GoogleCloud/Storage/Bucket.swift +++ b/Sources/GoogleCloud/Storage/Bucket.swift @@ -12,7 +12,7 @@ public struct Bucket: Sendable { self.authorizedClient = authorizedClient } - // INFO: https://cloud.google.com/storage/docs/json_api/v1/objects/list + // INFO: https://docs.cloud.google.com/storage/docs/json_api/v1/objects/list public func files( prefix: String, logger: Logger? = nil @@ -30,7 +30,22 @@ public struct Bucket: Sendable { ).items ?? [] } - // INFO: https://cloud.google.com/storage/docs/json_api/v1/objects/delete + // INFO: https://docs.cloud.google.com/storage/docs/json_api/v1/objects/move + @discardableResult + public func move( + src: String, + dst: String, + logger: Logger? = nil + ) async throws -> StorageFile { + return try await authorizedClient.execute( + method: .POST, + path: "storage/v1/b/\(bucketName)/o/\(src.choppingSlashPrefix)/moveTo/o/\(dst.choppingSlashPrefix)", + logger: logger, + responseType: StorageFile.self + ) + } + + // INFO: https://docs.cloud.google.com/storage/docs/json_api/v1/objects/delete public func delete( name: String, logger: Logger? = nil @@ -38,7 +53,7 @@ public struct Bucket: Sendable { let name = Self.nameOnPath(name: name) _ = try await authorizedClient.execute( method: .DELETE, - path: "storage/v1/b/\(bucketName)/o" + (name.hasPrefix("/") ? name : "/" + name), + path: "storage/v1/b/\(bucketName)/o/\(name.choppingSlashPrefix)", logger: logger ) } diff --git a/Sources/GoogleCloud/Storage/Entity/StorageFile.swift b/Sources/GoogleCloud/Storage/Entity/StorageFile.swift index 749efdb..1a79271 100644 --- a/Sources/GoogleCloud/Storage/Entity/StorageFile.swift +++ b/Sources/GoogleCloud/Storage/Entity/StorageFile.swift @@ -1,6 +1,6 @@ import Foundation -/// INFO: https://cloud.google.com/storage/docs/json_api/v1/objects +/// INFO: https://docs.cloud.google.com/storage/docs/json_api/v1/objects#resource /// varとletは↑のwritableかどうかに対応してるけど、意味はわかっていない public struct StorageFile: Decodable { public init(name: String, bucket: String) { diff --git a/Tests/SharedTests/Storage/BucketTest.swift b/Tests/SharedTests/Storage/BucketTest.swift index 774eed9..0fa7ec0 100644 --- a/Tests/SharedTests/Storage/BucketTest.swift +++ b/Tests/SharedTests/Storage/BucketTest.swift @@ -19,6 +19,26 @@ import Testing #expect(Set(response.map(\.name)) == ["testFiles/bar.png", "testFiles/baz.jpg"]) } + @Test(.disabled("fake-gcs-server doesn't support 'move'.")) + func move() async throws { + let bucket = makeBucket() + + let srcName = "testMove/src/Foo.txt" + let dstName = "testMove/dst/Foo_renamed.txt" + _ = try await bucket.uploadSimple(name: srcName, media: Data(#function.utf8)) + + let filesBefore = try await bucket.files(prefix: srcName) + #expect(!filesBefore.isEmpty) + + let movedFile = try await bucket.move(src: srcName, dst: dstName) + #expect(movedFile.name == dstName) + + let filesAfterSrc = try await bucket.files(prefix: srcName) + #expect(filesAfterSrc.isEmpty) + let filesAfterDst = try await bucket.files(prefix: dstName) + #expect(!filesAfterDst.isEmpty) + } + @Test func upload() async throws { let bucket = makeBucket() From 7eab534e7a166fa4cf981045e12a548f199072f3 Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 16 Dec 2025 12:00:25 +0900 Subject: [PATCH 2/3] Fix default error log level --- Sources/GoogleCloudBase/AuthorizedClient.swift | 5 +++-- Sources/GoogleCloudBase/GCPClient.swift | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/GoogleCloudBase/AuthorizedClient.swift b/Sources/GoogleCloudBase/AuthorizedClient.swift index 099658c..0b5714b 100644 --- a/Sources/GoogleCloudBase/AuthorizedClient.swift +++ b/Sources/GoogleCloudBase/AuthorizedClient.swift @@ -77,9 +77,10 @@ package struct AuthorizedClient: Sendable { if let data = resBody.getData(at: resBody.readerIndex, length: resBody.readableBytes), let string = String(data: data, encoding: .utf8) { // 404のときなど、JSONでないレスポンスが返ることがある - throw ErrorResponse(error: .init(code: Int(res.status.code), message: string)) + errorResponse = ErrorResponse(error: .init(code: Int(res.status.code), message: string)) + } else { + throw error } - throw error } throw errorResponse } diff --git a/Sources/GoogleCloudBase/GCPClient.swift b/Sources/GoogleCloudBase/GCPClient.swift index adb647d..fb9afe8 100644 --- a/Sources/GoogleCloudBase/GCPClient.swift +++ b/Sources/GoogleCloudBase/GCPClient.swift @@ -56,7 +56,7 @@ public struct GCPClient: Sendable { public init( requestLogLevel: Logger.Level = .debug, - errorLogLevel: Logger.Level = .debug + errorLogLevel: Logger.Level = .error ) { self.requestLogLevel = requestLogLevel self.errorLogLevel = errorLogLevel From 05c586319701f2e890c59eac7ce1af759a9b38fa Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 16 Dec 2025 12:02:25 +0900 Subject: [PATCH 3/3] Fix name encoding in url --- Sources/GoogleCloud/Storage/Bucket.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/GoogleCloud/Storage/Bucket.swift b/Sources/GoogleCloud/Storage/Bucket.swift index 19c2172..6283396 100644 --- a/Sources/GoogleCloud/Storage/Bucket.swift +++ b/Sources/GoogleCloud/Storage/Bucket.swift @@ -37,9 +37,11 @@ public struct Bucket: Sendable { dst: String, logger: Logger? = nil ) async throws -> StorageFile { + let src = Self.nameOnPath(name: src) + let dst = Self.nameOnPath(name: dst) return try await authorizedClient.execute( method: .POST, - path: "storage/v1/b/\(bucketName)/o/\(src.choppingSlashPrefix)/moveTo/o/\(dst.choppingSlashPrefix)", + path: "storage/v1/b/\(bucketName)/o/\(src)/moveTo/o/\(dst)", logger: logger, responseType: StorageFile.self ) @@ -53,7 +55,7 @@ public struct Bucket: Sendable { let name = Self.nameOnPath(name: name) _ = try await authorizedClient.execute( method: .DELETE, - path: "storage/v1/b/\(bucketName)/o/\(name.choppingSlashPrefix)", + path: "storage/v1/b/\(bucketName)/o/\(name)", logger: logger ) } @@ -90,7 +92,9 @@ public struct Bucket: Sendable { // INFO: https://cloud.google.com/storage/docs/request-endpoints#encoding static func nameOnPath(name: String) -> String { let targets = CharacterSet(charactersIn: " !#$&'()*+,/:;=?@[]") - return name.addingPercentEncoding(withAllowedCharacters: targets.inverted)! + return name + .choppingSlashPrefix + .addingPercentEncoding(withAllowedCharacters: targets.inverted)! } }