You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI iOS应用向Vapor 4多格式图片上传代码实现求助

SwiftUI客户端 + Vapor 4后端多格式图片上传实现

一、iOS客户端(SwiftUI)代码

图片选择与上传逻辑

使用PhotoPicker筛选支持的图片类型,将图片数据连同文件名一起发送到后端:

import SwiftUI
import PhotosUI

struct ImageUploadView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var uploadStatus: String = ""
    
    // 允许选择的图片类型UTI
    private let allowedUTIs = [
        UTType.jpeg,
        UTType.png,
        UTType.heic
    ]
    
    var body: some View {
        VStack(spacing: 20) {
            PhotosPicker(selection: $selectedItem, matching: .init(allowedUTIs)) {
                Text("选择图片")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            
            Text(uploadStatus)
            
            Button("上传图片") {
                guard selectedItem != nil else {
                    uploadStatus = "请先选择图片"
                    return
                }
                uploadImage(selectedItem!)
            }
            .disabled(selectedItem == nil)
        }
        .padding()
    }
    
    private func uploadImage(_ item: PhotosPickerItem) {
        uploadStatus = "上传中..."
        Task {
            do {
                let imageData = try await item.loadTransferable(type: Data.self)
                guard let data = imageData, let fileName = item.displayFilename else {
                    throw NSError(domain: "UploadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取图片数据"])
                }
                
                guard let url = URL(string: "http://你的后端地址/upload") else { return }
                var request = URLRequest(url: url)
                request.httpMethod = "POST"
                
                // 构建multipart/form-data请求体
                let boundary = UUID().uuidString
                request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
                
                var body = Data()
                body.append("--\(boundary)\r\n".data(using: .utf8)!)
                body.append("Content-Disposition: form-data; name=\"image\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
                body.append("Content-Type: \(mimeType(for: fileName))\r\n\r\n".data(using: .utf8)!)
                body.append(data)
                body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
                
                request.httpBody = body
                
                let (_, response) = try await URLSession.shared.data(for: request)
                guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else {
                    uploadStatus = "上传失败"
                    return
                }
                
                uploadStatus = "上传成功"
            } catch {
                uploadStatus = "上传出错:\(error.localizedDescription)"
            }
        }
    }
    
    // 根据文件名返回对应MIME类型
    private func mimeType(for fileName: String) -> String {
        let ext = fileName.components(separatedBy: ".").last?.lowercased()
        switch ext {
        case "jpg", "jpeg": return "image/jpeg"
        case "png": return "image/png"
        case "heic": return "image/heic"
        default: return "application/octet-stream"
        }
    }
}

二、Vapor 4服务端代码

上传路由与格式校验逻辑

解析客户端的multipart请求,通过扩展名+文件头双重校验图片格式,合法则保存到指定目录:

import Vapor

func routes(_ app: Application) throws {
    // 图片上传路由
    app.post("upload") { req async throws -> HTTPStatus in
        // 解析表单中的文件
        let form = try await req.content.decode(UploadForm.self)
        
        // 第一步:校验扩展名
        guard let fileExt = form.image.filename.split(separator: ".").last?.lowercased(),
              ["jpg", "jpeg", "png", "heic"].contains(fileExt) else {
            throw Abort(.badRequest, reason: "仅支持jpg/jpeg/png/heic格式")
        }
        
        // 第二步:校验文件头(防止扩展名伪造)
        let fileSignature = try form.image.data.readBytes(length: 4)
        guard isValidImageSignature(signature: fileSignature, ext: fileExt) else {
            throw Abort(.badRequest, reason: "图片格式不符合要求")
        }
        
        // 创建保存目录(项目Public下的images文件夹)
        let saveDir = app.directory.publicDirectory.appending(path: "images")
        try FileManager.default.createDirectory(at: saveDir, withIntermediateDirectories: true)
        
        // 生成唯一文件名避免冲突
        let uniqueName = UUID().uuidString + "." + fileExt
        let savePath = saveDir.appending(path: uniqueName)
        
        // 写入文件
        try form.image.data.write(to: savePath)
        
        return .ok
    }
}

// 定义表单结构
struct UploadForm: Content {
    var image: File
}

// 文件头校验工具函数
func isValidImageSignature(signature: [UInt8]?, ext: String) -> Bool {
    guard let sig = signature else { return false }
    switch ext {
    case "jpg", "jpeg":
        return sig[0] == 0xFF && sig[1] == 0xD8 && sig[2] == 0xFF
    case "png":
        return sig[0] == 0x89 && sig[1] == 0x50 && sig[2] == 0x4E && sig[3] == 0x47
    case "heic":
        return sig[0] == 0x66 && sig[1] == 0x74 && sig[2] == 0x79 && sig[3] == 0x70
    default:
        return false
    }
}

服务端基础配置

在configure.swift中启用静态文件服务,方便后续访问上传的图片:

import Vapor

func configure(_ app: Application) throws {
    // 启用静态文件服务
    app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    
    try routes(app)
}

三、相关文档与库说明

客户端侧

  • PhotosPicker:苹果原生SwiftUI组件,支持通过UTI精准筛选系统相册中的文件类型。
  • URLSession:原生网络API,无需第三方库即可构建multipart/form-data请求完成文件上传。

服务端侧

  • Vapor File类型:Vapor内置的文件解析类型,自动处理multipart表单中的文件数据。
  • Vapor FileMiddleware:提供静态文件托管能力,可直接通过域名访问/images/xxx路径的上传图片。
  • 文件头校验:基于图片格式的公开标准(如PNG的文件头标识为89 50 4E 47),避免仅通过扩展名校验的安全漏洞。

内容的提问来源于stack exchange,提问作者Edvard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 04:58:18