Media: uploads stripped of metadata, attachments both ways, a remote media proxy
- /api/v1/media and /api/v2/media (and GET/PUT /api/v1/media/:id):
images go through libvips (NetVips, its native build bundled):
autorotated, every kind of metadata dropped (EXIF, GPS, XMP, IPTC,
comments), capped at 4096 px, with a 640 px preview and a blurhash
(own encoder, the reference algorithm); animated GIFs are re-encoded;
video and audio are remuxed by ffmpeg with -map_metadata -1, never
re-encoded, and a video gets a still preview. Files get random names
under /var/lib/privapub/media, outside the web root deploys replace, and
are served at /media/files with nosniff and a sandbox CSP.
- media_ids on create and edit (four at most, the persona's own, each used
once); notes carry them as Document attachments with alt text, blurhash,
focalPoint and size; inbound attachments were already kept.
- avatar and header uploads in update_credentials, cropped to 400x400 and
1500x500, federated with Update{Person}.
- Remote media reaches clients only through /media/proxy/{hmac}/{url},
fetched by the guarded client (no SVG, 40 MB cap) and cached outside the
served root, trimmed to 5 GB; foreign avatars and headers use it too, so
a client never contacts another server.
- MediaJanitor deletes uploads left unattached for a day.
- nginx accepts 100 MB bodies on the upload endpoints only (applied on Max).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
8f75317050
commit
4a713f3fb6
22 files changed
+1030
-26
No files matched your search
@@ -0,0 +1,72 @@
|
||||
using NetVips;
|
||||
|
||||
using PrivaPub.Domain.Media;
|
||||
|
||||
namespace PrivaPub.Tests.Domain
|
||||
{
|
||||
public class MediaProcessingTests
|
||||
{
|
||||
static byte[] JpegWithMetadata(int width, int height)
|
||||
{
|
||||
using var image = (Image.Black(width, height, bands: 3) + new double[] { 200, 40, 90 }).Cast(Enums.BandFormat.Uchar);
|
||||
using var tagged = image.Mutate(m =>
|
||||
{
|
||||
m.Set(GValue.GStrType, "exif-ifd0-ImageDescription", "where I live");
|
||||
m.Set(GValue.GStrType, "exif-ifd0-Artist", "Alice Smith");
|
||||
m.Set(GValue.GStrType, "exif-ifd2-UserComment", "at home");
|
||||
m.Set(GValue.BlobType, "xmp-data", System.Text.Encoding.UTF8.GetBytes("<x:xmpmeta xmlns:x='adobe:ns:meta/'><secret/></x:xmpmeta>"));
|
||||
});
|
||||
return tagged.WriteToBuffer(".jpg");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Uploaded_images_lose_every_kind_of_metadata()
|
||||
{
|
||||
var input = JpegWithMetadata(800, 600);
|
||||
using (var original = Image.NewFromBuffer(input))
|
||||
Assert.Contains("exif-data", original.GetFields());
|
||||
|
||||
var processed = MediaService.ProcessImage(input, 4096, 640, animated: false);
|
||||
|
||||
using var output = Image.NewFromBuffer(processed.Bytes);
|
||||
var fields = output.GetFields();
|
||||
Assert.DoesNotContain("exif-data", fields);
|
||||
Assert.DoesNotContain("xmp-data", fields);
|
||||
Assert.DoesNotContain("iptc-data", fields);
|
||||
Assert.DoesNotContain(fields, f => f.StartsWith("exif-ifd"));
|
||||
Assert.Equal((800, 600), (processed.Width, processed.Height));
|
||||
Assert.Equal("image/jpeg", processed.ContentType);
|
||||
using var preview = Image.NewFromBuffer(processed.Preview);
|
||||
Assert.Equal(640, Math.Max(preview.Width, preview.Height));
|
||||
Assert.DoesNotContain("exif-data", preview.GetFields());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Large_images_are_capped()
|
||||
{
|
||||
var processed = MediaService.ProcessImage(JpegWithMetadata(5000, 2500), 4096, 640, animated: false);
|
||||
|
||||
Assert.Equal((4096, 2048), (processed.Width, processed.Height));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_blurhash_is_well_formed()
|
||||
{
|
||||
var processed = MediaService.ProcessImage(JpegWithMetadata(64, 64), 4096, 640, animated: false);
|
||||
|
||||
Assert.Equal(28, processed.Blurhash.Length);
|
||||
Assert.Equal('L', processed.Blurhash[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_white_image_encodes_white_as_its_average()
|
||||
{
|
||||
var pixels = Enumerable.Range(0, 16 * 16).SelectMany(_ => new byte[] { 255, 255, 255 }).ToArray();
|
||||
|
||||
var hash = Blurhash.Encode(pixels, 16, 16);
|
||||
|
||||
Assert.Equal(28, hash.Length);
|
||||
Assert.Equal("TSUA", hash[2..6]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user