using Godot; using System; using System.IO; using System.IO.Compression; using System.Threading.Tasks; using System.Security.Cryptography; // Fully qualify ambiguous types using HttpClient = System.Net.Http.HttpClient; using FileMode = System.IO.FileMode; using FileAccess = System.IO.FileAccess; using RandomNumberGenerator = System.Security.Cryptography.RandomNumberGenerator; /// /// Handles compressing and uploading data to a server. /// public class DataUploader { /// /// Compresses the specified directory, encrypts it if a password is provided, and uploads it to the server. /// /// Directory containing the data to compress /// User identifier for the server /// Optional password for AES encryption of the zip file /// The S3 address of the uploaded file if successful, null otherwise public static async Task CompressAndUploadDataAsync(string baseDir = "recorded_data", string userId = "default", string password = null, UploadProgress progress = null) { string zipPath = "recorded_data.zip"; string s3Address = null; try { GD.Print($"Compressing data from {baseDir}..."); if (!string.IsNullOrEmpty(password)) { // Create an encrypted zip file GD.Print("Creating encrypted zip file with AES encryption..."); await CreateEncryptedZipAsync(baseDir, zipPath, password); } else { // Create a standard zip file without encryption GD.Print("Creating standard zip file..."); CreateStandardZip(baseDir, zipPath); } // Upload the zip file to the server GD.Print("Uploading zip file to server..."); s3Address = await UploadZipFileAsync(zipPath, userId, progress); if (s3Address != null) { GD.Print($"Successfully uploaded recorded data to {s3Address}"); } else { GD.PrintErr("Failed to upload data"); } } catch (Exception ex) { GD.PrintErr($"Error compressing/uploading data: {ex.Message}"); GD.PrintErr(ex.StackTrace); } finally { // Clean up the zip file if (File.Exists(zipPath)) { try { File.Delete(zipPath); GD.Print($"Deleted temporary file: {zipPath}"); } catch (Exception ex) { GD.PrintErr($"Error deleting temporary file: {ex.Message}"); } } } return s3Address; } /// /// Get a directory's entire size for validation /// static long GetDirectorySize(string folderPath) { long totalSize = 0; try { // Add root file sizes string[] files = Directory.GetFiles(folderPath); foreach (string file in files) { FileInfo fileInfo = new FileInfo(file); totalSize += fileInfo.Length; } // Recursively add subdirectory sizes string[] subDirs = Directory.GetDirectories(folderPath); foreach (string dir in subDirs) { totalSize += GetDirectorySize(dir); } } catch (Exception ex) { GD.PrintErr("Could not get folder size: " + ex.Message); } return totalSize; } /// /// Creates a standard (non-encrypted) zip file from the specified directory. /// private static void CreateStandardZip(string sourceDir, string zipPath) { if (!Directory.Exists(sourceDir)) { throw new DirectoryNotFoundException($"Source directory not found: {sourceDir}"); } // Delete the zip file if it already exists if (File.Exists(zipPath)) { File.Delete(zipPath); } // Create the zip file ZipFile.CreateFromDirectory(sourceDir, zipPath, CompressionLevel.Optimal, false); } /// /// Creates an encrypted zip file from the specified directory. /// /// /// Note: Standard System.IO.Compression doesn't support encryption. /// This method uses a workaround by creating a standard zip file first, /// then encrypting the entire file using AES. /// private static async Task CreateEncryptedZipAsync(string sourceDir, string zipPath, string password) { string tempZipPath = zipPath + ".temp"; try { // Create a standard zip file first CreateStandardZip(sourceDir, tempZipPath); // Read the zip file byte[] zipBytes = File.ReadAllBytes(tempZipPath); // Encrypt the zip file byte[] encryptedBytes = EncryptBytes(zipBytes, password); // Write the encrypted bytes to the final zip file await File.WriteAllBytesAsync(zipPath, encryptedBytes); } finally { // Clean up the temporary file if (File.Exists(tempZipPath)) { File.Delete(tempZipPath); } } } /// /// Encrypts a byte array using AES encryption with the provided password. /// private static byte[] EncryptBytes(byte[] data, string password) { using (var aes = Aes.Create()) { // Generate a random salt byte[] salt = new byte[16]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(salt); } // Generate key and IV from password and salt var key = new Rfc2898DeriveBytes(password, salt, 10000); aes.Key = key.GetBytes(32); // 256 bits aes.IV = key.GetBytes(16); // 128 bits using (var ms = new MemoryStream()) { // Write the salt at the beginning of the output file ms.Write(salt, 0, salt.Length); using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write)) { cs.Write(data, 0, data.Length); cs.FlushFinalBlock(); } return ms.ToArray(); } } } /// /// Uploads the zip file to the server in chunks. /// /// The S3 address of the uploaded file if successful, null otherwise private static async Task UploadZipFileAsync(string zipPath, string username, UploadProgress progress = null) { string apiKey = "bl9SHGy16p7lkmD4hAEjk5QGx1pPs60a5Xpf3dev"; string endpoint = "https://r22f6qjskk.execute-api.us-west-2.amazonaws.com/test/upload-data"; try { // Sanitize username by removing any quotes username = username?.Replace("\"", "").Replace("'", "").Trim(); using (var httpClient = new HttpClient()) { // Instantiate the chunk uploader with the current HttpClient. FileChunkUploader uploader = new FileChunkUploader(httpClient); // Call the chunk uploader method and get the S3 address. return await uploader.UploadFileInChunksAsync(zipPath, endpoint, username, apiKey, progress); } } catch (Exception ex) { Console.WriteLine("Upload failed: " + ex.Message); return null; } } /// /// Synchronous wrapper for CompressAndUploadDataAsync. /// /// The S3 address of the uploaded file if successful, null otherwise public static string CompressAndUploadData(string baseDir = "recorded_data", string userId = "default", string password = null, UploadProgress progress = null) { try { // Validating data before uploading string[] root = Directory.GetFileSystemEntries(baseDir); string[] frames = Directory.GetFileSystemEntries(baseDir + "\\frames"); long dataSize = GetDirectorySize(baseDir); if (root.Length == 0) { throw new InvalidOperationException("Processed data has no files."); } if (frames.Length == 0) { throw new InvalidOperationException("No capture frames exist."); } if (dataSize < 40 * 1024 * 1024) // Capture is less than 40 MB { throw new InvalidOperationException("Processed data is too small. Try reconnecting your headset and then retry enrollment."); } // Run the async method synchronously return CompressAndUploadDataAsync(baseDir, userId, password, progress).GetAwaiter().GetResult(); } catch (Exception ex) { GD.PrintErr($"Error in CompressAndUploadData: {ex.Message}"); GD.PrintErr(ex.StackTrace); throw new InvalidOperationException(ex.Message); } } }