0x00 存在意义
权且当作Docker打包的练习。
显然可以通过构造请求获得每天的壁纸,但是如果想要优雅地在其它地方使用这一网络资源,封装一个RESTful API将会保证整洁美观,在编写CSS等场景中会非常方便。
0x01 代码实现
首先是项目的目录结构:
│ bw.exe
│ Dockerfile
│ go.mod
│ go.sum
│ main.go
│ README.md
│
├─command
│ command.go
│
├─image
│ favicon.ico
│
├─roothandler
│ roothandler.go
│
└─xmlhandler
response.go
xmlhandler.go
接口逻辑
先向上游请求XML,通过处理获得关键信息,然后形成JSON作为接口返回格式(也包括直接重定向到图片,但是没有更多文本信息,可用于CSS引入)。
接口参数
接口设置了几个参数:
-
resolution
: [1920
,1366
,3840
]图片的分辨率,默认为
1920
,递增依次为HD、Full HD、UHD(4K)品质。在请求XML时未被使用,会在后续拼接到图片的链接中。 -
format
: [json
,image
]返回形式,默认为
json
。在请求XML时未被使用,接口根据这个参数决定返回类型。 -
index
: [0
, +∞)图片序号,从
0
开始。在请求XML时需要使用。 -
mkt
: [zh-CN
,en-US
,ja-JP
,en-AU
,en-UK
,de-DE
,en-NZ
,en-CA
]地区,决定了使用语言,默认为
zh-CN
。在请求XML时需要使用。
处理XML
先向接口上游请求数据,以XML形式返回。
xmlhandler/xmlhandler.go
package xmlhandler
import (
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/beevik/etree"
)
const (
BingURL = "https://cn.bing.com"
BingAPI = "https://cn.bing.com/HPImageArchive.aspx?format=xml&idx=%d&n=1&mkt=%s"
)
var Resolution map[string]string
var Markets map[string]bool
func init() {
Resolution = map[string]string{
"1366": "1366x768.jpg", // HD
"1920": "1920x1080.jpg", // Full HD
"3840": "UHD.jpg", // 3840×2160
}
Markets = map[string]bool{
"en-US": true,
"zh-CN": true,
"ja-JP": true,
"en-AU": true,
"en-UK": true,
"de-DE": true,
"en-NZ": true,
"en-CA": true,
}
}
// Get: parse .XML from Bing API
func Get(index uint, market string, resolution string) (*Response, error) {
if _, ok := Resolution[resolution]; !ok {
return nil, fmt.Errorf("resolution %s not supported", resolution)
}
if _, ok := Markets[market]; !ok {
return nil, fmt.Errorf("market %s not supported", market)
}
// new http.Client
var client = &http.Client{
Timeout: 5 * time.Second,
}
// new http.Request
var request, err1 = http.NewRequest(http.MethodGet, fmt.Sprintf(BingAPI, index, market), nil)
if err1 != nil {
return nil, fmt.Errorf("http.NewRequest error: %s", err1)
}
request.Header.Add("Referer", "https://cn.bing.com")
request.Header.Add(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84",
)
// request
var resp, err2 = client.Do(request)
if err2 != nil {
return nil, fmt.Errorf("client.Do error: %s", err2)
}
defer resp.Body.Close()
// parse body
// nice etree!!! (github.com/beevik/etree)
var body, err3 = ioutil.ReadAll(resp.Body)
if err3 != nil {
return nil, fmt.Errorf("ioutil.ReadAll error: %s", err3)
}
var doc = etree.NewDocument()
if err4 := doc.ReadFromBytes(body); err4 != nil {
return nil, fmt.Errorf("ReadFromBytes error: %s", err4)
}
// return Response
img := doc.SelectElement("images").SelectElement("image")
return &Response{
SDate: img.SelectElement("startdate").Text(),
EDate: img.SelectElement("enddate").Text(),
URL: fmt.Sprintf("%s%s_%s", BingURL, img.SelectElement("urlBase").Text(), Resolution[resolution]),
Copyright: img.SelectElement("copyright").Text(),
CopyrightLink: img.SelectElement("copyrightlink").Text(),
}, nil
}
xmlhandler/response.go
定义了Response
结构体,作为程序内部首次请求的返回格式。
package xmlhandler
// definition of struct Response
type Response struct {
SDate string `json:"sdate"`
EDate string `json:"edate"`
URL string `json:"url"`
Copyright string `json:"copyright"`
CopyrightLink string `json:"copyright_link"`
}
形成JSON
roothandler/roothandler.go
package roothandler
import (
"Bing-Wallpaper-RESTful/xmlhandler"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// RootHandler: serve as default API using gin
func RootHandler(c *gin.Context) {
// set default query params
resolution := c.DefaultQuery("resolution", "1920")
format := c.DefaultQuery("format", "json")
index := c.DefaultQuery("index", "0")
market := c.DefaultQuery("mkt", "zh-CN")
// check index
uIndex, err := strconv.ParseUint(index, 10, 64)
if err != nil {
// index invalid
c.String(http.StatusInternalServerError, "image index invalid")
return
}
// check format
if format != "json" && format != "image" {
c.String(http.StatusInternalServerError, "format invalid, only `json` or `image` available")
return
}
// fetch info from Bing using xmlhandler
response, err := xmlhandler.Get(uint(uIndex), market, resolution)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
// redirect to image URL directly
if format == "image" && response.URL != "" {
c.Redirect(http.StatusTemporaryRedirect, response.URL)
return
}
// render response as JSON
c.JSON(http.StatusOK, response)
}
命令行设计
command/command.go
规定命令行参数并定义具体执行函数。
package command
import (
"Bing-Wallpaper-RESTful/roothandler"
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"github.com/thinkerou/favicon"
)
// command-line
var Cmd = &cobra.Command{
Use: "run",
Short: "Run this API service",
Long: `Run this API service`,
Run: run,
}
func run(cmd *cobra.Command, args []string) {
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
router.Use(favicon.New("./image/favicon.ico"))
router.GET("/", roothandler.RootHandler)
router.Run(":9002") // default port: 9002
fmt.Println("API is running...")
}
主函数
main.go
package main
import (
"Bing-Wallpaper-RESTful/command"
"fmt"
"os"
"github.com/spf13/cobra"
)
var argVerbose bool
var rootCmd *cobra.Command
func init() {
rootCmd = &cobra.Command{
Use: "bw",
Short: "Bing wallpaper API",
Long: "Top level command for Bing wallpaper API service",
}
rootCmd.PersistentFlags().BoolVarP(&argVerbose, "verbose", "v", false, "verbose output")
rootCmd.AddCommand(
command.Cmd,
)
}
func main() {
if err1 := rootCmd.Execute(); err1 != nil {
fmt.Println(err1)
os.Exit(1)
}
}
0x02 程序试用
打包和帮助
command.go中的run
函数对Web框架gin进行了设置,将作为发行版进行打包,仅对根路径/
进行服务,接受GET,占用端口9002。
虽然Golang图形库比较欠缺发展,但是命令行的格式化库确实很优雅。
go build -o bw.exe
运行
bw run
同时可以看到gin的日志信息。
CSS引入
测试CSS导入,format
要设置为image
。运行Live Server。
0x03 Docker
FROM golang:alpine
ENV GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY="https://goproxy.cn,direct"
RUN mkdir -p /home/www/bw
WORKDIR /home/www/bw
COPY . .
RUN go build -o bw .
WORKDIR /dist
RUN cp /home/www/bw/bw .
RUN mkdir image .
RUN cp /home/www/bw/image/favicon.ico ./image/favicon.ico
# server port
EXPOSE 9002
# run
CMD ["/dist/bw", "run"]
由于有favicon.ico这一静态资源,所以需要注意复制。打包二进制时默认不会嵌入静态资源,需要使用第三方包(大量可选)或者1.16版本新增的embed
功能进行嵌入,不然只能如上复制一遍资源。
用官方镜像打包使得体积过大了。