iOS 系统 Emoji 表情对应的 png 图片打包下载

最近有个需求,根据前台用户输入的内容在后台生成宣传图。

我的方法是,把内容渲染成一个 HTML 页面,然后使用 wkhtmltoimage 把网页转换成图片。遇到了一个问题:用户输入的内容带有 Emoji 表情符号,由于 Linux 服务器上没有对应的字体问题,生成图片的时候,Emoji 表情符号全变成了无法识别的框框。

当时我想,要是能把 iOS 系统的 Emoji 表情字体文件 Copy 过来用,那就完美了。在网上搜罗了很久,最终无功而返, iOS 系统的字体文件不能直接在其他系统上使用!

最后,我打算用最笨的方法,就是把 Emoji 表情符号替换成 PNG 图片来显示。即把原来的 Emoji 符号替换成 HTML 的 img 标签。

在 https://www.unicode.org/ 找到了所有 Emoji 表情对应的图片。两个页面,每个页面大概 25MB,打开很慢的,地址分别是...

分类至 杂项
0条评论

linux zip 打包时排除指定目录

zip 指令参数说明:

[root@VM_0_10_centos ~]# zip
Copyright (c) 1990-2008 Info-ZIP - Type 'zip "-L"' for software license.
Zip 3.0 (July 5th 2008). Usage:
zip [-options] [-b path] [-t mmddyyyy] [-n suffixes] [zipfile list] [-xi list]
  The default action is to add or replace zipfile entries from list, which
  can include the special name - to compress standard input.
  If zipfile and list are omitted, zip compresses stdin to stdout.
  -f   freshen: only changed files  -u   update: only changed or new files
  -d   delete entries in zipfile    -m   move into zipfile (delete OS files)
  -r   recurse into directories     -j   junk (don't record) directory names
  -0   store only                   -l   convert LF to CR LF (-ll CR LF to LF)
  -1   compress faster              -9   compress better
  -q   quiet operation              -v   verbose operation/print version info
  -c   add one-line comments        -z   add zipfile comment
  -@   read names from stdin        -o   make zipfile as old as latest entry
  -x   exclude the following names  -i   include only the following names
  -F   fix zipfile (-FF try harder) -D   do not add directory entries
  -A   adjust self-extracting exe   -J   junk zipfile prefix (unzipsfx)
  -T   test zipfile integrity       -X   eXclude eXtra file attributes
  -y   store symbolic links as the link instead of the referenced file
  -e   encrypt                      -n   don't compress these suffixes
  -h2  show more help

打包某个目录下的所有文件和子目录时,可以用 -r 递归参数,要排除某个目录或文件可以用 -x 参数。但是要注意,-x 参数如果是排除目录,要用双引号包裹,末尾要加 * 号。

打包时排除某个文件:

zip -r yangdx.zip yangdx/ -x yangdx/test.txt

打包时排除 cache、vendor 目录:

zip -r yangdx.zip yangdx/ -x "yangdx/cache/*" -x "yangdx/vendor/*"

 

分类至 Linux
0条评论

腾讯云服务器 CentOS 7.6 快速搭建 LNMP 环境

我在腾讯云上购买了云服务器,系统是 CentOS 7.6 x64,自己整理了以下脚本文件,可以快速安装 LNMP 环境。

文件列表及备注如下:

1-init.sh      # 初始化更新系统软件库
2-account.sh   # 创建 www、mysql 账号
3-openss1.sh   # 安装 openssl-1.1.1d
4-nginx.sh     # 安装 nginx-1.16.1
5-php.sh       # 安装 php-7.2.28
6-mysql.sh     # 安装 mysql-5.7.28
my.cnf         # mysql 的配置文件
nginx.conf     # nginx 的全局配置文件
mysite.conf    # nginx 的示例网站配置文件

包含了 6 个 shell 脚本文件和 3 个配置文件,按顺序执行以上 6 个脚本,php、nginx、mysql 都被安装在 /usr/local 目录下的各自目录。其中,mysql 采用二进制包安装,默认密码保存在 lnmp-script/mysql_new_password.log。默认的配置文件只保证服务能够正常跑起来,如果想要达到最佳性能,需要自己结合服务器硬件配置去做优化。

分类至 杂项
0条评论

Gin 框架学习:使用 BasicAuth 中间件

basicauth.go:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

var secrets = gin.H{
	"foo":    gin.H{"email": "foo@bar.com", "phone": "123433"},
	"austin": gin.H{"email": "austin@example.com", "phone": "666"},
	"lena":   gin.H{"email": "lena@guapa.com", "phone": "523443"},
}

func main() {
	r := gin.Default()

	// 路由组使用 gin.BasicAuth() 中间件
	// gin.Accounts 是 map[string]string 的一种快捷方式
	authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
		"foo":    "bor",
		"austin": "1234",
		"lena":   "hello2",
		"manu":   "4321",
	}))

	// /admin/secrets 端点
	// 触发 "localhost:8080/admin/secrets
	authorized.GET("/secrets", func(c *gin.Context) {
		// 获取用户,它是由 BasicAuth 中间件设置的
		user := c.MustGet(gin.AuthUserKey).(string)
		if secret, ok := secrets[user]; ok {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
		} else {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
		}
	})

	// 监听并在 0.0.0.0:8080 上启动服务
	r.Run(":8080")
}

关于 HTTP Basic 认证的介绍可以查看这篇博文:https://www.yangdx.com/2019/03/19.html

分类至 GO
0条评论

Gin 框架学习:优雅地重启或停止

Go 1.8+ 版本可以使用 http.Server 内置的 Shutdown() 方法优雅地关闭服务。

server.go:

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}

	go func() {
		// 服务连接
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)
	<-quit
	log.Println("Shutdown Server ...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown:", err)
	}
	log.Println("Server exiting")
}

启动服务后,使用浏览器访问 http://localhost:8080/ 这个链接。由于设置了 5 秒睡眠,页面会一直处于加载中,直到 5 秒后才显示出内容。在页面响应前,按 Ctrl + C 尝试关闭正在运行的服务(Linux 系统还可以使用 kill -2 pid 指令触发),会先看到打印 Shutdown Server ...,等待几秒后前台页面加载完成,最后才打印 Server exiting,服务关闭。

可以试着把代码第 17 行的 5 秒睡眠改成 10 秒或更大,重复一遍上面的操作,会有不同效果。

分类至 GO
0条评论

Gin 框架学习:从 reader 读取数据

fromreader.go:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()

	router.GET("/someDataFromReader", func(c *gin.Context) {
		response, err := http.Get("https://www.yangdx.com/res/img/avatar.jpg")
		if err != nil || response.StatusCode != http.StatusOK {
			c.Status(http.StatusServiceUnavailable)
			return
		}

		reader := response.Body
		contentLength := response.ContentLength
		contentType := response.Header.Get("Content-Type")

		extraHeaders := map[string]string{
			"Content-Disposition": `attachment; filename="gopher.jpg"`,
		}

		c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
	})

	router.Run(":8080")
}

使用浏览器访问 http://localhost:8080/someDataFromReader 将弹出文件的保持对话框。

分类至 GO
0条评论

Gin 框架学习:多文件上传

upload2.go:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"path/filepath"
)

func main() {
	router := gin.Default()
	// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
	// router.MaxMultipartMemory = 8 << 20 // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		form, _ := c.MultipartForm()
		files := form.File["upload[]"]

		for _, file := range files {
			log.Println(file.Filename)

			dst := "./upload/" + filepath.Base(file.Filename)
			c.SaveUploadedFile(file, dst)
		}

		c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
	})
	router.Run(":8080")
}

使用 curl 测试上传:

$ curl -X POST http://localhost:8080/upload \
    -F "upload[]=@/Users/appleboy/test1.zip" \
    -F "upload[]=@/Users/appleboy/test2.zip" \
    -H "Content-Type: multipart/form-data"

 

分类至 GO
0条评论

Gin 框架学习:单文件上传

upload1.go:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"path/filepath"
)

func main() {
	router := gin.Default()
	// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		file, err := c.FormFile("file")
		if err != nil {
			c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
		}

		log.Println(file.Filename)

		dst := "./upload/" + filepath.Base(file.Filename)
		if err := c.SaveUploadedFile(file, dst); err != nil {
			c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
		}

		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
	})

	router.Run(":8080")
}

使用 curl 测试上传:

$ curl -X POST http://localhost:8080/upload \
    -F "file=@/f/folder/test.zip" \
    -H "Content-Type: multipart/form-data"

 

分类至 GO
0条评论

Gin 框架学习:XML/JSON/YAML/ProtoBuf 渲染

xml_json_yaml_protobuf.go:

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/testdata/protoexample"
	"net/http"
)

func main() {
	r := gin.Default()

	// gin.H 是 map[string]interface{} 的一种快捷方式
	r.GET("/someJSON", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/moreJSON", func(c *gin.Context) {
		// 你也可以使用一个结构体
		var msg struct {
			Name    string `json:"user"`
			Message string
			Number  int
		}
		msg.Name = "Lena"
		msg.Message = "hey"
		msg.Number = 123
		// 注意 msg.Name 在 JSON 中变成了 "user"
		// 将输出:{"user":"Lena","Message":"hey","Number":123}
		c.JSON(http.StatusOK, msg)
	})

	r.GET("/someXML", func(c *gin.Context) {
		c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someYAML", func(c *gin.Context) {
		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someProtoBuf", func(c *gin.Context) {
		reps := []int64{int64(1), int64(2)}
		label := "test"
		// protobuf 的具体定义写在 testdata/protoexample 文件中。
		data := &protoexample.Test{
			Label: &label,
			Reps:  reps,
		}
		// 请注意,数据在响应中变为二进制数据
		// 将输出被 protoexample.Test protobuf 序列化了的数据
		c.ProtoBuf(http.StatusOK, data)
	})

	// 监听并在 0.0.0.0:8080 上启动服务
	r.Run(":8080")
}

 

分类至 GO
0条评论