@lazycatcloud/lzc-cli 1.3.14 → 2.0.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/changelog.md +4 -0
- package/lib/app/index.js +174 -58
- package/lib/app/lpk_build.js +192 -17
- package/lib/app/lpk_build_images.js +728 -0
- package/lib/app/lpk_create.js +93 -21
- package/lib/app/lpk_create_generator.js +144 -9
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +14 -7
- package/lib/app/project_cp.js +64 -0
- package/lib/app/project_deploy.js +33 -0
- package/lib/app/project_exec.js +45 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +67 -0
- package/lib/app/project_runtime.js +261 -0
- package/lib/app/project_start.js +100 -0
- package/lib/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +22 -0
- package/lib/config/index.js +1 -1
- package/lib/debug_bridge.js +837 -46
- package/lib/docker/index.js +30 -10
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/locales/en/translation.json +17 -5
- package/lib/i18n/locales/zh/translation.json +16 -4
- package/lib/lpk/core.js +487 -0
- package/lib/lpk/index.js +210 -0
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +3 -1
- package/package.json +2 -1
- package/scripts/cli.js +4 -0
- package/template/_lpk/README.md +11 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
- package/template/_lpk/manifest.yml.in +4 -2
- package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
- package/template/_lpk/todolist-java.manifest.yml.in +15 -0
- package/template/_lpk/todolist-python.manifest.yml.in +15 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/_gitignore +1 -0
- package/template/blank/lzc-build.yml +25 -40
- package/template/blank/lzc-manifest.yml +14 -7
- package/template/golang/Dockerfile +19 -0
- package/template/golang/README.md +33 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.yml +21 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/run.sh +3 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +19 -0
- package/template/gui-vnc/_gitignore +2 -0
- package/template/gui-vnc/images/Dockerfile +30 -0
- package/template/gui-vnc/images/kasmvnc.yaml +33 -0
- package/template/gui-vnc/images/startup-script.desktop +9 -0
- package/template/gui-vnc/images/startup-script.sh +6 -0
- package/template/gui-vnc/lzc-build.yml +23 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/README.md +33 -0
- package/template/python/_gitignore +3 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.yml +21 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +3 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/README.md +33 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/lzc-build.yml +21 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +3 -0
- package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
- package/template/springboot/src/main/resources/application.properties +1 -0
- package/template/springboot/src/main/resources/static/index.html +238 -0
- package/template/vue/README.md +17 -7
- package/template/vue/_gitignore +1 -0
- package/template/vue/lzc-build.yml +31 -42
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +34 -0
- package/template/vue-minidb/_gitignore +26 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.yml +48 -0
- package/template/vue-minidb/lzc-icon.png +0 -0
- package/template/vue-minidb/package.json +21 -0
- package/template/vue-minidb/public/vite.svg +1 -0
- package/template/vue-minidb/src/App.vue +206 -0
- package/template/vue-minidb/src/assets/vue.svg +1 -0
- package/template/vue-minidb/src/main.ts +5 -0
- package/template/vue-minidb/src/style.css +136 -0
- package/template/vue-minidb/src/vite-env.d.ts +1 -0
- package/template/vue-minidb/tsconfig.app.json +24 -0
- package/template/vue-minidb/tsconfig.json +7 -0
- package/template/vue-minidb/tsconfig.node.json +22 -0
- package/template/vue-minidb/vite.config.ts +10 -0
- /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: ${name} # app名称
|
|
2
|
+
package: ${package} # app的唯一标识符
|
|
3
|
+
version: 0.0.1 # app的版本
|
|
4
|
+
description: # app描述
|
|
5
|
+
|
|
6
|
+
license: https://choosealicense.com/licenses/mit/
|
|
7
|
+
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
+
author: # app author
|
|
9
|
+
|
|
10
|
+
application:
|
|
11
|
+
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
12
|
+
image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
|
|
13
|
+
routes:
|
|
14
|
+
# exec:// 路由会执行 /app/run.sh,并把请求转发到 3000 端口
|
|
15
|
+
# 适合「一个容器同时承载前端+后端」的入门场景
|
|
16
|
+
- /=exec://3000,/app/run.sh
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: ${name} # app名称
|
|
2
|
+
package: ${package} # app的唯一标识符
|
|
3
|
+
version: 0.0.1 # app的版本
|
|
4
|
+
description: # app描述
|
|
5
|
+
|
|
6
|
+
license: https://choosealicense.com/licenses/mit/
|
|
7
|
+
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
+
author: # app author
|
|
9
|
+
|
|
10
|
+
application:
|
|
11
|
+
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
12
|
+
image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
|
|
13
|
+
routes:
|
|
14
|
+
# exec:// 路由会执行 /app/run.sh,并把请求转发到 8080 端口
|
|
15
|
+
- /=exec://8080,/app/run.sh
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: ${name} # app名称
|
|
2
|
+
package: ${package} # app的唯一标识符
|
|
3
|
+
version: 0.0.1 # app的版本
|
|
4
|
+
description: # app描述
|
|
5
|
+
|
|
6
|
+
license: https://choosealicense.com/licenses/mit/
|
|
7
|
+
homepage: # 出现bug时候提交反馈的地方
|
|
8
|
+
author: # app author
|
|
9
|
+
|
|
10
|
+
application:
|
|
11
|
+
subdomain: ${subdomain} # 默认访问子域名前缀
|
|
12
|
+
image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
|
|
13
|
+
routes:
|
|
14
|
+
# exec:// 路由会执行 /app/run.sh,并把请求转发到 3000 端口
|
|
15
|
+
- /=exec://3000,/app/run.sh
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# 整个文件中,可以通过 ${var} 的方式,使用 manifest 字段指定的文件定义的值
|
|
2
|
-
|
|
3
1
|
# buildscript
|
|
4
2
|
# - 可以为构建脚本的路径地址
|
|
5
3
|
# - 如果构建命令简单,也可以直接写 sh 的命令
|
|
@@ -20,45 +18,3 @@ icon: ./lazycat.png
|
|
|
20
18
|
|
|
21
19
|
# deploy_params 指定用户运行时输入的模板字段
|
|
22
20
|
# deploy_params: ./lzc-deploy-params.yml
|
|
23
|
-
|
|
24
|
-
# devshell 自定义应用的开发容器环境
|
|
25
|
-
# - routes 指定应用容器的访问路由
|
|
26
|
-
|
|
27
|
-
# devshell 没有指定 image 的情况,将会默认使用 registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
|
|
28
|
-
# devshell:
|
|
29
|
-
# routes:
|
|
30
|
-
# - /=http://127.0.0.1:8080
|
|
31
|
-
|
|
32
|
-
# devshell 指定 image 的情况
|
|
33
|
-
# devshell:
|
|
34
|
-
# routes:
|
|
35
|
-
# - /=http://127.0.0.1:3000
|
|
36
|
-
# image: registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
|
|
37
|
-
|
|
38
|
-
# devshell 指定构建Dockerfile
|
|
39
|
-
# image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
|
|
40
|
-
# devshell:
|
|
41
|
-
# routes:
|
|
42
|
-
# - /=http://127.0.0.1:3000
|
|
43
|
-
# image: ${package}-devshell:${version}
|
|
44
|
-
# pull_policy: build
|
|
45
|
-
# build: .
|
|
46
|
-
|
|
47
|
-
# dvshell 指定开发依赖的情况
|
|
48
|
-
# 这种情况下,选用 alpine:latest 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
|
|
49
|
-
# 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
|
|
50
|
-
devshell:
|
|
51
|
-
routes:
|
|
52
|
-
- /=http://127.0.0.1:3000
|
|
53
|
-
dependencies:
|
|
54
|
-
- nodejs
|
|
55
|
-
- vim
|
|
56
|
-
- npm
|
|
57
|
-
# setupscript 每次进入到app container后都会执行的配置脚本
|
|
58
|
-
# - 可以为脚本的路径地址
|
|
59
|
-
# - 如果构建命令简单,也可以直接写 sh 的命令
|
|
60
|
-
# setupscript: export GOPROXY=https://goproxy.cn
|
|
61
|
-
# setupscript: ./setupscript.sh
|
|
62
|
-
setupscript: |
|
|
63
|
-
export GOPROXY=https://goproxy.cn
|
|
64
|
-
export npm_config_registry=https://registry.npmmirror.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lzc-build.base.yml
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 可选基础文件: lzc-build.base.yml(与当前文件同目录,先加载再与当前文件合并)
|
|
2
2
|
|
|
3
3
|
# buildscript
|
|
4
4
|
# - 可以为构建脚本的路径地址
|
|
@@ -11,48 +11,33 @@ manifest: ./lzc-manifest.yml
|
|
|
11
11
|
# contentdir: 指定打包的内容,将会打包到 lpk 中
|
|
12
12
|
# contentdir: ./
|
|
13
13
|
|
|
14
|
+
# images: 在盒子侧构建镜像并写入 lpk v2 images 目录
|
|
15
|
+
# - key 是 image alias
|
|
16
|
+
# - 在 lzc-manifest.yml 中通过 embed:alias 引用
|
|
17
|
+
# - dockerfile 与 dockerfile-content 二选一
|
|
18
|
+
# - context 可选,默认是 dockerfile 所在目录
|
|
19
|
+
# images:
|
|
20
|
+
# my-nginx:
|
|
21
|
+
# context: ./image
|
|
22
|
+
# dockerfile: ./image/Dockerfile
|
|
23
|
+
# # 可选,默认 registry.lazycat.cloud
|
|
24
|
+
# upstream-match: registry.lazycat.cloud
|
|
25
|
+
# other-image:
|
|
26
|
+
# dockerfile: ./other/Dockerfile
|
|
27
|
+
# inline-image:
|
|
28
|
+
# context: ./
|
|
29
|
+
# dockerfile-content: |
|
|
30
|
+
# FROM alpine:3.20
|
|
31
|
+
# RUN echo "hello"
|
|
32
|
+
#
|
|
33
|
+
# 构建时默认是混合模式:
|
|
34
|
+
# - 先沿父镜像链向上找 upstream
|
|
35
|
+
# - 找到 upstream 后,仅 embed 非 upstream 的 layers
|
|
36
|
+
# - 如果未匹配到 upstream,则自动全量 embed
|
|
37
|
+
|
|
14
38
|
# pkgout: lpk 包的输出路径
|
|
15
39
|
pkgout: ./
|
|
16
40
|
|
|
17
41
|
# icon 指定 lpk 包 icon 的路径路径,如果不指定将会警告
|
|
18
42
|
# icon 仅仅允许 png 后缀的文件
|
|
19
43
|
icon: ./lzc-icon.png
|
|
20
|
-
# devshell 自定义应用的开发容器环境
|
|
21
|
-
# - routes 指定应用容器的访问路由
|
|
22
|
-
|
|
23
|
-
# devshell 没有指定 image 的情况,将会默认使用 registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
|
|
24
|
-
# devshell:
|
|
25
|
-
# routes:
|
|
26
|
-
# - /=http://127.0.0.1:8080
|
|
27
|
-
|
|
28
|
-
# devshell 指定 image 的情况
|
|
29
|
-
# devshell:
|
|
30
|
-
# routes:
|
|
31
|
-
# - /=http://127.0.0.1:3000
|
|
32
|
-
# image: registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
|
|
33
|
-
|
|
34
|
-
# devshell 指定构建Dockerfile
|
|
35
|
-
# image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
|
|
36
|
-
# devshell:
|
|
37
|
-
# routes:
|
|
38
|
-
# - /=http://127.0.0.1:3000
|
|
39
|
-
# image: ${package}-devshell:${version}
|
|
40
|
-
# pull_policy: build
|
|
41
|
-
# build: .
|
|
42
|
-
|
|
43
|
-
# dvshell 指定开发依赖的情况
|
|
44
|
-
# 这种情况下,选用 alpine:latest 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
|
|
45
|
-
# 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
|
|
46
|
-
# devshell:
|
|
47
|
-
# routes:
|
|
48
|
-
# - /=http://127.0.0.1:3000
|
|
49
|
-
# dependencies:
|
|
50
|
-
# - nodejs
|
|
51
|
-
# - npm
|
|
52
|
-
# # setupscript 每次进入到app container后都会执行的配置脚本
|
|
53
|
-
# # - 可以为脚本的路径地址
|
|
54
|
-
# # - 如果构建命令简单,也可以直接写 sh 的命令
|
|
55
|
-
# # setupscript: export GOPROXY=https://goproxy.cn
|
|
56
|
-
# # setupscript: ./setupscript.sh
|
|
57
|
-
# setupscript: |
|
|
58
|
-
# export npm_config_registry=https://registry.npmmirror.com
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
name: helloworld
|
|
2
|
-
package: cloud.lazycat.app.helloworld
|
|
3
|
-
version: 0.0.1
|
|
4
|
-
description:
|
|
1
|
+
name: helloworld # 应用名称(启动器里显示)
|
|
2
|
+
package: cloud.lazycat.app.helloworld # 应用唯一ID,建议使用反向域名格式
|
|
3
|
+
version: 0.0.1 # 应用版本,建议遵循 semver
|
|
4
|
+
description: # 应用描述
|
|
5
5
|
license: https://choosealicense.com/licenses/mit/
|
|
6
|
-
homepage:
|
|
7
|
-
author:
|
|
6
|
+
homepage: # 项目主页或文档地址
|
|
7
|
+
author: # 作者信息
|
|
8
|
+
|
|
9
|
+
# application 是默认前台服务,对应固定 service 名 app
|
|
8
10
|
application:
|
|
9
|
-
subdomain: helloworld
|
|
11
|
+
subdomain: helloworld # 默认访问子域名前缀
|
|
10
12
|
routes:
|
|
13
|
+
# 路由示例:把首页转发到一个外部站点
|
|
14
|
+
# 常见写法:
|
|
15
|
+
# - /=file:///lzcapp/pkg/content/dist (静态前端)
|
|
16
|
+
# - /=exec://3000,/app/run.sh (执行命令并转发到端口)
|
|
17
|
+
# - /api/=http://backend:3000/api/ (转发到其他 service)
|
|
11
18
|
- /=https://developer.lazycat.cloud/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM registry.lazycat.cloud/lzc/lzcapp:3.20.3 AS builder
|
|
2
|
+
|
|
3
|
+
RUN apk add --no-cache go
|
|
4
|
+
WORKDIR /workspace
|
|
5
|
+
|
|
6
|
+
COPY go.mod ./
|
|
7
|
+
COPY main.go ./
|
|
8
|
+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /workspace/app ./main.go
|
|
9
|
+
|
|
10
|
+
FROM registry.lazycat.cloud/lzc/lzcapp:3.20.3
|
|
11
|
+
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY --from=builder /workspace/app /app/app
|
|
14
|
+
COPY run.sh /app/run.sh
|
|
15
|
+
RUN chmod +x /app/run.sh /app/app
|
|
16
|
+
COPY web /lzcapp/pkg/content/web
|
|
17
|
+
|
|
18
|
+
EXPOSE 3000
|
|
19
|
+
CMD ["/app/run.sh"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Lazycat Golang Todo App
|
|
2
|
+
|
|
3
|
+
## First Deploy
|
|
4
|
+
```bash
|
|
5
|
+
lzc-cli project deploy
|
|
6
|
+
lzc-cli project info
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then open the app from launcher or browser.
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
```text
|
|
13
|
+
GET /api/health
|
|
14
|
+
GET /api/todos
|
|
15
|
+
POST /api/todos
|
|
16
|
+
PUT /api/todos/{id}/toggle
|
|
17
|
+
DELETE /api/todos/{id}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Data Path
|
|
21
|
+
```text
|
|
22
|
+
/lzcapp/var/todos.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Build LPK
|
|
26
|
+
```bash
|
|
27
|
+
lzc-cli project build -o golang-app.lpk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
```bash
|
|
32
|
+
lzc-cli lpk install golang-app.lpk
|
|
33
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 可选基础文件: lzc-build.base.yml(与当前文件同目录,先加载再与当前文件合并)
|
|
2
|
+
|
|
3
|
+
# manifest: 指定 lpk 的 manifest 文件
|
|
4
|
+
manifest: ./lzc-manifest.yml
|
|
5
|
+
|
|
6
|
+
# contentdir: 需要打包进 lpk 的目录
|
|
7
|
+
contentdir: ./
|
|
8
|
+
|
|
9
|
+
# pkgout: lpk 输出目录
|
|
10
|
+
pkgout: ./
|
|
11
|
+
|
|
12
|
+
# icon: 应用图标(png)
|
|
13
|
+
icon: ./lzc-icon.png
|
|
14
|
+
|
|
15
|
+
# images: 构建容器镜像并写入 lpk v2 的 images 目录
|
|
16
|
+
images:
|
|
17
|
+
app-runtime:
|
|
18
|
+
# context: Docker 构建上下文目录
|
|
19
|
+
context: ./
|
|
20
|
+
# dockerfile: 镜像构建文件
|
|
21
|
+
dockerfile: ./Dockerfile
|
|
Binary file
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/http"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
"strconv"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync"
|
|
12
|
+
"sync/atomic"
|
|
13
|
+
"time"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
type todoItem struct {
|
|
17
|
+
ID int64 `json:"id"`
|
|
18
|
+
Title string `json:"title"`
|
|
19
|
+
Done bool `json:"done"`
|
|
20
|
+
UpdatedAt int64 `json:"updatedAt"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type todoStore struct {
|
|
24
|
+
mu sync.RWMutex
|
|
25
|
+
nextID atomic.Int64
|
|
26
|
+
items map[int64]todoItem
|
|
27
|
+
file string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func newTodoStore(file string) (*todoStore, error) {
|
|
31
|
+
store := &todoStore{
|
|
32
|
+
items: map[int64]todoItem{},
|
|
33
|
+
file: file,
|
|
34
|
+
}
|
|
35
|
+
store.nextID.Store(1)
|
|
36
|
+
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
|
|
37
|
+
return nil, err
|
|
38
|
+
}
|
|
39
|
+
if err := store.load(); err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
return store, nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (s *todoStore) load() error {
|
|
46
|
+
bs, err := os.ReadFile(s.file)
|
|
47
|
+
if err != nil {
|
|
48
|
+
if os.IsNotExist(err) {
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
return err
|
|
52
|
+
}
|
|
53
|
+
var items []todoItem
|
|
54
|
+
if len(bs) == 0 {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
if err := json.Unmarshal(bs, &items); err != nil {
|
|
58
|
+
return err
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var maxID int64 = 0
|
|
62
|
+
for _, item := range items {
|
|
63
|
+
s.items[item.ID] = item
|
|
64
|
+
if item.ID > maxID {
|
|
65
|
+
maxID = item.ID
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
s.nextID.Store(maxID + 1)
|
|
69
|
+
return nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (s *todoStore) persistLocked() error {
|
|
73
|
+
items := make([]todoItem, 0, len(s.items))
|
|
74
|
+
for _, item := range s.items {
|
|
75
|
+
items = append(items, item)
|
|
76
|
+
}
|
|
77
|
+
sort.Slice(items, func(i, j int) bool {
|
|
78
|
+
return items[i].ID < items[j].ID
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
bs, err := json.MarshalIndent(items, "", " ")
|
|
82
|
+
if err != nil {
|
|
83
|
+
return err
|
|
84
|
+
}
|
|
85
|
+
tmpFile := s.file + ".tmp"
|
|
86
|
+
if err := os.WriteFile(tmpFile, bs, 0o644); err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
return os.Rename(tmpFile, s.file)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func (s *todoStore) list() []todoItem {
|
|
93
|
+
s.mu.RLock()
|
|
94
|
+
defer s.mu.RUnlock()
|
|
95
|
+
items := make([]todoItem, 0, len(s.items))
|
|
96
|
+
for _, item := range s.items {
|
|
97
|
+
items = append(items, item)
|
|
98
|
+
}
|
|
99
|
+
sort.Slice(items, func(i, j int) bool {
|
|
100
|
+
return items[i].UpdatedAt > items[j].UpdatedAt
|
|
101
|
+
})
|
|
102
|
+
return items
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func (s *todoStore) add(title string) (todoItem, error) {
|
|
106
|
+
id := s.nextID.Add(1) - 1
|
|
107
|
+
item := todoItem{
|
|
108
|
+
ID: id,
|
|
109
|
+
Title: title,
|
|
110
|
+
Done: false,
|
|
111
|
+
UpdatedAt: time.Now().UnixMilli(),
|
|
112
|
+
}
|
|
113
|
+
s.mu.Lock()
|
|
114
|
+
s.items[id] = item
|
|
115
|
+
err := s.persistLocked()
|
|
116
|
+
s.mu.Unlock()
|
|
117
|
+
if err != nil {
|
|
118
|
+
return todoItem{}, err
|
|
119
|
+
}
|
|
120
|
+
return item, nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (s *todoStore) toggle(id int64) (todoItem, bool, error) {
|
|
124
|
+
s.mu.Lock()
|
|
125
|
+
defer s.mu.Unlock()
|
|
126
|
+
item, ok := s.items[id]
|
|
127
|
+
if !ok {
|
|
128
|
+
return todoItem{}, false, nil
|
|
129
|
+
}
|
|
130
|
+
item.Done = !item.Done
|
|
131
|
+
item.UpdatedAt = time.Now().UnixMilli()
|
|
132
|
+
s.items[id] = item
|
|
133
|
+
if err := s.persistLocked(); err != nil {
|
|
134
|
+
return todoItem{}, false, err
|
|
135
|
+
}
|
|
136
|
+
return item, true, nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func (s *todoStore) remove(id int64) (bool, error) {
|
|
140
|
+
s.mu.Lock()
|
|
141
|
+
defer s.mu.Unlock()
|
|
142
|
+
if _, ok := s.items[id]; !ok {
|
|
143
|
+
return false, nil
|
|
144
|
+
}
|
|
145
|
+
delete(s.items, id)
|
|
146
|
+
if err := s.persistLocked(); err != nil {
|
|
147
|
+
return false, err
|
|
148
|
+
}
|
|
149
|
+
return true, nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func writeJSON(w http.ResponseWriter, status int, value any) {
|
|
153
|
+
w.Header().Set("Content-Type", "application/json")
|
|
154
|
+
w.WriteHeader(status)
|
|
155
|
+
_ = json.NewEncoder(w).Encode(value)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func main() {
|
|
159
|
+
store, err := newTodoStore("/lzcapp/var/todos.json")
|
|
160
|
+
if err != nil {
|
|
161
|
+
panic(err)
|
|
162
|
+
}
|
|
163
|
+
mux := http.NewServeMux()
|
|
164
|
+
|
|
165
|
+
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
|
166
|
+
if r.Method != http.MethodGet {
|
|
167
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
mux.HandleFunc("/api/todos", func(w http.ResponseWriter, r *http.Request) {
|
|
174
|
+
switch r.Method {
|
|
175
|
+
case http.MethodGet:
|
|
176
|
+
writeJSON(w, http.StatusOK, map[string]any{"items": store.list()})
|
|
177
|
+
case http.MethodPost:
|
|
178
|
+
var payload struct {
|
|
179
|
+
Title string `json:"title"`
|
|
180
|
+
}
|
|
181
|
+
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
182
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
title := strings.TrimSpace(payload.Title)
|
|
186
|
+
if title == "" {
|
|
187
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "title is required"})
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
item, err := store.add(title)
|
|
191
|
+
if err != nil {
|
|
192
|
+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to persist todos"})
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
writeJSON(w, http.StatusOK, item)
|
|
196
|
+
default:
|
|
197
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
mux.HandleFunc("/api/todos/", func(w http.ResponseWriter, r *http.Request) {
|
|
202
|
+
tail := strings.TrimPrefix(r.URL.Path, "/api/todos/")
|
|
203
|
+
if strings.HasSuffix(tail, "/toggle") {
|
|
204
|
+
if r.Method != http.MethodPut {
|
|
205
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
idPart := strings.TrimSuffix(tail, "/toggle")
|
|
209
|
+
idPart = strings.TrimSuffix(idPart, "/")
|
|
210
|
+
id, err := strconv.ParseInt(idPart, 10, 64)
|
|
211
|
+
if err != nil {
|
|
212
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
item, ok, err := store.toggle(id)
|
|
216
|
+
if err != nil {
|
|
217
|
+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to persist todos"})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if !ok {
|
|
221
|
+
writeJSON(w, http.StatusNotFound, map[string]string{"error": "todo not found"})
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
writeJSON(w, http.StatusOK, item)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if r.Method != http.MethodDelete {
|
|
229
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
idPart := strings.TrimSuffix(tail, "/")
|
|
233
|
+
id, err := strconv.ParseInt(idPart, 10, 64)
|
|
234
|
+
if err != nil {
|
|
235
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
removed, err := store.remove(id)
|
|
239
|
+
if err != nil {
|
|
240
|
+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to persist todos"})
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if !removed {
|
|
244
|
+
writeJSON(w, http.StatusNotFound, map[string]string{"error": "todo not found"})
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
mux.Handle("/", http.FileServer(http.Dir("/lzcapp/pkg/content/web")))
|
|
251
|
+
_ = http.ListenAndServe(":3000", mux)
|
|
252
|
+
}
|