Установить границу в 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()
Шаблоны Angular PrimeNg
Шаблоны Angular PrimeNg
Как привнести проверку типов в наши шаблоны Angular, использующие компоненты библиотеки PrimeNg, и настроить их отображение с помощью встроенной...
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Если вы веб-разработчик (или хотите им стать), то вы наверняка гик и вам нравятся "Звездные войны". А как бы вы хотели, чтобы фоном для вашего...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Начала с розового дизайна
Начала с розового дизайна
Pink Design - это система дизайна Appwrite с открытым исходным кодом для создания последовательных и многократно используемых пользовательских...
Шлюз в PHP
Шлюз в PHP
API-шлюз (AG) - это сервер, который действует как единая точка входа для набора микросервисов.
14 Задание: Типы данных и структуры данных Python для DevOps
14 Задание: Типы данных и структуры данных Python для DevOps
проверить тип данных используемой переменной, мы можем просто написать: your_variable=100
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
        }
    }
}

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