需求
最近有
Android
和Lua
聯動的需求,但考慮到安全性想採用Go
實現雲端佈局分發,Lua
獲取到本地再進行解壓重新整理。
思路
後端:
Golang
安卓端:Java
+Lua
- 雲端編寫程式碼,
Go-server
實時進行分發 - 安卓端定時監聽雲端的檔案變化, 獲取到本地後解壓並裝載.
- 透過
SQLite
等本地資料庫,實現持久化儲存雲端佈局和相關配置. - 後續擴充可以使用
Java + Lua
重新打包apk包
程式碼實現
- 雲更
- 安卓打包
- 自定義請求庫
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)
]]