Skip to content

Commit 2c81650

Browse files
authored
Merge pull request #22 from csharpfritz/feature-ContentImages
2 parents a0b041d + 12d8241 commit 2c81650

20 files changed

Lines changed: 968 additions & 275 deletions

.github/workflows/azure-dev.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ jobs:
4646
dotnet-version: |
4747
9.x.x
4848
49+
- name: Run Unit Tests
50+
run: dotnet test --no-build --verbosity normal
51+
if: always()
52+
4953
- name: Log in with Azure (Federated Credentials)
5054
run: |
5155
azd auth login `

AppHost/AppHost.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
});
2525

2626
var tables = storage.AddTables("tables");
27+
var blobs = storage.AddBlobs("blobs");
2728

2829
var web = builder.AddProject<Web>("web")
2930
.WithReference(tables)
31+
.WithReference(blobs)
3032
.WithReference(redis)
3133
.WaitFor(tables)
3234
.WaitFor(redis)

ContentLoader/ContentLoader.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Azure.Data.Tables" Version="12.11.0" />
12+
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.1" />
1213
<PackageReference Include="Markdig" Version="0.41.2" />
14+
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" />
1315
<PackageReference Include="YamlDotNet" Version="16.3.0" />
1416
</ItemGroup>
1517

ContentLoader/Program.cs

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System.Text;
22
using System.Text.RegularExpressions;
3+
using System.Text.Json;
34
using Azure;
45
using Azure.Data.Tables;
6+
using Azure.Storage.Blobs;
57
using Markdig;
68
using Shared;
79
using YamlDotNet.Serialization;
@@ -36,23 +38,80 @@
3638
{
3739
// Check if the path is a directory or a file
3840
if (Directory.Exists(filePath))
39-
{
40-
// Recursively get all markdown files in the directory
41+
{ // Recursively get all markdown files
4142
var mdFiles = Directory.GetFiles(filePath, "*.md", SearchOption.AllDirectories);
43+
44+
// For each markdown file, look for images in its adjacent images folder
45+
var imageFiles = mdFiles
46+
.SelectMany(mdFile =>
47+
{
48+
var mdDirectory = Path.GetDirectoryName(mdFile);
49+
var imagesPath = Path.Combine(mdDirectory!, "images");
50+
return Directory.Exists(imagesPath)
51+
? Directory.GetFiles(imagesPath, "*.*", SearchOption.AllDirectories)
52+
.Where(f => new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" }
53+
.Contains(Path.GetExtension(f).ToLowerInvariant()))
54+
: Array.Empty<string>();
55+
})
56+
.Distinct()
57+
.ToArray();
58+
4259
if (mdFiles.Length == 0)
4360
{
4461
Console.WriteLine($"No markdown files found in directory: {filePath}");
4562
return 1;
46-
} int addedCount = 0;
63+
} // Create blob service client for image uploads
64+
var blobServiceClient = new BlobServiceClient(connectionString);
65+
66+
// Process images first
67+
Console.WriteLine("\nProcessing images...");
68+
var imageUploadResults = new List<ImageInfo>();
69+
foreach (var imageFile in imageFiles)
70+
{
71+
try
72+
{
73+
var imageInfo = await ImageUploadHelper.ProcessAndUploadImageAsync(imageFile, blobServiceClient);
74+
imageUploadResults.Add(imageInfo);
75+
Console.WriteLine($"Processed image: {imageFile}");
76+
}
77+
catch (Exception ex)
78+
{
79+
Console.WriteLine($"Error processing image {imageFile}: {ex.Message}");
80+
}
81+
}
82+
83+
// Now process markdown files
84+
Console.WriteLine("\nProcessing markdown files...");
85+
int addedCount = 0;
4786
int updatedCount = 0;
4887
int unchangedCount = 0;
4988
int failedCount = 0;
5089

5190
foreach (var mdFile in mdFiles)
52-
{
53-
try
91+
{ try
5492
{
93+
// Parse markdown and look for image references
5594
TipModel tip = Shared.ContentUploadHelper.ParseMarkdownFile(mdFile);
95+
// Replace image references in content with proper image IDs
96+
foreach (var image in imageUploadResults)
97+
{
98+
// Get the markdown file's directory
99+
var mdDirectory = Path.GetDirectoryName(mdFile)!;
100+
// Get the image's directory
101+
var imageDirectory = Path.GetDirectoryName(image.OriginalPath)!;
102+
// Calculate relative path from markdown to image
103+
var relativePath = Path.GetRelativePath(mdDirectory, imageDirectory);
104+
var localImagePath = Path.Combine(relativePath, image.FileName).Replace("\\", "/");
105+
106+
tip.Content = tip.Content.Replace(
107+
$"]({localImagePath}",
108+
$"](/article-images/{image.ImageId}/original.{Path.GetExtension(image.FileName).TrimStart('.')}"
109+
);
110+
}
111+
112+
// Add the image info to the content entity
113+
tip.Images = JsonSerializer.Serialize(imageUploadResults);
114+
56115
var status = await Shared.ContentUploadHelper.UploadToTableStorage(tip, connectionString);
57116

58117
switch (status)
@@ -86,16 +145,59 @@
86145
Console.WriteLine($"Total: {mdFiles.Length}");
87146

88147
return failedCount == 0 ? 0 : 1;
89-
}
90-
else
148+
} else
91149
{
92150
// Parse the markdown file
93151
if (!File.Exists(filePath))
94152
{
95153
Console.WriteLine($"File not found: {filePath}");
96154
return 1;
97155
}
156+
157+
// Create BlobServiceClient for single file
158+
var blobServiceClient = new BlobServiceClient(connectionString);
159+
var imageUploadResults = new List<ImageInfo>();
160+
161+
// Check if there are any images in the images directory
162+
var imageDir = Path.Combine(Path.GetDirectoryName(filePath)!, "images");
163+
if (Directory.Exists(imageDir))
164+
{
165+
var imageFiles = Directory.GetFiles(imageDir, "*.*")
166+
.Where(f => new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" }
167+
.Contains(Path.GetExtension(f).ToLowerInvariant()))
168+
.ToArray();
169+
170+
foreach (var imageFile in imageFiles)
171+
{
172+
try
173+
{
174+
var imageInfo = await ImageUploadHelper.ProcessAndUploadImageAsync(imageFile, blobServiceClient);
175+
imageUploadResults.Add(imageInfo);
176+
Console.WriteLine($"Processed image: {imageFile}");
177+
}
178+
catch (Exception ex)
179+
{
180+
Console.WriteLine($"Error processing image {imageFile}: {ex.Message}");
181+
}
182+
}
183+
}
184+
185+
// Parse and process markdown
98186
TipModel tip = Shared.ContentUploadHelper.ParseMarkdownFile(filePath);
187+
188+
// Replace image references
189+
foreach (var image in imageUploadResults)
190+
{
191+
var localImagePath = $"images/{image.FileName}";
192+
tip.Content = tip.Content.Replace(
193+
$"]({localImagePath}",
194+
$"](/images/{image.ImageId}/original"
195+
);
196+
}
197+
198+
// Add image info to content
199+
tip.Images = System.Text.Json.JsonSerializer.Serialize(imageUploadResults);
200+
99201
// Upload to Azure Table Storage
100202
await Shared.ContentUploadHelper.UploadToTableStorage(tip, connectionString);
101203
return 0;

CopilotThatJawn.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ContentLoader", "ContentLoa
1313
EndProject
1414
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{6DE4B66D-F85E-4DCE-BAE3-85F5AE2531A0}"
1515
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
17+
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web.Tests", "Tests\Web.Tests\Web.Tests.csproj", "{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}"
19+
EndProject
1620
Global
1721
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1822
Debug|Any CPU = Debug|Any CPU
@@ -83,8 +87,23 @@ Global
8387
{6DE4B66D-F85E-4DCE-BAE3-85F5AE2531A0}.Release|x64.Build.0 = Release|Any CPU
8488
{6DE4B66D-F85E-4DCE-BAE3-85F5AE2531A0}.Release|x86.ActiveCfg = Release|Any CPU
8589
{6DE4B66D-F85E-4DCE-BAE3-85F5AE2531A0}.Release|x86.Build.0 = Release|Any CPU
90+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
91+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Debug|Any CPU.Build.0 = Debug|Any CPU
92+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Debug|x64.ActiveCfg = Debug|Any CPU
93+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Debug|x64.Build.0 = Debug|Any CPU
94+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Debug|x86.ActiveCfg = Debug|Any CPU
95+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Debug|x86.Build.0 = Debug|Any CPU
96+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Release|Any CPU.ActiveCfg = Release|Any CPU
97+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Release|Any CPU.Build.0 = Release|Any CPU
98+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Release|x64.ActiveCfg = Release|Any CPU
99+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Release|x64.Build.0 = Release|Any CPU
100+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Release|x86.ActiveCfg = Release|Any CPU
101+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102}.Release|x86.Build.0 = Release|Any CPU
86102
EndGlobalSection
87103
GlobalSection(SolutionProperties) = preSolution
88104
HideSolutionNode = FALSE
89105
EndGlobalSection
106+
GlobalSection(NestedProjects) = preSolution
107+
{0CA4934D-8348-4E35-BA0B-1AD4D3AE0102} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
108+
EndGlobalSection
90109
EndGlobal

Shared/ContentEntity.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@ public class ContentEntity : ITableEntity
2626
public string Slug { get; set; } = string.Empty;
2727
// Content hash for change detection
2828
public string ContentHash { get; set; } = string.Empty;
29+
30+
// Store as JSON string of ImageInfo objects
31+
public string Images { get; set; } = string.Empty;
2932
}

Shared/ContentUploadHelper.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static TipModel ParseMarkdownFile(string filePath)
4040
Content = markdownContent,
4141
FileName = Path.GetFileName(filePath)
4242
};
43-
if (frontMatterData.TryGetValue("publishedDate", out object publishedDateObj) && publishedDateObj != null)
43+
if (frontMatterData.TryGetValue("publishedDate", out object? publishedDateObj) && publishedDateObj != null)
4444
{
4545
if (DateTime.TryParse(publishedDateObj.ToString(), out DateTime publishedDate))
4646
{
@@ -152,8 +152,8 @@ private static string CalculateContentHash(TipModel tip)
152152
PublishedDate = DateTime.SpecifyKind(tip.PublishedDate, DateTimeKind.Utc),
153153
Description = tip.Description,
154154
Content = tip.Content,
155-
FileName = tip.FileName,
156-
ContentHash = contentHash
155+
FileName = tip.FileName, ContentHash = contentHash,
156+
Images = tip.Images
157157
};
158158

159159
await tableClient.UpsertEntityAsync(entity);

Shared/ImageModel.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Shared;
4+
5+
/// <summary>
6+
/// Represents metadata for an image in content
7+
/// </summary>
8+
public class ImageInfo
9+
{
10+
/// <summary>
11+
/// Original filename of the image
12+
/// </summary>
13+
public string FileName { get; set; } = string.Empty;
14+
15+
/// <summary>
16+
/// Generated unique identifier for the image
17+
/// </summary>
18+
public string ImageId { get; set; } = string.Empty;
19+
20+
/// <summary>
21+
/// The blob storage path for the image
22+
/// </summary>
23+
public string BlobPath { get; set; } = string.Empty;
24+
25+
/// <summary>
26+
/// The public URL for the image (CDN or direct blob URL)
27+
/// </summary>
28+
public string PublicUrl { get; set; } = string.Empty;
29+
30+
/// <summary>
31+
/// Alt text for accessibility
32+
/// </summary>
33+
public string AltText { get; set; } = string.Empty;
34+
35+
/// <summary>
36+
/// Caption text (optional)
37+
/// </summary>
38+
public string? Caption { get; set; }
39+
40+
/// <summary>
41+
/// Original width of the image in pixels
42+
/// </summary>
43+
public int Width { get; set; }
44+
45+
/// <summary>
46+
/// Original height of the image in pixels
47+
/// </summary>
48+
public int Height { get; set; }
49+
50+
/// <summary>
51+
/// Size of the image in bytes
52+
/// </summary>
53+
public long SizeInBytes { get; set; }
54+
55+
/// <summary>
56+
/// MIME type of the image
57+
/// </summary>
58+
public string ContentType { get; set; } = string.Empty;
59+
60+
/// <summary>
61+
/// When the image was uploaded
62+
/// </summary>
63+
public DateTime UploadedAt { get; set; }
64+
65+
/// <summary>
66+
/// Hash of the image content for change detection
67+
/// </summary>
68+
public string ContentHash { get; set; } = string.Empty;
69+
70+
/// <summary>
71+
/// The original file path of the image
72+
/// </summary>
73+
public string OriginalPath { get; set; } = string.Empty;
74+
}

0 commit comments

Comments
 (0)