Установить границу в URLSession.uploadTask :with :fromFile

Я пытаюсь загрузить изображение в бэкэнд Spring. Он должен работать в фоновом режиме, поэтому я могу использовать только функцию session.uploadTask Моя проблема в том, что серверная часть ожидает от меня установки заголовка Content-Type. Одна из важнейших частей — определить boundary и использовать его соответствующим образом в теле запроса, но как мне установить boundary на изображение?

Большинство руководств, которые я видел, делают это с помощью функции session.uploadData, которая недоступна, когда вы хотите выполнить операцию в фоновом режиме. Там я мог бы просто добавить boundary к данным.

Подводя итог: как правильно использовать поле заголовка boundary при загрузке изображений с помощью uploadTask(with request: URLRequest, fromFile fileURL: URL)?

Я получаю эту ошибку с весны:

org.springframework.web.multipart.MultipartException: Current request is not a multipart request

Мой код:

let boundary = UUID().uuidString
// A background upload task must specify a file
var imageURLRequest = URLRequest(url: uploadURL)
imageURLRequest.httpMethod = "Post"
imageURLRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
                
let imageTask = URLBackgroundSession.shared.uploadTask(with: imageURLRequest, fromFile: URL(fileURLWithPath: imagePath))
imageTask.resume()
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
95
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В тех других примерах, которые вы нашли (например, Загрузить изображение с параметрами в Swift), мы создаем Data, который соответствует правильно сформированному multipart/form-data запросу, и используем его в теле запроса.

Здесь вам придется сделать то же самое, за исключением того, что вместо создания Data вы создадите временный файл, запишете все это в этот файл, а затем используете этот файл в своем uploadTask.


Например:

func uploadImage(from imageURL: URL, filePathKey: String, to uploadURL: URL) throws {
    let boundary = UUID().uuidString
    var imageURLRequest = URLRequest(url: uploadURL)
    imageURLRequest.httpMethod = "POST"
    imageURLRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    let folder = URL(filePath: NSTemporaryDirectory()).appending(path: "uploads")
    try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
    let fileURL = folder.appendingPathExtension(boundary)

    guard let outputStream = OutputStream(url: fileURL, append: false) else {
        throw OutputStream.OutputStreamError.unableToCreateFile(fileURL)
    }

    outputStream.open()
    try outputStream.write("--\(boundary)\r\n")
    try outputStream.write("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(imageURL.lastPathComponent)\"\r\n")
    try outputStream.write("Content-Type: \(imageURL.mimeType)\r\n\r\n")
    try outputStream.write(contentsOf: imageURL)
    try outputStream.write("\r\n")
    try outputStream.write("--\(boundary)--\r\n")
    outputStream.close()

    let imageTask = URLBackgroundSession.shared.uploadTask(with: imageURLRequest, fromFile: fileURL)
    imageTask.resume()
}

Вероятно, вам следует удалить временный файл в вашем urlSession(_:task:didCompleteWithError:).

FWIW, в приведенном выше примере используются следующие расширения для упрощения генерации OutputStream:

extension OutputStream {
    enum OutputStreamError: Error {
        case stringConversionFailure
        case unableToCreateFile(URL)
        case bufferFailure
        case writeFailure
        case readFailure(URL)
    }

    /// Write `String` to `OutputStream`
    ///
    /// - parameter string:                The `String` to write.
    /// - parameter encoding:              The `String.Encoding` to use when writing the string. This will default to `.utf8`.
    /// - parameter allowLossyConversion:  Whether to permit lossy conversion when writing the string. Defaults to `false`.

    func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) throws {
        guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else {
            throw OutputStreamError.stringConversionFailure
        }
        try write(data)
    }

    /// Write `Data` to `OutputStream`
    ///
    /// - parameter data:                  The `Data` to write.

    func write(_ data: Data) throws {
        try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
            guard let pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                throw OutputStreamError.bufferFailure
            }

            try write(buffer: pointer, length: buffer.count)
        }
    }

    /// Write contents of local `URL` to `OutputStream`
    ///
    /// - parameter fileURL:                  The `URL` of the file to written to this output stream.

    func write(contentsOf fileURL: URL) throws {
        guard let inputStream = InputStream(url: fileURL) else {
            throw OutputStreamError.readFailure(fileURL)
        }

        inputStream.open()
        defer { inputStream.close() }

        let bufferSize = 65_536
        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
        defer { buffer.deallocate() }

        while inputStream.hasBytesAvailable {
            let length = inputStream.read(buffer, maxLength: bufferSize)
            if length < 0 {
                throw OutputStreamError.readFailure(fileURL)
            } else if length > 0 {
                try write(buffer: buffer, length: length)
            }
        }
    }
}

private extension OutputStream {
    /// Writer buffer to output stream.
    ///
    /// This will loop until all bytes are written. On failure, this throws an error
    ///
    /// - Parameters:
    ///   - buffer: Unsafe pointer to the buffer.
    ///   - length: Number of bytes to be written.

    func write(buffer: UnsafePointer<UInt8>, length: Int) throws {
        var bytesRemaining = length
        var pointer = buffer

        while bytesRemaining > 0 {
            let bytesWritten = write(pointer, maxLength: bytesRemaining)
            if bytesWritten < 0 {
                throw OutputStreamError.writeFailure
            }

            bytesRemaining -= bytesWritten
            pointer += bytesWritten
        }
    }
}

Кроме того, одно из преимуществ загрузки и выгрузки с использованием файлов, а не Data, заключается в том, что объем памяти меньше, что позволяет избежать загрузки всего актива в память в любой момент времени. Итак, в духе этого, я использую небольшой буфер для записи содержимого изображения во временный файл. Это, вероятно, не критично при загрузке изображений, но может стать важным при загрузке более крупных ресурсов, таких как видео.

Несмотря на это, приведенное выше также определяет mimetype актива с помощью этого расширения:

extension URL {
    /// Mime type for the URL
    ///
    /// Requires `import UniformTypeIdentifiers` for iOS 14 solution.
    /// Requires `import MobileCoreServices` for pre-iOS 14 solution

    var mimeType: String {
        if #available(iOS 14.0, *) {
            return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
        } else {
            guard
                let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
                let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() as String?
            else {
                return "application/octet-stream"
            }

            return mimeType
        }
    }
}

Другие вопросы по теме