Upload 46 files
Browse files- .github/buildkit.toml +2 -0
- .github/dependabot.yml +13 -0
- .github/workflows/build.yml +43 -0
- .github/workflows/goreleaser.yml +30 -0
- .gitignore +30 -0
- .goreleaser.yaml +32 -0
- .vscode/launch.json +15 -0
- Dockerfile +37 -0
- LICENSE +674 -0
- cache/bigcache.go +99 -0
- cache/cache.go +72 -0
- cache/cachutil.go +63 -0
- database/database.go +77 -0
- database/migrations/20230817172319_init.sql +66 -0
- database/migrations/20230817172325_tables.sql +88 -0
- database/migrations/20230817172329_functions.sql +188 -0
- database/migrations/20230820014560_create_dirs_from_path.sql +48 -0
- docker-compose.yml +33 -0
- go.mod +74 -0
- go.sum +261 -0
- haproxy.cfg +24 -0
- main.go +64 -0
- models/file.model.go +41 -0
- models/upload.model.go +17 -0
- models/user.model.go +15 -0
- public/demo.png +0 -0
- routes/auth.go +50 -0
- routes/file.go +107 -0
- routes/main.go +11 -0
- routes/middleware.go +46 -0
- routes/upload.go +55 -0
- routes/user.go +19 -0
- schemas/common.go +6 -0
- schemas/file.schema.go +78 -0
- schemas/upload.schema.go +21 -0
- services/auth.service.go +347 -0
- services/file.service.go +582 -0
- services/upload.service.go +167 -0
- services/user.service.go +91 -0
- types/main.go +43 -0
- utils/auth/jwe.go +62 -0
- utils/config.go +51 -0
- utils/cron/cron.go +72 -0
- utils/logger.go +19 -0
- utils/main.go +113 -0
- utils/tgclient.go +297 -0
.github/buildkit.toml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[worker.oci]
|
| 2 |
+
max-parallelism = 4
|
.github/dependabot.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# To get started with Dependabot version updates, you'll need to specify which
|
| 2 |
+
# package ecosystems to update and where the package manifests are located.
|
| 3 |
+
# Please see the documentation for all configuration options:
|
| 4 |
+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
| 5 |
+
|
| 6 |
+
version: 2
|
| 7 |
+
updates:
|
| 8 |
+
- package-ecosystem: "gomod" # See documentation for possible values
|
| 9 |
+
directory: "/" # Location of package manifests
|
| 10 |
+
schedule:
|
| 11 |
+
interval: "daily"
|
| 12 |
+
assignees:
|
| 13 |
+
- "divyam234"
|
.github/workflows/build.yml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build App
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
push:
|
| 6 |
+
tags:
|
| 7 |
+
- '*'
|
| 8 |
+
env:
|
| 9 |
+
DOCKER_BUILDKIT: 1
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
build_image:
|
| 13 |
+
name: Build Image
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout
|
| 17 |
+
uses: actions/checkout@v3
|
| 18 |
+
with:
|
| 19 |
+
fetch-depth: 0
|
| 20 |
+
- name: Install buildx
|
| 21 |
+
uses: docker/setup-buildx-action@v2
|
| 22 |
+
with:
|
| 23 |
+
config: .github/buildkit.toml
|
| 24 |
+
|
| 25 |
+
- name: Login to Docker
|
| 26 |
+
uses: docker/login-action@v2
|
| 27 |
+
with:
|
| 28 |
+
registry: ghcr.io
|
| 29 |
+
username: ${{ github.actor }}
|
| 30 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 31 |
+
|
| 32 |
+
- name: Image Name
|
| 33 |
+
id: imagename
|
| 34 |
+
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}/server" >> $GITHUB_OUTPUT
|
| 35 |
+
|
| 36 |
+
- name: Build Image
|
| 37 |
+
uses: docker/build-push-action@v3
|
| 38 |
+
with:
|
| 39 |
+
context: ./
|
| 40 |
+
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
| 41 |
+
pull: true
|
| 42 |
+
push: true
|
| 43 |
+
tags: ${{ steps.imagename.outputs.name }}:latest
|
.github/workflows/goreleaser.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Goreleaser
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
tags:
|
| 6 |
+
- "*"
|
| 7 |
+
|
| 8 |
+
permissions:
|
| 9 |
+
contents: write
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
goreleaser:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
steps:
|
| 15 |
+
- name: Checkout
|
| 16 |
+
uses: actions/checkout@v3
|
| 17 |
+
with:
|
| 18 |
+
fetch-depth: 0
|
| 19 |
+
- name: Set up Go
|
| 20 |
+
uses: actions/setup-go@v4
|
| 21 |
+
with:
|
| 22 |
+
go-version: 1.21
|
| 23 |
+
- name: Run GoReleaser
|
| 24 |
+
uses: goreleaser/goreleaser-action@v4
|
| 25 |
+
with:
|
| 26 |
+
distribution: goreleaser
|
| 27 |
+
version: latest
|
| 28 |
+
args: release --clean
|
| 29 |
+
env:
|
| 30 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# If you prefer the allow list template instead of the deny list, see community template:
|
| 2 |
+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
| 3 |
+
#
|
| 4 |
+
# Binaries for programs and plugins
|
| 5 |
+
*.exe
|
| 6 |
+
*.exe~
|
| 7 |
+
*.dll
|
| 8 |
+
*.so
|
| 9 |
+
*.dylib
|
| 10 |
+
|
| 11 |
+
# Test binary, built with `go test -c`
|
| 12 |
+
*.test
|
| 13 |
+
|
| 14 |
+
# Output of the go coverage tool, specifically when used with LiteIDE
|
| 15 |
+
*.out
|
| 16 |
+
|
| 17 |
+
# Dependency directories (remove the comment below to include it)
|
| 18 |
+
# vendor/
|
| 19 |
+
|
| 20 |
+
# Go workspace file
|
| 21 |
+
go.work
|
| 22 |
+
|
| 23 |
+
sessions
|
| 24 |
+
certs
|
| 25 |
+
sslcerts
|
| 26 |
+
|
| 27 |
+
*.env
|
| 28 |
+
*.env.example
|
| 29 |
+
*.env.local
|
| 30 |
+
*.env.staging
|
.goreleaser.yaml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
project_name: teldrive
|
| 2 |
+
env:
|
| 3 |
+
- GO111MODULE=on
|
| 4 |
+
builds:
|
| 5 |
+
- env:
|
| 6 |
+
- CGO_ENABLED=0
|
| 7 |
+
flags:
|
| 8 |
+
- -trimpath
|
| 9 |
+
ldflags:
|
| 10 |
+
- -s -w -extldflags "-static"
|
| 11 |
+
- -X {{ .ModulePath }}/pkg/consts.Version={{ .Version }}
|
| 12 |
+
- -X {{ .ModulePath }}/pkg/consts.Commit={{ .ShortCommit }}
|
| 13 |
+
- -X {{ .ModulePath }}/pkg/consts.CommitDate={{ .CommitDate }}
|
| 14 |
+
mod_timestamp: "{{ .CommitTimestamp }}"
|
| 15 |
+
goos:
|
| 16 |
+
- linux
|
| 17 |
+
- windows
|
| 18 |
+
- darwin
|
| 19 |
+
goarch:
|
| 20 |
+
- amd64
|
| 21 |
+
- arm
|
| 22 |
+
- arm64
|
| 23 |
+
checksum:
|
| 24 |
+
name_template: "{{ .ProjectName }}_checksums.txt"
|
| 25 |
+
archives:
|
| 26 |
+
- name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
| 27 |
+
format_overrides:
|
| 28 |
+
- goos: windows
|
| 29 |
+
format: zip
|
| 30 |
+
files:
|
| 31 |
+
- README*.md
|
| 32 |
+
- LICENSE
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Use IntelliSense to learn about possible attributes.
|
| 3 |
+
// Hover to view descriptions of existing attributes.
|
| 4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
| 5 |
+
"version": "0.2.0",
|
| 6 |
+
"configurations": [
|
| 7 |
+
{
|
| 8 |
+
"name": "Launch Package",
|
| 9 |
+
"type": "go",
|
| 10 |
+
"request": "launch",
|
| 11 |
+
"mode": "auto",
|
| 12 |
+
"program": "main.go"
|
| 13 |
+
}
|
| 14 |
+
]
|
| 15 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:alpine as builder
|
| 2 |
+
|
| 3 |
+
ARG TARGETPLATFORM
|
| 4 |
+
ARG BUILDPLATFORM
|
| 5 |
+
ARG TARGETOS
|
| 6 |
+
ARG TARGETARCH
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
|
| 10 |
+
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
# use modules
|
| 14 |
+
COPY go.mod .
|
| 15 |
+
|
| 16 |
+
ENV GO111MODULE=on
|
| 17 |
+
RUN go mod download && go mod verify
|
| 18 |
+
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
| 22 |
+
-ldflags='-w -s -extldflags "-static"' -a \
|
| 23 |
+
-o /app/teldrive .
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
FROM --platform=${TARGETPLATFORM:-linux/amd64} scratch
|
| 27 |
+
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
| 31 |
+
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
| 32 |
+
|
| 33 |
+
COPY --from=builder /app/teldrive /app/teldrive
|
| 34 |
+
|
| 35 |
+
EXPOSE 8080
|
| 36 |
+
|
| 37 |
+
ENTRYPOINT ["/app/teldrive"]
|
LICENSE
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GNU GENERAL PUBLIC LICENSE
|
| 2 |
+
Version 3, 29 June 2007
|
| 3 |
+
|
| 4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
| 5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
| 6 |
+
of this license document, but changing it is not allowed.
|
| 7 |
+
|
| 8 |
+
Preamble
|
| 9 |
+
|
| 10 |
+
The GNU General Public License is a free, copyleft license for
|
| 11 |
+
software and other kinds of works.
|
| 12 |
+
|
| 13 |
+
The licenses for most software and other practical works are designed
|
| 14 |
+
to take away your freedom to share and change the works. By contrast,
|
| 15 |
+
the GNU General Public License is intended to guarantee your freedom to
|
| 16 |
+
share and change all versions of a program--to make sure it remains free
|
| 17 |
+
software for all its users. We, the Free Software Foundation, use the
|
| 18 |
+
GNU General Public License for most of our software; it applies also to
|
| 19 |
+
any other work released this way by its authors. You can apply it to
|
| 20 |
+
your programs, too.
|
| 21 |
+
|
| 22 |
+
When we speak of free software, we are referring to freedom, not
|
| 23 |
+
price. Our General Public Licenses are designed to make sure that you
|
| 24 |
+
have the freedom to distribute copies of free software (and charge for
|
| 25 |
+
them if you wish), that you receive source code or can get it if you
|
| 26 |
+
want it, that you can change the software or use pieces of it in new
|
| 27 |
+
free programs, and that you know you can do these things.
|
| 28 |
+
|
| 29 |
+
To protect your rights, we need to prevent others from denying you
|
| 30 |
+
these rights or asking you to surrender the rights. Therefore, you have
|
| 31 |
+
certain responsibilities if you distribute copies of the software, or if
|
| 32 |
+
you modify it: responsibilities to respect the freedom of others.
|
| 33 |
+
|
| 34 |
+
For example, if you distribute copies of such a program, whether
|
| 35 |
+
gratis or for a fee, you must pass on to the recipients the same
|
| 36 |
+
freedoms that you received. You must make sure that they, too, receive
|
| 37 |
+
or can get the source code. And you must show them these terms so they
|
| 38 |
+
know their rights.
|
| 39 |
+
|
| 40 |
+
Developers that use the GNU GPL protect your rights with two steps:
|
| 41 |
+
(1) assert copyright on the software, and (2) offer you this License
|
| 42 |
+
giving you legal permission to copy, distribute and/or modify it.
|
| 43 |
+
|
| 44 |
+
For the developers' and authors' protection, the GPL clearly explains
|
| 45 |
+
that there is no warranty for this free software. For both users' and
|
| 46 |
+
authors' sake, the GPL requires that modified versions be marked as
|
| 47 |
+
changed, so that their problems will not be attributed erroneously to
|
| 48 |
+
authors of previous versions.
|
| 49 |
+
|
| 50 |
+
Some devices are designed to deny users access to install or run
|
| 51 |
+
modified versions of the software inside them, although the manufacturer
|
| 52 |
+
can do so. This is fundamentally incompatible with the aim of
|
| 53 |
+
protecting users' freedom to change the software. The systematic
|
| 54 |
+
pattern of such abuse occurs in the area of products for individuals to
|
| 55 |
+
use, which is precisely where it is most unacceptable. Therefore, we
|
| 56 |
+
have designed this version of the GPL to prohibit the practice for those
|
| 57 |
+
products. If such problems arise substantially in other domains, we
|
| 58 |
+
stand ready to extend this provision to those domains in future versions
|
| 59 |
+
of the GPL, as needed to protect the freedom of users.
|
| 60 |
+
|
| 61 |
+
Finally, every program is threatened constantly by software patents.
|
| 62 |
+
States should not allow patents to restrict development and use of
|
| 63 |
+
software on general-purpose computers, but in those that do, we wish to
|
| 64 |
+
avoid the special danger that patents applied to a free program could
|
| 65 |
+
make it effectively proprietary. To prevent this, the GPL assures that
|
| 66 |
+
patents cannot be used to render the program non-free.
|
| 67 |
+
|
| 68 |
+
The precise terms and conditions for copying, distribution and
|
| 69 |
+
modification follow.
|
| 70 |
+
|
| 71 |
+
TERMS AND CONDITIONS
|
| 72 |
+
|
| 73 |
+
0. Definitions.
|
| 74 |
+
|
| 75 |
+
"This License" refers to version 3 of the GNU General Public License.
|
| 76 |
+
|
| 77 |
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
| 78 |
+
works, such as semiconductor masks.
|
| 79 |
+
|
| 80 |
+
"The Program" refers to any copyrightable work licensed under this
|
| 81 |
+
License. Each licensee is addressed as "you". "Licensees" and
|
| 82 |
+
"recipients" may be individuals or organizations.
|
| 83 |
+
|
| 84 |
+
To "modify" a work means to copy from or adapt all or part of the work
|
| 85 |
+
in a fashion requiring copyright permission, other than the making of an
|
| 86 |
+
exact copy. The resulting work is called a "modified version" of the
|
| 87 |
+
earlier work or a work "based on" the earlier work.
|
| 88 |
+
|
| 89 |
+
A "covered work" means either the unmodified Program or a work based
|
| 90 |
+
on the Program.
|
| 91 |
+
|
| 92 |
+
To "propagate" a work means to do anything with it that, without
|
| 93 |
+
permission, would make you directly or secondarily liable for
|
| 94 |
+
infringement under applicable copyright law, except executing it on a
|
| 95 |
+
computer or modifying a private copy. Propagation includes copying,
|
| 96 |
+
distribution (with or without modification), making available to the
|
| 97 |
+
public, and in some countries other activities as well.
|
| 98 |
+
|
| 99 |
+
To "convey" a work means any kind of propagation that enables other
|
| 100 |
+
parties to make or receive copies. Mere interaction with a user through
|
| 101 |
+
a computer network, with no transfer of a copy, is not conveying.
|
| 102 |
+
|
| 103 |
+
An interactive user interface displays "Appropriate Legal Notices"
|
| 104 |
+
to the extent that it includes a convenient and prominently visible
|
| 105 |
+
feature that (1) displays an appropriate copyright notice, and (2)
|
| 106 |
+
tells the user that there is no warranty for the work (except to the
|
| 107 |
+
extent that warranties are provided), that licensees may convey the
|
| 108 |
+
work under this License, and how to view a copy of this License. If
|
| 109 |
+
the interface presents a list of user commands or options, such as a
|
| 110 |
+
menu, a prominent item in the list meets this criterion.
|
| 111 |
+
|
| 112 |
+
1. Source Code.
|
| 113 |
+
|
| 114 |
+
The "source code" for a work means the preferred form of the work
|
| 115 |
+
for making modifications to it. "Object code" means any non-source
|
| 116 |
+
form of a work.
|
| 117 |
+
|
| 118 |
+
A "Standard Interface" means an interface that either is an official
|
| 119 |
+
standard defined by a recognized standards body, or, in the case of
|
| 120 |
+
interfaces specified for a particular programming language, one that
|
| 121 |
+
is widely used among developers working in that language.
|
| 122 |
+
|
| 123 |
+
The "System Libraries" of an executable work include anything, other
|
| 124 |
+
than the work as a whole, that (a) is included in the normal form of
|
| 125 |
+
packaging a Major Component, but which is not part of that Major
|
| 126 |
+
Component, and (b) serves only to enable use of the work with that
|
| 127 |
+
Major Component, or to implement a Standard Interface for which an
|
| 128 |
+
implementation is available to the public in source code form. A
|
| 129 |
+
"Major Component", in this context, means a major essential component
|
| 130 |
+
(kernel, window system, and so on) of the specific operating system
|
| 131 |
+
(if any) on which the executable work runs, or a compiler used to
|
| 132 |
+
produce the work, or an object code interpreter used to run it.
|
| 133 |
+
|
| 134 |
+
The "Corresponding Source" for a work in object code form means all
|
| 135 |
+
the source code needed to generate, install, and (for an executable
|
| 136 |
+
work) run the object code and to modify the work, including scripts to
|
| 137 |
+
control those activities. However, it does not include the work's
|
| 138 |
+
System Libraries, or general-purpose tools or generally available free
|
| 139 |
+
programs which are used unmodified in performing those activities but
|
| 140 |
+
which are not part of the work. For example, Corresponding Source
|
| 141 |
+
includes interface definition files associated with source files for
|
| 142 |
+
the work, and the source code for shared libraries and dynamically
|
| 143 |
+
linked subprograms that the work is specifically designed to require,
|
| 144 |
+
such as by intimate data communication or control flow between those
|
| 145 |
+
subprograms and other parts of the work.
|
| 146 |
+
|
| 147 |
+
The Corresponding Source need not include anything that users
|
| 148 |
+
can regenerate automatically from other parts of the Corresponding
|
| 149 |
+
Source.
|
| 150 |
+
|
| 151 |
+
The Corresponding Source for a work in source code form is that
|
| 152 |
+
same work.
|
| 153 |
+
|
| 154 |
+
2. Basic Permissions.
|
| 155 |
+
|
| 156 |
+
All rights granted under this License are granted for the term of
|
| 157 |
+
copyright on the Program, and are irrevocable provided the stated
|
| 158 |
+
conditions are met. This License explicitly affirms your unlimited
|
| 159 |
+
permission to run the unmodified Program. The output from running a
|
| 160 |
+
covered work is covered by this License only if the output, given its
|
| 161 |
+
content, constitutes a covered work. This License acknowledges your
|
| 162 |
+
rights of fair use or other equivalent, as provided by copyright law.
|
| 163 |
+
|
| 164 |
+
You may make, run and propagate covered works that you do not
|
| 165 |
+
convey, without conditions so long as your license otherwise remains
|
| 166 |
+
in force. You may convey covered works to others for the sole purpose
|
| 167 |
+
of having them make modifications exclusively for you, or provide you
|
| 168 |
+
with facilities for running those works, provided that you comply with
|
| 169 |
+
the terms of this License in conveying all material for which you do
|
| 170 |
+
not control copyright. Those thus making or running the covered works
|
| 171 |
+
for you must do so exclusively on your behalf, under your direction
|
| 172 |
+
and control, on terms that prohibit them from making any copies of
|
| 173 |
+
your copyrighted material outside their relationship with you.
|
| 174 |
+
|
| 175 |
+
Conveying under any other circumstances is permitted solely under
|
| 176 |
+
the conditions stated below. Sublicensing is not allowed; section 10
|
| 177 |
+
makes it unnecessary.
|
| 178 |
+
|
| 179 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
| 180 |
+
|
| 181 |
+
No covered work shall be deemed part of an effective technological
|
| 182 |
+
measure under any applicable law fulfilling obligations under article
|
| 183 |
+
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
| 184 |
+
similar laws prohibiting or restricting circumvention of such
|
| 185 |
+
measures.
|
| 186 |
+
|
| 187 |
+
When you convey a covered work, you waive any legal power to forbid
|
| 188 |
+
circumvention of technological measures to the extent such circumvention
|
| 189 |
+
is effected by exercising rights under this License with respect to
|
| 190 |
+
the covered work, and you disclaim any intention to limit operation or
|
| 191 |
+
modification of the work as a means of enforcing, against the work's
|
| 192 |
+
users, your or third parties' legal rights to forbid circumvention of
|
| 193 |
+
technological measures.
|
| 194 |
+
|
| 195 |
+
4. Conveying Verbatim Copies.
|
| 196 |
+
|
| 197 |
+
You may convey verbatim copies of the Program's source code as you
|
| 198 |
+
receive it, in any medium, provided that you conspicuously and
|
| 199 |
+
appropriately publish on each copy an appropriate copyright notice;
|
| 200 |
+
keep intact all notices stating that this License and any
|
| 201 |
+
non-permissive terms added in accord with section 7 apply to the code;
|
| 202 |
+
keep intact all notices of the absence of any warranty; and give all
|
| 203 |
+
recipients a copy of this License along with the Program.
|
| 204 |
+
|
| 205 |
+
You may charge any price or no price for each copy that you convey,
|
| 206 |
+
and you may offer support or warranty protection for a fee.
|
| 207 |
+
|
| 208 |
+
5. Conveying Modified Source Versions.
|
| 209 |
+
|
| 210 |
+
You may convey a work based on the Program, or the modifications to
|
| 211 |
+
produce it from the Program, in the form of source code under the
|
| 212 |
+
terms of section 4, provided that you also meet all of these conditions:
|
| 213 |
+
|
| 214 |
+
a) The work must carry prominent notices stating that you modified
|
| 215 |
+
it, and giving a relevant date.
|
| 216 |
+
|
| 217 |
+
b) The work must carry prominent notices stating that it is
|
| 218 |
+
released under this License and any conditions added under section
|
| 219 |
+
7. This requirement modifies the requirement in section 4 to
|
| 220 |
+
"keep intact all notices".
|
| 221 |
+
|
| 222 |
+
c) You must license the entire work, as a whole, under this
|
| 223 |
+
License to anyone who comes into possession of a copy. This
|
| 224 |
+
License will therefore apply, along with any applicable section 7
|
| 225 |
+
additional terms, to the whole of the work, and all its parts,
|
| 226 |
+
regardless of how they are packaged. This License gives no
|
| 227 |
+
permission to license the work in any other way, but it does not
|
| 228 |
+
invalidate such permission if you have separately received it.
|
| 229 |
+
|
| 230 |
+
d) If the work has interactive user interfaces, each must display
|
| 231 |
+
Appropriate Legal Notices; however, if the Program has interactive
|
| 232 |
+
interfaces that do not display Appropriate Legal Notices, your
|
| 233 |
+
work need not make them do so.
|
| 234 |
+
|
| 235 |
+
A compilation of a covered work with other separate and independent
|
| 236 |
+
works, which are not by their nature extensions of the covered work,
|
| 237 |
+
and which are not combined with it such as to form a larger program,
|
| 238 |
+
in or on a volume of a storage or distribution medium, is called an
|
| 239 |
+
"aggregate" if the compilation and its resulting copyright are not
|
| 240 |
+
used to limit the access or legal rights of the compilation's users
|
| 241 |
+
beyond what the individual works permit. Inclusion of a covered work
|
| 242 |
+
in an aggregate does not cause this License to apply to the other
|
| 243 |
+
parts of the aggregate.
|
| 244 |
+
|
| 245 |
+
6. Conveying Non-Source Forms.
|
| 246 |
+
|
| 247 |
+
You may convey a covered work in object code form under the terms
|
| 248 |
+
of sections 4 and 5, provided that you also convey the
|
| 249 |
+
machine-readable Corresponding Source under the terms of this License,
|
| 250 |
+
in one of these ways:
|
| 251 |
+
|
| 252 |
+
a) Convey the object code in, or embodied in, a physical product
|
| 253 |
+
(including a physical distribution medium), accompanied by the
|
| 254 |
+
Corresponding Source fixed on a durable physical medium
|
| 255 |
+
customarily used for software interchange.
|
| 256 |
+
|
| 257 |
+
b) Convey the object code in, or embodied in, a physical product
|
| 258 |
+
(including a physical distribution medium), accompanied by a
|
| 259 |
+
written offer, valid for at least three years and valid for as
|
| 260 |
+
long as you offer spare parts or customer support for that product
|
| 261 |
+
model, to give anyone who possesses the object code either (1) a
|
| 262 |
+
copy of the Corresponding Source for all the software in the
|
| 263 |
+
product that is covered by this License, on a durable physical
|
| 264 |
+
medium customarily used for software interchange, for a price no
|
| 265 |
+
more than your reasonable cost of physically performing this
|
| 266 |
+
conveying of source, or (2) access to copy the
|
| 267 |
+
Corresponding Source from a network server at no charge.
|
| 268 |
+
|
| 269 |
+
c) Convey individual copies of the object code with a copy of the
|
| 270 |
+
written offer to provide the Corresponding Source. This
|
| 271 |
+
alternative is allowed only occasionally and noncommercially, and
|
| 272 |
+
only if you received the object code with such an offer, in accord
|
| 273 |
+
with subsection 6b.
|
| 274 |
+
|
| 275 |
+
d) Convey the object code by offering access from a designated
|
| 276 |
+
place (gratis or for a charge), and offer equivalent access to the
|
| 277 |
+
Corresponding Source in the same way through the same place at no
|
| 278 |
+
further charge. You need not require recipients to copy the
|
| 279 |
+
Corresponding Source along with the object code. If the place to
|
| 280 |
+
copy the object code is a network server, the Corresponding Source
|
| 281 |
+
may be on a different server (operated by you or a third party)
|
| 282 |
+
that supports equivalent copying facilities, provided you maintain
|
| 283 |
+
clear directions next to the object code saying where to find the
|
| 284 |
+
Corresponding Source. Regardless of what server hosts the
|
| 285 |
+
Corresponding Source, you remain obligated to ensure that it is
|
| 286 |
+
available for as long as needed to satisfy these requirements.
|
| 287 |
+
|
| 288 |
+
e) Convey the object code using peer-to-peer transmission, provided
|
| 289 |
+
you inform other peers where the object code and Corresponding
|
| 290 |
+
Source of the work are being offered to the general public at no
|
| 291 |
+
charge under subsection 6d.
|
| 292 |
+
|
| 293 |
+
A separable portion of the object code, whose source code is excluded
|
| 294 |
+
from the Corresponding Source as a System Library, need not be
|
| 295 |
+
included in conveying the object code work.
|
| 296 |
+
|
| 297 |
+
A "User Product" is either (1) a "consumer product", which means any
|
| 298 |
+
tangible personal property which is normally used for personal, family,
|
| 299 |
+
or household purposes, or (2) anything designed or sold for incorporation
|
| 300 |
+
into a dwelling. In determining whether a product is a consumer product,
|
| 301 |
+
doubtful cases shall be resolved in favor of coverage. For a particular
|
| 302 |
+
product received by a particular user, "normally used" refers to a
|
| 303 |
+
typical or common use of that class of product, regardless of the status
|
| 304 |
+
of the particular user or of the way in which the particular user
|
| 305 |
+
actually uses, or expects or is expected to use, the product. A product
|
| 306 |
+
is a consumer product regardless of whether the product has substantial
|
| 307 |
+
commercial, industrial or non-consumer uses, unless such uses represent
|
| 308 |
+
the only significant mode of use of the product.
|
| 309 |
+
|
| 310 |
+
"Installation Information" for a User Product means any methods,
|
| 311 |
+
procedures, authorization keys, or other information required to install
|
| 312 |
+
and execute modified versions of a covered work in that User Product from
|
| 313 |
+
a modified version of its Corresponding Source. The information must
|
| 314 |
+
suffice to ensure that the continued functioning of the modified object
|
| 315 |
+
code is in no case prevented or interfered with solely because
|
| 316 |
+
modification has been made.
|
| 317 |
+
|
| 318 |
+
If you convey an object code work under this section in, or with, or
|
| 319 |
+
specifically for use in, a User Product, and the conveying occurs as
|
| 320 |
+
part of a transaction in which the right of possession and use of the
|
| 321 |
+
User Product is transferred to the recipient in perpetuity or for a
|
| 322 |
+
fixed term (regardless of how the transaction is characterized), the
|
| 323 |
+
Corresponding Source conveyed under this section must be accompanied
|
| 324 |
+
by the Installation Information. But this requirement does not apply
|
| 325 |
+
if neither you nor any third party retains the ability to install
|
| 326 |
+
modified object code on the User Product (for example, the work has
|
| 327 |
+
been installed in ROM).
|
| 328 |
+
|
| 329 |
+
The requirement to provide Installation Information does not include a
|
| 330 |
+
requirement to continue to provide support service, warranty, or updates
|
| 331 |
+
for a work that has been modified or installed by the recipient, or for
|
| 332 |
+
the User Product in which it has been modified or installed. Access to a
|
| 333 |
+
network may be denied when the modification itself materially and
|
| 334 |
+
adversely affects the operation of the network or violates the rules and
|
| 335 |
+
protocols for communication across the network.
|
| 336 |
+
|
| 337 |
+
Corresponding Source conveyed, and Installation Information provided,
|
| 338 |
+
in accord with this section must be in a format that is publicly
|
| 339 |
+
documented (and with an implementation available to the public in
|
| 340 |
+
source code form), and must require no special password or key for
|
| 341 |
+
unpacking, reading or copying.
|
| 342 |
+
|
| 343 |
+
7. Additional Terms.
|
| 344 |
+
|
| 345 |
+
"Additional permissions" are terms that supplement the terms of this
|
| 346 |
+
License by making exceptions from one or more of its conditions.
|
| 347 |
+
Additional permissions that are applicable to the entire Program shall
|
| 348 |
+
be treated as though they were included in this License, to the extent
|
| 349 |
+
that they are valid under applicable law. If additional permissions
|
| 350 |
+
apply only to part of the Program, that part may be used separately
|
| 351 |
+
under those permissions, but the entire Program remains governed by
|
| 352 |
+
this License without regard to the additional permissions.
|
| 353 |
+
|
| 354 |
+
When you convey a copy of a covered work, you may at your option
|
| 355 |
+
remove any additional permissions from that copy, or from any part of
|
| 356 |
+
it. (Additional permissions may be written to require their own
|
| 357 |
+
removal in certain cases when you modify the work.) You may place
|
| 358 |
+
additional permissions on material, added by you to a covered work,
|
| 359 |
+
for which you have or can give appropriate copyright permission.
|
| 360 |
+
|
| 361 |
+
Notwithstanding any other provision of this License, for material you
|
| 362 |
+
add to a covered work, you may (if authorized by the copyright holders of
|
| 363 |
+
that material) supplement the terms of this License with terms:
|
| 364 |
+
|
| 365 |
+
a) Disclaiming warranty or limiting liability differently from the
|
| 366 |
+
terms of sections 15 and 16 of this License; or
|
| 367 |
+
|
| 368 |
+
b) Requiring preservation of specified reasonable legal notices or
|
| 369 |
+
author attributions in that material or in the Appropriate Legal
|
| 370 |
+
Notices displayed by works containing it; or
|
| 371 |
+
|
| 372 |
+
c) Prohibiting misrepresentation of the origin of that material, or
|
| 373 |
+
requiring that modified versions of such material be marked in
|
| 374 |
+
reasonable ways as different from the original version; or
|
| 375 |
+
|
| 376 |
+
d) Limiting the use for publicity purposes of names of licensors or
|
| 377 |
+
authors of the material; or
|
| 378 |
+
|
| 379 |
+
e) Declining to grant rights under trademark law for use of some
|
| 380 |
+
trade names, trademarks, or service marks; or
|
| 381 |
+
|
| 382 |
+
f) Requiring indemnification of licensors and authors of that
|
| 383 |
+
material by anyone who conveys the material (or modified versions of
|
| 384 |
+
it) with contractual assumptions of liability to the recipient, for
|
| 385 |
+
any liability that these contractual assumptions directly impose on
|
| 386 |
+
those licensors and authors.
|
| 387 |
+
|
| 388 |
+
All other non-permissive additional terms are considered "further
|
| 389 |
+
restrictions" within the meaning of section 10. If the Program as you
|
| 390 |
+
received it, or any part of it, contains a notice stating that it is
|
| 391 |
+
governed by this License along with a term that is a further
|
| 392 |
+
restriction, you may remove that term. If a license document contains
|
| 393 |
+
a further restriction but permits relicensing or conveying under this
|
| 394 |
+
License, you may add to a covered work material governed by the terms
|
| 395 |
+
of that license document, provided that the further restriction does
|
| 396 |
+
not survive such relicensing or conveying.
|
| 397 |
+
|
| 398 |
+
If you add terms to a covered work in accord with this section, you
|
| 399 |
+
must place, in the relevant source files, a statement of the
|
| 400 |
+
additional terms that apply to those files, or a notice indicating
|
| 401 |
+
where to find the applicable terms.
|
| 402 |
+
|
| 403 |
+
Additional terms, permissive or non-permissive, may be stated in the
|
| 404 |
+
form of a separately written license, or stated as exceptions;
|
| 405 |
+
the above requirements apply either way.
|
| 406 |
+
|
| 407 |
+
8. Termination.
|
| 408 |
+
|
| 409 |
+
You may not propagate or modify a covered work except as expressly
|
| 410 |
+
provided under this License. Any attempt otherwise to propagate or
|
| 411 |
+
modify it is void, and will automatically terminate your rights under
|
| 412 |
+
this License (including any patent licenses granted under the third
|
| 413 |
+
paragraph of section 11).
|
| 414 |
+
|
| 415 |
+
However, if you cease all violation of this License, then your
|
| 416 |
+
license from a particular copyright holder is reinstated (a)
|
| 417 |
+
provisionally, unless and until the copyright holder explicitly and
|
| 418 |
+
finally terminates your license, and (b) permanently, if the copyright
|
| 419 |
+
holder fails to notify you of the violation by some reasonable means
|
| 420 |
+
prior to 60 days after the cessation.
|
| 421 |
+
|
| 422 |
+
Moreover, your license from a particular copyright holder is
|
| 423 |
+
reinstated permanently if the copyright holder notifies you of the
|
| 424 |
+
violation by some reasonable means, this is the first time you have
|
| 425 |
+
received notice of violation of this License (for any work) from that
|
| 426 |
+
copyright holder, and you cure the violation prior to 30 days after
|
| 427 |
+
your receipt of the notice.
|
| 428 |
+
|
| 429 |
+
Termination of your rights under this section does not terminate the
|
| 430 |
+
licenses of parties who have received copies or rights from you under
|
| 431 |
+
this License. If your rights have been terminated and not permanently
|
| 432 |
+
reinstated, you do not qualify to receive new licenses for the same
|
| 433 |
+
material under section 10.
|
| 434 |
+
|
| 435 |
+
9. Acceptance Not Required for Having Copies.
|
| 436 |
+
|
| 437 |
+
You are not required to accept this License in order to receive or
|
| 438 |
+
run a copy of the Program. Ancillary propagation of a covered work
|
| 439 |
+
occurring solely as a consequence of using peer-to-peer transmission
|
| 440 |
+
to receive a copy likewise does not require acceptance. However,
|
| 441 |
+
nothing other than this License grants you permission to propagate or
|
| 442 |
+
modify any covered work. These actions infringe copyright if you do
|
| 443 |
+
not accept this License. Therefore, by modifying or propagating a
|
| 444 |
+
covered work, you indicate your acceptance of this License to do so.
|
| 445 |
+
|
| 446 |
+
10. Automatic Licensing of Downstream Recipients.
|
| 447 |
+
|
| 448 |
+
Each time you convey a covered work, the recipient automatically
|
| 449 |
+
receives a license from the original licensors, to run, modify and
|
| 450 |
+
propagate that work, subject to this License. You are not responsible
|
| 451 |
+
for enforcing compliance by third parties with this License.
|
| 452 |
+
|
| 453 |
+
An "entity transaction" is a transaction transferring control of an
|
| 454 |
+
organization, or substantially all assets of one, or subdividing an
|
| 455 |
+
organization, or merging organizations. If propagation of a covered
|
| 456 |
+
work results from an entity transaction, each party to that
|
| 457 |
+
transaction who receives a copy of the work also receives whatever
|
| 458 |
+
licenses to the work the party's predecessor in interest had or could
|
| 459 |
+
give under the previous paragraph, plus a right to possession of the
|
| 460 |
+
Corresponding Source of the work from the predecessor in interest, if
|
| 461 |
+
the predecessor has it or can get it with reasonable efforts.
|
| 462 |
+
|
| 463 |
+
You may not impose any further restrictions on the exercise of the
|
| 464 |
+
rights granted or affirmed under this License. For example, you may
|
| 465 |
+
not impose a license fee, royalty, or other charge for exercise of
|
| 466 |
+
rights granted under this License, and you may not initiate litigation
|
| 467 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
| 468 |
+
any patent claim is infringed by making, using, selling, offering for
|
| 469 |
+
sale, or importing the Program or any portion of it.
|
| 470 |
+
|
| 471 |
+
11. Patents.
|
| 472 |
+
|
| 473 |
+
A "contributor" is a copyright holder who authorizes use under this
|
| 474 |
+
License of the Program or a work on which the Program is based. The
|
| 475 |
+
work thus licensed is called the contributor's "contributor version".
|
| 476 |
+
|
| 477 |
+
A contributor's "essential patent claims" are all patent claims
|
| 478 |
+
owned or controlled by the contributor, whether already acquired or
|
| 479 |
+
hereafter acquired, that would be infringed by some manner, permitted
|
| 480 |
+
by this License, of making, using, or selling its contributor version,
|
| 481 |
+
but do not include claims that would be infringed only as a
|
| 482 |
+
consequence of further modification of the contributor version. For
|
| 483 |
+
purposes of this definition, "control" includes the right to grant
|
| 484 |
+
patent sublicenses in a manner consistent with the requirements of
|
| 485 |
+
this License.
|
| 486 |
+
|
| 487 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
| 488 |
+
patent license under the contributor's essential patent claims, to
|
| 489 |
+
make, use, sell, offer for sale, import and otherwise run, modify and
|
| 490 |
+
propagate the contents of its contributor version.
|
| 491 |
+
|
| 492 |
+
In the following three paragraphs, a "patent license" is any express
|
| 493 |
+
agreement or commitment, however denominated, not to enforce a patent
|
| 494 |
+
(such as an express permission to practice a patent or covenant not to
|
| 495 |
+
sue for patent infringement). To "grant" such a patent license to a
|
| 496 |
+
party means to make such an agreement or commitment not to enforce a
|
| 497 |
+
patent against the party.
|
| 498 |
+
|
| 499 |
+
If you convey a covered work, knowingly relying on a patent license,
|
| 500 |
+
and the Corresponding Source of the work is not available for anyone
|
| 501 |
+
to copy, free of charge and under the terms of this License, through a
|
| 502 |
+
publicly available network server or other readily accessible means,
|
| 503 |
+
then you must either (1) cause the Corresponding Source to be so
|
| 504 |
+
available, or (2) arrange to deprive yourself of the benefit of the
|
| 505 |
+
patent license for this particular work, or (3) arrange, in a manner
|
| 506 |
+
consistent with the requirements of this License, to extend the patent
|
| 507 |
+
license to downstream recipients. "Knowingly relying" means you have
|
| 508 |
+
actual knowledge that, but for the patent license, your conveying the
|
| 509 |
+
covered work in a country, or your recipient's use of the covered work
|
| 510 |
+
in a country, would infringe one or more identifiable patents in that
|
| 511 |
+
country that you have reason to believe are valid.
|
| 512 |
+
|
| 513 |
+
If, pursuant to or in connection with a single transaction or
|
| 514 |
+
arrangement, you convey, or propagate by procuring conveyance of, a
|
| 515 |
+
covered work, and grant a patent license to some of the parties
|
| 516 |
+
receiving the covered work authorizing them to use, propagate, modify
|
| 517 |
+
or convey a specific copy of the covered work, then the patent license
|
| 518 |
+
you grant is automatically extended to all recipients of the covered
|
| 519 |
+
work and works based on it.
|
| 520 |
+
|
| 521 |
+
A patent license is "discriminatory" if it does not include within
|
| 522 |
+
the scope of its coverage, prohibits the exercise of, or is
|
| 523 |
+
conditioned on the non-exercise of one or more of the rights that are
|
| 524 |
+
specifically granted under this License. You may not convey a covered
|
| 525 |
+
work if you are a party to an arrangement with a third party that is
|
| 526 |
+
in the business of distributing software, under which you make payment
|
| 527 |
+
to the third party based on the extent of your activity of conveying
|
| 528 |
+
the work, and under which the third party grants, to any of the
|
| 529 |
+
parties who would receive the covered work from you, a discriminatory
|
| 530 |
+
patent license (a) in connection with copies of the covered work
|
| 531 |
+
conveyed by you (or copies made from those copies), or (b) primarily
|
| 532 |
+
for and in connection with specific products or compilations that
|
| 533 |
+
contain the covered work, unless you entered into that arrangement,
|
| 534 |
+
or that patent license was granted, prior to 28 March 2007.
|
| 535 |
+
|
| 536 |
+
Nothing in this License shall be construed as excluding or limiting
|
| 537 |
+
any implied license or other defenses to infringement that may
|
| 538 |
+
otherwise be available to you under applicable patent law.
|
| 539 |
+
|
| 540 |
+
12. No Surrender of Others' Freedom.
|
| 541 |
+
|
| 542 |
+
If conditions are imposed on you (whether by court order, agreement or
|
| 543 |
+
otherwise) that contradict the conditions of this License, they do not
|
| 544 |
+
excuse you from the conditions of this License. If you cannot convey a
|
| 545 |
+
covered work so as to satisfy simultaneously your obligations under this
|
| 546 |
+
License and any other pertinent obligations, then as a consequence you may
|
| 547 |
+
not convey it at all. For example, if you agree to terms that obligate you
|
| 548 |
+
to collect a royalty for further conveying from those to whom you convey
|
| 549 |
+
the Program, the only way you could satisfy both those terms and this
|
| 550 |
+
License would be to refrain entirely from conveying the Program.
|
| 551 |
+
|
| 552 |
+
13. Use with the GNU Affero General Public License.
|
| 553 |
+
|
| 554 |
+
Notwithstanding any other provision of this License, you have
|
| 555 |
+
permission to link or combine any covered work with a work licensed
|
| 556 |
+
under version 3 of the GNU Affero General Public License into a single
|
| 557 |
+
combined work, and to convey the resulting work. The terms of this
|
| 558 |
+
License will continue to apply to the part which is the covered work,
|
| 559 |
+
but the special requirements of the GNU Affero General Public License,
|
| 560 |
+
section 13, concerning interaction through a network will apply to the
|
| 561 |
+
combination as such.
|
| 562 |
+
|
| 563 |
+
14. Revised Versions of this License.
|
| 564 |
+
|
| 565 |
+
The Free Software Foundation may publish revised and/or new versions of
|
| 566 |
+
the GNU General Public License from time to time. Such new versions will
|
| 567 |
+
be similar in spirit to the present version, but may differ in detail to
|
| 568 |
+
address new problems or concerns.
|
| 569 |
+
|
| 570 |
+
Each version is given a distinguishing version number. If the
|
| 571 |
+
Program specifies that a certain numbered version of the GNU General
|
| 572 |
+
Public License "or any later version" applies to it, you have the
|
| 573 |
+
option of following the terms and conditions either of that numbered
|
| 574 |
+
version or of any later version published by the Free Software
|
| 575 |
+
Foundation. If the Program does not specify a version number of the
|
| 576 |
+
GNU General Public License, you may choose any version ever published
|
| 577 |
+
by the Free Software Foundation.
|
| 578 |
+
|
| 579 |
+
If the Program specifies that a proxy can decide which future
|
| 580 |
+
versions of the GNU General Public License can be used, that proxy's
|
| 581 |
+
public statement of acceptance of a version permanently authorizes you
|
| 582 |
+
to choose that version for the Program.
|
| 583 |
+
|
| 584 |
+
Later license versions may give you additional or different
|
| 585 |
+
permissions. However, no additional obligations are imposed on any
|
| 586 |
+
author or copyright holder as a result of your choosing to follow a
|
| 587 |
+
later version.
|
| 588 |
+
|
| 589 |
+
15. Disclaimer of Warranty.
|
| 590 |
+
|
| 591 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
| 592 |
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
| 593 |
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
| 594 |
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
| 595 |
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
| 596 |
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
| 597 |
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
| 598 |
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
| 599 |
+
|
| 600 |
+
16. Limitation of Liability.
|
| 601 |
+
|
| 602 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
| 603 |
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
| 604 |
+
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
| 605 |
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
| 606 |
+
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
| 607 |
+
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
| 608 |
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
| 609 |
+
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
| 610 |
+
SUCH DAMAGES.
|
| 611 |
+
|
| 612 |
+
17. Interpretation of Sections 15 and 16.
|
| 613 |
+
|
| 614 |
+
If the disclaimer of warranty and limitation of liability provided
|
| 615 |
+
above cannot be given local legal effect according to their terms,
|
| 616 |
+
reviewing courts shall apply local law that most closely approximates
|
| 617 |
+
an absolute waiver of all civil liability in connection with the
|
| 618 |
+
Program, unless a warranty or assumption of liability accompanies a
|
| 619 |
+
copy of the Program in return for a fee.
|
| 620 |
+
|
| 621 |
+
END OF TERMS AND CONDITIONS
|
| 622 |
+
|
| 623 |
+
How to Apply These Terms to Your New Programs
|
| 624 |
+
|
| 625 |
+
If you develop a new program, and you want it to be of the greatest
|
| 626 |
+
possible use to the public, the best way to achieve this is to make it
|
| 627 |
+
free software which everyone can redistribute and change under these terms.
|
| 628 |
+
|
| 629 |
+
To do so, attach the following notices to the program. It is safest
|
| 630 |
+
to attach them to the start of each source file to most effectively
|
| 631 |
+
state the exclusion of warranty; and each file should have at least
|
| 632 |
+
the "copyright" line and a pointer to where the full notice is found.
|
| 633 |
+
|
| 634 |
+
<one line to give the program's name and a brief idea of what it does.>
|
| 635 |
+
Copyright (C) <year> <name of author>
|
| 636 |
+
|
| 637 |
+
This program is free software: you can redistribute it and/or modify
|
| 638 |
+
it under the terms of the GNU General Public License as published by
|
| 639 |
+
the Free Software Foundation, either version 3 of the License, or
|
| 640 |
+
(at your option) any later version.
|
| 641 |
+
|
| 642 |
+
This program is distributed in the hope that it will be useful,
|
| 643 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 644 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 645 |
+
GNU General Public License for more details.
|
| 646 |
+
|
| 647 |
+
You should have received a copy of the GNU General Public License
|
| 648 |
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 649 |
+
|
| 650 |
+
Also add information on how to contact you by electronic and paper mail.
|
| 651 |
+
|
| 652 |
+
If the program does terminal interaction, make it output a short
|
| 653 |
+
notice like this when it starts in an interactive mode:
|
| 654 |
+
|
| 655 |
+
<program> Copyright (C) <year> <name of author>
|
| 656 |
+
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
| 657 |
+
This is free software, and you are welcome to redistribute it
|
| 658 |
+
under certain conditions; type `show c' for details.
|
| 659 |
+
|
| 660 |
+
The hypothetical commands `show w' and `show c' should show the appropriate
|
| 661 |
+
parts of the General Public License. Of course, your program's commands
|
| 662 |
+
might be different; for a GUI interface, you would use an "about box".
|
| 663 |
+
|
| 664 |
+
You should also get your employer (if you work as a programmer) or school,
|
| 665 |
+
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
| 666 |
+
For more information on this, and how to apply and follow the GNU GPL, see
|
| 667 |
+
<https://www.gnu.org/licenses/>.
|
| 668 |
+
|
| 669 |
+
The GNU General Public License does not permit incorporating your program
|
| 670 |
+
into proprietary programs. If your program is a subroutine library, you
|
| 671 |
+
may consider it more useful to permit linking proprietary applications with
|
| 672 |
+
the library. If this is what you want to do, use the GNU Lesser General
|
| 673 |
+
Public License instead of this License. But first, please read
|
| 674 |
+
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
cache/bigcache.go
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cache
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/gob"
|
| 7 |
+
"errors"
|
| 8 |
+
|
| 9 |
+
"github.com/allegro/bigcache/v3"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type bigCache struct {
|
| 13 |
+
cache *bigcache.BigCache
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func newBigCache(cacheConfig *cacheConfig) (*bigCache, error) {
|
| 17 |
+
cache, err := bigcache.New(context.Background(), bigcache.Config{
|
| 18 |
+
Shards: 16,
|
| 19 |
+
LifeWindow: cacheConfig.ttl,
|
| 20 |
+
CleanWindow: cacheConfig.cleanFreq,
|
| 21 |
+
MaxEntriesInWindow: 1000 * 10 * 60,
|
| 22 |
+
MaxEntrySize: 500,
|
| 23 |
+
Verbose: false,
|
| 24 |
+
HardMaxCacheSize: cacheConfig.size,
|
| 25 |
+
StatsEnabled: true,
|
| 26 |
+
})
|
| 27 |
+
if err != nil {
|
| 28 |
+
return nil, err
|
| 29 |
+
}
|
| 30 |
+
return &bigCache{
|
| 31 |
+
cache: cache,
|
| 32 |
+
}, nil
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Set inserts the key/value pair into the cache.
|
| 36 |
+
// Only the exported fields of the given struct will be
|
| 37 |
+
// serialized and stored
|
| 38 |
+
func (c *bigCache) Set(key, value interface{}) error {
|
| 39 |
+
keyString, ok := key.(string)
|
| 40 |
+
if !ok {
|
| 41 |
+
return errors.New("a cache key must be a string")
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
valueBytes, err := serializeGOB(value)
|
| 45 |
+
if err != nil {
|
| 46 |
+
return err
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return c.cache.Set(keyString, valueBytes)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Get returns the value correlating to the key in the cache
|
| 53 |
+
func (c *bigCache) Get(key interface{}) (interface{}, error) {
|
| 54 |
+
// Assert the key is of string type
|
| 55 |
+
keyString, ok := key.(string)
|
| 56 |
+
if !ok {
|
| 57 |
+
return nil, errors.New("a cache key must be a string")
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Get the value in the byte format it is stored in
|
| 61 |
+
valueBytes, err := c.cache.Get(keyString)
|
| 62 |
+
if err != nil {
|
| 63 |
+
return nil, err
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Deserialize the bytes of the value
|
| 67 |
+
value, err := deserializeGOB(valueBytes)
|
| 68 |
+
if err != nil {
|
| 69 |
+
return nil, err
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return value, nil
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
func serializeGOB(value interface{}) ([]byte, error) {
|
| 76 |
+
buf := bytes.Buffer{}
|
| 77 |
+
enc := gob.NewEncoder(&buf)
|
| 78 |
+
gob.Register(value)
|
| 79 |
+
|
| 80 |
+
err := enc.Encode(&value)
|
| 81 |
+
if err != nil {
|
| 82 |
+
return nil, err
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
return buf.Bytes(), nil
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
func deserializeGOB(valueBytes []byte) (interface{}, error) {
|
| 89 |
+
var value interface{}
|
| 90 |
+
buf := bytes.NewBuffer(valueBytes)
|
| 91 |
+
dec := gob.NewDecoder(buf)
|
| 92 |
+
|
| 93 |
+
err := dec.Decode(&value)
|
| 94 |
+
if err != nil {
|
| 95 |
+
return nil, err
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
return value, nil
|
| 99 |
+
}
|
cache/cache.go
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cache
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
type cacheConfig struct {
|
| 8 |
+
size int // Size in MB
|
| 9 |
+
ttl time.Duration
|
| 10 |
+
cleanFreq time.Duration
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// Interface to wrap any caching implementation
|
| 14 |
+
type Cache interface {
|
| 15 |
+
Set(key, value interface{}) error // Only exported fields in struct will be stored
|
| 16 |
+
Get(key interface{}) (interface{}, error)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// New builds a new default cache. You may pass options to modify the default values
|
| 20 |
+
func New(opts ...Option) (Cache, error) {
|
| 21 |
+
cacheConfig := &cacheConfig{
|
| 22 |
+
size: 1,
|
| 23 |
+
ttl: 60 * time.Second,
|
| 24 |
+
cleanFreq: 30 * time.Second,
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
for _, opt := range opts {
|
| 28 |
+
opt.apply(cacheConfig)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
cache, err := newBigCache(cacheConfig)
|
| 32 |
+
if err != nil {
|
| 33 |
+
return nil, err
|
| 34 |
+
}
|
| 35 |
+
return cache, nil
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
type Option interface {
|
| 39 |
+
apply(cacheConfig *cacheConfig)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
type optionFunc func(*cacheConfig)
|
| 43 |
+
|
| 44 |
+
func (opt optionFunc) apply(cacheConfig *cacheConfig) {
|
| 45 |
+
opt(cacheConfig)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// WithSizeInMB sets the size of the cache in MBs
|
| 49 |
+
// The minimum size of the cache is 1 MB
|
| 50 |
+
// If a size of 0 or less is passed the cache will have unlimited size
|
| 51 |
+
func WithSizeInMB(size int) Option {
|
| 52 |
+
return optionFunc(func(cacheConfig *cacheConfig) {
|
| 53 |
+
cacheConfig.size = size
|
| 54 |
+
})
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// WithTTL will cause the cache to expire any item that lives longer
|
| 58 |
+
// than the given ttl
|
| 59 |
+
func WithTTL(ttl time.Duration) Option {
|
| 60 |
+
return optionFunc(func(cacheConfig *cacheConfig) {
|
| 61 |
+
cacheConfig.ttl = ttl
|
| 62 |
+
})
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// WithCleanFrequency sets how often the cache will clean out expired items
|
| 66 |
+
// The lowest the frequency may be is 1 second
|
| 67 |
+
// If the time is 0 then no cleaning will happen and items will never be removed
|
| 68 |
+
func WithCleanFrequency(cleanFreq time.Duration) Option {
|
| 69 |
+
return optionFunc(func(cacheConfig *cacheConfig) {
|
| 70 |
+
cacheConfig.cleanFreq = cleanFreq
|
| 71 |
+
})
|
| 72 |
+
}
|
cache/cachutil.go
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cache
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"reflect"
|
| 5 |
+
"time"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
var globalCache Cache
|
| 9 |
+
|
| 10 |
+
func CacheInit() {
|
| 11 |
+
|
| 12 |
+
var err error
|
| 13 |
+
globalCache, err = New(
|
| 14 |
+
WithSizeInMB(10),
|
| 15 |
+
WithTTL(12*time.Hour),
|
| 16 |
+
WithCleanFrequency(24*time.Hour),
|
| 17 |
+
)
|
| 18 |
+
if err != nil {
|
| 19 |
+
panic("Failed to initialize global cache: " + err.Error())
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func GetCache() Cache {
|
| 24 |
+
return globalCache
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func CachedFunction(fn interface{}, key string) func(...interface{}) (interface{}, error) {
|
| 28 |
+
return func(args ...interface{}) (interface{}, error) {
|
| 29 |
+
|
| 30 |
+
// Check if the result is already cached
|
| 31 |
+
if cachedResult, err := globalCache.Get(key); err == nil {
|
| 32 |
+
return cachedResult, nil
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// If not cached, call the original function to get the result
|
| 36 |
+
f := reflect.ValueOf(fn)
|
| 37 |
+
if len(args) == 0 {
|
| 38 |
+
args = nil // Ensure nil is passed when there are no arguments.
|
| 39 |
+
}
|
| 40 |
+
result := f.Call(getArgs(args))
|
| 41 |
+
|
| 42 |
+
// Check if the function returned an error as the last return value
|
| 43 |
+
if err, ok := result[len(result)-1].Interface().(error); ok && err != nil {
|
| 44 |
+
return nil, err
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Extract the result from the function call
|
| 48 |
+
finalResult := result[0].Interface()
|
| 49 |
+
|
| 50 |
+
// Cache the result with a default TTL (time-to-live)
|
| 51 |
+
globalCache.Set(key, finalResult)
|
| 52 |
+
|
| 53 |
+
return finalResult, nil
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func getArgs(args []interface{}) []reflect.Value {
|
| 58 |
+
var values []reflect.Value
|
| 59 |
+
for _, arg := range args {
|
| 60 |
+
values = append(values, reflect.ValueOf(arg))
|
| 61 |
+
}
|
| 62 |
+
return values
|
| 63 |
+
}
|
database/database.go
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package database
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"os"
|
| 6 |
+
"path/filepath"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/divyam234/teldrive/utils"
|
| 10 |
+
"github.com/pressly/goose/v3"
|
| 11 |
+
"gorm.io/driver/postgres"
|
| 12 |
+
"gorm.io/gorm"
|
| 13 |
+
"gorm.io/gorm/logger"
|
| 14 |
+
"gorm.io/gorm/schema"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
var DB *gorm.DB
|
| 18 |
+
|
| 19 |
+
func InitDB() {
|
| 20 |
+
|
| 21 |
+
var err error
|
| 22 |
+
|
| 23 |
+
newLogger := logger.New(
|
| 24 |
+
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
| 25 |
+
logger.Config{
|
| 26 |
+
SlowThreshold: time.Second,
|
| 27 |
+
LogLevel: logger.Silent,
|
| 28 |
+
IgnoreRecordNotFoundError: true,
|
| 29 |
+
ParameterizedQueries: true,
|
| 30 |
+
Colorful: false,
|
| 31 |
+
},
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
DB, err = gorm.Open(postgres.Open(utils.GetConfig().DatabaseUrl), &gorm.Config{
|
| 35 |
+
NamingStrategy: schema.NamingStrategy{
|
| 36 |
+
TablePrefix: "teldrive.",
|
| 37 |
+
SingularTable: false,
|
| 38 |
+
},
|
| 39 |
+
PrepareStmt: false,
|
| 40 |
+
NowFunc: func() time.Time {
|
| 41 |
+
return time.Now().UTC()
|
| 42 |
+
},
|
| 43 |
+
Logger: newLogger,
|
| 44 |
+
})
|
| 45 |
+
if err != nil {
|
| 46 |
+
panic(err)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
sqlDB, err := DB.DB()
|
| 50 |
+
if err != nil {
|
| 51 |
+
panic(err)
|
| 52 |
+
}
|
| 53 |
+
sqlDB.SetMaxIdleConns(10)
|
| 54 |
+
sqlDB.SetMaxOpenConns(100)
|
| 55 |
+
|
| 56 |
+
sqlDB.SetConnMaxLifetime(time.Hour)
|
| 57 |
+
go func() {
|
| 58 |
+
DB.Exec(`create collation if not exists numeric (provider = icu, locale = 'en@colnumeric=yes');`)
|
| 59 |
+
if utils.GetConfig().RunMigrations {
|
| 60 |
+
migrate()
|
| 61 |
+
}
|
| 62 |
+
}()
|
| 63 |
+
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
func migrate() {
|
| 67 |
+
|
| 68 |
+
config := utils.GetConfig()
|
| 69 |
+
|
| 70 |
+
if err := goose.SetDialect("postgres"); err != nil {
|
| 71 |
+
panic(err)
|
| 72 |
+
}
|
| 73 |
+
db, _ := DB.DB()
|
| 74 |
+
if err := goose.Up(db, filepath.Join(config.ExecDir, "database", "migrations")); err != nil {
|
| 75 |
+
panic(err)
|
| 76 |
+
}
|
| 77 |
+
}
|
database/migrations/20230817172319_init.sql
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- +goose Up
|
| 2 |
+
create extension if not exists pgcrypto;
|
| 3 |
+
|
| 4 |
+
create extension if not exists btree_gin;
|
| 5 |
+
|
| 6 |
+
create schema if not exists teldrive;
|
| 7 |
+
|
| 8 |
+
create collation if not exists numeric (provider = icu, locale = 'en@colnumeric=yes');
|
| 9 |
+
|
| 10 |
+
-- +goose StatementBegin
|
| 11 |
+
create or replace
|
| 12 |
+
function teldrive.generate_uid(size int) returns text language plpgsql as $$
|
| 13 |
+
declare characters text := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
| 14 |
+
|
| 15 |
+
bytes bytea := gen_random_bytes(size);
|
| 16 |
+
|
| 17 |
+
l int := length(characters);
|
| 18 |
+
|
| 19 |
+
i int := 0;
|
| 20 |
+
|
| 21 |
+
output text := '';
|
| 22 |
+
|
| 23 |
+
begin while i < size loop output := output || substr(characters,
|
| 24 |
+
get_byte(bytes,
|
| 25 |
+
i) % l + 1,
|
| 26 |
+
1);
|
| 27 |
+
|
| 28 |
+
i := i + 1;
|
| 29 |
+
end loop;
|
| 30 |
+
|
| 31 |
+
return output;
|
| 32 |
+
end;
|
| 33 |
+
|
| 34 |
+
$$;
|
| 35 |
+
-- +goose StatementEnd
|
| 36 |
+
|
| 37 |
+
-- +goose StatementBegin
|
| 38 |
+
create or replace
|
| 39 |
+
function teldrive.get_tsvector(t text) returns tsvector language plpgsql immutable as $$
|
| 40 |
+
declare res tsvector := to_tsvector(regexp_replace(t,
|
| 41 |
+
'[^A-Za-z0-9 ]',
|
| 42 |
+
' ',
|
| 43 |
+
'g'));
|
| 44 |
+
|
| 45 |
+
begin return res;
|
| 46 |
+
end;
|
| 47 |
+
|
| 48 |
+
$$;
|
| 49 |
+
-- +goose StatementEnd
|
| 50 |
+
|
| 51 |
+
-- +goose StatementBegin
|
| 52 |
+
create or replace
|
| 53 |
+
function teldrive.get_tsquery(t text) returns tsquery language plpgsql immutable as $$
|
| 54 |
+
declare res tsquery = concat(
|
| 55 |
+
plainto_tsquery(regexp_replace(t,
|
| 56 |
+
'[^A-Za-z0-9 ]',
|
| 57 |
+
' ',
|
| 58 |
+
'g')),
|
| 59 |
+
':*'
|
| 60 |
+
)::tsquery;
|
| 61 |
+
|
| 62 |
+
begin return res;
|
| 63 |
+
end;
|
| 64 |
+
|
| 65 |
+
$$;
|
| 66 |
+
-- +goose StatementEnd
|
database/migrations/20230817172325_tables.sql
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- +goose Up
|
| 2 |
+
create table teldrive.files (
|
| 3 |
+
id text primary key not null default teldrive.generate_uid(16),
|
| 4 |
+
name text not null,
|
| 5 |
+
type text not null,
|
| 6 |
+
mime_type text not null,
|
| 7 |
+
path text null,
|
| 8 |
+
size bigint null,
|
| 9 |
+
starred bool not null,
|
| 10 |
+
depth integer null,
|
| 11 |
+
user_id bigint not null,
|
| 12 |
+
parent_id text null,
|
| 13 |
+
status text default 'active'::text,
|
| 14 |
+
channel_id bigint null,
|
| 15 |
+
parts jsonb null,
|
| 16 |
+
created_at timestamp not null default timezone('utc'::text,
|
| 17 |
+
now()),
|
| 18 |
+
updated_at timestamp not null default timezone('utc'::text,
|
| 19 |
+
now()),
|
| 20 |
+
constraint unique_file unique (name,
|
| 21 |
+
parent_id,user_id)
|
| 22 |
+
);
|
| 23 |
+
|
| 24 |
+
create table teldrive.uploads (
|
| 25 |
+
id text not null primary key default teldrive.generate_uid(16),
|
| 26 |
+
upload_id text not null,
|
| 27 |
+
name text not null,
|
| 28 |
+
part_no int4 not null,
|
| 29 |
+
part_id int4 not null,
|
| 30 |
+
total_parts int4 not null,
|
| 31 |
+
channel_id int8 not null,
|
| 32 |
+
size int8 not null,
|
| 33 |
+
created_at timestamp null default timezone('utc'::text,
|
| 34 |
+
now())
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
create table teldrive.users (
|
| 38 |
+
user_id bigint not null primary key,
|
| 39 |
+
name text null,
|
| 40 |
+
user_name text null,
|
| 41 |
+
is_premium bool not null,
|
| 42 |
+
tg_session text not null,
|
| 43 |
+
settings jsonb null,
|
| 44 |
+
created_at timestamptz not null default timezone('utc'::text,
|
| 45 |
+
now()),
|
| 46 |
+
updated_at timestamptz not null default timezone('utc'::text,
|
| 47 |
+
now())
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
create collation if not exists numeric (provider = icu, locale = 'en@colnumeric=yes');
|
| 51 |
+
create index name_search_idx on
|
| 52 |
+
teldrive.files
|
| 53 |
+
using gin (teldrive.get_tsvector(name),
|
| 54 |
+
updated_at);
|
| 55 |
+
|
| 56 |
+
create index name_numeric_idx on
|
| 57 |
+
teldrive.files(name collate numeric nulls first);
|
| 58 |
+
|
| 59 |
+
create index parent_name_numeric_idx on
|
| 60 |
+
teldrive.files (parent_id,
|
| 61 |
+
name collate numeric desc);
|
| 62 |
+
|
| 63 |
+
create index path_idx on
|
| 64 |
+
teldrive.files (path);
|
| 65 |
+
|
| 66 |
+
create index parent_idx on
|
| 67 |
+
teldrive.files (parent_id);
|
| 68 |
+
|
| 69 |
+
create index starred_updated_at_idx on
|
| 70 |
+
teldrive.files (starred,
|
| 71 |
+
updated_at desc);
|
| 72 |
+
|
| 73 |
+
create index status_idx on teldrive.files (status);
|
| 74 |
+
|
| 75 |
+
create index user_id_idx on teldrive.files (user_id);
|
| 76 |
+
|
| 77 |
+
-- +goose Down
|
| 78 |
+
drop table if exists teldrive.files;
|
| 79 |
+
drop table if exists teldrive.uploads;
|
| 80 |
+
drop table if exists teldrive.users;
|
| 81 |
+
drop index if exists teldrive.name_search_idx;
|
| 82 |
+
drop index if exists teldrive.name_numeric_idx;
|
| 83 |
+
drop index if exists teldrive.parent_name_numeric_idx;
|
| 84 |
+
drop index if exists teldrive.path_idx;
|
| 85 |
+
drop index if exists teldrive.parent_idx;
|
| 86 |
+
drop index if exists teldrive.starred_updated_at_idx;
|
| 87 |
+
drop index if exists teldrive.status_idx;
|
| 88 |
+
drop index if exists teldrive.user_id_idx;
|
database/migrations/20230817172329_functions.sql
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- +goose Up
|
| 2 |
+
|
| 3 |
+
-- +goose StatementBegin
|
| 4 |
+
create procedure teldrive.update_size() language plpgsql as $$
|
| 5 |
+
declare rec record;
|
| 6 |
+
|
| 7 |
+
total_size bigint;
|
| 8 |
+
|
| 9 |
+
begin
|
| 10 |
+
|
| 11 |
+
for rec in
|
| 12 |
+
select
|
| 13 |
+
id
|
| 14 |
+
from
|
| 15 |
+
files
|
| 16 |
+
where
|
| 17 |
+
type = 'folder'
|
| 18 |
+
order by
|
| 19 |
+
depth desc loop total_size := (
|
| 20 |
+
select
|
| 21 |
+
sum(size) as total_size
|
| 22 |
+
from
|
| 23 |
+
teldrive.files
|
| 24 |
+
where
|
| 25 |
+
parent_id = rec.id
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
update
|
| 29 |
+
teldrive.files
|
| 30 |
+
set
|
| 31 |
+
size = total_size
|
| 32 |
+
where
|
| 33 |
+
id = rec.id;
|
| 34 |
+
end loop;
|
| 35 |
+
end;
|
| 36 |
+
|
| 37 |
+
$$;
|
| 38 |
+
-- +goose StatementEnd
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
-- +goose StatementBegin
|
| 42 |
+
create function teldrive.update_folder(folder_id text,
|
| 43 |
+
new_name text default null,
|
| 44 |
+
new_path text default null)
|
| 45 |
+
returns setof teldrive.files
|
| 46 |
+
language plpgsql
|
| 47 |
+
as $$
|
| 48 |
+
declare folder record;
|
| 49 |
+
|
| 50 |
+
path_items text [];
|
| 51 |
+
|
| 52 |
+
begin
|
| 53 |
+
|
| 54 |
+
if new_path is null then
|
| 55 |
+
select
|
| 56 |
+
*
|
| 57 |
+
into
|
| 58 |
+
folder
|
| 59 |
+
from
|
| 60 |
+
teldrive.files
|
| 61 |
+
where
|
| 62 |
+
id = folder_id;
|
| 63 |
+
|
| 64 |
+
path_items := string_to_array(folder.path,
|
| 65 |
+
'/');
|
| 66 |
+
|
| 67 |
+
path_items [array_length(path_items,
|
| 68 |
+
1)] := new_name;
|
| 69 |
+
|
| 70 |
+
new_path := array_to_string(path_items,
|
| 71 |
+
'/');
|
| 72 |
+
end if;
|
| 73 |
+
|
| 74 |
+
update
|
| 75 |
+
teldrive.files
|
| 76 |
+
set
|
| 77 |
+
path = new_path,
|
| 78 |
+
name = new_name
|
| 79 |
+
where
|
| 80 |
+
id = folder_id;
|
| 81 |
+
|
| 82 |
+
for folder in
|
| 83 |
+
select
|
| 84 |
+
*
|
| 85 |
+
from
|
| 86 |
+
teldrive.files
|
| 87 |
+
where
|
| 88 |
+
type = 'folder'
|
| 89 |
+
and parent_id = folder_id loop call teldrive.update_folder(
|
| 90 |
+
folder.id,
|
| 91 |
+
folder.name,
|
| 92 |
+
concat(new_path,
|
| 93 |
+
'/',
|
| 94 |
+
folder.name)
|
| 95 |
+
);
|
| 96 |
+
end loop;
|
| 97 |
+
|
| 98 |
+
return query
|
| 99 |
+
select
|
| 100 |
+
*
|
| 101 |
+
from
|
| 102 |
+
teldrive.files
|
| 103 |
+
where
|
| 104 |
+
id = folder_id;
|
| 105 |
+
end;
|
| 106 |
+
|
| 107 |
+
$$
|
| 108 |
+
;
|
| 109 |
+
-- +goose StatementEnd
|
| 110 |
+
|
| 111 |
+
-- +goose StatementBegin
|
| 112 |
+
create procedure teldrive.delete_files(in file_ids text[],
|
| 113 |
+
in op text default 'bulk')
|
| 114 |
+
language plpgsql
|
| 115 |
+
as $$
|
| 116 |
+
declare
|
| 117 |
+
rec record;
|
| 118 |
+
|
| 119 |
+
begin
|
| 120 |
+
if op = 'bulk' then
|
| 121 |
+
for rec in
|
| 122 |
+
select
|
| 123 |
+
id,
|
| 124 |
+
type
|
| 125 |
+
from
|
| 126 |
+
teldrive.files
|
| 127 |
+
where
|
| 128 |
+
id = any (file_ids)
|
| 129 |
+
loop
|
| 130 |
+
if rec.type = 'folder' then
|
| 131 |
+
call teldrive.delete_files(array [rec.id],
|
| 132 |
+
'single');
|
| 133 |
+
|
| 134 |
+
delete
|
| 135 |
+
from
|
| 136 |
+
teldrive.files
|
| 137 |
+
where
|
| 138 |
+
id = rec.id;
|
| 139 |
+
else
|
| 140 |
+
update
|
| 141 |
+
teldrive.files
|
| 142 |
+
set
|
| 143 |
+
status = 'pending_deletion'
|
| 144 |
+
where
|
| 145 |
+
id = rec.id;
|
| 146 |
+
end if;
|
| 147 |
+
end loop;
|
| 148 |
+
else
|
| 149 |
+
|
| 150 |
+
for rec in
|
| 151 |
+
select
|
| 152 |
+
id,
|
| 153 |
+
type
|
| 154 |
+
from
|
| 155 |
+
teldrive.files
|
| 156 |
+
where
|
| 157 |
+
parent_id = file_ids[1]
|
| 158 |
+
loop
|
| 159 |
+
if rec.type = 'folder' then
|
| 160 |
+
call teldrive.delete_files(array [rec.id],
|
| 161 |
+
'single');
|
| 162 |
+
|
| 163 |
+
delete
|
| 164 |
+
from
|
| 165 |
+
teldrive.files
|
| 166 |
+
where
|
| 167 |
+
id = rec.id;
|
| 168 |
+
else
|
| 169 |
+
update
|
| 170 |
+
teldrive.files
|
| 171 |
+
set
|
| 172 |
+
status = 'pending_deletion'
|
| 173 |
+
where
|
| 174 |
+
id = rec.id;
|
| 175 |
+
end if;
|
| 176 |
+
end loop;
|
| 177 |
+
end if;
|
| 178 |
+
end;
|
| 179 |
+
|
| 180 |
+
$$
|
| 181 |
+
;
|
| 182 |
+
-- +goose StatementEnd
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
-- +goose Down
|
| 186 |
+
drop procedure if exists teldrive.update_size;
|
| 187 |
+
drop function if exists teldrive.update_folder;
|
| 188 |
+
drop procedure if exists teldrive.delete_files;
|
database/migrations/20230820014560_create_dirs_from_path.sql
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- +goose Up
|
| 2 |
+
-- +goose StatementBegin
|
| 3 |
+
|
| 4 |
+
CREATE OR REPLACE FUNCTION teldrive.create_directories(
|
| 5 |
+
IN tg_id BIGINT,
|
| 6 |
+
IN long_path TEXT
|
| 7 |
+
) RETURNS SETOF teldrive.files AS $$
|
| 8 |
+
DECLARE
|
| 9 |
+
path_parts TEXT[];
|
| 10 |
+
current_directory_id TEXT;
|
| 11 |
+
new_directory_id TEXT;
|
| 12 |
+
directory_name TEXT;
|
| 13 |
+
path_so_far TEXT;
|
| 14 |
+
depth_dir integer;
|
| 15 |
+
begin
|
| 16 |
+
|
| 17 |
+
path_parts := string_to_array(regexp_replace(long_path, '^/+', ''), '/');
|
| 18 |
+
|
| 19 |
+
path_so_far := '';
|
| 20 |
+
depth_dir := 0;
|
| 21 |
+
|
| 22 |
+
SELECT id into current_directory_id FROM teldrive.files WHERE parent_id='root';
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
FOR directory_name IN SELECT unnest(path_parts) LOOP
|
| 26 |
+
path_so_far := CONCAT(path_so_far,'/', directory_name);
|
| 27 |
+
depth_dir := depth_dir +1;
|
| 28 |
+
SELECT id INTO new_directory_id
|
| 29 |
+
FROM teldrive.files
|
| 30 |
+
WHERE parent_id = current_directory_id
|
| 31 |
+
AND "name" = directory_name AND "user_id"=tg_id;
|
| 32 |
+
|
| 33 |
+
IF new_directory_id IS NULL THEN
|
| 34 |
+
INSERT INTO teldrive.files ("name", "type", mime_type, parent_id, "user_id",starred,"depth","path")
|
| 35 |
+
VALUES (directory_name, 'folder', 'drive/folder', current_directory_id, tg_id,false,depth_dir,path_so_far)
|
| 36 |
+
RETURNING id INTO new_directory_id;
|
| 37 |
+
END IF;
|
| 38 |
+
|
| 39 |
+
current_directory_id := new_directory_id;
|
| 40 |
+
END LOOP;
|
| 41 |
+
|
| 42 |
+
RETURN QUERY SELECT * FROM teldrive.files WHERE id = current_directory_id;
|
| 43 |
+
END;
|
| 44 |
+
$$ LANGUAGE plpgsql;
|
| 45 |
+
-- +goose StatementEnd
|
| 46 |
+
|
| 47 |
+
-- +goose Down
|
| 48 |
+
DROP FUNCTION IF EXISTS teldrive.create_directories;
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.8"
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
server:
|
| 5 |
+
image: ghcr.io/divyam234/teldrive/server:latest
|
| 6 |
+
restart: always
|
| 7 |
+
container_name: server
|
| 8 |
+
volumes:
|
| 9 |
+
- ./sessions:/app/sessions:rw
|
| 10 |
+
- ./database:/app/database
|
| 11 |
+
env_file: .env
|
| 12 |
+
ports:
|
| 13 |
+
- 8080
|
| 14 |
+
|
| 15 |
+
client:
|
| 16 |
+
image: ghcr.io/divyam234/teldrive/client:latest
|
| 17 |
+
restart: always
|
| 18 |
+
container_name: client
|
| 19 |
+
ports:
|
| 20 |
+
- 3000
|
| 21 |
+
|
| 22 |
+
haproxy:
|
| 23 |
+
image: haproxy:latest
|
| 24 |
+
volumes:
|
| 25 |
+
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
|
| 26 |
+
# ports:
|
| 27 |
+
# - 80:80
|
| 28 |
+
# - 443:443
|
| 29 |
+
ports:
|
| 30 |
+
- 8000:8000
|
| 31 |
+
depends_on:
|
| 32 |
+
- server
|
| 33 |
+
- client
|
go.mod
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module github.com/divyam234/teldrive
|
| 2 |
+
|
| 3 |
+
go 1.21
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
github.com/allegro/bigcache/v3 v3.1.0
|
| 7 |
+
github.com/divyam234/cors v1.4.2
|
| 8 |
+
github.com/gin-gonic/gin v1.9.1
|
| 9 |
+
github.com/go-co-op/gocron v1.31.1
|
| 10 |
+
github.com/go-jose/go-jose/v3 v3.0.0
|
| 11 |
+
github.com/gotd/contrib v0.19.0
|
| 12 |
+
github.com/gotd/td v0.84.0
|
| 13 |
+
github.com/joho/godotenv v1.5.1
|
| 14 |
+
github.com/kelseyhightower/envconfig v1.4.0
|
| 15 |
+
github.com/mitchellh/mapstructure v1.5.0
|
| 16 |
+
github.com/pkg/errors v0.9.1
|
| 17 |
+
github.com/quantumsheep/range-parser v1.1.0
|
| 18 |
+
go.uber.org/zap v1.25.0
|
| 19 |
+
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
| 20 |
+
golang.org/x/time v0.3.0
|
| 21 |
+
gorm.io/driver/postgres v1.5.2
|
| 22 |
+
gorm.io/gorm v1.25.2
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
require github.com/robfig/cron/v3 v3.0.1 // indirect
|
| 26 |
+
|
| 27 |
+
require (
|
| 28 |
+
github.com/bytedance/sonic v1.9.1 // indirect
|
| 29 |
+
github.com/cenkalti/backoff/v4 v4.2.1
|
| 30 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
| 31 |
+
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
| 32 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
| 33 |
+
github.com/go-faster/errors v0.6.1 // indirect
|
| 34 |
+
github.com/go-faster/jx v1.0.1 // indirect
|
| 35 |
+
github.com/go-faster/xor v1.0.0 // indirect
|
| 36 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
| 37 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 38 |
+
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
| 39 |
+
github.com/goccy/go-json v0.10.2 // indirect
|
| 40 |
+
github.com/gorilla/websocket v1.5.0
|
| 41 |
+
github.com/gotd/ige v0.2.2 // indirect
|
| 42 |
+
github.com/gotd/neo v0.1.5 // indirect
|
| 43 |
+
github.com/jackc/pgpassfile v1.0.0 // indirect
|
| 44 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
| 45 |
+
github.com/jackc/pgx/v5 v5.4.3
|
| 46 |
+
github.com/jinzhu/inflection v1.0.0 // indirect
|
| 47 |
+
github.com/jinzhu/now v1.1.5 // indirect
|
| 48 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 49 |
+
github.com/klauspost/compress v1.16.7 // indirect
|
| 50 |
+
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
| 51 |
+
github.com/leodido/go-urn v1.2.4 // indirect
|
| 52 |
+
github.com/mattn/go-isatty v0.0.19 // indirect
|
| 53 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 54 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 55 |
+
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
| 56 |
+
github.com/pressly/goose/v3 v3.15.0
|
| 57 |
+
github.com/segmentio/asm v1.2.0 // indirect
|
| 58 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 59 |
+
github.com/ugorji/go/codec v1.2.11 // indirect
|
| 60 |
+
go.opentelemetry.io/otel v1.16.0 // indirect
|
| 61 |
+
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
| 62 |
+
go.uber.org/atomic v1.11.0 // indirect
|
| 63 |
+
go.uber.org/multierr v1.11.0 // indirect
|
| 64 |
+
golang.org/x/arch v0.3.0 // indirect
|
| 65 |
+
golang.org/x/crypto v0.11.0 // indirect
|
| 66 |
+
golang.org/x/net v0.12.0 // indirect
|
| 67 |
+
golang.org/x/sync v0.3.0 // indirect
|
| 68 |
+
golang.org/x/sys v0.10.0 // indirect
|
| 69 |
+
golang.org/x/text v0.11.0 // indirect
|
| 70 |
+
google.golang.org/protobuf v1.30.0 // indirect
|
| 71 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 72 |
+
nhooyr.io/websocket v1.8.7 // indirect
|
| 73 |
+
rsc.io/qr v0.2.0 // indirect
|
| 74 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
|
| 2 |
+
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
| 3 |
+
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
| 4 |
+
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
| 5 |
+
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
| 6 |
+
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
| 7 |
+
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
| 8 |
+
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
| 9 |
+
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
| 10 |
+
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
| 11 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
| 12 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
| 13 |
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
| 14 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 15 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 16 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 17 |
+
github.com/divyam234/cors v1.4.2 h1:moAxStmYpvG9/SkPz+Wld02iutgo3JcUvrez6Kit/D8=
|
| 18 |
+
github.com/divyam234/cors v1.4.2/go.mod h1:JrxBJAqTU7jtPItodwf2mzxbbZm0Qq0NFkK8jo9UUDk=
|
| 19 |
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
| 20 |
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
| 21 |
+
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
| 22 |
+
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
| 23 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
| 24 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
| 25 |
+
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
| 26 |
+
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
| 27 |
+
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
| 28 |
+
github.com/go-co-op/gocron v1.31.1 h1:LZAuBlU0t3SPGUMJGhrJ6VuCc3CsrYzkzicygvVWlfA=
|
| 29 |
+
github.com/go-co-op/gocron v1.31.1/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y=
|
| 30 |
+
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
| 31 |
+
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
| 32 |
+
github.com/go-faster/jx v1.0.1 h1:NhSJEZtqj6KmXf63On7Hg7/sjUX+gotSc/eM6bZCZ00=
|
| 33 |
+
github.com/go-faster/jx v1.0.1/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
| 34 |
+
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
| 35 |
+
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
|
| 36 |
+
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
| 37 |
+
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
| 38 |
+
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
| 39 |
+
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 40 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
| 41 |
+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 42 |
+
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
| 43 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 44 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 45 |
+
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
| 46 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 47 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 48 |
+
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
| 49 |
+
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
| 50 |
+
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
| 51 |
+
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
| 52 |
+
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
| 53 |
+
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
| 54 |
+
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
| 55 |
+
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
| 56 |
+
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
| 57 |
+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
| 58 |
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
| 59 |
+
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
| 60 |
+
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
| 61 |
+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
| 62 |
+
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
| 63 |
+
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
| 64 |
+
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 65 |
+
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 66 |
+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 67 |
+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
| 68 |
+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
| 69 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 70 |
+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
| 71 |
+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 72 |
+
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
| 73 |
+
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
| 74 |
+
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
| 75 |
+
github.com/gotd/contrib v0.19.0 h1:O6GvMrRVeFslIHLUcpaHVzcl9/5PcgR2jQTIIeTyds0=
|
| 76 |
+
github.com/gotd/contrib v0.19.0/go.mod h1:LzPxzRF0FvtpBt/WyODWQnPpk0tm/G9z6RHUoPqMakU=
|
| 77 |
+
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
| 78 |
+
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
| 79 |
+
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
| 80 |
+
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
| 81 |
+
github.com/gotd/td v0.84.0 h1:oWMp5HczCAFSgKWgWFCuYjELBgcRVcRpGLdQ1bP2kpg=
|
| 82 |
+
github.com/gotd/td v0.84.0/go.mod h1:3dQsGL9rxMcS1Z9Na3S7U8e/pLMzbLIT2jM3E5IuUk0=
|
| 83 |
+
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
| 84 |
+
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
| 85 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
| 86 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
| 87 |
+
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
| 88 |
+
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
| 89 |
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
| 90 |
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
| 91 |
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
| 92 |
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 93 |
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
| 94 |
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
| 95 |
+
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
| 96 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 97 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 98 |
+
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
| 99 |
+
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
| 100 |
+
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
| 101 |
+
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
| 102 |
+
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
| 103 |
+
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
| 104 |
+
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
| 105 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 106 |
+
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
| 107 |
+
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
| 108 |
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
| 109 |
+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
| 110 |
+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
| 111 |
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
| 112 |
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
| 113 |
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
| 114 |
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
| 115 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 116 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 117 |
+
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
| 118 |
+
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
| 119 |
+
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
| 120 |
+
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
| 121 |
+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
| 122 |
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 123 |
+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
| 124 |
+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
| 125 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 126 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 127 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 128 |
+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
| 129 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 130 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 131 |
+
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
| 132 |
+
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
| 133 |
+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
| 134 |
+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
| 135 |
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 136 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 137 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 138 |
+
github.com/pressly/goose/v3 v3.15.0 h1:6tY5aDqFknY6VZkorFGgZtWygodZQxfmmEF4rqyJW9k=
|
| 139 |
+
github.com/pressly/goose/v3 v3.15.0/go.mod h1:LlIo3zGccjb/YUgG+Svdb9Er14vefRdlDI7URCDrwYo=
|
| 140 |
+
github.com/quantumsheep/range-parser v1.1.0 h1:k4f1F58f8FF54FBYc9dYBRM+8JkAxFo11gC3IeMH4rU=
|
| 141 |
+
github.com/quantumsheep/range-parser v1.1.0/go.mod h1:acv4Vt2PvpGvRsvGju7Gk2ahKluZJsIUNR69W53J22I=
|
| 142 |
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
| 143 |
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
| 144 |
+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
| 145 |
+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
| 146 |
+
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
| 147 |
+
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
| 148 |
+
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
| 149 |
+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
| 150 |
+
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
| 151 |
+
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
| 152 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 153 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 154 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 155 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 156 |
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
| 157 |
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 158 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 159 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 160 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 161 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 162 |
+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 163 |
+
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 164 |
+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
| 165 |
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 166 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 167 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 168 |
+
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
| 169 |
+
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
| 170 |
+
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
| 171 |
+
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
| 172 |
+
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
| 173 |
+
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
| 174 |
+
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
| 175 |
+
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
| 176 |
+
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
| 177 |
+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
| 178 |
+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
| 179 |
+
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
| 180 |
+
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
| 181 |
+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
| 182 |
+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
| 183 |
+
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
|
| 184 |
+
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
|
| 185 |
+
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 186 |
+
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
| 187 |
+
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 188 |
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
| 189 |
+
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
| 190 |
+
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
| 191 |
+
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
| 192 |
+
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
|
| 193 |
+
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
| 194 |
+
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
| 195 |
+
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
| 196 |
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
| 197 |
+
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
| 198 |
+
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
| 199 |
+
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 200 |
+
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
| 201 |
+
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
| 202 |
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 203 |
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 204 |
+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 205 |
+
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 206 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 207 |
+
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
| 208 |
+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 209 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
| 210 |
+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
| 211 |
+
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
| 212 |
+
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
| 213 |
+
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
| 214 |
+
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
| 215 |
+
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
| 216 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 217 |
+
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
|
| 218 |
+
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
| 219 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 220 |
+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
| 221 |
+
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
| 222 |
+
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
| 223 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 224 |
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 225 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 226 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 227 |
+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
| 228 |
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 229 |
+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 230 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 231 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 232 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 233 |
+
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
| 234 |
+
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
| 235 |
+
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
| 236 |
+
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
| 237 |
+
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
| 238 |
+
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
| 239 |
+
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
| 240 |
+
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
| 241 |
+
modernc.org/ccgo/v3 v3.16.14 h1:af6KNtFgsVmnDYrWk3PQCS9XT6BXe7o3ZFJKkIKvXNQ=
|
| 242 |
+
modernc.org/ccgo/v3 v3.16.14/go.mod h1:mPDSujUIaTNWQSG4eqKw+atqLOEbma6Ncsa94WbC9zo=
|
| 243 |
+
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
| 244 |
+
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
| 245 |
+
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
| 246 |
+
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
| 247 |
+
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
|
| 248 |
+
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
| 249 |
+
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
| 250 |
+
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
| 251 |
+
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
|
| 252 |
+
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
| 253 |
+
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
| 254 |
+
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
| 255 |
+
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
| 256 |
+
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
| 257 |
+
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
| 258 |
+
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
| 259 |
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
| 260 |
+
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
| 261 |
+
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
haproxy.cfg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# global
|
| 2 |
+
# tune.h2.initial-window-size 1048576
|
| 3 |
+
# tune.h2.max-concurrent-streams 1000
|
| 4 |
+
|
| 5 |
+
defaults
|
| 6 |
+
mode http
|
| 7 |
+
log global
|
| 8 |
+
|
| 9 |
+
frontend http-in
|
| 10 |
+
# bind *:80
|
| 11 |
+
# bind *:443 ssl crt /etc/ssl/fullchain.pem alpn h2,http/1.1
|
| 12 |
+
bind *:8000
|
| 13 |
+
mode http
|
| 14 |
+
acl is_api path_beg /api
|
| 15 |
+
use_backend api-backend if is_api
|
| 16 |
+
default_backend frontend
|
| 17 |
+
|
| 18 |
+
backend api-backend
|
| 19 |
+
balance roundrobin
|
| 20 |
+
server go-backend server:8080 check
|
| 21 |
+
|
| 22 |
+
backend frontend
|
| 23 |
+
balance roundrobin
|
| 24 |
+
server nextjs-frontend client:3000 check
|
main.go
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"path/filepath"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"github.com/divyam234/teldrive/cache"
|
| 9 |
+
"github.com/divyam234/teldrive/database"
|
| 10 |
+
"github.com/divyam234/teldrive/routes"
|
| 11 |
+
"github.com/divyam234/teldrive/utils"
|
| 12 |
+
|
| 13 |
+
"github.com/divyam234/cors"
|
| 14 |
+
"github.com/divyam234/teldrive/utils/cron"
|
| 15 |
+
"github.com/gin-gonic/gin"
|
| 16 |
+
"github.com/go-co-op/gocron"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
func main() {
|
| 20 |
+
|
| 21 |
+
gin.SetMode(gin.ReleaseMode)
|
| 22 |
+
|
| 23 |
+
router := gin.Default()
|
| 24 |
+
|
| 25 |
+
utils.InitConfig()
|
| 26 |
+
|
| 27 |
+
utils.InitializeLogger()
|
| 28 |
+
|
| 29 |
+
database.InitDB()
|
| 30 |
+
|
| 31 |
+
cache.CacheInit()
|
| 32 |
+
|
| 33 |
+
utils.InitBotClients()
|
| 34 |
+
|
| 35 |
+
cron.FilesDeleteJob()
|
| 36 |
+
|
| 37 |
+
scheduler := gocron.NewScheduler(time.UTC)
|
| 38 |
+
|
| 39 |
+
scheduler.Every(1).Hours().Do(cron.FilesDeleteJob)
|
| 40 |
+
|
| 41 |
+
router.Use(cors.New(cors.Config{
|
| 42 |
+
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
|
| 43 |
+
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"},
|
| 44 |
+
AllowCredentials: true,
|
| 45 |
+
AllowOriginFunc: func(origin string) bool {
|
| 46 |
+
return true
|
| 47 |
+
},
|
| 48 |
+
MaxAge: 12 * time.Hour,
|
| 49 |
+
}))
|
| 50 |
+
|
| 51 |
+
router.Use(gin.ErrorLogger())
|
| 52 |
+
|
| 53 |
+
routes.GetRoutes(router)
|
| 54 |
+
|
| 55 |
+
config := utils.GetConfig()
|
| 56 |
+
certDir := filepath.Join(config.ExecDir, "sslcerts")
|
| 57 |
+
ok, _ := utils.PathExists(certDir)
|
| 58 |
+
if ok && config.Https {
|
| 59 |
+
router.RunTLS(fmt.Sprintf(":%d", config.Port), filepath.Join(certDir, "cert.pem"), filepath.Join(certDir, "key.pem"))
|
| 60 |
+
} else {
|
| 61 |
+
router.Run(fmt.Sprintf(":%d", config.Port))
|
| 62 |
+
}
|
| 63 |
+
scheduler.StartAsync()
|
| 64 |
+
}
|
models/file.model.go
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"database/sql/driver"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"time"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type File struct {
|
| 10 |
+
ID string `gorm:"type:text;primaryKey;default:generate_uid(16)"`
|
| 11 |
+
Name string `gorm:"type:text;not null"`
|
| 12 |
+
Type string `gorm:"type:text;not null"`
|
| 13 |
+
MimeType string `gorm:"type:text;not null"`
|
| 14 |
+
Path string `gorm:"type:text;index"`
|
| 15 |
+
Size int64 `gorm:"type:bigint"`
|
| 16 |
+
Starred *bool `gorm:"default:false"`
|
| 17 |
+
Depth *int `gorm:"type:integer"`
|
| 18 |
+
UserID int64 `gorm:"type:bigint;not null"`
|
| 19 |
+
Status string `gorm:"type:text"`
|
| 20 |
+
ParentID string `gorm:"type:text;index"`
|
| 21 |
+
Parts *Parts `gorm:"type:jsonb"`
|
| 22 |
+
ChannelID *int64 `gorm:"type:bigint"`
|
| 23 |
+
CreatedAt time.Time `gorm:"default:timezone('utc'::text, now())"`
|
| 24 |
+
UpdatedAt time.Time `gorm:"default:timezone('utc'::text, now())"`
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
type Parts []Part
|
| 28 |
+
type Part struct {
|
| 29 |
+
ID int64 `json:"id"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func (a Parts) Value() (driver.Value, error) {
|
| 33 |
+
return json.Marshal(a)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func (a *Parts) Scan(value interface{}) error {
|
| 37 |
+
if err := json.Unmarshal(value.([]byte), &a); err != nil {
|
| 38 |
+
return err
|
| 39 |
+
}
|
| 40 |
+
return nil
|
| 41 |
+
}
|
models/upload.model.go
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
type Upload struct {
|
| 8 |
+
ID string `gorm:"type:text;primary_key;default:generate_uid(16)"`
|
| 9 |
+
UploadId string `gorm:"type:text"`
|
| 10 |
+
Name string `gorm:"type:text"`
|
| 11 |
+
PartNo int `gorm:"type:integer"`
|
| 12 |
+
TotalParts int `gorm:"type:integer"`
|
| 13 |
+
PartId int `gorm:"type:integer"`
|
| 14 |
+
ChannelID int64 `gorm:"type:bigint"`
|
| 15 |
+
Size int64 `gorm:"type:bigint"`
|
| 16 |
+
CreatedAt time.Time `gorm:"default:timezone('utc'::text, now())"`
|
| 17 |
+
}
|
models/user.model.go
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
type User struct {
|
| 8 |
+
UserId int64 `gorm:"type:bigint;primaryKey"`
|
| 9 |
+
Name string `gorm:"type:text"`
|
| 10 |
+
UserName string `gorm:"type:text"`
|
| 11 |
+
IsPremium bool `gorm:"type:bool"`
|
| 12 |
+
TgSession string `gorm:"type:text"`
|
| 13 |
+
UpdatedAt time.Time `gorm:"default:timezone('utc'::text, now())"`
|
| 14 |
+
CreatedAt time.Time `gorm:"default:timezone('utc'::text, now())"`
|
| 15 |
+
}
|
public/demo.png
ADDED
|
routes/auth.go
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
"github.com/divyam234/teldrive/database"
|
| 7 |
+
"github.com/divyam234/teldrive/services"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func addAuthRoutes(rg *gin.RouterGroup) {
|
| 13 |
+
|
| 14 |
+
r := rg.Group("/auth")
|
| 15 |
+
|
| 16 |
+
authService := services.AuthService{Db: database.DB, SessionMaxAge: 30 * 24 * 60 * 60, SessionCookieName: "user-session"}
|
| 17 |
+
|
| 18 |
+
r.POST("/login", func(c *gin.Context) {
|
| 19 |
+
|
| 20 |
+
res, err := authService.LogIn(c)
|
| 21 |
+
|
| 22 |
+
if err != nil {
|
| 23 |
+
c.AbortWithError(err.Code, err.Error)
|
| 24 |
+
return
|
| 25 |
+
}
|
| 26 |
+
c.JSON(http.StatusOK, res)
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
r.GET("/logout", Authmiddleware, func(c *gin.Context) {
|
| 30 |
+
|
| 31 |
+
res, err := authService.Logout(c)
|
| 32 |
+
|
| 33 |
+
if err != nil {
|
| 34 |
+
c.AbortWithError(err.Code, err.Error)
|
| 35 |
+
return
|
| 36 |
+
}
|
| 37 |
+
c.JSON(http.StatusOK, res)
|
| 38 |
+
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
r.GET("/ws", authService.HandleMultipleLogin)
|
| 42 |
+
|
| 43 |
+
r.GET("/session", func(c *gin.Context) {
|
| 44 |
+
|
| 45 |
+
session := authService.GetSession(c)
|
| 46 |
+
|
| 47 |
+
c.JSON(http.StatusOK, session)
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
}
|
routes/file.go
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
"github.com/divyam234/teldrive/database"
|
| 7 |
+
"github.com/divyam234/teldrive/services"
|
| 8 |
+
"github.com/divyam234/teldrive/utils"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func addFileRoutes(rg *gin.RouterGroup) {
|
| 14 |
+
|
| 15 |
+
r := rg.Group("/files")
|
| 16 |
+
r.Use(Authmiddleware)
|
| 17 |
+
fileService := services.FileService{Db: database.DB, ChannelID: utils.GetConfig().ChannelID}
|
| 18 |
+
|
| 19 |
+
r.GET("", func(c *gin.Context) {
|
| 20 |
+
res, err := fileService.ListFiles(c)
|
| 21 |
+
|
| 22 |
+
if err != nil {
|
| 23 |
+
c.AbortWithError(err.Code, err.Error)
|
| 24 |
+
return
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
c.JSON(http.StatusOK, res)
|
| 28 |
+
})
|
| 29 |
+
|
| 30 |
+
r.POST("", func(c *gin.Context) {
|
| 31 |
+
|
| 32 |
+
res, err := fileService.CreateFile(c)
|
| 33 |
+
|
| 34 |
+
if err != nil {
|
| 35 |
+
c.AbortWithError(err.Code, err.Error)
|
| 36 |
+
return
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
c.JSON(http.StatusOK, res)
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
r.GET("/:fileID", func(c *gin.Context) {
|
| 43 |
+
|
| 44 |
+
res, err := fileService.GetFileByID(c)
|
| 45 |
+
|
| 46 |
+
if err != nil {
|
| 47 |
+
c.AbortWithError(http.StatusNotFound, err)
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
c.JSON(http.StatusOK, res)
|
| 52 |
+
})
|
| 53 |
+
|
| 54 |
+
r.PATCH("/:fileID", func(c *gin.Context) {
|
| 55 |
+
|
| 56 |
+
res, err := fileService.UpdateFile(c)
|
| 57 |
+
|
| 58 |
+
if err != nil {
|
| 59 |
+
c.AbortWithError(err.Code, err.Error)
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
c.JSON(http.StatusOK, res)
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
r.GET("/:fileID/:fileName", func(c *gin.Context) {
|
| 67 |
+
|
| 68 |
+
fileService.GetFileStream(c)
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
r.POST("/movefiles", func(c *gin.Context) {
|
| 72 |
+
|
| 73 |
+
res, err := fileService.MoveFiles(c)
|
| 74 |
+
|
| 75 |
+
if err != nil {
|
| 76 |
+
c.AbortWithError(err.Code, err.Error)
|
| 77 |
+
return
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
c.JSON(http.StatusOK, res)
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
r.POST("/makekdir", func(c *gin.Context) {
|
| 84 |
+
|
| 85 |
+
res, err := fileService.MakeDirectory(c)
|
| 86 |
+
|
| 87 |
+
if err != nil {
|
| 88 |
+
c.AbortWithError(err.Code, err.Error)
|
| 89 |
+
return
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
c.JSON(http.StatusOK, res)
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
r.POST("/deletefiles", func(c *gin.Context) {
|
| 96 |
+
|
| 97 |
+
res, err := fileService.DeleteFiles(c)
|
| 98 |
+
|
| 99 |
+
if err != nil {
|
| 100 |
+
c.AbortWithError(err.Code, err.Error)
|
| 101 |
+
return
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
c.JSON(http.StatusOK, res)
|
| 105 |
+
})
|
| 106 |
+
|
| 107 |
+
}
|
routes/main.go
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import "github.com/gin-gonic/gin"
|
| 4 |
+
|
| 5 |
+
func GetRoutes(router *gin.Engine) {
|
| 6 |
+
api := router.Group("/api")
|
| 7 |
+
addAuthRoutes(api)
|
| 8 |
+
addFileRoutes(api)
|
| 9 |
+
addUploadRoutes(api)
|
| 10 |
+
addUserRoutes(api)
|
| 11 |
+
}
|
routes/middleware.go
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"time"
|
| 6 |
+
|
| 7 |
+
"github.com/divyam234/teldrive/utils"
|
| 8 |
+
"github.com/divyam234/teldrive/utils/auth"
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"github.com/go-jose/go-jose/v3/jwt"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func Authmiddleware(c *gin.Context) {
|
| 14 |
+
|
| 15 |
+
if c.FullPath() == "/api/files/:fileID/:fileName" && utils.GetConfig().MultiClient {
|
| 16 |
+
c.Next()
|
| 17 |
+
}
|
| 18 |
+
cookie, err := c.Request.Cookie("user-session")
|
| 19 |
+
|
| 20 |
+
if err != nil {
|
| 21 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing session cookie"})
|
| 22 |
+
c.Abort()
|
| 23 |
+
return
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
now := time.Now().UTC()
|
| 27 |
+
|
| 28 |
+
jwePayload, err := auth.Decode(cookie.Value)
|
| 29 |
+
|
| 30 |
+
if err != nil {
|
| 31 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
| 32 |
+
c.Abort()
|
| 33 |
+
return
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
if *jwePayload.Expiry < *jwt.NewNumericDate(now) {
|
| 37 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "token expired"})
|
| 38 |
+
c.Abort()
|
| 39 |
+
return
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
c.Set("jwtUser", jwePayload)
|
| 43 |
+
|
| 44 |
+
c.Next()
|
| 45 |
+
|
| 46 |
+
}
|
routes/upload.go
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
"github.com/divyam234/teldrive/database"
|
| 7 |
+
"github.com/divyam234/teldrive/services"
|
| 8 |
+
"github.com/divyam234/teldrive/utils"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func addUploadRoutes(rg *gin.RouterGroup) {
|
| 14 |
+
|
| 15 |
+
r := rg.Group("/uploads")
|
| 16 |
+
r.Use(Authmiddleware)
|
| 17 |
+
|
| 18 |
+
uploadService := services.UploadService{Db: database.DB, ChannelID: utils.GetConfig().ChannelID}
|
| 19 |
+
|
| 20 |
+
r.GET("/:id", func(c *gin.Context) {
|
| 21 |
+
|
| 22 |
+
res, err := uploadService.GetUploadFileById(c)
|
| 23 |
+
|
| 24 |
+
if err != nil {
|
| 25 |
+
c.AbortWithError(http.StatusNotFound, err.Error)
|
| 26 |
+
return
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
c.JSON(http.StatusOK, res)
|
| 30 |
+
})
|
| 31 |
+
|
| 32 |
+
r.POST("/:id", func(c *gin.Context) {
|
| 33 |
+
|
| 34 |
+
res, err := uploadService.UploadFile(c)
|
| 35 |
+
|
| 36 |
+
if err != nil {
|
| 37 |
+
c.AbortWithError(err.Code, err.Error)
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
c.JSON(http.StatusOK, res)
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
r.DELETE("/:id", func(c *gin.Context) {
|
| 45 |
+
err := uploadService.DeleteUploadFile(c)
|
| 46 |
+
|
| 47 |
+
if err != nil {
|
| 48 |
+
c.AbortWithError(err.Code, err.Error)
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
c.JSON(http.StatusOK, gin.H{"message": "upload deleted"})
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
}
|
routes/user.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/divyam234/teldrive/services"
|
| 5 |
+
|
| 6 |
+
"github.com/gin-gonic/gin"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func addUserRoutes(rg *gin.RouterGroup) {
|
| 10 |
+
r := rg.Group("/users")
|
| 11 |
+
r.Use(Authmiddleware)
|
| 12 |
+
userService := services.UserService{}
|
| 13 |
+
|
| 14 |
+
r.GET("/profile", func(c *gin.Context) {
|
| 15 |
+
if c.Query("photo") != "" {
|
| 16 |
+
userService.GetProfilePhoto(c)
|
| 17 |
+
}
|
| 18 |
+
})
|
| 19 |
+
}
|
schemas/common.go
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package schemas
|
| 2 |
+
|
| 3 |
+
type Message struct {
|
| 4 |
+
Status bool `json:"status"`
|
| 5 |
+
Message string `json:"message,omitempty"`
|
| 6 |
+
}
|
schemas/file.schema.go
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package schemas
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/divyam234/teldrive/models"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type PaginationQuery struct {
|
| 10 |
+
PerPage int `form:"perPage"`
|
| 11 |
+
NextPageToken string `form:"nextPageToken"`
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
type SortingQuery struct {
|
| 15 |
+
Sort string `form:"sort"`
|
| 16 |
+
Order string `form:"order"`
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type FileQuery struct {
|
| 20 |
+
Name string `form:"name" mapstructure:"name,omitempty"`
|
| 21 |
+
Search string `form:"search" mapstructure:"search,omitempty"`
|
| 22 |
+
Type string `form:"type" mapstructure:"type,omitempty"`
|
| 23 |
+
Path string `form:"path" mapstructure:"path,omitempty"`
|
| 24 |
+
Op string `form:"op" mapstructure:"op,omitempty"`
|
| 25 |
+
Starred *bool `form:"starred" mapstructure:"starred,omitempty"`
|
| 26 |
+
MimeType string `form:"mimeType" mapstructure:"mime_type,omitempty"`
|
| 27 |
+
ParentID string `form:"parentId" mapstructure:"parent_id,omitempty"`
|
| 28 |
+
UpdatedAt *time.Time `form:"updatedAt" mapstructure:"updated_at,omitempty"`
|
| 29 |
+
Status string `mapstructure:"status"`
|
| 30 |
+
UserID int64 `mapstructure:"user_id"`
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
type FileIn struct {
|
| 34 |
+
Name string `json:"name" mapstructure:"name,omitempty"`
|
| 35 |
+
Type string `json:"type" mapstructure:"type,omitempty"`
|
| 36 |
+
Parts *models.Parts `json:"parts,omitempty" mapstructure:"parts,omitempty"`
|
| 37 |
+
MimeType string `json:"mimeType" mapstructure:"mime_type,omitempty"`
|
| 38 |
+
ChannelID *int64 `json:"channelId,omitempty" mapstructure:"channel_id,omitempty"`
|
| 39 |
+
Path string `json:"path" mapstructure:"path,omitempty"`
|
| 40 |
+
Size int64 `json:"size" mapstructure:"size,omitempty"`
|
| 41 |
+
Starred *bool `json:"starred" mapstructure:"starred,omitempty"`
|
| 42 |
+
Depth *int `json:"depth,omitempty" mapstructure:"depth,omitempty"`
|
| 43 |
+
Status string `mapstructure:"status,omitempty"`
|
| 44 |
+
UserID int64 `json:"userId" mapstructure:"user_id,omitempty"`
|
| 45 |
+
ParentID string `json:"parentId" mapstructure:"parent_id,omitempty"`
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
type FileOut struct {
|
| 49 |
+
ID string `json:"id"`
|
| 50 |
+
Name string `json:"name"`
|
| 51 |
+
Type string `json:"type"`
|
| 52 |
+
MimeType string `json:"mimeType" mapstructure:"mime_type"`
|
| 53 |
+
Path string `json:"path,omitempty" mapstructure:"path,omitempty"`
|
| 54 |
+
Size int64 `json:"size,omitempty" mapstructure:"size,omitempty"`
|
| 55 |
+
Starred *bool `json:"starred"`
|
| 56 |
+
ParentID string `json:"parentId,omitempty" mapstructure:"parent_id"`
|
| 57 |
+
UpdatedAt time.Time `json:"updatedAt,omitempty" mapstructure:"updated_at"`
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
type FileResponse struct {
|
| 61 |
+
Results []FileOut `json:"results"`
|
| 62 |
+
NextPageToken string `json:"nextPageToken,omitempty"`
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
type FileOutFull struct {
|
| 66 |
+
FileOut
|
| 67 |
+
Parts *models.Parts `json:"parts,omitempty"`
|
| 68 |
+
ChannelID *int64 `json:"channelId"`
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
type FileOperation struct {
|
| 72 |
+
Files []string `json:"files"`
|
| 73 |
+
Destination string `json:"destination,omitempty"`
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
type MkDir struct {
|
| 77 |
+
Path string `json:"path"`
|
| 78 |
+
}
|
schemas/upload.schema.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package schemas
|
| 2 |
+
|
| 3 |
+
type UploadQuery struct {
|
| 4 |
+
Filename string `form:"fileName"`
|
| 5 |
+
PartNo int `form:"partNo,omitempty"`
|
| 6 |
+
TotalParts int `form:"totalparts"`
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
type UploadPartOut struct {
|
| 10 |
+
ID string `json:"id"`
|
| 11 |
+
Name string `json:"name"`
|
| 12 |
+
PartId int `json:"partId"`
|
| 13 |
+
PartNo int `json:"partNo"`
|
| 14 |
+
TotalParts int `json:"totalParts"`
|
| 15 |
+
ChannelID int64 `json:"channelId"`
|
| 16 |
+
Size int64 `json:"size"`
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
type UploadOut struct {
|
| 20 |
+
Parts []UploadPartOut `json:"parts"`
|
| 21 |
+
}
|
services/auth.service.go
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/base64"
|
| 7 |
+
"encoding/binary"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"errors"
|
| 10 |
+
"fmt"
|
| 11 |
+
"log"
|
| 12 |
+
"math/big"
|
| 13 |
+
"net"
|
| 14 |
+
"net/http"
|
| 15 |
+
"strconv"
|
| 16 |
+
"time"
|
| 17 |
+
|
| 18 |
+
"github.com/divyam234/teldrive/models"
|
| 19 |
+
"github.com/divyam234/teldrive/schemas"
|
| 20 |
+
"github.com/divyam234/teldrive/types"
|
| 21 |
+
"github.com/divyam234/teldrive/utils"
|
| 22 |
+
"github.com/divyam234/teldrive/utils/auth"
|
| 23 |
+
"github.com/gin-gonic/gin"
|
| 24 |
+
"github.com/go-jose/go-jose/v3/jwt"
|
| 25 |
+
"github.com/gorilla/websocket"
|
| 26 |
+
"github.com/gotd/td/session"
|
| 27 |
+
tgauth "github.com/gotd/td/telegram/auth"
|
| 28 |
+
"github.com/gotd/td/telegram/auth/qrlogin"
|
| 29 |
+
"github.com/gotd/td/tg"
|
| 30 |
+
"github.com/gotd/td/tgerr"
|
| 31 |
+
"gorm.io/gorm"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
type AuthService struct {
|
| 35 |
+
Db *gorm.DB
|
| 36 |
+
SessionMaxAge int
|
| 37 |
+
SessionCookieName string
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
type SessionData struct {
|
| 41 |
+
Version int
|
| 42 |
+
Data session.Data
|
| 43 |
+
}
|
| 44 |
+
type SocketMessage struct {
|
| 45 |
+
AuthType string `json:"authType"`
|
| 46 |
+
Message string `json:"message"`
|
| 47 |
+
PhoneNo string `json:"phoneNo,omitempty"`
|
| 48 |
+
PhoneCodeHash string `json:"phoneCodeHash,omitempty"`
|
| 49 |
+
PhoneCode string `json:"phoneCode,omitempty"`
|
| 50 |
+
Password string `json:"password,omitempty"`
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func IP4toInt(IPv4Address net.IP) int64 {
|
| 54 |
+
IPv4Int := big.NewInt(0)
|
| 55 |
+
IPv4Int.SetBytes(IPv4Address.To4())
|
| 56 |
+
return IPv4Int.Int64()
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func Pack32BinaryIP4(ip4Address string) []byte {
|
| 60 |
+
ipv4Decimal := IP4toInt(net.ParseIP(ip4Address))
|
| 61 |
+
|
| 62 |
+
buf := new(bytes.Buffer)
|
| 63 |
+
binary.Write(buf, binary.BigEndian, uint32(ipv4Decimal))
|
| 64 |
+
return buf.Bytes()
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
func generateTgSession(dcID int, authKey []byte, port int) string {
|
| 68 |
+
|
| 69 |
+
dcMaps := map[int]string{
|
| 70 |
+
1: "149.154.175.53",
|
| 71 |
+
2: "149.154.167.51",
|
| 72 |
+
3: "149.154.175.100",
|
| 73 |
+
4: "149.154.167.91",
|
| 74 |
+
5: "91.108.56.130",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
dcIDByte := byte(dcID)
|
| 78 |
+
serverAddressBytes := Pack32BinaryIP4(dcMaps[dcID])
|
| 79 |
+
portByte := make([]byte, 2)
|
| 80 |
+
binary.BigEndian.PutUint16(portByte, uint16(port))
|
| 81 |
+
|
| 82 |
+
packet := make([]byte, 0)
|
| 83 |
+
packet = append(packet, dcIDByte)
|
| 84 |
+
packet = append(packet, serverAddressBytes...)
|
| 85 |
+
packet = append(packet, portByte...)
|
| 86 |
+
packet = append(packet, authKey...)
|
| 87 |
+
|
| 88 |
+
base64Encoded := base64.URLEncoding.EncodeToString(packet)
|
| 89 |
+
return "1" + base64Encoded
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func setCookie(c *gin.Context, key string, value string, age int) {
|
| 93 |
+
|
| 94 |
+
config := utils.GetConfig()
|
| 95 |
+
|
| 96 |
+
if config.CookieSameSite {
|
| 97 |
+
c.SetSameSite(2)
|
| 98 |
+
} else {
|
| 99 |
+
c.SetSameSite(4)
|
| 100 |
+
}
|
| 101 |
+
c.SetCookie(key, value, age, "/", c.Request.Host, config.Https, true)
|
| 102 |
+
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
func (as *AuthService) LogIn(c *gin.Context) (*schemas.Message, *types.AppError) {
|
| 106 |
+
var session types.TgSession
|
| 107 |
+
if err := c.ShouldBindJSON(&session); err != nil {
|
| 108 |
+
return nil, &types.AppError{Error: errors.New("invalid request payload"), Code: http.StatusBadRequest}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
now := time.Now().UTC()
|
| 112 |
+
|
| 113 |
+
jwtClaims := &types.JWTClaims{Claims: jwt.Claims{
|
| 114 |
+
Subject: strconv.FormatInt(session.UserID, 10),
|
| 115 |
+
IssuedAt: jwt.NewNumericDate(now),
|
| 116 |
+
Expiry: jwt.NewNumericDate(now.Add(time.Duration(as.SessionMaxAge) * time.Second)),
|
| 117 |
+
}, TgSession: session.Sesssion,
|
| 118 |
+
Name: session.Name,
|
| 119 |
+
UserName: session.UserName,
|
| 120 |
+
Bot: session.Bot,
|
| 121 |
+
IsPremium: session.IsPremium,
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
jweToken, err := auth.Encode(jwtClaims)
|
| 125 |
+
|
| 126 |
+
if err != nil {
|
| 127 |
+
return nil, &types.AppError{Error: err, Code: http.StatusBadRequest}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
user := models.User{
|
| 131 |
+
UserId: session.UserID,
|
| 132 |
+
Name: session.Name,
|
| 133 |
+
UserName: session.UserName,
|
| 134 |
+
IsPremium: session.IsPremium,
|
| 135 |
+
TgSession: session.Sesssion,
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
var result []models.User
|
| 139 |
+
|
| 140 |
+
if err := as.Db.Model(&models.User{}).Where("user_id = ?", session.UserID).Find(&result).Error; err != nil {
|
| 141 |
+
return nil, &types.AppError{Error: errors.New("failed to create or update user"), Code: http.StatusInternalServerError}
|
| 142 |
+
}
|
| 143 |
+
if len(result) == 0 {
|
| 144 |
+
if err := as.Db.Create(&user).Error; err != nil {
|
| 145 |
+
return nil, &types.AppError{Error: errors.New("failed to create or update user"), Code: http.StatusInternalServerError}
|
| 146 |
+
}
|
| 147 |
+
//Create root folder on first login
|
| 148 |
+
|
| 149 |
+
file := &models.File{
|
| 150 |
+
Name: "root",
|
| 151 |
+
Type: "folder",
|
| 152 |
+
MimeType: "drive/folder",
|
| 153 |
+
Path: "/",
|
| 154 |
+
Depth: utils.IntPointer(0),
|
| 155 |
+
UserID: session.UserID,
|
| 156 |
+
Status: "active",
|
| 157 |
+
ParentID: "root",
|
| 158 |
+
}
|
| 159 |
+
if err := as.Db.Create(file).Error; err != nil {
|
| 160 |
+
return nil, &types.AppError{Error: errors.New("failed to create or update user"), Code: http.StatusInternalServerError}
|
| 161 |
+
}
|
| 162 |
+
} else {
|
| 163 |
+
if err := as.Db.Model(&models.User{}).Where("user_id = ?", session.UserID).Update("tg_session", session.Sesssion).Error; err != nil {
|
| 164 |
+
return nil, &types.AppError{Error: errors.New("failed to create or update user"), Code: http.StatusInternalServerError}
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
setCookie(c, as.SessionCookieName, jweToken, as.SessionMaxAge)
|
| 168 |
+
return &schemas.Message{Status: true, Message: "login success"}, nil
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
func (as *AuthService) GetSession(c *gin.Context) *types.Session {
|
| 172 |
+
|
| 173 |
+
cookie, err := c.Request.Cookie(as.SessionCookieName)
|
| 174 |
+
|
| 175 |
+
if err != nil {
|
| 176 |
+
return nil
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
jwePayload, err := auth.Decode(cookie.Value)
|
| 180 |
+
|
| 181 |
+
if err != nil {
|
| 182 |
+
return nil
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
now := time.Now().UTC()
|
| 186 |
+
|
| 187 |
+
newExpires := now.Add(time.Duration(as.SessionMaxAge) * time.Second)
|
| 188 |
+
|
| 189 |
+
session := &types.Session{Name: jwePayload.Name, UserName: jwePayload.UserName, Expires: newExpires.Format(time.RFC3339)}
|
| 190 |
+
|
| 191 |
+
jwePayload.IssuedAt = jwt.NewNumericDate(now)
|
| 192 |
+
|
| 193 |
+
jwePayload.Expiry = jwt.NewNumericDate(newExpires)
|
| 194 |
+
|
| 195 |
+
jweToken, err := auth.Encode(jwePayload)
|
| 196 |
+
|
| 197 |
+
if err != nil {
|
| 198 |
+
return nil
|
| 199 |
+
}
|
| 200 |
+
setCookie(c, as.SessionCookieName, jweToken, as.SessionMaxAge)
|
| 201 |
+
return session
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
func (as *AuthService) Logout(c *gin.Context) (*schemas.Message, *types.AppError) {
|
| 205 |
+
val, _ := c.Get("jwtUser")
|
| 206 |
+
jwtUser := val.(*types.JWTClaims)
|
| 207 |
+
userId, _ := strconv.ParseInt(jwtUser.Subject, 10, 64)
|
| 208 |
+
|
| 209 |
+
client, _ := utils.GetAuthClient(c, jwtUser.TgSession, userId)
|
| 210 |
+
|
| 211 |
+
client.Run(c, func(ctx context.Context) error {
|
| 212 |
+
_, err := client.API().AuthLogOut(c)
|
| 213 |
+
return err
|
| 214 |
+
})
|
| 215 |
+
|
| 216 |
+
setCookie(c, as.SessionCookieName, "", -1)
|
| 217 |
+
return &schemas.Message{Status: true, Message: "logout success"}, nil
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
func prepareSession(user *tg.User, data *session.Data) *types.TgSession {
|
| 221 |
+
sessionString := generateTgSession(data.DC, data.AuthKey, 443)
|
| 222 |
+
session := &types.TgSession{
|
| 223 |
+
Sesssion: sessionString,
|
| 224 |
+
UserID: user.ID,
|
| 225 |
+
Bot: user.Bot,
|
| 226 |
+
UserName: user.Username,
|
| 227 |
+
Name: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
|
| 228 |
+
IsPremium: user.Premium,
|
| 229 |
+
}
|
| 230 |
+
return session
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
func (as *AuthService) HandleMultipleLogin(c *gin.Context) {
|
| 234 |
+
upgrader := websocket.Upgrader{
|
| 235 |
+
CheckOrigin: func(r *http.Request) bool {
|
| 236 |
+
return true
|
| 237 |
+
},
|
| 238 |
+
}
|
| 239 |
+
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
| 240 |
+
if err != nil {
|
| 241 |
+
log.Println(err)
|
| 242 |
+
return
|
| 243 |
+
}
|
| 244 |
+
defer conn.Close()
|
| 245 |
+
|
| 246 |
+
dispatcher := tg.NewUpdateDispatcher()
|
| 247 |
+
loggedIn := qrlogin.OnLoginToken(dispatcher)
|
| 248 |
+
sessionStorage := &session.StorageMemory{}
|
| 249 |
+
tgClient, stop, _ := utils.StartNonAuthClient(dispatcher, sessionStorage)
|
| 250 |
+
|
| 251 |
+
defer stop()
|
| 252 |
+
|
| 253 |
+
for {
|
| 254 |
+
message := &SocketMessage{}
|
| 255 |
+
err := conn.ReadJSON(message)
|
| 256 |
+
|
| 257 |
+
if err != nil {
|
| 258 |
+
log.Println(err)
|
| 259 |
+
return
|
| 260 |
+
}
|
| 261 |
+
if message.AuthType == "qr" {
|
| 262 |
+
go func() {
|
| 263 |
+
authorization, err := tgClient.QR().Auth(c, loggedIn, func(ctx context.Context, token qrlogin.Token) error {
|
| 264 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "payload": map[string]string{"token": token.URL()}})
|
| 265 |
+
return nil
|
| 266 |
+
})
|
| 267 |
+
|
| 268 |
+
if tgerr.Is(err, "SESSION_PASSWORD_NEEDED") {
|
| 269 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "message": "2FA required"})
|
| 270 |
+
return
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
if err != nil {
|
| 274 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": err.Error()})
|
| 275 |
+
return
|
| 276 |
+
}
|
| 277 |
+
user, ok := authorization.User.AsNotEmpty()
|
| 278 |
+
if !ok {
|
| 279 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": errors.New("auth failed")})
|
| 280 |
+
return
|
| 281 |
+
}
|
| 282 |
+
res, _ := sessionStorage.LoadSession(c)
|
| 283 |
+
sessionData := &SessionData{}
|
| 284 |
+
json.Unmarshal(res, sessionData)
|
| 285 |
+
session := prepareSession(user, &sessionData.Data)
|
| 286 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "payload": session, "message": "success"})
|
| 287 |
+
}()
|
| 288 |
+
}
|
| 289 |
+
if message.AuthType == "phone" && message.Message == "sendcode" {
|
| 290 |
+
go func() {
|
| 291 |
+
res, err := tgClient.Auth().SendCode(c, message.PhoneNo, tgauth.SendCodeOptions{})
|
| 292 |
+
if err != nil {
|
| 293 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": err.Error()})
|
| 294 |
+
return
|
| 295 |
+
}
|
| 296 |
+
code := res.(*tg.AuthSentCode)
|
| 297 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "payload": map[string]string{"phoneCodeHash": code.PhoneCodeHash}})
|
| 298 |
+
}()
|
| 299 |
+
}
|
| 300 |
+
if message.AuthType == "phone" && message.Message == "signin" {
|
| 301 |
+
go func() {
|
| 302 |
+
auth, err := tgClient.Auth().SignIn(c, message.PhoneNo, message.PhoneCode, message.PhoneCodeHash)
|
| 303 |
+
|
| 304 |
+
if errors.Is(err, tgauth.ErrPasswordAuthNeeded) {
|
| 305 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "message": "2FA required"})
|
| 306 |
+
return
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
if err != nil {
|
| 310 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": err.Error()})
|
| 311 |
+
return
|
| 312 |
+
}
|
| 313 |
+
user, ok := auth.User.AsNotEmpty()
|
| 314 |
+
if !ok {
|
| 315 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": errors.New("auth failed")})
|
| 316 |
+
return
|
| 317 |
+
}
|
| 318 |
+
res, _ := sessionStorage.LoadSession(c)
|
| 319 |
+
sessionData := &SessionData{}
|
| 320 |
+
json.Unmarshal(res, sessionData)
|
| 321 |
+
session := prepareSession(user, &sessionData.Data)
|
| 322 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "payload": session, "message": "success"})
|
| 323 |
+
}()
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
if message.AuthType == "2fa" && message.Password != "" {
|
| 327 |
+
go func() {
|
| 328 |
+
auth, err := tgClient.Auth().Password(c, message.Password)
|
| 329 |
+
if err != nil {
|
| 330 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": err.Error()})
|
| 331 |
+
return
|
| 332 |
+
}
|
| 333 |
+
user, ok := auth.User.AsNotEmpty()
|
| 334 |
+
if !ok {
|
| 335 |
+
conn.WriteJSON(map[string]interface{}{"type": "error", "message": errors.New("auth failed")})
|
| 336 |
+
return
|
| 337 |
+
}
|
| 338 |
+
res, _ := sessionStorage.LoadSession(c)
|
| 339 |
+
sessionData := &SessionData{}
|
| 340 |
+
json.Unmarshal(res, sessionData)
|
| 341 |
+
session := prepareSession(user, &sessionData.Data)
|
| 342 |
+
conn.WriteJSON(map[string]interface{}{"type": "auth", "payload": session, "message": "success"})
|
| 343 |
+
}()
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
}
|
services/file.service.go
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/base64"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"math"
|
| 10 |
+
"net/http"
|
| 11 |
+
"strconv"
|
| 12 |
+
"strings"
|
| 13 |
+
|
| 14 |
+
"github.com/divyam234/teldrive/cache"
|
| 15 |
+
"github.com/divyam234/teldrive/models"
|
| 16 |
+
"github.com/divyam234/teldrive/schemas"
|
| 17 |
+
"github.com/divyam234/teldrive/utils"
|
| 18 |
+
|
| 19 |
+
"github.com/divyam234/teldrive/types"
|
| 20 |
+
|
| 21 |
+
"github.com/gin-gonic/gin"
|
| 22 |
+
"github.com/gotd/td/telegram"
|
| 23 |
+
"github.com/gotd/td/tg"
|
| 24 |
+
"github.com/jackc/pgx/v5/pgconn"
|
| 25 |
+
"github.com/mitchellh/mapstructure"
|
| 26 |
+
range_parser "github.com/quantumsheep/range-parser"
|
| 27 |
+
"gorm.io/gorm"
|
| 28 |
+
"gorm.io/gorm/clause"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
type FileService struct {
|
| 32 |
+
Db *gorm.DB
|
| 33 |
+
ChannelID int64
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func getAuthUserId(c *gin.Context) int64 {
|
| 37 |
+
val, _ := c.Get("jwtUser")
|
| 38 |
+
jwtUser := val.(*types.JWTClaims)
|
| 39 |
+
userId, _ := strconv.ParseInt(jwtUser.Subject, 10, 64)
|
| 40 |
+
return userId
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func (fs *FileService) CreateFile(c *gin.Context) (*schemas.FileOut, *types.AppError) {
|
| 44 |
+
userId := getAuthUserId(c)
|
| 45 |
+
var fileIn schemas.FileIn
|
| 46 |
+
if err := c.ShouldBindJSON(&fileIn); err != nil {
|
| 47 |
+
return nil, &types.AppError{Error: errors.New("invalid request payload"), Code: http.StatusBadRequest}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
fileIn.Path = strings.TrimSpace(fileIn.Path)
|
| 51 |
+
|
| 52 |
+
if fileIn.Path != "" {
|
| 53 |
+
var parent models.File
|
| 54 |
+
if err := fs.Db.Where("type = ? AND path = ?", "folder", fileIn.Path).First(&parent).Error; err != nil {
|
| 55 |
+
return nil, &types.AppError{Error: errors.New("parent directory not found"), Code: http.StatusNotFound}
|
| 56 |
+
}
|
| 57 |
+
fileIn.ParentID = parent.ID
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if fileIn.Type == "folder" {
|
| 61 |
+
fileIn.MimeType = "drive/folder"
|
| 62 |
+
var fullPath string
|
| 63 |
+
if fileIn.Path == "/" {
|
| 64 |
+
fullPath = "/" + fileIn.Name
|
| 65 |
+
} else {
|
| 66 |
+
fullPath = fileIn.Path + "/" + fileIn.Name
|
| 67 |
+
}
|
| 68 |
+
fileIn.Path = fullPath
|
| 69 |
+
fileIn.Depth = utils.IntPointer(len(strings.Split(fileIn.Path, "/")) - 1)
|
| 70 |
+
} else if fileIn.Type == "file" {
|
| 71 |
+
fileIn.Path = ""
|
| 72 |
+
fileIn.ChannelID = &fs.ChannelID
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
fileIn.UserID = userId
|
| 76 |
+
fileIn.Starred = utils.BoolPointer(false)
|
| 77 |
+
fileIn.Status = "active"
|
| 78 |
+
|
| 79 |
+
fileDb := mapFileInToFile(fileIn)
|
| 80 |
+
|
| 81 |
+
if err := fs.Db.Create(&fileDb).Error; err != nil {
|
| 82 |
+
pgErr := err.(*pgconn.PgError)
|
| 83 |
+
if pgErr.Code == "23505" {
|
| 84 |
+
return nil, &types.AppError{Error: errors.New("file exists"), Code: http.StatusBadRequest}
|
| 85 |
+
}
|
| 86 |
+
return nil, &types.AppError{Error: errors.New("failed to create a file"), Code: http.StatusBadRequest}
|
| 87 |
+
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
res := mapFileToFileOut(fileDb)
|
| 91 |
+
|
| 92 |
+
return &res, nil
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
func (fs *FileService) UpdateFile(c *gin.Context) (*schemas.FileOut, *types.AppError) {
|
| 96 |
+
|
| 97 |
+
fileID := c.Param("fileID")
|
| 98 |
+
|
| 99 |
+
var fileUpdate schemas.FileIn
|
| 100 |
+
|
| 101 |
+
var files []models.File
|
| 102 |
+
|
| 103 |
+
if err := c.ShouldBindJSON(&fileUpdate); err != nil {
|
| 104 |
+
return nil, &types.AppError{Error: errors.New("invalid request payload"), Code: http.StatusBadRequest}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
if fileUpdate.Type == "folder" && fileUpdate.Name != "" {
|
| 108 |
+
if err := fs.Db.Raw("select * from teldrive.update_folder(?, ?)", fileID, fileUpdate.Name).Scan(&files).Error; err != nil {
|
| 109 |
+
return nil, &types.AppError{Error: errors.New("failed to update the file"), Code: http.StatusInternalServerError}
|
| 110 |
+
}
|
| 111 |
+
} else {
|
| 112 |
+
fileDb := mapFileInToFile(fileUpdate)
|
| 113 |
+
if err := fs.Db.Model(&files).Clauses(clause.Returning{}).Where("id = ?", fileID).Updates(fileDb).Error; err != nil {
|
| 114 |
+
return nil, &types.AppError{Error: errors.New("failed to update the file"), Code: http.StatusInternalServerError}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
if len(files) == 0 {
|
| 119 |
+
return nil, &types.AppError{Error: errors.New("file not updated"), Code: http.StatusNotFound}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
file := mapFileToFileOut(files[0])
|
| 123 |
+
|
| 124 |
+
return &file, nil
|
| 125 |
+
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func (fs *FileService) GetFileByID(c *gin.Context) (*schemas.FileOutFull, error) {
|
| 129 |
+
|
| 130 |
+
fileID := c.Param("fileID")
|
| 131 |
+
|
| 132 |
+
var file []models.File
|
| 133 |
+
|
| 134 |
+
fs.Db.Model(&models.File{}).Where("id = ?", fileID).Find(&file)
|
| 135 |
+
|
| 136 |
+
if len(file) == 0 {
|
| 137 |
+
return nil, errors.New("file not found")
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return mapFileToFileOutFull(file[0]), nil
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
func (fs *FileService) ListFiles(c *gin.Context) (*schemas.FileResponse, *types.AppError) {
|
| 144 |
+
|
| 145 |
+
userId := getAuthUserId(c)
|
| 146 |
+
|
| 147 |
+
var pagingParams schemas.PaginationQuery
|
| 148 |
+
pagingParams.PerPage = 200
|
| 149 |
+
if err := c.ShouldBindQuery(&pagingParams); err != nil {
|
| 150 |
+
return nil, &types.AppError{Error: errors.New(""), Code: http.StatusBadRequest}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
var sortingParams schemas.SortingQuery
|
| 154 |
+
sortingParams.Order = "asc"
|
| 155 |
+
sortingParams.Sort = "name"
|
| 156 |
+
if err := c.ShouldBindQuery(&sortingParams); err != nil {
|
| 157 |
+
return nil, &types.AppError{Error: errors.New(""), Code: http.StatusBadRequest}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
var fileQuery schemas.FileQuery
|
| 161 |
+
fileQuery.Op = "list"
|
| 162 |
+
fileQuery.Status = "active"
|
| 163 |
+
fileQuery.UserID = userId
|
| 164 |
+
if err := c.ShouldBindQuery(&fileQuery); err != nil {
|
| 165 |
+
return nil, &types.AppError{Error: errors.New(""), Code: http.StatusBadRequest}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
query := fs.Db.Model(&models.File{}).Limit(pagingParams.PerPage).
|
| 169 |
+
Where(map[string]interface{}{"user_id": userId, "status": "active"})
|
| 170 |
+
|
| 171 |
+
if fileQuery.Op == "list" {
|
| 172 |
+
|
| 173 |
+
if pathExists, message := fs.CheckIfPathExists(&fileQuery.Path); !pathExists {
|
| 174 |
+
return nil, &types.AppError{Error: errors.New(message), Code: http.StatusNotFound}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
setOrderFilter(query, &pagingParams, &sortingParams)
|
| 178 |
+
|
| 179 |
+
query.Order("type DESC").Order(getOrder(sortingParams)).
|
| 180 |
+
Where("parent_id in (?)", fs.Db.Model(&models.File{}).Select("id").Where("path = ?", fileQuery.Path))
|
| 181 |
+
|
| 182 |
+
} else if fileQuery.Op == "find" {
|
| 183 |
+
|
| 184 |
+
filterQuery := map[string]interface{}{}
|
| 185 |
+
|
| 186 |
+
err := mapstructure.Decode(fileQuery, &filterQuery)
|
| 187 |
+
|
| 188 |
+
if err != nil {
|
| 189 |
+
return nil, &types.AppError{Error: err, Code: http.StatusBadRequest}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
delete(filterQuery, "op")
|
| 193 |
+
|
| 194 |
+
if filterQuery["updated_at"] == nil {
|
| 195 |
+
delete(filterQuery, "updated_at")
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
setOrderFilter(query, &pagingParams, &sortingParams)
|
| 199 |
+
|
| 200 |
+
query.Order("type DESC").Order(getOrder(sortingParams)).Where(filterQuery)
|
| 201 |
+
|
| 202 |
+
} else if fileQuery.Op == "search" {
|
| 203 |
+
|
| 204 |
+
query.Where("teldrive.get_tsquery(?) @@ teldrive.get_tsvector(name)", fileQuery.Search)
|
| 205 |
+
|
| 206 |
+
setOrderFilter(query, &pagingParams, &sortingParams)
|
| 207 |
+
query.Order(getOrder(sortingParams))
|
| 208 |
+
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
var results []schemas.FileOut
|
| 212 |
+
|
| 213 |
+
query.Find(&results)
|
| 214 |
+
|
| 215 |
+
token := ""
|
| 216 |
+
|
| 217 |
+
if len(results) == pagingParams.PerPage {
|
| 218 |
+
lastItem := results[len(results)-1]
|
| 219 |
+
token = utils.GetField(&lastItem, utils.CamelToPascalCase(sortingParams.Sort))
|
| 220 |
+
token = base64.StdEncoding.EncodeToString([]byte(token))
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
res := &schemas.FileResponse{Results: results, NextPageToken: token}
|
| 224 |
+
|
| 225 |
+
return res, nil
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
func (fs *FileService) CheckIfPathExists(path *string) (bool, string) {
|
| 229 |
+
query := fs.Db.Model(&models.File{}).Select("id").Where("path = ?", path)
|
| 230 |
+
var results []schemas.FileOut
|
| 231 |
+
query.Find(&results)
|
| 232 |
+
if len(results) == 0 {
|
| 233 |
+
return false, "This directory doesn't exist."
|
| 234 |
+
}
|
| 235 |
+
return true, ""
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
func (fs *FileService) MakeDirectory(c *gin.Context) (*schemas.FileOut, *types.AppError) {
|
| 239 |
+
|
| 240 |
+
var payload schemas.MkDir
|
| 241 |
+
|
| 242 |
+
var files []models.File
|
| 243 |
+
|
| 244 |
+
if err := c.ShouldBindJSON(&payload); err != nil {
|
| 245 |
+
return nil, &types.AppError{Error: errors.New("invalid request payload"), Code: http.StatusBadRequest}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
userId := getAuthUserId(c)
|
| 249 |
+
if err := fs.Db.Raw("select * from teldrive.create_directories(?, ?)", userId, payload.Path).Scan(&files).Error; err != nil {
|
| 250 |
+
return nil, &types.AppError{Error: errors.New("failed to create directories"), Code: http.StatusInternalServerError}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
file := mapFileToFileOut(files[0])
|
| 254 |
+
|
| 255 |
+
return &file, nil
|
| 256 |
+
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
func (fs *FileService) MoveFiles(c *gin.Context) (*schemas.Message, *types.AppError) {
|
| 260 |
+
|
| 261 |
+
var payload schemas.FileOperation
|
| 262 |
+
|
| 263 |
+
if err := c.ShouldBindJSON(&payload); err != nil {
|
| 264 |
+
return nil, &types.AppError{Error: errors.New("invalid request payload"), Code: http.StatusBadRequest}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
var destination models.File
|
| 268 |
+
|
| 269 |
+
if pathExists, message := fs.CheckIfPathExists(&payload.Destination); !pathExists {
|
| 270 |
+
return nil, &types.AppError{Error: errors.New(message), Code: http.StatusBadRequest}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
if err := fs.Db.Model(&models.File{}).Select("id").Where("path = ?", payload.Destination).First(&destination).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
| 274 |
+
return nil, &types.AppError{Error: errors.New("destination not found"), Code: http.StatusNotFound}
|
| 275 |
+
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
if err := fs.Db.Model(&models.File{}).Where("id IN ?", payload.Files).UpdateColumn("parent_id", destination.ID).Error; err != nil {
|
| 279 |
+
return nil, &types.AppError{Error: errors.New("move failed"), Code: http.StatusInternalServerError}
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
return &schemas.Message{Status: true, Message: "files moved"}, nil
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
func (fs *FileService) DeleteFiles(c *gin.Context) (*schemas.Message, *types.AppError) {
|
| 286 |
+
|
| 287 |
+
var payload schemas.FileOperation
|
| 288 |
+
|
| 289 |
+
if err := c.ShouldBindJSON(&payload); err != nil {
|
| 290 |
+
return nil, &types.AppError{Error: errors.New("invalid request payload"), Code: http.StatusBadRequest}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
if err := fs.Db.Exec("call teldrive.delete_files($1)", payload.Files).Error; err != nil {
|
| 294 |
+
return nil, &types.AppError{Error: errors.New("failed to delete files"), Code: http.StatusInternalServerError}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
return &schemas.Message{Status: true, Message: "files deleted"}, nil
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
func (fs *FileService) GetFileStream(c *gin.Context) {
|
| 301 |
+
|
| 302 |
+
w := c.Writer
|
| 303 |
+
r := c.Request
|
| 304 |
+
|
| 305 |
+
fileID := c.Param("fileID")
|
| 306 |
+
|
| 307 |
+
var err error
|
| 308 |
+
|
| 309 |
+
res, err := cache.CachedFunction(fs.GetFileByID, fmt.Sprintf("files:%s", fileID))(c)
|
| 310 |
+
|
| 311 |
+
if err != nil {
|
| 312 |
+
http.Error(w, err.Error(), http.StatusBadRequest)
|
| 313 |
+
return
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
file := res.(*schemas.FileOutFull)
|
| 317 |
+
|
| 318 |
+
w.Header().Set("Accept-Ranges", "bytes")
|
| 319 |
+
|
| 320 |
+
var start, end int64
|
| 321 |
+
|
| 322 |
+
rangeHeader := r.Header.Get("Range")
|
| 323 |
+
|
| 324 |
+
if rangeHeader == "" {
|
| 325 |
+
start = 0
|
| 326 |
+
end = file.Size - 1
|
| 327 |
+
w.WriteHeader(http.StatusOK)
|
| 328 |
+
} else {
|
| 329 |
+
ranges, err := range_parser.Parse(file.Size, r.Header.Get("Range"))
|
| 330 |
+
if err != nil {
|
| 331 |
+
http.Error(w, err.Error(), http.StatusBadRequest)
|
| 332 |
+
return
|
| 333 |
+
}
|
| 334 |
+
start = ranges[0].Start
|
| 335 |
+
end = ranges[0].End
|
| 336 |
+
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, file.Size))
|
| 337 |
+
w.WriteHeader(http.StatusPartialContent)
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
contentLength := end - start + 1
|
| 341 |
+
|
| 342 |
+
w.Header().Set("Content-Type", file.MimeType)
|
| 343 |
+
|
| 344 |
+
w.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
|
| 345 |
+
|
| 346 |
+
disposition := "inline"
|
| 347 |
+
|
| 348 |
+
if c.Query("d") == "1" {
|
| 349 |
+
disposition = "attachment"
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", disposition, file.Name))
|
| 353 |
+
|
| 354 |
+
client, idx := utils.GetDownloadClient(c)
|
| 355 |
+
|
| 356 |
+
defer func() {
|
| 357 |
+
utils.GetClientWorkload().Dec(idx)
|
| 358 |
+
}()
|
| 359 |
+
|
| 360 |
+
ir, iw := io.Pipe()
|
| 361 |
+
parts, err := fs.getParts(c, client, file)
|
| 362 |
+
if err != nil {
|
| 363 |
+
return
|
| 364 |
+
}
|
| 365 |
+
parts = rangedParts(parts, int64(start), int64(end))
|
| 366 |
+
|
| 367 |
+
if r.Method != "HEAD" {
|
| 368 |
+
go func() {
|
| 369 |
+
defer iw.Close()
|
| 370 |
+
for _, part := range parts {
|
| 371 |
+
streamFilePart(c, client, iw, &part, part.Start, part.End, 1024*1024)
|
| 372 |
+
}
|
| 373 |
+
}()
|
| 374 |
+
io.CopyN(w, ir, contentLength)
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
func (fs *FileService) getParts(ctx context.Context, tgClient *telegram.Client, file *schemas.FileOutFull) ([]types.Part, error) {
|
| 380 |
+
|
| 381 |
+
ids := []tg.InputMessageID{}
|
| 382 |
+
|
| 383 |
+
for _, part := range *file.Parts {
|
| 384 |
+
ids = append(ids, tg.InputMessageID{ID: int(part.ID)})
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
s := make([]tg.InputMessageClass, len(ids))
|
| 388 |
+
|
| 389 |
+
for i := range ids {
|
| 390 |
+
s[i] = &ids[i]
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
api := tgClient.API()
|
| 394 |
+
|
| 395 |
+
res, err := cache.CachedFunction(utils.GetChannelById, fmt.Sprintf("channels:%d", fs.ChannelID))(ctx, api, fs.ChannelID)
|
| 396 |
+
|
| 397 |
+
if err != nil {
|
| 398 |
+
return nil, err
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
channel := res.(*tg.Channel)
|
| 402 |
+
|
| 403 |
+
messageRequest := tg.ChannelsGetMessagesRequest{Channel: &tg.InputChannel{ChannelID: fs.ChannelID, AccessHash: channel.AccessHash},
|
| 404 |
+
ID: s}
|
| 405 |
+
|
| 406 |
+
res, err = cache.CachedFunction(api.ChannelsGetMessages, fmt.Sprintf("messages:%s", file.ID))(ctx, &messageRequest)
|
| 407 |
+
|
| 408 |
+
if err != nil {
|
| 409 |
+
return nil, err
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
messages := res.(*tg.MessagesChannelMessages)
|
| 413 |
+
|
| 414 |
+
parts := []types.Part{}
|
| 415 |
+
|
| 416 |
+
for _, message := range messages.Messages {
|
| 417 |
+
item := message.(*tg.Message)
|
| 418 |
+
media := item.Media.(*tg.MessageMediaDocument)
|
| 419 |
+
document := media.Document.(*tg.Document)
|
| 420 |
+
location := document.AsInputDocumentFileLocation()
|
| 421 |
+
parts = append(parts, types.Part{Location: location, Start: 0, End: document.Size - 1, Size: document.Size})
|
| 422 |
+
}
|
| 423 |
+
return parts, nil
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
func mapFileToFileOut(file models.File) schemas.FileOut {
|
| 427 |
+
return schemas.FileOut{
|
| 428 |
+
ID: file.ID,
|
| 429 |
+
Name: file.Name,
|
| 430 |
+
Type: file.Type,
|
| 431 |
+
MimeType: file.MimeType,
|
| 432 |
+
Path: file.Path,
|
| 433 |
+
Size: file.Size,
|
| 434 |
+
Starred: file.Starred,
|
| 435 |
+
ParentID: file.ParentID,
|
| 436 |
+
UpdatedAt: file.UpdatedAt,
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
func mapFileInToFile(file schemas.FileIn) models.File {
|
| 441 |
+
return models.File{
|
| 442 |
+
Name: file.Name,
|
| 443 |
+
Type: file.Type,
|
| 444 |
+
MimeType: file.MimeType,
|
| 445 |
+
Path: file.Path,
|
| 446 |
+
Size: file.Size,
|
| 447 |
+
Starred: file.Starred,
|
| 448 |
+
Depth: file.Depth,
|
| 449 |
+
UserID: file.UserID,
|
| 450 |
+
ParentID: file.ParentID,
|
| 451 |
+
Parts: file.Parts,
|
| 452 |
+
ChannelID: file.ChannelID,
|
| 453 |
+
Status: file.Status,
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
func mapFileToFileOutFull(file models.File) *schemas.FileOutFull {
|
| 458 |
+
return &schemas.FileOutFull{
|
| 459 |
+
FileOut: mapFileToFileOut(file),
|
| 460 |
+
Parts: file.Parts, ChannelID: file.ChannelID,
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
func setOrderFilter(query *gorm.DB, pagingParams *schemas.PaginationQuery, sortingParams *schemas.SortingQuery) *gorm.DB {
|
| 465 |
+
if pagingParams.NextPageToken != "" {
|
| 466 |
+
sortColumn := sortingParams.Sort
|
| 467 |
+
if sortColumn == "name" {
|
| 468 |
+
sortColumn = "name collate numeric"
|
| 469 |
+
} else {
|
| 470 |
+
sortColumn = utils.CamelToSnake(sortingParams.Sort)
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
tokenValue, err := base64.StdEncoding.DecodeString(pagingParams.NextPageToken)
|
| 474 |
+
if err == nil {
|
| 475 |
+
if sortingParams.Order == "asc" {
|
| 476 |
+
return query.Where(fmt.Sprintf("%s > ?", sortColumn), string(tokenValue))
|
| 477 |
+
} else {
|
| 478 |
+
return query.Where(fmt.Sprintf("%s < ?", sortColumn), string(tokenValue))
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
return query
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
func getOrder(sortingParams schemas.SortingQuery) string {
|
| 486 |
+
sortColumn := utils.CamelToSnake(sortingParams.Sort)
|
| 487 |
+
if sortingParams.Sort == "name" {
|
| 488 |
+
sortColumn = "name collate numeric"
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
return fmt.Sprintf("%s %s", sortColumn, strings.ToUpper(sortingParams.Order))
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
func chunk(ctx context.Context, tgClient *telegram.Client, part *types.Part, offset int64, limit int64) ([]byte, error) {
|
| 495 |
+
|
| 496 |
+
req := &tg.UploadGetFileRequest{
|
| 497 |
+
Offset: offset,
|
| 498 |
+
Limit: int(limit),
|
| 499 |
+
Location: part.Location,
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
r, err := tgClient.API().UploadGetFile(ctx, req)
|
| 503 |
+
|
| 504 |
+
if err != nil {
|
| 505 |
+
return nil, err
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
switch result := r.(type) {
|
| 509 |
+
case *tg.UploadFile:
|
| 510 |
+
return result.Bytes, nil
|
| 511 |
+
default:
|
| 512 |
+
return nil, fmt.Errorf("unexpected type %T", r)
|
| 513 |
+
}
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
func totalParts(start, end, chunkSize int64) int {
|
| 517 |
+
|
| 518 |
+
totalBytes := end - start + 1
|
| 519 |
+
parts := totalBytes / chunkSize
|
| 520 |
+
|
| 521 |
+
if totalBytes%chunkSize != 0 {
|
| 522 |
+
parts++
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
return int(parts)
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
func streamFilePart(ctx context.Context, tgClient *telegram.Client, writer *io.PipeWriter, part *types.Part, start, end, chunkSize int64) error {
|
| 529 |
+
|
| 530 |
+
offset := start - (start % chunkSize)
|
| 531 |
+
firstPartCut := start - offset
|
| 532 |
+
lastPartCut := (end % chunkSize) + 1
|
| 533 |
+
|
| 534 |
+
partCount := totalParts(start, end, chunkSize)
|
| 535 |
+
|
| 536 |
+
currentPart := 1
|
| 537 |
+
|
| 538 |
+
for {
|
| 539 |
+
r, _ := chunk(ctx, tgClient, part, offset, chunkSize)
|
| 540 |
+
|
| 541 |
+
if len(r) == 0 {
|
| 542 |
+
break
|
| 543 |
+
} else if partCount == 1 {
|
| 544 |
+
r = r[firstPartCut:lastPartCut]
|
| 545 |
+
|
| 546 |
+
} else if currentPart == 1 {
|
| 547 |
+
r = r[firstPartCut:]
|
| 548 |
+
|
| 549 |
+
} else if currentPart == partCount {
|
| 550 |
+
r = r[:lastPartCut]
|
| 551 |
+
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
writer.Write(r)
|
| 555 |
+
|
| 556 |
+
currentPart++
|
| 557 |
+
|
| 558 |
+
offset += chunkSize
|
| 559 |
+
|
| 560 |
+
if currentPart > partCount {
|
| 561 |
+
break
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
return nil
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
func rangedParts(parts []types.Part, start, end int64) []types.Part {
|
| 570 |
+
|
| 571 |
+
chunkSize := parts[0].Size
|
| 572 |
+
|
| 573 |
+
startPartNumber := utils.Max(int64(math.Ceil(float64(start)/float64(chunkSize)))-1, 0)
|
| 574 |
+
|
| 575 |
+
endPartNumber := int64(math.Ceil(float64(end) / float64(chunkSize)))
|
| 576 |
+
|
| 577 |
+
partsToDownload := parts[startPartNumber:endPartNumber]
|
| 578 |
+
partsToDownload[0].Start = start % chunkSize
|
| 579 |
+
partsToDownload[len(partsToDownload)-1].End = end % chunkSize
|
| 580 |
+
|
| 581 |
+
return partsToDownload
|
| 582 |
+
}
|
services/upload.service.go
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net/http"
|
| 8 |
+
|
| 9 |
+
"github.com/divyam234/teldrive/cache"
|
| 10 |
+
"github.com/divyam234/teldrive/schemas"
|
| 11 |
+
"github.com/divyam234/teldrive/utils"
|
| 12 |
+
|
| 13 |
+
"github.com/divyam234/teldrive/types"
|
| 14 |
+
|
| 15 |
+
"github.com/divyam234/teldrive/models"
|
| 16 |
+
"github.com/gin-gonic/gin"
|
| 17 |
+
"github.com/gotd/td/telegram/message"
|
| 18 |
+
"github.com/gotd/td/telegram/uploader"
|
| 19 |
+
"github.com/gotd/td/tg"
|
| 20 |
+
"gorm.io/gorm"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
type UploadService struct {
|
| 24 |
+
Db *gorm.DB
|
| 25 |
+
ChannelID int64
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func (us *UploadService) GetUploadFileById(c *gin.Context) (*schemas.UploadOut, *types.AppError) {
|
| 29 |
+
uploadId := c.Param("id")
|
| 30 |
+
parts := []schemas.UploadPartOut{}
|
| 31 |
+
if err := us.Db.Model(&models.Upload{}).Order("part_no").Where("upload_id = ?", uploadId).Find(&parts).Error; err != nil {
|
| 32 |
+
return nil, &types.AppError{Error: errors.New("failed to fetch from db"), Code: http.StatusInternalServerError}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
return &schemas.UploadOut{Parts: parts}, nil
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func (us *UploadService) DeleteUploadFile(c *gin.Context) *types.AppError {
|
| 39 |
+
uploadId := c.Param("id")
|
| 40 |
+
if err := us.Db.Where("upload_id = ?", uploadId).Delete(&models.Upload{}).Error; err != nil {
|
| 41 |
+
return &types.AppError{Error: errors.New("failed to delete upload"), Code: http.StatusInternalServerError}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return nil
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
func (us *UploadService) UploadFile(c *gin.Context) (*schemas.UploadPartOut, *types.AppError) {
|
| 48 |
+
|
| 49 |
+
var uploadQuery schemas.UploadQuery
|
| 50 |
+
|
| 51 |
+
uploadQuery.PartNo = 1
|
| 52 |
+
uploadQuery.TotalParts = 1
|
| 53 |
+
|
| 54 |
+
if err := c.ShouldBindQuery(&uploadQuery); err != nil {
|
| 55 |
+
return nil, &types.AppError{Error: err, Code: http.StatusBadRequest}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if uploadQuery.Filename == "" {
|
| 59 |
+
return nil, &types.AppError{Error: errors.New("filename missing"), Code: http.StatusBadRequest}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
uploadId := c.Param("id")
|
| 63 |
+
|
| 64 |
+
var uploadPart []models.Upload
|
| 65 |
+
|
| 66 |
+
us.Db.Model(&models.Upload{}).Where("upload_id = ?", uploadId).Where("part_no = ?", uploadQuery.PartNo).Find(&uploadPart)
|
| 67 |
+
|
| 68 |
+
if len(uploadPart) == 1 {
|
| 69 |
+
out := mapSchema(&uploadPart[0])
|
| 70 |
+
return out, nil
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
client, idx := utils.GetUploadClient(c)
|
| 74 |
+
|
| 75 |
+
file := c.Request.Body
|
| 76 |
+
|
| 77 |
+
fileSize := c.Request.ContentLength
|
| 78 |
+
|
| 79 |
+
fileName := uploadQuery.Filename
|
| 80 |
+
|
| 81 |
+
var msgId int
|
| 82 |
+
|
| 83 |
+
ctx := context.Background()
|
| 84 |
+
|
| 85 |
+
ctx, cancel := context.WithCancel(ctx)
|
| 86 |
+
|
| 87 |
+
defer func() {
|
| 88 |
+
if idx != -1 {
|
| 89 |
+
utils.GetClientWorkload().Dec(idx)
|
| 90 |
+
}
|
| 91 |
+
cancel()
|
| 92 |
+
}()
|
| 93 |
+
|
| 94 |
+
err := client.Run(ctx, func(ctx context.Context) error {
|
| 95 |
+
|
| 96 |
+
api := client.API()
|
| 97 |
+
|
| 98 |
+
u := uploader.NewUploader(api).WithThreads(8).WithPartSize(512 * 1024)
|
| 99 |
+
|
| 100 |
+
upload, err := u.Upload(c, uploader.NewUpload(fileName, file, fileSize))
|
| 101 |
+
|
| 102 |
+
if err != nil {
|
| 103 |
+
return err
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
document := message.UploadedDocument(upload).Filename(fileName).ForceFile(true)
|
| 107 |
+
|
| 108 |
+
res, err := cache.CachedFunction(utils.GetChannelById, fmt.Sprintf("channels:%d", us.ChannelID))(c, client.API(), us.ChannelID)
|
| 109 |
+
|
| 110 |
+
if err != nil {
|
| 111 |
+
return err
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
channel := res.(*tg.Channel)
|
| 115 |
+
|
| 116 |
+
sender := message.NewSender(client.API())
|
| 117 |
+
|
| 118 |
+
target := sender.To(&tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash})
|
| 119 |
+
|
| 120 |
+
res, err = target.Media(c, document)
|
| 121 |
+
|
| 122 |
+
if err != nil {
|
| 123 |
+
return err
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
updates := res.(*tg.Updates)
|
| 127 |
+
|
| 128 |
+
msgId = updates.Updates[0].(*tg.UpdateMessageID).ID
|
| 129 |
+
|
| 130 |
+
return nil
|
| 131 |
+
})
|
| 132 |
+
|
| 133 |
+
if err != nil {
|
| 134 |
+
return nil, &types.AppError{Error: err, Code: http.StatusInternalServerError}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
partUpload := &models.Upload{
|
| 138 |
+
Name: fileName,
|
| 139 |
+
UploadId: uploadId,
|
| 140 |
+
PartId: msgId,
|
| 141 |
+
ChannelID: us.ChannelID,
|
| 142 |
+
Size: fileSize,
|
| 143 |
+
PartNo: uploadQuery.PartNo,
|
| 144 |
+
TotalParts: uploadQuery.TotalParts,
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
if err := us.Db.Create(partUpload).Error; err != nil {
|
| 148 |
+
return nil, &types.AppError{Error: errors.New("failed to upload part"), Code: http.StatusInternalServerError}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
out := mapSchema(partUpload)
|
| 152 |
+
|
| 153 |
+
return out, nil
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
func mapSchema(in *models.Upload) *schemas.UploadPartOut {
|
| 157 |
+
out := &schemas.UploadPartOut{
|
| 158 |
+
ID: in.ID,
|
| 159 |
+
Name: in.Name,
|
| 160 |
+
PartId: in.PartId,
|
| 161 |
+
ChannelID: in.ChannelID,
|
| 162 |
+
PartNo: in.PartNo,
|
| 163 |
+
TotalParts: in.TotalParts,
|
| 164 |
+
Size: in.Size,
|
| 165 |
+
}
|
| 166 |
+
return out
|
| 167 |
+
}
|
services/user.service.go
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net/http"
|
| 8 |
+
"strconv"
|
| 9 |
+
|
| 10 |
+
"github.com/divyam234/teldrive/types"
|
| 11 |
+
"github.com/divyam234/teldrive/utils"
|
| 12 |
+
"github.com/gotd/td/telegram"
|
| 13 |
+
"github.com/gotd/td/tg"
|
| 14 |
+
|
| 15 |
+
"github.com/gin-gonic/gin"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
type UserService struct {
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func getChunk(ctx context.Context, tgClient *telegram.Client, location tg.InputFileLocationClass, offset int64, limit int) ([]byte, error) {
|
| 22 |
+
|
| 23 |
+
req := &tg.UploadGetFileRequest{
|
| 24 |
+
Offset: offset,
|
| 25 |
+
Limit: int(limit),
|
| 26 |
+
Location: location,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
r, err := tgClient.API().UploadGetFile(ctx, req)
|
| 30 |
+
|
| 31 |
+
if err != nil {
|
| 32 |
+
return nil, err
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
switch result := r.(type) {
|
| 36 |
+
case *tg.UploadFile:
|
| 37 |
+
return result.Bytes, nil
|
| 38 |
+
default:
|
| 39 |
+
return nil, fmt.Errorf("unexpected type %T", r)
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func iterContent(ctx context.Context, tgClient *telegram.Client, location tg.InputFileLocationClass) (*bytes.Buffer, error) {
|
| 44 |
+
offset := int64(0)
|
| 45 |
+
limit := 1024 * 1024
|
| 46 |
+
buff := &bytes.Buffer{}
|
| 47 |
+
for {
|
| 48 |
+
r, err := getChunk(ctx, tgClient, location, offset, limit)
|
| 49 |
+
if err != nil {
|
| 50 |
+
return buff, err
|
| 51 |
+
}
|
| 52 |
+
if len(r) == 0 {
|
| 53 |
+
break
|
| 54 |
+
}
|
| 55 |
+
buff.Write(r)
|
| 56 |
+
offset += int64(limit)
|
| 57 |
+
}
|
| 58 |
+
return buff, nil
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
func (us *UserService) GetProfilePhoto(c *gin.Context) {
|
| 62 |
+
val, _ := c.Get("jwtUser")
|
| 63 |
+
jwtUser := val.(*types.JWTClaims)
|
| 64 |
+
userId, _ := strconv.ParseInt(jwtUser.Subject, 10, 64)
|
| 65 |
+
client, _ := utils.GetAuthClient(c, jwtUser.TgSession, userId)
|
| 66 |
+
|
| 67 |
+
err := client.Run(c, func(ctx context.Context) error {
|
| 68 |
+
self, err := client.Self(c)
|
| 69 |
+
if err != nil {
|
| 70 |
+
return err
|
| 71 |
+
}
|
| 72 |
+
peer := self.AsInputPeer()
|
| 73 |
+
photo, _ := self.Photo.AsNotEmpty()
|
| 74 |
+
location := &tg.InputPeerPhotoFileLocation{Big: false, Peer: peer, PhotoID: photo.PhotoID}
|
| 75 |
+
buff, err := iterContent(c, client, location)
|
| 76 |
+
if err != nil {
|
| 77 |
+
return err
|
| 78 |
+
}
|
| 79 |
+
content := buff.Bytes()
|
| 80 |
+
c.Writer.Header().Set("Content-Type", "image/jpeg")
|
| 81 |
+
c.Writer.Header().Set("Cache-Control", "public, max-age=86400")
|
| 82 |
+
c.Writer.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
| 83 |
+
c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", "profile.jpeg"))
|
| 84 |
+
c.Writer.Write(content)
|
| 85 |
+
return nil
|
| 86 |
+
})
|
| 87 |
+
if err != nil {
|
| 88 |
+
http.Error(c.Writer, err.Error(), http.StatusBadRequest)
|
| 89 |
+
return
|
| 90 |
+
}
|
| 91 |
+
}
|
types/main.go
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package types
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/go-jose/go-jose/v3/jwt"
|
| 5 |
+
"github.com/gotd/td/tg"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type AppError struct {
|
| 9 |
+
Error error
|
| 10 |
+
Code int
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
type Part struct {
|
| 14 |
+
Location *tg.InputDocumentFileLocation
|
| 15 |
+
Size int64
|
| 16 |
+
Start int64
|
| 17 |
+
End int64
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type JWTClaims struct {
|
| 21 |
+
jwt.Claims
|
| 22 |
+
TgSession string `json:"tgSession"`
|
| 23 |
+
Name string `json:"name"`
|
| 24 |
+
UserName string `json:"userName"`
|
| 25 |
+
Bot bool `json:"bot"`
|
| 26 |
+
IsPremium bool `json:"isPremium"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type TgSession struct {
|
| 30 |
+
Sesssion string `json:"session"`
|
| 31 |
+
UserID int64 `json:"userId"`
|
| 32 |
+
Bot bool `json:"bot"`
|
| 33 |
+
UserName string `json:"userName"`
|
| 34 |
+
Name string `json:"name"`
|
| 35 |
+
IsPremium bool `json:"isPremium"`
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
type Session struct {
|
| 39 |
+
Name string `json:"name"`
|
| 40 |
+
UserName string `json:"userName"`
|
| 41 |
+
IsPremium bool `json:"isPremium"`
|
| 42 |
+
Expires string `json:"expires"`
|
| 43 |
+
}
|
utils/auth/jwe.go
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"os"
|
| 6 |
+
|
| 7 |
+
"github.com/divyam234/teldrive/types"
|
| 8 |
+
"github.com/go-jose/go-jose/v3"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func Encode(payload *types.JWTClaims) (string, error) {
|
| 12 |
+
|
| 13 |
+
rcpt := jose.Recipient{
|
| 14 |
+
Algorithm: jose.PBES2_HS256_A128KW,
|
| 15 |
+
Key: os.Getenv("JWT_SECRET"),
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
enc, err := jose.NewEncrypter(jose.A128CBC_HS256, rcpt, nil)
|
| 19 |
+
|
| 20 |
+
if err != nil {
|
| 21 |
+
return "", err
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
jwt, _ := json.Marshal(payload)
|
| 25 |
+
|
| 26 |
+
jweObject, err := enc.Encrypt(jwt)
|
| 27 |
+
|
| 28 |
+
if err != nil {
|
| 29 |
+
return "", err
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
jweToken, err := jweObject.CompactSerialize()
|
| 33 |
+
|
| 34 |
+
if err != nil {
|
| 35 |
+
return "", err
|
| 36 |
+
}
|
| 37 |
+
return jweToken, nil
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
func Decode(token string) (*types.JWTClaims, error) {
|
| 41 |
+
jwe, err := jose.ParseEncrypted(token)
|
| 42 |
+
if err != nil {
|
| 43 |
+
return nil, err
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
decryptedData, err := jwe.Decrypt(os.Getenv("JWT_SECRET"))
|
| 47 |
+
|
| 48 |
+
if err != nil {
|
| 49 |
+
return nil, err
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
jwtToken := &types.JWTClaims{}
|
| 53 |
+
|
| 54 |
+
err = json.Unmarshal(decryptedData, jwtToken)
|
| 55 |
+
|
| 56 |
+
if err != nil {
|
| 57 |
+
return nil, err
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return jwtToken, nil
|
| 61 |
+
|
| 62 |
+
}
|
utils/config.go
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"path/filepath"
|
| 5 |
+
|
| 6 |
+
"github.com/joho/godotenv"
|
| 7 |
+
"github.com/kelseyhightower/envconfig"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type MultiToken string
|
| 11 |
+
|
| 12 |
+
type Config struct {
|
| 13 |
+
AppId int `envconfig:"APP_ID" required:"true"`
|
| 14 |
+
AppHash string `envconfig:"APP_HASH" required:"true"`
|
| 15 |
+
ChannelID int64 `envconfig:"CHANNEL_ID" required:"true"`
|
| 16 |
+
JwtSecret string `envconfig:"JWT_SECRET" required:"true"`
|
| 17 |
+
MultiClient bool `envconfig:"MULTI_CLIENT" default:"false"`
|
| 18 |
+
Https bool `envconfig:"HTTPS" default:"false"`
|
| 19 |
+
CookieSameSite bool `envconfig:"COOKIE_SAME_SITE" default:"true"`
|
| 20 |
+
DatabaseUrl string `envconfig:"DATABASE_URL" required:"true"`
|
| 21 |
+
RateLimit bool `envconfig:"RATE_LIMIT" default:"true"`
|
| 22 |
+
TgClientDeviceModel string `envconfig:"TG_CLIENT_DEVICE_MODEL" required:"true"`
|
| 23 |
+
TgClientSystemVersion string `envconfig:"TG_CLIENT_SYSTEM_VERSION" default:"Win32"`
|
| 24 |
+
TgClientAppVersion string `envconfig:"TG_CLIENT_APP_VERSION" default:"2.1.9 K"`
|
| 25 |
+
TgClientLangCode string `envconfig:"TG_CLIENT_LANG_CODE" default:"en"`
|
| 26 |
+
TgClientSystemLangCode string `envconfig:"TG_CLIENT_SYSTEM_LANG_CODE" default:"en"`
|
| 27 |
+
TgClientLangPack string `envconfig:"TG_CLIENT_LANG_PACK" default:"webk"`
|
| 28 |
+
RunMigrations bool `envconfig:"RUN_MIGRATIONS" default:"true"`
|
| 29 |
+
Port int `envconfig:"PORT" default:"8080"`
|
| 30 |
+
ExecDir string
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
var config Config
|
| 34 |
+
|
| 35 |
+
func InitConfig() {
|
| 36 |
+
|
| 37 |
+
execDir := getExecutableDir()
|
| 38 |
+
|
| 39 |
+
godotenv.Load(filepath.Join(execDir, ".env"))
|
| 40 |
+
|
| 41 |
+
godotenv.Load(filepath.Join(execDir, "teldrive.env"))
|
| 42 |
+
err := envconfig.Process("", &config)
|
| 43 |
+
if err != nil {
|
| 44 |
+
panic(err)
|
| 45 |
+
}
|
| 46 |
+
config.ExecDir = execDir
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func GetConfig() *Config {
|
| 50 |
+
return &config
|
| 51 |
+
}
|
utils/cron/cron.go
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cron
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
|
| 7 |
+
"github.com/divyam234/teldrive/cache"
|
| 8 |
+
"github.com/divyam234/teldrive/database"
|
| 9 |
+
"github.com/divyam234/teldrive/models"
|
| 10 |
+
"github.com/divyam234/teldrive/utils"
|
| 11 |
+
"github.com/gotd/td/tg"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type Result struct {
|
| 15 |
+
ID string
|
| 16 |
+
Parts models.Parts
|
| 17 |
+
TgSession string
|
| 18 |
+
UserId int64
|
| 19 |
+
ChannelId int64
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func deleteTGMessage(ctx context.Context, client *tg.Client, result Result) error {
|
| 23 |
+
|
| 24 |
+
ids := make([]int, len(result.Parts))
|
| 25 |
+
|
| 26 |
+
for i, part := range result.Parts {
|
| 27 |
+
ids[i] = int(part.ID)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
res, err := cache.CachedFunction(utils.GetChannelById, fmt.Sprintf("channels:%d", result.ChannelId))(ctx, client, result.ChannelId)
|
| 31 |
+
|
| 32 |
+
if err != nil {
|
| 33 |
+
return err
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
channel := res.(*tg.Channel)
|
| 37 |
+
|
| 38 |
+
messageDeleteRequest := tg.ChannelsDeleteMessagesRequest{Channel: &tg.InputChannel{ChannelID: result.ChannelId, AccessHash: channel.AccessHash},
|
| 39 |
+
ID: ids}
|
| 40 |
+
|
| 41 |
+
_, err = client.ChannelsDeleteMessages(ctx, &messageDeleteRequest)
|
| 42 |
+
if err != nil {
|
| 43 |
+
return err
|
| 44 |
+
}
|
| 45 |
+
return nil
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
func FilesDeleteJob() {
|
| 49 |
+
db := database.DB
|
| 50 |
+
ctx := context.Background()
|
| 51 |
+
var results []Result
|
| 52 |
+
if err := db.Model(&models.File{}).Select("files.id", "files.parts", "files.user_id", "files.channel_id", "u.tg_session").
|
| 53 |
+
Joins("left join teldrive.users as u on u.user_id = files.user_id").
|
| 54 |
+
Where("status = ?", "pending_deletion").Scan(&results).Error; err != nil {
|
| 55 |
+
return
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
for _, file := range results {
|
| 59 |
+
client, err := utils.GetAuthClient(ctx, file.TgSession, file.UserId)
|
| 60 |
+
if err != nil {
|
| 61 |
+
break
|
| 62 |
+
}
|
| 63 |
+
err = client.Run(ctx, func(ctx context.Context) error {
|
| 64 |
+
err = deleteTGMessage(ctx, client.API(), file)
|
| 65 |
+
return err
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
if err == nil {
|
| 69 |
+
db.Where("id = ?", file.ID).Delete(&models.File{})
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
utils/logger.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"os"
|
| 5 |
+
|
| 6 |
+
"go.uber.org/zap"
|
| 7 |
+
"go.uber.org/zap/zapcore"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
var Logger *zap.Logger
|
| 11 |
+
|
| 12 |
+
func InitializeLogger() {
|
| 13 |
+
config := zap.NewProductionEncoderConfig()
|
| 14 |
+
config.EncodeTime = zapcore.ISO8601TimeEncoder
|
| 15 |
+
consoleEncoder := zapcore.NewConsoleEncoder(config)
|
| 16 |
+
defaultLogLevel := zapcore.DebugLevel
|
| 17 |
+
core := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), defaultLogLevel)
|
| 18 |
+
Logger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
| 19 |
+
}
|
utils/main.go
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
"regexp"
|
| 9 |
+
"strings"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"reflect"
|
| 13 |
+
|
| 14 |
+
"github.com/gotd/td/tg"
|
| 15 |
+
"golang.org/x/exp/constraints"
|
| 16 |
+
|
| 17 |
+
"unicode"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
func Max[T constraints.Ordered](a, b T) T {
|
| 21 |
+
if a > b {
|
| 22 |
+
return a
|
| 23 |
+
}
|
| 24 |
+
return b
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func CamelToPascalCase(input string) string {
|
| 28 |
+
var result strings.Builder
|
| 29 |
+
upperNext := true
|
| 30 |
+
|
| 31 |
+
for _, char := range input {
|
| 32 |
+
if unicode.IsLetter(char) || unicode.IsDigit(char) {
|
| 33 |
+
if upperNext {
|
| 34 |
+
result.WriteRune(unicode.ToUpper(char))
|
| 35 |
+
upperNext = false
|
| 36 |
+
} else {
|
| 37 |
+
result.WriteRune(char)
|
| 38 |
+
}
|
| 39 |
+
} else {
|
| 40 |
+
upperNext = true
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return result.String()
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
func CamelToSnake(input string) string {
|
| 48 |
+
re := regexp.MustCompile("([a-z0-9])([A-Z])")
|
| 49 |
+
snake := re.ReplaceAllString(input, "${1}_${2}")
|
| 50 |
+
return strings.ToLower(snake)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func GetField(v interface{}, field string) string {
|
| 54 |
+
r := reflect.ValueOf(v)
|
| 55 |
+
f := reflect.Indirect(r).FieldByName(field)
|
| 56 |
+
fieldValue := f.Interface()
|
| 57 |
+
|
| 58 |
+
switch v := fieldValue.(type) {
|
| 59 |
+
case string:
|
| 60 |
+
return v
|
| 61 |
+
case time.Time:
|
| 62 |
+
return v.Format(time.RFC3339)
|
| 63 |
+
default:
|
| 64 |
+
return ""
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
func GetChannelById(ctx context.Context, client *tg.Client, channelID int64) (*tg.Channel, error) {
|
| 69 |
+
inputChannel := &tg.InputChannel{
|
| 70 |
+
ChannelID: channelID,
|
| 71 |
+
AccessHash: 0,
|
| 72 |
+
}
|
| 73 |
+
channels, err := client.ChannelsGetChannels(ctx, []tg.InputChannelClass{inputChannel})
|
| 74 |
+
|
| 75 |
+
if err != nil {
|
| 76 |
+
return nil, fmt.Errorf("failed to fetch channel: %w", err)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
if len(channels.GetChats()) == 0 {
|
| 80 |
+
return nil, fmt.Errorf("no channels found")
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
channel := channels.GetChats()[0].(*tg.Channel)
|
| 84 |
+
return channel, nil
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
func BoolPointer(b bool) *bool {
|
| 88 |
+
return &b
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func IntPointer(b int) *int {
|
| 92 |
+
return &b
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
func PathExists(path string) (bool, error) {
|
| 96 |
+
_, err := os.Stat(path)
|
| 97 |
+
if err == nil {
|
| 98 |
+
return true, nil
|
| 99 |
+
}
|
| 100 |
+
if os.IsNotExist(err) {
|
| 101 |
+
return false, nil
|
| 102 |
+
}
|
| 103 |
+
return false, err
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
func getExecutableDir() string {
|
| 107 |
+
|
| 108 |
+
path, _ := os.Executable()
|
| 109 |
+
|
| 110 |
+
executableDir := filepath.Dir(path)
|
| 111 |
+
|
| 112 |
+
return executableDir
|
| 113 |
+
}
|
utils/tgclient.go
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
"sort"
|
| 9 |
+
"strconv"
|
| 10 |
+
"strings"
|
| 11 |
+
"sync"
|
| 12 |
+
"time"
|
| 13 |
+
|
| 14 |
+
"github.com/cenkalti/backoff/v4"
|
| 15 |
+
"github.com/divyam234/teldrive/types"
|
| 16 |
+
"github.com/gin-gonic/gin"
|
| 17 |
+
"github.com/gotd/contrib/bg"
|
| 18 |
+
"github.com/gotd/contrib/middleware/floodwait"
|
| 19 |
+
"github.com/gotd/contrib/middleware/ratelimit"
|
| 20 |
+
tdclock "github.com/gotd/td/clock"
|
| 21 |
+
"github.com/gotd/td/session"
|
| 22 |
+
"github.com/gotd/td/telegram"
|
| 23 |
+
"github.com/pkg/errors"
|
| 24 |
+
"go.uber.org/zap"
|
| 25 |
+
"golang.org/x/time/rate"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
var clients map[int64]*telegram.Client
|
| 29 |
+
|
| 30 |
+
type Workload struct {
|
| 31 |
+
mu sync.Mutex
|
| 32 |
+
workloads map[int]int
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
func (w *Workload) Set(key int, value int) {
|
| 36 |
+
w.mu.Lock()
|
| 37 |
+
defer w.mu.Unlock()
|
| 38 |
+
w.workloads[key] = value
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func (w *Workload) Get(key int) int {
|
| 42 |
+
w.mu.Lock()
|
| 43 |
+
defer w.mu.Unlock()
|
| 44 |
+
return w.workloads[key]
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
func (w *Workload) Inc(key int) {
|
| 48 |
+
w.mu.Lock()
|
| 49 |
+
defer w.mu.Unlock()
|
| 50 |
+
w.workloads[key]++
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func (w *Workload) Dec(key int) {
|
| 54 |
+
w.mu.Lock()
|
| 55 |
+
defer w.mu.Unlock()
|
| 56 |
+
w.workloads[key]--
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func (w *Workload) GetMinIndex() int {
|
| 60 |
+
w.mu.Lock()
|
| 61 |
+
defer w.mu.Unlock()
|
| 62 |
+
smallest := w.workloads[0]
|
| 63 |
+
idx := 0
|
| 64 |
+
for i, workload := range clientWorkload.workloads {
|
| 65 |
+
if workload < smallest {
|
| 66 |
+
smallest = workload
|
| 67 |
+
idx = i
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
return idx
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
var clientWorkload *Workload
|
| 74 |
+
|
| 75 |
+
func GetClientWorkload() *Workload {
|
| 76 |
+
return clientWorkload
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
func getDeviceConfig() telegram.DeviceConfig {
|
| 80 |
+
appConfig := GetConfig()
|
| 81 |
+
config := telegram.DeviceConfig{
|
| 82 |
+
DeviceModel: appConfig.TgClientDeviceModel,
|
| 83 |
+
SystemVersion: appConfig.TgClientSystemVersion,
|
| 84 |
+
AppVersion: appConfig.TgClientAppVersion,
|
| 85 |
+
SystemLangCode: appConfig.TgClientSystemLangCode,
|
| 86 |
+
LangPack: appConfig.TgClientLangPack,
|
| 87 |
+
LangCode: appConfig.TgClientLangCode,
|
| 88 |
+
}
|
| 89 |
+
return config
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func reconnectionBackoff() backoff.BackOff {
|
| 93 |
+
_clock := tdclock.System
|
| 94 |
+
b := backoff.NewExponentialBackOff()
|
| 95 |
+
b.Multiplier = 1.1
|
| 96 |
+
b.MaxElapsedTime = time.Duration(120) * time.Second
|
| 97 |
+
b.Clock = _clock
|
| 98 |
+
return b
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
func GetBotClient(clientName string) *telegram.Client {
|
| 102 |
+
|
| 103 |
+
config := GetConfig()
|
| 104 |
+
sessionStorage := &telegram.FileSessionStorage{
|
| 105 |
+
Path: filepath.Join(config.ExecDir, "sessions", clientName+".json"),
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
middlewares := []telegram.Middleware{floodwait.NewSimpleWaiter()}
|
| 109 |
+
|
| 110 |
+
if config.RateLimit {
|
| 111 |
+
middlewares = append(middlewares, ratelimit.New(rate.Every(time.Millisecond*100), 5))
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
options := telegram.Options{
|
| 115 |
+
SessionStorage: sessionStorage,
|
| 116 |
+
Middlewares: middlewares,
|
| 117 |
+
ReconnectionBackoff: reconnectionBackoff,
|
| 118 |
+
RetryInterval: 5 * time.Second,
|
| 119 |
+
MaxRetries: 5,
|
| 120 |
+
Device: getDeviceConfig(),
|
| 121 |
+
Clock: tdclock.System,
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
client := telegram.NewClient(config.AppId, config.AppHash, options)
|
| 125 |
+
|
| 126 |
+
return client
|
| 127 |
+
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
func GetAuthClient(ctx context.Context, sessionStr string, userId int64) (*telegram.Client, error) {
|
| 131 |
+
|
| 132 |
+
data, err := session.TelethonSession(sessionStr)
|
| 133 |
+
|
| 134 |
+
if err != nil {
|
| 135 |
+
return nil, err
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
var (
|
| 139 |
+
storage = new(session.StorageMemory)
|
| 140 |
+
loader = session.Loader{Storage: storage}
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
if err := loader.Save(ctx, data); err != nil {
|
| 144 |
+
return nil, err
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
middlewares := []telegram.Middleware{floodwait.NewSimpleWaiter()}
|
| 148 |
+
|
| 149 |
+
if config.RateLimit {
|
| 150 |
+
middlewares = append(middlewares, ratelimit.New(rate.Every(time.Millisecond*100), 5))
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
client := telegram.NewClient(config.AppId, config.AppHash, telegram.Options{
|
| 154 |
+
SessionStorage: storage,
|
| 155 |
+
Middlewares: middlewares,
|
| 156 |
+
ReconnectionBackoff: reconnectionBackoff,
|
| 157 |
+
RetryInterval: 5 * time.Second,
|
| 158 |
+
MaxRetries: 5,
|
| 159 |
+
Device: getDeviceConfig(),
|
| 160 |
+
Clock: tdclock.System,
|
| 161 |
+
})
|
| 162 |
+
|
| 163 |
+
return client, nil
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
func StartNonAuthClient(handler telegram.UpdateHandler, storage telegram.SessionStorage) (*telegram.Client, bg.StopFunc, error) {
|
| 167 |
+
middlewares := []telegram.Middleware{}
|
| 168 |
+
if config.RateLimit {
|
| 169 |
+
middlewares = append(middlewares, ratelimit.New(rate.Every(time.Millisecond*100), 5))
|
| 170 |
+
}
|
| 171 |
+
client := telegram.NewClient(config.AppId, config.AppHash, telegram.Options{
|
| 172 |
+
SessionStorage: storage,
|
| 173 |
+
Middlewares: middlewares,
|
| 174 |
+
Device: getDeviceConfig(),
|
| 175 |
+
UpdateHandler: handler,
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
stop, err := bg.Connect(client)
|
| 179 |
+
|
| 180 |
+
if err != nil {
|
| 181 |
+
return nil, nil, err
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
return client, stop, nil
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
func startBotClient(ctx context.Context, client *telegram.Client, token string) (bg.StopFunc, error) {
|
| 188 |
+
|
| 189 |
+
stop, err := bg.Connect(client)
|
| 190 |
+
|
| 191 |
+
if err != nil {
|
| 192 |
+
return nil, errors.Wrap(err, "failed to start client")
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
tguser, err := client.Self(ctx)
|
| 196 |
+
|
| 197 |
+
if err != nil {
|
| 198 |
+
|
| 199 |
+
if _, err := client.Auth().Bot(ctx, token); err != nil {
|
| 200 |
+
return nil, err
|
| 201 |
+
}
|
| 202 |
+
tguser, _ = client.Self(ctx)
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
Logger.Info("started Client", zap.String("user", tguser.Username))
|
| 206 |
+
return stop, nil
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
func startAuthClient(c *gin.Context, client *telegram.Client) (bg.StopFunc, error) {
|
| 210 |
+
stop, err := bg.Connect(client)
|
| 211 |
+
|
| 212 |
+
if err != nil {
|
| 213 |
+
return nil, err
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
tguser, err := client.Self(c)
|
| 217 |
+
|
| 218 |
+
if err != nil {
|
| 219 |
+
return nil, err
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
Logger.Info("started Client", zap.String("user", tguser.Username))
|
| 223 |
+
|
| 224 |
+
clients[tguser.GetID()] = client
|
| 225 |
+
|
| 226 |
+
return stop, nil
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
func InitBotClients() {
|
| 230 |
+
|
| 231 |
+
ctx := context.Background()
|
| 232 |
+
|
| 233 |
+
clients = make(map[int64]*telegram.Client)
|
| 234 |
+
|
| 235 |
+
clientWorkload = &Workload{workloads: make(map[int]int)}
|
| 236 |
+
|
| 237 |
+
if config.MultiClient {
|
| 238 |
+
|
| 239 |
+
if err := os.MkdirAll(filepath.Join(config.ExecDir, "sessions"), 0700); err != nil {
|
| 240 |
+
return
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
var keysToSort []string
|
| 244 |
+
|
| 245 |
+
for _, e := range os.Environ() {
|
| 246 |
+
if strings.HasPrefix(e, "MULTI_TOKEN") {
|
| 247 |
+
if i := strings.Index(e, "="); i >= 0 {
|
| 248 |
+
keysToSort = append(keysToSort, e[:i])
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
sort.Strings(keysToSort)
|
| 254 |
+
|
| 255 |
+
for idx, key := range keysToSort {
|
| 256 |
+
client := GetBotClient(fmt.Sprintf("client%d", idx))
|
| 257 |
+
clientWorkload.Set(idx, 0)
|
| 258 |
+
clients[int64(idx)] = client
|
| 259 |
+
go func(k string) {
|
| 260 |
+
startBotClient(ctx, client, os.Getenv(k))
|
| 261 |
+
}(key)
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
func GetUploadClient(c *gin.Context) (*telegram.Client, int) {
|
| 268 |
+
if config.MultiClient {
|
| 269 |
+
idx := clientWorkload.GetMinIndex()
|
| 270 |
+
clientWorkload.Inc(idx)
|
| 271 |
+
return GetBotClient(fmt.Sprintf("client%d", idx)), idx
|
| 272 |
+
} else {
|
| 273 |
+
val, _ := c.Get("jwtUser")
|
| 274 |
+
jwtUser := val.(*types.JWTClaims)
|
| 275 |
+
userId, _ := strconv.ParseInt(jwtUser.Subject, 10, 64)
|
| 276 |
+
client, _ := GetAuthClient(c, jwtUser.TgSession, userId)
|
| 277 |
+
return client, -1
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
func GetDownloadClient(c *gin.Context) (*telegram.Client, int) {
|
| 282 |
+
if config.MultiClient {
|
| 283 |
+
idx := clientWorkload.GetMinIndex()
|
| 284 |
+
clientWorkload.Inc(idx)
|
| 285 |
+
return clients[int64(idx)], idx
|
| 286 |
+
} else {
|
| 287 |
+
val, _ := c.Get("jwtUser")
|
| 288 |
+
jwtUser := val.(*types.JWTClaims)
|
| 289 |
+
userId, _ := strconv.ParseInt(jwtUser.Subject, 10, 64)
|
| 290 |
+
if client, ok := clients[userId]; ok {
|
| 291 |
+
return client, -1
|
| 292 |
+
}
|
| 293 |
+
client, _ := GetAuthClient(c, jwtUser.TgSession, userId)
|
| 294 |
+
startAuthClient(c, client)
|
| 295 |
+
return client, -1
|
| 296 |
+
}
|
| 297 |
+
}
|