media: кодеки для magick в образе и диагностика ресайза
Базовый пакет imagemagick в alpine идёт без кодеков, поэтому magick не мог прочитать jpeg-вложение и падал с "exit status 1". Ставим imagemagick-jpeg, -heic, -webp, -tiff. Чтобы такое было видно из логов: runMagick подставляет stderr в ошибку и отдельно ловит "код выхода 0, но пустой вывод" (так magick ведёт себя без кодека на запись), а веб логирует формат, размеры до и после, время скачивания и ресайза. Если ресайз всё же не удался, отдаём оригинал вместо 502; в кэш не кладём записи больше 4 МБ, чтобы неужатые оригиналы не съели память. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+41
-4
@@ -2,11 +2,13 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/remembos/internal/media"
|
||||
"git.vakhrushev.me/av/remembos/internal/memos"
|
||||
@@ -21,6 +23,9 @@ const imagePathPrefix = "/image/"
|
||||
// чтобы не пережимать их заново на каждую перезагрузку страницы.
|
||||
const imageCacheSize = 20
|
||||
|
||||
// imageCacheMaxBytes — верхняя граница размера одной записи кэша.
|
||||
const imageCacheMaxBytes = 4 * 1024 * 1024
|
||||
|
||||
// attachmentUID returns the identifier used in image URLs ("attachments/{uid}" → "{uid}").
|
||||
func attachmentUID(att memos.Attachment) string {
|
||||
return strings.TrimPrefix(att.Name, "attachments/")
|
||||
@@ -42,7 +47,10 @@ func (h *Handler) handleImage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
data, contentType, err := h.imageBytes(r.Context(), att)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to prepare image", "name", att.Name, "error", err)
|
||||
h.logger.Error("failed to prepare image",
|
||||
"name", att.Name, "filename", att.Filename, "mime", att.Type,
|
||||
"declared_bytes", att.Size, "external", att.ExternalLink != "",
|
||||
"error", err)
|
||||
http.Error(w, "не удалось загрузить изображение", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
@@ -76,26 +84,55 @@ func (h *Handler) findAttachment(ctx context.Context, uid string) (memos.Attachm
|
||||
return att, true
|
||||
}
|
||||
}
|
||||
|
||||
// Обычно это устаревшая ссылка: страницу открыли до того, как воспоминание сменилось.
|
||||
h.logger.Info("image not found in current memory", "uid", uid, "memo", mem.Memo.Name)
|
||||
return memos.Attachment{}, false
|
||||
}
|
||||
|
||||
// imageBytes downloads and resizes the attachment, caching the result.
|
||||
func (h *Handler) imageBytes(ctx context.Context, att memos.Attachment) ([]byte, string, error) {
|
||||
if img, ok := h.images.get(att.Name); ok {
|
||||
h.logger.Debug("image served from cache",
|
||||
"name", att.Name, "mime", img.contentType, "bytes", len(img.data))
|
||||
return img.data, img.contentType, nil
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
|
||||
raw, err := h.client.DownloadAttachment(ctx, att)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", fmt.Errorf("download %s (%s): %w", att.Filename, att.Type, err)
|
||||
}
|
||||
downloaded := time.Since(started)
|
||||
|
||||
resizeStart := time.Now()
|
||||
data, contentType, err := media.ResizeForWeb(ctx, raw, att.Type)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
// Отдать картинку неужатой лучше, чем не отдать вовсе.
|
||||
h.logger.Warn("failed to resize image, serving original",
|
||||
"name", att.Name, "filename", att.Filename, "mime", att.Type,
|
||||
"src_bytes", len(raw), "error", err)
|
||||
data, contentType = raw, att.Type
|
||||
}
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
h.images.put(att.Name, cachedImage{data: data, contentType: contentType})
|
||||
// Оригиналы бывают на десятки мегабайт — такие в кэш не кладём.
|
||||
cached := len(data) <= imageCacheMaxBytes
|
||||
if cached {
|
||||
h.images.put(att.Name, cachedImage{data: data, contentType: contentType})
|
||||
}
|
||||
|
||||
h.logger.Info("image prepared",
|
||||
"name", att.Name, "filename", att.Filename,
|
||||
"src_mime", att.Type, "src_bytes", len(raw),
|
||||
"out_mime", contentType, "out_bytes", len(data),
|
||||
"download", downloaded.Round(time.Millisecond).String(),
|
||||
"resize", time.Since(resizeStart).Round(time.Millisecond).String(),
|
||||
"cached", cached)
|
||||
|
||||
return data, contentType, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user