From 328331768a7faa0fc61cff4e81311ca4726cfda2 Mon Sep 17 00:00:00 2001 From: nxshock Date: Sun, 8 Jan 2023 14:52:16 +0500 Subject: [PATCH] Upload code --- README.md | 22 ++++++ black.png | Bin 0 -> 69 bytes config.go | 30 +++++++ consts.go | 5 ++ go.mod | 16 ++++ go.sum | 19 +++++ handlers.go | 72 +++++++++++++++++ index.html | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++ itemtype.go | 47 +++++++++++ main.go | 58 ++++++++++++++ preview.go | 103 ++++++++++++++++++++++++ template.go | 11 +++ utils.go | 99 +++++++++++++++++++++++ 13 files changed, 703 insertions(+) create mode 100644 README.md create mode 100644 black.png create mode 100644 config.go create mode 100644 consts.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers.go create mode 100644 index.html create mode 100644 itemtype.go create mode 100644 main.go create mode 100644 preview.go create mode 100644 template.go create mode 100644 utils.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..eeef79b --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Gallery + +Simple gallery. + +## Usage + +1. Create config file and set `WorkingDirectory` +2. Start program + + ```sh + gallery + ``` + +## Config structure + +```toml +WorkingDirectory = "/home/user/pictures" # Working directory +Crf = 40 # Preview CRF + # from 1 - maximum quality + # to 63 - minimum preview cache size +ProcessCount = 4 # Preview generator threads +``` diff --git a/black.png b/black.png new file mode 100644 index 0000000000000000000000000000000000000000..cf671d132881ed90f439205faa0b9831b9ca996c GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Asp9}6A}`DJQfB + + + + + Gallery + + + + + + +
    + {{range .}} {{if eq .ItemType 1}} +
  • + {{.Path.Base}}{{end}} {{if eq .ItemType 2}} +
  • {{end}} {{if eq .ItemType 3}} +
  • {{end}} {{end}} +
+ + + + + + + + \ No newline at end of file diff --git a/itemtype.go b/itemtype.go new file mode 100644 index 0000000..c0a4efa --- /dev/null +++ b/itemtype.go @@ -0,0 +1,47 @@ +package main + +import ( + "os" + "path/filepath" + "strings" +) + +type ItemType int + +const ( + Unknown ItemType = iota + Directory + Picture + Video +) + +type Path string + +type Item struct { + ItemType + Path Path +} + +func (p *Path) Base() string { + return filepath.Base(string(*p)) +} + +func getType(path string) (ItemType, error) { + stat, err := os.Stat(path) + if err != nil { + return Unknown, err + } + + if stat.IsDir() { + return Directory, nil + } + + switch strings.ToLower(filepath.Ext(path)) { + case ".jpg", ".jpeg", ".png", ".avif", ".bmp", ".gif": + return Picture, nil + case ".mov", ".mp4", ".avi", ".mkv", ".3gp", ".webm": + return Video, nil + } + + return Unknown, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..971bb1e --- /dev/null +++ b/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "log" + "math/rand" + "net/http" + "os" + "os/signal" + "time" +) + +var ( + previewCache = NewPreviewCache("cache.zkv") + config *Config + semaphore chan struct{} +) + +func init() { + log.SetFlags(0) + rand.Seed(time.Now().Unix()) + + configPath := defaultConfigPath + if len(os.Args) > 1 { + configPath = os.Args[1] + } + + var err error + + config, err = loadConfig(configPath) + if err != nil { + log.Fatalln(err) + } + + err = os.Chdir(config.WorkingDirectory) + if err != nil { + log.Fatalln(err) + } + + semaphore = make(chan struct{}, config.ProcessCount) +} + +func main() { + go func() { + http.HandleFunc("/preview/", previewHandler) + http.HandleFunc("/view/", viewHandler) + http.HandleFunc("/", rootHandler) + log.Fatalln(http.ListenAndServe(":8080", nil)) + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + err := previewCache.Save() + if err != nil { + log.Fatalln(err) + } +} diff --git a/preview.go b/preview.go new file mode 100644 index 0000000..63548e0 --- /dev/null +++ b/preview.go @@ -0,0 +1,103 @@ +package main + +import ( + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/nxshock/zkv" +) + +type PreviewCache struct { + store *zkv.Store +} + +func NewPreviewCache(filePath string) *PreviewCache { + store, err := zkv.Open(filePath) + if err != nil { + log.Fatalln(err) + } + + reviewCache := &PreviewCache{store} + + return reviewCache +} + +func (pc *PreviewCache) Add(filePath string) ([]byte, error) { + defer func() { + <-semaphore + }() + semaphore <- struct{}{} + + tempFileName, err := TempFileName("preview*.avif") + if err != nil { + return nil, err + } + defer os.Remove(tempFileName) + + stat, err := os.Stat(filePath) + if err != nil { + return nil, err + } + + var cmd *exec.Cmd + if stat.IsDir() { // https://trac.ffmpeg.org/wiki/Create%20a%20mosaic%20out%20of%20several%20input%20videos + fileNames, err := getRandomFiles(filePath, 4) + if err != nil { + return nil, err + } + + cmd = exec.Command("ffmpeg", + "-i", fileNames[0], + "-i", fileNames[1], + "-i", fileNames[2], + "-i", fileNames[3], + "-filter_complex", "nullsrc=size=240x240 [base];[0:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [upperleft];[1:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [upperright];[2:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [lowerleft];[3:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [lowerright];[base][upperleft] overlay=shortest=1 [tmp1];[tmp1][upperright] overlay=shortest=1:x=120 [tmp2];[tmp2][lowerleft] overlay=shortest=1:y=120 [tmp3];[tmp3][lowerright] overlay=shortest=1:x=120:y=120", + "-frames:v", "1", + "-crf", strconv.FormatUint(config.Crf, 10), + "-f", "avif", + tempFileName) + } else { + cmd = exec.Command("ffmpeg.exe", + "-i", filepath.FromSlash(filePath), + "-vf", "scale=240:240:force_original_aspect_ratio=increase,crop=240:240:exact=1", + "-frames:v", "1", + "-crf", "40", + "-f", "avif", + tempFileName) + } + + err = cmd.Run() + if err != nil { + return nil, err + } + + b, err := os.ReadFile(tempFileName) + if err != nil { + return nil, err + } + + err = pc.store.Set(filePath, b) + if err != nil { + return nil, err + } + + return b, nil +} + +func (pc *PreviewCache) Read(filePath string) ([]byte, error) { + var b []byte + + err := pc.store.Get(filePath, &b) + if err != nil { + return pc.Add(filePath) + } + + return b, nil +} + +func (pc *PreviewCache) Save() error { + return pc.store.Close() +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..b7f1835 --- /dev/null +++ b/template.go @@ -0,0 +1,11 @@ +package main + +import ( + _ "embed" + "html/template" +) + +//go:embed index.html +var mainTemplateStr string + +var t = template.Must(template.New("main").Parse(mainTemplateStr)) diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..229acda --- /dev/null +++ b/utils.go @@ -0,0 +1,99 @@ +package main + +import ( + "errors" + "math/rand" + "os" + "path/filepath" + "strconv" + "time" +) + +var errPatternHasSeparator = errors.New("pattern contains path separator") + +func TempFileName(pattern string) (string, error) { + dir := os.TempDir() + + prefix, suffix, err := prefixAndSuffix(pattern) + if err != nil { + return "", &os.PathError{Op: "createtemp", Path: pattern, Err: err} + } + prefix = joinPath(dir, prefix) + + try := 0 + for { + name := prefix + nextRandom() + suffix + if exists, _ := IsFileExists(name); exists { + if try++; try < 10000 { + continue + } + return "", &os.PathError{Op: "createtemp", Path: prefix + "*" + suffix, Err: os.ErrExist} + } + return name, err + } +} + +func IsFileExists(filePath string) (bool, error) { + if _, err := os.Stat(filePath); err == nil { + return true, nil + } else if errors.Is(err, os.ErrNotExist) { + return false, nil + + } else { + return false, err + } +} + +func prefixAndSuffix(pattern string) (prefix, suffix string, err error) { + for i := 0; i < len(pattern); i++ { + if os.IsPathSeparator(pattern[i]) { + return "", "", errPatternHasSeparator + } + } + if pos := lastIndex(pattern, '*'); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return prefix, suffix, nil +} + +func joinPath(dir, name string) string { + if len(dir) > 0 && os.IsPathSeparator(dir[len(dir)-1]) { + return dir + name + } + return dir + string(os.PathSeparator) + name +} + +func nextRandom() string { + + return strconv.Itoa(int(rand.Uint64())) +} + +func lastIndex(s string, sep byte) int { + for i := len(s) - 1; i >= 0; i-- { + if s[i] == sep { + return i + } + } + return -1 +} + +func getRandomFiles(path string, numberOfFiles int) ([]string, error) { // TODO: оптимизировать алгоритм и предусмотреть папки с менее 4 шт. файлов + fileNames, err := filepath.Glob(path + "/*") + if err != nil { + return nil, err + } + + rand.Seed(time.Now().UnixNano()) + for i := len(fileNames) - 1; i > 0; i-- { + j := rand.Intn(i + 1) + fileNames[i], fileNames[j] = fileNames[j], fileNames[i] + } + + for len(fileNames) < 4 { + fileNames = append(fileNames, "black.png") + } + + return fileNames[:4], nil +}