Skip to content

Commit

Permalink
Set correct content type of file in S3 storage (#151)
Browse files Browse the repository at this point in the history
* Set correct content type of file in S3 storage

* Fix build on Ubuntu
  • Loading branch information
mczachurski authored Oct 18, 2024
1 parent 1d28c06 commit b38b038
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 9 deletions.
120 changes: 118 additions & 2 deletions Sources/VernissageServer/Extensions/String+Url.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,129 @@
//

import Foundation
import Vapor

internal let mimeTypes = [
"html": "text/html",
"htm": "text/html",
"shtml": "text/html",
"css": "text/css",
"xml": "text/xml",
"gif": "image/gif",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "application/javascript",
"atom": "application/atom+xml",
"rss": "application/rss+xml",
"mml": "text/mathml",
"txt": "text/plain",
"jad": "text/vnd.sun.j2me.app-descriptor",
"wml": "text/vnd.wap.wml",
"htc": "text/x-component",
"png": "image/png",
"tif": "image/tiff",
"tiff": "image/tiff",
"wbmp": "image/vnd.wap.wbmp",
"ico": "image/x-icon",
"jng": "image/x-jng",
"bmp": "image/x-ms-bmp",
"svg": "image/svg+xml",
"svgz": "image/svg+xml",
"webp": "image/webp",
"woff": "application/font-woff",
"jar": "application/java-archive",
"war": "application/java-archive",
"ear": "application/java-archive",
"json": "application/json",
"hqx": "application/mac-binhex40",
"doc": "application/msword",
"pdf": "application/pdf",
"ps": "application/postscript",
"eps": "application/postscript",
"ai": "application/postscript",
"rtf": "application/rtf",
"m3u8": "application/vnd.apple.mpegurl",
"xls": "application/vnd.ms-excel",
"eot": "application/vnd.ms-fontobject",
"ppt": "application/vnd.ms-powerpoint",
"wmlc": "application/vnd.wap.wmlc",
"kml": "application/vnd.google-earth.kml+xml",
"kmz": "application/vnd.google-earth.kmz",
"7z": "application/x-7z-compressed",
"cco": "application/x-cocoa",
"jardiff": "application/x-java-archive-diff",
"jnlp": "application/x-java-jnlp-file",
"run": "application/x-makeself",
"pl": "application/x-perl",
"pm": "application/x-perl",
"prc": "application/x-pilot",
"pdb": "application/x-pilot",
"rar": "application/x-rar-compressed",
"rpm": "application/x-redhat-package-manager",
"sea": "application/x-sea",
"swf": "application/x-shockwave-flash",
"sit": "application/x-stuffit",
"tcl": "application/x-tcl",
"tk": "application/x-tcl",
"der": "application/x-x509-ca-cert",
"pem": "application/x-x509-ca-cert",
"crt": "application/x-x509-ca-cert",
"xpi": "application/x-xpinstall",
"xhtml": "application/xhtml+xml",
"xspf": "application/xspf+xml",
"zip": "application/zip",
"epub": "application/epub+zip",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"mid": "audio/midi",
"midi": "audio/midi",
"kar": "audio/midi",
"mp3": "audio/mpeg",
"ogg": "audio/ogg",
"m4a": "audio/x-m4a",
"ra": "audio/x-realaudio",
"3gpp": "video/3gpp",
"3gp": "video/3gpp",
"ts": "video/mp2t",
"mp4": "video/mp4",
"mpeg": "video/mpeg",
"mpg": "video/mpeg",
"mov": "video/quicktime",
"webm": "video/webm",
"flv": "video/x-flv",
"m4v": "video/x-m4v",
"mng": "video/x-mng",
"asx": "video/x-ms-asf",
"asf": "video/x-ms-asf",
"wmv": "video/x-ms-wmv",
"avi": "video/x-msvideo"
]

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
}

let pathExtensionLowercased = pathExtension.lowercased()
return mimeTypes[pathExtensionLowercased]
}
}
2 changes: 1 addition & 1 deletion Sources/VernissageServer/Services/StatusesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
12 changes: 9 additions & 3 deletions Sources/VernissageServer/Services/StorageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)

Expand All @@ -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
)

Expand All @@ -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
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions Sources/VernissageServer/Services/UsersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
117 changes: 117 additions & 0 deletions Tests/VernissageServerTests/ExtensionsTests/String+Url.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit b38b038

Please sign in to comment.