Я пытаюсь загрузить изображение в бэкэнд 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()
В тех других примерах, которые вы нашли (например, Загрузить изображение с параметрами в 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
}
}
}