(原) 静态文件打包:把Hugo写的博客打包带走

原创文章,请后转载,并注明出处。

想把本博客站打包成一个可执行的Web服务:运行此程序,就可以通过浏览器访问,且可以跨平台使用(Golang先天优势)。

从打包的情况来看,可执行程序本身未压缩就在10MB左右。加上资源后540MB,upx压缩后535MB,hugo的发布文件夹public共有533MB。看起来它只是压缩了自己。

整站打包对于一些文章类网站有用,例如 Gorm、QOR5、Godot 这些用法文档一类。

全程没有自己写代码,一直与AI缠斗。

package main

import (
	"embed"
	"io"
	"io/fs"

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

// 嵌入 public 目录及其所有子文件和子目录
//
//go:embed public/**
var EmbedFS embed.FS

func main() {
	// 创建 Gin 实例
	r := gin.Default()

	// 获取 public 子目录的嵌入文件系统
	subFS, err := fs.Sub(EmbedFS, "public")
	if err != nil {
		panic(err)
	}

	// 统一处理所有静态资源和目录请求
	r.GET("/*filepath", func(c *gin.Context) {
		file := c.Param("filepath")
		// 访问根路径时,默认返回 index.html
		if file == "/" || file == "" {
			file = "/index.html"
		}
		// 如果路径以 / 结尾,自动补全 index.html
		if file[len(file)-1] == '/' {
			file = file + "index.html"
		}
		// 打开目标文件或目录
		f, err := subFS.Open(file[1:])
		if err != nil {
			c.Status(404)
			return
		}
		info, err := f.Stat()
		if err != nil {
			c.Status(500)
			return
		}
		// 如果是目录,则自动查找 index.html
		if info.IsDir() {
			indexFile := file
			if indexFile[len(indexFile)-1] != '/' {
				indexFile += "/"
			}
			indexFile += "index.html"
			f2, err := subFS.Open(indexFile[1:])
			if err != nil {
				c.Status(404)
				return
			}
			defer f2.Close()
			data, err := io.ReadAll(f2)
			if err != nil {
				c.Status(500)
				return
			}
			// 根据文件类型设置 Content-Type 并返回内容
			c.Data(200, detectContentType(indexFile), data)
			return
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			c.Status(500)
			return
		}
		// 根据文件类型设置 Content-Type 并返回内容
		c.Data(200, detectContentType(file), data)
	})

	// 启动 HTTP 服务
	r.Run()
}

// 根据文件扩展名判断 Content-Type
func detectContentType(name string) string {
	switch {
	case len(name) > 5 && name[len(name)-5:] == ".html":
		return "text/html; charset=utf-8"
	case len(name) > 4 && name[len(name)-4:] == ".css":
		return "text/css; charset=utf-8"
	case len(name) > 3 && name[len(name)-3:] == ".js":
		return "application/javascript"
	case len(name) > 4 && name[len(name)-4:] == ".xml":
		return "application/xml"
	case len(name) > 4 && name[len(name)-4:] == ".png":
		return "image/png"
	case len(name) > 4 && name[len(name)-4:] == ".jpg":
		return "image/jpeg"
	case len(name) > 5 && name[len(name)-5:] == ".jpeg":
		return "image/jpeg"
	default:
		return "application/octet-stream"
	}
}

2025.6.4

今天打包下载回来的中文Godot文档,原想依上文方法泡制。但出现错误:

too much data, last section SGOSTRING (2032906294, over 2e+09 bytes)
too much data, last section SGOFUNC (2033389920, over 2e+09 bytes)
too much data, last section SGCBITS (2033393856, over 2e+09 bytes)
too much data, last section SRODATA (2033784360, over 2e+09 bytes)
too much data, last section SRODATAFIPSSTART (2033784361, over 2e+09 bytes)
too much data, last section SRODATAFIPS (2033785760, over 2e+09 bytes)
too much data, last section SRODATAFIPSEND (2033785761, over 2e+09 bytes)
too much data, last section SRODATAEND (2033785761, over 2e+09 bytes)
too much data, last section SFUNCTAB (2033785761, over 2e+09 bytes)
too much data, last section SELFROSECT (2033785761, over 2e+09 bytes)
too much data, last section STYPELINK (2033796716, over 2e+09 bytes)
too much data, last section SITABLINK (2033799880, over 2e+09 bytes)
too much data, last section SSEHSECT (2036217340, over 2e+09 bytes)
too much data, last section SSEHSECT (2036217520, over 2e+09 bytes)

大意是说:这些这些都超过大了。
看来打包成一个文件不是个好方法。
那就把静态文件zip为一个压缩包。
这里把资源文件打包为docs.zip。还支持通过环境变量修改zip文件名

zipfs\zipfs.go

package zipfs

import (
	"archive/zip"
	"io/fs"
)

func New(zipPath string) (fs.FS, error) {
	zr, err := zip.OpenReader(zipPath)
	if err != nil {
		return nil, err
	}
	return fs.Sub(zr, "docs")
}

main.go

package main

import (
	"io"
	"os"

	"github.com/gin-gonic/gin"

	"godot_docs/zipfs"
)

func main() {
	// 创建 Gin 实例
	r := gin.Default()

	// 统一处理所有静态资源和目录请求
	// 从环境变量获取ZIP路径
	zipPath := os.Getenv("DOCS_ZIP")
	if zipPath == "" {
		zipPath = "docs.zip" // 默认值
	}

	// 初始化ZIP文件系统
	docsFS, err := zipfs.New(zipPath)
	if err != nil {
		panic(err)
	}

	// 替换原有subFS使用
	r.GET("/*filepath", func(c *gin.Context) {
		file := c.Param("filepath")
		// 访问根路径时,默认返回 index.html
		if file == "/" || file == "" {
			file = "/index.html"
		}
		// 如果路径以 / 结尾,自动补全 index.html
		if file[len(file)-1] == '/' {
			file = file + "index.html"
		}
		// 打开目标文件或目录
		f, err := docsFS.Open(file[1:])
		if err != nil {
			c.Status(404)
			return
		}
		info, err := f.Stat()
		if err != nil {
			c.Status(500)
			return
		}
		// 如果是目录,则自动查找 index.html
		if info.IsDir() {
			indexFile := file
			if indexFile[len(indexFile)-1] != '/' {
				indexFile += "/"
			}
			indexFile += "index.html"
			f2, err := docsFS.Open(indexFile[1:])
			if err != nil {
				c.Status(404)
				return
			}
			defer f2.Close()
			data, err := io.ReadAll(f2)
			if err != nil {
				c.Status(500)
				return
			}
			// 根据文件类型设置 Content-Type 并返回内容
			c.Data(200, detectContentType(indexFile), data)
			return
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			c.Status(500)
			return
		}
		// 根据文件类型设置 Content-Type 并返回内容
		c.Data(200, detectContentType(file), data)
	})

	// 启动 HTTP 服务
	r.Run()
}

// 根据文件扩展名判断 Content-Type
func detectContentType(name string) string {
	switch {
	case len(name) > 5 && name[len(name)-5:] == ".html":
		return "text/html; charset=utf-8"
	case len(name) > 4 && name[len(name)-4:] == ".css":
		return "text/css; charset=utf-8"
	case len(name) > 3 && name[len(name)-3:] == ".js":
		return "application/javascript"
	case len(name) > 4 && name[len(name)-4:] == ".xml":
		return "application/xml"
	case len(name) > 4 && name[len(name)-4:] == ".png":
		return "image/png"
	case len(name) > 4 && name[len(name)-4:] == ".jpg":
		return "image/jpeg"
	case len(name) > 5 && name[len(name)-5:] == ".jpeg":
		return "image/jpeg"
	case len(name) > 5 && name[len(name)-5:] == ".webp":
		return "image/webp"
	default:
		return "application/octet-stream"
	}
}

PowerShell下:

$env:GIN_MODE="release"
$env:PORT=80
$env:DOCS_ZIP="./docs.zip"
doc_server.exe

打开url http://127.0.1 运行如飞,再也不用等待官网了。

相关文章