Select Git revision
image.go 12.91 KiB
package filesystem
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"log"
"os"
"random.chars.jp/git/image-board/v2/store"
)
// ImageHashes returns a slice of image hashes.
func (s *Store) ImageHashes() ([]string, error) {
var images []string
if entries, err := os.ReadDir(s.ImagesHashDir()); err != nil {
return nil, err
} else {
for _, entry := range entries {
if entry.IsDir() {
var subEntries []os.DirEntry
if subEntries, err = os.ReadDir(s.ImagesHashDir() + "/" + entry.Name()); err != nil {
return nil, err
} else {
for _, subEntry := range subEntries {
images = append(images, entry.Name()+subEntry.Name())
}
}
}
}
}
return images, nil
}
// ImageHash returns an image with specific hash.
func (s *Store) ImageHash(hash string) (*store.Image, error) {
if !store.MatchSha256(hash) {
return nil, store.ErrInvalidInput
} else if !s.file(s.ImageMetadataPath(hash)) {
return nil, store.ErrNoEntry
}
s.getLock(hash).RLock()
defer s.getLock(hash).RUnlock()
return s.imageMetadataRead(s.ImageMetadataPath(hash))
}
// imageMetadataRead reads an image metadata file.
func (s *Store) imageMetadataRead(path string) (*store.Image, error) {
var metadata store.Image
if payload, err := os.ReadFile(path); err != nil {
if os.IsNotExist(err) {
return nil, store.ErrNoEntry
}
return nil, err
} else {
if err = json.Unmarshal(payload, &metadata); err != nil {
return nil, err
}
}
return &metadata, nil
}
// ImageData returns an image and its data with a specific hash.
func (s *Store) ImageData(hash string, preview bool) (*store.Image, []byte, error) {
if !store.MatchSha256(hash) {
return nil, nil, store.ErrInvalidInput
} else if !s.file(s.ImageMetadataPath(hash)) {
return nil, nil, store.ErrNoEntry
}
s.getLock(hash).RLock()
defer s.getLock(hash).RUnlock()
var metadata *store.Image
if m, err := s.imageMetadataRead(s.ImageMetadataPath(hash)); err != nil {
return nil, nil, err
} else {
metadata = m
}
var path string
if !preview {
path = s.ImageFilePath(hash)
} else {
path = s.ImagePreviewFilePath(hash)
}
if data, err := os.ReadFile(path); err != nil {
return nil, nil, err
} else {
return metadata, data, nil
}
}
// ImageTags returns tags of an image with specific flake.
func (s *Store) ImageTags(flake string) ([]string, error) {
if !store.Numerical(flake) {
return nil, store.ErrInvalidInput
} else if !s.dir(s.ImageTagsPath(flake)) {
return nil, store.ErrNoEntry
}
// Lock flake for directory-based operations
s.getLock(flake).RLock()
defer s.getLock(flake).RUnlock()
return s.imageTags(flake)
}
func (s *Store) imageTags(flake string) ([]string, error) {
var tags []string
if entries, err := os.ReadDir(s.ImageTagsPath(flake)); err != nil {
return nil, err
} else {
for _, entry := range entries {
tags = append(tags, entry.Name())
}
}
return tags, nil
}
// ImageHasTag figures out if an image has a tag.
func (s *Store) ImageHasTag(flake, tag string) (bool, error) {
if !store.Numerical(flake) {
return false, store.ErrInvalidInput
} else if !store.MatchName(tag) {
return false, store.ErrNoEntry
}
return s.file(s.ImageTagsPath(flake) + "/" + tag), nil
}
//ImageSearch searches for images with specific tags.
func (s *Store) ImageSearch(tags []string) ([]string, error) {
if len(tags) < 1 || tags == nil {
return nil, store.ErrInvalidInput
}
// Check if every tag matches name regex and exists
for _, tag := range tags {
if !store.MatchName(tag) {
return nil, store.ErrInvalidInput
} else if !s.file(s.TagPath(tag)) {
return nil, store.ErrNoEntry
}
}
// Return if there's only one tag to search for
if len(tags) == 1 {
return s.TagImages(tags[0])
}
// Find entry with the least pages
entry := struct {
min uint64
index int
}{}
entry.index = 0
if pt, err := s.PageTotal("tag_" + tags[0]); err != nil {
return nil, err
} else {
entry.min = pt
}
for i := 1; i < len(tags); i++ {
if entry.min <= 1 {
break
}
if pages, err := s.PageTotal("tag_" + tags[i]); err != nil {
return nil, err
} else if pages < entry.min {
entry.min = pages
entry.index = i
}
}
// Get initial tag
var initial []string
if init, err := s.TagImages(tags[entry.index]); err != nil {
return nil, err
} else {
initial = init
}
// Result slice
var result []string
// Walk flakes from initial tag
for _, flake := range initial {
match := true
// Walk all remaining tags
for i, tag := range tags {
// Skip the entrypoint entry
if i == entry.index {
continue
}
// Check if match
if b, err := s.ImageHasTag(flake, tag); err != nil {
return nil, err
} else if !b {
match = false
break
}
}
// Append flake if all tags matched
if match {
result = append(result, flake)
}
}
return result, nil
}
// ImageAdd adds an image to the store.
func (s *Store) ImageAdd(data []byte, flake string) (*store.Image, error) {
if !store.Numerical(flake) {
return nil, store.ErrInvalidInput
} else if !s.dir(s.UserPath(flake)) {
return nil, store.ErrNoEntry
}
info := store.Image{Snowflake: store.MakeFlake(store.ImageNode).String(), User: flake}
info.Hash = fmt.Sprintf("%x", sha256.Sum256(data))
if s.file(s.ImagePath(info.Hash)) {
return nil, store.ErrAlreadyExists
}
s.getLock(info.Hash).Lock()
defer s.getLock(info.Hash).Unlock()
var prev image.Image
if i, format, err := image.Decode(bytes.NewReader(data)); err != nil {
return nil, err
} else {
prev = store.MakePreview(i)
info.Type = format
}
if err := os.MkdirAll(s.ImageHashTagsPath(info.Hash), s.PermissionDir); err != nil {
return nil, err
}
if payload, err := json.Marshal(info); err != nil {
return nil, err
} else if err = os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile); err != nil {
return nil, err
}
if err := os.WriteFile(s.ImageFilePath(info.Hash), data, s.PermissionFile); err != nil {
return nil, err
}
if preview, err := os.Create(s.ImagePreviewFilePath(info.Hash)); err != nil {
return nil, err
} else if err = jpeg.Encode(preview, prev, &jpeg.Options{Quality: 100}); err != nil {
return nil, err
} else if err = preview.Close(); err != nil {
return nil, err
}
if err := s.link("../hashes/"+s.ImageHashSplit(info.Hash), s.ImageSnowflakePath(info.Snowflake)); err != nil {
return nil, err
}
if err := s.link("../../../images/hashes/"+s.ImageHashSplit(info.Hash), s.UserImagesPath(flake)+"/"+info.Snowflake); err != nil {
return nil, err
}
if err := s.pageInsert(store.ImageRootPageVariant, info.Snowflake); err != nil {
return nil, err
}
log.Printf("image hash %s snowflake %s type %s added by user %s", info.Hash, info.Snowflake, info.Type, info.User)
return &info, nil
}
// ImageUpdate updates image metadata.
func (s *Store) ImageUpdate(flake, source, parent, commentary, commentaryTranslation string) error {
if len(source) >= 1024 ||
len(commentary) >= 65536 || len(commentaryTranslation) >= 65536 {
return store.ErrInvalidInput
}
var info *store.Image
if i, err := s.Image(flake); err != nil {
return err
} else {
info = i
}
s.getLock(info.Hash).Lock()
defer s.getLock(info.Hash).Unlock()
var msg string
if source != "\000" && store.MatchURL(source) {
info.Source = source
msg += "source"
}
if parent != "\000" && parent != info.Snowflake && parent != info.Parent {
var p *store.Image
if parent == "" {
if par, err := s.Image(info.Parent); err != nil {
return err
} else {
p = par
}
p.Child = ""
} else {
if par, err := s.Image(parent); err != nil {
return err
} else {
p = par
}
if p.Child != "" {
goto end
}
p.Child = info.Snowflake
}
info.Parent = parent
s.getLock(p.Hash).Lock()
if err := s.imageMetadataWrite(p); err != nil {
return err
}
s.getLock(p.Hash).Unlock()
if msg != "" {
msg += ", "
}
msg += "parent " + parent
end:
}
if commentary != "\000" {
info.Commentary = commentary
if msg != "" {
msg += ", "
}
msg += "commentary"
}
if commentaryTranslation != "\000" {
info.CommentaryTranslation = commentaryTranslation
if msg != "" {
msg += ", "
}
msg += "commentary translation"
}
if msg != "" {
if err := s.imageMetadataWrite(info); err != nil {
return err
} else {
log.Printf("image %s %s updated", info.Snowflake, msg)
return nil
}
}
return nil
}
func (s *Store) imageMetadataWrite(info *store.Image) error {
if payload, err := json.Marshal(info); err != nil {
return err
} else {
return os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile)
}
}
// Images returns a slice of image snowflakes.
func (s *Store) Images() ([]string, error) {
var snowflakes []string
if entries, err := os.ReadDir(s.ImagesSnowflakeDir()); err != nil {
return nil, err
} else {
for _, entry := range entries {
snowflakes = append(snowflakes, entry.Name())
}
}
return snowflakes, nil
}
// ImageSnowflakeHash returns image hash from snowflake.
func (s *Store) ImageSnowflakeHash(flake string) (string, error) {
if !store.Numerical(flake) {
return "", store.ErrInvalidInput
}
if !s.Compat {
if img, err := s.imageMetadataRead(s.ImageSnowflakePath(flake) + "/" + infoJson); err != nil {
return "", err
} else {
return img.Hash, nil
}
} else {
if path, err := os.ReadFile(s.ImageSnowflakePath(flake)); err != nil {
if os.IsNotExist(err) {
return "", store.ErrNoEntry
}
return "", err
} else {
var img *store.Image
if img, err = s.imageMetadataRead(string(path) + "/" + infoJson); err != nil {
return "", err
} else {
return img.Hash, nil
}
}
}
}
// Image returns image that has specific snowflake.
func (s *Store) Image(flake string) (*store.Image, error) {
if hash, err := s.ImageSnowflakeHash(flake); err != nil {
return nil, err
} else {
return s.ImageHash(hash)
}
}
// ImageDestroy destroys an image.
func (s *Store) ImageDestroy(flake string) error {
if !store.Numerical(flake) {
return store.ErrInvalidInput
} else if !s.dir(s.ImageSnowflakePath(flake)) {
return store.ErrNoEntry
}
var hash string
if h, err := s.ImageSnowflakeHash(flake); err != nil {
return err
} else {
hash = h
}
// Attempt to disassociate parent
if err := s.ImageUpdate(flake, "\000", "", "\000", "\000"); err != nil {
return err
}
s.getLock(hash).Lock()
defer s.getLock(hash).Unlock()
var info *store.Image
if i, err := s.imageMetadataRead(s.ImageMetadataPath(hash)); err != nil {
return err
} else {
info = i
}
// Disassociate child if set
if info.Child != "" {
if err := s.ImageUpdate(info.Child, "\000", "", "\000", "\000"); err != nil {
return err
}
}
// Untag the image completely
if tags, err := s.imageTags(info.Snowflake); err != nil {
return err
} else {
for _, tag := range tags {
if err = s.imageTagRemove(info.Snowflake, tag); err != nil {
return err
}
}
}
if err := os.Remove(s.ImageSnowflakePath(info.Snowflake)); err != nil {
return err
}
if err := os.Remove(s.UserImagesPath(info.User) + "/" + info.Snowflake); err != nil {
return err
}
if err := os.RemoveAll(s.ImagePath(hash)); err != nil {
return err
}
if err := s.pageRegisterRemove(store.ImageRootPageVariant, info.Snowflake); err != nil {
return err
}
log.Printf("image hash %s snowflake %s destroyed", info.Hash, info.Snowflake)
return nil
}
// ImageTagAdd adds a tag to an image with specific snowflake.
func (s *Store) ImageTagAdd(flake, tag string) error {
if !store.MatchName(tag) || !store.Numerical(flake) {
return store.ErrInvalidInput
} else if !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) || s.file(s.TagPath(tag)+"/"+flake) {
return store.ErrNoEntry
}
s.getLock(flake).Lock()
defer s.getLock(flake).Unlock()
if err := s.link("../../images/snowflakes/"+flake, s.TagPath(tag)+"/"+flake); err != nil {
return err
}
if err := s.link("../../../../tags/"+tag, s.ImageSnowflakePath(flake)+"/tags/"+tag); err != nil {
return err
}
if err := s.pageInsert("tag_"+tag, flake); err != nil {
return err
}
log.Printf("image snowflake %s tagged with %s", flake, tag)
return nil
}
// ImageTagRemove removes a tag from an image with specific snowflake.
func (s *Store) ImageTagRemove(flake, tag string) error {
if !store.MatchName(tag) || !store.Numerical(flake) {
return store.ErrInvalidInput
} else if !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) {
return store.ErrNoEntry
}
s.getLock(flake).Lock()
defer s.getLock(flake).Unlock()
return s.imageTagRemove(flake, tag)
}
func (s *Store) imageTagRemove(flake, tag string) error {
if s.file(s.ImageTagsPath(flake) + "/" + tag) {
if err := os.Remove(s.ImageTagsPath(flake) + "/" + tag); err != nil {
return err
}
}
if s.file(s.TagPath(tag) + "/" + flake) {
if err := os.Remove(s.TagPath(tag) + "/" + flake); err != nil {
return err
}
}
if err := s.pageRegisterRemove("tag_"+tag, flake); err != nil {
return err
} else {
log.Printf("image snowflake %s untagged %s", flake, tag)
return nil
}
}