Skip to content

Instantly share code, notes, and snippets.

@todbot
Created February 13, 2023 17:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save todbot/fb635a7d84b9caca261d810dde2527a3 to your computer and use it in GitHub Desktop.
Save todbot/fb635a7d84b9caca261d810dde2527a3 to your computer and use it in GitHub Desktop.
Hacky media upload to mastodon in Swift, does not post
// Hacky media upload to mastodon, does not post
// A condensation of things I learned about Swift and Mastodon API w/ @carlynorama
// 12 Feb 2023 - @todbot / Tod Kurt
//
// Compile with:
// swiftc masto-media-up.swift -o masto-media-up
//
// Requires:
// - existing Mastodon account on a given server ("--url")
// - authorization token from a created app in Mastodon ("--auth")
// - a file to upload ("--file")
//
// Invoke with:
// ./masto-media-up --url https://mastodon.social/api/v2/media \
// --auth cUW2ZBs5BWu-IopQ05ImudTnTP4LLrD2KVrLV9S1KE0 \
// --file myimage.jpg
// And it returns with the JSON response from the server
//
// Uses bits stolen from:
// - https://github.com/carlynorama/APIng/tree/main/Sources/APIng
// - https://igomobile.de/2020/06/16/swift-upload-a-file-with-multipart-form-data-in-ios-using-uploadtask-and-urlsession/
//
import Foundation
import UniformTypeIdentifiers
var fileStr = "fractal5x5.jpg"
var urlStr = "https://httpbin.org/post" // "http://localhost:8080/"
var yourAuthToken = "fakeyauth12345678ABCDEFGH12345"
for pos in 0..<CommandLine.arguments.count {
if( CommandLine.arguments[pos] == "--url" ) {
urlStr = CommandLine.arguments[pos+1]
}
if( CommandLine.arguments[pos] == "--file" ) {
fileStr = CommandLine.arguments[pos+1]
}
if( CommandLine.arguments[pos] == "--auth" ) {
yourAuthToken = CommandLine.arguments[pos+1]
}
}
print("Uploading to...\n\turl =",urlStr,"\n\tuploading file =",fileStr)
let url = URL(string:urlStr)!
let fileURL = URL(fileURLWithPath:fileStr)
let boundary = UUID().uuidString // generate boundary string using a unique string
public func mimeType(for path: String) -> String {
let pathExtension = URL(fileURLWithPath: path).pathExtension as String
return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
}
// Set the URLRequest to POST and to the specified URL
var request = URLRequest(url: url )
request.httpMethod = "POST"
request.addValue("Bearer \(yourAuthToken)", forHTTPHeaderField: "Authorization")
// Content-Type is multipart/form-data, the same as submitting form data with file upload in a web browser
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let fileName = fileURL.lastPathComponent
let mimetype = mimeType(for: fileName)
let paramName = "file"
let fileData = try? Data(contentsOf: fileURL)
var data = Data()
// Add the file data to the raw http request data
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: \(mimetype)\r\n\r\n".data(using: .utf8)!)
data.append(fileData!)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
// do not forget to set the content-length!
request.setValue(String(data.count), forHTTPHeaderField: "Content-Length")
let (responseData, response) = try await URLSession.shared.upload(for: request, from: data, delegate: nil)
print("Upload done.\nResponse from server:\n")
print(String(data:responseData, encoding: .utf8) ?? "Nothing")
@carlynorama
Copy link

carlynorama commented Feb 13, 2023

Notes:

  • content length is calculated by shared.upload, no need to set manually.
  • can also use shared.data(for: request) if manually set request.httpBody to data (which will also calc the length for you)
  • Add note on that that closing boundary being only for the last item or to leave it off if you know the socket will be closing?
  • Add note on the consequences of leaving off the file name when the API wants (but maybe doesn't mention) it wants it as an attachment and not really as integrated form data?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment