Go + AideLua 實現雲端佈局

空小安發表於2024-05-27

需求

最近有AndroidLua聯動的需求,但考慮到安全性想採用Go實現雲端佈局分發,Lua獲取到本地再進行解壓重新整理。

思路

後端: Golang
安卓端: Java + Lua


  1. 雲端編寫程式碼,Go-server 實時進行分發
  2. 安卓端定時監聽雲端的檔案變化, 獲取到本地後解壓並裝載.
  3. 透過 SQLite等本地資料庫,實現持久化儲存雲端佈局和相關配置.
  4. 後續擴充可以使用 Java + Lua重新打包apk包

程式碼實現

  1. 雲更
  2. 安卓打包
  3. 自定義請求庫

Go 雲端更新方案

go_server.go

package main

import (
	"archive/zip"
	"bytes"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sync"
	"time"

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

var (
	mu           sync.Mutex
	zipBuffer    *bytes.Buffer
	lastAccess   time.Time
	accessPeriod = 10 * time.Second
)

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

	r.GET("/api/v1/file", func(c *gin.Context) {
		mu.Lock()
		defer mu.Unlock()

		if time.Since(lastAccess) < accessPeriod {
			c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "暫時沒有"})
			return
		}

		lastAccess = time.Now()

		if zipBuffer == nil {
			c.JSON(http.StatusNoContent, gin.H{"message": "No file available"})
			return
		}

		c.Header("Content-Type", "application/zip")
		c.Header("Content-Disposition", "attachment; filename=demo.zip")
		c.Data(http.StatusOK, "application/zip", zipBuffer.Bytes())
	})

	go func() {
		for {
			packFiles()
			time.Sleep(10 * time.Second)
		}
	}()

	if err := r.Run(":9090"); err != nil {
		log.Fatal(err)
	}
}

func packFiles() {
	mu.Lock()
	defer mu.Unlock()

	// 檢查目錄下是否有檔案變更
	files, err := os.ReadDir("res") // 替換為你的目錄路徑
	if err != nil {
		log.Println("Failed to read directory:", err)
		return
	}

	if len(files) == 0 {
		log.Println("No files available in the directory")
		return
	}

	// 建立一個記憶體緩衝區來儲存zip檔案
	buf := new(bytes.Buffer)
	zipWriter := zip.NewWriter(buf)

	// 遍歷目錄下的檔案,並將它們新增到zip檔案中
	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join("res", file.Name()) // 替換為你的目錄路徑
		data, err := os.ReadFile(filePath)
		if err != nil {
			log.Println("Failed to read file:", file.Name(), err)
			continue
		}

		fileWriter, err := zipWriter.Create(file.Name())
		if err != nil {
			log.Println("Failed to create zip file entry:", file.Name(), err)
			continue
		}

		_, err = fileWriter.Write(data)
		if err != nil {
			log.Println("Failed to write to zip file:", file.Name(), err)
		}
	}

	err = zipWriter.Close()
	if err != nil {
		log.Println("Failed to close zip writer:", err)
		return
	}

	// 將zip檔案儲存到快取中
	zipBuffer = buf

	fmt.Println("Files packed into zip successfully")
}

Lua 安卓apk打包

bin.lua

require "lua.import"
import "java.util.zip.ZipOutputStream"
import "android.net.Uri"
import "java.io.File"
import "android.widget.Toast"
import "java.util.zip.CheckedInputStream"
import "java.io.FileInputStream"
import "android.content.Intent"
import "java.security.Signer"
import "java.util.ArrayList"
import "java.io.FileOutputStream"
import "java.io.BufferedOutputStream"
import "java.util.zip.ZipInputStream"
import "java.io.BufferedInputStream"
import "java.util.zip.ZipEntry"
import "android.app.ProgressDialog"
import "java.util.zip.CheckedOutputStream"
import "java.util.zip.Adler32"

local bin_dlg, error_dlg
local function update(s)
    bin_dlg.setMessage(s)
end

local function callback(s)
    LuaUtil.rmDir(File(activity.getLuaExtDir("bin/.temp")))
    bin_dlg.hide()
    bin_dlg.Message = ""
    if not s:find("成功") then
        error_dlg.Message = s
        error_dlg.show()
    end
end

local function create_bin_dlg()
    if bin_dlg then
        return
    end
    bin_dlg = ProgressDialog(activity);
    bin_dlg.setTitle("正在打包");
    bin_dlg.setMax(100);
end

local function create_error_dlg2()
    if error_dlg then
        return
    end
    error_dlg = AlertDialogBuilder(activity)
    error_dlg.Title = "出錯"
    error_dlg.setPositiveButton("確定", nil)
end

local function binapk(luapath, apkpath)
    require "lua.import"
    import "console"
    compile "mao"
    compile "sign"
    import "java.util.zip.*"
    import "java.io.*"
    import "mao.res.*"
    import "apksigner.*"
    local b = byte[2 ^ 16]
    local function copy(input, output)
        LuaUtil.copyFile(input, output)
        input.close()
        --[[local l=input.read(b)
      while l>1 do
        output.write(b,0,l)
        l=input.read(b)
      end]]
    end

    local function copy2(input, output)
        LuaUtil.copyFile(input, output)
    end

    local temp = File(apkpath).getParentFile();
    if (not temp.exists()) then

        if (not temp.mkdirs()) then

            error("create file " .. temp.getName() .. " fail");
        end
    end


    local tmp = activity.getLuaPath("tmp.apk")
    local info = activity.getApplicationInfo()
    local ver = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionName
    local code = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode

    --local zip=ZipFile(info.publicSourceDir)
    local zipFile = File(info.publicSourceDir)
    local fis = FileInputStream(zipFile);
    --local checksum = CheckedInputStream(fis, Adler32());
    local zis = ZipInputStream(BufferedInputStream(fis));

    local fot = FileOutputStream(tmp)
    --local checksum2 = CheckedOutputStream(fot, Adler32());

    local out = ZipOutputStream(BufferedOutputStream(fot))
    local f = File(luapath)
    local errbuffer = {}
    local replace = {}
    local checked = {}
    local lualib = {}
    local md5s = {}
    local libs = File(activity.ApplicationInfo.nativeLibraryDir).list()
    libs = luajava.astable(libs)
    for k, v in ipairs(libs) do
        --libs[k]="lib/armeabi/"..libs[k]
        replace[v] = true
    end

    local mdp = activity.Application.MdDir
    local function getmodule(dir)
        local mds = File(activity.Application.MdDir .. dir).listFiles()
        mds = luajava.astable(mds)
        for k, v in ipairs(mds) do
            if mds[k].isDirectory() then
                getmodule(dir .. mds[k].Name .. "/")
            else
                mds[k] = "lua" .. dir .. mds[k].Name
                replace[mds[k]] = true
            end
        end
    end

    getmodule("/")

    local function checklib(path)
        if checked[path] then
            return
        end
        local cp, lp
        checked[path] = true
        local f = io.open(path)
        local s = f:read("*a")
        f:close()
        for m, n in s:gmatch("require *%(? *\"([%w_]+)%.?([%w_]*)") do
            cp = string.format("lib%s.so", m)
            if n ~= "" then
                lp = string.format("lua/%s/%s.lua", m, n)
                m = m .. '/' .. n
            else
                lp = string.format("lua/%s.lua", m)
            end
            if replace[cp] then
                replace[cp] = false
            end
            if replace[lp] then
                checklib(mdp .. "/" .. m .. ".lua")
                replace[lp] = false
                lualib[lp] = mdp .. "/" .. m .. ".lua"
            end
        end
        for m, n in s:gmatch("import *%(? *\"([%w_]+)%.?([%w_]*)") do
            cp = string.format("lib%s.so", m)
            if n ~= "" then
                lp = string.format("lua/%s/%s.lua", m, n)
                m = m .. '/' .. n
            else
                lp = string.format("lua/%s.lua", m)
            end
            if replace[cp] then
                replace[cp] = false
            end
            if replace[lp] then
                checklib(mdp .. "/" .. m .. ".lua")
                replace[lp] = false
                lualib[lp] = mdp .. "/" .. m .. ".lua"
            end
        end
    end

    replace["libluajava.so"] = false

    local function addDir(out, dir, f)
        local entry = ZipEntry("assets/" .. dir)
        out.putNextEntry(entry)
        local ls = f.listFiles()
        for n = 0, #ls - 1 do
            local name = ls[n].getName()
            if name==(".using") then
                checklib(luapath .. dir .. name)
            elseif name:find("%.apk$") or name:find("%.luac$") or name:find("^%.") then
            elseif name:find("%.lua$") then
                checklib(luapath .. dir .. name)
                local path, err = console.build(luapath .. dir .. name)
                if path then
                    if replace["assets/" .. dir .. name] then
                        table.insert(errbuffer, dir .. name .. "/.aly")
                    end
                    local entry = ZipEntry("assets/" .. dir .. name)
                    out.putNextEntry(entry)

                    replace["assets/" .. dir .. name] = true
                    copy(FileInputStream(File(path)), out)
                    table.insert(md5s, LuaUtil.getFileMD5(path))
                    os.remove(path)
                else
                    table.insert(errbuffer, err)
                end
            elseif name:find("%.aly$") then
                local path, err = console.build(luapath .. dir .. name)
                if path then
                    name = name:gsub("aly$", "lua")
                    if replace["assets/" .. dir .. name] then
                        table.insert(errbuffer, dir .. name .. "/.aly")
                    end
                    local entry = ZipEntry("assets/" .. dir .. name)
                    out.putNextEntry(entry)

                    replace["assets/" .. dir .. name] = true
                    copy(FileInputStream(File(path)), out)
                    table.insert(md5s, LuaUtil.getFileMD5(path))
                    os.remove(path)
                else
                    table.insert(errbuffer, err)
                end
            elseif ls[n].isDirectory() then
                addDir(out, dir .. name .. "/", ls[n])
            else
                local entry = ZipEntry("assets/" .. dir .. name)
                out.putNextEntry(entry)
                replace["assets/" .. dir .. name] = true
                copy(FileInputStream(ls[n]), out)
                table.insert(md5s, LuaUtil.getFileMD5(ls[n]))
            end
        end
    end


    this.update("正在編譯...");
    if f.isDirectory() then
        require "permission"
        dofile(luapath .. "init.lua")
        if user_permission then
            for k, v in ipairs(user_permission) do
                user_permission[v] = true
            end
        end


        local ss, ee = pcall(addDir, out, "", f)
        if not ss then
            table.insert(errbuffer, ee)
        end
        --print(ee,dump(errbuffer),dump(replace))


        local wel = File(luapath .. "icon.png")
        if wel.exists() then
            local entry = ZipEntry("res/drawable/icon.png")
            out.putNextEntry(entry)
            replace["res/drawable/icon.png"] = true
            copy(FileInputStream(wel), out)
        end
        local wel = File(luapath .. "welcome.png")
        if wel.exists() then
            local entry = ZipEntry("res/drawable/welcome.png")
            out.putNextEntry(entry)
            replace["res/drawable/welcome.png"] = true
            copy(FileInputStream(wel), out)
        end
    else
        return "error"
    end

    --print(dump(lualib))
    for name, v in pairs(lualib) do
        local path, err = console.build(v)
        if path then
            local entry = ZipEntry(name)
            out.putNextEntry(entry)
            copy(FileInputStream(File(path)), out)
            table.insert(md5s, LuaUtil.getFileMD5(path))
            os.remove(path)
        else
            table.insert(errbuffer, err)
        end
    end

    function touint32(i)
        local code = string.format("%08x", i)
        local uint = {}
        for n in code:gmatch("..") do
            table.insert(uint, 1, string.char(tonumber(n, 16)))
        end
        return table.concat(uint)
    end

    this.update("正在打包...");
    local entry = zis.getNextEntry();
    while entry do
        local name = entry.getName()
        local lib = name:match("([^/]+%.so)$")
        if replace[name] then
        elseif lib and replace[lib] then
        elseif name:find("^assets/") then
        elseif name:find("^lua/") then
        elseif name:find("META%-INF") then
        elseif !name:find("%a") then
        else
            local entry = ZipEntry(name)
            out.putNextEntry(entry)
            if entry.getName() == "AndroidManifest.xml" then
                if path_pattern and #path_pattern > 1 then
                    path_pattern = ".*\\\\." .. path_pattern:match("%w+$")
                end
                local list = ArrayList()
                local xml = AXmlDecoder.read(list, zis)
                local req = {
                    [activity.getPackageName()] = packagename,
                    [info.nonLocalizedLabel] = appname,
                    [ver] = appver,
                    [".*\\\\.lua"] = "",
                    [".*\\\\.luac"] = "",
                }
                if path_pattern==nil or path_pattern=="" then
                    req[".*\\\\.alp"] = ""
                    req["application/alp"] = "application/1234567890"
                  else
                    path_pattern=path_pattern:match("%w+$") or path_pattern
                    req[".*\\\\.alp"] = ".*\\\\."..path_pattern
                    req["application/alp"] = "application/"..path_pattern
                end
                for n = 0, list.size() - 1 do
                    local v = list.get(n)
                    if req[v] then
                        list.set(n, req[v])
                    elseif user_permission then
                        local p = v:match("%.permission%.([%w_]+)$")
                        if p and (not user_permission[p]) then
                            list.set(n, "android.permission.UNKNOWN")
                        end
                    end
                end
                local pt = activity.getLuaPath(".tmp")
                local fo = FileOutputStream(pt)
                xml.write(list, fo)
                local code = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode
                fo.close()
                local f = io.open(pt)
                local s = f:read("a")
                f:close()
                s = string.gsub(s, touint32(code), touint32(tointeger(appcode) or 1),1)
                s = string.gsub(s, touint32(18), touint32(tointeger(appsdk) or 18),1)

                local f = io.open(pt, "w")
                f:write(s)
                f:close()
                local fi = FileInputStream(pt)
                copy(fi, out)
                os.remove(pt)
            elseif not entry.isDirectory() then
                copy2(zis, out)
            end
        end
        entry = zis.getNextEntry()
    end
    out.setComment(table.concat(md5s))
    --print(table.concat(md5s,"/n"))
    zis.close();
    out.closeEntry()
    out.close()

    if #errbuffer == 0 then
        this.update("正在簽名...");
        os.remove(apkpath)
        Signer.sign(tmp, apkpath)
        os.remove(tmp)
        activity.installApk(apkpath)
        --[[import "android.net.*"
        import "android.content.*"
        i = Intent(Intent.ACTION_VIEW);
        i.setDataAndType(activity.getUriForFile(File(apkpath)), "application/vnd.android.package-archive");
        i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        this.update("正在開啟...");
        activity.startActivityForResult(i, 0);]]
        return "打包成功:" .. apkpath
    else
        os.remove(tmp)
        this.update("打包出錯:\n " .. table.concat(errbuffer, "\n"));
        return "打包出錯:\n " .. table.concat(errbuffer, "\n")
    end
end

--luabindir=activity.getLuaExtDir("bin")
--print(activity.getLuaExtPath("bin","a"))
local function bin(path)
    local p = {}
    local e, s = pcall(loadfile(path .. "init.lua", "bt", p))
    if e then
        create_error_dlg2()
        create_bin_dlg()
        bin_dlg.show()
        activity.newTask(binapk, update, callback).execute { path, activity.getLuaExtPath("bin", p.appname .. "_" .. p.appver .. ".apk") }
    else
        Toast.makeText(activity, "工程配置檔案錯誤." .. s, Toast.LENGTH_SHORT).show()
    end
end

--bin(activity.getLuaExtDir("project/demo").."/")
return bin

自定義的Lua請求庫

knet.lua

--
-- 更為便捷的請求封裝
--

local kox = {
    _URL = nil,
    _DATA = nil,
    SomeThing = {},
    _COOKIES = nil,
    _METHOD = "get",
    _ChARSET = nil,
    _HEADERS = {
        ["User-Agent"] =
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69"
    }
}

---設定請求地址
---@param fetchUrl string
---@return table
function kox:setUrl(fetchUrl)
    if self._URL == nil then
        self._URL = fetchUrl
    end
    return self
end

--- 增加請求頭
---@param key string
---@param value string
function kox:setHeaders(key, value, ...)
    local op = { ... }

    self._HEADERS[string.lower(key)] = value

    if #op > 0 then
        for k, v in pairs(op) do
            self._HEADERS[string.lower(tostring(k))] = v
        end
    end

    return self
end

--- 設定請求模式
---@param method string
function kox:setMethod(method)
    self._METHOD = string.lower(method)
    return self
end

--- 增加引數
---@param data string
function kox:setData(data)
    self._DATA = data
    return self
end

--- 傳送請求
---@param callBack function
function kox:send(callBack, errorBack)
    xpcall(function()
        if self._METHOD == "get" then
            Http.get(self._URL, self._COOKIES, self._ChARSET, self._HEADERS, callBack)
        elseif self._METHOD == "post" then
            Http.post(self._URL, self._DATA, self._COOKIES, self._ChARSET, self._HEADERS, callBack)
        end
    end, errorBack)
    return self
end

-- 預設傳遞引數
function kox.Err(err)
    print(err)
end

return kox

--[[
Useage:
    local Request = import "kox.knet"
    Request:setUrl("https://www.baidu.com")
    Request:setMethod("Post")
    Request:setData(self.Params)
    Request:setHeaders("Content-Type", "application/x-www-form-urlencoded")
    Request:send(self.SuccessCaller, kox.Err)
]]

相關文章