Browse Source

Move image thumbnails generation to background

Vova Tkach 5 years ago
parent
commit
506a397163

+ 1 - 0
assets/assets.go

@@ -7,6 +7,7 @@ import (
 )
 
 func PopulateResources(res *resource.Resource) {
+	res.Add(consts.AssetsCpImgLoadGif, "image/gif", CpImgLoadGif)
 	res.Add(consts.AssetsCpCodeMirrorCss, "text/css", CpCodeMirrorCss)
 	res.Add(consts.AssetsCpCodeMirrorJs, "application/javascript; charset=utf-8", CpCodeMirrorJs)
 	res.Add(consts.AssetsCpStylesCss, "text/css", CpStylesCss)

BIN
assets/cp.img.load.gif


File diff suppressed because it is too large
+ 2 - 0
assets/cp.img.load.gif.go


+ 9 - 1
assets/cp.scripts.js

@@ -529,8 +529,8 @@
 		'>': '>',
 		'"': '"',
 		"'": ''',
-		'`': '`'
 	};
+	escapeMap[String.fromCharCode(96)] = '`';
 
 	// Functions for escaping and unescaping strings to/from HTML interpolation.
 	var createEscaper = function (map) {
@@ -3938,6 +3938,14 @@
 				});
 			},
 
+			ShopProductsRetryImage: function(img) {
+				var original = $(img).attr('src');
+				$(img).attr('src', '/assets/cp/img-load.gif');
+				setTimeout(function() {
+					$(img).attr('src', original);
+				}, 1000);
+			},
+
 			ActionLogout: function(message) {
 				if(confirm(message)) {
 					$.ajax({

File diff suppressed because it is too large
+ 0 - 0
assets/cp.scripts.js.go


+ 5 - 0
assets/cp.styles.css

@@ -995,6 +995,11 @@ ul.pagination {
 	border-radius: .25rem;
 }
 
+#list-images .attached-img a img {
+	width: 100px;
+	height: 100px;
+}
+
 #upload-msg {
 	position: absolute;
 	background: white;

File diff suppressed because it is too large
+ 0 - 0
assets/cp.styles.css.go


+ 2 - 1
consts/consts.go

@@ -5,7 +5,7 @@ import (
 )
 
 const AssetsPath = "assets"
-const AssetsVersion = "44"
+const AssetsVersion = "45"
 const DirIndexFile = "index.html"
 
 // Bootstrap resources
@@ -15,6 +15,7 @@ const AssetsJqueryJs = AssetsPath + "/jquery.js"
 const AssetsPopperJs = AssetsPath + "/popper.js"
 
 // System resources
+const AssetsCpImgLoadGif = AssetsPath + "/cp/img-load.gif"
 const AssetsCpScriptsJs = AssetsPath + "/cp/scripts.js"
 const AssetsCpStylesCss = AssetsPath + "/cp/styles.css"
 const AssetsSysBgPng = AssetsPath + "/sys/bg.png"

+ 4 - 4
engine/wrapper/config.go → engine/wrapper/config/config.go

@@ -1,4 +1,4 @@
-package wrapper
+package config
 
 import (
 	"encoding/json"
@@ -38,7 +38,7 @@ type Config struct {
 	}
 }
 
-func configNew() *Config {
+func ConfigNew() *Config {
 	c := &Config{}
 	c.configDefault()
 	return c
@@ -79,7 +79,7 @@ func (this *Config) configDefault() {
 	this.API.XML.Url = ""
 }
 
-func (this *Config) configRead(file string) error {
+func (this *Config) ConfigRead(file string) error {
 	f, err := os.Open(file)
 	if err != nil {
 		return err
@@ -90,7 +90,7 @@ func (this *Config) configRead(file string) error {
 	return dec.Decode(this)
 }
 
-func (this *Config) configWrite(file string) error {
+func (this *Config) ConfigWrite(file string) error {
 	r, err := json.Marshal(this)
 	if err != nil {
 		return err

+ 5 - 4
engine/wrapper/wrapper.go

@@ -16,6 +16,7 @@ import (
 	"golang-fave/consts"
 	"golang-fave/engine/mysqlpool"
 	"golang-fave/engine/sqlw"
+	"golang-fave/engine/wrapper/config"
 	"golang-fave/logger"
 	"golang-fave/utils"
 
@@ -49,7 +50,7 @@ type Wrapper struct {
 	CurrModule      string
 	CurrSubModule   string
 	MSPool          *mysqlpool.MySqlPool
-	Config          *Config
+	Config          *config.Config
 
 	DB   *sqlw.DB
 	User *utils.MySql_user
@@ -57,8 +58,8 @@ type Wrapper struct {
 
 func New(l *logger.Logger, w http.ResponseWriter, r *http.Request, s *session.Session, c *cblocks.CacheBlocks, host, port, chost, dirConfig, dirHtdocs, dirLogs, dirTemplate, dirTmp string, mp *mysqlpool.MySqlPool) *Wrapper {
 
-	conf := configNew()
-	if err := conf.configRead(dirConfig + string(os.PathSeparator) + "config.json"); err != nil {
+	conf := config.ConfigNew()
+	if err := conf.ConfigRead(dirConfig + string(os.PathSeparator) + "config.json"); err != nil {
 		l.Log("Host config file: %s", r, true, err.Error())
 	}
 
@@ -270,7 +271,7 @@ func (this *Wrapper) GetCurrentPage(max int) int {
 }
 
 func (this *Wrapper) ConfigSave() error {
-	return this.Config.configWrite(this.DConfig + string(os.PathSeparator) + "config.json")
+	return this.Config.ConfigWrite(this.DConfig + string(os.PathSeparator) + "config.json")
 }
 
 func (this *Wrapper) RemoveProductImageThumbnails(product_id, filename string) error {

+ 154 - 0
image.go

@@ -0,0 +1,154 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"golang-fave/engine/wrapper/config"
+	"golang-fave/utils"
+
+	"github.com/disintegration/imaging"
+)
+
+func image_generate(width, height int, resize bool, fsrc, fdst string) {
+	src, err := imaging.Open(fsrc)
+	if err == nil {
+		if !resize {
+			src = imaging.Fill(src, width, height, imaging.Center, imaging.Lanczos)
+		} else {
+			src = imaging.Fit(src, width, height, imaging.Lanczos)
+		}
+		if err := imaging.Save(src, fdst); err != nil {
+			fmt.Printf("Image generation error: %v\n", err)
+		}
+	}
+}
+
+func image_create(www, src, dst, typ string, conf *config.Config) {
+	width := (*conf).Shop.Thumbnails.Thumbnail0[0]
+	height := (*conf).Shop.Thumbnails.Thumbnail0[1]
+	resize := false
+
+	if typ == "thumb-1" {
+		width = (*conf).Shop.Thumbnails.Thumbnail1[0]
+		height = (*conf).Shop.Thumbnails.Thumbnail1[1]
+		if (*conf).Shop.Thumbnails.Thumbnail1[2] == 1 {
+			resize = true
+		}
+	} else if typ == "thumb-2" {
+		width = (*conf).Shop.Thumbnails.Thumbnail2[0]
+		height = (*conf).Shop.Thumbnails.Thumbnail2[1]
+		if (*conf).Shop.Thumbnails.Thumbnail2[2] == 1 {
+			resize = true
+		}
+	} else if typ == "thumb-3" {
+		width = (*conf).Shop.Thumbnails.Thumbnail3[0]
+		height = (*conf).Shop.Thumbnails.Thumbnail3[1]
+		if (*conf).Shop.Thumbnails.Thumbnail3[2] == 1 {
+			resize = true
+		}
+	} else if typ == "thumb-full" {
+		width = (*conf).Shop.Thumbnails.ThumbnailFull[0]
+		height = (*conf).Shop.Thumbnails.ThumbnailFull[1]
+		if (*conf).Shop.Thumbnails.ThumbnailFull[2] == 1 {
+			resize = true
+		}
+	}
+
+	image_generate(width, height, resize, src, dst)
+}
+
+func image_detect(www, file string, conf *config.Config) {
+	index := strings.LastIndex(file, string(os.PathSeparator))
+	if index != -1 {
+		file_name := file[index+1:]
+		if !strings.HasPrefix(file_name, "thumb-") {
+			file_thumb_0 := file[:index+1] + "thumb-0-" + file_name
+			file_thumb_1 := file[:index+1] + "thumb-1-" + file_name
+			file_thumb_2 := file[:index+1] + "thumb-2-" + file_name
+			file_thumb_3 := file[:index+1] + "thumb-3-" + file_name
+			file_thumb_full := file[:index+1] + "thumb-full-" + file_name
+			if !utils.IsFileExists(file_thumb_0) {
+				image_create(www, file, file_thumb_0, "thumb-0", conf)
+			}
+			if !utils.IsFileExists(file_thumb_1) {
+				image_create(www, file, file_thumb_1, "thumb-1", conf)
+			}
+			if !utils.IsFileExists(file_thumb_2) {
+				image_create(www, file, file_thumb_2, "thumb-2", conf)
+			}
+			if !utils.IsFileExists(file_thumb_3) {
+				image_create(www, file, file_thumb_3, "thumb-3", conf)
+			}
+			if !utils.IsFileExists(file_thumb_full) {
+				image_create(www, file, file_thumb_full, "thumb-full", conf)
+			}
+		}
+	}
+}
+
+func image_loop(www_dir string, stop chan bool) {
+	dirs, err := ioutil.ReadDir(www_dir)
+	if err == nil {
+		for _, dir := range dirs {
+			target_dir := strings.Join([]string{www_dir, dir.Name(), "htdocs", "products", "images"}, string(os.PathSeparator))
+			if utils.IsDirExists(target_dir) {
+				cfile := strings.Join([]string{www_dir, dir.Name(), "config", "config.json"}, string(os.PathSeparator))
+				conf := config.ConfigNew()
+				if err := conf.ConfigRead(cfile); err == nil {
+					pattern := target_dir + string(os.PathSeparator) + "*" + string(os.PathSeparator) + "*.*"
+					if files, err := filepath.Glob(pattern); err == nil {
+						for _, file := range files {
+							select {
+							case <-stop:
+								break
+							default:
+								image_detect(www_dir, file, conf)
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
+func image_start(www_dir string) (chan bool, chan bool) {
+	ch := make(chan bool)
+	stop := make(chan bool)
+
+	// Run at start
+	image_loop(www_dir, stop)
+
+	go func() {
+		for {
+			select {
+			case <-time.After(1 * time.Second):
+				// Run every second
+				image_loop(www_dir, stop)
+			case <-ch:
+				ch <- true
+				return
+			}
+		}
+	}()
+	return ch, stop
+}
+
+func image_stop(ch, stop chan bool) {
+	for {
+		select {
+		case stop <- true:
+		case ch <- true:
+			<-ch
+			return
+		case <-time.After(8 * time.Second):
+			fmt.Println("Image error: force exit by timeout after 8 seconds")
+			return
+		}
+	}
+}

+ 4 - 0
main.go

@@ -85,6 +85,10 @@ func main() {
 	sess_cl_ch, sess_cl_stop := session_clean_start(consts.ParamWwwDir)
 	defer session_clean_stop(sess_cl_ch, sess_cl_stop)
 
+	// Image processing
+	imgs_cl_ch, imgs_cl_stop := image_start(consts.ParamWwwDir)
+	defer image_stop(imgs_cl_ch, imgs_cl_stop)
+
 	// Init mounted resources
 	res := resource.New()
 	assets.PopulateResources(res)

+ 0 - 113
modules/module_api.go

@@ -3,7 +3,6 @@ package modules
 import (
 	"net/http"
 	"os"
-	"strings"
 
 	"golang-fave/assets"
 	"golang-fave/engine/fetdata"
@@ -82,115 +81,3 @@ func (this *Modules) RegisterModule_Api() *Module {
 		return "", "", ""
 	})
 }
-
-func (this *Modules) RegisterModule_ApiProducts() *Module {
-	return this.newModule(MInfo{
-		WantDB: true,
-		Mount:  "products",
-		Name:   "Api Products",
-		Order:  804,
-		System: true,
-		Icon:   assets.SysSvgIconPage,
-		Sub:    &[]MISub{},
-	}, func(wrap *wrapper.Wrapper) {
-		if len(wrap.UrlArgs) == 4 && wrap.UrlArgs[0] == "products" && wrap.UrlArgs[1] == "images" && utils.IsNumeric(wrap.UrlArgs[2]) && wrap.UrlArgs[3] != "" {
-			thumb_type := ""
-			file_name := ""
-
-			if strings.HasPrefix(wrap.UrlArgs[3], "thumb-0-") {
-				thumb_type = "thumb-0"
-				file_name = wrap.UrlArgs[3][len(thumb_type)+1:]
-			} else if strings.HasPrefix(wrap.UrlArgs[3], "thumb-1-") {
-				thumb_type = "thumb-1"
-				file_name = wrap.UrlArgs[3][len(thumb_type)+1:]
-			} else if strings.HasPrefix(wrap.UrlArgs[3], "thumb-2-") {
-				thumb_type = "thumb-2"
-				file_name = wrap.UrlArgs[3][len(thumb_type)+1:]
-			} else if strings.HasPrefix(wrap.UrlArgs[3], "thumb-3-") {
-				thumb_type = "thumb-3"
-				file_name = wrap.UrlArgs[3][len(thumb_type)+1:]
-			} else if strings.HasPrefix(wrap.UrlArgs[3], "thumb-full-") {
-				thumb_type = "thumb-full"
-				file_name = wrap.UrlArgs[3][len(thumb_type)+1:]
-			}
-
-			if !(thumb_type == "" && file_name == "") {
-				original_file := wrap.DHtdocs + string(os.PathSeparator) + "products" + string(os.PathSeparator) + "images" + string(os.PathSeparator) + wrap.UrlArgs[2] + string(os.PathSeparator) + file_name
-				if !utils.IsFileExists(original_file) {
-					// User error 404 page
-					wrap.RenderFrontEnd("404", fetdata.New(wrap, true, nil, nil), http.StatusNotFound)
-					return
-				}
-
-				width := (*wrap.Config).Shop.Thumbnails.Thumbnail0[0]
-				height := (*wrap.Config).Shop.Thumbnails.Thumbnail0[1]
-				resize := false
-
-				if thumb_type == "thumb-1" {
-					width = (*wrap.Config).Shop.Thumbnails.Thumbnail1[0]
-					height = (*wrap.Config).Shop.Thumbnails.Thumbnail1[1]
-					if (*wrap.Config).Shop.Thumbnails.Thumbnail1[2] == 1 {
-						resize = true
-					}
-				} else if thumb_type == "thumb-2" {
-					width = (*wrap.Config).Shop.Thumbnails.Thumbnail2[0]
-					height = (*wrap.Config).Shop.Thumbnails.Thumbnail2[1]
-					if (*wrap.Config).Shop.Thumbnails.Thumbnail2[2] == 1 {
-						resize = true
-					}
-				} else if thumb_type == "thumb-3" {
-					width = (*wrap.Config).Shop.Thumbnails.Thumbnail3[0]
-					height = (*wrap.Config).Shop.Thumbnails.Thumbnail3[1]
-					if (*wrap.Config).Shop.Thumbnails.Thumbnail3[2] == 1 {
-						resize = true
-					}
-				} else if thumb_type == "thumb-full" {
-					width = (*wrap.Config).Shop.Thumbnails.ThumbnailFull[0]
-					height = (*wrap.Config).Shop.Thumbnails.ThumbnailFull[1]
-					if (*wrap.Config).Shop.Thumbnails.ThumbnailFull[2] == 1 {
-						resize = true
-					}
-				}
-
-				target_file := wrap.DHtdocs + string(os.PathSeparator) + "products" + string(os.PathSeparator) + "images" + string(os.PathSeparator) + wrap.UrlArgs[2] + string(os.PathSeparator) + thumb_type + "-" + file_name
-				if !utils.IsFileExists(target_file) {
-					data, ok, ext, err := this.api_GenerateImage(wrap, width, height, resize, original_file)
-					if err != nil {
-						// System error 500
-						utils.SystemErrorPageEngine(wrap.W, err)
-						return
-					}
-
-					if !ok {
-						// User error 404 page
-						wrap.RenderFrontEnd("404", fetdata.New(wrap, true, nil, nil), http.StatusNotFound)
-						return
-					}
-
-					// Save file
-					if file, err := os.Create(target_file); err == nil {
-						file.Write(data)
-						file.Close()
-					}
-
-					wrap.W.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
-					wrap.W.Header().Set("Content-Type", ext)
-					wrap.W.Write(data)
-				} else {
-					http.ServeFile(wrap.W, wrap.R, target_file)
-				}
-			} else {
-				// User error 404 page
-				wrap.RenderFrontEnd("404", fetdata.New(wrap, true, nil, nil), http.StatusNotFound)
-				return
-			}
-		} else {
-			// User error 404 page
-			wrap.RenderFrontEnd("404", fetdata.New(wrap, true, nil, nil), http.StatusNotFound)
-			return
-		}
-	}, func(wrap *wrapper.Wrapper) (string, string, string) {
-		// No any page for back-end
-		return "", "", ""
-	})
-}

+ 0 - 52
modules/module_api_images.go

@@ -1,52 +0,0 @@
-package modules
-
-import (
-	"bufio"
-	"bytes"
-	"path/filepath"
-	"strings"
-
-	"golang-fave/engine/wrapper"
-
-	"github.com/disintegration/imaging"
-)
-
-func (this *Modules) api_GenerateImage(wrap *wrapper.Wrapper, width, height int, resize bool, filename string) ([]byte, bool, string, error) {
-	file_ext := ""
-	if strings.ToLower(filepath.Ext(filename)) == ".png" {
-		file_ext = "image/png"
-	} else if strings.ToLower(filepath.Ext(filename)) == ".jpg" {
-		file_ext = "image/jpeg"
-	} else if strings.ToLower(filepath.Ext(filename)) == ".jpeg" {
-		file_ext = "image/jpeg"
-	}
-
-	src, err := imaging.Open(filename)
-	if err != nil {
-		return []byte(""), false, file_ext, err
-	}
-
-	if !resize {
-		src = imaging.Fill(src, width, height, imaging.Center, imaging.Lanczos)
-	} else {
-		src = imaging.Fit(src, width, height, imaging.Lanczos)
-	}
-
-	var out_bytes bytes.Buffer
-	out := bufio.NewWriter(&out_bytes)
-
-	defer func() {
-		out.Flush()
-		out_bytes.Reset()
-	}()
-
-	if file_ext == "image/png" {
-		imaging.Encode(out, src, imaging.PNG)
-	} else if file_ext == "image/jpeg" {
-		imaging.Encode(out, src, imaging.JPEG)
-	} else {
-		return []byte(""), false, file_ext, nil
-	}
-
-	return out_bytes.Bytes(), true, file_ext, nil
-}

+ 1 - 1
modules/module_shop.go

@@ -241,7 +241,7 @@ func (this *Modules) shop_GetAllProductImages(wrap *wrapper.Wrapper, product_id
 		for rows.Next() {
 			err = rows.Scan(scan...)
 			if err == nil {
-				result += `<div class="attached-img"><a href="/products/images/` + html.EscapeString(string(values[0])) + `/` + html.EscapeString(string(values[1])) + `" title="` + html.EscapeString(string(values[1])) + `" target="_blank"><img src="/products/images/` + string(values[0]) + `/thumb-0-` + string(values[1]) + `" /></a>, <a href="javascript:fave.ShopProductsDeleteImage(this, ` + html.EscapeString(string(values[0])) + `, '` + html.EscapeString(string(values[1])) + `');">Delete</a></div>`
+				result += `<div class="attached-img"><a href="/products/images/` + html.EscapeString(string(values[0])) + `/` + html.EscapeString(string(values[1])) + `" title="` + html.EscapeString(string(values[1])) + `" target="_blank"><img src="/products/images/` + string(values[0]) + `/thumb-0-` + string(values[1]) + `" onerror="fave.ShopProductsRetryImage(this);" /></a>, <a href="javascript:fave.ShopProductsDeleteImage(this, ` + html.EscapeString(string(values[0])) + `, '` + html.EscapeString(string(values[1])) + `');">Delete</a></div>`
 			}
 		}
 	}

+ 5 - 5
modules/module_shop_act_upload_delete.go

@@ -40,17 +40,17 @@ func (this *Modules) RegisterAction_ShopUploadDelete() *Action {
 				return err
 			}
 
-			// Delete thumbnails
-			if err := wrap.RemoveProductImageThumbnails(pf_id, "thumb-*-"+pf_file); err != nil {
-				return err
-			}
-
 			// Delete file
 			target_file_full := wrap.DHtdocs + string(os.PathSeparator) + "products" + string(os.PathSeparator) + "images" + string(os.PathSeparator) + pf_id + string(os.PathSeparator) + pf_file
 			if err := os.Remove(target_file_full); err != nil {
 				return err
 			}
 
+			// Delete thumbnails
+			if err := wrap.RemoveProductImageThumbnails(pf_id, "thumb-*-"+pf_file); err != nil {
+				return err
+			}
+
 			return nil
 		}); err != nil {
 			wrap.MsgError(err.Error())

+ 1 - 1
modules/module_shop_act_upload_image.go

@@ -74,7 +74,7 @@ func (this *Modules) RegisterAction_ShopUploadImage() *Action {
 									}
 									return nil
 								}); err == nil {
-									wrap.Write(`$('#list-images').append('<div class="attached-img"><a href="/products/images/` + pf_id + `/` + target_file_name + `" title="` + target_file_name + `" target="_blank"><img src="/products/images/` + pf_id + `/thumb-0-` + target_file_name + `" /></a>, <a href="javascript:fave.ShopProductsDeleteImage(this, ` + pf_id + `, \'` + target_file_name + `\');">Delete</a></div>');`)
+									wrap.Write(`$('#list-images').append('<div class="attached-img"><a href="/products/images/` + pf_id + `/` + target_file_name + `" title="` + target_file_name + `" target="_blank"><img src="/products/images/` + pf_id + `/thumb-0-` + target_file_name + `" onerror="fave.ShopProductsRetryImage(this);" /></a>, <a href="javascript:fave.ShopProductsDeleteImage(this, ` + pf_id + `, \'` + target_file_name + `\');">Delete</a></div>');`)
 								}
 							}
 						}

+ 3 - 3
modules/modules.go

@@ -205,7 +205,7 @@ func (this *Modules) getNavMenuModules(wrap *wrapper.Wrapper, sys bool) string {
 		if mod.Mount == "index" {
 			href = `/cp/`
 		}
-		if !(sys && (mod.Mount == "api" || mod.Mount == "products")) {
+		if !(sys && (mod.Mount == "api")) {
 			html += `<a class="dropdown-item` + class + `" href="` + href + `">` + mod.Name + `</a>`
 		}
 	}
@@ -234,7 +234,7 @@ func (this *Modules) getSidebarModules(wrap *wrapper.Wrapper) string {
 		if !mod.System {
 			html_def += `<li class="nav-item` + class + `"><a class="nav-link" href="` + href + `">` + icon + mod.Name + `</a>` + submenu + `</li>`
 		} else {
-			if !(mod.Mount == "api" || mod.Mount == "products") {
+			if !(mod.Mount == "api") {
 				html_sys += `<li class="nav-item` + class + `"><a class="nav-link" href="` + href + `">` + icon + mod.Name + `</a>` + submenu + `</li>`
 			}
 		}
@@ -353,7 +353,7 @@ func (this *Modules) XXXFrontEnd(wrap *wrapper.Wrapper) bool {
 			}
 			start := time.Now()
 			mod.Front(wrap)
-			if !(mod.Info.Mount == "api" || mod.Info.Mount == "products") {
+			if !(mod.Info.Mount == "api") {
 				wrap.W.Write([]byte(fmt.Sprintf("<!-- %.3f ms -->", time.Now().Sub(start).Seconds())))
 			}
 			return true

Some files were not shown because too many files changed in this diff