update 2025-02-16 22:24:52

This commit is contained in:
actions-user 2025-02-16 22:24:52 +08:00
parent 6c79d18ef4
commit a2e77a7982
45 changed files with 5265 additions and 0 deletions

21
istore/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 易有云团队
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
istore/README.en.md Normal file
View File

@ -0,0 +1,4 @@
# iStore
## Introduce
The iStore is a app store for OpenWRT, it is implemented using Shell scripts, only depends on OpenWRT standard components, and does not contain binary files itself

51
istore/README.md Normal file
View File

@ -0,0 +1,51 @@
# iStore
## 介绍
iStore 是一个[开源](https://github.com/linkease/istore)且标准的 OpenWRT 软件中心实现。
是属于易有云团队的固件 [iStoreOS](https://github.com/istoreos) 的一部分。
iStore 的设计目标:
1. 方便用户安装插件
2. 每个插件都有教程,方便初学者入门使用插件
3. 适配所有的 OpenWRT 皮肤,以及移动端
4. 全部基于 OpenWRT 的标准接口开发,不用以前 KoolShare LEDE 的特有的插件风格的软件中心
iStore 的无法弥补的缺陷:
1. 因为 OpenWRT 版本特别多,导致不同平台的插件依赖不一样。所以即使你的系统能安装 iStoreiStore 里面的插件不一定能安装
2. 我们需要固件开发者自主集成 iStore并解决 iStore 里面的各种插件的依赖
我们建议固件开发者集成 iStore这样固件开发者只需要发布一个精简固件用户可以按需安装自己喜欢的插件。还能共享插件的教程独立更新某个插件版本。
> 21版本的固件安装 iStore 需要依赖 `luci-compat`
## 安装 iStore 到 OpenWRT 官方固件
只支持 x86_64 和 arm64 设备。
```
opkg update || exit 1
cd /tmp
wget https://github.com/linkease/openwrt-app-actions/raw/main/applications/luci-app-systools/root/usr/share/systools/istore-reinstall.run
chmod 755 istore-reinstall.run
./istore-reinstall.run
```
## 集成到自己编译的固件中
iStore 官方的软件仓库支持 `x86_64``arm64` 两个架构,这两个架构的基于 **OpenWRT** 固件都可以直接集成 iStore
只需在固件编译目录下执行:
```shell
echo >> feeds.conf.default
echo 'src-git istore https://github.com/linkease/istore;main' >> feeds.conf.default
./scripts/feeds update istore
./scripts/feeds install -d y -p istore luci-app-store
```
然后正常编译固件即可
## 功能预览
![istore-preview.png](./preview/istore-preview.png)

View File

@ -0,0 +1,161 @@
### 路由器端API
0. 获取csrfToken用于POST请求
```
GET /cgi-bin/luci/admin/store/token
{"token":"xxx"}
```
1. 已安装软件列表
```
GET /cgi-bin/luci/admin/store/installed
[
{
"description": "DDNS.TO内网穿透",
"tags": [
"net",
"tool"
],
"entry": "/cgi-bin/luci/admin/services/ddnsto",
"author": "xiaobao",
"depends": [
"ddnsto",
"luci-app-ddnsto",
"luci-i18n-ddnsto-zh-cn"
],
"title": "DDNS.TO",
"time": 1629356347,
"release": 1,
"website": "https://www.ddnsto.com/",
"name": "ddnsto",
"version": "1.0.0"
}
]
```
2. 安装软件
```
POST /cgi-bin/luci/admin/store/install
token=xxx&package=upnp&autoconf=1&path=/mnt/nvme&enable=1
autoconf=1&path=/mnt/nvme&enable=1 是可选参数,表示安装完以后是否自动配置插件
{"code":0, "stdout":"", "stderr":""}
```
3. 更新软件
```
POST /cgi-bin/luci/admin/store/upgrade
token=xxx&package=upnp
{"code":0, "stdout":"", "stderr":""}
```
4. 卸载软件
```
POST /cgi-bin/luci/admin/store/remove
token=xxx&package=upnp
{"code":0, "stdout":"", "stderr":""}
```
5. 刷新可用软件列表
```
POST /cgi-bin/luci/admin/store/update
token=xxx
{"code":0, "stdout":"", "stderr":""}
```
6. 查询特定软件状态
```
GET /cgi-bin/luci/admin/store/status?package=ddnsto
{
"description": "DDNS.TO内网穿透",
"tags": [
"net",
"tool"
],
"entry": "/cgi-bin/luci/admin/services/ddnsto",
"author": "xiaobao",
"depends": [
"ddnsto",
"luci-app-ddnsto",
"luci-i18n-ddnsto-zh-cn"
],
"installed": true,
"title": "DDNS.TO",
"time": "1629356347",
"release": 1,
"website": "https://www.ddnsto.com/",
"name": "ddnsto",
"version": "1.0.0"
}
{"installed":false}
```
7. 任务状态日志已废弃使用luci-lib-taskd提供的封装
```
GET /cgi-bin/luci/admin/store/log
{
"stdout": "Installing app-meta-ddnsto (1.0.0) to root...\nDownloading http://192.168.9.168:9999/packages/aarch64_cortex-a53/meta/app-meta-ddnsto_1.0.0_all.ipk\nConfiguring app-meta-ddnsto.\n",
"stderr": "",
"code": 206
}
{"stdout":"","stderr":"","code":0}
```
8. 上传安装
```
POST /cgi-bin/luci/admin/store/upload
(文件上传表单,支持文件扩展名".ipk,.run")
{"code":0, "stdout":"", "stderr":""}
```
9. 检查iStore自身更新
```
GET /cgi-bin/luci/admin/store/check_self_upgrade
{"code":500, "msg":"Internal Error"}
{"code":200, "msg":"1.1.2"}
{"code":304, "msg":""}
```
1. 更新iStore自身
> 检查iStore自身更新接口返回code为200时才调用这个接口
```
POST /cgi-bin/luci/admin/store/do_self_upgrade
token=xxx
{"code":0, "stdout":"", "stderr":""}
```
2. 枚举块设备
```
GET /cgi-bin/luci/admin/store/get_block_devices
{"code":500, "msg":"Unable to execute block utility"}
{"code":200, "data":{"sda1":{"dev":"\/dev\/sda1","uuid":"f54566dd-ec58-4e24-9451-bbf75834add3","version":"1.0","type":"ext4","size":"238.46 GB"},"mmcblk0p2":{"dev":"\/dev\/mmcblk0p2","uuid":"dba3d0dc-f072-4e81-a0ac-ac35197fb286","version":"1.0","label":"etc","mount":"\/overlay","type":"ext4","size":"6.87 GB"},"mmcblk0p1":{"dev":"\/dev\/mmcblk0p1","uuid":"8f9564a1-68006e25-c4c26df6-de88ef16","version":"4.0","mount":"\/rom","type":"squashfs","size":"127.99 MB"}}}
```
3. 检查插件是否已经配置过
```
GET /cgi-bin/luci/admin/store/configured?uci=ddnsto
uci参数是uci配置文件的名称不是包名不过大部分情况下都是一致的。
{"code":200}
{"code":200, "configured":true}
```

View File

@ -0,0 +1,39 @@
# Copyright (C) 2016 Openwrt.org
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI based ipk store
LUCI_DESCRIPTION:=luci-app-store is a ipk store developed by LinkEase team
LUCI_DEPENDS:=+curl +opkg +luci-lib-ipkg +tar +libuci-lua +mount-utils +luci-lib-taskd
LUCI_EXTRA_DEPENDS:=luci-lib-taskd (>=1.0.19)
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.27-4
# PKG_RELEASE MUST be empty for luci.mk
PKG_RELEASE:=
ISTORE_UI_VERSION:=0.1.27
ISTORE_UI_RELEASE:=2
PKG_HASH:=7fb8e9983dd33b14c43de3caf6a6121df0727e0adac6c666354e0d8a0cffa0b8
PKG_SOURCE_URL_FILE:=v$(ISTORE_UI_VERSION)-$(ISTORE_UI_RELEASE).tar.gz
PKG_SOURCE:=istore-ui-$(PKG_SOURCE_URL_FILE)
PKG_SOURCE_URL:=https://github.com/linkease/istore-ui/archive/refs/tags
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
TARGET_CONFIGURE_OPTS= FRONTEND_DIST="$(BUILD_DIR)/istore-ui-$(ISTORE_UI_VERSION)-$(ISTORE_UI_RELEASE)/app-store-ui/src/dist" APP_STORE_VERSION="$(PKG_VERSION)"
TARGET_CONFIGURE_OPTS+= SED="$(SED)"
define Package/luci-app-store/conffiles
/etc/.app_store.id
/etc/config/istore
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,3 @@
# luci-app-store
OpenWRT应用商店

View File

@ -0,0 +1,971 @@
module("luci.controller.store", package.seeall)
local myopkg = "is-opkg"
local is_backup = "/usr/libexec/istore/backup"
local page_index = {"admin", "store", "pages"}
function index()
local function store_api(action, onlypost)
local e = entry({"admin", "store", action}, onlypost and post("store_action", {action = action}) or call("store_action", {action = action}))
e.dependent = false -- 父节点不是必须的
e.leaf = true -- 没有子节点
end
local action
entry({"admin", "store"}, call("redirect_index"), _("iStore"), 31)
entry({"admin", "store", "pages"}, call("store_index")).leaf = true
if nixio.fs.access("/usr/lib/lua/luci/view/store/main_dev.htm") then
entry({"admin", "store", "dev"}, call("store_dev")).leaf = true
end
entry({"admin", "store", "token"}, call("store_token"))
entry({"admin", "store", "log"}, call("store_log"))
entry({"admin", "store", "uid"}, call("action_user_id"))
entry({"admin", "store", "upload"}, post("store_upload"))
entry({"admin", "store", "check_self_upgrade"}, call("check_self_upgrade"))
entry({"admin", "store", "do_self_upgrade"}, post("do_self_upgrade"))
entry({"admin", "store", "toggle_docker"}, post("toggle_docker"))
entry({"admin", "store", "toggle_arch"}, post("toggle_arch"))
entry({"admin", "store", "get_block_devices"}, call("get_block_devices"))
entry({"admin", "store", "configured"}, call("configured"))
entry({"admin", "store", "entrysh"}, post("entrysh"))
-- docker
entry({"admin", "store", "docker_check_dir"}, call("docker_check_dir"))
entry({"admin", "store", "docker_check_migrate"}, call("docker_check_migrate"))
entry({"admin", "store", "docker_migrate"}, post("docker_migrate"))
-- package
for _, action in ipairs({"update", "install", "upgrade", "remove", "autoconf"}) do
store_api(action, true)
end
for _, action in ipairs({"status", "installed"}) do
store_api(action, false)
end
-- backup
if nixio.fs.access("/usr/libexec/istore/backup") then
entry({"admin", "store", "get_support_backup_features"}, call("get_support_backup_features"))
entry({"admin", "store", "light_backup"}, post("light_backup"))
entry({"admin", "store", "get_light_backup_file"}, call("get_light_backup_file"))
entry({"admin", "store", "local_backup"}, post("local_backup"))
entry({"admin", "store", "light_restore"}, post("light_restore"))
entry({"admin", "store", "local_restore"}, post("local_restore"))
entry({"admin", "store", "get_backup_app_list_file_path"}, call("get_backup_app_list_file_path"))
entry({"admin", "store", "get_backup_app_list"}, call("get_backup_app_list"))
entry({"admin", "store", "get_available_backup_file_list"}, call("get_available_backup_file_list"))
entry({"admin", "store", "set_local_backup_dir_path"}, post("set_local_backup_dir_path"))
entry({"admin", "store", "get_local_backup_dir_path"}, call("get_local_backup_dir_path"))
end
end
local function user_id()
local jsonc = require "luci.jsonc"
local json_parse = jsonc.parse
local fs = require "nixio.fs"
local data = fs.readfile("/etc/.app_store.id")
local id
if data ~= nil then
id = json_parse(data)
end
if id == nil then
fs.unlink("/etc/.app_store.id")
id = {arch="",uid=""}
end
id.version = (fs.readfile("/etc/.app_store.version") or "?"):gsub("[\r\n]", "")
return id
end
local function user_config()
local uci = require "luci.model.uci".cursor()
local data = {
hide_docker = uci:get("istore", "istore", "hide_docker") == "1",
ignore_arch = uci:get("istore", "istore", "ignore_arch") == "1",
last_path = uci:get("istore", "istore", "last_path"),
super_arch = uci:get("istore", "istore", "super_arch"),
channel = uci:get("istore", "istore", "channel")
}
return data
end
local function vue_lang()
local i18n = require("luci.i18n")
local lang = i18n.translate("istore_vue_lang")
if lang == "istore_vue_lang" or lang == "" then
lang = "en"
end
return lang
end
local function flock(file, type)
local nixio = require "nixio"
local oflags = nixio.open_flags("wronly", "creat")
local lock, code, msg = nixio.open(file, oflags)
if not lock then
return nil, "Open lock failed: " .. msg
end
-- Acquire lock
local stat, code, msg = lock:lock(type)
if not stat then
lock:close()
return nil, "Lock failed: " .. msg
end
return lock, nil
end
local function is_exec(cmd, async)
local nixio = require "nixio"
local os = require "os"
local fs = require "nixio.fs"
local rshift = nixio.bit.rshift
local lock, msg = flock("/var/lock/istore.lock", "tlock")
if lock == nil then
return 255, "", msg
end
if async then
cmd = "/etc/init.d/tasks task_add istore " .. luci.util.shellquote(cmd)
end
local r = os.execute(cmd .. " >/var/log/istore.stdout 2>/var/log/istore.stderr")
local e = fs.readfile("/var/log/istore.stderr")
local o = fs.readfile("/var/log/istore.stdout")
fs.unlink("/var/log/istore.stderr")
fs.unlink("/var/log/istore.stdout")
lock:lock("ulock")
lock:close()
e = e or ""
if r == 256 and e == "" then
e = "os.execute exit code 1"
end
return rshift(r,8), o or "", e or ""
end
function redirect_index()
luci.http.redirect(luci.dispatcher.build_url(unpack(page_index)))
end
function store_index()
local fs = require "nixio.fs"
local features = { "_lua_force_array_" }
if fs.access("/usr/libexec/istore/backup") then
features[#features+1] = "backup"
end
if luci.sys.call("which docker >/dev/null 2>&1") == 0 then
features[#features+1] = "docker"
end
if luci.sys.call("[ -d /ext_overlay ] >/dev/null 2>&1") == 0 then
features[#features+1] = "sandbox"
end
if luci.sys.call("[ -f /www/luci-static/resources/luci.js ] >/dev/null 2>&1") == 0 then
features[#features+1] = "luci-js"
end
luci.template.render("store/main", {prefix=luci.dispatcher.build_url(unpack(page_index)),id=user_id(),lang=vue_lang(),user_config=user_config(),features=features})
end
function store_dev()
luci.template.render("store/main_dev", {prefix=luci.dispatcher.build_url(unpack({"admin", "store", "dev"})),id=user_id(),lang=vue_lang(),user_config=user_config()})
end
function store_log()
local fs = require "nixio.fs"
local code = 0
local e = fs.readfile("/var/log/istore.stderr")
local o = fs.readfile("/var/log/istore.stdout")
if o ~= nil then
code = 206
end
luci.http.prepare_content("application/json")
luci.http.write_json({code=code,stdout=o or "",stderr=e or ""})
end
function action_user_id()
luci.http.prepare_content("application/json")
luci.http.write_json(user_id())
end
function check_self_upgrade()
local ret = {
code = 500,
msg = "Unknown"
}
local r,o,e = is_exec(myopkg .. " check_self_upgrade")
if r ~= 0 then
ret.msg = e
else
ret.code = o == "" and 304 or 200
ret.msg = o:gsub("[\r\n]", "")
end
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function do_self_upgrade()
local code, out, err, ret
code,out,err = is_exec(myopkg .. " do_self_upgrade")
ret = {
code = code,
stdout = out,
stderr = err
}
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
-- Internal action function
local function _action(exe, cmd, ...)
local pkg = ""
for k, v in pairs({...}) do
pkg = pkg .. " " .. luci.util.shellquote(v)
end
local c = "%s %s %s" %{ exe, cmd, pkg }
return is_exec(c, true)
end
function validate_pkgname(val)
return (val ~= nil and val:match("^[a-zA-Z0-9_-]+$") ~= nil)
end
local function get_installed_and_cache()
local metadir = "/usr/lib/opkg/meta"
local cachedir = "/tmp/cache/istore"
local cachefile = cachedir .. "/installed.json"
local metapkgpre = "app-meta-"
local nixio = require "nixio"
local fs = require "nixio.fs"
local ipkg = require "luci.model.ipkg"
local jsonc = require "luci.jsonc"
local result = {}
local lock, msg = flock("/var/lock/istore-installed.lock", "lock")
local ms = fs.stat(metadir)
local cs = fs.stat(cachefile)
if not ms then
result = {}
elseif not cs or ms["mtime"] > cs["mtime"] then
local itr = fs.dir(metadir)
local data = {}
if itr then
local i18n = require("luci.i18n")
local pkg
for pkg in itr do
if pkg:match("^.*%.json$") then
local metadata = fs.readfile(metadir .. "/" .. pkg)
if metadata ~= nil then
local meta = jsonc.parse(metadata)
if meta == nil then
local name = pkg:gsub("^(.-)%.json$", "%1")
meta = {
name = name,
title = "{ " .. name .. " }",
author = "<UNKNOWN>",
version = "0.0.0",
description = i18n.translate("This package is broken! Please reinstall or uninstall it."),
depends = {},
tags = {"broken"},
broken = true,
}
end
local metapkg = metapkgpre .. meta.name
local status = ipkg.status(metapkg)
if next(status) ~= nil then
meta.time = tonumber(status[metapkg]["Installed-Time"])
data[#data+1] = meta
end
end
end
end
end
result = data
fs.mkdirr(cachedir)
local oflags = nixio.open_flags("rdwr", "creat")
local mfile, code, msg = nixio.open(cachefile, oflags)
mfile:writeall(jsonc.stringify(result))
mfile:close()
else
result = jsonc.parse(fs.readfile(cachefile) or "")
end
lock:lock("ulock")
lock:close()
return result
end
function store_action(param)
local metadir = "/usr/lib/opkg/meta"
local metapkgpre = "app-meta-"
local code, out, err, ret
local fs = require "nixio.fs"
local ipkg = require "luci.model.ipkg"
local jsonc = require "luci.jsonc"
local json_parse = jsonc.parse
local action = param.action or ""
if action == "status" then
local pkg = luci.http.formvalue("package")
if not validate_pkgname(pkg) then
luci.http.status(400, "Bad Request")
return
end
local metapkg = metapkgpre .. pkg
local meta = {}
local metadata = fs.readfile(metadir .. "/" .. pkg .. ".json")
if metadata ~= nil then
meta = json_parse(metadata) or {}
end
meta.installed = false
local status = ipkg.status(metapkg)
if next(status) ~= nil then
meta.installed=true
meta.time=tonumber(status[metapkg]["Installed-Time"])
end
ret = meta
elseif action == "installed" then
local data = get_installed_and_cache()
ret = data
else
local pkg = luci.http.formvalue("package")
if not validate_pkgname(pkg) then
luci.http.status(400, "Bad Request")
return
end
local metapkg = pkg and (metapkgpre .. pkg) or ""
if action == "update" or pkg then
if action == "update" or action == "install" or action == "autoconf" then
if (action == "install" and "1" == luci.http.formvalue("autoconf")) or action == "autoconf" then
local autoenv = "AUTOCONF=" .. pkg
local autopath = luci.http.formvalue("path")
local autoenable = luci.http.formvalue("enable")
if autopath ~= nil then
autoenv = autoenv .. " path=" .. luci.util.shellquote(autopath)
local uci = require "luci.model.uci".cursor()
uci:set("istore", "istore", "last_path", autopath)
uci:commit("istore")
end
if autoenable ~= nil then
autoenv = autoenv .. " enable=" .. autoenable
end
code, out, err = _action(myopkg, luci.util.shellquote(autoenv), action, metapkg)
else
code, out, err = _action(myopkg, action, metapkg)
end
else
local meta = json_parse(fs.readfile(metadir .. "/" .. pkg .. ".json"))
local pkgs = {}
if meta == nil then
meta = {
depends = {},
}
end
if action == "upgrade" then
pkgs = meta.depends
table.insert(pkgs, metapkg)
code, out, err = _action(myopkg, action, unpack(pkgs))
else -- remove
for _, dep in ipairs(meta.depends) do
if dep ~= "docker-deps" and dep ~= "luci-js-deps" then
pkgs[#pkgs+1] = dep
end
end
table.insert(pkgs, metapkg)
code, out, err = _action(myopkg, action, unpack(pkgs))
fs.unlink("/tmp/luci-indexcache")
end
end
else
code = 400
err = "package is null"
end
ret = {
code = code,
stdout = out,
stderr = err
}
end
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function store_token()
luci.http.prepare_content("application/json")
require "luci.template".render_string("{\"token\":\"<%=token%>\"}")
end
function store_upload()
local fd
local path
local finished = false
local tmpdir = "/tmp/is-root/tmp"
luci.http.setfilehandler(
function(meta, chunk, eof)
if not fd then
path = tmpdir .. "/" .. meta.file
nixio.fs.mkdirr(tmpdir)
fd = io.open(path, "w")
end
if chunk then
fd:write(chunk)
end
if eof then
fd:close()
finished = true
end
end
)
local code, out, err
out = ""
if finished then
if string.lower(string.sub(path, -4, -1)) == ".run" then
code, out, err = _action("sh", "-c", "ls -l \"%s\"; md5sum \"%s\" 2>/dev/null; chmod 755 \"%s\" && \"%s\"; RET=$?; rm -f \"%s\"; exit $RET" %{ path, path, path, path, path })
else
code, out, err = _action("sh", "-c", "opkg install \"%s\"; RET=$?; rm -f \"%s\"; exit $RET" %{ path, path })
end
else
code = 500
err = "upload failed!"
end
--nixio.fs.unlink(path)
local ret = {
code = code,
stdout = out,
stderr = err
}
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function configured()
local uci = luci.http.formvalue("uci")
if not validate_pkgname(uci) then
luci.http.status(400, "Bad Request")
return
end
local configured = nixio.fs.access("/etc/config/" .. uci)
luci.http.prepare_content("application/json")
luci.http.write_json({code=200, configured=configured})
end
function entrysh()
local package = luci.http.formvalue("package")
local update = luci.http.formvalue("update")
local hostname = luci.http.formvalue("hostname")
if hostname == nil or hostname == "" or not hostname:match("^[a-zA-Z0-9_%[][a-zA-Z0-9_%-%.%:%]]*$") then
luci.http.status(400, "Bad Request")
return
end
local nixio = require "nixio"
local fs = require "nixio.fs"
local hostnameq = luci.util.shellquote(hostname)
local cachedir = "/tmp/cache/istore/entrysh/" .. hostname
fs.mkdirr(cachedir)
local jsonc = require "luci.jsonc"
local results = {}
local errors = {}
local force = update == "1"
local candidate = nil
if package ~= nil and package ~= "" then
candidate = luci.util.split(package, ",")
end
local installed = get_installed_and_cache()
local lock, msg = flock("/var/lock/istore-entrysh.lock", "lock")
local meta
for _, meta in ipairs(installed) do
if meta.flags ~= nil and meta.uci ~= nil and luci.util.contains(meta.flags, "entrysh")
and (candidate == nil or luci.util.contains(candidate, meta.name)) then
local entryfile = "/usr/libexec/istoree/" .. meta.name .. ".sh"
local ucifile = "/etc/config/" .. meta.uci
local cachefile = cachedir .. "/" .. meta.name .. ".json"
local status = nil
if not force then
local us = fs.stat(ucifile)
local cs = fs.stat(cachefile)
if cs ~= nil and us["mtime"] <= cs["mtime"] then
status = jsonc.parse(fs.readfile(cachefile) or "")
end
end
if status ~= nil then
results[#results+1] = status
elseif fs.access(entryfile) then
local o = luci.util.exec(entryfile .. " status " .. hostnameq)
if o == nil or o == "" then
errors[#errors+1] = {app=meta.name, code=500, msg="entrysh execute failed"}
else
status = jsonc.parse(o)
if status == nil then
errors[#errors+1] = {app=meta.name, code=500, msg="json parse failed: " .. o}
else
results[#results+1] = status
local oflags = nixio.open_flags("rdwr", "creat")
local mfile, code, msg = nixio.open(cachefile, oflags)
mfile:writeall(jsonc.stringify(status))
mfile:close()
end
end
else
errors[#errors+1] = {app=meta.name, code=404, msg="entrysh of this package not found"}
end
end
end
lock:lock("ulock")
lock:close()
luci.http.prepare_content("application/json")
luci.http.write_json({code=200, status=results, errors=errors})
end
function docker_check_dir()
local docker_on_system = luci.sys.call("/usr/libexec/istore/docker check_dir >/dev/null 2>&1") ~= 0
luci.http.prepare_content("application/json")
luci.http.write_json({code=200, docker_on_system=docker_on_system})
end
function docker_check_migrate()
local path = luci.http.formvalue("path")
if path == nil or path == "" then
luci.http.status(400, "Bad Request")
return
end
local r,o,e = is_exec("/usr/libexec/istore/docker migrate_check " .. luci.util.shellquote(path))
local result = "good"
if r == 1 then
result = "bad"
elseif r == 2 then
result = "existed"
end
luci.http.prepare_content("application/json")
luci.http.write_json({code=200, result=result, error=e})
end
function docker_migrate()
local path = luci.http.formvalue("path")
if path == nil or path == "" then
luci.http.status(400, "Bad Request")
return
end
local action = "migrate"
local overwrite = luci.http.formvalue("overwrite")
if overwrite == "chdir" then
action = "change_dir"
end
local r,o,e = is_exec("/usr/libexec/istore/docker " .. action .. " " .. luci.util.shellquote(path), true)
luci.http.prepare_content("application/json")
luci.http.write_json({code=r, stdout=o, stderr=e})
end
local function split(str,reps)
local resultStrList = {}
string.gsub(str,'[^'..reps..']+',function (w)
table.insert(resultStrList,w)
end)
return resultStrList
end
local function ltn12_popen(command)
local fdi, fdo = nixio.pipe()
local pid = nixio.fork()
if pid > 0 then
fdo:close()
local close
return function()
local buffer = fdi:read(2048)
local wpid, stat = nixio.waitpid(pid, "nohang")
if not close and wpid and stat == "exited" then
close = true
end
if buffer and #buffer > 0 then
return buffer
elseif close then
fdi:close()
return nil
end
end
elseif pid == 0 then
nixio.dup(fdo, nixio.stdout)
fdi:close()
fdo:close()
nixio.exec("/bin/sh", "-c", command)
end
end
-- call get_support_backup_features
function get_support_backup_features()
local jsonc = require "luci.jsonc"
local error_ret = {code = 500, msg = "Unknown"}
local success_ret = {code = 200, msg = "Unknown"}
local r,o,e = is_exec(is_backup .. " get_support_backup_features")
if r ~= 0 then
error_ret.msg = e
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
success_ret.code = 200
success_ret.msg = jsonc.stringify(split(o,'\n'))
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
end
end
-- post light_backup
function light_backup()
local jsonc = require "luci.jsonc"
local error_ret = {code = 500, msg = "Unknown"}
local success_ret = {code = 200,msg = "Unknown"}
local r,o,e = is_exec(is_backup .. " backup")
if r ~= 0 then
error_ret.msg = e
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
success_ret.code = 200
success_ret.msg = o:gsub("[\r\n]", "")
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
end
end
-- call get_light_backup_file
function get_light_backup_file()
local light_backup_cmd = "tar -c %s | gzip 2>/dev/null"
local loght_backup_filelist = "/etc/istore/app.list"
local reader = ltn12_popen(light_backup_cmd:format(loght_backup_filelist))
luci.http.header('Content-Disposition', 'attachment; filename="light-backup-%s-%s.tar.gz"' % {
luci.sys.hostname(), os.date("%Y-%m-%d")})
luci.http.prepare_content("application/x-targz")
luci.ltn12.pump.all(reader, luci.http.write)
end
local function update_local_backup_path(path)
local uci = require "uci"
local fs = require "nixio.fs"
local x = uci.cursor()
local local_backup_path
if fs.access("/etc/config/istore") then
local_backup_path = x:get("istore","istore","local_backup_path")
else
--create config file
local f=io.open("/etc/config/istore","a+")
f:write("config istore \'istore\'\n\toption local_backup_path \'\'")
f:flush()
f:close()
end
if path ~= local_backup_path then
-- set uci config
x:set("istore","istore","local_backup_path",path)
x:commit("istore")
end
end
-- post local_backup
function local_backup()
local code, out, err, ret
local error_ret
local path = luci.http.formvalue("path")
if path ~= "" then
-- judge path
code,out,err = is_exec("findmnt -T " .. path .. " -o TARGET|sed -n 2p")
if out:gsub("[\r\n]", "") == "/" or out:gsub("[\r\n]", "") == "/tmp" then
-- error
error_ret = {code = 500, stderr = "Path Error,Can not be / or tmp."}
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
-- update local backup path
update_local_backup_path(path)
code,out,err = _action(is_backup, "backup", path)
ret = {
code = code,
stdout = out,
stderr = err
}
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
else
-- error
error_ret = {code = 500, stderr = "Path Unknown"}
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
end
-- post light_restore
function light_restore()
local fd
local path
local finished = false
local tmpdir = "/tmp/"
luci.http.setfilehandler(
function(meta, chunk, eof)
if not fd then
path = tmpdir .. "/" .. meta.file
fd = io.open(path, "w")
end
if chunk then
fd:write(chunk)
end
if eof then
fd:close()
finished = true
end
end
)
local code, out, err, ret
if finished then
is_exec("rm /etc/istore/app.list;tar -xzf " .. path .. " -C /")
nixio.fs.unlink(path)
if nixio.fs.access("/etc/istore/app.list") then
code,out,err = _action(is_backup, "restore")
ret = {
code = code,
stdout = out,
stderr = err
}
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
else
local error_ret = {code = 500, stderr = "File is error!"}
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
else
ret = {code = 500, stderr = "upload failed!"}
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
end
-- post local_restore
function local_restore()
local path = luci.http.formvalue("path")
local code, out, err, ret
if path ~= "" then
code,out,err = _action(is_backup, "restore", path)
ret = {
code = code,
stdout = out,
stderr = err
}
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
else
-- error
error_ret = {code = 500, stderr = "Path Unknown"}
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
end
-- call get_backup_app_list_file_path
function get_backup_app_list_file_path()
local jsonc = require "luci.jsonc"
local error_ret = {code = 500, msg = "Unknown"}
local success_ret = {code = 200,msg = "Unknown"}
local r,o,e = is_exec(is_backup .. " get_backup_app_list_file_path")
if r ~= 0 then
error_ret.msg = e
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
success_ret.code = 200
success_ret.msg = o:gsub("[\r\n]", "")
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
end
end
-- call get_backup_app_list
function get_backup_app_list()
local jsonc = require "luci.jsonc"
local error_ret = {code = 500, msg = "Unknown"}
local success_ret = {code = 200,msg = "Unknown"}
local r,o,e = is_exec(is_backup .. " get_backup_app_list")
if r ~= 0 then
error_ret.msg = e
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
success_ret.code = 200
success_ret.msg = jsonc.stringify(split(o,'\n'))
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
end
end
-- call get_available_backup_file_list
function get_available_backup_file_list()
local jsonc = require "luci.jsonc"
local error_ret = {code = 500, msg = "Unknown"}
local success_ret = {code = 200,msg = "Unknown"}
local path = luci.http.formvalue("path")
local r,o,e
if path ~= "" then
-- update local backup path
update_local_backup_path(path)
r,o,e = is_exec(is_backup .. " get_available_backup_file_list " .. luci.util.shellquote(path))
if r ~= 0 then
error_ret.msg = e
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
success_ret.code = 200
success_ret.msg = jsonc.stringify(split(o,'\n'))
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
end
else
-- set error code
error_ret.msg = "Path Unknown"
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
end
-- post set_local_backup_dir_path
function set_local_backup_dir_path()
local path = luci.http.formvalue("path")
local success_ret = {code = 200, msg = "Success"}
local error_ret = {code = 500, msg = "Unknown"}
if path ~= "" then
-- update local backup path
update_local_backup_path(path)
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
else
-- set error code
error_ret.msg = "Path Unknown"
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
end
-- call get_local_backup_dir_path
function get_local_backup_dir_path()
local uci = require "uci"
local fs = require "nixio.fs"
local x = uci.cursor()
local local_backup_path = nil
local success_ret = {code = 200,msg = "Unknown"}
local error_ret = {code = 500, msg = "Path Unknown"}
if fs.access("/etc/config/istore") then
local_backup_path = x:get("istore","istore","local_backup_path")
if local_backup_path == nil then
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
else
success_ret.msg = local_backup_path:gsub("[\r\n]", "")
luci.http.prepare_content("application/json")
luci.http.write_json(success_ret)
end
else
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
end
-- copy from /usr/lib/lua/luci/model/diskman.lua
local function byte_format(byte)
local suff = {"B", "KB", "MB", "GB", "TB"}
for i=1, 5 do
if byte > 1024 and i < 5 then
byte = byte / 1024
else
return string.format("%.2f %s", byte, suff[i])
end
end
end
-- copy from /usr/libexec/rpcd/luci
local function getBlockDevices()
local fs = require "nixio.fs"
local block = io.popen("/sbin/block info", "r")
if block then
local rv = {}
while true do
local ln = block:read("*l")
if not ln then
break
end
local dev = ln:match("^/dev/(.-):")
if dev then
local s = tonumber((fs.readfile("/sys/class/block/" .. dev .."/size")))
local e = {
dev = "/dev/" .. dev,
size = s and byte_format(s * 512)
}
local key, val = { }
for key, val in ln:gmatch([[(%w+)="(.-)"]]) do
e[key:lower()] = val
end
rv[dev] = e
end
end
block:close()
return rv
else
return
end
end
function get_block_devices()
local error_ret = {code = 500, msg = "Unable to execute block utility"}
local devices = getBlockDevices()
if devices ~= nil then
luci.http.prepare_content("application/json")
luci.http.write_json({code = 200, data = devices})
else
luci.http.prepare_content("application/json")
luci.http.write_json(error_ret)
end
end
function toggle_docker()
local uci = require "luci.model.uci".cursor()
local hide = luci.http.formvalue("hide")
uci:set("istore", "istore", "hide_docker", hide == "true" and "1" or "0")
uci:commit("istore")
luci.http.prepare_content("application/json")
luci.http.write_json({code = 200, msg = "Success"})
end
function toggle_arch()
local uci = require "luci.model.uci".cursor()
local ignore = luci.http.formvalue("ignore")
uci:set("istore", "istore", "ignore_arch", ignore == "true" and "1" or "0")
uci:commit("istore")
luci.http.prepare_content("application/json")
luci.http.write_json({code = 200, msg = "Success"})
end

View File

@ -0,0 +1,63 @@
<%+header%>
<%
local jsonc = require "luci.jsonc"
%>
<script>
(function(){
var vue_prefix="<%=prefix%>";
var myurl = window.location.pathname;
window.addEventListener('popstate', function(){
if (myurl != window.location.pathname
&& window.location.pathname != vue_prefix
&& ! window.location.pathname.startsWith(vue_prefix+'/')) {
window.location.href = window.location.pathname;
}
});
window.vue_base = vue_prefix + '/';
// window.istore_api_base = "https://istore.istoreos.com";
window.vue_lang_data = '/luci-static/istore/i18n/<%=lang%>.json?v=<%=id.version%>';
window.vue_lang = '<%=lang%>';
window.token = "<%=token%>";
window.device_id = {arch:"<%=id.arch%>",uid:"<%=id.uid%>",version:"<%=id.version%>"};
window.istore_features = <%=jsonc.stringify(features)%>.filter(f => f !== '_lua_force_array_');
window.istore_config = <%=jsonc.stringify(user_config or {})%>;
})();
</script>
<h2 name="content"><%:iStore%>
<a onclick="void(0)" href="https://github.com/linkease/istore/issues/22" target="_blank" style="text-decoration: none;">
v<%=id.version%>
</a>
</h2>
<link rel="stylesheet" href="/luci-static/istore/style.css?v=<%=id.version%>">
<div id="app">
</div>
<%+tasks/embed%>
<script>
(function() {
let beforeunloadRegistered = false;
taskd.show_mask_on_stopped = true;
window.istore_log = function(flush_menu_onclose, onExit) {
if (flush_menu_onclose && !beforeunloadRegistered) {
beforeunloadRegistered = true;
window.addEventListener("beforeunload", function(event) {
try { window.L.ui.menu.flushCache() } catch (e) { }
return true;
});
}
taskd.show_log("istore", true, onExit);
};
})();
<%
local taskd = require "luci.model.tasks"
local status = taskd.status("istore")
if status.running or status.exit_code ~= 404 then
-%>
window.istore_log(true);
<%
end
%>
</script>
<script type="module" crossorigin src="/luci-static/istore/index.js?v=<%=id.version%>"></script>
<%+footer%>

View File

@ -0,0 +1,291 @@
#!/bin/sh
# this script MUST supports executting without luci-app-store installed,
# so we can use this script to install luci-app-store itself
action=${1}
shift
if [ "${action:0:9}" = "AUTOCONF=" ]; then
export "ISTORE_${action}"
exec "$0" "$@"
fi
IS_ROOT=/tmp/is-root
DL_DIR=${IS_ROOT}/tmp/dl
LISTS_DIR_O=/tmp/opkg-lists
LISTS_DIR=${IS_ROOT}${LISTS_DIR_O}
OPKG_CONF_DIR=${IS_ROOT}/etc/opkg
OPKG_CONF_DIR_M=${IS_ROOT}/etc/opkg_m
FEEDS_SERVER=https://istore.istoreos.com/repo
FEEDS_SERVER_MIRRORS="https://repo.istoreos.com/repo"
DISABLE_MIRROR=false
ARCH=`sed -n -e 's/^Architecture: *\([^ ]\+\) *$/\1/p' /rom/usr/lib/opkg/info/libc.control /usr/lib/opkg/info/libc.control 2>/dev/null | head -1`
# for istore self upgrade
ISTORE_PKG=luci-app-store
ISTORE_DEP_PKGS="luci-lib-taskd luci-lib-xterm taskd"
ISTORE_INDEX=https://istore.istoreos.com/repo/all/store/Packages.gz
is_init() {
mkdir -p ${DL_DIR} ${LISTS_DIR} ${IS_ROOT}/etc ${IS_ROOT}/var
cat /etc/opkg.conf | grep -Fv lists_dir | grep -Fv check_signature > ${IS_ROOT}/etc/opkg.conf
cp ${IS_ROOT}/etc/opkg.conf ${IS_ROOT}/etc/opkg_o.conf
echo >> ${IS_ROOT}/etc/opkg.conf
echo "lists_dir ext ${LISTS_DIR}" >> ${IS_ROOT}/etc/opkg.conf
# create opkg_o.conf for executting 'opkg update' with offline-root, so we don't overwrite system opkg list
echo >> ${IS_ROOT}/etc/opkg_o.conf
echo "lists_dir ext ${LISTS_DIR_O}" >> ${IS_ROOT}/etc/opkg_o.conf
cp -au /etc/opkg ${IS_ROOT}/etc/
[ -e ${IS_ROOT}/var/lock ] || ln -s /var/lock ${IS_ROOT}/var/lock
}
opkg_wrap() {
OPKG_CONF_DIR=${OPKG_CONF_DIR} opkg -f ${IS_ROOT}/etc/opkg.conf "$@"
}
opkg_wrap_mirrors() {
local server
local file
if ! $DISABLE_MIRROR; then
for server in $FEEDS_SERVER_MIRRORS ; do
rm -rf "${OPKG_CONF_DIR_M}" 2>/dev/null
mkdir -p "${OPKG_CONF_DIR_M}" 2>/dev/null
ls "${OPKG_CONF_DIR}/" | while read; do
file="$REPLY"
if [ -f "${OPKG_CONF_DIR}/$file" -a "${file: -5}" = ".conf" ]; then
sed "s#$FEEDS_SERVER/#$server/#g" "${OPKG_CONF_DIR}/$file" >"${OPKG_CONF_DIR_M}/$file"
touch -r "${OPKG_CONF_DIR}/$file" "${OPKG_CONF_DIR_M}/$file" 2>/dev/null
else
cp -a "${OPKG_CONF_DIR}/$file" "${OPKG_CONF_DIR_M}/"
fi
done
echo "Try mirror server $server"
OPKG_CONF_DIR=${OPKG_CONF_DIR_M} opkg -f ${IS_ROOT}/etc/opkg.conf "$@" && return 0
done
DISABLE_MIRROR=true
fi
echo "Try origin server $FEEDS_SERVER"
OPKG_CONF_DIR=${OPKG_CONF_DIR} opkg -f ${IS_ROOT}/etc/opkg.conf "$@"
}
alias fcurl='curl -L --fail --show-error'
check_space() {
local free="$((`df -kP / | awk 'NR==2 {print $4}'` >> 10 ))"
if [ "$free" -lt 1 ]; then
echo "Root disk full!" >&2
exit 1
fi
return 0
}
update() {
if [ -z "${ARCH}" ]; then
echo "Get architecture failed" >&2
return 1
fi
echo "Fetch feed list for ${ARCH}"
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/meta.conf "${FEEDS_SERVER}/all/meta.conf" && \
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/all.conf "${FEEDS_SERVER}/all/isfeeds.conf" && \
fcurl --no-progress-meter -o ${OPKG_CONF_DIR}/arch.conf "${FEEDS_SERVER}/${ARCH}/isfeeds.conf" || \
return 1
echo "Update feeds index"
opkg -f ${IS_ROOT}/etc/opkg_o.conf --offline-root ${IS_ROOT} update
return 0
}
update_if_outdate() {
local idle_t=$((`date '+%s'` - `date -r ${IS_ROOT}/.last_force_ts '+%s' 2>/dev/null || echo '0'`))
[ $idle_t -gt ${1:-120} ] || return 2
update || return 1
touch ${IS_ROOT}/.last_force_ts
return 0
}
check_self_upgrade() {
local newest=`curl --connect-timeout 2 --max-time 5 -s ${ISTORE_INDEX} | gunzip | grep -FA10 "Package: ${ISTORE_PKG}" | grep -Fm1 'Version: ' | sed 's/^Version: //'`
local current=`grep -Fm1 'Version: ' /usr/lib/opkg/info/${ISTORE_PKG}.control | sed 's/^Version: //'`
if [ "v$newest" = "v" -o "v$current" = "v" ]; then
echo "Check version failed!" >&2
exit 255
fi
if [ "$newest" != "$current" ]; then
echo "$newest"
fi
return 0
}
do_self_upgrade_0() {
opkg_wrap upgrade ${ISTORE_DEP_PKGS} && opkg_wrap upgrade ${ISTORE_PKG}
}
do_self_upgrade() {
check_mtime || return 1
local newest=`curl --connect-timeout 2 --max-time 5 -s ${ISTORE_INDEX} | gunzip | grep -FA10 "Package: ${ISTORE_PKG}" | grep -Fm1 'Version: ' | sed 's/^Version: //'`
local current=`grep -Fm1 'Version: ' /usr/lib/opkg/info/${ISTORE_PKG}.control | sed 's/^Version: //'`
if [ "v$newest" = "v" -o "v$current" = "v" ]; then
echo "Check version failed!" >&2
return 1
fi
if [ "$newest" = "$current" ]; then
echo "Already the latest version!" >&2
return 1
fi
if opkg_wrap info ${ISTORE_PKG} | grep -qFm1 "Version: $newest"; then
do_self_upgrade_0 && return 0
update_if_outdate || return 1
do_self_upgrade_0
else
update_if_outdate || return 1
do_self_upgrade_0
fi
}
check_mtime() {
find ${OPKG_CONF_DIR}/arch.conf -mtime -1 2>/dev/null | grep -q . || update
}
wrapped_in_update() {
check_mtime || return 1
eval "$@" && return 0
update_if_outdate || return 1
eval "$@"
}
step_upgrade() {
local pkg
local pkgs=""
local metapkg=""
for pkg in $@; do
if [[ $pkg == app-meta-* ]]; then
metapkg="$metapkg $pkg"
else
pkgs="$pkgs $pkg"
fi
done
if [ -n "$pkgs" ]; then
opkg_wrap_mirrors upgrade $pkgs || return 1
fi
if [ -n "$metapkg" ]; then
opkg_wrap_mirrors upgrade $metapkg || return 1
fi
return 0
}
new_upgrade() {
check_mtime || return 1
local metapkg=`echo "$@" | sed 's/ /\n/g' | grep -F app-meta-`
if [ -z "$metapkg" ] || opkg_wrap info $metapkg | grep -qF not-installed ; then
true
else
update_if_outdate
fi
wrapped_in_update step_upgrade "$@"
}
remove() {
opkg_wrap --autoremove --force-removal-of-dependent-packages remove "$@"
}
autoconf_to_env() {
local autoconf path enable
eval "local autoconf=$ISTORE_AUTOCONF"
export -n ISTORE_AUTOCONF
export -n ISTORE_DONT_START
export -n ISTORE_CONF_DIR
export -n ISTORE_CACHE_DIR
export -n ISTORE_PUBLIC_DIR
export -n ISTORE_DL_DIR
ISTORE_AUTOCONF=$autoconf
if [ -n "$path" ]; then
export ISTORE_CONF_DIR="$path/Configs"
export ISTORE_CACHE_DIR="$path/Caches"
export ISTORE_PUBLIC_DIR="$path/Public"
export ISTORE_DL_DIR="$ISTORE_PUBLIC_DIR/Downloads"
fi
[ "$enable" = 0 ] && export ISTORE_DONT_START="1"
}
try_autoconf() {
[ -n "$ISTORE_AUTOCONF" ] || return 0
autoconf_to_env
[ -n "$ISTORE_AUTOCONF" ] || return 1
echo "Auto configure $ISTORE_AUTOCONF"
/usr/libexec/istorea/${ISTORE_AUTOCONF}.sh
}
try_upgrade_depends() {
local pkg="$1"
if [[ $pkg == app-meta-* ]]; then
local deps=$(grep '^Depends: ' /usr/lib/opkg/info/$pkg.control | busybox sed -e 's/^Depends: //' -e 's/,/\n/g' -e 's/ //g' | grep -vFw libc | xargs echo)
[ -z "$deps" ] || opkg_wrap_mirrors install $deps
fi
return 0
}
usage() {
echo "usage: is-opkg sub-command [arguments...]"
echo "where sub-command is one of:"
echo " update Update list of available packages"
echo " upgrade <pkgs> Upgrade package(s)"
echo " install <pkgs> Install package(s)"
echo " remove <pkgs|regexp> Remove package(s)"
echo " info [pkg|regexp] Display all info for <pkg>"
echo " list-upgradable List installed and upgradable packages"
echo " check_self_upgrade Check iStore upgrade"
echo " do_self_upgrade Upgrade iStore"
echo " arch Show libc architecture"
echo " opkg sys opkg wrap"
}
is_init >/dev/null 2>&1
case $action in
"update")
update
;;
"install")
check_space
wrapped_in_update opkg_wrap_mirrors install "$@" && try_upgrade_depends "$1" && try_autoconf
;;
"autoconf")
try_autoconf
;;
"upgrade")
new_upgrade "$@"
;;
"remove")
remove "$@" || remove "$@"
;;
"info")
opkg_wrap info "$@"
;;
"list-upgradable")
opkg_wrap list-upgradable
;;
"check_self_upgrade")
check_self_upgrade
;;
"do_self_upgrade")
check_space
do_self_upgrade
;;
"arch")
echo "$ARCH"
;;
"opkg")
opkg_wrap "$@"
;;
*)
usage
;;
esac

View File

@ -0,0 +1,7 @@
config istore 'istore'
# option hide_docker '0'
# option ignore_arch '0'
# option last_path '/mnt/nvme'
# option channel 'istore'
# option super_arch 'x86_64'
# option super_arch 'aarch64'

View File

@ -0,0 +1,54 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2016 OpenWrt.org
START=45
boot() {
local ARCH=`/bin/is-opkg arch`
generate_store_id $ARCH
generate_super_arch $ARCH
}
generate_store_id() {
[ -s /etc/.app_store.id ] && return 0
local ARCH=$1
local iface HASH
for iface in eth0 br-lan; do
if [ -e /sys/class/net/$iface/address ]; then
HASH=`md5sum /sys/class/net/$iface/address | cut -d ' ' -f1`
break
fi
done
if [ -z "$HASH" ]; then
HASH=`dd if=/dev/urandom bs=512 count=1 2>/dev/null | md5sum | cut -d ' ' -f1`
fi
echo "{\"arch\":\"${ARCH}\", \"uid\":\"${HASH}\"}" > /etc/.app_store.id
}
generate_super_arch() {
local ARCH=$1
local super_arch
case "$ARCH" in
arm*)
super_arch="arm"
;;
i386)
super_arch="x86"
;;
x86_64)
super_arch="x86_64"
;;
*)
super_arch="${ARCH%%_*}"
;;
esac
local old=`uci -q get istore.istore.super_arch`
[ "$old" = "$super_arch" ] && return 0
uci -q batch <<-EOF >/dev/null
set istore.istore.super_arch=$super_arch
commit istore
EOF
}

View File

@ -0,0 +1,12 @@
#!/bin/sh
/etc/init.d/istore boot
if [ -z "`uci -q get istore.istore.hide_docker`" ] && ! which docker >/dev/null; then
uci -q batch <<-EOF >/dev/null
set istore.istore.hide_docker=1
commit istore
EOF
fi
rm -rf /tmp/luci-indexcache /tmp/luci-modulecache

View File

@ -0,0 +1,295 @@
#!/bin/sh
#set -x
#IS_DEBUG=1
IS_ROOT=/tmp/is-backup
APP_LIST_FILE=/etc/istore/app.list
BACKUP_CONFIG_FILE=/etc/config/istore
action=${1}
shift
is_init() {
mkdir -p ${IS_ROOT}
}
opkg_list_installed_packages() {
target=$1
case $target in
"preinstalled")
OPKG_INFO_DIR="/rom/usr/lib/opkg/info"
;;
"userinstalled")
OPKG_INFO_DIR="/overlay/upper/usr/lib/opkg/info"
;;
"allinstalled")
OPKG_INFO_DIR="/usr/lib/opkg/info"
;;
*)
echo "invalid target"
exit
;;
esac
(cd $OPKG_INFO_DIR && find . -depth -maxdepth 1 -name "*.list" -type f | sed 's#^\./\(.*\)\.list$#\1#g')
}
ipk_build() {
PKG_NAME_TEMP=$1
IPK_OUTPUT_DIR=$2
UCI_BAK_DIR="/etc/istore/uci-defaults_bak/"
UCI_DEF_DIR="etc/uci-defaults"
OPKG_INFO_DIR="/usr/lib/opkg/info/"
[ -n "${PKG_NAME_TEMP}" ] || exit 1
#get real pkg name in opkg
PKG_NAME_TEMP=`cat ${IS_ROOT}/all_installed_package.list | sort -u | grep "^${PKG_NAME_TEMP}" | head -n 1`
[ -n "${PKG_NAME_TEMP}" ] || exit 1
PKG_NAME=`cat ${OPKG_INFO_DIR}${PKG_NAME_TEMP}.control | grep "^Package: " | cut -d ' ' -f2`
PKG_VER=`cat ${OPKG_INFO_DIR}${PKG_NAME}.control | grep "^Version: " | cut -d ' ' -f2`
PKG_ARCH=`cat ${OPKG_INFO_DIR}${PKG_NAME}.control | grep "^Architecture: " | cut -d ' ' -f2`
IPK_FILE_NAME="${PKG_NAME}_${PKG_VER}_${PKG_ARCH}"
rm -rf ${IS_ROOT}/${IPK_FILE_NAME}
mkdir -p ${IS_ROOT}/${IPK_FILE_NAME}
#(1)make CONTROL dir; (2)copy control file to dir
cd ${IS_ROOT}/${IPK_FILE_NAME}
mkdir -p CONTROL
for control_file in `ls ${OPKG_INFO_DIR}${PKG_NAME}.* | grep -v ".list$"`; do
file=${control_file##*/}
suffix=${file##*.}
cp ${control_file} CONTROL/${suffix}
done
#(1)make DATA depend dir; (2)copy uci-defaults_bak file to dir; (3)copy other file to dir
for pkgfile in `cat ${OPKG_INFO_DIR}${PKG_NAME}.list | cut -b 2-`; do
file=${pkgfile##*/}
path=${pkgfile%/*}
mkdir -p ${path}
if [ `echo "${path}" | grep "^${UCI_DEF_DIR}"` ]; then
cp "${UCI_BAK_DIR}${file}" "${pkgfile}"
else
cp "/${pkgfile}" "${pkgfile}"
fi
done
#call ipkg-build script to build ipk
/usr/libexec/istore/ipkg-build ${IS_ROOT}/${IPK_FILE_NAME} ${IPK_OUTPUT_DIR}
echo "${IPK_FILE_NAME}.ipk" >> ${IPK_OUTPUT_DIR}/appdepipk.list
[ -n "${IS_DEBUG}" ] || rm -rf ${IS_ROOT}/${IPK_FILE_NAME}
}
# if arg is NULL, use light backup, otherwise use local backup
backup() {
[ -n "$1" ] && BACKUP_PATH=$1
#1.add all istore self data to sysupgrade config file,
#sysupgrade will backup/restore it auto when flash new firmware
echo "/etc/.app_store.id" > /lib/upgrade/keep.d/luci-app-store
cat /usr/lib/opkg/info/luci-app-store.list >> /lib/upgrade/keep.d/luci-app-store
echo "/etc/rc.d/S45istore" >> /lib/upgrade/keep.d/luci-app-store
echo "/etc/istore/uci-defaults_bak" >> /lib/upgrade/keep.d/luci-app-store
echo "${APP_LIST_FILE}" >> /lib/upgrade/keep.d/luci-app-store
echo "${BACKUP_CONFIG_FILE}" >> /lib/upgrade/keep.d/luci-app-store
#write user installed package list to file
opkg_list_installed_packages "userinstalled" 2>/dev/null | sort -u > ${IS_ROOT}/user_installed_package.list
#write installed package list by istore feed to file
cat ${IS_ROOT}/user_installed_package.list | \
grep '^app-meta-' > ${IS_ROOT}/istore_installed_package.list
#if no input backup path, only back app.list
mkdir -p /etc/istore
cp ${IS_ROOT}/istore_installed_package.list ${APP_LIST_FILE}
echo "backup installed package list to ${APP_LIST_FILE}"
if [ ! -n "${BACKUP_PATH}" ]; then
echo "backup success"
exit 0
fi
if [ ! -d "${BACKUP_PATH}" ] && ! mkdir -p "${BACKUP_PATH}" ; then
echo "invalid backup path, can not backup ipk"
exit 1
fi
#write all installed package list to file
opkg_list_installed_packages "allinstalled" 2>/dev/null | sort -u > ${IS_ROOT}/all_installed_package.list
#write system pre installed package list to file
opkg_list_installed_packages "preinstalled" 2>/dev/null | sort -u > ${IS_ROOT}/pre_installed_package.list
#write installed packages and depends list by istore feed to file by depend sequence
appdep_list=""
temp_list=`cat ${IS_ROOT}/istore_installed_package.list | sed 's/^/\t/'`
while [ -n "${temp_list}" ]
do
#get real pkg name
for PKG_NAME_TEMP in ${temp_list}; do
REAL_PKG_NAME=`cat ${IS_ROOT}/all_installed_package.list | sort -u | grep "^${PKG_NAME_TEMP}" | head -n 1`
if [ "${REAL_PKG_NAME}" != "${PKG_NAME_TEMP}" ]; then
temp_list=`echo "${temp_list}" | sed 's/^\t'"${PKG_NAME_TEMP}"'$/\t'"${REAL_PKG_NAME}"'/'`
fi
done
appdep_list=`echo -e "${temp_list}\n${appdep_list}"`
[ -n "${IS_DEBUG}" ] && echo -e "temp_list:\n""${temp_list}"
[ -n "${IS_DEBUG}" ] && echo -e "appdep_list:\n""${appdep_list}"
temp_list=`echo "${temp_list}" | xargs opkg depends | grep -v "depends on:" | grep -v " (>= " | grep -v " (= " | sort -u`
done
appdep_list_all=`echo "${appdep_list}" | cut -f2 | grep -v "^$" | awk '!seen[$0]++'`
[ -n "${IS_DEBUG}" ] && echo -e "appdep_list_all:\n""${appdep_list_all}"
echo "${appdep_list_all}" > ${IS_ROOT}/appdep.list
#3.rebuild all istore installed package to ipk and backup to userdata partation
# 4. create dir
date=$(date +%Y-%m%d-%H%M)
if [ ! -d "$BACKUP_PATH/backup_istore_$date" ];then
mkdir $BACKUP_PATH/backup_istore_$date
fi
cp ${IS_ROOT}/istore_installed_package.list $BACKUP_PATH/backup_istore_$date/app.list
cp ${IS_ROOT}/appdep.list $BACKUP_PATH/backup_istore_$date/appdep.list
#only backup non pre installed ipk
cp ${IS_ROOT}/appdep.list ${IS_ROOT}/appdep_strip.list
for pre_installed_pkg in `cat ${IS_ROOT}/appdep.list ${IS_ROOT}/pre_installed_package.list | sort -n | uniq -d`; do
sed -i '/^'"$pre_installed_pkg"'$/d' ${IS_ROOT}/appdep_strip.list
done
rm -f $BACKUP_PATH/backup_istore_$date/appdepipk.list
echo "build ipk"
for pkg_name in `cat ${IS_ROOT}/appdep_strip.list`; do
ipk_build ${pkg_name} $BACKUP_PATH/backup_istore_$date
done
# 5. create tar.gz file,and remove fir
cd $BACKUP_PATH
echo "write backup file to $BACKUP_PATH/backup_istore_$date.backup.tar.gz"
tar -czf $BACKUP_PATH/backup_istore_$date.backup.tar.gz backup_istore_$date
rm -rf $BACKUP_PATH/backup_istore_$date
echo "backup success"
}
# if arg is NULL, use light backup, otherwise use local backup
restore() {
if [ -n "$1" ]; then
BACKUP_PATH_FILE=$1
else
echo "install package by ${APP_LIST_FILE}"
is-opkg update
for app in `cat ${APP_LIST_FILE}`; do
#skip resotre istore self
[ "A${app}" == "A""luci-app-store" ] && continue
is-opkg install ${app}
done
exit 0
fi
if [ ! -f "${BACKUP_PATH_FILE}" ];then
echo "invalid backup file, can not restore ipk"
exit 1
fi
#1. Unzip file to dir
BACKUP_PATH_FILE_NAME=${BACKUP_PATH_FILE##*/}
BACKUP_PATH=/tmp/${BACKUP_PATH_FILE_NAME%.backup.tar.gz*}
if [ -d "$BACKUP_PATH" ];then
rm -rf $BACKUP_PATH
fi
mkdir -p $BACKUP_PATH
echo "unpack input file..."
# fix tar path error
tar -zxf ${BACKUP_PATH_FILE} -C /tmp/
echo "check file"
if [ ! -f "${BACKUP_PATH}/appdep.list" ];then
echo "no available appdep.list, can not restore ipk"
exit 1
fi
echo "check success"
#2. install ipk by backup path
echo "restore begin"
( cd ${BACKUP_PATH}; opkg install `cat ${BACKUP_PATH}/appdepipk.list` )
#3. rm dir
rm -rf ${BACKUP_PATH}
echo "restore success"
}
get_support_backup_features() {
echo "light_backup"
#istore custom img mean support local_backup
if [ -f /etc/istore_img_flag ];then
echo "local_backup"
fi
}
get_backup_app_list_file_path() {
echo "${APP_LIST_FILE}"
}
get_backup_app_list() {
if [ ! -f "${APP_LIST_FILE}" ];then
echo "no app.list, can not get backup app list"
exit 1
fi
cat ${APP_LIST_FILE}
}
get_available_backup_file_list() {
if [ -n "$1" ]; then
for backup_file in `ls $1/*.backup.tar.gz`; do
filename=${backup_file##*/}
echo "${filename}"
done
else
echo "input backup path is null"
exit 1
fi
}
usage() {
echo "usage: backup sub-command [arguments...]"
echo "where sub-command is one of:"
echo " backup [dir] Backup all installed package(s) to [directory]"
echo " restore [dir] Restore package(s) by [directory]"
echo " get_support_backup_features get device support backup features"
echo " get_backup_app_list_file_path get light backup app list file path"
echo " get_backup_app_list get light backup app list"
echo " get_available_backup_file_list get local available backup file list"
}
is_init >/dev/null 2>&1
case $action in
"get_support_backup_features")
get_support_backup_features
;;
"backup")
backup "$@"
;;
"restore")
restore "$@"
;;
"get_backup_app_list_file_path")
get_backup_app_list_file_path
;;
"get_backup_app_list")
get_backup_app_list
;;
"get_available_backup_file_list")
get_available_backup_file_list "$@"
;;
*)
usage
;;
esac

View File

@ -0,0 +1,114 @@
#!/bin/sh
handle_part() {
case "$MOUNT" in
"/overlay")
return 1
;;
esac
return 0
}
check_dir() {
local data_root=$(uci -q get dockerd.globals.data_root)
[ -n "$data_root" ] || return 0
local block_dev=$(findmnt -T "$data_root" -v -o SOURCE | sed -n 2p)
[ -n "$block_dev" ] || return 0
[ "overlayfs:/overlay" = "$block_dev" ] && return 1
local line=$(block info "$block_dev" 2>/dev/null)
[ -n "$line" ] || return 0
eval "${line##*: } handle_part ${line%%: *}"
}
migrate_check(){
local dest="$1"
[ -n "$dest" ] || {
echo "dest dir not specified!" >&2
return 1
}
local data_root="$2"
[ -n "$data_root" ] || data_root=$(uci -q get dockerd.globals.data_root)
[ -n "$data_root" ] || {
echo "get docker data_root failed!" >&2
return 1
}
[ "$data_root" = "/" ] || data_root="${data_root%%/}"
[ "$dest" = "/" ] || dest="${dest%%/}"
[ "$data_root" = "$dest" ] && {
echo "dest dir is the same as data_root!" >&2
return 1
}
[ 1 = "$FORCE" ] && return 0
[ -e "$dest" ] || return 0
[ -d "$dest" ] || {
echo "$dest is existed and it's not a dir, use FORCE=1 to overwrite it" >&2
return 2
}
[ 0 = "$(ls -A "$dest" | head -1 | wc -l)" ] || {
echo "$dest is existed and it's not empty, use FORCE=1 to overwrite it" >&2
return 2
}
return 0
}
migrate() {
local dest="$1"
local data_root=$(uci -q get dockerd.globals.data_root)
[ -n "$data_root" ] || {
echo "get docker data_root failed!" >&2
return 1
}
[ "$data_root" = "/" ] || data_root="${data_root%%/}"
[ "$dest" = "/" ] || dest="${dest%%/}"
FORCE=1 migrate_check "$dest" "$data_root"
local check_result=$?
[ 0 = $check_result ] || return $check_result
if [ "$UCI_ONLY" != 1 ]; then
rm -rf "$dest"
mkdir -p "$dest"
echo "Copy $data_root to $dest ..."
cp -a "$data_root/." "$dest/" || return 1
fi
echo "Change dockerd data_root to $dest and restart"
uci set dockerd.globals.data_root="$dest"
uci commit dockerd
/etc/init.d/dockerd restart
echo "Done"
}
action=${1}
shift
usage() {
echo "usage: $1 sub-command [arguments...]"
echo "where sub-command is one of:"
echo " check_dir Check docker data_root is on extrnal disk"
echo " migrate_check {target_dir} Check target_dir is valid for migration, return 2 if target_dir existed and not empty"
echo " migrate {target_dir} Migrate docker data_root to target_dir"
echo " change_dir {target_dir} Migrate docker data_root to target_dir but change dir only (no data copy)"
}
case $action in
"check_dir")
check_dir
;;
"migrate")
migrate "$@"
;;
"migrate_check")
migrate_check "$@"
;;
"change_dir")
UCI_ONLY=1 migrate "$@"
;;
*)
usage "$0"
;;
esac

View File

@ -0,0 +1,200 @@
#!/bin/sh
# ipkg-build -- construct a .ipk from a directory
# Carl Worth <cworth@east.isi.edu>
# based on a script by Steve Redler IV, steve@sr-tech.com 5-21-2001
# 2003-04-25 rea@sr.unh.edu
# Updated to work on Familiar Pre0.7rc1, with busybox tar.
# Note it Requires: binutils-ar (since the busybox ar can't create)
# For UID debugging it needs a better "find".
set -e
version=1.0
FIND="$(command -v find)"
FIND="${FIND:-$(command -v gfind)}"
TAR="${TAR:-$(command -v tar)}"
GZIP="$(command -v gzip)"
# try to use fixed source epoch
if [ -n "$PKG_SOURCE_DATE_EPOCH" ]; then
TIMESTAMP=$(date --date="@$PKG_SOURCE_DATE_EPOCH")
elif [ -n "$SOURCE_DATE_EPOCH" ]; then
TIMESTAMP=$(date --date="@$SOURCE_DATE_EPOCH")
else
TIMESTAMP=$(date)
fi
ipkg_extract_value() {
sed -e "s/^[^:]*:[[:space:]]*//"
}
required_field() {
field=$1
grep "^$field:" < $CONTROL/control | ipkg_extract_value
}
pkg_appears_sane() {
local pkg_dir=$1
local owd=$PWD
cd $pkg_dir
PKG_ERROR=0
pkg=`required_field Package`
version=`required_field Version | sed 's/Version://; s/^.://g;'`
arch=`required_field Architecture`
if echo $pkg | grep '[^a-zA-Z0-9_.+-]'; then
echo "*** Error: Package name $name contains illegal characters, (other than [a-z0-9.+-])" >&2
PKG_ERROR=1;
fi
if [ -f $CONTROL/conffiles ]; then
rm -f $CONTROL/conffiles.resolved
for cf in `$FIND $(sed -e "s!^/!$pkg_dir/!" $CONTROL/conffiles) -type f`; do
echo "${cf#$pkg_dir}" >> $CONTROL/conffiles.resolved
done
rm $CONTROL/conffiles
if [ -f $CONTROL/conffiles.resolved ]; then
mv $CONTROL/conffiles.resolved $CONTROL/conffiles
chmod 0644 $CONTROL/conffiles
fi
fi
cd $owd
return $PKG_ERROR
}
resolve_file_mode_id() {
local var=$1 type=$2 name=$3 id
case "$name" in
root)
id=0
;;
*[!0-9]*)
id=$(sed -ne "s#^$type $name \\([0-9]\\+\\)\\b.*\$#\\1#p" "$TOPDIR/tmp/.packageusergroup" 2>/dev/null)
;;
*)
id=$name
;;
esac
export "$var=$id"
[ -n "$id" ]
}
###
# ipkg-build "main"
###
file_modes=""
usage="Usage: $0 [-v] [-h] [-m] <pkg_directory> [<destination_directory>]"
while getopts "hvm:" opt; do
case $opt in
v ) echo $version
exit 0
;;
h ) echo $usage >&2 ;;
m ) file_modes=$OPTARG ;;
\? ) echo $usage >&2
esac
done
shift $(($OPTIND - 1))
# continue on to process additional arguments
case $# in
1)
dest_dir=$PWD
;;
2)
dest_dir=$2
if [ "$dest_dir" = "." -o "$dest_dir" = "./" ] ; then
dest_dir=$PWD
fi
;;
*)
echo $usage >&2
exit 1
;;
esac
pkg_dir=$1
if [ ! -d $pkg_dir ]; then
echo "*** Error: Directory $pkg_dir does not exist" >&2
exit 1
fi
# CONTROL is second so that it takes precedence
CONTROL=
[ -d $pkg_dir/CONTROL ] && CONTROL=CONTROL
if [ -z "$CONTROL" ]; then
echo "*** Error: Directory $pkg_dir has no CONTROL subdirectory." >&2
exit 1
fi
if ! pkg_appears_sane $pkg_dir; then
echo >&2
echo "ipkg-build: Please fix the above errors and try again." >&2
exit 1
fi
tmp_dir=$dest_dir/IPKG_BUILD.$$
mkdir $tmp_dir
echo $CONTROL > $tmp_dir/tarX
cd $pkg_dir
for file_mode in $file_modes; do
case $file_mode in
/*:*:*:*)
;;
*)
echo "ERROR: file modes must use absolute path and contain user:group:mode"
echo "$file_mode"
exit 1
;;
esac
mode=${file_mode##*:}; path=${file_mode%:*}
group=${path##*:}; path=${path%:*}
user=${path##*:}; path=${path%:*}
if ! resolve_file_mode_id uid user "$user"; then
echo "ERROR: unable to resolve uid of $user" >&2
exit 1
fi
if ! resolve_file_mode_id gid group "$group"; then
echo "ERROR: unable to resolve gid of $group" >&2
exit 1
fi
chown "$uid:$gid" "$pkg_dir/$path"
chmod "$mode" "$pkg_dir/$path"
done
$TAR -X $tmp_dir/tarX --format=gnu --sort=name -cpf - --mtime="$TIMESTAMP" . | $GZIP -n - > $tmp_dir/data.tar.gz
installed_size=`ls -ln $tmp_dir/data.tar.gz | awk 'NR==1 {print $5}'`
sed -i -e "s/^Installed-Size: .*/Installed-Size: $installed_size/" \
$pkg_dir/$CONTROL/control
( cd $pkg_dir/$CONTROL && $TAR --format=gnu --sort=name -cf - --mtime="$TIMESTAMP" . | $GZIP -n - > $tmp_dir/control.tar.gz )
rm $tmp_dir/tarX
echo "2.0" > $tmp_dir/debian-binary
pkg_file=$dest_dir/${pkg}_${version}_${arch}.ipk
rm -f $pkg_file
( cd $tmp_dir && $TAR --format=gnu --sort=name -cf - --mtime="$TIMESTAMP" ./debian-binary ./data.tar.gz ./control.tar.gz | $GZIP -n - > $pkg_file )
rm $tmp_dir/debian-binary $tmp_dir/data.tar.gz $tmp_dir/control.tar.gz
rmdir $tmp_dir
echo "Packaged contents of $pkg_dir into $pkg_file"

View File

@ -0,0 +1,7 @@
#!/bin/sh
for i in "$@"; do
echo "$i" | grep -s -q "/etc/uci-defaults/" \
&& mkdir -p /etc/istore/uci-defaults_bak \
&& cp -f "$i" /etc/istore/uci-defaults_bak/
done
/bin/rm "$@"

View File

@ -0,0 +1,23 @@
clean:
compile:
include $(TOPDIR)/rules.mk
LUCI_NAME:=luci-lib-dummy
INCLUDE_DIR:=./dummy
include $(TOPDIR)/feeds/luci/luci.mk
install:
[ -n "$(APP_STORE_VERSION)" ]
mkdir -p "$(DESTDIR)/www/luci-static"
cp -a "$(FRONTEND_DIST)/luci-static/istore" "$(DESTDIR)/www/luci-static/"
$(SED) 's#\.js"#.js?v=$(APP_STORE_VERSION)"#g' "$(DESTDIR)/www/luci-static/istore/index.js"
mkdir -p "$(DESTDIR)/etc/opkg/keys"
echo "$(APP_STORE_VERSION)" > "$(DESTDIR)/etc/.app_store.version"
cp -a ./compat.conf "$(DESTDIR)/etc/opkg/compatfeeds.conf"
cp -a ./key-build.pub $(DESTDIR)/etc/opkg/keys/`$(STAGING_DIR_HOST)/bin/usign -F -p ./key-build.pub`
mkdir -p "$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n"
$(foreach lang,$(LUCI_LANGUAGES),$(foreach po,$(wildcard ${CURDIR}/po/$(lang)/*.po), \
po2lmo $(po) \
$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n/$(basename $(notdir $(po))).$(lang).lmo;))

View File

@ -0,0 +1 @@
src/gz istore_compat https://istore.istoreos.com/repo/all/compat

View File

@ -0,0 +1,2 @@
define BuildPackage
endef

View File

@ -0,0 +1,2 @@
untrusted comment: istore key
RWSlbxYnTG1Ia0BvB+xd7YdP7QDQACljfpve7sx9KYq94QgIqtlljuME

View File

@ -0,0 +1,11 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "istore_vue_lang"
msgstr "en"
msgid "iStore"
msgstr ""
msgid "This package is broken! Please reinstall or uninstall it."
msgstr ""

View File

@ -0,0 +1,11 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "istore_vue_lang"
msgstr "zh-cn"
msgid "iStore"
msgstr "iStore"
msgid "This package is broken! Please reinstall or uninstall it."
msgstr "此软件包已损坏!请重新安装或卸载它。"

View File

@ -0,0 +1,11 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "istore_vue_lang"
msgstr "zh-cn"
msgid "iStore"
msgstr "iStore"
msgid "This package is broken! Please reinstall or uninstall it."
msgstr "此軟體包已損壞!請重新安裝或卸載它。"

View File

@ -0,0 +1,513 @@
swagger: "2.0"
info:
description: Store API V1
title: Store API V1
version: 3.0.0
paths:
/cgi-bin/luci/admin/store/token:
get:
tags:
- token
summary: 获取csrfToken用于POST请求
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreToken"
/cgi-bin/luci/admin/store/installed:
get:
tags:
- installed
summary: 获取已安装的插件
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreInstalled"
/cgi-bin/luci/admin/store/install:
post:
tags:
- install
summary: 安装插件。安装过程中使用taskd接口获取日志
parameters:
- in: "query"
name: "token"
type: string
required: true
- in: "query"
name: "package"
type: string
required: true
- in: "query"
name: "autoconf"
type: string
description: "可选参数,表示安装完以后是否自动配置插件"
- in: "query"
name: "path"
type: string
description: "可选参数,表示安装完以后是否自动配置插件"
- in: "query"
name: "enable"
type: string
description: "可选参数,表示安装完以后是否自动配置插件"
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/autoconf:
post:
tags:
- install
summary: 自动配置插件
parameters:
- in: "query"
name: "token"
type: string
required: true
- in: "query"
name: "package"
type: string
required: true
- in: "query"
name: "path"
type: string
description: "可选参数"
- in: "query"
name: "enable"
type: string
description: "可选参数"
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/remove:
post:
tags:
- remove
summary: 卸载插件
parameters:
- in: "query"
name: "token"
type: string
required: true
- in: "query"
name: "package"
type: string
required: true
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/upgrade:
post:
tags:
- upgrade
summary: 更新插件
parameters:
- in: "query"
name: "token"
type: string
required: true
- in: "query"
name: "package"
type: string
required: true
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/upload:
post:
tags:
- upload
summary: 上传插件
description: "(文件上传表单,支持文件扩展名 .ipk,.run )"
consumes:
- multipart/form-data
parameters:
- in: formData
name: file
type: file
description: The file to upload.
- in: "query"
name: "token"
type: string
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/update:
post:
tags:
- update
summary: 刷新可用软件列表
parameters:
- in: "query"
name: "token"
type: string
required: true
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/status:
get:
tags:
- status
summary: 查询特定软件状态
parameters:
- in: "query"
name: "package"
type: string
required: true
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreStatus"
/cgi-bin/luci/admin/store/log:
get:
tags:
- log
summary: 任务日志,已废弃
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/check_self_upgrade:
get:
tags:
- check_self_upgrade
summary: 检查iStore自身更新
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreCheckSelfUpgrade"
/cgi-bin/luci/admin/store/do_self_upgrade:
post:
tags:
- do_self_upgrade
summary: 更新iStore自身
parameters:
- in: "query"
name: "token"
type: string
required: true
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
/cgi-bin/luci/admin/store/get_block_devices:
get:
tags:
- get_block_devices
summary: 枚举块设备
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreGetBlockDevices"
/cgi-bin/luci/admin/store/configured:
get:
tags:
- configured
summary: 检查插件是否已经配置过
parameters:
- in: "query"
name: "uci"
type: string
required: true
description: "要检查的uci配置文件名称"
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreConfigured"
/cgi-bin/luci/admin/store/entrysh:
post:
tags:
- entrysh
summary: 查询插件运行状态,主要为了获取入口信息。
description: 调用前应该检查meta数据中flags数组包含entrysh注意如果是安装插件则检查服务器端的flags如果是调用已安装插件则检查路由器端的flags。
parameters:
- in: "query"
name: "token"
type: string
required: true
- in: "query"
name: "package"
type: string
description: "包名例如aria2。支持英文逗号分隔多个包名。如果未提供则返回所有支持entrysh的包"
- in: "query"
name: "hostname"
type: string
required: true
description: "主机名不包含端口。前端应该使用location.hostname获取"
- in: "query"
name: "update"
type: integer
description: "如果为1则强制更新缓存"
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStoreEntrysh"
"400":
description: Bad Request。hostname不合法
/cgi-bin/luci/admin/store/docker_check_dir:
get:
tags:
- docker_check_dir
summary: 检查docker目录是否在系统盘
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseDockerCheckDir"
/cgi-bin/luci/admin/store/docker_check_migrate:
get:
tags:
- docker_check_migrate
summary: 检查docker迁移目标目录是否有效
parameters:
- in: "query"
name: "path"
type: string
required: true
description: "完整目标路径"
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseDockerCheckMigrate"
/cgi-bin/luci/admin/store/docker_migrate:
post:
tags:
- docker_migrate
summary: docker迁移到目标目录异步
description: 当返回code为0的时候可以使用taskd接口展示日志跟安装插件时一样
parameters:
- in: "query"
name: "token"
type: string
required: true
- in: "query"
name: "path"
type: string
required: true
description: "完整目标路径"
- in: "query"
name: "overwrite"
type: string
description: "如果docker_check_migrate返回的result是existed使用此参数指定解决方案true表示覆盖目标目录chdir表示只修改路径不复制文件"
responses:
"200":
description: OK
schema:
$ref: "#/definitions/ResponseStore"
definitions:
ResponseStoreToken:
type: object
properties:
token:
type: string
StoreInstalledApp:
type: object
properties:
tutorial:
type: string
description: "教程"
arch:
type: array
description: "适用的架构平台"
items:
type: string
entry:
type: string
description: "对于的页面地址"
author:
type: string
description: "作者"
depends:
type: array
description: "关联的依赖"
items:
type: string
time:
type: integer
description: "更新日期"
tags:
type: array
description: "标签"
items:
type: string
description_en:
type: string
description: "简介,英文"
description:
type: string
description: "简介,英文"
title:
type: string
description: "标题"
release:
type: integer
description: "发布"
website:
type: string
description: "官网"
version:
type: string
description: "版本号"
name:
type: string
description: "名称"
ResponseStoreInstalled:
type: array
items:
$ref: "#/definitions/StoreInstalledApp"
ResponseStore:
type: object
properties:
code:
type: integer
stdout:
type: string
stderr:
type: string
ResponseStoreStatus:
type: object
allOf:
- $ref: "#/definitions/StoreInstalledApp"
properties:
installed:
type: boolean
ResponseStoreCheckSelfUpgrade:
type: object
properties:
code:
type: integer
description: "为200时,msg会显示版本号"
msg:
type: string
description: "内容"
GetBlockDevice:
type: object
properties:
dev:
type: string
uuid:
type: string
version:
type: string
label:
type: string
mount:
type: string
type:
type: string
size:
type: string
ResponseStoreGetBlockDevices:
type: object
properties:
code:
type: integer
description: "为200时"
msg:
type: string
description: "内容"
data:
type: object
additionalProperties:
$ref: "#/definitions/GetBlockDevice"
ResponseStoreConfigured:
type: object
properties:
code:
type: integer
description: "为200时"
configured:
type: boolean
ResponseDockerCheckDir:
type: object
properties:
code:
type: integer
description: "为200时"
docker_on_system:
type: boolean
description: "docker数据在系统盘"
ResponseDockerCheckMigrate:
type: object
properties:
code:
type: integer
description: "为200时"
result:
type: string
description: "检查结果。可能值goodbadexisted"
error:
type: string
description: "当result为bad时此处返回错误日志"
StoreEntrysh:
type: object
description: "状态和入口信息,不同插件可能有些不一样,仅列出常用公共参数"
properties:
app:
type: string
description: "插件名称"
docker:
type: boolean
description: "如果是docker插件"
running:
type: boolean
description: "是否运行中"
deployed:
type: boolean
description: "如果是docker插件且未运行则是否已经部署"
web:
type: string
description: "url"
href:
type: string
description: "web端直接跳转url"
ResponseStoreEntrysh:
type: object
properties:
code:
type: integer
description: "为200时"
status:
description: "处理成功的结果集"
type: array
items:
$ref: "#/definitions/StoreEntrysh"
errors:
description: "处理失败的结果集"
type: array
items:
type: object
description: "失败信息"
properties:
app:
type: string
description: "插件名称"
code:
type: integer
description: "404或500"
msg:
type: string
description: "错误信息"

View File

@ -0,0 +1,20 @@
#
# Copyright (C) 2022 jjm2473 <jjm2473@gmail.com>
#
# This is free software, licensed under the MIT License.
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Task library
LUCI_DEPENDS:=+luci-lib-xterm +taskd
LUCI_EXTRA_DEPENDS:=taskd (>=1.0.3-1)
LUCI_PKGARCH:=all
PKG_VERSION:=1.0.22
PKG_RELEASE:=
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,151 @@
[hidden] {
display: none !important;
}
#tasks_detail_container {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #0008;
}
#tasks_dialog {
position: absolute;
width: 770px;
max-width: 100%;
max-height: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #000;
border-radius: 10px;
box-shadow: 2px 2px 6px #000a;
padding: 20px;
color: white;
}
.dialog-title-bar {
margin-top: -10px;
margin-right: -10px;
margin-bottom: 5px;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: nowrap;
align-items: center;
}
.dialog-title-bar .dialog-title {
word-break: break-all;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.dialog-content {
position: relative;
max-height: 500px;
overflow-y: scroll;
margin-right: -10px;
}
.dialog-icons {
align-self: center;
display: flex;
justify-content: flex-end;
}
.dialog-icon {
display: flex;
align-items: center;
flex-wrap: nowrap;
width: 20px;
height: 20px;
background-color: white;
color: black;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
user-select: none;
margin-left: 10px;
line-height: 1;
font-family: sans-serif;
justify-content: center;
cursor: pointer;
}
.dialog-icon.dialog-icon-min {
background-color: darkorange;
}
.dialog-icon.dialog-icon-close {
background-color: #ff5f56;
}
.dialog-icons:hover .dialog-icon.dialog-icon-min:before {
content: "_";
}
.dialog-icons:hover .dialog-icon.dialog-icon-close:before {
content: "X";
}
.tasks_stopped .dialog-icon.dialog-icon-close {
background-color: #27c840;
}
.tasks_stopped #tasks_dialog, .tasks_unknown #tasks_dialog {
padding: 19px;
border: 1px #27c840 solid;
animation: border-blink 1s;
animation-iteration-count: infinite;
}
.tasks_failed #tasks_dialog {
border-color: #ff0000;
}
.tasks_failed .dialog-icon.dialog-icon-close {
background-color: #ff0000;
}
.tasks_unknown #tasks_dialog {
border-color: darkorange;
}
#tasks_result_mask {
display: none;
background-color: #000d;
position: absolute;
left: 0;
top: 0;
z-index: 100;
width: 100%;
height: 100%;
cursor: default;
user-select: none;
font-size: small;
}
.tasks_stopped #tasks_result_mask {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.tasks_result_success, .tasks_stopped.tasks_failed .tasks_result_success, .tasks_result_failed {
display: none;
font-size: xx-large;
}
.tasks_stopped .tasks_result_success {
display: initial;
color: #27c840;
}
.tasks_failed .tasks_result_failed {
display: initial;
color: darkorange;
}
@keyframes border-blink { 50% { border-color:#fff ; } }

View File

@ -0,0 +1,241 @@
(function(){
const taskd={};
const $gettext = function(str) {
return taskd.i18n[str] || str;
};
const retryPromise = function(fn) {
return new Promise((resolve, reject) => {
const retry = function() {
fn(resolve, reject, retry);
};
retry();
});
};
const retry403XHR = function(url, method, responseType) {
return retryPromise((resolve, reject, retry) => {
var oReq = new XMLHttpRequest();
oReq.onerror = reject;
oReq.open(method || 'GET', url, true);
if (responseType) {
oReq.responseType = responseType;
}
oReq.onload = function (oEvent) {
if (oReq.status == 403) {
alert($gettext("Lost login status"));
location.href = location.href;
} else if (oReq.status >= 400) {
reject(oEvent);
} else {
resolve(oReq);
}
};
if (method=='POST') {
oReq.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
}
oReq.send(method=='POST'?("token="+taskd.csrfToken):null);
});
};
const request = function(url, method) {
return retry403XHR(url, method).then(oReq => oReq.responseText);
};
const getBin = function(url) {
return retry403XHR(url, null, "arraybuffer").then(oReq => {return {status: oReq.status, buffer: new Uint8Array(oReq.response)}});
};
const getTaskDetail = function(task_id) {
return request("/cgi-bin/luci/admin/system/tasks/status?task_id="+task_id).then(data=>JSON.parse(data));
};
const create_dialog = function(cfg) {
const container = document.createElement('div');
container.id = "tasks_detail_container";
container.innerHTML = taskd.dialog_template;
document.body.appendChild(container);
const title_view = container.querySelector(".dialog-title-bar .dialog-title");
title_view.innerText = cfg.title;
const term = new Terminal({convertEol: cfg.convertEol||false});
if (cfg.nohide) {
container.querySelector(".dialog-icon-min").hidden = true;
} else {
container.querySelector(".dialog-icon-min").onclick = function(){
container.hidden=true;
term.dispose();
document.body.removeChild(container);
cfg.onhide && cfg.onhide();
return false;
};
}
const tasks_result_mask = container.querySelector("#tasks_result_mask");
if (taskd.show_mask_on_stopped) {
tasks_result_mask.onclick = function(){
tasks_result_mask.hidden=true;
};
} else {
tasks_result_mask.hidden=true;
}
term.open(document.getElementById("tasks_xterm_log"));
return {term,container};
};
const show_log_txt = function(title, content, onclose) {
const dialog = create_dialog({title, convertEol:true, onhine:onclose});
const container = dialog.container;
const term = dialog.term;
container.querySelector(".dialog-icon-close").hidden = true;
term.write(content);
};
const show_log = function(task_id, nohide, onExit) {
let showing = true;
let running = true;
const dialog = create_dialog({title:task_id, nohide, onhide:function(){showing=false;}});
const container = dialog.container;
const term = dialog.term;
const title_view = container.querySelector(".dialog-title-bar .dialog-title");
container.querySelector(".dialog-icon-close").onclick = function(){
if (!running || confirm($gettext("Stop running task?"))) {
running=false;
showing=false;
del_task(task_id).then(()=>{
location.href = location.href;
});
}
return false;
};
const checkTask = function() {
return getTaskDetail(task_id).then(data=>{
if (!running) {
return false;
}
running = data.running;
let title = task_id;
if (!data.running && data.stop) {
title += " (" + (data.exit_code?$gettext("Failed at:"):$gettext("Finished at:")) + " " + new Date(data.stop * 1000).toLocaleString() + ")";
}
title += " > " + (data.command || '');
title_view.title = title;
title_view.innerText = title;
if (!data.running) {
container.classList.add('tasks_stopped');
if (data.exit_code) {
container.classList.add('tasks_failed');
}
onExit && onExit(data.exit_code);
}
// last pull
return showing;
});
};
let logoffset = 0;
const pulllog = function(check) {
let starter = Promise.resolve(showing);
if (check) {
starter = checkTask();
}
starter.then(again => {
if (again)
return getBin("/cgi-bin/luci/admin/system/tasks/log?task_id="+task_id+"&offset="+logoffset);
else
return {status: 204};
}).then(function(res){
if (!showing) {
return false;
}
switch(res.status){
case 205:
term.reset();
logoffset = 0;
return running;
break;
case 204:
return running && checkTask();
break;
case 200:
logoffset += res.buffer.byteLength;
term.write(res.buffer);
return running;
break;
}
}).then(again => {
if (again) {
setTimeout(pulllog, 0);
}
}).catch(err => {
if (showing) {
if (err.target.status == 0 || err.target.status == 502) {
title_view.innerText = task_id + ' (' + $gettext("Fetch log failed, retrying...") + ')';
setTimeout(()=>pulllog(true), 1000);
} else if (err.target.status == 403 || err.target.status == 404) {
title_view.innerText = task_id + ' (' + $gettext(err.target.status == 403?"Lost login status":"Task does not exist or has been deleted") + ')';
container.querySelector(".dialog-icon-close").hidden = true;
container.classList.add('tasks_unknown');
} else {
console.error(err);
}
}
});
};
pulllog(true);
};
const del_task = function(task_id) {
return request("/cgi-bin/luci/admin/system/tasks/stop?task_id="+task_id, "POST");
};
taskd.show_log = show_log;
taskd.remove = del_task;
taskd.show_log_txt = show_log_txt;
window.taskd=taskd;
})();
(function(){
// compat
if (typeof(window.findParent) !== 'function') {
const elem = function(e) {
return (e != null && typeof(e) == 'object' && 'nodeType' in e);
};
const matches = function(node, selector) {
var m = elem(node) ? node.matches || node.msMatchesSelector : null;
return m ? m.call(node, selector) : false;
};
window.findParent = function (node, selector) {
if (elem(node) && node.closest)
return node.closest(selector);
while (elem(node))
if (matches(node, selector))
return node;
else
node = node.parentNode;
return null;
};
}
if (typeof(window.cbi_submit) !== 'function') {
const makeHidden = function(name) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
return input;
};
window.cbi_submit = function(elem, name, value, action) {
var form = elem.form || findParent(elem, 'form');
if (!form)
return false;
if (action)
form.action = action;
if (name) {
var hidden = form.querySelector('input[type="hidden"][name="%s"]'.format(name)) ||
makeHidden(name);
hidden.value = value || '1';
form.appendChild(hidden);
}
form.submit();
return true;
};
}
})();

View File

@ -0,0 +1,98 @@
module("luci.controller.tasks-lib", package.seeall)
function index()
entry({"admin", "system", "tasks"}, call("tasks_ping")).dependent=false -- just for compatible
entry({"admin", "system", "tasks", "status"}, call("tasks_status")).dependent=false
entry({"admin", "system", "tasks", "log"}, call("tasks_log")).dependent=false
entry({"admin", "system", "tasks", "stop"}, post("tasks_stop")).dependent=false
end
local util = require "luci.util"
local jsonc = require "luci.jsonc"
local ltn12 = require "luci.ltn12"
local taskd = require "luci.model.tasks"
function tasks_ping()
luci.http.prepare_content("application/json")
luci.http.write_json({})
end
function tasks_status()
local data = taskd.status(luci.http.formvalue("task_id"))
luci.http.prepare_content("application/json")
luci.http.write_json(data)
end
function tasks_log()
local wait = 107
local task_id = luci.http.formvalue("task_id")
local offset = luci.http.formvalue("offset")
offset = offset and tonumber(offset) or 0
local logpath = "/var/log/tasks/"..task_id..".log"
local i
local logfd = io.open(logpath, "rb")
if logfd == nil then
luci.http.status(404)
luci.http.write("log not found")
return
end
local size = logfd:seek("end")
if size < offset then
luci.http.status(205, "Reset Content")
luci.http.write("reset offset")
return
end
i = 0
while (i < wait)
do
if size > offset then
break
end
nixio.nanosleep(0, 10000000) -- sleep 10ms
size = logfd:seek("end")
i = i+1
end
if i == wait then
logfd:close()
luci.http.status(204)
luci.http.prepare_content("application/octet-stream")
return
end
logfd:seek("set", offset)
local write_log = function()
local buffer = logfd:read(4096)
if buffer and #buffer > 0 then
return buffer
else
logfd:close()
return nil
end
end
luci.http.prepare_content("application/octet-stream")
if logfd then
ltn12.pump.all(write_log, luci.http.write)
end
end
function tasks_stop()
local sys = require("luci.sys")
local task_id = luci.http.formvalue("task_id") or ""
if task_id == "" then
luci.http.status(400)
luci.http.write("task_id is empty")
return
end
if sys.call("/etc/init.d/tasks task_del "..task_id.." >/dev/null 2>&1") ~= 0 then
nixio.nanosleep(2, 10000000)
end
luci.http.status(204)
end

View File

@ -0,0 +1,100 @@
local util = require "luci.util"
local jsonc = require "luci.jsonc"
local taskd = {}
local function output(data)
local ret={}
ret.running=data.running
if not data.running then
ret.exit_code=data.exit_code
if nil == ret.exit_code then
if data["data"] and data["data"]["exit_code"] and data["data"]["exit_code"] ~= "" then
ret.exit_code=tonumber(data["data"]["exit_code"])
else
ret.exit_code=143
end
end
end
ret.command=data["command"] and data["command"][4] or '#'
if data["data"] then
ret.start=tonumber(data["data"]["start"])
if not data.running and data["data"]["stop"] then
ret.stop=tonumber(data["data"]["stop"])
end
end
return ret
end
taskd.status = function (task_id)
task_id = task_id or ""
local data = util.trim(util.exec("/etc/init.d/tasks task_status "..task_id.." 2>/dev/null")) or ""
if data ~= "" then
data = jsonc.parse(data)
else
if task_id == "" then
data = {}
else
data = {running=false, exit_code=404}
end
end
if task_id ~= "" then
return output(data)
end
local ary={}
for k, v in pairs(data) do
ary[k] = output(v)
end
return ary
end
taskd.docker_map = function(config, task_id, script_path, title, desc)
require("luci.cbi")
require("luci.http")
require("luci.sys")
local translate = require("luci.i18n").translate
local m
m = luci.cbi.Map(config, title, desc)
m.template = "tasks/docker"
-- hide default buttons
m.pageaction = false
-- we want hook 'on_after_apply' works, 'apply_on_parse' can be true (rollback) or false (no rollback),
-- but 'apply_on_parse' must be true for luci 17.01 and below
m.apply_on_parse = true
m.script_path = script_path
m.task_id = task_id
m.auto_show_task = true
m.on_before_apply = function(self)
if self.uci.rollback then
-- luci 18.06+ has 'rollback' function
-- rollback dialog will show because 'apply_on_parse' is true,
-- hide rollback dialog by hook 'apply' function
local apply = self.uci.apply
self.uci.apply = function(uci, rollback)
apply(uci, false)
end
end
end
m.on_after_apply = function(self)
local cmd
local action = luci.http.formvalue("cbi.apply") or "null"
if "upgrade" == action or "install" == action
or "start" == action or "stop" == action or "restart" == action or "rm" == action then
cmd = string.format("\"%s\" %s", script_path, action)
end
if cmd then
if luci.sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. luci.util.shellquote(cmd) .. " >/dev/null 2>&1") ~= 0 then
self.task_start_failed = true
self.message = translate("Config saved, but apply failed")
end
else
self.message = translate("Unknown command: ") .. action
end
if self.message then
self.auto_show_task = false
end
end
return m
end
return taskd

View File

@ -0,0 +1,56 @@
<% if self.task_start_failed then %>
<div class="alert-message warning"><%:Another task running, try again later.%> <a href="javascript:void(taskd.show_log('<%=self.task_id%>'))"><%:Click here to check running task%></a></div>
<% end %>
<%+cbi/map%>
<%
local task_running = false
local taskd = require "luci.model.tasks"
local status = taskd.status(self.task_id)
task_running = status.running
-%>
<div class="cbi-page-actions control-group">
<%
if not task_running then
%>
<%
local util = require "luci.util"
local container_status = util.trim(util.exec(self.script_path.." status"))
local container_install = (string.len(container_status) > 0)
local container_running = container_status == "running"
if container_install then
-%>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Upgrade%>/<%:Apply%>" onclick="cbi_submit(this, 'cbi.apply', 'upgrade')" />
<%
if container_running then
-%>
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Stop%>" onclick="cbi_submit(this, 'cbi.apply', 'stop')" />
<input class="btn cbi-button cbi-button-reload" type="button" value="<%:Restart%>" onclick="cbi_submit(this, 'cbi.apply', 'restart')" />
<% else %>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Start%>" onclick="cbi_submit(this, 'cbi.apply', 'start')" />
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Remove%>" onclick="cbi_submit(this, 'cbi.apply', 'rm')" />
<% end
else %>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Install%>" onclick="cbi_submit(this, 'cbi.apply', 'install')" />
<% end
else
%>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Task Running%>&hellip;" onclick="taskd.show_log('<%=self.task_id%>')" />
<%
end
%>
</div>
<%+tasks/embed%>
<%
if self.auto_show_task and task_running then
-%>
<script>
taskd.show_log("<%=self.task_id%>");
</script>
<%
end
%>

View File

@ -0,0 +1,40 @@
<%+xterm/embed%>
<link rel="stylesheet" href="<%=resource%>/tasks/tasks.css<%# ?v=PKG_VERSION %>">
<script src="<%=resource%>/tasks/tasks.js<%# ?v=PKG_VERSION %>"></script>
<%
local i18n = {}
local function tr(str)
i18n[str]=translate(str)
end
tr("Stop running task?")
tr("Stopped at:")
tr("Finished at:")
tr("Failed at:")
tr("Lost login status")
tr("Fetch log failed, retrying...")
tr("Task does not exist or has been deleted")
-%>
<script>
window.taskd.csrfToken="<%=token%>";
window.taskd.i18n=<% luci.http.write_json(i18n) %>;
window.taskd.dialog_template=`
<div id="tasks_dialog">
<div class="dialog-title-bar">
<span class="dialog-title" id="tasks_id"></span>
<span class="dialog-icons">
<span class="dialog-icon dialog-icon-close" title="<%:Stop and Remove%>"></span>
<span class="dialog-icon dialog-icon-min" title="<%:Hide%>"></span>
</span>
</div>
<div class="dialog-content">
<div id="tasks_xterm_log"></div>
<div id="tasks_result_mask">
<span class="tasks_result_success"><%:Task execution successful !%></span>
<span class="tasks_result_failed"><%:Task execution failed !%></span>
<br/>
<span><%:Click the button in the upper right corner of the dialog box to close it, or click here to view the log%></span>
</div>
</div>
</div>
`;
</script>

View File

@ -0,0 +1,15 @@
clean:
compile:
include $(TOPDIR)/rules.mk
LUCI_NAME:=luci-lib-dummy
INCLUDE_DIR:=./dummy
include $(TOPDIR)/feeds/luci/luci.mk
install:
mkdir -p "$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n"
$(foreach lang,$(LUCI_LANGUAGES),$(foreach po,$(wildcard ${CURDIR}/po/$(lang)/*.po), \
po2lmo $(po) \
$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n/$(basename $(notdir $(po))).$(lang).lmo;))

View File

@ -0,0 +1,2 @@
define BuildPackage
endef

View File

@ -0,0 +1,50 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Stop running task?"
msgstr "删除运行中的任务?"
msgid "Finished at:"
msgstr "完成于:"
msgid "Failed at:"
msgstr "失败于:"
msgid "Lost login status"
msgstr "丢失登陆状态"
msgid "Fetch log failed, retrying..."
msgstr "拉取日志失败,正在重试..."
msgid "Task does not exist or has been deleted"
msgstr "任务不存在或已删除"
msgid "Stop and Remove"
msgstr "停止并删除"
msgid "Hide"
msgstr "隐藏"
msgid "Config saved, but apply failed"
msgstr "配置已保存,但应用失败"
msgid "Unknown command: "
msgstr "未知命令:"
msgid "Another task running, try again later."
msgstr "已有后台任务运行中,请稍后重试。"
msgid "Click here to check running task"
msgstr "点此查看运行中的任务"
msgid "Task Running"
msgstr "任务执行中"
msgid "Task execution successful !"
msgstr "任务执行成功!"
msgid "Task execution failed !"
msgstr "任务执行失败!"
msgid "Click the button in the upper right corner of the dialog box to close it, or click here to view the log"
msgstr "点击对话框右上角按钮关闭,或者点此查看日志"

View File

@ -0,0 +1,21 @@
#
# Copyright (c) 2017-2019, The xterm.js authors (MIT License)
# Copyright (c) 2014-2017, SourceLair, Private Company (www.sourcelair.com) (MIT License)
# Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
#
# This is free software, licensed under the MIT License.
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Xterm.js library
LUCI_DEPENDS:=
PKG_LICENSE:=MIT
PKG_VERSION:=4.18.0
PKG_RELEASE:=
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,180 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
<link rel="stylesheet" href="<%=resource%>/xterm/xterm.css<%# ?v=PKG_VERSION %>">
<script src="<%=resource%>/xterm/xterm.js<%# ?v=PKG_VERSION %>"></script>

View File

@ -0,0 +1,41 @@
#
# Copyright (C) 2022 jjm2473 <jjm2473@gmail.com>
#
# This is free software, licensed under the MIT License.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=taskd
PKG_VERSION:=1.0.3
PKG_RELEASE:=2
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Simple Task Manager
DEPENDS:=+procd +script-utils +coreutils +coreutils-stty
PKGARCH:=all
endef
define Package/$(PKG_NAME)/description
Simple Task Manager based on procd
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/etc/init.d $(1)/usr/libexec
$(INSTALL_BIN) ./files/tasks.init $(1)/etc/init.d/tasks
$(INSTALL_BIN) ./files/taskd.sh $(1)/usr/libexec/taskd
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -0,0 +1,16 @@
#!/bin/sh
TASK_ID="$1"
TASK_CMD="$2"
exec </dev/null >>"/var/log/tasks/$TASK_ID.log" 2>&1
export HOME=/root
export TERM=xterm-256color
exec script -efqc 'onexit() {
/etc/init.d/tasks _task_onstop "'"$TASK_ID"'" "$?"
}
trap onexit EXIT;
stty cols 80 rows 24;
'"$TASK_CMD" /dev/null

View File

@ -0,0 +1,157 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2022 jjm2473@gmail.com
USE_PROCD=1
START=49
extra_command "task_add" "<task_id> <task_cmd> [<time_wait>] Add and run a task, time_wait is wait time before auto delete stopped task, in seconds, -1 means forever"
extra_command "task_del" "<task_id> Stop and delete task"
extra_command "task_status" "[<task_id>] Dump task status, dump all tasks if no task_id specified"
extra_command "task_gc" "Auto delete exipred (stopped and after timw_wait) tasks"
extra_command "_task_onstop" "<task_id> Update stop time, for internal usage"
_task_add() {
local task_id="${1}"
local task_cmd="${2}"
local time_wait="${3}"
> "/var/log/tasks/$task_id.log"
procd_open_instance "$task_id"
procd_set_param data start=`date +'%s'` time_wait="$time_wait"
procd_set_param command sh -c "exec /usr/libexec/taskd '$task_id' \"\$0\"" "$task_cmd"
procd_set_param stderr 1
procd_close_instance
}
task_add() {
local task_id="${1}"
local task_cmd="${2}"
local time_wait="${3}"
[ -z "$task_id" -o -z "$task_cmd" ] && return 127
if service_running "$task_id"; then
echo "already running" >&2
return 1
fi
if ! mkdir -p /var/log/tasks; then
echo "create /var/log/tasks failed!" >&2
return 1
fi
rc_procd _task_add "$task_id" "$task_cmd" "$time_wait"
return 0
}
_task_del() {
local service="${1}"
local task_id="${2}"
procd_kill "$service" "$task_id"
> "/var/log/tasks/$task_id.log"
rm -f "/var/log/tasks/$task_id.log"
}
task_del() {
local task_id="${1}"
[ -z "$task_id" ] && return 127
procd_lock
_task_del "$(basename ${basescript:-$initscript})" "$task_id"
if [ "$(_task_status "$task_id" | jsonfilter -e '$.running' 2>/dev/null)" = "true" ]; then
return 1
else
return 0
fi
}
_task_status() {
local service="$(basename ${basescript:-$initscript})"
local instance="$1"
local data
json_init
json_add_string name "$service"
data=$(_procd_ubus_call list | jsonfilter -e '@["'"$service"'"]')
[ -z "$data" ] && return 1
data=$(echo "$data" | jsonfilter -e '$.instances')
if [ -z "$data" ]; then
if [ -z "$instance" ]; then
echo "{}"
return 0
fi
return 1
fi
if [ -z "$instance" ]; then
echo "$data"
else
instance="\"$instance\""
echo "$data" | jsonfilter -e '$['"$instance"']'
fi
return 0
}
task_status() {
local task_id="${1}"
_task_status "$task_id"
}
task_gc() {
local service="$(basename ${basescript:-$initscript})"
local task_id instance time_wait
local data
json_init
[ -n "$service" ] && json_add_string name "$service"
data=$(_procd_ubus_call list | jsonfilter -e '@["'"$service"'"]')
[ -z "$data" ] && return 1
data=$(echo "$data" | jsonfilter -e '$.instances')
[ -z "$data" ] && return 1
procd_lock
ls /var/log/tasks/ | sed 's/.log$//g' | while read task_id; do
instance=$(echo "$data" | jsonfilter -e '$["'"$task_id"'"]')
[ "$(echo "$instance" | jsonfilter -e '$.running')" = "false" ] || continue
time_wait=$(echo "$instance" | jsonfilter -e '$.data.time_wait')
[ "$time_wait" = "-1" ] && continue
[ $(($(date +'%s' -r "/var/log/tasks/$task_id.log") + ${time_wait:-0})) -lt `date +'%s'` ] && _task_del "$service" "$task_id"
done
}
_insert_exit() {
local exit_code="$2"
eval "`jshn -r "$1" | grep -v json_init`"
json_select data || {
_procd_set_param data stop=`date +'%s'` exit_code="$exit_code"
return
}
json_add_string stop `date +'%s'`
json_add_string exit_code "$exit_code"
json_select ..
}
_task_exit() {
local task_id="$1"
local exit_code="$2"
local inst_json="$3"
_procd_call json_add_object "$task_id"
_procd_call _insert_exit "$inst_json" "$exit_code"
_procd_call json_close_object
}
_task_onstop() {
local task_id="${1}"
local exit_code="${2}"
[ -z "$task_id" ] && return 127
local service="$(basename ${basescript:-$initscript})"
json_init
json_add_string name "$service"
data=$(_procd_ubus_call list | jsonfilter -e '@["'"$service"'"].instances["'"$task_id"'"]')
[ -z "$data" ] && return 1
json_cleanup
rc_procd _task_exit "$task_id" "$exit_code" "$data"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -0,0 +1,414 @@
msgid ""
msgstr ""
"Project-Id-Version: istore 0.1\n"
"Last-Translator: jjm2473\n"
"Language-Team: none\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/components/app/index.vue:25
msgid "%{ num } download"
msgid_plural "%{ num } downloads"
msgstr[0] "%{ num } download"
msgstr[1] "%{ num } downloads"
#: src/components/app/index.vue:32
msgid "%{ num } like"
msgid_plural "%{ num } likes"
msgstr[0] "%{ num } like"
msgstr[1] "%{ num } likes"
#: src/components/tabs/index.vue:27
msgid "all app"
msgstr "all app"
#: src/pages/maintance/index.vue:12
msgid "app backup"
msgstr "app backup"
#: src/components/app/index.vue:168
msgid "are you sure you want to uninstall %{name}?"
msgstr "are you sure you want to uninstall %{name}?"
#: src/components/app/index.vue:51
msgid "author"
msgstr "author"
#: src/pages/maintance/lightBackup.vue:41
msgid "backup fail"
msgstr "backup fail"
#: src/pages/maintance/localBackup.vue:198
#: src/pages/maintance/localBackup.vue:203
msgid "backup fail with error code"
msgstr "backup fail with error code"
#: src/pages/maintance/lightBackup.vue:18
#: src/pages/maintance/localBackup.vue:34
msgid "backup now"
msgstr "backup now"
#: src/pages/maintance/localBackup.vue:61
msgid ""
"backup now backs up installed software (unlimited installation sources) to "
"external storage"
msgstr ""
"backup now backs up installed software (unlimited installation sources) to "
"external storage"
#: src/pages/maintance/lightBackup.vue:9
msgid "backup now will download a list of iStore installed software"
msgstr "backup now will download a list of iStore installed software"
#: src/pages/maintance/lightBackup.vue:40
msgid "backup success"
msgstr "backup success"
#: src/pages/maintance/lightBackup.vue:39
msgid "backuping"
msgstr "backuping"
#: src/plugins/i18n/index.ts:45
msgid "Broken"
msgstr "Broken"
#: src/pages/store/components/sort.vue:24
msgid "By download"
msgstr "By download"
#: src/pages/store/components/sort.vue:28
msgid "By rating"
msgstr "By rating"
#: src/pages/maintance/update.vue:4
msgid "checking for latest version"
msgstr "checking for latest version"
#: src/pages/maintance/localBackup.vue:22
msgid "choose"
msgstr "choose"
#: src/pages/maintance/localBackup.vue:41
msgid "choose backup file"
msgstr "choose backup file"
#: src/pages/maintance/lightBackup.vue:42
msgid "click to download"
msgstr "click to download"
#: src/pages/maintance/update.vue:11
msgid "click to update"
msgstr "click to update"
#: src/components/toast/index.vue:3
msgid "closed"
msgstr "closed"
#: src/pages/maintance/index.vue:8
msgid "config"
msgstr "config"
#: src/pages/maintance/localBackup.vue:26
msgid "customize"
msgstr "customize"
#: src/pages/store/components/sort.vue:20 src/pages/store/components/tag.vue:8
msgid "default"
msgstr "default"
#: src/components/app/index.vue:148
msgid "Docker is not installed on the system, try to install it?"
msgstr "Docker is not installed on the system, try to install it?"
#: src/plugins/i18n/index.ts:38
msgid "download"
msgstr "Download"
#: src/pages/maintance/lightBackup.vue:4 src/pages/maintance/localBackup.vue:4
msgid "explain"
msgstr "explain"
#: src/pages/maintance/localBackup.vue:16
msgid "external storage directory"
msgstr "external storage directory"
#: src/pages/maintance/config.vue:5
msgid "hide docker packages"
msgstr "hide docker packages"
#: src/pages/maintance/config.vue:7
msgid "hide incompatible packages"
msgstr "hide incompatible packages"
#: src/components/app/index.vue:6
msgid "Icon is gone"
msgstr "Icon is gone"
#: src/pages/upload/index.vue:6
msgid "in this page, you can upload and install packages"
msgstr "in this page, you can upload and install packages"
#: src/components/app/index.vue:89
msgid "install"
msgstr "install"
#: src/components/app/index.vue:154 src/components/app/index.vue:164
msgid "installation failed, error code"
msgstr "installation failed, error code"
#: src/components/tabs/index.vue:23
msgid "installed"
msgstr "installed"
#: src/pages/maintance/update.vue:9
msgid "last version"
msgstr "last version"
#: src/pages/store/components/sort.vue:32
msgid "Latest update"
msgstr "Latest date"
#: src/pages/maintance/index.vue:17
msgid "light backup"
msgstr "light backup"
#: src/components/app/index.vue:41
msgid "like"
msgstr "like"
#: src/components/app/index.vue:38
msgid "liked"
msgstr "liked"
#: src/pages/store/components/apps.vue:6
msgid "loading data"
msgstr "loading data"
#: src/pages/maintance/index.vue:22
msgid "local backup"
msgstr "local backup"
#: src/components/tabs/index.vue:35
msgid "maintain"
msgstr "maintain"
#: src/components/tabs/index.vue:31
msgid "manual install"
msgstr "manual install"
#: src/plugins/i18n/index.ts:42
msgid "monitor"
msgstr "Monitor"
#: src/plugins/i18n/index.ts:40
msgid "multimedia"
msgstr "Multimedia"
#: src/plugins/i18n/index.ts:41
msgid "nas"
msgstr "NAS"
#: src/plugins/i18n/index.ts:36
msgid "net"
msgstr "Net"
#: src/plugins/i18n/index.ts:44
msgid "networking"
msgstr "Networking"
#: src/pages/upload/index.vue:17 src/pages/upload/index.vue:18
msgid "no files selected"
msgstr "no files selected"
#: src/pages/maintance/localBackup.vue:12
msgid "no internet required for recovery"
msgstr "no internet required for recovery"
#: src/pages/upload/index.vue:4
msgid "offline install"
msgstr "offline install"
#: src/pages/upload/index.vue:92 src/pages/upload/index.vue:97
msgid "offline installation failed with error code"
msgstr "offline installation failed with error code"
#: src/components/app/index.vue:80
msgid "open"
msgstr "open"
#: src/pages/store/components/sort.vue:3
msgid "order"
msgstr "order"
#: src/pages/upload/index.vue:12
msgid "packages installed offline may not appear in iStore"
msgstr "packages installed offline may not appear in iStore"
#: src/pages/maintance/localBackup.vue:29
msgid "please enter an absolute path starting with %{name}"
msgstr "please enter an absolute path starting with %{name}"
#: src/pages/maintance/localBackup.vue:139
msgid "please enter the path, give up the input, please clear the input box"
msgstr "please enter the path, give up the input, please clear the input box"
#: src/pages/store/components/search.vue:6
msgid "please enter the search keyword"
msgstr "please enter the search keyword"
#: src/pages/maintance/localBackup.vue:157
#: src/pages/maintance/localBackup.vue:191
msgid "please fill in the external storage directory"
msgstr "please fill in the external storage directory"
#: src/pages/maintance/localBackup.vue:218
msgid "please select a backup file to restore"
msgstr "please select a backup file to restore"
#: src/pages/maintance/lightBackup.vue:21
#: src/pages/maintance/localBackup.vue:36
msgid "restore backup"
msgstr "restore backup"
#: src/pages/maintance/lightBackup.vue:76
#: src/pages/maintance/lightBackup.vue:81
#: src/pages/maintance/localBackup.vue:224
#: src/pages/maintance/localBackup.vue:229
msgid "restore failed with error code"
msgstr "restore failed with error code"
#: src/pages/maintance/localBackup.vue:47
msgid "restore now"
msgstr "restore now"
#: src/pages/upload/index.vue:20
msgid "select or drag and drop files"
msgstr "select or drag and drop files"
#: src/plugins/i18n/index.ts:39
msgid "service"
msgstr "Service"
#: src/pages/maintance/config.vue:5
msgid "show docker packages"
msgstr "show docker packages"
#: src/pages/maintance/config.vue:7
msgid "show incompatible packages"
msgstr "show incompatible packages"
#: src/components/app/index.vue:92
msgid "Stick to the top"
msgstr "Stick to the top"
#: src/pages/upload/index.vue:9
msgid "supports .ipk packages, .run self-extracting packages"
msgstr "supports .ipk packages, .run self-extracting packages"
#: src/plugins/i18n/index.ts:43
msgid "system"
msgstr "System"
#: src/pages/store/components/tag.vue:4
msgid "tag"
msgstr "Tag"
#: src/pages/maintance/update.vue:13
msgid "the is last version"
msgstr "the is last version"
#: src/pages/maintance/localBackup.vue:167
msgid "there are no backup files in this path"
msgstr "there are no backup files in this path"
#: src/plugins/i18n/index.ts:37
msgid "tool"
msgstr "Tool"
#: src/components/app/index.vue:62
msgid "tutorial"
msgstr "tutorial"
#: src/components/app/index.vue:83 src/components/app/index.vue:84
msgid "uninstall"
msgstr "uninstall"
#: src/components/app/index.vue:177 src/components/app/index.vue:182
msgid "uninstall failed, error code"
msgstr "uninstall failed, error code"
#: src/components/app/index.vue:55
msgid "update date"
msgstr "update date"
#: src/pages/maintance/update.vue:49
msgid "update error"
msgstr "update error"
#: src/components/app/index.vue:138 src/components/app/index.vue:143
msgid "update failed with error code"
msgstr "update failed with error code"
#: src/pages/maintance/update.vue:37
msgid "update success, reloading"
msgstr "update success, reloading"
#: src/pages/maintance/update.vue:32
msgid "updateing"
msgstr "updateing"
#: src/components/app/index.vue:74 src/pages/maintance/index.vue:4
msgid "upgrade"
msgstr "upgrade"
#: src/pages/maintance/lightBackup.vue:13
msgid "upload software list and install from iStore when restoring backup"
msgstr "upload software list and install from iStore when restoring backup"
#: src/pages/maintance/lightBackup.vue:72 src/pages/upload/index.vue:88
msgid "uploading"
msgstr "uploading"
#: src/components/app/index.vue:65
msgid "website"
msgstr "website"
#: src/components/app/index.vue:201
msgid "you have already liked"
msgstr "you have already liked"
#~ msgid "do not refresh this page during plugin installation/uninstallation"
#~ msgstr "do not refresh this page during plugin installation/uninstallation"
#~ msgid "installation is complete, please close the current window"
#~ msgstr "installation is complete, please close the current window"
#~ msgid "installing plugin"
#~ msgstr "installing plugin"
#~ msgid "offline installation is successful, please close the current window"
#~ msgstr "offline installation is successful, please close the current window"
#~ msgid "restoring backup"
#~ msgstr "restoring backup"
#~ msgid "the backup was restored fail"
#~ msgstr "the backup was restored fail"
#~ msgid "the backup was restored success"
#~ msgstr "the backup was restored success"
#~ msgid "the update is success, please close the current window"
#~ msgstr "the update is success, please close the current window"
#~ msgid "uninstallation succeeded, please close the current window"
#~ msgstr "uninstallation succeeded, please close the current window"
#~ msgid "uninstalling plugin"
#~ msgstr "uninstalling plugin"
#~ msgid "updating plugin"
#~ msgstr "updating plugin"

View File

@ -0,0 +1,380 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
#: src/components/app/index.vue:25
msgid "%{ num } download"
msgid_plural "%{ num } downloads"
msgstr[0] ""
msgstr[1] ""
#: src/components/app/index.vue:32
msgid "%{ num } like"
msgid_plural "%{ num } likes"
msgstr[0] ""
msgstr[1] ""
#: src/components/tabs/index.vue:27
msgid "all app"
msgstr ""
#: src/pages/maintance/index.vue:12
msgid "app backup"
msgstr ""
#: src/components/app/index.vue:168
msgid "are you sure you want to uninstall %{name}?"
msgstr ""
#: src/components/app/index.vue:51
msgid "author"
msgstr ""
#: src/pages/maintance/lightBackup.vue:41
msgid "backup fail"
msgstr ""
#: src/pages/maintance/localBackup.vue:198
#: src/pages/maintance/localBackup.vue:203
msgid "backup fail with error code"
msgstr ""
#: src/pages/maintance/lightBackup.vue:18
#: src/pages/maintance/localBackup.vue:34
msgid "backup now"
msgstr ""
#: src/pages/maintance/localBackup.vue:61
msgid "backup now backs up installed software (unlimited installation sources) to external storage"
msgstr ""
#: src/pages/maintance/lightBackup.vue:9
msgid "backup now will download a list of iStore installed software"
msgstr ""
#: src/pages/maintance/lightBackup.vue:40
msgid "backup success"
msgstr ""
#: src/pages/maintance/lightBackup.vue:39
msgid "backuping"
msgstr ""
#: src/plugins/i18n/index.ts:45
msgid "Broken"
msgstr ""
#: src/pages/store/components/sort.vue:24
msgid "By download"
msgstr ""
#: src/pages/store/components/sort.vue:28
msgid "By rating"
msgstr ""
#: src/pages/maintance/update.vue:4
msgid "checking for latest version"
msgstr ""
#: src/pages/maintance/localBackup.vue:22
msgid "choose"
msgstr ""
#: src/pages/maintance/localBackup.vue:41
msgid "choose backup file"
msgstr ""
#: src/pages/maintance/lightBackup.vue:42
msgid "click to download"
msgstr ""
#: src/pages/maintance/update.vue:11
msgid "click to update"
msgstr ""
#: src/components/toast/index.vue:3
msgid "closed"
msgstr ""
#: src/pages/maintance/index.vue:8
msgid "config"
msgstr ""
#: src/pages/maintance/localBackup.vue:26
msgid "customize"
msgstr ""
#: src/pages/store/components/sort.vue:20
#: src/pages/store/components/tag.vue:8
msgid "default"
msgstr ""
#: src/components/app/index.vue:148
msgid "Docker is not installed on the system, try to install it?"
msgstr ""
#: src/plugins/i18n/index.ts:38
msgid "download"
msgstr ""
#: src/pages/maintance/lightBackup.vue:4
#: src/pages/maintance/localBackup.vue:4
msgid "explain"
msgstr ""
#: src/pages/maintance/localBackup.vue:16
msgid "external storage directory"
msgstr ""
#: src/pages/maintance/config.vue:5
msgid "hide docker packages"
msgstr ""
#: src/pages/maintance/config.vue:7
msgid "hide incompatible packages"
msgstr ""
#: src/components/app/index.vue:6
msgid "Icon is gone"
msgstr ""
#: src/pages/upload/index.vue:6
msgid "in this page, you can upload and install packages"
msgstr ""
#: src/components/app/index.vue:89
msgid "install"
msgstr ""
#: src/components/app/index.vue:154
#: src/components/app/index.vue:164
msgid "installation failed, error code"
msgstr ""
#: src/components/tabs/index.vue:23
msgid "installed"
msgstr ""
#: src/pages/maintance/update.vue:9
msgid "last version"
msgstr ""
#: src/pages/store/components/sort.vue:32
msgid "Latest update"
msgstr ""
#: src/pages/maintance/index.vue:17
msgid "light backup"
msgstr ""
#: src/components/app/index.vue:41
msgid "like"
msgstr ""
#: src/components/app/index.vue:38
msgid "liked"
msgstr ""
#: src/pages/store/components/apps.vue:6
msgid "loading data"
msgstr ""
#: src/pages/maintance/index.vue:22
msgid "local backup"
msgstr ""
#: src/components/tabs/index.vue:35
msgid "maintain"
msgstr ""
#: src/components/tabs/index.vue:31
msgid "manual install"
msgstr ""
#: src/plugins/i18n/index.ts:42
msgid "monitor"
msgstr ""
#: src/plugins/i18n/index.ts:40
msgid "multimedia"
msgstr ""
#: src/plugins/i18n/index.ts:41
msgid "nas"
msgstr ""
#: src/plugins/i18n/index.ts:36
msgid "net"
msgstr ""
#: src/plugins/i18n/index.ts:44
msgid "networking"
msgstr ""
#: src/pages/upload/index.vue:17
#: src/pages/upload/index.vue:18
msgid "no files selected"
msgstr ""
#: src/pages/maintance/localBackup.vue:12
msgid "no internet required for recovery"
msgstr ""
#: src/pages/upload/index.vue:4
msgid "offline install"
msgstr ""
#: src/pages/upload/index.vue:92
#: src/pages/upload/index.vue:97
msgid "offline installation failed with error code"
msgstr ""
#: src/components/app/index.vue:80
msgid "open"
msgstr ""
#: src/pages/store/components/sort.vue:3
msgid "order"
msgstr ""
#: src/pages/upload/index.vue:12
msgid "packages installed offline may not appear in iStore"
msgstr ""
#: src/pages/maintance/localBackup.vue:29
msgid "please enter an absolute path starting with %{name}"
msgstr ""
#: src/pages/maintance/localBackup.vue:139
msgid "please enter the path, give up the input, please clear the input box"
msgstr ""
#: src/pages/store/components/search.vue:6
msgid "please enter the search keyword"
msgstr ""
#: src/pages/maintance/localBackup.vue:157
#: src/pages/maintance/localBackup.vue:191
msgid "please fill in the external storage directory"
msgstr ""
#: src/pages/maintance/localBackup.vue:218
msgid "please select a backup file to restore"
msgstr ""
#: src/pages/maintance/lightBackup.vue:21
#: src/pages/maintance/localBackup.vue:36
msgid "restore backup"
msgstr ""
#: src/pages/maintance/lightBackup.vue:76
#: src/pages/maintance/lightBackup.vue:81
#: src/pages/maintance/localBackup.vue:224
#: src/pages/maintance/localBackup.vue:229
msgid "restore failed with error code"
msgstr ""
#: src/pages/maintance/localBackup.vue:47
msgid "restore now"
msgstr ""
#: src/pages/upload/index.vue:20
msgid "select or drag and drop files"
msgstr ""
#: src/plugins/i18n/index.ts:39
msgid "service"
msgstr ""
#: src/pages/maintance/config.vue:5
msgid "show docker packages"
msgstr ""
#: src/pages/maintance/config.vue:7
msgid "show incompatible packages"
msgstr ""
#: src/components/app/index.vue:92
msgid "Stick to the top"
msgstr ""
#: src/pages/upload/index.vue:9
msgid "supports .ipk packages, .run self-extracting packages"
msgstr ""
#: src/plugins/i18n/index.ts:43
msgid "system"
msgstr ""
#: src/pages/store/components/tag.vue:4
msgid "tag"
msgstr ""
#: src/pages/maintance/update.vue:13
msgid "the is last version"
msgstr ""
#: src/pages/maintance/localBackup.vue:167
msgid "there are no backup files in this path"
msgstr ""
#: src/plugins/i18n/index.ts:37
msgid "tool"
msgstr ""
#: src/components/app/index.vue:62
msgid "tutorial"
msgstr ""
#: src/components/app/index.vue:83
#: src/components/app/index.vue:84
msgid "uninstall"
msgstr ""
#: src/components/app/index.vue:177
#: src/components/app/index.vue:182
msgid "uninstall failed, error code"
msgstr ""
#: src/components/app/index.vue:55
msgid "update date"
msgstr ""
#: src/pages/maintance/update.vue:49
msgid "update error"
msgstr ""
#: src/components/app/index.vue:138
#: src/components/app/index.vue:143
msgid "update failed with error code"
msgstr ""
#: src/pages/maintance/update.vue:37
msgid "update success, reloading"
msgstr ""
#: src/pages/maintance/update.vue:32
msgid "updateing"
msgstr ""
#: src/components/app/index.vue:74
#: src/pages/maintance/index.vue:4
msgid "upgrade"
msgstr ""
#: src/pages/maintance/lightBackup.vue:13
msgid "upload software list and install from iStore when restoring backup"
msgstr ""
#: src/pages/maintance/lightBackup.vue:72
#: src/pages/upload/index.vue:88
msgid "uploading"
msgstr ""
#: src/components/app/index.vue:65
msgid "website"
msgstr ""
#: src/components/app/index.vue:201
msgid "you have already liked"
msgstr ""

View File

@ -0,0 +1,411 @@
msgid ""
msgstr ""
"Project-Id-Version: istore 0.1\n"
"Last-Translator: jjm2473\n"
"Language-Team: none\n"
"Language: zh-cn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/components/app/index.vue:25
msgid "%{ num } download"
msgid_plural "%{ num } downloads"
msgstr[0] "%{ num } 次下载"
msgstr[1] "%{ num } 次下载"
#: src/components/app/index.vue:32
msgid "%{ num } like"
msgid_plural "%{ num } likes"
msgstr[0] "%{ num } 次点赞"
msgstr[1] "%{ num } 次点赞"
#: src/components/tabs/index.vue:27
msgid "all app"
msgstr "全部软件"
#: src/pages/maintance/index.vue:12
msgid "app backup"
msgstr "软件备份"
#: src/components/app/index.vue:168
msgid "are you sure you want to uninstall %{name}?"
msgstr "确定要卸载 %{name} 吗?"
#: src/components/app/index.vue:51
msgid "author"
msgstr "作者"
#: src/pages/maintance/lightBackup.vue:41
msgid "backup fail"
msgstr "备份失败"
#: src/pages/maintance/localBackup.vue:198
#: src/pages/maintance/localBackup.vue:203
msgid "backup fail with error code"
msgstr "备份失败,错误代码"
#: src/pages/maintance/lightBackup.vue:18
#: src/pages/maintance/localBackup.vue:34
msgid "backup now"
msgstr "立即备份"
#: src/pages/maintance/localBackup.vue:61
msgid ""
"backup now backs up installed software (unlimited installation sources) to "
"external storage"
msgstr "立即备份将备份已安装软件(不限安装来源)到外部存储"
#: src/pages/maintance/lightBackup.vue:9
msgid "backup now will download a list of iStore installed software"
msgstr "立即备份将下载iStore已安装的软件列表"
#: src/pages/maintance/lightBackup.vue:40
msgid "backup success"
msgstr "备份成功"
#: src/pages/maintance/lightBackup.vue:39
msgid "backuping"
msgstr "正在备份"
#: src/plugins/i18n/index.ts:45
msgid "Broken"
msgstr "已损坏"
#: src/pages/store/components/sort.vue:24
msgid "By download"
msgstr "按下载"
#: src/pages/store/components/sort.vue:28
msgid "By rating"
msgstr "按评分"
#: src/pages/maintance/update.vue:4
msgid "checking for latest version"
msgstr "正在检查最新版本"
#: src/pages/maintance/localBackup.vue:22
msgid "choose"
msgstr "请选择"
#: src/pages/maintance/localBackup.vue:41
msgid "choose backup file"
msgstr "选择备份文件"
#: src/pages/maintance/lightBackup.vue:42
msgid "click to download"
msgstr "点此下载"
#: src/pages/maintance/update.vue:11
msgid "click to update"
msgstr "点我更新"
#: src/components/toast/index.vue:3
msgid "closed"
msgstr "关闭"
#: src/pages/maintance/index.vue:8
msgid "config"
msgstr "配置"
#: src/pages/maintance/localBackup.vue:26
msgid "customize"
msgstr "自定义"
#: src/pages/store/components/sort.vue:20 src/pages/store/components/tag.vue:8
msgid "default"
msgstr "默认"
#: src/components/app/index.vue:148
msgid "Docker is not installed on the system, try to install it?"
msgstr "系统中未安装Docker是否尝试安装"
#: src/plugins/i18n/index.ts:38
msgid "download"
msgstr "下载"
#: src/pages/maintance/lightBackup.vue:4 src/pages/maintance/localBackup.vue:4
msgid "explain"
msgstr "说明"
#: src/pages/maintance/localBackup.vue:16
msgid "external storage directory"
msgstr "外部存储目录"
#: src/pages/maintance/config.vue:5
msgid "hide docker packages"
msgstr "隐藏 Docker 插件"
#: src/pages/maintance/config.vue:7
msgid "hide incompatible packages"
msgstr "隐藏架构不兼容插件"
#: src/components/app/index.vue:6
msgid "Icon is gone"
msgstr "图标出走啦"
#: src/pages/upload/index.vue:6
msgid "in this page, you can upload and install packages"
msgstr "通过本页面,你可以上传并安装插件包"
#: src/components/app/index.vue:89
msgid "install"
msgstr "安装"
#: src/components/app/index.vue:154 src/components/app/index.vue:164
msgid "installation failed, error code"
msgstr "安装失败,错误码"
#: src/components/tabs/index.vue:23
msgid "installed"
msgstr "已安装"
#: src/pages/maintance/update.vue:9
msgid "last version"
msgstr "最新版本"
#: src/pages/store/components/sort.vue:32
msgid "Latest update"
msgstr "最近更新"
#: src/pages/maintance/index.vue:17
msgid "light backup"
msgstr "轻量备份"
#: src/components/app/index.vue:41
msgid "like"
msgstr "点赞"
#: src/components/app/index.vue:38
msgid "liked"
msgstr "已点赞"
#: src/pages/store/components/apps.vue:6
msgid "loading data"
msgstr "正在努力的获取数据"
#: src/pages/maintance/index.vue:22
msgid "local backup"
msgstr "本地备份"
#: src/components/tabs/index.vue:35
msgid "maintain"
msgstr "维护"
#: src/components/tabs/index.vue:31
msgid "manual install"
msgstr "手动安装"
#: src/plugins/i18n/index.ts:42
msgid "monitor"
msgstr "监控"
#: src/plugins/i18n/index.ts:40
msgid "multimedia"
msgstr "多媒体"
#: src/plugins/i18n/index.ts:41
msgid "nas"
msgstr "NAS"
#: src/plugins/i18n/index.ts:36
msgid "net"
msgstr "网络"
#: src/plugins/i18n/index.ts:44
msgid "networking"
msgstr "组网"
#: src/pages/upload/index.vue:17 src/pages/upload/index.vue:18
msgid "no files selected"
msgstr "未选择任何文件"
#: src/pages/maintance/localBackup.vue:12
msgid "no internet required for recovery"
msgstr "恢复时不需要网络"
#: src/pages/upload/index.vue:4
msgid "offline install"
msgstr "离线安装"
#: src/pages/upload/index.vue:92 src/pages/upload/index.vue:97
msgid "offline installation failed with error code"
msgstr "离线安装失败,错误码"
#: src/components/app/index.vue:80
msgid "open"
msgstr "打开"
#: src/pages/store/components/sort.vue:3
msgid "order"
msgstr "排序"
#: src/pages/upload/index.vue:12
msgid "packages installed offline may not appear in iStore"
msgstr "离线安装的插件可能不会出现在iStore中"
#: src/pages/maintance/localBackup.vue:29
msgid "please enter an absolute path starting with %{name}"
msgstr "请输入 %{name} 开头的绝对路径"
#: src/pages/maintance/localBackup.vue:139
msgid "please enter the path, give up the input, please clear the input box"
msgstr "请输入路径,放弃输入请清空输入框"
#: src/pages/store/components/search.vue:6
msgid "please enter the search keyword"
msgstr "请输入搜索关键词"
#: src/pages/maintance/localBackup.vue:157
#: src/pages/maintance/localBackup.vue:191
msgid "please fill in the external storage directory"
msgstr "请填写外部存储目录"
#: src/pages/maintance/localBackup.vue:218
msgid "please select a backup file to restore"
msgstr "请选择要恢复的备份文件"
#: src/pages/maintance/lightBackup.vue:21
#: src/pages/maintance/localBackup.vue:36
msgid "restore backup"
msgstr "恢复备份"
#: src/pages/maintance/lightBackup.vue:76
#: src/pages/maintance/lightBackup.vue:81
#: src/pages/maintance/localBackup.vue:224
#: src/pages/maintance/localBackup.vue:229
msgid "restore failed with error code"
msgstr "恢复失败,错误代码"
#: src/pages/maintance/localBackup.vue:47
msgid "restore now"
msgstr "立即恢复"
#: src/pages/upload/index.vue:20
msgid "select or drag and drop files"
msgstr "选择或拖放文件"
#: src/plugins/i18n/index.ts:39
msgid "service"
msgstr "服务"
#: src/pages/maintance/config.vue:5
msgid "show docker packages"
msgstr "显示 Docker 插件"
#: src/pages/maintance/config.vue:7
msgid "show incompatible packages"
msgstr "显示架构不兼容插件"
#: src/components/app/index.vue:92
msgid "Stick to the top"
msgstr "置顶"
#: src/pages/upload/index.vue:9
msgid "supports .ipk packages, .run self-extracting packages"
msgstr "支持 .ipk 包,.run 自解压格式"
#: src/plugins/i18n/index.ts:43
msgid "system"
msgstr "系统"
#: src/pages/store/components/tag.vue:4
msgid "tag"
msgstr "标签"
#: src/pages/maintance/update.vue:13
msgid "the is last version"
msgstr "当前已经是最新版本"
#: src/pages/maintance/localBackup.vue:167
msgid "there are no backup files in this path"
msgstr "此路径下没有备份文件"
#: src/plugins/i18n/index.ts:37
msgid "tool"
msgstr "工具"
#: src/components/app/index.vue:62
msgid "tutorial"
msgstr "教程"
#: src/components/app/index.vue:83 src/components/app/index.vue:84
msgid "uninstall"
msgstr "卸载"
#: src/components/app/index.vue:177 src/components/app/index.vue:182
msgid "uninstall failed, error code"
msgstr "卸载失败,错误码"
#: src/components/app/index.vue:55
msgid "update date"
msgstr "更新日期"
#: src/pages/maintance/update.vue:49
msgid "update error"
msgstr "更新失败"
#: src/components/app/index.vue:138 src/components/app/index.vue:143
msgid "update failed with error code"
msgstr "更新失败,错误代码"
#: src/pages/maintance/update.vue:37
msgid "update success, reloading"
msgstr "更新成功,重新加载页面"
#: src/pages/maintance/update.vue:32
msgid "updateing"
msgstr "正在更新中"
#: src/components/app/index.vue:74 src/pages/maintance/index.vue:4
msgid "upgrade"
msgstr "更新"
#: src/pages/maintance/lightBackup.vue:13
msgid "upload software list and install from iStore when restoring backup"
msgstr "恢复备份时上传软件列表并从iStore安装"
#: src/pages/maintance/lightBackup.vue:72 src/pages/upload/index.vue:88
msgid "uploading"
msgstr "上传中"
#: src/components/app/index.vue:65
msgid "website"
msgstr "官网"
#: src/components/app/index.vue:201
msgid "you have already liked"
msgstr "您已经点赞过啦"
#~ msgid "do not refresh this page during plugin installation/uninstallation"
#~ msgstr "插件安装/卸载过程中请勿刷新此页面"
#~ msgid "installation is complete, please close the current window"
#~ msgstr "安装完成,请关闭当前窗口"
#~ msgid "installing plugin"
#~ msgstr "正在安装插件"
#~ msgid "offline installation is successful, please close the current window"
#~ msgstr "离线安装成功,请关闭当前窗口"
#~ msgid "restoring backup"
#~ msgstr "正在恢复备份"
#~ msgid "the backup was restored fail"
#~ msgstr "恢复备份失败"
#~ msgid "the backup was restored success"
#~ msgstr "恢复备份成功"
#~ msgid "the update is success, please close the current window"
#~ msgstr "更新成功,请关闭当前窗口"
#~ msgid "uninstallation succeeded, please close the current window"
#~ msgstr "卸载完成,请关闭当前窗口"
#~ msgid "uninstalling plugin"
#~ msgstr "正在卸载插件"
#~ msgid "updating plugin"
#~ msgstr "正在更新插件"