using System.Text; using Octokit; namespace AiAgent.Core.Infrastructure { public static class GitHubHelper { // === CONFIGURATION === // Replace these with your actual details. private const string GitHubToken = "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"; private const string RepoOwner = "fredfesty"; // Your GitHub username private const string RepoName = "NewsAI"; private const string Branch = "main"; // Or "master", depending on your repo /// /// Creates and configures a new instance of the class. /// /// The client is initialized with a custom product header value and authenticated using /// the provided GitHub token. Ensure that the GitHub token is valid and has the necessary permissions for the /// intended operations. /// A configured instance ready for use. private static GitHubClient GetClient() { var github = new GitHubClient(new ProductHeaderValue("My-CSharp-App")); github.Credentials = new Credentials(GitHubToken); return github; } /// /// Downloads a file from a GitHub repository and saves it to a specified local directory. /// /// The file content is retrieved from the GitHub repository, decoded from Base64, and /// written to the specified local directory. If the file already exists in the target directory, it will be /// overwritten. /// The relative path of the file within the repository. This path must be valid and point to an existing file. /// The local directory where the file will be saved. The directory must exist, and the method will create a /// file with the same name as the source file in this directory. /// A representing the asynchronous operation. The task result contains the original /// content of the downloaded file as a string. /// Thrown if the file cannot be accessed in the repository, the file does not exist, or an error occurs during /// the download or save operation. public static async Task DownloadFile(string filePathInRepo, string localPath) { // Get the github client var github = GetClient(); //string filePathInRepo = "src/uploaded-file.txt"; // We get the file's metadata, including its content (Base64 encoded) and SHA hash. var file = await github.Repository.Content.GetAllContents(RepoOwner, RepoName, filePathInRepo) ?? throw new Exception($"Error: Unable to access the repository or path '{filePathInRepo}'."); var targetFile = file.FirstOrDefault() ?? throw new Exception($"Error: The file '{filePathInRepo}' was not found."); // The content is Base64 encoded, so we need to decode it. //string originalFileContent = Encoding.UTF8.GetString(Convert.FromBase64String(targetFile.Content)); string fileName = Path.GetFileName(filePathInRepo); string localFile = Path.Combine(localPath, fileName); await File.WriteAllTextAsync(localFile, targetFile.Content); return localFile; } /// /// Uploads a file to a specified path in the repository, updating the file if it already exists. /// /// This method updates the file in the repository if it already exists at the specified /// path. If the file does not exist, an exception is thrown. Ensure that the repository and branch are /// accessible and that the provided paths are valid. /// The path within the repository where the file should be uploaded. This must include the file name and /// extension. /// The local file path of the file to be uploaded. The file must exist at this path. /// The commit message to associate with the file upload operation. /// A task that represents the asynchronous operation. The task result contains the commit message of the /// operation. /// Thrown if the repository or specified path cannot be accessed, if the local file cannot be read, or if the /// file update operation fails. public static async Task UploadFile(string filePathInRepo, string localPath, string commitMessage) { // Get the github client var github = GetClient(); // Try to get the existing file's metadata (if it exists) var existingFile = await GetFileSha(github, filePathInRepo) ?? throw new Exception($"Error: Unable to access the repository or path '{filePathInRepo}'."); // Read the content off the local file string fileContent = await File.ReadAllTextAsync(localPath); // Create a request to update the file var updateFileRequest = new UpdateFileRequest(commitMessage, fileContent, existingFile.Sha, Branch) ?? throw new Exception("Error: Failed to create update request."); // 4b. If the file exists, update it var result = await github.Repository.Content.UpdateFile(RepoOwner, RepoName, filePathInRepo, updateFileRequest) ?? throw new Exception("Error: Failed to update the file in the repository."); return result.Commit.Message; } /// /// Uploads a new file to the specified path in the repository. /// /// This method reads the content of the specified local file and uploads it to the given /// path in the repository. The file is created in the repository's default branch. /// The path within the repository where the file will be created. This should include the file name and /// extension. /// The local file system path of the file to be uploaded. The file's content will be read from this path. /// The commit message to associate with the file upload operation. /// A representing the asynchronous operation. The task result contains the commit /// message of the operation. /// Thrown if the file upload request cannot be created or if the upload operation fails. public static async Task UploadNewFile(string filePathInRepo, string localPath, string commitMessage) { // Get the github client var github = GetClient(); // Read the content off the local file string fileContent = await File.ReadAllTextAsync(localPath); try { // First, try to get the file's metadata to see if it exists. var existingFile = await github.Repository.Content.GetAllContents(RepoOwner, RepoName, filePathInRepo); // If the file exists, we update it. // The GetAllContents method returns a list; we need the SHA of the specific file. var updateRequest = new UpdateFileRequest(commitMessage, fileContent, existingFile.First().Sha, Branch); var updateResponse = await github.Repository.Content.UpdateFile(RepoOwner, RepoName, filePathInRepo, updateRequest); return updateResponse.Commit.Message; } catch (NotFoundException) { // If the file does not exist, a NotFoundException is thrown. We then create it. var createRequest = new CreateFileRequest(commitMessage, fileContent, Branch); var createResponse = await github.Repository.Content.CreateFile(RepoOwner, RepoName, filePathInRepo, createRequest); return createResponse.Commit.Message; } } /// /// Uploads a binary file (like an image) to the specified path in the repository. /// /// /// DEPRECATED for larger files. This method can fail silently on files larger than ~1MB. Use UploadFileAsGitBlobAsync instead. /// This method reads the local file as a byte array and encodes it to Base64, which is required for binary files. /// It now includes a retry mechanism for file reading to handle potential file locks, plus diagnostic logging and robust error handling. /// /// The path within the repository where the file will be created. /// The local file system path of the file to be uploaded. /// The commit message to associate with the upload operation. /// A string containing the commit message of the operation, or an error message if it fails. public static async Task UploadImageFile(string filePathInRepo, string localPath, string commitMessage) { try { // Get the github client var github = GetClient(); // --- Start of Diagnostic Checks --- Console.WriteLine($"\n[DEBUG] Attempting to read file from: {localPath}"); if (!File.Exists(localPath)) { var errorMsg = $"[ERROR] File not found at path: {localPath}"; Console.WriteLine(errorMsg); return errorMsg; } // --- End of Diagnostic Checks --- // Read the file content as a byte array with a retry mechanism byte[] fileBytes = null; const int maxRetries = 3; const int delayMilliseconds = 500; for (int i = 0; i < maxRetries; i++) { try { fileBytes = await File.ReadAllBytesAsync(localPath); if (fileBytes.Length > 0) { break; // Success, exit loop } Console.WriteLine($"[WARN] Attempt {i + 1}: File was read as empty. Retrying after {delayMilliseconds}ms..."); } catch (IOException ex) { Console.WriteLine($"[WARN] Attempt {i + 1}: Could not read file (IO Exception: {ex.Message}). Retrying after {delayMilliseconds}ms..."); } await Task.Delay(delayMilliseconds); } if (fileBytes == null || fileBytes.Length == 0) { return "[ERROR] Failed to read file after multiple attempts. It might be locked or empty."; } // Convert the byte array to a Base64 string var fileContentBase64 = Convert.ToBase64String(fileBytes); // --- Start of Diagnostic Logging --- Console.WriteLine($"[DEBUG] Original file size: {fileBytes.Length} bytes."); Console.WriteLine($"[DEBUG] Base64 content length: {fileContentBase64.Length} characters."); // --- End of Diagnostic Logging --- // Create a request to create the file in the repository var createFileRequest = new CreateFileRequest(commitMessage, fileContentBase64, Branch); // Create the file in the repository Console.WriteLine("[DEBUG] Sending create file request to GitHub API..."); var createFileResponse = await github.Repository.Content.CreateFile(RepoOwner, RepoName, filePathInRepo, createFileRequest); Console.WriteLine("[DEBUG] GitHub API request successful."); return createFileResponse.Commit.Message; } catch (ApiException apiEx) { // Catch specific GitHub API errors var errorMsg = $"[ERROR] GitHub API Error: {apiEx.Message}"; Console.WriteLine($"\n{errorMsg}"); Console.WriteLine($"[ERROR] Details: {apiEx.ApiError?.Message}"); return errorMsg; } catch (Exception ex) { // Catch any other unexpected errors var errorMsg = $"[ERROR] An unexpected error occurred during file upload: {ex.Message}"; Console.WriteLine($"\n{errorMsg}"); return errorMsg; } } /// /// Uploads a file using the robust Git Data API, suitable for any file size. /// /// /// This is the recommended method for all file uploads, especially binary files like images. /// It creates a blob, a new tree, a new commit, and updates the branch reference. /// public static async Task UploadFileAsGitBlobAsync(string filePathInRepo, string localPath, string commitMessage) { // 1. Create the Blob (can stay outside the loop if content is static) var github = GetClient(); var fileBytes = await File.ReadAllBytesAsync(localPath); var fileContentAsBase64 = Convert.ToBase64String(fileBytes); var newBlob = new NewBlob { Content = fileContentAsBase64, Encoding = EncodingType.Base64 }; var blobRef = await github.Git.Blob.Create(RepoOwner, RepoName, newBlob); Console.WriteLine($"[DEBUG] Created Blob with SHA: {blobRef.Sha}"); int maxRetries = 5; int retryCount = 0; bool pushSucceeded = false; while (!pushSucceeded && retryCount < maxRetries) { try { // 2. Get the *current* commit and tree var head = await github.Git.Reference.Get(RepoOwner, RepoName, $"heads/{Branch}"); var latestCommit = await github.Git.Commit.Get(RepoOwner, RepoName, head.Object.Sha); Console.WriteLine($"[DEBUG] Attempt {retryCount + 1}: Base commit is {latestCommit.Sha}"); // 3. Create a new tree with the new file var newTree = new NewTree { BaseTree = latestCommit.Tree.Sha }; newTree.Tree.Add(new NewTreeItem { Path = filePathInRepo, Mode = "100644", Type = TreeType.Blob, Sha = blobRef.Sha }); var createdTree = await github.Git.Tree.Create(RepoOwner, RepoName, newTree); // 4. Create a new commit var newCommit = new NewCommit(commitMessage, createdTree.Sha, latestCommit.Sha); var createdCommit = await github.Git.Commit.Create(RepoOwner, RepoName, newCommit); // 5. Update the branch reference await github.Git.Reference.Update(RepoOwner, RepoName, $"heads/{Branch}", new ReferenceUpdate(createdCommit.Sha)); Console.WriteLine($"[DEBUG] Branch '{Branch}' updated to new commit: {createdCommit.Sha}"); pushSucceeded = true; // Success! Exit the loop. return $"Successfully committed {createdCommit.Sha}"; } catch (ApiValidationException ex) when (ex.Message.Contains("Update is not a fast forward")) { // Race condition detected. Increment retry count and loop again. retryCount++; Console.WriteLine($"[WARN] Race condition detected. Retrying attempt {retryCount}..."); await Task.Delay(100 * retryCount); // Optional: add a small, increasing delay } // Note: Other exceptions will fall through and fail, as they should. } //// If we exit the loop without success //throw new Exception($"Failed to commit to branch '{Branch}' after {maxRetries} attempts due to repeated race conditions."); //var github = GetClient(); //// 1. Create the Blob //var fileBytes = await File.ReadAllBytesAsync(localPath); //var fileContentBase64 = Convert.ToBase64String(fileBytes); //var newBlob = new NewBlob { Content = fileContentBase64, Encoding = EncodingType.Base64 }; //var blobRef = await github.Git.Blob.Create(RepoOwner, RepoName, newBlob); //Console.WriteLine($"[DEBUG] Created blob with SHA: {blobRef.Sha}"); //// 2. Get the current commit and tree //var head = await github.Git.Reference.Get(RepoOwner, RepoName, $"heads/{Branch}"); //var latestCommit = await github.Git.Commit.Get(RepoOwner, RepoName, head.Object.Sha); //// 3. Create a new tree with the new file //var newTree = new NewTree { BaseTree = latestCommit.Tree.Sha }; //newTree.Tree.Add(new NewTreeItem { Path = filePathInRepo, Mode = "100644", Type = TreeType.Blob, Sha = blobRef.Sha }); //var createdTree = await github.Git.Tree.Create(RepoOwner, RepoName, newTree); //Console.WriteLine($"[DEBUG] Created tree with SHA: {createdTree.Sha}"); //// 4. Create a new commit //var newCommit = new NewCommit(commitMessage, createdTree.Sha, latestCommit.Sha); //var createdCommit = await github.Git.Commit.Create(RepoOwner, RepoName, newCommit); //Console.WriteLine($"[DEBUG] Created commit with SHA: {createdCommit.Sha}"); //// 5. Update the branch reference to point to the new commit //await github.Git.Reference.Update(RepoOwner, RepoName, $"heads/{Branch}", new ReferenceUpdate(createdCommit.Sha)); //Console.WriteLine($"[DEBUG] Branch '{Branch}' updated to new commit."); return "Failed"; } // Helper method to check if a file exists and get its SHA private static async Task GetFileSha(IGitHubClient github, string filePath) { try { // Get all content in the file's directory var contents = await github.Repository.Content.GetAllContents(RepoOwner, RepoName, filePath); // Return the specific file's metadata return contents.FirstOrDefault(); } catch (NotFoundException) { // If the file or directory doesn't exist, a NotFoundException is thrown return null; } } } }