diff --git a/deploy/portainer-stack.yml b/deploy/portainer-stack.yml index 252767a..f16e389 100644 --- a/deploy/portainer-stack.yml +++ b/deploy/portainer-stack.yml @@ -1,6 +1,6 @@ services: beacon-server: - image: beacon-server:0.1.10 + image: beacon-server:0.1.11 restart: always environment: ASPNETCORE_ENVIRONMENT: Production @@ -59,7 +59,7 @@ services: /bin/sh -c '(while sleep 86400; do nginx -s reload; done) & exec nginx -g "daemon off;"' beacon-backup: - image: beacon-backup:0.1.10 + image: beacon-backup:0.1.11 restart: always environment: BACKUP_INTERVAL_SECONDS: "86400" diff --git a/src/Beacon.Plugin/Beacon.Plugin.csproj b/src/Beacon.Plugin/Beacon.Plugin.csproj index 76e2b92..7c1c611 100644 --- a/src/Beacon.Plugin/Beacon.Plugin.csproj +++ b/src/Beacon.Plugin/Beacon.Plugin.csproj @@ -4,7 +4,7 @@ Beacon Beacon - 0.1.10.0 + 0.1.11.0 enable diff --git a/src/Beacon.Plugin/UI/ProfileLabels.cs b/src/Beacon.Plugin/UI/ProfileLabels.cs index d83edd2..8a3fa10 100644 --- a/src/Beacon.Plugin/UI/ProfileLabels.cs +++ b/src/Beacon.Plugin/UI/ProfileLabels.cs @@ -190,7 +190,7 @@ public static string LastSeen(DateTimeOffset? lastActive) < 90 => "Seen within the hour", < 60 * 12 => "Seen today", < 60 * 36 => "Seen yesterday", - < 60 * 24 * 7 => $"Seen {(int)span.TotalDays} days ago", + < 60 * 24 * 7 => (int)span.TotalDays == 1 ? "Seen yesterday" : $"Seen {(int)span.TotalDays} days ago", < 60 * 24 * 14 => "Seen last week", < 60 * 24 * 60 => $"Seen {(int)(span.TotalDays / 7)} weeks ago", _ => "Not seen in months", diff --git a/src/Beacon.Server/Endpoints/ImageEndpoints.cs b/src/Beacon.Server/Endpoints/ImageEndpoints.cs index 8a895ff..d61672e 100644 --- a/src/Beacon.Server/Endpoints/ImageEndpoints.cs +++ b/src/Beacon.Server/Endpoints/ImageEndpoints.cs @@ -155,9 +155,29 @@ private static async Task ServeAsync( BeaconDbContext db, CancellationToken ct) { + // Beacon screenshots and profile gallery images share one store on disk but keep separate + // metadata tables. Looking in only the screenshot table is what made every profile portrait + // return 404 while its bytes sat on disk the whole time. var record = await db.Images.FirstOrDefaultAsync(i => i.Id == imageId, ct); - if (record is null) - return Results.NotFound(); + + string contentType; + DateTimeOffset createdAt; + + if (record is not null) + { + contentType = record.ContentType; + createdAt = record.CreatedAt; + } + else + { + var gallery = await db.ProfileImages.FirstOrDefaultAsync(i => i.ImageId == imageId, ct); + if (gallery is null) + return Results.NotFound(); + + // The gallery stores no content type because everything in the store is written as WebP. + contentType = "image/webp"; + createdAt = gallery.CreatedAt; + } var wantsPng = string.Equals(format, "png", StringComparison.OrdinalIgnoreCase); @@ -168,16 +188,16 @@ private static async Task ServeAsync( if (path is null || !File.Exists(path)) return Results.NotFound(); - var contentType = wantsPng ? "image/png" : record.ContentType; + var served = wantsPng ? "image/png" : contentType; var variant = $"{(thumb ? "t" : "f")}{(wantsPng ? "p" : "w")}"; // Image ids are never reused and the bytes never change, so this can be cached hard. // The tag includes the variant so a PNG and a WebP of the same image never collide in a cache. return Results.File( path, - contentType, + served, enableRangeProcessing: true, - lastModified: record.CreatedAt, + lastModified: createdAt, entityTag: new Microsoft.Net.Http.Headers.EntityTagHeaderValue($"\"{imageId:N}{variant}\"")); }