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);
}
}
}