From e5a42cdee0af663a54507826fcdc4c30a405371e Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Fri, 18 Oct 2024 18:28:56 +0200 Subject: [PATCH] Set correct content type of file in S3 storage --- .../Extensions/String+Url.swift | 23 +++- .../Services/StatusesService.swift | 2 +- .../Services/StorageService.swift | 12 +- .../Services/TemporaryFileService.swift | 2 +- .../Services/UsersService.swift | 4 +- .../ExtensionsTests/String+Url.swift | 117 ++++++++++++++++++ 6 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 Tests/VernissageServerTests/ExtensionsTests/String+Url.swift diff --git a/Sources/VernissageServer/Extensions/String+Url.swift b/Sources/VernissageServer/Extensions/String+Url.swift index 043832ce..af017183 100644 --- a/Sources/VernissageServer/Extensions/String+Url.swift +++ b/Sources/VernissageServer/Extensions/String+Url.swift @@ -5,13 +5,32 @@ // import Foundation +import Vapor +import UniformTypeIdentifiers extension String { - public func host() -> String { + var host: String { return URLComponents(string: self)?.host ?? "" } - public func fileName() -> String { + var fileName: String { return String(self.split(separator: "/").last ?? "") } + + var pathExtension: String? { + let uri = URI(string: self) + guard let fileExtension = uri.path.split(separator: ".").last else { + return nil + } + + return String(fileExtension) + } + + var mimeType: String? { + guard let pathExtension else { + return nil + } + + return UTType(filenameExtension: pathExtension)?.preferredMIMEType + } } diff --git a/Sources/VernissageServer/Services/StatusesService.swift b/Sources/VernissageServer/Services/StatusesService.swift index b8a95a96..e0aa7dc6 100644 --- a/Sources/VernissageServer/Services/StatusesService.swift +++ b/Sources/VernissageServer/Services/StatusesService.swift @@ -1427,7 +1427,7 @@ final class StatusesService: StatusesServiceType { } // Get fileName from URL. - let fileName = attachment.url.fileName() + let fileName = attachment.url.fileName // Save resized image in temp folder. context.logger.info("Saving resized image '\(fileName)' in temporary folder.") diff --git a/Sources/VernissageServer/Services/StorageService.swift b/Sources/VernissageServer/Services/StorageService.swift index c7a0cb57..ccf84114 100644 --- a/Sources/VernissageServer/Services/StorageService.swift +++ b/Sources/VernissageServer/Services/StorageService.swift @@ -71,8 +71,7 @@ extension StorageServiceType { } func generateFileName(url: String) -> String { - let uri = URI(string: url) - let fileExtension = uri.path.split(separator: ".").last ?? "jpg" + let fileExtension = url.pathExtension ?? "jpg" let fileName = UUID().uuidString.lowercased().replacingOccurrences(of: "-", with: "") + "." + fileExtension return fileName @@ -264,12 +263,14 @@ fileprivate final class S3StorageService: StorageServiceType { } let fileName = self.generateFileName(url: fileUri) + let contentType = fileUri.mimeType let putObjectRequest = S3.PutObjectRequest( acl: .publicRead, body: .init(buffer: byteBuffer), bucket: bucket, cacheControl: MaxAge.year.rawValue, + contentType: contentType, key: fileName ) @@ -289,13 +290,15 @@ fileprivate final class S3StorageService: StorageServiceType { } let byteBuffer = try await request.fileio.collectFile(at: url.absoluteString) - let fileName = self.generateFileName(url: fileUri) + let contentType = fileUri.mimeType + let putObjectRequest = S3.PutObjectRequest( acl: .publicRead, body: .init(buffer: byteBuffer), bucket: bucket, cacheControl: MaxAge.year.rawValue, + contentType: contentType, key: fileName ) @@ -319,11 +322,14 @@ fileprivate final class S3StorageService: StorageServiceType { eventLoop: context.eventLoop) let fileName = self.generateFileName(url: fileUri) + let contentType = fileUri.mimeType + let putObjectRequest = S3.PutObjectRequest( acl: .publicRead, body: .init(buffer: byteBuffer), bucket: bucket, cacheControl: MaxAge.year.rawValue, + contentType: contentType, key: fileName ) diff --git a/Sources/VernissageServer/Services/TemporaryFileService.swift b/Sources/VernissageServer/Services/TemporaryFileService.swift index 7378f459..59635f6f 100644 --- a/Sources/VernissageServer/Services/TemporaryFileService.swift +++ b/Sources/VernissageServer/Services/TemporaryFileService.swift @@ -40,7 +40,7 @@ final class TemporaryFileService: TemporaryFileServiceType { } func save(url: String, on context: QueueContext) async throws -> URL { - let fileName = url.fileName() + let fileName = url.fileName let temporaryPath = try self.temporaryPath(on: context.application, based: fileName) // Download file. diff --git a/Sources/VernissageServer/Services/UsersService.swift b/Sources/VernissageServer/Services/UsersService.swift index 13ad1ad2..5494fefd 100644 --- a/Sources/VernissageServer/Services/UsersService.swift +++ b/Sources/VernissageServer/Services/UsersService.swift @@ -450,7 +450,7 @@ final class UsersService: UsersServiceType { } func update(user: User, on application: Application, basedOn person: PersonDto, withAvatarFileName avatarFileName: String?, withHeaderFileName headerFileName: String?) async throws -> User { - let remoteUserName = "\(person.preferredUsername)@\(person.url.host())" + let remoteUserName = "\(person.preferredUsername)@\(person.url.host)" user.url = person.url user.userName = remoteUserName @@ -477,7 +477,7 @@ final class UsersService: UsersServiceType { } func create(on application: Application, basedOn person: PersonDto, withAvatarFileName avatarFileName: String?, withHeaderFileName headerFileName: String?) async throws -> User { - let remoteUserName = "\(person.preferredUsername)@\(person.url.host())" + let remoteUserName = "\(person.preferredUsername)@\(person.url.host)" let newUserId = application.services.snowflakeService.generate() let user = User(id: newUserId, diff --git a/Tests/VernissageServerTests/ExtensionsTests/String+Url.swift b/Tests/VernissageServerTests/ExtensionsTests/String+Url.swift new file mode 100644 index 00000000..443fa602 --- /dev/null +++ b/Tests/VernissageServerTests/ExtensionsTests/String+Url.swift @@ -0,0 +1,117 @@ +// +// https://mczachurski.dev +// Copyright © 2024 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +@testable import VernissageServer +import Testing + +@Suite("String URL tests") +struct StringUrlTests { + + @Test("Simple file extension should be recognized") + func simpleFileExtensionShouldBeRecognized() async throws { + + // Arrange. + let fileName = "file.JPG" + + // Act. + let pathExtension = fileName.pathExtension + + // Assert. + #expect(pathExtension == "JPG", "JPG extension should be returned") + } + + @Test("Complex file extension should be recognized") + func complesFileExtensionShouldBeRecognized() async throws { + + // Arrange. + let fileName = "file://path/jakies/file-123.png" + + // Act. + let pathExtension = fileName.pathExtension + + // Assert. + #expect(pathExtension == "png", "png extension should be returned") + } + + @Test("JPG file extension should be recognized as image/jpeg mime type") + func JPGFileExtensionShouldBeRecognizedAsImageJpegMimeType() async throws { + + // Arrange. + let fileName = "file.JPG" + + // Act. + let mimeType = fileName.mimeType + + // Assert. + #expect(mimeType == "image/jpeg", "JPG extension should be returned") + } + + @Test("jpg file extension should be recognized as image/jpeg mime type") + func jpgFileExtensionShouldBeRecognizedAsImageJpegMimeType() async throws { + + // Arrange. + let fileName = "file.jpg" + + // Act. + let mimeType = fileName.mimeType + + // Assert. + #expect(mimeType == "image/jpeg", "jpg extension should be returned") + } + + @Test("JPEG file extension should be recognized as image/jpeg mime type") + func JPEGFileExtensionShouldBeRecognizedAsImageJpegMimeType() async throws { + + // Arrange. + let fileName = "file.JPEG" + + // Act. + let mimeType = fileName.mimeType + + // Assert. + #expect(mimeType == "image/jpeg", "JPEG extension should be returned") + } + + @Test("jpeg file extension should be recognized as image/jpeg mime type") + func jpegFileExtensionShouldBeRecognizedAsImageJpegMimeType() async throws { + + // Arrange. + let fileName = "file.jpeg" + + // Act. + let mimeType = fileName.mimeType + + // Assert. + #expect(mimeType == "image/jpeg", "jpeg extension should be returned") + } + + @Test("PNG file extension should be recognized as image/png mime type") + func PNGFileExtensionShouldBeRecognizedAsImageJpegMimeType() async throws { + + // Arrange. + let fileName = "file.PNG" + + // Act. + let mimeType = fileName.mimeType + + // Assert. + #expect(mimeType == "image/png", "PNG extension should be returned") + } + + @Test("png file extension should be recognized as image/png mime type") + func pngFileExtensionShouldBeRecognizedAsImageJpegMimeType() async throws { + + // Arrange. + let fileName = "file.png" + + // Act. + let mimeType = fileName.mimeType + + // Assert. + #expect(mimeType == "image/png", "jpeg extension should be returned") + } +} +