import Vapor struct TryFilesMiddleware: AsyncMiddleware { let publicDirectory: String func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { let relativePath = request.url.path let potentialPaths = [ publicDirectory + relativePath, publicDirectory + relativePath + ".html", publicDirectory + relativePath + "/1.html" ] for path in potentialPaths { var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue else { continue } let url = URL(fileURLWithPath: path) let data = try Data(contentsOf: url) let response = Response(status: .ok, body: .init(data: data)) response.headers.replaceOrAdd(name: .contentType, value: url.mimeType) response.headers.replaceOrAdd(name: .contentLength, value: "\(data.count)") return response } return .init(status: .notFound) } } private extension URL { var mimeType: String { switch pathExtension.lowercased() { case "html": return "text/html" case "css": return "text/css" case "js": return "application/javascript" case "json": return "application/json" case "xml": return "application/xml" case "txt": return "text/plain" case "jpg", "jpeg": return "image/jpeg" case "png": return "image/png" case "gif": return "image/gif" case "svg": return "image/svg+xml" case "webp": return "image/webp" case "ico": return "image/x-icon" case "pdf": return "application/pdf" case "zip": return "application/zip" case "tar": return "application/x-tar" case "gz": return "application/gzip" case "rar": return "application/vnd.rar" case "mp3": return "audio/mpeg" case "wav": return "audio/wav" case "ogg": return "audio/ogg" case "mp4": return "video/mp4" case "mov": return "video/quicktime" case "avi": return "video/x-msvideo" case "webm": return "video/webm" default: return "application/octet-stream" // Default binary data type } } }