Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7dd2bbfa8 | ||
|
|
b02b1ea90b | ||
|
|
74c886f8c2 | ||
|
|
7452f8015f | ||
|
|
392569bab4 | ||
|
|
cfd7923530 | ||
|
|
cec9a5de46 | ||
|
|
68360e2508 | ||
|
|
798219bb19 | ||
|
|
667dfa240d | ||
|
|
a45b6ca368 | ||
|
|
b333669521 | ||
|
|
cd89b66d7f | ||
|
|
5cb9c405b6 | ||
|
|
d446e600bb | ||
|
|
64d34370da | ||
|
|
5e829391f4 | ||
|
|
c926de6e85 | ||
|
|
8653af95d9 | ||
|
|
f3c11e2ae3 | ||
|
|
c1841bd334 | ||
|
|
a427d9270a | ||
|
|
85a7ff173b | ||
|
|
dbbb315d1f | ||
|
|
5837f550ed | ||
|
|
3d361e93db | ||
|
|
acd2233079 | ||
|
|
20c281156b | ||
|
|
8977d5fdec | ||
|
|
d2173a4302 | ||
|
|
3ca3497078 | ||
|
|
a3ff339509 | ||
|
|
a35f8f1b1b | ||
|
|
cb43a27848 | ||
|
|
cef6e2b17f | ||
|
|
4ebbfda9fd | ||
|
|
a97e160060 | ||
|
|
e62cd9aa4a | ||
|
|
22d0603b53 | ||
|
|
1f96b6cfba | ||
|
|
9e02e5c889 | ||
|
|
7ddcb4681b | ||
|
|
edcbc5a0ba | ||
|
|
8aae4c757e | ||
|
|
42b62b589f | ||
|
|
c5edf91a41 | ||
|
|
b4d32da7bb | ||
|
|
8db71d3ee7 | ||
|
|
15235dd4e1 | ||
|
|
b3b1d9ea4f | ||
|
|
7f2b17e14b | ||
|
|
26a3c17aa5 | ||
|
|
f1cc1cf099 | ||
|
|
34b69ca7e4 | ||
|
|
1b66ae25fa | ||
|
|
cc706d7f9f | ||
|
|
3051213b12 | ||
|
|
c5a4ce6aac | ||
|
|
b4dc47f17e | ||
|
|
bc3ad79b70 | ||
|
|
f8344f79a4 | ||
|
|
2d5ebb64d3 | ||
|
|
6d14ddee7f | ||
|
|
f037aa023a | ||
|
|
56c91c28b3 | ||
|
|
ef1b2ef364 | ||
|
|
cdeac8fa50 | ||
|
|
7049271d16 | ||
|
|
cc716e3074 | ||
|
|
effc71cec0 | ||
|
|
9a5c6c1f64 | ||
|
|
13eb812c51 | ||
|
|
c1ecf275d4 | ||
|
|
36a36ba469 | ||
|
|
217e9274c6 | ||
|
|
b2324e60a8 | ||
|
|
8c6c1fa8c3 | ||
|
|
1f239cc0c2 | ||
|
|
3f5b8ac05d | ||
|
|
178b35b48d | ||
|
|
8e9c361536 | ||
|
|
b14409d100 | ||
|
|
ab5e2ad9b4 | ||
|
|
1c7a3f36b7 | ||
|
|
98b9bf6b81 | ||
|
|
196788ea2b | ||
|
|
e013ce5889 | ||
|
|
58060ab20a |
@ -1,3 +1,3 @@
|
||||
MYURLS_PORT=8002
|
||||
MYURLS_PORT=8080
|
||||
MYURLS_DOMAIN=example.com
|
||||
MYURLS_TTL=90
|
||||
MYURLS_PROTO=https
|
||||
|
||||
31
.github/workflows/docker_build_push.yml
vendored
Normal file
31
.github/workflows/docker_build_push.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: Build and Push Multi-Arch Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/386,linux/amd64,linux/arm64,linux/ppc64le,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_PROJECT }}
|
||||
102
.github/workflows/go.yml
vendored
102
.github/workflows/go.yml
vendored
@ -1,30 +1,92 @@
|
||||
name: Go
|
||||
name: Github CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
linux_amd64_build:
|
||||
name: Linux amd64 build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.22.1'
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@master
|
||||
- name: Install dependencies
|
||||
run: sudo apt install gcc-aarch64-linux-gnu
|
||||
- name: Build
|
||||
run: /bin/sh -c "chmod +x scripts/build_linux_amd64.sh && bash scripts/build_linux_amd64.sh"
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: myurls-linux-amd64
|
||||
path: build/myurls-linux-amd64.tar.gz
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
linux_arm64_build:
|
||||
name: Linux arm64 build
|
||||
runs-on: ubuntu-latest
|
||||
# runs-on: self-hosted
|
||||
steps:
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.22.1'
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@master
|
||||
- name: Install dependencies
|
||||
run: sudo apt install gcc-aarch64-linux-gnu
|
||||
- name: Build
|
||||
run: /bin/sh -c "chmod +x scripts/build_linux_arm64.sh && bash scripts/build_linux_arm64.sh"
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: myurls-linux-arm64
|
||||
path: build/myurls-linux-arm64.tar.gz
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
# darwin_amd64_build:
|
||||
# name: Darwin amd64 build
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Set up Go 1.22
|
||||
# uses: actions/setup-go@v5
|
||||
# with:
|
||||
# go-version: '^1.22.1'
|
||||
# id: go
|
||||
# - name: Check out code into the Go module directory
|
||||
# uses: actions/checkout@master
|
||||
# - name: Install dependencies
|
||||
# run: sudo apt install gcc-aarch64-linux-gnu
|
||||
# - name: Build
|
||||
# run: /bin/sh -c "chmod +x scripts/build_darwin_amd64.sh && bash scripts/build_darwin_amd64.sh"
|
||||
# - name: Upload
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: myurls-darwin-amd64.tar.gz
|
||||
# path: build/myurls-darwin-amd64.tar.gz
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
|
||||
- name: Build
|
||||
run: go build -o myurls main.go
|
||||
windows_x64_build:
|
||||
name: Windows x64 build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.22.1'
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@master
|
||||
- name: Install dependencies
|
||||
run: sudo apt install gcc-aarch64-linux-gnu
|
||||
- name: Build
|
||||
run: /bin/sh -c "chmod +x scripts/build_windows_x64.sh && bash scripts/build_windows_x64.sh"
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: myurls-windows-x64.tar.gz
|
||||
path: build/myurls-windows-x64.tar.gz
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,4 +1,9 @@
|
||||
.idea
|
||||
build/
|
||||
logs/
|
||||
data/
|
||||
*.log
|
||||
|
||||
.env
|
||||
dist/
|
||||
MyUrls
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@ -1,17 +1,14 @@
|
||||
FROM golang:1.13-alpine AS dependencies
|
||||
FROM golang:1.22-alpine AS build
|
||||
WORKDIR /app
|
||||
RUN go env -w GO111MODULE="on" && go env -w GOPROXY="https://goproxy.cn,direct"
|
||||
COPY . .
|
||||
|
||||
COPY go.sum go.mod ./
|
||||
RUN go mod tidy
|
||||
|
||||
FROM dependencies as build
|
||||
WORKDIR /app
|
||||
COPY main.go ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myurls main.go
|
||||
# RUN go env -w GOPROXY=https://mirrors.cloud.tencent.com/go/,direct
|
||||
RUN go mod download
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myurls
|
||||
|
||||
FROM scratch
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/myurls ./
|
||||
EXPOSE 8002
|
||||
COPY public/* ./public/
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/myurls"]
|
||||
|
||||
37
Makefile
37
Makefile
@ -1,35 +1,44 @@
|
||||
BINARY_DEFAULT="build/myurls.service"
|
||||
BINARY_LINUX="build/linux-amd64-myurls.service"
|
||||
BINARY_DARWIN="build/darwin-amd64-myurls.service"
|
||||
BINARY_WINDOWS="build/windows-amd64-myurls.service"
|
||||
BINARY_DEFAULT="build/myurls"
|
||||
BINARY_LINUX="build/myurls-linux-amd64"
|
||||
BINARY_DARWIN="build/myurls-darwin-amd64"
|
||||
BINARY_DARWIN_ARM64="build/myurls-darwin-arm64"
|
||||
BINARY_WINDOWS="build/myurls-windows-x64"
|
||||
BINARY_ARM64="build/myurls-linux-arm64"
|
||||
|
||||
GOFILES="main.go"
|
||||
VERSION=1.0.0
|
||||
BUILD=`date +%FT%T%z`
|
||||
|
||||
default:
|
||||
@echo ${BINARY_DEFAULT}
|
||||
@go build -o ${BINARY_DEFAULT} ${GOFILES}
|
||||
@CGO_ENABLED=0 go build -ldflags="-s -w" -o ${BINARY_DEFAULT}
|
||||
|
||||
all:
|
||||
@echo ${BINARY_LINUX}
|
||||
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BINARY_LINUX} ${GOFILES}
|
||||
@echo ${BINARY_DARWIN}
|
||||
@CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ${BINARY_DARWIN} ${GOFILES}
|
||||
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_LINUX}
|
||||
# @echo ${BINARY_DARWIN}
|
||||
# @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_DARWIN}
|
||||
# @echo ${BINARY_DARWIN_ARM64}
|
||||
# @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_DARWIN_ARM64}
|
||||
@echo ${BINARY_WINDOWS}
|
||||
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ${BINARY_WINDOWS} ${GOFILES}
|
||||
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_WINDOWS}
|
||||
@echo ${BINARY_ARM64}
|
||||
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_ARM64}
|
||||
|
||||
linux:
|
||||
@echo ${BINARY_LINUX}
|
||||
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BINARY_LINUX} ${GOFILES}
|
||||
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_LINUX}
|
||||
|
||||
darwin:
|
||||
@echo ${BINARY_DARWIN}
|
||||
@CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ${BINARY_DARWIN} ${GOFILES}
|
||||
@CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_DARWIN}
|
||||
|
||||
windows:
|
||||
@echo ${BINARY_WINDOWS}
|
||||
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ${BINARY_WINDOWS} ${GOFILES}
|
||||
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_WINDOWS}
|
||||
|
||||
aarch64:
|
||||
@echo ${BINARY_ARM64}
|
||||
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_ARM64}
|
||||
|
||||
install:
|
||||
@go mod tidy
|
||||
@ -41,4 +50,6 @@ clean:
|
||||
@if [ -f ${BINARY_DEFAULT} ] ; then rm ${BINARY_DEFAULT} ; fi
|
||||
@if [ -f ${BINARY_LINUX} ] ; then rm ${BINARY_LINUX} ; fi
|
||||
@if [ -f ${BINARY_DARWIN} ] ; then rm ${BINARY_DARWIN} ; fi
|
||||
@if [ -f ${BINARY_DARWIN_ARM64} ] ; then rm ${BINARY_DARWIN_ARM64} ; fi
|
||||
@if [ -f ${BINARY_WINDOWS} ] ; then rm ${BINARY_WINDOWS} ; fi
|
||||
@if [ -f ${BINARY_ARM64} ] ; then rm ${BINARY_ARM64} ; fi
|
||||
|
||||
71
README.md
71
README.md
@ -1,18 +1,17 @@
|
||||
# bitly
|
||||
# MyUrls
|
||||
|
||||
基于 golang1.13 与 Redis 实现的本地短链接服务,用于缩短请求链接与短链接还原。
|
||||
基于 Go 1.22 与 Redis 实现的本地短链接服务,用于缩短 URL 与短链接还原。
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Update](#update)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Docker](#Docker)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [API](#api)
|
||||
- [Maintainers](#maintainers)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Docker](#docker)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [日志清理](#日志清理)
|
||||
- [Maintainers](#maintainers)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
# Dependencies
|
||||
|
||||
@ -29,7 +28,11 @@ sudo apt-get install redis-server -y
|
||||
|
||||
## Docker
|
||||
|
||||
现在你可以无需安装其他服务,使用 [docker-compose](https://docs.docker.com/compose/install/) 部署本项目。注:请自行修改 .env 中参数。
|
||||
现在你可以无需安装其他服务,使用 docker 或 [docker-compose](https://docs.docker.com/compose/install/) 部署本项目。注:请自行修改 .env 中参数。
|
||||
|
||||
```
|
||||
docker run -d --restart always --name myurls careywong/myurls:latest -domain example.com -port 8002 -conn 127.0.0.1:6379 -password ''
|
||||
```
|
||||
|
||||
```shell script
|
||||
git clone https://github.com/CareyWang/MyUrls.git MyUrls
|
||||
@ -56,32 +59,50 @@ make
|
||||
|
||||
## Usage
|
||||
|
||||
前往 [Release](https://github.com/CareyWang/MyUrls/releases) 下载对应平台可执行文件。
|
||||
前往 [Actions](https://github.com/CareyWang/MyUrls/actions/workflows/go.yml) 下载对应平台可执行文件。
|
||||
|
||||
```shell script
|
||||
./build/linux-amd64-myurls.service -h
|
||||
|
||||
Usage of ./build/linux-amd64-myurls.service:
|
||||
Usage of ./MyUrls:
|
||||
-conn string
|
||||
Redis连接,格式: host:port (default "127.0.0.1:6379")
|
||||
address of the redis server (default "localhost:6379")
|
||||
-domain string
|
||||
短链接域名,必填项
|
||||
-port int
|
||||
服务端口 (default 8002)
|
||||
-ttl int
|
||||
短链接有效期,单位(天),默认90天。 (default 90)
|
||||
domain of the server (default "localhost:8080")
|
||||
-h display help
|
||||
-password string
|
||||
password of the redis server
|
||||
-port string
|
||||
port to run the server on (default "8080")
|
||||
-proto string
|
||||
protocol of the server (default "https")
|
||||
```
|
||||
|
||||
建议配合 [pm2](https://pm2.keymetrics.io/) 开启守护进程。
|
||||
|
||||
```shell script
|
||||
pm2 start myurls.service --watch --name myurls -- -domain example.com
|
||||
pm2 start myurls --name myurls -- -domain example.com
|
||||
```
|
||||
|
||||
## API
|
||||
### 日志清理
|
||||
|
||||
[参考文档](https://myurls.mydoc.li)
|
||||
假定工作目录为 `/app`,可基于 logrotate 配置应用日志的自动轮转与清理。可参考示例配置,每天轮转一次日志文件,保留最近7天
|
||||
|
||||
```shell
|
||||
tee > /etc/logrotate.d/myurls <<EOF
|
||||
/app/logs/access.log {
|
||||
daily
|
||||
rotate 7
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
create 640 root adm
|
||||
}
|
||||
EOF
|
||||
|
||||
# 测试是否正常工作,不会实际执行切割
|
||||
logrotate -d /etc/logrotate.d/myurls
|
||||
```
|
||||
|
||||
## Maintainers
|
||||
|
||||
@ -95,4 +116,4 @@ Small note: If editing the README, please conform to the [standard-readme](https
|
||||
|
||||
## License
|
||||
|
||||
MIT © 2020 CareyWang
|
||||
MIT © 2024 CareyWang
|
||||
|
||||
13
const.go
Normal file
13
const.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"Code"`
|
||||
Msg string `json:"Message"`
|
||||
Data any `json:"Data"`
|
||||
}
|
||||
|
||||
// Response codes
|
||||
const ResponseCodeSuccess = 0 // Success
|
||||
const ResponseCodeSuccessLegacy = 1 // Success
|
||||
const ResponseCodeParamsCheckError = 1001 // Parameter check error
|
||||
const ResponseCodeServerError = 1002 // Server error
|
||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
@ -6,14 +6,18 @@ services:
|
||||
restart: always
|
||||
env_file: .env
|
||||
ports:
|
||||
- "${MYURLS_PORT}:8002"
|
||||
- "${MYURLS_PORT}:${MYURLS_PORT}"
|
||||
volumes:
|
||||
- ./data/myurls/logs:/app/logs
|
||||
depends_on:
|
||||
- redis
|
||||
entrypoint: ["/app/myurls", "-domain", "${MYURLS_DOMAIN}", "-conn", redis:6379, "-ttl", "${MYURLS_TTL}"]
|
||||
- myurls-redis
|
||||
entrypoint: ["/app/myurls", "-domain", "${MYURLS_DOMAIN}", "-conn", myurls-redis:6379]
|
||||
|
||||
redis:
|
||||
image: "redis:5"
|
||||
container_name: redis
|
||||
myurls-redis:
|
||||
image: "redis:7"
|
||||
container_name: myurls-redis
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
expose:
|
||||
- "6379"
|
||||
|
||||
48
go.mod
48
go.mod
@ -1,12 +1,46 @@
|
||||
module github.com/CareyWang/MyUrls
|
||||
|
||||
go 1.13
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.5.0
|
||||
github.com/gomodule/redigo v2.0.0+incompatible
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.19.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
151
go.sum
151
go.sum
@ -1,57 +1,124 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
|
||||
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
|
||||
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
112
handlers.go
Normal file
112
handlers.go
Normal file
@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const defaultTTL = time.Hour * 24 * 365 // 默认过期时间,1年
|
||||
const defaultRenewTime = time.Hour * 48 // 默认续命时间,2天
|
||||
const defaultShortKeyLength = 7 // 默认短链接长度,7位
|
||||
|
||||
// ShortToLongHandler gets the long URL from a short URL
|
||||
func ShortToLongHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
resp := Response{}
|
||||
shortKey := c.Param("shortKey")
|
||||
longURL := ShortToLong(c, shortKey)
|
||||
if longURL == "" {
|
||||
resp.Code = ResponseCodeServerError
|
||||
resp.Msg = "failed to get long URL, please check the short URL if exists or expired"
|
||||
|
||||
c.JSON(404, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// todo
|
||||
// check whether need renew expiration time
|
||||
// only renew once per day
|
||||
// if err := Renew(c, shortKey, defaultRenewTime); err != nil {
|
||||
// logger.Warn("failed to renew short URL: ", err.Error())
|
||||
// }
|
||||
|
||||
c.Redirect(301, longURL)
|
||||
}
|
||||
}
|
||||
|
||||
type LongToShortParams struct {
|
||||
LongUrl string `form:"longUrl" json:"longUrl" binding:"required"`
|
||||
ShortKey string `form:"shortKey" json:"shortKey" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// LongToShortHandler creates a short URL from a long URL
|
||||
func LongToShortHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
resp := Response{}
|
||||
|
||||
// check parameters
|
||||
req := LongToShortParams{}
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
resp.Code = ResponseCodeParamsCheckError
|
||||
resp.Msg = "invalid parameters"
|
||||
logger.Warn("invalid parameters: ", err.Error())
|
||||
|
||||
c.JSON(200, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 兼容以前的实现,这里如果是 base64 编码的字符串,进行解码
|
||||
_longUrl, err := base64.StdEncoding.DecodeString(req.LongUrl)
|
||||
if err == nil {
|
||||
req.LongUrl = string(_longUrl)
|
||||
}
|
||||
|
||||
// generate short key
|
||||
if req.ShortKey == "" {
|
||||
req.ShortKey = GenerateRandomString(defaultShortKeyLength)
|
||||
}
|
||||
// check whether short key exists
|
||||
exists, err := CheckRedisKeyIfExist(c, req.ShortKey)
|
||||
if err != nil {
|
||||
resp.Code = ResponseCodeServerError
|
||||
resp.Msg = "failed to check short key"
|
||||
logger.Error("failed to check short key: ", err.Error())
|
||||
|
||||
c.JSON(200, resp)
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
resp.Code = ResponseCodeParamsCheckError
|
||||
resp.Msg = "short key already exists, please use another one or leave it empty to generate automatically"
|
||||
|
||||
logger.Info("short key already exists: ", req.ShortKey)
|
||||
c.JSON(200, resp)
|
||||
return
|
||||
}
|
||||
|
||||
options := &LongToShortOptions{
|
||||
ShortKey: req.ShortKey,
|
||||
URL: req.LongUrl,
|
||||
expiration: defaultTTL,
|
||||
}
|
||||
if err := LongToShort(c, options); err != nil {
|
||||
resp.Code = ResponseCodeServerError
|
||||
resp.Msg = "failed to create short URL"
|
||||
logger.Warn("failed to create short URL: ", err.Error())
|
||||
|
||||
c.JSON(200, resp)
|
||||
return
|
||||
}
|
||||
|
||||
shortURL := proto + "://" + domain + "/" + options.ShortKey
|
||||
|
||||
// 兼容以前的返回结构体
|
||||
respDataLegacy := gin.H{
|
||||
"Code": ResponseCodeSuccessLegacy,
|
||||
"ShortUrl": shortURL,
|
||||
}
|
||||
c.JSON(200, respDataLegacy)
|
||||
}
|
||||
}
|
||||
112
logger.go
Normal file
112
logger.go
Normal file
@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var logger *zap.SugaredLogger
|
||||
|
||||
const (
|
||||
logFileMaxSize = 50 // 日志文件最大大小(MB)
|
||||
logFileMaxBackups = 10 // 最多保留的备份文件数量
|
||||
logFileMaxAge = 7 // 日志文件最长保留天数
|
||||
logFileCompress = false // 是否压缩备份文件
|
||||
)
|
||||
|
||||
func InitLogger() {
|
||||
// 创建 logs 目录
|
||||
createLogPath()
|
||||
|
||||
// 初始化 zap logger
|
||||
initZapLogger()
|
||||
}
|
||||
|
||||
// createLogPath 创建 logs 目录
|
||||
func createLogPath() error {
|
||||
if dir, err := os.Getwd(); err == nil {
|
||||
logFilePath := dir + "/logs/"
|
||||
if err := os.MkdirAll(logFilePath, 0777); err != nil {
|
||||
panic("create log path failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLogPath 获取 logs 目录
|
||||
func getLogPath() string {
|
||||
if dir, err := os.Getwd(); err == nil {
|
||||
return dir + "/logs/"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 定义 zap logger
|
||||
func initZapLogger() {
|
||||
encoder := getEncoder()
|
||||
core := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel)
|
||||
|
||||
logger = zap.New(core).Sugar()
|
||||
}
|
||||
|
||||
// getEncoder 获取 zap encoder
|
||||
func getEncoder() zapcore.Encoder {
|
||||
return zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
||||
}
|
||||
|
||||
// initGinLogger 初始化 gin logger
|
||||
func initGinLogger() *zap.Logger {
|
||||
logPath := getLogPath()
|
||||
logFileName := "access.log"
|
||||
|
||||
// 日志文件
|
||||
logFile := path.Join(logPath, logFileName)
|
||||
|
||||
lumberJackLogger := &lumberjack.Logger{
|
||||
Filename: logFile,
|
||||
MaxSize: logFileMaxSize, // 日志文件最大大小(MB)
|
||||
MaxBackups: logFileMaxBackups, // 最多保留的备份文件数量
|
||||
MaxAge: logFileMaxAge, // 日志文件最长保留天数
|
||||
Compress: logFileCompress, // 是否压缩备份文件
|
||||
}
|
||||
writeSyncer := zapcore.AddSync(lumberJackLogger)
|
||||
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
encoderConfig.EncodeCaller = nil
|
||||
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
|
||||
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
|
||||
core := zapcore.NewCore(encoder, writeSyncer, zapcore.InfoLevel)
|
||||
|
||||
return zap.New(core, zap.AddCaller())
|
||||
}
|
||||
|
||||
// initServiceLogger 初始化服务日志
|
||||
func initServiceLogger() gin.HandlerFunc {
|
||||
_logger := initGinLogger()
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
|
||||
c.Next()
|
||||
|
||||
_logger.Info(
|
||||
"request",
|
||||
zap.String("time", start.Format(time.RFC3339)),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
zap.String("user-agent", c.Request.UserAgent()),
|
||||
zap.String("path", path),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.Duration("latency", time.Since(start)),
|
||||
)
|
||||
}
|
||||
}
|
||||
52
logic.go
Normal file
52
logic.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ShortToLong gets the long URL from a short URL
|
||||
func ShortToLong(ctx context.Context, shortKey string) string {
|
||||
rc := GetRedisClient()
|
||||
return rc.Get(ctx, shortKey).Val()
|
||||
}
|
||||
|
||||
// LongToShortOptions are the options for the LongToShort function
|
||||
type LongToShortOptions struct {
|
||||
ShortKey string
|
||||
URL string
|
||||
expiration time.Duration
|
||||
}
|
||||
|
||||
// LongToShort creates a short URL from a long URL
|
||||
func LongToShort(ctx context.Context, options *LongToShortOptions) error {
|
||||
rc := GetRedisClient()
|
||||
return rc.SetEx(ctx, options.ShortKey, options.URL, options.expiration).Err()
|
||||
}
|
||||
|
||||
// Renew updates the expiration time of a short URL
|
||||
func Renew(ctx context.Context, shortKey string, expiration time.Duration) error {
|
||||
rc := GetRedisClient()
|
||||
|
||||
rs := rc.TTL(ctx, shortKey)
|
||||
if rs.Err() != nil {
|
||||
return rs.Err()
|
||||
}
|
||||
|
||||
ttl := rs.Val()
|
||||
if ttl < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rc.Expire(ctx, shortKey, ttl+expiration).Err()
|
||||
}
|
||||
|
||||
func CheckRedisKeyIfExist(ctx context.Context, key string) (bool, error) {
|
||||
rc := GetRedisClient()
|
||||
rs := rc.Exists(ctx, key)
|
||||
if rs.Err() != nil {
|
||||
return false, rs.Err()
|
||||
}
|
||||
|
||||
return rs.Val() > 0, nil
|
||||
}
|
||||
31
logic_test.go
Normal file
31
logic_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
// FILEPATH: /root/CareyWang/MyUrls/logic_test.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLongToShortAndShortToLong(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
initRedisClient(mockRedisOptions)
|
||||
|
||||
shortKey := "testKey"
|
||||
longURL := "https://example.com"
|
||||
|
||||
err := LongToShort(ctx, &LongToShortOptions{
|
||||
ShortKey: shortKey,
|
||||
URL: longURL,
|
||||
expiration: 60 * time.Second,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
// delete test data from redis
|
||||
defer GetRedisClient().Del(ctx, shortKey)
|
||||
|
||||
resultLongURL := ShortToLong(ctx, shortKey)
|
||||
assert.Equal(t, longURL, resultLongURL)
|
||||
}
|
||||
258
main.go
258
main.go
@ -1,186 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int
|
||||
Message string
|
||||
LongUrl string
|
||||
ShortUrl string
|
||||
var helpFlag bool
|
||||
|
||||
var (
|
||||
port = "8080"
|
||||
domain = "localhost:8080"
|
||||
proto = "https"
|
||||
redisAddr = "localhost:6379"
|
||||
redisPassword = ""
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&helpFlag, "h", false, "display help")
|
||||
|
||||
flag.StringVar(&port, "port", port, "port to run the server on")
|
||||
flag.StringVar(&domain, "domain", domain, "domain of the server")
|
||||
flag.StringVar(&proto, "proto", proto, "protocol of the server")
|
||||
flag.StringVar(&redisAddr, "conn", redisAddr, "address of the redis server")
|
||||
flag.StringVar(&redisPassword, "password", redisPassword, "password of the redis server")
|
||||
}
|
||||
|
||||
type redisPoolConf struct {
|
||||
maxIdle int
|
||||
maxActive int
|
||||
maxIdleTimeout int
|
||||
host string
|
||||
password string
|
||||
db int
|
||||
handleTimeout int
|
||||
}
|
||||
|
||||
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
const defaultPort int = 8002
|
||||
const defaultExpire = 90
|
||||
const defaultRedisConfig = "127.0.0.1:6379"
|
||||
|
||||
var redisPool *redis.Pool
|
||||
var redisPoolConfig *redisPoolConf
|
||||
var redisClient redis.Conn
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if helpFlag {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 从环境变量中读取配置,且环境变量优先级高于命令行参数
|
||||
parseEnvirons()
|
||||
|
||||
InitLogger()
|
||||
|
||||
// init and check redis
|
||||
initRedisClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: redisPassword,
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
rc := GetRedisClient()
|
||||
rs := rc.Ping(ctx)
|
||||
if rs.Err() != nil {
|
||||
logger.Fatalln("redis ping failed: ", rs.Err())
|
||||
}
|
||||
logger.Info("redis ping success")
|
||||
|
||||
// GC optimize
|
||||
ballast := make([]byte, 1<<30) // 预分配 1G 内存,不会实际占用物理内存,不可读写该变量
|
||||
defer func() {
|
||||
logger.Info("ballast len %v", len(ballast))
|
||||
}()
|
||||
|
||||
// start http server
|
||||
run()
|
||||
}
|
||||
|
||||
func parseEnvirons() {
|
||||
if p := os.Getenv("MYURLS_PORT"); p != "" {
|
||||
port = p
|
||||
}
|
||||
if d := os.Getenv("MYURLS_DOMAIN"); d != "" {
|
||||
domain = d
|
||||
}
|
||||
if p := os.Getenv("MYURLS_PROTO"); p != "" {
|
||||
proto = p
|
||||
}
|
||||
if c := os.Getenv("MYURLS_REDIS_CONN"); c != "" {
|
||||
redisAddr = c
|
||||
}
|
||||
if p := os.Getenv("MYURLS_REDIS_PASSWORD"); p != "" {
|
||||
redisPassword = p
|
||||
}
|
||||
}
|
||||
|
||||
func run() {
|
||||
// init and run server
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
|
||||
port := flag.Int("port", defaultPort, "服务端口")
|
||||
domain := flag.String("domain", "", "短链接域名,必填项")
|
||||
ttl := flag.Int("ttl", defaultExpire, "短链接有效期,单位(天),默认90天。")
|
||||
conn := flag.String("conn", defaultRedisConfig, "Redis连接,格式: host:port")
|
||||
passwd := flag.String("passwd", "", "Redis连接密码")
|
||||
flag.Parse()
|
||||
// logger
|
||||
router.Use(initServiceLogger())
|
||||
|
||||
if *domain == "" {
|
||||
flag.Usage()
|
||||
log.Fatalln("缺少关键参数")
|
||||
}
|
||||
// static files
|
||||
router.LoadHTMLGlob("public/*.html")
|
||||
router.StaticFile("/logo.png", "public/logo.png")
|
||||
|
||||
redisPoolConfig = &redisPoolConf{
|
||||
maxIdle: 1024,
|
||||
maxActive: 1024,
|
||||
maxIdleTimeout: 30,
|
||||
host: *conn,
|
||||
password: *passwd,
|
||||
db: 0,
|
||||
handleTimeout: 30,
|
||||
}
|
||||
initRedisPool()
|
||||
|
||||
router.POST("/short", func(context *gin.Context) {
|
||||
res := &Response{
|
||||
Code: 1,
|
||||
Message: "",
|
||||
LongUrl: "",
|
||||
ShortUrl: "",
|
||||
}
|
||||
|
||||
longUrl := context.PostForm("longUrl")
|
||||
if longUrl == "" {
|
||||
res.Message = "longUrl为空"
|
||||
context.JSON(400, *res)
|
||||
return
|
||||
}
|
||||
|
||||
_longUrl, _ := base64.StdEncoding.DecodeString(longUrl)
|
||||
longUrl = string(_longUrl)
|
||||
|
||||
shortKey := longToShort(longUrl, *ttl*24*3600)
|
||||
if shortKey == "" {
|
||||
res.Code = 0
|
||||
res.Message = "短链接生成失败"
|
||||
context.JSON(500, *res)
|
||||
return
|
||||
}
|
||||
|
||||
res.LongUrl = longUrl
|
||||
res.ShortUrl = "http://" + *domain + "/" + shortKey
|
||||
context.JSON(200, *res)
|
||||
router.GET("/", func(context *gin.Context) {
|
||||
context.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "MyUrls",
|
||||
})
|
||||
})
|
||||
|
||||
router.GET("/:shortKey", func(context *gin.Context) {
|
||||
shortKey := context.Param("shortKey")
|
||||
longUrl := shortToLong(shortKey)
|
||||
router.POST("/short", LongToShortHandler())
|
||||
router.GET("/:shortKey", ShortToLongHandler())
|
||||
|
||||
if longUrl == "" {
|
||||
context.String(http.StatusNotFound, "短链接不存在或已过期")
|
||||
} else {
|
||||
context.Redirect(http.StatusMovedPermanently, longUrl)
|
||||
}
|
||||
})
|
||||
|
||||
router.Run(fmt.Sprintf(":%d", *port))
|
||||
}
|
||||
|
||||
// 短链接转长链接
|
||||
func shortToLong(shortKey string) string {
|
||||
redisClient = redisPool.Get()
|
||||
defer redisClient.Close()
|
||||
|
||||
longUrl, _ := redis.String(redisClient.Do("get", shortKey))
|
||||
return longUrl
|
||||
}
|
||||
|
||||
// 长链接转短链接
|
||||
func longToShort(longUrl string, ttl int) string {
|
||||
redisClient = redisPool.Get()
|
||||
defer redisClient.Close()
|
||||
|
||||
// 是否生成过该长链接对应短链接
|
||||
_existsKey, _ := redis.String(redisClient.Do("get", longUrl))
|
||||
if _existsKey != "" {
|
||||
_, _ = redisClient.Do("expire", _existsKey, ttl)
|
||||
|
||||
return _existsKey
|
||||
}
|
||||
|
||||
// 重试三次
|
||||
var shortKey string
|
||||
for i := 0; i < 3; i++ {
|
||||
shortKey = generate(6)
|
||||
|
||||
_existsLongUrl, _ := redis.String(redisClient.Do("get", longUrl))
|
||||
if _existsLongUrl != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shortKey != "" {
|
||||
_, _ = redisClient.Do("mset", shortKey, longUrl, longUrl, shortKey)
|
||||
|
||||
_, _ = redisClient.Do("expire", shortKey, ttl)
|
||||
_, _ = redisClient.Do("expire", longUrl, ttl)
|
||||
}
|
||||
|
||||
return shortKey
|
||||
}
|
||||
|
||||
// 产生一个63位随机整数,除以字符数取余获取对应字符
|
||||
func generate(bits int) string {
|
||||
b := make([]byte, bits)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func initRedisPool() {
|
||||
// 建立连接池
|
||||
redisPool = &redis.Pool{
|
||||
MaxIdle: redisPoolConfig.maxIdle,
|
||||
MaxActive: redisPoolConfig.maxActive,
|
||||
IdleTimeout: time.Duration(redisPoolConfig.maxIdleTimeout) * time.Second,
|
||||
Wait: true,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
con, err := redis.Dial("tcp", redisPoolConfig.host,
|
||||
redis.DialPassword(redisPoolConfig.password),
|
||||
redis.DialDatabase(redisPoolConfig.db),
|
||||
redis.DialConnectTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second),
|
||||
redis.DialReadTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second),
|
||||
redis.DialWriteTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return con, nil
|
||||
},
|
||||
}
|
||||
logger.Infof("server running on :%s", port)
|
||||
router.Run(fmt.Sprintf(":%s", port))
|
||||
}
|
||||
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
154
public/index.html
Normal file
154
public/index.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MyUrls</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
|
||||
<script src="https://unpkg.com/vue@2.6.11/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/axios@0.19.2/dist/axios.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue-clipboard2@0.3.1/dist/vue-clipboard.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<el-container>
|
||||
<el-header></el-header>
|
||||
<el-main>
|
||||
<div :class="[isPc ? 'body-center body-width-pc' : 'body-center body-width-mb']">
|
||||
<img width="300" src="/logo.png" @click="goToGayHub">
|
||||
<el-input ref="long" v-model="longUrl" size="medium" @keyup.enter.native="enterToDoShort">
|
||||
<el-button slot="append" icon="el-icon-magic-stick" @click="doShort" :loading="loading"></el-button>
|
||||
</el-input>
|
||||
<el-input ref="shortUrl" @dblclick.native="changeDisableStatus" class="copy-content" v-model="shortUrl" size="medium">
|
||||
<el-button slot="append" v-clipboard:copy="shortUrl" v-clipboard:success="onCopy" ref="copy-btn"
|
||||
icon="el-icon-document-copy"></el-button>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const repo = 'https://github.com/CareyWang/MyUrls'
|
||||
|
||||
let app = new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
isPc: true,
|
||||
|
||||
loading: false,
|
||||
longUrl: '',
|
||||
shortUrl: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const os = this.getOS()
|
||||
if (os.isPc !== true) {
|
||||
this.isPc = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.long.focus()
|
||||
},
|
||||
methods: {
|
||||
enterToDoShort(ev) {
|
||||
ev.keyCode === 13 && this.doShort()
|
||||
},
|
||||
doShort() {
|
||||
let re = new RegExp('http(s*)://[^\s]*')
|
||||
if (re.exec(this.longUrl) === null) {
|
||||
this.$message.warning('请输入正确格式的长链接')
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
let data = new FormData();
|
||||
data.append("longUrl", btoa(this.longUrl));
|
||||
data.append("shortKey", this.shortUrl.indexOf('http') < 0 ? this.shortUrl : '');
|
||||
axios.post('/short', data, {
|
||||
header: {
|
||||
"Content-Type": "application/form-data; charset=utf-8"
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (res.data.Code === 1 && res.data.ShortUrl !== "") {
|
||||
this.shortUrl = res.data.ShortUrl;
|
||||
this.$copyText(this.shortUrl)
|
||||
this.$refs.shortUrl.disabled = true
|
||||
this.$message.success("短链接已复制到剪贴板");
|
||||
} else {
|
||||
this.$message.error("短链接获取失败:" + res.data.Message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error("短链接获取失败");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
goToGayHub() {
|
||||
window.open(repo)
|
||||
},
|
||||
getOS() {
|
||||
let ua = navigator.userAgent,
|
||||
isWindowsPhone = /(?:Windows Phone)/.test(ua),
|
||||
isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone,
|
||||
isAndroid = /(?:Android)/.test(ua),
|
||||
isFireFox = /(?:Firefox)/.test(ua),
|
||||
isChrome = /(?:Chrome|CriOS)/.test(ua),
|
||||
isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox && /(?:Tablet)/.test(ua)),
|
||||
isPhone = /(?:iPhone)/.test(ua) && !isTablet,
|
||||
isPc = !isPhone && !isAndroid && !isSymbian;
|
||||
|
||||
return {
|
||||
isTablet: isTablet,
|
||||
isPhone: isPhone,
|
||||
isAndroid: isAndroid,
|
||||
isPc: isPc
|
||||
};
|
||||
},
|
||||
getBodyClass() {
|
||||
return this.isPc ? 'body-center body-width-pc' : 'body-center'
|
||||
},
|
||||
onCopy() {
|
||||
this.$message.success("Copied!");
|
||||
},
|
||||
changeDisableStatus(event) {
|
||||
this.$refs.shortUrl.disabled = false
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.body-center {
|
||||
width: 40%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 30%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-width-pc {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.body-width-mb {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
28
random.go
Normal file
28
random.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// generate is a function that takes an integer bits and returns a string.
|
||||
// The function generates a random string of length equal to bits using the letterBytes slice.
|
||||
// The letterBytes slice contains characters that can be used to generate a random string.
|
||||
// The generation of the random string is based on the current time using the UnixNano() function.
|
||||
func GenerateRandomString(bits int) string {
|
||||
// Create a byte slice b of length bits.
|
||||
b := make([]byte, bits)
|
||||
|
||||
// Create a new random number generator with the current time as the seed.
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Generate a random byte for each element in the byte slice b using the letterBytes slice.
|
||||
for i := range b {
|
||||
b[i] = letterBytes[r.Intn(len(letterBytes))]
|
||||
}
|
||||
|
||||
// Convert the byte slice to a string and return it.
|
||||
return string(b)
|
||||
}
|
||||
16
redis.go
Normal file
16
redis.go
Normal file
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var RedisClient *redis.Client
|
||||
|
||||
// initRedisClient is a function that takes a pointer to a RedisOptions struct and returns a pointer to a Redis client.
|
||||
func initRedisClient(options *redis.Options) {
|
||||
RedisClient = redis.NewClient(options)
|
||||
}
|
||||
|
||||
func GetRedisClient() *redis.Client {
|
||||
return RedisClient
|
||||
}
|
||||
41
redis_test.go
Normal file
41
redis_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var mockRedisOptions = &redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "",
|
||||
DB: 0,
|
||||
}
|
||||
|
||||
func TestGetRedisClient(t *testing.T) {
|
||||
client := GetRedisClient()
|
||||
assert.Nil(t, client)
|
||||
|
||||
initRedisClient(mockRedisOptions)
|
||||
client = GetRedisClient()
|
||||
assert.NotNil(t, client)
|
||||
|
||||
// Test redis exec commands and response
|
||||
ctx := context.Background()
|
||||
rs := client.Ping(ctx)
|
||||
assert.Nil(t, rs.Err())
|
||||
assert.Equal(t, "PONG", rs.Val())
|
||||
|
||||
rsCmd := GetRedisClient().Do(ctx, "dbsize")
|
||||
assert.Nil(t, rsCmd.Err())
|
||||
}
|
||||
|
||||
func BenchmarkGetRedisClient(b *testing.B) {
|
||||
initRedisClient(mockRedisOptions)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetRedisClient().Get(context.Background(), "key")
|
||||
}
|
||||
}
|
||||
13
scripts/build_darwin_amd64.sh
Normal file
13
scripts/build_darwin_amd64.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
make install
|
||||
make all
|
||||
|
||||
mkdir -p myurls
|
||||
cp -r public myurls/
|
||||
|
||||
# darwin-amd64
|
||||
cp build/myurls-darwin-amd64 myurls/
|
||||
tar -czvf myurls-darwin-amd64.tar.gz myurls
|
||||
mv myurls-darwin-amd64.tar.gz build/
|
||||
rm -rf myurls
|
||||
13
scripts/build_linux_amd64.sh
Normal file
13
scripts/build_linux_amd64.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
make install
|
||||
make all
|
||||
|
||||
mkdir -p myurls
|
||||
cp -r public myurls/
|
||||
|
||||
# linux-amd64
|
||||
cp build/myurls-linux-amd64 myurls/
|
||||
tar -czvf myurls-linux-amd64.tar.gz myurls
|
||||
mv myurls-linux-amd64.tar.gz build/
|
||||
rm -rf myurls
|
||||
13
scripts/build_linux_arm64.sh
Normal file
13
scripts/build_linux_arm64.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
make install
|
||||
make all
|
||||
|
||||
mkdir -p myurls
|
||||
cp -r public myurls/
|
||||
|
||||
# linux-arm64
|
||||
cp build/myurls-linux-arm64 myurls/
|
||||
tar -czvf myurls-linux-arm64.tar.gz myurls
|
||||
mv myurls-linux-arm64.tar.gz build/
|
||||
rm -rf myurls
|
||||
13
scripts/build_windows_x64.sh
Normal file
13
scripts/build_windows_x64.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
make install
|
||||
make all
|
||||
|
||||
mkdir -p myurls
|
||||
cp -r public myurls/
|
||||
|
||||
# windows-x64
|
||||
cp build/myurls-windows-x64 myurls/
|
||||
tar -czvf myurls-windows-x64.tar.gz myurls
|
||||
mv myurls-windows-x64.tar.gz build/
|
||||
rm -rf myurls
|
||||
Loading…
x
Reference in New Issue
Block a user