🎨 大部分的 make 指令

This commit is contained in:
zhaoyupeng
2025-11-24 18:37:44 +08:00
commit 27fa38aef0
38 changed files with 4356 additions and 0 deletions

225
pkg/archiver/archiver.go Normal file
View File

@@ -0,0 +1,225 @@
package archiver
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"gitea.loveuer.com/yizhisec/pkg3/logger"
)
// Options defines options for downloading and extracting archives
type Options struct {
// InsecureSkipVerify skips TLS certificate verification (equivalent to wget --no-check-certificate)
InsecureSkipVerify bool
// HTTPClient allows providing a custom HTTP client
HTTPClient *http.Client
// OnProgress is called during extraction with the current file being extracted
OnProgress func(filename string, index int, total int)
// IsGzipped explicitly specifies whether the archive is gzip compressed
// If nil, auto-detect based on file extension (.tar.gz, .tgz)
IsGzipped *bool
}
// Option is a functional option for configuring the archiver
type Option func(*Options)
// WithInsecureSkipVerify skips TLS certificate verification
func WithInsecureSkipVerify() Option {
return func(o *Options) {
o.InsecureSkipVerify = true
}
}
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(client *http.Client) Option {
return func(o *Options) {
o.HTTPClient = client
}
}
// WithProgress sets a progress callback
func WithProgress(callback func(filename string, index int, total int)) Option {
return func(o *Options) {
o.OnProgress = callback
}
}
// WithGzipCompression explicitly sets whether the archive is gzip compressed
func WithGzipCompression(isGzipped bool) Option {
return func(o *Options) {
o.IsGzipped = &isGzipped
}
}
// DownloadAndExtract downloads a tar or tar.gz file from URL and extracts it to destDir
// Supports both .tar and .tar.gz formats
func DownloadAndExtract(ctx context.Context, url, destDir string, opts ...Option) error {
options := &Options{
InsecureSkipVerify: false,
}
for _, opt := range opts {
opt(options)
}
logger.Debug("开始下载和解压: %s -> %s", url, destDir)
// Create HTTP client
client := options.HTTPClient
if client == nil {
client = &http.Client{}
if options.InsecureSkipVerify {
logger.Debug("TLS 证书验证已禁用")
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
}
// Download the file
logger.Debug("发起 HTTP 请求: %s", url)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.Debug("创建请求失败: %v", err)
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
logger.Debug("下载失败: %v", err)
return fmt.Errorf("failed to download: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Debug("HTTP 状态码异常: %d", resp.StatusCode)
return fmt.Errorf("bad status: %s", resp.Status)
}
logger.Debug("下载成功,准备解压")
// Determine if the file is gzipped
var reader io.Reader = resp.Body
var isGzipped bool
if options.IsGzipped != nil {
// Explicitly specified by option
isGzipped = *options.IsGzipped
logger.Debug("压缩格式由选项指定: isGzipped=%v", isGzipped)
} else {
// Auto-detect based on file extension
isGzipped = strings.HasSuffix(url, ".tar.gz") || strings.HasSuffix(url, ".tgz")
logger.Debug("根据文件扩展名自动检测压缩格式: isGzipped=%v", isGzipped)
}
if isGzipped {
logger.Debug("使用 gzip 解压")
gzReader, err := gzip.NewReader(resp.Body)
if err != nil {
logger.Debug("创建 gzip reader 失败: %v", err)
return fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzReader.Close()
reader = gzReader
} else {
logger.Debug("tar 格式(未压缩)")
}
// Extract tar archive
if err := extractTar(reader, destDir, options); err != nil {
return fmt.Errorf("failed to extract tar: %w", err)
}
logger.Debug("解压完成")
return nil
}
// extractTar extracts a tar archive to the destination directory
func extractTar(r io.Reader, destDir string, options *Options) error {
tarReader := tar.NewReader(r)
fileCount := 0
totalFiles := 0
// First pass: count total files (optional, for progress reporting)
// For now, we'll just extract directly
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
logger.Debug("读取 tar 条目失败: %v", err)
return fmt.Errorf("failed to read tar header: %w", err)
}
target := filepath.Join(destDir, header.Name)
logger.Debug("解压文件: %s", header.Name)
// Call progress callback if provided
if options.OnProgress != nil {
options.OnProgress(header.Name, fileCount, totalFiles)
}
switch header.Typeflag {
case tar.TypeDir:
// Create directory
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
logger.Debug("创建目录失败 %s: %v", target, err)
return fmt.Errorf("failed to create directory %s: %w", target, err)
}
case tar.TypeReg:
// Create regular file
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
logger.Debug("创建父目录失败 %s: %v", filepath.Dir(target), err)
return fmt.Errorf("failed to create parent directory: %w", err)
}
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
logger.Debug("创建文件失败 %s: %v", target, err)
return fmt.Errorf("failed to create file %s: %w", target, err)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
logger.Debug("写入文件失败 %s: %v", target, err)
return fmt.Errorf("failed to write file %s: %w", target, err)
}
outFile.Close()
fileCount++
case tar.TypeSymlink:
// Create symbolic link
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
logger.Debug("创建符号链接父目录失败 %s: %v", filepath.Dir(target), err)
return fmt.Errorf("failed to create parent directory for symlink: %w", err)
}
// Remove existing file/link if exists
os.Remove(target)
if err := os.Symlink(header.Linkname, target); err != nil {
logger.Debug("创建符号链接失败 %s -> %s: %v", target, header.Linkname, err)
return fmt.Errorf("failed to create symlink %s -> %s: %w", target, header.Linkname, err)
}
fileCount++
default:
logger.Debug("跳过不支持的文件类型: %s (type: %v)", header.Name, header.Typeflag)
}
}
logger.Debug("解压完成,共 %d 个文件", fileCount)
return nil
}

View File

@@ -0,0 +1,47 @@
package archiver
import (
"context"
"testing"
"gitea.loveuer.com/yizhisec/pkg3/logger"
)
func TestDownloadAndExtract(t *testing.T) {
logger.SetLogLevel(logger.LogLevelDebug)
ctx := context.Background()
// Example: download and extract a tar.gz file
err := DownloadAndExtract(
ctx,
"https://example.com/archive.tar.gz",
"/tmp/test-extract",
WithInsecureSkipVerify(),
)
if err != nil {
t.Logf("Expected error for test URL: %v", err)
}
}
func TestDownloadAndExtractWithProgress(t *testing.T) {
logger.SetLogLevel(logger.LogLevelDebug)
ctx := context.Background()
// Example: with progress callback
err := DownloadAndExtract(
ctx,
"https://example.com/archive.tar",
"/tmp/test-extract",
WithInsecureSkipVerify(),
WithProgress(func(filename string, index int, total int) {
t.Logf("Extracting: %s", filename)
}),
)
if err != nil {
t.Logf("Expected error for test URL: %v", err)
}
}

View File

@@ -0,0 +1,195 @@
package downloader
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"gitea.loveuer.com/yizhisec/pkg3/logger"
)
// Options defines options for downloading files
type Options struct {
// InsecureSkipVerify skips TLS certificate verification
InsecureSkipVerify bool
// HTTPClient allows providing a custom HTTP client
HTTPClient *http.Client
// OnProgress is called during download with bytes downloaded and total size
OnProgress func(downloaded, total int64)
// CreateDirs automatically creates parent directories if they don't exist
CreateDirs bool
// Overwrite allows overwriting existing files
Overwrite bool
FileMode os.FileMode
}
// Option is a functional option for configuring the downloader
type Option func(*Options)
// WithInsecureSkipVerify skips TLS certificate verification
func WithInsecureSkipVerify() Option {
return func(o *Options) {
o.InsecureSkipVerify = true
}
}
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(client *http.Client) Option {
return func(o *Options) {
o.HTTPClient = client
}
}
// WithProgress sets a progress callback
func WithProgress(callback func(downloaded, total int64)) Option {
return func(o *Options) {
o.OnProgress = callback
}
}
// WithoutCreateDirs disables automatic creation of parent directories
func WithoutCreateDirs() Option {
return func(o *Options) {
o.CreateDirs = false
}
}
// WithoutOverwrite prevents overwriting existing files
func WithoutOverwrite() Option {
return func(o *Options) {
o.Overwrite = false
}
}
func WithFileMode(mode os.FileMode) Option {
return func(o *Options) {
o.FileMode = mode
}
}
// Download downloads a file from URL to the specified destination
func Download(ctx context.Context, url, dest string, opts ...Option) error {
options := &Options{
InsecureSkipVerify: false,
CreateDirs: true,
Overwrite: true,
FileMode: 0644,
}
for _, opt := range opts {
opt(options)
}
logger.Debug("开始下载文件: %s -> %s", url, dest)
// Check if file exists and overwrite is disabled
if !options.Overwrite {
if _, err := os.Stat(dest); err == nil {
logger.Debug("文件已存在且不允许覆盖: %s", dest)
return fmt.Errorf("file already exists: %s", dest)
}
}
// Create parent directories if needed
if options.CreateDirs {
dir := filepath.Dir(dest)
if err := os.MkdirAll(dir, 0755); err != nil {
logger.Debug("创建目录失败 %s: %v", dir, err)
return fmt.Errorf("failed to create directory: %w", err)
}
}
// Create HTTP client
client := options.HTTPClient
if client == nil {
client = &http.Client{}
if options.InsecureSkipVerify {
logger.Debug("TLS 证书验证已禁用")
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
}
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.Debug("创建请求失败: %v", err)
return fmt.Errorf("failed to create request: %w", err)
}
// Execute request
logger.Debug("发起 HTTP 请求: %s", url)
resp, err := client.Do(req)
if err != nil {
logger.Debug("下载失败: %v", err)
return fmt.Errorf("failed to download: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Debug("HTTP 状态码异常: %d", resp.StatusCode)
return fmt.Errorf("bad status: %s", resp.Status)
}
// Get content length for progress reporting
contentLength := resp.ContentLength
logger.Debug("文件大小: %d bytes", contentLength)
outFile, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, options.FileMode)
if err != nil {
logger.Debug("创建文件失败 %s: %v", dest, err)
return fmt.Errorf("failed to create file: %w", err)
}
defer outFile.Close()
// Copy content with optional progress reporting
var written int64
if options.OnProgress != nil && contentLength > 0 {
// Use progress reader
reader := &progressReader{
reader: resp.Body,
callback: options.OnProgress,
total: contentLength,
}
written, err = io.Copy(outFile, reader)
} else {
written, err = io.Copy(outFile, resp.Body)
}
if err != nil {
logger.Debug("写入文件失败 %s: %v", dest, err)
return fmt.Errorf("failed to write file: %w", err)
}
if options.FileMode != 0 {
if err := os.Chmod(dest, options.FileMode); err != nil {
logger.Debug("设置文件权限失败 %s: %v", dest, err)
return fmt.Errorf("failed to set file mode: %w", err)
}
}
logger.Debug("文件下载成功: %s (%d bytes)", dest, written)
return nil
}
// progressReader wraps an io.Reader to report progress
type progressReader struct {
reader io.Reader
callback func(downloaded, total int64)
total int64
downloaded int64
}
func (pr *progressReader) Read(p []byte) (int, error) {
n, err := pr.reader.Read(p)
pr.downloaded += int64(n)
if pr.callback != nil {
pr.callback(pr.downloaded, pr.total)
}
return n, err
}

62
pkg/resource/emqx.yaml Normal file
View File

@@ -0,0 +1,62 @@
apiVersion: v1
kind: Namespace
metadata:
name: db-emqx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: emqx
namespace: db-emqx
spec:
replicas: 1
selector:
matchLabels:
app: emqx
template:
metadata:
labels:
app: emqx
spec:
containers:
- name: emqx
image: hub.yizhisec.com/external/emqx:5.1
ports:
- containerPort: 1883
name: mqtt
- containerPort: 8883
name: mqtt-ssl
- containerPort: 18083
name: dashboard
- containerPort: 18084
name: websocket
env:
- name: EMQX_NODE_NAME
value: "emqx@single-node"
- name: EMQX_DASHBOARD__DEFAULT_PASSWORD
value: "YizhiSEC@123"
---
apiVersion: v1
kind: Service
metadata:
name: emqx-service
namespace: db-emqx
spec:
selector:
app: emqx
type: ClusterIP
ports:
- name: mqtt
port: 1883
targetPort: 1883
- name: mqtt-ssl
port: 8883
targetPort: 8883
- name: dashboard
port: 18083
targetPort: 18083
- name: websocket
port: 18084
targetPort: 18084

1817
pkg/resource/es.init.sh Normal file

File diff suppressed because it is too large Load Diff

121
pkg/resource/es.yaml Normal file
View File

@@ -0,0 +1,121 @@
apiVersion: v1
kind: Namespace
metadata:
name: db-es
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: db-es
spec:
serviceName: elasticsearch
replicas: 1
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
volumes:
- name: shared-data
emptyDir: {}
securityContext:
fsGroup: 1000
initContainers:
- name: fix-permissions
image: hub.yizhisec.com/hybridscope/v2/es-init-helper:alpine-3.22.2
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
args:
- |
#/bin/sh
cp -rf /data/plugins/* /app/shared/
chown -R 1000:1000 /usr/share/elasticsearch/data
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
- name: shared-data
mountPath: /app/shared
securityContext:
runAsUser: 0
containers:
- name: elasticsearch
image: hub.yizhisec.com/external/elasticsearch:7.17.28
imagePullPolicy: IfNotPresent
env:
- name: discovery.type
value: single-node
- name: ES_JAVA_OPTS
value: "-Xms%dg -Xmx%dg"
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- containerPort: 9200
name: http
- containerPort: 9300
name: transport
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
- name: shared-data
mountPath: /usr/share/elasticsearch/plugins
resources:
requests:
memory: "%dGi"
cpu: "%d"
limits:
memory: "%dGi"
cpu: "%d"
volumeClaimTemplates:
- metadata:
name: es-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn
resources:
requests:
storage: %dGi
---
apiVersion: v1
kind: Service
metadata:
name: es-service
namespace: db-es
spec:
type: ClusterIP
selector:
app: elasticsearch
ports:
- name: http
protocol: TCP
port: 9200
targetPort: http
- name: transport
protocol: TCP
port: 9300
targetPort: transport
---
apiVersion: batch/v1
kind: Job
metadata:
name: es-init-job
namespace: db-es
spec:
template:
spec:
containers:
- name: es-init
image: hub.yizhisec.com/hybridscope/v2/es-init-helper:alpine-3.22.2
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- /data/create_index.sh
restartPolicy: Never
backoffLimit: 2

214
pkg/resource/flannel.yaml Normal file
View File

@@ -0,0 +1,214 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
k8s-app: flannel
pod-security.kubernetes.io/enforce: privileged
name: kube-flannel
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: flannel
name: flannel
namespace: kube-flannel
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: flannel
name: flannel
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: flannel
name: flannel
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flannel
subjects:
- kind: ServiceAccount
name: flannel
namespace: kube-flannel
---
apiVersion: v1
data:
cni-conf.json: |
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.244.0.0/16",
"EnableNFTables": false,
"Backend": {
"Type": "vxlan"
}
}
kind: ConfigMap
metadata:
labels:
app: flannel
k8s-app: flannel
tier: node
name: kube-flannel-cfg
namespace: kube-flannel
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
app: flannel
k8s-app: flannel
tier: node
name: kube-flannel-ds
namespace: kube-flannel
spec:
selector:
matchLabels:
app: flannel
k8s-app: flannel
template:
metadata:
labels:
app: flannel
k8s-app: flannel
tier: node
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
containers:
- args:
- --ip-masq
- --kube-subnet-mgr
command:
- /opt/bin/flanneld
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: EVENT_QUEUE_DEPTH
value: "5000"
- name: CONT_WHEN_CACHE_NOT_READY
value: "false"
image: ghcr.io/flannel-io/flannel:v0.27.4
name: kube-flannel
resources:
requests:
cpu: 100m
memory: 50Mi
securityContext:
capabilities:
add:
- NET_ADMIN
- NET_RAW
privileged: false
volumeMounts:
- mountPath: /run/flannel
name: run
- mountPath: /etc/kube-flannel/
name: flannel-cfg
- mountPath: /run/xtables.lock
name: xtables-lock
hostNetwork: true
initContainers:
- args:
- -f
- /flannel
- /opt/cni/bin/flannel
command:
- cp
image: ghcr.io/flannel-io/flannel-cni-plugin:v1.8.0-flannel1
name: install-cni-plugin
volumeMounts:
- mountPath: /opt/cni/bin
name: cni-plugin
- args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
command:
- cp
image: ghcr.io/flannel-io/flannel:v0.27.4
name: install-cni
volumeMounts:
- mountPath: /etc/cni/net.d
name: cni
- mountPath: /etc/kube-flannel/
name: flannel-cfg
priorityClassName: system-node-critical
serviceAccountName: flannel
tolerations:
- effect: NoSchedule
operator: Exists
volumes:
- hostPath:
path: /run/flannel
name: run
- hostPath:
path: /opt/cni/bin
name: cni-plugin
- hostPath:
path: /etc/cni/net.d
name: cni
- configMap:
name: kube-flannel-cfg
name: flannel-cfg
- hostPath:
path: /run/xtables.lock
type: FileOrCreate
name: xtables-lock

47
pkg/resource/kibana.yaml Normal file
View File

@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: db-es
spec:
replicas: 0
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
containers:
- name: kibana
image: hub.yizhisec.com/external/kibana:7.17.28
imagePullPolicy: IfNotPresent
env:
- name: ELASTICSEARCH_HOSTS
value: http://es-service:9200
- name: SERVER_HOST
value: 0.0.0.0
ports:
- containerPort: 5601
name: http
resources:
limits:
memory: 2Gi
cpu: 1
---
apiVersion: v1
kind: Service
metadata:
name: kibana-service
namespace: db-es
spec:
type: NodePort
selector:
app: kibana
ports:
- name: http
protocol: TCP
port: 5601
targetPort: 5601
nodePort: 31601

View File

@@ -0,0 +1,83 @@
# k8s-hs-less-dns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: hs-net
---
apiVersion: v1
kind: ConfigMap
metadata:
name: config-less-dns
namespace: hs-net
data:
config.yml: |
{
"log": {
"level": "info"
},
"vnet4": "100.64.0.1/10",
"vnet6": "fc00:eeaa:0000:0000::/48",
"redis": {
"custom": [
{
"username": null,
"password": "HybridScope0xRed1s.",
"host": "redis-master.db-redis",
"port": 6379,
"tls_insecure": null,
"db": 10
}
]
},
"mqtt": {
"client_id": "dns_mqtt_client",
"protocol": "tls",
"host": "emqx-service.db-emqx",
"port": 1883,
"cert": "",
"key": "",
"keep_alive": 60
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: less-dns
namespace: hs-net
labels:
app: less-dns
spec:
replicas: 1
selector:
matchLabels:
app: less-dns
template:
metadata:
labels:
app: less-dns
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: less-dns
containers:
- name: less-dns
image: hub.yizhisec.com/hybridscope/less_dns_service:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: config-volume
mountPath: /etc/less_dns_service
securityContext:
privileged: true
volumes:
- name: config-volume
configMap:
name: config-less-dns
items:
- key: config.yml
path: config.yml
restartPolicy: Always

28
pkg/resource/resource.go Normal file
View File

@@ -0,0 +1,28 @@
package resource
import (
_ "embed"
)
var (
//go:embed flannel.yaml
YAMLFlannel []byte
//go:embed es.yaml
YAMLES string
//go:embed kibana.yaml
YAMLKibana []byte
//go:embed es.init.sh
BashESInit []byte
//go:embed emqx.yaml
YAMLEMQX []byte
//go:embed yosguard.create.sql
SQLYosguard []byte
//go:embed less-dns.yaml
YAMLLessDNS []byte
)

View File

@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS `pkg`
(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`path` TEXT,
`installed` INTEGER,
`create_timestamp` INTEGER,
`install_timestamp` INTEGER
);
CREATE TABLE IF NOT EXISTS `patch`
(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`path` TEXT,
`verified` INTEGER,
`installed` INTEGER,
`create_timestamp` INTEGER,
`verify_timestamp` INTEGER,
`install_timestamp` INTEGER
);
-- 记录注册的机器的信息
CREATE TABLE IF NOT EXISTS `machine`
(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`uuid` CHAR(32),
`create_timestamp` INTEGER
);
-- 记录下发的指令
CREATE TABLE IF NOT EXISTS `action`
(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`uuid` CHAR(32),
`action` INTEGER, -- 要下发的指令
`create_timestamp` INTEGER -- 下发命令的时间戳,秒为单位
);