Skip to content
Snippets Groups Projects
Select Git revision
  • 8c05fe4351cda23e97dd3c016c2d9122a7215e9f
  • master default protected
  • v1.4.5
  • v1.4.4
  • v1.4.3
  • v1.4.2
  • v1.4.1
  • v1.4.0
  • v1.3.10
  • v1.3.9
  • v1.3.8
  • v1.3.7
  • v1.3.6
  • v1.3.5
  • v1.3.4
  • v1.3.3
  • v1.3.2
  • v1.3.1
  • v1.3.0
  • v1.2.9
  • v1.2.8
  • v1.2.7
22 results

image.go

Blame
  • 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
    	}
    }