Skip to content

Instantly share code, notes, and snippets.

@janwirth
Created April 2, 2025 02:19
Show Gist options
  • Save janwirth/6b28874dae9fc98d980a81fb61c97703 to your computer and use it in GitHub Desktop.
Save janwirth/6b28874dae9fc98d980a81fb61c97703 to your computer and use it in GitHub Desktop.
Upload stuff to B2 with swift
//
// S3_Backblaze.swift
// Tuna Family
//
// Created by Jan Wirth on 14/12/24.
//
//❯ cat config
//[default]
//region = EU-CENTRAL
//endpoint_url=https://s3.eu-central-003.backblazeb2.com
//
//
//~/.aws
//❯ cat credentials
import Foundation
import CryptoKit
import CommonCrypto
class BackblazeS3Uploader {
private let accessKeyId = "TODO"
private let secretAccessKey = "TODO"
private let region = "EU-CENTRAL"
private let endpoint = "https://s3.eu-central-003.backblazeb2.com"
private let bucketName = "tuna-family" // Replace with your bucket name
func downloadAudio(fileName: String) async throws -> Data {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
let dateString = dateFormatter.string(from: Date())
let url = URL(string: "\(endpoint)/\(bucketName)/\(fileName)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
// Add required headers in alphabetical order
let host = URL(string: endpoint)!.host!
request.setValue(host, forHTTPHeaderField: "Host")
request.setValue("UNSIGNED-PAYLOAD", forHTTPHeaderField: "x-amz-content-sha256")
request.setValue(dateString, forHTTPHeaderField: "x-amz-date")
// Generate signature with headers in correct order
let signature = generateAWSSignature(
httpMethod: "GET",
path: "/\(bucketName)/\(fileName)",
headers: [
"host": host,
"x-amz-content-sha256": "UNSIGNED-PAYLOAD",
"x-amz-date": dateString
],
payload: Data()
)
// Create authorization header with sorted signed headers
let authHeader = "AWS4-HMAC-SHA256 " +
"Credential=\(accessKeyId)/\(dateString.prefix(8))/\(region)/s3/aws4_request, " +
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, " +
"Signature=\(signature)"
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
// Debug prints
print("Download Request URL: \(url)")
print("Download Headers:")
request.allHTTPHeaderFields?.forEach { print(" \($0): \($1)") }
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
print("Download Status code: \(httpResponse.statusCode)")
guard (200...299).contains(httpResponse.statusCode) else {
if let responseString = String(data: data, encoding: .utf8) {
print("Error Response: \(responseString)")
}
throw URLError(.badServerResponse)
}
}
return data
}
func uploadAudio(fileURL: URL, fileName: String) async throws {
// let date = ISO8601DateFormatter().string(from: Date())
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
let date = dateFormatter.string(from: Date())
let contentType = "audio/mpeg" // Adjust based on your audio format
let url = URL(string: "\(endpoint)/\(bucketName)/\(fileName)")!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
// Verify file data
let fileData = try Data(contentsOf: fileURL)
let fileSize = fileData.count
print("File size: \(fileSize) bytes") // Debug print
guard fileSize > 0 else {
throw URLError(.fileDoesNotExist)
}
// Step 1: Create canonical request
let payloadHash = SHA256.hash(data: fileData)
.compactMap { String(format: "%02x", $0) }
.joined()
// Add all required headers
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
request.setValue(date, forHTTPHeaderField: "x-amz-date")
request.setValue(String(fileSize), forHTTPHeaderField: "Content-Length")
request.setValue(URL(string: endpoint)!.host!, forHTTPHeaderField: "Host")
request.setValue(payloadHash, forHTTPHeaderField: "x-amz-content-sha256")
// Generate signature with updated headers
let signature = generateAWSSignature(
httpMethod: "PUT",
path: "/\(bucketName)/\(fileName)",
headers: [
"host": URL(string: endpoint)!.host!,
"x-amz-date": date,
"content-type": contentType,
"content-length": String(fileSize),
"x-amz-content-sha256": payloadHash
],
payload: fileData
)
let authHeader = "AWS4-HMAC-SHA256 " +
"Credential=\(accessKeyId)/\(date.prefix(8))/\(region)/s3/aws4_request, " +
"SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date, " +
"Signature=\(signature)"
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
// Debug print
print("Request URL: \(url)")
print("Headers:")
request.allHTTPHeaderFields?.forEach { print(" \($0): \($1)") }
// Add this before the upload
print("File exists: \(FileManager.default.fileExists(atPath: fileURL.path))")
print("File is readable: \(FileManager.default.isReadableFile(atPath: fileURL.path))")
// Use URLSession.upload(for:from:) with the file URL directly instead of data
let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
if let httpResponse = response as? HTTPURLResponse {
print("Status code: \(httpResponse.statusCode)")
if let responseString = String(data: data, encoding: .utf8) {
print("Response body: \(responseString)")
}
guard (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
}
print("ock", response)
}
// Update generateAWSSignature to ensure canonical request is exactly correct
private func generateAWSSignature(httpMethod: String, path: String, headers: [String: String], payload: Data) -> String {
// Step 1: Create canonical request
let payloadHash = payload.isEmpty ? "UNSIGNED-PAYLOAD" : SHA256.hash(data: payload)
.compactMap { String(format: "%02x", $0) }
.joined()
let canonicalHeaders = headers
.sorted(by: { $0.key < $1.key })
.map { "\($0.key.lowercased()):\($0.value)\n" }
.joined()
let signedHeaders = headers.keys
.map { $0.lowercased() }
.sorted()
.joined(separator: ";")
let canonicalRequest = [
httpMethod,
path,
"", // Query string (empty in this case)
canonicalHeaders,
signedHeaders,
payloadHash
].joined(separator: "\n")
// Step 2: Create string to sign
let algorithm = "AWS4-HMAC-SHA256"
let requestDate = headers["x-amz-date"]!
let credentialScope = "\(requestDate.prefix(8))/\(region)/s3/aws4_request"
let canonicalRequestHash = SHA256.hash(data: canonicalRequest.data(using: .utf8)!)
.compactMap { String(format: "%02x", $0) }
.joined()
let stringToSign = [
algorithm,
requestDate,
credentialScope,
canonicalRequestHash
].joined(separator: "\n")
// Step 3: Calculate the signature
let dateKey = hmac(key: "AWS4" + secretAccessKey, data: String(requestDate.prefix(8)))
let dateRegionKey = hmac(key: dateKey, data: region)
let dateRegionServiceKey = hmac(key: dateRegionKey, data: "s3")
let signingKey = hmac(key: dateRegionServiceKey, data: "aws4_request")
let signature = hmac(key: signingKey, data: stringToSign)
.compactMap { String(format: "%02x", $0) }
.joined()
return signature
}
private func hmac(key: [UInt8], data: String) -> [UInt8] {
let symmetricKey = SymmetricKey(data: key)
let dataToHash = data.data(using: .utf8)!
let signature = HMAC<SHA256>.authenticationCode(for: dataToHash, using: symmetricKey)
return Array(signature)
}
private func hmac(key: String, data: String) -> [UInt8] {
return hmac(key: Array(key.utf8), data: data)
}
}
import SwiftUI
struct BackblazeTest: View {
let bb = BackblazeS3Uploader()
func runTest() async {
// test_audi
let assets_folder = try! root_folder.createSubfolder(named: "assets")
let test_audio = try! assets_folder.subfolder(at: "dropped").createFileIfNeeded(at: "test.mp3")
print("running test")
try? await bb.uploadAudio(fileURL: test_audio.url, fileName: test_audio.name)
}
var body: some View {
// Button("permssions") {
// Task {
// FileAccessManager.request_home_access(cb: {
// print("yay")
// })
// }
// }
Button("Upload") {
Task {
await runTest()
}
print("APPEARED")
}
Button("Download") {
Task {
do {
let data = try await bb.downloadAudio(fileName: "test.mp3")
// let payloadHash = SHA256.hash(data: data)
// .compactMap { String(format: "%02x", $0) }
// .joined()
let target_location = assets_folder.url.appendingPathComponent("test.mp3")
try data.write(to: target_location)
// try TunaMusicPlayer.shared.playFile(file: target_location)
} catch {
print(error)
}
}
print("APPEARED")
}
}
}
#Preview {
BackblazeTest()
}
import Files
struct S3File {
let sha256: String
let file_extension: String
func check_integrity() -> Bool {
fatalError("not implemented")
}
}
extension File {
func on_s3 (s3_files: [S3File]) -> Bool {
let hash = self.sha256
let found: S3File? = s3_files.first {s3_file in hash == s3_file.sha256 }
return found != nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment