// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 package store import ( "fmt" "io" "path" "path/filepath" "slices" "strings" "time" "uuid" "sourcedock.dev/petrbalvin/volumen/internal/imagefile" ) // MediaPath returns the absolute path of a media file, for a caller that // serves it. The name must carry an allowed image extension, and the file // is opened through the store's root, so a name that would escape the // content directory is refused rather than checked for. func (s *Store) MediaPath(name string) (string, error) { if !imagefile.Allowed(name) { return "", fmt.Errorf("%q is not an allowed image name", name) } base := filepath.Base(name) root, err := s.openRoot() if err != nil { return "", err } handle, err := root.Open(path.Join(MediaDirName, base)) if err != nil { return "", err } defer handle.Close() info, err := handle.Stat() if err != nil { return "", err } if !info.Mode().IsRegular() { return "", fmt.Errorf("%q is not a regular file", base) } return filepath.Join(s.ContentDir, MediaDirName, base), nil } // StoreUpload persists an uploaded image and returns its public URL. The // extension comes from the byte signature, so a stored file always // matches the type it is served as; data that carries no image signature // is refused. The caller is responsible for size validation. func (s *Store) StoreUpload(originalName string, data []byte) (string, error) { ext := imagefile.Detect(data) if ext == "" { return "", fmt.Errorf("unsupported image format in %q", filepath.Base(originalName)) } root, err := s.openRoot() if err != nil { return "", err } if err := root.MkdirAll(MediaDirName, 0o755); err != nil { return "", fmt.Errorf("create media directory: %w", err) } name := uuid.NewV4().String() + ext if err := atomicWriteIn(root, path.Join(MediaDirName, name), data); err != nil { return "", err } return "/media/" + name, nil } // DeleteMedia removes a media file by its public URL and reports whether a // file was removed. func (s *Store) DeleteMedia(url string) bool { if url == "" || !strings.HasPrefix(url, "/media/") { return false } root, err := s.openRoot() if err != nil { return false } return root.Remove(path.Join(MediaDirName, filepath.Base(url))) == nil } // Media is one file in the media library. type Media struct { Name string URL string Size int64 MTime time.Time // Width and Height are the pixel dimensions the image header // carries, or 0 when the header does not yield them. Only a short // prefix of the file is read to learn them. Width int Height int } // headerPrefix is how much of a media file is read to find the headers // that carry the pixel dimensions: the WebP chunks, the AVIF `ispe` box, // or the size attributes of an SVG root element. const headerPrefix = 64 << 10 // ListMedia returns metadata for every file in the media directory, // newest first, with the pixel dimensions the headers carry. func (s *Store) ListMedia() []Media { root, err := s.openRoot() if err != nil { return nil } var names []string for _, entry := range readDirIn(root, MediaDirName) { if !entry.IsDir() { names = append(names, entry.Name()) } } slices.Sort(names) out := make([]Media, 0, len(names)) for _, name := range names { info, ok := statIn(root, MediaDirName, name) if !ok { continue } item := Media{ Name: name, URL: "/media/" + name, Size: info.Size(), MTime: info.ModTime(), } // A header that cannot be read or parsed leaves the dimensions // at zero; the tile simply shows no size then. if handle, err := root.Open(path.Join(MediaDirName, name)); err == nil { head := make([]byte, min(headerPrefix, info.Size())) if n, err := io.ReadFull(handle, head); n > 0 && (err == nil || err == io.ErrUnexpectedEOF) { item.Width, item.Height, _ = imagefile.Dimensions(head[:n]) } handle.Close() } out = append(out, item) } slices.SortStableFunc(out, func(a, b Media) int { return b.MTime.Compare(a.MTime) }) return out }