diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..4862d6b --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,59 @@ +run-name: build ushare +on: + push: + tags: + - 'v*' + +jobs: + build ushare: + runs-on: tencent-sg + steps: + - name: prepare enviroment + uses: actions/checkout@v4 + + - name: prints date + run: date '+%Y-%m-%dT%H:%M:%S' + + - name: print operator + run: whoami + + - name: print tag name + run: echo "Tag name = ${{ gitea.ref_name }}" + + - name: build prepare config + run: | + cat << EOF > .docker.config.json + ${{ secrets.DOCKER_CONFIG }} + EOF + + - name: print work dir and files + run: pwd & ls -alsh . + + - name: build image by docker build + run: docker build -t gitea.loveuer.com/loveuer/build/ushare:${{ gitea.ref_name }} . + + - name: login repository + run: echo ${{ secrets.DOCKER_REPOSITORY_PASSWORD }} | docker login --username loveuer --password-stdin gitea.loveuer.com/loveuer + + - name: push image to repository + run: docker push gitea.loveuer.com/loveuer/build/ushare:${{ gitea.ref_name }} + +# - name: build by kaniko in docker +# run: | +# docker run --rm -v $(pwd):/workspace \ +# -v $(pwd)/.docker.config.json:/kaniko/.docker/config.json:ro \ +# alpine:latest \ +# ls -alsh /workspace +# gcr.io/kaniko-project/executor:latest \ +# --dockerfile=/workspace/Dockerfile \ +# --context=/workspace \ +# --destination=gitea.loveuer.com/loveuer/build/u-api:${{ gitea.ref_name }} \ +# --single-snapshot + + clean: + if: always() + runs-on: tencent-sg + steps: + - name: clean docker config + run: | + rm -rf .docker.config.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7537b90..f347b52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,4 +32,4 @@ COPY deployment/nginx.conf /etc/nginx/conf.d EXPOSE 80 # 启动服务 -CMD sh -c "ushare & nginx -g 'daemon off;'" \ No newline at end of file +CMD ["sh", "-c", "nginx -g 'daemon off;' & exec ushare"] \ No newline at end of file diff --git a/deployment/nginx.conf b/deployment/nginx.conf index f4e47ac..f19f5dd 100644 --- a/deployment/nginx.conf +++ b/deployment/nginx.conf @@ -1,6 +1,5 @@ server { listen 80; - server_name localhost; location / { root /usr/share/nginx/html; @@ -13,4 +12,11 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } + + location /ushare { + proxy_pass http://localhost:9119; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } } \ No newline at end of file diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..5b29f78 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,32 @@ +import {useState} from "react"; +import {message} from "../component/message/u-message.tsx"; + +export interface User { + id: number; + username: string; + login_at: number; + token: string; +} + +export const useAuth = () => { + const [user, setUser] = useState(null) + + const login = async (username: string, password: string) => { + let res = await fetch("/api/uauth/login", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({username: username, password: password}), + }) + + if(res.status !== 200) { + message.error("账号或密码错误") + throw new Error(await res.text()) + } + + let jes = await res.json() as User; + + setUser(jes) + } + + return {user, login} +} \ No newline at end of file diff --git a/frontend/src/api/upload.ts b/frontend/src/api/upload.ts index 0bad589..705d167 100644 --- a/frontend/src/api/upload.ts +++ b/frontend/src/api/upload.ts @@ -17,7 +17,7 @@ export const useFileUpload = () => { try { console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file); - const url = `/api/share/${file.name}`; + const url = `/api/ushare/${file.name}`; // 1. 初始化上传 const res1 = await fetch(url, { @@ -51,7 +51,7 @@ export const useFileUpload = () => { const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); - const res = await fetch(`/api/share/${code}`, { + const res = await fetch(`/api/ushare/${code}`, { method: "POST", headers: { "Range": `bytes=${start}-${end - 1}`, diff --git a/frontend/src/page/component/panel-right.tsx b/frontend/src/page/component/panel-right.tsx index 47433aa..f7d6c3a 100644 --- a/frontend/src/page/component/panel-right.tsx +++ b/frontend/src/page/component/panel-right.tsx @@ -41,7 +41,7 @@ export const PanelRight = () => { } async function onFetchFile() { - const url = `/api/share/${code}` + const url = `/ushare/${code}` console.log('[D] onFetchFile: url =', url) const link = document.createElement('a'); link.href = url; diff --git a/frontend/src/page/login.tsx b/frontend/src/page/login.tsx index 7d07d0d..0dfc70c 100644 --- a/frontend/src/page/login.tsx +++ b/frontend/src/page/login.tsx @@ -1,6 +1,7 @@ -import React from "react"; -import {createUseStyles} from "react-jss"; -import {CloudBackground} from "../component/fluid/cloud.tsx"; +import React, { useState } from "react"; +import { createUseStyles } from "react-jss"; +import { CloudBackground } from "../component/fluid/cloud.tsx"; +import {useAuth} from "../api/auth.ts"; const useClass = createUseStyles({ container: { @@ -75,24 +76,106 @@ const useClass = createUseStyles({ }, }, }, + + inputContainer: { + position: 'relative', + width: '100%', + marginTop: '20px', + }, + inputField: { + width: "calc(100% - 52px)", + padding: "12px 35px 12px 15px", + border: "1px solid #ddd", + borderRadius: "6px", + fontSize: "16px", + transition: "border-color 0.3s", + "&:focus": { + outline: "none", + borderColor: "#1a73e8", + boxShadow: "0 0 0 2px rgba(26, 115, 232, 0.2)", + }, + "&:hover": { + borderColor: "#1a73e8", + } + }, + iconButton: { + position: 'absolute', + right: '10px', + top: '50%', + transform: 'translateY(-50%)', + background: 'transparent', + border: 'none', + cursor: 'pointer', + color: '#666', + "&:hover": { + color: '#333', + } + } }) export const Login: React.FC = () => { const classes = useClass() + const {login} = useAuth() + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [showPassword, setShowPassword] = useState(false) + + const onLogin = async () => { + try { + await login(username, password) + window.location.href = "/" + } catch (_e) { + + } + } return

UShare

-
- + + {/* 用户名输入框 */} +
+ setUsername(e.target.value)} + /> + {username && ( + + )}
-
- + + {/* 密码输入框 */} +
+ setPassword(e.target.value)} + /> +
+
- +
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c755c1e..8cca44f 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,7 +9,11 @@ export default defineConfig({ '/api': { target: 'http://127.0.0.1:9119', changeOrigin: true - } + }, + '/ushare': { + target: 'http://127.0.0.1:9119', + changeOrigin: true + }, } } }) diff --git a/go.mod b/go.mod index 56935b0..6c3e22a 100644 --- a/go.mod +++ b/go.mod @@ -24,12 +24,15 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/matoous/go-nanoid/v2 v2.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect @@ -44,9 +47,9 @@ require ( golang.org/x/crypto v0.32.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 679111f..f5da7c5 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= +github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -66,6 +68,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -76,6 +80,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= @@ -135,6 +142,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -151,6 +159,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -167,6 +177,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/api/api.go b/internal/api/api.go index 5d6d585..3774e7c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -18,9 +18,10 @@ func Start(ctx context.Context) <-chan struct{} { return c.SendStatus(http.StatusOK) }) - app.Get("/api/share/:code", handler.Fetch()) - app.Put("/api/share/:filename", handler.ShareNew()) // 获取上传 code, 分片大小 - app.Post("/api/share/:code", handler.ShareUpload()) // 分片上传接口 + app.Get("/ushare/:code", handler.Fetch()) + app.Put("/api/ushare/:filename", handler.AuthVerify(), handler.ShareNew()) // 获取上传 code, 分片大小 + app.Post("/api/ushare/:code", handler.ShareUpload()) // 分片上传接口 + app.Post("/api/uauth/login", handler.AuthLogin()) ready := make(chan struct{}) ln, err := net.Listen("tcp", opt.Cfg.Address) diff --git a/internal/controller/meta.go b/internal/controller/meta.go index 1ee708c..818061f 100644 --- a/internal/controller/meta.go +++ b/internal/controller/meta.go @@ -4,10 +4,14 @@ import ( "context" "fmt" "github.com/loveuer/nf/nft/log" + "github.com/loveuer/ushare/internal/model" "github.com/loveuer/ushare/internal/opt" gonanoid "github.com/matoous/go-nanoid/v2" + "github.com/spf13/viper" "io" "os" + "path/filepath" + "strings" "sync" "time" ) @@ -19,12 +23,12 @@ type metaInfo struct { last time.Time size int64 cursor int64 - ip string + user string } func (m *metaInfo) generateMeta(code string) error { - content := fmt.Sprintf("filename=%s\ncreated_at=%d\nsize=%d\nuploader_ip=%s", - m.name, m.create.UnixMilli(), m.size, m.ip, + content := fmt.Sprintf("filename=%s\ncreated_at=%d\nsize=%d\nuploader=%s", + m.name, m.create.UnixMilli(), m.size, m.user, ) return os.WriteFile(opt.MetaPath(code), []byte(content), 0644) @@ -62,7 +66,7 @@ func (m *meta) New(size int64, filename, ip string) (string, error) { m.Lock() defer m.Unlock() - m.m[code] = &metaInfo{f: f, name: filename, last: now, size: size, cursor: 0, create: now, ip: ip} + m.m[code] = &metaInfo{f: f, name: filename, last: now, size: size, cursor: 0, create: now, user: ip} return code, nil } @@ -100,6 +104,7 @@ func (m *meta) Start(ctx context.Context) { ticker := time.NewTicker(time.Minute) m.ctx = ctx + // 清理 2 分钟内没有继续上传的 part go func() { for { select { @@ -107,7 +112,7 @@ func (m *meta) Start(ctx context.Context) { return case now := <-ticker.C: for code, info := range m.m { - if now.Sub(info.last) > 1*time.Minute { + if now.Sub(info.last) > 2*time.Minute { m.Lock() if err := info.f.Close(); err != nil { log.Warn("handler.Meta: [timer] close file failed, file = %s, err = %s", opt.FilePath(code), err.Error()) @@ -123,4 +128,57 @@ func (m *meta) Start(ctx context.Context) { } } }() + + // 清理一天前的文件 + go func() { + ticker := time.NewTicker(5 * time.Minute) + + for { + select { + case <-ctx.Done(): + return + case now := <-ticker.C: + _ = filepath.Walk(opt.Cfg.DataPath, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + name := filepath.Base(info.Name()) + if !strings.HasPrefix(name, ".meta.") { + return nil + } + + viper.SetConfigFile(path) + viper.SetConfigType("env") + if err = viper.ReadInConfig(); err != nil { + // todo log + return nil + } + + mi := new(model.Meta) + + if err = viper.Unmarshal(mi); err != nil { + // todo log + return nil + } + + code := strings.TrimPrefix(name, ".meta.") + + if now.Sub(time.UnixMilli(mi.CreatedAt)) > 24*time.Hour { + + log.Debug("controller.meta: file out of date, code = %s, user_key = %s", code, mi.Uploader) + + os.RemoveAll(opt.FilePath(code)) + os.RemoveAll(path) + + m.Lock() + delete(m.m, code) + m.Unlock() + } + + return nil + }) + } + } + }() } diff --git a/internal/controller/user.go b/internal/controller/user.go new file mode 100644 index 0000000..9a6de51 --- /dev/null +++ b/internal/controller/user.go @@ -0,0 +1,86 @@ +package controller + +import ( + "context" + "github.com/loveuer/ushare/internal/model" + "github.com/loveuer/ushare/internal/opt" + "github.com/loveuer/ushare/internal/pkg/tool" + "github.com/pkg/errors" + "sync" + "time" +) + +type userManager struct { + sync.Mutex + ctx context.Context + um map[string]*model.User +} + +func (um *userManager) Login(username string, password string) (*model.User, error) { + var ( + now = time.Now() + ) + + if username != "admin" { + return nil, errors.New("账号或密码错误") + } + + if !tool.ComparePassword(password, opt.Cfg.Auth) { + return nil, errors.New("账号或密码错误") + } + + op := &model.User{ + Id: 1, + Username: username, + LoginAt: now.Unix(), + Token: tool.RandomString(32), + } + + um.Lock() + defer um.Unlock() + um.um[op.Token] = op + + return op, nil +} + +func (um *userManager) Verify(token string) (*model.User, error) { + um.Lock() + defer um.Unlock() + + op, ok := um.um[token] + if !ok { + return nil, errors.New("未登录或凭证已失效, 请重新登录") + } + + return op, nil +} + +func (um *userManager) Start(ctx context.Context) { + um.ctx = ctx + + go func() { + + ticker := time.NewTicker(time.Minute) + + for { + select { + case <-um.ctx.Done(): + return + case now := <-ticker.C: + um.Lock() + for _, op := range um.um { + if now.Sub(time.UnixMilli(op.LoginAt)) > 8*time.Hour { + delete(um.um, op.Token) + } + } + um.Unlock() + } + } + }() +} + +var ( + UserManager = &userManager{ + um: make(map[string]*model.User), + } +) diff --git a/internal/handler/auth.go b/internal/handler/auth.go new file mode 100644 index 0000000..35e8d09 --- /dev/null +++ b/internal/handler/auth.go @@ -0,0 +1,70 @@ +package handler + +import ( + "fmt" + "github.com/loveuer/nf" + "github.com/loveuer/ushare/internal/controller" + "github.com/loveuer/ushare/internal/model" + "github.com/loveuer/ushare/internal/opt" + "net/http" +) + +func AuthVerify() nf.HandlerFunc { + tokenFn := func(c *nf.Ctx) (token string) { + if token = c.Get("Authorization"); token != "" { + return + } + + token = c.Cookies("ushare") + + return + } + + return func(c *nf.Ctx) error { + if opt.Cfg.Auth == "" { + return c.Next() + } + + token := tokenFn(c) + if token == "" { + return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"}) + } + + op, err := controller.UserManager.Verify(token) + if err != nil { + return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized", "msg": err.Error()}) + } + + c.Locals("user", op) + + return c.Next() + } +} + +func AuthLogin() nf.HandlerFunc { + return func(c *nf.Ctx) error { + type Req struct { + Username string `json:"username"` + Password string `json:"password"` + } + + var ( + err error + req Req + op *model.User + ) + + if err = c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "错误的用户名或密码<1>"}) + } + + if op, err = controller.UserManager.Login(req.Username, req.Password); err != nil { + return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()}) + } + + header := fmt.Sprintf("ushare=%s; Path=/; Max-Age=%d", op.Token, 8*3600) + c.SetHeader("Set-Cookie", header) + + return c.Status(http.StatusOK).JSON(map[string]any{"data": op}) + } +} diff --git a/internal/handler/share.go b/internal/handler/share.go index ce06c0f..3eb0df2 100644 --- a/internal/handler/share.go +++ b/internal/handler/share.go @@ -49,11 +49,6 @@ func Fetch() nf.HandlerFunc { func ShareNew() nf.HandlerFunc { return func(c *nf.Ctx) error { - - if opt.Cfg.Auth { - return c.SendStatus(http.StatusUnauthorized) - } - filename := strings.TrimSpace(c.Param("filename")) if filename == "" { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "filename required"}) diff --git a/internal/model/meta.go b/internal/model/meta.go index 6a96ef5..6b7a4d4 100644 --- a/internal/model/meta.go +++ b/internal/model/meta.go @@ -1,8 +1,8 @@ package model type Meta struct { - Filename string `json:"filename" mapstructure:"filename"` - CreatedAt int64 `json:"created_at" mapstructure:"created_at"` - Size int64 `json:"size" mapstructure:"size"` - UploaderIp string `json:"uploader_ip" mapstructure:"uploader_ip"` + Filename string `json:"filename" mapstructure:"filename"` + CreatedAt int64 `json:"created_at" mapstructure:"created_at"` + Size int64 `json:"size" mapstructure:"size"` + Uploader string `json:"uploader" mapstructure:"uploader"` } diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..ddc7a03 --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,10 @@ +package model + +type User struct { + Id int `json:"id"` + Username string `json:"username"` + Key string `json:"key"` + Password string `json:"-"` + LoginAt int64 `json:"login_at"` + Token string `json:"token"` +} diff --git a/internal/opt/opt.go b/internal/opt/opt.go index 47daee7..35049f4 100644 --- a/internal/opt/opt.go +++ b/internal/opt/opt.go @@ -1,12 +1,25 @@ package opt +import ( + "context" + "github.com/loveuer/nf/nft/log" + "github.com/loveuer/ushare/internal/pkg/tool" +) + type config struct { Debug bool Address string DataPath string - Auth bool + Auth string } var ( Cfg = &config{} ) + +func Init(_ context.Context) { + if Cfg.Auth != "" { + Cfg.Auth = tool.NewPassword(Cfg.Auth) + log.Debug("opt.Init: encrypted password = %s", Cfg.Auth) + } +} diff --git a/internal/pkg/db/client.go b/internal/pkg/db/client.go new file mode 100644 index 0000000..cc35ab2 --- /dev/null +++ b/internal/pkg/db/client.go @@ -0,0 +1,51 @@ +package db + +import ( + "au99999/internal/opt" + "au99999/pkg/tool" + "context" + + "gorm.io/gorm" +) + +var Default *Client + +type DBType string + +const ( + DBTypeSqlite = "sqlite" + DBTypeMysql = "mysql" + DBTypePostgres = "postgres" +) + +type Client struct { + ctx context.Context + cli *gorm.DB + dbType DBType +} + +func (c *Client) Type() DBType { + return c.dbType +} + +func (c *Client) Session(ctxs ...context.Context) *gorm.DB { + var ctx context.Context + if len(ctxs) > 0 && ctxs[0] != nil { + ctx = ctxs[0] + } else { + ctx = tool.Timeout(30) + } + + session := c.cli.Session(&gorm.Session{Context: ctx}) + + if opt.Cfg.Debug { + session = session.Debug() + } + + return session +} + +func (c *Client) Close() { + d, _ := c.cli.DB() + d.Close() +} diff --git a/internal/pkg/db/new.go b/internal/pkg/db/new.go new file mode 100644 index 0000000..54b4165 --- /dev/null +++ b/internal/pkg/db/new.go @@ -0,0 +1,65 @@ +package db + +import ( + "context" + "fmt" + "strings" + + "github.com/glebarez/sqlite" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +const tip = `example: +for sqlite -> sqlite:: + sqlite::data.sqlite + sqlite::/data/data.db +for mysql -> mysql:: + mysql::user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local +for postgres -> postgres:: + postgres::host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai +` + +func New(ctx context.Context, uri string) (*Client, error) { + parts := strings.SplitN(uri, "::", 2) + + if len(parts) != 2 { + return nil, fmt.Errorf("db.Init: db uri invalid\n%s", tip) + } + + c := &Client{} + + var ( + err error + dsn = parts[1] + ) + + switch parts[0] { + case "sqlite": + c.dbType = DBTypeSqlite + c.cli, err = gorm.Open(sqlite.Open(dsn)) + case "mysql": + c.dbType = DBTypeMysql + c.cli, err = gorm.Open(mysql.Open(dsn)) + case "postgres": + c.dbType = DBTypePostgres + c.cli, err = gorm.Open(postgres.Open(dsn)) + default: + return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", parts[0]) + } + + if err != nil { + return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", parts[0], dsn, err) + } + + return c, nil +} + +func Init(ctx context.Context, uri string) (err error) { + if Default, err = New(ctx, uri); err != nil { + return err + } + + return nil +} diff --git a/internal/pkg/tool/ctx.go b/internal/pkg/tool/ctx.go new file mode 100644 index 0000000..82242a3 --- /dev/null +++ b/internal/pkg/tool/ctx.go @@ -0,0 +1,38 @@ +package tool + +import ( + "context" + "time" +) + +func Timeout(seconds ...int) (ctx context.Context) { + var ( + duration time.Duration + ) + + if len(seconds) > 0 && seconds[0] > 0 { + duration = time.Duration(seconds[0]) * time.Second + } else { + duration = time.Duration(30) * time.Second + } + + ctx, _ = context.WithTimeout(context.Background(), duration) + + return +} + +func TimeoutCtx(ctx context.Context, seconds ...int) context.Context { + var ( + duration time.Duration + ) + + if len(seconds) > 0 && seconds[0] > 0 { + duration = time.Duration(seconds[0]) * time.Second + } else { + duration = time.Duration(30) * time.Second + } + + nctx, _ := context.WithTimeout(ctx, duration) + + return nctx +} diff --git a/internal/pkg/tool/human.go b/internal/pkg/tool/human.go new file mode 100644 index 0000000..92144e3 --- /dev/null +++ b/internal/pkg/tool/human.go @@ -0,0 +1,50 @@ +package tool + +import "fmt" + +func HumanDuration(nano int64) string { + duration := float64(nano) + unit := "ns" + if duration >= 1000 { + duration /= 1000 + unit = "us" + } + + if duration >= 1000 { + duration /= 1000 + unit = "ms" + } + + if duration >= 1000 { + duration /= 1000 + unit = " s" + } + + return fmt.Sprintf("%6.2f%s", duration, unit) +} + +func HumanSize(size int64) string { + const ( + _ = iota + KB = 1 << (10 * iota) // 1 KB = 1024 bytes + MB // 1 MB = 1024 KB + GB // 1 GB = 1024 MB + TB // 1 TB = 1024 GB + PB // 1 PB = 1024 TB + ) + + switch { + case size >= PB: + return fmt.Sprintf("%.2f PB", float64(size)/PB) + case size >= TB: + return fmt.Sprintf("%.2f TB", float64(size)/TB) + case size >= GB: + return fmt.Sprintf("%.2f GB", float64(size)/GB) + case size >= MB: + return fmt.Sprintf("%.2f MB", float64(size)/MB) + case size >= KB: + return fmt.Sprintf("%.2f KB", float64(size)/KB) + default: + return fmt.Sprintf("%d bytes", size) + } +} diff --git a/internal/pkg/tool/loadash.go b/internal/pkg/tool/loadash.go new file mode 100644 index 0000000..e8a732f --- /dev/null +++ b/internal/pkg/tool/loadash.go @@ -0,0 +1,68 @@ +package tool + +import "math" + +func Map[T, R any](vals []T, fn func(item T, index int) R) []R { + var result = make([]R, len(vals)) + for idx, v := range vals { + result[idx] = fn(v, idx) + } + return result +} + +func Chunk[T any](vals []T, size int) [][]T { + if size <= 0 { + panic("Second parameter must be greater than 0") + } + + chunksNum := len(vals) / size + if len(vals)%size != 0 { + chunksNum += 1 + } + + result := make([][]T, 0, chunksNum) + + for i := 0; i < chunksNum; i++ { + last := (i + 1) * size + if last > len(vals) { + last = len(vals) + } + result = append(result, vals[i*size:last:last]) + } + + return result +} + +// 对 vals 取样 x 个 +func Sample[T any](vals []T, x int) []T { + if x < 0 { + panic("Second parameter can't be negative") + } + + n := len(vals) + if n == 0 { + return []T{} + } + + if x >= n { + return vals + } + + // 处理x=1的特殊情况 + if x == 1 { + return []T{vals[(n-1)/2]} + } + + // 计算采样步长并生成结果数组 + step := float64(n-1) / float64(x-1) + result := make([]T, x) + + for i := 0; i < x; i++ { + // 计算采样位置并四舍五入 + pos := float64(i) * step + index := int(math.Round(pos)) + result[i] = vals[index] + } + + return result +} diff --git a/internal/pkg/tool/must.go b/internal/pkg/tool/must.go new file mode 100644 index 0000000..0615f8d --- /dev/null +++ b/internal/pkg/tool/must.go @@ -0,0 +1,11 @@ +package tool + +import "github.com/loveuer/nf/nft/log" + +func Must(errs ...error) { + for _, err := range errs { + if err != nil { + log.Panic(err.Error()) + } + } +} diff --git a/internal/pkg/tool/password.go b/internal/pkg/tool/password.go new file mode 100644 index 0000000..c2d1a17 --- /dev/null +++ b/internal/pkg/tool/password.go @@ -0,0 +1,84 @@ +package tool + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "github.com/loveuer/nf/nft/log" + "golang.org/x/crypto/pbkdf2" + "regexp" + "strconv" + "strings" +) + +const ( + EncryptHeader string = "pbkdf2:sha256" // 用户密码加密 +) + +func NewPassword(password string) string { + return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000)) +} + +func ComparePassword(in, db string) bool { + strs := strings.Split(db, "$") + if len(strs) != 3 { + log.Error("password in db invalid: %s", db) + return false + } + + encs := strings.Split(strs[0], ":") + if len(encs) != 3 { + log.Error("password in db invalid: %s", db) + return false + } + + encIteration, err := strconv.Atoi(encs[2]) + if err != nil { + log.Error("password in db invalid: %s, convert iter err: %s", db, err) + return false + } + + return EncryptPassword(in, strs[1], encIteration) == db +} + +func EncryptPassword(password, salt string, iter int) string { + hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New) + encrypted := hex.EncodeToString(hash) + return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted) +} + +func CheckPassword(password string) error { + if len(password) < 8 || len(password) > 32 { + return errors.New("密码长度不符合") + } + + var ( + err error + match bool + patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`} + matchAccount = 0 + tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"} + locktips = make([]string, 0) + ) + + for idx, pattern := range patternList { + match, err = regexp.MatchString(pattern, password) + if err != nil { + log.Warn("regex match string err, reg_str: %s, err: %v", pattern, err) + return errors.New("密码强度不够") + } + + if match { + matchAccount++ + } else { + locktips = append(locktips, tips[idx]) + } + } + + if matchAccount < 3 { + return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", ")) + } + + return nil +} diff --git a/internal/pkg/tool/password_test.go b/internal/pkg/tool/password_test.go new file mode 100644 index 0000000..9aadac1 --- /dev/null +++ b/internal/pkg/tool/password_test.go @@ -0,0 +1,20 @@ +package tool + +import "testing" + +func TestEncPassword(t *testing.T) { + password := "123456" + + result := EncryptPassword(password, RandomString(8), 50000) + + t.Logf("sum => %s", result) +} + +func TestPassword(t *testing.T) { + p := "wahaha@123" + p = NewPassword(p) + t.Logf("password => %s", p) + + result := ComparePassword("wahaha@123", p) + t.Logf("compare result => %v", result) +} diff --git a/internal/pkg/tool/random.go b/internal/pkg/tool/random.go new file mode 100644 index 0000000..266cb4c --- /dev/null +++ b/internal/pkg/tool/random.go @@ -0,0 +1,54 @@ +package tool + +import ( + "crypto/rand" + "math/big" +) + +var ( + letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + letterNum = []byte("0123456789") + letterLow = []byte("abcdefghijklmnopqrstuvwxyz") + letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + letterSyb = []byte("!@#$%^&*()_+-=") +) + +func RandomInt(max int64) int64 { + num, _ := rand.Int(rand.Reader, big.NewInt(max)) + return num.Int64() +} + +func RandomString(length int) string { + result := make([]byte, length) + for i := 0; i < length; i++ { + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + result[i] = letters[num.Int64()] + } + return string(result) +} + +func RandomPassword(length int, withSymbol bool) string { + result := make([]byte, length) + kind := 3 + if withSymbol { + kind++ + } + + for i := 0; i < length; i++ { + switch i % kind { + case 0: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum)))) + result[i] = letterNum[num.Int64()] + case 1: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow)))) + result[i] = letterLow[num.Int64()] + case 2: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap)))) + result[i] = letterCap[num.Int64()] + case 3: + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb)))) + result[i] = letterSyb[num.Int64()] + } + } + return string(result) +} diff --git a/internal/pkg/tool/table.go b/internal/pkg/tool/table.go new file mode 100644 index 0000000..ffaaf31 --- /dev/null +++ b/internal/pkg/tool/table.go @@ -0,0 +1,124 @@ +package tool + +import ( + "encoding/json" + "fmt" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/loveuer/nf/nft/log" + "io" + "os" + "reflect" + "strings" +) + +func TablePrinter(data any, writers ...io.Writer) { + var w io.Writer = os.Stdout + if len(writers) > 0 && writers[0] != nil { + w = writers[0] + } + + t := table.NewWriter() + structPrinter(t, "", data) + _, _ = fmt.Fprintln(w, t.Render()) +} + +func structPrinter(w table.Writer, prefix string, item any) { +Start: + rv := reflect.ValueOf(item) + if rv.IsZero() { + return + } + + for rv.Type().Kind() == reflect.Pointer { + rv = rv.Elem() + } + + switch rv.Type().Kind() { + case reflect.Invalid, + reflect.Uintptr, + reflect.Chan, + reflect.Func, + reflect.UnsafePointer: + case reflect.Bool, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64, + reflect.Complex64, + reflect.Complex128, + reflect.Interface: + w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()}) + case reflect.String: + val := rv.String() + if len(val) <= 160 { + w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val}) + return + } + + w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]}) + case reflect.Array, reflect.Slice: + for i := 0; i < rv.Len(); i++ { + p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".") + structPrinter(w, p, rv.Index(i).Interface()) + } + case reflect.Map: + for _, k := range rv.MapKeys() { + structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface()) + } + case reflect.Pointer: + goto Start + case reflect.Struct: + for i := 0; i < rv.NumField(); i++ { + p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name) + field := rv.Field(i) + + //log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i)) + + if !field.CanInterface() { + return + } + + structPrinter(w, p, field.Interface()) + } + } +} + +func TableMapPrinter(data []byte) { + m := make(map[string]any) + if err := json.Unmarshal(data, &m); err != nil { + log.Warn(err.Error()) + return + } + + t := table.NewWriter() + addRow(t, "", m) + fmt.Println(t.Render()) +} + +func addRow(w table.Writer, prefix string, m any) { + rv := reflect.ValueOf(m) + switch rv.Type().Kind() { + case reflect.Map: + for _, k := range rv.MapKeys() { + key := k.String() + if prefix != "" { + key = strings.Join([]string{prefix, k.String()}, ".") + } + addRow(w, key, rv.MapIndex(k).Interface()) + } + case reflect.Slice, reflect.Array: + for i := 0; i < rv.Len(); i++ { + addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface()) + } + default: + w.AppendRow(table.Row{prefix, m}) + } +} diff --git a/internal/pkg/tool/tools.go b/internal/pkg/tool/tools.go new file mode 100644 index 0000000..ff99948 --- /dev/null +++ b/internal/pkg/tool/tools.go @@ -0,0 +1,73 @@ +package tool + +import ( + "fmt" + "math" +) + +func Min[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T { + if a <= b { + return a + } + + return b +} + +func Mins[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T { + var val T + + if len(vals) == 0 { + return val + } + + val = vals[0] + + for _, item := range vals[1:] { + if item < val { + val = item + } + } + + return val +} + +func Max[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T { + if a >= b { + return a + } + + return b +} + +func Maxs[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T { + var val T + + if len(vals) == 0 { + return val + } + + for _, item := range vals { + if item > val { + val = item + } + } + + return val +} + +func Sum[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T { + var sum T = 0 + for i := range vals { + sum += vals[i] + } + return sum +} + +func Percent(val, minVal, maxVal, minPercent, maxPercent float64) string { + return fmt.Sprintf( + "%d%%", + int(math.Round( + ((val-minVal)/(maxVal-minVal)*(maxPercent-minPercent)+minPercent)*100, + )), + ) +} diff --git a/internal/pkg/tool/tools_test.go b/internal/pkg/tool/tools_test.go new file mode 100644 index 0000000..3622e76 --- /dev/null +++ b/internal/pkg/tool/tools_test.go @@ -0,0 +1,70 @@ +package tool + +import "testing" + +func TestPercent(t *testing.T) { + type args struct { + val float64 + minVal float64 + maxVal float64 + minPercent float64 + maxPercent float64 + } + tests := []struct { + name string + args args + want string + }{ + { + name: "case 1", + args: args{ + val: 0.5, + minVal: 0, + maxVal: 1, + minPercent: 0, + maxPercent: 1, + }, + want: "50%", + }, + { + name: "case 2", + args: args{ + val: 0.3, + minVal: 0.1, + maxVal: 0.6, + minPercent: 0, + maxPercent: 1, + }, + want: "40%", + }, + { + name: "case 3", + args: args{ + val: 700, + minVal: 700, + maxVal: 766, + minPercent: 0.1, + maxPercent: 0.7, + }, + want: "10%", + }, + { + name: "case 4", + args: args{ + val: 766, + minVal: 700, + maxVal: 766, + minPercent: 0.1, + maxPercent: 0.7, + }, + want: "70%", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Percent(tt.args.val, tt.args.minVal, tt.args.maxVal, tt.args.minPercent, tt.args.maxPercent); got != tt.want { + t.Errorf("Percent() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/main.go b/main.go index 73ec70e..47c8044 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ func init() { flag.BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode") flag.StringVar(&opt.Cfg.Address, "address", "0.0.0.0:9119", "") flag.StringVar(&opt.Cfg.DataPath, "data", "/data", "") - flag.BoolVar(&opt.Cfg.Auth, "auth", false, "upload need login") + flag.StringVar(&opt.Cfg.Auth, "auth", "", "auth required(admin, password)") flag.Parse() if opt.Cfg.Debug { @@ -28,6 +28,8 @@ func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() + opt.Init(ctx) + controller.UserManager.Start(ctx) controller.MetaManager.Start(ctx) api.Start(ctx)