-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
200 lines (173 loc) · 8.5 KB
/
Program.cs
File metadata and controls
200 lines (173 loc) · 8.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "FFMPEG API", Version = "v1" });
});
builder.Services.AddLogging(); // Ensure logging is configured
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "FFMPEG API v1"));
}
app.MapPost("/convert", async (HttpRequest req, ILogger<Program> logger) =>
{
if (!req.HasFormContentType)
{
return Results.BadRequest("Expected form data.");
}
var form = await req.ReadFormAsync();
var inputFile = form.Files["inputFile"];
var outputFormat = form["outputFormat"].ToString();
var ffmpegOptions = form["ffmpegOptions"].ToString(); // Optional extra FFMPEG args
if (inputFile == null || inputFile.Length == 0)
{
return Results.BadRequest("Input file is required.");
}
if (string.IsNullOrWhiteSpace(outputFormat))
{
return Results.BadRequest("Output format is required.");
}
// Basic sanitization for output format (allow only alphanumeric)
outputFormat = System.Text.RegularExpressions.Regex.Replace(outputFormat, "[^a-zA-Z0-9]", "");
if (string.IsNullOrWhiteSpace(outputFormat)) {
return Results.BadRequest("Invalid output format provided after sanitization.");
}
var tempInputFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(inputFile.FileName));
var tempOutputFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + "." + outputFormat);
try
{
// Save uploaded file temporarily
logger.LogInformation("Saving input file to {TempInputPath}", tempInputFilePath);
await using (var stream = new FileStream(tempInputFilePath, FileMode.Create))
{
await inputFile.CopyToAsync(stream);
}
logger.LogInformation("Input file saved successfully.");
// Construct FFMPEG command
// WARNING: Directly using ffmpegOptions from user input is a security risk.
// In a real application, validate/sanitize this input rigorously or only allow specific presets.
var arguments = $"-i \"{tempInputFilePath}\" {ffmpegOptions} \"{tempOutputFilePath}\"";
logger.LogInformation("Executing FFMPEG command: ffmpeg {Arguments}", arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = "ffmpeg", // Assumes ffmpeg is in the PATH
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = new Process { StartInfo = processStartInfo };
var stdErr = new System.Text.StringBuilder();
process.ErrorDataReceived += (sender, args) => {
if(args.Data != null) {
stdErr.AppendLine(args.Data);
logger.LogWarning("FFMPEG STDERR: {Data}", args.Data); // Log stderr in real-time
}
};
process.Start();
process.BeginErrorReadLine(); // Start reading stderr asynchronously
// Optionally capture stdout if needed
// string stdOut = await process.StandardOutput.ReadToEndAsync();
// logger.LogInformation("FFMPEG STDOUT: {StdOut}", stdOut);
await process.WaitForExitAsync(); // Wait for FFMPEG to complete
if (process.ExitCode != 0)
{
logger.LogError("FFMPEG failed with exit code {ExitCode}. Error: {StdErr}", process.ExitCode, stdErr.ToString());
return Results.Problem($"FFMPEG execution failed. Exit Code: {process.ExitCode}. Error: {stdErr.ToString()}", statusCode: 500);
}
if (!File.Exists(tempOutputFilePath))
{
logger.LogError("FFMPEG completed but output file {OutputFile} not found.", tempOutputFilePath);
return Results.Problem("FFMPEG finished, but the output file was not created.", statusCode: 500);
}
logger.LogInformation("FFMPEG conversion successful. Output file: {OutputFile}", tempOutputFilePath);
// Return the converted file
var fileBytes = await File.ReadAllBytesAsync(tempOutputFilePath);
var contentType = GetContentType(outputFormat); // Helper to determine MIME type
var downloadFileName = Path.GetFileNameWithoutExtension(inputFile.FileName) + "." + outputFormat;
// Return file stream for better memory usage with large files
// Note: The FileStreamResult will manage the disposal of the stream.
var fileStream = new FileStream(tempOutputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.DeleteOnClose);
return Results.File(fileStream, contentType, downloadFileName);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred during conversion.");
return Results.Problem($"An unexpected error occurred: {ex.Message}", statusCode: 500);
}
finally
{
// Clean up temporary input file if it wasn't handled by FileStreamResult's DeleteOnClose
if (File.Exists(tempInputFilePath))
{
try { File.Delete(tempInputFilePath); } catch (Exception ex) { logger.LogWarning(ex, "Failed to delete temporary input file: {TempInputPath}", tempInputFilePath); }
}
// Clean up temporary output file ONLY if it wasn't successfully returned (e.g., an error occurred before Results.File)
// If Results.File was called with FileOptions.DeleteOnClose, it handles deletion.
if (File.Exists(tempOutputFilePath) && !(app.Logger.IsEnabled(LogLevel.Information))) // Crude check if file was likely returned
{
try { File.Delete(tempOutputFilePath); } catch (Exception ex) { logger.LogWarning(ex, "Failed to delete temporary output file: {TempOutputPath}", tempOutputFilePath); }
}
}
})
.Accepts<IFormFile>("multipart/form-data") // Hint for Swagger UI
.Produces<IResult>(StatusCodes.Status200OK, "application/octet-stream") // Adjust content type if known
.Produces(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithOpenApi(operation => new(operation)
{
Summary = "Converts an uploaded media file using FFMPEG.",
Description = "Upload a media file, specify the desired output format, and optionally provide additional FFMPEG command-line options. Returns the converted file.",
RequestBody = new()
{
Content = new Dictionary<string, Microsoft.OpenApi.Models.OpenApiMediaType>
{
["multipart/form-data"] = new()
{
Schema = new()
{
Type = "object",
Required = new HashSet<string> { "inputFile", "outputFormat" },
Properties = new Dictionary<string, Microsoft.OpenApi.Models.OpenApiSchema>
{
["inputFile"] = new() { Type = "string", Format = "binary", Description = "The media file to convert." },
["outputFormat"] = new() { Type = "string", Description = "The desired output format (e.g., 'mp4', 'mp3', 'webm').", Example = new Microsoft.OpenApi.Any.OpenApiString("mp4") },
["ffmpegOptions"] = new() { Type = "string", Description = "(Optional) Additional FFMPEG command-line options. Use with caution due to security risks.", Example = new Microsoft.OpenApi.Any.OpenApiString("-vf scale=640:-1 -crf 28") }
}
}
}
}
}
});
// Helper to get basic content types
string GetContentType(string format) => format.ToLowerInvariant() switch
{
"mp4" => "video/mp4",
"webm" => "video/webm",
"ogv" => "video/ogg",
"mp3" => "audio/mpeg",
"ogg" => "audio/ogg",
"wav" => "audio/wav",
"aac" => "audio/aac",
"flac" => "audio/flac",
"jpg" or "jpeg" => "image/jpeg",
"png" => "image/png",
"gif" => "image/gif",
_ => "application/octet-stream", // Default binary type
};
app.Run();
// Add a public partial class for Program to make it accessible for tests if needed
public partial class Program { }